From 3f228ec173ddbd1573281e54ee12080ac8c9035b Mon Sep 17 00:00:00 2001 From: samyuktaprabhu <33195453+samyuktaprabhu@users.noreply.github.com> Date: Wed, 11 Feb 2026 12:57:35 +0100 Subject: [PATCH 01/17] implement feature to restrict mime-types --- README.md | 33 ++++ .../CreateAttachmentsHandler.java | 13 +- .../helper/AttachmentValidationHelper.java | 152 ++++++++++++++++++ .../common/ApplicationHandlerHelper.java | 84 ++++++++-- samples/bookshop/srv/attachments.cds | 20 ++- 5 files changed, 281 insertions(+), 21 deletions(-) create mode 100644 cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelper.java diff --git a/README.md b/README.md index a5b3f206..af5d0704 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ It supports the [AWS, Azure, and Google object stores](storage-targets/cds-featu * [Storage Targets](#storage-targets) * [Malware Scanner](#malware-scanner) * [Specify the maximum file size](#specify-the-maximum-file-size) + * [Restrict allowed MIME types](#restrict-allowed-mime-types) * [Outbox](#outbox) * [Restore Endpoint](#restore-endpoint) * [Motivation](#motivation) @@ -214,6 +215,38 @@ The @Validation.Maximum value is a size string consisting of a number followed b The default is 400MB +### Restrict allowed MIME types + +You can restrict which MIME types are allowed for attachments by annotating the content property with @Core.AcceptableMediaTypes. This validation is performed during file upload. + +```cds +entity Books { + ... + attachments: Composition of many Attachments; +} + +annotate Books.attachments with { + content @Core.AcceptableMediaTypes : ['image/jpeg', 'image/png', 'application/pdf']; +} +``` + +Wildcard patterns are supported: + +```cds +annotate Books.attachments with { + content @Core.AcceptableMediaTypes : ['image/*', 'application/pdf']; +} +``` + +To allow all MIME types (default behavior), either omit the annotation or use: + +```cds +annotate Books.attachments with { + content @Core.AcceptableMediaTypes : ['*/*']; +} +``` + + ### Outbox In this plugin the [persistent outbox](https://cap.cloud.sap/docs/java/outbox#persistent) is used to mark attachments as diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/CreateAttachmentsHandler.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/CreateAttachmentsHandler.java index 6a10baf6..05fb47ec 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/CreateAttachmentsHandler.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/CreateAttachmentsHandler.java @@ -12,6 +12,7 @@ import com.sap.cds.feature.attachments.handler.applicationservice.helper.ThreadDataStorageReader; import com.sap.cds.feature.attachments.handler.applicationservice.modifyevents.ModifyAttachmentEventFactory; import com.sap.cds.feature.attachments.handler.common.ApplicationHandlerHelper; +import com.sap.cds.reflect.CdsEntity; import com.sap.cds.services.EventContext; import com.sap.cds.services.ServiceException; import com.sap.cds.services.cds.ApplicationService; @@ -30,7 +31,8 @@ import org.slf4j.LoggerFactory; /** - * The class {@link CreateAttachmentsHandler} is an event handler that is responsible for creating + * The class {@link CreateAttachmentsHandler} is an event handler that is + * responsible for creating * attachments for entities. It is called before a create event is executed. */ @ServiceName(value = "*", type = ApplicationService.class) @@ -61,6 +63,13 @@ void processBeforeForDraft(CdsCreateEventContext context, List data) { context.getTarget(), data, storageReader.get()); } + @Before(event = { CqnService.EVENT_CREATE, DraftService.EVENT_DRAFT_NEW }) + @HandlerOrder(HandlerOrder.BEFORE) + void processBeforeForMetadata(EventContext context, List data) { + CdsEntity target = context.getTarget(); + ApplicationHandlerHelper.validateAcceptableMediaTypes(target, data); + } + @Before @HandlerOrder(HandlerOrder.LATE) void processBefore(CdsCreateEventContext context, List data) { @@ -72,7 +81,7 @@ void processBefore(CdsCreateEventContext context, List data) { } } - @On(event = {CqnService.EVENT_CREATE, CqnService.EVENT_UPDATE, DraftService.EVENT_DRAFT_PATCH}) + @On(event = { CqnService.EVENT_CREATE, CqnService.EVENT_UPDATE, DraftService.EVENT_DRAFT_PATCH }) @HandlerOrder(HandlerOrder.EARLY) void restoreError(EventContext context) { try { diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelper.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelper.java new file mode 100644 index 00000000..5d09a6ee --- /dev/null +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelper.java @@ -0,0 +1,152 @@ +/* +* © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. +*/ +package com.sap.cds.feature.attachments.handler.applicationservice.helper; + +import java.net.URLConnection; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.sap.cds.services.ServiceException; +import com.sap.cds.services.ErrorStatuses; + +public class AttachmentValidationHelper { + + public static final String DEFAULT_MEDIA_TYPE = "application/octet-stream"; + public static final Map EXT_TO_MEDIA_TYPE = Map.ofEntries( + Map.entry("aac", "audio/aac"), + Map.entry("abw", "application/x-abiword"), + Map.entry("arc", "application/octet-stream"), + Map.entry("avi", "video/x-msvideo"), + Map.entry("azw", "application/vnd.amazon.ebook"), + Map.entry("bin", "application/octet-stream"), + Map.entry("png", "image/png"), + Map.entry("gif", "image/gif"), + Map.entry("bmp", "image/bmp"), + Map.entry("bz", "application/x-bzip"), + Map.entry("bz2", "application/x-bzip2"), + Map.entry("csh", "application/x-csh"), + Map.entry("css", "text/css"), + Map.entry("csv", "text/csv"), + Map.entry("doc", "application/msword"), + Map.entry("docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"), + Map.entry("odp", "application/vnd.oasis.opendocument.presentation"), + Map.entry("ods", "application/vnd.oasis.opendocument.spreadsheet"), + Map.entry("odt", "application/vnd.oasis.opendocument.text"), + Map.entry("epub", "application/epub+zip"), + Map.entry("gz", "application/gzip"), + Map.entry("htm", "text/html"), + Map.entry("html", "text/html"), + Map.entry("ico", "image/x-icon"), + Map.entry("ics", "text/calendar"), + Map.entry("jar", "application/java-archive"), + Map.entry("jpg", "image/jpeg"), + Map.entry("jpeg", "image/jpeg"), + Map.entry("js", "text/javascript"), + Map.entry("json", "application/json"), + Map.entry("mid", "audio/midi"), + Map.entry("midi", "audio/midi"), + Map.entry("mjs", "text/javascript"), + Map.entry("mov", "video/quicktime"), + Map.entry("mp3", "audio/mpeg"), + Map.entry("mp4", "video/mp4"), + Map.entry("mpeg", "video/mpeg"), + Map.entry("mpkg", "application/vnd.apple.installer+xml"), + Map.entry("otf", "font/otf"), + Map.entry("pdf", "application/pdf"), + Map.entry("ppt", "application/vnd.ms-powerpoint"), + Map.entry("pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"), + Map.entry("rar", "application/x-rar-compressed"), + Map.entry("rtf", "application/rtf"), + Map.entry("svg", "image/svg+xml"), + Map.entry("tar", "application/x-tar"), + Map.entry("tif", "image/tiff"), + Map.entry("tiff", "image/tiff"), + Map.entry("ttf", "font/ttf"), + Map.entry("vsd", "application/vnd.visio"), + Map.entry("wav", "audio/wav"), + Map.entry("woff", "font/woff"), + Map.entry("woff2", "font/woff2"), + Map.entry("xhtml", "application/xhtml+xml"), + Map.entry("xls", "application/vnd.ms-excel"), + Map.entry("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"), + Map.entry("xml", "application/xml"), + Map.entry("zip", "application/zip"), + Map.entry("txt", "application/txt"), + Map.entry("lst", "application/txt"), + Map.entry("webp", "image/webp")); + + private static final Logger logger = LoggerFactory.getLogger(AttachmentValidationHelper.class); + + /** + * Validates the file name and resolves its media type. Ensures that the + * detected media type is part of the list of acceptable media types. + * + * @param fileName the name of the attachment file + * @param acceptableMediaTypes list of allowed media types (e.g. "image/*", + * "application/pdf") + * @return the detected media type + * @throws ServiceException if the file name is invalid or the media type is not + * allowed + */ + public static String validateMediaTypeForAttachment(String fileName, List acceptableMediaTypes) { + validateFileName(fileName); + String detectedMediaType = resolveMimeType(fileName); + validateAcceptableMediaType(acceptableMediaTypes, detectedMediaType); + return detectedMediaType; + } + + private static void validateFileName(String fileName) { + String clean = fileName.trim(); + int lastDotIndex = clean.lastIndexOf('.'); + if (lastDotIndex <= 0 || lastDotIndex == clean.length() - 1) { + throw new ServiceException( + ErrorStatuses.UNSUPPORTED_MEDIA_TYPE, + "Invalid filename format: " + fileName); + } + } + + private static void validateAcceptableMediaType(List acceptableMediaTypes, String actualMimeType) { + if (!checkMimeTypeMatch(acceptableMediaTypes, actualMimeType)) { + throw new ServiceException( + ErrorStatuses.UNSUPPORTED_MEDIA_TYPE, + "The attachment file type '{}' is not allowed. Allowed types are: {}", actualMimeType, + String.join(", ", acceptableMediaTypes)); + } + } + + private static String resolveMimeType(String fileName) { + String actualMimeType = URLConnection.guessContentTypeFromName(fileName); + + if (actualMimeType == null) { + String fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase(); + actualMimeType = EXT_TO_MEDIA_TYPE.get(fileExtension); + + if (actualMimeType == null) { + logger.warn("Could not determine mime type for file: {}. Setting mime type to default: {}", + fileName, DEFAULT_MEDIA_TYPE); + actualMimeType = DEFAULT_MEDIA_TYPE; + } + } + return actualMimeType; + } + + private static boolean checkMimeTypeMatch(Collection acceptableMediaTypes, String mimeType) { + if (acceptableMediaTypes == null || acceptableMediaTypes.isEmpty() || acceptableMediaTypes.contains("*/*")) + return true; + + String baseMimeType = mimeType.trim().toLowerCase(); + + return acceptableMediaTypes.stream().anyMatch(type -> { + String normalizedType = type.trim().toLowerCase(); + return normalizedType.endsWith("/*") + ? baseMimeType.startsWith(normalizedType.substring(0, normalizedType.length() - 2) + "/") + : baseMimeType.equals(normalizedType); + }); + } + +} \ No newline at end of file diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelper.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelper.java index c8308f51..be9459bd 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelper.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelper.java @@ -5,43 +5,58 @@ import static java.util.Objects.nonNull; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import com.sap.cds.CdsData; import com.sap.cds.CdsDataProcessor; import com.sap.cds.CdsDataProcessor.Filter; import com.sap.cds.CdsDataProcessor.Validator; import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.Attachments; +import com.sap.cds.feature.attachments.handler.applicationservice.helper.AttachmentValidationHelper; +import com.sap.cds.reflect.CdsAnnotation; import com.sap.cds.reflect.CdsEntity; import com.sap.cds.reflect.CdsStructuredType; +import com.sap.cds.services.ErrorStatuses; +import com.sap.cds.services.ServiceException; import com.sap.cds.services.draft.Drafts; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; /** - * The class {@link ApplicationHandlerHelper} provides helper methods for the attachment application + * The class {@link ApplicationHandlerHelper} provides helper methods for the + * attachment application * handlers. */ public final class ApplicationHandlerHelper { private static final String ANNOTATION_IS_MEDIA_DATA = "_is_media_data"; private static final String ANNOTATION_CORE_MEDIA_TYPE = "Core.MediaType"; + private static final ObjectMapper objectMapper = new ObjectMapper(); /** - * A filter for media content fields. The filter checks if the entity is a media entity and if the + * Filter to support extraction of file name for attachment validation + */ + public static final Filter FILE_NAME_FILTER = (path, element, type) -> element.getName().contentEquals("fileName"); + /** + * A filter for media content fields. The filter checks if the entity is a media + * entity and if the * element has the annotation "Core.MediaType". */ - public static final Filter MEDIA_CONTENT_FILTER = - (path, element, type) -> - isMediaEntity(path.target().type()) - && element.findAnnotation(ANNOTATION_CORE_MEDIA_TYPE).isPresent(); + public static final Filter MEDIA_CONTENT_FILTER = (path, element, type) -> isMediaEntity(path.target().type()) + && element.findAnnotation(ANNOTATION_CORE_MEDIA_TYPE).isPresent(); /** * Checks if the data contains a content field. * - * @param entity The {@link CdsEntity entity} type of the given the data to check - * @param data The data to check - * @return true if the data contains a content field, false otherwise + * @param entity The {@link CdsEntity entity} type of the given the data to + * check + * @param data The data to check + * @return true if the data contains a content field, + * false otherwise */ public static boolean containsContentField(CdsEntity entity, List data) { AtomicBoolean isIncluded = new AtomicBoolean(); @@ -52,11 +67,13 @@ public static boolean containsContentField(CdsEntity entity, Listtrue if the entity is a media entity, false otherwise + * @return true if the entity is a media entity, false + * otherwise */ public static boolean isMediaEntity(CdsStructuredType baseEntity) { return baseEntity.getAnnotationValue(ANNOTATION_IS_MEDIA_DATA, false); @@ -65,7 +82,7 @@ public static boolean isMediaEntity(CdsStructuredType baseEntity) { /** * Extracts key fields from CdsData based on the entity definition. * - * @param data The CdsData to extract keys from + * @param data The CdsData to extract keys from * @param entity The entity definition * @return A map of key fields and their values */ @@ -85,9 +102,10 @@ public static Map extractKeys(CdsData data, CdsEntity entity) { } /** - * Condenses the attachments from the given data into a list of {@link Attachments attachments}. + * Condenses the attachments from the given data into a list of + * {@link Attachments attachments}. * - * @param data the list of {@link CdsData} to process + * @param data the list of {@link CdsData} to process * @param entity the {@link CdsEntity entity} type of the given data * @return a list of {@link Attachments attachments} condensed from the data */ @@ -95,8 +113,7 @@ public static List condenseAttachments( List data, CdsEntity entity) { List resultList = new ArrayList<>(); - Validator validator = - (path, element, value) -> resultList.add(Attachments.of(path.target().values())); + Validator validator = (path, element, value) -> resultList.add(Attachments.of(path.target().values())); CdsDataProcessor.create().addValidator(MEDIA_CONTENT_FILTER, validator).process(data, entity); return resultList; @@ -123,6 +140,41 @@ public static Map removeDraftKey(Map keys) { return keyMap; } + public static void validateAcceptableMediaTypes(CdsEntity entity, List data) { + List allowedTypes = getEntityAcceptableMediaTypes(entity); + String fileName = extractFileName(entity, data); + AttachmentValidationHelper.validateMediaTypeForAttachment(fileName, allowedTypes); + } + + private static List getEntityAcceptableMediaTypes(CdsEntity entity) { + Optional> acceptableMediaTypeFromContent = entity.getElement("content") + .findAnnotation("Core.AcceptableMediaTypes"); + + return acceptableMediaTypeFromContent + .map(a -> objectMapper.convertValue( + a.getValue(), + new TypeReference>() { + })) + .orElse(List.of("*/*")); + } + + private static String extractFileName(CdsEntity entity, List data) { + CdsDataProcessor processor = CdsDataProcessor.create(); + AtomicReference fileNameRef = new AtomicReference<>(); + Validator validator = (path, element, value) -> { + if (element.getName().contentEquals("fileName") && value instanceof String) { + fileNameRef.set((String) value); + } + }; + + processor.addValidator(FILE_NAME_FILTER, validator).process(data, entity); + + if (fileNameRef.get() == null) { + throw new ServiceException(ErrorStatuses.BAD_REQUEST, "Filename is missing"); + } + return fileNameRef.get(); + } + private ApplicationHandlerHelper() { // avoid instantiation } diff --git a/samples/bookshop/srv/attachments.cds b/samples/bookshop/srv/attachments.cds index 04f4d554..9b42f8f2 100644 --- a/samples/bookshop/srv/attachments.cds +++ b/samples/bookshop/srv/attachments.cds @@ -4,15 +4,25 @@ using {sap.attachments.Attachments} from 'com.sap.cds/cds-feature-attachments'; // Extend Books entity to support file attachments (images, PDFs, documents) // Each book can have multiple attachments via composition relationship extend my.Books with { - attachments : Composition of many Attachments; + attachments : Composition of many Attachments; @UI.Hidden - sizeLimitedAttachments : Composition of many Attachments; + sizeLimitedAttachments : Composition of many Attachments; + @UI.Hidden + mediaValidatedAttachments : Composition of many Attachments; } annotate my.Books.sizeLimitedAttachments with { content @Validation.Maximum: '5MB'; } +// Media type validation for attachments +annotate my.Books.mediaValidatedAttachments with { + content @Core.AcceptableMediaTypes: [ + 'image/jpeg', + 'image/png' + ]; +} + // Add UI component for attachments table to the Browse Books App using {CatalogService as service} from '../app/services'; @@ -30,5 +40,9 @@ annotate adminService.Books with @(UI.Facets: [{ $Type : 'UI.ReferenceFacet', ID : 'AttachmentsFacet', Label : '{i18n>attachments}', - Target: 'attachments/@UI.LineItem' + Target: 'mediaValidatedAttachments/@UI.LineItem' }]); + +service nonDraft { + entity Books as projection on my.Books; +} From 8ca3b4bcb7b6bdd6efde67d5032be47dcac2242b Mon Sep 17 00:00:00 2001 From: samyuktaprabhu <33195453+samyuktaprabhu@users.noreply.github.com> Date: Wed, 11 Feb 2026 14:34:48 +0100 Subject: [PATCH 02/17] update a comment + access modifiers --- .../handler/common/ApplicationHandlerHelper.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelper.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelper.java index be9459bd..14cb4a79 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelper.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelper.java @@ -140,13 +140,22 @@ public static Map removeDraftKey(Map keys) { return keyMap; } + /** + * Validates if the media type of the attachment in the given fileName is + * acceptable + * + * @param entity the {@link CdsEntity entity} type of the given data + * @param data the list of {@link CdsData} to process + * @throws ServiceException if the media type of the attachment is not + * acceptable + */ public static void validateAcceptableMediaTypes(CdsEntity entity, List data) { List allowedTypes = getEntityAcceptableMediaTypes(entity); String fileName = extractFileName(entity, data); AttachmentValidationHelper.validateMediaTypeForAttachment(fileName, allowedTypes); } - private static List getEntityAcceptableMediaTypes(CdsEntity entity) { + protected static List getEntityAcceptableMediaTypes(CdsEntity entity) { Optional> acceptableMediaTypeFromContent = entity.getElement("content") .findAnnotation("Core.AcceptableMediaTypes"); @@ -158,7 +167,7 @@ private static List getEntityAcceptableMediaTypes(CdsEntity entity) { .orElse(List.of("*/*")); } - private static String extractFileName(CdsEntity entity, List data) { + protected static String extractFileName(CdsEntity entity, List data) { CdsDataProcessor processor = CdsDataProcessor.create(); AtomicReference fileNameRef = new AtomicReference<>(); Validator validator = (path, element, value) -> { From 4173128f58f5bb9ee2c5e87b73286f62046901ac Mon Sep 17 00:00:00 2001 From: samyuktaprabhu <33195453+samyuktaprabhu@users.noreply.github.com> Date: Wed, 11 Feb 2026 14:59:38 +0100 Subject: [PATCH 03/17] first set of tests --- .../CreateAttachmentsHandlerTest.java | 45 +++++- .../AttachmentValidationHelperTest.java | 148 +++++++++++++++++ .../common/ApplicationHandlerHelperTest.java | 149 +++++++++++++++++- 3 files changed, 332 insertions(+), 10 deletions(-) create mode 100644 cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelperTest.java diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/CreateAttachmentsHandlerTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/CreateAttachmentsHandlerTest.java index 4eabcf23..5b25113a 100644 --- a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/CreateAttachmentsHandlerTest.java +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/CreateAttachmentsHandlerTest.java @@ -11,6 +11,7 @@ import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; @@ -30,6 +31,7 @@ import com.sap.cds.feature.attachments.handler.applicationservice.modifyevents.ModifyAttachmentEvent; import com.sap.cds.feature.attachments.handler.applicationservice.modifyevents.ModifyAttachmentEventFactory; import com.sap.cds.feature.attachments.handler.applicationservice.readhelper.CountingInputStream; +import com.sap.cds.feature.attachments.handler.common.ApplicationHandlerHelper; import com.sap.cds.feature.attachments.handler.helper.RuntimeHelper; import com.sap.cds.reflect.CdsEntity; import com.sap.cds.services.ErrorStatuses; @@ -48,6 +50,7 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.List; @@ -55,6 +58,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; +import org.mockito.MockedStatic; class CreateAttachmentsHandlerTest { @@ -76,9 +80,8 @@ static void classSetup() { void setup() { eventFactory = mock(ModifyAttachmentEventFactory.class); storageReader = mock(ThreadDataStorageReader.class); - cut = - new CreateAttachmentsHandler( - eventFactory, storageReader, ModifyApplicationHandlerHelper.DEFAULT_SIZE_WITH_SCANNER); + cut = new CreateAttachmentsHandler( + eventFactory, storageReader, ModifyApplicationHandlerHelper.DEFAULT_SIZE_WITH_SCANNER); createContext = mock(CdsCreateEventContext.class); event = mock(ModifyAttachmentEvent.class); @@ -332,8 +335,7 @@ void restoreError_proceedsSuccessfully_noException() { @Test void restoreError_contentTooLargeWithMaxSize_throwsWithMaxSize() { var context = mock(EventContext.class); - var originalException = - new ServiceException(ExtendedErrorStatuses.CONTENT_TOO_LARGE, "original message"); + var originalException = new ServiceException(ExtendedErrorStatuses.CONTENT_TOO_LARGE, "original message"); doThrow(originalException).when(context).proceed(); when(context.get("attachment.MaxSize")).thenReturn("10MB"); @@ -347,8 +349,7 @@ void restoreError_contentTooLargeWithMaxSize_throwsWithMaxSize() { @Test void restoreError_contentTooLargeWithoutMaxSize_throwsWithoutMaxSize() { var context = mock(EventContext.class); - var originalException = - new ServiceException(ExtendedErrorStatuses.CONTENT_TOO_LARGE, "original message"); + var originalException = new ServiceException(ExtendedErrorStatuses.CONTENT_TOO_LARGE, "original message"); doThrow(originalException).when(context).proceed(); when(context.get("attachment.MaxSize")).thenReturn(null); @@ -383,6 +384,36 @@ void restoreError_methodHasCorrectAnnotations() throws NoSuchMethodException { assertThat(handlerOrderAnnotation.value()).isEqualTo(HandlerOrder.EARLY); } + @Test + void processBeforeForMetadata_methodHasCorrectAnnotations() throws NoSuchMethodException { + Method method = cut.getClass().getDeclaredMethod("processBeforeForMetadata", EventContext.class, List.class); + + Before beforeAnnotation = method.getAnnotation(Before.class); + HandlerOrder handlerOrderAnnotation = method.getAnnotation(HandlerOrder.class); + + assertThat(beforeAnnotation.event()) + .containsExactlyInAnyOrder( + CqnService.EVENT_CREATE, DraftService.EVENT_DRAFT_NEW); + assertThat(handlerOrderAnnotation.value()).isEqualTo(HandlerOrder.BEFORE); + } + + @Test + void processBeforeForMetadata_executesValidation() { + EventContext context = mock(EventContext.class); + CdsEntity entity = mock(CdsEntity.class); + List data = List.of(mock(CdsData.class)); + when(context.getTarget()).thenReturn(entity); + + try (MockedStatic helper = mockStatic(ApplicationHandlerHelper.class)) { + helper.when(() -> ApplicationHandlerHelper.validateAcceptableMediaTypes(entity, data)) + .thenAnswer(invocation -> null); + // when + new CreateAttachmentsHandler(eventFactory, storageReader, "400MB").processBeforeForMetadata(context, data); + // then + helper.verify(() -> ApplicationHandlerHelper.validateAcceptableMediaTypes(entity, data)); + } + } + private void getEntityAndMockContext(String cdsName) { var serviceEntity = runtime.getCdsModel().findEntity(cdsName); mockTargetInCreateContext(serviceEntity.orElseThrow()); diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelperTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelperTest.java new file mode 100644 index 00000000..c0ca8826 --- /dev/null +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelperTest.java @@ -0,0 +1,148 @@ +/* +* © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. +*/ +package com.sap.cds.feature.attachments.handler.applicationservice.helper; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.sap.cds.services.ErrorStatuses; +import com.sap.cds.services.ServiceException; + +class AttachmentValidationHelperTest { + + // ---------- constructor ---------- + + @Test + void constructorShouldBeCallableForCoverage() { + new AttachmentValidationHelper(); + } + + // ---------- validateMimeTypeForAttachment ---------- + @Test + void shouldAcceptMimeTypeFromURLConnection() { + String result = AttachmentValidationHelper.validateMediaTypeForAttachment( + "document.pdf", + List.of("application/pdf")); + assertEquals("application/pdf", result); + } + + @Test + void shouldUseExtensionMapWhenURLConnectionDoesNotDetectMimeType() { + String result = AttachmentValidationHelper.validateMediaTypeForAttachment( + "image.webp", + List.of("image/webp")); + assertEquals("image/webp", result); + } + + @Test + void shouldSupportWildCardTypes() { + String result = AttachmentValidationHelper.validateMediaTypeForAttachment( + "image.jpeg", + List.of("image/*")); + assertEquals("image/jpeg", result); + } + + @Test + void shouldAllowAllMimeTypesWithStarSlashStar() { + String result = AttachmentValidationHelper.validateMediaTypeForAttachment( + "anyfile.jpeg", + List.of("*/*")); + assertEquals("image/jpeg", result); + } + + @Test + void shouldAllowRandomInvalidExtensionWithStarSlashStar() { + String result = AttachmentValidationHelper.validateMediaTypeForAttachment( + "anyfile.anyext", + List.of("*/*")); + assertEquals(AttachmentValidationHelper.DEFAULT_MEDIA_TYPE, result); + } + + @Test + void shouldAllowRandomInvalidExtensionWithNullAcceptableMediaTypes() { + String result = AttachmentValidationHelper.validateMediaTypeForAttachment( + "anyfile.anyext", + null); + assertEquals(AttachmentValidationHelper.DEFAULT_MEDIA_TYPE, result); + } + + @Test + void shouldNotAllowRandomInvalidExtensionWithStarSlashStar() { + ServiceException ex = assertThrows(ServiceException.class, + () -> AttachmentValidationHelper.validateMediaTypeForAttachment( + "anyfile.anyext", + List.of("application/pdf"))); + + assertTrue(ex.getErrorStatus().equals(ErrorStatuses.UNSUPPORTED_MEDIA_TYPE)); + } + + @Test + void shouldAllowAllMimeTypesIfNoRestrictionsGiven() { + String result = AttachmentValidationHelper.validateMediaTypeForAttachment( + "anyfile.anyext", + List.of()); + assertEquals(AttachmentValidationHelper.DEFAULT_MEDIA_TYPE, result); + } + + @Test + void shouldUseDefaultMimeTypeWhenExtensionIsUnknown() { + String result = AttachmentValidationHelper.validateMediaTypeForAttachment( + "file.unknownext", + List.of(AttachmentValidationHelper.DEFAULT_MEDIA_TYPE)); + assertEquals(AttachmentValidationHelper.DEFAULT_MEDIA_TYPE, result); + } + + @Test + void shouldThrowExceptionWhenInvalidFileName() { + ServiceException ex = assertThrows(ServiceException.class, + () -> AttachmentValidationHelper.validateMediaTypeForAttachment( + "invalidfilename", + List.of("application/pdf"))); + + assertTrue(ex.getErrorStatus().equals(ErrorStatuses.UNSUPPORTED_MEDIA_TYPE)); + } + + @Test + void shouldThrowExceptionWhenFilenameEndsWithDot() { + ServiceException ex = assertThrows(ServiceException.class, + () -> AttachmentValidationHelper.validateMediaTypeForAttachment( + "file.", + List.of("application/pdf"))); + + assertEquals(ErrorStatuses.UNSUPPORTED_MEDIA_TYPE, ex.getErrorStatus()); + } + + @Test + void shouldHandleUppercaseExtension() { + String result = AttachmentValidationHelper.validateMediaTypeForAttachment( + "photo.JPG", + List.of("image/jpeg")); + + assertEquals("image/jpeg", result); + } + + @Test + void shouldThrowExceptionWhenMimeTypeNotAllowed() { + ServiceException ex = assertThrows(ServiceException.class, + () -> AttachmentValidationHelper.validateMediaTypeForAttachment( + "document.pdf", + List.of("image/png"))); + + assertTrue(ex.getMessage().contains("not allowed")); + } + + @Test + void shouldThrowExceptionWhenDefaultMimeTypeNotAllowed() { + ServiceException ex = assertThrows(ServiceException.class, + () -> AttachmentValidationHelper.validateMediaTypeForAttachment( + "file.unknownext", + List.of("application/pdf"))); + + assertTrue(ex.getMessage().contains("not allowed")); + } + +} \ No newline at end of file diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelperTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelperTest.java index e7d8cfa3..7086e28a 100644 --- a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelperTest.java +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelperTest.java @@ -4,12 +4,26 @@ package com.sap.cds.feature.attachments.handler.common; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.*; import com.sap.cds.CdsData; -import java.util.Map; +import com.sap.cds.CdsDataProcessor; +import com.sap.cds.CdsDataProcessor.Filter; +import com.sap.cds.CdsDataProcessor.Validator; +import com.sap.cds.feature.attachments.handler.applicationservice.helper.AttachmentValidationHelper; +import com.sap.cds.reflect.CdsEntity; +import com.sap.cds.services.ServiceException; +import com.sap.cds.reflect.CdsAnnotation; + +import java.util.*; + import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import com.sap.cds.reflect.CdsElement; class ApplicationHandlerHelperTest { @@ -57,4 +71,133 @@ void removeDraftKey() { assertFalse(result.containsKey("IsActiveEntity")); assertTrue(result.containsKey("key1")); } + + // ----------- Test validateAcceptableMediaTypes ----------- + + @Test + void extractFileName_whenFileNamePresent_returnsValue() { + + CdsEntity entity = mock(CdsEntity.class); + CdsData data = mock(CdsData.class); + CdsElement fileNameElement = mock(CdsElement.class); + when(fileNameElement.getName()).thenReturn("fileName"); + CdsDataProcessor processor = mock(CdsDataProcessor.class); + + try (MockedStatic mocked = mockStatic(CdsDataProcessor.class)) { + mocked.when(CdsDataProcessor::create).thenReturn(processor); + doAnswer(invocation -> { + Filter filter = invocation.getArgument(0); + Validator validator = invocation.getArgument(1); + + // Simulate processor visiting fileName with String value + if (filter.test(null, fileNameElement, null)) { + validator.validate(null, fileNameElement, "test.pdf"); + } + return processor; + }).when(processor).addValidator(any(), any()); + + doNothing().when(processor).process(anyList(), any()); + String result = ApplicationHandlerHelper.extractFileName(entity, List.of(data)); + assertThat(result).isEqualTo("test.pdf"); + } + } + + @Test + void extractFileName_whenElementIsNotFileName_throws() { + + CdsEntity entity = mock(CdsEntity.class); + CdsData data = mock(CdsData.class); + CdsElement element = mock(CdsElement.class); + when(element.getName()).thenReturn("content"); + CdsDataProcessor processor = mock(CdsDataProcessor.class); + + try (MockedStatic mocked = mockStatic(CdsDataProcessor.class)) { + mocked.when(CdsDataProcessor::create).thenReturn(processor); + doAnswer(invocation -> { + Filter filter = invocation.getArgument(0); + Validator validator = invocation.getArgument(1); + if (filter.test(null, element, null)) { + validator.validate(null, element, "test.pdf"); + } + return processor; + }).when(processor).addValidator(any(), any()); + + doNothing().when(processor).process(anyList(), any()); + assertThrows(ServiceException.class, () -> ApplicationHandlerHelper.extractFileName(entity, List.of(data))); + } + } + + @Test + void extractFileName_valueIsNotString_branchCovered() { + + CdsEntity entity = mock(CdsEntity.class); + CdsData data = mock(CdsData.class); + CdsElement element = mock(CdsElement.class); + when(element.getName()).thenReturn("fileName"); + CdsDataProcessor processor = mock(CdsDataProcessor.class); + + try (MockedStatic mocked = mockStatic(CdsDataProcessor.class)) { + mocked.when(CdsDataProcessor::create).thenReturn(processor); + doAnswer(invocation -> { + Filter filter = invocation.getArgument(0); + Validator validator = invocation.getArgument(1); + if (filter.test(null, element, null)) { + validator.validate(null, element, 42); // non-String + } + return processor; + }).when(processor).addValidator(any(), any()); + doNothing().when(processor).process(anyList(), any()); + assertThrows(ServiceException.class, () -> ApplicationHandlerHelper.extractFileName(entity, List.of(data))); + } + } + + // ----------- Test getEntityAcceptableMediaTypes ----------- + @Test + void getEntityAcceptableMediaTypes_withAnnotation() { + CdsEntity entity = mock(CdsEntity.class); + CdsElement content = mock(CdsElement.class); + CdsAnnotation annotation = mock(CdsAnnotation.class); + when(entity.getElement("content")).thenReturn(content); + when(content.findAnnotation("Core.AcceptableMediaTypes")) + .thenReturn(Optional.of(annotation)); + when(annotation.getValue()) + .thenReturn(List.of("image/png", "image/jpeg")); + List result = ApplicationHandlerHelper.getEntityAcceptableMediaTypes(entity); + assertThat(result).containsExactly("image/png", "image/jpeg"); + } + + @Test + void getEntityAcceptableMediaTypes_withoutAnnotation_returnsWildcard() { + CdsEntity entity = mock(CdsEntity.class); + CdsElement content = mock(CdsElement.class); + when(entity.getElement("content")).thenReturn(content); + when(content.findAnnotation("Core.AcceptableMediaTypes")) + .thenReturn(Optional.empty()); + List result = ApplicationHandlerHelper.getEntityAcceptableMediaTypes(entity); + assertThat(result).containsExactly("*/*"); + } + + @Test + void validateAcceptableMediaTypes_success_executesAllLines() { + CdsEntity entity = mock(CdsEntity.class); + CdsElement content = mock(CdsElement.class); + CdsAnnotation annotation = mock(CdsAnnotation.class); + when(entity.getElement("content")).thenReturn(content); + when(content.findAnnotation("Core.AcceptableMediaTypes")) + .thenReturn(Optional.of(annotation)); + when(annotation.getValue()) + .thenReturn(List.of("image/png")); + List data = List.of(mock(CdsData.class)); + try (MockedStatic helper = mockStatic(ApplicationHandlerHelper.class, CALLS_REAL_METHODS); + MockedStatic validator = mockStatic(AttachmentValidationHelper.class)) { + // allow real getEntityAcceptableMediaTypes + helper.when(() -> ApplicationHandlerHelper.extractFileName(entity, data)) + .thenReturn("test.png"); + assertDoesNotThrow(() -> ApplicationHandlerHelper.validateAcceptableMediaTypes(entity, data)); + validator.verify(() -> AttachmentValidationHelper.validateMediaTypeForAttachment( + "test.png", + List.of("image/png"))); + } + } + } From 4a0b5f1cde63bfddec4154aa326eb180a2285c75 Mon Sep 17 00:00:00 2001 From: samyuktaprabhu <33195453+samyuktaprabhu@users.noreply.github.com> Date: Fri, 13 Feb 2026 11:44:48 +0100 Subject: [PATCH 04/17] access annotations through CdsRuntime model --- .../configuration/Registration.java | 12 +- .../CreateAttachmentsHandler.java | 22 +- .../helper/AttachmentValidationHelper.java | 280 +++++++++--------- .../common/ApplicationHandlerHelper.java | 103 ++++--- ...mitedAttachmentValidationNonDraftTest.java | 24 +- 5 files changed, 236 insertions(+), 205 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 e99586ea..9f5d4fcf 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 @@ -89,7 +89,8 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) { OutboxService.PERSISTENT_UNORDERED_NAME); } - // build malware scanner client, could be null if no service binding is available + // build malware scanner client, could be null if no service binding is + // available MalwareScanClient scanClient = buildMalwareScanClient(runtime.getEnvironment()); // determine default max size based on malware scanner binding availability @@ -118,12 +119,14 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) { new AttachmentsReader(new AssociationCascader(), persistenceService); ThreadLocalDataStorage storage = new ThreadLocalDataStorage(); - // register event handlers for application service, if at least one application service is + // register event handlers for application service, if at least one application + // service is // available boolean hasApplicationServices = serviceCatalog.getServices(ApplicationService.class).findFirst().isPresent(); if (hasApplicationServices) { - configurer.eventHandler(new CreateAttachmentsHandler(eventFactory, storage, defaultMaxSize)); + configurer.eventHandler( + new CreateAttachmentsHandler(eventFactory, storage, defaultMaxSize, runtime)); configurer.eventHandler( new UpdateAttachmentsHandler( eventFactory, attachmentsReader, outboxedAttachmentService, storage, defaultMaxSize)); @@ -138,7 +141,8 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) { "No application service is available. Application service event handlers will not be registered."); } - // register event handlers on draft service, if at least one draft service is available + // register event handlers on draft service, if at least one draft service is + // available boolean hasDraftServices = serviceCatalog.getServices(DraftService.class).findFirst().isPresent(); if (hasDraftServices) { diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/CreateAttachmentsHandler.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/CreateAttachmentsHandler.java index 05fb47ec..e520344c 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/CreateAttachmentsHandler.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/CreateAttachmentsHandler.java @@ -24,15 +24,16 @@ 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 com.sap.cds.services.runtime.CdsRuntime; import com.sap.cds.services.utils.OrderConstants; +import java.io.InputStream; import java.util.ArrayList; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * The class {@link CreateAttachmentsHandler} is an event handler that is - * responsible for creating + * The class {@link CreateAttachmentsHandler} is an event handler that is responsible for creating * attachments for entities. It is called before a create event is executed. */ @ServiceName(value = "*", type = ApplicationService.class) @@ -43,14 +44,17 @@ public class CreateAttachmentsHandler implements EventHandler { private final ModifyAttachmentEventFactory eventFactory; private final ThreadDataStorageReader storageReader; private final String defaultMaxSize; + private final CdsRuntime cdsRuntime; public CreateAttachmentsHandler( ModifyAttachmentEventFactory eventFactory, ThreadDataStorageReader storageReader, - String defaultMaxSize) { + String defaultMaxSize, + CdsRuntime cdsRuntime) { this.eventFactory = requireNonNull(eventFactory, "eventFactory must not be null"); this.storageReader = requireNonNull(storageReader, "storageReader must not be null"); this.defaultMaxSize = requireNonNull(defaultMaxSize, "defaultMaxSize must not be null"); + this.cdsRuntime = requireNonNull(cdsRuntime, "cdsRuntime must not be null"); } @Before @@ -63,11 +67,17 @@ void processBeforeForDraft(CdsCreateEventContext context, List data) { context.getTarget(), data, storageReader.get()); } - @Before(event = { CqnService.EVENT_CREATE, DraftService.EVENT_DRAFT_NEW }) + @Before(event = {CqnService.EVENT_CREATE, DraftService.EVENT_DRAFT_NEW}) @HandlerOrder(HandlerOrder.BEFORE) void processBeforeForMetadata(EventContext context, List data) { + data.forEach( + entry -> { + if (!entry.containsKey("content")) { + entry.put("content", InputStream.nullInputStream()); + } + }); CdsEntity target = context.getTarget(); - ApplicationHandlerHelper.validateAcceptableMediaTypes(target, data); + ApplicationHandlerHelper.validateAcceptableMediaTypes(target, data, cdsRuntime); } @Before @@ -81,7 +91,7 @@ void processBefore(CdsCreateEventContext context, List data) { } } - @On(event = { CqnService.EVENT_CREATE, CqnService.EVENT_UPDATE, DraftService.EVENT_DRAFT_PATCH }) + @On(event = {CqnService.EVENT_CREATE, CqnService.EVENT_UPDATE, DraftService.EVENT_DRAFT_PATCH}) @HandlerOrder(HandlerOrder.EARLY) void restoreError(EventContext context) { try { diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelper.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelper.java index 5d09a6ee..2efa445e 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelper.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelper.java @@ -1,152 +1,158 @@ /* -* © 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.handler.applicationservice.helper; +import com.sap.cds.services.ErrorStatuses; +import com.sap.cds.services.ServiceException; import java.net.URLConnection; import java.util.Collection; import java.util.List; import java.util.Map; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.sap.cds.services.ServiceException; -import com.sap.cds.services.ErrorStatuses; - public class AttachmentValidationHelper { - public static final String DEFAULT_MEDIA_TYPE = "application/octet-stream"; - public static final Map EXT_TO_MEDIA_TYPE = Map.ofEntries( - Map.entry("aac", "audio/aac"), - Map.entry("abw", "application/x-abiword"), - Map.entry("arc", "application/octet-stream"), - Map.entry("avi", "video/x-msvideo"), - Map.entry("azw", "application/vnd.amazon.ebook"), - Map.entry("bin", "application/octet-stream"), - Map.entry("png", "image/png"), - Map.entry("gif", "image/gif"), - Map.entry("bmp", "image/bmp"), - Map.entry("bz", "application/x-bzip"), - Map.entry("bz2", "application/x-bzip2"), - Map.entry("csh", "application/x-csh"), - Map.entry("css", "text/css"), - Map.entry("csv", "text/csv"), - Map.entry("doc", "application/msword"), - Map.entry("docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"), - Map.entry("odp", "application/vnd.oasis.opendocument.presentation"), - Map.entry("ods", "application/vnd.oasis.opendocument.spreadsheet"), - Map.entry("odt", "application/vnd.oasis.opendocument.text"), - Map.entry("epub", "application/epub+zip"), - Map.entry("gz", "application/gzip"), - Map.entry("htm", "text/html"), - Map.entry("html", "text/html"), - Map.entry("ico", "image/x-icon"), - Map.entry("ics", "text/calendar"), - Map.entry("jar", "application/java-archive"), - Map.entry("jpg", "image/jpeg"), - Map.entry("jpeg", "image/jpeg"), - Map.entry("js", "text/javascript"), - Map.entry("json", "application/json"), - Map.entry("mid", "audio/midi"), - Map.entry("midi", "audio/midi"), - Map.entry("mjs", "text/javascript"), - Map.entry("mov", "video/quicktime"), - Map.entry("mp3", "audio/mpeg"), - Map.entry("mp4", "video/mp4"), - Map.entry("mpeg", "video/mpeg"), - Map.entry("mpkg", "application/vnd.apple.installer+xml"), - Map.entry("otf", "font/otf"), - Map.entry("pdf", "application/pdf"), - Map.entry("ppt", "application/vnd.ms-powerpoint"), - Map.entry("pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"), - Map.entry("rar", "application/x-rar-compressed"), - Map.entry("rtf", "application/rtf"), - Map.entry("svg", "image/svg+xml"), - Map.entry("tar", "application/x-tar"), - Map.entry("tif", "image/tiff"), - Map.entry("tiff", "image/tiff"), - Map.entry("ttf", "font/ttf"), - Map.entry("vsd", "application/vnd.visio"), - Map.entry("wav", "audio/wav"), - Map.entry("woff", "font/woff"), - Map.entry("woff2", "font/woff2"), - Map.entry("xhtml", "application/xhtml+xml"), - Map.entry("xls", "application/vnd.ms-excel"), - Map.entry("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"), - Map.entry("xml", "application/xml"), - Map.entry("zip", "application/zip"), - Map.entry("txt", "application/txt"), - Map.entry("lst", "application/txt"), - Map.entry("webp", "image/webp")); - - private static final Logger logger = LoggerFactory.getLogger(AttachmentValidationHelper.class); - - /** - * Validates the file name and resolves its media type. Ensures that the - * detected media type is part of the list of acceptable media types. - * - * @param fileName the name of the attachment file - * @param acceptableMediaTypes list of allowed media types (e.g. "image/*", - * "application/pdf") - * @return the detected media type - * @throws ServiceException if the file name is invalid or the media type is not - * allowed - */ - public static String validateMediaTypeForAttachment(String fileName, List acceptableMediaTypes) { - validateFileName(fileName); - String detectedMediaType = resolveMimeType(fileName); - validateAcceptableMediaType(acceptableMediaTypes, detectedMediaType); - return detectedMediaType; + public static final String DEFAULT_MEDIA_TYPE = "application/octet-stream"; + public static final Map EXT_TO_MEDIA_TYPE = + Map.ofEntries( + Map.entry("aac", "audio/aac"), + Map.entry("abw", "application/x-abiword"), + Map.entry("arc", "application/octet-stream"), + Map.entry("avi", "video/x-msvideo"), + Map.entry("azw", "application/vnd.amazon.ebook"), + Map.entry("bin", "application/octet-stream"), + Map.entry("png", "image/png"), + Map.entry("gif", "image/gif"), + Map.entry("bmp", "image/bmp"), + Map.entry("bz", "application/x-bzip"), + Map.entry("bz2", "application/x-bzip2"), + Map.entry("csh", "application/x-csh"), + Map.entry("css", "text/css"), + Map.entry("csv", "text/csv"), + Map.entry("doc", "application/msword"), + Map.entry( + "docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"), + Map.entry("odp", "application/vnd.oasis.opendocument.presentation"), + Map.entry("ods", "application/vnd.oasis.opendocument.spreadsheet"), + Map.entry("odt", "application/vnd.oasis.opendocument.text"), + Map.entry("epub", "application/epub+zip"), + Map.entry("gz", "application/gzip"), + Map.entry("htm", "text/html"), + Map.entry("html", "text/html"), + Map.entry("ico", "image/x-icon"), + Map.entry("ics", "text/calendar"), + Map.entry("jar", "application/java-archive"), + Map.entry("jpg", "image/jpeg"), + Map.entry("jpeg", "image/jpeg"), + Map.entry("js", "text/javascript"), + Map.entry("json", "application/json"), + Map.entry("mid", "audio/midi"), + Map.entry("midi", "audio/midi"), + Map.entry("mjs", "text/javascript"), + Map.entry("mov", "video/quicktime"), + Map.entry("mp3", "audio/mpeg"), + Map.entry("mp4", "video/mp4"), + Map.entry("mpeg", "video/mpeg"), + Map.entry("mpkg", "application/vnd.apple.installer+xml"), + Map.entry("otf", "font/otf"), + Map.entry("pdf", "application/pdf"), + Map.entry("ppt", "application/vnd.ms-powerpoint"), + Map.entry( + "pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"), + Map.entry("rar", "application/x-rar-compressed"), + Map.entry("rtf", "application/rtf"), + Map.entry("svg", "image/svg+xml"), + Map.entry("tar", "application/x-tar"), + Map.entry("tif", "image/tiff"), + Map.entry("tiff", "image/tiff"), + Map.entry("ttf", "font/ttf"), + Map.entry("vsd", "application/vnd.visio"), + Map.entry("wav", "audio/wav"), + Map.entry("woff", "font/woff"), + Map.entry("woff2", "font/woff2"), + Map.entry("xhtml", "application/xhtml+xml"), + Map.entry("xls", "application/vnd.ms-excel"), + Map.entry("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"), + Map.entry("xml", "application/xml"), + Map.entry("zip", "application/zip"), + Map.entry("txt", "text/plain"), + Map.entry("webp", "image/webp")); + + private static final Logger logger = LoggerFactory.getLogger(AttachmentValidationHelper.class); + + /** + * Validates the file name and resolves its media type. Ensures that the detected media type is + * part of the list of acceptable media types. + * + * @param fileName the name of the attachment file + * @param acceptableMediaTypes list of allowed media types (e.g. "image/*", "application/pdf") + * @return the detected media type + * @throws ServiceException if the file name is invalid or the media type is not allowed + */ + public static String validateMediaTypeForAttachment( + String fileName, List acceptableMediaTypes) { + validateFileName(fileName); + String detectedMediaType = resolveMimeType(fileName); + validateAcceptableMediaType(acceptableMediaTypes, detectedMediaType); + return detectedMediaType; + } + + private static void validateFileName(String fileName) { + String clean = fileName.trim(); + int lastDotIndex = clean.lastIndexOf('.'); + if (lastDotIndex <= 0 || lastDotIndex == clean.length() - 1) { + throw new ServiceException( + ErrorStatuses.UNSUPPORTED_MEDIA_TYPE, "Invalid filename format: " + fileName); } - - private static void validateFileName(String fileName) { - String clean = fileName.trim(); - int lastDotIndex = clean.lastIndexOf('.'); - if (lastDotIndex <= 0 || lastDotIndex == clean.length() - 1) { - throw new ServiceException( - ErrorStatuses.UNSUPPORTED_MEDIA_TYPE, - "Invalid filename format: " + fileName); - } + } + + private static void validateAcceptableMediaType( + List acceptableMediaTypes, String actualMimeType) { + if (!checkMimeTypeMatch(acceptableMediaTypes, actualMimeType)) { + throw new ServiceException( + ErrorStatuses.UNSUPPORTED_MEDIA_TYPE, + "The attachment file type '{}' is not allowed. Allowed types are: {}", + actualMimeType, + String.join(", ", acceptableMediaTypes)); } - - private static void validateAcceptableMediaType(List acceptableMediaTypes, String actualMimeType) { - if (!checkMimeTypeMatch(acceptableMediaTypes, actualMimeType)) { - throw new ServiceException( - ErrorStatuses.UNSUPPORTED_MEDIA_TYPE, - "The attachment file type '{}' is not allowed. Allowed types are: {}", actualMimeType, - String.join(", ", acceptableMediaTypes)); - } + } + + private static String resolveMimeType(String fileName) { + String actualMimeType = URLConnection.guessContentTypeFromName(fileName); + + if (actualMimeType == null) { + String fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase(); + actualMimeType = EXT_TO_MEDIA_TYPE.get(fileExtension); + + if (actualMimeType == null) { + logger.warn( + "Could not determine mime type for file: {}. Setting mime type to default: {}", + fileName, + DEFAULT_MEDIA_TYPE); + actualMimeType = DEFAULT_MEDIA_TYPE; + } } - - private static String resolveMimeType(String fileName) { - String actualMimeType = URLConnection.guessContentTypeFromName(fileName); - - if (actualMimeType == null) { - String fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase(); - actualMimeType = EXT_TO_MEDIA_TYPE.get(fileExtension); - - if (actualMimeType == null) { - logger.warn("Could not determine mime type for file: {}. Setting mime type to default: {}", - fileName, DEFAULT_MEDIA_TYPE); - actualMimeType = DEFAULT_MEDIA_TYPE; - } - } - return actualMimeType; - } - - private static boolean checkMimeTypeMatch(Collection acceptableMediaTypes, String mimeType) { - if (acceptableMediaTypes == null || acceptableMediaTypes.isEmpty() || acceptableMediaTypes.contains("*/*")) - return true; - - String baseMimeType = mimeType.trim().toLowerCase(); - - return acceptableMediaTypes.stream().anyMatch(type -> { - String normalizedType = type.trim().toLowerCase(); - return normalizedType.endsWith("/*") - ? baseMimeType.startsWith(normalizedType.substring(0, normalizedType.length() - 2) + "/") - : baseMimeType.equals(normalizedType); - }); - } - -} \ No newline at end of file + return actualMimeType; + } + + private static boolean checkMimeTypeMatch( + Collection acceptableMediaTypes, String mimeType) { + if (acceptableMediaTypes == null + || acceptableMediaTypes.isEmpty() + || acceptableMediaTypes.contains("*/*")) return true; + + String baseMimeType = mimeType.trim().toLowerCase(); + + return acceptableMediaTypes.stream() + .anyMatch( + type -> { + String normalizedType = type.trim().toLowerCase(); + return normalizedType.endsWith("/*") + ? baseMimeType.startsWith( + normalizedType.substring(0, normalizedType.length() - 2) + "/") + : baseMimeType.equals(normalizedType); + }); + } +} diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelper.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelper.java index 14cb4a79..95885903 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelper.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelper.java @@ -15,10 +15,12 @@ import com.sap.cds.feature.attachments.handler.applicationservice.helper.AttachmentValidationHelper; import com.sap.cds.reflect.CdsAnnotation; import com.sap.cds.reflect.CdsEntity; +import com.sap.cds.reflect.CdsModel; import com.sap.cds.reflect.CdsStructuredType; import com.sap.cds.services.ErrorStatuses; import com.sap.cds.services.ServiceException; import com.sap.cds.services.draft.Drafts; +import com.sap.cds.services.runtime.CdsRuntime; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -28,8 +30,7 @@ import java.util.concurrent.atomic.AtomicReference; /** - * The class {@link ApplicationHandlerHelper} provides helper methods for the - * attachment application + * The class {@link ApplicationHandlerHelper} provides helper methods for the attachment application * handlers. */ public final class ApplicationHandlerHelper { @@ -37,26 +38,34 @@ public final class ApplicationHandlerHelper { private static final String ANNOTATION_CORE_MEDIA_TYPE = "Core.MediaType"; private static final ObjectMapper objectMapper = new ObjectMapper(); + /** Filter to support extraction of file name for attachment validation */ + public static final Filter FILE_NAME_FILTER = + (path, element, type) -> element.getName().contentEquals("fileName"); + /** - * Filter to support extraction of file name for attachment validation + * A filter for acceptable media types. The filter checks if the entity is a media entity and if + * the element has the annotation "Core.AcceptableMediaTypes". */ - public static final Filter FILE_NAME_FILTER = (path, element, type) -> element.getName().contentEquals("fileName"); + public static final Filter ACCEPTABLE_MEDIA_TYPE_FILTER = + (path, element, type) -> + element.getName().contentEquals("content") + && element.findAnnotation("Core.AcceptableMediaTypes").isPresent(); + /** - * A filter for media content fields. The filter checks if the entity is a media - * entity and if the + * A filter for media content fields. The filter checks if the entity is a media entity and if the * element has the annotation "Core.MediaType". */ - public static final Filter MEDIA_CONTENT_FILTER = (path, element, type) -> isMediaEntity(path.target().type()) - && element.findAnnotation(ANNOTATION_CORE_MEDIA_TYPE).isPresent(); + public static final Filter MEDIA_CONTENT_FILTER = + (path, element, type) -> + isMediaEntity(path.target().type()) + && element.findAnnotation(ANNOTATION_CORE_MEDIA_TYPE).isPresent(); /** * Checks if the data contains a content field. * - * @param entity The {@link CdsEntity entity} type of the given the data to - * check - * @param data The data to check - * @return true if the data contains a content field, - * false otherwise + * @param entity The {@link CdsEntity entity} type of the given the data to check + * @param data The data to check + * @return true if the data contains a content field, false otherwise */ public static boolean containsContentField(CdsEntity entity, List data) { AtomicBoolean isIncluded = new AtomicBoolean(); @@ -67,13 +76,11 @@ public static boolean containsContentField(CdsEntity entity, Listtrue if the entity is a media entity, false - * otherwise + * @return true if the entity is a media entity, false otherwise */ public static boolean isMediaEntity(CdsStructuredType baseEntity) { return baseEntity.getAnnotationValue(ANNOTATION_IS_MEDIA_DATA, false); @@ -82,7 +89,7 @@ public static boolean isMediaEntity(CdsStructuredType baseEntity) { /** * Extracts key fields from CdsData based on the entity definition. * - * @param data The CdsData to extract keys from + * @param data The CdsData to extract keys from * @param entity The entity definition * @return A map of key fields and their values */ @@ -102,10 +109,9 @@ public static Map extractKeys(CdsData data, CdsEntity entity) { } /** - * Condenses the attachments from the given data into a list of - * {@link Attachments attachments}. + * Condenses the attachments from the given data into a list of {@link Attachments attachments}. * - * @param data the list of {@link CdsData} to process + * @param data the list of {@link CdsData} to process * @param entity the {@link CdsEntity entity} type of the given data * @return a list of {@link Attachments attachments} condensed from the data */ @@ -113,7 +119,8 @@ public static List condenseAttachments( List data, CdsEntity entity) { List resultList = new ArrayList<>(); - Validator validator = (path, element, value) -> resultList.add(Attachments.of(path.target().values())); + Validator validator = + (path, element, value) -> resultList.add(Attachments.of(path.target().values())); CdsDataProcessor.create().addValidator(MEDIA_CONTENT_FILTER, validator).process(data, entity); return resultList; @@ -141,44 +148,50 @@ public static Map removeDraftKey(Map keys) { } /** - * Validates if the media type of the attachment in the given fileName is - * acceptable + * Validates if the media type of the attachment in the given fileName is acceptable * * @param entity the {@link CdsEntity entity} type of the given data - * @param data the list of {@link CdsData} to process - * @throws ServiceException if the media type of the attachment is not - * acceptable + * @param data the list of {@link CdsData} to process + * @throws ServiceException if the media type of the attachment is not acceptable */ - public static void validateAcceptableMediaTypes(CdsEntity entity, List data) { - List allowedTypes = getEntityAcceptableMediaTypes(entity); + public static void validateAcceptableMediaTypes( + CdsEntity entity, List data, CdsRuntime cdsRuntime) { + List allowedTypes = List.of("image/jpeg", "image/png"); String fileName = extractFileName(entity, data); AttachmentValidationHelper.validateMediaTypeForAttachment(fileName, allowedTypes); } - protected static List getEntityAcceptableMediaTypes(CdsEntity entity) { - Optional> acceptableMediaTypeFromContent = entity.getElement("content") - .findAnnotation("Core.AcceptableMediaTypes"); - - return acceptableMediaTypeFromContent - .map(a -> objectMapper.convertValue( - a.getValue(), - new TypeReference>() { - })) - .orElse(List.of("*/*")); + protected static List getEntityAcceptableMediaTypes( + CdsEntity entity, CdsRuntime cdsRuntime) { + CdsModel cdsModel = cdsRuntime.getCdsModel(); + Optional retrievedEntity = cdsModel.findEntity(entity.getQualifiedName()); + Optional> flatMap = + retrievedEntity.flatMap( + serviceEntity -> + serviceEntity.getElement("content").findAnnotation("Core.AcceptableMediaTypes")); + List result = + flatMap + .map( + annotation -> + objectMapper.convertValue( + annotation.getValue(), new TypeReference>() {})) + .orElse(List.of("*/*")); + return result; } protected static String extractFileName(CdsEntity entity, List data) { CdsDataProcessor processor = CdsDataProcessor.create(); AtomicReference fileNameRef = new AtomicReference<>(); - Validator validator = (path, element, value) -> { - if (element.getName().contentEquals("fileName") && value instanceof String) { - fileNameRef.set((String) value); - } - }; + Validator validator = + (path, element, value) -> { + if (element.getName().contentEquals("fileName") && value instanceof String) { + fileNameRef.set((String) value); + } + }; processor.addValidator(FILE_NAME_FILTER, validator).process(data, entity); - if (fileNameRef.get() == null) { + if (fileNameRef.get() == null || fileNameRef.get().isBlank()) { throw new ServiceException(ErrorStatuses.BAD_REQUEST, "Filename is missing"); } return fileNameRef.get(); diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/SizeLimitedAttachmentValidationNonDraftTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/SizeLimitedAttachmentValidationNonDraftTest.java index 9dcb1fa3..8b23f711 100644 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/SizeLimitedAttachmentValidationNonDraftTest.java +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/SizeLimitedAttachmentValidationNonDraftTest.java @@ -30,12 +30,12 @@ void uploadContentWithin5MBLimitSucceeds() throws Exception { var selectedRoot = selectStoredRootWithSizeLimitedAttachments(); var attachment = getRandomRootSizeLimitedAttachment(selectedRoot); + attachment.setFileName("test.txt"); // Act & Assert: Upload 3MB content (within limit) succeeds byte[] content = new byte[3 * 1024 * 1024]; // 3MB - var url = - buildNavigationSizeLimitedAttachmentUrl(selectedRoot.getId(), attachment.getId()) - + "/content"; + var url = buildNavigationSizeLimitedAttachmentUrl(selectedRoot.getId(), attachment.getId()) + + "/content"; requestHelper.setContentType(org.springframework.http.MediaType.APPLICATION_OCTET_STREAM); requestHelper.executePutWithMatcher(url, content, status().isNoContent()); } @@ -48,16 +48,16 @@ void uploadContentExceeding5MBLimitFails() throws Exception { var selectedRoot = selectStoredRootWithSizeLimitedAttachments(); var attachment = getRandomRootSizeLimitedAttachment(selectedRoot); - + attachment.setFileName("test.txt"); // Act: Try to upload 6MB content (exceeds limit) byte[] content = new byte[6 * 1024 * 1024]; // 6MB - var url = - buildNavigationSizeLimitedAttachmentUrl(selectedRoot.getId(), attachment.getId()) - + "/content"; + var url = buildNavigationSizeLimitedAttachmentUrl(selectedRoot.getId(), attachment.getId()) + + "/content"; requestHelper.setContentType(org.springframework.http.MediaType.APPLICATION_OCTET_STREAM); requestHelper.executePutWithMatcher(url, content, status().is(413)); - // Assert: Error response with HTTP 413 status code indicates size limit exceeded + // Assert: Error response with HTTP 413 status code indicates size limit + // exceeded } // Helper methods @@ -70,11 +70,9 @@ private Roots buildServiceRootWithSizeLimitedAttachments() { } private Roots selectStoredRootWithSizeLimitedAttachments() { - var select = - com.sap.cds.ql.Select.from( - com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.Roots_ - .class) - .columns(r -> r._all(), r -> r.sizeLimitedAttachments().expand()); + var select = com.sap.cds.ql.Select.from( + com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.Roots_.class) + .columns(r -> r._all(), r -> r.sizeLimitedAttachments().expand()); var result = persistenceService.run(select); return result.single(Roots.class); From 2893755917cf6a72a707fbe5e81e61717e60363c Mon Sep 17 00:00:00 2001 From: samyuktaprabhu <33195453+samyuktaprabhu@users.noreply.github.com> Date: Mon, 16 Feb 2026 11:00:44 +0100 Subject: [PATCH 05/17] revert a small test change --- .../attachments/handler/common/ApplicationHandlerHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelper.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelper.java index 95885903..5979b177 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelper.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelper.java @@ -156,7 +156,7 @@ public static Map removeDraftKey(Map keys) { */ public static void validateAcceptableMediaTypes( CdsEntity entity, List data, CdsRuntime cdsRuntime) { - List allowedTypes = List.of("image/jpeg", "image/png"); + List allowedTypes = getEntityAcceptableMediaTypes(entity, cdsRuntime); String fileName = extractFileName(entity, data); AttachmentValidationHelper.validateMediaTypeForAttachment(fileName, allowedTypes); } From c83237aab32ab41a2245ad1bc4195b26b405d88b Mon Sep 17 00:00:00 2001 From: samyuktaprabhu <33195453+samyuktaprabhu@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:45:38 +0100 Subject: [PATCH 06/17] prevent 500 error during parent entity creation --- .../CreateAttachmentsHandler.java | 7 ------- .../handler/common/ApplicationHandlerHelper.java | 16 ++++++++-------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/CreateAttachmentsHandler.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/CreateAttachmentsHandler.java index e520344c..8d3a3464 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/CreateAttachmentsHandler.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/CreateAttachmentsHandler.java @@ -26,7 +26,6 @@ import com.sap.cds.services.handler.annotations.ServiceName; import com.sap.cds.services.runtime.CdsRuntime; import com.sap.cds.services.utils.OrderConstants; -import java.io.InputStream; import java.util.ArrayList; import java.util.List; import org.slf4j.Logger; @@ -70,12 +69,6 @@ void processBeforeForDraft(CdsCreateEventContext context, List data) { @Before(event = {CqnService.EVENT_CREATE, DraftService.EVENT_DRAFT_NEW}) @HandlerOrder(HandlerOrder.BEFORE) void processBeforeForMetadata(EventContext context, List data) { - data.forEach( - entry -> { - if (!entry.containsKey("content")) { - entry.put("content", InputStream.nullInputStream()); - } - }); CdsEntity target = context.getTarget(); ApplicationHandlerHelper.validateAcceptableMediaTypes(target, data, cdsRuntime); } diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelper.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelper.java index 5979b177..436dbd13 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelper.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelper.java @@ -156,19 +156,19 @@ public static Map removeDraftKey(Map keys) { */ public static void validateAcceptableMediaTypes( CdsEntity entity, List data, CdsRuntime cdsRuntime) { - List allowedTypes = getEntityAcceptableMediaTypes(entity, cdsRuntime); + CdsModel cdsModel = cdsRuntime.getCdsModel(); + CdsEntity serviceEntity = cdsModel.findEntity(entity.getQualifiedName()).orElse(null); + if (serviceEntity == null || !isMediaEntity(serviceEntity)) { + return; + } + List allowedTypes = getEntityAcceptableMediaTypes(serviceEntity, cdsModel); String fileName = extractFileName(entity, data); AttachmentValidationHelper.validateMediaTypeForAttachment(fileName, allowedTypes); } - protected static List getEntityAcceptableMediaTypes( - CdsEntity entity, CdsRuntime cdsRuntime) { - CdsModel cdsModel = cdsRuntime.getCdsModel(); - Optional retrievedEntity = cdsModel.findEntity(entity.getQualifiedName()); + protected static List getEntityAcceptableMediaTypes(CdsEntity entity, CdsModel cdsModel) { Optional> flatMap = - retrievedEntity.flatMap( - serviceEntity -> - serviceEntity.getElement("content").findAnnotation("Core.AcceptableMediaTypes")); + entity.getElement("content").findAnnotation("Core.AcceptableMediaTypes"); List result = flatMap .map( From 55c0af0a69d990b34cd894cfaf96428d343b5853 Mon Sep 17 00:00:00 2001 From: samyuktaprabhu <33195453+samyuktaprabhu@users.noreply.github.com> Date: Tue, 17 Feb 2026 09:34:21 +0100 Subject: [PATCH 07/17] add & update tests --- .../common/ApplicationHandlerHelper.java | 17 +- .../CreateAttachmentsHandlerTest.java | 33 ++- .../AttachmentValidationHelperTest.java | 268 +++++++++--------- .../common/ApplicationHandlerHelperTest.java | 253 +++++++++++------ integration-tests/db/data-model.cds | 15 +- .../DraftOdataRequestValidationBase.java | 213 ++++++-------- .../MediaValidatedAttachmentsDraftTest.java | 145 ++++++++++ ...tedAttachmentsSizeValidationDraftTest.java | 19 +- ...MediaValidatedAttachmentsNonDraftTest.java | 184 ++++++++++++ .../helper/RootEntityBuilder.java | 11 +- integration-tests/srv/test-service.cds | 8 + 11 files changed, 770 insertions(+), 396 deletions(-) create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/MediaValidatedAttachmentsDraftTest.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelper.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelper.java index 436dbd13..c72ec754 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelper.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelper.java @@ -37,20 +37,12 @@ public final class ApplicationHandlerHelper { private static final String ANNOTATION_IS_MEDIA_DATA = "_is_media_data"; private static final String ANNOTATION_CORE_MEDIA_TYPE = "Core.MediaType"; private static final ObjectMapper objectMapper = new ObjectMapper(); + private static final TypeReference> STRING_LIST_TYPE_REF = new TypeReference<>() {}; /** Filter to support extraction of file name for attachment validation */ public static final Filter FILE_NAME_FILTER = (path, element, type) -> element.getName().contentEquals("fileName"); - /** - * A filter for acceptable media types. The filter checks if the entity is a media entity and if - * the element has the annotation "Core.AcceptableMediaTypes". - */ - public static final Filter ACCEPTABLE_MEDIA_TYPE_FILTER = - (path, element, type) -> - element.getName().contentEquals("content") - && element.findAnnotation("Core.AcceptableMediaTypes").isPresent(); - /** * A filter for media content fields. The filter checks if the entity is a media entity and if the * element has the annotation "Core.MediaType". @@ -161,20 +153,19 @@ public static void validateAcceptableMediaTypes( if (serviceEntity == null || !isMediaEntity(serviceEntity)) { return; } - List allowedTypes = getEntityAcceptableMediaTypes(serviceEntity, cdsModel); + List allowedTypes = getEntityAcceptableMediaTypes(serviceEntity); String fileName = extractFileName(entity, data); AttachmentValidationHelper.validateMediaTypeForAttachment(fileName, allowedTypes); } - protected static List getEntityAcceptableMediaTypes(CdsEntity entity, CdsModel cdsModel) { + protected static List getEntityAcceptableMediaTypes(CdsEntity entity) { Optional> flatMap = entity.getElement("content").findAnnotation("Core.AcceptableMediaTypes"); List result = flatMap .map( annotation -> - objectMapper.convertValue( - annotation.getValue(), new TypeReference>() {})) + objectMapper.convertValue(annotation.getValue(), STRING_LIST_TYPE_REF)) .orElse(List.of("*/*")); return result; } diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/CreateAttachmentsHandlerTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/CreateAttachmentsHandlerTest.java index 5b25113a..437f3f3b 100644 --- a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/CreateAttachmentsHandlerTest.java +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/CreateAttachmentsHandlerTest.java @@ -80,8 +80,12 @@ static void classSetup() { void setup() { eventFactory = mock(ModifyAttachmentEventFactory.class); storageReader = mock(ThreadDataStorageReader.class); - cut = new CreateAttachmentsHandler( - eventFactory, storageReader, ModifyApplicationHandlerHelper.DEFAULT_SIZE_WITH_SCANNER); + cut = + new CreateAttachmentsHandler( + eventFactory, + storageReader, + ModifyApplicationHandlerHelper.DEFAULT_SIZE_WITH_SCANNER, + runtime); createContext = mock(CdsCreateEventContext.class); event = mock(ModifyAttachmentEvent.class); @@ -335,7 +339,8 @@ void restoreError_proceedsSuccessfully_noException() { @Test void restoreError_contentTooLargeWithMaxSize_throwsWithMaxSize() { var context = mock(EventContext.class); - var originalException = new ServiceException(ExtendedErrorStatuses.CONTENT_TOO_LARGE, "original message"); + var originalException = + new ServiceException(ExtendedErrorStatuses.CONTENT_TOO_LARGE, "original message"); doThrow(originalException).when(context).proceed(); when(context.get("attachment.MaxSize")).thenReturn("10MB"); @@ -349,7 +354,8 @@ void restoreError_contentTooLargeWithMaxSize_throwsWithMaxSize() { @Test void restoreError_contentTooLargeWithoutMaxSize_throwsWithoutMaxSize() { var context = mock(EventContext.class); - var originalException = new ServiceException(ExtendedErrorStatuses.CONTENT_TOO_LARGE, "original message"); + var originalException = + new ServiceException(ExtendedErrorStatuses.CONTENT_TOO_LARGE, "original message"); doThrow(originalException).when(context).proceed(); when(context.get("attachment.MaxSize")).thenReturn(null); @@ -386,14 +392,15 @@ void restoreError_methodHasCorrectAnnotations() throws NoSuchMethodException { @Test void processBeforeForMetadata_methodHasCorrectAnnotations() throws NoSuchMethodException { - Method method = cut.getClass().getDeclaredMethod("processBeforeForMetadata", EventContext.class, List.class); + Method method = + cut.getClass() + .getDeclaredMethod("processBeforeForMetadata", EventContext.class, List.class); Before beforeAnnotation = method.getAnnotation(Before.class); HandlerOrder handlerOrderAnnotation = method.getAnnotation(HandlerOrder.class); assertThat(beforeAnnotation.event()) - .containsExactlyInAnyOrder( - CqnService.EVENT_CREATE, DraftService.EVENT_DRAFT_NEW); + .containsExactlyInAnyOrder(CqnService.EVENT_CREATE, DraftService.EVENT_DRAFT_NEW); assertThat(handlerOrderAnnotation.value()).isEqualTo(HandlerOrder.BEFORE); } @@ -404,13 +411,17 @@ void processBeforeForMetadata_executesValidation() { List data = List.of(mock(CdsData.class)); when(context.getTarget()).thenReturn(entity); - try (MockedStatic helper = mockStatic(ApplicationHandlerHelper.class)) { - helper.when(() -> ApplicationHandlerHelper.validateAcceptableMediaTypes(entity, data)) + try (MockedStatic helper = + mockStatic(ApplicationHandlerHelper.class)) { + helper + .when(() -> ApplicationHandlerHelper.validateAcceptableMediaTypes(entity, data, runtime)) .thenAnswer(invocation -> null); // when - new CreateAttachmentsHandler(eventFactory, storageReader, "400MB").processBeforeForMetadata(context, data); + new CreateAttachmentsHandler(eventFactory, storageReader, "400MB", runtime) + .processBeforeForMetadata(context, data); // then - helper.verify(() -> ApplicationHandlerHelper.validateAcceptableMediaTypes(entity, data)); + helper.verify( + () -> ApplicationHandlerHelper.validateAcceptableMediaTypes(entity, data, runtime)); } } diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelperTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelperTest.java index c0ca8826..c738bff5 100644 --- a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelperTest.java +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelperTest.java @@ -1,148 +1,140 @@ /* -* © 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.handler.applicationservice.helper; import static org.junit.jupiter.api.Assertions.*; -import java.util.List; - -import org.junit.jupiter.api.Test; - import com.sap.cds.services.ErrorStatuses; import com.sap.cds.services.ServiceException; +import java.util.List; +import org.junit.jupiter.api.Test; class AttachmentValidationHelperTest { - // ---------- constructor ---------- - - @Test - void constructorShouldBeCallableForCoverage() { - new AttachmentValidationHelper(); - } - - // ---------- validateMimeTypeForAttachment ---------- - @Test - void shouldAcceptMimeTypeFromURLConnection() { - String result = AttachmentValidationHelper.validateMediaTypeForAttachment( - "document.pdf", - List.of("application/pdf")); - assertEquals("application/pdf", result); - } - - @Test - void shouldUseExtensionMapWhenURLConnectionDoesNotDetectMimeType() { - String result = AttachmentValidationHelper.validateMediaTypeForAttachment( - "image.webp", - List.of("image/webp")); - assertEquals("image/webp", result); - } - - @Test - void shouldSupportWildCardTypes() { - String result = AttachmentValidationHelper.validateMediaTypeForAttachment( - "image.jpeg", - List.of("image/*")); - assertEquals("image/jpeg", result); - } - - @Test - void shouldAllowAllMimeTypesWithStarSlashStar() { - String result = AttachmentValidationHelper.validateMediaTypeForAttachment( - "anyfile.jpeg", - List.of("*/*")); - assertEquals("image/jpeg", result); - } - - @Test - void shouldAllowRandomInvalidExtensionWithStarSlashStar() { - String result = AttachmentValidationHelper.validateMediaTypeForAttachment( - "anyfile.anyext", - List.of("*/*")); - assertEquals(AttachmentValidationHelper.DEFAULT_MEDIA_TYPE, result); - } - - @Test - void shouldAllowRandomInvalidExtensionWithNullAcceptableMediaTypes() { - String result = AttachmentValidationHelper.validateMediaTypeForAttachment( - "anyfile.anyext", - null); - assertEquals(AttachmentValidationHelper.DEFAULT_MEDIA_TYPE, result); - } - - @Test - void shouldNotAllowRandomInvalidExtensionWithStarSlashStar() { - ServiceException ex = assertThrows(ServiceException.class, - () -> AttachmentValidationHelper.validateMediaTypeForAttachment( - "anyfile.anyext", - List.of("application/pdf"))); - - assertTrue(ex.getErrorStatus().equals(ErrorStatuses.UNSUPPORTED_MEDIA_TYPE)); - } - - @Test - void shouldAllowAllMimeTypesIfNoRestrictionsGiven() { - String result = AttachmentValidationHelper.validateMediaTypeForAttachment( - "anyfile.anyext", - List.of()); - assertEquals(AttachmentValidationHelper.DEFAULT_MEDIA_TYPE, result); - } - - @Test - void shouldUseDefaultMimeTypeWhenExtensionIsUnknown() { - String result = AttachmentValidationHelper.validateMediaTypeForAttachment( - "file.unknownext", - List.of(AttachmentValidationHelper.DEFAULT_MEDIA_TYPE)); - assertEquals(AttachmentValidationHelper.DEFAULT_MEDIA_TYPE, result); - } - - @Test - void shouldThrowExceptionWhenInvalidFileName() { - ServiceException ex = assertThrows(ServiceException.class, - () -> AttachmentValidationHelper.validateMediaTypeForAttachment( - "invalidfilename", - List.of("application/pdf"))); - - assertTrue(ex.getErrorStatus().equals(ErrorStatuses.UNSUPPORTED_MEDIA_TYPE)); - } - - @Test - void shouldThrowExceptionWhenFilenameEndsWithDot() { - ServiceException ex = assertThrows(ServiceException.class, - () -> AttachmentValidationHelper.validateMediaTypeForAttachment( - "file.", - List.of("application/pdf"))); - - assertEquals(ErrorStatuses.UNSUPPORTED_MEDIA_TYPE, ex.getErrorStatus()); - } - - @Test - void shouldHandleUppercaseExtension() { - String result = AttachmentValidationHelper.validateMediaTypeForAttachment( - "photo.JPG", - List.of("image/jpeg")); - - assertEquals("image/jpeg", result); - } - - @Test - void shouldThrowExceptionWhenMimeTypeNotAllowed() { - ServiceException ex = assertThrows(ServiceException.class, - () -> AttachmentValidationHelper.validateMediaTypeForAttachment( - "document.pdf", - List.of("image/png"))); - - assertTrue(ex.getMessage().contains("not allowed")); - } - - @Test - void shouldThrowExceptionWhenDefaultMimeTypeNotAllowed() { - ServiceException ex = assertThrows(ServiceException.class, - () -> AttachmentValidationHelper.validateMediaTypeForAttachment( - "file.unknownext", - List.of("application/pdf"))); - - assertTrue(ex.getMessage().contains("not allowed")); - } - -} \ No newline at end of file + @Test + void constructor_shouldBeCallable_forCoverage() { + new AttachmentValidationHelper(); + } + + @Test + void shouldDetectMimeTypeFromURLConnection() { + assertMediaType("document.pdf", List.of("application/pdf"), "application/pdf"); + } + + @Test + void shouldUseExtensionFallbackWhenURLConnectionFails() { + assertMediaType("image.webp", List.of("image/webp"), "image/webp"); + } + + @Test + void shouldSupportWildcardSubtype() { + assertMediaType("image.jpeg", List.of("image/*"), "image/jpeg"); + } + + @Test + void shouldAllowAllWithStarSlashStar() { + assertMediaType("anyfile.jpeg", List.of("*/*"), "image/jpeg"); + } + + @Test + void shouldAllowUnknownExtensionWithStarSlashStar() { + assertMediaType( + "anyfile.anyext", + List.of("*/*"), + AttachmentValidationHelper.DEFAULT_MEDIA_TYPE); + } + + @Test + void shouldAllowUnknownExtensionWhenAcceptableTypesNull() { + assertMediaType( + "anyfile.anyext", + null, + AttachmentValidationHelper.DEFAULT_MEDIA_TYPE); + } + + @Test + void shouldAllowAllWhenAcceptableTypesEmpty() { + assertMediaType( + "anyfile.anyext", + List.of(), + AttachmentValidationHelper.DEFAULT_MEDIA_TYPE); + } + + @Test + void shouldUseDefaultWhenUnknownExtensionExplicitlyAllowed() { + assertMediaType( + "file.unknownext", + List.of(AttachmentValidationHelper.DEFAULT_MEDIA_TYPE), + AttachmentValidationHelper.DEFAULT_MEDIA_TYPE); + } + + @Test + void shouldHandleUppercaseExtension() { + assertMediaType("photo.JPG", List.of("image/jpeg"), "image/jpeg"); + } + + @Test + void shouldRejectUnknownExtensionWhenNotAllowed() { + assertUnsupported( + "anyfile.anyext", + List.of("application/pdf")); + } + + @Test + void shouldThrowWhenFilenameHasNoExtension() { + assertUnsupported( + "invalidfilename", + List.of("application/pdf")); + } + + @Test + void shouldThrowWhenFilenameEndsWithDot() { + assertUnsupported( + "file.", + List.of("application/pdf")); + } + + @Test + void shouldThrowWhenMimeTypeNotAllowed() { + ServiceException ex = assertThrows( + ServiceException.class, + () -> AttachmentValidationHelper.validateMediaTypeForAttachment( + "document.pdf", + List.of("image/png"))); + + assertTrue(ex.getMessage().contains("not allowed")); + } + + @Test + void shouldThrowWhenDefaultMimeTypeNotAllowed() { + ServiceException ex = assertThrows( + ServiceException.class, + () -> AttachmentValidationHelper.validateMediaTypeForAttachment( + "file.unknownext", + List.of("application/pdf"))); + + assertTrue(ex.getMessage().contains("not allowed")); + } + + private void assertMediaType( + String fileName, + List allowed, + String expectedType) { + + String result = AttachmentValidationHelper.validateMediaTypeForAttachment(fileName, allowed); + + assertEquals(expectedType, result); + } + + private void assertUnsupported( + String fileName, + List allowed) { + + ServiceException ex = assertThrows( + ServiceException.class, + () -> AttachmentValidationHelper.validateMediaTypeForAttachment(fileName, allowed)); + + assertEquals(ErrorStatuses.UNSUPPORTED_MEDIA_TYPE, ex.getErrorStatus()); + } +} diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelperTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelperTest.java index 7086e28a..a81dd9b0 100644 --- a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelperTest.java +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelperTest.java @@ -5,8 +5,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import com.sap.cds.CdsData; @@ -14,19 +13,36 @@ import com.sap.cds.CdsDataProcessor.Filter; import com.sap.cds.CdsDataProcessor.Validator; import com.sap.cds.feature.attachments.handler.applicationservice.helper.AttachmentValidationHelper; +import com.sap.cds.reflect.CdsAnnotation; +import com.sap.cds.reflect.CdsElement; import com.sap.cds.reflect.CdsEntity; +import com.sap.cds.reflect.CdsModel; import com.sap.cds.services.ServiceException; -import com.sap.cds.reflect.CdsAnnotation; - +import com.sap.cds.services.runtime.CdsRuntime; import java.util.*; - +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Mock; import org.mockito.MockedStatic; - -import com.sap.cds.reflect.CdsElement; +import org.mockito.MockitoAnnotations; class ApplicationHandlerHelperTest { + @Mock private CdsEntity entity; + @Mock private CdsData data; + @Mock private CdsRuntime cdsRuntime; + @Mock private CdsModel cdsModel; + @Mock private CdsEntity serviceEntity; + @Mock private CdsElement cdsElement; + @Mock private CdsAnnotation annotation; + @Mock private CdsDataProcessor processor; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + when(cdsRuntime.getCdsModel()).thenReturn(cdsModel); + } + @Test void keysAreInData() { Map keys = Map.of("key1", "value1", "key2", "value2"); @@ -72,29 +88,51 @@ void removeDraftKey() { assertTrue(result.containsKey("key1")); } - // ----------- Test validateAcceptableMediaTypes ----------- + @Test + void getEntityAcceptableMediaTypes_returnsAnnotationValue() { + List expectedTypes = List.of("image/png", "application/pdf"); + + when(entity.getElement("content")).thenReturn(cdsElement); + when(cdsElement.findAnnotation("Core.AcceptableMediaTypes")) + .thenReturn(Optional.of(annotation)); + when(annotation.getValue()).thenReturn(expectedTypes); + assertEquals(expectedTypes, ApplicationHandlerHelper.getEntityAcceptableMediaTypes(entity)); + } @Test - void extractFileName_whenFileNamePresent_returnsValue() { + void getEntityAcceptableMediaTypes_missingAnnotation_returnsWildcard() { + when(entity.getElement("content")).thenReturn(cdsElement); + when(cdsElement.findAnnotation("Core.AcceptableMediaTypes")).thenReturn(Optional.empty()); + assertEquals(List.of("*/*"), ApplicationHandlerHelper.getEntityAcceptableMediaTypes(entity)); + } - CdsEntity entity = mock(CdsEntity.class); - CdsData data = mock(CdsData.class); - CdsElement fileNameElement = mock(CdsElement.class); - when(fileNameElement.getName()).thenReturn("fileName"); - CdsDataProcessor processor = mock(CdsDataProcessor.class); + @Test + void getEntityAcceptableMediaTypes_nullAnnotationValue_returnsWildcard() { + when(entity.getElement("content")).thenReturn(cdsElement); + when(cdsElement.findAnnotation("Core.AcceptableMediaTypes")) + .thenReturn(Optional.of(annotation)); + when(annotation.getValue()).thenReturn(null); + assertEquals(List.of("*/*"), ApplicationHandlerHelper.getEntityAcceptableMediaTypes(entity)); + } + @Test + void extractFileName_whenFileNamePresent_returnsValue() { + when(cdsElement.getName()).thenReturn("fileName"); try (MockedStatic mocked = mockStatic(CdsDataProcessor.class)) { mocked.when(CdsDataProcessor::create).thenReturn(processor); - doAnswer(invocation -> { - Filter filter = invocation.getArgument(0); - Validator validator = invocation.getArgument(1); + doAnswer( + invocation -> { + Filter filter = invocation.getArgument(0); + Validator validator = invocation.getArgument(1); - // Simulate processor visiting fileName with String value - if (filter.test(null, fileNameElement, null)) { - validator.validate(null, fileNameElement, "test.pdf"); - } - return processor; - }).when(processor).addValidator(any(), any()); + // Simulate processor visiting fileName with String value + if (filter.test(null, cdsElement, null)) { + validator.validate(null, cdsElement, "test.pdf"); + } + return processor; + }) + .when(processor) + .addValidator(any(), any()); doNothing().when(processor).process(anyList(), any()); String result = ApplicationHandlerHelper.extractFileName(entity, List.of(data)); @@ -104,100 +142,127 @@ void extractFileName_whenFileNamePresent_returnsValue() { @Test void extractFileName_whenElementIsNotFileName_throws() { - - CdsEntity entity = mock(CdsEntity.class); - CdsData data = mock(CdsData.class); - CdsElement element = mock(CdsElement.class); - when(element.getName()).thenReturn("content"); - CdsDataProcessor processor = mock(CdsDataProcessor.class); - + when(cdsElement.getName()).thenReturn("content"); try (MockedStatic mocked = mockStatic(CdsDataProcessor.class)) { mocked.when(CdsDataProcessor::create).thenReturn(processor); - doAnswer(invocation -> { - Filter filter = invocation.getArgument(0); - Validator validator = invocation.getArgument(1); - if (filter.test(null, element, null)) { - validator.validate(null, element, "test.pdf"); - } - return processor; - }).when(processor).addValidator(any(), any()); + doAnswer( + invocation -> { + Filter filter = invocation.getArgument(0); + Validator validator = invocation.getArgument(1); + if (filter.test(null, cdsElement, null)) { + validator.validate(null, cdsElement, "test.pdf"); + } + return processor; + }) + .when(processor) + .addValidator(any(), any()); doNothing().when(processor).process(anyList(), any()); - assertThrows(ServiceException.class, () -> ApplicationHandlerHelper.extractFileName(entity, List.of(data))); + assertThrows( + ServiceException.class, + () -> ApplicationHandlerHelper.extractFileName(entity, List.of(data))); } } @Test void extractFileName_valueIsNotString_branchCovered() { - - CdsEntity entity = mock(CdsEntity.class); - CdsData data = mock(CdsData.class); - CdsElement element = mock(CdsElement.class); - when(element.getName()).thenReturn("fileName"); - CdsDataProcessor processor = mock(CdsDataProcessor.class); - + when(cdsElement.getName()).thenReturn("fileName"); try (MockedStatic mocked = mockStatic(CdsDataProcessor.class)) { mocked.when(CdsDataProcessor::create).thenReturn(processor); - doAnswer(invocation -> { - Filter filter = invocation.getArgument(0); - Validator validator = invocation.getArgument(1); - if (filter.test(null, element, null)) { - validator.validate(null, element, 42); // non-String - } - return processor; - }).when(processor).addValidator(any(), any()); + doAnswer( + invocation -> { + Filter filter = invocation.getArgument(0); + Validator validator = invocation.getArgument(1); + if (filter.test(null, cdsElement, null)) { + validator.validate(null, cdsElement, 42); // non-String + } + return processor; + }) + .when(processor) + .addValidator(any(), any()); doNothing().when(processor).process(anyList(), any()); - assertThrows(ServiceException.class, () -> ApplicationHandlerHelper.extractFileName(entity, List.of(data))); + + assertThrows( + ServiceException.class, + () -> ApplicationHandlerHelper.extractFileName(entity, List.of(data))); } } - // ----------- Test getEntityAcceptableMediaTypes ----------- @Test - void getEntityAcceptableMediaTypes_withAnnotation() { - CdsEntity entity = mock(CdsEntity.class); - CdsElement content = mock(CdsElement.class); - CdsAnnotation annotation = mock(CdsAnnotation.class); - when(entity.getElement("content")).thenReturn(content); - when(content.findAnnotation("Core.AcceptableMediaTypes")) - .thenReturn(Optional.of(annotation)); - when(annotation.getValue()) - .thenReturn(List.of("image/png", "image/jpeg")); - List result = ApplicationHandlerHelper.getEntityAcceptableMediaTypes(entity); - assertThat(result).containsExactly("image/png", "image/jpeg"); + void validateAcceptableMediaTypes_nonMediaOrMissingEntity_doesNothing() { + when(entity.getQualifiedName()).thenReturn("TestService.Roots"); + when(cdsModel.findEntity("TestService.Roots")).thenReturn(Optional.empty()); + + assertDoesNotThrow( + () -> ApplicationHandlerHelper.validateAcceptableMediaTypes(entity, List.of(), cdsRuntime)); } @Test - void getEntityAcceptableMediaTypes_withoutAnnotation_returnsWildcard() { - CdsEntity entity = mock(CdsEntity.class); - CdsElement content = mock(CdsElement.class); - when(entity.getElement("content")).thenReturn(content); - when(content.findAnnotation("Core.AcceptableMediaTypes")) - .thenReturn(Optional.empty()); - List result = ApplicationHandlerHelper.getEntityAcceptableMediaTypes(entity); - assertThat(result).containsExactly("*/*"); + void validateAcceptableMediaTypes_notMediaEntity_doesNotCallValidation() { + when(entity.getQualifiedName()).thenReturn("TestService.Roots"); + when(cdsModel.findEntity("TestService.Roots")).thenReturn(Optional.of(serviceEntity)); + when(serviceEntity.getAnnotationValue("_is_media_data", false)).thenReturn(false); + + try (MockedStatic mocked = + mockStatic(AttachmentValidationHelper.class)) { + ApplicationHandlerHelper.validateAcceptableMediaTypes(entity, List.of(), cdsRuntime); + mocked.verifyNoInteractions(); + } } @Test - void validateAcceptableMediaTypes_success_executesAllLines() { - CdsEntity entity = mock(CdsEntity.class); - CdsElement content = mock(CdsElement.class); - CdsAnnotation annotation = mock(CdsAnnotation.class); - when(entity.getElement("content")).thenReturn(content); - when(content.findAnnotation("Core.AcceptableMediaTypes")) - .thenReturn(Optional.of(annotation)); - when(annotation.getValue()) - .thenReturn(List.of("image/png")); - List data = List.of(mock(CdsData.class)); - try (MockedStatic helper = mockStatic(ApplicationHandlerHelper.class, CALLS_REAL_METHODS); - MockedStatic validator = mockStatic(AttachmentValidationHelper.class)) { - // allow real getEntityAcceptableMediaTypes - helper.when(() -> ApplicationHandlerHelper.extractFileName(entity, data)) - .thenReturn("test.png"); - assertDoesNotThrow(() -> ApplicationHandlerHelper.validateAcceptableMediaTypes(entity, data)); - validator.verify(() -> AttachmentValidationHelper.validateMediaTypeForAttachment( - "test.png", - List.of("image/png"))); + void shouldNotThrowWhenEntityNotFoundInModel() { + when(entity.getQualifiedName()).thenReturn("Test.Entity"); + when(cdsModel.findEntity("Test.Entity")).thenReturn(Optional.empty()); + + assertDoesNotThrow( + () -> ApplicationHandlerHelper.validateAcceptableMediaTypes(entity, List.of(), cdsRuntime)); + } + + @Test + void validateAcceptableMediaTypes_mediaTypeMatches_succeeds() { + setupMediaEntity(List.of("image/png")); + try (MockedStatic helperStatic = + mockStatic(ApplicationHandlerHelper.class, CALLS_REAL_METHODS)) { + + // file.png → media type image/png → allowed + helperStatic + .when(() -> ApplicationHandlerHelper.extractFileName(entity, List.of(data))) + .thenReturn("file.png"); + + assertDoesNotThrow( + () -> + ApplicationHandlerHelper.validateAcceptableMediaTypes( + entity, List.of(data), cdsRuntime)); } } + @Test + void validateAcceptableMediaTypes_mediaTypeMismatch_throws() { + setupMediaEntity(List.of("image/png")); + try (MockedStatic helperStatic = + mockStatic(ApplicationHandlerHelper.class, CALLS_REAL_METHODS)) { + + // file.jpg → media type image/jpeg → NOT allowed + helperStatic + .when(() -> ApplicationHandlerHelper.extractFileName(entity, List.of(data))) + .thenReturn("file.jpg"); + + assertThrows( + ServiceException.class, + () -> + ApplicationHandlerHelper.validateAcceptableMediaTypes( + entity, List.of(data), cdsRuntime)); + } + } + + private void setupMediaEntity(List allowedTypes) { + when(entity.getQualifiedName()).thenReturn("Test.Entity"); + when(cdsModel.findEntity("Test.Entity")).thenReturn(Optional.of(serviceEntity)); + when(serviceEntity.getAnnotationValue("_is_media_data", false)).thenReturn(true); + when(serviceEntity.getElement("content")).thenReturn(cdsElement); + when(cdsElement.findAnnotation("Core.AcceptableMediaTypes")) + .thenReturn(Optional.of(annotation)); + when(annotation.getValue()).thenReturn(allowedTypes); + } } diff --git a/integration-tests/db/data-model.cds b/integration-tests/db/data-model.cds index f6c35e19..9837be0a 100644 --- a/integration-tests/db/data-model.cds +++ b/integration-tests/db/data-model.cds @@ -1,19 +1,20 @@ namespace test.data.model; using {cuid} from '@sap/cds/common'; -using {sap.attachments.Attachments} from `com.sap.cds/cds-feature-attachments`; +using {sap.attachments.Attachments} from`com.sap.cds/cds-feature-attachments`; entity AttachmentEntity : Attachments { parentKey : UUID; } entity Roots : cuid { - title : String; - attachments : Composition of many AttachmentEntity - on attachments.parentKey = $self.ID; - items : Composition of many Items - on items.parentID = $self.ID; - sizeLimitedAttachments : Composition of many Attachments; + title : String; + attachments : Composition of many AttachmentEntity + on attachments.parentKey = $self.ID; + items : Composition of many Items + on items.parentID = $self.ID; + sizeLimitedAttachments : Composition of many Attachments; + mediaValidatedAttachments : Composition of many Attachments; } entity Items : cuid { diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationBase.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationBase.java index 843fa2bb..b926b1be 100644 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationBase.java +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationBase.java @@ -42,18 +42,21 @@ @AutoConfigureMockMvc abstract class DraftOdataRequestValidationBase { - protected static final Logger logger = - LoggerFactory.getLogger(DraftOdataRequestValidationBase.class); + protected static final Logger logger = LoggerFactory.getLogger(DraftOdataRequestValidationBase.class); private static final String BASE_URL = MockHttpRequestHelper.ODATA_BASE_URL + "TestDraftService/"; private static final String BASE_ROOT_URL = BASE_URL + "DraftRoots"; @Autowired(required = false) protected TestPluginAttachmentsServiceHandler serviceHandler; - @Autowired protected MockHttpRequestHelper requestHelper; - @Autowired protected PersistenceService persistenceService; - @Autowired private TableDataDeleter dataDeleter; - @Autowired private TestPersistenceHandler testPersistenceHandler; + @Autowired + protected MockHttpRequestHelper requestHelper; + @Autowired + protected PersistenceService persistenceService; + @Autowired + private TableDataDeleter dataDeleter; + @Autowired + private TestPersistenceHandler testPersistenceHandler; @AfterEach void teardown() { @@ -107,16 +110,14 @@ void contentCanBeReadFromDraft() throws Exception { clearServiceHandlerContext(); createNewDraftForExistingRoot(selectedRoot.getId()); - var attachmentUrl = - getAttachmentBaseUrl( - selectedRoot.getItems().get(0).getId(), - selectedRoot.getItems().get(0).getAttachments().get(0).getId(), - false) - + "/content"; - var attachmentEntityUrl = - getAttachmentEntityBaseUrl( - selectedRoot.getItems().get(0).getAttachmentEntities().get(0).getId(), false) - + "/content"; + var attachmentUrl = getAttachmentBaseUrl( + selectedRoot.getItems().get(0).getId(), + selectedRoot.getItems().get(0).getAttachments().get(0).getId(), + false) + + "/content"; + var attachmentEntityUrl = getAttachmentEntityBaseUrl( + selectedRoot.getItems().get(0).getAttachmentEntities().get(0).getId(), false) + + "/content"; Awaitility.await() .atMost(60, TimeUnit.SECONDS) @@ -128,9 +129,8 @@ void contentCanBeReadFromDraft() throws Exception { var attachmentEntityResponse = requestHelper.executeGet(attachmentEntityUrl); var attachmentResponseContent = getResponseContent(attachmentResponse); var attachmentEntityResponseContent = getResponseContent(attachmentEntityResponse); - var result = - attachmentResponseContent.equals(testContentAttachment) - && attachmentEntityResponseContent.equals(testContentAttachmentEntity); + var result = attachmentResponseContent.equals(testContentAttachment) + && attachmentEntityResponseContent.equals(testContentAttachmentEntity); if (!result) { logger.info( "Attachment response content: {}, Attachment Test Content: {}, Attachment Entity response content: {}, Attachment Entity Test Content: {}", @@ -154,16 +154,15 @@ void contentCanBeReadFromDraft() throws Exception { @Test void deleteAttachmentAndActivateDraft() throws Exception { - var selectedRoot = - deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); + var selectedRoot = deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); clearServiceHandlerContext(); createNewDraftForExistingRoot(selectedRoot.getId()); var itemAttachment = selectedRoot.getItems().get(0).getAttachments().get(0); var itemAttachmentEntity = selectedRoot.getItems().get(0).getAttachmentEntities().get(0); - var attachmentDeleteUrl = - getAttachmentBaseUrl(selectedRoot.getItems().get(0).getId(), itemAttachment.getId(), false); + var attachmentDeleteUrl = getAttachmentBaseUrl(selectedRoot.getItems().get(0).getId(), itemAttachment.getId(), + false); var attachmentEntityDeleteUrl = getAttachmentEntityBaseUrl(itemAttachmentEntity.getId(), false); requestHelper.executeDeleteWithMatcher(attachmentDeleteUrl, status().isNoContent()); @@ -180,8 +179,7 @@ void deleteAttachmentAndActivateDraft() throws Exception { @Test void updateAttachmentAndActivateDraft() throws Exception { - var selectedRoot = - deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); + var selectedRoot = deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); clearServiceHandlerContext(); createNewDraftForExistingRoot(selectedRoot.getId()); @@ -203,15 +201,14 @@ void updateAttachmentAndActivateDraft() throws Exception { assertThat(selectedRootAfterUpdate.getItems().get(0).getAttachments().get(0).getFileName()) .isEqualTo(changedAttachmentFileName); assertThat( - selectedRootAfterUpdate.getItems().get(0).getAttachmentEntities().get(0).getFileName()) + selectedRootAfterUpdate.getItems().get(0).getAttachmentEntities().get(0).getFileName()) .isEqualTo(changedAttachmentEntityFileName); verifyNoAttachmentEventsCalled(); } @Test void updateAttachmentAndCancelDraft() throws Exception { - var selectedRoot = - deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); + var selectedRoot = deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); clearServiceHandlerContext(); createNewDraftForExistingRoot(selectedRoot.getId()); @@ -233,15 +230,14 @@ void updateAttachmentAndCancelDraft() throws Exception { assertThat(selectedRootAfterUpdate.getItems().get(0).getAttachments().get(0).getFileName()) .isEqualTo(originAttachmentFileName); assertThat( - selectedRootAfterUpdate.getItems().get(0).getAttachmentEntities().get(0).getFileName()) + selectedRootAfterUpdate.getItems().get(0).getAttachmentEntities().get(0).getFileName()) .isEqualTo(originAttachmentEntityFileName); verifyNoAttachmentEventsCalled(); } @Test void createAttachmentAndActivateDraft() throws Exception { - var selectedRoot = - deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); + var selectedRoot = deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); clearServiceHandlerContext(); createNewDraftForExistingRoot(selectedRoot.getId()); @@ -261,8 +257,7 @@ void createAttachmentAndActivateDraft() throws Exception { @Test void createAttachmentAndCancelDraft() throws Exception { - var selectedRoot = - deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); + var selectedRoot = deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); clearServiceHandlerContext(); createNewDraftForExistingRoot(selectedRoot.getId()); @@ -282,9 +277,8 @@ void createAttachmentAndCancelDraft() throws Exception { @Test void deleteContentInDraft() throws Exception { - var selectedRoot = - deepCreateAndActivate( - "testContent attachment for delete", "testContent attachmentEntity for delete"); + var selectedRoot = deepCreateAndActivate( + "testContent attachment for delete", "testContent attachmentEntity for delete"); clearServiceHandlerContext(); createNewDraftForExistingRoot(selectedRoot.getId()); var itemAttachment = selectedRoot.getItems().get(0).getAttachments().get(0); @@ -305,8 +299,7 @@ void deleteContentInDraft() throws Exception { @Test void doNotDeleteContentInCancelledDraft() throws Exception { - var selectedRoot = - deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); + var selectedRoot = deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); clearServiceHandlerContext(); createNewDraftForExistingRoot(selectedRoot.getId()); @@ -328,8 +321,7 @@ void doNotDeleteContentInCancelledDraft() throws Exception { @Test void updateContentInDraft() throws Exception { - var selectedRoot = - deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); + var selectedRoot = deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); clearServiceHandlerContext(); createNewDraftForExistingRoot(selectedRoot.getId()); @@ -363,12 +355,12 @@ void updateContentInDraft() throws Exception { assertThat(selectedRootAfterDeletion.getItems().get(0).getAttachments().get(0).getContentId()) .isNotEmpty(); assertThat( - selectedRootAfterDeletion - .getItems() - .get(0) - .getAttachmentEntities() - .get(0) - .getContentId()) + selectedRootAfterDeletion + .getItems() + .get(0) + .getAttachmentEntities() + .get(0) + .getContentId()) .isNotEmpty(); } @@ -439,7 +431,7 @@ void deleteItemAndCancelDraft() throws Exception { .isNotEmpty(); assertThat(selectedRootAfterDelete.getItems().get(0).getAttachmentEntities()).isNotEmpty(); assertThat( - selectedRootAfterDelete.getItems().get(0).getAttachmentEntities().get(0).getContentId()) + selectedRootAfterDelete.getItems().get(0).getAttachmentEntities().get(0).getContentId()) .isNotEmpty(); verifyNoAttachmentEventsCalled(); } @@ -462,16 +454,14 @@ void noEventsForForDeletedRoot() throws Exception { assertThat(result).isEmpty(); var attachmentContentId = selectedRoot.getItems().get(0).getAttachments().get(0).getContentId(); - var attachmentEntityContentId = - selectedRoot.getItems().get(0).getAttachmentEntities().get(0).getContentId(); + var attachmentEntityContentId = selectedRoot.getItems().get(0).getAttachmentEntities().get(0).getContentId(); verifyOnlyTwoDeleteEvents(attachmentContentId, attachmentEntityContentId); } @Test void errorInTransactionAfterCreateCallsDelete() throws Exception { - var selectedRoot = - deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); + var selectedRoot = deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); clearServiceHandlerContext(); createNewDraftForExistingRoot(selectedRoot.getId()); @@ -486,8 +476,7 @@ void errorInTransactionAfterCreateCallsDelete() throws Exception { @Test void errorInTransactionAfterCreateCallsDeleteAndNothingForCancel() throws Exception { - var selectedRoot = - deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); + var selectedRoot = deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); clearServiceHandlerContext(); createNewDraftForExistingRoot(selectedRoot.getId()); @@ -538,8 +527,7 @@ void errorInTransactionAfterUpdateCallsDeleteEvenIfDraftIsCancelled() throws Exc @Test void createAndDeleteAttachmentWorks() throws Exception { - var selectedRoot = - deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); + var selectedRoot = deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); clearServiceHandlerContext(); createNewDraftForExistingRoot(selectedRoot.getId()); @@ -555,21 +543,18 @@ void createAndDeleteAttachmentWorks() throws Exception { var existingAttachment = selectedRoot.getItems().get(0).getAttachments().get(0); var existingAttachmentEntity = selectedRoot.getItems().get(0).getAttachmentEntities().get(0); - var newAttachment = - draftRoot.getItems().get(0).getAttachments().stream() - .filter(attachment -> !attachment.getId().equals(existingAttachment.getId())) - .findAny() - .orElseThrow(); - var newAttachmentEntity = - draftRoot.getItems().get(0).getAttachmentEntities().stream() - .filter( - attachmentEntity -> - !attachmentEntity.getId().equals(existingAttachmentEntity.getId())) - .findAny() - .orElseThrow(); - - var attachmentDeleteUrl = - getAttachmentBaseUrl(selectedRoot.getItems().get(0).getId(), newAttachment.getId(), false); + var newAttachment = draftRoot.getItems().get(0).getAttachments().stream() + .filter(attachment -> !attachment.getId().equals(existingAttachment.getId())) + .findAny() + .orElseThrow(); + var newAttachmentEntity = draftRoot.getItems().get(0).getAttachmentEntities().stream() + .filter( + attachmentEntity -> !attachmentEntity.getId().equals(existingAttachmentEntity.getId())) + .findAny() + .orElseThrow(); + + var attachmentDeleteUrl = getAttachmentBaseUrl(selectedRoot.getItems().get(0).getId(), newAttachment.getId(), + false); var attachmentEntityDeleteUrl = getAttachmentEntityBaseUrl(newAttachmentEntity.getId(), false); requestHelper.executeDeleteWithMatcher(attachmentDeleteUrl, status().isNoContent()); @@ -595,8 +580,7 @@ protected DraftRoots deepCreateAndActivate( } private DraftRoots createNewDraft() throws Exception { - var responseRootCdsData = - requestHelper.executePostWithODataResponseAndAssertStatusCreated(BASE_ROOT_URL, "{}"); + var responseRootCdsData = requestHelper.executePostWithODataResponseAndAssertStatusCreated(BASE_ROOT_URL, "{}"); return Struct.access(responseRootCdsData).as(DraftRoots.class); } @@ -621,8 +605,7 @@ private Items createItem(String rootUrl) throws Exception { var item = Items.create(); item.setTitle("some item"); var itemUrl = rootUrl + "/items"; - var responseItemCdsData = - requestHelper.executePostWithODataResponseAndAssertStatusCreated(itemUrl, item.toJson()); + var responseItemCdsData = requestHelper.executePostWithODataResponseAndAssertStatusCreated(itemUrl, item.toJson()); return Struct.access(responseItemCdsData).as(Items.class); } @@ -661,9 +644,8 @@ private Attachments createAttachment(String itemId) throws Exception { itemAttachment.setFileName("itemAttachment.txt"); var attachmentPostUrl = BASE_URL + "Items(ID=" + itemId + ",IsActiveEntity=false)/attachments"; - var responseAttachmentCdsData = - requestHelper.executePostWithODataResponseAndAssertStatusCreated( - attachmentPostUrl, itemAttachment.toJson()); + var responseAttachmentCdsData = requestHelper.executePostWithODataResponseAndAssertStatusCreated( + attachmentPostUrl, itemAttachment.toJson()); return Struct.access(responseAttachmentCdsData).as(Attachments.class); } @@ -696,8 +678,7 @@ private void putNewContentForAttachmentEntity( private void putNewContentForAttachmentEntity( String testContentAttachmentEntity, String attachmentId, ResultMatcher matcher) throws Exception { - var attachmentEntityPutUrl = - BASE_URL + "/AttachmentEntity(ID=" + attachmentId + ",IsActiveEntity=false)/content"; + var attachmentEntityPutUrl = BASE_URL + "/AttachmentEntity(ID=" + attachmentId + ",IsActiveEntity=false)/content"; requestHelper.setContentType("image/jpeg"); requestHelper.executePutWithMatcher( attachmentEntityPutUrl, @@ -711,9 +692,8 @@ private AttachmentEntity createAttachmentEntity(Items responseItem) throws Excep itemAttachmentEntity.setFileName("itemAttachmentEntity.txt"); var attachmentEntityPostUrl = getItemUrl(responseItem, false) + "/attachmentEntities"; - var responseAttachmentEntityCdsData = - requestHelper.executePostWithODataResponseAndAssertStatusCreated( - attachmentEntityPostUrl, itemAttachmentEntity.toJson()); + var responseAttachmentEntityCdsData = requestHelper.executePostWithODataResponseAndAssertStatusCreated( + attachmentEntityPostUrl, itemAttachmentEntity.toJson()); return Struct.access(responseAttachmentEntityCdsData).as(AttachmentEntity.class); } @@ -764,33 +744,29 @@ private DraftRoots selectStoredRootData(DraftRoots responseRoot) { } private DraftRoots selectStoredRootData(String entityName, DraftRoots responseRoot) { - var select = - Select.from(entityName) - .where(root -> root.get(DraftRoots.ID).eq(responseRoot.getId())) - .columns( - StructuredType::_all, - root -> - root.to(DraftRoots.ITEMS) - .expand( - StructuredType::_all, - item -> item.to(Items.ATTACHMENTS).expand(), - item -> item.to(Items.ATTACHMENT_ENTITIES).expand())); + var select = Select.from(entityName) + .where(root -> root.get(DraftRoots.ID).eq(responseRoot.getId())) + .columns( + StructuredType::_all, + root -> root.to(DraftRoots.ITEMS) + .expand( + StructuredType::_all, + item -> item.to(Items.ATTACHMENTS).expand(), + item -> item.to(Items.ATTACHMENT_ENTITIES).expand())); return persistenceService.run(select).single(DraftRoots.class); } protected void readAndValidateActiveContent( DraftRoots selectedRoot, String attachmentContent, String attachmentEntityContent) throws Exception { - var attachmentUrl = - getAttachmentBaseUrl( - selectedRoot.getItems().get(0).getId(), - selectedRoot.getItems().get(0).getAttachments().get(0).getId(), - true) - + "/content"; - var attachmentEntityUrl = - getAttachmentEntityBaseUrl( - selectedRoot.getItems().get(0).getAttachmentEntities().get(0).getId(), true) - + "/content"; + var attachmentUrl = getAttachmentBaseUrl( + selectedRoot.getItems().get(0).getId(), + selectedRoot.getItems().get(0).getAttachments().get(0).getId(), + true) + + "/content"; + var attachmentEntityUrl = getAttachmentEntityBaseUrl( + selectedRoot.getItems().get(0).getAttachmentEntities().get(0).getId(), true) + + "/content"; Awaitility.await() .atMost(60, TimeUnit.SECONDS) @@ -801,12 +777,10 @@ protected void readAndValidateActiveContent( var attachmentResponse = requestHelper.executeGet(attachmentUrl); var attachmentEntityResponse = requestHelper.executeGet(attachmentEntityUrl); var attachmentContentAsString = attachmentResponse.getResponse().getContentAsString(); - var attachmentEntityContentAsString = - attachmentEntityResponse.getResponse().getContentAsString(); + var attachmentEntityContentAsString = attachmentEntityResponse.getResponse().getContentAsString(); - var booleanResult = - attachmentContentAsString.equals(attachmentContent) - && attachmentEntityContentAsString.equals(attachmentEntityContent); + var booleanResult = attachmentContentAsString.equals(attachmentContent) + && attachmentEntityContentAsString.equals(attachmentEntityContent); if (!booleanResult) { logger.info( @@ -832,11 +806,9 @@ protected void readAndValidateActiveContent( private void deleteContent( DraftRoots selectedRoot, Attachments itemAttachment, AttachmentEntity itemAttachmentEntity) throws Exception { - var attachmentUrl = - getAttachmentBaseUrl(selectedRoot.getItems().get(0).getId(), itemAttachment.getId(), false) - + "/content"; - var attachmentEntityUrl = - getAttachmentEntityBaseUrl(itemAttachmentEntity.getId(), false) + "/content"; + var attachmentUrl = getAttachmentBaseUrl(selectedRoot.getItems().get(0).getId(), itemAttachment.getId(), false) + + "/content"; + var attachmentEntityUrl = getAttachmentEntityBaseUrl(itemAttachmentEntity.getId(), false) + "/content"; requestHelper.executeDeleteWithMatcher(attachmentUrl, status().isNoContent()); requestHelper.executeDeleteWithMatcher(attachmentEntityUrl, status().isNoContent()); @@ -866,8 +838,7 @@ private void updateFileName( String changedAttachmentEntityFileName, HttpStatus httpStatus) throws Exception { - var attachmentUrl = - getAttachmentBaseUrl(selectedRoot.getItems().get(0).getId(), itemAttachment.getId(), false); + var attachmentUrl = getAttachmentBaseUrl(selectedRoot.getItems().get(0).getId(), itemAttachment.getId(), false); var attachmentEntityUrl = getAttachmentEntityBaseUrl(itemAttachmentEntity.getId(), false); requestHelper.executePatchWithODataResponseAndAssertStatus( @@ -911,12 +882,12 @@ private void verifyNothingHasChangedInDraft( assertThat(selectedRootAfterDeletion.getItems().get(0).getAttachments().get(0).getContentId()) .isNotEmpty(); assertThat( - selectedRootAfterDeletion - .getItems() - .get(0) - .getAttachmentEntities() - .get(0) - .getContentId()) + selectedRootAfterDeletion + .getItems() + .get(0) + .getAttachmentEntities() + .get(0) + .getContentId()) .isNotEmpty(); } diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/MediaValidatedAttachmentsDraftTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/MediaValidatedAttachmentsDraftTest.java new file mode 100644 index 00000000..058d1cee --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/MediaValidatedAttachmentsDraftTest.java @@ -0,0 +1,145 @@ +package com.sap.cds.feature.attachments.integrationtests.draftservice; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.sap.cds.CdsData; +import com.sap.cds.Struct; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testdraftservice.DraftRoots; +import com.sap.cds.feature.attachments.integrationtests.common.MockHttpRequestHelper; +import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles(Profiles.TEST_HANDLER_DISABLED) +public class MediaValidatedAttachmentsDraftTest extends DraftOdataRequestValidationBase { + + private static final String BASE_URL = MockHttpRequestHelper.ODATA_BASE_URL + "TestDraftService/"; + private static final String BASE_ROOT_URL = BASE_URL + "DraftRoots"; + + @BeforeEach + void setup() { + requestHelper.setContentType( + MediaType.APPLICATION_JSON); + } + + @ParameterizedTest + @CsvSource({ + "test.png,201", + "test.jpeg,201", + "test.pdf,415", + "test.txt,415" + }) + void shouldValidateMediaType_whenCreatingAttachmentInDraft(String fileName, int expectedStatus) throws Exception { + + String rootId = createDraftRootAndReturnId(); + String metadata = """ + { + "fileName": "%s" + } + """.formatted(fileName); + + requestHelper.executePostWithMatcher( + buildDraftAttachmentCreationUrl(rootId), + metadata, + status().is(expectedStatus)); + } + + private String buildDraftAttachmentCreationUrl(String rootId) { + return BASE_ROOT_URL + + "(ID=" + rootId + ",IsActiveEntity=false)" + + "/mediaValidatedAttachments"; + } + + // Helper methods + private String createDraftRootAndReturnId() throws Exception { + CdsData response = requestHelper.executePostWithODataResponseAndAssertStatusCreated( + BASE_ROOT_URL, "{}"); + + DraftRoots draftRoot = Struct.access(response).as(DraftRoots.class); + + requestHelper.executePatchWithODataResponseAndAssertStatusOk( + getRootUrl(draftRoot.getId(), false), + """ + { + "title": "Draft with mediaValidatedAttachments" + } + """); + + return draftRoot.getId(); + } + + private String getRootUrl(String rootId, boolean isActiveEntity) { + return BASE_ROOT_URL + "(ID=" + rootId + ",IsActiveEntity=" + isActiveEntity + ")"; + } + + // Required abstract method implementations + @Override + protected void verifyContentId(String contentId, String attachmentId) { + assertThat(contentId).isEqualTo(attachmentId); + } + + @Override + protected void verifyContent(InputStream attachment, String testContent) throws IOException { + if (Objects.nonNull(testContent)) { + assertThat(attachment.readAllBytes()) + .isEqualTo(testContent.getBytes(java.nio.charset.StandardCharsets.UTF_8)); + } else { + assertThat(attachment).isNull(); + } + } + + @Override + protected void verifyNoAttachmentEventsCalled() { + // Implementation not required for this test + } + + @Override + protected void clearServiceHandlerContext() { + // Implementation not required for this test + } + + @Override + protected void verifyEventContextEmptyForEvent(String... events) { + // Implementation not required for this test + } + + @Override + protected void verifyOnlyTwoCreateEvents(String newAttachmentContent, String newAttachmentEntityContent) { + // Implementation not required for this test + } + + @Override + protected void verifyTwoCreateAndDeleteEvents(String newAttachmentContent, String newAttachmentEntityContent) { + // Implementation not required for this test + } + + @Override + protected void verifyTwoReadEvents() { + // Implementation not required for this test + } + + @Override + protected void verifyOnlyTwoDeleteEvents(String attachmentContentId, String attachmentEntityContentId) { + // Implementation not required for this test + } + + @Override + protected void verifyTwoUpdateEvents(String newAttachmentContent, String attachmentContentId, + String newAttachmentEntityContent, String attachmentEntityContentId) { + // Implementation not required for this test + } + + @Override + protected void verifyTwoCreateAndRevertedDeleteEvents() { + // Implementation not required for this test + } + +} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/SizeLimitedAttachmentsSizeValidationDraftTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/SizeLimitedAttachmentsSizeValidationDraftTest.java index bd332021..e71ad1b6 100644 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/SizeLimitedAttachmentsSizeValidationDraftTest.java +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/SizeLimitedAttachmentsSizeValidationDraftTest.java @@ -13,6 +13,7 @@ import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; import java.io.IOException; import java.io.InputStream; +import java.util.List; import java.util.Objects; import org.junit.jupiter.api.Test; import org.springframework.test.context.ActiveProfiles; @@ -28,7 +29,7 @@ void uploadContentWithin5MBLimitSucceeds() throws Exception { // Arrange: Create draft with sizeLimitedAttachments var draftRoot = createNewDraftWithSizeLimitedAttachments(); var attachment = draftRoot.getSizeLimitedAttachments().get(0); - + attachment.setFileName("test.txt"); // Act & Assert: Upload 3MB content (within limit) succeeds byte[] content = new byte[3 * 1024 * 1024]; // 3MB var url = buildDraftSizeLimitedAttachmentContentUrl(draftRoot.getId(), attachment.getId()); @@ -41,21 +42,22 @@ void uploadContentExceeding5MBLimitFails() throws Exception { // Arrange: Create draft with sizeLimitedAttachments var draftRoot = createNewDraftWithSizeLimitedAttachments(); var attachment = draftRoot.getSizeLimitedAttachments().get(0); - + attachment.setFileName("test.txt"); // Act: Try to upload 6MB content (exceeds limit) byte[] content = new byte[6 * 1024 * 1024]; // 6MB var url = buildDraftSizeLimitedAttachmentContentUrl(draftRoot.getId(), attachment.getId()); requestHelper.setContentType(org.springframework.http.MediaType.APPLICATION_OCTET_STREAM); requestHelper.executePutWithMatcher(url, content, status().is(413)); - // Assert: Error response with HTTP 413 status code indicates size limit exceeded + // Assert: Error response with HTTP 413 status code indicates size limit + // exceeded } // Helper methods private DraftRoots createNewDraftWithSizeLimitedAttachments() throws Exception { // Create new draft - var responseRootCdsData = - requestHelper.executePostWithODataResponseAndAssertStatusCreated(BASE_ROOT_URL, "{}"); + var responseRootCdsData = requestHelper.executePostWithODataResponseAndAssertStatusCreated(BASE_ROOT_URL, + "{}"); var draftRoot = Struct.access(responseRootCdsData).as(DraftRoots.class); // Update root with title @@ -68,13 +70,12 @@ private DraftRoots createNewDraftWithSizeLimitedAttachments() throws Exception { attachment.setFileName("testFile.txt"); attachment.setMimeType("text/plain"); var attachmentUrl = rootUrl + "/sizeLimitedAttachments"; - var responseAttachmentCdsData = - requestHelper.executePostWithODataResponseAndAssertStatusCreated( - attachmentUrl, attachment.toJson()); + var responseAttachmentCdsData = requestHelper.executePostWithODataResponseAndAssertStatusCreated( + attachmentUrl, attachment.toJson()); var createdAttachment = Struct.access(responseAttachmentCdsData).as(Attachments.class); // Build result with the attachment - draftRoot.setSizeLimitedAttachments(java.util.List.of(createdAttachment)); + draftRoot.setSizeLimitedAttachments(List.of(createdAttachment)); return draftRoot; } diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java new file mode 100644 index 00000000..564b7599 --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java @@ -0,0 +1,184 @@ +package com.sap.cds.feature.attachments.integrationtests.nondraftservice; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; + +import com.sap.cds.Result; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.sap.attachments.Attachments; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.AttachmentEntity; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.Roots; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.Roots_; +import com.sap.cds.feature.attachments.integrationtests.common.MockHttpRequestHelper; +import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; +import com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper.AttachmentsBuilder; +import com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper.RootEntityBuilder; +import com.sap.cds.ql.Select; + +@ActiveProfiles(Profiles.TEST_HANDLER_DISABLED) +class MediaValidatedAttachmentsNonDraftTest extends OdataRequestValidationBase { + private static final String BASE_URL = MockHttpRequestHelper.ODATA_BASE_URL + "TestService/Roots"; + private static final String MEDIA_VALIDATED_ATTACHMENTS = "mediaValidatedAttachments"; + + protected void postServiceRoot(Roots serviceRoot) throws Exception { + String url = MockHttpRequestHelper.ODATA_BASE_URL + "TestService/Roots"; + requestHelper.executePostWithMatcher(url, serviceRoot.toJson(), status().isCreated()); + } + + private Roots selectStoredRootWithMediaValidatedAttachments() { + Select select = Select.from( + Roots_.class) + .columns(r -> r._all(), r -> r.mediaValidatedAttachments().expand()); + + Result result = persistenceService.run(select); + return result.single(Roots.class); + } + + @BeforeEach + void setup() { + requestHelper.setContentType(MediaType.APPLICATION_JSON); + } + + @ParameterizedTest + @CsvSource({ + "image/jpeg,201", + "image/png,201", + "application/pdf,415", + "text/plain,415" + }) + void shouldValidateMediaTypes(String mediaType, int expectedStatus) throws Exception { + String rootId = createRootAndReturnId(); + String fileName = "test." + mediaType.split("/")[1]; + String attachmentMetadata = createAttachmentMetadata(fileName); + + requestHelper.executePostWithMatcher( + createUrl(rootId, MEDIA_VALIDATED_ATTACHMENTS), + attachmentMetadata, + status().is(expectedStatus)); + } + + @Test + void shouldRejectAttachment_whenFileNameIsEmpty() throws Exception { + String rootId = createRootAndReturnId(); + String fileName = ""; + String attachmentMetadata = createAttachmentMetadata(fileName); + + requestHelper.executePostWithMatcher( + createUrl(rootId, MEDIA_VALIDATED_ATTACHMENTS), + attachmentMetadata, + status().isBadRequest()); + } + + private String createRootAndReturnId() throws Exception { + // Build the initial Java object.. Root + Roots serviceRoot = buildServiceRootWithMediaValidatedAttachments(); + + // POST the root object to the server to create it in the database + postServiceRoot(serviceRoot); + + // Read the newly created entity back from the database + Roots selectedRoot = selectStoredRootWithMediaValidatedAttachments(); + + return selectedRoot.getId(); + } + + private String createUrl(String rootId, String path) { + return BASE_URL + "(" + rootId + ")/" + path; + } + + private String createAttachmentMetadata(String fileName) { + return String.format(""" + { + "fileName": "%s" + } + """, fileName); + } + + // helper method + private Roots buildServiceRootWithMediaValidatedAttachments() { + return RootEntityBuilder.create() + .setTitle("Root with mediaValidatedAttachments") + .addMediaValidatedAttachments( + AttachmentsBuilder.create().setFileName("parent.text") + .setMimeType(MediaType.APPLICATION_JSON_VALUE)) + .build(); + } + + // Override abstract methods from OdataRequestValidationBase + + @Override + protected void executeContentRequestAndValidateContent(String url, String content) throws Exception { + // Implementation not required for this test + } + + @Override + protected void verifyContentId(Attachments attachmentWithExpectedContent, String attachmentId, String contentId) { + // Implementation not required for this test + } + + @Override + protected void verifyContentAndContentId(Attachments attachment, String testContent, Attachments itemAttachment) { + // Implementation not required for this test + } + + @Override + protected void verifyContentAndContentIdForAttachmentEntity(AttachmentEntity attachment, String testContent, + AttachmentEntity itemAttachment) { + // Implementation not required for this test + } + + @Override + public void verifySingleCreateAndUpdateEvent(String arg1, String arg2, String arg3) { + // Implementation not required for this test + } + + @Override + public void clearServiceHandlerContext() { + // Implementation not required for this test + } + + @Override + public void verifySingleReadEvent(String arg) { + // Implementation not required for this test + } + + @Override + public void verifyTwoDeleteEvents(AttachmentEntity entity, Attachments attachments) { + // Implementation not required for this test + } + + @Override + public void clearServiceHandlerDocuments() { + // Implementation not required for this test + } + + @Override + public void verifyEventContextEmptyForEvent(String... args) { + // Implementation not required for this test + } + + @Override + public void verifyNoAttachmentEventsCalled() { + // Implementation not required for this test + } + + @Override + public void verifyNumberOfEvents(String arg, int count) { + // Implementation not required for this test + } + + @Override + public void verifySingleCreateEvent(String arg1, String arg2) { + // Implementation not required for this test + } + + @Override + public void verifySingleDeletionEvent(String arg) { + // Implementation not required for this test + } +} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/RootEntityBuilder.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/RootEntityBuilder.java index cf91a423..f6dc5179 100644 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/RootEntityBuilder.java +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/RootEntityBuilder.java @@ -15,6 +15,8 @@ private RootEntityBuilder() { rootEntity = Roots.create(); rootEntity.setAttachments(new ArrayList<>()); rootEntity.setItems(new ArrayList<>()); + rootEntity.setSizeLimitedAttachments(new ArrayList<>()); + rootEntity.setMediaValidatedAttachments(new ArrayList<>()); } public static RootEntityBuilder create() { @@ -33,14 +35,17 @@ public RootEntityBuilder addAttachments(AttachmentsEntityBuilder... attachments) } public RootEntityBuilder addSizeLimitedAttachments(AttachmentsBuilder... attachments) { - if (rootEntity.getSizeLimitedAttachments() == null) { - rootEntity.setSizeLimitedAttachments(new ArrayList<>()); - } Arrays.stream(attachments) .forEach(attachment -> rootEntity.getSizeLimitedAttachments().add(attachment.build())); return this; } + public RootEntityBuilder addMediaValidatedAttachments(AttachmentsBuilder... attachments) { + Arrays.stream(attachments) + .forEach(attachment -> rootEntity.getMediaValidatedAttachments().add(attachment.build())); + return this; + } + public RootEntityBuilder addItems(ItemEntityBuilder... items) { Arrays.stream(items).forEach(item -> rootEntity.getItems().add(item.build())); return this; diff --git a/integration-tests/srv/test-service.cds b/integration-tests/srv/test-service.cds index e4974ac0..c07ddaed 100644 --- a/integration-tests/srv/test-service.cds +++ b/integration-tests/srv/test-service.cds @@ -4,6 +4,14 @@ annotate db.Roots.sizeLimitedAttachments with { content @Validation.Maximum: '5MB'; }; +// Media type validation for attachments - for testing purposes. +annotate db.Roots.mediaValidatedAttachments with { + content @(Core.AcceptableMediaTypes: [ + 'image/jpeg', + 'image/png' + ]); +} + service TestService { entity Roots as projection on db.Roots; entity AttachmentEntity as projection on db.AttachmentEntity; From e596bb26a864675e6763062e3dd5f7e6c239d76c Mon Sep 17 00:00:00 2001 From: samyuktaprabhu <33195453+samyuktaprabhu@users.noreply.github.com> Date: Tue, 17 Feb 2026 10:08:37 +0100 Subject: [PATCH 08/17] add more guard checks & tests --- .../helper/AttachmentValidationHelper.java | 150 ++++++++++-------- .../common/ApplicationHandlerHelper.java | 3 + .../AttachmentValidationHelperTest.java | 43 +++++ .../common/ApplicationHandlerHelperTest.java | 112 +++++++------ integration-tests/db/data-model.cds | 2 +- ...MediaValidatedAttachmentsNonDraftTest.java | 13 +- 6 files changed, 200 insertions(+), 123 deletions(-) diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelper.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelper.java index 2efa445e..2c1b5258 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelper.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelper.java @@ -15,81 +15,83 @@ public class AttachmentValidationHelper { public static final String DEFAULT_MEDIA_TYPE = "application/octet-stream"; - public static final Map EXT_TO_MEDIA_TYPE = - Map.ofEntries( - Map.entry("aac", "audio/aac"), - Map.entry("abw", "application/x-abiword"), - Map.entry("arc", "application/octet-stream"), - Map.entry("avi", "video/x-msvideo"), - Map.entry("azw", "application/vnd.amazon.ebook"), - Map.entry("bin", "application/octet-stream"), - Map.entry("png", "image/png"), - Map.entry("gif", "image/gif"), - Map.entry("bmp", "image/bmp"), - Map.entry("bz", "application/x-bzip"), - Map.entry("bz2", "application/x-bzip2"), - Map.entry("csh", "application/x-csh"), - Map.entry("css", "text/css"), - Map.entry("csv", "text/csv"), - Map.entry("doc", "application/msword"), - Map.entry( - "docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"), - Map.entry("odp", "application/vnd.oasis.opendocument.presentation"), - Map.entry("ods", "application/vnd.oasis.opendocument.spreadsheet"), - Map.entry("odt", "application/vnd.oasis.opendocument.text"), - Map.entry("epub", "application/epub+zip"), - Map.entry("gz", "application/gzip"), - Map.entry("htm", "text/html"), - Map.entry("html", "text/html"), - Map.entry("ico", "image/x-icon"), - Map.entry("ics", "text/calendar"), - Map.entry("jar", "application/java-archive"), - Map.entry("jpg", "image/jpeg"), - Map.entry("jpeg", "image/jpeg"), - Map.entry("js", "text/javascript"), - Map.entry("json", "application/json"), - Map.entry("mid", "audio/midi"), - Map.entry("midi", "audio/midi"), - Map.entry("mjs", "text/javascript"), - Map.entry("mov", "video/quicktime"), - Map.entry("mp3", "audio/mpeg"), - Map.entry("mp4", "video/mp4"), - Map.entry("mpeg", "video/mpeg"), - Map.entry("mpkg", "application/vnd.apple.installer+xml"), - Map.entry("otf", "font/otf"), - Map.entry("pdf", "application/pdf"), - Map.entry("ppt", "application/vnd.ms-powerpoint"), - Map.entry( - "pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"), - Map.entry("rar", "application/x-rar-compressed"), - Map.entry("rtf", "application/rtf"), - Map.entry("svg", "image/svg+xml"), - Map.entry("tar", "application/x-tar"), - Map.entry("tif", "image/tiff"), - Map.entry("tiff", "image/tiff"), - Map.entry("ttf", "font/ttf"), - Map.entry("vsd", "application/vnd.visio"), - Map.entry("wav", "audio/wav"), - Map.entry("woff", "font/woff"), - Map.entry("woff2", "font/woff2"), - Map.entry("xhtml", "application/xhtml+xml"), - Map.entry("xls", "application/vnd.ms-excel"), - Map.entry("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"), - Map.entry("xml", "application/xml"), - Map.entry("zip", "application/zip"), - Map.entry("txt", "text/plain"), - Map.entry("webp", "image/webp")); + public static final Map EXT_TO_MEDIA_TYPE = Map.ofEntries( + Map.entry("aac", "audio/aac"), + Map.entry("abw", "application/x-abiword"), + Map.entry("arc", "application/octet-stream"), + Map.entry("avi", "video/x-msvideo"), + Map.entry("azw", "application/vnd.amazon.ebook"), + Map.entry("bin", "application/octet-stream"), + Map.entry("png", "image/png"), + Map.entry("gif", "image/gif"), + Map.entry("bmp", "image/bmp"), + Map.entry("bz", "application/x-bzip"), + Map.entry("bz2", "application/x-bzip2"), + Map.entry("csh", "application/x-csh"), + Map.entry("css", "text/css"), + Map.entry("csv", "text/csv"), + Map.entry("doc", "application/msword"), + Map.entry( + "docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"), + Map.entry("odp", "application/vnd.oasis.opendocument.presentation"), + Map.entry("ods", "application/vnd.oasis.opendocument.spreadsheet"), + Map.entry("odt", "application/vnd.oasis.opendocument.text"), + Map.entry("epub", "application/epub+zip"), + Map.entry("gz", "application/gzip"), + Map.entry("htm", "text/html"), + Map.entry("html", "text/html"), + Map.entry("ico", "image/x-icon"), + Map.entry("ics", "text/calendar"), + Map.entry("jar", "application/java-archive"), + Map.entry("jpg", "image/jpeg"), + Map.entry("jpeg", "image/jpeg"), + Map.entry("js", "text/javascript"), + Map.entry("json", "application/json"), + Map.entry("mid", "audio/midi"), + Map.entry("midi", "audio/midi"), + Map.entry("mjs", "text/javascript"), + Map.entry("mov", "video/quicktime"), + Map.entry("mp3", "audio/mpeg"), + Map.entry("mp4", "video/mp4"), + Map.entry("mpeg", "video/mpeg"), + Map.entry("mpkg", "application/vnd.apple.installer+xml"), + Map.entry("otf", "font/otf"), + Map.entry("pdf", "application/pdf"), + Map.entry("ppt", "application/vnd.ms-powerpoint"), + Map.entry( + "pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"), + Map.entry("rar", "application/x-rar-compressed"), + Map.entry("rtf", "application/rtf"), + Map.entry("svg", "image/svg+xml"), + Map.entry("tar", "application/x-tar"), + Map.entry("tif", "image/tiff"), + Map.entry("tiff", "image/tiff"), + Map.entry("ttf", "font/ttf"), + Map.entry("vsd", "application/vnd.visio"), + Map.entry("wav", "audio/wav"), + Map.entry("woff", "font/woff"), + Map.entry("woff2", "font/woff2"), + Map.entry("xhtml", "application/xhtml+xml"), + Map.entry("xls", "application/vnd.ms-excel"), + Map.entry("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"), + Map.entry("xml", "application/xml"), + Map.entry("zip", "application/zip"), + Map.entry("txt", "text/plain"), + Map.entry("webp", "image/webp")); private static final Logger logger = LoggerFactory.getLogger(AttachmentValidationHelper.class); /** - * Validates the file name and resolves its media type. Ensures that the detected media type is + * Validates the file name and resolves its media type. Ensures that the + * detected media type is * part of the list of acceptable media types. * - * @param fileName the name of the attachment file - * @param acceptableMediaTypes list of allowed media types (e.g. "image/*", "application/pdf") + * @param fileName the name of the attachment file + * @param acceptableMediaTypes list of allowed media types (e.g. "image/*", + * "application/pdf") * @return the detected media type - * @throws ServiceException if the file name is invalid or the media type is not allowed + * @throws ServiceException if the file name is invalid or the media type is not + * allowed */ public static String validateMediaTypeForAttachment( String fileName, List acceptableMediaTypes) { @@ -100,6 +102,10 @@ public static String validateMediaTypeForAttachment( } private static void validateFileName(String fileName) { + if (fileName == null || fileName.isBlank()) { + throw new ServiceException( + ErrorStatuses.UNSUPPORTED_MEDIA_TYPE, "Filename must not be null or blank"); + } String clean = fileName.trim(); int lastDotIndex = clean.lastIndexOf('.'); if (lastDotIndex <= 0 || lastDotIndex == clean.length() - 1) { @@ -137,11 +143,15 @@ private static String resolveMimeType(String fileName) { return actualMimeType; } - private static boolean checkMimeTypeMatch( + protected static boolean checkMimeTypeMatch( Collection acceptableMediaTypes, String mimeType) { + if (mimeType == null) { + return false; // forces UNSUPPORTED_MEDIA_TYPE + } if (acceptableMediaTypes == null || acceptableMediaTypes.isEmpty() - || acceptableMediaTypes.contains("*/*")) return true; + || acceptableMediaTypes.contains("*/*")) + return true; String baseMimeType = mimeType.trim().toLowerCase(); diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelper.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelper.java index c72ec754..f70c671a 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelper.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelper.java @@ -148,6 +148,9 @@ public static Map removeDraftKey(Map keys) { */ public static void validateAcceptableMediaTypes( CdsEntity entity, List data, CdsRuntime cdsRuntime) { + if (entity == null) { + return; + } CdsModel cdsModel = cdsRuntime.getCdsModel(); CdsEntity serviceEntity = cdsModel.findEntity(entity.getQualifiedName()).orElse(null); if (serviceEntity == null || !isMediaEntity(serviceEntity)) { diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelperTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelperTest.java index c738bff5..ff550f83 100644 --- a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelperTest.java +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelperTest.java @@ -117,6 +117,49 @@ void shouldThrowWhenDefaultMimeTypeNotAllowed() { assertTrue(ex.getMessage().contains("not allowed")); } + @Test + void shouldThrowWhenFileNameIsNull() { + ServiceException ex = assertThrows( + ServiceException.class, + () -> AttachmentValidationHelper.validateMediaTypeForAttachment( + null, + List.of("application/pdf"))); + + assertEquals(ErrorStatuses.UNSUPPORTED_MEDIA_TYPE, ex.getErrorStatus()); + assertTrue(ex.getMessage().contains("must not be null or blank")); + } + + @Test + void shouldThrowWhenFileNameIsEmpty() { + ServiceException ex = assertThrows( + ServiceException.class, + () -> AttachmentValidationHelper.validateMediaTypeForAttachment( + "", + List.of("application/pdf"))); + + assertEquals(ErrorStatuses.UNSUPPORTED_MEDIA_TYPE, ex.getErrorStatus()); + } + + @Test + void shouldThrowWhenFileNameIsBlank() { + ServiceException ex = assertThrows( + ServiceException.class, + () -> AttachmentValidationHelper.validateMediaTypeForAttachment( + " ", + List.of("application/pdf"))); + + assertEquals(ErrorStatuses.UNSUPPORTED_MEDIA_TYPE, ex.getErrorStatus()); + } + + @Test + void shouldReturnFalseWhenMimeTypeIsNull() { + boolean result = AttachmentValidationHelper.checkMimeTypeMatch( + List.of("image/png"), + null); + + assertFalse(result); + } + private void assertMediaType( String fileName, List allowed, diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelperTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelperTest.java index a81dd9b0..16e081d4 100644 --- a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelperTest.java +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelperTest.java @@ -28,14 +28,22 @@ class ApplicationHandlerHelperTest { - @Mock private CdsEntity entity; - @Mock private CdsData data; - @Mock private CdsRuntime cdsRuntime; - @Mock private CdsModel cdsModel; - @Mock private CdsEntity serviceEntity; - @Mock private CdsElement cdsElement; - @Mock private CdsAnnotation annotation; - @Mock private CdsDataProcessor processor; + @Mock + private CdsEntity entity; + @Mock + private CdsData data; + @Mock + private CdsRuntime cdsRuntime; + @Mock + private CdsModel cdsModel; + @Mock + private CdsEntity serviceEntity; + @Mock + private CdsElement cdsElement; + @Mock + private CdsAnnotation annotation; + @Mock + private CdsDataProcessor processor; @BeforeEach void setUp() { @@ -88,6 +96,21 @@ void removeDraftKey() { assertTrue(result.containsKey("key1")); } + @Test + void validateAcceptableMediaTypes_shouldReturnWhenEntityIsNull() { + // given + CdsRuntime runtime = mock(CdsRuntime.class); + + // when / then + assertDoesNotThrow(() -> ApplicationHandlerHelper.validateAcceptableMediaTypes( + null, + List.of(), + runtime)); + + // ensure no further interaction happens + verifyNoInteractions(runtime); + } + @Test void getEntityAcceptableMediaTypes_returnsAnnotationValue() { List expectedTypes = List.of("image/png", "application/pdf"); @@ -121,16 +144,16 @@ void extractFileName_whenFileNamePresent_returnsValue() { try (MockedStatic mocked = mockStatic(CdsDataProcessor.class)) { mocked.when(CdsDataProcessor::create).thenReturn(processor); doAnswer( - invocation -> { - Filter filter = invocation.getArgument(0); - Validator validator = invocation.getArgument(1); - - // Simulate processor visiting fileName with String value - if (filter.test(null, cdsElement, null)) { - validator.validate(null, cdsElement, "test.pdf"); - } - return processor; - }) + invocation -> { + Filter filter = invocation.getArgument(0); + Validator validator = invocation.getArgument(1); + + // Simulate processor visiting fileName with String value + if (filter.test(null, cdsElement, null)) { + validator.validate(null, cdsElement, "test.pdf"); + } + return processor; + }) .when(processor) .addValidator(any(), any()); @@ -146,14 +169,14 @@ void extractFileName_whenElementIsNotFileName_throws() { try (MockedStatic mocked = mockStatic(CdsDataProcessor.class)) { mocked.when(CdsDataProcessor::create).thenReturn(processor); doAnswer( - invocation -> { - Filter filter = invocation.getArgument(0); - Validator validator = invocation.getArgument(1); - if (filter.test(null, cdsElement, null)) { - validator.validate(null, cdsElement, "test.pdf"); - } - return processor; - }) + invocation -> { + Filter filter = invocation.getArgument(0); + Validator validator = invocation.getArgument(1); + if (filter.test(null, cdsElement, null)) { + validator.validate(null, cdsElement, "test.pdf"); + } + return processor; + }) .when(processor) .addValidator(any(), any()); @@ -170,14 +193,14 @@ void extractFileName_valueIsNotString_branchCovered() { try (MockedStatic mocked = mockStatic(CdsDataProcessor.class)) { mocked.when(CdsDataProcessor::create).thenReturn(processor); doAnswer( - invocation -> { - Filter filter = invocation.getArgument(0); - Validator validator = invocation.getArgument(1); - if (filter.test(null, cdsElement, null)) { - validator.validate(null, cdsElement, 42); // non-String - } - return processor; - }) + invocation -> { + Filter filter = invocation.getArgument(0); + Validator validator = invocation.getArgument(1); + if (filter.test(null, cdsElement, null)) { + validator.validate(null, cdsElement, 42); // non-String + } + return processor; + }) .when(processor) .addValidator(any(), any()); doNothing().when(processor).process(anyList(), any()); @@ -203,8 +226,7 @@ void validateAcceptableMediaTypes_notMediaEntity_doesNotCallValidation() { when(cdsModel.findEntity("TestService.Roots")).thenReturn(Optional.of(serviceEntity)); when(serviceEntity.getAnnotationValue("_is_media_data", false)).thenReturn(false); - try (MockedStatic mocked = - mockStatic(AttachmentValidationHelper.class)) { + try (MockedStatic mocked = mockStatic(AttachmentValidationHelper.class)) { ApplicationHandlerHelper.validateAcceptableMediaTypes(entity, List.of(), cdsRuntime); mocked.verifyNoInteractions(); } @@ -222,8 +244,8 @@ void shouldNotThrowWhenEntityNotFoundInModel() { @Test void validateAcceptableMediaTypes_mediaTypeMatches_succeeds() { setupMediaEntity(List.of("image/png")); - try (MockedStatic helperStatic = - mockStatic(ApplicationHandlerHelper.class, CALLS_REAL_METHODS)) { + try (MockedStatic helperStatic = mockStatic(ApplicationHandlerHelper.class, + CALLS_REAL_METHODS)) { // file.png → media type image/png → allowed helperStatic @@ -231,17 +253,16 @@ void validateAcceptableMediaTypes_mediaTypeMatches_succeeds() { .thenReturn("file.png"); assertDoesNotThrow( - () -> - ApplicationHandlerHelper.validateAcceptableMediaTypes( - entity, List.of(data), cdsRuntime)); + () -> ApplicationHandlerHelper.validateAcceptableMediaTypes( + entity, List.of(data), cdsRuntime)); } } @Test void validateAcceptableMediaTypes_mediaTypeMismatch_throws() { setupMediaEntity(List.of("image/png")); - try (MockedStatic helperStatic = - mockStatic(ApplicationHandlerHelper.class, CALLS_REAL_METHODS)) { + try (MockedStatic helperStatic = mockStatic(ApplicationHandlerHelper.class, + CALLS_REAL_METHODS)) { // file.jpg → media type image/jpeg → NOT allowed helperStatic @@ -250,9 +271,8 @@ void validateAcceptableMediaTypes_mediaTypeMismatch_throws() { assertThrows( ServiceException.class, - () -> - ApplicationHandlerHelper.validateAcceptableMediaTypes( - entity, List.of(data), cdsRuntime)); + () -> ApplicationHandlerHelper.validateAcceptableMediaTypes( + entity, List.of(data), cdsRuntime)); } } diff --git a/integration-tests/db/data-model.cds b/integration-tests/db/data-model.cds index 9837be0a..5073824b 100644 --- a/integration-tests/db/data-model.cds +++ b/integration-tests/db/data-model.cds @@ -1,7 +1,7 @@ namespace test.data.model; using {cuid} from '@sap/cds/common'; -using {sap.attachments.Attachments} from`com.sap.cds/cds-feature-attachments`; +using {sap.attachments.Attachments} from 'com.sap.cds/cds-feature-attachments'; entity AttachmentEntity : Attachments { parentKey : UUID; diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java index 564b7599..5c7fecf5 100644 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java @@ -46,14 +46,15 @@ void setup() { @ParameterizedTest @CsvSource({ - "image/jpeg,201", - "image/png,201", - "application/pdf,415", - "text/plain,415" + "image.jpg,image/jpeg,201", + "image.png,image/png,201", + "document.pdf,application/pdf,415", + "notes.txt,text/plain,415" }) - void shouldValidateMediaTypes(String mediaType, int expectedStatus) throws Exception { + void shouldValidateMediaTypes(String fileName, + String mediaType, + int expectedStatus) throws Exception { String rootId = createRootAndReturnId(); - String fileName = "test." + mediaType.split("/")[1]; String attachmentMetadata = createAttachmentMetadata(fileName); requestHelper.executePostWithMatcher( From 9c29f79b500928adccf19fa314fca9c2fd7a2cb3 Mon Sep 17 00:00:00 2001 From: samyuktaprabhu <33195453+samyuktaprabhu@users.noreply.github.com> Date: Tue, 17 Feb 2026 10:12:04 +0100 Subject: [PATCH 09/17] fix file formatting --- .../helper/AttachmentValidationHelper.java | 141 +++++++++--------- .../AttachmentValidationHelperTest.java | 101 ++++++------- .../common/ApplicationHandlerHelperTest.java | 103 ++++++------- 3 files changed, 160 insertions(+), 185 deletions(-) diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelper.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelper.java index 2c1b5258..0bf5c166 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelper.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelper.java @@ -15,83 +15,81 @@ public class AttachmentValidationHelper { public static final String DEFAULT_MEDIA_TYPE = "application/octet-stream"; - public static final Map EXT_TO_MEDIA_TYPE = Map.ofEntries( - Map.entry("aac", "audio/aac"), - Map.entry("abw", "application/x-abiword"), - Map.entry("arc", "application/octet-stream"), - Map.entry("avi", "video/x-msvideo"), - Map.entry("azw", "application/vnd.amazon.ebook"), - Map.entry("bin", "application/octet-stream"), - Map.entry("png", "image/png"), - Map.entry("gif", "image/gif"), - Map.entry("bmp", "image/bmp"), - Map.entry("bz", "application/x-bzip"), - Map.entry("bz2", "application/x-bzip2"), - Map.entry("csh", "application/x-csh"), - Map.entry("css", "text/css"), - Map.entry("csv", "text/csv"), - Map.entry("doc", "application/msword"), - Map.entry( - "docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"), - Map.entry("odp", "application/vnd.oasis.opendocument.presentation"), - Map.entry("ods", "application/vnd.oasis.opendocument.spreadsheet"), - Map.entry("odt", "application/vnd.oasis.opendocument.text"), - Map.entry("epub", "application/epub+zip"), - Map.entry("gz", "application/gzip"), - Map.entry("htm", "text/html"), - Map.entry("html", "text/html"), - Map.entry("ico", "image/x-icon"), - Map.entry("ics", "text/calendar"), - Map.entry("jar", "application/java-archive"), - Map.entry("jpg", "image/jpeg"), - Map.entry("jpeg", "image/jpeg"), - Map.entry("js", "text/javascript"), - Map.entry("json", "application/json"), - Map.entry("mid", "audio/midi"), - Map.entry("midi", "audio/midi"), - Map.entry("mjs", "text/javascript"), - Map.entry("mov", "video/quicktime"), - Map.entry("mp3", "audio/mpeg"), - Map.entry("mp4", "video/mp4"), - Map.entry("mpeg", "video/mpeg"), - Map.entry("mpkg", "application/vnd.apple.installer+xml"), - Map.entry("otf", "font/otf"), - Map.entry("pdf", "application/pdf"), - Map.entry("ppt", "application/vnd.ms-powerpoint"), - Map.entry( - "pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"), - Map.entry("rar", "application/x-rar-compressed"), - Map.entry("rtf", "application/rtf"), - Map.entry("svg", "image/svg+xml"), - Map.entry("tar", "application/x-tar"), - Map.entry("tif", "image/tiff"), - Map.entry("tiff", "image/tiff"), - Map.entry("ttf", "font/ttf"), - Map.entry("vsd", "application/vnd.visio"), - Map.entry("wav", "audio/wav"), - Map.entry("woff", "font/woff"), - Map.entry("woff2", "font/woff2"), - Map.entry("xhtml", "application/xhtml+xml"), - Map.entry("xls", "application/vnd.ms-excel"), - Map.entry("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"), - Map.entry("xml", "application/xml"), - Map.entry("zip", "application/zip"), - Map.entry("txt", "text/plain"), - Map.entry("webp", "image/webp")); + public static final Map EXT_TO_MEDIA_TYPE = + Map.ofEntries( + Map.entry("aac", "audio/aac"), + Map.entry("abw", "application/x-abiword"), + Map.entry("arc", "application/octet-stream"), + Map.entry("avi", "video/x-msvideo"), + Map.entry("azw", "application/vnd.amazon.ebook"), + Map.entry("bin", "application/octet-stream"), + Map.entry("png", "image/png"), + Map.entry("gif", "image/gif"), + Map.entry("bmp", "image/bmp"), + Map.entry("bz", "application/x-bzip"), + Map.entry("bz2", "application/x-bzip2"), + Map.entry("csh", "application/x-csh"), + Map.entry("css", "text/css"), + Map.entry("csv", "text/csv"), + Map.entry("doc", "application/msword"), + Map.entry( + "docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"), + Map.entry("odp", "application/vnd.oasis.opendocument.presentation"), + Map.entry("ods", "application/vnd.oasis.opendocument.spreadsheet"), + Map.entry("odt", "application/vnd.oasis.opendocument.text"), + Map.entry("epub", "application/epub+zip"), + Map.entry("gz", "application/gzip"), + Map.entry("htm", "text/html"), + Map.entry("html", "text/html"), + Map.entry("ico", "image/x-icon"), + Map.entry("ics", "text/calendar"), + Map.entry("jar", "application/java-archive"), + Map.entry("jpg", "image/jpeg"), + Map.entry("jpeg", "image/jpeg"), + Map.entry("js", "text/javascript"), + Map.entry("json", "application/json"), + Map.entry("mid", "audio/midi"), + Map.entry("midi", "audio/midi"), + Map.entry("mjs", "text/javascript"), + Map.entry("mov", "video/quicktime"), + Map.entry("mp3", "audio/mpeg"), + Map.entry("mp4", "video/mp4"), + Map.entry("mpeg", "video/mpeg"), + Map.entry("mpkg", "application/vnd.apple.installer+xml"), + Map.entry("otf", "font/otf"), + Map.entry("pdf", "application/pdf"), + Map.entry("ppt", "application/vnd.ms-powerpoint"), + Map.entry( + "pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"), + Map.entry("rar", "application/x-rar-compressed"), + Map.entry("rtf", "application/rtf"), + Map.entry("svg", "image/svg+xml"), + Map.entry("tar", "application/x-tar"), + Map.entry("tif", "image/tiff"), + Map.entry("tiff", "image/tiff"), + Map.entry("ttf", "font/ttf"), + Map.entry("vsd", "application/vnd.visio"), + Map.entry("wav", "audio/wav"), + Map.entry("woff", "font/woff"), + Map.entry("woff2", "font/woff2"), + Map.entry("xhtml", "application/xhtml+xml"), + Map.entry("xls", "application/vnd.ms-excel"), + Map.entry("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"), + Map.entry("xml", "application/xml"), + Map.entry("zip", "application/zip"), + Map.entry("txt", "text/plain"), + Map.entry("webp", "image/webp")); private static final Logger logger = LoggerFactory.getLogger(AttachmentValidationHelper.class); /** - * Validates the file name and resolves its media type. Ensures that the - * detected media type is + * Validates the file name and resolves its media type. Ensures that the detected media type is * part of the list of acceptable media types. * - * @param fileName the name of the attachment file - * @param acceptableMediaTypes list of allowed media types (e.g. "image/*", - * "application/pdf") + * @param fileName the name of the attachment file + * @param acceptableMediaTypes list of allowed media types (e.g. "image/*", "application/pdf") * @return the detected media type - * @throws ServiceException if the file name is invalid or the media type is not - * allowed + * @throws ServiceException if the file name is invalid or the media type is not allowed */ public static String validateMediaTypeForAttachment( String fileName, List acceptableMediaTypes) { @@ -150,8 +148,7 @@ protected static boolean checkMimeTypeMatch( } if (acceptableMediaTypes == null || acceptableMediaTypes.isEmpty() - || acceptableMediaTypes.contains("*/*")) - return true; + || acceptableMediaTypes.contains("*/*")) return true; String baseMimeType = mimeType.trim().toLowerCase(); diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelperTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelperTest.java index ff550f83..4fc333d8 100644 --- a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelperTest.java +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelperTest.java @@ -40,25 +40,17 @@ void shouldAllowAllWithStarSlashStar() { @Test void shouldAllowUnknownExtensionWithStarSlashStar() { assertMediaType( - "anyfile.anyext", - List.of("*/*"), - AttachmentValidationHelper.DEFAULT_MEDIA_TYPE); + "anyfile.anyext", List.of("*/*"), AttachmentValidationHelper.DEFAULT_MEDIA_TYPE); } @Test void shouldAllowUnknownExtensionWhenAcceptableTypesNull() { - assertMediaType( - "anyfile.anyext", - null, - AttachmentValidationHelper.DEFAULT_MEDIA_TYPE); + assertMediaType("anyfile.anyext", null, AttachmentValidationHelper.DEFAULT_MEDIA_TYPE); } @Test void shouldAllowAllWhenAcceptableTypesEmpty() { - assertMediaType( - "anyfile.anyext", - List.of(), - AttachmentValidationHelper.DEFAULT_MEDIA_TYPE); + assertMediaType("anyfile.anyext", List.of(), AttachmentValidationHelper.DEFAULT_MEDIA_TYPE); } @Test @@ -76,54 +68,51 @@ void shouldHandleUppercaseExtension() { @Test void shouldRejectUnknownExtensionWhenNotAllowed() { - assertUnsupported( - "anyfile.anyext", - List.of("application/pdf")); + assertUnsupported("anyfile.anyext", List.of("application/pdf")); } @Test void shouldThrowWhenFilenameHasNoExtension() { - assertUnsupported( - "invalidfilename", - List.of("application/pdf")); + assertUnsupported("invalidfilename", List.of("application/pdf")); } @Test void shouldThrowWhenFilenameEndsWithDot() { - assertUnsupported( - "file.", - List.of("application/pdf")); + assertUnsupported("file.", List.of("application/pdf")); } @Test void shouldThrowWhenMimeTypeNotAllowed() { - ServiceException ex = assertThrows( - ServiceException.class, - () -> AttachmentValidationHelper.validateMediaTypeForAttachment( - "document.pdf", - List.of("image/png"))); + ServiceException ex = + assertThrows( + ServiceException.class, + () -> + AttachmentValidationHelper.validateMediaTypeForAttachment( + "document.pdf", List.of("image/png"))); assertTrue(ex.getMessage().contains("not allowed")); } @Test void shouldThrowWhenDefaultMimeTypeNotAllowed() { - ServiceException ex = assertThrows( - ServiceException.class, - () -> AttachmentValidationHelper.validateMediaTypeForAttachment( - "file.unknownext", - List.of("application/pdf"))); + ServiceException ex = + assertThrows( + ServiceException.class, + () -> + AttachmentValidationHelper.validateMediaTypeForAttachment( + "file.unknownext", List.of("application/pdf"))); assertTrue(ex.getMessage().contains("not allowed")); } @Test void shouldThrowWhenFileNameIsNull() { - ServiceException ex = assertThrows( - ServiceException.class, - () -> AttachmentValidationHelper.validateMediaTypeForAttachment( - null, - List.of("application/pdf"))); + ServiceException ex = + assertThrows( + ServiceException.class, + () -> + AttachmentValidationHelper.validateMediaTypeForAttachment( + null, List.of("application/pdf"))); assertEquals(ErrorStatuses.UNSUPPORTED_MEDIA_TYPE, ex.getErrorStatus()); assertTrue(ex.getMessage().contains("must not be null or blank")); @@ -131,52 +120,48 @@ void shouldThrowWhenFileNameIsNull() { @Test void shouldThrowWhenFileNameIsEmpty() { - ServiceException ex = assertThrows( - ServiceException.class, - () -> AttachmentValidationHelper.validateMediaTypeForAttachment( - "", - List.of("application/pdf"))); + ServiceException ex = + assertThrows( + ServiceException.class, + () -> + AttachmentValidationHelper.validateMediaTypeForAttachment( + "", List.of("application/pdf"))); assertEquals(ErrorStatuses.UNSUPPORTED_MEDIA_TYPE, ex.getErrorStatus()); } @Test void shouldThrowWhenFileNameIsBlank() { - ServiceException ex = assertThrows( - ServiceException.class, - () -> AttachmentValidationHelper.validateMediaTypeForAttachment( - " ", - List.of("application/pdf"))); + ServiceException ex = + assertThrows( + ServiceException.class, + () -> + AttachmentValidationHelper.validateMediaTypeForAttachment( + " ", List.of("application/pdf"))); assertEquals(ErrorStatuses.UNSUPPORTED_MEDIA_TYPE, ex.getErrorStatus()); } @Test void shouldReturnFalseWhenMimeTypeIsNull() { - boolean result = AttachmentValidationHelper.checkMimeTypeMatch( - List.of("image/png"), - null); + boolean result = AttachmentValidationHelper.checkMimeTypeMatch(List.of("image/png"), null); assertFalse(result); } - private void assertMediaType( - String fileName, - List allowed, - String expectedType) { + private void assertMediaType(String fileName, List allowed, String expectedType) { String result = AttachmentValidationHelper.validateMediaTypeForAttachment(fileName, allowed); assertEquals(expectedType, result); } - private void assertUnsupported( - String fileName, - List allowed) { + private void assertUnsupported(String fileName, List allowed) { - ServiceException ex = assertThrows( - ServiceException.class, - () -> AttachmentValidationHelper.validateMediaTypeForAttachment(fileName, allowed)); + ServiceException ex = + assertThrows( + ServiceException.class, + () -> AttachmentValidationHelper.validateMediaTypeForAttachment(fileName, allowed)); assertEquals(ErrorStatuses.UNSUPPORTED_MEDIA_TYPE, ex.getErrorStatus()); } diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelperTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelperTest.java index 16e081d4..4b6e1684 100644 --- a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelperTest.java +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelperTest.java @@ -28,22 +28,14 @@ class ApplicationHandlerHelperTest { - @Mock - private CdsEntity entity; - @Mock - private CdsData data; - @Mock - private CdsRuntime cdsRuntime; - @Mock - private CdsModel cdsModel; - @Mock - private CdsEntity serviceEntity; - @Mock - private CdsElement cdsElement; - @Mock - private CdsAnnotation annotation; - @Mock - private CdsDataProcessor processor; + @Mock private CdsEntity entity; + @Mock private CdsData data; + @Mock private CdsRuntime cdsRuntime; + @Mock private CdsModel cdsModel; + @Mock private CdsEntity serviceEntity; + @Mock private CdsElement cdsElement; + @Mock private CdsAnnotation annotation; + @Mock private CdsDataProcessor processor; @BeforeEach void setUp() { @@ -102,10 +94,8 @@ void validateAcceptableMediaTypes_shouldReturnWhenEntityIsNull() { CdsRuntime runtime = mock(CdsRuntime.class); // when / then - assertDoesNotThrow(() -> ApplicationHandlerHelper.validateAcceptableMediaTypes( - null, - List.of(), - runtime)); + assertDoesNotThrow( + () -> ApplicationHandlerHelper.validateAcceptableMediaTypes(null, List.of(), runtime)); // ensure no further interaction happens verifyNoInteractions(runtime); @@ -144,16 +134,16 @@ void extractFileName_whenFileNamePresent_returnsValue() { try (MockedStatic mocked = mockStatic(CdsDataProcessor.class)) { mocked.when(CdsDataProcessor::create).thenReturn(processor); doAnswer( - invocation -> { - Filter filter = invocation.getArgument(0); - Validator validator = invocation.getArgument(1); - - // Simulate processor visiting fileName with String value - if (filter.test(null, cdsElement, null)) { - validator.validate(null, cdsElement, "test.pdf"); - } - return processor; - }) + invocation -> { + Filter filter = invocation.getArgument(0); + Validator validator = invocation.getArgument(1); + + // Simulate processor visiting fileName with String value + if (filter.test(null, cdsElement, null)) { + validator.validate(null, cdsElement, "test.pdf"); + } + return processor; + }) .when(processor) .addValidator(any(), any()); @@ -169,14 +159,14 @@ void extractFileName_whenElementIsNotFileName_throws() { try (MockedStatic mocked = mockStatic(CdsDataProcessor.class)) { mocked.when(CdsDataProcessor::create).thenReturn(processor); doAnswer( - invocation -> { - Filter filter = invocation.getArgument(0); - Validator validator = invocation.getArgument(1); - if (filter.test(null, cdsElement, null)) { - validator.validate(null, cdsElement, "test.pdf"); - } - return processor; - }) + invocation -> { + Filter filter = invocation.getArgument(0); + Validator validator = invocation.getArgument(1); + if (filter.test(null, cdsElement, null)) { + validator.validate(null, cdsElement, "test.pdf"); + } + return processor; + }) .when(processor) .addValidator(any(), any()); @@ -193,14 +183,14 @@ void extractFileName_valueIsNotString_branchCovered() { try (MockedStatic mocked = mockStatic(CdsDataProcessor.class)) { mocked.when(CdsDataProcessor::create).thenReturn(processor); doAnswer( - invocation -> { - Filter filter = invocation.getArgument(0); - Validator validator = invocation.getArgument(1); - if (filter.test(null, cdsElement, null)) { - validator.validate(null, cdsElement, 42); // non-String - } - return processor; - }) + invocation -> { + Filter filter = invocation.getArgument(0); + Validator validator = invocation.getArgument(1); + if (filter.test(null, cdsElement, null)) { + validator.validate(null, cdsElement, 42); // non-String + } + return processor; + }) .when(processor) .addValidator(any(), any()); doNothing().when(processor).process(anyList(), any()); @@ -226,7 +216,8 @@ void validateAcceptableMediaTypes_notMediaEntity_doesNotCallValidation() { when(cdsModel.findEntity("TestService.Roots")).thenReturn(Optional.of(serviceEntity)); when(serviceEntity.getAnnotationValue("_is_media_data", false)).thenReturn(false); - try (MockedStatic mocked = mockStatic(AttachmentValidationHelper.class)) { + try (MockedStatic mocked = + mockStatic(AttachmentValidationHelper.class)) { ApplicationHandlerHelper.validateAcceptableMediaTypes(entity, List.of(), cdsRuntime); mocked.verifyNoInteractions(); } @@ -244,8 +235,8 @@ void shouldNotThrowWhenEntityNotFoundInModel() { @Test void validateAcceptableMediaTypes_mediaTypeMatches_succeeds() { setupMediaEntity(List.of("image/png")); - try (MockedStatic helperStatic = mockStatic(ApplicationHandlerHelper.class, - CALLS_REAL_METHODS)) { + try (MockedStatic helperStatic = + mockStatic(ApplicationHandlerHelper.class, CALLS_REAL_METHODS)) { // file.png → media type image/png → allowed helperStatic @@ -253,16 +244,17 @@ void validateAcceptableMediaTypes_mediaTypeMatches_succeeds() { .thenReturn("file.png"); assertDoesNotThrow( - () -> ApplicationHandlerHelper.validateAcceptableMediaTypes( - entity, List.of(data), cdsRuntime)); + () -> + ApplicationHandlerHelper.validateAcceptableMediaTypes( + entity, List.of(data), cdsRuntime)); } } @Test void validateAcceptableMediaTypes_mediaTypeMismatch_throws() { setupMediaEntity(List.of("image/png")); - try (MockedStatic helperStatic = mockStatic(ApplicationHandlerHelper.class, - CALLS_REAL_METHODS)) { + try (MockedStatic helperStatic = + mockStatic(ApplicationHandlerHelper.class, CALLS_REAL_METHODS)) { // file.jpg → media type image/jpeg → NOT allowed helperStatic @@ -271,8 +263,9 @@ void validateAcceptableMediaTypes_mediaTypeMismatch_throws() { assertThrows( ServiceException.class, - () -> ApplicationHandlerHelper.validateAcceptableMediaTypes( - entity, List.of(data), cdsRuntime)); + () -> + ApplicationHandlerHelper.validateAcceptableMediaTypes( + entity, List.of(data), cdsRuntime)); } } From 26b499c1f6a806740741f889099cc61dc029139b Mon Sep 17 00:00:00 2001 From: samyuktaprabhu <33195453+samyuktaprabhu@users.noreply.github.com> Date: Tue, 17 Feb 2026 10:14:30 +0100 Subject: [PATCH 10/17] more file formatting --- .../DraftOdataRequestValidationBase.java | 213 ++++++----- .../MediaValidatedAttachmentsDraftTest.java | 226 ++++++------ ...tedAttachmentsSizeValidationDraftTest.java | 9 +- ...MediaValidatedAttachmentsNonDraftTest.java | 335 +++++++++--------- ...mitedAttachmentValidationNonDraftTest.java | 18 +- 5 files changed, 423 insertions(+), 378 deletions(-) diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationBase.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationBase.java index b926b1be..843fa2bb 100644 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationBase.java +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationBase.java @@ -42,21 +42,18 @@ @AutoConfigureMockMvc abstract class DraftOdataRequestValidationBase { - protected static final Logger logger = LoggerFactory.getLogger(DraftOdataRequestValidationBase.class); + protected static final Logger logger = + LoggerFactory.getLogger(DraftOdataRequestValidationBase.class); private static final String BASE_URL = MockHttpRequestHelper.ODATA_BASE_URL + "TestDraftService/"; private static final String BASE_ROOT_URL = BASE_URL + "DraftRoots"; @Autowired(required = false) protected TestPluginAttachmentsServiceHandler serviceHandler; - @Autowired - protected MockHttpRequestHelper requestHelper; - @Autowired - protected PersistenceService persistenceService; - @Autowired - private TableDataDeleter dataDeleter; - @Autowired - private TestPersistenceHandler testPersistenceHandler; + @Autowired protected MockHttpRequestHelper requestHelper; + @Autowired protected PersistenceService persistenceService; + @Autowired private TableDataDeleter dataDeleter; + @Autowired private TestPersistenceHandler testPersistenceHandler; @AfterEach void teardown() { @@ -110,14 +107,16 @@ void contentCanBeReadFromDraft() throws Exception { clearServiceHandlerContext(); createNewDraftForExistingRoot(selectedRoot.getId()); - var attachmentUrl = getAttachmentBaseUrl( - selectedRoot.getItems().get(0).getId(), - selectedRoot.getItems().get(0).getAttachments().get(0).getId(), - false) - + "/content"; - var attachmentEntityUrl = getAttachmentEntityBaseUrl( - selectedRoot.getItems().get(0).getAttachmentEntities().get(0).getId(), false) - + "/content"; + var attachmentUrl = + getAttachmentBaseUrl( + selectedRoot.getItems().get(0).getId(), + selectedRoot.getItems().get(0).getAttachments().get(0).getId(), + false) + + "/content"; + var attachmentEntityUrl = + getAttachmentEntityBaseUrl( + selectedRoot.getItems().get(0).getAttachmentEntities().get(0).getId(), false) + + "/content"; Awaitility.await() .atMost(60, TimeUnit.SECONDS) @@ -129,8 +128,9 @@ void contentCanBeReadFromDraft() throws Exception { var attachmentEntityResponse = requestHelper.executeGet(attachmentEntityUrl); var attachmentResponseContent = getResponseContent(attachmentResponse); var attachmentEntityResponseContent = getResponseContent(attachmentEntityResponse); - var result = attachmentResponseContent.equals(testContentAttachment) - && attachmentEntityResponseContent.equals(testContentAttachmentEntity); + var result = + attachmentResponseContent.equals(testContentAttachment) + && attachmentEntityResponseContent.equals(testContentAttachmentEntity); if (!result) { logger.info( "Attachment response content: {}, Attachment Test Content: {}, Attachment Entity response content: {}, Attachment Entity Test Content: {}", @@ -154,15 +154,16 @@ void contentCanBeReadFromDraft() throws Exception { @Test void deleteAttachmentAndActivateDraft() throws Exception { - var selectedRoot = deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); + var selectedRoot = + deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); clearServiceHandlerContext(); createNewDraftForExistingRoot(selectedRoot.getId()); var itemAttachment = selectedRoot.getItems().get(0).getAttachments().get(0); var itemAttachmentEntity = selectedRoot.getItems().get(0).getAttachmentEntities().get(0); - var attachmentDeleteUrl = getAttachmentBaseUrl(selectedRoot.getItems().get(0).getId(), itemAttachment.getId(), - false); + var attachmentDeleteUrl = + getAttachmentBaseUrl(selectedRoot.getItems().get(0).getId(), itemAttachment.getId(), false); var attachmentEntityDeleteUrl = getAttachmentEntityBaseUrl(itemAttachmentEntity.getId(), false); requestHelper.executeDeleteWithMatcher(attachmentDeleteUrl, status().isNoContent()); @@ -179,7 +180,8 @@ void deleteAttachmentAndActivateDraft() throws Exception { @Test void updateAttachmentAndActivateDraft() throws Exception { - var selectedRoot = deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); + var selectedRoot = + deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); clearServiceHandlerContext(); createNewDraftForExistingRoot(selectedRoot.getId()); @@ -201,14 +203,15 @@ void updateAttachmentAndActivateDraft() throws Exception { assertThat(selectedRootAfterUpdate.getItems().get(0).getAttachments().get(0).getFileName()) .isEqualTo(changedAttachmentFileName); assertThat( - selectedRootAfterUpdate.getItems().get(0).getAttachmentEntities().get(0).getFileName()) + selectedRootAfterUpdate.getItems().get(0).getAttachmentEntities().get(0).getFileName()) .isEqualTo(changedAttachmentEntityFileName); verifyNoAttachmentEventsCalled(); } @Test void updateAttachmentAndCancelDraft() throws Exception { - var selectedRoot = deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); + var selectedRoot = + deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); clearServiceHandlerContext(); createNewDraftForExistingRoot(selectedRoot.getId()); @@ -230,14 +233,15 @@ void updateAttachmentAndCancelDraft() throws Exception { assertThat(selectedRootAfterUpdate.getItems().get(0).getAttachments().get(0).getFileName()) .isEqualTo(originAttachmentFileName); assertThat( - selectedRootAfterUpdate.getItems().get(0).getAttachmentEntities().get(0).getFileName()) + selectedRootAfterUpdate.getItems().get(0).getAttachmentEntities().get(0).getFileName()) .isEqualTo(originAttachmentEntityFileName); verifyNoAttachmentEventsCalled(); } @Test void createAttachmentAndActivateDraft() throws Exception { - var selectedRoot = deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); + var selectedRoot = + deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); clearServiceHandlerContext(); createNewDraftForExistingRoot(selectedRoot.getId()); @@ -257,7 +261,8 @@ void createAttachmentAndActivateDraft() throws Exception { @Test void createAttachmentAndCancelDraft() throws Exception { - var selectedRoot = deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); + var selectedRoot = + deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); clearServiceHandlerContext(); createNewDraftForExistingRoot(selectedRoot.getId()); @@ -277,8 +282,9 @@ void createAttachmentAndCancelDraft() throws Exception { @Test void deleteContentInDraft() throws Exception { - var selectedRoot = deepCreateAndActivate( - "testContent attachment for delete", "testContent attachmentEntity for delete"); + var selectedRoot = + deepCreateAndActivate( + "testContent attachment for delete", "testContent attachmentEntity for delete"); clearServiceHandlerContext(); createNewDraftForExistingRoot(selectedRoot.getId()); var itemAttachment = selectedRoot.getItems().get(0).getAttachments().get(0); @@ -299,7 +305,8 @@ void deleteContentInDraft() throws Exception { @Test void doNotDeleteContentInCancelledDraft() throws Exception { - var selectedRoot = deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); + var selectedRoot = + deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); clearServiceHandlerContext(); createNewDraftForExistingRoot(selectedRoot.getId()); @@ -321,7 +328,8 @@ void doNotDeleteContentInCancelledDraft() throws Exception { @Test void updateContentInDraft() throws Exception { - var selectedRoot = deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); + var selectedRoot = + deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); clearServiceHandlerContext(); createNewDraftForExistingRoot(selectedRoot.getId()); @@ -355,12 +363,12 @@ void updateContentInDraft() throws Exception { assertThat(selectedRootAfterDeletion.getItems().get(0).getAttachments().get(0).getContentId()) .isNotEmpty(); assertThat( - selectedRootAfterDeletion - .getItems() - .get(0) - .getAttachmentEntities() - .get(0) - .getContentId()) + selectedRootAfterDeletion + .getItems() + .get(0) + .getAttachmentEntities() + .get(0) + .getContentId()) .isNotEmpty(); } @@ -431,7 +439,7 @@ void deleteItemAndCancelDraft() throws Exception { .isNotEmpty(); assertThat(selectedRootAfterDelete.getItems().get(0).getAttachmentEntities()).isNotEmpty(); assertThat( - selectedRootAfterDelete.getItems().get(0).getAttachmentEntities().get(0).getContentId()) + selectedRootAfterDelete.getItems().get(0).getAttachmentEntities().get(0).getContentId()) .isNotEmpty(); verifyNoAttachmentEventsCalled(); } @@ -454,14 +462,16 @@ void noEventsForForDeletedRoot() throws Exception { assertThat(result).isEmpty(); var attachmentContentId = selectedRoot.getItems().get(0).getAttachments().get(0).getContentId(); - var attachmentEntityContentId = selectedRoot.getItems().get(0).getAttachmentEntities().get(0).getContentId(); + var attachmentEntityContentId = + selectedRoot.getItems().get(0).getAttachmentEntities().get(0).getContentId(); verifyOnlyTwoDeleteEvents(attachmentContentId, attachmentEntityContentId); } @Test void errorInTransactionAfterCreateCallsDelete() throws Exception { - var selectedRoot = deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); + var selectedRoot = + deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); clearServiceHandlerContext(); createNewDraftForExistingRoot(selectedRoot.getId()); @@ -476,7 +486,8 @@ void errorInTransactionAfterCreateCallsDelete() throws Exception { @Test void errorInTransactionAfterCreateCallsDeleteAndNothingForCancel() throws Exception { - var selectedRoot = deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); + var selectedRoot = + deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); clearServiceHandlerContext(); createNewDraftForExistingRoot(selectedRoot.getId()); @@ -527,7 +538,8 @@ void errorInTransactionAfterUpdateCallsDeleteEvenIfDraftIsCancelled() throws Exc @Test void createAndDeleteAttachmentWorks() throws Exception { - var selectedRoot = deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); + var selectedRoot = + deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); clearServiceHandlerContext(); createNewDraftForExistingRoot(selectedRoot.getId()); @@ -543,18 +555,21 @@ void createAndDeleteAttachmentWorks() throws Exception { var existingAttachment = selectedRoot.getItems().get(0).getAttachments().get(0); var existingAttachmentEntity = selectedRoot.getItems().get(0).getAttachmentEntities().get(0); - var newAttachment = draftRoot.getItems().get(0).getAttachments().stream() - .filter(attachment -> !attachment.getId().equals(existingAttachment.getId())) - .findAny() - .orElseThrow(); - var newAttachmentEntity = draftRoot.getItems().get(0).getAttachmentEntities().stream() - .filter( - attachmentEntity -> !attachmentEntity.getId().equals(existingAttachmentEntity.getId())) - .findAny() - .orElseThrow(); - - var attachmentDeleteUrl = getAttachmentBaseUrl(selectedRoot.getItems().get(0).getId(), newAttachment.getId(), - false); + var newAttachment = + draftRoot.getItems().get(0).getAttachments().stream() + .filter(attachment -> !attachment.getId().equals(existingAttachment.getId())) + .findAny() + .orElseThrow(); + var newAttachmentEntity = + draftRoot.getItems().get(0).getAttachmentEntities().stream() + .filter( + attachmentEntity -> + !attachmentEntity.getId().equals(existingAttachmentEntity.getId())) + .findAny() + .orElseThrow(); + + var attachmentDeleteUrl = + getAttachmentBaseUrl(selectedRoot.getItems().get(0).getId(), newAttachment.getId(), false); var attachmentEntityDeleteUrl = getAttachmentEntityBaseUrl(newAttachmentEntity.getId(), false); requestHelper.executeDeleteWithMatcher(attachmentDeleteUrl, status().isNoContent()); @@ -580,7 +595,8 @@ protected DraftRoots deepCreateAndActivate( } private DraftRoots createNewDraft() throws Exception { - var responseRootCdsData = requestHelper.executePostWithODataResponseAndAssertStatusCreated(BASE_ROOT_URL, "{}"); + var responseRootCdsData = + requestHelper.executePostWithODataResponseAndAssertStatusCreated(BASE_ROOT_URL, "{}"); return Struct.access(responseRootCdsData).as(DraftRoots.class); } @@ -605,7 +621,8 @@ private Items createItem(String rootUrl) throws Exception { var item = Items.create(); item.setTitle("some item"); var itemUrl = rootUrl + "/items"; - var responseItemCdsData = requestHelper.executePostWithODataResponseAndAssertStatusCreated(itemUrl, item.toJson()); + var responseItemCdsData = + requestHelper.executePostWithODataResponseAndAssertStatusCreated(itemUrl, item.toJson()); return Struct.access(responseItemCdsData).as(Items.class); } @@ -644,8 +661,9 @@ private Attachments createAttachment(String itemId) throws Exception { itemAttachment.setFileName("itemAttachment.txt"); var attachmentPostUrl = BASE_URL + "Items(ID=" + itemId + ",IsActiveEntity=false)/attachments"; - var responseAttachmentCdsData = requestHelper.executePostWithODataResponseAndAssertStatusCreated( - attachmentPostUrl, itemAttachment.toJson()); + var responseAttachmentCdsData = + requestHelper.executePostWithODataResponseAndAssertStatusCreated( + attachmentPostUrl, itemAttachment.toJson()); return Struct.access(responseAttachmentCdsData).as(Attachments.class); } @@ -678,7 +696,8 @@ private void putNewContentForAttachmentEntity( private void putNewContentForAttachmentEntity( String testContentAttachmentEntity, String attachmentId, ResultMatcher matcher) throws Exception { - var attachmentEntityPutUrl = BASE_URL + "/AttachmentEntity(ID=" + attachmentId + ",IsActiveEntity=false)/content"; + var attachmentEntityPutUrl = + BASE_URL + "/AttachmentEntity(ID=" + attachmentId + ",IsActiveEntity=false)/content"; requestHelper.setContentType("image/jpeg"); requestHelper.executePutWithMatcher( attachmentEntityPutUrl, @@ -692,8 +711,9 @@ private AttachmentEntity createAttachmentEntity(Items responseItem) throws Excep itemAttachmentEntity.setFileName("itemAttachmentEntity.txt"); var attachmentEntityPostUrl = getItemUrl(responseItem, false) + "/attachmentEntities"; - var responseAttachmentEntityCdsData = requestHelper.executePostWithODataResponseAndAssertStatusCreated( - attachmentEntityPostUrl, itemAttachmentEntity.toJson()); + var responseAttachmentEntityCdsData = + requestHelper.executePostWithODataResponseAndAssertStatusCreated( + attachmentEntityPostUrl, itemAttachmentEntity.toJson()); return Struct.access(responseAttachmentEntityCdsData).as(AttachmentEntity.class); } @@ -744,29 +764,33 @@ private DraftRoots selectStoredRootData(DraftRoots responseRoot) { } private DraftRoots selectStoredRootData(String entityName, DraftRoots responseRoot) { - var select = Select.from(entityName) - .where(root -> root.get(DraftRoots.ID).eq(responseRoot.getId())) - .columns( - StructuredType::_all, - root -> root.to(DraftRoots.ITEMS) - .expand( - StructuredType::_all, - item -> item.to(Items.ATTACHMENTS).expand(), - item -> item.to(Items.ATTACHMENT_ENTITIES).expand())); + var select = + Select.from(entityName) + .where(root -> root.get(DraftRoots.ID).eq(responseRoot.getId())) + .columns( + StructuredType::_all, + root -> + root.to(DraftRoots.ITEMS) + .expand( + StructuredType::_all, + item -> item.to(Items.ATTACHMENTS).expand(), + item -> item.to(Items.ATTACHMENT_ENTITIES).expand())); return persistenceService.run(select).single(DraftRoots.class); } protected void readAndValidateActiveContent( DraftRoots selectedRoot, String attachmentContent, String attachmentEntityContent) throws Exception { - var attachmentUrl = getAttachmentBaseUrl( - selectedRoot.getItems().get(0).getId(), - selectedRoot.getItems().get(0).getAttachments().get(0).getId(), - true) - + "/content"; - var attachmentEntityUrl = getAttachmentEntityBaseUrl( - selectedRoot.getItems().get(0).getAttachmentEntities().get(0).getId(), true) - + "/content"; + var attachmentUrl = + getAttachmentBaseUrl( + selectedRoot.getItems().get(0).getId(), + selectedRoot.getItems().get(0).getAttachments().get(0).getId(), + true) + + "/content"; + var attachmentEntityUrl = + getAttachmentEntityBaseUrl( + selectedRoot.getItems().get(0).getAttachmentEntities().get(0).getId(), true) + + "/content"; Awaitility.await() .atMost(60, TimeUnit.SECONDS) @@ -777,10 +801,12 @@ protected void readAndValidateActiveContent( var attachmentResponse = requestHelper.executeGet(attachmentUrl); var attachmentEntityResponse = requestHelper.executeGet(attachmentEntityUrl); var attachmentContentAsString = attachmentResponse.getResponse().getContentAsString(); - var attachmentEntityContentAsString = attachmentEntityResponse.getResponse().getContentAsString(); + var attachmentEntityContentAsString = + attachmentEntityResponse.getResponse().getContentAsString(); - var booleanResult = attachmentContentAsString.equals(attachmentContent) - && attachmentEntityContentAsString.equals(attachmentEntityContent); + var booleanResult = + attachmentContentAsString.equals(attachmentContent) + && attachmentEntityContentAsString.equals(attachmentEntityContent); if (!booleanResult) { logger.info( @@ -806,9 +832,11 @@ protected void readAndValidateActiveContent( private void deleteContent( DraftRoots selectedRoot, Attachments itemAttachment, AttachmentEntity itemAttachmentEntity) throws Exception { - var attachmentUrl = getAttachmentBaseUrl(selectedRoot.getItems().get(0).getId(), itemAttachment.getId(), false) - + "/content"; - var attachmentEntityUrl = getAttachmentEntityBaseUrl(itemAttachmentEntity.getId(), false) + "/content"; + var attachmentUrl = + getAttachmentBaseUrl(selectedRoot.getItems().get(0).getId(), itemAttachment.getId(), false) + + "/content"; + var attachmentEntityUrl = + getAttachmentEntityBaseUrl(itemAttachmentEntity.getId(), false) + "/content"; requestHelper.executeDeleteWithMatcher(attachmentUrl, status().isNoContent()); requestHelper.executeDeleteWithMatcher(attachmentEntityUrl, status().isNoContent()); @@ -838,7 +866,8 @@ private void updateFileName( String changedAttachmentEntityFileName, HttpStatus httpStatus) throws Exception { - var attachmentUrl = getAttachmentBaseUrl(selectedRoot.getItems().get(0).getId(), itemAttachment.getId(), false); + var attachmentUrl = + getAttachmentBaseUrl(selectedRoot.getItems().get(0).getId(), itemAttachment.getId(), false); var attachmentEntityUrl = getAttachmentEntityBaseUrl(itemAttachmentEntity.getId(), false); requestHelper.executePatchWithODataResponseAndAssertStatus( @@ -882,12 +911,12 @@ private void verifyNothingHasChangedInDraft( assertThat(selectedRootAfterDeletion.getItems().get(0).getAttachments().get(0).getContentId()) .isNotEmpty(); assertThat( - selectedRootAfterDeletion - .getItems() - .get(0) - .getAttachmentEntities() - .get(0) - .getContentId()) + selectedRootAfterDeletion + .getItems() + .get(0) + .getAttachmentEntities() + .get(0) + .getContentId()) .isNotEmpty(); } diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/MediaValidatedAttachmentsDraftTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/MediaValidatedAttachmentsDraftTest.java index 058d1cee..3447bb20 100644 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/MediaValidatedAttachmentsDraftTest.java +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/MediaValidatedAttachmentsDraftTest.java @@ -1,3 +1,6 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ package com.sap.cds.feature.attachments.integrationtests.draftservice; import static org.assertj.core.api.Assertions.assertThat; @@ -11,7 +14,6 @@ import java.io.IOException; import java.io.InputStream; import java.util.Objects; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -21,125 +23,127 @@ @ActiveProfiles(Profiles.TEST_HANDLER_DISABLED) public class MediaValidatedAttachmentsDraftTest extends DraftOdataRequestValidationBase { - private static final String BASE_URL = MockHttpRequestHelper.ODATA_BASE_URL + "TestDraftService/"; - private static final String BASE_ROOT_URL = BASE_URL + "DraftRoots"; + private static final String BASE_URL = MockHttpRequestHelper.ODATA_BASE_URL + "TestDraftService/"; + private static final String BASE_ROOT_URL = BASE_URL + "DraftRoots"; - @BeforeEach - void setup() { - requestHelper.setContentType( - MediaType.APPLICATION_JSON); - } + @BeforeEach + void setup() { + requestHelper.setContentType(MediaType.APPLICATION_JSON); + } + + @ParameterizedTest + @CsvSource({"test.png,201", "test.jpeg,201", "test.pdf,415", "test.txt,415"}) + void shouldValidateMediaType_whenCreatingAttachmentInDraft(String fileName, int expectedStatus) + throws Exception { - @ParameterizedTest - @CsvSource({ - "test.png,201", - "test.jpeg,201", - "test.pdf,415", - "test.txt,415" - }) - void shouldValidateMediaType_whenCreatingAttachmentInDraft(String fileName, int expectedStatus) throws Exception { - - String rootId = createDraftRootAndReturnId(); - String metadata = """ + String rootId = createDraftRootAndReturnId(); + String metadata = + """ { "fileName": "%s" } - """.formatted(fileName); - - requestHelper.executePostWithMatcher( - buildDraftAttachmentCreationUrl(rootId), - metadata, - status().is(expectedStatus)); - } - - private String buildDraftAttachmentCreationUrl(String rootId) { - return BASE_ROOT_URL - + "(ID=" + rootId + ",IsActiveEntity=false)" - + "/mediaValidatedAttachments"; - } - - // Helper methods - private String createDraftRootAndReturnId() throws Exception { - CdsData response = requestHelper.executePostWithODataResponseAndAssertStatusCreated( - BASE_ROOT_URL, "{}"); - - DraftRoots draftRoot = Struct.access(response).as(DraftRoots.class); - - requestHelper.executePatchWithODataResponseAndAssertStatusOk( - getRootUrl(draftRoot.getId(), false), """ + .formatted(fileName); + + requestHelper.executePostWithMatcher( + buildDraftAttachmentCreationUrl(rootId), metadata, status().is(expectedStatus)); + } + + private String buildDraftAttachmentCreationUrl(String rootId) { + return BASE_ROOT_URL + + "(ID=" + + rootId + + ",IsActiveEntity=false)" + + "/mediaValidatedAttachments"; + } + + // Helper methods + private String createDraftRootAndReturnId() throws Exception { + CdsData response = + requestHelper.executePostWithODataResponseAndAssertStatusCreated(BASE_ROOT_URL, "{}"); + + DraftRoots draftRoot = Struct.access(response).as(DraftRoots.class); + + requestHelper.executePatchWithODataResponseAndAssertStatusOk( + getRootUrl(draftRoot.getId(), false), + """ { "title": "Draft with mediaValidatedAttachments" } """); - return draftRoot.getId(); - } - - private String getRootUrl(String rootId, boolean isActiveEntity) { - return BASE_ROOT_URL + "(ID=" + rootId + ",IsActiveEntity=" + isActiveEntity + ")"; - } - - // Required abstract method implementations - @Override - protected void verifyContentId(String contentId, String attachmentId) { - assertThat(contentId).isEqualTo(attachmentId); - } - - @Override - protected void verifyContent(InputStream attachment, String testContent) throws IOException { - if (Objects.nonNull(testContent)) { - assertThat(attachment.readAllBytes()) - .isEqualTo(testContent.getBytes(java.nio.charset.StandardCharsets.UTF_8)); - } else { - assertThat(attachment).isNull(); - } - } - - @Override - protected void verifyNoAttachmentEventsCalled() { - // Implementation not required for this test - } - - @Override - protected void clearServiceHandlerContext() { - // Implementation not required for this test - } - - @Override - protected void verifyEventContextEmptyForEvent(String... events) { - // Implementation not required for this test - } - - @Override - protected void verifyOnlyTwoCreateEvents(String newAttachmentContent, String newAttachmentEntityContent) { - // Implementation not required for this test - } - - @Override - protected void verifyTwoCreateAndDeleteEvents(String newAttachmentContent, String newAttachmentEntityContent) { - // Implementation not required for this test - } - - @Override - protected void verifyTwoReadEvents() { - // Implementation not required for this test - } - - @Override - protected void verifyOnlyTwoDeleteEvents(String attachmentContentId, String attachmentEntityContentId) { - // Implementation not required for this test - } - - @Override - protected void verifyTwoUpdateEvents(String newAttachmentContent, String attachmentContentId, - String newAttachmentEntityContent, String attachmentEntityContentId) { - // Implementation not required for this test - } - - @Override - protected void verifyTwoCreateAndRevertedDeleteEvents() { - // Implementation not required for this test - } - + return draftRoot.getId(); + } + + private String getRootUrl(String rootId, boolean isActiveEntity) { + return BASE_ROOT_URL + "(ID=" + rootId + ",IsActiveEntity=" + isActiveEntity + ")"; + } + + // Required abstract method implementations + @Override + protected void verifyContentId(String contentId, String attachmentId) { + assertThat(contentId).isEqualTo(attachmentId); + } + + @Override + protected void verifyContent(InputStream attachment, String testContent) throws IOException { + if (Objects.nonNull(testContent)) { + assertThat(attachment.readAllBytes()) + .isEqualTo(testContent.getBytes(java.nio.charset.StandardCharsets.UTF_8)); + } else { + assertThat(attachment).isNull(); + } + } + + @Override + protected void verifyNoAttachmentEventsCalled() { + // Implementation not required for this test + } + + @Override + protected void clearServiceHandlerContext() { + // Implementation not required for this test + } + + @Override + protected void verifyEventContextEmptyForEvent(String... events) { + // Implementation not required for this test + } + + @Override + protected void verifyOnlyTwoCreateEvents( + String newAttachmentContent, String newAttachmentEntityContent) { + // Implementation not required for this test + } + + @Override + protected void verifyTwoCreateAndDeleteEvents( + String newAttachmentContent, String newAttachmentEntityContent) { + // Implementation not required for this test + } + + @Override + protected void verifyTwoReadEvents() { + // Implementation not required for this test + } + + @Override + protected void verifyOnlyTwoDeleteEvents( + String attachmentContentId, String attachmentEntityContentId) { + // Implementation not required for this test + } + + @Override + protected void verifyTwoUpdateEvents( + String newAttachmentContent, + String attachmentContentId, + String newAttachmentEntityContent, + String attachmentEntityContentId) { + // Implementation not required for this test + } + + @Override + protected void verifyTwoCreateAndRevertedDeleteEvents() { + // Implementation not required for this test + } } diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/SizeLimitedAttachmentsSizeValidationDraftTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/SizeLimitedAttachmentsSizeValidationDraftTest.java index e71ad1b6..b28055fd 100644 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/SizeLimitedAttachmentsSizeValidationDraftTest.java +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/SizeLimitedAttachmentsSizeValidationDraftTest.java @@ -56,8 +56,8 @@ void uploadContentExceeding5MBLimitFails() throws Exception { // Helper methods private DraftRoots createNewDraftWithSizeLimitedAttachments() throws Exception { // Create new draft - var responseRootCdsData = requestHelper.executePostWithODataResponseAndAssertStatusCreated(BASE_ROOT_URL, - "{}"); + var responseRootCdsData = + requestHelper.executePostWithODataResponseAndAssertStatusCreated(BASE_ROOT_URL, "{}"); var draftRoot = Struct.access(responseRootCdsData).as(DraftRoots.class); // Update root with title @@ -70,8 +70,9 @@ private DraftRoots createNewDraftWithSizeLimitedAttachments() throws Exception { attachment.setFileName("testFile.txt"); attachment.setMimeType("text/plain"); var attachmentUrl = rootUrl + "/sizeLimitedAttachments"; - var responseAttachmentCdsData = requestHelper.executePostWithODataResponseAndAssertStatusCreated( - attachmentUrl, attachment.toJson()); + var responseAttachmentCdsData = + requestHelper.executePostWithODataResponseAndAssertStatusCreated( + attachmentUrl, attachment.toJson()); var createdAttachment = Struct.access(responseAttachmentCdsData).as(Attachments.class); // Build result with the attachment diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java index 5c7fecf5..04be3621 100644 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java @@ -1,14 +1,10 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ package com.sap.cds.feature.attachments.integrationtests.nondraftservice; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; -import org.springframework.http.MediaType; -import org.springframework.test.context.ActiveProfiles; - import com.sap.cds.Result; import com.sap.cds.feature.attachments.generated.integration.test.cds4j.sap.attachments.Attachments; import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.AttachmentEntity; @@ -19,167 +15,178 @@ import com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper.AttachmentsBuilder; import com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper.RootEntityBuilder; import com.sap.cds.ql.Select; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; @ActiveProfiles(Profiles.TEST_HANDLER_DISABLED) class MediaValidatedAttachmentsNonDraftTest extends OdataRequestValidationBase { - private static final String BASE_URL = MockHttpRequestHelper.ODATA_BASE_URL + "TestService/Roots"; - private static final String MEDIA_VALIDATED_ATTACHMENTS = "mediaValidatedAttachments"; - - protected void postServiceRoot(Roots serviceRoot) throws Exception { - String url = MockHttpRequestHelper.ODATA_BASE_URL + "TestService/Roots"; - requestHelper.executePostWithMatcher(url, serviceRoot.toJson(), status().isCreated()); - } - - private Roots selectStoredRootWithMediaValidatedAttachments() { - Select select = Select.from( - Roots_.class) - .columns(r -> r._all(), r -> r.mediaValidatedAttachments().expand()); - - Result result = persistenceService.run(select); - return result.single(Roots.class); - } - - @BeforeEach - void setup() { - requestHelper.setContentType(MediaType.APPLICATION_JSON); - } - - @ParameterizedTest - @CsvSource({ - "image.jpg,image/jpeg,201", - "image.png,image/png,201", - "document.pdf,application/pdf,415", - "notes.txt,text/plain,415" - }) - void shouldValidateMediaTypes(String fileName, - String mediaType, - int expectedStatus) throws Exception { - String rootId = createRootAndReturnId(); - String attachmentMetadata = createAttachmentMetadata(fileName); - - requestHelper.executePostWithMatcher( - createUrl(rootId, MEDIA_VALIDATED_ATTACHMENTS), - attachmentMetadata, - status().is(expectedStatus)); - } - - @Test - void shouldRejectAttachment_whenFileNameIsEmpty() throws Exception { - String rootId = createRootAndReturnId(); - String fileName = ""; - String attachmentMetadata = createAttachmentMetadata(fileName); - - requestHelper.executePostWithMatcher( - createUrl(rootId, MEDIA_VALIDATED_ATTACHMENTS), - attachmentMetadata, - status().isBadRequest()); - } - - private String createRootAndReturnId() throws Exception { - // Build the initial Java object.. Root - Roots serviceRoot = buildServiceRootWithMediaValidatedAttachments(); - - // POST the root object to the server to create it in the database - postServiceRoot(serviceRoot); - - // Read the newly created entity back from the database - Roots selectedRoot = selectStoredRootWithMediaValidatedAttachments(); - - return selectedRoot.getId(); - } - - private String createUrl(String rootId, String path) { - return BASE_URL + "(" + rootId + ")/" + path; - } - - private String createAttachmentMetadata(String fileName) { - return String.format(""" + private static final String BASE_URL = MockHttpRequestHelper.ODATA_BASE_URL + "TestService/Roots"; + private static final String MEDIA_VALIDATED_ATTACHMENTS = "mediaValidatedAttachments"; + + protected void postServiceRoot(Roots serviceRoot) throws Exception { + String url = MockHttpRequestHelper.ODATA_BASE_URL + "TestService/Roots"; + requestHelper.executePostWithMatcher(url, serviceRoot.toJson(), status().isCreated()); + } + + private Roots selectStoredRootWithMediaValidatedAttachments() { + Select select = + Select.from(Roots_.class) + .columns(r -> r._all(), r -> r.mediaValidatedAttachments().expand()); + + Result result = persistenceService.run(select); + return result.single(Roots.class); + } + + @BeforeEach + void setup() { + requestHelper.setContentType(MediaType.APPLICATION_JSON); + } + + @ParameterizedTest + @CsvSource({ + "image.jpg,image/jpeg,201", + "image.png,image/png,201", + "document.pdf,application/pdf,415", + "notes.txt,text/plain,415" + }) + void shouldValidateMediaTypes(String fileName, String mediaType, int expectedStatus) + throws Exception { + String rootId = createRootAndReturnId(); + String attachmentMetadata = createAttachmentMetadata(fileName); + + requestHelper.executePostWithMatcher( + createUrl(rootId, MEDIA_VALIDATED_ATTACHMENTS), + attachmentMetadata, + status().is(expectedStatus)); + } + + @Test + void shouldRejectAttachment_whenFileNameIsEmpty() throws Exception { + String rootId = createRootAndReturnId(); + String fileName = ""; + String attachmentMetadata = createAttachmentMetadata(fileName); + + requestHelper.executePostWithMatcher( + createUrl(rootId, MEDIA_VALIDATED_ATTACHMENTS), + attachmentMetadata, + status().isBadRequest()); + } + + private String createRootAndReturnId() throws Exception { + // Build the initial Java object.. Root + Roots serviceRoot = buildServiceRootWithMediaValidatedAttachments(); + + // POST the root object to the server to create it in the database + postServiceRoot(serviceRoot); + + // Read the newly created entity back from the database + Roots selectedRoot = selectStoredRootWithMediaValidatedAttachments(); + + return selectedRoot.getId(); + } + + private String createUrl(String rootId, String path) { + return BASE_URL + "(" + rootId + ")/" + path; + } + + private String createAttachmentMetadata(String fileName) { + return String.format( + """ { "fileName": "%s" } - """, fileName); - } - - // helper method - private Roots buildServiceRootWithMediaValidatedAttachments() { - return RootEntityBuilder.create() - .setTitle("Root with mediaValidatedAttachments") - .addMediaValidatedAttachments( - AttachmentsBuilder.create().setFileName("parent.text") - .setMimeType(MediaType.APPLICATION_JSON_VALUE)) - .build(); - } - - // Override abstract methods from OdataRequestValidationBase - - @Override - protected void executeContentRequestAndValidateContent(String url, String content) throws Exception { - // Implementation not required for this test - } - - @Override - protected void verifyContentId(Attachments attachmentWithExpectedContent, String attachmentId, String contentId) { - // Implementation not required for this test - } - - @Override - protected void verifyContentAndContentId(Attachments attachment, String testContent, Attachments itemAttachment) { - // Implementation not required for this test - } - - @Override - protected void verifyContentAndContentIdForAttachmentEntity(AttachmentEntity attachment, String testContent, - AttachmentEntity itemAttachment) { - // Implementation not required for this test - } - - @Override - public void verifySingleCreateAndUpdateEvent(String arg1, String arg2, String arg3) { - // Implementation not required for this test - } - - @Override - public void clearServiceHandlerContext() { - // Implementation not required for this test - } - - @Override - public void verifySingleReadEvent(String arg) { - // Implementation not required for this test - } - - @Override - public void verifyTwoDeleteEvents(AttachmentEntity entity, Attachments attachments) { - // Implementation not required for this test - } - - @Override - public void clearServiceHandlerDocuments() { - // Implementation not required for this test - } - - @Override - public void verifyEventContextEmptyForEvent(String... args) { - // Implementation not required for this test - } - - @Override - public void verifyNoAttachmentEventsCalled() { - // Implementation not required for this test - } - - @Override - public void verifyNumberOfEvents(String arg, int count) { - // Implementation not required for this test - } - - @Override - public void verifySingleCreateEvent(String arg1, String arg2) { - // Implementation not required for this test - } - - @Override - public void verifySingleDeletionEvent(String arg) { - // Implementation not required for this test - } + """, + fileName); + } + + // helper method + private Roots buildServiceRootWithMediaValidatedAttachments() { + return RootEntityBuilder.create() + .setTitle("Root with mediaValidatedAttachments") + .addMediaValidatedAttachments( + AttachmentsBuilder.create() + .setFileName("parent.text") + .setMimeType(MediaType.APPLICATION_JSON_VALUE)) + .build(); + } + + // Override abstract methods from OdataRequestValidationBase + + @Override + protected void executeContentRequestAndValidateContent(String url, String content) + throws Exception { + // Implementation not required for this test + } + + @Override + protected void verifyContentId( + Attachments attachmentWithExpectedContent, String attachmentId, String contentId) { + // Implementation not required for this test + } + + @Override + protected void verifyContentAndContentId( + Attachments attachment, String testContent, Attachments itemAttachment) { + // Implementation not required for this test + } + + @Override + protected void verifyContentAndContentIdForAttachmentEntity( + AttachmentEntity attachment, String testContent, AttachmentEntity itemAttachment) { + // Implementation not required for this test + } + + @Override + public void verifySingleCreateAndUpdateEvent(String arg1, String arg2, String arg3) { + // Implementation not required for this test + } + + @Override + public void clearServiceHandlerContext() { + // Implementation not required for this test + } + + @Override + public void verifySingleReadEvent(String arg) { + // Implementation not required for this test + } + + @Override + public void verifyTwoDeleteEvents(AttachmentEntity entity, Attachments attachments) { + // Implementation not required for this test + } + + @Override + public void clearServiceHandlerDocuments() { + // Implementation not required for this test + } + + @Override + public void verifyEventContextEmptyForEvent(String... args) { + // Implementation not required for this test + } + + @Override + public void verifyNoAttachmentEventsCalled() { + // Implementation not required for this test + } + + @Override + public void verifyNumberOfEvents(String arg, int count) { + // Implementation not required for this test + } + + @Override + public void verifySingleCreateEvent(String arg1, String arg2) { + // Implementation not required for this test + } + + @Override + public void verifySingleDeletionEvent(String arg) { + // Implementation not required for this test + } } diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/SizeLimitedAttachmentValidationNonDraftTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/SizeLimitedAttachmentValidationNonDraftTest.java index 8b23f711..9bc77e76 100644 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/SizeLimitedAttachmentValidationNonDraftTest.java +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/SizeLimitedAttachmentValidationNonDraftTest.java @@ -34,8 +34,9 @@ void uploadContentWithin5MBLimitSucceeds() throws Exception { // Act & Assert: Upload 3MB content (within limit) succeeds byte[] content = new byte[3 * 1024 * 1024]; // 3MB - var url = buildNavigationSizeLimitedAttachmentUrl(selectedRoot.getId(), attachment.getId()) - + "/content"; + var url = + buildNavigationSizeLimitedAttachmentUrl(selectedRoot.getId(), attachment.getId()) + + "/content"; requestHelper.setContentType(org.springframework.http.MediaType.APPLICATION_OCTET_STREAM); requestHelper.executePutWithMatcher(url, content, status().isNoContent()); } @@ -51,8 +52,9 @@ void uploadContentExceeding5MBLimitFails() throws Exception { attachment.setFileName("test.txt"); // Act: Try to upload 6MB content (exceeds limit) byte[] content = new byte[6 * 1024 * 1024]; // 6MB - var url = buildNavigationSizeLimitedAttachmentUrl(selectedRoot.getId(), attachment.getId()) - + "/content"; + var url = + buildNavigationSizeLimitedAttachmentUrl(selectedRoot.getId(), attachment.getId()) + + "/content"; requestHelper.setContentType(org.springframework.http.MediaType.APPLICATION_OCTET_STREAM); requestHelper.executePutWithMatcher(url, content, status().is(413)); @@ -70,9 +72,11 @@ private Roots buildServiceRootWithSizeLimitedAttachments() { } private Roots selectStoredRootWithSizeLimitedAttachments() { - var select = com.sap.cds.ql.Select.from( - com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.Roots_.class) - .columns(r -> r._all(), r -> r.sizeLimitedAttachments().expand()); + var select = + com.sap.cds.ql.Select.from( + com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.Roots_ + .class) + .columns(r -> r._all(), r -> r.sizeLimitedAttachments().expand()); var result = persistenceService.run(select); return result.single(Roots.class); From 2a3189d80faf80c9ca4af4d16ba49440caa7520f Mon Sep 17 00:00:00 2001 From: samyuktaprabhu <33195453+samyuktaprabhu@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:39:37 +0100 Subject: [PATCH 11/17] revert test changes --- samples/bookshop/srv/attachments.cds | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/samples/bookshop/srv/attachments.cds b/samples/bookshop/srv/attachments.cds index 9b42f8f2..1f453323 100644 --- a/samples/bookshop/srv/attachments.cds +++ b/samples/bookshop/srv/attachments.cds @@ -40,9 +40,5 @@ annotate adminService.Books with @(UI.Facets: [{ $Type : 'UI.ReferenceFacet', ID : 'AttachmentsFacet', Label : '{i18n>attachments}', - Target: 'mediaValidatedAttachments/@UI.LineItem' + Target: 'attachments/@UI.LineItem' }]); - -service nonDraft { - entity Books as projection on my.Books; -} From 6951e860d5e9c8be084f48749f98a43e2a813c72 Mon Sep 17 00:00:00 2001 From: samyuktaprabhu <33195453+samyuktaprabhu@users.noreply.github.com> Date: Wed, 18 Feb 2026 15:58:20 +0100 Subject: [PATCH 12/17] review comments fixes --- .../helper/AttachmentValidationHelper.java | 6 ++++- .../AttachmentValidationHelperTest.java | 5 ----- .../MediaValidatedAttachmentsDraftTest.java | 22 ++++++------------- ...MediaValidatedAttachmentsNonDraftTest.java | 14 +++++------- 4 files changed, 18 insertions(+), 29 deletions(-) diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelper.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelper.java index 0bf5c166..9763b09e 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelper.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelper.java @@ -12,7 +12,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class AttachmentValidationHelper { +public final class AttachmentValidationHelper { public static final String DEFAULT_MEDIA_TYPE = "application/octet-stream"; public static final Map EXT_TO_MEDIA_TYPE = @@ -162,4 +162,8 @@ protected static boolean checkMimeTypeMatch( : baseMimeType.equals(normalizedType); }); } + + private AttachmentValidationHelper() { + // prevent instantiation + } } diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelperTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelperTest.java index 4fc333d8..afe9bf53 100644 --- a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelperTest.java +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelperTest.java @@ -12,11 +12,6 @@ class AttachmentValidationHelperTest { - @Test - void constructor_shouldBeCallable_forCoverage() { - new AttachmentValidationHelper(); - } - @Test void shouldDetectMimeTypeFromURLConnection() { assertMediaType("document.pdf", List.of("application/pdf"), "application/pdf"); diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/MediaValidatedAttachmentsDraftTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/MediaValidatedAttachmentsDraftTest.java index 3447bb20..3ec5fbbb 100644 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/MediaValidatedAttachmentsDraftTest.java +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/MediaValidatedAttachmentsDraftTest.java @@ -6,6 +6,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.fasterxml.jackson.databind.ObjectMapper; import com.sap.cds.CdsData; import com.sap.cds.Struct; import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testdraftservice.DraftRoots; @@ -13,6 +14,7 @@ import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; import java.io.IOException; import java.io.InputStream; +import java.util.Map; import java.util.Objects; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; @@ -25,6 +27,7 @@ public class MediaValidatedAttachmentsDraftTest extends DraftOdataRequestValidat private static final String BASE_URL = MockHttpRequestHelper.ODATA_BASE_URL + "TestDraftService/"; private static final String BASE_ROOT_URL = BASE_URL + "DraftRoots"; + private static final ObjectMapper objectMapper = new ObjectMapper(); @BeforeEach void setup() { @@ -35,15 +38,8 @@ void setup() { @CsvSource({"test.png,201", "test.jpeg,201", "test.pdf,415", "test.txt,415"}) void shouldValidateMediaType_whenCreatingAttachmentInDraft(String fileName, int expectedStatus) throws Exception { - String rootId = createDraftRootAndReturnId(); - String metadata = - """ - { - "fileName": "%s" - } - """ - .formatted(fileName); + String metadata = objectMapper.writeValueAsString(Map.of("fileName", fileName)); requestHelper.executePostWithMatcher( buildDraftAttachmentCreationUrl(rootId), metadata, status().is(expectedStatus)); @@ -63,14 +59,10 @@ private String createDraftRootAndReturnId() throws Exception { requestHelper.executePostWithODataResponseAndAssertStatusCreated(BASE_ROOT_URL, "{}"); DraftRoots draftRoot = Struct.access(response).as(DraftRoots.class); - + String payload = + objectMapper.writeValueAsString(Map.of("title", "Draft with mediaValidatedAttachments")); requestHelper.executePatchWithODataResponseAndAssertStatusOk( - getRootUrl(draftRoot.getId(), false), - """ - { - "title": "Draft with mediaValidatedAttachments" - } - """); + getRootUrl(draftRoot.getId(), false), payload); return draftRoot.getId(); } diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java index 04be3621..1c2f4203 100644 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java @@ -5,6 +5,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import com.sap.cds.Result; import com.sap.cds.feature.attachments.generated.integration.test.cds4j.sap.attachments.Attachments; import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.AttachmentEntity; @@ -15,6 +17,7 @@ import com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper.AttachmentsBuilder; import com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper.RootEntityBuilder; import com.sap.cds.ql.Select; +import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -26,6 +29,7 @@ class MediaValidatedAttachmentsNonDraftTest extends OdataRequestValidationBase { private static final String BASE_URL = MockHttpRequestHelper.ODATA_BASE_URL + "TestService/Roots"; private static final String MEDIA_VALIDATED_ATTACHMENTS = "mediaValidatedAttachments"; + private static final ObjectMapper objectMapper = new ObjectMapper(); protected void postServiceRoot(Roots serviceRoot) throws Exception { String url = MockHttpRequestHelper.ODATA_BASE_URL + "TestService/Roots"; @@ -93,14 +97,8 @@ private String createUrl(String rootId, String path) { return BASE_URL + "(" + rootId + ")/" + path; } - private String createAttachmentMetadata(String fileName) { - return String.format( - """ - { - "fileName": "%s" - } - """, - fileName); + private String createAttachmentMetadata(String fileName) throws JsonProcessingException { + return objectMapper.writeValueAsString(Map.of("fileName", fileName)); } // helper method From 277df0546b8c91246a094875a9a4b37f10201038 Mon Sep 17 00:00:00 2001 From: samyuktaprabhu <33195453+samyuktaprabhu@users.noreply.github.com> Date: Wed, 18 Feb 2026 16:27:39 +0100 Subject: [PATCH 13/17] add tests --- .../AttachmentValidationHelperTest.java | 20 ++++++++++ .../MediaValidatedAttachmentsDraftTest.java | 21 +++++++++- ...MediaValidatedAttachmentsNonDraftTest.java | 40 +++++++++++++++++++ 3 files changed, 80 insertions(+), 1 deletion(-) diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelperTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelperTest.java index afe9bf53..a4cc137f 100644 --- a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelperTest.java +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelperTest.java @@ -125,6 +125,18 @@ void shouldThrowWhenFileNameIsEmpty() { assertEquals(ErrorStatuses.UNSUPPORTED_MEDIA_TYPE, ex.getErrorStatus()); } + @Test + void shouldThrowWhenMimeTypeDoesNotMatchWildcard() { + ServiceException ex = + assertThrows( + ServiceException.class, + () -> + AttachmentValidationHelper.validateMediaTypeForAttachment( + "test.pdf", List.of("image/*"))); + + assertEquals(ErrorStatuses.UNSUPPORTED_MEDIA_TYPE, ex.getErrorStatus()); + } + @Test void shouldThrowWhenFileNameIsBlank() { ServiceException ex = @@ -160,4 +172,12 @@ private void assertUnsupported(String fileName, List allowed) { assertEquals(ErrorStatuses.UNSUPPORTED_MEDIA_TYPE, ex.getErrorStatus()); } + + @Test + void shouldHandleDotFiles() { + assertUnsupported(".gitignore", List.of("text/plain", "application/octet-stream")); + assertUnsupported(".ssh", List.of("application/octet-stream")); + assertUnsupported(".dockerignore", List.of("text/plain", "application/octet-stream")); + assertUnsupported(".invalid.ext", List.of("application/pdf")); + } } diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/MediaValidatedAttachmentsDraftTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/MediaValidatedAttachmentsDraftTest.java index 3ec5fbbb..43ac22b4 100644 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/MediaValidatedAttachmentsDraftTest.java +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/MediaValidatedAttachmentsDraftTest.java @@ -17,6 +17,7 @@ import java.util.Map; import java.util.Objects; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.springframework.http.MediaType; @@ -35,7 +36,17 @@ void setup() { } @ParameterizedTest - @CsvSource({"test.png,201", "test.jpeg,201", "test.pdf,415", "test.txt,415"}) + @CsvSource({ + "test.png,201", + "test.jpeg,201", + "test.pdf,415", + "test.txt,415", + "'',400", + "' ',400", + ".gitignore,415", + ".env,415", + ".hiddenfile,415" + }) void shouldValidateMediaType_whenCreatingAttachmentInDraft(String fileName, int expectedStatus) throws Exception { String rootId = createDraftRootAndReturnId(); @@ -53,6 +64,14 @@ private String buildDraftAttachmentCreationUrl(String rootId) { + "/mediaValidatedAttachments"; } + @Test + void shouldFail_whenFileNameMissing_inDraft() throws Exception { + String rootId = createDraftRootAndReturnId(); + String metadata = "{}"; + requestHelper.executePostWithMatcher( + buildDraftAttachmentCreationUrl(rootId), metadata, status().isBadRequest()); + } + // Helper methods private String createDraftRootAndReturnId() throws Exception { CdsData response = diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java index 1c2f4203..26de4e65 100644 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java @@ -80,6 +80,46 @@ void shouldRejectAttachment_whenFileNameIsEmpty() throws Exception { status().isBadRequest()); } + @Test + void shouldAcceptUppercaseExtension_whenMimeTypeIsAllowed() throws Exception { + String rootId = createRootAndReturnId(); + String attachmentMetadata = createAttachmentMetadata("IMAGE.JPG"); + + requestHelper.executePostWithMatcher( + createUrl(rootId, MEDIA_VALIDATED_ATTACHMENTS), attachmentMetadata, status().isCreated()); + } + + @Test + void shouldAcceptMixedCaseExtension() throws Exception { + String rootId = createRootAndReturnId(); + String attachmentMetadata = createAttachmentMetadata("image.JpEg"); + + requestHelper.executePostWithMatcher( + createUrl(rootId, MEDIA_VALIDATED_ATTACHMENTS), attachmentMetadata, status().isCreated()); + } + + @Test + void shouldRejectAttachment_whenFileHasNoExtension() throws Exception { + String rootId = createRootAndReturnId(); + String attachmentMetadata = createAttachmentMetadata("filename"); + + requestHelper.executePostWithMatcher( + createUrl(rootId, MEDIA_VALIDATED_ATTACHMENTS), + attachmentMetadata, + status().isUnsupportedMediaType()); + } + + @Test + void shouldRejectHiddenFile_whenFileStartsWithDot() throws Exception { + String rootId = createRootAndReturnId(); + String attachmentMetadata = createAttachmentMetadata(".gitignore"); + + requestHelper.executePostWithMatcher( + createUrl(rootId, MEDIA_VALIDATED_ATTACHMENTS), + attachmentMetadata, + status().isUnsupportedMediaType()); + } + private String createRootAndReturnId() throws Exception { // Build the initial Java object.. Root Roots serviceRoot = buildServiceRootWithMediaValidatedAttachments(); From c012fc882b852930dc33de011a482e713fbb1f39 Mon Sep 17 00:00:00 2001 From: samyuktaprabhu <33195453+samyuktaprabhu@users.noreply.github.com> Date: Wed, 18 Feb 2026 17:18:07 +0100 Subject: [PATCH 14/17] swap a couple of conditions --- .../helper/AttachmentValidationHelper.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelper.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelper.java index 9763b09e..bbcdfafe 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelper.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelper.java @@ -124,11 +124,12 @@ private static void validateAcceptableMediaType( } private static String resolveMimeType(String fileName) { - String actualMimeType = URLConnection.guessContentTypeFromName(fileName); + String fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase(); + String actualMimeType = EXT_TO_MEDIA_TYPE.get(fileExtension); if (actualMimeType == null) { - String fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase(); - actualMimeType = EXT_TO_MEDIA_TYPE.get(fileExtension); + + actualMimeType = URLConnection.guessContentTypeFromName(fileName); if (actualMimeType == null) { logger.warn( From fc97d830889f92fcec840856a24c13b90a13e95b Mon Sep 17 00:00:00 2001 From: samyuktaprabhu <33195453+samyuktaprabhu@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:51:11 +0100 Subject: [PATCH 15/17] update the header copyrights comment --- .../sap/cds/feature/attachments/configuration/Registration.java | 2 +- .../handler/common/ApplicationHandlerHelperTest.java | 2 +- 2 files changed, 2 insertions(+), 2 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 9f5d4fcf..5fce2d0e 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 @@ -1,5 +1,5 @@ /* - * © 2024-2025 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + * © 2024-2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. */ package com.sap.cds.feature.attachments.configuration; diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelperTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelperTest.java index 4b6e1684..ff87db66 100644 --- a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelperTest.java +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelperTest.java @@ -1,5 +1,5 @@ /* - * © 2024-2025 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + * © 2024-2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. */ package com.sap.cds.feature.attachments.handler.common; From 6eff985603691a151d5f773bd0354e7b9c4bf150 Mon Sep 17 00:00:00 2001 From: samyuktaprabhu <33195453+samyuktaprabhu@users.noreply.github.com> Date: Thu, 19 Feb 2026 13:51:05 +0100 Subject: [PATCH 16/17] move methods to another file --- .../CreateAttachmentsHandler.java | 3 +- .../helper/AttachmentValidationHelper.java | 74 +++++- .../common/ApplicationHandlerHelper.java | 68 ----- .../CreateAttachmentsHandlerTest.java | 11 +- .../AttachmentValidationHelperTest.java | 233 +++++++++++++++++- .../common/ApplicationHandlerHelperTest.java | 222 ----------------- 6 files changed, 307 insertions(+), 304 deletions(-) diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/CreateAttachmentsHandler.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/CreateAttachmentsHandler.java index 8d3a3464..b43f105a 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/CreateAttachmentsHandler.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/CreateAttachmentsHandler.java @@ -6,6 +6,7 @@ import static java.util.Objects.requireNonNull; import com.sap.cds.CdsData; +import com.sap.cds.feature.attachments.handler.applicationservice.helper.AttachmentValidationHelper; import com.sap.cds.feature.attachments.handler.applicationservice.helper.ExtendedErrorStatuses; import com.sap.cds.feature.attachments.handler.applicationservice.helper.ModifyApplicationHandlerHelper; import com.sap.cds.feature.attachments.handler.applicationservice.helper.ReadonlyDataContextEnhancer; @@ -70,7 +71,7 @@ void processBeforeForDraft(CdsCreateEventContext context, List data) { @HandlerOrder(HandlerOrder.BEFORE) void processBeforeForMetadata(EventContext context, List data) { CdsEntity target = context.getTarget(); - ApplicationHandlerHelper.validateAcceptableMediaTypes(target, data, cdsRuntime); + AttachmentValidationHelper.validateAcceptableMediaTypes(target, data, cdsRuntime); } @Before diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelper.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelper.java index bbcdfafe..3973555b 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelper.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelper.java @@ -3,12 +3,25 @@ */ package com.sap.cds.feature.attachments.handler.applicationservice.helper; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sap.cds.CdsData; +import com.sap.cds.CdsDataProcessor; +import com.sap.cds.CdsDataProcessor.Filter; +import com.sap.cds.CdsDataProcessor.Validator; +import com.sap.cds.feature.attachments.handler.common.ApplicationHandlerHelper; +import com.sap.cds.reflect.CdsAnnotation; +import com.sap.cds.reflect.CdsEntity; +import com.sap.cds.reflect.CdsModel; import com.sap.cds.services.ErrorStatuses; import com.sap.cds.services.ServiceException; +import com.sap.cds.services.runtime.CdsRuntime; import java.net.URLConnection; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -81,16 +94,65 @@ public final class AttachmentValidationHelper { Map.entry("webp", "image/webp")); private static final Logger logger = LoggerFactory.getLogger(AttachmentValidationHelper.class); + private static final ObjectMapper objectMapper = new ObjectMapper(); + private static final TypeReference> STRING_LIST_TYPE_REF = new TypeReference<>() {}; + + /** Filter to support extraction of file name for attachment validation */ + public static final Filter FILE_NAME_FILTER = + (path, element, type) -> element.getName().contentEquals("fileName"); /** - * Validates the file name and resolves its media type. Ensures that the detected media type is - * part of the list of acceptable media types. + * Validates if the media type of the attachment in the given fileName is acceptable * - * @param fileName the name of the attachment file - * @param acceptableMediaTypes list of allowed media types (e.g. "image/*", "application/pdf") - * @return the detected media type - * @throws ServiceException if the file name is invalid or the media type is not allowed + * @param entity the {@link CdsEntity entity} type of the given data + * @param data the list of {@link CdsData} to process + * @throws ServiceException if the media type of the attachment is not acceptable */ + public static void validateAcceptableMediaTypes( + CdsEntity entity, List data, CdsRuntime cdsRuntime) { + if (entity == null) { + return; + } + CdsModel cdsModel = cdsRuntime.getCdsModel(); + CdsEntity serviceEntity = cdsModel.findEntity(entity.getQualifiedName()).orElse(null); + if (serviceEntity == null || !ApplicationHandlerHelper.isMediaEntity(serviceEntity)) { + return; + } + List allowedTypes = getEntityAcceptableMediaTypes(serviceEntity); + String fileName = extractFileName(entity, data); + validateMediaTypeForAttachment(fileName, allowedTypes); + } + + protected static List getEntityAcceptableMediaTypes(CdsEntity entity) { + Optional> flatMap = + entity.getElement("content").findAnnotation("Core.AcceptableMediaTypes"); + List result = + flatMap + .map( + annotation -> + objectMapper.convertValue(annotation.getValue(), STRING_LIST_TYPE_REF)) + .orElse(List.of("*/*")); + return result; + } + + protected static String extractFileName(CdsEntity entity, List data) { + CdsDataProcessor processor = CdsDataProcessor.create(); + AtomicReference fileNameRef = new AtomicReference<>(); + Validator validator = + (path, element, value) -> { + if (element.getName().contentEquals("fileName") && value instanceof String) { + fileNameRef.set((String) value); + } + }; + + processor.addValidator(FILE_NAME_FILTER, validator).process(data, entity); + + if (fileNameRef.get() == null || fileNameRef.get().isBlank()) { + throw new ServiceException(ErrorStatuses.BAD_REQUEST, "Filename is missing"); + } + return fileNameRef.get(); + } + public static String validateMediaTypeForAttachment( String fileName, List acceptableMediaTypes) { validateFileName(fileName); diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelper.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelper.java index f70c671a..c8308f51 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelper.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelper.java @@ -5,29 +5,19 @@ import static java.util.Objects.nonNull; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; import com.sap.cds.CdsData; import com.sap.cds.CdsDataProcessor; import com.sap.cds.CdsDataProcessor.Filter; import com.sap.cds.CdsDataProcessor.Validator; import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.Attachments; -import com.sap.cds.feature.attachments.handler.applicationservice.helper.AttachmentValidationHelper; -import com.sap.cds.reflect.CdsAnnotation; import com.sap.cds.reflect.CdsEntity; -import com.sap.cds.reflect.CdsModel; import com.sap.cds.reflect.CdsStructuredType; -import com.sap.cds.services.ErrorStatuses; -import com.sap.cds.services.ServiceException; import com.sap.cds.services.draft.Drafts; -import com.sap.cds.services.runtime.CdsRuntime; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; /** * The class {@link ApplicationHandlerHelper} provides helper methods for the attachment application @@ -36,12 +26,6 @@ public final class ApplicationHandlerHelper { private static final String ANNOTATION_IS_MEDIA_DATA = "_is_media_data"; private static final String ANNOTATION_CORE_MEDIA_TYPE = "Core.MediaType"; - private static final ObjectMapper objectMapper = new ObjectMapper(); - private static final TypeReference> STRING_LIST_TYPE_REF = new TypeReference<>() {}; - - /** Filter to support extraction of file name for attachment validation */ - public static final Filter FILE_NAME_FILTER = - (path, element, type) -> element.getName().contentEquals("fileName"); /** * A filter for media content fields. The filter checks if the entity is a media entity and if the @@ -139,58 +123,6 @@ public static Map removeDraftKey(Map keys) { return keyMap; } - /** - * Validates if the media type of the attachment in the given fileName is acceptable - * - * @param entity the {@link CdsEntity entity} type of the given data - * @param data the list of {@link CdsData} to process - * @throws ServiceException if the media type of the attachment is not acceptable - */ - public static void validateAcceptableMediaTypes( - CdsEntity entity, List data, CdsRuntime cdsRuntime) { - if (entity == null) { - return; - } - CdsModel cdsModel = cdsRuntime.getCdsModel(); - CdsEntity serviceEntity = cdsModel.findEntity(entity.getQualifiedName()).orElse(null); - if (serviceEntity == null || !isMediaEntity(serviceEntity)) { - return; - } - List allowedTypes = getEntityAcceptableMediaTypes(serviceEntity); - String fileName = extractFileName(entity, data); - AttachmentValidationHelper.validateMediaTypeForAttachment(fileName, allowedTypes); - } - - protected static List getEntityAcceptableMediaTypes(CdsEntity entity) { - Optional> flatMap = - entity.getElement("content").findAnnotation("Core.AcceptableMediaTypes"); - List result = - flatMap - .map( - annotation -> - objectMapper.convertValue(annotation.getValue(), STRING_LIST_TYPE_REF)) - .orElse(List.of("*/*")); - return result; - } - - protected static String extractFileName(CdsEntity entity, List data) { - CdsDataProcessor processor = CdsDataProcessor.create(); - AtomicReference fileNameRef = new AtomicReference<>(); - Validator validator = - (path, element, value) -> { - if (element.getName().contentEquals("fileName") && value instanceof String) { - fileNameRef.set((String) value); - } - }; - - processor.addValidator(FILE_NAME_FILTER, validator).process(data, entity); - - if (fileNameRef.get() == null || fileNameRef.get().isBlank()) { - throw new ServiceException(ErrorStatuses.BAD_REQUEST, "Filename is missing"); - } - return fileNameRef.get(); - } - private ApplicationHandlerHelper() { // avoid instantiation } diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/CreateAttachmentsHandlerTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/CreateAttachmentsHandlerTest.java index 437f3f3b..28b1fa67 100644 --- a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/CreateAttachmentsHandlerTest.java +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/CreateAttachmentsHandlerTest.java @@ -25,13 +25,13 @@ import com.sap.cds.feature.attachments.generated.test.cds4j.unit.test.testservice.Items; import com.sap.cds.feature.attachments.generated.test.cds4j.unit.test.testservice.RootTable; import com.sap.cds.feature.attachments.generated.test.cds4j.unit.test.testservice.RootTable_; +import com.sap.cds.feature.attachments.handler.applicationservice.helper.AttachmentValidationHelper; import com.sap.cds.feature.attachments.handler.applicationservice.helper.ExtendedErrorStatuses; import com.sap.cds.feature.attachments.handler.applicationservice.helper.ModifyApplicationHandlerHelper; import com.sap.cds.feature.attachments.handler.applicationservice.helper.ThreadDataStorageReader; import com.sap.cds.feature.attachments.handler.applicationservice.modifyevents.ModifyAttachmentEvent; import com.sap.cds.feature.attachments.handler.applicationservice.modifyevents.ModifyAttachmentEventFactory; import com.sap.cds.feature.attachments.handler.applicationservice.readhelper.CountingInputStream; -import com.sap.cds.feature.attachments.handler.common.ApplicationHandlerHelper; import com.sap.cds.feature.attachments.handler.helper.RuntimeHelper; import com.sap.cds.reflect.CdsEntity; import com.sap.cds.services.ErrorStatuses; @@ -411,17 +411,18 @@ void processBeforeForMetadata_executesValidation() { List data = List.of(mock(CdsData.class)); when(context.getTarget()).thenReturn(entity); - try (MockedStatic helper = - mockStatic(ApplicationHandlerHelper.class)) { + try (MockedStatic helper = + mockStatic(AttachmentValidationHelper.class)) { helper - .when(() -> ApplicationHandlerHelper.validateAcceptableMediaTypes(entity, data, runtime)) + .when( + () -> AttachmentValidationHelper.validateAcceptableMediaTypes(entity, data, runtime)) .thenAnswer(invocation -> null); // when new CreateAttachmentsHandler(eventFactory, storageReader, "400MB", runtime) .processBeforeForMetadata(context, data); // then helper.verify( - () -> ApplicationHandlerHelper.validateAcceptableMediaTypes(entity, data, runtime)); + () -> AttachmentValidationHelper.validateAcceptableMediaTypes(entity, data, runtime)); } } diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelperTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelperTest.java index a4cc137f..b004b35f 100644 --- a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelperTest.java +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelperTest.java @@ -3,14 +3,45 @@ */ package com.sap.cds.feature.attachments.handler.applicationservice.helper; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; - +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import com.sap.cds.CdsData; +import com.sap.cds.CdsDataProcessor; +import com.sap.cds.CdsDataProcessor.Filter; +import com.sap.cds.CdsDataProcessor.Validator; +import com.sap.cds.feature.attachments.handler.common.ApplicationHandlerHelper; +import com.sap.cds.reflect.CdsAnnotation; +import com.sap.cds.reflect.CdsElement; +import com.sap.cds.reflect.CdsEntity; +import com.sap.cds.reflect.CdsModel; import com.sap.cds.services.ErrorStatuses; import com.sap.cds.services.ServiceException; -import java.util.List; +import com.sap.cds.services.runtime.CdsRuntime; +import java.util.*; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.MockitoAnnotations; class AttachmentValidationHelperTest { + @Mock private CdsEntity entity; + @Mock private CdsData data; + @Mock private CdsRuntime cdsRuntime; + @Mock private CdsModel cdsModel; + @Mock private CdsEntity serviceEntity; + @Mock private CdsElement cdsElement; + @Mock private CdsAnnotation annotation; + @Mock private CdsDataProcessor processor; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + when(cdsRuntime.getCdsModel()).thenReturn(cdsModel); + } @Test void shouldDetectMimeTypeFromURLConnection() { @@ -180,4 +211,202 @@ void shouldHandleDotFiles() { assertUnsupported(".dockerignore", List.of("text/plain", "application/octet-stream")); assertUnsupported(".invalid.ext", List.of("application/pdf")); } + + @Test + void validateAcceptableMediaTypes_shouldReturnWhenEntityIsNull() { + // given + CdsRuntime runtime = mock(CdsRuntime.class); + + // when / then + assertDoesNotThrow( + () -> AttachmentValidationHelper.validateAcceptableMediaTypes(null, List.of(), runtime)); + + // ensure no further interaction happens + verifyNoInteractions(runtime); + } + + @Test + void validateAcceptableMediaTypes_whenNotMediaEntity_returns() { + when(entity.getQualifiedName()).thenReturn("Test.Entity"); + when(cdsModel.findEntity("Test.Entity")).thenReturn(Optional.of(serviceEntity)); + + // IMPORTANT: force isMediaEntity = false + try (MockedStatic mocked = + mockStatic(ApplicationHandlerHelper.class)) { + + mocked.when(() -> ApplicationHandlerHelper.isMediaEntity(serviceEntity)).thenReturn(false); + + assertDoesNotThrow( + () -> + AttachmentValidationHelper.validateAcceptableMediaTypes( + entity, List.of(data), cdsRuntime)); + } + } + + @Test + void getEntityAcceptableMediaTypes_returnsAnnotationValue() { + List expectedTypes = List.of("image/png", "application/pdf"); + + when(entity.getElement("content")).thenReturn(cdsElement); + when(cdsElement.findAnnotation("Core.AcceptableMediaTypes")) + .thenReturn(Optional.of(annotation)); + when(annotation.getValue()).thenReturn(expectedTypes); + assertEquals(expectedTypes, AttachmentValidationHelper.getEntityAcceptableMediaTypes(entity)); + } + + @Test + void getEntityAcceptableMediaTypes_missingAnnotation_returnsWildcard() { + when(entity.getElement("content")).thenReturn(cdsElement); + when(cdsElement.findAnnotation("Core.AcceptableMediaTypes")).thenReturn(Optional.empty()); + assertEquals(List.of("*/*"), AttachmentValidationHelper.getEntityAcceptableMediaTypes(entity)); + } + + @Test + void getEntityAcceptableMediaTypes_nullAnnotationValue_returnsWildcard() { + when(entity.getElement("content")).thenReturn(cdsElement); + when(cdsElement.findAnnotation("Core.AcceptableMediaTypes")) + .thenReturn(Optional.of(annotation)); + when(annotation.getValue()).thenReturn(null); + assertEquals(List.of("*/*"), AttachmentValidationHelper.getEntityAcceptableMediaTypes(entity)); + } + + @Test + void extractFileName_whenFileNamePresent_returnsValue() { + when(cdsElement.getName()).thenReturn("fileName"); + try (MockedStatic mocked = mockStatic(CdsDataProcessor.class)) { + mocked.when(CdsDataProcessor::create).thenReturn(processor); + doAnswer( + invocation -> { + Filter filter = invocation.getArgument(0); + Validator validator = invocation.getArgument(1); + + // Simulate processor visiting fileName with String value + if (filter.test(null, cdsElement, null)) { + validator.validate(null, cdsElement, "test.pdf"); + } + return processor; + }) + .when(processor) + .addValidator(any(), any()); + + doNothing().when(processor).process(anyList(), any()); + String result = AttachmentValidationHelper.extractFileName(entity, List.of(data)); + assertThat(result).isEqualTo("test.pdf"); + } + } + + @Test + void extractFileName_whenElementIsNotFileName_throws() { + when(cdsElement.getName()).thenReturn("content"); + try (MockedStatic mocked = mockStatic(CdsDataProcessor.class)) { + mocked.when(CdsDataProcessor::create).thenReturn(processor); + doAnswer( + invocation -> { + Filter filter = invocation.getArgument(0); + Validator validator = invocation.getArgument(1); + if (filter.test(null, cdsElement, null)) { + validator.validate(null, cdsElement, "test.pdf"); + } + return processor; + }) + .when(processor) + .addValidator(any(), any()); + + doNothing().when(processor).process(anyList(), any()); + assertThrows( + ServiceException.class, + () -> AttachmentValidationHelper.extractFileName(entity, List.of(data))); + } + } + + @Test + void extractFileName_valueIsNotString_branchCovered() { + when(cdsElement.getName()).thenReturn("fileName"); + try (MockedStatic mocked = mockStatic(CdsDataProcessor.class)) { + mocked.when(CdsDataProcessor::create).thenReturn(processor); + doAnswer( + invocation -> { + Filter filter = invocation.getArgument(0); + Validator validator = invocation.getArgument(1); + if (filter.test(null, cdsElement, null)) { + validator.validate(null, cdsElement, 42); // non-String + } + return processor; + }) + .when(processor) + .addValidator(any(), any()); + doNothing().when(processor).process(anyList(), any()); + + assertThrows( + ServiceException.class, + () -> AttachmentValidationHelper.extractFileName(entity, List.of(data))); + } + } + + @Test + void validateAcceptableMediaTypes_nonMediaOrMissingEntity_doesNothing() { + when(entity.getQualifiedName()).thenReturn("TestService.Roots"); + when(cdsModel.findEntity("TestService.Roots")).thenReturn(Optional.empty()); + + assertDoesNotThrow( + () -> + AttachmentValidationHelper.validateAcceptableMediaTypes(entity, List.of(), cdsRuntime)); + } + + @Test + void shouldNotThrowWhenEntityNotFoundInModel() { + when(entity.getQualifiedName()).thenReturn("Test.Entity"); + when(cdsModel.findEntity("Test.Entity")).thenReturn(Optional.empty()); + + assertDoesNotThrow( + () -> + AttachmentValidationHelper.validateAcceptableMediaTypes(entity, List.of(), cdsRuntime)); + } + + @Test + void validateAcceptableMediaTypes_mediaTypeMatches_succeeds() { + setupMediaEntity(List.of("image/png")); + try (MockedStatic helperStatic = + mockStatic(AttachmentValidationHelper.class, CALLS_REAL_METHODS)) { + + // file.png → media type image/png → allowed + helperStatic + .when(() -> AttachmentValidationHelper.extractFileName(entity, List.of(data))) + .thenReturn("file.png"); + + assertDoesNotThrow( + () -> + AttachmentValidationHelper.validateAcceptableMediaTypes( + entity, List.of(data), cdsRuntime)); + } + } + + @Test + void validateAcceptableMediaTypes_mediaTypeMismatch_throws() { + setupMediaEntity(List.of("image/png")); + try (MockedStatic helperStatic = + mockStatic(AttachmentValidationHelper.class, CALLS_REAL_METHODS)) { + + // file.jpg → media type image/jpeg → NOT allowed + helperStatic + .when(() -> AttachmentValidationHelper.extractFileName(entity, List.of(data))) + .thenReturn("file.jpg"); + + assertThrows( + ServiceException.class, + () -> + AttachmentValidationHelper.validateAcceptableMediaTypes( + entity, List.of(data), cdsRuntime)); + } + } + + private void setupMediaEntity(List allowedTypes) { + when(entity.getQualifiedName()).thenReturn("Test.Entity"); + when(cdsModel.findEntity("Test.Entity")).thenReturn(Optional.of(serviceEntity)); + when(serviceEntity.getAnnotationValue("_is_media_data", false)).thenReturn(true); + when(serviceEntity.getElement("content")).thenReturn(cdsElement); + when(cdsElement.findAnnotation("Core.AcceptableMediaTypes")) + .thenReturn(Optional.of(annotation)); + when(annotation.getValue()).thenReturn(allowedTypes); + } } diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelperTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelperTest.java index ff87db66..3c44c320 100644 --- a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelperTest.java +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/common/ApplicationHandlerHelperTest.java @@ -5,44 +5,13 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; import com.sap.cds.CdsData; -import com.sap.cds.CdsDataProcessor; -import com.sap.cds.CdsDataProcessor.Filter; -import com.sap.cds.CdsDataProcessor.Validator; -import com.sap.cds.feature.attachments.handler.applicationservice.helper.AttachmentValidationHelper; -import com.sap.cds.reflect.CdsAnnotation; -import com.sap.cds.reflect.CdsElement; -import com.sap.cds.reflect.CdsEntity; -import com.sap.cds.reflect.CdsModel; -import com.sap.cds.services.ServiceException; -import com.sap.cds.services.runtime.CdsRuntime; import java.util.*; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.mockito.MockedStatic; -import org.mockito.MockitoAnnotations; class ApplicationHandlerHelperTest { - @Mock private CdsEntity entity; - @Mock private CdsData data; - @Mock private CdsRuntime cdsRuntime; - @Mock private CdsModel cdsModel; - @Mock private CdsEntity serviceEntity; - @Mock private CdsElement cdsElement; - @Mock private CdsAnnotation annotation; - @Mock private CdsDataProcessor processor; - - @BeforeEach - void setUp() { - MockitoAnnotations.openMocks(this); - when(cdsRuntime.getCdsModel()).thenReturn(cdsModel); - } - @Test void keysAreInData() { Map keys = Map.of("key1", "value1", "key2", "value2"); @@ -87,195 +56,4 @@ void removeDraftKey() { assertFalse(result.containsKey("IsActiveEntity")); assertTrue(result.containsKey("key1")); } - - @Test - void validateAcceptableMediaTypes_shouldReturnWhenEntityIsNull() { - // given - CdsRuntime runtime = mock(CdsRuntime.class); - - // when / then - assertDoesNotThrow( - () -> ApplicationHandlerHelper.validateAcceptableMediaTypes(null, List.of(), runtime)); - - // ensure no further interaction happens - verifyNoInteractions(runtime); - } - - @Test - void getEntityAcceptableMediaTypes_returnsAnnotationValue() { - List expectedTypes = List.of("image/png", "application/pdf"); - - when(entity.getElement("content")).thenReturn(cdsElement); - when(cdsElement.findAnnotation("Core.AcceptableMediaTypes")) - .thenReturn(Optional.of(annotation)); - when(annotation.getValue()).thenReturn(expectedTypes); - assertEquals(expectedTypes, ApplicationHandlerHelper.getEntityAcceptableMediaTypes(entity)); - } - - @Test - void getEntityAcceptableMediaTypes_missingAnnotation_returnsWildcard() { - when(entity.getElement("content")).thenReturn(cdsElement); - when(cdsElement.findAnnotation("Core.AcceptableMediaTypes")).thenReturn(Optional.empty()); - assertEquals(List.of("*/*"), ApplicationHandlerHelper.getEntityAcceptableMediaTypes(entity)); - } - - @Test - void getEntityAcceptableMediaTypes_nullAnnotationValue_returnsWildcard() { - when(entity.getElement("content")).thenReturn(cdsElement); - when(cdsElement.findAnnotation("Core.AcceptableMediaTypes")) - .thenReturn(Optional.of(annotation)); - when(annotation.getValue()).thenReturn(null); - assertEquals(List.of("*/*"), ApplicationHandlerHelper.getEntityAcceptableMediaTypes(entity)); - } - - @Test - void extractFileName_whenFileNamePresent_returnsValue() { - when(cdsElement.getName()).thenReturn("fileName"); - try (MockedStatic mocked = mockStatic(CdsDataProcessor.class)) { - mocked.when(CdsDataProcessor::create).thenReturn(processor); - doAnswer( - invocation -> { - Filter filter = invocation.getArgument(0); - Validator validator = invocation.getArgument(1); - - // Simulate processor visiting fileName with String value - if (filter.test(null, cdsElement, null)) { - validator.validate(null, cdsElement, "test.pdf"); - } - return processor; - }) - .when(processor) - .addValidator(any(), any()); - - doNothing().when(processor).process(anyList(), any()); - String result = ApplicationHandlerHelper.extractFileName(entity, List.of(data)); - assertThat(result).isEqualTo("test.pdf"); - } - } - - @Test - void extractFileName_whenElementIsNotFileName_throws() { - when(cdsElement.getName()).thenReturn("content"); - try (MockedStatic mocked = mockStatic(CdsDataProcessor.class)) { - mocked.when(CdsDataProcessor::create).thenReturn(processor); - doAnswer( - invocation -> { - Filter filter = invocation.getArgument(0); - Validator validator = invocation.getArgument(1); - if (filter.test(null, cdsElement, null)) { - validator.validate(null, cdsElement, "test.pdf"); - } - return processor; - }) - .when(processor) - .addValidator(any(), any()); - - doNothing().when(processor).process(anyList(), any()); - assertThrows( - ServiceException.class, - () -> ApplicationHandlerHelper.extractFileName(entity, List.of(data))); - } - } - - @Test - void extractFileName_valueIsNotString_branchCovered() { - when(cdsElement.getName()).thenReturn("fileName"); - try (MockedStatic mocked = mockStatic(CdsDataProcessor.class)) { - mocked.when(CdsDataProcessor::create).thenReturn(processor); - doAnswer( - invocation -> { - Filter filter = invocation.getArgument(0); - Validator validator = invocation.getArgument(1); - if (filter.test(null, cdsElement, null)) { - validator.validate(null, cdsElement, 42); // non-String - } - return processor; - }) - .when(processor) - .addValidator(any(), any()); - doNothing().when(processor).process(anyList(), any()); - - assertThrows( - ServiceException.class, - () -> ApplicationHandlerHelper.extractFileName(entity, List.of(data))); - } - } - - @Test - void validateAcceptableMediaTypes_nonMediaOrMissingEntity_doesNothing() { - when(entity.getQualifiedName()).thenReturn("TestService.Roots"); - when(cdsModel.findEntity("TestService.Roots")).thenReturn(Optional.empty()); - - assertDoesNotThrow( - () -> ApplicationHandlerHelper.validateAcceptableMediaTypes(entity, List.of(), cdsRuntime)); - } - - @Test - void validateAcceptableMediaTypes_notMediaEntity_doesNotCallValidation() { - when(entity.getQualifiedName()).thenReturn("TestService.Roots"); - when(cdsModel.findEntity("TestService.Roots")).thenReturn(Optional.of(serviceEntity)); - when(serviceEntity.getAnnotationValue("_is_media_data", false)).thenReturn(false); - - try (MockedStatic mocked = - mockStatic(AttachmentValidationHelper.class)) { - ApplicationHandlerHelper.validateAcceptableMediaTypes(entity, List.of(), cdsRuntime); - mocked.verifyNoInteractions(); - } - } - - @Test - void shouldNotThrowWhenEntityNotFoundInModel() { - when(entity.getQualifiedName()).thenReturn("Test.Entity"); - when(cdsModel.findEntity("Test.Entity")).thenReturn(Optional.empty()); - - assertDoesNotThrow( - () -> ApplicationHandlerHelper.validateAcceptableMediaTypes(entity, List.of(), cdsRuntime)); - } - - @Test - void validateAcceptableMediaTypes_mediaTypeMatches_succeeds() { - setupMediaEntity(List.of("image/png")); - try (MockedStatic helperStatic = - mockStatic(ApplicationHandlerHelper.class, CALLS_REAL_METHODS)) { - - // file.png → media type image/png → allowed - helperStatic - .when(() -> ApplicationHandlerHelper.extractFileName(entity, List.of(data))) - .thenReturn("file.png"); - - assertDoesNotThrow( - () -> - ApplicationHandlerHelper.validateAcceptableMediaTypes( - entity, List.of(data), cdsRuntime)); - } - } - - @Test - void validateAcceptableMediaTypes_mediaTypeMismatch_throws() { - setupMediaEntity(List.of("image/png")); - try (MockedStatic helperStatic = - mockStatic(ApplicationHandlerHelper.class, CALLS_REAL_METHODS)) { - - // file.jpg → media type image/jpeg → NOT allowed - helperStatic - .when(() -> ApplicationHandlerHelper.extractFileName(entity, List.of(data))) - .thenReturn("file.jpg"); - - assertThrows( - ServiceException.class, - () -> - ApplicationHandlerHelper.validateAcceptableMediaTypes( - entity, List.of(data), cdsRuntime)); - } - } - - private void setupMediaEntity(List allowedTypes) { - when(entity.getQualifiedName()).thenReturn("Test.Entity"); - when(cdsModel.findEntity("Test.Entity")).thenReturn(Optional.of(serviceEntity)); - when(serviceEntity.getAnnotationValue("_is_media_data", false)).thenReturn(true); - when(serviceEntity.getElement("content")).thenReturn(cdsElement); - when(cdsElement.findAnnotation("Core.AcceptableMediaTypes")) - .thenReturn(Optional.of(annotation)); - when(annotation.getValue()).thenReturn(allowedTypes); - } } From 3df6d499a253e0415c18f0c721d0b42889f0cd03 Mon Sep 17 00:00:00 2001 From: samyuktaprabhu <33195453+samyuktaprabhu@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:22:57 +0100 Subject: [PATCH 17/17] remove guessing the extension from filename --- .../helper/AttachmentValidationHelper.java | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelper.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelper.java index 3973555b..e8f2e2e3 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelper.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/AttachmentValidationHelper.java @@ -16,7 +16,6 @@ import com.sap.cds.services.ErrorStatuses; import com.sap.cds.services.ServiceException; import com.sap.cds.services.runtime.CdsRuntime; -import java.net.URLConnection; import java.util.Collection; import java.util.List; import java.util.Map; @@ -190,16 +189,11 @@ private static String resolveMimeType(String fileName) { String fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase(); String actualMimeType = EXT_TO_MEDIA_TYPE.get(fileExtension); if (actualMimeType == null) { - - actualMimeType = URLConnection.guessContentTypeFromName(fileName); - - if (actualMimeType == null) { - logger.warn( - "Could not determine mime type for file: {}. Setting mime type to default: {}", - fileName, - DEFAULT_MEDIA_TYPE); - actualMimeType = DEFAULT_MEDIA_TYPE; - } + logger.warn( + "Could not determine mime type for file: {}. Setting mime type to default: {}", + fileName, + DEFAULT_MEDIA_TYPE); + actualMimeType = DEFAULT_MEDIA_TYPE; } return actualMimeType; }