From 5629dc2ede0f7d64d0aced2ddc47e935e6971cf1 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Fri, 10 Apr 2026 00:01:14 +0200 Subject: [PATCH 01/11] add new independent MalwareScanner --- .../configuration/Registration.java | 6 ++ .../service/MalwareScannerService.java | 31 ++++++ .../service/MalwareScannerServiceImpl.java | 31 ++++++ .../DefaultMalwareScannerServiceHandler.java | 63 +++++++++++ .../MalwareScanEventContext.java | 51 +++++++++ .../configuration/RegistrationTest.java | 17 ++- .../MalwareScannerServiceImplTest.java | 68 ++++++++++++ ...faultMalwareScannerServiceHandlerTest.java | 101 ++++++++++++++++++ .../MalwareScanEventContextTest.java | 33 ++++++ 9 files changed, 396 insertions(+), 5 deletions(-) create mode 100644 cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/MalwareScannerService.java create mode 100644 cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/MalwareScannerServiceImpl.java create mode 100644 cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandler.java create mode 100644 cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/model/servicehandler/MalwareScanEventContext.java create mode 100644 cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/MalwareScannerServiceImplTest.java create mode 100644 cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandlerTest.java create mode 100644 cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/model/servicehandler/MalwareScanEventContextTest.java 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 b04f726e9..8a0228244 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 @@ -24,7 +24,9 @@ import com.sap.cds.feature.attachments.handler.draftservice.DraftPatchAttachmentsHandler; import com.sap.cds.feature.attachments.service.AttachmentService; import com.sap.cds.feature.attachments.service.AttachmentsServiceImpl; +import com.sap.cds.feature.attachments.service.MalwareScannerServiceImpl; import com.sap.cds.feature.attachments.service.handler.DefaultAttachmentsServiceHandler; +import com.sap.cds.feature.attachments.service.handler.DefaultMalwareScannerServiceHandler; import com.sap.cds.feature.attachments.service.handler.transaction.EndTransactionMalwareScanProvider; import com.sap.cds.feature.attachments.service.handler.transaction.EndTransactionMalwareScanRunner; import com.sap.cds.feature.attachments.service.malware.AttachmentMalwareScanner; @@ -88,6 +90,7 @@ public void environment(CdsRuntimeConfigurer configurer) { @Override public void services(CdsRuntimeConfigurer configurer) { configurer.service(new AttachmentsServiceImpl()); + configurer.service(new MalwareScannerServiceImpl()); } @Override @@ -137,6 +140,9 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) { configurer.eventHandler( new DefaultAttachmentsServiceHandler(malwareScanEndTransactionListener)); + // register event handler for malware scanner service + configurer.eventHandler(new DefaultMalwareScannerServiceHandler(scanClient)); + MarkAsDeletedAttachmentEvent deleteEvent = new MarkAsDeletedAttachmentEvent(outboxedAttachmentService); ModifyAttachmentEventFactory eventFactory = diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/MalwareScannerService.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/MalwareScannerService.java new file mode 100644 index 000000000..44de7e846 --- /dev/null +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/MalwareScannerService.java @@ -0,0 +1,31 @@ +/* + * © 2024-2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.service; + +import com.sap.cds.feature.attachments.service.malware.client.MalwareScanResultStatus; +import com.sap.cds.services.Service; +import java.io.InputStream; + +/** + * The {@link MalwareScannerService} provides malware scanning capabilities independent of the + * {@link AttachmentService}. It can be injected from the CAP service catalog and used to scan + * arbitrary content for malware. + */ +public interface MalwareScannerService extends Service { + + /** The {@link MalwareScannerService} uses this name in the service catalog */ + String DEFAULT_NAME = "MalwareScannerService$Default"; + + /** This event is emitted when content shall be scanned for malware */ + String EVENT_SCAN_CONTENT = "SCAN_CONTENT"; + + /** + * Scans the given content for malware. + * + * @param content the {@link InputStream} of the content to scan + * @return the result of the malware scan + * @see MalwareScanResultStatus + */ + MalwareScanResultStatus scanContent(InputStream content); +} diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/MalwareScannerServiceImpl.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/MalwareScannerServiceImpl.java new file mode 100644 index 000000000..f8f5a727a --- /dev/null +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/MalwareScannerServiceImpl.java @@ -0,0 +1,31 @@ +/* + * © 2024-2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.service; + +import com.sap.cds.feature.attachments.service.malware.client.MalwareScanResultStatus; +import com.sap.cds.feature.attachments.service.model.servicehandler.MalwareScanEventContext; +import com.sap.cds.services.ServiceDelegator; +import java.io.InputStream; + +/** + * Implementation of the {@link MalwareScannerService} interface. The main purpose of this class is + * to set data in the corresponding context and to call the emit method for the + * MalwareScannerService. + */ +public class MalwareScannerServiceImpl extends ServiceDelegator implements MalwareScannerService { + + public MalwareScannerServiceImpl() { + super(DEFAULT_NAME); + } + + @Override + public MalwareScanResultStatus scanContent(InputStream content) { + var scanContext = MalwareScanEventContext.create(); + scanContext.setContent(content); + + emit(scanContext); + + return scanContext.getScanResult(); + } +} diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandler.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandler.java new file mode 100644 index 000000000..3184e7dd8 --- /dev/null +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandler.java @@ -0,0 +1,63 @@ +/* + * © 2024-2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.service.handler; + +import com.sap.cds.feature.attachments.service.MalwareScannerService; +import com.sap.cds.feature.attachments.service.malware.client.MalwareScanClient; +import com.sap.cds.feature.attachments.service.malware.client.MalwareScanResultStatus; +import com.sap.cds.feature.attachments.service.model.servicehandler.MalwareScanEventContext; +import com.sap.cds.services.handler.EventHandler; +import com.sap.cds.services.handler.annotations.HandlerOrder; +import com.sap.cds.services.handler.annotations.On; +import com.sap.cds.services.handler.annotations.ServiceName; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The class {@link DefaultMalwareScannerServiceHandler} is the default event handler for the {@link + * MalwareScannerService}. It delegates to the {@link MalwareScanClient} to perform the actual + * malware scan. + */ +@ServiceName(value = "*", type = MalwareScannerService.class) +public class DefaultMalwareScannerServiceHandler implements EventHandler { + + private static final int DEFAULT_ON = 10 * HandlerOrder.AFTER + HandlerOrder.LATE; + + private static final Logger logger = + LoggerFactory.getLogger(DefaultMalwareScannerServiceHandler.class); + + private final MalwareScanClient malwareScanClient; + + /** + * Constructs a new instance of {@link DefaultMalwareScannerServiceHandler}. + * + * @param malwareScanClient an optional {@link MalwareScanClient} instance, may be {@code null} if + * no malware scanner service binding is available + */ + public DefaultMalwareScannerServiceHandler(MalwareScanClient malwareScanClient) { + this.malwareScanClient = malwareScanClient; + } + + @On + @HandlerOrder(DEFAULT_ON) + void scanContent(MalwareScanEventContext context) { + if (malwareScanClient == null) { + logger.info("No malware scanner service binding available. Returning NO_SCANNER status."); + context.setScanResult(MalwareScanResultStatus.NO_SCANNER); + context.setCompleted(); + return; + } + + try { + logger.debug("Starting malware scan of content."); + MalwareScanResultStatus result = malwareScanClient.scanContent(context.getContent()); + context.setScanResult(result); + logger.debug("Malware scan completed with status '{}'.", result); + } catch (RuntimeException e) { + logger.error("Error during malware scan.", e); + context.setScanResult(MalwareScanResultStatus.FAILED); + } + context.setCompleted(); + } +} diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/model/servicehandler/MalwareScanEventContext.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/model/servicehandler/MalwareScanEventContext.java new file mode 100644 index 000000000..a87d6a422 --- /dev/null +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/model/servicehandler/MalwareScanEventContext.java @@ -0,0 +1,51 @@ +/* + * © 2024-2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.service.model.servicehandler; + +import com.sap.cds.feature.attachments.service.MalwareScannerService; +import com.sap.cds.feature.attachments.service.malware.client.MalwareScanResultStatus; +import com.sap.cds.services.EventContext; +import com.sap.cds.services.EventName; +import java.io.InputStream; + +/** + * The {@link MalwareScanEventContext} is used to store the context of a malware scan event. + */ +@EventName(MalwareScannerService.EVENT_SCAN_CONTENT) +public interface MalwareScanEventContext extends EventContext { + + /** + * Creates an {@link EventContext} already overlaid with this interface. The event is set to be + * {@link MalwareScannerService#EVENT_SCAN_CONTENT} + * + * @return the {@link MalwareScanEventContext} + */ + static MalwareScanEventContext create() { + return EventContext.create(MalwareScanEventContext.class, null); + } + + /** + * @return the content {@link InputStream} to scan for malware + */ + InputStream getContent(); + + /** + * Sets the content to scan for malware + * + * @param content the content {@link InputStream} to scan + */ + void setContent(InputStream content); + + /** + * @return the result of the malware scan or {@code null} if not yet scanned + */ + MalwareScanResultStatus getScanResult(); + + /** + * Sets the result of the malware scan + * + * @param scanResult the result of the malware scan + */ + void setScanResult(MalwareScanResultStatus scanResult); +} diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/configuration/RegistrationTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/configuration/RegistrationTest.java index 987b2b4c1..c1370d5cf 100644 --- a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/configuration/RegistrationTest.java +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/configuration/RegistrationTest.java @@ -19,7 +19,9 @@ import com.sap.cds.feature.attachments.handler.draftservice.DraftCancelAttachmentsHandler; import com.sap.cds.feature.attachments.handler.draftservice.DraftPatchAttachmentsHandler; import com.sap.cds.feature.attachments.service.AttachmentService; +import com.sap.cds.feature.attachments.service.MalwareScannerService; import com.sap.cds.feature.attachments.service.handler.DefaultAttachmentsServiceHandler; +import com.sap.cds.feature.attachments.service.handler.DefaultMalwareScannerServiceHandler; import com.sap.cds.feature.attachments.service.malware.DefaultAttachmentMalwareScanner; import com.sap.cds.services.Service; import com.sap.cds.services.ServiceCatalog; @@ -96,13 +98,16 @@ void setup() { void serviceIsRegistered() { cut.services(configurer); - verify(configurer).service(serviceArgumentCaptor.capture()); + verify(configurer, times(2)).service(serviceArgumentCaptor.capture()); var services = serviceArgumentCaptor.getAllValues(); - assertThat(services).hasSize(1); + assertThat(services).hasSize(2); var attachmentServiceFound = services.stream().anyMatch(AttachmentService.class::isInstance); + var malwareScannerServiceFound = + services.stream().anyMatch(MalwareScannerService.class::isInstance); assertThat(attachmentServiceFound).isTrue(); + assertThat(malwareScannerServiceFound).isTrue(); } @Test @@ -119,7 +124,7 @@ void handlersAreRegistered() { cut.eventHandlers(configurer); - var handlerSize = 8; + var handlerSize = 9; verify(configurer, times(handlerSize)).eventHandler(handlerArgumentCaptor.capture()); checkHandlers(handlerArgumentCaptor.getAllValues(), handlerSize); } @@ -139,7 +144,7 @@ void handlersAreRegisteredWithoutOutboxService() { cut.eventHandlers(configurer); - var handlerSize = 8; + var handlerSize = 9; verify(configurer, times(handlerSize)).eventHandler(handlerArgumentCaptor.capture()); checkHandlers(handlerArgumentCaptor.getAllValues(), handlerSize); } @@ -147,6 +152,7 @@ void handlersAreRegisteredWithoutOutboxService() { private void checkHandlers(List handlers, int handlerSize) { assertThat(handlers).hasSize(handlerSize); isHandlerForClassIncluded(handlers, DefaultAttachmentsServiceHandler.class); + isHandlerForClassIncluded(handlers, DefaultMalwareScannerServiceHandler.class); isHandlerForClassIncluded(handlers, CreateAttachmentsHandler.class); isHandlerForClassIncluded(handlers, UpdateAttachmentsHandler.class); isHandlerForClassIncluded(handlers, DeleteAttachmentsHandler.class); @@ -167,11 +173,12 @@ void lessHandlersAreRegistered() { cut.eventHandlers(configurer); - var handlerSize = 1; + var handlerSize = 2; verify(configurer, times(handlerSize)).eventHandler(handlerArgumentCaptor.capture()); var handlers = handlerArgumentCaptor.getAllValues(); assertThat(handlers).hasSize(handlerSize); isHandlerForClassIncluded(handlers, DefaultAttachmentsServiceHandler.class); + isHandlerForClassIncluded(handlers, DefaultMalwareScannerServiceHandler.class); // event handlers for application services are not registered isHandlerForClassMissing(handlers, CreateAttachmentsHandler.class); isHandlerForClassMissing(handlers, UpdateAttachmentsHandler.class); diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/MalwareScannerServiceImplTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/MalwareScannerServiceImplTest.java new file mode 100644 index 000000000..f3ca1656e --- /dev/null +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/MalwareScannerServiceImplTest.java @@ -0,0 +1,68 @@ +/* + * © 2024-2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.sap.cds.feature.attachments.service.malware.client.MalwareScanResultStatus; +import com.sap.cds.feature.attachments.service.model.servicehandler.MalwareScanEventContext; +import com.sap.cds.services.environment.CdsEnvironment; +import com.sap.cds.services.environment.CdsProperties; +import com.sap.cds.services.handler.Handler; +import com.sap.cds.services.impl.ServiceSPI; +import com.sap.cds.services.runtime.CdsRuntime; +import java.io.InputStream; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +class MalwareScannerServiceImplTest { + + private MalwareScannerServiceImpl cut; + private Handler handler; + private ServiceSPI serviceSpi; + + @BeforeEach + void setup() { + cut = new MalwareScannerServiceImpl(); + + CdsEnvironment env = mock(CdsEnvironment.class); + CdsProperties props = new CdsProperties(); + when(env.getCdsProperties()).thenReturn(props); + CdsRuntime runtime = mock(CdsRuntime.class); + when(runtime.getEnvironment()).thenReturn(env); + handler = mock(Handler.class); + serviceSpi = (ServiceSPI) cut.getDelegatedService(); + serviceSpi.setCdsRuntime(runtime); + } + + @ParameterizedTest + @EnumSource(MalwareScanResultStatus.class) + void scanContentReturnsCorrectStatus(MalwareScanResultStatus expectedStatus) { + var contextReference = new AtomicReference(); + var stream = mock(InputStream.class); + doAnswer( + input -> { + var context = (MalwareScanEventContext) input.getArgument(0); + contextReference.set(context); + context.setCompleted(); + context.setScanResult(expectedStatus); + return null; + }) + .when(handler) + .process(any()); + serviceSpi.on(MalwareScannerService.EVENT_SCAN_CONTENT, "", handler); + + var result = cut.scanContent(stream); + + assertThat(result).isEqualTo(expectedStatus); + assertThat(contextReference.get().getContent()).isEqualTo(stream); + } +} diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandlerTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandlerTest.java new file mode 100644 index 000000000..93ec87ef6 --- /dev/null +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandlerTest.java @@ -0,0 +1,101 @@ +/* + * © 2024-2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.service.handler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.sap.cds.feature.attachments.service.MalwareScannerService; +import com.sap.cds.feature.attachments.service.malware.client.MalwareScanClient; +import com.sap.cds.feature.attachments.service.malware.client.MalwareScanResultStatus; +import com.sap.cds.feature.attachments.service.model.servicehandler.MalwareScanEventContext; +import com.sap.cds.services.handler.annotations.HandlerOrder; +import com.sap.cds.services.handler.annotations.On; +import com.sap.cds.services.handler.annotations.ServiceName; +import java.io.InputStream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class DefaultMalwareScannerServiceHandlerTest { + + private static final int EXPECTED_HANDLER_ORDER = 11000; + + private DefaultMalwareScannerServiceHandler cut; + private MalwareScanClient malwareScanClient; + + @BeforeEach + void setup() { + malwareScanClient = mock(MalwareScanClient.class); + cut = new DefaultMalwareScannerServiceHandler(malwareScanClient); + } + + @Test + void scanContentWithClientReturnsClean() { + when(malwareScanClient.scanContent(any())).thenReturn(MalwareScanResultStatus.CLEAN); + var context = MalwareScanEventContext.create(); + context.setContent(mock(InputStream.class)); + + cut.scanContent(context); + + assertThat(context.isCompleted()).isTrue(); + assertThat(context.getScanResult()).isEqualTo(MalwareScanResultStatus.CLEAN); + } + + @Test + void scanContentWithClientReturnsInfected() { + when(malwareScanClient.scanContent(any())).thenReturn(MalwareScanResultStatus.INFECTED); + var context = MalwareScanEventContext.create(); + context.setContent(mock(InputStream.class)); + + cut.scanContent(context); + + assertThat(context.isCompleted()).isTrue(); + assertThat(context.getScanResult()).isEqualTo(MalwareScanResultStatus.INFECTED); + } + + @Test + void scanContentWithoutClientReturnsNoScanner() { + cut = new DefaultMalwareScannerServiceHandler(null); + var context = MalwareScanEventContext.create(); + context.setContent(mock(InputStream.class)); + + cut.scanContent(context); + + assertThat(context.isCompleted()).isTrue(); + assertThat(context.getScanResult()).isEqualTo(MalwareScanResultStatus.NO_SCANNER); + } + + @Test + void scanContentWithClientThrowingExceptionReturnsFailed() { + when(malwareScanClient.scanContent(any())).thenThrow(new RuntimeException("scan error")); + var context = MalwareScanEventContext.create(); + context.setContent(mock(InputStream.class)); + + cut.scanContent(context); + + assertThat(context.isCompleted()).isTrue(); + assertThat(context.getScanResult()).isEqualTo(MalwareScanResultStatus.FAILED); + } + + @Test + void classHasCorrectAnnotation() { + var annotation = cut.getClass().getAnnotation(ServiceName.class); + + assertThat(annotation.value()).containsOnly("*"); + assertThat(annotation.type()).containsOnly(MalwareScannerService.class); + } + + @Test + void scanMethodHasCorrectAnnotation() throws NoSuchMethodException { + var scanMethod = + cut.getClass().getDeclaredMethod("scanContent", MalwareScanEventContext.class); + var onAnnotation = scanMethod.getAnnotation(On.class); + var handlerOrderAnnotation = scanMethod.getAnnotation(HandlerOrder.class); + + assertThat(onAnnotation.event()).isEmpty(); + assertThat(handlerOrderAnnotation.value()).isEqualTo(EXPECTED_HANDLER_ORDER); + } +} diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/model/servicehandler/MalwareScanEventContextTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/model/servicehandler/MalwareScanEventContextTest.java new file mode 100644 index 000000000..048f831b0 --- /dev/null +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/model/servicehandler/MalwareScanEventContextTest.java @@ -0,0 +1,33 @@ +/* + * © 2024-2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.service.model.servicehandler; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.sap.cds.feature.attachments.service.malware.client.MalwareScanResultStatus; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class MalwareScanEventContextTest { + private MalwareScanEventContext cut; + + @BeforeEach + void setup() { + cut = MalwareScanEventContext.create(); + } + + @Test + void fieldsCanBeSetAndRead() throws IOException { + try (var testStream = new ByteArrayInputStream("testString".getBytes(StandardCharsets.UTF_8))) { + cut.setContent(testStream); + cut.setScanResult(MalwareScanResultStatus.CLEAN); + + assertThat(cut.getContent()).isEqualTo(testStream); + assertThat(cut.getScanResult()).isEqualTo(MalwareScanResultStatus.CLEAN); + } + } +} From c82df9ad1c552c8839fe81e36925406b82335e07 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Fri, 10 Apr 2026 00:03:14 +0200 Subject: [PATCH 02/11] spotless --- .../service/model/servicehandler/MalwareScanEventContext.java | 4 +--- .../attachments/service/MalwareScannerServiceImplTest.java | 1 - .../handler/DefaultMalwareScannerServiceHandlerTest.java | 3 +-- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/model/servicehandler/MalwareScanEventContext.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/model/servicehandler/MalwareScanEventContext.java index a87d6a422..374757d6a 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/model/servicehandler/MalwareScanEventContext.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/model/servicehandler/MalwareScanEventContext.java @@ -9,9 +9,7 @@ import com.sap.cds.services.EventName; import java.io.InputStream; -/** - * The {@link MalwareScanEventContext} is used to store the context of a malware scan event. - */ +/** The {@link MalwareScanEventContext} is used to store the context of a malware scan event. */ @EventName(MalwareScannerService.EVENT_SCAN_CONTENT) public interface MalwareScanEventContext extends EventContext { diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/MalwareScannerServiceImplTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/MalwareScannerServiceImplTest.java index f3ca1656e..8b813ec0a 100644 --- a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/MalwareScannerServiceImplTest.java +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/MalwareScannerServiceImplTest.java @@ -19,7 +19,6 @@ import java.io.InputStream; import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandlerTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandlerTest.java index 93ec87ef6..8fa8b6c09 100644 --- a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandlerTest.java +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandlerTest.java @@ -90,8 +90,7 @@ void classHasCorrectAnnotation() { @Test void scanMethodHasCorrectAnnotation() throws NoSuchMethodException { - var scanMethod = - cut.getClass().getDeclaredMethod("scanContent", MalwareScanEventContext.class); + var scanMethod = cut.getClass().getDeclaredMethod("scanContent", MalwareScanEventContext.class); var onAnnotation = scanMethod.getAnnotation(On.class); var handlerOrderAnnotation = scanMethod.getAnnotation(HandlerOrder.class); From d09a8e302996c5b9dbbe614b6cddc71026f475d1 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Mon, 13 Apr 2026 09:37:00 +0200 Subject: [PATCH 03/11] Add ENCRYPTED status test for DefaultMalwareScannerServiceHandler --- .../DefaultMalwareScannerServiceHandlerTest.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandlerTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandlerTest.java index 8fa8b6c09..8ef08a6bc 100644 --- a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandlerTest.java +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandlerTest.java @@ -56,6 +56,18 @@ void scanContentWithClientReturnsInfected() { assertThat(context.getScanResult()).isEqualTo(MalwareScanResultStatus.INFECTED); } + @Test + void scanContentWithClientReturnsEncrypted() { + when(malwareScanClient.scanContent(any())).thenReturn(MalwareScanResultStatus.ENCRYPTED); + var context = MalwareScanEventContext.create(); + context.setContent(mock(InputStream.class)); + + cut.scanContent(context); + + assertThat(context.isCompleted()).isTrue(); + assertThat(context.getScanResult()).isEqualTo(MalwareScanResultStatus.ENCRYPTED); + } + @Test void scanContentWithoutClientReturnsNoScanner() { cut = new DefaultMalwareScannerServiceHandler(null); From 3d634883d4a0011e87a0c1688274a3938a7d6363 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Mon, 13 Apr 2026 09:41:38 +0200 Subject: [PATCH 04/11] formatting --- .../cds/feature/attachments/service/MalwareScannerService.java | 2 +- .../feature/attachments/service/MalwareScannerServiceImpl.java | 2 +- .../service/handler/DefaultMalwareScannerServiceHandler.java | 2 +- .../attachments/service/MalwareScannerServiceImplTest.java | 2 +- .../handler/DefaultMalwareScannerServiceHandlerTest.java | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/MalwareScannerService.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/MalwareScannerService.java index 44de7e846..a06078446 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/MalwareScannerService.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/MalwareScannerService.java @@ -1,5 +1,5 @@ /* - * © 2024-2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. */ package com.sap.cds.feature.attachments.service; diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/MalwareScannerServiceImpl.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/MalwareScannerServiceImpl.java index f8f5a727a..9854daf4d 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/MalwareScannerServiceImpl.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/MalwareScannerServiceImpl.java @@ -1,5 +1,5 @@ /* - * © 2024-2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. */ package com.sap.cds.feature.attachments.service; diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandler.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandler.java index 3184e7dd8..13efaa905 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandler.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandler.java @@ -1,5 +1,5 @@ /* - * © 2024-2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. */ package com.sap.cds.feature.attachments.service.handler; diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/MalwareScannerServiceImplTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/MalwareScannerServiceImplTest.java index 8b813ec0a..723daf512 100644 --- a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/MalwareScannerServiceImplTest.java +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/MalwareScannerServiceImplTest.java @@ -1,5 +1,5 @@ /* - * © 2024-2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. */ package com.sap.cds.feature.attachments.service; diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandlerTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandlerTest.java index 8ef08a6bc..2c9abd7e3 100644 --- a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandlerTest.java +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandlerTest.java @@ -1,5 +1,5 @@ /* - * © 2024-2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. */ package com.sap.cds.feature.attachments.service.handler; From 11899c450b95117816be84b7b02725fc7b275cea Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Mon, 13 Apr 2026 09:42:42 +0200 Subject: [PATCH 05/11] alignment --- .../service/model/servicehandler/MalwareScanEventContext.java | 2 +- .../model/servicehandler/MalwareScanEventContextTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/model/servicehandler/MalwareScanEventContext.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/model/servicehandler/MalwareScanEventContext.java index 374757d6a..898efbe1a 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/model/servicehandler/MalwareScanEventContext.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/model/servicehandler/MalwareScanEventContext.java @@ -1,5 +1,5 @@ /* - * © 2024-2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. */ package com.sap.cds.feature.attachments.service.model.servicehandler; diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/model/servicehandler/MalwareScanEventContextTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/model/servicehandler/MalwareScanEventContextTest.java index 048f831b0..8271aa8d0 100644 --- a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/model/servicehandler/MalwareScanEventContextTest.java +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/model/servicehandler/MalwareScanEventContextTest.java @@ -1,5 +1,5 @@ /* - * © 2024-2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. */ package com.sap.cds.feature.attachments.service.model.servicehandler; From 1bc761d25ed192cb47fcf8906f9551ae583158fa Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Mon, 13 Apr 2026 14:54:56 +0200 Subject: [PATCH 06/11] add ITests --- .../integrationtests/constants/Profiles.java | 1 + .../TestCustomMalwareScannerHandler.java | 26 ++++ .../TestScanContentActionHandler.java | 42 +++++++ .../MalwareScannerServiceActionTest.java | 111 ++++++++++++++++++ ...alwareScannerServiceCustomHandlerTest.java | 48 ++++++++ .../MalwareScannerServiceNoScannerTest.java | 48 ++++++++ integration-tests/generic/test-service.cds | 6 + 7 files changed, 282 insertions(+) create mode 100644 integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestCustomMalwareScannerHandler.java create mode 100644 integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestScanContentActionHandler.java create mode 100644 integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MalwareScannerServiceActionTest.java create mode 100644 integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MalwareScannerServiceCustomHandlerTest.java create mode 100644 integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MalwareScannerServiceNoScannerTest.java diff --git a/integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/constants/Profiles.java b/integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/constants/Profiles.java index 28273aefc..07301e06a 100644 --- a/integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/constants/Profiles.java +++ b/integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/constants/Profiles.java @@ -8,6 +8,7 @@ public final class Profiles { public static final String TEST_HANDLER_ENABLED = "test-handler-enabled"; public static final String TEST_HANDLER_DISABLED = "test-handler-disabled"; public static final String MALWARE_SCAN_ENABLED = "malware-scan-enabled"; + public static final String CUSTOM_MALWARE_HANDLER = "custom-malware-handler"; private Profiles() { // prevent instantiation diff --git a/integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestCustomMalwareScannerHandler.java b/integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestCustomMalwareScannerHandler.java new file mode 100644 index 000000000..f3b066d51 --- /dev/null +++ b/integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestCustomMalwareScannerHandler.java @@ -0,0 +1,26 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.testhandler; + +import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; +import com.sap.cds.feature.attachments.service.MalwareScannerService; +import com.sap.cds.feature.attachments.service.malware.client.MalwareScanResultStatus; +import com.sap.cds.feature.attachments.service.model.servicehandler.MalwareScanEventContext; +import com.sap.cds.services.handler.EventHandler; +import com.sap.cds.services.handler.annotations.On; +import com.sap.cds.services.handler.annotations.ServiceName; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Component +@ServiceName(value = "*", type = MalwareScannerService.class) +@Profile(Profiles.CUSTOM_MALWARE_HANDLER) +public class TestCustomMalwareScannerHandler implements EventHandler { + + @On + public void scanContent(MalwareScanEventContext context) { + context.setScanResult(MalwareScanResultStatus.ENCRYPTED); + context.setCompleted(); + } +} diff --git a/integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestScanContentActionHandler.java b/integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestScanContentActionHandler.java new file mode 100644 index 000000000..ea7c1a641 --- /dev/null +++ b/integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestScanContentActionHandler.java @@ -0,0 +1,42 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.testhandler; + +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.ScanContentContext; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.ScanResult; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.TestService_; +import com.sap.cds.feature.attachments.service.MalwareScannerService; +import com.sap.cds.feature.attachments.service.malware.client.MalwareScanResultStatus; +import com.sap.cds.services.handler.EventHandler; +import com.sap.cds.services.handler.annotations.On; +import com.sap.cds.services.handler.annotations.ServiceName; +import com.sap.cds.services.runtime.CdsRuntime; +import java.io.ByteArrayInputStream; +import org.springframework.stereotype.Component; + +@Component +@ServiceName(TestService_.CDS_NAME) +public class TestScanContentActionHandler implements EventHandler { + + private final CdsRuntime cdsRuntime; + + public TestScanContentActionHandler(CdsRuntime cdsRuntime) { + this.cdsRuntime = cdsRuntime; + } + + @On(event = ScanContentContext.CDS_NAME) + public void onScanContent(ScanContentContext context) { + MalwareScannerService scanner = + cdsRuntime + .getServiceCatalog() + .getService(MalwareScannerService.class, MalwareScannerService.DEFAULT_NAME); + + MalwareScanResultStatus result = + scanner.scanContent(new ByteArrayInputStream(context.getContent())); + + ScanResult scanResult = ScanResult.create(); + scanResult.setStatus(result.name()); + context.setResult(scanResult); + } +} diff --git a/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MalwareScannerServiceActionTest.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MalwareScannerServiceActionTest.java new file mode 100644 index 000000000..be517e1b7 --- /dev/null +++ b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MalwareScannerServiceActionTest.java @@ -0,0 +1,111 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.nondraftservice; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static org.assertj.core.api.Assertions.assertThat; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.sap.cds.CdsData; +import com.sap.cds.feature.attachments.integrationtests.common.MalwareScanResultProvider; +import com.sap.cds.feature.attachments.integrationtests.common.MockHttpRequestHelper; +import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.ActiveProfiles; +import org.wiremock.spring.ConfigureWireMock; +import org.wiremock.spring.EnableWireMock; +import org.wiremock.spring.InjectWireMock; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles({Profiles.TEST_HANDLER_DISABLED, Profiles.MALWARE_SCAN_ENABLED}) +@EnableWireMock({@ConfigureWireMock(port = 1111, name = "malware-scanner")}) +class MalwareScannerServiceActionTest { + + private static final String SCAN_CONTENT_URL = + MockHttpRequestHelper.ODATA_BASE_URL + "TestService/scanContent"; + + @InjectWireMock("malware-scanner") + private WireMockServer wiremock; + + @Autowired private MockHttpRequestHelper requestHelper; + @Autowired private MalwareScanResultProvider malwareScanResultProvider; + + @BeforeEach + void setup() { + mockMalwareScanResult(false); + } + + @AfterEach + void teardown() { + wiremock.resetAll(); + requestHelper.resetHelper(); + } + + @Test + void scanContentReturnsCleanForSafeContent() throws Exception { + String body = buildScanRequestBody("safe content"); + + CdsData result = + requestHelper.executePostWithODataResponseAndAssertStatus( + SCAN_CONTENT_URL, body, HttpStatus.OK); + + assertThat(result.get("status")).isEqualTo("CLEAN"); + } + + @Test + void scanContentReturnsInfectedForMalware() throws Exception { + wiremock.resetAll(); + mockMalwareScanResult(true); + String body = buildScanRequestBody("malicious content"); + + CdsData result = + requestHelper.executePostWithODataResponseAndAssertStatus( + SCAN_CONTENT_URL, body, HttpStatus.OK); + + assertThat(result.get("status")).isEqualTo("INFECTED"); + } + + @Test + void scanContentReturnsFailedOnScannerError() throws Exception { + wiremock.resetAll(); + wiremock.stubFor( + post(urlEqualTo("/scan")) + .withBasicAuth("test-user", "test-password") + .willReturn(aResponse().withStatus(500))); + String body = buildScanRequestBody("some content"); + + CdsData result = + requestHelper.executePostWithODataResponseAndAssertStatus( + SCAN_CONTENT_URL, body, HttpStatus.OK); + + assertThat(result.get("status")).isEqualTo("FAILED"); + } + + private void mockMalwareScanResult(boolean malware) { + wiremock.stubFor( + post(urlEqualTo("/scan")) + .withBasicAuth("test-user", "test-password") + .willReturn( + aResponse() + .withBody(malwareScanResultProvider.buildMalwareScanResult(malware)) + .withStatus(200))); + } + + private String buildScanRequestBody(String content) { + String base64Content = + Base64.getEncoder().encodeToString(content.getBytes(StandardCharsets.UTF_8)); + return "{\"content\": \"" + base64Content + "\"}"; + } +} diff --git a/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MalwareScannerServiceCustomHandlerTest.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MalwareScannerServiceCustomHandlerTest.java new file mode 100644 index 000000000..930d1f7d3 --- /dev/null +++ b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MalwareScannerServiceCustomHandlerTest.java @@ -0,0 +1,48 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.nondraftservice; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.sap.cds.CdsData; +import com.sap.cds.feature.attachments.integrationtests.common.MockHttpRequestHelper; +import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles({Profiles.TEST_HANDLER_DISABLED, Profiles.CUSTOM_MALWARE_HANDLER}) +class MalwareScannerServiceCustomHandlerTest { + + private static final String SCAN_CONTENT_URL = + MockHttpRequestHelper.ODATA_BASE_URL + "TestService/scanContent"; + + @Autowired private MockHttpRequestHelper requestHelper; + + @AfterEach + void teardown() { + requestHelper.resetHelper(); + } + + @Test + void customHandlerOverridesDefaultScanResult() throws Exception { + String base64Content = + Base64.getEncoder().encodeToString("any content".getBytes(StandardCharsets.UTF_8)); + String body = "{\"content\": \"" + base64Content + "\"}"; + + CdsData result = + requestHelper.executePostWithODataResponseAndAssertStatus( + SCAN_CONTENT_URL, body, HttpStatus.OK); + + assertThat(result.get("status")).isEqualTo("ENCRYPTED"); + } +} diff --git a/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MalwareScannerServiceNoScannerTest.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MalwareScannerServiceNoScannerTest.java new file mode 100644 index 000000000..f9b276434 --- /dev/null +++ b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MalwareScannerServiceNoScannerTest.java @@ -0,0 +1,48 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.nondraftservice; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.sap.cds.CdsData; +import com.sap.cds.feature.attachments.integrationtests.common.MockHttpRequestHelper; +import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles(Profiles.TEST_HANDLER_DISABLED) +class MalwareScannerServiceNoScannerTest { + + private static final String SCAN_CONTENT_URL = + MockHttpRequestHelper.ODATA_BASE_URL + "TestService/scanContent"; + + @Autowired private MockHttpRequestHelper requestHelper; + + @AfterEach + void teardown() { + requestHelper.resetHelper(); + } + + @Test + void scanContentReturnsNoScannerWhenNoBinding() throws Exception { + String base64Content = + Base64.getEncoder().encodeToString("any content".getBytes(StandardCharsets.UTF_8)); + String body = "{\"content\": \"" + base64Content + "\"}"; + + CdsData result = + requestHelper.executePostWithODataResponseAndAssertStatus( + SCAN_CONTENT_URL, body, HttpStatus.OK); + + assertThat(result.get("status")).isEqualTo("NO_SCANNER"); + } +} diff --git a/integration-tests/generic/test-service.cds b/integration-tests/generic/test-service.cds index ff68a31ff..65f166601 100644 --- a/integration-tests/generic/test-service.cds +++ b/integration-tests/generic/test-service.cds @@ -19,6 +19,12 @@ annotate db.Roots.mimeValidatedAttachments with { service TestService { entity Roots as projection on db.Roots; entity AttachmentEntity as projection on db.AttachmentEntity; + + type ScanResult { + status : String; + } + + action scanContent(content : LargeBinary) returns ScanResult; } service TestDraftService { From ba96194a4b8e1b1f87d9f649694aa2ac3d276f11 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Mon, 13 Apr 2026 15:47:48 +0200 Subject: [PATCH 07/11] adapt to oss ITest structure --- .../client/MalwareScannerClientIT.java | 76 ++++++++++++ .../integrationtests/constants/Profiles.java | 1 - .../TestCustomMalwareScannerHandler.java | 26 ---- .../TestScanContentActionHandler.java | 42 ------- .../MalwareScannerServiceActionTest.java | 111 ------------------ ...alwareScannerServiceCustomHandlerTest.java | 48 -------- .../MalwareScannerServiceNoScannerTest.java | 48 -------- integration-tests/generic/test-service.cds | 6 - 8 files changed, 76 insertions(+), 282 deletions(-) create mode 100644 cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/malware/client/MalwareScannerClientIT.java delete mode 100644 integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestCustomMalwareScannerHandler.java delete mode 100644 integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestScanContentActionHandler.java delete mode 100644 integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MalwareScannerServiceActionTest.java delete mode 100644 integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MalwareScannerServiceCustomHandlerTest.java delete mode 100644 integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MalwareScannerServiceNoScannerTest.java diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/malware/client/MalwareScannerClientIT.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/malware/client/MalwareScannerClientIT.java new file mode 100644 index 000000000..3aca78752 --- /dev/null +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/malware/client/MalwareScannerClientIT.java @@ -0,0 +1,76 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.service.malware.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.sap.cds.services.environment.CdsProperties.ConnectionPool; +import com.sap.cloud.environment.servicebinding.api.ServiceBinding; +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.HashMap; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class MalwareScannerClientIT { + // The tests in this class are intended to run against a real Malware Scanner instance. + // They require valid credentials set up in the environment. + + private static final String EICAR_TEST_STRING = + "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"; + + private MalwareScanClient client; + + @BeforeAll + void setUp() { + client = buildClientFromEnvVars(); + Assumptions.assumeTrue( + client != null, "Malware scanner credentials not available — skipping tests"); + } + + @Test + void scanCleanContent() { + MalwareScanResultStatus result = + client.scanContent( + new ByteArrayInputStream("safe content".getBytes(StandardCharsets.UTF_8))); + + assertThat(result).isEqualTo(MalwareScanResultStatus.CLEAN); + } + + @Test + void scanEicarTestFile() { + MalwareScanResultStatus result = + client.scanContent( + new ByteArrayInputStream(EICAR_TEST_STRING.getBytes(StandardCharsets.UTF_8))); + + assertThat(result).isEqualTo(MalwareScanResultStatus.INFECTED); + } + + private static MalwareScanClient buildClientFromEnvVars() { + String url = System.getenv("MALWARE_SCANNER_URL"); + String username = System.getenv("MALWARE_SCANNER_USERNAME"); + String password = System.getenv("MALWARE_SCANNER_PASSWORD"); + + if (url == null || username == null || password == null) { + return null; + } + + ServiceBinding binding = mock(ServiceBinding.class); + HashMap creds = new HashMap<>(); + creds.put("url", url); + creds.put("username", username); + creds.put("password", password); + when(binding.getCredentials()).thenReturn(creds); + + ConnectionPool connectionPool = new ConnectionPool(Duration.ofSeconds(120), 20, 20); + HttpClientProvider clientProvider = new MalwareScanClientProvider(binding, connectionPool); + return new DefaultMalwareScanClient(clientProvider); + } +} diff --git a/integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/constants/Profiles.java b/integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/constants/Profiles.java index 07301e06a..28273aefc 100644 --- a/integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/constants/Profiles.java +++ b/integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/constants/Profiles.java @@ -8,7 +8,6 @@ public final class Profiles { public static final String TEST_HANDLER_ENABLED = "test-handler-enabled"; public static final String TEST_HANDLER_DISABLED = "test-handler-disabled"; public static final String MALWARE_SCAN_ENABLED = "malware-scan-enabled"; - public static final String CUSTOM_MALWARE_HANDLER = "custom-malware-handler"; private Profiles() { // prevent instantiation diff --git a/integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestCustomMalwareScannerHandler.java b/integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestCustomMalwareScannerHandler.java deleted file mode 100644 index f3b066d51..000000000 --- a/integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestCustomMalwareScannerHandler.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.testhandler; - -import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; -import com.sap.cds.feature.attachments.service.MalwareScannerService; -import com.sap.cds.feature.attachments.service.malware.client.MalwareScanResultStatus; -import com.sap.cds.feature.attachments.service.model.servicehandler.MalwareScanEventContext; -import com.sap.cds.services.handler.EventHandler; -import com.sap.cds.services.handler.annotations.On; -import com.sap.cds.services.handler.annotations.ServiceName; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Component -@ServiceName(value = "*", type = MalwareScannerService.class) -@Profile(Profiles.CUSTOM_MALWARE_HANDLER) -public class TestCustomMalwareScannerHandler implements EventHandler { - - @On - public void scanContent(MalwareScanEventContext context) { - context.setScanResult(MalwareScanResultStatus.ENCRYPTED); - context.setCompleted(); - } -} diff --git a/integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestScanContentActionHandler.java b/integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestScanContentActionHandler.java deleted file mode 100644 index ea7c1a641..000000000 --- a/integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestScanContentActionHandler.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.testhandler; - -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.ScanContentContext; -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.ScanResult; -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.TestService_; -import com.sap.cds.feature.attachments.service.MalwareScannerService; -import com.sap.cds.feature.attachments.service.malware.client.MalwareScanResultStatus; -import com.sap.cds.services.handler.EventHandler; -import com.sap.cds.services.handler.annotations.On; -import com.sap.cds.services.handler.annotations.ServiceName; -import com.sap.cds.services.runtime.CdsRuntime; -import java.io.ByteArrayInputStream; -import org.springframework.stereotype.Component; - -@Component -@ServiceName(TestService_.CDS_NAME) -public class TestScanContentActionHandler implements EventHandler { - - private final CdsRuntime cdsRuntime; - - public TestScanContentActionHandler(CdsRuntime cdsRuntime) { - this.cdsRuntime = cdsRuntime; - } - - @On(event = ScanContentContext.CDS_NAME) - public void onScanContent(ScanContentContext context) { - MalwareScannerService scanner = - cdsRuntime - .getServiceCatalog() - .getService(MalwareScannerService.class, MalwareScannerService.DEFAULT_NAME); - - MalwareScanResultStatus result = - scanner.scanContent(new ByteArrayInputStream(context.getContent())); - - ScanResult scanResult = ScanResult.create(); - scanResult.setStatus(result.name()); - context.setResult(scanResult); - } -} diff --git a/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MalwareScannerServiceActionTest.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MalwareScannerServiceActionTest.java deleted file mode 100644 index be517e1b7..000000000 --- a/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MalwareScannerServiceActionTest.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.nondraftservice; - -import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; -import static com.github.tomakehurst.wiremock.client.WireMock.post; -import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; -import static org.assertj.core.api.Assertions.assertThat; - -import com.github.tomakehurst.wiremock.WireMockServer; -import com.sap.cds.CdsData; -import com.sap.cds.feature.attachments.integrationtests.common.MalwareScanResultProvider; -import com.sap.cds.feature.attachments.integrationtests.common.MockHttpRequestHelper; -import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; -import java.nio.charset.StandardCharsets; -import java.util.Base64; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.HttpStatus; -import org.springframework.test.context.ActiveProfiles; -import org.wiremock.spring.ConfigureWireMock; -import org.wiremock.spring.EnableWireMock; -import org.wiremock.spring.InjectWireMock; - -@SpringBootTest -@AutoConfigureMockMvc -@ActiveProfiles({Profiles.TEST_HANDLER_DISABLED, Profiles.MALWARE_SCAN_ENABLED}) -@EnableWireMock({@ConfigureWireMock(port = 1111, name = "malware-scanner")}) -class MalwareScannerServiceActionTest { - - private static final String SCAN_CONTENT_URL = - MockHttpRequestHelper.ODATA_BASE_URL + "TestService/scanContent"; - - @InjectWireMock("malware-scanner") - private WireMockServer wiremock; - - @Autowired private MockHttpRequestHelper requestHelper; - @Autowired private MalwareScanResultProvider malwareScanResultProvider; - - @BeforeEach - void setup() { - mockMalwareScanResult(false); - } - - @AfterEach - void teardown() { - wiremock.resetAll(); - requestHelper.resetHelper(); - } - - @Test - void scanContentReturnsCleanForSafeContent() throws Exception { - String body = buildScanRequestBody("safe content"); - - CdsData result = - requestHelper.executePostWithODataResponseAndAssertStatus( - SCAN_CONTENT_URL, body, HttpStatus.OK); - - assertThat(result.get("status")).isEqualTo("CLEAN"); - } - - @Test - void scanContentReturnsInfectedForMalware() throws Exception { - wiremock.resetAll(); - mockMalwareScanResult(true); - String body = buildScanRequestBody("malicious content"); - - CdsData result = - requestHelper.executePostWithODataResponseAndAssertStatus( - SCAN_CONTENT_URL, body, HttpStatus.OK); - - assertThat(result.get("status")).isEqualTo("INFECTED"); - } - - @Test - void scanContentReturnsFailedOnScannerError() throws Exception { - wiremock.resetAll(); - wiremock.stubFor( - post(urlEqualTo("/scan")) - .withBasicAuth("test-user", "test-password") - .willReturn(aResponse().withStatus(500))); - String body = buildScanRequestBody("some content"); - - CdsData result = - requestHelper.executePostWithODataResponseAndAssertStatus( - SCAN_CONTENT_URL, body, HttpStatus.OK); - - assertThat(result.get("status")).isEqualTo("FAILED"); - } - - private void mockMalwareScanResult(boolean malware) { - wiremock.stubFor( - post(urlEqualTo("/scan")) - .withBasicAuth("test-user", "test-password") - .willReturn( - aResponse() - .withBody(malwareScanResultProvider.buildMalwareScanResult(malware)) - .withStatus(200))); - } - - private String buildScanRequestBody(String content) { - String base64Content = - Base64.getEncoder().encodeToString(content.getBytes(StandardCharsets.UTF_8)); - return "{\"content\": \"" + base64Content + "\"}"; - } -} diff --git a/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MalwareScannerServiceCustomHandlerTest.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MalwareScannerServiceCustomHandlerTest.java deleted file mode 100644 index 930d1f7d3..000000000 --- a/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MalwareScannerServiceCustomHandlerTest.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.nondraftservice; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.sap.cds.CdsData; -import com.sap.cds.feature.attachments.integrationtests.common.MockHttpRequestHelper; -import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; -import java.nio.charset.StandardCharsets; -import java.util.Base64; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.HttpStatus; -import org.springframework.test.context.ActiveProfiles; - -@SpringBootTest -@AutoConfigureMockMvc -@ActiveProfiles({Profiles.TEST_HANDLER_DISABLED, Profiles.CUSTOM_MALWARE_HANDLER}) -class MalwareScannerServiceCustomHandlerTest { - - private static final String SCAN_CONTENT_URL = - MockHttpRequestHelper.ODATA_BASE_URL + "TestService/scanContent"; - - @Autowired private MockHttpRequestHelper requestHelper; - - @AfterEach - void teardown() { - requestHelper.resetHelper(); - } - - @Test - void customHandlerOverridesDefaultScanResult() throws Exception { - String base64Content = - Base64.getEncoder().encodeToString("any content".getBytes(StandardCharsets.UTF_8)); - String body = "{\"content\": \"" + base64Content + "\"}"; - - CdsData result = - requestHelper.executePostWithODataResponseAndAssertStatus( - SCAN_CONTENT_URL, body, HttpStatus.OK); - - assertThat(result.get("status")).isEqualTo("ENCRYPTED"); - } -} diff --git a/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MalwareScannerServiceNoScannerTest.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MalwareScannerServiceNoScannerTest.java deleted file mode 100644 index f9b276434..000000000 --- a/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MalwareScannerServiceNoScannerTest.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.nondraftservice; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.sap.cds.CdsData; -import com.sap.cds.feature.attachments.integrationtests.common.MockHttpRequestHelper; -import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; -import java.nio.charset.StandardCharsets; -import java.util.Base64; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.HttpStatus; -import org.springframework.test.context.ActiveProfiles; - -@SpringBootTest -@AutoConfigureMockMvc -@ActiveProfiles(Profiles.TEST_HANDLER_DISABLED) -class MalwareScannerServiceNoScannerTest { - - private static final String SCAN_CONTENT_URL = - MockHttpRequestHelper.ODATA_BASE_URL + "TestService/scanContent"; - - @Autowired private MockHttpRequestHelper requestHelper; - - @AfterEach - void teardown() { - requestHelper.resetHelper(); - } - - @Test - void scanContentReturnsNoScannerWhenNoBinding() throws Exception { - String base64Content = - Base64.getEncoder().encodeToString("any content".getBytes(StandardCharsets.UTF_8)); - String body = "{\"content\": \"" + base64Content + "\"}"; - - CdsData result = - requestHelper.executePostWithODataResponseAndAssertStatus( - SCAN_CONTENT_URL, body, HttpStatus.OK); - - assertThat(result.get("status")).isEqualTo("NO_SCANNER"); - } -} diff --git a/integration-tests/generic/test-service.cds b/integration-tests/generic/test-service.cds index 65f166601..ff68a31ff 100644 --- a/integration-tests/generic/test-service.cds +++ b/integration-tests/generic/test-service.cds @@ -19,12 +19,6 @@ annotate db.Roots.mimeValidatedAttachments with { service TestService { entity Roots as projection on db.Roots; entity AttachmentEntity as projection on db.AttachmentEntity; - - type ScanResult { - status : String; - } - - action scanContent(content : LargeBinary) returns ScanResult; } service TestDraftService { From a34abb71ad536c13b9b69b5d80a31fb9e59f15f0 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Mon, 13 Apr 2026 16:26:02 +0200 Subject: [PATCH 08/11] add mtls tests --- .../client/MalwareScannerClientIT.java | 127 +++++++++++++----- 1 file changed, 92 insertions(+), 35 deletions(-) diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/malware/client/MalwareScannerClientIT.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/malware/client/MalwareScannerClientIT.java index 3aca78752..def6323c2 100644 --- a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/malware/client/MalwareScannerClientIT.java +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/malware/client/MalwareScannerClientIT.java @@ -15,62 +15,119 @@ import java.util.HashMap; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; -@TestInstance(TestInstance.Lifecycle.PER_CLASS) class MalwareScannerClientIT { // The tests in this class are intended to run against a real Malware Scanner instance. // They require valid credentials set up in the environment. + // Basic auth and mTLS tests skip independently when their credentials are missing. private static final String EICAR_TEST_STRING = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"; - private MalwareScanClient client; + private static final ConnectionPool CONNECTION_POOL = + new ConnectionPool(Duration.ofSeconds(120), 20, 20); - @BeforeAll - void setUp() { - client = buildClientFromEnvVars(); - Assumptions.assumeTrue( - client != null, "Malware scanner credentials not available — skipping tests"); + private static MalwareScanClient buildClient(HashMap creds) { + ServiceBinding binding = mock(ServiceBinding.class); + when(binding.getCredentials()).thenReturn(creds); + HttpClientProvider clientProvider = new MalwareScanClientProvider(binding, CONNECTION_POOL); + return new DefaultMalwareScanClient(clientProvider); } - @Test - void scanCleanContent() { - MalwareScanResultStatus result = - client.scanContent( - new ByteArrayInputStream("safe content".getBytes(StandardCharsets.UTF_8))); - - assertThat(result).isEqualTo(MalwareScanResultStatus.CLEAN); + private static String normalizePem(String pem) { + return pem.replace("\\n", "\n"); } - @Test - void scanEicarTestFile() { - MalwareScanResultStatus result = - client.scanContent( - new ByteArrayInputStream(EICAR_TEST_STRING.getBytes(StandardCharsets.UTF_8))); + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + class BasicAuth { + + private MalwareScanClient client; + + @BeforeAll + void setUp() { + String url = System.getenv("MALWARE_SCANNER_URL"); + String username = System.getenv("MALWARE_SCANNER_USERNAME"); + String password = System.getenv("MALWARE_SCANNER_PASSWORD"); + + if (url == null || username == null || password == null) { + client = null; + } else { + HashMap creds = new HashMap<>(); + creds.put("url", url); + creds.put("username", username); + creds.put("password", password); + client = buildClient(creds); + } + + Assumptions.assumeTrue( + client != null, "Basic auth malware scanner credentials not available — skipping tests"); + } + + @Test + void scanCleanContent() { + MalwareScanResultStatus result = + client.scanContent( + new ByteArrayInputStream("safe content".getBytes(StandardCharsets.UTF_8))); + + assertThat(result).isEqualTo(MalwareScanResultStatus.CLEAN); + } + + @Test + void scanEicarTestFile() { + MalwareScanResultStatus result = + client.scanContent( + new ByteArrayInputStream(EICAR_TEST_STRING.getBytes(StandardCharsets.UTF_8))); - assertThat(result).isEqualTo(MalwareScanResultStatus.INFECTED); + assertThat(result).isEqualTo(MalwareScanResultStatus.INFECTED); + } } - private static MalwareScanClient buildClientFromEnvVars() { - String url = System.getenv("MALWARE_SCANNER_URL"); - String username = System.getenv("MALWARE_SCANNER_USERNAME"); - String password = System.getenv("MALWARE_SCANNER_PASSWORD"); + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + class Mtls { + + private MalwareScanClient client; - if (url == null || username == null || password == null) { - return null; + @BeforeAll + void setUp() { + String uri = System.getenv("MALWARE_SCANNER_MTLS_URI"); + String certificate = System.getenv("MALWARE_SCANNER_MTLS_CERTIFICATE"); + String key = System.getenv("MALWARE_SCANNER_MTLS_KEY"); + + if (uri == null || certificate == null || key == null) { + client = null; + } else { + HashMap creds = new HashMap<>(); + creds.put("uri", uri); + creds.put("certificate", normalizePem(certificate)); + creds.put("key", normalizePem(key)); + client = buildClient(creds); + } + + Assumptions.assumeTrue( + client != null, "mTLS malware scanner credentials not available — skipping tests"); } - ServiceBinding binding = mock(ServiceBinding.class); - HashMap creds = new HashMap<>(); - creds.put("url", url); - creds.put("username", username); - creds.put("password", password); - when(binding.getCredentials()).thenReturn(creds); + @Test + void scanCleanContent() { + MalwareScanResultStatus result = + client.scanContent( + new ByteArrayInputStream("safe content".getBytes(StandardCharsets.UTF_8))); - ConnectionPool connectionPool = new ConnectionPool(Duration.ofSeconds(120), 20, 20); - HttpClientProvider clientProvider = new MalwareScanClientProvider(binding, connectionPool); - return new DefaultMalwareScanClient(clientProvider); + assertThat(result).isEqualTo(MalwareScanResultStatus.CLEAN); + } + + @Test + void scanEicarTestFile() { + MalwareScanResultStatus result = + client.scanContent( + new ByteArrayInputStream(EICAR_TEST_STRING.getBytes(StandardCharsets.UTF_8))); + + assertThat(result).isEqualTo(MalwareScanResultStatus.INFECTED); + } } } From f0193d78c12e757edd038406158872335f5966cd Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Mon, 13 Apr 2026 17:49:56 +0200 Subject: [PATCH 09/11] add new credentials to pipeline --- .github/workflows/pipeline.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index e4a41fe2f..197360b38 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -16,6 +16,14 @@ env: GS_BASE_64_ENCODED_PRIVATE_KEY_DATA: ${{ secrets.GS_BASE_64_ENCODED_PRIVATE_KEY_DATA }} GS_BUCKET: ${{ secrets.GS_BUCKET }} GS_PROJECT_ID: ${{ secrets.GS_PROJECT_ID }} + ## Malware Scanner (Basic Auth) + MALWARE_SCANNER_URL: ${{ secrets.MALWARE_SCANNER_URL }} + MALWARE_SCANNER_USERNAME: ${{ secrets.MALWARE_SCANNER_USERNAME }} + MALWARE_SCANNER_PASSWORD: ${{ secrets.MALWARE_SCANNER_PASSWORD }} + ## Malware Scanner (mTLS) + MALWARE_SCANNER_MTLS_URI: ${{ secrets.MALWARE_SCANNER_MTLS_URI }} + MALWARE_SCANNER_MTLS_CERTIFICATE: ${{ secrets.MALWARE_SCANNER_MTLS_CERTIFICATE }} + MALWARE_SCANNER_MTLS_KEY: ${{ secrets.MALWARE_SCANNER_MTLS_KEY }} on: workflow_call: From 3518f6e505fe78070f39d801d05d57d432bd729c Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Mon, 13 Apr 2026 21:41:46 +0200 Subject: [PATCH 10/11] minor improvements --- .github/workflows/pipeline.yml | 44 +++++++++---------- .../DefaultMalwareScannerServiceHandler.java | 2 +- ...ClientIT.java => MalwareScanClientIT.java} | 26 +++++------ 3 files changed, 36 insertions(+), 36 deletions(-) rename cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/malware/client/{MalwareScannerClientIT.java => MalwareScanClientIT.java} (87%) diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 197360b38..04115d95f 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -2,28 +2,6 @@ name: Reusable Workflow env: MAVEN_VERSION: '3.9.12' - # Cloud storage environment variables (available to all jobs that need them) - ## AWS - AWS_S3_HOST: ${{ secrets.AWS_S3_HOST }} - AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }} - AWS_S3_REGION: ${{ secrets.AWS_S3_REGION }} - AWS_S3_ACCESS_KEY_ID: ${{ secrets.AWS_S3_ACCESS_KEY_ID }} - AWS_S3_SECRET_ACCESS_KEY: ${{ secrets.AWS_S3_SECRET_ACCESS_KEY }} - ## Azure - AZURE_CONTAINER_URI: ${{ secrets.AZURE_CONTAINER_URI }} - AZURE_SAS_TOKEN: ${{ secrets.AZURE_SAS_TOKEN }} - ## GCP - GS_BASE_64_ENCODED_PRIVATE_KEY_DATA: ${{ secrets.GS_BASE_64_ENCODED_PRIVATE_KEY_DATA }} - GS_BUCKET: ${{ secrets.GS_BUCKET }} - GS_PROJECT_ID: ${{ secrets.GS_PROJECT_ID }} - ## Malware Scanner (Basic Auth) - MALWARE_SCANNER_URL: ${{ secrets.MALWARE_SCANNER_URL }} - MALWARE_SCANNER_USERNAME: ${{ secrets.MALWARE_SCANNER_USERNAME }} - MALWARE_SCANNER_PASSWORD: ${{ secrets.MALWARE_SCANNER_PASSWORD }} - ## Malware Scanner (mTLS) - MALWARE_SCANNER_MTLS_URI: ${{ secrets.MALWARE_SCANNER_MTLS_URI }} - MALWARE_SCANNER_MTLS_CERTIFICATE: ${{ secrets.MALWARE_SCANNER_MTLS_CERTIFICATE }} - MALWARE_SCANNER_MTLS_KEY: ${{ secrets.MALWARE_SCANNER_MTLS_KEY }} on: workflow_call: @@ -73,6 +51,28 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 needs: build + env: + ## AWS + AWS_S3_HOST: ${{ secrets.AWS_S3_HOST }} + AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }} + AWS_S3_REGION: ${{ secrets.AWS_S3_REGION }} + AWS_S3_ACCESS_KEY_ID: ${{ secrets.AWS_S3_ACCESS_KEY_ID }} + AWS_S3_SECRET_ACCESS_KEY: ${{ secrets.AWS_S3_SECRET_ACCESS_KEY }} + ## Azure + AZURE_CONTAINER_URI: ${{ secrets.AZURE_CONTAINER_URI }} + AZURE_SAS_TOKEN: ${{ secrets.AZURE_SAS_TOKEN }} + ## GCP + GS_BASE_64_ENCODED_PRIVATE_KEY_DATA: ${{ secrets.GS_BASE_64_ENCODED_PRIVATE_KEY_DATA }} + GS_BUCKET: ${{ secrets.GS_BUCKET }} + GS_PROJECT_ID: ${{ secrets.GS_PROJECT_ID }} + ## Malware Scanner (Basic Auth) + MALWARE_SCANNER_URL: ${{ secrets.MALWARE_SCANNER_URL }} + MALWARE_SCANNER_USERNAME: ${{ secrets.MALWARE_SCANNER_USERNAME }} + MALWARE_SCANNER_PASSWORD: ${{ secrets.MALWARE_SCANNER_PASSWORD }} + ## Malware Scanner (mTLS) + MALWARE_SCANNER_MTLS_URI: ${{ secrets.MALWARE_SCANNER_MTLS_URI }} + MALWARE_SCANNER_MTLS_CERTIFICATE: ${{ secrets.MALWARE_SCANNER_MTLS_CERTIFICATE }} + MALWARE_SCANNER_MTLS_KEY: ${{ secrets.MALWARE_SCANNER_MTLS_KEY }} strategy: fail-fast: false matrix: diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandler.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandler.java index 13efaa905..97d038a34 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandler.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandler.java @@ -54,7 +54,7 @@ void scanContent(MalwareScanEventContext context) { MalwareScanResultStatus result = malwareScanClient.scanContent(context.getContent()); context.setScanResult(result); logger.debug("Malware scan completed with status '{}'.", result); - } catch (RuntimeException e) { + } catch (Exception e) { logger.error("Error during malware scan.", e); context.setScanResult(MalwareScanResultStatus.FAILED); } diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/malware/client/MalwareScannerClientIT.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/malware/client/MalwareScanClientIT.java similarity index 87% rename from cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/malware/client/MalwareScannerClientIT.java rename to cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/malware/client/MalwareScanClientIT.java index def6323c2..e29302c74 100644 --- a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/malware/client/MalwareScannerClientIT.java +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/malware/client/MalwareScanClientIT.java @@ -12,14 +12,14 @@ import java.io.ByteArrayInputStream; import java.nio.charset.StandardCharsets; import java.time.Duration; -import java.util.HashMap; +import java.util.Map; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; -class MalwareScannerClientIT { +class MalwareScanClientIT { // The tests in this class are intended to run against a real Malware Scanner instance. // They require valid credentials set up in the environment. // Basic auth and mTLS tests skip independently when their credentials are missing. @@ -30,7 +30,7 @@ class MalwareScannerClientIT { private static final ConnectionPool CONNECTION_POOL = new ConnectionPool(Duration.ofSeconds(120), 20, 20); - private static MalwareScanClient buildClient(HashMap creds) { + private static MalwareScanClient buildClient(Map creds) { ServiceBinding binding = mock(ServiceBinding.class); when(binding.getCredentials()).thenReturn(creds); HttpClientProvider clientProvider = new MalwareScanClientProvider(binding, CONNECTION_POOL); @@ -56,11 +56,7 @@ void setUp() { if (url == null || username == null || password == null) { client = null; } else { - HashMap creds = new HashMap<>(); - creds.put("url", url); - creds.put("username", username); - creds.put("password", password); - client = buildClient(creds); + client = buildClient(Map.of("url", url, "username", username, "password", password)); } Assumptions.assumeTrue( @@ -101,11 +97,15 @@ void setUp() { if (uri == null || certificate == null || key == null) { client = null; } else { - HashMap creds = new HashMap<>(); - creds.put("uri", uri); - creds.put("certificate", normalizePem(certificate)); - creds.put("key", normalizePem(key)); - client = buildClient(creds); + client = + buildClient( + Map.of( + "uri", + uri, + "certificate", + normalizePem(certificate), + "key", + normalizePem(key))); } Assumptions.assumeTrue( From 0881fb8be2d4fc2feb437d5c33793b0794d24ee9 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Mon, 13 Apr 2026 21:51:51 +0200 Subject: [PATCH 11/11] parameterize tests --- ...faultMalwareScannerServiceHandlerTest.java | 37 +++++-------------- 1 file changed, 9 insertions(+), 28 deletions(-) diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandlerTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandlerTest.java index 2c9abd7e3..2fd99ca6b 100644 --- a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandlerTest.java +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandlerTest.java @@ -18,6 +18,8 @@ import java.io.InputStream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; class DefaultMalwareScannerServiceHandlerTest { @@ -32,40 +34,19 @@ void setup() { cut = new DefaultMalwareScannerServiceHandler(malwareScanClient); } - @Test - void scanContentWithClientReturnsClean() { - when(malwareScanClient.scanContent(any())).thenReturn(MalwareScanResultStatus.CLEAN); - var context = MalwareScanEventContext.create(); - context.setContent(mock(InputStream.class)); - - cut.scanContent(context); - - assertThat(context.isCompleted()).isTrue(); - assertThat(context.getScanResult()).isEqualTo(MalwareScanResultStatus.CLEAN); - } - - @Test - void scanContentWithClientReturnsInfected() { - when(malwareScanClient.scanContent(any())).thenReturn(MalwareScanResultStatus.INFECTED); - var context = MalwareScanEventContext.create(); - context.setContent(mock(InputStream.class)); - - cut.scanContent(context); - - assertThat(context.isCompleted()).isTrue(); - assertThat(context.getScanResult()).isEqualTo(MalwareScanResultStatus.INFECTED); - } - - @Test - void scanContentWithClientReturnsEncrypted() { - when(malwareScanClient.scanContent(any())).thenReturn(MalwareScanResultStatus.ENCRYPTED); + @ParameterizedTest + @EnumSource( + value = MalwareScanResultStatus.class, + names = {"CLEAN", "INFECTED", "ENCRYPTED"}) + void scanContentWithClientReturnsStatus(MalwareScanResultStatus expectedStatus) { + when(malwareScanClient.scanContent(any())).thenReturn(expectedStatus); var context = MalwareScanEventContext.create(); context.setContent(mock(InputStream.class)); cut.scanContent(context); assertThat(context.isCompleted()).isTrue(); - assertThat(context.getScanResult()).isEqualTo(MalwareScanResultStatus.ENCRYPTED); + assertThat(context.getScanResult()).isEqualTo(expectedStatus); } @Test