@@ -325,6 +325,172 @@ void testCopyAttachments_AttachmentCopyFails_FolderExists_AttachmentsDeleted()
325325 assertTrue (ex .getMessage ().contains ("Copy failed" ));
326326 }
327327
328+ @ Test
329+ void testCopyAttachments_InvalidSecondaryProperty_BlocksCopy () throws IOException {
330+ // Mock SDMCredentials
331+ SDMCredentials sdmCredentials = mock (SDMCredentials .class );
332+ when (tokenHandler .getSDMCredentials ()).thenReturn (sdmCredentials );
333+
334+ // Mock folder id retrieval
335+ when (sdmService .getFolderIdByPath (any (), any (), any (), anyBoolean ())).thenReturn (FOLDER_ID );
336+
337+ // Create mock context with custom property annotations on the entity
338+ AttachmentCopyEventContext context = createMockContextWithCustomProperties ();
339+ when (context .getObjectIds ()).thenReturn (List .of (OBJECT_ID ));
340+
341+ // Mock secondary types and valid secondary properties from SDM
342+ when (sdmService .getSecondaryTypes (any (), any (), anyBoolean ()))
343+ .thenReturn (List .of ("secondaryType1" ));
344+ when (sdmService .getValidSecondaryProperties (any (), any (), any (), anyBoolean ()))
345+ .thenReturn (List .of ("validProp1" , "validProp2" ));
346+
347+ // Mock getObject: SDM response contains "invalidCustomProp" which is NOT in valid list
348+ JSONObject sdmMetadata = new JSONObject ();
349+ JSONObject succinctProperties = new JSONObject ();
350+ succinctProperties .put ("cmis:name" , "test.pdf" );
351+ succinctProperties .put ("invalidCustomProp" , "someValue" );
352+ sdmMetadata .put ("succinctProperties" , succinctProperties );
353+ when (sdmService .getObject (eq (OBJECT_ID ), any (), anyBoolean ())).thenReturn (sdmMetadata );
354+
355+ // Act
356+ sdmCustomServiceHandler .copyAttachments (context );
357+
358+ // Assert: copy should be blocked entirely, no copy operation performed
359+ verify (sdmService , never ())
360+ .copyAttachment (any (), any (SDMCredentials .class ), anyBoolean (), any ());
361+ // Warning message should be issued
362+ verify (context .getMessages (), times (1 )).warn (any (String .class ));
363+ verify (context , times (1 )).setCompleted ();
364+ }
365+
366+ @ Test
367+ void testCopyAttachments_MixedValidAndInvalidSecondaryProps_CopiesValidRejectsInvalid ()
368+ throws IOException {
369+ // Mock SDMCredentials
370+ SDMCredentials sdmCredentials = mock (SDMCredentials .class );
371+ when (tokenHandler .getSDMCredentials ()).thenReturn (sdmCredentials );
372+
373+ // Mock folder id retrieval
374+ when (sdmService .getFolderIdByPath (any (), any (), any (), anyBoolean ())).thenReturn (FOLDER_ID );
375+
376+ String validObjectId = "validObjectId" ;
377+ String invalidObjectId = "invalidObjectId" ;
378+
379+ // Create mock context with custom property annotations
380+ AttachmentCopyEventContext context = createMockContextWithCustomProperties ();
381+ when (context .getObjectIds ()).thenReturn (List .of (validObjectId , invalidObjectId ));
382+
383+ // Mock secondary types and valid secondary properties
384+ when (sdmService .getSecondaryTypes (any (), any (), anyBoolean ()))
385+ .thenReturn (List .of ("secondaryType1" ));
386+ when (sdmService .getValidSecondaryProperties (any (), any (), any (), anyBoolean ()))
387+ .thenReturn (List .of ("validProp1" , "validProp2" ));
388+
389+ // Mock getObject for valid attachment (does NOT have "invalidCustomProp" in response)
390+ JSONObject validMetadata = new JSONObject ();
391+ JSONObject validProps = new JSONObject ();
392+ validProps .put ("cmis:name" , "valid.pdf" );
393+ validProps .put ("validProp1" , "someValue" );
394+ validMetadata .put ("succinctProperties" , validProps );
395+ when (sdmService .getObject (eq (validObjectId ), any (), anyBoolean ())).thenReturn (validMetadata );
396+
397+ // Mock getObject for invalid attachment (HAS "invalidCustomProp" which is NOT in valid list)
398+ JSONObject invalidMetadata = new JSONObject ();
399+ JSONObject invalidProps = new JSONObject ();
400+ invalidProps .put ("cmis:name" , "invalid.pdf" );
401+ invalidProps .put ("invalidCustomProp" , "someValue" );
402+ invalidMetadata .put ("succinctProperties" , invalidProps );
403+ when (sdmService .getObject (eq (invalidObjectId ), any (), anyBoolean ()))
404+ .thenReturn (invalidMetadata );
405+
406+ // Mock attachment metadata and copy for the valid attachment
407+ CmisDocument cmisDocument = new CmisDocument ();
408+ cmisDocument .setType ("sap-icon://document" );
409+ when (dbQuery .getAttachmentForObjectID (any (), any (), any (AttachmentCopyEventContext .class )))
410+ .thenReturn (cmisDocument );
411+
412+ Map <String , String > attachmentData = new HashMap <>();
413+ attachmentData .put ("cmis:name" , "valid.pdf" );
414+ attachmentData .put ("cmis:contentStreamMimeType" , "application/pdf" );
415+ attachmentData .put ("cmis:objectId" , "newCopiedObjectId" );
416+ when (sdmService .copyAttachment (any (), any (SDMCredentials .class ), anyBoolean (), any ()))
417+ .thenReturn (attachmentData );
418+
419+ // Act
420+ sdmCustomServiceHandler .copyAttachments (context );
421+
422+ // Assert: valid attachment should be copied, invalid one should be warned about
423+ verify (sdmService , times (1 ))
424+ .copyAttachment (any (), any (SDMCredentials .class ), anyBoolean (), any ());
425+ verify (context .getMessages (), times (1 )).warn (any (String .class ));
426+ verify (draftService , times (1 )).newDraft (any ());
427+ verify (context , times (1 )).setCompleted ();
428+ }
429+
430+ private AttachmentCopyEventContext createMockContextWithCustomProperties () {
431+ AttachmentCopyEventContext context = mock (AttachmentCopyEventContext .class );
432+ CdsElement mockAssociationElement = mock (CdsElement .class );
433+ CdsAssociationType mockAssociationType = mock (CdsAssociationType .class );
434+ CqnElementRef mockCqnElementRef = mock (CqnElementRef .class );
435+
436+ when (context .getParentEntity ()).thenReturn ("prefix.someIdentifier." + FACET );
437+ when (context .getCompositionName ()).thenReturn (FACET );
438+ when (context .getUpId ()).thenReturn (UP_ID );
439+ when (context .getSystemUser ()).thenReturn (true );
440+ when (context .getObjectIds ()).thenReturn (List .of (OBJECT_ID ));
441+
442+ // Mock CdsModel and relevant entities and associations
443+ CdsModel model = mock (CdsModel .class );
444+ CdsEntity parentEntity = mock (CdsEntity .class );
445+ CdsEntity draftEntity = mock (CdsEntity .class );
446+ CdsEntity targetEntity = mock (CdsEntity .class );
447+ CdsEntity compositionEntity = mock (CdsEntity .class );
448+
449+ // Mock composition element and its type
450+ CdsElement compositionElement = mock (CdsElement .class );
451+ CdsAssociationType compositionType = mock (CdsAssociationType .class );
452+
453+ // Setup expected behavior for model and parent entity
454+ when (context .getModel ()).thenReturn (model );
455+ when (model .findEntity ("prefix.someIdentifier." + FACET )).thenReturn (Optional .of (parentEntity ));
456+ when (model .findEntity ("prefix.someIdentifier." + FACET + "." + FACET ))
457+ .thenReturn (Optional .of (compositionEntity ));
458+ when (model .findEntity (endsWith ("_drafts" ))).thenReturn (Optional .of (draftEntity ));
459+
460+ // Mock the composition entity with a custom property annotation
461+ CdsElement customPropertyElement = mock (CdsElement .class );
462+ com .sap .cds .reflect .CdsAnnotation <Object > nameAnnotation =
463+ mock (com .sap .cds .reflect .CdsAnnotation .class );
464+ com .sap .cds .reflect .CdsType elementType = mock (com .sap .cds .reflect .CdsType .class );
465+ when (elementType .isAssociation ()).thenReturn (false );
466+ when (customPropertyElement .getType ()).thenReturn (elementType );
467+ when (customPropertyElement .getName ()).thenReturn ("customField" );
468+ when (customPropertyElement .findAnnotation ("SDM.Attachments.AdditionalProperty.name" ))
469+ .thenReturn (Optional .of (nameAnnotation ));
470+ when (nameAnnotation .getValue ()).thenReturn ("invalidCustomProp" );
471+ when (compositionEntity .elements ()).thenReturn (Stream .of (customPropertyElement ));
472+
473+ // Mock the composition element in parent entity
474+ when (parentEntity .findElement (FACET )).thenReturn (Optional .of (compositionElement ));
475+ when (compositionElement .getType ()).thenReturn (compositionType );
476+ when (compositionType .isAssociation ()).thenReturn (true );
477+ when (compositionType .getTarget ()).thenReturn (targetEntity );
478+ when (targetEntity .getQualifiedName ()).thenReturn ("target.entity.name" );
479+
480+ // Mock the draft entity's up_ association
481+ when (draftEntity .findAssociation ("up_" )).thenReturn (Optional .of (mockAssociationElement ));
482+ when (mockAssociationElement .getType ()).thenReturn (mockAssociationType );
483+ when (mockAssociationType .refs ()).thenReturn (Stream .of (mockCqnElementRef ));
484+ when (mockCqnElementRef .path ()).thenReturn ("ID" );
485+
486+ // Mock messages
487+ com .sap .cds .services .messages .Messages messages =
488+ mock (com .sap .cds .services .messages .Messages .class );
489+ when (context .getMessages ()).thenReturn (messages );
490+
491+ return context ;
492+ }
493+
328494 private AttachmentCopyEventContext createMockContext () {
329495 AttachmentCopyEventContext context = mock (AttachmentCopyEventContext .class );
330496 CdsElement mockAssociationElement = mock (CdsElement .class );
0 commit comments