@@ -1797,6 +1797,211 @@ void testHandleDraftDiscardForLinks_CallsRevertNestedEntityLinks() throws IOExce
17971797 }
17981798 }
17991799
1800+ @ Test
1801+ void testHandleDraftDiscardForLinks_OnlyDirectAttachmentsProcessedViaPathMapping ()
1802+ throws IOException {
1803+ // Verifies that handleDraftDiscardForLinks uses getDirectAttachmentPathMapping (not
1804+ // getAttachmentPathMapping) on the root entity — i.e. only direct attachments on the root
1805+ // are processed via Path 1. Nested attachments are handled exclusively by
1806+ // revertNestedEntityLinks.
1807+ DraftCancelEventContext draftContext = mock (DraftCancelEventContext .class );
1808+ CdsEntity parentDraftEntity = mock (CdsEntity .class );
1809+ CqnAnalyzer analyzer = mock (CqnAnalyzer .class );
1810+ AnalysisResult analysisResult = mock (AnalysisResult .class );
1811+ CqnDelete cqnDelete = mock (CqnDelete .class );
1812+ CdsEntity parentActiveEntity = mock (CdsEntity .class );
1813+
1814+ when (draftContext .getTarget ()).thenReturn (parentDraftEntity );
1815+ when (parentDraftEntity .getQualifiedName ()).thenReturn ("AdminService.Books_drafts" );
1816+ when (draftContext .getModel ()).thenReturn (cdsModel );
1817+ when (draftContext .getCqn ()).thenReturn (cqnDelete );
1818+ cqnAnalyzerMock .when (() -> CqnAnalyzer .create (cdsModel )).thenReturn (analyzer );
1819+ when (analyzer .analyze (cqnDelete )).thenReturn (analysisResult );
1820+ when (analysisResult .rootKeys ()).thenReturn (Map .of ("ID" , "book123" ));
1821+ when (cdsModel .findEntity ("AdminService.Books" )).thenReturn (Optional .of (parentActiveEntity ));
1822+ when (parentActiveEntity .compositions ()).thenReturn (Stream .empty ());
1823+
1824+ try (var attachmentUtilsMock =
1825+ mockStatic (
1826+ com .sap .cds .sdm .handler .applicationservice .helper .AttachmentsHandlerUtils .class )) {
1827+
1828+ attachmentUtilsMock
1829+ .when (
1830+ () -> AttachmentsHandlerUtils .getDirectAttachmentPathMapping (eq (parentActiveEntity )))
1831+ .thenReturn (new HashMap <>());
1832+
1833+ sdmServiceGenericHandler .handleDraftDiscardForLinks (draftContext );
1834+
1835+ // getDirectAttachmentPathMapping must be called on the root entity
1836+ attachmentUtilsMock .verify (
1837+ () -> AttachmentsHandlerUtils .getDirectAttachmentPathMapping (eq (parentActiveEntity )),
1838+ times (1 ));
1839+
1840+ // getAttachmentPathMapping must NOT be called on the root entity
1841+ attachmentUtilsMock .verify (
1842+ () ->
1843+ AttachmentsHandlerUtils .getAttachmentPathMapping (
1844+ eq (cdsModel ), eq (parentActiveEntity ), any ()),
1845+ never ());
1846+ }
1847+ }
1848+
1849+ @ Test
1850+ void testHandleDraftDiscardForLinks_DirectAttachmentOnRootIsReverted () throws IOException {
1851+ // Verifies that when the root entity has a direct attachment composition,
1852+ // revertLinksForComposition is called for it via Path 1 (getDirectAttachmentPathMapping).
1853+ DraftCancelEventContext draftContext = mock (DraftCancelEventContext .class );
1854+ CdsEntity parentDraftEntity = mock (CdsEntity .class );
1855+ CqnAnalyzer analyzer = mock (CqnAnalyzer .class );
1856+ AnalysisResult analysisResult = mock (AnalysisResult .class );
1857+ CqnDelete cqnDelete = mock (CqnDelete .class );
1858+ CdsEntity parentActiveEntity = mock (CdsEntity .class );
1859+ CdsEntity attachmentDraftEntity = mock (CdsEntity .class );
1860+ CdsEntity attachmentActiveEntity = mock (CdsEntity .class );
1861+
1862+ when (draftContext .getTarget ()).thenReturn (parentDraftEntity );
1863+ when (parentDraftEntity .getQualifiedName ()).thenReturn ("AdminService.Books_drafts" );
1864+ when (draftContext .getModel ()).thenReturn (cdsModel );
1865+ when (draftContext .getCqn ()).thenReturn (cqnDelete );
1866+ cqnAnalyzerMock .when (() -> CqnAnalyzer .create (cdsModel )).thenReturn (analyzer );
1867+ when (analyzer .analyze (cqnDelete )).thenReturn (analysisResult );
1868+ when (analysisResult .rootKeys ()).thenReturn (Map .of ("ID" , "book123" ));
1869+ when (cdsModel .findEntity ("AdminService.Books" )).thenReturn (Optional .of (parentActiveEntity ));
1870+ when (parentActiveEntity .compositions ()).thenReturn (Stream .empty ());
1871+
1872+ // Direct attachment on root
1873+ Map <String , String > directMapping = new HashMap <>();
1874+ directMapping .put ("AdminService.Books.attachments" , "AdminService.Books.attachments" );
1875+
1876+ when (cdsModel .findEntity ("AdminService.Books.attachments_drafts" ))
1877+ .thenReturn (Optional .of (attachmentDraftEntity ));
1878+ when (cdsModel .findEntity ("AdminService.Books.attachments" ))
1879+ .thenReturn (Optional .of (attachmentActiveEntity ));
1880+
1881+ CdsElement upElement = mock (CdsElement .class );
1882+ when (attachmentDraftEntity .elements ()).thenReturn (Stream .of (upElement ));
1883+ when (upElement .getName ()).thenReturn ("up__ID" );
1884+ sdmUtilsMock .when (() -> SDMUtils .getUpIdKey (attachmentDraftEntity )).thenReturn ("up__ID" );
1885+
1886+ Result emptyResult = mock (Result .class );
1887+ when (emptyResult .iterator ()).thenReturn (Collections .emptyIterator ());
1888+ when (persistenceService .run (any (CqnSelect .class ))).thenReturn (emptyResult );
1889+
1890+ SDMCredentials sdmCredentials = mock (SDMCredentials .class );
1891+ UserInfo userInfo = mock (UserInfo .class );
1892+ when (tokenHandler .getSDMCredentials ()).thenReturn (sdmCredentials );
1893+ when (draftContext .getUserInfo ()).thenReturn (userInfo );
1894+ when (userInfo .isSystemUser ()).thenReturn (false );
1895+
1896+ try (var attachmentUtilsMock =
1897+ mockStatic (
1898+ com .sap .cds .sdm .handler .applicationservice .helper .AttachmentsHandlerUtils .class )) {
1899+ attachmentUtilsMock
1900+ .when (
1901+ () -> AttachmentsHandlerUtils .getDirectAttachmentPathMapping (eq (parentActiveEntity )))
1902+ .thenReturn (directMapping );
1903+
1904+ assertDoesNotThrow (() -> sdmServiceGenericHandler .handleDraftDiscardForLinks (draftContext ));
1905+
1906+ // persistence was called — confirming revertLinksForComposition was entered for the direct
1907+ // attachment
1908+ verify (persistenceService , atLeastOnce ()).run (any (CqnSelect .class ));
1909+ }
1910+ }
1911+
1912+ @ Test
1913+ void testHandleDraftDiscardForLinks_GrandchildAttachmentDoesNotCrash () throws IOException {
1914+ // Regression test for the bug: root → Chapters (no attachments) → Sections (has attachments).
1915+ // With the old code, getAttachmentPathMapping on root would construct a wrong entity name
1916+ // "AdminService.Chapters.attachments" causing NoSuchElementException.
1917+ // With the fix, getDirectAttachmentPathMapping returns empty for root (no direct attachments),
1918+ // and revertNestedEntityLinks correctly handles Sections via Chapters.
1919+ DraftCancelEventContext draftContext = mock (DraftCancelEventContext .class );
1920+ CdsEntity parentDraftEntity = mock (CdsEntity .class );
1921+ CqnAnalyzer analyzer = mock (CqnAnalyzer .class );
1922+ AnalysisResult analysisResult = mock (AnalysisResult .class );
1923+ CqnDelete cqnDelete = mock (CqnDelete .class );
1924+ CdsEntity parentActiveEntity = mock (CdsEntity .class );
1925+ CdsElement chaptersComposition = mock (CdsElement .class );
1926+ CdsAssociationType chaptersAssocType = mock (CdsAssociationType .class );
1927+ CdsEntity chaptersEntity = mock (CdsEntity .class );
1928+
1929+ when (draftContext .getTarget ()).thenReturn (parentDraftEntity );
1930+ when (parentDraftEntity .getQualifiedName ()).thenReturn ("AdminService.Books_drafts" );
1931+ when (draftContext .getModel ()).thenReturn (cdsModel );
1932+ when (draftContext .getCqn ()).thenReturn (cqnDelete );
1933+ cqnAnalyzerMock .when (() -> CqnAnalyzer .create (cdsModel )).thenReturn (analyzer );
1934+ when (analyzer .analyze (cqnDelete )).thenReturn (analysisResult );
1935+ when (analysisResult .rootKeys ()).thenReturn (Map .of ("ID" , "book123" ));
1936+ when (cdsModel .findEntity ("AdminService.Books" )).thenReturn (Optional .of (parentActiveEntity ));
1937+
1938+ // Root has one composition: Chapters (no direct attachments)
1939+ when (parentActiveEntity .compositions ()).thenReturn (Stream .of (chaptersComposition ));
1940+ when (chaptersComposition .getType ()).thenReturn (chaptersAssocType );
1941+ when (chaptersAssocType .getTarget ()).thenReturn (chaptersEntity );
1942+ when (chaptersEntity .getQualifiedName ()).thenReturn ("AdminService.Chapters" );
1943+
1944+ // Chapters_drafts does not exist (simulates non-draft-enabled or grandchild scenario)
1945+ when (cdsModel .findEntity ("AdminService.Chapters_drafts" )).thenReturn (Optional .empty ());
1946+
1947+ try (var attachmentUtilsMock =
1948+ mockStatic (
1949+ com .sap .cds .sdm .handler .applicationservice .helper .AttachmentsHandlerUtils .class )) {
1950+
1951+ // Root has NO direct attachments
1952+ attachmentUtilsMock
1953+ .when (
1954+ () -> AttachmentsHandlerUtils .getDirectAttachmentPathMapping (eq (parentActiveEntity )))
1955+ .thenReturn (new HashMap <>());
1956+
1957+ // Must not throw NoSuchElementException
1958+ assertDoesNotThrow (() -> sdmServiceGenericHandler .handleDraftDiscardForLinks (draftContext ));
1959+
1960+ // getDirectAttachmentPathMapping called on root — not getAttachmentPathMapping
1961+ attachmentUtilsMock .verify (
1962+ () -> AttachmentsHandlerUtils .getDirectAttachmentPathMapping (eq (parentActiveEntity )),
1963+ times (1 ));
1964+ attachmentUtilsMock .verify (
1965+ () ->
1966+ AttachmentsHandlerUtils .getAttachmentPathMapping (
1967+ eq (cdsModel ), eq (parentActiveEntity ), any ()),
1968+ never ());
1969+ }
1970+ }
1971+
1972+ @ Test
1973+ void testHandleDraftDiscardForLinks_ActiveEntityNotFound_SkipsDirectAttachments ()
1974+ throws IOException {
1975+ // When the active entity is not found in the model, no attachment processing should occur.
1976+ DraftCancelEventContext draftContext = mock (DraftCancelEventContext .class );
1977+ CdsEntity parentDraftEntity = mock (CdsEntity .class );
1978+ CqnAnalyzer analyzer = mock (CqnAnalyzer .class );
1979+ AnalysisResult analysisResult = mock (AnalysisResult .class );
1980+ CqnDelete cqnDelete = mock (CqnDelete .class );
1981+
1982+ when (draftContext .getTarget ()).thenReturn (parentDraftEntity );
1983+ when (parentDraftEntity .getQualifiedName ()).thenReturn ("AdminService.Books_drafts" );
1984+ when (draftContext .getModel ()).thenReturn (cdsModel );
1985+ when (draftContext .getCqn ()).thenReturn (cqnDelete );
1986+ cqnAnalyzerMock .when (() -> CqnAnalyzer .create (cdsModel )).thenReturn (analyzer );
1987+ when (analyzer .analyze (cqnDelete )).thenReturn (analysisResult );
1988+ when (analysisResult .rootKeys ()).thenReturn (Map .of ("ID" , "book123" ));
1989+ when (cdsModel .findEntity ("AdminService.Books" )).thenReturn (Optional .empty ());
1990+
1991+ try (var attachmentUtilsMock =
1992+ mockStatic (
1993+ com .sap .cds .sdm .handler .applicationservice .helper .AttachmentsHandlerUtils .class )) {
1994+
1995+ assertDoesNotThrow (() -> sdmServiceGenericHandler .handleDraftDiscardForLinks (draftContext ));
1996+
1997+ // Neither method should be called since active entity is absent
1998+ attachmentUtilsMock .verify (
1999+ () -> AttachmentsHandlerUtils .getDirectAttachmentPathMapping (any ()), never ());
2000+ attachmentUtilsMock .verify (
2001+ () -> AttachmentsHandlerUtils .getAttachmentPathMapping (any (), any (), any ()), never ());
2002+ }
2003+ }
2004+
18002005 @ Test
18012006 void testRevertNestedEntityLinks_WithNullParentId () throws IOException {
18022007
0 commit comments