From d2da4fd7003a42e45a51cc1e455d4b150d6b2155 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Sun, 22 Mar 2026 15:59:10 +0100 Subject: [PATCH 1/3] add scan check on download --- .../configuration/Registration.java | 2 +- .../ReadAttachmentsHandler.java | 71 ++++++++++-- .../readhelper/BeforeReadItemsModifier.java | 7 +- .../ReadAttachmentsHandlerTest.java | 101 +++++++++++++++++- .../BeforeReadItemsModifierTest.java | 33 ++++++ 5 files changed, 200 insertions(+), 14 deletions(-) diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/configuration/Registration.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/configuration/Registration.java index d749a42fb..c27fdb893 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/configuration/Registration.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/configuration/Registration.java @@ -133,7 +133,7 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) { new EndTransactionMalwareScanRunner(null, null, malwareScanner, runtime); configurer.eventHandler( new ReadAttachmentsHandler( - attachmentService, new AttachmentStatusValidator(), scanRunner)); + attachmentService, new AttachmentStatusValidator(), scanRunner, persistenceService)); } else { logger.debug( "No application service is available. Application service event handlers will not be registered."); diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/ReadAttachmentsHandler.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/ReadAttachmentsHandler.java index 98f9c44ec..96cb6ca33 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/ReadAttachmentsHandler.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/ReadAttachmentsHandler.java @@ -18,7 +18,9 @@ import com.sap.cds.feature.attachments.service.AttachmentService; import com.sap.cds.feature.attachments.service.malware.AsyncMalwareScanExecutor; import com.sap.cds.ql.CQL; +import com.sap.cds.ql.Update; import com.sap.cds.ql.cqn.CqnSelect; +import com.sap.cds.ql.cqn.CqnUpdate; import com.sap.cds.ql.cqn.Path; import com.sap.cds.reflect.CdsAssociationType; import com.sap.cds.reflect.CdsElementDefinition; @@ -32,7 +34,10 @@ import com.sap.cds.services.handler.annotations.Before; import com.sap.cds.services.handler.annotations.HandlerOrder; import com.sap.cds.services.handler.annotations.ServiceName; +import com.sap.cds.services.persistence.PersistenceService; import java.io.InputStream; +import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -45,29 +50,45 @@ /** * The class {@link ReadAttachmentsHandler} is an event handler that is responsible for reading - * attachments for entities. In the before read event, it modifies the CQN to include the content ID - * and status. In the after read event, it adds a proxy for the stream of the attachments service to - * the data. Only if the data are read the proxy forwards the request to the attachment service to - * read the attachment. This is needed to have a filled stream in the data to enable the OData V4 - * adapter to enrich the data that a link to the content can be shown on the UI. + * attachments for entities. In the before read event, it modifies the CQN to include the content + * ID, status and scanned-at timestamp. In the after read event, it adds a proxy for the stream of + * the attachments service to the data. Only if the data are read the proxy forwards the request to + * the attachment service to read the attachment. This is needed to have a filled stream in the data + * to enable the OData V4 adapter to enrich the data that a link to the content can be shown on the + * UI. + * + *

Additionally, this handler implements rescan-on-download: if an attachment's last scan is + * older than {@link #RESCAN_THRESHOLD}, the handler transitions the attachment status to {@code + * SCANNING}, triggers an asynchronous malware rescan, and rejects the current download with a "not + * scanned" error. The client must retry the download after the rescan completes. */ @ServiceName(value = "*", type = ApplicationService.class) public class ReadAttachmentsHandler implements EventHandler { private static final Logger logger = LoggerFactory.getLogger(ReadAttachmentsHandler.class); + /** + * The duration after which a previously scanned attachment is considered stale and must be + * rescanned on download, as recommended by the SAP Malware Scanning Service FAQ. + */ + static final Duration RESCAN_THRESHOLD = Duration.ofDays(3); + private final AttachmentService attachmentService; private final AttachmentStatusValidator statusValidator; private final AsyncMalwareScanExecutor scanExecutor; + private final PersistenceService persistenceService; public ReadAttachmentsHandler( AttachmentService attachmentService, AttachmentStatusValidator statusValidator, - AsyncMalwareScanExecutor scanExecutor) { + AsyncMalwareScanExecutor scanExecutor, + PersistenceService persistenceService) { this.attachmentService = requireNonNull(attachmentService, "attachmentService must not be null"); this.statusValidator = requireNonNull(statusValidator, "statusValidator must not be null"); this.scanExecutor = requireNonNull(scanExecutor, "scanExecutor must not be null"); + this.persistenceService = + requireNonNull(persistenceService, "persistenceService must not be null"); } @Before @@ -153,9 +174,10 @@ private void verifyStatus(Path path, Attachments attachment) { "In verify status for content id {} and status {}", attachment.getContentId(), currentStatus); - if (StatusCode.UNSCANNED.equals(currentStatus) - || StatusCode.SCANNING.equals(currentStatus) - || currentStatus == null) { + if (needsScan(currentStatus, attachment.getScannedAt())) { + if (StatusCode.CLEAN.equals(currentStatus)) { + transitionToScanning(path.target().entity(), attachment); + } logger.debug( "Scanning content with ID {} for malware, has current status {}", attachment.getContentId(), @@ -166,6 +188,37 @@ private void verifyStatus(Path path, Attachments attachment) { } } + private boolean needsScan(String status, Instant scannedAt) { + if (StatusCode.UNSCANNED.equals(status) + || StatusCode.SCANNING.equals(status) + || status == null) { + return true; + } + return StatusCode.CLEAN.equals(status) && isScanStale(scannedAt); + } + + private boolean isScanStale(Instant scannedAt) { + return scannedAt == null || Instant.now().isAfter(scannedAt.plus(RESCAN_THRESHOLD)); + } + + private void transitionToScanning(CdsEntity entity, Attachments attachment) { + logger.debug( + "Attachment {} has stale scan (scannedAt={}), transitioning to SCANNING for rescan.", + attachment.getContentId(), + attachment.getScannedAt()); + + Attachments updateData = Attachments.create(); + updateData.setStatus(StatusCode.SCANNING); + + CqnUpdate update = + Update.entity(entity) + .data(updateData) + .where(entry -> entry.get(Attachments.CONTENT_ID).eq(attachment.getContentId())); + persistenceService.run(update); + + attachment.setStatus(StatusCode.SCANNING); + } + private boolean areKeysEmpty(Map keys) { return keys.values().stream().allMatch(Objects::isNull); } diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/readhelper/BeforeReadItemsModifier.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/readhelper/BeforeReadItemsModifier.java index c2890fef4..e5b16ab7b 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/readhelper/BeforeReadItemsModifier.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/readhelper/BeforeReadItemsModifier.java @@ -15,8 +15,8 @@ import org.slf4j.LoggerFactory; /** - * The class {@link BeforeReadItemsModifier} is a modifier that adds the content id field and status - * code to the select items. + * The class {@link BeforeReadItemsModifier} is a modifier that adds the content id field, status + * code and scanned-at timestamp to the select items. */ public class BeforeReadItemsModifier implements Modifier { @@ -74,9 +74,10 @@ private List processExpandedEntities(List private void enhanceWithNewFieldForMediaAssociation( String association, List list, List listToEnhance) { if (isMediaAssociationAndNeedNewContentIdField(association, list)) { - logger.debug("Adding document id and status code to select items"); + logger.debug("Adding document id, status code and scanned-at timestamp to select items"); listToEnhance.add(CQL.get(Attachments.CONTENT_ID)); listToEnhance.add(CQL.get(Attachments.STATUS)); + listToEnhance.add(CQL.get(Attachments.SCANNED_AT)); } } diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/ReadAttachmentsHandlerTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/ReadAttachmentsHandlerTest.java index 588236b4f..a9c610c55 100644 --- a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/ReadAttachmentsHandlerTest.java +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/ReadAttachmentsHandlerTest.java @@ -36,11 +36,14 @@ import com.sap.cds.services.handler.annotations.Before; import com.sap.cds.services.handler.annotations.HandlerOrder; import com.sap.cds.services.handler.annotations.ServiceName; +import com.sap.cds.services.persistence.PersistenceService; import com.sap.cds.services.runtime.CdsRuntime; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.List; import java.util.UUID; import org.junit.jupiter.api.BeforeAll; @@ -61,6 +64,7 @@ class ReadAttachmentsHandlerTest { private AttachmentStatusValidator attachmentStatusValidator; private CdsReadEventContext readEventContext; private AsyncMalwareScanExecutor asyncMalwareScanExecutor; + private PersistenceService persistenceService; @BeforeAll static void classSetup() { @@ -72,9 +76,13 @@ void setup() { attachmentService = mock(AttachmentService.class); attachmentStatusValidator = mock(AttachmentStatusValidator.class); asyncMalwareScanExecutor = mock(AsyncMalwareScanExecutor.class); + persistenceService = mock(PersistenceService.class); cut = new ReadAttachmentsHandler( - attachmentService, attachmentStatusValidator, asyncMalwareScanExecutor); + attachmentService, + attachmentStatusValidator, + asyncMalwareScanExecutor, + persistenceService); readEventContext = mock(CdsReadEventContext.class); } @@ -164,6 +172,7 @@ void setAttachmentServiceCalled() throws IOException { attachment.setContentId("some ID"); attachment.setContent(null); attachment.setStatus(StatusCode.CLEAN); + attachment.setScannedAt(Instant.now()); cut.processAfter(readEventContext, List.of(attachment)); @@ -253,6 +262,96 @@ void scannerNotCalledForInfectedAttachments() { verifyNoInteractions(asyncMalwareScanExecutor); } + @Test + void scannerCalledForStaleCleanAttachment() { + mockEventContext(Attachment_.CDS_NAME, mock(CqnSelect.class)); + var attachment = Attachments.create(); + attachment.setContentId("some ID"); + attachment.setContent(mock(InputStream.class)); + attachment.setStatus(StatusCode.CLEAN); + attachment.setScannedAt(Instant.now().minus(4, ChronoUnit.DAYS)); + doThrow(AttachmentStatusException.class) + .when(attachmentStatusValidator) + .verifyStatus(StatusCode.SCANNING); + + List attachments = List.of(attachment); + assertThrows( + AttachmentStatusException.class, () -> cut.processAfter(readEventContext, attachments)); + + verify(persistenceService).run(any(com.sap.cds.ql.cqn.CqnUpdate.class)); + verify(asyncMalwareScanExecutor) + .scanAsync(readEventContext.getTarget(), attachment.getContentId()); + assertThat(attachment.getStatus()).isEqualTo(StatusCode.SCANNING); + } + + @Test + void scannerCalledForCleanAttachmentWithNullScannedAt() { + mockEventContext(Attachment_.CDS_NAME, mock(CqnSelect.class)); + var attachment = Attachments.create(); + attachment.setContentId("some ID"); + attachment.setContent(mock(InputStream.class)); + attachment.setStatus(StatusCode.CLEAN); + attachment.setScannedAt(null); + doThrow(AttachmentStatusException.class) + .when(attachmentStatusValidator) + .verifyStatus(StatusCode.SCANNING); + + List attachments = List.of(attachment); + assertThrows( + AttachmentStatusException.class, () -> cut.processAfter(readEventContext, attachments)); + + verify(persistenceService).run(any(com.sap.cds.ql.cqn.CqnUpdate.class)); + verify(asyncMalwareScanExecutor) + .scanAsync(readEventContext.getTarget(), attachment.getContentId()); + assertThat(attachment.getStatus()).isEqualTo(StatusCode.SCANNING); + } + + @Test + void scannerNotCalledForFreshCleanAttachment() { + mockEventContext(Attachment_.CDS_NAME, mock(CqnSelect.class)); + var attachment = Attachments.create(); + attachment.setContentId("some ID"); + attachment.setContent(mock(InputStream.class)); + attachment.setStatus(StatusCode.CLEAN); + attachment.setScannedAt(Instant.now().minus(1, ChronoUnit.DAYS)); + + cut.processAfter(readEventContext, List.of(attachment)); + + verifyNoInteractions(asyncMalwareScanExecutor); + verifyNoInteractions(persistenceService); + assertThat(attachment.getStatus()).isEqualTo(StatusCode.CLEAN); + } + + @Test + void scannerNotCalledForCleanAttachmentScannedExactlyAtThreshold() { + mockEventContext(Attachment_.CDS_NAME, mock(CqnSelect.class)); + var attachment = Attachments.create(); + attachment.setContentId("some ID"); + attachment.setContent(mock(InputStream.class)); + attachment.setStatus(StatusCode.CLEAN); + attachment.setScannedAt(Instant.now().minus(2, ChronoUnit.DAYS)); + + cut.processAfter(readEventContext, List.of(attachment)); + + verifyNoInteractions(asyncMalwareScanExecutor); + verifyNoInteractions(persistenceService); + } + + @Test + void persistenceServiceNotCalledForUnscannedAttachments() { + mockEventContext(Attachment_.CDS_NAME, mock(CqnSelect.class)); + var attachment = Attachments.create(); + attachment.setContentId("some ID"); + attachment.setContent(mock(InputStream.class)); + attachment.setStatus(StatusCode.UNSCANNED); + + cut.processAfter(readEventContext, List.of(attachment)); + + verify(asyncMalwareScanExecutor) + .scanAsync(readEventContext.getTarget(), attachment.getContentId()); + verifyNoInteractions(persistenceService); + } + @Test void attachmentServiceNotCalledIfNoMediaType() { var eventItem = EventItems.create(); diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/readhelper/BeforeReadItemsModifierTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/readhelper/BeforeReadItemsModifierTest.java index d106cfea6..3c2444cc2 100644 --- a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/readhelper/BeforeReadItemsModifierTest.java +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/readhelper/BeforeReadItemsModifierTest.java @@ -32,6 +32,7 @@ void expandSelectExtendsContentId() { cut = new BeforeReadItemsModifier(List.of("attachments")); runTestForExpand(cut, select, 1); + runTestForExpandScannedAt(cut, select, 1); } @Test @@ -111,6 +112,7 @@ void directSelectExtendsContentId() { Select.from(Attachment_.class).columns(Attachment_::ID, Attachment_::content); runTestForDirectSelect(select, 1); + runTestForDirectSelectScannedAt(select, 1); } @Test @@ -165,4 +167,35 @@ private void runTestForDirectSelect(CqnSelect select, int expectedFieldCount) { .count(); assertThat(count).isEqualTo(expectedFieldCount); } + + private void runTestForExpandScannedAt( + BeforeReadItemsModifier cut, CqnSelect select, int expectedFieldCount) { + List resultItems = cut.items(select.items()); + + var rootExpandedItem = + resultItems.stream().filter(CqnSelectListItem::isExpand).findAny().orElseThrow(); + var itemExpandedItem = + rootExpandedItem.asExpand().items().stream() + .filter(CqnSelectListItem::isExpand) + .findAny() + .orElseThrow(); + var count = + itemExpandedItem.asExpand().items().stream() + .filter( + item -> item.isRef() && item.asRef().displayName().equals(Attachments.SCANNED_AT)) + .count(); + assertThat(count).isEqualTo(expectedFieldCount); + } + + private void runTestForDirectSelectScannedAt(CqnSelect select, int expectedFieldCount) { + cut = new BeforeReadItemsModifier(List.of("")); + List resultItems = cut.items(select.items()); + + var count = + resultItems.stream() + .filter( + item -> item.isRef() && item.asRef().displayName().equals(Attachments.SCANNED_AT)) + .count(); + assertThat(count).isEqualTo(expectedFieldCount); + } } From 4f5b4dc817dcaa38dbf89dc67d1e02293a042dad Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Sun, 22 Mar 2026 16:16:57 +0100 Subject: [PATCH 2/3] Address PR review: fix boundary test and strengthen assertions --- .../handler/applicationservice/ReadAttachmentsHandler.java | 2 ++ .../applicationservice/ReadAttachmentsHandlerTest.java | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/ReadAttachmentsHandler.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/ReadAttachmentsHandler.java index 96cb6ca33..810152ec3 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/ReadAttachmentsHandler.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/ReadAttachmentsHandler.java @@ -210,6 +210,8 @@ private void transitionToScanning(CdsEntity entity, Attachments attachment) { Attachments updateData = Attachments.create(); updateData.setStatus(StatusCode.SCANNING); + // Filter by contentId because primary keys are unavailable during content-only reads + // (areKeysEmpty returns true). This is consistent with DefaultAttachmentMalwareScanner. CqnUpdate update = Update.entity(entity) .data(updateData) diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/ReadAttachmentsHandlerTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/ReadAttachmentsHandlerTest.java index a9c610c55..0e70d7380 100644 --- a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/ReadAttachmentsHandlerTest.java +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/ReadAttachmentsHandlerTest.java @@ -329,7 +329,8 @@ void scannerNotCalledForCleanAttachmentScannedExactlyAtThreshold() { attachment.setContentId("some ID"); attachment.setContent(mock(InputStream.class)); attachment.setStatus(StatusCode.CLEAN); - attachment.setScannedAt(Instant.now().minus(2, ChronoUnit.DAYS)); + attachment.setScannedAt( + Instant.now().minus(ReadAttachmentsHandler.RESCAN_THRESHOLD).plusSeconds(60)); cut.processAfter(readEventContext, List.of(attachment)); @@ -349,6 +350,7 @@ void persistenceServiceNotCalledForUnscannedAttachments() { verify(asyncMalwareScanExecutor) .scanAsync(readEventContext.getTarget(), attachment.getContentId()); + verify(attachmentStatusValidator).verifyStatus(StatusCode.UNSCANNED); verifyNoInteractions(persistenceService); } From 444dcb7dde3c76d831f786d758fad34ad8b03a3f Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Sun, 22 Mar 2026 17:41:49 +0100 Subject: [PATCH 3/3] Propagate scannedAt through create flow to fix integration tests Add scannedAt to AttachmentModificationResult so that when an attachment service handler (e.g. TestPluginAttachmentsServiceHandler) sets both status=CLEAN and scannedAt on create, the timestamp is persisted to the DB via CreateAttachmentEvent. Without this, scannedAt remained null for CLEAN attachments, causing the rescan-on-download logic to incorrectly treat them as stale. --- .../modifyevents/CreateAttachmentEvent.java | 3 +++ .../attachments/service/AttachmentsServiceImpl.java | 3 ++- .../model/service/AttachmentModificationResult.java | 5 ++++- .../modifyevents/CreateAttachmentEventTest.java | 10 +++++----- .../TestPluginAttachmentsServiceHandler.java | 2 ++ 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/modifyevents/CreateAttachmentEvent.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/modifyevents/CreateAttachmentEvent.java index 0b3295fb8..6d66f2793 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/modifyevents/CreateAttachmentEvent.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/modifyevents/CreateAttachmentEvent.java @@ -66,6 +66,9 @@ public InputStream processEvent( eventContext.getChangeSetContext().register(createListener); path.target().values().put(Attachments.CONTENT_ID, result.contentId()); path.target().values().put(Attachments.STATUS, result.status()); + if (nonNull(result.scannedAt())) { + path.target().values().put(Attachments.SCANNED_AT, result.scannedAt()); + } return result.isInternalStored() ? content : null; } diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/AttachmentsServiceImpl.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/AttachmentsServiceImpl.java index 8fca05a57..37bf8469b 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/AttachmentsServiceImpl.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/AttachmentsServiceImpl.java @@ -56,7 +56,8 @@ public AttachmentModificationResult createAttachment(CreateAttachmentInput input return new AttachmentModificationResult( Boolean.TRUE.equals(createContext.getIsInternalStored()), createContext.getContentId(), - createContext.getData().getStatus()); + createContext.getData().getStatus(), + createContext.getData().getScannedAt()); } @Override diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/model/service/AttachmentModificationResult.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/model/service/AttachmentModificationResult.java index 0e1fd277a..0c62a4062 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/model/service/AttachmentModificationResult.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/model/service/AttachmentModificationResult.java @@ -3,12 +3,15 @@ */ package com.sap.cds.feature.attachments.service.model.service; +import java.time.Instant; + /** * This record is used to store the result of the attachment modification. * * @param isInternalStored Indicates if the attachment is stored internally (in DB) or externally. * @param contentId The content id of the attachment. * @param status The status of the attachment. + * @param scannedAt The timestamp when the attachment was last scanned for malware. */ public record AttachmentModificationResult( - boolean isInternalStored, String contentId, String status) {} + boolean isInternalStored, String contentId, String status, Instant scannedAt) {} diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/modifyevents/CreateAttachmentEventTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/modifyevents/CreateAttachmentEventTest.java index 05f913939..df10cf9e8 100644 --- a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/modifyevents/CreateAttachmentEventTest.java +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/modifyevents/CreateAttachmentEventTest.java @@ -92,7 +92,7 @@ void storageCalledWithAllFieldsFilledFromExistingData() { when(target.values()).thenReturn(attachment); when(target.keys()).thenReturn(Map.of("ID", attachment.getId(), "up__ID", "test")); when(attachmentService.createAttachment(any())) - .thenReturn(new AttachmentModificationResult(false, "id", "test")); + .thenReturn(new AttachmentModificationResult(false, "id", "test", null)); var existingData = Attachments.create(); existingData.setFileName("some file name"); existingData.setMimeType("some mime type"); @@ -116,7 +116,7 @@ void resultFromServiceStoredInPath() { var attachment = Attachments.create(); attachment.setId("test"); var attachmentServiceResult = - new AttachmentModificationResult(false, "some document id", "test"); + new AttachmentModificationResult(false, "some document id", "test", null); when(attachmentService.createAttachment(any())).thenReturn(attachmentServiceResult); when(target.values()).thenReturn(attachment); @@ -134,7 +134,7 @@ void changesetIstRegistered() { var listener = mock(ChangeSetListener.class); when(listenerProvider.provideListener(contentId, runtime)).thenReturn(listener); when(attachmentService.createAttachment(any())) - .thenReturn(new AttachmentModificationResult(false, contentId, "test")); + .thenReturn(new AttachmentModificationResult(false, contentId, "test", null)); cut.processEvent(path, null, Attachments.create(), eventContext); @@ -154,7 +154,7 @@ void contentIsReturnedIfNotExternalStored(boolean isExternalStored) throws IOExc } when(target.values()).thenReturn(attachment); when(attachmentService.createAttachment(any())) - .thenReturn(new AttachmentModificationResult(isExternalStored, "id", "test")); + .thenReturn(new AttachmentModificationResult(isExternalStored, "id", "test", null)); var result = cut.processEvent(path, attachment.getContent(), Attachments.create(), eventContext); @@ -174,7 +174,7 @@ private Attachments prepareAndExecuteEventWithData() { when(target.values()).thenReturn(attachment); when(target.keys()).thenReturn(Map.of("ID", attachment.getId())); when(attachmentService.createAttachment(any())) - .thenReturn(new AttachmentModificationResult(false, "id", "test")); + .thenReturn(new AttachmentModificationResult(false, "id", "test", null)); cut.processEvent(path, attachment.getContent(), Attachments.create(), eventContext); return attachment; diff --git a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandler.java b/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandler.java index a8c25829c..341002905 100644 --- a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandler.java +++ b/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandler.java @@ -15,6 +15,7 @@ import com.sap.cds.services.handler.annotations.ServiceName; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -48,6 +49,7 @@ public void createAttachment(AttachmentCreateEventContext context) throws IOExce documents.put(contentId, context.getData().getContent().readAllBytes()); context.setContentId(contentId); context.getData().setStatus(StatusCode.CLEAN); + context.getData().setScannedAt(Instant.now()); context.setCompleted(); eventContextHolder.add( new EventContextHolder(AttachmentService.EVENT_CREATE_ATTACHMENT, context));