From 87504332f2fdd36372c891d10b21cf9025ff6a0d Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Wed, 25 Mar 2026 16:32:27 +0100 Subject: [PATCH 01/18] add single bucket mt --- .../attachments/oss/client/AWSClient.java | 41 +++ .../attachments/oss/client/AzureClient.java | 19 + .../attachments/oss/client/GoogleClient.java | 26 ++ .../attachments/oss/client/OSClient.java | 6 + .../oss/configuration/Registration.java | 33 +- .../handler/OSSAttachmentsServiceHandler.java | 54 ++- .../oss/handler/TenantCleanupHandler.java | 39 ++ .../attachments/oss/client/AWSClientTest.java | 102 +++++- .../oss/client/AzureClientTest.java | 61 ++++ .../oss/client/GoogleClientTest.java | 98 +++++ .../attachments/oss/client/OSClientTest.java | 41 +++ .../oss/configuration/RegistrationTest.java | 102 ++++++ .../oss/handler/MultiTenantIsolationTest.java | 234 ++++++++++++ .../OSSAttachmentsServiceHandlerTest.java | 343 +++++++++++++++++- ...OSSAttachmentsServiceHandlerTestUtils.java | 3 +- .../oss/handler/TenantCleanupHandlerTest.java | 82 +++++ 16 files changed, 1268 insertions(+), 16 deletions(-) create mode 100644 storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandler.java create mode 100644 storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/OSClientTest.java create mode 100644 storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/MultiTenantIsolationTest.java create mode 100644 storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java index 04084d9f3..831818ccd 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java @@ -6,6 +6,7 @@ import com.sap.cds.feature.attachments.oss.handler.ObjectStoreServiceException; import com.sap.cloud.environment.servicebinding.api.ServiceBinding; import java.io.InputStream; +import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; @@ -18,9 +19,14 @@ import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.S3AsyncClient; import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.Delete; import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; import software.amazon.awssdk.services.s3.model.DeleteObjectResponse; +import software.amazon.awssdk.services.s3.model.DeleteObjectsRequest; import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; +import software.amazon.awssdk.services.s3.model.ListObjectsV2Response; +import software.amazon.awssdk.services.s3.model.ObjectIdentifier; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectResponse; @@ -129,4 +135,39 @@ public Future readContent(String completeFileName) { } }); } + + @Override + public Future deleteContentByPrefix(String prefix) { + return executor.submit( + () -> { + try { + ListObjectsV2Request listReq = + ListObjectsV2Request.builder().bucket(this.bucketName).prefix(prefix).build(); + ListObjectsV2Response listResp; + do { + listResp = s3Client.listObjectsV2(listReq); + if (!listResp.contents().isEmpty()) { + List keys = + listResp.contents().stream() + .map(obj -> ObjectIdentifier.builder().key(obj.key()).build()) + .toList(); + DeleteObjectsRequest deleteReq = + DeleteObjectsRequest.builder() + .bucket(this.bucketName) + .delete(Delete.builder().objects(keys).build()) + .build(); + s3Client.deleteObjects(deleteReq); + } + listReq = + listReq.toBuilder() + .continuationToken(listResp.nextContinuationToken()) + .build(); + } while (listResp.isTruncated()); + } catch (RuntimeException e) { + throw new ObjectStoreServiceException( + "Failed to delete objects by prefix from the AWS Object Store", e); + } + return null; + }); + } } diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AzureClient.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AzureClient.java index 62c0f6fb9..b7ad983f9 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AzureClient.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AzureClient.java @@ -6,6 +6,8 @@ import com.azure.storage.blob.BlobClient; import com.azure.storage.blob.BlobContainerClient; import com.azure.storage.blob.BlobContainerClientBuilder; +import com.azure.storage.blob.models.BlobItem; +import com.azure.storage.blob.models.ListBlobsOptions; import com.azure.storage.blob.specialized.BlobOutputStream; import com.azure.storage.blob.specialized.BlockBlobClient; import com.sap.cds.feature.attachments.oss.handler.ObjectStoreServiceException; @@ -86,4 +88,21 @@ public Future readContent(String completeFileName) { } }); } + + @Override + public Future deleteContentByPrefix(String prefix) { + return executor.submit( + () -> { + try { + ListBlobsOptions options = new ListBlobsOptions().setPrefix(prefix); + for (BlobItem blobItem : blobContainerClient.listBlobs(options, null)) { + blobContainerClient.getBlobClient(blobItem.getName()).delete(); + } + } catch (RuntimeException e) { + throw new ObjectStoreServiceException( + "Failed to delete objects by prefix from the Azure Object Store", e); + } + return null; + }); + } } diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/GoogleClient.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/GoogleClient.java index c502898ee..ec3a0a5aa 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/GoogleClient.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/GoogleClient.java @@ -134,4 +134,30 @@ public Future readContent(String completeFileName) { } }); } + + @Override + public Future deleteContentByPrefix(String prefix) { + return executor.submit( + () -> { + try { + Page blobs = + storage.list(bucketName, Storage.BlobListOption.prefix(prefix)); + for (Blob blob : blobs.iterateAll()) { + Page versions = + storage.list( + bucketName, + Storage.BlobListOption.versions(true), + Storage.BlobListOption.prefix(blob.getName())); + for (Blob version : versions.iterateAll()) { + storage.delete( + BlobId.of(bucketName, version.getName(), version.getGeneration())); + } + } + } catch (RuntimeException e) { + throw new ObjectStoreServiceException( + "Failed to delete objects by prefix from Google Object Store", e); + } + return null; + }); + } } diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/OSClient.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/OSClient.java index a690dd7bb..e89e5ca98 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/OSClient.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/OSClient.java @@ -4,6 +4,7 @@ package com.sap.cds.feature.attachments.oss.client; import java.io.InputStream; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; /** The {@link OSClient} is the connection to the object store service. */ @@ -14,4 +15,9 @@ public interface OSClient { Future deleteContent(String completeFileName); Future readContent(String completeFileName); + + default Future deleteContentByPrefix(String prefix) { + return CompletableFuture.failedFuture( + new UnsupportedOperationException("deleteContentByPrefix not supported by this client")); + } } diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/configuration/Registration.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/configuration/Registration.java index 6fdbfcfff..5a3fff178 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/configuration/Registration.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/configuration/Registration.java @@ -4,6 +4,7 @@ package com.sap.cds.feature.attachments.oss.configuration; import com.sap.cds.feature.attachments.oss.handler.OSSAttachmentsServiceHandler; +import com.sap.cds.feature.attachments.oss.handler.TenantCleanupHandler; import com.sap.cds.services.environment.CdsEnvironment; import com.sap.cds.services.runtime.CdsRuntimeConfiguration; import com.sap.cds.services.runtime.CdsRuntimeConfigurer; @@ -14,18 +15,31 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** The class registers the event handlers for the attachments feature based on filesystem. */ +/** The class registers the event handlers for the attachments feature based on object store. */ public class Registration implements CdsRuntimeConfiguration { private static final Logger logger = LoggerFactory.getLogger(Registration.class); @Override public void eventHandlers(CdsRuntimeConfigurer configurer) { - Optional bindingOpt = getOSBinding(configurer.getCdsRuntime().getEnvironment()); + CdsEnvironment env = configurer.getCdsRuntime().getEnvironment(); + Optional bindingOpt = getOSBinding(env); if (bindingOpt.isPresent()) { + boolean multitenancyEnabled = isMultitenancyEnabled(env); + String objectStoreKind = getObjectStoreKind(env); + ExecutorService executor = Executors.newCachedThreadPool(); - // Thread count could be made configurable via CdsProperties if needed in the future. - configurer.eventHandler(new OSSAttachmentsServiceHandler(bindingOpt.get(), executor)); - logger.info("Registered OSS Attachments Service Handler."); + OSSAttachmentsServiceHandler handler = + new OSSAttachmentsServiceHandler( + bindingOpt.get(), executor, multitenancyEnabled, objectStoreKind); + configurer.eventHandler(handler); + + if (multitenancyEnabled && "shared".equals(objectStoreKind)) { + configurer.eventHandler(new TenantCleanupHandler(handler.getOsClient())); + logger.info( + "Registered OSS Attachments Service Handler with shared multitenancy mode and tenant cleanup."); + } else { + logger.info("Registered OSS Attachments Service Handler."); + } } else { logger.warn( "No service binding to Object Store Service found, hence the OSS Attachments Service Handler is not connected!"); @@ -46,4 +60,13 @@ private static Optional getOSBinding(CdsEnvironment environment) .filter(b -> b.getServiceName().map(name -> name.equals("objectstore")).orElse(false)) .findFirst(); } + + private static boolean isMultitenancyEnabled(CdsEnvironment env) { + return Boolean.TRUE.equals( + env.getProperty("cds.multitenancy.enabled", Boolean.class, Boolean.FALSE)); + } + + private static String getObjectStoreKind(CdsEnvironment env) { + return env.getProperty("cds.attachments.objectStore.kind", String.class, null); + } } diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java index 9a44d9c8a..09f7b8e31 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java @@ -15,6 +15,7 @@ import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentMarkAsDeletedEventContext; import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentReadEventContext; import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentRestoreEventContext; +import com.sap.cds.services.EventContext; import com.sap.cds.services.ServiceException; import com.sap.cds.services.handler.EventHandler; import com.sap.cds.services.handler.annotations.On; @@ -39,6 +40,8 @@ public class OSSAttachmentsServiceHandler implements EventHandler { private static final Logger logger = LoggerFactory.getLogger(OSSAttachmentsServiceHandler.class); private final OSClient osClient; + private final boolean multitenancyEnabled; + private final String objectStoreKind; /** * Creates a new OSSAttachmentsServiceHandler using the provided {@link ServiceBinding}. @@ -55,9 +58,18 @@ public class OSSAttachmentsServiceHandler implements EventHandler { * * * @param binding the {@link ServiceBinding} containing credentials for the object store service + * @param executor the {@link ExecutorService} for async operations + * @param multitenancyEnabled whether multitenancy is enabled + * @param objectStoreKind the object store kind (e.g. "shared") * @throws ObjectStoreServiceException if no valid object store service binding is found */ - public OSSAttachmentsServiceHandler(ServiceBinding binding, ExecutorService executor) { + public OSSAttachmentsServiceHandler( + ServiceBinding binding, + ExecutorService executor, + boolean multitenancyEnabled, + String objectStoreKind) { + this.multitenancyEnabled = multitenancyEnabled; + this.objectStoreKind = objectStoreKind; final String host = (String) binding.getCredentials().get("host"); // AWS final String containerUri = (String) binding.getCredentials().get("container_uri"); // Azure final String base64EncodedPrivateKeyData = @@ -106,9 +118,10 @@ void createAttachment(AttachmentCreateEventContext context) { String contentId = (String) context.getAttachmentIds().get(Attachments.ID); MediaData data = context.getData(); String fileName = data.getFileName(); + String objectKey = buildObjectKey(context, contentId); try { - osClient.uploadContent(data.getContent(), contentId, data.getMimeType()).get(); + osClient.uploadContent(data.getContent(), objectKey, data.getMimeType()).get(); logger.info("Uploaded file {}", fileName); context.getData().setStatus(StatusCode.SCANNING); context.setIsInternalStored(false); @@ -130,7 +143,8 @@ void markAttachmentAsDeleted(AttachmentMarkAsDeletedEventContext context) { context.getContentId()); try { - osClient.deleteContent(context.getContentId()).get(); + String objectKey = buildObjectKey(context, context.getContentId()); + osClient.deleteContent(objectKey).get(); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); throw new ServiceException( @@ -159,7 +173,8 @@ void readAttachment(AttachmentReadEventContext context) { "OS Attachment Service handler called for reading attachment with document id: {}", context.getContentId()); try { - Future future = osClient.readContent(context.getContentId()); + String objectKey = buildObjectKey(context, context.getContentId()); + Future future = osClient.readContent(objectKey); InputStream inputStream = future.get(); // Wait for the content to be read if (inputStream != null) { context.getData().setContent(inputStream); @@ -179,4 +194,35 @@ void readAttachment(AttachmentReadEventContext context) { context.setCompleted(); } } + + public OSClient getOsClient() { + return osClient; + } + + private String buildObjectKey(EventContext context, String contentId) { + if (multitenancyEnabled && "shared".equals(objectStoreKind)) { + String tenant = getTenant(context); + validateTenantId(tenant); + return tenant + "/" + contentId; + } + return contentId; + } + + private String getTenant(EventContext context) { + String tenant = context.getUserInfo().getTenant(); + if (tenant == null && multitenancyEnabled) { + throw new ServiceException("Tenant ID is required for multitenant attachment operations"); + } + return tenant != null ? tenant : "default"; + } + + private static void validateTenantId(String tenantId) { + if (tenantId.isEmpty() + || tenantId.contains("/") + || tenantId.contains("\\") + || tenantId.contains("..")) { + throw new ServiceException( + "Invalid tenant ID for attachment storage: must not be empty or contain path separators"); + } + } } diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandler.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandler.java new file mode 100644 index 000000000..6b305cebd --- /dev/null +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandler.java @@ -0,0 +1,39 @@ +/* + * © 2025 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.oss.handler; + +import com.sap.cds.feature.attachments.oss.client.OSClient; +import com.sap.cds.services.handler.EventHandler; +import com.sap.cds.services.handler.annotations.After; +import com.sap.cds.services.handler.annotations.ServiceName; +import com.sap.cds.services.mt.DeploymentService; +import com.sap.cds.services.mt.UnsubscribeEventContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ServiceName(DeploymentService.DEFAULT_NAME) +public class TenantCleanupHandler implements EventHandler { + + private static final Logger logger = LoggerFactory.getLogger(TenantCleanupHandler.class); + private final OSClient osClient; + + public TenantCleanupHandler(OSClient osClient) { + this.osClient = osClient; + } + + @After(event = DeploymentService.EVENT_UNSUBSCRIBE) + void cleanupTenantData(UnsubscribeEventContext context) { + String tenantId = context.getTenant(); + String prefix = tenantId + "/"; + try { + osClient.deleteContentByPrefix(prefix).get(); + logger.info("Cleaned up all objects for tenant {} from shared object store", tenantId); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.error("Interrupted while cleaning up objects for tenant {}", tenantId, e); + } catch (Exception e) { + logger.error("Failed to clean up objects for tenant {}", tenantId, e); + } + } +} diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java index 032c1a8a2..7fc91a811 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java @@ -9,6 +9,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.sap.cds.feature.attachments.oss.handler.OSSAttachmentsServiceHandler; @@ -30,10 +31,15 @@ import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; import software.amazon.awssdk.services.s3.model.DeleteObjectResponse; +import software.amazon.awssdk.services.s3.model.DeleteObjectsRequest; +import software.amazon.awssdk.services.s3.model.DeleteObjectsResponse; import software.amazon.awssdk.services.s3.model.GetObjectRequest; import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; +import software.amazon.awssdk.services.s3.model.ListObjectsV2Response; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectResponse; +import software.amazon.awssdk.services.s3.model.S3Object; class AWSClientTest { ExecutorService executor = Executors.newCachedThreadPool(); @@ -42,7 +48,7 @@ class AWSClientTest { void testConstructorWithAwsBindingUsesAwsClient() throws NoSuchFieldException, IllegalAccessException { OSSAttachmentsServiceHandler handler = - new OSSAttachmentsServiceHandler(getDummyBinding(), executor); + new OSSAttachmentsServiceHandler(getDummyBinding(), executor, false, null); OSClient client = OSSAttachmentsServiceHandlerTestUtils.getOsClient(handler); assertInstanceOf(AWSClient.class, client); } @@ -225,6 +231,100 @@ void testDeleteContentThrowsOnUnsuccessfulResponse() assertInstanceOf(ObjectStoreServiceException.class, thrown.getCause()); } + @Test + void testDeleteContentByPrefix_DeletesAllMatchingObjects() throws Exception { + AWSClient awsClient = new AWSClient(getDummyBinding(), executor); + + S3Client mockS3Client = mock(S3Client.class); + + // Create mock list response with 2 objects matching the prefix + S3Object obj1 = S3Object.builder().key("tenantA/file1.txt").build(); + S3Object obj2 = S3Object.builder().key("tenantA/file2.txt").build(); + ListObjectsV2Response listResponse = + ListObjectsV2Response.builder() + .contents(obj1, obj2) + .isTruncated(false) + .build(); + + when(mockS3Client.listObjectsV2(any(ListObjectsV2Request.class))).thenReturn(listResponse); + DeleteObjectsResponse deleteResponse = mock(DeleteObjectsResponse.class); + when(mockS3Client.deleteObjects(any(DeleteObjectsRequest.class))).thenReturn(deleteResponse); + + var field = AWSClient.class.getDeclaredField("s3Client"); + field.setAccessible(true); + field.set(awsClient, mockS3Client); + + assertDoesNotThrow(() -> awsClient.deleteContentByPrefix("tenantA/").get()); + verify(mockS3Client).listObjectsV2(any(ListObjectsV2Request.class)); + verify(mockS3Client).deleteObjects(any(DeleteObjectsRequest.class)); + } + + @Test + void testDeleteContentByPrefix_HandlesPagination() throws Exception { + AWSClient awsClient = new AWSClient(getDummyBinding(), executor); + + S3Client mockS3Client = mock(S3Client.class); + + // First page: truncated, has a continuation token + S3Object obj1 = S3Object.builder().key("tenantB/file1.txt").build(); + ListObjectsV2Response firstPage = + ListObjectsV2Response.builder() + .contents(obj1) + .isTruncated(true) + .nextContinuationToken("token-page2") + .build(); + + // Second page: not truncated, final page + S3Object obj2 = S3Object.builder().key("tenantB/file2.txt").build(); + ListObjectsV2Response secondPage = + ListObjectsV2Response.builder() + .contents(obj2) + .isTruncated(false) + .build(); + + when(mockS3Client.listObjectsV2(any(ListObjectsV2Request.class))) + .thenReturn(firstPage, secondPage); + DeleteObjectsResponse deleteResponse = mock(DeleteObjectsResponse.class); + when(mockS3Client.deleteObjects(any(DeleteObjectsRequest.class))).thenReturn(deleteResponse); + + var field = AWSClient.class.getDeclaredField("s3Client"); + field.setAccessible(true); + field.set(awsClient, mockS3Client); + + assertDoesNotThrow(() -> awsClient.deleteContentByPrefix("tenantB/").get()); + // Should call listObjectsV2 twice (two pages) and deleteObjects twice + verify(mockS3Client, org.mockito.Mockito.times(2)) + .listObjectsV2(any(ListObjectsV2Request.class)); + verify(mockS3Client, org.mockito.Mockito.times(2)) + .deleteObjects(any(DeleteObjectsRequest.class)); + } + + @Test + void testDeleteContentByPrefix_EmptyPrefix_NoObjects() throws Exception { + AWSClient awsClient = new AWSClient(getDummyBinding(), executor); + + S3Client mockS3Client = mock(S3Client.class); + + // Empty listing + ListObjectsV2Response emptyResponse = + ListObjectsV2Response.builder() + .contents(java.util.Collections.emptyList()) + .isTruncated(false) + .build(); + + when(mockS3Client.listObjectsV2(any(ListObjectsV2Request.class))).thenReturn(emptyResponse); + + var field = AWSClient.class.getDeclaredField("s3Client"); + field.setAccessible(true); + field.set(awsClient, mockS3Client); + + assertDoesNotThrow(() -> awsClient.deleteContentByPrefix("nonexistent/").get()); + verify(mockS3Client).listObjectsV2(any(ListObjectsV2Request.class)); + // No deleteObjects call when there are no objects + verify(mockS3Client, org.mockito.Mockito.never()) + .deleteObjects(any(DeleteObjectsRequest.class)); + } + private ServiceBinding getDummyBinding() { ServiceBinding binding = mock(ServiceBinding.class); HashMap creds = new HashMap<>(); diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java index 9b7100468..59b42bfaf 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java @@ -7,16 +7,22 @@ import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; +import com.azure.core.http.rest.PagedIterable; import com.azure.storage.blob.BlobClient; import com.azure.storage.blob.BlobContainerClient; +import com.azure.storage.blob.models.BlobItem; +import com.azure.storage.blob.models.ListBlobsOptions; import com.azure.storage.blob.specialized.BlobOutputStream; import com.azure.storage.blob.specialized.BlockBlobClient; import com.sap.cds.feature.attachments.oss.handler.ObjectStoreServiceException; import java.io.IOException; import java.io.InputStream; +import java.util.Arrays; +import java.util.Collections; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; class AzureClientTest { @@ -189,4 +195,59 @@ void testReadContentThrowsOnRuntimeException() assertThrows(ExecutionException.class, () -> azureClient.readContent("file.txt").get()); assertInstanceOf(ObjectStoreServiceException.class, thrown.getCause()); } + + @Test + @SuppressWarnings("unchecked") + void testDeleteContentByPrefix_DeletesAllMatchingBlobs() + throws NoSuchFieldException, IllegalAccessException { + AzureClient azureClient = mock(AzureClient.class, CALLS_REAL_METHODS); + + BlobContainerClient mockContainer = mock(BlobContainerClient.class); + BlobClient mockBlobClient = mock(BlobClient.class); + + var field = AzureClient.class.getDeclaredField("blobContainerClient"); + field.setAccessible(true); + field.set(azureClient, mockContainer); + var executorField = AzureClient.class.getDeclaredField("executor"); + executorField.setAccessible(true); + executorField.set(azureClient, executor); + + // Create mock blob items matching the prefix + BlobItem blob1 = mock(BlobItem.class); + when(blob1.getName()).thenReturn("tenantA/file1.txt"); + BlobItem blob2 = mock(BlobItem.class); + when(blob2.getName()).thenReturn("tenantA/file2.txt"); + + PagedIterable mockPagedIterable = mock(PagedIterable.class); + when(mockPagedIterable.stream()).thenReturn(Stream.of(blob1, blob2)); + when(mockPagedIterable.iterator()).thenReturn(Arrays.asList(blob1, blob2).iterator()); + when(mockContainer.listBlobs(any(ListBlobsOptions.class), any())).thenReturn(mockPagedIterable); + when(mockContainer.getBlobClient(anyString())).thenReturn(mockBlobClient); + + assertDoesNotThrow(() -> azureClient.deleteContentByPrefix("tenantA/").get()); + } + + @Test + @SuppressWarnings("unchecked") + void testDeleteContentByPrefix_EmptyResult() + throws NoSuchFieldException, IllegalAccessException { + AzureClient azureClient = mock(AzureClient.class, CALLS_REAL_METHODS); + + BlobContainerClient mockContainer = mock(BlobContainerClient.class); + + var field = AzureClient.class.getDeclaredField("blobContainerClient"); + field.setAccessible(true); + field.set(azureClient, mockContainer); + var executorField = AzureClient.class.getDeclaredField("executor"); + executorField.setAccessible(true); + executorField.set(azureClient, executor); + + // Empty listing + PagedIterable emptyIterable = mock(PagedIterable.class); + when(emptyIterable.stream()).thenReturn(Stream.empty()); + when(emptyIterable.iterator()).thenReturn(Collections.emptyIterator()); + when(mockContainer.listBlobs(any(ListBlobsOptions.class), any())).thenReturn(emptyIterable); + + assertDoesNotThrow(() -> azureClient.deleteContentByPrefix("nonexistent-tenant/").get()); + } } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java index f4c2fd188..5da93a59e 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java @@ -272,4 +272,102 @@ void testReadContentThrowsOnRuntimeException() assertThrows(ExecutionException.class, () -> googleClient.readContent("file.txt").get()); assertInstanceOf(ObjectStoreServiceException.class, thrown.getCause()); } + + @Test + @SuppressWarnings("unchecked") + void testDeleteContentByPrefix_DeletesAllMatchingBlobs() + throws NoSuchFieldException, IllegalAccessException { + GoogleClient googleClient = mock(GoogleClient.class, CALLS_REAL_METHODS); + + Storage mockStorage = mock(Storage.class); + + var field = GoogleClient.class.getDeclaredField("storage"); + field.setAccessible(true); + field.set(googleClient, mockStorage); + var executorField = GoogleClient.class.getDeclaredField("executor"); + executorField.setAccessible(true); + executorField.set(googleClient, executor); + var bucketField = GoogleClient.class.getDeclaredField("bucketName"); + bucketField.setAccessible(true); + bucketField.set(googleClient, "my-bucket"); + + // Mock listing of blobs by prefix + Blob blobA = mock(Blob.class); + when(blobA.getName()).thenReturn("tenantX/file1.txt"); + when(blobA.getGeneration()).thenReturn(1L); + Blob blobB = mock(Blob.class); + when(blobB.getName()).thenReturn("tenantX/file2.txt"); + when(blobB.getGeneration()).thenReturn(2L); + + // First call: list by prefix (without versions) returns the blobs + Page prefixPage = mock(Page.class); + when(prefixPage.iterateAll()).thenReturn(() -> java.util.Arrays.asList(blobA, blobB).iterator()); + + // For each blob, list versions returns the same blob (single version each) + Page versionPageA = mock(Page.class); + when(versionPageA.iterateAll()).thenReturn(() -> Collections.singletonList(blobA).iterator()); + Page versionPageB = mock(Page.class); + when(versionPageB.iterateAll()).thenReturn(() -> Collections.singletonList(blobB).iterator()); + + // First call: prefix listing; subsequent calls: version listings + when(mockStorage.list(anyString(), any())) + .thenReturn(prefixPage); + when(mockStorage.list(anyString(), any(), any())) + .thenReturn(versionPageA, versionPageB); + + when(mockStorage.delete(any(BlobId.class))).thenReturn(true); + + assertDoesNotThrow(() -> googleClient.deleteContentByPrefix("tenantX/").get()); + } + + @Test + @SuppressWarnings("unchecked") + void testDeleteContentByPrefix_HandlesVersioning() + throws NoSuchFieldException, IllegalAccessException { + GoogleClient googleClient = mock(GoogleClient.class, CALLS_REAL_METHODS); + + Storage mockStorage = mock(Storage.class); + + var field = GoogleClient.class.getDeclaredField("storage"); + field.setAccessible(true); + field.set(googleClient, mockStorage); + var executorField = GoogleClient.class.getDeclaredField("executor"); + executorField.setAccessible(true); + executorField.set(googleClient, executor); + var bucketField = GoogleClient.class.getDeclaredField("bucketName"); + bucketField.setAccessible(true); + bucketField.set(googleClient, "my-bucket"); + + // Blob with 2 versions + Blob blob = mock(Blob.class); + when(blob.getName()).thenReturn("tenantY/versioned-file.txt"); + when(blob.getGeneration()).thenReturn(1L); + + Blob version1 = mock(Blob.class); + when(version1.getName()).thenReturn("tenantY/versioned-file.txt"); + when(version1.getGeneration()).thenReturn(1L); + + Blob version2 = mock(Blob.class); + when(version2.getName()).thenReturn("tenantY/versioned-file.txt"); + when(version2.getGeneration()).thenReturn(2L); + + Page prefixPage = mock(Page.class); + when(prefixPage.iterateAll()).thenReturn(() -> Collections.singletonList(blob).iterator()); + + Page versionPage = mock(Page.class); + when(versionPage.iterateAll()) + .thenReturn(() -> java.util.Arrays.asList(version1, version2).iterator()); + + when(mockStorage.list(anyString(), any())) + .thenReturn(prefixPage); + when(mockStorage.list(anyString(), any(), any())) + .thenReturn(versionPage); + when(mockStorage.delete(any(BlobId.class))).thenReturn(true); + + assertDoesNotThrow(() -> googleClient.deleteContentByPrefix("tenantY/").get()); + + // Should delete both versions + org.mockito.Mockito.verify(mockStorage, org.mockito.Mockito.times(2)) + .delete(any(BlobId.class)); + } } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/OSClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/OSClientTest.java new file mode 100644 index 000000000..f1861534b --- /dev/null +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/OSClientTest.java @@ -0,0 +1,41 @@ +/* + * © 2025 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.oss.client; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.concurrent.ExecutionException; +import org.junit.jupiter.api.Test; + +class OSClientTest { + + @Test + void testDeleteContentByPrefix_DefaultThrowsUnsupported() { + // Anonymous implementation that only has the default methods + OSClient client = + new OSClient() { + @Override + public java.util.concurrent.Future uploadContent( + java.io.InputStream content, String completeFileName, String contentType) { + return null; + } + + @Override + public java.util.concurrent.Future deleteContent(String completeFileName) { + return null; + } + + @Override + public java.util.concurrent.Future readContent( + String completeFileName) { + return null; + } + }; + + ExecutionException ex = + assertThrows(ExecutionException.class, () -> client.deleteContentByPrefix("prefix").get()); + assertInstanceOf(UnsupportedOperationException.class, ex.getCause()); + } +} diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java index a364aa7d7..5babe024d 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java @@ -7,6 +7,7 @@ import static org.mockito.Mockito.*; import com.sap.cds.feature.attachments.oss.handler.OSSAttachmentsServiceHandler; +import com.sap.cds.feature.attachments.oss.handler.TenantCleanupHandler; import com.sap.cds.services.environment.CdsEnvironment; import com.sap.cds.services.runtime.CdsRuntime; import com.sap.cds.services.runtime.CdsRuntimeConfigurer; @@ -48,4 +49,105 @@ void testEventHandlersRegistersOSSHandler() { // Assert: OSSAttachmentsServiceHandler should be registered verify(configurer).eventHandler(any(OSSAttachmentsServiceHandler.class)); } + + @Test + void testRegistration_SharedMode_RegistersCleanupHandler() { + Registration registration = new Registration(); + CdsRuntimeConfigurer configurer = mock(CdsRuntimeConfigurer.class); + CdsRuntime cdsRuntime = mock(CdsRuntime.class); + CdsEnvironment environment = mock(CdsEnvironment.class); + ServiceBinding binding = mock(ServiceBinding.class); + + Map credentials = new HashMap<>(); + credentials.put("host", "aws.example.com"); + credentials.put("region", "us-east-1"); + credentials.put("access_key_id", "test-access-key"); + credentials.put("secret_access_key", "test-secret-key"); + credentials.put("bucket", "test-bucket"); + + when(configurer.getCdsRuntime()).thenReturn(cdsRuntime); + when(cdsRuntime.getEnvironment()).thenReturn(environment); + when(binding.getServiceName()).thenReturn(Optional.of("objectstore")); + when(binding.getCredentials()).thenReturn(credentials); + when(environment.getServiceBindings()).thenReturn(Stream.of(binding)); + + // Configure shared mode multitenancy + when(environment.getProperty("cds.multitenancy.enabled", Boolean.class, Boolean.FALSE)) + .thenReturn(Boolean.TRUE); + when(environment.getProperty("cds.attachments.objectStore.kind", String.class, null)) + .thenReturn("shared"); + + registration.eventHandlers(configurer); + + // Should register both the handler AND the TenantCleanupHandler + verify(configurer).eventHandler(any(OSSAttachmentsServiceHandler.class)); + verify(configurer).eventHandler(any(TenantCleanupHandler.class)); + } + + @Test + void testRegistration_NonMTMode_NoCleanupHandler() { + Registration registration = new Registration(); + CdsRuntimeConfigurer configurer = mock(CdsRuntimeConfigurer.class); + CdsRuntime cdsRuntime = mock(CdsRuntime.class); + CdsEnvironment environment = mock(CdsEnvironment.class); + ServiceBinding binding = mock(ServiceBinding.class); + + Map credentials = new HashMap<>(); + credentials.put("host", "aws.example.com"); + credentials.put("region", "us-east-1"); + credentials.put("access_key_id", "test-access-key"); + credentials.put("secret_access_key", "test-secret-key"); + credentials.put("bucket", "test-bucket"); + + when(configurer.getCdsRuntime()).thenReturn(cdsRuntime); + when(cdsRuntime.getEnvironment()).thenReturn(environment); + when(binding.getServiceName()).thenReturn(Optional.of("objectstore")); + when(binding.getCredentials()).thenReturn(credentials); + when(environment.getServiceBindings()).thenReturn(Stream.of(binding)); + + // No MT configuration — defaults + when(environment.getProperty("cds.multitenancy.enabled", Boolean.class, Boolean.FALSE)) + .thenReturn(Boolean.FALSE); + when(environment.getProperty("cds.attachments.objectStore.kind", String.class, null)) + .thenReturn(null); + + registration.eventHandlers(configurer); + + // Only the handler, NOT the cleanup handler + verify(configurer).eventHandler(any(OSSAttachmentsServiceHandler.class)); + verify(configurer, never()).eventHandler(any(TenantCleanupHandler.class)); + } + + @Test + void testRegistration_PassesMTConfigToHandler() { + Registration registration = new Registration(); + CdsRuntimeConfigurer configurer = mock(CdsRuntimeConfigurer.class); + CdsRuntime cdsRuntime = mock(CdsRuntime.class); + CdsEnvironment environment = mock(CdsEnvironment.class); + ServiceBinding binding = mock(ServiceBinding.class); + + Map credentials = new HashMap<>(); + credentials.put("host", "aws.example.com"); + credentials.put("region", "us-east-1"); + credentials.put("access_key_id", "test-access-key"); + credentials.put("secret_access_key", "test-secret-key"); + credentials.put("bucket", "test-bucket"); + + when(configurer.getCdsRuntime()).thenReturn(cdsRuntime); + when(cdsRuntime.getEnvironment()).thenReturn(environment); + when(binding.getServiceName()).thenReturn(Optional.of("objectstore")); + when(binding.getCredentials()).thenReturn(credentials); + when(environment.getServiceBindings()).thenReturn(Stream.of(binding)); + + when(environment.getProperty("cds.multitenancy.enabled", Boolean.class, Boolean.FALSE)) + .thenReturn(Boolean.TRUE); + when(environment.getProperty("cds.attachments.objectStore.kind", String.class, null)) + .thenReturn("shared"); + + registration.eventHandlers(configurer); + + // Verify a handler was registered — we trust the implementation passes the config + // through the constructor (which is verified by OSSAttachmentsServiceHandlerTest) + verify(configurer, times(2)).eventHandler(any()); + } } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/MultiTenantIsolationTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/MultiTenantIsolationTest.java new file mode 100644 index 000000000..96c9cccbd --- /dev/null +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/MultiTenantIsolationTest.java @@ -0,0 +1,234 @@ +/* + * © 2025 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.oss.handler; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.Attachments; +import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.MediaData; +import com.sap.cds.feature.attachments.oss.client.OSClient; +import com.sap.cds.services.ServiceException; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentCreateEventContext; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentMarkAsDeletedEventContext; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentReadEventContext; +import com.sap.cds.services.request.UserInfo; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Integration-level test that verifies complete tenant isolation in shared mode. Uses a tracking + * mock OSClient to record all operations and verify that Tenant A's operations never touch Tenant + * B's key space. + */ +class MultiTenantIsolationTest { + + /** Tracks all upload, read, and delete keys passed to the OSClient. */ + private final List uploadedKeys = new ArrayList<>(); + + private final List readKeys = new ArrayList<>(); + private final List deletedKeys = new ArrayList<>(); + private final Map storage = new HashMap<>(); + + private OSClient trackingClient; + private OSSAttachmentsServiceHandler handler; + + @BeforeEach + void setUp() throws NoSuchFieldException, IllegalAccessException { + uploadedKeys.clear(); + readKeys.clear(); + deletedKeys.clear(); + storage.clear(); + + // Create a tracking OSClient that records all operations + trackingClient = mock(OSClient.class); + + when(trackingClient.uploadContent(any(InputStream.class), anyString(), anyString())) + .thenAnswer( + invocation -> { + InputStream content = invocation.getArgument(0); + String key = invocation.getArgument(1); + uploadedKeys.add(key); + storage.put(key, content.readAllBytes()); + return CompletableFuture.completedFuture(null); + }); + + when(trackingClient.readContent(anyString())) + .thenAnswer( + invocation -> { + String key = invocation.getArgument(0); + readKeys.add(key); + byte[] data = storage.get(key); + if (data != null) { + return CompletableFuture.completedFuture( + (InputStream) new ByteArrayInputStream(data)); + } + return CompletableFuture.completedFuture(null); + }); + + when(trackingClient.deleteContent(anyString())) + .thenAnswer( + invocation -> { + String key = invocation.getArgument(0); + deletedKeys.add(key); + storage.remove(key); + return CompletableFuture.completedFuture(null); + }); + + // Create handler with multitenancy enabled in shared mode + handler = mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); + + var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); + osClientField.setAccessible(true); + osClientField.set(handler, trackingClient); + + var mtEnabledField = + OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); + mtEnabledField.setAccessible(true); + mtEnabledField.set(handler, true); + + var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); + kindField.setAccessible(true); + kindField.set(handler, "shared"); + } + + @Test + void testTwoTenants_Upload_Read_Delete_CompleteIsolation() { + String tenantA = "tenantA"; + String tenantB = "tenantB"; + String contentIdA = "content-id-A"; + String contentIdB = "content-id-B"; + + // Tenant A uploads + simulateCreate(tenantA, contentIdA, "Data from Tenant A"); + // Tenant B uploads + simulateCreate(tenantB, contentIdB, "Data from Tenant B"); + + // Verify keys are prefixed correctly + assertEquals(2, uploadedKeys.size()); + assertTrue(uploadedKeys.contains(tenantA + "/" + contentIdA)); + assertTrue(uploadedKeys.contains(tenantB + "/" + contentIdB)); + + // Verify no cross-tenant key overlap + assertFalse(uploadedKeys.stream().anyMatch(k -> k.startsWith(tenantA) && k.contains(contentIdB))); + assertFalse(uploadedKeys.stream().anyMatch(k -> k.startsWith(tenantB) && k.contains(contentIdA))); + + // Tenant A reads its own content + simulateRead(tenantA, contentIdA); + assertEquals(tenantA + "/" + contentIdA, readKeys.get(0)); + + // Tenant B reads its own content + simulateRead(tenantB, contentIdB); + assertEquals(tenantB + "/" + contentIdB, readKeys.get(1)); + + // Tenant A deletes its content + simulateDelete(tenantA, contentIdA); + assertEquals(tenantA + "/" + contentIdA, deletedKeys.get(0)); + + // Tenant B's content is still in storage + assertTrue(storage.containsKey(tenantB + "/" + contentIdB)); + assertFalse(storage.containsKey(tenantA + "/" + contentIdA)); + + // Tenant B deletes its content + simulateDelete(tenantB, contentIdB); + assertEquals(tenantB + "/" + contentIdB, deletedKeys.get(1)); + + // Both are gone + assertTrue(storage.isEmpty()); + } + + @Test + void testTenantA_CannotAccess_TenantB_Data() { + String tenantA = "tenantA"; + String tenantB = "tenantB"; + String sharedContentId = "same-content-id"; + + // Both tenants upload with the same contentId (UUID) + simulateCreate(tenantA, sharedContentId, "Tenant A secret"); + simulateCreate(tenantB, sharedContentId, "Tenant B secret"); + + // Keys should be different due to tenant prefix + assertEquals(2, uploadedKeys.size()); + assertEquals(tenantA + "/" + sharedContentId, uploadedKeys.get(0)); + assertEquals(tenantB + "/" + sharedContentId, uploadedKeys.get(1)); + + // Storage should have 2 separate entries + assertEquals(2, storage.size()); + + // Reading as tenant A only returns tenant A's data + simulateRead(tenantA, sharedContentId); + assertEquals(tenantA + "/" + sharedContentId, readKeys.get(0)); + + // The read key never touches tenant B's namespace + assertFalse(readKeys.get(0).startsWith(tenantB)); + } + + @Test + void testNullTenant_ThrowsInSharedMTMode() { + String contentId = "content-no-tenant"; + + // In MT shared mode, null tenant must throw (H-1 security fix) + assertThrows( + ServiceException.class, () -> simulateCreate(null, contentId, "No tenant data")); + + // No upload should have occurred + assertEquals(0, uploadedKeys.size()); + } + + private void simulateCreate(String tenant, String contentId, String content) { + AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); + MediaData mockMediaData = mock(MediaData.class); + var mockEntity = mock(com.sap.cds.reflect.CdsEntity.class); + UserInfo userInfo = mock(UserInfo.class); + + when(userInfo.getTenant()).thenReturn(tenant); + when(context.getUserInfo()).thenReturn(userInfo); + when(context.getAttachmentEntity()).thenReturn(mockEntity); + when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); + when(context.getData()).thenReturn(mockMediaData); + when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream(content.getBytes())); + when(mockMediaData.getMimeType()).thenReturn("text/plain"); + + handler.createAttachment(context); + } + + private void simulateRead(String tenant, String contentId) { + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + MediaData mockMediaData = mock(MediaData.class); + UserInfo userInfo = mock(UserInfo.class); + + when(userInfo.getTenant()).thenReturn(tenant); + when(context.getUserInfo()).thenReturn(userInfo); + when(context.getContentId()).thenReturn(contentId); + when(context.getData()).thenReturn(mockMediaData); + + handler.readAttachment(context); + } + + private void simulateDelete(String tenant, String contentId) { + AttachmentMarkAsDeletedEventContext context = mock(AttachmentMarkAsDeletedEventContext.class); + UserInfo userInfo = mock(UserInfo.class); + + when(userInfo.getTenant()).thenReturn(tenant); + when(context.getUserInfo()).thenReturn(userInfo); + when(context.getContentId()).thenReturn(contentId); + + handler.markAttachmentAsDeleted(context); + } +} diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java index 2d973221b..26404733d 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java @@ -11,6 +11,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.Attachments; import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.MediaData; import com.sap.cds.feature.attachments.oss.client.OSClient; import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentCreateEventContext; @@ -19,16 +20,19 @@ import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentRestoreEventContext; import com.sap.cds.reflect.CdsEntity; import com.sap.cds.services.ServiceException; +import com.sap.cds.services.request.UserInfo; import com.sap.cloud.environment.servicebinding.api.ServiceBinding; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.util.Base64; import java.util.HashMap; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; class OSSAttachmentsServiceHandlerTest { ExecutorService executor = Executors.newCachedThreadPool(); @@ -45,7 +49,8 @@ void testRestoreAttachmentCallsSetCompleted() { creds.put("bucket", "test-bucket"); when(binding.getCredentials()).thenReturn(creds); - OSSAttachmentsServiceHandler handler = new OSSAttachmentsServiceHandler(binding, executor); + OSSAttachmentsServiceHandler handler = + new OSSAttachmentsServiceHandler(binding, executor, false, null); AttachmentRestoreEventContext context = mock(AttachmentRestoreEventContext.class); handler.restoreAttachment(context); verify(context).setCompleted(); @@ -179,7 +184,7 @@ void testConstructorHandlesInvalidBase64EncodedPrivateKeyData() { assertThrows( ObjectStoreServiceException.class, - () -> new OSSAttachmentsServiceHandler(binding, executor)); + () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); } @Test @@ -195,7 +200,7 @@ void testConstructorHandlesValidBase64ButNoGoogleOrGcp() { assertThrows( ObjectStoreServiceException.class, - () -> new OSSAttachmentsServiceHandler(binding, executor)); + () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); } @Test @@ -207,7 +212,7 @@ void testConstructorHandlesInValidBase64() { assertThrows( ObjectStoreServiceException.class, - () -> new OSSAttachmentsServiceHandler(binding, executor)); + () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); } @Test @@ -221,7 +226,7 @@ void testConstructorHandlesNoValidObjectStoreService() { assertThrows( ObjectStoreServiceException.class, - () -> new OSSAttachmentsServiceHandler(binding, executor)); + () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); } // Helper method to setup common mocks for createAttachment exception tests @@ -377,4 +382,332 @@ void testReadAttachmentHandlesInterruptedException() assertThrows(ServiceException.class, () -> handler.readAttachment(context)); verify(context).setCompleted(); } + + // ==================== Multi-Tenancy Tests (Phase 1 Shared Mode) ==================== + + /** + * Helper to create a handler with MT config injected via reflection. The implementation agent is + * adding multitenancyEnabled and objectStoreKind fields to the handler class. + */ + private OSSAttachmentsServiceHandler createMTHandler( + OSClient mockOsClient, boolean multitenancyEnabled, String objectStoreKind) + throws NoSuchFieldException, IllegalAccessException { + OSSAttachmentsServiceHandler handler = + mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); + + var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); + osClientField.setAccessible(true); + osClientField.set(handler, mockOsClient); + + var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); + mtField.setAccessible(true); + mtField.set(handler, multitenancyEnabled); + + var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); + kindField.setAccessible(true); + kindField.set(handler, objectStoreKind); + + return handler; + } + + private static UserInfo mockUserInfo(String tenant) { + UserInfo userInfo = mock(UserInfo.class); + when(userInfo.getTenant()).thenReturn(tenant); + return userInfo; + } + + @Test + void testBuildObjectKey_SharedMode_PrefixesTenantId() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, true, "shared"); + + String tenantId = "myTenant"; + String contentId = "content-uuid-123"; + + // Setup create context with tenant + AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); + MediaData mockMediaData = mock(MediaData.class); + var mockEntity = mock(CdsEntity.class); + when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getAttachmentEntity()).thenReturn(mockEntity); + when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); + when(context.getData()).thenReturn(mockMediaData); + when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("test".getBytes())); + when(mockMediaData.getMimeType()).thenReturn("text/plain"); + UserInfo userInfo = mockUserInfo(tenantId); + when(context.getUserInfo()).thenReturn(userInfo); + when(mockOsClient.uploadContent(any(), anyString(), anyString())) + .thenReturn(CompletableFuture.completedFuture(null)); + + handler.createAttachment(context); + + // Capture the object key passed to uploadContent + ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class); + verify(mockOsClient).uploadContent(any(), keyCaptor.capture(), anyString()); + // In shared mode, the key should be tenantId/contentId + org.junit.jupiter.api.Assertions.assertEquals( + tenantId + "/" + contentId, keyCaptor.getValue()); + } + + @Test + void testBuildObjectKey_NonMTMode_NoPrefixing() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + // multitenancy disabled + OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, false, null); + + String contentId = "content-uuid-456"; + + AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); + MediaData mockMediaData = mock(MediaData.class); + var mockEntity = mock(CdsEntity.class); + when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getAttachmentEntity()).thenReturn(mockEntity); + when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); + when(context.getData()).thenReturn(mockMediaData); + when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("test".getBytes())); + when(mockMediaData.getMimeType()).thenReturn("text/plain"); + when(mockOsClient.uploadContent(any(), anyString(), anyString())) + .thenReturn(CompletableFuture.completedFuture(null)); + + handler.createAttachment(context); + + // Capture the object key passed to uploadContent + ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class); + verify(mockOsClient).uploadContent(any(), keyCaptor.capture(), anyString()); + // When MT is off, key should be plain contentId with no prefix + org.junit.jupiter.api.Assertions.assertEquals(contentId, keyCaptor.getValue()); + } + + @Test + void testBuildObjectKey_NullTenant_ThrowsInMTMode() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, true, "shared"); + + String contentId = "content-uuid-789"; + + AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); + MediaData mockMediaData = mock(MediaData.class); + var mockEntity = mock(CdsEntity.class); + when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getAttachmentEntity()).thenReturn(mockEntity); + when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); + when(context.getData()).thenReturn(mockMediaData); + when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("test".getBytes())); + when(mockMediaData.getMimeType()).thenReturn("text/plain"); + // null tenant + UserInfo userInfo = mockUserInfo(null); + when(context.getUserInfo()).thenReturn(userInfo); + + // In MT mode, null tenant should throw ServiceException + assertThrows(ServiceException.class, () -> handler.createAttachment(context)); + } + + @Test + void testCreateAttachment_SharedMode_UsesObjectKey() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, true, "shared"); + + String tenantId = "tenantX"; + String contentId = "doc-create-123"; + + AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); + MediaData mockMediaData = mock(MediaData.class); + var mockEntity = mock(CdsEntity.class); + when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getAttachmentEntity()).thenReturn(mockEntity); + when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); + when(context.getData()).thenReturn(mockMediaData); + when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("data".getBytes())); + when(mockMediaData.getMimeType()).thenReturn("application/pdf"); + UserInfo userInfoX = mockUserInfo(tenantId); + when(context.getUserInfo()).thenReturn(userInfoX); + when(mockOsClient.uploadContent(any(), anyString(), anyString())) + .thenReturn(CompletableFuture.completedFuture(null)); + + handler.createAttachment(context); + + ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class); + verify(mockOsClient).uploadContent(any(), keyCaptor.capture(), anyString()); + org.junit.jupiter.api.Assertions.assertEquals( + tenantId + "/" + contentId, keyCaptor.getValue()); + // contentId stored in the context should remain unprefixed + verify(context).setContentId(contentId); + } + + @Test + void testReadAttachment_SharedMode_UsesObjectKey() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, true, "shared"); + + String tenantId = "tenantRead"; + String contentId = "doc-read-456"; + + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + MediaData mockMediaData = mock(MediaData.class); + when(context.getContentId()).thenReturn(contentId); + when(context.getData()).thenReturn(mockMediaData); + UserInfo userInfoRead = mockUserInfo(tenantId); + when(context.getUserInfo()).thenReturn(userInfoRead); + when(mockOsClient.readContent(anyString())) + .thenReturn( + CompletableFuture.completedFuture(new ByteArrayInputStream("test".getBytes()))); + + handler.readAttachment(context); + + // Verify the read uses the prefixed key + ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class); + verify(mockOsClient).readContent(keyCaptor.capture()); + org.junit.jupiter.api.Assertions.assertEquals( + tenantId + "/" + contentId, keyCaptor.getValue()); + } + + @Test + void testDeleteAttachment_SharedMode_UsesObjectKey() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, true, "shared"); + + String tenantId = "tenantDel"; + String contentId = "doc-del-789"; + + AttachmentMarkAsDeletedEventContext context = mock(AttachmentMarkAsDeletedEventContext.class); + when(context.getContentId()).thenReturn(contentId); + UserInfo userInfoDel = mockUserInfo(tenantId); + when(context.getUserInfo()).thenReturn(userInfoDel); + when(mockOsClient.deleteContent(anyString())) + .thenReturn(CompletableFuture.completedFuture(null)); + + handler.markAttachmentAsDeleted(context); + + // Verify the delete uses the prefixed key + ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class); + verify(mockOsClient).deleteContent(keyCaptor.capture()); + org.junit.jupiter.api.Assertions.assertEquals( + tenantId + "/" + contentId, keyCaptor.getValue()); + } + + @Test + void testCreateAttachment_SingleTenant_NoPrefixing() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + // Backward compatibility: MT disabled + OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, false, null); + + String contentId = "doc-single-abc"; + + AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); + MediaData mockMediaData = mock(MediaData.class); + var mockEntity = mock(CdsEntity.class); + when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getAttachmentEntity()).thenReturn(mockEntity); + when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); + when(context.getData()).thenReturn(mockMediaData); + when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("data".getBytes())); + when(mockMediaData.getMimeType()).thenReturn("text/plain"); + when(mockOsClient.uploadContent(any(), anyString(), anyString())) + .thenReturn(CompletableFuture.completedFuture(null)); + + handler.createAttachment(context); + + ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class); + verify(mockOsClient).uploadContent(any(), keyCaptor.capture(), anyString()); + // No prefix, just contentId + org.junit.jupiter.api.Assertions.assertEquals(contentId, keyCaptor.getValue()); + verify(context).setContentId(contentId); + } + + // ==================== Tenant ID Validation Tests ==================== + + @Test + void testValidateTenantId_EmptyTenant_ThrowsException() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, true, "shared"); + + String contentId = "content-id"; + AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); + MediaData mockMediaData = mock(MediaData.class); + var mockEntity = mock(CdsEntity.class); + when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getAttachmentEntity()).thenReturn(mockEntity); + when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); + when(context.getData()).thenReturn(mockMediaData); + when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("test".getBytes())); + when(mockMediaData.getMimeType()).thenReturn("text/plain"); + UserInfo userInfo = mockUserInfo(""); + when(context.getUserInfo()).thenReturn(userInfo); + + assertThrows(ServiceException.class, () -> handler.createAttachment(context)); + } + + @Test + void testValidateTenantId_SlashInTenant_ThrowsException() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, true, "shared"); + + String contentId = "content-id"; + AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); + MediaData mockMediaData = mock(MediaData.class); + var mockEntity = mock(CdsEntity.class); + when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getAttachmentEntity()).thenReturn(mockEntity); + when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); + when(context.getData()).thenReturn(mockMediaData); + when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("test".getBytes())); + when(mockMediaData.getMimeType()).thenReturn("text/plain"); + UserInfo userInfo = mockUserInfo("tenant/evil"); + when(context.getUserInfo()).thenReturn(userInfo); + + assertThrows(ServiceException.class, () -> handler.createAttachment(context)); + } + + @Test + void testValidateTenantId_BackslashInTenant_ThrowsException() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, true, "shared"); + + String contentId = "content-id"; + AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); + MediaData mockMediaData = mock(MediaData.class); + var mockEntity = mock(CdsEntity.class); + when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getAttachmentEntity()).thenReturn(mockEntity); + when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); + when(context.getData()).thenReturn(mockMediaData); + when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("test".getBytes())); + when(mockMediaData.getMimeType()).thenReturn("text/plain"); + UserInfo userInfo = mockUserInfo("tenant\\evil"); + when(context.getUserInfo()).thenReturn(userInfo); + + assertThrows(ServiceException.class, () -> handler.createAttachment(context)); + } + + @Test + void testValidateTenantId_PathTraversal_ThrowsException() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, true, "shared"); + + String contentId = "content-id"; + AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); + MediaData mockMediaData = mock(MediaData.class); + var mockEntity = mock(CdsEntity.class); + when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getAttachmentEntity()).thenReturn(mockEntity); + when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); + when(context.getData()).thenReturn(mockMediaData); + when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("test".getBytes())); + when(mockMediaData.getMimeType()).thenReturn("text/plain"); + UserInfo userInfo = mockUserInfo("..evil"); + when(context.getUserInfo()).thenReturn(userInfo); + + assertThrows(ServiceException.class, () -> handler.createAttachment(context)); + } } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTestUtils.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTestUtils.java index 615aba16c..f96178dd3 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTestUtils.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTestUtils.java @@ -39,7 +39,8 @@ public static void testCreateReadDeleteAttachmentFlow( String testFileName = "testFileName-" + System.currentTimeMillis() + ".txt"; String testFileContent = "test"; - OSSAttachmentsServiceHandler handler = new OSSAttachmentsServiceHandler(binding, executor); + OSSAttachmentsServiceHandler handler = + new OSSAttachmentsServiceHandler(binding, executor, false, null); // Create an AttachmentCreateEventContext with mocked data - to upload a test attachment MediaData createMediaData = mock(MediaData.class); diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java new file mode 100644 index 000000000..7f81186ff --- /dev/null +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java @@ -0,0 +1,82 @@ +/* + * © 2025 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.oss.handler; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.sap.cds.feature.attachments.oss.client.OSClient; +import com.sap.cds.services.mt.UnsubscribeEventContext; +import java.util.concurrent.CompletableFuture; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class TenantCleanupHandlerTest { + + private OSClient osClient; + private UnsubscribeEventContext context; + private TenantCleanupHandler handler; + + @BeforeEach + void setUp() { + osClient = mock(OSClient.class); + context = mock(UnsubscribeEventContext.class); + handler = new TenantCleanupHandler(osClient); + } + + @Test + void testCleanupTenantData_CallsDeleteByPrefix() throws Exception { + String tenantId = "tenant-abc"; + when(context.getTenant()).thenReturn(tenantId); + when(osClient.deleteContentByPrefix(tenantId + "/")) + .thenReturn(CompletableFuture.completedFuture(null)); + + handler.cleanupTenantData(context); + + verify(osClient).deleteContentByPrefix(tenantId + "/"); + } + + @Test + void testCleanupTenantData_UsesCorrectPrefix() throws Exception { + String tenantId = "my-tenant-123"; + when(context.getTenant()).thenReturn(tenantId); + when(osClient.deleteContentByPrefix("my-tenant-123/")) + .thenReturn(CompletableFuture.completedFuture(null)); + + handler.cleanupTenantData(context); + + verify(osClient).deleteContentByPrefix("my-tenant-123/"); + } + + @Test + void testCleanupTenantData_HandlesException() throws Exception { + String tenantId = "tenant-fail"; + when(context.getTenant()).thenReturn(tenantId); + + CompletableFuture failedFuture = new CompletableFuture<>(); + failedFuture.completeExceptionally(new RuntimeException("Storage error")); + when(osClient.deleteContentByPrefix(tenantId + "/")).thenReturn(failedFuture); + + // Should NOT rethrow — the handler logs the error but does not fail the unsubscribe + handler.cleanupTenantData(context); + + verify(osClient).deleteContentByPrefix(tenantId + "/"); + } + + @Test + void testCleanupTenantData_HandlesInterruptedException() throws Exception { + String tenantId = "tenant-interrupt"; + when(context.getTenant()).thenReturn(tenantId); + + CompletableFuture failedFuture = new CompletableFuture<>(); + failedFuture.completeExceptionally(new InterruptedException("Interrupted")); + when(osClient.deleteContentByPrefix(tenantId + "/")).thenReturn(failedFuture); + + // Should NOT rethrow — the handler logs the error but does not fail the unsubscribe + handler.cleanupTenantData(context); + + verify(osClient).deleteContentByPrefix(tenantId + "/"); + } +} From 5c24888aabd3a6ba41e773c3d9afd3f5ea8db190 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Sun, 29 Mar 2026 13:23:21 +0200 Subject: [PATCH 02/18] bug fixes --- .../attachments/oss/client/AWSClient.java | 10 ++- .../attachments/oss/client/AzureClient.java | 21 ++++++- .../attachments/oss/client/GoogleClient.java | 15 ++--- .../handler/OSSAttachmentsServiceHandler.java | 12 ++++ .../oss/client/GoogleClientTest.java | 40 +++--------- .../oss/handler/MultiTenantIsolationTest.java | 61 +++++++++++++++++++ .../OSSAttachmentsServiceHandlerTest.java | 1 + 7 files changed, 118 insertions(+), 42 deletions(-) diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java index 831818ccd..137a6c9fc 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java @@ -23,7 +23,9 @@ import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; import software.amazon.awssdk.services.s3.model.DeleteObjectResponse; import software.amazon.awssdk.services.s3.model.DeleteObjectsRequest; +import software.amazon.awssdk.services.s3.model.DeleteObjectsResponse; import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.S3Error; import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; import software.amazon.awssdk.services.s3.model.ListObjectsV2Response; import software.amazon.awssdk.services.s3.model.ObjectIdentifier; @@ -156,7 +158,13 @@ public Future deleteContentByPrefix(String prefix) { .bucket(this.bucketName) .delete(Delete.builder().objects(keys).build()) .build(); - s3Client.deleteObjects(deleteReq); + DeleteObjectsResponse deleteResp = s3Client.deleteObjects(deleteReq); + if (deleteResp.hasErrors() && !deleteResp.errors().isEmpty()) { + logger.warn( + "Failed to delete {} objects during prefix cleanup: {}", + deleteResp.errors().size(), + deleteResp.errors().stream().map(S3Error::key).toList()); + } } listReq = listReq.toBuilder() diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AzureClient.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AzureClient.java index b7ad983f9..a81258823 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AzureClient.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AzureClient.java @@ -13,6 +13,8 @@ import com.sap.cds.feature.attachments.oss.handler.ObjectStoreServiceException; import com.sap.cloud.environment.servicebinding.api.ServiceBinding; import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import org.slf4j.Logger; @@ -95,12 +97,29 @@ public Future deleteContentByPrefix(String prefix) { () -> { try { ListBlobsOptions options = new ListBlobsOptions().setPrefix(prefix); + List blobNames = new ArrayList<>(); for (BlobItem blobItem : blobContainerClient.listBlobs(options, null)) { - blobContainerClient.getBlobClient(blobItem.getName()).delete(); + blobNames.add(blobItem.getName()); + } + List> deleteFutures = + blobNames.stream() + .map( + name -> + executor.submit( + () -> { + blobContainerClient.getBlobClient(name).delete(); + return (Void) null; + })) + .toList(); + for (Future f : deleteFutures) { + f.get(); } } catch (RuntimeException e) { throw new ObjectStoreServiceException( "Failed to delete objects by prefix from the Azure Object Store", e); + } catch (Exception e) { + throw new ObjectStoreServiceException( + "Failed to delete objects by prefix from the Azure Object Store", e); } return null; }); diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/GoogleClient.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/GoogleClient.java index ec3a0a5aa..e82fb5a64 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/GoogleClient.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/GoogleClient.java @@ -141,17 +141,12 @@ public Future deleteContentByPrefix(String prefix) { () -> { try { Page blobs = - storage.list(bucketName, Storage.BlobListOption.prefix(prefix)); + storage.list( + bucketName, + Storage.BlobListOption.prefix(prefix), + Storage.BlobListOption.versions(true)); for (Blob blob : blobs.iterateAll()) { - Page versions = - storage.list( - bucketName, - Storage.BlobListOption.versions(true), - Storage.BlobListOption.prefix(blob.getName())); - for (Blob version : versions.iterateAll()) { - storage.delete( - BlobId.of(bucketName, version.getName(), version.getGeneration())); - } + storage.delete(BlobId.of(bucketName, blob.getName(), blob.getGeneration())); } } catch (RuntimeException e) { throw new ObjectStoreServiceException( diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java index 09f7b8e31..1a41d0bb3 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java @@ -203,6 +203,7 @@ private String buildObjectKey(EventContext context, String contentId) { if (multitenancyEnabled && "shared".equals(objectStoreKind)) { String tenant = getTenant(context); validateTenantId(tenant); + validateContentId(contentId); return tenant + "/" + contentId; } return contentId; @@ -225,4 +226,15 @@ private static void validateTenantId(String tenantId) { "Invalid tenant ID for attachment storage: must not be empty or contain path separators"); } } + + private static void validateContentId(String contentId) { + if (contentId == null + || contentId.isEmpty() + || contentId.contains("/") + || contentId.contains("\\") + || contentId.contains("..")) { + throw new ServiceException( + "Invalid content ID for attachment storage: must not be empty or contain path separators"); + } + } } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java index 5da93a59e..f682dfaae 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java @@ -291,7 +291,7 @@ void testDeleteContentByPrefix_DeletesAllMatchingBlobs() bucketField.setAccessible(true); bucketField.set(googleClient, "my-bucket"); - // Mock listing of blobs by prefix + // Mock blobs returned by single list call with prefix + versions Blob blobA = mock(Blob.class); when(blobA.getName()).thenReturn("tenantX/file1.txt"); when(blobA.getGeneration()).thenReturn(1L); @@ -299,25 +299,15 @@ void testDeleteContentByPrefix_DeletesAllMatchingBlobs() when(blobB.getName()).thenReturn("tenantX/file2.txt"); when(blobB.getGeneration()).thenReturn(2L); - // First call: list by prefix (without versions) returns the blobs - Page prefixPage = mock(Page.class); - when(prefixPage.iterateAll()).thenReturn(() -> java.util.Arrays.asList(blobA, blobB).iterator()); - - // For each blob, list versions returns the same blob (single version each) - Page versionPageA = mock(Page.class); - when(versionPageA.iterateAll()).thenReturn(() -> Collections.singletonList(blobA).iterator()); - Page versionPageB = mock(Page.class); - when(versionPageB.iterateAll()).thenReturn(() -> Collections.singletonList(blobB).iterator()); - - // First call: prefix listing; subsequent calls: version listings - when(mockStorage.list(anyString(), any())) - .thenReturn(prefixPage); - when(mockStorage.list(anyString(), any(), any())) - .thenReturn(versionPageA, versionPageB); + Page page = mock(Page.class); + when(page.iterateAll()).thenReturn(() -> java.util.Arrays.asList(blobA, blobB).iterator()); + when(mockStorage.list(anyString(), any(), any())).thenReturn(page); when(mockStorage.delete(any(BlobId.class))).thenReturn(true); assertDoesNotThrow(() -> googleClient.deleteContentByPrefix("tenantX/").get()); + org.mockito.Mockito.verify(mockStorage, org.mockito.Mockito.times(2)) + .delete(any(BlobId.class)); } @Test @@ -338,11 +328,7 @@ void testDeleteContentByPrefix_HandlesVersioning() bucketField.setAccessible(true); bucketField.set(googleClient, "my-bucket"); - // Blob with 2 versions - Blob blob = mock(Blob.class); - when(blob.getName()).thenReturn("tenantY/versioned-file.txt"); - when(blob.getGeneration()).thenReturn(1L); - + // Single blob with 2 versions returned directly by the combined list call Blob version1 = mock(Blob.class); when(version1.getName()).thenReturn("tenantY/versioned-file.txt"); when(version1.getGeneration()).thenReturn(1L); @@ -351,17 +337,11 @@ void testDeleteContentByPrefix_HandlesVersioning() when(version2.getName()).thenReturn("tenantY/versioned-file.txt"); when(version2.getGeneration()).thenReturn(2L); - Page prefixPage = mock(Page.class); - when(prefixPage.iterateAll()).thenReturn(() -> Collections.singletonList(blob).iterator()); - - Page versionPage = mock(Page.class); - when(versionPage.iterateAll()) + Page page = mock(Page.class); + when(page.iterateAll()) .thenReturn(() -> java.util.Arrays.asList(version1, version2).iterator()); - when(mockStorage.list(anyString(), any())) - .thenReturn(prefixPage); - when(mockStorage.list(anyString(), any(), any())) - .thenReturn(versionPage); + when(mockStorage.list(anyString(), any(), any())).thenReturn(page); when(mockStorage.delete(any(BlobId.class))).thenReturn(true); assertDoesNotThrow(() -> googleClient.deleteContentByPrefix("tenantY/").get()); diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/MultiTenantIsolationTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/MultiTenantIsolationTest.java index 96c9cccbd..dfbcd0cc0 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/MultiTenantIsolationTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/MultiTenantIsolationTest.java @@ -190,6 +190,67 @@ void testNullTenant_ThrowsInSharedMTMode() { assertEquals(0, uploadedKeys.size()); } + // ==================== Content ID Validation Integration Tests ==================== + + @Test + void testPathTraversalInContentId_BlockedInSharedMode() { + // A malicious contentId like "../other-tenant/secret" must be rejected + // to prevent tenant A from reading/writing tenant B's namespace + assertThrows( + ServiceException.class, + () -> simulateCreate("tenantA", "../other-tenant/secret", "malicious data")); + assertEquals(0, uploadedKeys.size()); + } + + @Test + void testSlashInContentId_BlockedInSharedMode() { + assertThrows( + ServiceException.class, + () -> simulateCreate("tenantA", "sub/path", "data")); + assertEquals(0, uploadedKeys.size()); + } + + @Test + void testBackslashInContentId_BlockedInSharedMode() { + assertThrows( + ServiceException.class, + () -> simulateCreate("tenantA", "evil\\path", "data")); + assertEquals(0, uploadedKeys.size()); + } + + @Test + void testEmptyContentId_BlockedInSharedMode() { + assertThrows( + ServiceException.class, + () -> simulateCreate("tenantA", "", "data")); + assertEquals(0, uploadedKeys.size()); + } + + @Test + void testDoubleDotInContentId_BlockedInSharedMode() { + assertThrows( + ServiceException.class, + () -> simulateCreate("tenantA", "..otherTenant", "data")); + assertEquals(0, uploadedKeys.size()); + } + + @Test + void testPathTraversalInContentId_FullIsolationScenario() { + // Tenant B uploads a legitimate file + simulateCreate("tenantB", "legit-content-id", "Tenant B secret"); + assertEquals(1, uploadedKeys.size()); + assertEquals("tenantB/legit-content-id", uploadedKeys.get(0)); + + // Tenant A tries to use path traversal to access tenant B's file + assertThrows( + ServiceException.class, + () -> simulateCreate("tenantA", "../tenantB/legit-content-id", "overwrite attempt")); + + // Only the original upload should exist + assertEquals(1, uploadedKeys.size()); + assertTrue(storage.containsKey("tenantB/legit-content-id")); + } + private void simulateCreate(String tenant, String contentId, String content) { AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); MediaData mockMediaData = mock(MediaData.class); diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java index 26404733d..e952aa102 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java @@ -710,4 +710,5 @@ void testValidateTenantId_PathTraversal_ThrowsException() assertThrows(ServiceException.class, () -> handler.createAttachment(context)); } + } From d0cc4130926756a358a917292637f6eae94777f9 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Mon, 30 Mar 2026 08:43:52 +0200 Subject: [PATCH 03/18] revert --- .../attachments/oss/client/AWSClientTest.java | 102 +----- .../oss/client/AzureClientTest.java | 61 ---- .../oss/client/GoogleClientTest.java | 78 ---- .../attachments/oss/client/OSClientTest.java | 41 --- .../oss/configuration/RegistrationTest.java | 102 ------ .../oss/handler/MultiTenantIsolationTest.java | 295 --------------- .../OSSAttachmentsServiceHandlerTest.java | 344 +----------------- ...OSSAttachmentsServiceHandlerTestUtils.java | 3 +- .../oss/handler/TenantCleanupHandlerTest.java | 82 ----- 9 files changed, 7 insertions(+), 1101 deletions(-) delete mode 100644 storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/OSClientTest.java delete mode 100644 storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/MultiTenantIsolationTest.java delete mode 100644 storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java index 7fc91a811..032c1a8a2 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java @@ -9,7 +9,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.sap.cds.feature.attachments.oss.handler.OSSAttachmentsServiceHandler; @@ -31,15 +30,10 @@ import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; import software.amazon.awssdk.services.s3.model.DeleteObjectResponse; -import software.amazon.awssdk.services.s3.model.DeleteObjectsRequest; -import software.amazon.awssdk.services.s3.model.DeleteObjectsResponse; import software.amazon.awssdk.services.s3.model.GetObjectRequest; import software.amazon.awssdk.services.s3.model.GetObjectResponse; -import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; -import software.amazon.awssdk.services.s3.model.ListObjectsV2Response; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectResponse; -import software.amazon.awssdk.services.s3.model.S3Object; class AWSClientTest { ExecutorService executor = Executors.newCachedThreadPool(); @@ -48,7 +42,7 @@ class AWSClientTest { void testConstructorWithAwsBindingUsesAwsClient() throws NoSuchFieldException, IllegalAccessException { OSSAttachmentsServiceHandler handler = - new OSSAttachmentsServiceHandler(getDummyBinding(), executor, false, null); + new OSSAttachmentsServiceHandler(getDummyBinding(), executor); OSClient client = OSSAttachmentsServiceHandlerTestUtils.getOsClient(handler); assertInstanceOf(AWSClient.class, client); } @@ -231,100 +225,6 @@ void testDeleteContentThrowsOnUnsuccessfulResponse() assertInstanceOf(ObjectStoreServiceException.class, thrown.getCause()); } - @Test - void testDeleteContentByPrefix_DeletesAllMatchingObjects() throws Exception { - AWSClient awsClient = new AWSClient(getDummyBinding(), executor); - - S3Client mockS3Client = mock(S3Client.class); - - // Create mock list response with 2 objects matching the prefix - S3Object obj1 = S3Object.builder().key("tenantA/file1.txt").build(); - S3Object obj2 = S3Object.builder().key("tenantA/file2.txt").build(); - ListObjectsV2Response listResponse = - ListObjectsV2Response.builder() - .contents(obj1, obj2) - .isTruncated(false) - .build(); - - when(mockS3Client.listObjectsV2(any(ListObjectsV2Request.class))).thenReturn(listResponse); - DeleteObjectsResponse deleteResponse = mock(DeleteObjectsResponse.class); - when(mockS3Client.deleteObjects(any(DeleteObjectsRequest.class))).thenReturn(deleteResponse); - - var field = AWSClient.class.getDeclaredField("s3Client"); - field.setAccessible(true); - field.set(awsClient, mockS3Client); - - assertDoesNotThrow(() -> awsClient.deleteContentByPrefix("tenantA/").get()); - verify(mockS3Client).listObjectsV2(any(ListObjectsV2Request.class)); - verify(mockS3Client).deleteObjects(any(DeleteObjectsRequest.class)); - } - - @Test - void testDeleteContentByPrefix_HandlesPagination() throws Exception { - AWSClient awsClient = new AWSClient(getDummyBinding(), executor); - - S3Client mockS3Client = mock(S3Client.class); - - // First page: truncated, has a continuation token - S3Object obj1 = S3Object.builder().key("tenantB/file1.txt").build(); - ListObjectsV2Response firstPage = - ListObjectsV2Response.builder() - .contents(obj1) - .isTruncated(true) - .nextContinuationToken("token-page2") - .build(); - - // Second page: not truncated, final page - S3Object obj2 = S3Object.builder().key("tenantB/file2.txt").build(); - ListObjectsV2Response secondPage = - ListObjectsV2Response.builder() - .contents(obj2) - .isTruncated(false) - .build(); - - when(mockS3Client.listObjectsV2(any(ListObjectsV2Request.class))) - .thenReturn(firstPage, secondPage); - DeleteObjectsResponse deleteResponse = mock(DeleteObjectsResponse.class); - when(mockS3Client.deleteObjects(any(DeleteObjectsRequest.class))).thenReturn(deleteResponse); - - var field = AWSClient.class.getDeclaredField("s3Client"); - field.setAccessible(true); - field.set(awsClient, mockS3Client); - - assertDoesNotThrow(() -> awsClient.deleteContentByPrefix("tenantB/").get()); - // Should call listObjectsV2 twice (two pages) and deleteObjects twice - verify(mockS3Client, org.mockito.Mockito.times(2)) - .listObjectsV2(any(ListObjectsV2Request.class)); - verify(mockS3Client, org.mockito.Mockito.times(2)) - .deleteObjects(any(DeleteObjectsRequest.class)); - } - - @Test - void testDeleteContentByPrefix_EmptyPrefix_NoObjects() throws Exception { - AWSClient awsClient = new AWSClient(getDummyBinding(), executor); - - S3Client mockS3Client = mock(S3Client.class); - - // Empty listing - ListObjectsV2Response emptyResponse = - ListObjectsV2Response.builder() - .contents(java.util.Collections.emptyList()) - .isTruncated(false) - .build(); - - when(mockS3Client.listObjectsV2(any(ListObjectsV2Request.class))).thenReturn(emptyResponse); - - var field = AWSClient.class.getDeclaredField("s3Client"); - field.setAccessible(true); - field.set(awsClient, mockS3Client); - - assertDoesNotThrow(() -> awsClient.deleteContentByPrefix("nonexistent/").get()); - verify(mockS3Client).listObjectsV2(any(ListObjectsV2Request.class)); - // No deleteObjects call when there are no objects - verify(mockS3Client, org.mockito.Mockito.never()) - .deleteObjects(any(DeleteObjectsRequest.class)); - } - private ServiceBinding getDummyBinding() { ServiceBinding binding = mock(ServiceBinding.class); HashMap creds = new HashMap<>(); diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java index 59b42bfaf..9b7100468 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java @@ -7,22 +7,16 @@ import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; -import com.azure.core.http.rest.PagedIterable; import com.azure.storage.blob.BlobClient; import com.azure.storage.blob.BlobContainerClient; -import com.azure.storage.blob.models.BlobItem; -import com.azure.storage.blob.models.ListBlobsOptions; import com.azure.storage.blob.specialized.BlobOutputStream; import com.azure.storage.blob.specialized.BlockBlobClient; import com.sap.cds.feature.attachments.oss.handler.ObjectStoreServiceException; import java.io.IOException; import java.io.InputStream; -import java.util.Arrays; -import java.util.Collections; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.stream.Stream; import org.junit.jupiter.api.Test; class AzureClientTest { @@ -195,59 +189,4 @@ void testReadContentThrowsOnRuntimeException() assertThrows(ExecutionException.class, () -> azureClient.readContent("file.txt").get()); assertInstanceOf(ObjectStoreServiceException.class, thrown.getCause()); } - - @Test - @SuppressWarnings("unchecked") - void testDeleteContentByPrefix_DeletesAllMatchingBlobs() - throws NoSuchFieldException, IllegalAccessException { - AzureClient azureClient = mock(AzureClient.class, CALLS_REAL_METHODS); - - BlobContainerClient mockContainer = mock(BlobContainerClient.class); - BlobClient mockBlobClient = mock(BlobClient.class); - - var field = AzureClient.class.getDeclaredField("blobContainerClient"); - field.setAccessible(true); - field.set(azureClient, mockContainer); - var executorField = AzureClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(azureClient, executor); - - // Create mock blob items matching the prefix - BlobItem blob1 = mock(BlobItem.class); - when(blob1.getName()).thenReturn("tenantA/file1.txt"); - BlobItem blob2 = mock(BlobItem.class); - when(blob2.getName()).thenReturn("tenantA/file2.txt"); - - PagedIterable mockPagedIterable = mock(PagedIterable.class); - when(mockPagedIterable.stream()).thenReturn(Stream.of(blob1, blob2)); - when(mockPagedIterable.iterator()).thenReturn(Arrays.asList(blob1, blob2).iterator()); - when(mockContainer.listBlobs(any(ListBlobsOptions.class), any())).thenReturn(mockPagedIterable); - when(mockContainer.getBlobClient(anyString())).thenReturn(mockBlobClient); - - assertDoesNotThrow(() -> azureClient.deleteContentByPrefix("tenantA/").get()); - } - - @Test - @SuppressWarnings("unchecked") - void testDeleteContentByPrefix_EmptyResult() - throws NoSuchFieldException, IllegalAccessException { - AzureClient azureClient = mock(AzureClient.class, CALLS_REAL_METHODS); - - BlobContainerClient mockContainer = mock(BlobContainerClient.class); - - var field = AzureClient.class.getDeclaredField("blobContainerClient"); - field.setAccessible(true); - field.set(azureClient, mockContainer); - var executorField = AzureClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(azureClient, executor); - - // Empty listing - PagedIterable emptyIterable = mock(PagedIterable.class); - when(emptyIterable.stream()).thenReturn(Stream.empty()); - when(emptyIterable.iterator()).thenReturn(Collections.emptyIterator()); - when(mockContainer.listBlobs(any(ListBlobsOptions.class), any())).thenReturn(emptyIterable); - - assertDoesNotThrow(() -> azureClient.deleteContentByPrefix("nonexistent-tenant/").get()); - } } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java index f682dfaae..f4c2fd188 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java @@ -272,82 +272,4 @@ void testReadContentThrowsOnRuntimeException() assertThrows(ExecutionException.class, () -> googleClient.readContent("file.txt").get()); assertInstanceOf(ObjectStoreServiceException.class, thrown.getCause()); } - - @Test - @SuppressWarnings("unchecked") - void testDeleteContentByPrefix_DeletesAllMatchingBlobs() - throws NoSuchFieldException, IllegalAccessException { - GoogleClient googleClient = mock(GoogleClient.class, CALLS_REAL_METHODS); - - Storage mockStorage = mock(Storage.class); - - var field = GoogleClient.class.getDeclaredField("storage"); - field.setAccessible(true); - field.set(googleClient, mockStorage); - var executorField = GoogleClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(googleClient, executor); - var bucketField = GoogleClient.class.getDeclaredField("bucketName"); - bucketField.setAccessible(true); - bucketField.set(googleClient, "my-bucket"); - - // Mock blobs returned by single list call with prefix + versions - Blob blobA = mock(Blob.class); - when(blobA.getName()).thenReturn("tenantX/file1.txt"); - when(blobA.getGeneration()).thenReturn(1L); - Blob blobB = mock(Blob.class); - when(blobB.getName()).thenReturn("tenantX/file2.txt"); - when(blobB.getGeneration()).thenReturn(2L); - - Page page = mock(Page.class); - when(page.iterateAll()).thenReturn(() -> java.util.Arrays.asList(blobA, blobB).iterator()); - - when(mockStorage.list(anyString(), any(), any())).thenReturn(page); - when(mockStorage.delete(any(BlobId.class))).thenReturn(true); - - assertDoesNotThrow(() -> googleClient.deleteContentByPrefix("tenantX/").get()); - org.mockito.Mockito.verify(mockStorage, org.mockito.Mockito.times(2)) - .delete(any(BlobId.class)); - } - - @Test - @SuppressWarnings("unchecked") - void testDeleteContentByPrefix_HandlesVersioning() - throws NoSuchFieldException, IllegalAccessException { - GoogleClient googleClient = mock(GoogleClient.class, CALLS_REAL_METHODS); - - Storage mockStorage = mock(Storage.class); - - var field = GoogleClient.class.getDeclaredField("storage"); - field.setAccessible(true); - field.set(googleClient, mockStorage); - var executorField = GoogleClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(googleClient, executor); - var bucketField = GoogleClient.class.getDeclaredField("bucketName"); - bucketField.setAccessible(true); - bucketField.set(googleClient, "my-bucket"); - - // Single blob with 2 versions returned directly by the combined list call - Blob version1 = mock(Blob.class); - when(version1.getName()).thenReturn("tenantY/versioned-file.txt"); - when(version1.getGeneration()).thenReturn(1L); - - Blob version2 = mock(Blob.class); - when(version2.getName()).thenReturn("tenantY/versioned-file.txt"); - when(version2.getGeneration()).thenReturn(2L); - - Page page = mock(Page.class); - when(page.iterateAll()) - .thenReturn(() -> java.util.Arrays.asList(version1, version2).iterator()); - - when(mockStorage.list(anyString(), any(), any())).thenReturn(page); - when(mockStorage.delete(any(BlobId.class))).thenReturn(true); - - assertDoesNotThrow(() -> googleClient.deleteContentByPrefix("tenantY/").get()); - - // Should delete both versions - org.mockito.Mockito.verify(mockStorage, org.mockito.Mockito.times(2)) - .delete(any(BlobId.class)); - } } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/OSClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/OSClientTest.java deleted file mode 100644 index f1861534b..000000000 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/OSClientTest.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * © 2025 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.oss.client; - -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.util.concurrent.ExecutionException; -import org.junit.jupiter.api.Test; - -class OSClientTest { - - @Test - void testDeleteContentByPrefix_DefaultThrowsUnsupported() { - // Anonymous implementation that only has the default methods - OSClient client = - new OSClient() { - @Override - public java.util.concurrent.Future uploadContent( - java.io.InputStream content, String completeFileName, String contentType) { - return null; - } - - @Override - public java.util.concurrent.Future deleteContent(String completeFileName) { - return null; - } - - @Override - public java.util.concurrent.Future readContent( - String completeFileName) { - return null; - } - }; - - ExecutionException ex = - assertThrows(ExecutionException.class, () -> client.deleteContentByPrefix("prefix").get()); - assertInstanceOf(UnsupportedOperationException.class, ex.getCause()); - } -} diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java index 5babe024d..a364aa7d7 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java @@ -7,7 +7,6 @@ import static org.mockito.Mockito.*; import com.sap.cds.feature.attachments.oss.handler.OSSAttachmentsServiceHandler; -import com.sap.cds.feature.attachments.oss.handler.TenantCleanupHandler; import com.sap.cds.services.environment.CdsEnvironment; import com.sap.cds.services.runtime.CdsRuntime; import com.sap.cds.services.runtime.CdsRuntimeConfigurer; @@ -49,105 +48,4 @@ void testEventHandlersRegistersOSSHandler() { // Assert: OSSAttachmentsServiceHandler should be registered verify(configurer).eventHandler(any(OSSAttachmentsServiceHandler.class)); } - - @Test - void testRegistration_SharedMode_RegistersCleanupHandler() { - Registration registration = new Registration(); - CdsRuntimeConfigurer configurer = mock(CdsRuntimeConfigurer.class); - CdsRuntime cdsRuntime = mock(CdsRuntime.class); - CdsEnvironment environment = mock(CdsEnvironment.class); - ServiceBinding binding = mock(ServiceBinding.class); - - Map credentials = new HashMap<>(); - credentials.put("host", "aws.example.com"); - credentials.put("region", "us-east-1"); - credentials.put("access_key_id", "test-access-key"); - credentials.put("secret_access_key", "test-secret-key"); - credentials.put("bucket", "test-bucket"); - - when(configurer.getCdsRuntime()).thenReturn(cdsRuntime); - when(cdsRuntime.getEnvironment()).thenReturn(environment); - when(binding.getServiceName()).thenReturn(Optional.of("objectstore")); - when(binding.getCredentials()).thenReturn(credentials); - when(environment.getServiceBindings()).thenReturn(Stream.of(binding)); - - // Configure shared mode multitenancy - when(environment.getProperty("cds.multitenancy.enabled", Boolean.class, Boolean.FALSE)) - .thenReturn(Boolean.TRUE); - when(environment.getProperty("cds.attachments.objectStore.kind", String.class, null)) - .thenReturn("shared"); - - registration.eventHandlers(configurer); - - // Should register both the handler AND the TenantCleanupHandler - verify(configurer).eventHandler(any(OSSAttachmentsServiceHandler.class)); - verify(configurer).eventHandler(any(TenantCleanupHandler.class)); - } - - @Test - void testRegistration_NonMTMode_NoCleanupHandler() { - Registration registration = new Registration(); - CdsRuntimeConfigurer configurer = mock(CdsRuntimeConfigurer.class); - CdsRuntime cdsRuntime = mock(CdsRuntime.class); - CdsEnvironment environment = mock(CdsEnvironment.class); - ServiceBinding binding = mock(ServiceBinding.class); - - Map credentials = new HashMap<>(); - credentials.put("host", "aws.example.com"); - credentials.put("region", "us-east-1"); - credentials.put("access_key_id", "test-access-key"); - credentials.put("secret_access_key", "test-secret-key"); - credentials.put("bucket", "test-bucket"); - - when(configurer.getCdsRuntime()).thenReturn(cdsRuntime); - when(cdsRuntime.getEnvironment()).thenReturn(environment); - when(binding.getServiceName()).thenReturn(Optional.of("objectstore")); - when(binding.getCredentials()).thenReturn(credentials); - when(environment.getServiceBindings()).thenReturn(Stream.of(binding)); - - // No MT configuration — defaults - when(environment.getProperty("cds.multitenancy.enabled", Boolean.class, Boolean.FALSE)) - .thenReturn(Boolean.FALSE); - when(environment.getProperty("cds.attachments.objectStore.kind", String.class, null)) - .thenReturn(null); - - registration.eventHandlers(configurer); - - // Only the handler, NOT the cleanup handler - verify(configurer).eventHandler(any(OSSAttachmentsServiceHandler.class)); - verify(configurer, never()).eventHandler(any(TenantCleanupHandler.class)); - } - - @Test - void testRegistration_PassesMTConfigToHandler() { - Registration registration = new Registration(); - CdsRuntimeConfigurer configurer = mock(CdsRuntimeConfigurer.class); - CdsRuntime cdsRuntime = mock(CdsRuntime.class); - CdsEnvironment environment = mock(CdsEnvironment.class); - ServiceBinding binding = mock(ServiceBinding.class); - - Map credentials = new HashMap<>(); - credentials.put("host", "aws.example.com"); - credentials.put("region", "us-east-1"); - credentials.put("access_key_id", "test-access-key"); - credentials.put("secret_access_key", "test-secret-key"); - credentials.put("bucket", "test-bucket"); - - when(configurer.getCdsRuntime()).thenReturn(cdsRuntime); - when(cdsRuntime.getEnvironment()).thenReturn(environment); - when(binding.getServiceName()).thenReturn(Optional.of("objectstore")); - when(binding.getCredentials()).thenReturn(credentials); - when(environment.getServiceBindings()).thenReturn(Stream.of(binding)); - - when(environment.getProperty("cds.multitenancy.enabled", Boolean.class, Boolean.FALSE)) - .thenReturn(Boolean.TRUE); - when(environment.getProperty("cds.attachments.objectStore.kind", String.class, null)) - .thenReturn("shared"); - - registration.eventHandlers(configurer); - - // Verify a handler was registered — we trust the implementation passes the config - // through the constructor (which is verified by OSSAttachmentsServiceHandlerTest) - verify(configurer, times(2)).eventHandler(any()); - } } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/MultiTenantIsolationTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/MultiTenantIsolationTest.java deleted file mode 100644 index dfbcd0cc0..000000000 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/MultiTenantIsolationTest.java +++ /dev/null @@ -1,295 +0,0 @@ -/* - * © 2025 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.oss.handler; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.CALLS_REAL_METHODS; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.Attachments; -import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.MediaData; -import com.sap.cds.feature.attachments.oss.client.OSClient; -import com.sap.cds.services.ServiceException; -import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentCreateEventContext; -import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentMarkAsDeletedEventContext; -import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentReadEventContext; -import com.sap.cds.services.request.UserInfo; -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -/** - * Integration-level test that verifies complete tenant isolation in shared mode. Uses a tracking - * mock OSClient to record all operations and verify that Tenant A's operations never touch Tenant - * B's key space. - */ -class MultiTenantIsolationTest { - - /** Tracks all upload, read, and delete keys passed to the OSClient. */ - private final List uploadedKeys = new ArrayList<>(); - - private final List readKeys = new ArrayList<>(); - private final List deletedKeys = new ArrayList<>(); - private final Map storage = new HashMap<>(); - - private OSClient trackingClient; - private OSSAttachmentsServiceHandler handler; - - @BeforeEach - void setUp() throws NoSuchFieldException, IllegalAccessException { - uploadedKeys.clear(); - readKeys.clear(); - deletedKeys.clear(); - storage.clear(); - - // Create a tracking OSClient that records all operations - trackingClient = mock(OSClient.class); - - when(trackingClient.uploadContent(any(InputStream.class), anyString(), anyString())) - .thenAnswer( - invocation -> { - InputStream content = invocation.getArgument(0); - String key = invocation.getArgument(1); - uploadedKeys.add(key); - storage.put(key, content.readAllBytes()); - return CompletableFuture.completedFuture(null); - }); - - when(trackingClient.readContent(anyString())) - .thenAnswer( - invocation -> { - String key = invocation.getArgument(0); - readKeys.add(key); - byte[] data = storage.get(key); - if (data != null) { - return CompletableFuture.completedFuture( - (InputStream) new ByteArrayInputStream(data)); - } - return CompletableFuture.completedFuture(null); - }); - - when(trackingClient.deleteContent(anyString())) - .thenAnswer( - invocation -> { - String key = invocation.getArgument(0); - deletedKeys.add(key); - storage.remove(key); - return CompletableFuture.completedFuture(null); - }); - - // Create handler with multitenancy enabled in shared mode - handler = mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - - var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - osClientField.setAccessible(true); - osClientField.set(handler, trackingClient); - - var mtEnabledField = - OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); - mtEnabledField.setAccessible(true); - mtEnabledField.set(handler, true); - - var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); - kindField.setAccessible(true); - kindField.set(handler, "shared"); - } - - @Test - void testTwoTenants_Upload_Read_Delete_CompleteIsolation() { - String tenantA = "tenantA"; - String tenantB = "tenantB"; - String contentIdA = "content-id-A"; - String contentIdB = "content-id-B"; - - // Tenant A uploads - simulateCreate(tenantA, contentIdA, "Data from Tenant A"); - // Tenant B uploads - simulateCreate(tenantB, contentIdB, "Data from Tenant B"); - - // Verify keys are prefixed correctly - assertEquals(2, uploadedKeys.size()); - assertTrue(uploadedKeys.contains(tenantA + "/" + contentIdA)); - assertTrue(uploadedKeys.contains(tenantB + "/" + contentIdB)); - - // Verify no cross-tenant key overlap - assertFalse(uploadedKeys.stream().anyMatch(k -> k.startsWith(tenantA) && k.contains(contentIdB))); - assertFalse(uploadedKeys.stream().anyMatch(k -> k.startsWith(tenantB) && k.contains(contentIdA))); - - // Tenant A reads its own content - simulateRead(tenantA, contentIdA); - assertEquals(tenantA + "/" + contentIdA, readKeys.get(0)); - - // Tenant B reads its own content - simulateRead(tenantB, contentIdB); - assertEquals(tenantB + "/" + contentIdB, readKeys.get(1)); - - // Tenant A deletes its content - simulateDelete(tenantA, contentIdA); - assertEquals(tenantA + "/" + contentIdA, deletedKeys.get(0)); - - // Tenant B's content is still in storage - assertTrue(storage.containsKey(tenantB + "/" + contentIdB)); - assertFalse(storage.containsKey(tenantA + "/" + contentIdA)); - - // Tenant B deletes its content - simulateDelete(tenantB, contentIdB); - assertEquals(tenantB + "/" + contentIdB, deletedKeys.get(1)); - - // Both are gone - assertTrue(storage.isEmpty()); - } - - @Test - void testTenantA_CannotAccess_TenantB_Data() { - String tenantA = "tenantA"; - String tenantB = "tenantB"; - String sharedContentId = "same-content-id"; - - // Both tenants upload with the same contentId (UUID) - simulateCreate(tenantA, sharedContentId, "Tenant A secret"); - simulateCreate(tenantB, sharedContentId, "Tenant B secret"); - - // Keys should be different due to tenant prefix - assertEquals(2, uploadedKeys.size()); - assertEquals(tenantA + "/" + sharedContentId, uploadedKeys.get(0)); - assertEquals(tenantB + "/" + sharedContentId, uploadedKeys.get(1)); - - // Storage should have 2 separate entries - assertEquals(2, storage.size()); - - // Reading as tenant A only returns tenant A's data - simulateRead(tenantA, sharedContentId); - assertEquals(tenantA + "/" + sharedContentId, readKeys.get(0)); - - // The read key never touches tenant B's namespace - assertFalse(readKeys.get(0).startsWith(tenantB)); - } - - @Test - void testNullTenant_ThrowsInSharedMTMode() { - String contentId = "content-no-tenant"; - - // In MT shared mode, null tenant must throw (H-1 security fix) - assertThrows( - ServiceException.class, () -> simulateCreate(null, contentId, "No tenant data")); - - // No upload should have occurred - assertEquals(0, uploadedKeys.size()); - } - - // ==================== Content ID Validation Integration Tests ==================== - - @Test - void testPathTraversalInContentId_BlockedInSharedMode() { - // A malicious contentId like "../other-tenant/secret" must be rejected - // to prevent tenant A from reading/writing tenant B's namespace - assertThrows( - ServiceException.class, - () -> simulateCreate("tenantA", "../other-tenant/secret", "malicious data")); - assertEquals(0, uploadedKeys.size()); - } - - @Test - void testSlashInContentId_BlockedInSharedMode() { - assertThrows( - ServiceException.class, - () -> simulateCreate("tenantA", "sub/path", "data")); - assertEquals(0, uploadedKeys.size()); - } - - @Test - void testBackslashInContentId_BlockedInSharedMode() { - assertThrows( - ServiceException.class, - () -> simulateCreate("tenantA", "evil\\path", "data")); - assertEquals(0, uploadedKeys.size()); - } - - @Test - void testEmptyContentId_BlockedInSharedMode() { - assertThrows( - ServiceException.class, - () -> simulateCreate("tenantA", "", "data")); - assertEquals(0, uploadedKeys.size()); - } - - @Test - void testDoubleDotInContentId_BlockedInSharedMode() { - assertThrows( - ServiceException.class, - () -> simulateCreate("tenantA", "..otherTenant", "data")); - assertEquals(0, uploadedKeys.size()); - } - - @Test - void testPathTraversalInContentId_FullIsolationScenario() { - // Tenant B uploads a legitimate file - simulateCreate("tenantB", "legit-content-id", "Tenant B secret"); - assertEquals(1, uploadedKeys.size()); - assertEquals("tenantB/legit-content-id", uploadedKeys.get(0)); - - // Tenant A tries to use path traversal to access tenant B's file - assertThrows( - ServiceException.class, - () -> simulateCreate("tenantA", "../tenantB/legit-content-id", "overwrite attempt")); - - // Only the original upload should exist - assertEquals(1, uploadedKeys.size()); - assertTrue(storage.containsKey("tenantB/legit-content-id")); - } - - private void simulateCreate(String tenant, String contentId, String content) { - AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); - MediaData mockMediaData = mock(MediaData.class); - var mockEntity = mock(com.sap.cds.reflect.CdsEntity.class); - UserInfo userInfo = mock(UserInfo.class); - - when(userInfo.getTenant()).thenReturn(tenant); - when(context.getUserInfo()).thenReturn(userInfo); - when(context.getAttachmentEntity()).thenReturn(mockEntity); - when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); - when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); - when(context.getData()).thenReturn(mockMediaData); - when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream(content.getBytes())); - when(mockMediaData.getMimeType()).thenReturn("text/plain"); - - handler.createAttachment(context); - } - - private void simulateRead(String tenant, String contentId) { - AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); - MediaData mockMediaData = mock(MediaData.class); - UserInfo userInfo = mock(UserInfo.class); - - when(userInfo.getTenant()).thenReturn(tenant); - when(context.getUserInfo()).thenReturn(userInfo); - when(context.getContentId()).thenReturn(contentId); - when(context.getData()).thenReturn(mockMediaData); - - handler.readAttachment(context); - } - - private void simulateDelete(String tenant, String contentId) { - AttachmentMarkAsDeletedEventContext context = mock(AttachmentMarkAsDeletedEventContext.class); - UserInfo userInfo = mock(UserInfo.class); - - when(userInfo.getTenant()).thenReturn(tenant); - when(context.getUserInfo()).thenReturn(userInfo); - when(context.getContentId()).thenReturn(contentId); - - handler.markAttachmentAsDeleted(context); - } -} diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java index e952aa102..2d973221b 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java @@ -11,7 +11,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.Attachments; import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.MediaData; import com.sap.cds.feature.attachments.oss.client.OSClient; import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentCreateEventContext; @@ -20,19 +19,16 @@ import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentRestoreEventContext; import com.sap.cds.reflect.CdsEntity; import com.sap.cds.services.ServiceException; -import com.sap.cds.services.request.UserInfo; import com.sap.cloud.environment.servicebinding.api.ServiceBinding; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.util.Base64; import java.util.HashMap; -import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; class OSSAttachmentsServiceHandlerTest { ExecutorService executor = Executors.newCachedThreadPool(); @@ -49,8 +45,7 @@ void testRestoreAttachmentCallsSetCompleted() { creds.put("bucket", "test-bucket"); when(binding.getCredentials()).thenReturn(creds); - OSSAttachmentsServiceHandler handler = - new OSSAttachmentsServiceHandler(binding, executor, false, null); + OSSAttachmentsServiceHandler handler = new OSSAttachmentsServiceHandler(binding, executor); AttachmentRestoreEventContext context = mock(AttachmentRestoreEventContext.class); handler.restoreAttachment(context); verify(context).setCompleted(); @@ -184,7 +179,7 @@ void testConstructorHandlesInvalidBase64EncodedPrivateKeyData() { assertThrows( ObjectStoreServiceException.class, - () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); + () -> new OSSAttachmentsServiceHandler(binding, executor)); } @Test @@ -200,7 +195,7 @@ void testConstructorHandlesValidBase64ButNoGoogleOrGcp() { assertThrows( ObjectStoreServiceException.class, - () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); + () -> new OSSAttachmentsServiceHandler(binding, executor)); } @Test @@ -212,7 +207,7 @@ void testConstructorHandlesInValidBase64() { assertThrows( ObjectStoreServiceException.class, - () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); + () -> new OSSAttachmentsServiceHandler(binding, executor)); } @Test @@ -226,7 +221,7 @@ void testConstructorHandlesNoValidObjectStoreService() { assertThrows( ObjectStoreServiceException.class, - () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); + () -> new OSSAttachmentsServiceHandler(binding, executor)); } // Helper method to setup common mocks for createAttachment exception tests @@ -382,333 +377,4 @@ void testReadAttachmentHandlesInterruptedException() assertThrows(ServiceException.class, () -> handler.readAttachment(context)); verify(context).setCompleted(); } - - // ==================== Multi-Tenancy Tests (Phase 1 Shared Mode) ==================== - - /** - * Helper to create a handler with MT config injected via reflection. The implementation agent is - * adding multitenancyEnabled and objectStoreKind fields to the handler class. - */ - private OSSAttachmentsServiceHandler createMTHandler( - OSClient mockOsClient, boolean multitenancyEnabled, String objectStoreKind) - throws NoSuchFieldException, IllegalAccessException { - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - - var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - osClientField.setAccessible(true); - osClientField.set(handler, mockOsClient); - - var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); - mtField.setAccessible(true); - mtField.set(handler, multitenancyEnabled); - - var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); - kindField.setAccessible(true); - kindField.set(handler, objectStoreKind); - - return handler; - } - - private static UserInfo mockUserInfo(String tenant) { - UserInfo userInfo = mock(UserInfo.class); - when(userInfo.getTenant()).thenReturn(tenant); - return userInfo; - } - - @Test - void testBuildObjectKey_SharedMode_PrefixesTenantId() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, true, "shared"); - - String tenantId = "myTenant"; - String contentId = "content-uuid-123"; - - // Setup create context with tenant - AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); - MediaData mockMediaData = mock(MediaData.class); - var mockEntity = mock(CdsEntity.class); - when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); - when(context.getAttachmentEntity()).thenReturn(mockEntity); - when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); - when(context.getData()).thenReturn(mockMediaData); - when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("test".getBytes())); - when(mockMediaData.getMimeType()).thenReturn("text/plain"); - UserInfo userInfo = mockUserInfo(tenantId); - when(context.getUserInfo()).thenReturn(userInfo); - when(mockOsClient.uploadContent(any(), anyString(), anyString())) - .thenReturn(CompletableFuture.completedFuture(null)); - - handler.createAttachment(context); - - // Capture the object key passed to uploadContent - ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class); - verify(mockOsClient).uploadContent(any(), keyCaptor.capture(), anyString()); - // In shared mode, the key should be tenantId/contentId - org.junit.jupiter.api.Assertions.assertEquals( - tenantId + "/" + contentId, keyCaptor.getValue()); - } - - @Test - void testBuildObjectKey_NonMTMode_NoPrefixing() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - // multitenancy disabled - OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, false, null); - - String contentId = "content-uuid-456"; - - AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); - MediaData mockMediaData = mock(MediaData.class); - var mockEntity = mock(CdsEntity.class); - when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); - when(context.getAttachmentEntity()).thenReturn(mockEntity); - when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); - when(context.getData()).thenReturn(mockMediaData); - when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("test".getBytes())); - when(mockMediaData.getMimeType()).thenReturn("text/plain"); - when(mockOsClient.uploadContent(any(), anyString(), anyString())) - .thenReturn(CompletableFuture.completedFuture(null)); - - handler.createAttachment(context); - - // Capture the object key passed to uploadContent - ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class); - verify(mockOsClient).uploadContent(any(), keyCaptor.capture(), anyString()); - // When MT is off, key should be plain contentId with no prefix - org.junit.jupiter.api.Assertions.assertEquals(contentId, keyCaptor.getValue()); - } - - @Test - void testBuildObjectKey_NullTenant_ThrowsInMTMode() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, true, "shared"); - - String contentId = "content-uuid-789"; - - AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); - MediaData mockMediaData = mock(MediaData.class); - var mockEntity = mock(CdsEntity.class); - when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); - when(context.getAttachmentEntity()).thenReturn(mockEntity); - when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); - when(context.getData()).thenReturn(mockMediaData); - when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("test".getBytes())); - when(mockMediaData.getMimeType()).thenReturn("text/plain"); - // null tenant - UserInfo userInfo = mockUserInfo(null); - when(context.getUserInfo()).thenReturn(userInfo); - - // In MT mode, null tenant should throw ServiceException - assertThrows(ServiceException.class, () -> handler.createAttachment(context)); - } - - @Test - void testCreateAttachment_SharedMode_UsesObjectKey() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, true, "shared"); - - String tenantId = "tenantX"; - String contentId = "doc-create-123"; - - AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); - MediaData mockMediaData = mock(MediaData.class); - var mockEntity = mock(CdsEntity.class); - when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); - when(context.getAttachmentEntity()).thenReturn(mockEntity); - when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); - when(context.getData()).thenReturn(mockMediaData); - when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("data".getBytes())); - when(mockMediaData.getMimeType()).thenReturn("application/pdf"); - UserInfo userInfoX = mockUserInfo(tenantId); - when(context.getUserInfo()).thenReturn(userInfoX); - when(mockOsClient.uploadContent(any(), anyString(), anyString())) - .thenReturn(CompletableFuture.completedFuture(null)); - - handler.createAttachment(context); - - ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class); - verify(mockOsClient).uploadContent(any(), keyCaptor.capture(), anyString()); - org.junit.jupiter.api.Assertions.assertEquals( - tenantId + "/" + contentId, keyCaptor.getValue()); - // contentId stored in the context should remain unprefixed - verify(context).setContentId(contentId); - } - - @Test - void testReadAttachment_SharedMode_UsesObjectKey() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, true, "shared"); - - String tenantId = "tenantRead"; - String contentId = "doc-read-456"; - - AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); - MediaData mockMediaData = mock(MediaData.class); - when(context.getContentId()).thenReturn(contentId); - when(context.getData()).thenReturn(mockMediaData); - UserInfo userInfoRead = mockUserInfo(tenantId); - when(context.getUserInfo()).thenReturn(userInfoRead); - when(mockOsClient.readContent(anyString())) - .thenReturn( - CompletableFuture.completedFuture(new ByteArrayInputStream("test".getBytes()))); - - handler.readAttachment(context); - - // Verify the read uses the prefixed key - ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class); - verify(mockOsClient).readContent(keyCaptor.capture()); - org.junit.jupiter.api.Assertions.assertEquals( - tenantId + "/" + contentId, keyCaptor.getValue()); - } - - @Test - void testDeleteAttachment_SharedMode_UsesObjectKey() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, true, "shared"); - - String tenantId = "tenantDel"; - String contentId = "doc-del-789"; - - AttachmentMarkAsDeletedEventContext context = mock(AttachmentMarkAsDeletedEventContext.class); - when(context.getContentId()).thenReturn(contentId); - UserInfo userInfoDel = mockUserInfo(tenantId); - when(context.getUserInfo()).thenReturn(userInfoDel); - when(mockOsClient.deleteContent(anyString())) - .thenReturn(CompletableFuture.completedFuture(null)); - - handler.markAttachmentAsDeleted(context); - - // Verify the delete uses the prefixed key - ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class); - verify(mockOsClient).deleteContent(keyCaptor.capture()); - org.junit.jupiter.api.Assertions.assertEquals( - tenantId + "/" + contentId, keyCaptor.getValue()); - } - - @Test - void testCreateAttachment_SingleTenant_NoPrefixing() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - // Backward compatibility: MT disabled - OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, false, null); - - String contentId = "doc-single-abc"; - - AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); - MediaData mockMediaData = mock(MediaData.class); - var mockEntity = mock(CdsEntity.class); - when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); - when(context.getAttachmentEntity()).thenReturn(mockEntity); - when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); - when(context.getData()).thenReturn(mockMediaData); - when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("data".getBytes())); - when(mockMediaData.getMimeType()).thenReturn("text/plain"); - when(mockOsClient.uploadContent(any(), anyString(), anyString())) - .thenReturn(CompletableFuture.completedFuture(null)); - - handler.createAttachment(context); - - ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class); - verify(mockOsClient).uploadContent(any(), keyCaptor.capture(), anyString()); - // No prefix, just contentId - org.junit.jupiter.api.Assertions.assertEquals(contentId, keyCaptor.getValue()); - verify(context).setContentId(contentId); - } - - // ==================== Tenant ID Validation Tests ==================== - - @Test - void testValidateTenantId_EmptyTenant_ThrowsException() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, true, "shared"); - - String contentId = "content-id"; - AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); - MediaData mockMediaData = mock(MediaData.class); - var mockEntity = mock(CdsEntity.class); - when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); - when(context.getAttachmentEntity()).thenReturn(mockEntity); - when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); - when(context.getData()).thenReturn(mockMediaData); - when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("test".getBytes())); - when(mockMediaData.getMimeType()).thenReturn("text/plain"); - UserInfo userInfo = mockUserInfo(""); - when(context.getUserInfo()).thenReturn(userInfo); - - assertThrows(ServiceException.class, () -> handler.createAttachment(context)); - } - - @Test - void testValidateTenantId_SlashInTenant_ThrowsException() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, true, "shared"); - - String contentId = "content-id"; - AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); - MediaData mockMediaData = mock(MediaData.class); - var mockEntity = mock(CdsEntity.class); - when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); - when(context.getAttachmentEntity()).thenReturn(mockEntity); - when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); - when(context.getData()).thenReturn(mockMediaData); - when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("test".getBytes())); - when(mockMediaData.getMimeType()).thenReturn("text/plain"); - UserInfo userInfo = mockUserInfo("tenant/evil"); - when(context.getUserInfo()).thenReturn(userInfo); - - assertThrows(ServiceException.class, () -> handler.createAttachment(context)); - } - - @Test - void testValidateTenantId_BackslashInTenant_ThrowsException() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, true, "shared"); - - String contentId = "content-id"; - AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); - MediaData mockMediaData = mock(MediaData.class); - var mockEntity = mock(CdsEntity.class); - when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); - when(context.getAttachmentEntity()).thenReturn(mockEntity); - when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); - when(context.getData()).thenReturn(mockMediaData); - when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("test".getBytes())); - when(mockMediaData.getMimeType()).thenReturn("text/plain"); - UserInfo userInfo = mockUserInfo("tenant\\evil"); - when(context.getUserInfo()).thenReturn(userInfo); - - assertThrows(ServiceException.class, () -> handler.createAttachment(context)); - } - - @Test - void testValidateTenantId_PathTraversal_ThrowsException() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, true, "shared"); - - String contentId = "content-id"; - AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); - MediaData mockMediaData = mock(MediaData.class); - var mockEntity = mock(CdsEntity.class); - when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); - when(context.getAttachmentEntity()).thenReturn(mockEntity); - when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); - when(context.getData()).thenReturn(mockMediaData); - when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("test".getBytes())); - when(mockMediaData.getMimeType()).thenReturn("text/plain"); - UserInfo userInfo = mockUserInfo("..evil"); - when(context.getUserInfo()).thenReturn(userInfo); - - assertThrows(ServiceException.class, () -> handler.createAttachment(context)); - } - } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTestUtils.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTestUtils.java index f96178dd3..615aba16c 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTestUtils.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTestUtils.java @@ -39,8 +39,7 @@ public static void testCreateReadDeleteAttachmentFlow( String testFileName = "testFileName-" + System.currentTimeMillis() + ".txt"; String testFileContent = "test"; - OSSAttachmentsServiceHandler handler = - new OSSAttachmentsServiceHandler(binding, executor, false, null); + OSSAttachmentsServiceHandler handler = new OSSAttachmentsServiceHandler(binding, executor); // Create an AttachmentCreateEventContext with mocked data - to upload a test attachment MediaData createMediaData = mock(MediaData.class); diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java deleted file mode 100644 index 7f81186ff..000000000 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * © 2025 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.oss.handler; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.sap.cds.feature.attachments.oss.client.OSClient; -import com.sap.cds.services.mt.UnsubscribeEventContext; -import java.util.concurrent.CompletableFuture; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -class TenantCleanupHandlerTest { - - private OSClient osClient; - private UnsubscribeEventContext context; - private TenantCleanupHandler handler; - - @BeforeEach - void setUp() { - osClient = mock(OSClient.class); - context = mock(UnsubscribeEventContext.class); - handler = new TenantCleanupHandler(osClient); - } - - @Test - void testCleanupTenantData_CallsDeleteByPrefix() throws Exception { - String tenantId = "tenant-abc"; - when(context.getTenant()).thenReturn(tenantId); - when(osClient.deleteContentByPrefix(tenantId + "/")) - .thenReturn(CompletableFuture.completedFuture(null)); - - handler.cleanupTenantData(context); - - verify(osClient).deleteContentByPrefix(tenantId + "/"); - } - - @Test - void testCleanupTenantData_UsesCorrectPrefix() throws Exception { - String tenantId = "my-tenant-123"; - when(context.getTenant()).thenReturn(tenantId); - when(osClient.deleteContentByPrefix("my-tenant-123/")) - .thenReturn(CompletableFuture.completedFuture(null)); - - handler.cleanupTenantData(context); - - verify(osClient).deleteContentByPrefix("my-tenant-123/"); - } - - @Test - void testCleanupTenantData_HandlesException() throws Exception { - String tenantId = "tenant-fail"; - when(context.getTenant()).thenReturn(tenantId); - - CompletableFuture failedFuture = new CompletableFuture<>(); - failedFuture.completeExceptionally(new RuntimeException("Storage error")); - when(osClient.deleteContentByPrefix(tenantId + "/")).thenReturn(failedFuture); - - // Should NOT rethrow — the handler logs the error but does not fail the unsubscribe - handler.cleanupTenantData(context); - - verify(osClient).deleteContentByPrefix(tenantId + "/"); - } - - @Test - void testCleanupTenantData_HandlesInterruptedException() throws Exception { - String tenantId = "tenant-interrupt"; - when(context.getTenant()).thenReturn(tenantId); - - CompletableFuture failedFuture = new CompletableFuture<>(); - failedFuture.completeExceptionally(new InterruptedException("Interrupted")); - when(osClient.deleteContentByPrefix(tenantId + "/")).thenReturn(failedFuture); - - // Should NOT rethrow — the handler logs the error but does not fail the unsubscribe - handler.cleanupTenantData(context); - - verify(osClient).deleteContentByPrefix(tenantId + "/"); - } -} From b1972eef3a46d82dff9e31248f1d13ab39c0c320 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Mon, 30 Mar 2026 21:15:34 +0200 Subject: [PATCH 04/18] spotless --- .../sap/cds/feature/attachments/oss/client/AWSClient.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java index 137a6c9fc..513a2b738 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java @@ -25,12 +25,12 @@ import software.amazon.awssdk.services.s3.model.DeleteObjectsRequest; import software.amazon.awssdk.services.s3.model.DeleteObjectsResponse; import software.amazon.awssdk.services.s3.model.GetObjectRequest; -import software.amazon.awssdk.services.s3.model.S3Error; import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; import software.amazon.awssdk.services.s3.model.ListObjectsV2Response; import software.amazon.awssdk.services.s3.model.ObjectIdentifier; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectResponse; +import software.amazon.awssdk.services.s3.model.S3Error; public class AWSClient implements OSClient { private final S3Client s3Client; @@ -167,9 +167,7 @@ public Future deleteContentByPrefix(String prefix) { } } listReq = - listReq.toBuilder() - .continuationToken(listResp.nextContinuationToken()) - .build(); + listReq.toBuilder().continuationToken(listResp.nextContinuationToken()).build(); } while (listResp.isTruncated()); } catch (RuntimeException e) { throw new ObjectStoreServiceException( From 1df4b6bcfb45426cb2f1d218c81449de18fd2e66 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Mon, 30 Mar 2026 21:29:20 +0200 Subject: [PATCH 05/18] fix tests --- .../feature/attachments/oss/client/AWSClientTest.java | 2 +- .../oss/handler/OSSAttachmentsServiceHandlerTest.java | 11 ++++++----- .../OSSAttachmentsServiceHandlerTestUtils.java | 3 ++- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java index 032c1a8a2..658157b3b 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java @@ -42,7 +42,7 @@ class AWSClientTest { void testConstructorWithAwsBindingUsesAwsClient() throws NoSuchFieldException, IllegalAccessException { OSSAttachmentsServiceHandler handler = - new OSSAttachmentsServiceHandler(getDummyBinding(), executor); + new OSSAttachmentsServiceHandler(getDummyBinding(), executor, false, null); OSClient client = OSSAttachmentsServiceHandlerTestUtils.getOsClient(handler); assertInstanceOf(AWSClient.class, client); } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java index 2d973221b..62598354c 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java @@ -45,7 +45,8 @@ void testRestoreAttachmentCallsSetCompleted() { creds.put("bucket", "test-bucket"); when(binding.getCredentials()).thenReturn(creds); - OSSAttachmentsServiceHandler handler = new OSSAttachmentsServiceHandler(binding, executor); + OSSAttachmentsServiceHandler handler = + new OSSAttachmentsServiceHandler(binding, executor, false, null); AttachmentRestoreEventContext context = mock(AttachmentRestoreEventContext.class); handler.restoreAttachment(context); verify(context).setCompleted(); @@ -179,7 +180,7 @@ void testConstructorHandlesInvalidBase64EncodedPrivateKeyData() { assertThrows( ObjectStoreServiceException.class, - () -> new OSSAttachmentsServiceHandler(binding, executor)); + () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); } @Test @@ -195,7 +196,7 @@ void testConstructorHandlesValidBase64ButNoGoogleOrGcp() { assertThrows( ObjectStoreServiceException.class, - () -> new OSSAttachmentsServiceHandler(binding, executor)); + () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); } @Test @@ -207,7 +208,7 @@ void testConstructorHandlesInValidBase64() { assertThrows( ObjectStoreServiceException.class, - () -> new OSSAttachmentsServiceHandler(binding, executor)); + () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); } @Test @@ -221,7 +222,7 @@ void testConstructorHandlesNoValidObjectStoreService() { assertThrows( ObjectStoreServiceException.class, - () -> new OSSAttachmentsServiceHandler(binding, executor)); + () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); } // Helper method to setup common mocks for createAttachment exception tests diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTestUtils.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTestUtils.java index 615aba16c..f96178dd3 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTestUtils.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTestUtils.java @@ -39,7 +39,8 @@ public static void testCreateReadDeleteAttachmentFlow( String testFileName = "testFileName-" + System.currentTimeMillis() + ".txt"; String testFileContent = "test"; - OSSAttachmentsServiceHandler handler = new OSSAttachmentsServiceHandler(binding, executor); + OSSAttachmentsServiceHandler handler = + new OSSAttachmentsServiceHandler(binding, executor, false, null); // Create an AttachmentCreateEventContext with mocked data - to upload a test attachment MediaData createMediaData = mock(MediaData.class); From b6d76956ab7f799b1f0cc520fb3d01de44d1eb4b Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Tue, 31 Mar 2026 09:58:45 +0200 Subject: [PATCH 06/18] splut up test modules --- integration-tests/.cdsrc.json | 5 + integration-tests/{srv => generic}/pom.xml | 4 +- .../integrationtests/Application.java | 0 .../integrationtests/constants/Profiles.java | 0 .../testhandler/EventContextHolder.java | 0 .../testhandler/TestPersistenceHandler.java | 0 .../TestPluginAttachmentsServiceHandler.java | 0 .../src/main/resources/application.yaml | 0 .../src/main/resources/banner.txt | 0 .../src/main/resources/messages.properties | 0 .../resources/spotbugs-exclusion-filter.xml | 0 .../common/JsonToCapMapperTestHelper.java | 0 .../common/MalwareScanResultProvider.java | 0 .../common/MockHttpRequestHelper.java | 0 .../common/TableDataDeleter.java | 0 .../DraftOdataRequestValidationBase.java | 0 ...aRequestValidationWithTestHandlerTest.java | 0 ...thoutTestHandlerAndMalwareScannerTest.java | 0 ...stHandlerAndWithoutMalwareScannerTest.java | 0 .../MediaValidatedAttachmentsDraftTest.java | 0 ...tedAttachmentsSizeValidationDraftTest.java | 0 ...MediaValidatedAttachmentsNonDraftTest.java | 0 .../OdataRequestValidationBase.java | 0 ...aRequestValidationWithTestHandlerTest.java | 0 ...thoutTestHandlerAndMalwareScannerTest.java | 0 ...stHandlerAndWithoutMalwareScannerTest.java | 0 ...mitedAttachmentValidationNonDraftTest.java | 0 .../helper/AttachmentsBuilder.java | 0 .../helper/AttachmentsEntityBuilder.java | 0 .../helper/ItemEntityBuilder.java | 0 .../helper/RootEntityBuilder.java | 0 .../TestPersistenceHandlerTest.java | 0 ...stPluginAttachmentsServiceHandlerTest.java | 0 .../src/test/resources/application.yaml | 0 .../src/test/resources/logback-test.xml | 0 .../src/test/resources/xsuaa-env.json | 0 .../{srv => generic}/test-service.cds | 0 integration-tests/mtx-local/.cdsrc.json | 14 + integration-tests/mtx-local/db/index.cds | 1 + integration-tests/mtx-local/db/schema.cds | 9 + .../mtx-local/mtx/sidecar/package.json | 30 + integration-tests/mtx-local/package-lock.json | 4144 +++++++++++++++++ integration-tests/mtx-local/package.json | 11 + integration-tests/mtx-local/srv/pom.xml | 250 + integration-tests/mtx-local/srv/service.cds | 5 + .../integrationtests/mt/Application.java | 15 + .../system/SubscribeModelTenantsHandler.java | 90 + .../srv/src/main/resources/application.yaml | 23 + .../MultiTenantAttachmentIsolationTest.java | 115 + .../mt/SubscribeAndUnsubscribeTest.java | 72 + .../mt/utils/SubscriptionEndpointClient.java | 67 + integration-tests/pom.xml | 3 +- 52 files changed, 4855 insertions(+), 3 deletions(-) rename integration-tests/{srv => generic}/pom.xml (96%) rename integration-tests/{srv => generic}/src/main/java/com/sap/cds/feature/attachments/integrationtests/Application.java (100%) rename integration-tests/{srv => generic}/src/main/java/com/sap/cds/feature/attachments/integrationtests/constants/Profiles.java (100%) rename integration-tests/{srv => generic}/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/EventContextHolder.java (100%) rename integration-tests/{srv => generic}/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandler.java (100%) rename integration-tests/{srv => generic}/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandler.java (100%) rename integration-tests/{srv => generic}/src/main/resources/application.yaml (100%) rename integration-tests/{srv => generic}/src/main/resources/banner.txt (100%) rename integration-tests/{srv => generic}/src/main/resources/messages.properties (100%) rename integration-tests/{srv => generic}/src/main/resources/spotbugs-exclusion-filter.xml (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/JsonToCapMapperTestHelper.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MalwareScanResultProvider.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MockHttpRequestHelper.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/TableDataDeleter.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationBase.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithTestHandlerTest.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/MediaValidatedAttachmentsDraftTest.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/SizeLimitedAttachmentsSizeValidationDraftTest.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationBase.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithTestHandlerTest.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/SizeLimitedAttachmentValidationNonDraftTest.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsBuilder.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsEntityBuilder.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/ItemEntityBuilder.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/RootEntityBuilder.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandlerTest.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java (100%) rename integration-tests/{srv => generic}/src/test/resources/application.yaml (100%) rename integration-tests/{srv => generic}/src/test/resources/logback-test.xml (100%) rename integration-tests/{srv => generic}/src/test/resources/xsuaa-env.json (100%) rename integration-tests/{srv => generic}/test-service.cds (100%) create mode 100644 integration-tests/mtx-local/.cdsrc.json create mode 100644 integration-tests/mtx-local/db/index.cds create mode 100644 integration-tests/mtx-local/db/schema.cds create mode 100644 integration-tests/mtx-local/mtx/sidecar/package.json create mode 100644 integration-tests/mtx-local/package-lock.json create mode 100644 integration-tests/mtx-local/package.json create mode 100644 integration-tests/mtx-local/srv/pom.xml create mode 100644 integration-tests/mtx-local/srv/service.cds create mode 100644 integration-tests/mtx-local/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/mt/Application.java create mode 100644 integration-tests/mtx-local/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/mt/handlers/system/SubscribeModelTenantsHandler.java create mode 100644 integration-tests/mtx-local/srv/src/main/resources/application.yaml create mode 100644 integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/MultiTenantAttachmentIsolationTest.java create mode 100644 integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/SubscribeAndUnsubscribeTest.java create mode 100644 integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/utils/SubscriptionEndpointClient.java diff --git a/integration-tests/.cdsrc.json b/integration-tests/.cdsrc.json index c2d556179..0b8f71263 100644 --- a/integration-tests/.cdsrc.json +++ b/integration-tests/.cdsrc.json @@ -7,6 +7,11 @@ }, "fiori": { "draft_messages": false + }, + "build": { + "tasks": [ + { "for": "java", "src": "generic" } + ] } } } \ No newline at end of file diff --git a/integration-tests/srv/pom.xml b/integration-tests/generic/pom.xml similarity index 96% rename from integration-tests/srv/pom.xml rename to integration-tests/generic/pom.xml index 6a5b6eada..265826f00 100644 --- a/integration-tests/srv/pom.xml +++ b/integration-tests/generic/pom.xml @@ -8,10 +8,10 @@ ${revision} - cds-feature-attachments-integration-tests-srv + cds-feature-attachments-integration-tests-generic jar - Integration Tests - Service + Integration Tests - Generic com.sap.cds.feature.attachments.generated diff --git a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/Application.java b/integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/Application.java similarity index 100% rename from integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/Application.java rename to integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/Application.java diff --git a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/constants/Profiles.java b/integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/constants/Profiles.java similarity index 100% rename from integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/constants/Profiles.java rename to integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/constants/Profiles.java diff --git a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/EventContextHolder.java b/integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/EventContextHolder.java similarity index 100% rename from integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/EventContextHolder.java rename to integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/EventContextHolder.java diff --git a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandler.java b/integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandler.java similarity index 100% rename from integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandler.java rename to integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandler.java diff --git a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandler.java b/integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandler.java similarity index 100% rename from integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandler.java rename to integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandler.java diff --git a/integration-tests/srv/src/main/resources/application.yaml b/integration-tests/generic/src/main/resources/application.yaml similarity index 100% rename from integration-tests/srv/src/main/resources/application.yaml rename to integration-tests/generic/src/main/resources/application.yaml diff --git a/integration-tests/srv/src/main/resources/banner.txt b/integration-tests/generic/src/main/resources/banner.txt similarity index 100% rename from integration-tests/srv/src/main/resources/banner.txt rename to integration-tests/generic/src/main/resources/banner.txt diff --git a/integration-tests/srv/src/main/resources/messages.properties b/integration-tests/generic/src/main/resources/messages.properties similarity index 100% rename from integration-tests/srv/src/main/resources/messages.properties rename to integration-tests/generic/src/main/resources/messages.properties diff --git a/integration-tests/srv/src/main/resources/spotbugs-exclusion-filter.xml b/integration-tests/generic/src/main/resources/spotbugs-exclusion-filter.xml similarity index 100% rename from integration-tests/srv/src/main/resources/spotbugs-exclusion-filter.xml rename to integration-tests/generic/src/main/resources/spotbugs-exclusion-filter.xml diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/JsonToCapMapperTestHelper.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/JsonToCapMapperTestHelper.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/JsonToCapMapperTestHelper.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/JsonToCapMapperTestHelper.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MalwareScanResultProvider.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MalwareScanResultProvider.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MalwareScanResultProvider.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MalwareScanResultProvider.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MockHttpRequestHelper.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MockHttpRequestHelper.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MockHttpRequestHelper.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MockHttpRequestHelper.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/TableDataDeleter.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/TableDataDeleter.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/TableDataDeleter.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/TableDataDeleter.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationBase.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationBase.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationBase.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationBase.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithTestHandlerTest.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithTestHandlerTest.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithTestHandlerTest.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithTestHandlerTest.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/MediaValidatedAttachmentsDraftTest.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/MediaValidatedAttachmentsDraftTest.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/MediaValidatedAttachmentsDraftTest.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/MediaValidatedAttachmentsDraftTest.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/SizeLimitedAttachmentsSizeValidationDraftTest.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/SizeLimitedAttachmentsSizeValidationDraftTest.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/SizeLimitedAttachmentsSizeValidationDraftTest.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/SizeLimitedAttachmentsSizeValidationDraftTest.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationBase.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationBase.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationBase.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationBase.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithTestHandlerTest.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithTestHandlerTest.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithTestHandlerTest.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithTestHandlerTest.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/SizeLimitedAttachmentValidationNonDraftTest.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/SizeLimitedAttachmentValidationNonDraftTest.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/SizeLimitedAttachmentValidationNonDraftTest.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/SizeLimitedAttachmentValidationNonDraftTest.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsBuilder.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsBuilder.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsBuilder.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsBuilder.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsEntityBuilder.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsEntityBuilder.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsEntityBuilder.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsEntityBuilder.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/ItemEntityBuilder.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/ItemEntityBuilder.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/ItemEntityBuilder.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/ItemEntityBuilder.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/RootEntityBuilder.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/RootEntityBuilder.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/RootEntityBuilder.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/RootEntityBuilder.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandlerTest.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandlerTest.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandlerTest.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandlerTest.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java diff --git a/integration-tests/srv/src/test/resources/application.yaml b/integration-tests/generic/src/test/resources/application.yaml similarity index 100% rename from integration-tests/srv/src/test/resources/application.yaml rename to integration-tests/generic/src/test/resources/application.yaml diff --git a/integration-tests/srv/src/test/resources/logback-test.xml b/integration-tests/generic/src/test/resources/logback-test.xml similarity index 100% rename from integration-tests/srv/src/test/resources/logback-test.xml rename to integration-tests/generic/src/test/resources/logback-test.xml diff --git a/integration-tests/srv/src/test/resources/xsuaa-env.json b/integration-tests/generic/src/test/resources/xsuaa-env.json similarity index 100% rename from integration-tests/srv/src/test/resources/xsuaa-env.json rename to integration-tests/generic/src/test/resources/xsuaa-env.json diff --git a/integration-tests/srv/test-service.cds b/integration-tests/generic/test-service.cds similarity index 100% rename from integration-tests/srv/test-service.cds rename to integration-tests/generic/test-service.cds diff --git a/integration-tests/mtx-local/.cdsrc.json b/integration-tests/mtx-local/.cdsrc.json new file mode 100644 index 000000000..75b24c377 --- /dev/null +++ b/integration-tests/mtx-local/.cdsrc.json @@ -0,0 +1,14 @@ +{ + "profile": "with-mtx-sidecar", + "requires": { + "multitenancy": true, + "extensibility": true, + "toggles": true, + "db": { + "kind": "sqlite" + } + }, + "cdsc": { + "moduleLookupDirectories": ["node_modules/", "target/cds/"] + } +} diff --git a/integration-tests/mtx-local/db/index.cds b/integration-tests/mtx-local/db/index.cds new file mode 100644 index 000000000..3771e1112 --- /dev/null +++ b/integration-tests/mtx-local/db/index.cds @@ -0,0 +1 @@ +using from './schema.cds'; diff --git a/integration-tests/mtx-local/db/schema.cds b/integration-tests/mtx-local/db/schema.cds new file mode 100644 index 000000000..9edbfde50 --- /dev/null +++ b/integration-tests/mtx-local/db/schema.cds @@ -0,0 +1,9 @@ +namespace mt.test.data; + +using { cuid } from '@sap/cds/common'; +using { sap.attachments.Attachments } from 'com.sap.cds/cds-feature-attachments'; + +entity Documents : cuid { + title : String; + attachments : Composition of many Attachments; +} diff --git a/integration-tests/mtx-local/mtx/sidecar/package.json b/integration-tests/mtx-local/mtx/sidecar/package.json new file mode 100644 index 000000000..b2936eb9d --- /dev/null +++ b/integration-tests/mtx-local/mtx/sidecar/package.json @@ -0,0 +1,30 @@ +{ + "name": "mtx-local-sidecar", + "version": "0.0.0", + "dependencies": { + "@sap/cds": "^9", + "@sap/cds-mtxs": "^3", + "@sap/xssec": "^4", + "express": "^4" + }, + "devDependencies": { + "@cap-js/sqlite": "^2" + }, + "cds": { + "profile": "mtx-sidecar", + "[development]": { + "requires": { + "auth": "dummy" + }, + "db": { + "kind": "sqlite", + "credentials": { + "url": "../../db.sqlite" + } + } + } + }, + "scripts": { + "start": "cds-serve --profile development" + } +} diff --git a/integration-tests/mtx-local/package-lock.json b/integration-tests/mtx-local/package-lock.json new file mode 100644 index 000000000..3383043ec --- /dev/null +++ b/integration-tests/mtx-local/package-lock.json @@ -0,0 +1,4144 @@ +{ + "name": "mtx-local-integration-tests", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mtx-local-integration-tests", + "version": "0.0.0", + "workspaces": [ + "mtx/sidecar" + ], + "devDependencies": { + "@sap/cds-dk": "^9", + "@sap/cds-mtxs": "^3" + } + }, + "mtx/sidecar": { + "name": "mtx-local-sidecar", + "version": "0.0.0", + "dependencies": { + "@sap/cds": "^9", + "@sap/cds-mtxs": "^3", + "@sap/xssec": "^4", + "express": "^4" + }, + "devDependencies": { + "@cap-js/sqlite": "^2" + } + }, + "mtx/sidecar/node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@cap-js/db-service": { + "version": "2.9.0", + "resolved": "https://int.repositories.cloud.sap/artifactory/api/npm/build-milestones-npm/@cap-js/db-service/-/db-service-2.9.0.tgz", + "integrity": "sha512-WCXhoqezaF6A5I2l0MNZeHKXXtHRNEq7Rp0R89/uccOHQIx0DuU0U9NuJJPV/1G5RGk2QKQ9VBo/KYn+MZuuNQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "generic-pool": "^3.9.0" + }, + "peerDependencies": { + "@sap/cds": ">=9.8" + } + }, + "node_modules/@cap-js/sqlite": { + "version": "2.2.0", + "resolved": "https://int.repositories.cloud.sap/artifactory/api/npm/build-milestones-npm/@cap-js/sqlite/-/sqlite-2.2.0.tgz", + "integrity": "sha512-FPj+uVU/14vtGUl2P/Q8y7XhZbsLgrCav2O5PjHPXnupegjby4sMJkgVNxVHnkyKPFgO/W8uEsq9r5TU9VPx8w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@cap-js/db-service": "^2.9.0", + "better-sqlite3": "^12.0.0" + }, + "peerDependencies": { + "@sap/cds": ">=9.8", + "sql.js": "^1.13.0" + }, + "peerDependenciesMeta": { + "sql.js": { + "optional": true + } + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "license": "MIT", + "peer": true, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@sap/cds": { + "version": "9.8.4", + "resolved": "https://registry.npmjs.org/@sap/cds/-/cds-9.8.4.tgz", + "integrity": "sha512-1cnFdz4Ex4LV15dztDsSLDiilVdJ2mfTqr5A3TGsRd/6cM31SnHhuZ/Ra0FEErodokNS2pwasGZmSfgHVDW8KA==", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@sap/cds-compiler": "^6.4", + "@sap/cds-fiori": "^2", + "express": "^4.22.1 || ^5", + "js-yaml": "^4.1.1" + }, + "bin": { + "cds-deploy": "bin/deploy.js", + "cds-serve": "bin/serve.js" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@eslint/js": "^9 || ^10", + "tar": "^7.5.6" + }, + "peerDependenciesMeta": { + "tar": { + "optional": true + } + } + }, + "node_modules/@sap/cds-compiler": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@sap/cds-compiler/-/cds-compiler-6.8.0.tgz", + "integrity": "sha512-yRoTZcH8DFiP4PcEuIHe42YAaNt51V736+6RRr+U4nbO91sz36L8be2djcE8n760t8IbqLxzZ/UkATiL0fjRmA==", + "license": "SEE LICENSE IN LICENSE", + "bin": { + "cdsc": "bin/cdsc.js", + "cdshi": "bin/cdshi.js", + "cdsse": "bin/cdsse.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@sap/cds-dk": { + "version": "9.8.2", + "resolved": "https://registry.npmjs.org/@sap/cds-dk/-/cds-dk-9.8.2.tgz", + "integrity": "sha512-PGynoQpOCD8Nsr0EEzsEHWg8szYBLVtVd6byxvaaAPYBkNAj2WA+kin6gWaDTiyG3kJa0BSGPk4PCbA2xDoE9A==", + "dev": true, + "hasShrinkwrap": true, + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@cap-js/asyncapi": "^1.0.0", + "@cap-js/openapi": "^1.0.0", + "@sap/cds": ">=8.3", + "@sap/cds-mtxs": ">=2", + "@sap/hdi-deploy": "^5", + "axios": "^1", + "express": "^4.22.1 || ^5", + "hdb": "^2.0.0", + "livereload-js": "^4.0.1", + "mustache": "^4.0.1", + "ws": "^8.4.2", + "xml-js": "^1.6.11", + "yaml": "^2" + }, + "bin": { + "cds": "bin/cds.js", + "cds-ts": "bin/cds-ts.js", + "cds-tsx": "bin/cds-tsx.js" + }, + "optionalDependencies": { + "@cap-js/sqlite": ">=1" + } + }, + "node_modules/@sap/cds-dk/node_modules/@cap-js/asyncapi": { + "version": "1.0.3", + "integrity": "sha512-vZSWKAe+3qfvZDXV5SSFiObGWmqyS9MDyEADb5PLVT8kzO39qGaSDPv/GzI/gwvRfCayGAjU4ThiBKrFA7Gclg==", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "peerDependencies": { + "@sap/cds": ">=7.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/@cap-js/db-service": { + "version": "2.9.0", + "integrity": "sha512-WCXhoqezaF6A5I2l0MNZeHKXXtHRNEq7Rp0R89/uccOHQIx0DuU0U9NuJJPV/1G5RGk2QKQ9VBo/KYn+MZuuNQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "generic-pool": "^3.9.0" + }, + "peerDependencies": { + "@sap/cds": ">=9.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/@cap-js/openapi": { + "version": "1.4.0", + "integrity": "sha512-/LRSwn4SDxAi3qKwl09zoOhEVGaPGlYOPz/0S3UBnaMJVvaLyPiKbbaOtOnrrgulUX5OXt+ujPIQznOsbTzuAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "pluralize": "^8.0.0" + }, + "peerDependencies": { + "@sap/cds": ">=7.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/@cap-js/sqlite": { + "version": "2.2.0", + "integrity": "sha512-FPj+uVU/14vtGUl2P/Q8y7XhZbsLgrCav2O5PjHPXnupegjby4sMJkgVNxVHnkyKPFgO/W8uEsq9r5TU9VPx8w==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@cap-js/db-service": "^2.9.0", + "better-sqlite3": "^12.0.0" + }, + "peerDependencies": { + "@sap/cds": ">=9.8", + "sql.js": "^1.13.0" + }, + "peerDependenciesMeta": { + "sql.js": { + "optional": true + } + } + }, + "node_modules/@sap/cds-dk/node_modules/@eslint/js": { + "version": "10.0.1", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@sap/cds-dk/node_modules/@sap/cds": { + "version": "9.8.3", + "integrity": "sha512-up/kNVCZK8bOTyye1QN2BjMpH+IgcdyhT3U9mS3TqkpmjNE2JXq8Qz1iNAN+nYcXRWJ1XAUNpgebLfuh/49MGQ==", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@sap/cds-compiler": "^6.4", + "@sap/cds-fiori": "^2", + "express": "^4.22.1 || ^5", + "js-yaml": "^4.1.1" + }, + "bin": { + "cds-deploy": "bin/deploy.js", + "cds-serve": "bin/serve.js" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@eslint/js": "^9 || ^10", + "tar": "^7.5.6" + }, + "peerDependenciesMeta": { + "tar": { + "optional": true + } + } + }, + "node_modules/@sap/cds-dk/node_modules/@sap/cds-compiler": { + "version": "6.8.0", + "integrity": "sha512-yRoTZcH8DFiP4PcEuIHe42YAaNt51V736+6RRr+U4nbO91sz36L8be2djcE8n760t8IbqLxzZ/UkATiL0fjRmA==", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "bin": { + "cdsc": "bin/cdsc.js", + "cdshi": "bin/cdshi.js", + "cdsse": "bin/cdsse.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@sap/cds-dk/node_modules/@sap/cds-fiori": { + "version": "2.3.0", + "integrity": "sha512-6oWov+DSpFrSTgxXR0dZhak6aZ/IVRZvaHERMi0EgSTzIJdlvZlpw3Kf18ePMcTrRrtEXwD4RIjKt8pbs0g2Hg==", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "peerDependencies": { + "@sap/cds": ">=8" + } + }, + "node_modules/@sap/cds-dk/node_modules/@sap/cds-mtxs": { + "version": "3.8.1", + "integrity": "sha512-FUm1of08WsZ1O3eur7OGj7ZYp9Gy3RuYEGBZK+zosSPyJ3QrBhBzKwYvbh2DJdbv3P/xuk46NGg5yz914qZxrw==", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@sap/hdi-deploy": "^5" + }, + "bin": { + "cds-mtx": "bin/cds-mtx.js", + "cds-mtx-migrate": "bin/cds-mtx-migrate.js" + }, + "peerDependencies": { + "@sap/cds": "^9" + } + }, + "node_modules/@sap/cds-dk/node_modules/@sap/hdi": { + "version": "4.8.0", + "integrity": "sha512-tkJmY2ffm6mt4/LFwRBihlQkMxNAXa3ngvRe2N/6+qLIsUNdrH/M03S5mkygXq56K+KoVVZYuradajCusMWwsw==", + "dev": true, + "license": "See LICENSE file", + "dependencies": { + "async": "^3.2.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@sap/hana-client": "^2 >= 2.5", + "hdb": "^2 || ^0" + }, + "peerDependenciesMeta": { + "@sap/hana-client": { + "optional": true + }, + "hdb": { + "optional": true + } + } + }, + "node_modules/@sap/cds-dk/node_modules/@sap/hdi-deploy": { + "version": "5.6.1", + "integrity": "sha512-+qQ7qwG8lko303L5yRj2dud/nDAVuVblV/mmzJT44oPbF0Nry18eD2LUS23hFeuxjRa7rYK5YKQ8ffGgWxVrYQ==", + "dev": true, + "license": "See LICENSE file", + "dependencies": { + "@sap/hdi": "^4.8.0", + "@sap/xsenv": "^6.0.0", + "async": "^3.2.6", + "dotenv": "^16.4.5", + "handlebars": "^4.7.8", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=18.x" + }, + "peerDependencies": { + "@sap/hana-client": "^2 >= 2.6", + "hdb": "^2 || ^0" + }, + "peerDependenciesMeta": { + "@sap/hana-client": { + "optional": true + }, + "hdb": { + "optional": true + } + } + }, + "node_modules/@sap/cds-dk/node_modules/@sap/xsenv": { + "version": "6.1.0", + "integrity": "sha512-vlW4Zad3uiDqHtnYdQ0TsEIH8VIO4HmPGDowfBL5dIcHPmeKDISEQ9ibeHL5FkceqvYcXJEQAVZ5/hsHDqlXZg==", + "dev": true, + "license": "SEE LICENSE IN LICENSE file", + "dependencies": { + "debug": "4.4.3", + "node-cache": "^5.1.2", + "verror": "1.10.1" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/accepts": { + "version": "2.0.0", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/argparse": { + "version": "2.0.1", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/@sap/cds-dk/node_modules/assert-plus": { + "version": "1.0.0", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/async": { + "version": "3.2.6", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/asynckit": { + "version": "0.4.0", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/axios": { + "version": "1.13.6", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/base64-js": { + "version": "1.5.1", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/better-sqlite3": { + "version": "12.8.0", + "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, + "node_modules/@sap/cds-dk/node_modules/bindings": { + "version": "1.5.0", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/bl": { + "version": "4.1.0", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/body-parser": { + "version": "2.2.2", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@sap/cds-dk/node_modules/braces": { + "version": "3.0.3", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sap/cds-dk/node_modules/buffer": { + "version": "5.7.1", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/@sap/cds-dk/node_modules/bytes": { + "version": "3.1.2", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/call-bound": { + "version": "1.0.4", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/chownr": { + "version": "1.1.4", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/clone": { + "version": "2.1.2", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/combined-stream": { + "version": "1.0.8", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/content-disposition": { + "version": "1.0.1", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@sap/cds-dk/node_modules/content-type": { + "version": "1.0.5", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/cookie": { + "version": "0.7.2", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/cookie-signature": { + "version": "1.2.2", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/core-util-is": { + "version": "1.0.2", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/debug": { + "version": "4.4.3", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@sap/cds-dk/node_modules/decompress-response": { + "version": "6.0.0", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sap/cds-dk/node_modules/deep-extend": { + "version": "0.6.0", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/delayed-stream": { + "version": "1.0.0", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/depd": { + "version": "2.0.0", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/detect-libc": { + "version": "2.1.2", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sap/cds-dk/node_modules/dotenv": { + "version": "16.6.1", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/@sap/cds-dk/node_modules/dunder-proto": { + "version": "1.0.1", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/ee-first": { + "version": "1.1.1", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/encodeurl": { + "version": "2.0.0", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/end-of-stream": { + "version": "1.4.5", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/es-define-property": { + "version": "1.0.1", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/es-errors": { + "version": "1.3.0", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/es-object-atoms": { + "version": "1.1.1", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/es-set-tostringtag": { + "version": "2.1.0", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/escape-html": { + "version": "1.0.3", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/etag": { + "version": "1.8.1", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/expand-template": { + "version": "2.0.3", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sap/cds-dk/node_modules/express": { + "version": "5.2.1", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@sap/cds-dk/node_modules/extsprintf": { + "version": "1.4.1", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/file-uri-to-path": { + "version": "1.0.0", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/fill-range": { + "version": "7.1.1", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sap/cds-dk/node_modules/finalhandler": { + "version": "2.1.1", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@sap/cds-dk/node_modules/follow-redirects": { + "version": "1.15.11", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/@sap/cds-dk/node_modules/form-data": { + "version": "4.0.5", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@sap/cds-dk/node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/forwarded": { + "version": "0.2.0", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/fresh": { + "version": "2.0.0", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/fs-constants": { + "version": "1.0.0", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/function-bind": { + "version": "1.1.2", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/generic-pool": { + "version": "3.9.0", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@sap/cds-dk/node_modules/get-intrinsic": { + "version": "1.3.0", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/get-proto": { + "version": "1.0.1", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/github-from-package": { + "version": "0.0.0", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/gopd": { + "version": "1.2.0", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/handlebars": { + "version": "4.7.8", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/has-symbols": { + "version": "1.1.0", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/has-tostringtag": { + "version": "1.0.2", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/hasown": { + "version": "2.0.2", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/hdb": { + "version": "2.27.1", + "integrity": "sha512-xYL/W+fq2TyGHyzm8muolQnw8tdh4+2NQ8mQP2FpLSuhfJ8l0jQNSUZoAXic7NfMEan1Jvf8V1L4blwkgTc6+A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "iconv-lite": "0.7.0" + }, + "engines": { + "node": ">= 18" + }, + "optionalDependencies": { + "lz4-wasm-nodejs": "0.9.2" + } + }, + "node_modules/@sap/cds-dk/node_modules/hdb/node_modules/iconv-lite": { + "version": "0.7.0", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@sap/cds-dk/node_modules/http-errors": { + "version": "2.0.1", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@sap/cds-dk/node_modules/iconv-lite": { + "version": "0.7.2", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@sap/cds-dk/node_modules/ieee754": { + "version": "1.2.1", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/inherits": { + "version": "2.0.4", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@sap/cds-dk/node_modules/ini": { + "version": "1.3.8", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/ipaddr.js": { + "version": "1.9.1", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@sap/cds-dk/node_modules/is-number": { + "version": "7.0.0", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/is-promise": { + "version": "4.0.0", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/js-yaml": { + "version": "4.1.1", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@sap/cds-dk/node_modules/livereload-js": { + "version": "4.0.2", + "integrity": "sha512-Fy7VwgQNiOkynYyNBTo3v9hQUhcW5pFAheJN148+DTgpShjsy/22pLHKKwDK5v0kOsZsJBK+6q1PMgLvRmrwFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/lz4-wasm-nodejs": { + "version": "0.9.2", + "integrity": "sha512-hSwgJPS98q/Oe/89Y1OxzeA/UdnASG8GvldRyKa7aZyoAFCC8VPRtViBSava7wWC66WocjUwBpWau2rEmyFPsw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/math-intrinsics": { + "version": "1.1.0", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/media-typer": { + "version": "1.1.0", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/merge-descriptors": { + "version": "2.0.0", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sap/cds-dk/node_modules/micromatch": { + "version": "4.0.8", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@sap/cds-dk/node_modules/mime-db": { + "version": "1.54.0", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/mime-types": { + "version": "3.0.2", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@sap/cds-dk/node_modules/mimic-response": { + "version": "3.1.0", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sap/cds-dk/node_modules/minimist": { + "version": "1.2.8", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/mkdirp-classic": { + "version": "0.5.3", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/ms": { + "version": "2.1.3", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/mustache": { + "version": "4.2.0", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "dev": true, + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, + "node_modules/@sap/cds-dk/node_modules/napi-build-utils": { + "version": "2.0.0", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/negotiator": { + "version": "1.0.0", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/neo-async": { + "version": "2.6.2", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/node-abi": { + "version": "3.89.0", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@sap/cds-dk/node_modules/node-cache": { + "version": "5.1.2", + "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "2.x" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/object-inspect": { + "version": "1.13.4", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/on-finished": { + "version": "2.4.1", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/once": { + "version": "1.4.0", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/@sap/cds-dk/node_modules/parseurl": { + "version": "1.3.3", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/path-to-regexp": { + "version": "8.3.0", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@sap/cds-dk/node_modules/pluralize": { + "version": "8.0.0", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@sap/cds-dk/node_modules/prebuild-install": { + "version": "7.1.3", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@sap/cds-dk/node_modules/proxy-addr": { + "version": "2.0.7", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@sap/cds-dk/node_modules/proxy-from-env": { + "version": "1.1.0", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/pump": { + "version": "3.0.4", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/@sap/cds-dk/node_modules/qs": { + "version": "6.15.0", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/range-parser": { + "version": "1.2.1", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/raw-body": { + "version": "3.0.2", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@sap/cds-dk/node_modules/rc": { + "version": "1.2.8", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/@sap/cds-dk/node_modules/readable-stream": { + "version": "3.6.2", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@sap/cds-dk/node_modules/router": { + "version": "2.2.0", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@sap/cds-dk/node_modules/safe-buffer": { + "version": "5.2.1", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/safer-buffer": { + "version": "2.1.2", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/sax": { + "version": "1.6.0", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/semver": { + "version": "7.7.4", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@sap/cds-dk/node_modules/send": { + "version": "1.2.1", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@sap/cds-dk/node_modules/serve-static": { + "version": "2.2.1", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@sap/cds-dk/node_modules/setprototypeof": { + "version": "1.2.0", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/@sap/cds-dk/node_modules/side-channel": { + "version": "1.1.0", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/side-channel-list": { + "version": "1.0.0", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/side-channel-map": { + "version": "1.0.1", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/side-channel-weakmap": { + "version": "1.0.2", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/simple-concat": { + "version": "1.0.1", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/simple-get": { + "version": "4.0.1", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/source-map": { + "version": "0.6.1", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/statuses": { + "version": "2.0.2", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/string_decoder": { + "version": "1.3.0", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/strip-json-comments": { + "version": "2.0.1", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/tar-fs": { + "version": "2.1.4", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/tar-stream": { + "version": "2.2.0", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sap/cds-dk/node_modules/to-regex-range": { + "version": "5.0.1", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/toidentifier": { + "version": "1.0.1", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/tunnel-agent": { + "version": "0.6.0", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@sap/cds-dk/node_modules/type-is": { + "version": "2.0.1", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/uglify-js": { + "version": "3.19.3", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/unpipe": { + "version": "1.0.0", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/util-deprecate": { + "version": "1.0.2", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/vary": { + "version": "1.1.2", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/verror": { + "version": "1.10.1", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/wordwrap": { + "version": "1.0.0", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/wrappy": { + "version": "1.0.2", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@sap/cds-dk/node_modules/ws": { + "version": "8.20.0", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@sap/cds-dk/node_modules/xml-js": { + "version": "1.6.11", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + }, + "node_modules/@sap/cds-dk/node_modules/yaml": { + "version": "2.8.3", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/@sap/cds-fiori": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sap/cds-fiori/-/cds-fiori-2.3.0.tgz", + "integrity": "sha512-6oWov+DSpFrSTgxXR0dZhak6aZ/IVRZvaHERMi0EgSTzIJdlvZlpw3Kf18ePMcTrRrtEXwD4RIjKt8pbs0g2Hg==", + "license": "SEE LICENSE IN LICENSE", + "peerDependencies": { + "@sap/cds": ">=8" + } + }, + "node_modules/@sap/cds-mtxs": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@sap/cds-mtxs/-/cds-mtxs-3.8.1.tgz", + "integrity": "sha512-FUm1of08WsZ1O3eur7OGj7ZYp9Gy3RuYEGBZK+zosSPyJ3QrBhBzKwYvbh2DJdbv3P/xuk46NGg5yz914qZxrw==", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@sap/hdi-deploy": "^5" + }, + "bin": { + "cds-mtx": "bin/cds-mtx.js", + "cds-mtx-migrate": "bin/cds-mtx-migrate.js" + }, + "peerDependencies": { + "@sap/cds": "^9" + } + }, + "node_modules/@sap/hdi": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@sap/hdi/-/hdi-4.8.0.tgz", + "integrity": "sha512-tkJmY2ffm6mt4/LFwRBihlQkMxNAXa3ngvRe2N/6+qLIsUNdrH/M03S5mkygXq56K+KoVVZYuradajCusMWwsw==", + "license": "See LICENSE file", + "dependencies": { + "async": "^3.2.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@sap/hana-client": "^2 >= 2.5", + "hdb": "^2 || ^0" + }, + "peerDependenciesMeta": { + "@sap/hana-client": { + "optional": true + }, + "hdb": { + "optional": true + } + } + }, + "node_modules/@sap/hdi-deploy": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@sap/hdi-deploy/-/hdi-deploy-5.6.1.tgz", + "integrity": "sha512-+qQ7qwG8lko303L5yRj2dud/nDAVuVblV/mmzJT44oPbF0Nry18eD2LUS23hFeuxjRa7rYK5YKQ8ffGgWxVrYQ==", + "license": "See LICENSE file", + "dependencies": { + "@sap/hdi": "^4.8.0", + "@sap/xsenv": "^6.0.0", + "async": "^3.2.6", + "dotenv": "^16.4.5", + "handlebars": "^4.7.8", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=18.x" + }, + "peerDependencies": { + "@sap/hana-client": "^2 >= 2.6", + "hdb": "^2 || ^0" + }, + "peerDependenciesMeta": { + "@sap/hana-client": { + "optional": true + }, + "hdb": { + "optional": true + } + } + }, + "node_modules/@sap/xsenv": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@sap/xsenv/-/xsenv-6.1.0.tgz", + "integrity": "sha512-vlW4Zad3uiDqHtnYdQ0TsEIH8VIO4HmPGDowfBL5dIcHPmeKDISEQ9ibeHL5FkceqvYcXJEQAVZ5/hsHDqlXZg==", + "license": "SEE LICENSE IN LICENSE file", + "dependencies": { + "debug": "4.4.3", + "node-cache": "^5.1.2", + "verror": "1.10.1" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + } + }, + "node_modules/@sap/xsenv/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@sap/xsenv/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/@sap/xssec": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@sap/xssec/-/xssec-4.13.0.tgz", + "integrity": "sha512-8e+bU+OyAIpAGXQanOopZa5YEK+yHKw84dhhihcCotF40MSNFbVHjQ4xM5hf4QndlqDGfXIuvXmoOMuDATa/gA==", + "license": "SAP DEVELOPER LICENSE AGREEMENT", + "dependencies": { + "debug": "^4.4.3", + "jwt-decode": "^4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sap/xssec/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@sap/xssec/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "12.8.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.8.0.tgz", + "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC" + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/express/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/express/node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/express/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/express/node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/mtx-local-sidecar": { + "resolved": "mtx/sidecar", + "link": true + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.89.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-cache": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", + "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", + "license": "MIT", + "dependencies": { + "clone": "2.x" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/router/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz", + "integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/integration-tests/mtx-local/package.json b/integration-tests/mtx-local/package.json new file mode 100644 index 000000000..ccad7b80d --- /dev/null +++ b/integration-tests/mtx-local/package.json @@ -0,0 +1,11 @@ +{ + "name": "mtx-local-integration-tests", + "version": "0.0.0", + "devDependencies": { + "@sap/cds-dk": "^9", + "@sap/cds-mtxs": "^3" + }, + "workspaces": [ + "mtx/sidecar" + ] +} diff --git a/integration-tests/mtx-local/srv/pom.xml b/integration-tests/mtx-local/srv/pom.xml new file mode 100644 index 000000000..5d9601fca --- /dev/null +++ b/integration-tests/mtx-local/srv/pom.xml @@ -0,0 +1,250 @@ + + + 4.0.0 + + + com.sap.cds.integration-tests + cds-feature-attachments-integration-tests-parent + ${revision} + ../../pom.xml + + + cds-feature-attachments-integration-tests-mtx-local + jar + + Integration Tests - MTX Local + + + com.sap.cds.feature.attachments.generated + ${project.basedir}/../mtx/sidecar + true + true + true + + + + + + com.sap.cds + cds-starter-spring-boot + + + + + com.sap.cds + cds-adapter-odata-v4 + runtime + + + + + com.sap.cds + cds-starter-cloudfoundry + + + + + org.xerial + sqlite-jdbc + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.security + spring-security-test + test + + + + + + + + org.apache.maven.plugins + maven-clean-plugin + + + + ${project.basedir}/.. + false + + *.db + *.sqlite* + + + + + + + sidecar-dbs-clean + + clean + + initialize + + + + + + + com.sap.cds + cds-maven-plugin + + + cds.clean + + clean + + + + + cds.install-node + + install-node + + + + + cds.resolve + + resolve + + + ${project.basedir}/.. + ${project.basedir}/.. + + + + + install-dependencies + + npm + + + ${project.basedir}/.. + + install + + + + + + cds.build + + cds + + + ${project.basedir}/.. + + build --for java + + + + + + cds.generate + + generate + + + ${project.basedir}/.. + ${generation-package}.mt.test.cds4j + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + execute-local-integration-tests + + integration-test + + integration-test + + + **/**/*Test.java + + + + + verify-local-integration-tests + + verify + + + + + + + + org.apache.maven.plugins + maven-resources-plugin + + + copy-cds-models-to-sidecar + + copy-resources + + pre-integration-test + + ${project.basedir}/../node_modules/com.sap.cds/cds-feature-attachments + + + ${project.basedir}/../target/cds/com.sap.cds/cds-feature-attachments + + + + + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.5.0 + + ${cds.npm.executable} + + ${cds.node.directory}${path.separator}${env.PATH} + + ${skipTests} + ${sidecar.dir} + true + true + run start + + + + start-sidecar + + exec + + pre-integration-test + + + + + + + diff --git a/integration-tests/mtx-local/srv/service.cds b/integration-tests/mtx-local/srv/service.cds new file mode 100644 index 000000000..38b843210 --- /dev/null +++ b/integration-tests/mtx-local/srv/service.cds @@ -0,0 +1,5 @@ +using { mt.test.data as db } from '../db/index'; + +service MtTestService { + entity Documents as projection on db.Documents; +} diff --git a/integration-tests/mtx-local/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/mt/Application.java b/integration-tests/mtx-local/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/mt/Application.java new file mode 100644 index 000000000..fc98c7fb8 --- /dev/null +++ b/integration-tests/mtx-local/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/mt/Application.java @@ -0,0 +1,15 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.mt; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/integration-tests/mtx-local/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/mt/handlers/system/SubscribeModelTenantsHandler.java b/integration-tests/mtx-local/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/mt/handlers/system/SubscribeModelTenantsHandler.java new file mode 100644 index 000000000..cde1364f6 --- /dev/null +++ b/integration-tests/mtx-local/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/mt/handlers/system/SubscribeModelTenantsHandler.java @@ -0,0 +1,90 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.mt.handlers.system; + +import com.sap.cds.services.application.ApplicationLifecycleService; +import com.sap.cds.services.application.ApplicationPreparedEventContext; +import com.sap.cds.services.application.ApplicationStoppedEventContext; +import com.sap.cds.services.environment.CdsProperties; +import com.sap.cds.services.handler.EventHandler; +import com.sap.cds.services.handler.annotations.On; +import com.sap.cds.services.handler.annotations.ServiceName; +import com.sap.cds.services.mt.DeploymentService; +import com.sap.cds.services.runtime.CdsRuntime; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.stream.Collectors; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +@Component +@ServiceName(ApplicationLifecycleService.DEFAULT_NAME) +public class SubscribeModelTenantsHandler implements EventHandler { + + @Autowired private CdsRuntime runtime; + @Autowired private DeploymentService service; + + @On(event = ApplicationLifecycleService.EVENT_APPLICATION_PREPARED) + public void subscribeMockTenants(ApplicationPreparedEventContext context) { + var multiTenancy = runtime.getEnvironment().getCdsProperties().getMultiTenancy(); + + if (Boolean.FALSE.equals(multiTenancy.getMock().isEnabled())) { + return; + } + if (readMockedTenants().isEmpty()) { + return; + } + if (!StringUtils.hasText(multiTenancy.getSidecar().getUrl())) { + return; + } + + readMockedTenants().forEach(this::subscribeTenant); + } + + @On(event = ApplicationLifecycleService.EVENT_APPLICATION_STOPPED) + public void unsubscribeMockTenants(ApplicationStoppedEventContext context) { + var multiTenancy = runtime.getEnvironment().getCdsProperties().getMultiTenancy(); + + if (Boolean.FALSE.equals(multiTenancy.getMock().isEnabled())) { + return; + } + if (readMockedTenants().isEmpty()) { + return; + } + + readMockedTenants().forEach(this::unsubscribeTenant); + } + + private void subscribeTenant(String tenant) { + runtime + .requestContext() + .privilegedUser() + .run( + c -> { + service.subscribe( + tenant, + new HashMap<>( + Collections.singletonMap("subscribedSubdomain", "mt-" + tenant))); + }); + } + + private void unsubscribeTenant(String tenant) { + runtime + .requestContext() + .privilegedUser() + .run( + c -> { + service.unsubscribe(tenant, Collections.emptyMap()); + }); + } + + private List readMockedTenants() { + return runtime.getEnvironment().getCdsProperties().getSecurity().getMock().getTenants().values() + .stream() + .map(CdsProperties.Security.Mock.Tenant::getName) + .collect(Collectors.toList()); + } +} diff --git a/integration-tests/mtx-local/srv/src/main/resources/application.yaml b/integration-tests/mtx-local/srv/src/main/resources/application.yaml new file mode 100644 index 000000000..2bf2b72a3 --- /dev/null +++ b/integration-tests/mtx-local/srv/src/main/resources/application.yaml @@ -0,0 +1,23 @@ +cds: + multi-tenancy: + sidecar: + url: http://localhost:4005 + security: + mock: + users: + - name: user-in-tenant-1 + tenant: tenant-1 + - name: user-in-tenant-2 + tenant: tenant-2 + - name: user-in-tenant-3 + tenant: tenant-3 + +--- +spring: + config.activate.on-profile: local-with-tenants +cds: + security: + mock: + tenants: + - name: tenant-1 + - name: tenant-2 diff --git a/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/MultiTenantAttachmentIsolationTest.java b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/MultiTenantAttachmentIsolationTest.java new file mode 100644 index 000000000..14a22d2d4 --- /dev/null +++ b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/MultiTenantAttachmentIsolationTest.java @@ -0,0 +1,115 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.mt; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.junit.jupiter.api.Test; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("local-with-tenants") +class MultiTenantAttachmentIsolationTest { + + private static final String DOCUMENTS_URL = "/odata/v4/MtTestService/Documents"; + + @Autowired MockMvc client; + @Autowired ObjectMapper objectMapper; + + @Test + void createDocumentInTenant1_notVisibleInTenant2() throws Exception { + // Create a document in tenant-1 + client + .perform( + post(DOCUMENTS_URL) + .with(httpBasic("user-in-tenant-1", "")) + .contentType(MediaType.APPLICATION_JSON) + .content("{ \"title\": \"Only in tenant-1\" }")) + .andExpect(status().isCreated()); + + // Read documents in tenant-2 — should NOT see the tenant-1 document + String response = + client + .perform(get(DOCUMENTS_URL).with(httpBasic("user-in-tenant-2", ""))) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + JsonNode values = objectMapper.readTree(response).path("value"); + values.forEach( + node -> assertThat(node.get("title").asText("")).isNotEqualTo("Only in tenant-1")); + } + + @Test + void createDocumentsInBothTenants_eachSeeOnlyOwn() throws Exception { + // Create in tenant-1 + client + .perform( + post(DOCUMENTS_URL) + .with(httpBasic("user-in-tenant-1", "")) + .contentType(MediaType.APPLICATION_JSON) + .content("{ \"title\": \"Doc-T1\" }")) + .andExpect(status().isCreated()); + + // Create in tenant-2 + client + .perform( + post(DOCUMENTS_URL) + .with(httpBasic("user-in-tenant-2", "")) + .contentType(MediaType.APPLICATION_JSON) + .content("{ \"title\": \"Doc-T2\" }")) + .andExpect(status().isCreated()); + + // Read from tenant-1 — should see Doc-T1 but not Doc-T2 + String response1 = + client + .perform(get(DOCUMENTS_URL).with(httpBasic("user-in-tenant-1", ""))) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + JsonNode values1 = objectMapper.readTree(response1).path("value"); + boolean foundT1 = false; + for (JsonNode node : values1) { + assertThat(node.get("title").asText("")).isNotEqualTo("Doc-T2"); + if ("Doc-T1".equals(node.get("title").asText(""))) { + foundT1 = true; + } + } + assertThat(foundT1).isTrue(); + + // Read from tenant-2 — should see Doc-T2 but not Doc-T1 + String response2 = + client + .perform(get(DOCUMENTS_URL).with(httpBasic("user-in-tenant-2", ""))) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + JsonNode values2 = objectMapper.readTree(response2).path("value"); + boolean foundT2 = false; + for (JsonNode node : values2) { + assertThat(node.get("title").asText("")).isNotEqualTo("Doc-T1"); + if ("Doc-T2".equals(node.get("title").asText(""))) { + foundT2 = true; + } + } + assertThat(foundT2).isTrue(); + } +} diff --git a/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/SubscribeAndUnsubscribeTest.java b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/SubscribeAndUnsubscribeTest.java new file mode 100644 index 000000000..aaeb0673d --- /dev/null +++ b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/SubscribeAndUnsubscribeTest.java @@ -0,0 +1,72 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.mt; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sap.cds.feature.attachments.integrationtests.mt.utils.SubscriptionEndpointClient; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("local-with-tenants") +class SubscribeAndUnsubscribeTest { + + private static final String DOCUMENTS_URL = "/odata/v4/MtTestService/Documents"; + + @Autowired MockMvc client; + @Autowired ObjectMapper objectMapper; + + SubscriptionEndpointClient subscriptionEndpointClient; + + @BeforeEach + void setup() { + subscriptionEndpointClient = new SubscriptionEndpointClient(objectMapper, client); + } + + @Test + void subscribeTenant3_thenServiceIsReachable() throws Exception { + subscriptionEndpointClient.subscribeTenant("tenant-3"); + + client + .perform(get(DOCUMENTS_URL).with(httpBasic("user-in-tenant-3", ""))) + .andExpect(status().isOk()); + } + + @Test + void unsubscribeTenant3_thenServiceFails() throws Exception { + subscriptionEndpointClient.subscribeTenant("tenant-3"); + + // Verify it works + client + .perform(get(DOCUMENTS_URL).with(httpBasic("user-in-tenant-3", ""))) + .andExpect(status().isOk()); + + subscriptionEndpointClient.unsubscribeTenant("tenant-3"); + + // Service should fail after unsubscription + client + .perform(get(DOCUMENTS_URL).with(httpBasic("user-in-tenant-3", ""))) + .andExpect(status().isInternalServerError()); + } + + @AfterEach + void tearDown() { + try { + subscriptionEndpointClient.unsubscribeTenant("tenant-3"); + } catch (Exception ignored) { + // best effort cleanup + } + } +} diff --git a/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/utils/SubscriptionEndpointClient.java b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/utils/SubscriptionEndpointClient.java new file mode 100644 index 000000000..07fc0c8ff --- /dev/null +++ b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/utils/SubscriptionEndpointClient.java @@ -0,0 +1,67 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.mt.utils; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.codec.binary.Base64; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +public class SubscriptionEndpointClient { + + private static final String MT_SUBSCRIPTIONS_TENANTS = "/mt/v1.0/subscriptions/tenants/"; + + private final ObjectMapper objectMapper; + private final MockMvc client; + private final String credentials = + "Basic " + new String(Base64.encodeBase64("privileged:".getBytes())); + + public SubscriptionEndpointClient(ObjectMapper objectMapper, MockMvc client) { + this.objectMapper = objectMapper; + this.client = client; + } + + public void subscribeTenant(String tenant) throws Exception { + SubscriptionPayload payload = new SubscriptionPayload(); + payload.subscribedTenantId = tenant; + payload.subscribedSubdomain = tenant.concat("sap.com"); + payload.eventType = "CREATE"; + + client + .perform( + put(MT_SUBSCRIPTIONS_TENANTS.concat(payload.subscribedTenantId)) + .header(HttpHeaders.AUTHORIZATION, credentials) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(payload))) + .andExpect(status().isCreated()); + } + + public void unsubscribeTenant(String tenant) throws Exception { + DeletePayload payload = new DeletePayload(); + payload.subscribedTenantId = tenant; + + client + .perform( + delete(MT_SUBSCRIPTIONS_TENANTS.concat(payload.subscribedTenantId)) + .header(HttpHeaders.AUTHORIZATION, credentials) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(payload))) + .andExpect(status().isNoContent()); + } + + static class SubscriptionPayload { + public String subscribedTenantId; + public String subscribedSubdomain; + public String eventType; + } + + static class DeletePayload { + public String subscribedTenantId; + } +} diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 213c34492..593ea7971 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -17,7 +17,8 @@ db - srv + generic + mtx-local/srv From da621f413d0930cf1ceb1604ad10eca534a7d271 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Tue, 31 Mar 2026 10:01:51 +0200 Subject: [PATCH 07/18] Remove package-lock.json from mtx-local integration tests The lock file is not needed in version control since Maven handles npm install during the build via cds-maven-plugin. --- integration-tests/mtx-local/.gitignore | 1 + integration-tests/mtx-local/package-lock.json | 4144 ----------------- 2 files changed, 1 insertion(+), 4144 deletions(-) create mode 100644 integration-tests/mtx-local/.gitignore delete mode 100644 integration-tests/mtx-local/package-lock.json diff --git a/integration-tests/mtx-local/.gitignore b/integration-tests/mtx-local/.gitignore new file mode 100644 index 000000000..41fd97059 --- /dev/null +++ b/integration-tests/mtx-local/.gitignore @@ -0,0 +1 @@ +integration-tests/mtx-local/package-lock.json diff --git a/integration-tests/mtx-local/package-lock.json b/integration-tests/mtx-local/package-lock.json deleted file mode 100644 index 3383043ec..000000000 --- a/integration-tests/mtx-local/package-lock.json +++ /dev/null @@ -1,4144 +0,0 @@ -{ - "name": "mtx-local-integration-tests", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "mtx-local-integration-tests", - "version": "0.0.0", - "workspaces": [ - "mtx/sidecar" - ], - "devDependencies": { - "@sap/cds-dk": "^9", - "@sap/cds-mtxs": "^3" - } - }, - "mtx/sidecar": { - "name": "mtx-local-sidecar", - "version": "0.0.0", - "dependencies": { - "@sap/cds": "^9", - "@sap/cds-mtxs": "^3", - "@sap/xssec": "^4", - "express": "^4" - }, - "devDependencies": { - "@cap-js/sqlite": "^2" - } - }, - "mtx/sidecar/node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "~1.20.3", - "content-disposition": "~0.5.4", - "content-type": "~1.0.4", - "cookie": "~0.7.1", - "cookie-signature": "~1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.3.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.12", - "proxy-addr": "~2.0.7", - "qs": "~6.14.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "~0.19.0", - "serve-static": "~1.16.2", - "setprototypeof": "1.2.0", - "statuses": "~2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@cap-js/db-service": { - "version": "2.9.0", - "resolved": "https://int.repositories.cloud.sap/artifactory/api/npm/build-milestones-npm/@cap-js/db-service/-/db-service-2.9.0.tgz", - "integrity": "sha512-WCXhoqezaF6A5I2l0MNZeHKXXtHRNEq7Rp0R89/uccOHQIx0DuU0U9NuJJPV/1G5RGk2QKQ9VBo/KYn+MZuuNQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "generic-pool": "^3.9.0" - }, - "peerDependencies": { - "@sap/cds": ">=9.8" - } - }, - "node_modules/@cap-js/sqlite": { - "version": "2.2.0", - "resolved": "https://int.repositories.cloud.sap/artifactory/api/npm/build-milestones-npm/@cap-js/sqlite/-/sqlite-2.2.0.tgz", - "integrity": "sha512-FPj+uVU/14vtGUl2P/Q8y7XhZbsLgrCav2O5PjHPXnupegjby4sMJkgVNxVHnkyKPFgO/W8uEsq9r5TU9VPx8w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@cap-js/db-service": "^2.9.0", - "better-sqlite3": "^12.0.0" - }, - "peerDependencies": { - "@sap/cds": ">=9.8", - "sql.js": "^1.13.0" - }, - "peerDependenciesMeta": { - "sql.js": { - "optional": true - } - } - }, - "node_modules/@eslint/js": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", - "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", - "license": "MIT", - "peer": true, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "eslint": "^10.0.0" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/@sap/cds": { - "version": "9.8.4", - "resolved": "https://registry.npmjs.org/@sap/cds/-/cds-9.8.4.tgz", - "integrity": "sha512-1cnFdz4Ex4LV15dztDsSLDiilVdJ2mfTqr5A3TGsRd/6cM31SnHhuZ/Ra0FEErodokNS2pwasGZmSfgHVDW8KA==", - "license": "SEE LICENSE IN LICENSE", - "dependencies": { - "@sap/cds-compiler": "^6.4", - "@sap/cds-fiori": "^2", - "express": "^4.22.1 || ^5", - "js-yaml": "^4.1.1" - }, - "bin": { - "cds-deploy": "bin/deploy.js", - "cds-serve": "bin/serve.js" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@eslint/js": "^9 || ^10", - "tar": "^7.5.6" - }, - "peerDependenciesMeta": { - "tar": { - "optional": true - } - } - }, - "node_modules/@sap/cds-compiler": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@sap/cds-compiler/-/cds-compiler-6.8.0.tgz", - "integrity": "sha512-yRoTZcH8DFiP4PcEuIHe42YAaNt51V736+6RRr+U4nbO91sz36L8be2djcE8n760t8IbqLxzZ/UkATiL0fjRmA==", - "license": "SEE LICENSE IN LICENSE", - "bin": { - "cdsc": "bin/cdsc.js", - "cdshi": "bin/cdshi.js", - "cdsse": "bin/cdsse.js" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@sap/cds-dk": { - "version": "9.8.2", - "resolved": "https://registry.npmjs.org/@sap/cds-dk/-/cds-dk-9.8.2.tgz", - "integrity": "sha512-PGynoQpOCD8Nsr0EEzsEHWg8szYBLVtVd6byxvaaAPYBkNAj2WA+kin6gWaDTiyG3kJa0BSGPk4PCbA2xDoE9A==", - "dev": true, - "hasShrinkwrap": true, - "license": "SEE LICENSE IN LICENSE", - "dependencies": { - "@cap-js/asyncapi": "^1.0.0", - "@cap-js/openapi": "^1.0.0", - "@sap/cds": ">=8.3", - "@sap/cds-mtxs": ">=2", - "@sap/hdi-deploy": "^5", - "axios": "^1", - "express": "^4.22.1 || ^5", - "hdb": "^2.0.0", - "livereload-js": "^4.0.1", - "mustache": "^4.0.1", - "ws": "^8.4.2", - "xml-js": "^1.6.11", - "yaml": "^2" - }, - "bin": { - "cds": "bin/cds.js", - "cds-ts": "bin/cds-ts.js", - "cds-tsx": "bin/cds-tsx.js" - }, - "optionalDependencies": { - "@cap-js/sqlite": ">=1" - } - }, - "node_modules/@sap/cds-dk/node_modules/@cap-js/asyncapi": { - "version": "1.0.3", - "integrity": "sha512-vZSWKAe+3qfvZDXV5SSFiObGWmqyS9MDyEADb5PLVT8kzO39qGaSDPv/GzI/gwvRfCayGAjU4ThiBKrFA7Gclg==", - "dev": true, - "license": "SEE LICENSE IN LICENSE", - "peerDependencies": { - "@sap/cds": ">=7.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/@cap-js/db-service": { - "version": "2.9.0", - "integrity": "sha512-WCXhoqezaF6A5I2l0MNZeHKXXtHRNEq7Rp0R89/uccOHQIx0DuU0U9NuJJPV/1G5RGk2QKQ9VBo/KYn+MZuuNQ==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "generic-pool": "^3.9.0" - }, - "peerDependencies": { - "@sap/cds": ">=9.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/@cap-js/openapi": { - "version": "1.4.0", - "integrity": "sha512-/LRSwn4SDxAi3qKwl09zoOhEVGaPGlYOPz/0S3UBnaMJVvaLyPiKbbaOtOnrrgulUX5OXt+ujPIQznOsbTzuAw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "pluralize": "^8.0.0" - }, - "peerDependencies": { - "@sap/cds": ">=7.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/@cap-js/sqlite": { - "version": "2.2.0", - "integrity": "sha512-FPj+uVU/14vtGUl2P/Q8y7XhZbsLgrCav2O5PjHPXnupegjby4sMJkgVNxVHnkyKPFgO/W8uEsq9r5TU9VPx8w==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@cap-js/db-service": "^2.9.0", - "better-sqlite3": "^12.0.0" - }, - "peerDependencies": { - "@sap/cds": ">=9.8", - "sql.js": "^1.13.0" - }, - "peerDependenciesMeta": { - "sql.js": { - "optional": true - } - } - }, - "node_modules/@sap/cds-dk/node_modules/@eslint/js": { - "version": "10.0.1", - "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "eslint": "^10.0.0" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/@sap/cds-dk/node_modules/@sap/cds": { - "version": "9.8.3", - "integrity": "sha512-up/kNVCZK8bOTyye1QN2BjMpH+IgcdyhT3U9mS3TqkpmjNE2JXq8Qz1iNAN+nYcXRWJ1XAUNpgebLfuh/49MGQ==", - "dev": true, - "license": "SEE LICENSE IN LICENSE", - "dependencies": { - "@sap/cds-compiler": "^6.4", - "@sap/cds-fiori": "^2", - "express": "^4.22.1 || ^5", - "js-yaml": "^4.1.1" - }, - "bin": { - "cds-deploy": "bin/deploy.js", - "cds-serve": "bin/serve.js" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@eslint/js": "^9 || ^10", - "tar": "^7.5.6" - }, - "peerDependenciesMeta": { - "tar": { - "optional": true - } - } - }, - "node_modules/@sap/cds-dk/node_modules/@sap/cds-compiler": { - "version": "6.8.0", - "integrity": "sha512-yRoTZcH8DFiP4PcEuIHe42YAaNt51V736+6RRr+U4nbO91sz36L8be2djcE8n760t8IbqLxzZ/UkATiL0fjRmA==", - "dev": true, - "license": "SEE LICENSE IN LICENSE", - "bin": { - "cdsc": "bin/cdsc.js", - "cdshi": "bin/cdshi.js", - "cdsse": "bin/cdsse.js" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@sap/cds-dk/node_modules/@sap/cds-fiori": { - "version": "2.3.0", - "integrity": "sha512-6oWov+DSpFrSTgxXR0dZhak6aZ/IVRZvaHERMi0EgSTzIJdlvZlpw3Kf18ePMcTrRrtEXwD4RIjKt8pbs0g2Hg==", - "dev": true, - "license": "SEE LICENSE IN LICENSE", - "peerDependencies": { - "@sap/cds": ">=8" - } - }, - "node_modules/@sap/cds-dk/node_modules/@sap/cds-mtxs": { - "version": "3.8.1", - "integrity": "sha512-FUm1of08WsZ1O3eur7OGj7ZYp9Gy3RuYEGBZK+zosSPyJ3QrBhBzKwYvbh2DJdbv3P/xuk46NGg5yz914qZxrw==", - "dev": true, - "license": "SEE LICENSE IN LICENSE", - "dependencies": { - "@sap/hdi-deploy": "^5" - }, - "bin": { - "cds-mtx": "bin/cds-mtx.js", - "cds-mtx-migrate": "bin/cds-mtx-migrate.js" - }, - "peerDependencies": { - "@sap/cds": "^9" - } - }, - "node_modules/@sap/cds-dk/node_modules/@sap/hdi": { - "version": "4.8.0", - "integrity": "sha512-tkJmY2ffm6mt4/LFwRBihlQkMxNAXa3ngvRe2N/6+qLIsUNdrH/M03S5mkygXq56K+KoVVZYuradajCusMWwsw==", - "dev": true, - "license": "See LICENSE file", - "dependencies": { - "async": "^3.2.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@sap/hana-client": "^2 >= 2.5", - "hdb": "^2 || ^0" - }, - "peerDependenciesMeta": { - "@sap/hana-client": { - "optional": true - }, - "hdb": { - "optional": true - } - } - }, - "node_modules/@sap/cds-dk/node_modules/@sap/hdi-deploy": { - "version": "5.6.1", - "integrity": "sha512-+qQ7qwG8lko303L5yRj2dud/nDAVuVblV/mmzJT44oPbF0Nry18eD2LUS23hFeuxjRa7rYK5YKQ8ffGgWxVrYQ==", - "dev": true, - "license": "See LICENSE file", - "dependencies": { - "@sap/hdi": "^4.8.0", - "@sap/xsenv": "^6.0.0", - "async": "^3.2.6", - "dotenv": "^16.4.5", - "handlebars": "^4.7.8", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=18.x" - }, - "peerDependencies": { - "@sap/hana-client": "^2 >= 2.6", - "hdb": "^2 || ^0" - }, - "peerDependenciesMeta": { - "@sap/hana-client": { - "optional": true - }, - "hdb": { - "optional": true - } - } - }, - "node_modules/@sap/cds-dk/node_modules/@sap/xsenv": { - "version": "6.1.0", - "integrity": "sha512-vlW4Zad3uiDqHtnYdQ0TsEIH8VIO4HmPGDowfBL5dIcHPmeKDISEQ9ibeHL5FkceqvYcXJEQAVZ5/hsHDqlXZg==", - "dev": true, - "license": "SEE LICENSE IN LICENSE file", - "dependencies": { - "debug": "4.4.3", - "node-cache": "^5.1.2", - "verror": "1.10.1" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/accepts": { - "version": "2.0.0", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/argparse": { - "version": "2.0.1", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/@sap/cds-dk/node_modules/assert-plus": { - "version": "1.0.0", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/async": { - "version": "3.2.6", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/asynckit": { - "version": "0.4.0", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/axios": { - "version": "1.13.6", - "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.11", - "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/base64-js": { - "version": "1.5.1", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/better-sqlite3": { - "version": "12.8.0", - "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" - }, - "engines": { - "node": "20.x || 22.x || 23.x || 24.x || 25.x" - } - }, - "node_modules/@sap/cds-dk/node_modules/bindings": { - "version": "1.5.0", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/bl": { - "version": "4.1.0", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/body-parser": { - "version": "2.2.2", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/braces": { - "version": "3.0.3", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sap/cds-dk/node_modules/buffer": { - "version": "5.7.1", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/@sap/cds-dk/node_modules/bytes": { - "version": "3.1.2", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/call-bound": { - "version": "1.0.4", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/chownr": { - "version": "1.1.4", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/clone": { - "version": "2.1.2", - "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/combined-stream": { - "version": "1.0.8", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/content-disposition": { - "version": "1.0.1", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/content-type": { - "version": "1.0.5", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/cookie": { - "version": "0.7.2", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/cookie-signature": { - "version": "1.2.2", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/core-util-is": { - "version": "1.0.2", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/debug": { - "version": "4.4.3", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@sap/cds-dk/node_modules/decompress-response": { - "version": "6.0.0", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@sap/cds-dk/node_modules/deep-extend": { - "version": "0.6.0", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/delayed-stream": { - "version": "1.0.0", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/depd": { - "version": "2.0.0", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/detect-libc": { - "version": "2.1.2", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sap/cds-dk/node_modules/dotenv": { - "version": "16.6.1", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/@sap/cds-dk/node_modules/dunder-proto": { - "version": "1.0.1", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/ee-first": { - "version": "1.1.1", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/encodeurl": { - "version": "2.0.0", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/end-of-stream": { - "version": "1.4.5", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/es-define-property": { - "version": "1.0.1", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/es-errors": { - "version": "1.3.0", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/es-object-atoms": { - "version": "1.1.1", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/es-set-tostringtag": { - "version": "2.1.0", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/escape-html": { - "version": "1.0.3", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/etag": { - "version": "1.8.1", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/expand-template": { - "version": "2.0.3", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "dev": true, - "license": "(MIT OR WTFPL)", - "optional": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/@sap/cds-dk/node_modules/express": { - "version": "5.2.1", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/extsprintf": { - "version": "1.4.1", - "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/file-uri-to-path": { - "version": "1.0.0", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/fill-range": { - "version": "7.1.1", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sap/cds-dk/node_modules/finalhandler": { - "version": "2.1.1", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/follow-redirects": { - "version": "1.15.11", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/@sap/cds-dk/node_modules/form-data": { - "version": "4.0.5", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@sap/cds-dk/node_modules/form-data/node_modules/mime-db": { - "version": "1.52.0", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/form-data/node_modules/mime-types": { - "version": "2.1.35", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/forwarded": { - "version": "0.2.0", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/fresh": { - "version": "2.0.0", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/fs-constants": { - "version": "1.0.0", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/function-bind": { - "version": "1.1.2", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/generic-pool": { - "version": "3.9.0", - "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/@sap/cds-dk/node_modules/get-intrinsic": { - "version": "1.3.0", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/get-proto": { - "version": "1.0.1", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/github-from-package": { - "version": "0.0.0", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/gopd": { - "version": "1.2.0", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/handlebars": { - "version": "4.7.8", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/has-symbols": { - "version": "1.1.0", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/has-tostringtag": { - "version": "1.0.2", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/hasown": { - "version": "2.0.2", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/hdb": { - "version": "2.27.1", - "integrity": "sha512-xYL/W+fq2TyGHyzm8muolQnw8tdh4+2NQ8mQP2FpLSuhfJ8l0jQNSUZoAXic7NfMEan1Jvf8V1L4blwkgTc6+A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "iconv-lite": "0.7.0" - }, - "engines": { - "node": ">= 18" - }, - "optionalDependencies": { - "lz4-wasm-nodejs": "0.9.2" - } - }, - "node_modules/@sap/cds-dk/node_modules/hdb/node_modules/iconv-lite": { - "version": "0.7.0", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/http-errors": { - "version": "2.0.1", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/iconv-lite": { - "version": "0.7.2", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/ieee754": { - "version": "1.2.1", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/inherits": { - "version": "2.0.4", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/@sap/cds-dk/node_modules/ini": { - "version": "1.3.8", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/ipaddr.js": { - "version": "1.9.1", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/@sap/cds-dk/node_modules/is-number": { - "version": "7.0.0", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/is-promise": { - "version": "4.0.0", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/js-yaml": { - "version": "4.1.1", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@sap/cds-dk/node_modules/livereload-js": { - "version": "4.0.2", - "integrity": "sha512-Fy7VwgQNiOkynYyNBTo3v9hQUhcW5pFAheJN148+DTgpShjsy/22pLHKKwDK5v0kOsZsJBK+6q1PMgLvRmrwFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/lz4-wasm-nodejs": { - "version": "0.9.2", - "integrity": "sha512-hSwgJPS98q/Oe/89Y1OxzeA/UdnASG8GvldRyKa7aZyoAFCC8VPRtViBSava7wWC66WocjUwBpWau2rEmyFPsw==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/math-intrinsics": { - "version": "1.1.0", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/media-typer": { - "version": "1.1.0", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/merge-descriptors": { - "version": "2.0.0", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@sap/cds-dk/node_modules/micromatch": { - "version": "4.0.8", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.2", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@sap/cds-dk/node_modules/mime-db": { - "version": "1.54.0", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/mime-types": { - "version": "3.0.2", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/mimic-response": { - "version": "3.1.0", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@sap/cds-dk/node_modules/minimist": { - "version": "1.2.8", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/mkdirp-classic": { - "version": "0.5.3", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/ms": { - "version": "2.1.3", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/mustache": { - "version": "4.2.0", - "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", - "dev": true, - "license": "MIT", - "bin": { - "mustache": "bin/mustache" - } - }, - "node_modules/@sap/cds-dk/node_modules/napi-build-utils": { - "version": "2.0.0", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/negotiator": { - "version": "1.0.0", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/neo-async": { - "version": "2.6.2", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/node-abi": { - "version": "3.89.0", - "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@sap/cds-dk/node_modules/node-cache": { - "version": "5.1.2", - "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone": "2.x" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/object-inspect": { - "version": "1.13.4", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/on-finished": { - "version": "2.4.1", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/once": { - "version": "1.4.0", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/@sap/cds-dk/node_modules/parseurl": { - "version": "1.3.3", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/path-to-regexp": { - "version": "8.3.0", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/pluralize": { - "version": "8.0.0", - "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@sap/cds-dk/node_modules/prebuild-install": { - "version": "7.1.3", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@sap/cds-dk/node_modules/proxy-addr": { - "version": "2.0.7", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/@sap/cds-dk/node_modules/proxy-from-env": { - "version": "1.1.0", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/pump": { - "version": "3.0.4", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/@sap/cds-dk/node_modules/qs": { - "version": "6.15.0", - "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/range-parser": { - "version": "1.2.1", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/raw-body": { - "version": "3.0.2", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/@sap/cds-dk/node_modules/rc": { - "version": "1.2.8", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "optional": true, - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/@sap/cds-dk/node_modules/readable-stream": { - "version": "3.6.2", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@sap/cds-dk/node_modules/router": { - "version": "2.2.0", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@sap/cds-dk/node_modules/safe-buffer": { - "version": "5.2.1", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/safer-buffer": { - "version": "2.1.2", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/sax": { - "version": "1.6.0", - "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=11.0.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/semver": { - "version": "7.7.4", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@sap/cds-dk/node_modules/send": { - "version": "1.2.1", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/serve-static": { - "version": "2.2.1", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/setprototypeof": { - "version": "1.2.0", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true, - "license": "ISC" - }, - "node_modules/@sap/cds-dk/node_modules/side-channel": { - "version": "1.1.0", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/side-channel-list": { - "version": "1.0.0", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/side-channel-map": { - "version": "1.0.1", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/side-channel-weakmap": { - "version": "1.0.2", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/simple-concat": { - "version": "1.0.1", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/simple-get": { - "version": "4.0.1", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/source-map": { - "version": "0.6.1", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/statuses": { - "version": "2.0.2", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/string_decoder": { - "version": "1.3.0", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/strip-json-comments": { - "version": "2.0.1", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/tar-fs": { - "version": "2.1.4", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/tar-stream": { - "version": "2.2.0", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@sap/cds-dk/node_modules/to-regex-range": { - "version": "5.0.1", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/toidentifier": { - "version": "1.0.1", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/tunnel-agent": { - "version": "0.6.0", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@sap/cds-dk/node_modules/type-is": { - "version": "2.0.1", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "dev": true, - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/uglify-js": { - "version": "3.19.3", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/unpipe": { - "version": "1.0.0", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/util-deprecate": { - "version": "1.0.2", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/vary": { - "version": "1.1.2", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/verror": { - "version": "1.10.1", - "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", - "dev": true, - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/wordwrap": { - "version": "1.0.0", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/wrappy": { - "version": "1.0.2", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/@sap/cds-dk/node_modules/ws": { - "version": "8.20.0", - "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/@sap/cds-dk/node_modules/xml-js": { - "version": "1.6.11", - "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "sax": "^1.2.4" - }, - "bin": { - "xml-js": "bin/cli.js" - } - }, - "node_modules/@sap/cds-dk/node_modules/yaml": { - "version": "2.8.3", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", - "dev": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, - "node_modules/@sap/cds-fiori": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@sap/cds-fiori/-/cds-fiori-2.3.0.tgz", - "integrity": "sha512-6oWov+DSpFrSTgxXR0dZhak6aZ/IVRZvaHERMi0EgSTzIJdlvZlpw3Kf18ePMcTrRrtEXwD4RIjKt8pbs0g2Hg==", - "license": "SEE LICENSE IN LICENSE", - "peerDependencies": { - "@sap/cds": ">=8" - } - }, - "node_modules/@sap/cds-mtxs": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@sap/cds-mtxs/-/cds-mtxs-3.8.1.tgz", - "integrity": "sha512-FUm1of08WsZ1O3eur7OGj7ZYp9Gy3RuYEGBZK+zosSPyJ3QrBhBzKwYvbh2DJdbv3P/xuk46NGg5yz914qZxrw==", - "license": "SEE LICENSE IN LICENSE", - "dependencies": { - "@sap/hdi-deploy": "^5" - }, - "bin": { - "cds-mtx": "bin/cds-mtx.js", - "cds-mtx-migrate": "bin/cds-mtx-migrate.js" - }, - "peerDependencies": { - "@sap/cds": "^9" - } - }, - "node_modules/@sap/hdi": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@sap/hdi/-/hdi-4.8.0.tgz", - "integrity": "sha512-tkJmY2ffm6mt4/LFwRBihlQkMxNAXa3ngvRe2N/6+qLIsUNdrH/M03S5mkygXq56K+KoVVZYuradajCusMWwsw==", - "license": "See LICENSE file", - "dependencies": { - "async": "^3.2.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@sap/hana-client": "^2 >= 2.5", - "hdb": "^2 || ^0" - }, - "peerDependenciesMeta": { - "@sap/hana-client": { - "optional": true - }, - "hdb": { - "optional": true - } - } - }, - "node_modules/@sap/hdi-deploy": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/@sap/hdi-deploy/-/hdi-deploy-5.6.1.tgz", - "integrity": "sha512-+qQ7qwG8lko303L5yRj2dud/nDAVuVblV/mmzJT44oPbF0Nry18eD2LUS23hFeuxjRa7rYK5YKQ8ffGgWxVrYQ==", - "license": "See LICENSE file", - "dependencies": { - "@sap/hdi": "^4.8.0", - "@sap/xsenv": "^6.0.0", - "async": "^3.2.6", - "dotenv": "^16.4.5", - "handlebars": "^4.7.8", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=18.x" - }, - "peerDependencies": { - "@sap/hana-client": "^2 >= 2.6", - "hdb": "^2 || ^0" - }, - "peerDependenciesMeta": { - "@sap/hana-client": { - "optional": true - }, - "hdb": { - "optional": true - } - } - }, - "node_modules/@sap/xsenv": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@sap/xsenv/-/xsenv-6.1.0.tgz", - "integrity": "sha512-vlW4Zad3uiDqHtnYdQ0TsEIH8VIO4HmPGDowfBL5dIcHPmeKDISEQ9ibeHL5FkceqvYcXJEQAVZ5/hsHDqlXZg==", - "license": "SEE LICENSE IN LICENSE file", - "dependencies": { - "debug": "4.4.3", - "node-cache": "^5.1.2", - "verror": "1.10.1" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@sap/xsenv/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@sap/xsenv/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/@sap/xssec": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@sap/xssec/-/xssec-4.13.0.tgz", - "integrity": "sha512-8e+bU+OyAIpAGXQanOopZa5YEK+yHKw84dhhihcCotF40MSNFbVHjQ4xM5hf4QndlqDGfXIuvXmoOMuDATa/gA==", - "license": "SAP DEVELOPER LICENSE AGREEMENT", - "dependencies": { - "debug": "^4.4.3", - "jwt-decode": "^4" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@sap/xssec/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@sap/xssec/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/better-sqlite3": { - "version": "12.8.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.8.0.tgz", - "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" - }, - "engines": { - "node": "20.x || 22.x || 23.x || 24.x || 25.x" - } - }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.14.0", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true, - "license": "ISC" - }, - "node_modules/clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "license": "MIT" - }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "dev": true, - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/content-disposition": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/express/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/express/node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/express/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/express/node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/express/node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/extsprintf": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", - "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", - "engines": [ - "node >=0.6.0" - ], - "license": "MIT" - }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "statuses": "~2.0.2", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true, - "license": "MIT" - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/generic-pool": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", - "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "dev": true, - "license": "MIT" - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/handlebars": { - "version": "4.7.9", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", - "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jwt-decode": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", - "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true, - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/mtx-local-sidecar": { - "resolved": "mtx/sidecar", - "link": true - }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "license": "MIT" - }, - "node_modules/node-abi": { - "version": "3.89.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", - "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-cache": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", - "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", - "license": "MIT", - "dependencies": { - "clone": "2.x" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", - "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", - "license": "MIT" - }, - "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/router/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/router/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/router/node_modules/path-to-regexp": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz", - "integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", - "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.1", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "~2.4.1", - "range-parser": "~1.2.1", - "statuses": "~2.0.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/serve-static": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", - "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "~0.19.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/uglify-js": { - "version": "3.19.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "license": "BSD-2-Clause", - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/verror": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", - "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "license": "MIT" - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - } - } -} From 21d5c090dce2540bbb5b133459916b7e4521a97e Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Tue, 31 Mar 2026 10:13:23 +0200 Subject: [PATCH 08/18] spotless --- .../handlers/system/SubscribeModelTenantsHandler.java | 11 ++++++++--- .../mt/MultiTenantAttachmentIsolationTest.java | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/integration-tests/mtx-local/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/mt/handlers/system/SubscribeModelTenantsHandler.java b/integration-tests/mtx-local/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/mt/handlers/system/SubscribeModelTenantsHandler.java index cde1364f6..3bed0e2c6 100644 --- a/integration-tests/mtx-local/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/mt/handlers/system/SubscribeModelTenantsHandler.java +++ b/integration-tests/mtx-local/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/mt/handlers/system/SubscribeModelTenantsHandler.java @@ -66,8 +66,7 @@ private void subscribeTenant(String tenant) { c -> { service.subscribe( tenant, - new HashMap<>( - Collections.singletonMap("subscribedSubdomain", "mt-" + tenant))); + new HashMap<>(Collections.singletonMap("subscribedSubdomain", "mt-" + tenant))); }); } @@ -82,7 +81,13 @@ private void unsubscribeTenant(String tenant) { } private List readMockedTenants() { - return runtime.getEnvironment().getCdsProperties().getSecurity().getMock().getTenants().values() + return runtime + .getEnvironment() + .getCdsProperties() + .getSecurity() + .getMock() + .getTenants() + .values() .stream() .map(CdsProperties.Security.Mock.Tenant::getName) .collect(Collectors.toList()); diff --git a/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/MultiTenantAttachmentIsolationTest.java b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/MultiTenantAttachmentIsolationTest.java index 14a22d2d4..f8cd24b2c 100644 --- a/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/MultiTenantAttachmentIsolationTest.java +++ b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/MultiTenantAttachmentIsolationTest.java @@ -11,13 +11,13 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; -import org.junit.jupiter.api.Test; @SpringBootTest @AutoConfigureMockMvc From 6d987ae46223e67e071cd3422a833e4b769f0009 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Tue, 31 Mar 2026 10:51:49 +0200 Subject: [PATCH 09/18] unit tests for coverage --- .../oss/handler/TenantCleanupHandler.java | 2 +- .../attachments/oss/client/AWSClientTest.java | 72 ++++ .../oss/client/AzureClientTest.java | 59 +++ .../oss/client/GoogleClientTest.java | 61 ++++ .../attachments/oss/client/OSClientTest.java | 40 ++ .../oss/configuration/RegistrationTest.java | 52 ++- .../OSSAttachmentsServiceHandlerTest.java | 341 ++++++++++++++++++ .../oss/handler/TenantCleanupHandlerTest.java | 62 ++++ 8 files changed, 687 insertions(+), 2 deletions(-) create mode 100644 storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/OSClientTest.java create mode 100644 storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandler.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandler.java index 6b305cebd..f719bb181 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandler.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandler.java @@ -1,5 +1,5 @@ /* - * © 2025 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.oss.handler; diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java index 658157b3b..05e86bff6 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java @@ -9,6 +9,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.sap.cds.feature.attachments.oss.handler.OSSAttachmentsServiceHandler; @@ -17,7 +18,9 @@ import com.sap.cloud.environment.servicebinding.api.ServiceBinding; import java.io.ByteArrayInputStream; import java.io.InputStream; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -30,10 +33,15 @@ import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; import software.amazon.awssdk.services.s3.model.DeleteObjectResponse; +import software.amazon.awssdk.services.s3.model.DeleteObjectsRequest; +import software.amazon.awssdk.services.s3.model.DeleteObjectsResponse; import software.amazon.awssdk.services.s3.model.GetObjectRequest; import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; +import software.amazon.awssdk.services.s3.model.ListObjectsV2Response; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectResponse; +import software.amazon.awssdk.services.s3.model.S3Object; class AWSClientTest { ExecutorService executor = Executors.newCachedThreadPool(); @@ -225,6 +233,70 @@ void testDeleteContentThrowsOnUnsuccessfulResponse() assertInstanceOf(ObjectStoreServiceException.class, thrown.getCause()); } + @Test + void testDeleteContentByPrefix() throws Exception { + AWSClient awsClient = new AWSClient(getDummyBinding(), executor); + + S3Client mockS3Client = mock(S3Client.class); + + S3Object obj1 = S3Object.builder().key("prefix/file1.txt").build(); + S3Object obj2 = S3Object.builder().key("prefix/file2.txt").build(); + + ListObjectsV2Response listResponse = mock(ListObjectsV2Response.class); + when(listResponse.contents()).thenReturn(List.of(obj1, obj2)); + when(listResponse.isTruncated()).thenReturn(false); + when(mockS3Client.listObjectsV2(any(ListObjectsV2Request.class))).thenReturn(listResponse); + + DeleteObjectsResponse deleteResponse = mock(DeleteObjectsResponse.class); + when(deleteResponse.hasErrors()).thenReturn(false); + when(deleteResponse.errors()).thenReturn(Collections.emptyList()); + when(mockS3Client.deleteObjects(any(DeleteObjectsRequest.class))).thenReturn(deleteResponse); + + var field = AWSClient.class.getDeclaredField("s3Client"); + field.setAccessible(true); + field.set(awsClient, mockS3Client); + + awsClient.deleteContentByPrefix("prefix/").get(); + + verify(mockS3Client).deleteObjects(any(DeleteObjectsRequest.class)); + } + + @Test + void testDeleteContentByPrefixEmptyList() throws Exception { + AWSClient awsClient = new AWSClient(getDummyBinding(), executor); + + S3Client mockS3Client = mock(S3Client.class); + + ListObjectsV2Response listResponse = mock(ListObjectsV2Response.class); + when(listResponse.contents()).thenReturn(Collections.emptyList()); + when(listResponse.isTruncated()).thenReturn(false); + when(mockS3Client.listObjectsV2(any(ListObjectsV2Request.class))).thenReturn(listResponse); + + var field = AWSClient.class.getDeclaredField("s3Client"); + field.setAccessible(true); + field.set(awsClient, mockS3Client); + + assertDoesNotThrow(() -> awsClient.deleteContentByPrefix("prefix/").get()); + } + + @Test + void testDeleteContentByPrefixThrowsOnRuntimeException() throws Exception { + AWSClient awsClient = new AWSClient(getDummyBinding(), executor); + + S3Client mockS3Client = mock(S3Client.class); + when(mockS3Client.listObjectsV2(any(ListObjectsV2Request.class))) + .thenThrow(new RuntimeException("Simulated failure")); + + var field = AWSClient.class.getDeclaredField("s3Client"); + field.setAccessible(true); + field.set(awsClient, mockS3Client); + + ExecutionException thrown = + assertThrows( + ExecutionException.class, () -> awsClient.deleteContentByPrefix("prefix/").get()); + assertInstanceOf(ObjectStoreServiceException.class, thrown.getCause()); + } + private ServiceBinding getDummyBinding() { ServiceBinding binding = mock(ServiceBinding.class); HashMap creds = new HashMap<>(); diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java index 9b7100468..94fd262de 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java @@ -7,13 +7,17 @@ import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; +import com.azure.core.http.rest.PagedIterable; import com.azure.storage.blob.BlobClient; import com.azure.storage.blob.BlobContainerClient; +import com.azure.storage.blob.models.BlobItem; +import com.azure.storage.blob.models.ListBlobsOptions; import com.azure.storage.blob.specialized.BlobOutputStream; import com.azure.storage.blob.specialized.BlockBlobClient; import com.sap.cds.feature.attachments.oss.handler.ObjectStoreServiceException; import java.io.IOException; import java.io.InputStream; +import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -189,4 +193,59 @@ void testReadContentThrowsOnRuntimeException() assertThrows(ExecutionException.class, () -> azureClient.readContent("file.txt").get()); assertInstanceOf(ObjectStoreServiceException.class, thrown.getCause()); } + + @Test + void testDeleteContentByPrefix() + throws NoSuchFieldException, IllegalAccessException, InterruptedException, + ExecutionException { + AzureClient azureClient = mock(AzureClient.class, CALLS_REAL_METHODS); + + BlobContainerClient mockContainer = mock(BlobContainerClient.class); + BlobClient mockBlobClient = mock(BlobClient.class); + + var field = AzureClient.class.getDeclaredField("blobContainerClient"); + field.setAccessible(true); + field.set(azureClient, mockContainer); + var executorField = AzureClient.class.getDeclaredField("executor"); + executorField.setAccessible(true); + executorField.set(azureClient, executor); + + BlobItem item1 = mock(BlobItem.class); + when(item1.getName()).thenReturn("prefix/file1.txt"); + BlobItem item2 = mock(BlobItem.class); + when(item2.getName()).thenReturn("prefix/file2.txt"); + + @SuppressWarnings("unchecked") + PagedIterable pagedIterable = mock(PagedIterable.class); + when(pagedIterable.iterator()).thenReturn(List.of(item1, item2).iterator()); + when(mockContainer.listBlobs(any(ListBlobsOptions.class), isNull())).thenReturn(pagedIterable); + when(mockContainer.getBlobClient(anyString())).thenReturn(mockBlobClient); + + azureClient.deleteContentByPrefix("prefix/").get(); + + verify(mockBlobClient, times(2)).delete(); + } + + @Test + void testDeleteContentByPrefixThrowsOnRuntimeException() + throws NoSuchFieldException, IllegalAccessException { + AzureClient azureClient = mock(AzureClient.class, CALLS_REAL_METHODS); + + BlobContainerClient mockContainer = mock(BlobContainerClient.class); + + var field = AzureClient.class.getDeclaredField("blobContainerClient"); + field.setAccessible(true); + field.set(azureClient, mockContainer); + var executorField = AzureClient.class.getDeclaredField("executor"); + executorField.setAccessible(true); + executorField.set(azureClient, executor); + + when(mockContainer.listBlobs(any(ListBlobsOptions.class), isNull())) + .thenThrow(new RuntimeException("Simulated failure")); + + ExecutionException thrown = + assertThrows( + ExecutionException.class, () -> azureClient.deleteContentByPrefix("prefix/").get()); + assertInstanceOf(ObjectStoreServiceException.class, thrown.getCause()); + } } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java index f4c2fd188..dbc1cc3fb 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java @@ -272,4 +272,65 @@ void testReadContentThrowsOnRuntimeException() assertThrows(ExecutionException.class, () -> googleClient.readContent("file.txt").get()); assertInstanceOf(ObjectStoreServiceException.class, thrown.getCause()); } + + @Test + void testDeleteContentByPrefix() + throws NoSuchFieldException, IllegalAccessException, InterruptedException, + ExecutionException { + GoogleClient googleClient = mock(GoogleClient.class, CALLS_REAL_METHODS); + + Storage mockStorage = mock(Storage.class); + Page mockPage = mock(Page.class); + Blob mockBlob1 = mock(Blob.class); + when(mockBlob1.getName()).thenReturn("prefix/file1.txt"); + when(mockBlob1.getGeneration()).thenReturn(1L); + Blob mockBlob2 = mock(Blob.class); + when(mockBlob2.getName()).thenReturn("prefix/file2.txt"); + when(mockBlob2.getGeneration()).thenReturn(2L); + + Iterator blobIterator = java.util.List.of(mockBlob1, mockBlob2).iterator(); + when(mockPage.iterateAll()).thenReturn(() -> blobIterator); + when(mockStorage.list(anyString(), any(), any())).thenReturn(mockPage); + when(mockStorage.delete(any(BlobId.class))).thenReturn(true); + + var field = GoogleClient.class.getDeclaredField("storage"); + field.setAccessible(true); + field.set(googleClient, mockStorage); + var executorField = GoogleClient.class.getDeclaredField("executor"); + executorField.setAccessible(true); + executorField.set(googleClient, executor); + var bucketField = GoogleClient.class.getDeclaredField("bucketName"); + bucketField.setAccessible(true); + bucketField.set(googleClient, "my-bucket"); + + googleClient.deleteContentByPrefix("prefix/").get(); + + verify(mockStorage, times(2)).delete(any(BlobId.class)); + } + + @Test + void testDeleteContentByPrefixThrowsOnRuntimeException() + throws NoSuchFieldException, IllegalAccessException { + GoogleClient googleClient = mock(GoogleClient.class, CALLS_REAL_METHODS); + + Storage mockStorage = mock(Storage.class); + when(mockStorage.list(anyString(), any(), any())) + .thenThrow(new RuntimeException("Simulated failure")); + + var field = GoogleClient.class.getDeclaredField("storage"); + field.setAccessible(true); + field.set(googleClient, mockStorage); + var executorField = GoogleClient.class.getDeclaredField("executor"); + executorField.setAccessible(true); + executorField.set(googleClient, executor); + var bucketField = GoogleClient.class.getDeclaredField("bucketName"); + bucketField.setAccessible(true); + bucketField.set(googleClient, "my-bucket"); + + ExecutionException thrown = + assertThrows( + ExecutionException.class, + () -> googleClient.deleteContentByPrefix("prefix/").get()); + assertInstanceOf(ObjectStoreServiceException.class, thrown.getCause()); + } } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/OSClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/OSClientTest.java new file mode 100644 index 000000000..48adf9f63 --- /dev/null +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/OSClientTest.java @@ -0,0 +1,40 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.oss.client; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.concurrent.ExecutionException; +import org.junit.jupiter.api.Test; + +class OSClientTest { + + @Test + void testDefaultDeleteContentByPrefixThrowsUnsupportedOperationException() { + OSClient client = + new OSClient() { + @Override + public java.util.concurrent.Future uploadContent( + java.io.InputStream content, String completeFileName, String contentType) { + return null; + } + + @Override + public java.util.concurrent.Future deleteContent(String completeFileName) { + return null; + } + + @Override + public java.util.concurrent.Future readContent( + String completeFileName) { + return null; + } + }; + + ExecutionException thrown = + assertThrows(ExecutionException.class, () -> client.deleteContentByPrefix("prefix/").get()); + assertInstanceOf(UnsupportedOperationException.class, thrown.getCause()); + } +} diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java index a364aa7d7..2318424f1 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java @@ -4,7 +4,11 @@ package com.sap.cds.feature.attachments.oss.configuration; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import com.sap.cds.feature.attachments.oss.handler.OSSAttachmentsServiceHandler; import com.sap.cds.services.environment.CdsEnvironment; @@ -48,4 +52,50 @@ void testEventHandlersRegistersOSSHandler() { // Assert: OSSAttachmentsServiceHandler should be registered verify(configurer).eventHandler(any(OSSAttachmentsServiceHandler.class)); } + + @Test + void testEventHandlersRegistersCleanupHandlerWhenMultitenancyShared() { + Registration registration = new Registration(); + CdsRuntimeConfigurer configurer = mock(CdsRuntimeConfigurer.class); + CdsRuntime cdsRuntime = mock(CdsRuntime.class); + CdsEnvironment environment = mock(CdsEnvironment.class); + ServiceBinding binding = mock(ServiceBinding.class); + + Map credentials = new HashMap<>(); + credentials.put("host", "aws.example.com"); + credentials.put("region", "us-east-1"); + credentials.put("access_key_id", "test-access-key"); + credentials.put("secret_access_key", "test-secret-key"); + credentials.put("bucket", "test-bucket"); + + when(configurer.getCdsRuntime()).thenReturn(cdsRuntime); + when(cdsRuntime.getEnvironment()).thenReturn(environment); + when(binding.getServiceName()).thenReturn(Optional.of("objectstore")); + when(binding.getCredentials()).thenReturn(credentials); + when(environment.getServiceBindings()).thenReturn(Stream.of(binding)); + when(environment.getProperty("cds.multitenancy.enabled", Boolean.class, Boolean.FALSE)) + .thenReturn(Boolean.TRUE); + when(environment.getProperty("cds.attachments.objectStore.kind", String.class, null)) + .thenReturn("shared"); + + registration.eventHandlers(configurer); + + verify(configurer, times(2)).eventHandler(any()); + } + + @Test + void testEventHandlersNoBindingDoesNotRegister() { + Registration registration = new Registration(); + CdsRuntimeConfigurer configurer = mock(CdsRuntimeConfigurer.class); + CdsRuntime cdsRuntime = mock(CdsRuntime.class); + CdsEnvironment environment = mock(CdsEnvironment.class); + + when(configurer.getCdsRuntime()).thenReturn(cdsRuntime); + when(cdsRuntime.getEnvironment()).thenReturn(environment); + when(environment.getServiceBindings()).thenReturn(Stream.empty()); + + registration.eventHandlers(configurer); + + verify(configurer, never()).eventHandler(any()); + } } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java index 62598354c..6a066586c 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java @@ -19,6 +19,7 @@ import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentRestoreEventContext; import com.sap.cds.reflect.CdsEntity; import com.sap.cds.services.ServiceException; +import com.sap.cds.services.request.UserInfo; import com.sap.cloud.environment.servicebinding.api.ServiceBinding; import java.io.ByteArrayInputStream; import java.io.InputStream; @@ -378,4 +379,344 @@ void testReadAttachmentHandlesInterruptedException() assertThrows(ServiceException.class, () -> handler.readAttachment(context)); verify(context).setCompleted(); } + + @Test + void testCreateAttachmentWithMultitenancyBuildsObjectKey() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = + mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); + + var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); + osClientField.setAccessible(true); + osClientField.set(handler, mockOsClient); + var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); + mtField.setAccessible(true); + mtField.set(handler, true); + var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); + kindField.setAccessible(true); + kindField.set(handler, "shared"); + + AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); + MediaData mockMediaData = mock(MediaData.class); + CdsEntity mockEntity = mock(CdsEntity.class); + UserInfo userInfo = mock(UserInfo.class); + + when(context.getAttachmentEntity()).thenReturn(mockEntity); + when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getAttachmentIds()).thenReturn(java.util.Map.of("ID", "content123")); + when(context.getData()).thenReturn(mockMediaData); + when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("test".getBytes())); + when(mockMediaData.getMimeType()).thenReturn("text/plain"); + when(mockMediaData.getFileName()).thenReturn("file.txt"); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.getTenant()).thenReturn("myTenant"); + + when(mockOsClient.uploadContent(any(), anyString(), anyString())) + .thenReturn(CompletableFuture.completedFuture(null)); + + handler.createAttachment(context); + + // Verify the object key includes tenant prefix + verify(mockOsClient).uploadContent(any(), org.mockito.ArgumentMatchers.eq("myTenant/content123"), anyString()); + } + + @Test + void testMultitenancyWithNullTenantThrows() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = + mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); + + var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); + osClientField.setAccessible(true); + osClientField.set(handler, mockOsClient); + var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); + mtField.setAccessible(true); + mtField.set(handler, true); + var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); + kindField.setAccessible(true); + kindField.set(handler, "shared"); + + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + UserInfo userInfo = mock(UserInfo.class); + + when(context.getContentId()).thenReturn("content123"); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.getTenant()).thenReturn(null); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testValidateTenantIdWithSlashThrows() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = + mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); + + var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); + osClientField.setAccessible(true); + osClientField.set(handler, mockOsClient); + var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); + mtField.setAccessible(true); + mtField.set(handler, true); + var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); + kindField.setAccessible(true); + kindField.set(handler, "shared"); + + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + UserInfo userInfo = mock(UserInfo.class); + + when(context.getContentId()).thenReturn("content123"); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.getTenant()).thenReturn("tenant/evil"); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testValidateTenantIdWithBackslashThrows() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = + mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); + + var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); + osClientField.setAccessible(true); + osClientField.set(handler, mockOsClient); + var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); + mtField.setAccessible(true); + mtField.set(handler, true); + var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); + kindField.setAccessible(true); + kindField.set(handler, "shared"); + + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + UserInfo userInfo = mock(UserInfo.class); + + when(context.getContentId()).thenReturn("content123"); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.getTenant()).thenReturn("tenant\\evil"); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testValidateTenantIdWithDotsThrows() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = + mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); + + var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); + osClientField.setAccessible(true); + osClientField.set(handler, mockOsClient); + var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); + mtField.setAccessible(true); + mtField.set(handler, true); + var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); + kindField.setAccessible(true); + kindField.set(handler, "shared"); + + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + UserInfo userInfo = mock(UserInfo.class); + + when(context.getContentId()).thenReturn("content123"); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.getTenant()).thenReturn("..evil"); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testValidateEmptyTenantIdThrows() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = + mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); + + var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); + osClientField.setAccessible(true); + osClientField.set(handler, mockOsClient); + var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); + mtField.setAccessible(true); + mtField.set(handler, true); + var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); + kindField.setAccessible(true); + kindField.set(handler, "shared"); + + // Need to mock tenant as empty string but not null (null triggers different path) + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + UserInfo userInfo = mock(UserInfo.class); + + when(context.getContentId()).thenReturn("content123"); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.getTenant()).thenReturn(""); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testValidateContentIdWithSlashThrows() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = + mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); + + var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); + osClientField.setAccessible(true); + osClientField.set(handler, mockOsClient); + var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); + mtField.setAccessible(true); + mtField.set(handler, true); + var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); + kindField.setAccessible(true); + kindField.set(handler, "shared"); + + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + UserInfo userInfo = mock(UserInfo.class); + + when(context.getContentId()).thenReturn("content/evil"); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.getTenant()).thenReturn("validTenant"); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testValidateContentIdWithNullThrows() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = + mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); + + var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); + osClientField.setAccessible(true); + osClientField.set(handler, mockOsClient); + var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); + mtField.setAccessible(true); + mtField.set(handler, true); + var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); + kindField.setAccessible(true); + kindField.set(handler, "shared"); + + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + UserInfo userInfo = mock(UserInfo.class); + + when(context.getContentId()).thenReturn(null); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.getTenant()).thenReturn("validTenant"); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testValidateContentIdWithBackslashThrows() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = + mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); + + var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); + osClientField.setAccessible(true); + osClientField.set(handler, mockOsClient); + var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); + mtField.setAccessible(true); + mtField.set(handler, true); + var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); + kindField.setAccessible(true); + kindField.set(handler, "shared"); + + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + UserInfo userInfo = mock(UserInfo.class); + + when(context.getContentId()).thenReturn("content\\evil"); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.getTenant()).thenReturn("validTenant"); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testValidateContentIdWithDotsThrows() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = + mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); + + var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); + osClientField.setAccessible(true); + osClientField.set(handler, mockOsClient); + var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); + mtField.setAccessible(true); + mtField.set(handler, true); + var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); + kindField.setAccessible(true); + kindField.set(handler, "shared"); + + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + UserInfo userInfo = mock(UserInfo.class); + + when(context.getContentId()).thenReturn("..evil"); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.getTenant()).thenReturn("validTenant"); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testValidateEmptyContentIdThrows() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = + mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); + + var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); + osClientField.setAccessible(true); + osClientField.set(handler, mockOsClient); + var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); + mtField.setAccessible(true); + mtField.set(handler, true); + var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); + kindField.setAccessible(true); + kindField.set(handler, "shared"); + + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + UserInfo userInfo = mock(UserInfo.class); + + when(context.getContentId()).thenReturn(""); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.getTenant()).thenReturn("validTenant"); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testReadAttachmentHandlesExecutionException() + throws NoSuchFieldException, IllegalAccessException, InterruptedException, + ExecutionException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = + mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + + var field = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); + field.setAccessible(true); + field.set(handler, mockOsClient); + + String contentId = "doc123"; + MediaData mockMediaData = mock(MediaData.class); + + when(context.getContentId()).thenReturn(contentId); + when(context.getData()).thenReturn(mockMediaData); + + @SuppressWarnings("unchecked") + CompletableFuture future = mock(CompletableFuture.class); + when(mockOsClient.readContent(contentId)).thenReturn(future); + when(future.get()).thenThrow(new ExecutionException("failed", new RuntimeException())); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + verify(context).setCompleted(); + } } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java new file mode 100644 index 000000000..f92e76cdf --- /dev/null +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java @@ -0,0 +1,62 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.oss.handler; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.sap.cds.feature.attachments.oss.client.OSClient; +import com.sap.cds.services.mt.UnsubscribeEventContext; +import java.util.concurrent.CompletableFuture; +import org.junit.jupiter.api.Test; + +class TenantCleanupHandlerTest { + + @Test + void testCleanupTenantDataCallsDeleteByPrefix() throws Exception { + OSClient mockOsClient = mock(OSClient.class); + UnsubscribeEventContext context = mock(UnsubscribeEventContext.class); + when(context.getTenant()).thenReturn("tenant1"); + when(mockOsClient.deleteContentByPrefix("tenant1/")) + .thenReturn(CompletableFuture.completedFuture(null)); + + TenantCleanupHandler handler = new TenantCleanupHandler(mockOsClient); + handler.cleanupTenantData(context); + + verify(mockOsClient).deleteContentByPrefix("tenant1/"); + } + + @Test + void testCleanupTenantDataHandlesInterruptedException() throws Exception { + OSClient mockOsClient = mock(OSClient.class); + UnsubscribeEventContext context = mock(UnsubscribeEventContext.class); + when(context.getTenant()).thenReturn("tenant2"); + + @SuppressWarnings("unchecked") + CompletableFuture future = mock(CompletableFuture.class); + when(mockOsClient.deleteContentByPrefix("tenant2/")).thenReturn(future); + when(future.get()).thenThrow(new InterruptedException("interrupted")); + + TenantCleanupHandler handler = new TenantCleanupHandler(mockOsClient); + handler.cleanupTenantData(context); + + verify(mockOsClient).deleteContentByPrefix("tenant2/"); + } + + @Test + void testCleanupTenantDataHandlesRuntimeException() throws Exception { + OSClient mockOsClient = mock(OSClient.class); + UnsubscribeEventContext context = mock(UnsubscribeEventContext.class); + when(context.getTenant()).thenReturn("tenant3"); + + when(mockOsClient.deleteContentByPrefix("tenant3/")) + .thenReturn(CompletableFuture.failedFuture(new RuntimeException("fail"))); + + TenantCleanupHandler handler = new TenantCleanupHandler(mockOsClient); + handler.cleanupTenantData(context); + + verify(mockOsClient).deleteContentByPrefix("tenant3/"); + } +} From 33ea3cfc8b72de41f0000b5f9c1c2d87d38dfb91 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Tue, 31 Mar 2026 10:53:07 +0200 Subject: [PATCH 10/18] spotless --- .../oss/client/AzureClientTest.java | 4 ++- .../oss/client/GoogleClientTest.java | 7 +++-- .../OSSAttachmentsServiceHandlerTest.java | 31 ++++++++----------- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java index 94fd262de..7f6fe6de2 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java @@ -196,7 +196,9 @@ void testReadContentThrowsOnRuntimeException() @Test void testDeleteContentByPrefix() - throws NoSuchFieldException, IllegalAccessException, InterruptedException, + throws NoSuchFieldException, + IllegalAccessException, + InterruptedException, ExecutionException { AzureClient azureClient = mock(AzureClient.class, CALLS_REAL_METHODS); diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java index dbc1cc3fb..51147491d 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java @@ -275,7 +275,9 @@ void testReadContentThrowsOnRuntimeException() @Test void testDeleteContentByPrefix() - throws NoSuchFieldException, IllegalAccessException, InterruptedException, + throws NoSuchFieldException, + IllegalAccessException, + InterruptedException, ExecutionException { GoogleClient googleClient = mock(GoogleClient.class, CALLS_REAL_METHODS); @@ -329,8 +331,7 @@ void testDeleteContentByPrefixThrowsOnRuntimeException() ExecutionException thrown = assertThrows( - ExecutionException.class, - () -> googleClient.deleteContentByPrefix("prefix/").get()); + ExecutionException.class, () -> googleClient.deleteContentByPrefix("prefix/").get()); assertInstanceOf(ObjectStoreServiceException.class, thrown.getCause()); } } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java index 6a066586c..c4350e2e7 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java @@ -418,12 +418,12 @@ void testCreateAttachmentWithMultitenancyBuildsObjectKey() handler.createAttachment(context); // Verify the object key includes tenant prefix - verify(mockOsClient).uploadContent(any(), org.mockito.ArgumentMatchers.eq("myTenant/content123"), anyString()); + verify(mockOsClient) + .uploadContent(any(), org.mockito.ArgumentMatchers.eq("myTenant/content123"), anyString()); } @Test - void testMultitenancyWithNullTenantThrows() - throws NoSuchFieldException, IllegalAccessException { + void testMultitenancyWithNullTenantThrows() throws NoSuchFieldException, IllegalAccessException { OSClient mockOsClient = mock(OSClient.class); OSSAttachmentsServiceHandler handler = mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); @@ -449,8 +449,7 @@ void testMultitenancyWithNullTenantThrows() } @Test - void testValidateTenantIdWithSlashThrows() - throws NoSuchFieldException, IllegalAccessException { + void testValidateTenantIdWithSlashThrows() throws NoSuchFieldException, IllegalAccessException { OSClient mockOsClient = mock(OSClient.class); OSSAttachmentsServiceHandler handler = mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); @@ -503,8 +502,7 @@ void testValidateTenantIdWithBackslashThrows() } @Test - void testValidateTenantIdWithDotsThrows() - throws NoSuchFieldException, IllegalAccessException { + void testValidateTenantIdWithDotsThrows() throws NoSuchFieldException, IllegalAccessException { OSClient mockOsClient = mock(OSClient.class); OSSAttachmentsServiceHandler handler = mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); @@ -530,8 +528,7 @@ void testValidateTenantIdWithDotsThrows() } @Test - void testValidateEmptyTenantIdThrows() - throws NoSuchFieldException, IllegalAccessException { + void testValidateEmptyTenantIdThrows() throws NoSuchFieldException, IllegalAccessException { OSClient mockOsClient = mock(OSClient.class); OSSAttachmentsServiceHandler handler = mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); @@ -558,8 +555,7 @@ void testValidateEmptyTenantIdThrows() } @Test - void testValidateContentIdWithSlashThrows() - throws NoSuchFieldException, IllegalAccessException { + void testValidateContentIdWithSlashThrows() throws NoSuchFieldException, IllegalAccessException { OSClient mockOsClient = mock(OSClient.class); OSSAttachmentsServiceHandler handler = mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); @@ -585,8 +581,7 @@ void testValidateContentIdWithSlashThrows() } @Test - void testValidateContentIdWithNullThrows() - throws NoSuchFieldException, IllegalAccessException { + void testValidateContentIdWithNullThrows() throws NoSuchFieldException, IllegalAccessException { OSClient mockOsClient = mock(OSClient.class); OSSAttachmentsServiceHandler handler = mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); @@ -639,8 +634,7 @@ void testValidateContentIdWithBackslashThrows() } @Test - void testValidateContentIdWithDotsThrows() - throws NoSuchFieldException, IllegalAccessException { + void testValidateContentIdWithDotsThrows() throws NoSuchFieldException, IllegalAccessException { OSClient mockOsClient = mock(OSClient.class); OSSAttachmentsServiceHandler handler = mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); @@ -666,8 +660,7 @@ void testValidateContentIdWithDotsThrows() } @Test - void testValidateEmptyContentIdThrows() - throws NoSuchFieldException, IllegalAccessException { + void testValidateEmptyContentIdThrows() throws NoSuchFieldException, IllegalAccessException { OSClient mockOsClient = mock(OSClient.class); OSSAttachmentsServiceHandler handler = mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); @@ -694,7 +687,9 @@ void testValidateEmptyContentIdThrows() @Test void testReadAttachmentHandlesExecutionException() - throws NoSuchFieldException, IllegalAccessException, InterruptedException, + throws NoSuchFieldException, + IllegalAccessException, + InterruptedException, ExecutionException { OSClient mockOsClient = mock(OSClient.class); OSSAttachmentsServiceHandler handler = From 9658edbd05d5dd4da87b5a315727262032597aff Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Tue, 31 Mar 2026 11:11:00 +0200 Subject: [PATCH 11/18] fix tests --- integration-tests/generic/pom.xml | 3 +- integration-tests/srv/pom.xml | 121 +++ .../integrationtests/Application.java | 15 + .../integrationtests/constants/Profiles.java | 15 + .../testhandler/EventContextHolder.java | 8 + .../testhandler/TestPersistenceHandler.java | 49 + .../TestPluginAttachmentsServiceHandler.java | 120 +++ .../srv/src/main/resources/application.yaml | 4 + .../srv/src/main/resources/banner.txt | 9 + .../src/main/resources/messages.properties | 1 + .../resources/spotbugs-exclusion-filter.xml | 26 + .../common/JsonToCapMapperTestHelper.java | 21 + .../common/MalwareScanResultProvider.java | 27 + .../common/MockHttpRequestHelper.java | 167 +++ .../common/TableDataDeleter.java | 21 + .../DraftOdataRequestValidationBase.java | 973 ++++++++++++++++++ ...aRequestValidationWithTestHandlerTest.java | 288 ++++++ ...thoutTestHandlerAndMalwareScannerTest.java | 114 ++ ...stHandlerAndWithoutMalwareScannerTest.java | 90 ++ .../MediaValidatedAttachmentsDraftTest.java | 159 +++ ...tedAttachmentsSizeValidationDraftTest.java | 202 ++++ ...MediaValidatedAttachmentsNonDraftTest.java | 296 ++++++ .../OdataRequestValidationBase.java | 884 ++++++++++++++++ ...aRequestValidationWithTestHandlerTest.java | 245 +++++ ...thoutTestHandlerAndMalwareScannerTest.java | 138 +++ ...stHandlerAndWithoutMalwareScannerTest.java | 116 +++ ...mitedAttachmentValidationNonDraftTest.java | 175 ++++ .../helper/AttachmentsBuilder.java | 33 + .../helper/AttachmentsEntityBuilder.java | 31 + .../helper/ItemEntityBuilder.java | 44 + .../helper/RootEntityBuilder.java | 50 + .../TestPersistenceHandlerTest.java | 116 +++ ...stPluginAttachmentsServiceHandlerTest.java | 266 +++++ .../srv/src/test/resources/application.yaml | 15 + .../srv/src/test/resources/logback-test.xml | 21 + .../srv/src/test/resources/xsuaa-env.json | 30 + integration-tests/srv/test-service.cds | 27 + 37 files changed, 4919 insertions(+), 1 deletion(-) create mode 100644 integration-tests/srv/pom.xml create mode 100644 integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/Application.java create mode 100644 integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/constants/Profiles.java create mode 100644 integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/EventContextHolder.java create mode 100644 integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandler.java create mode 100644 integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandler.java create mode 100644 integration-tests/srv/src/main/resources/application.yaml create mode 100644 integration-tests/srv/src/main/resources/banner.txt create mode 100644 integration-tests/srv/src/main/resources/messages.properties create mode 100644 integration-tests/srv/src/main/resources/spotbugs-exclusion-filter.xml create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/JsonToCapMapperTestHelper.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MalwareScanResultProvider.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MockHttpRequestHelper.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/TableDataDeleter.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationBase.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithTestHandlerTest.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java 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/draftservice/SizeLimitedAttachmentsSizeValidationDraftTest.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationBase.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithTestHandlerTest.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/SizeLimitedAttachmentValidationNonDraftTest.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsBuilder.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsEntityBuilder.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/ItemEntityBuilder.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/RootEntityBuilder.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandlerTest.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java create mode 100644 integration-tests/srv/src/test/resources/application.yaml create mode 100644 integration-tests/srv/src/test/resources/logback-test.xml create mode 100644 integration-tests/srv/src/test/resources/xsuaa-env.json create mode 100644 integration-tests/srv/test-service.cds diff --git a/integration-tests/generic/pom.xml b/integration-tests/generic/pom.xml index 265826f00..d47623f06 100644 --- a/integration-tests/generic/pom.xml +++ b/integration-tests/generic/pom.xml @@ -97,7 +97,7 @@ ${project.basedir}/.. build --for java - deploy --to h2 --dry > + deploy --to h2 --dry db generic > "${project.basedir}/src/main/resources/schema.sql" @@ -110,6 +110,7 @@ ${project.basedir}/.. + ${project.basedir}/src/main/resources/edmx/csn.json ${generation-package}.integration.test.cds4j diff --git a/integration-tests/srv/pom.xml b/integration-tests/srv/pom.xml new file mode 100644 index 000000000..6a5b6eada --- /dev/null +++ b/integration-tests/srv/pom.xml @@ -0,0 +1,121 @@ + + + 4.0.0 + + + com.sap.cds.integration-tests + cds-feature-attachments-integration-tests-parent + ${revision} + + + cds-feature-attachments-integration-tests-srv + jar + + Integration Tests - Service + + + com.sap.cds.feature.attachments.generated + + + + + + + com.sap.cds + cds-starter-spring-boot + + + + + com.sap.cds + cds-adapter-odata-v4 + runtime + + + + org.springframework.security + spring-security-test + test + + + + org.springframework.boot + spring-boot-starter-test + test + + + + com.h2database + h2 + test + + + + org.wiremock.integrations + wiremock-spring-boot + test + + + + + + + + com.sap.cds + cds-maven-plugin + + + cds.clean + + clean + + + + + cds.install-node + + install-node + + + + + cds.resolve + + resolve + + + ${project.basedir}/.. + + + + + cds.build + + cds + + + ${project.basedir}/.. + + build --for java + deploy --to h2 --dry > + "${project.basedir}/src/main/resources/schema.sql" + + + + + + cds.generate + + generate + + + ${project.basedir}/.. + ${generation-package}.integration.test.cds4j + + + + + + + + diff --git a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/Application.java b/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/Application.java new file mode 100644 index 000000000..b0b23079c --- /dev/null +++ b/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/Application.java @@ -0,0 +1,15 @@ +/* + * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/constants/Profiles.java b/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/constants/Profiles.java new file mode 100644 index 000000000..8ac4f8f3e --- /dev/null +++ b/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/constants/Profiles.java @@ -0,0 +1,15 @@ +/* + * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.constants; + +public final class Profiles { + + public static final String TEST_HANDLER_ENABLED = "test-handler-enabled"; + public static final String TEST_HANDLER_DISABLED = "test-handler-disabled"; + public static final String MALWARE_SCAN_ENABLED = "malware-scan-enabled"; + + private Profiles() { + // prevent instantiation + } +} diff --git a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/EventContextHolder.java b/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/EventContextHolder.java new file mode 100644 index 000000000..280227a89 --- /dev/null +++ b/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/EventContextHolder.java @@ -0,0 +1,8 @@ +/* + * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.testhandler; + +import com.sap.cds.services.EventContext; + +public record EventContextHolder(String event, EventContext context) {} diff --git a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandler.java b/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandler.java new file mode 100644 index 000000000..207f49183 --- /dev/null +++ b/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandler.java @@ -0,0 +1,49 @@ +/* + * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.testhandler; + +import static com.sap.cds.services.cds.CqnService.EVENT_CREATE; +import static com.sap.cds.services.cds.CqnService.EVENT_UPDATE; + +import com.sap.cds.services.ServiceException; +import com.sap.cds.services.handler.EventHandler; +import com.sap.cds.services.handler.annotations.Before; +import com.sap.cds.services.handler.annotations.ServiceName; +import com.sap.cds.services.persistence.PersistenceService; +import org.springframework.stereotype.Component; + +@ServiceName(value = "*", type = PersistenceService.class) +@Component +public class TestPersistenceHandler implements EventHandler { + + private volatile boolean throwExceptionOnUpdate = false; + private volatile boolean throwExceptionOnCreate = false; + + @Before(event = EVENT_UPDATE) + public void throwExceptionOnUpdate() { + if (throwExceptionOnUpdate) { + throw new ServiceException("Exception on update"); + } + } + + @Before(event = EVENT_CREATE) + public void throwExceptionOnCreate() { + if (throwExceptionOnCreate) { + throw new ServiceException("Exception on create"); + } + } + + public void reset() { + throwExceptionOnUpdate = false; + throwExceptionOnCreate = false; + } + + public void setThrowExceptionOnUpdate(boolean throwExceptionOnUpdate) { + this.throwExceptionOnUpdate = throwExceptionOnUpdate; + } + + public void setThrowExceptionOnCreate(boolean throwExceptionOnCreate) { + this.throwExceptionOnCreate = throwExceptionOnCreate; + } +} diff --git a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandler.java b/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandler.java new file mode 100644 index 000000000..341002905 --- /dev/null +++ b/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandler.java @@ -0,0 +1,120 @@ +/* + * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.testhandler; + +import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.StatusCode; +import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; +import com.sap.cds.feature.attachments.service.AttachmentService; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentCreateEventContext; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentMarkAsDeletedEventContext; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentReadEventContext; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentRestoreEventContext; +import com.sap.cds.services.handler.EventHandler; +import com.sap.cds.services.handler.annotations.On; +import com.sap.cds.services.handler.annotations.ServiceName; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.Marker; +import org.slf4j.MarkerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@ServiceName(value = "*", type = AttachmentService.class) +@Profile(Profiles.TEST_HANDLER_ENABLED) +@Component +public class TestPluginAttachmentsServiceHandler implements EventHandler { + + private static final Marker marker = MarkerFactory.getMarker("DUMMY_HANDLER"); + private static final Logger logger = + LoggerFactory.getLogger(TestPluginAttachmentsServiceHandler.class); + + private static final Map documents = new ConcurrentHashMap<>(); + private static final List eventContextHolder = + Collections.synchronizedList(new ArrayList<>()); + + @On(event = AttachmentService.EVENT_CREATE_ATTACHMENT) + public void createAttachment(AttachmentCreateEventContext context) throws IOException { + logger.info(marker, "CREATE Attachment called in dummy handler"); + var contentId = UUID.randomUUID().toString(); + documents.put(contentId, context.getData().getContent().readAllBytes()); + context.setContentId(contentId); + context.getData().setStatus(StatusCode.CLEAN); + context.getData().setScannedAt(Instant.now()); + context.setCompleted(); + eventContextHolder.add( + new EventContextHolder(AttachmentService.EVENT_CREATE_ATTACHMENT, context)); + } + + @On(event = AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED) + public void markAttachmentAsDeleted(AttachmentMarkAsDeletedEventContext context) { + logger.info( + marker, + "DELETE Attachment called in dummy handler for document id {}", + context.getContentId()); + context.setCompleted(); + eventContextHolder.add( + new EventContextHolder(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED, context)); + } + + @On(event = AttachmentService.EVENT_READ_ATTACHMENT) + public void readAttachment(AttachmentReadEventContext context) { + logger.info( + marker, + "READ Attachment called in dummy handler for content id {}", + context.getContentId()); + var contentId = context.getContentId(); + var content = contentId != null ? documents.get(contentId) : null; + var stream = content != null ? new ByteArrayInputStream(content) : null; + context.getData().setContent(stream); + context.setCompleted(); + eventContextHolder.add( + new EventContextHolder(AttachmentService.EVENT_READ_ATTACHMENT, context)); + } + + @On(event = AttachmentService.EVENT_RESTORE_ATTACHMENT) + public void restoreAttachment(AttachmentRestoreEventContext context) { + logger.info( + marker, + "RESTORE Attachment called in dummy handler for timestamp {}", + context.getRestoreTimestamp()); + context.setCompleted(); + eventContextHolder.add( + new EventContextHolder(AttachmentService.EVENT_RESTORE_ATTACHMENT, context)); + } + + public List getEventContextForEvent(String event) { + var context = eventContextHolder.stream().filter(e -> e.event().equals(event)).toList(); + if (event.equals(AttachmentService.EVENT_CREATE_ATTACHMENT) && !context.isEmpty()) { + context.forEach( + c -> { + var createContext = (AttachmentCreateEventContext) c.context(); + createContext + .getData() + .setContent(new ByteArrayInputStream(documents.get(createContext.getContentId()))); + }); + } + return context; + } + + public List getEventContext() { + return eventContextHolder; + } + + public void clearEventContext() { + eventContextHolder.clear(); + } + + public void clearDocuments() { + documents.clear(); + } +} diff --git a/integration-tests/srv/src/main/resources/application.yaml b/integration-tests/srv/src/main/resources/application.yaml new file mode 100644 index 000000000..68a4a6e63 --- /dev/null +++ b/integration-tests/srv/src/main/resources/application.yaml @@ -0,0 +1,4 @@ +--- +spring: + config.activate.on-profile: default + sql.init.schema-locations: classpath:schema.sql diff --git a/integration-tests/srv/src/main/resources/banner.txt b/integration-tests/srv/src/main/resources/banner.txt new file mode 100644 index 000000000..875e346ff --- /dev/null +++ b/integration-tests/srv/src/main/resources/banner.txt @@ -0,0 +1,9 @@ + __ _ _ _ _ __ + / / /\ | | | | | | | | \ \ + / / / \ | |_ | |_ __ _ ___ | |__ _ __ ___ ___ _ __ | |_ ___ \ \ + < < / /\ \ | __| | __| / _` | / __| | '_ \ | '_ ` _ \ / _ \ | '_ \ | __| / __| > > + \ \ / ____ \ | |_ | |_ | (_| | | (__ | | | | | | | | | | | __/ | | | | | |_ \__ \ / / + \_\ /_/ \_\ \__| \__| \__,_| \___| |_| |_| |_| |_| |_| \___| |_| |_| \__| |___/ /_/ + ================================================================================================= + :: Spring Boot :: ${spring-boot.formatted-version} + diff --git a/integration-tests/srv/src/main/resources/messages.properties b/integration-tests/srv/src/main/resources/messages.properties new file mode 100644 index 000000000..81680eda5 --- /dev/null +++ b/integration-tests/srv/src/main/resources/messages.properties @@ -0,0 +1 @@ +not_clean=Error text for not clean \ No newline at end of file diff --git a/integration-tests/srv/src/main/resources/spotbugs-exclusion-filter.xml b/integration-tests/srv/src/main/resources/spotbugs-exclusion-filter.xml new file mode 100644 index 000000000..b5e184082 --- /dev/null +++ b/integration-tests/srv/src/main/resources/spotbugs-exclusion-filter.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/JsonToCapMapperTestHelper.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/JsonToCapMapperTestHelper.java new file mode 100644 index 000000000..5bc8eb32e --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/JsonToCapMapperTestHelper.java @@ -0,0 +1,21 @@ +/* + * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.common; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sap.cds.CdsData; +import com.sap.cds.Struct; +import java.util.HashMap; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +class JsonToCapMapperTestHelper { + + @Autowired private ObjectMapper objectMapper; + + public CdsData mapResponseToSingleResult(String resultBody) throws Exception { + return Struct.access(objectMapper.readValue(resultBody, HashMap.class)).as(CdsData.class); + } +} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MalwareScanResultProvider.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MalwareScanResultProvider.java new file mode 100644 index 000000000..f36ec06f0 --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MalwareScanResultProvider.java @@ -0,0 +1,27 @@ +/* + * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.common; + +import org.springframework.stereotype.Component; + +@Component +public class MalwareScanResultProvider { + + public String buildMalwareScanResult(boolean malware) { + return """ + { + \t"malwareDetected": %s, + \t"encryptedContentDetected": false, + \t"scanSize": 68, + \t"finding": "Win.Test.EICAR_HDB-1", + \t"mimeType": "text/plain", + \t"SHA256": "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", + \t"extensions": [ + \t\t"txt" + \t] + } + """ + .formatted(malware); + } +} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MockHttpRequestHelper.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MockHttpRequestHelper.java new file mode 100644 index 000000000..426e88f2a --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MockHttpRequestHelper.java @@ -0,0 +1,167 @@ +/* + * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.common; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.sap.cds.CdsData; +import com.sap.cds.Struct; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultMatcher; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +@Component +public class MockHttpRequestHelper { + + public static final String ODATA_BASE_URL = "/odata/v4/"; + public static final String IF_MATCH = "If-Match"; + + @Autowired private JsonToCapMapperTestHelper mapper; + @Autowired private MockMvc mvc; + + private String contentType = MediaType.APPLICATION_JSON.toString(); + private String accept = MediaType.APPLICATION_JSON.toString(); + + public MvcResult executeGet(String url) throws Exception { + MockHttpServletRequestBuilder requestBuilder = + MockMvcRequestBuilders.get(url).contentType(contentType).accept(accept); + return mvc.perform(requestBuilder).andReturn(); + } + + public String executeGetWithSingleODataResponseAndAssertStatus(String url, HttpStatus status) + throws Exception { + var result = executeGet(url); + assertThat(result.getResponse().getStatus()).isEqualTo(status.value()); + return result.getResponse().getContentAsString(); + } + + public T executeGetWithSingleODataResponseAndAssertStatus( + String url, Class resultType, HttpStatus status) throws Exception { + var resultBody = executeGetWithSingleODataResponseAndAssertStatus(url, status); + return Struct.access(mapper.mapResponseToSingleResult(resultBody)).as(resultType); + } + + public MvcResult executePost(String url, String body) throws Exception { + return mvc.perform( + MockMvcRequestBuilders.post(url).contentType(contentType).accept(accept).content(body)) + .andReturn(); + } + + public MvcResult executePatch(String url, String body) throws Exception { + return executePatch(url, body, "*"); + } + + public MvcResult executePatch(String url, String body, String etag) throws Exception { + return mvc.perform( + MockMvcRequestBuilders.patch(url) + .contentType(contentType) + .accept(accept) + .header(IF_MATCH, etag) + .content(body)) + .andReturn(); + } + + public void executePostWithMatcher(String url, String body, ResultMatcher matcher) + throws Exception { + mvc.perform( + MockMvcRequestBuilders.post(url).contentType(contentType).accept(accept).content(body)) + .andExpect(matcher); + } + + public MvcResult executeDelete(String url) throws Exception { + return executeDelete(url, "*"); + } + + public MvcResult executeDelete(String url, String etag) throws Exception { + return mvc.perform( + MockMvcRequestBuilders.delete(url) + .contentType(contentType) + .accept(accept) + .header(IF_MATCH, etag)) + .andReturn(); + } + + public void executeDeleteWithMatcher(String url, ResultMatcher matcher) throws Exception { + executeDeleteWithMatcher(url, "*", matcher); + } + + public void executeDeleteWithMatcher(String url, String etag, ResultMatcher matcher) + throws Exception { + mvc.perform( + MockMvcRequestBuilders.delete(url) + .contentType(contentType) + .accept(accept) + .header(IF_MATCH, etag)) + .andExpect(matcher); + } + + public CdsData executePostWithODataResponseAndAssertStatusCreated(String url, String body) + throws Exception { + return executePostWithODataResponseAndAssertStatus(url, body, HttpStatus.CREATED); + } + + public void executePatchWithODataResponseAndAssertStatusOk(String url, String body) + throws Exception { + executePatchWithODataResponseAndAssertStatus(url, body, HttpStatus.OK); + } + + public CdsData executePostWithODataResponseAndAssertStatus( + String url, String body, HttpStatus status) throws Exception { + MvcResult result = executePost(url, body); + String resultBody = result.getResponse().getContentAsString(); + assertThat(result.getResponse().getStatus()) + .as("Unexpected HTTP status, with response body " + resultBody) + .isEqualTo(status.value()); + return mapper.mapResponseToSingleResult(resultBody); + } + + public void executePatchWithODataResponseAndAssertStatus( + String url, String body, HttpStatus status) throws Exception { + executePatchWithODataResponseAndAssertStatus(url, body, "*", status); + } + + public void executePatchWithODataResponseAndAssertStatus( + String url, String body, String etag, HttpStatus status) throws Exception { + MvcResult result = executePatch(url, body, etag); + String resultBody = result.getResponse().getContentAsString(); + assertThat(result.getResponse().getStatus()) + .as("Unexpected HTTP status, with response body " + resultBody) + .isEqualTo(status.value()); + } + + public void executePutWithMatcher(String url, byte[] body, ResultMatcher matcher) + throws Exception { + executePutWithMatcher(url, body, "*", matcher); + } + + public void executePutWithMatcher(String url, byte[] body, String etag, ResultMatcher matcher) + throws Exception { + mvc.perform( + MockMvcRequestBuilders.put(url) + .contentType(contentType) + .accept(accept) + .header(IF_MATCH, etag) + .content(body)) + .andExpect(matcher); + } + + public void setContentType(MediaType contentType) { + this.contentType = contentType.toString(); + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + public void resetHelper() { + contentType = MediaType.APPLICATION_JSON.toString(); + accept = MediaType.APPLICATION_JSON.toString(); + } +} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/TableDataDeleter.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/TableDataDeleter.java new file mode 100644 index 000000000..a56d44a7c --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/TableDataDeleter.java @@ -0,0 +1,21 @@ +/* + * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.common; + +import com.sap.cds.ql.Delete; +import com.sap.cds.services.persistence.PersistenceService; +import java.util.Arrays; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class TableDataDeleter { + + @Autowired private PersistenceService persistenceService; + + public void deleteData(String... entityNames) { + Arrays.stream(entityNames) + .forEach(entityName -> persistenceService.run(Delete.from(entityName))); + } +} 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 new file mode 100644 index 000000000..843fa2bbc --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationBase.java @@ -0,0 +1,973 @@ +/* + * © 2024-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; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.sap.cds.Struct; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.sap.attachments.Attachments; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testdraftservice.AttachmentEntity; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testdraftservice.DraftRoots; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testdraftservice.DraftRoots_; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testdraftservice.Items; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testdraftservice.TestDraftService_; +import com.sap.cds.feature.attachments.integrationtests.common.MockHttpRequestHelper; +import com.sap.cds.feature.attachments.integrationtests.common.TableDataDeleter; +import com.sap.cds.feature.attachments.integrationtests.testhandler.TestPersistenceHandler; +import com.sap.cds.feature.attachments.integrationtests.testhandler.TestPluginAttachmentsServiceHandler; +import com.sap.cds.feature.attachments.service.AttachmentService; +import com.sap.cds.ql.Select; +import com.sap.cds.ql.StructuredType; +import com.sap.cds.services.persistence.PersistenceService; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpStatus; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultMatcher; + +@SpringBootTest +@AutoConfigureMockMvc +abstract class DraftOdataRequestValidationBase { + + 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; + + @AfterEach + void teardown() { + dataDeleter.deleteData( + DraftRoots_.CDS_NAME, DraftRoots_.CDS_NAME + "_drafts", "cds.outbox.Messages"); + requestHelper.resetHelper(); + clearServiceHandlerContext(); + testPersistenceHandler.reset(); + } + + @Test + void deepCreateWorks() throws Exception { + var testContentAttachment = "testContent attachment"; + var testContentAttachmentEntity = "testContent attachmentEntity"; + + var selectedRoot = deepCreateAndActivate(testContentAttachment, testContentAttachmentEntity); + + assertThat(selectedRoot.getIsActiveEntity()).isTrue(); + + var selectedAttachment = selectedRoot.getItems().get(0).getAttachments().get(0); + var selectedAttachmentEntity = selectedRoot.getItems().get(0).getAttachmentEntities().get(0); + + verifyContentId(selectedAttachment.getContentId(), selectedAttachment.getId()); + assertThat(selectedAttachment.getFileName()).isEqualTo("itemAttachment.txt"); + assertThat(selectedAttachment.getMimeType()).contains("text/plain"); + verifyContent(selectedAttachment.getContent(), testContentAttachment); + verifyContentId(selectedAttachmentEntity.getContentId(), selectedAttachmentEntity.getId()); + assertThat(selectedAttachmentEntity.getFileName()).isEqualTo("itemAttachmentEntity.txt"); + assertThat(selectedAttachmentEntity.getMimeType()).contains("image/jpeg"); + verifyContent(selectedAttachmentEntity.getContent(), testContentAttachmentEntity); + verifyOnlyTwoCreateEvents(testContentAttachment, testContentAttachmentEntity); + } + + @Test + void contentCanBeReadFromDraft() throws Exception { + var testContentAttachment = "testContent attachment"; + var testContentAttachmentEntity = "testContent attachmentEntity"; + + var root = deepCreateAndActivate(testContentAttachment, testContentAttachmentEntity); + + var selectedRoot = selectStoredRootData(root); + assertThat(selectedRoot.getItems().get(0).getAttachments()) + .hasSize(1) + .first() + .satisfies(attachment -> verifyContent(attachment.getContent(), testContentAttachment)); + assertThat(selectedRoot.getItems().get(0).getAttachmentEntities()) + .hasSize(1) + .first() + .satisfies( + attachment -> verifyContent(attachment.getContent(), testContentAttachmentEntity)); + 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"; + + Awaitility.await() + .atMost(60, TimeUnit.SECONDS) + .pollDelay(1, TimeUnit.SECONDS) + .pollInterval(2, TimeUnit.SECONDS) + .until( + () -> { + var attachmentResponse = requestHelper.executeGet(attachmentUrl); + var attachmentEntityResponse = requestHelper.executeGet(attachmentEntityUrl); + var attachmentResponseContent = getResponseContent(attachmentResponse); + var attachmentEntityResponseContent = getResponseContent(attachmentEntityResponse); + 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: {}", + attachmentResponseContent, + testContentAttachment, + attachmentEntityResponseContent, + testContentAttachmentEntity); + } + return result; + }); + clearServiceHandlerContext(); + + var attachmentResponse = requestHelper.executeGet(attachmentUrl); + assertThat(attachmentResponse.getResponse().getContentAsString()) + .isEqualTo(testContentAttachment); + var attachmentEntityResponse = requestHelper.executeGet(attachmentEntityUrl); + assertThat(attachmentEntityResponse.getResponse().getContentAsString()) + .isEqualTo(testContentAttachmentEntity); + verifyTwoReadEvents(); + } + + @Test + void deleteAttachmentAndActivateDraft() throws Exception { + 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 attachmentEntityDeleteUrl = getAttachmentEntityBaseUrl(itemAttachmentEntity.getId(), false); + + requestHelper.executeDeleteWithMatcher(attachmentDeleteUrl, status().isNoContent()); + requestHelper.executeDeleteWithMatcher(attachmentEntityDeleteUrl, status().isNoContent()); + verifyNoAttachmentEventsCalled(); + + prepareAndActiveDraft(getRootUrl(selectedRoot.getId(), false)); + + var selectedRootAfterDelete = selectStoredRootData(selectedRoot); + assertThat(selectedRootAfterDelete.getItems().get(0).getAttachments()).isEmpty(); + assertThat(selectedRootAfterDelete.getItems().get(0).getAttachmentEntities()).isEmpty(); + verifyOnlyTwoDeleteEvents(itemAttachment.getContentId(), itemAttachmentEntity.getContentId()); + } + + @Test + void updateAttachmentAndActivateDraft() throws Exception { + 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 changedAttachmentFileName = "changedAttachmentFileName.txt"; + var changedAttachmentEntityFileName = "changedAttachmentEntityFileName.txt"; + + updateFileName( + selectedRoot, + itemAttachment, + itemAttachmentEntity, + changedAttachmentFileName, + changedAttachmentEntityFileName); + + prepareAndActiveDraft(getRootUrl(selectedRoot.getId(), false)); + var selectedRootAfterUpdate = selectStoredRootData(selectedRoot); + assertThat(selectedRootAfterUpdate.getItems().get(0).getAttachments().get(0).getFileName()) + .isEqualTo(changedAttachmentFileName); + assertThat( + selectedRootAfterUpdate.getItems().get(0).getAttachmentEntities().get(0).getFileName()) + .isEqualTo(changedAttachmentEntityFileName); + verifyNoAttachmentEventsCalled(); + } + + @Test + void updateAttachmentAndCancelDraft() throws Exception { + 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 originAttachmentFileName = itemAttachment.getFileName(); + var originAttachmentEntityFileName = itemAttachmentEntity.getFileName(); + + updateFileName( + selectedRoot, + itemAttachment, + itemAttachmentEntity, + "changedAttachmentFileName.txt", + "changedAttachmentEntityFileName.txt"); + + cancelDraft(getRootUrl(selectedRoot.getId(), false)); + var selectedRootAfterUpdate = selectStoredRootData(selectedRoot); + assertThat(selectedRootAfterUpdate.getItems().get(0).getAttachments().get(0).getFileName()) + .isEqualTo(originAttachmentFileName); + assertThat( + selectedRootAfterUpdate.getItems().get(0).getAttachmentEntities().get(0).getFileName()) + .isEqualTo(originAttachmentEntityFileName); + verifyNoAttachmentEventsCalled(); + } + + @Test + void createAttachmentAndActivateDraft() throws Exception { + var selectedRoot = + deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); + clearServiceHandlerContext(); + createNewDraftForExistingRoot(selectedRoot.getId()); + + var itemAttachment = selectedRoot.getItems().get(0); + + var newAttachmentContent = "new attachment content"; + createAttachmentWithContent(newAttachmentContent, itemAttachment.getId()); + var newAttachmentEntityContent = "new attachmentEntity content"; + createAttachmentEntityWithContent(newAttachmentEntityContent, itemAttachment); + + prepareAndActiveDraft(getRootUrl(selectedRoot.getId(), false)); + var selectedRootAfterCreate = selectStoredRootData(selectedRoot); + assertThat(selectedRootAfterCreate.getItems().get(0).getAttachments()).hasSize(2); + assertThat(selectedRootAfterCreate.getItems().get(0).getAttachmentEntities()).hasSize(2); + verifyOnlyTwoCreateEvents(newAttachmentContent, newAttachmentEntityContent); + } + + @Test + void createAttachmentAndCancelDraft() throws Exception { + var selectedRoot = + deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); + clearServiceHandlerContext(); + createNewDraftForExistingRoot(selectedRoot.getId()); + + var itemAttachment = selectedRoot.getItems().get(0); + + var newAttachmentContent = "new attachment content"; + createAttachmentWithContent(newAttachmentContent, itemAttachment.getId()); + var newAttachmentEntityContent = "new attachmentEntity content"; + createAttachmentEntityWithContent(newAttachmentEntityContent, itemAttachment); + + cancelDraft(getRootUrl(selectedRoot.getId(), false)); + var selectedRootAfterCreate = selectStoredRootData(selectedRoot); + assertThat(selectedRootAfterCreate.getItems().get(0).getAttachments()).hasSize(1); + assertThat(selectedRootAfterCreate.getItems().get(0).getAttachmentEntities()).hasSize(1); + verifyTwoCreateAndDeleteEvents(newAttachmentContent, newAttachmentEntityContent); + } + + @Test + void deleteContentInDraft() throws Exception { + var selectedRoot = + deepCreateAndActivate( + "testContent attachment for delete", "testContent attachmentEntity for delete"); + clearServiceHandlerContext(); + createNewDraftForExistingRoot(selectedRoot.getId()); + var itemAttachment = selectedRoot.getItems().get(0).getAttachments().get(0); + var itemAttachmentEntity = selectedRoot.getItems().get(0).getAttachmentEntities().get(0); + + deleteContent(selectedRoot, itemAttachment, itemAttachmentEntity); + verifyNoAttachmentEventsCalled(); + + prepareAndActiveDraft(getRootUrl(selectedRoot.getId(), false)); + var selectedRootAfterDelete = selectStoredRootData(selectedRoot); + verifyContent( + selectedRootAfterDelete.getItems().get(0).getAttachments().get(0).getContent(), null); + verifyContent( + selectedRootAfterDelete.getItems().get(0).getAttachmentEntities().get(0).getContent(), + null); + verifyOnlyTwoDeleteEvents(itemAttachment.getContentId(), itemAttachmentEntity.getContentId()); + } + + @Test + void doNotDeleteContentInCancelledDraft() throws Exception { + 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); + + deleteContent(selectedRoot, itemAttachment, itemAttachmentEntity); + + cancelDraft(getRootUrl(selectedRoot.getId(), false)); + var selectedRootAfterDelete = selectStoredRootData(selectedRoot); + verifyContent( + selectedRootAfterDelete.getItems().get(0).getAttachments().get(0).getContent(), + "testContent attachment"); + verifyContent( + selectedRootAfterDelete.getItems().get(0).getAttachmentEntities().get(0).getContent(), + "testContent attachmentEntity"); + verifyNoAttachmentEventsCalled(); + } + + @Test + void updateContentInDraft() throws Exception { + 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 attachmentContentId = itemAttachment.getContentId(); + var attachmentEntityContentId = itemAttachmentEntity.getContentId(); + + var newAttachmentContent = "new content attachment"; + putNewContentForAttachment( + newAttachmentContent, selectedRoot.getItems().get(0).getId(), itemAttachment.getId()); + var newAttachmentEntityContent = "new content attachmentEntity"; + putNewContentForAttachmentEntity(newAttachmentEntityContent, itemAttachmentEntity.getId()); + + prepareAndActiveDraft(getRootUrl(selectedRoot.getId(), false)); + var selectedRootAfterUpdate = selectStoredRootData(selectedRoot); + verifyContent( + selectedRootAfterUpdate.getItems().get(0).getAttachments().get(0).getContent(), + newAttachmentContent); + verifyContent( + selectedRootAfterUpdate.getItems().get(0).getAttachmentEntities().get(0).getContent(), + newAttachmentEntityContent); + verifyEventContextEmptyForEvent(AttachmentService.EVENT_READ_ATTACHMENT); + verifyTwoUpdateEvents( + newAttachmentContent, + attachmentContentId, + newAttachmentEntityContent, + attachmentEntityContentId); + var selectedRootAfterDeletion = selectStoredRootData(selectedRoot); + assertThat(selectedRootAfterDeletion.getItems().get(0).getAttachments().get(0).getContentId()) + .isNotEmpty(); + assertThat( + selectedRootAfterDeletion + .getItems() + .get(0) + .getAttachmentEntities() + .get(0) + .getContentId()) + .isNotEmpty(); + } + + @Test + void contentCanBeReadForActiveRoot() throws Exception { + var attachmentContent = "attachment Content"; + var attachmentEntityContent = "attachmentEntity Content"; + var selectedRoot = deepCreateAndActivate(attachmentContent, attachmentEntityContent); + clearServiceHandlerContext(); + + readAndValidateActiveContent(selectedRoot, attachmentContent, attachmentEntityContent); + } + + @Test + void noChangesOnAttachmentsContentStillAvailable() throws Exception { + var attachmentContent = "attachment Content"; + var attachmentEntityContent = "attachmentEntity Content"; + var selectedRoot = deepCreateAndActivate(attachmentContent, attachmentEntityContent); + clearServiceHandlerContext(); + createNewDraftForExistingRoot(selectedRoot.getId()); + + var rootUrl = getRootUrl(selectedRoot.getId(), false); + requestHelper.executePatchWithODataResponseAndAssertStatusOk( + rootUrl, "{\"title\":\"some other title\"}"); + + prepareAndActiveDraft(rootUrl); + verifyNoAttachmentEventsCalled(); + + readAndValidateActiveContent(selectedRoot, attachmentContent, attachmentEntityContent); + } + + @Test + void deleteItemAndActivateDraft() throws Exception { + var attachmentContent = "attachment Content"; + var attachmentEntityContent = "attachmentEntity Content"; + var selectedRoot = deepCreateAndActivate(attachmentContent, attachmentEntityContent); + clearServiceHandlerContext(); + createNewDraftForExistingRoot(selectedRoot.getId()); + + var itemUrl = getItemUrl(selectedRoot.getItems().get(0), false); + requestHelper.executeDeleteWithMatcher(itemUrl, status().isNoContent()); + verifyNoAttachmentEventsCalled(); + + prepareAndActiveDraft(getRootUrl(selectedRoot.getId(), false)); + var selectedRootAfterDelete = selectStoredRootData(selectedRoot); + assertThat(selectedRootAfterDelete.getItems()).isEmpty(); + verifyOnlyTwoDeleteEvents( + selectedRoot.getItems().get(0).getAttachments().get(0).getContentId(), + selectedRoot.getItems().get(0).getAttachmentEntities().get(0).getContentId()); + } + + @Test + void deleteItemAndCancelDraft() throws Exception { + var attachmentContent = "attachment Content"; + var attachmentEntityContent = "attachmentEntity Content"; + var selectedRoot = deepCreateAndActivate(attachmentContent, attachmentEntityContent); + clearServiceHandlerContext(); + createNewDraftForExistingRoot(selectedRoot.getId()); + + var itemUrl = getItemUrl(selectedRoot.getItems().get(0), false); + requestHelper.executeDeleteWithMatcher(itemUrl, status().isNoContent()); + + cancelDraft(getRootUrl(selectedRoot.getId(), false)); + var selectedRootAfterDelete = selectStoredRootData(selectedRoot); + assertThat(selectedRootAfterDelete.getItems()).isNotEmpty(); + assertThat(selectedRootAfterDelete.getItems().get(0).getAttachments()).isNotEmpty(); + assertThat(selectedRootAfterDelete.getItems().get(0).getAttachments().get(0).getContentId()) + .isNotEmpty(); + assertThat(selectedRootAfterDelete.getItems().get(0).getAttachmentEntities()).isNotEmpty(); + assertThat( + selectedRootAfterDelete.getItems().get(0).getAttachmentEntities().get(0).getContentId()) + .isNotEmpty(); + verifyNoAttachmentEventsCalled(); + } + + @Test + void noEventsForForDeletedRoot() throws Exception { + var selectedRoot = deepCreateAndActivate("attachmentContent", "attachmentEntityContent"); + clearServiceHandlerContext(); + createNewDraftForExistingRoot(selectedRoot.getId()); + + var rootUrl = getRootUrl(selectedRoot.getId(), true); + requestHelper.executeDeleteWithMatcher(rootUrl, status().isNoContent()); + + var draftPrepareUrl = rootUrl + "/TestDraftService.draftPrepare"; + requestHelper.executePostWithMatcher( + draftPrepareUrl, "{\"SideEffectsQualifier\":\"\"}", status().isNotFound()); + + var select = Select.from(TestDraftService_.DRAFT_ROOTS); + var result = persistenceService.run(select).listOf(DraftRoots.class); + assertThat(result).isEmpty(); + + var attachmentContentId = selectedRoot.getItems().get(0).getAttachments().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"); + clearServiceHandlerContext(); + createNewDraftForExistingRoot(selectedRoot.getId()); + + createNewContentAndValidateEvents(selectedRoot); + + prepareAndActiveDraft(getRootUrl(selectedRoot.getId(), false)); + var selectedRootAfterCreate = selectStoredRootData(selectedRoot); + assertThat(selectedRootAfterCreate.getItems().get(0).getAttachments()).hasSize(2); + assertThat(selectedRootAfterCreate.getItems().get(0).getAttachmentEntities()).hasSize(2); + verifyNoAttachmentEventsCalled(); + } + + @Test + void errorInTransactionAfterCreateCallsDeleteAndNothingForCancel() throws Exception { + var selectedRoot = + deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); + clearServiceHandlerContext(); + createNewDraftForExistingRoot(selectedRoot.getId()); + + createNewContentAndValidateEvents(selectedRoot); + + cancelDraft(getRootUrl(selectedRoot.getId(), false)); + var selectedRootAfterCreate = selectStoredRootData(selectedRoot); + assertThat(selectedRootAfterCreate.getItems().get(0).getAttachments()).hasSize(1); + assertThat(selectedRootAfterCreate.getItems().get(0).getAttachmentEntities()).hasSize(1); + verifyNoAttachmentEventsCalled(); + } + + @Test + void errorInTransactionAfterUpdateCallsDelete() throws Exception { + var attachmentContent = "testContent attachment"; + var attachmentEntityContent = "testContent attachmentEntity"; + var selectedRoot = deepCreateAndActivate(attachmentContent, attachmentEntityContent); + clearServiceHandlerContext(); + createNewDraftForExistingRoot(selectedRoot.getId()); + + var itemAttachment = selectedRoot.getItems().get(0).getAttachments().get(0); + var itemAttachmentEntity = selectedRoot.getItems().get(0).getAttachmentEntities().get(0); + + updateContentWithErrorAndValidateEvents(selectedRoot, itemAttachment, itemAttachmentEntity); + + testPersistenceHandler.reset(); + prepareAndActiveDraft(getRootUrl(selectedRoot.getId(), false)); + verifyNothingHasChangedInDraft(selectedRoot, attachmentContent, attachmentEntityContent); + } + + @Test + void errorInTransactionAfterUpdateCallsDeleteEvenIfDraftIsCancelled() throws Exception { + var attachmentContent = "testContent attachment"; + var attachmentEntityContent = "testContent attachmentEntity"; + var selectedRoot = deepCreateAndActivate(attachmentContent, attachmentEntityContent); + clearServiceHandlerContext(); + createNewDraftForExistingRoot(selectedRoot.getId()); + + var itemAttachment = selectedRoot.getItems().get(0).getAttachments().get(0); + var itemAttachmentEntity = selectedRoot.getItems().get(0).getAttachmentEntities().get(0); + + updateContentWithErrorAndValidateEvents(selectedRoot, itemAttachment, itemAttachmentEntity); + + testPersistenceHandler.reset(); + cancelDraft(getRootUrl(selectedRoot.getId(), false)); + verifyNothingHasChangedInDraft(selectedRoot, attachmentContent, attachmentEntityContent); + } + + @Test + void createAndDeleteAttachmentWorks() throws Exception { + var selectedRoot = + deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); + clearServiceHandlerContext(); + createNewDraftForExistingRoot(selectedRoot.getId()); + + var itemAttachment = selectedRoot.getItems().get(0); + + var newAttachmentContent = "new attachment content"; + createAttachmentWithContent(newAttachmentContent, itemAttachment.getId()); + var newAttachmentEntityContent = "new attachmentEntity content"; + createAttachmentEntityWithContent(newAttachmentEntityContent, itemAttachment); + + var draftRoot = selectStoredRootData(DraftRoots_.CDS_NAME + "_drafts", selectedRoot); + + 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 attachmentEntityDeleteUrl = getAttachmentEntityBaseUrl(newAttachmentEntity.getId(), false); + + requestHelper.executeDeleteWithMatcher(attachmentDeleteUrl, status().isNoContent()); + requestHelper.executeDeleteWithMatcher(attachmentEntityDeleteUrl, status().isNoContent()); + + verifyTwoCreateAndDeleteEvents(newAttachmentContent, newAttachmentEntityContent); + clearServiceHandlerContext(); + + prepareAndActiveDraft(getRootUrl(selectedRoot.getId(), false)); + verifyNoAttachmentEventsCalled(); + } + + protected DraftRoots deepCreateAndActivate( + String testContentAttachment, String testContentAttachmentEntity) throws Exception { + var responseRoot = createNewDraft(); + var rootUrl = updateRoot(responseRoot); + var responseItem = createItem(rootUrl); + createAttachmentWithContent(testContentAttachment, responseItem.getId()); + createAttachmentEntityWithContent(testContentAttachmentEntity, responseItem); + prepareAndActiveDraft(rootUrl); + + return selectStoredRootData(responseRoot); + } + + private DraftRoots createNewDraft() throws Exception { + var responseRootCdsData = + requestHelper.executePostWithODataResponseAndAssertStatusCreated(BASE_ROOT_URL, "{}"); + return Struct.access(responseRootCdsData).as(DraftRoots.class); + } + + private void createNewDraftForExistingRoot(String rootId) throws Exception { + var url = getRootUrl(rootId, true) + "/TestDraftService.draftEdit"; + requestHelper.executePostWithODataResponseAndAssertStatus( + url, "{\"PreserveChanges\":true}", HttpStatus.OK); + } + + private String updateRoot(DraftRoots responseRoot) throws Exception { + responseRoot.setTitle("some title"); + var rootUrl = getRootUrl(responseRoot.getId(), false); + requestHelper.executePatchWithODataResponseAndAssertStatusOk(rootUrl, responseRoot.toJson()); + return rootUrl; + } + + private String getRootUrl(String rootId, boolean isActiveEntity) { + return BASE_ROOT_URL + "(ID=" + rootId + ",IsActiveEntity=" + isActiveEntity + ")"; + } + + 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()); + return Struct.access(responseItemCdsData).as(Items.class); + } + + private void createAttachmentWithContent(String testContentAttachment, String itemId) + throws Exception { + createAttachmentWithContent(testContentAttachment, itemId, status().isNoContent(), false); + } + + private void createAttachmentWithContent( + String testContentAttachment, String itemId, ResultMatcher matcher, boolean withError) + throws Exception { + var responseAttachment = createAttachment(itemId); + if (withError) { + testPersistenceHandler.setThrowExceptionOnUpdate(true); + } + putNewContentForAttachment(testContentAttachment, itemId, responseAttachment.getId(), matcher); + } + + private void putNewContentForAttachment( + String testContentAttachment, String itemId, String attachmentId) throws Exception { + putNewContentForAttachment(testContentAttachment, itemId, attachmentId, status().isNoContent()); + } + + private void putNewContentForAttachment( + String testContentAttachment, String itemId, String attachmentId, ResultMatcher matcher) + throws Exception { + var attachmentPutUrl = getAttachmentBaseUrl(itemId, attachmentId, false) + "/content"; + requestHelper.setContentType("text/plain"); + requestHelper.executePutWithMatcher( + attachmentPutUrl, testContentAttachment.getBytes(StandardCharsets.UTF_8), matcher); + requestHelper.resetHelper(); + } + + private Attachments createAttachment(String itemId) throws Exception { + var itemAttachment = Attachments.create(); + itemAttachment.setFileName("itemAttachment.txt"); + + var attachmentPostUrl = BASE_URL + "Items(ID=" + itemId + ",IsActiveEntity=false)/attachments"; + var responseAttachmentCdsData = + requestHelper.executePostWithODataResponseAndAssertStatusCreated( + attachmentPostUrl, itemAttachment.toJson()); + return Struct.access(responseAttachmentCdsData).as(Attachments.class); + } + + private void createAttachmentEntityWithContent( + String testContentAttachmentEntity, Items responseItem) throws Exception { + createAttachmentEntityWithContent( + testContentAttachmentEntity, responseItem, status().isNoContent(), false); + } + + private void createAttachmentEntityWithContent( + String testContentAttachmentEntity, + Items responseItem, + ResultMatcher matcher, + boolean withError) + throws Exception { + var responseAttachmentEntity = createAttachmentEntity(responseItem); + if (withError) { + testPersistenceHandler.setThrowExceptionOnUpdate(true); + } + putNewContentForAttachmentEntity( + testContentAttachmentEntity, responseAttachmentEntity.getId(), matcher); + } + + private void putNewContentForAttachmentEntity( + String testContentAttachmentEntity, String attachmentId) throws Exception { + putNewContentForAttachmentEntity( + testContentAttachmentEntity, attachmentId, status().isNoContent()); + } + + private void putNewContentForAttachmentEntity( + String testContentAttachmentEntity, String attachmentId, ResultMatcher matcher) + throws Exception { + var attachmentEntityPutUrl = + BASE_URL + "/AttachmentEntity(ID=" + attachmentId + ",IsActiveEntity=false)/content"; + requestHelper.setContentType("image/jpeg"); + requestHelper.executePutWithMatcher( + attachmentEntityPutUrl, + testContentAttachmentEntity.getBytes(StandardCharsets.UTF_8), + matcher); + requestHelper.resetHelper(); + } + + private AttachmentEntity createAttachmentEntity(Items responseItem) throws Exception { + var itemAttachmentEntity = AttachmentEntity.create(); + itemAttachmentEntity.setFileName("itemAttachmentEntity.txt"); + + var attachmentEntityPostUrl = getItemUrl(responseItem, false) + "/attachmentEntities"; + var responseAttachmentEntityCdsData = + requestHelper.executePostWithODataResponseAndAssertStatusCreated( + attachmentEntityPostUrl, itemAttachmentEntity.toJson()); + return Struct.access(responseAttachmentEntityCdsData).as(AttachmentEntity.class); + } + + private String getItemUrl(Items responseItem, boolean isActiveEntity) { + return BASE_URL + + "Items(ID=" + + responseItem.getId() + + ",IsActiveEntity=" + + isActiveEntity + + ")"; + } + + protected String getAttachmentBaseUrl( + String itemId, String attachmentId, boolean isActiveEntity) { + return BASE_URL + + "Items_attachments(up__ID=" + + itemId + + ",ID=" + + attachmentId + + ",IsActiveEntity=" + + isActiveEntity + + ")"; + } + + protected String getAttachmentEntityBaseUrl(String attachmentId, boolean isActiveEntity) { + return BASE_URL + + "AttachmentEntity(ID=" + + attachmentId + + ",IsActiveEntity=" + + isActiveEntity + + ")"; + } + + private void prepareAndActiveDraft(String rootUrl) throws Exception { + var draftPrepareUrl = rootUrl + "/TestDraftService.draftPrepare"; + var draftActivateUrl = rootUrl + "/TestDraftService.draftActivate"; + requestHelper.executePostWithMatcher( + draftPrepareUrl, "{\"SideEffectsQualifier\":\"\"}", status().isOk()); + requestHelper.executePostWithMatcher(draftActivateUrl, "{}", status().isOk()); + } + + private void cancelDraft(String rootUrl) throws Exception { + requestHelper.executeDeleteWithMatcher(rootUrl, status().isNoContent()); + } + + private DraftRoots selectStoredRootData(DraftRoots responseRoot) { + return selectStoredRootData(DraftRoots_.CDS_NAME, 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())); + 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"; + + Awaitility.await() + .atMost(60, TimeUnit.SECONDS) + .pollDelay(1, TimeUnit.SECONDS) + .pollInterval(2, TimeUnit.SECONDS) + .until( + () -> { + var attachmentResponse = requestHelper.executeGet(attachmentUrl); + var attachmentEntityResponse = requestHelper.executeGet(attachmentEntityUrl); + var attachmentContentAsString = attachmentResponse.getResponse().getContentAsString(); + var attachmentEntityContentAsString = + attachmentEntityResponse.getResponse().getContentAsString(); + + var booleanResult = + attachmentContentAsString.equals(attachmentContent) + && attachmentEntityContentAsString.equals(attachmentEntityContent); + + if (!booleanResult) { + logger.info( + "Attachment response content: {}, Attachment Test Content: {}, Attachment Entity response content: {}, Attachment Entity Test Content: {}", + attachmentContentAsString, + attachmentContent, + attachmentEntityContentAsString, + attachmentEntityContent); + } + return booleanResult; + }); + clearServiceHandlerContext(); + + var attachmentResponse = requestHelper.executeGet(attachmentUrl); + var attachmentEntityResponse = requestHelper.executeGet(attachmentEntityUrl); + + assertThat(attachmentResponse.getResponse().getContentAsString()).isEqualTo(attachmentContent); + assertThat(attachmentEntityResponse.getResponse().getContentAsString()) + .isEqualTo(attachmentEntityContent); + verifyTwoReadEvents(); + } + + 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"; + + requestHelper.executeDeleteWithMatcher(attachmentUrl, status().isNoContent()); + requestHelper.executeDeleteWithMatcher(attachmentEntityUrl, status().isNoContent()); + } + + private void updateFileName( + DraftRoots selectedRoot, + Attachments itemAttachment, + AttachmentEntity itemAttachmentEntity, + String changedAttachmentFileName, + String changedAttachmentEntityFileName) + throws Exception { + updateFileName( + selectedRoot, + itemAttachment, + itemAttachmentEntity, + changedAttachmentFileName, + changedAttachmentEntityFileName, + HttpStatus.OK); + } + + private void updateFileName( + DraftRoots selectedRoot, + Attachments itemAttachment, + AttachmentEntity itemAttachmentEntity, + String changedAttachmentFileName, + String changedAttachmentEntityFileName, + HttpStatus httpStatus) + throws Exception { + var attachmentUrl = + getAttachmentBaseUrl(selectedRoot.getItems().get(0).getId(), itemAttachment.getId(), false); + var attachmentEntityUrl = getAttachmentEntityBaseUrl(itemAttachmentEntity.getId(), false); + + requestHelper.executePatchWithODataResponseAndAssertStatus( + attachmentUrl, "{\"fileName\":\"" + changedAttachmentFileName + "\"}", httpStatus); + requestHelper.executePatchWithODataResponseAndAssertStatus( + attachmentEntityUrl, + "{\"fileName\":\"" + changedAttachmentEntityFileName + "\"}", + httpStatus); + } + + private void updateContentWithErrorAndValidateEvents( + DraftRoots selectedRoot, Attachments itemAttachment, AttachmentEntity itemAttachmentEntity) + throws Exception { + testPersistenceHandler.setThrowExceptionOnUpdate(true); + var newAttachmentContent = "new content attachment"; + putNewContentForAttachment( + newAttachmentContent, + selectedRoot.getItems().get(0).getId(), + itemAttachment.getId(), + status().is5xxServerError()); + var newAttachmentEntityContent = "new content attachmentEntity"; + putNewContentForAttachmentEntity( + newAttachmentEntityContent, itemAttachmentEntity.getId(), status().is5xxServerError()); + verifyTwoCreateAndRevertedDeleteEvents(); + clearServiceHandlerContext(); + } + + private void verifyNothingHasChangedInDraft( + DraftRoots selectedRoot, String attachmentContent, String attachmentEntityContent) + throws IOException { + var selectedRootAfterUpdate = selectStoredRootData(selectedRoot); + + verifyContent( + selectedRootAfterUpdate.getItems().get(0).getAttachments().get(0).getContent(), + attachmentContent); + verifyContent( + selectedRootAfterUpdate.getItems().get(0).getAttachmentEntities().get(0).getContent(), + attachmentEntityContent); + verifyNoAttachmentEventsCalled(); + var selectedRootAfterDeletion = selectStoredRootData(selectedRoot); + assertThat(selectedRootAfterDeletion.getItems().get(0).getAttachments().get(0).getContentId()) + .isNotEmpty(); + assertThat( + selectedRootAfterDeletion + .getItems() + .get(0) + .getAttachmentEntities() + .get(0) + .getContentId()) + .isNotEmpty(); + } + + private void createNewContentAndValidateEvents(DraftRoots selectedRoot) throws Exception { + var itemAttachment = selectedRoot.getItems().get(0); + var newAttachmentContent = "new attachment content"; + createAttachmentWithContent( + newAttachmentContent, itemAttachment.getId(), status().is5xxServerError(), true); + testPersistenceHandler.reset(); + var newAttachmentEntityContent = "new attachmentEntity content"; + createAttachmentEntityWithContent( + newAttachmentEntityContent, itemAttachment, status().is5xxServerError(), true); + verifyTwoCreateAndDeleteEvents(newAttachmentContent, newAttachmentEntityContent); + clearServiceHandlerContext(); + testPersistenceHandler.reset(); + } + + private String getResponseContent(MvcResult attachmentResponse) + throws UnsupportedEncodingException { + return attachmentResponse.getResponse().getStatus() == HttpStatus.OK.value() + ? attachmentResponse.getResponse().getContentAsString() + : ""; + } + + protected abstract void verifyContentId(String contentId, String attachmentId); + + protected abstract void verifyContent(InputStream attachment, String testContent) + throws IOException; + + protected abstract void verifyNoAttachmentEventsCalled(); + + protected abstract void clearServiceHandlerContext(); + + protected abstract void verifyEventContextEmptyForEvent(String... events); + + protected abstract void verifyOnlyTwoCreateEvents( + String newAttachmentContent, String newAttachmentEntityContent); + + protected abstract void verifyTwoCreateAndDeleteEvents( + String newAttachmentContent, String newAttachmentEntityContent); + + protected abstract void verifyTwoReadEvents(); + + protected abstract void verifyOnlyTwoDeleteEvents( + String attachmentContentId, String attachmentEntityContentId); + + protected abstract void verifyTwoUpdateEvents( + String newAttachmentContent, + String attachmentContentId, + String newAttachmentEntityContent, + String attachmentEntityContentId); + + protected abstract void verifyTwoCreateAndRevertedDeleteEvents(); +} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithTestHandlerTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithTestHandlerTest.java new file mode 100644 index 000000000..4ddb6280a --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithTestHandlerTest.java @@ -0,0 +1,288 @@ +/* + * © 2024-2024 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; + +import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; +import com.sap.cds.feature.attachments.integrationtests.testhandler.EventContextHolder; +import com.sap.cds.feature.attachments.service.AttachmentService; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentCreateEventContext; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentMarkAsDeletedEventContext; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles(Profiles.TEST_HANDLER_ENABLED) +class DraftOdataRequestValidationWithTestHandlerTest extends DraftOdataRequestValidationBase { + + private static final Logger logger = + LoggerFactory.getLogger(DraftOdataRequestValidationWithTestHandlerTest.class); + + @Test + void serviceHandlerIsNotEmpty() { + assertThat(serviceHandler).isNotNull(); + verifyNoAttachmentEventsCalled(); + } + + @Override + protected void verifyContentId(String contentId, String attachmentId) { + assertThat(contentId).isNotEmpty().isNotEqualTo(attachmentId); + } + + @Override + protected void verifyContent(InputStream attachment, String testContent) { + assertThat(attachment).isNull(); + } + + @Override + protected void verifyNoAttachmentEventsCalled() { + assertThat(serviceHandler.getEventContext()).isEmpty(); + } + + @Override + protected void clearServiceHandlerContext() { + serviceHandler.clearEventContext(); + } + + @Override + protected void verifyEventContextEmptyForEvent(String... events) { + Arrays.stream(events) + .forEach(event -> assertThat(serviceHandler.getEventContextForEvent(event)).isEmpty()); + } + + @Override + protected void verifyOnlyTwoCreateEvents( + String newAttachmentContent, String newAttachmentEntityContent) { + verifyEventContextEmptyForEvent( + AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED, + AttachmentService.EVENT_READ_ATTACHMENT); + var createEvents = + serviceHandler.getEventContextForEvent(AttachmentService.EVENT_CREATE_ATTACHMENT); + assertThat(createEvents).hasSize(2); + var attachmentContentFound = + isAttachmentContentFoundInCreateEvent(createEvents, newAttachmentContent); + assertThat(attachmentContentFound).isTrue(); + var attachmentEntityContentFound = + isAttachmentContentFoundInCreateEvent(createEvents, newAttachmentEntityContent); + assertThat(attachmentEntityContentFound).isTrue(); + } + + @Override + protected void verifyTwoCreateAndDeleteEvents( + String newAttachmentContent, String newAttachmentEntityContent) { + awaitNumberOfExpectedEvents(4); + verifyEventContextEmptyForEvent(AttachmentService.EVENT_READ_ATTACHMENT); + var createEvents = + serviceHandler.getEventContextForEvent(AttachmentService.EVENT_CREATE_ATTACHMENT); + assertThat(createEvents).hasSize(2); + var attachmentContentFound = + isAttachmentContentFoundInCreateEvent(createEvents, newAttachmentContent); + assertThat(attachmentContentFound).isTrue(); + var attachmentEntityContentFound = + isAttachmentContentFoundInCreateEvent(createEvents, newAttachmentEntityContent); + assertThat(attachmentEntityContentFound).isTrue(); + var deleteEvents = + serviceHandler.getEventContextForEvent(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); + assertThat(deleteEvents).hasSize(2); + deleteEvents.forEach( + event -> { + var deleteContext = (AttachmentMarkAsDeletedEventContext) event.context(); + assertThat(deleteContext.getContentId()).isNotEmpty(); + var createEventFound = + createEvents.stream() + .anyMatch( + createEvent -> { + var createContext = (AttachmentCreateEventContext) createEvent.context(); + return createContext.getContentId().equals(deleteContext.getContentId()); + }); + assertThat(createEventFound).isTrue(); + }); + } + + @Override + protected void verifyTwoReadEvents() { + verifyEventContextEmptyForEvent( + AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED, + AttachmentService.EVENT_CREATE_ATTACHMENT); + var readEvents = + serviceHandler.getEventContextForEvent(AttachmentService.EVENT_READ_ATTACHMENT); + assertThat(readEvents).hasSize(2); + } + + @Override + protected void verifyOnlyTwoDeleteEvents( + String attachmentContentId, String attachmentEntityContentId) { + awaitNumberOfExpectedEvents(2); + verifyEventContextEmptyForEvent( + AttachmentService.EVENT_CREATE_ATTACHMENT, AttachmentService.EVENT_READ_ATTACHMENT); + var deleteEvents = + serviceHandler.getEventContextForEvent(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); + assertThat(deleteEvents).hasSize(2); + verifyDeleteEventContainsContentId(deleteEvents, attachmentContentId); + verifyDeleteEventContainsContentId(deleteEvents, attachmentEntityContentId); + } + + @Override + protected void verifyTwoUpdateEvents( + String newAttachmentContent, + String attachmentContentId, + String newAttachmentEntityContent, + String attachmentEntityContentId) { + awaitNumberOfExpectedEvents(4); + var createEvents = + serviceHandler.getEventContextForEvent(AttachmentService.EVENT_CREATE_ATTACHMENT); + var deleteEvents = + serviceHandler.getEventContextForEvent(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); + assertThat(createEvents).hasSize(2); + verifyCreateEventFound(createEvents, newAttachmentContent); + verifyCreateEventFound(createEvents, newAttachmentEntityContent); + assertThat(deleteEvents).hasSize(2); + verifyDeleteEventContainsContentId(deleteEvents, attachmentContentId); + verifyDeleteEventContainsContentId(deleteEvents, attachmentEntityContentId); + } + + @Override + protected void verifyTwoCreateAndRevertedDeleteEvents() { + awaitNumberOfExpectedEvents(4); + var createEvents = + serviceHandler.getEventContextForEvent(AttachmentService.EVENT_CREATE_ATTACHMENT); + var deleteEvents = + serviceHandler.getEventContextForEvent(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); + assertThat(createEvents).hasSize(2); + assertThat(deleteEvents).hasSize(2); + deleteEvents.forEach( + event -> { + var deleteContext = (AttachmentMarkAsDeletedEventContext) event.context(); + var createEventFound = + createEvents.stream() + .anyMatch( + createEvent -> { + var createContext = (AttachmentCreateEventContext) createEvent.context(); + return createContext.getContentId().equals(deleteContext.getContentId()); + }); + assertThat(createEventFound).isTrue(); + }); + } + + private void awaitNumberOfExpectedEvents(int expectedEvents) { + Awaitility.await() + .atMost(60, TimeUnit.SECONDS) + .pollDelay(1, TimeUnit.SECONDS) + .pollInterval(2, TimeUnit.SECONDS) + .until( + () -> { + var eventCalls = serviceHandler.getEventContext().size(); + logger.info( + "Waiting for expected size '{}' in handler context, was '{}'", + expectedEvents, + eventCalls); + var numberMatch = eventCalls >= expectedEvents; + if (!numberMatch) { + serviceHandler.getEventContext().forEach(event -> logger.info("Event: {}", event)); + } + return numberMatch; + }); + } + + private void verifyCreateEventFound(List createEvents, String newContent) { + var eventContentFound = + createEvents.stream() + .anyMatch( + event -> { + var createContext = (AttachmentCreateEventContext) event.context(); + try { + return Arrays.equals( + createContext.getData().getContent().readAllBytes(), + newContent.getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + assertThat(eventContentFound).isTrue(); + } + + private boolean isAttachmentContentFoundInCreateEvent( + List createEvents, String newAttachmentContent) { + return createEvents.stream() + .anyMatch( + event -> { + var createContext = (AttachmentCreateEventContext) event.context(); + try { + return Arrays.equals( + createContext.getData().getContent().readAllBytes(), + newAttachmentContent.getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + + private void verifyDeleteEventContainsContentId( + List deleteEvents, String contentId) { + var eventFound = + deleteEvents.stream() + .anyMatch( + event -> { + var deleteContext = (AttachmentMarkAsDeletedEventContext) event.context(); + return deleteContext.getContentId().equals(contentId); + }); + assertThat(eventFound).isTrue(); + } + + // Override flaky tests from base class to disable them. + // These tests are affected by a race condition in the CAP runtime's outbox TaskScheduler + // where the second DELETE event is not processed when two transactions fail in quick succession. + + @Disabled("Flaky due to CAP runtime outbox race condition - second DELETE event not processed") + @Test + @Override + void errorInTransactionAfterCreateCallsDelete() throws Exception { + super.errorInTransactionAfterCreateCallsDelete(); + } + + @Disabled("Flaky due to CAP runtime outbox race condition - second DELETE event not processed") + @Test + @Override + void errorInTransactionAfterCreateCallsDeleteAndNothingForCancel() throws Exception { + super.errorInTransactionAfterCreateCallsDeleteAndNothingForCancel(); + } + + @Disabled("Flaky due to CAP runtime outbox race condition - second DELETE event not processed") + @Test + @Override + void errorInTransactionAfterUpdateCallsDelete() throws Exception { + super.errorInTransactionAfterUpdateCallsDelete(); + } + + @Disabled("Flaky due to CAP runtime outbox race condition - second DELETE event not processed") + @Test + @Override + void errorInTransactionAfterUpdateCallsDeleteEvenIfDraftIsCancelled() throws Exception { + super.errorInTransactionAfterUpdateCallsDeleteEvenIfDraftIsCancelled(); + } + + @Disabled("Flaky due to CAP runtime outbox race condition - second DELETE event not processed") + @Test + @Override + void createAttachmentAndCancelDraft() throws Exception { + super.createAttachmentAndCancelDraft(); + } + + @Disabled("Flaky due to CAP runtime outbox race condition - second DELETE event not processed") + @Test + @Override + void createAndDeleteAttachmentWorks() throws Exception { + super.createAndDeleteAttachmentWorks(); + } +} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java new file mode 100644 index 000000000..817700d8a --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java @@ -0,0 +1,114 @@ +/* + * © 2024-2025 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.draftservice; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static org.assertj.core.api.Assertions.assertThat; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.sap.cds.feature.attachments.integrationtests.common.MalwareScanResultProvider; +import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; +import java.util.concurrent.TimeUnit; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ActiveProfiles; +import org.wiremock.spring.ConfigureWireMock; +import org.wiremock.spring.EnableWireMock; +import org.wiremock.spring.InjectWireMock; + +@ActiveProfiles(profiles = {Profiles.TEST_HANDLER_DISABLED, Profiles.MALWARE_SCAN_ENABLED}) +@EnableWireMock({@ConfigureWireMock(port = 1111, name = "malware-scanner")}) +class DraftOdataRequestValidationWithoutTestHandlerAndMalwareScannerTest + extends DraftOdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest { + + private static final Logger logger = + LoggerFactory.getLogger( + DraftOdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.class); + + @InjectWireMock("malware-scanner") + private WireMockServer wiremock; + + @Autowired private MalwareScanResultProvider malwareScanResultProvider; + + @BeforeEach + void setup() { + mockMalwareScanResult(false); + } + + @Override + @AfterEach + void teardown() { + super.teardown(); + wiremock.resetAll(); + } + + @Test + void contentCanNotBeReadForActiveRoot() throws Exception { + wiremock.resetAll(); + mockMalwareScanResult(true); + var attachmentContent = "attachment Content"; + var attachmentEntityContent = "attachmentEntity Content"; + var selectedRoot = deepCreateAndActivate(attachmentContent, attachmentEntityContent); + clearServiceHandlerContext(); + + 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) + .pollDelay(1, TimeUnit.SECONDS) + .until( + () -> { + var attachmentResponse = requestHelper.executeGet(attachmentUrl); + var attachmentEntityResponse = requestHelper.executeGet(attachmentEntityUrl); + + var attachmentResponseContent = attachmentResponse.getResponse().getContentAsString(); + var attachmentEntityResponseContent = + attachmentEntityResponse.getResponse().getContentAsString(); + + logger.info( + "Status should contain 'not_clean' for attachment and attachment entity but was: {} for attachment and {} for attachment entity", + attachmentResponseContent, + attachmentEntityResponseContent); + + return attachmentResponseContent.contains("not_clean") + && attachmentEntityResponseContent.contains("not_clean"); + }); + clearServiceHandlerContext(); + + var attachmentResponse = requestHelper.executeGet(attachmentUrl); + var attachmentEntityResponse = requestHelper.executeGet(attachmentEntityUrl); + + assertThat(attachmentResponse.getResponse().getContentAsString()) + .contains("Error text for not clean"); + assertThat(attachmentEntityResponse.getResponse().getContentAsString()) + .contains("Error text for not clean"); + verifyTwoReadEvents(); + } + + private void mockMalwareScanResult(boolean malware) { + wiremock.stubFor( + post(urlEqualTo("/scan")) + .withBasicAuth("test-user", "test-password") + .willReturn( + aResponse() + .withBody(malwareScanResultProvider.buildMalwareScanResult(malware)) + .withStatus(200))); + } +} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java new file mode 100644 index 000000000..7dce33a89 --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java @@ -0,0 +1,90 @@ +/* + * © 2024-2024 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; + +import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import org.junit.jupiter.api.Test; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles(Profiles.TEST_HANDLER_DISABLED) +class DraftOdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest + extends DraftOdataRequestValidationBase { + + @Test + void serviceHandlerIsNull() { + assertThat(serviceHandler).isNull(); + } + + @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(StandardCharsets.UTF_8)); + } else { + assertThat(attachment).isNull(); + } + } + + @Override + protected void verifyNoAttachmentEventsCalled() { + // no service handler - nothing to do + } + + @Override + protected void clearServiceHandlerContext() { + // no service handler - nothing to do + } + + @Override + protected void verifyEventContextEmptyForEvent(String... events) { + // no service handler - nothing to do + } + + @Override + protected void verifyOnlyTwoCreateEvents( + String newAttachmentContent, String newAttachmentEntityContent) { + // no service handler - nothing to do + } + + @Override + protected void verifyTwoCreateAndDeleteEvents( + String newAttachmentContent, String newAttachmentEntityContent) { + // no service handler - nothing to do + } + + @Override + protected void verifyTwoReadEvents() { + // no service handler - nothing to do + } + + @Override + protected void verifyOnlyTwoDeleteEvents( + String attachmentContentId, String attachmentEntityContentId) { + // no service handler - nothing to do + } + + @Override + protected void verifyTwoUpdateEvents( + String newAttachmentContent, + String attachmentContentId, + String newAttachmentEntityContent, + String attachmentEntityContentId) { + // no service handler - nothing to do + } + + @Override + protected void verifyTwoCreateAndRevertedDeleteEvents() { + // no service handler - nothing to do + } +} 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 000000000..f1ddbb49d --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/MediaValidatedAttachmentsDraftTest.java @@ -0,0 +1,159 @@ +/* + * © 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; +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; +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.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; +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"; + private static final ObjectMapper objectMapper = new ObjectMapper(); + + @BeforeEach + void setup() { + requestHelper.setContentType(MediaType.APPLICATION_JSON); + } + + @ParameterizedTest + @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(); + String metadata = objectMapper.writeValueAsString(Map.of("fileName", fileName)); + + requestHelper.executePostWithMatcher( + buildDraftAttachmentCreationUrl(rootId), metadata, status().is(expectedStatus)); + } + + private String buildDraftAttachmentCreationUrl(String rootId) { + return BASE_ROOT_URL + + "(ID=" + + rootId + + ",IsActiveEntity=false)" + + "/mediaValidatedAttachments"; + } + + @Test + void shouldPass_whenFileNameMissing_inDraft() throws Exception { + String rootId = createDraftRootAndReturnId(); + String metadata = "{}"; + requestHelper.executePostWithMatcher( + buildDraftAttachmentCreationUrl(rootId), metadata, status().isCreated()); + } + + // Helper methods + private String createDraftRootAndReturnId() throws Exception { + CdsData response = + requestHelper.executePostWithODataResponseAndAssertStatusCreated(BASE_ROOT_URL, "{}"); + + DraftRoots draftRoot = Struct.access(response).as(DraftRoots.class); + String payload = objectMapper.writeValueAsString(Map.of("title", "Draft")); + requestHelper.executePatchWithODataResponseAndAssertStatusOk( + getRootUrl(draftRoot.getId(), false), payload); + + 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 new file mode 100644 index 000000000..df8db48a9 --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/SizeLimitedAttachmentsSizeValidationDraftTest.java @@ -0,0 +1,202 @@ +/* + * © 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; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.sap.cds.Struct; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.sap.attachments.Attachments; +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.List; +import java.util.Objects; +import org.junit.jupiter.api.Test; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles(Profiles.TEST_HANDLER_DISABLED) +class SizeLimitedAttachmentsSizeValidationDraftTest extends DraftOdataRequestValidationBase { + + private static final String BASE_URL = MockHttpRequestHelper.ODATA_BASE_URL + "TestDraftService/"; + private static final String BASE_ROOT_URL = BASE_URL + "DraftRoots"; + + @Test + 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()); + requestHelper.setContentType(org.springframework.http.MediaType.APPLICATION_OCTET_STREAM); + requestHelper.executePutWithMatcher(url, content, status().isNoContent()); + } + + @Test + 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 + } + + @Test + void uploadContentWithinLimitAndActivateDraftSucceeds() throws Exception { + // Arrange: Create draft with sizeLimitedAttachments (no prior activation) + var draftRoot = createNewDraftWithSizeLimitedAttachments(); + var attachment = draftRoot.getSizeLimitedAttachments().get(0); + + // Act: Upload 3MB content (within 5MB limit) + byte[] content = new byte[3 * 1024 * 1024]; // 3MB + var url = buildDraftSizeLimitedAttachmentContentUrl(draftRoot.getId(), attachment.getId()); + requestHelper.setContentType(org.springframework.http.MediaType.APPLICATION_OCTET_STREAM); + requestHelper.executePutWithMatcher(url, content, status().isNoContent()); + + // Assert: Draft activation succeeds + requestHelper.setContentType(org.springframework.http.MediaType.APPLICATION_JSON); + var rootUrl = getRootUrl(draftRoot.getId(), false); + var draftPrepareUrl = rootUrl + "/TestDraftService.draftPrepare"; + var draftActivateUrl = rootUrl + "/TestDraftService.draftActivate"; + requestHelper.executePostWithMatcher( + draftPrepareUrl, "{\"SideEffectsQualifier\":\"\"}", status().isOk()); + requestHelper.executePostWithMatcher(draftActivateUrl, "{}", status().isOk()); + } + + @Test + void uploadContentExceedingLimitOnFirstDraftRejects() throws Exception { + // Arrange: Create draft with sizeLimitedAttachments (no prior activation) + var draftRoot = createNewDraftWithSizeLimitedAttachments(); + var attachment = draftRoot.getSizeLimitedAttachments().get(0); + + // Act & Assert: Upload 6MB content to a brand-new draft attachment fails immediately + 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)); + } + + // Helper methods + private DraftRoots createNewDraftWithSizeLimitedAttachments() throws Exception { + // Create new draft + var responseRootCdsData = + requestHelper.executePostWithODataResponseAndAssertStatusCreated(BASE_ROOT_URL, "{}"); + var draftRoot = Struct.access(responseRootCdsData).as(DraftRoots.class); + + // Update root with title + draftRoot.setTitle("Root with sizeLimitedAttachments"); + var rootUrl = getRootUrl(draftRoot.getId(), false); + requestHelper.executePatchWithODataResponseAndAssertStatusOk(rootUrl, draftRoot.toJson()); + + // Create sizeLimitedAttachment + var attachment = Attachments.create(); + attachment.setFileName("testFile.txt"); + attachment.setMimeType("text/plain"); + var attachmentUrl = rootUrl + "/sizeLimitedAttachments"; + var responseAttachmentCdsData = + requestHelper.executePostWithODataResponseAndAssertStatusCreated( + attachmentUrl, attachment.toJson()); + var createdAttachment = Struct.access(responseAttachmentCdsData).as(Attachments.class); + + // Build result with the attachment + draftRoot.setSizeLimitedAttachments(List.of(createdAttachment)); + return draftRoot; + } + + private String getRootUrl(String rootId, boolean isActiveEntity) { + return BASE_ROOT_URL + "(ID=" + rootId + ",IsActiveEntity=" + isActiveEntity + ")"; + } + + private String buildDraftSizeLimitedAttachmentContentUrl(String rootId, String attachmentId) { + return BASE_ROOT_URL + + "(ID=" + + rootId + + ",IsActiveEntity=false)" + + "/sizeLimitedAttachments(ID=" + + attachmentId + + ",up__ID=" + + rootId + + ",IsActiveEntity=false)" + + "/content"; + } + + // 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() { + // no service handler - nothing to do + } + + @Override + protected void clearServiceHandlerContext() { + // no service handler - nothing to do + } + + @Override + protected void verifyEventContextEmptyForEvent(String... events) { + // no service handler - nothing to do + } + + @Override + protected void verifyOnlyTwoCreateEvents( + String newAttachmentContent, String newAttachmentEntityContent) { + // no service handler - nothing to do + } + + @Override + protected void verifyTwoCreateAndDeleteEvents( + String newAttachmentContent, String newAttachmentEntityContent) { + // no service handler - nothing to do + } + + @Override + protected void verifyTwoReadEvents() { + // no service handler - nothing to do + } + + @Override + protected void verifyOnlyTwoDeleteEvents( + String attachmentContentId, String attachmentEntityContentId) { + // no service handler - nothing to do + } + + @Override + protected void verifyTwoUpdateEvents( + String newAttachmentContent, + String attachmentContentId, + String newAttachmentEntityContent, + String attachmentEntityContentId) { + // no service handler - nothing to do + } + + @Override + protected void verifyTwoCreateAndRevertedDeleteEvents() { + // no service handler - nothing to do + } +} 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 000000000..35a3b549b --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java @@ -0,0 +1,296 @@ +/* + * © 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 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; +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.RootEntityBuilder; +import com.sap.cds.ql.Select; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +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"; + private static final ObjectMapper objectMapper = new ObjectMapper(); + + 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()); + } + + @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()); + } + + @ParameterizedTest + @CsvSource({ + // valid cases + "'test1.jpeg|test2.jpeg',201", + // invalid media types + "'test.pdf',415", + "'test1.jpeg|test2.pdf',415", + // invalid filenames + "'',400", + "' ',400", + // edge cases + "'.gitignore',415" + }) + void shouldValidateMediaTypes_forMultipleAttachments(String fileNames, int expectedStatus) + throws Exception { + String payload = buildPayload(fileNames); + requestHelper.executePostWithMatcher(BASE_URL, payload, status().is(expectedStatus)); + } + + @Test + void shouldAcceptWhenMediaValidatedAttachments_hasNoAttachments() throws Exception { + Map payload = new HashMap<>(); + payload.put("title", "Hello World!"); + payload.put("mediaValidatedAttachments", List.of()); + + String payloadStr = objectMapper.writeValueAsString(payload); + requestHelper.executePostWithMatcher(BASE_URL, payloadStr, status().is(201)); + } + + @Test + void shouldAcceptDeepCreate_whenMixedValidAndAllValidAttachments() throws Exception { + Map payload = new HashMap<>(); + payload.put("title", "Hello World!"); + payload.put( + "mediaValidatedAttachments", + List.of(Map.of("fileName", "test1.jpeg"), Map.of("fileName", "test2.jpeg"))); + + payload.put("mimeValidatedAttachments", List.of(Map.of("fileName", "test3.pdf"))); + + requestHelper.executePostWithMatcher( + BASE_URL, objectMapper.writeValueAsString(payload), status().isCreated()); + } + + @Test + void shouldRejectDeepCreate_whenMixedValidAndInvalidAttachments() throws Exception { + Map payload = new HashMap<>(); + payload.put("title", "Hello World!"); + payload.put( + "mediaValidatedAttachments", + List.of(Map.of("fileName", "test1.pdf"), Map.of("fileName", "test2.jpeg"))); + + payload.put("mimeValidatedAttachments", List.of(Map.of("fileName", "test3.pdf"))); + + requestHelper.executePostWithMatcher( + BASE_URL, objectMapper.writeValueAsString(payload), status().isUnsupportedMediaType()); + } + + private String createRootAndReturnId() throws Exception { + // Build the initial Java object.. Root + Roots serviceRoot = buildServiceRoot(); + + // 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 buildPayload(String fileNames) throws JsonProcessingException { + List> attachments = new ArrayList<>(); + fileNames = fileNames.replaceAll("^'+|'+$", ""); + for (String name : fileNames.split("\\|")) { + attachments.add(Map.of("fileName", name)); + } + Map payload = new HashMap<>(); + payload.put("title", "Hello World!"); + payload.put("mediaValidatedAttachments", attachments); + + return objectMapper.writeValueAsString(payload); + } + + private String createUrl(String rootId, String path) { + return BASE_URL + "(" + rootId + ")" + (path == null || path.isBlank() ? "" : "/" + path); + } + + private String createAttachmentMetadata(String fileName) throws JsonProcessingException { + return objectMapper.writeValueAsString(Map.of("fileName", fileName)); + } + + // helper method + private Roots buildServiceRoot() { + return RootEntityBuilder.create().setTitle("Root").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/OdataRequestValidationBase.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationBase.java new file mode 100644 index 000000000..a4774951f --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationBase.java @@ -0,0 +1,884 @@ +/* + * © 2024-2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.nondraftservice; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +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.AttachmentEntity_; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.Items; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.Items_; +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.common.TableDataDeleter; +import com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper.AttachmentsBuilder; +import com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper.AttachmentsEntityBuilder; +import com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper.ItemEntityBuilder; +import com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper.RootEntityBuilder; +import com.sap.cds.feature.attachments.integrationtests.testhandler.TestPersistenceHandler; +import com.sap.cds.feature.attachments.integrationtests.testhandler.TestPluginAttachmentsServiceHandler; +import com.sap.cds.feature.attachments.service.AttachmentService; +import com.sap.cds.ql.Select; +import com.sap.cds.ql.StructuredType; +import com.sap.cds.ql.cqn.CqnSelect; +import com.sap.cds.services.persistence.PersistenceService; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultMatcher; + +@SpringBootTest +@AutoConfigureMockMvc +abstract class OdataRequestValidationBase { + + protected static final Logger logger = LoggerFactory.getLogger(OdataRequestValidationBase.class); + + @Autowired(required = false) + protected TestPluginAttachmentsServiceHandler serviceHandler; + + @Autowired protected MockHttpRequestHelper requestHelper; + @Autowired protected PersistenceService persistenceService; + @Autowired private TableDataDeleter dataDeleter; + @Autowired private TestPersistenceHandler testPersistenceHandler; + + @AfterEach + void teardown() { + dataDeleter.deleteData(Roots_.CDS_NAME); + clearServiceHandlerContext(); + clearServiceHandlerDocuments(); + requestHelper.resetHelper(); + testPersistenceHandler.reset(); + } + + @Test + void deepCreateWorks() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + verifySelectedRoot(selectedRoot, serviceRoot); + verifyNoAttachmentEventsCalled(); + } + + @Test + void putContentWorksForUrlsWithNavigation() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachment(selectedRoot); + var itemAttachment = getRandomItemAttachment(item); + var content = putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); + var attachment = selectUpdatedAttachmentWithExpand(selectedRoot, itemAttachment); + + verifyContentAndContentId(attachment, content, itemAttachment); + verifySingleCreateEvent(attachment.getContentId(), content); + } + + @Test + void putContentWorksForUrlsWithoutNavigation() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachmentEntity(selectedRoot); + var itemAttachment = getRandomItemAttachmentEntity(item); + + var content = putContentForAttachmentWithoutNavigation(itemAttachment); + var attachment = selectUpdatedAttachment(itemAttachment); + + verifyContentAndContentIdForAttachmentEntity(attachment, content, itemAttachment); + verifySingleCreateEvent(attachment.getContentId(), content); + } + + @Test + void expandReadOfAttachmentsHasNoFilledContent() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachment(selectedRoot); + + var url = buildExpandAttachmentUrl(selectedRoot.getId(), item.getId()); + var responseItem = + requestHelper.executeGetWithSingleODataResponseAndAssertStatus( + url, Items.class, HttpStatus.OK); + + assertThat(responseItem.getAttachments()).hasSameSizeAs(item.getAttachments()); + assertThat(responseItem.getAttachments()) + .allSatisfy( + attachment -> { + assertThat(attachment.getContent()).isNull(); + assertThat(attachment.get("content@mediaContentType")).isNull(); + assertThat(attachment.getContentId()).isNull(); + }); + verifyNoAttachmentEventsCalled(); + } + + @Test + void navigationReadOfAttachmentsHasFilledContent() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachment(selectedRoot); + var itemAttachment = getRandomItemAttachment(item); + var content = putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); + + var url = buildExpandAttachmentUrl(selectedRoot.getId(), item.getId()); + var responseItem = + requestHelper.executeGetWithSingleODataResponseAndAssertStatus( + url, Items.class, HttpStatus.OK); + + assertThat(responseItem.getAttachments()).hasSameSizeAs(item.getAttachments()); + + var attachmentWithExpectedContent = + responseItem.getAttachments().stream() + .filter(attach -> attach.getId().equals(itemAttachment.getId())) + .findAny() + .orElseThrow(); + assertThat(attachmentWithExpectedContent) + .containsEntry("content@mediaContentType", "application/octet-stream;charset=UTF-8") + .containsEntry(Attachments.FILE_NAME, itemAttachment.getFileName()); + assertThat(attachmentWithExpectedContent.getStatus()).isNotEmpty(); + verifyContentId( + attachmentWithExpectedContent, itemAttachment.getId(), itemAttachment.getContentId()); + verifySingleCreateEvent(attachmentWithExpectedContent.getContentId(), content); + } + + @Test + void navigationReadOfAttachmentsReturnsContent() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachment(selectedRoot); + var itemAttachment = getRandomItemAttachment(item); + var content = putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); + clearServiceHandlerContext(); + var selectedItemAfterChange = selectItem(item); + var itemAttachmentAfterChange = getRandomItemAttachment(selectedItemAfterChange); + + var url = + buildNavigationAttachmentUrl(selectedRoot.getId(), item.getId(), itemAttachment.getId()) + + "/content"; + executeContentRequestAndValidateContent(url, content); + verifySingleReadEvent(itemAttachmentAfterChange.getContentId()); + } + + @Test + void navigationDeleteOfContentClears() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachment(selectedRoot); + var itemAttachment = getRandomItemAttachment(item); + putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); + clearServiceHandlerContext(); + var selectedItemAfterChange = selectItem(item); + var itemAttachmentAfterChange = getRandomItemAttachment(selectedItemAfterChange); + + executeDeleteAndCheckNoDataCanBeRead( + buildNavigationAttachmentUrl(selectedRoot.getId(), item.getId(), itemAttachment.getId()), + itemAttachmentAfterChange.getContentId()); + + var expandUrl = buildExpandAttachmentUrl(selectedRoot.getId(), item.getId()); + var responseItem = + requestHelper.executeGetWithSingleODataResponseAndAssertStatus( + expandUrl, Items.class, HttpStatus.OK); + + assertThat(responseItem.getAttachments()).hasSameSizeAs(item.getAttachments()); + assertThat(responseItem.getAttachments()) + .allSatisfy( + attachment -> { + assertThat(attachment.getContent()).isNull(); + assertThat(attachment.get("content@mediaContentType")).isNull(); + assertThat(attachment.getContentId()).isNull(); + }); + verifyNoAttachmentEventsCalled(); + } + + @Test + void navigationDeleteOfAttachmentClearsContentField() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachment(selectedRoot); + var itemAttachment = getRandomItemAttachment(item); + putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); + clearServiceHandlerContext(); + var selectedItemAfterChange = selectItem(item); + var itemAttachmentAfterChange = getRandomItemAttachment(selectedItemAfterChange); + + var url = + buildNavigationAttachmentUrl(selectedRoot.getId(), item.getId(), itemAttachment.getId()); + requestHelper.executeDelete(url); + var expandUrl = buildExpandAttachmentUrl(selectedRoot.getId(), item.getId()); + var responseItem = + requestHelper.executeGetWithSingleODataResponseAndAssertStatus( + expandUrl, Items.class, HttpStatus.OK); + + assertThat(responseItem.getAttachments()).hasSize(1); + assertThat(responseItem.getAttachments()) + .first() + .satisfies( + attachment -> { + assertThat(attachment.getContent()).isNull(); + assertThat(attachment.get("content@mediaContentType")).isNull(); + assertThat(attachment.getContentId()).isNull(); + }); + verifySingleDeletionEvent(itemAttachmentAfterChange.getContentId()); + } + + @Test + void navigationDeleteCallsTwiceReturnsError() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachment(selectedRoot); + var itemAttachment = getRandomItemAttachment(item); + putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); + clearServiceHandlerContext(); + var selectedItemAfterChange = selectItem(item); + var itemAttachmentAfterChange = getRandomItemAttachment(selectedItemAfterChange); + + var url = + buildNavigationAttachmentUrl(selectedRoot.getId(), item.getId(), itemAttachment.getId()); + requestHelper.executeDelete(url); + var result = requestHelper.executeDelete(url); + + assertThat(result.getResponse().getStatus()).isEqualTo(HttpStatus.PRECONDITION_FAILED.value()); + verifySingleDeletionEvent(itemAttachmentAfterChange.getContentId()); + } + + @Test + void directReadOfAttachmentsHasNoContentFilled() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachmentEntity(selectedRoot); + var itemAttachment = getRandomItemAttachmentEntity(item); + + var url = buildDirectAttachmentEntityUrl(itemAttachment.getId()); + var responseAttachment = + requestHelper.executeGetWithSingleODataResponseAndAssertStatus( + url, Attachments.class, HttpStatus.OK); + + assertThat(responseAttachment.get("content@mediaContentType")).isNull(); + assertThat(responseAttachment.getContentId()).isNull(); + assertThat(responseAttachment.getFileName()).isEqualTo(itemAttachment.getFileName()); + verifyNoAttachmentEventsCalled(); + } + + @Test + void directReadOfAttachmentsHasFilledContent() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachmentEntity(selectedRoot); + var itemAttachment = getRandomItemAttachmentEntity(item); + putContentForAttachmentWithoutNavigation(itemAttachment); + clearServiceHandlerContext(); + + var url = buildDirectAttachmentEntityUrl(itemAttachment.getId()); + var responseAttachment = + requestHelper.executeGetWithSingleODataResponseAndAssertStatus( + url, Attachments.class, HttpStatus.OK); + + assertThat(responseAttachment) + .containsEntry("content@mediaContentType", "application/octet-stream;charset=UTF-8") + .containsEntry(Attachments.FILE_NAME, itemAttachment.getFileName()); + verifyContentId(responseAttachment, itemAttachment.getId(), itemAttachment.getContentId()); + verifyNoAttachmentEventsCalled(); + } + + @Test + void directReadOfAttachmentsReturnsContent() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachmentEntity(selectedRoot); + var itemAttachment = getRandomItemAttachmentEntity(item); + var content = putContentForAttachmentWithoutNavigation(itemAttachment); + clearServiceHandlerContext(); + var selectedItemAfterChange = selectItem(item); + var itemAttachmentAfterChange = getRandomItemAttachmentEntity(selectedItemAfterChange); + + var url = buildDirectAttachmentEntityUrl(itemAttachment.getId()) + "/content"; + executeContentRequestAndValidateContent(url, content); + verifySingleReadEvent(itemAttachmentAfterChange.getContentId()); + } + + @Test + void directDeleteOfContentClears() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachmentEntity(selectedRoot); + var itemAttachment = getRandomItemAttachmentEntity(item); + putContentForAttachmentWithoutNavigation(itemAttachment); + clearServiceHandlerContext(); + var selectedItemAfterChange = selectItem(item); + var itemAttachmentAfterChange = getRandomItemAttachmentEntity(selectedItemAfterChange); + + executeDeleteAndCheckNoDataCanBeRead( + buildDirectAttachmentEntityUrl(itemAttachment.getId()), + itemAttachmentAfterChange.getContentId()); + + var expandUrl = buildExpandAttachmentUrl(selectedRoot.getId(), item.getId()); + var responseItem = + requestHelper.executeGetWithSingleODataResponseAndAssertStatus( + expandUrl, Items.class, HttpStatus.OK); + + assertThat(responseItem.getAttachmentEntities()).hasSameSizeAs(item.getAttachmentEntities()); + assertThat(responseItem.getAttachmentEntities()) + .allSatisfy( + attachment -> { + assertThat(attachment.getContent()).isNull(); + assertThat(attachment.get("content@mediaContentType")).isNull(); + assertThat(attachment.getContentId()).isNull(); + }); + verifyNoAttachmentEventsCalled(); + } + + @Test + void directDeleteOfAttachmentClearsContentField() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachmentEntity(selectedRoot); + var itemAttachment = getRandomItemAttachmentEntity(item); + putContentForAttachmentWithoutNavigation(itemAttachment); + clearServiceHandlerContext(); + var selectedItemAfterChange = selectItem(item); + var itemAttachmentAfterChange = getRandomItemAttachmentEntity(selectedItemAfterChange); + + var url = buildDirectAttachmentEntityUrl(itemAttachment.getId()); + requestHelper.executeDelete(url); + var expandUrl = buildExpandAttachmentUrl(selectedRoot.getId(), item.getId()); + var responseItem = + requestHelper.executeGetWithSingleODataResponseAndAssertStatus( + expandUrl, Items.class, HttpStatus.OK); + + assertThat(responseItem.getAttachmentEntities()).isEmpty(); + verifySingleDeletionEvent(itemAttachmentAfterChange.getContentId()); + } + + @Test + void directDeleteCalledTwiceReturnsError() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachmentEntity(selectedRoot); + var itemAttachment = getRandomItemAttachmentEntity(item); + putContentForAttachmentWithoutNavigation(itemAttachment); + clearServiceHandlerContext(); + + var url = buildDirectAttachmentEntityUrl(itemAttachment.getId()); + requestHelper.executeDelete(url); + MvcResult mvcResult = requestHelper.executeDelete(url); + + assertThat(mvcResult.getResponse().getStatus()) + .isEqualTo(HttpStatus.PRECONDITION_FAILED.value()); + if (Objects.nonNull(serviceHandler)) { + Awaitility.await().until(() -> serviceHandler.getEventContext().size() == 1); + verifyNumberOfEvents(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED, 1); + verifyEventContextEmptyForEvent( + AttachmentService.EVENT_CREATE_ATTACHMENT, AttachmentService.EVENT_READ_ATTACHMENT); + } + } + + @Test + void rootDeleteDeletesAllContents() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachmentEntity(selectedRoot); + var itemAttachmentEntity = getRandomItemAttachmentEntity(item); + var itemAttachment = getRandomItemAttachment(item); + + putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); + putContentForAttachmentWithoutNavigation(itemAttachmentEntity); + verifyNumberOfEvents(AttachmentService.EVENT_CREATE_ATTACHMENT, 2); + clearServiceHandlerContext(); + var selectedItemAfterChange = selectItem(item); + var itemAttachmentEntityAfterChange = getRandomItemAttachmentEntity(selectedItemAfterChange); + var itemAttachmentAfterChange = getRandomItemAttachment(selectedItemAfterChange); + + var url = + MockHttpRequestHelper.ODATA_BASE_URL + "TestService/Roots(" + selectedRoot.getId() + ")"; + requestHelper.executeDeleteWithMatcher(url, status().isNoContent()); + + verifyTwoDeleteEvents(itemAttachmentEntityAfterChange, itemAttachmentAfterChange); + } + + @Test + void updateContentWorksForUrlsWithNavigation() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachment(selectedRoot); + var itemAttachment = getRandomItemAttachment(item); + itemAttachment.setNote("note 1"); + putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); + itemAttachment = selectUpdatedAttachmentWithExpand(selectedRoot, itemAttachment); + itemAttachment.setNote("note 2"); + var content = putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); + var attachment = selectUpdatedAttachmentWithExpand(selectedRoot, itemAttachment); + + verifyContentAndContentId(attachment, content, itemAttachment); + verifySingleCreateAndUpdateEvent( + attachment.getContentId(), itemAttachment.getContentId(), content); + } + + @Test + void updateContentWorksForUrlsWithoutNavigation() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachmentEntity(selectedRoot); + var itemAttachment = getRandomItemAttachmentEntity(item); + itemAttachment.setNote("note 1"); + putContentForAttachmentWithoutNavigation(itemAttachment); + itemAttachment = selectUpdatedAttachment(itemAttachment); + itemAttachment.setNote("note 2"); + var content = putContentForAttachmentWithoutNavigation(itemAttachment); + var attachment = selectUpdatedAttachment(itemAttachment); + + verifyContentAndContentIdForAttachmentEntity(attachment, content, itemAttachment); + verifySingleCreateAndUpdateEvent( + attachment.getContentId(), itemAttachment.getContentId(), content); + } + + @Test + void errorInTransactionAfterCreateCallsDelete() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachment(selectedRoot); + var itemAttachment = getRandomItemAttachment(item); + testPersistenceHandler.setThrowExceptionOnUpdate(true); + putContentForAttachmentWithNavigation( + selectedRoot, itemAttachment, status().is5xxServerError()); + var attachment = selectUpdatedAttachmentWithExpand(selectedRoot, itemAttachment); + + assertThat(attachment.getContentId()).isEqualTo(itemAttachment.getContentId()); + assertThat(attachment.getContent()).isEqualTo(itemAttachment.getContent()); + } + + @Test + void updateContentWithErrorsResetsForUrlsWithNavigation() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachment(selectedRoot); + var itemAttachment = getRandomItemAttachment(item); + itemAttachment.setNote("note 1"); + var content = putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); + itemAttachment = selectUpdatedAttachmentWithExpand(selectedRoot, itemAttachment); + itemAttachment.setNote("note 2"); + testPersistenceHandler.setThrowExceptionOnUpdate(true); + putContentForAttachmentWithNavigation( + selectedRoot, itemAttachment, status().is5xxServerError()); + var attachment = selectUpdatedAttachmentWithExpand(selectedRoot, itemAttachment); + + verifyContentAndContentId(attachment, content, itemAttachment); + assertThat(attachment.getContentId()).isEqualTo(itemAttachment.getContentId()); + verifySingleCreateAndUpdateEvent( + attachment.getContentId(), itemAttachment.getContentId(), content); + } + + @Test + void updateContentWithErrorResetsForUrlsWithoutNavigation() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachmentEntity(selectedRoot); + var itemAttachment = getRandomItemAttachmentEntity(item); + itemAttachment.setNote("note 1"); + var content = putContentForAttachmentWithoutNavigation(itemAttachment); + itemAttachment = selectUpdatedAttachment(itemAttachment); + itemAttachment.setNote("note 2"); + testPersistenceHandler.setThrowExceptionOnUpdate(true); + putContentForAttachmentWithoutNavigation(itemAttachment, status().is5xxServerError()); + var attachment = selectUpdatedAttachment(itemAttachment); + + verifyContentAndContentIdForAttachmentEntity(attachment, content, itemAttachment); + assertThat(attachment.getContentId()).isEqualTo(itemAttachment.getContentId()); + verifySingleCreateAndUpdateEvent( + attachment.getContentId(), itemAttachment.getContentId(), content); + } + + @ParameterizedTest + @CsvSource({"status,INFECTED", "contentId,TEST"}) + void statusCannotBeUpdated(String field, String value) throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachmentEntity(selectedRoot); + var itemAttachment = getRandomItemAttachmentEntity(item); + putContentForAttachmentWithoutNavigation(itemAttachment); + itemAttachment.setStatus(value); + var url = buildDirectAttachmentEntityUrl(itemAttachment.getId()); + + requestHelper.resetHelper(); + requestHelper.executePatchWithODataResponseAndAssertStatus( + url, "{\"" + field + "\":\"" + value + "\"}", HttpStatus.OK); + + selectedRoot = selectStoredRootWithDeepData(); + item = getItemWithAttachmentEntity(selectedRoot); + itemAttachment = getRandomItemAttachmentEntity(item); + assertThat(itemAttachment.get(field)).isNotNull().isNotEqualTo(value); + } + + @Test + void wrongEtagCouldNotBeUpdated() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachmentEntity(selectedRoot); + var itemAttachment = getRandomItemAttachmentEntity(item); + + var url = buildDirectAttachmentEntityUrl(itemAttachment.getId()); + requestHelper.executePatchWithODataResponseAndAssertStatus( + url, + "{\"fileName\":\"test_for_change.txt\"}", + "W/\"2024-05-06T15:24:29.657713600Z\"", + HttpStatus.PRECONDITION_FAILED); + + var selectedRootAfterChange = selectStoredRootWithDeepData(); + var itemAfterChange = getItemWithAttachmentEntity(selectedRootAfterChange); + var itemAttachmentAfterChange = getRandomItemAttachmentEntity(itemAfterChange); + assertThat(itemAttachmentAfterChange.getFileName()).isEqualTo(itemAttachment.getFileName()); + } + + @Test + void correctEtagCanBeUpdated() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachmentEntity(selectedRoot); + var itemAttachment = getRandomItemAttachmentEntity(item); + var modifiedAt = itemAttachment.getModifiedAt(); + var eTag = "W/\"" + modifiedAt + "\""; + + var url = buildDirectAttachmentEntityUrl(itemAttachment.getId()); + requestHelper.executePatchWithODataResponseAndAssertStatus( + url, "{\"fileName\":\"test_for_change.txt\"}", eTag, HttpStatus.OK); + + var selectedRootAfterChange = selectStoredRootWithDeepData(); + var itemAfterChange = getItemWithAttachmentEntity(selectedRootAfterChange); + var itemAttachmentAfterChange = getRandomItemAttachmentEntity(itemAfterChange); + assertThat(itemAttachmentAfterChange.getFileName()).isEqualTo("test_for_change.txt"); + } + + protected Items selectItem(Items item) { + var selectedRootAfterContentCreated = selectStoredRootWithDeepData(); + return selectedRootAfterContentCreated.getItems().stream() + .filter(i -> i.getId().equals(item.getId())) + .findAny() + .orElseThrow(); + } + + protected Roots buildServiceRootWithDeepData() { + return RootEntityBuilder.create() + .setTitle("some root title") + .addAttachments( + AttachmentsEntityBuilder.create().setFileName("fileRoot.txt").setMimeType("text/plain")) + .addItems( + ItemEntityBuilder.create() + .setTitle("some item 1 title") + .addAttachments( + AttachmentsBuilder.create() + .setFileName("fileItem1.txt") + .setMimeType("text/plain"), + AttachmentsBuilder.create() + .setFileName("fileItem2.txt") + .setMimeType("text/plain")), + ItemEntityBuilder.create() + .setTitle("some item 2 title") + .addAttachmentEntities( + AttachmentsEntityBuilder.create() + .setFileName("fileItem3.text") + .setMimeType("text/plain")) + .addAttachments( + AttachmentsBuilder.create() + .setFileName("fileItem3.text") + .setMimeType("text/plain"))) + .build(); + } + + protected void postServiceRoot(Roots serviceRoot) throws Exception { + var url = MockHttpRequestHelper.ODATA_BASE_URL + "TestService/Roots"; + requestHelper.executePostWithMatcher(url, serviceRoot.toJson(), status().isCreated()); + } + + protected Roots selectStoredRootWithDeepData() { + CqnSelect select = + Select.from(Roots_.class) + .columns( + StructuredType::_all, + root -> root.attachments().expand(), + root -> + root.items() + .expand( + StructuredType::_all, + item -> item.attachments().expand(), + item -> item.attachmentEntities().expand())); + var result = persistenceService.run(select); + return result.single(Roots.class); + } + + private void verifySelectedRoot(Roots selectedRoot, Roots serviceRoot) { + assertThat(selectedRoot.getId()).isNotEmpty(); + assertThat(selectedRoot.getTitle()).isEqualTo(serviceRoot.getTitle()); + assertThat(selectedRoot.getAttachments()) + .hasSize(1) + .first() + .satisfies( + attachment -> { + assertThat(attachment.getId()).isNotEmpty(); + assertThat(attachment.getFileName()) + .isEqualTo(serviceRoot.getAttachments().get(0).getFileName()); + assertThat(attachment.getMimeType()) + .isEqualTo(serviceRoot.getAttachments().get(0).getMimeType()); + }); + assertThat(selectedRoot.getItems()) + .hasSize(2) + .first() + .satisfies( + item -> { + assertThat(item.getId()).isNotEmpty(); + assertThat(item.getTitle()).isEqualTo(serviceRoot.getItems().get(0).getTitle()); + assertThat(item.getAttachments()).hasSize(2); + }); + assertThat(selectedRoot.getItems().get(1).getId()).isNotEmpty(); + assertThat(selectedRoot.getItems().get(1).getTitle()) + .isEqualTo(serviceRoot.getItems().get(1).getTitle()); + assertThat(selectedRoot.getItems().get(1).getAttachments()).hasSize(1); + } + + protected Attachments getRandomItemAttachment(Items selectedItem) { + return selectedItem.getAttachments().get(0); + } + + protected Attachments getRandomRootSizeLimitedAttachment(Roots selectedRoot) { + return selectedRoot.getSizeLimitedAttachments().get(0); + } + + private AttachmentEntity getRandomItemAttachmentEntity(Items selectedItem) { + return selectedItem.getAttachmentEntities().get(0); + } + + protected Items getItemWithAttachment(Roots selectedRoot) { + return selectedRoot.getItems().stream() + .filter(item -> !item.getAttachments().isEmpty()) + .findAny() + .orElseThrow(); + } + + private Items getItemWithAttachmentEntity(Roots selectedRoot) { + return selectedRoot.getItems().stream() + .filter(item -> !item.getAttachmentEntities().isEmpty()) + .findAny() + .orElseThrow(); + } + + protected String putContentForAttachmentWithNavigation( + Roots selectedRoot, Attachments itemAttachment) throws Exception { + return putContentForAttachmentWithNavigation( + selectedRoot, itemAttachment, status().isNoContent()); + } + + private String putContentForAttachmentWithNavigation( + Roots selectedRoot, Attachments itemAttachment, ResultMatcher matcher) throws Exception { + var selectedItem = + selectedRoot.getItems().stream() + .filter( + item -> + item.getAttachments().stream() + .anyMatch(attach -> attach.getId().equals(itemAttachment.getId()))) + .findAny() + .orElseThrow(); + var url = + buildNavigationAttachmentUrl( + selectedRoot.getId(), selectedItem.getId(), itemAttachment.getId()) + + "/content"; + + var testContent = "testContent" + itemAttachment.getNote(); + requestHelper.setContentType(MediaType.APPLICATION_OCTET_STREAM); + requestHelper.executePutWithMatcher(url, testContent.getBytes(StandardCharsets.UTF_8), matcher); + return testContent; + } + + protected String buildNavigationAttachmentUrl(String rootId, String itemId, String attachmentId) { + return "/odata/v4/TestService/Roots(" + + rootId + + ")/items(" + + itemId + + ")" + + "/attachments(ID=" + + attachmentId + + ",up__ID=" + + itemId + + ")"; + } + + protected String buildNavigationSizeLimitedAttachmentUrl(String rootId, String attachmentId) { + return "/odata/v4/TestService/Roots(" + + rootId + + ")/sizeLimitedAttachments(ID=" + + attachmentId + + ",up__ID=" + + rootId + + ")"; + } + + protected String putContentForSizeLimitedAttachment(Roots selectedRoot, Attachments attachment) + throws Exception { + return putContentForSizeLimitedAttachment(selectedRoot, attachment, status().isNoContent()); + } + + protected String putContentForSizeLimitedAttachment( + Roots selectedRoot, Attachments attachment, ResultMatcher matcher) throws Exception { + var url = + buildNavigationSizeLimitedAttachmentUrl(selectedRoot.getId(), attachment.getId()) + + "/content"; + var testContent = "testContent" + attachment.getNote(); + requestHelper.setContentType(MediaType.APPLICATION_OCTET_STREAM); + requestHelper.executePutWithMatcher(url, testContent.getBytes(StandardCharsets.UTF_8), matcher); + return testContent; + } + + protected String buildExpandAttachmentUrl(String rootId, String itemId) { + return "/odata/v4/TestService/Roots(" + + rootId + + ")/items(" + + itemId + + ")" + + "?$expand=attachments,attachmentEntities"; + } + + private String putContentForAttachmentWithoutNavigation(AttachmentEntity itemAttachment) + throws Exception { + return putContentForAttachmentWithoutNavigation(itemAttachment, status().isNoContent()); + } + + private String putContentForAttachmentWithoutNavigation( + AttachmentEntity itemAttachment, ResultMatcher matcher) throws Exception { + var url = buildDirectAttachmentEntityUrl(itemAttachment.getId()) + "/content"; + var testContent = "testContent" + itemAttachment.getNote(); + requestHelper.setContentType(MediaType.APPLICATION_OCTET_STREAM); + requestHelper.executePutWithMatcher(url, testContent.getBytes(StandardCharsets.UTF_8), matcher); + return testContent; + } + + private String buildDirectAttachmentEntityUrl(String attachmentId) { + return MockHttpRequestHelper.ODATA_BASE_URL + + "TestService/AttachmentEntity(" + + attachmentId + + ")"; + } + + private Attachments selectUpdatedAttachmentWithExpand( + Roots selectedRoot, Attachments itemAttachment) { + CqnSelect attachmentSelect = + Select.from(Items_.class) + .where(a -> a.ID().eq(selectedRoot.getItems().get(0).getId())) + .columns(item -> item.attachments().expand()); + var result = persistenceService.run(attachmentSelect); + var items = result.single(Items.class); + return items.getAttachments().stream() + .filter(attach -> itemAttachment.getId().equals(attach.getId())) + .findAny() + .orElseThrow(); + } + + private AttachmentEntity selectUpdatedAttachment(AttachmentEntity itemAttachment) { + CqnSelect attachmentSelect = + Select.from(AttachmentEntity_.class).where(a -> a.ID().eq(itemAttachment.getId())); + var result = persistenceService.run(attachmentSelect); + return result.single(AttachmentEntity.class); + } + + private void executeDeleteAndCheckNoDataCanBeRead(String baseUrl, String contentId) + throws Exception { + var url = baseUrl + "/content"; + requestHelper.executeDelete(url); + verifySingleDeletionEvent(contentId); + clearServiceHandlerContext(); + var response = requestHelper.executeGet(url); + + assertThat(response.getResponse().getContentLength()).isZero(); + assertThat(response.getResponse().getStatus()).isEqualTo(HttpStatus.NO_CONTENT.value()); + } + + protected abstract void executeContentRequestAndValidateContent(String url, String content) + throws Exception; + + protected abstract void verifyTwoDeleteEvents( + AttachmentEntity itemAttachmentEntityAfterChange, Attachments itemAttachmentAfterChange); + + protected abstract void verifyNumberOfEvents(String event, int number); + + protected abstract void verifyContentId( + Attachments attachmentWithExpectedContent, String attachmentId, String contentId); + + protected abstract void verifyContentAndContentId( + Attachments attachment, String testContent, Attachments itemAttachment) throws IOException; + + protected abstract void verifyContentAndContentIdForAttachmentEntity( + AttachmentEntity attachment, String testContent, AttachmentEntity itemAttachment) + throws IOException; + + protected abstract void clearServiceHandlerContext(); + + protected abstract void clearServiceHandlerDocuments(); + + protected abstract void verifySingleCreateEvent(String contentId, String content); + + protected abstract void verifySingleCreateAndUpdateEvent( + String resultContentId, String toBeDeletedContentId, String content); + + protected abstract void verifySingleDeletionEvent(String contentId); + + protected abstract void verifySingleReadEvent(String contentId); + + protected abstract void verifyNoAttachmentEventsCalled(); + + protected abstract void verifyEventContextEmptyForEvent(String... events); +} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithTestHandlerTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithTestHandlerTest.java new file mode 100644 index 000000000..94fca3378 --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithTestHandlerTest.java @@ -0,0 +1,245 @@ +/* + * © 2024-2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.nondraftservice; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.sap.cds.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.integrationtests.constants.Profiles; +import com.sap.cds.feature.attachments.integrationtests.testhandler.EventContextHolder; +import com.sap.cds.feature.attachments.service.AttachmentService; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentCreateEventContext; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentMarkAsDeletedEventContext; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentReadEventContext; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles(Profiles.TEST_HANDLER_ENABLED) +class OdataRequestValidationWithTestHandlerTest extends OdataRequestValidationBase { + + @Test + void serviceHandlerAvailable() { + assertThat(serviceHandler).isNotNull(); + } + + @Override + protected void executeContentRequestAndValidateContent(String url, String content) + throws Exception { + var response = requestHelper.executeGet(url); + assertThat(response.getResponse().getContentAsString()).isEqualTo(content); + } + + @Override + protected void verifyTwoDeleteEvents( + AttachmentEntity itemAttachmentEntityAfterChange, Attachments itemAttachmentAfterChange) { + waitTillExpectedHandlerMessageSize(2); + verifyEventContextEmptyForEvent( + AttachmentService.EVENT_READ_ATTACHMENT, AttachmentService.EVENT_CREATE_ATTACHMENT); + var deleteEvents = + serviceHandler.getEventContextForEvent(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); + assertThat(deleteEvents).hasSize(2); + assertThat( + deleteEvents.stream() + .anyMatch( + verifyContentIdAndUserInfo(itemAttachmentEntityAfterChange.getContentId()))) + .isTrue(); + assertThat( + deleteEvents.stream() + .anyMatch(verifyContentIdAndUserInfo(itemAttachmentAfterChange.getContentId()))) + .isTrue(); + } + + @Override + protected void verifyNumberOfEvents(String event, int number) { + assertThat(serviceHandler.getEventContextForEvent(event)).hasSize(number); + } + + @Override + protected void verifyContentId( + Attachments attachmentWithExpectedContent, String attachmentId, String contentId) { + assertThat(attachmentWithExpectedContent.getContentId()).isNotEmpty().isNotEqualTo(contentId); + } + + @Override + protected void verifyContentAndContentId( + Attachments attachment, String content, Attachments itemAttachment) { + assertThat(attachment.getContent()).isNull(); + assertThat(attachment.getContentId()).isNotEmpty().isNotEqualTo(itemAttachment.getId()); + } + + @Override + protected void verifyContentAndContentIdForAttachmentEntity( + AttachmentEntity attachment, String content, AttachmentEntity itemAttachment) { + assertThat(attachment.getContent()).isNull(); + assertThat(attachment.getContentId()).isNotEmpty().isNotEqualTo(itemAttachment.getId()); + } + + @Override + protected void clearServiceHandlerContext() { + serviceHandler.clearEventContext(); + } + + @Override + protected void clearServiceHandlerDocuments() { + serviceHandler.clearDocuments(); + } + + @Override + protected void verifySingleCreateEvent(String contentId, String content) { + verifyEventContextEmptyForEvent( + AttachmentService.EVENT_READ_ATTACHMENT, + AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); + var createEvent = + serviceHandler.getEventContextForEvent(AttachmentService.EVENT_CREATE_ATTACHMENT); + assertThat(createEvent) + .hasSize(1) + .first() + .satisfies( + event -> { + assertThat(event.context()).isInstanceOf(AttachmentCreateEventContext.class); + var createContext = (AttachmentCreateEventContext) event.context(); + assertThat(createContext.getContentId()).isEqualTo(contentId); + assertThat(createContext.getData().getContent().readAllBytes()) + .isEqualTo(content.getBytes(StandardCharsets.UTF_8)); + }); + } + + @Override + protected void verifySingleCreateAndUpdateEvent( + String resultContentId, String toBeDeletedContentId, String content) { + waitTillExpectedHandlerMessageSize(3); + verifyEventContextEmptyForEvent(AttachmentService.EVENT_READ_ATTACHMENT); + var createEvents = + serviceHandler.getEventContextForEvent(AttachmentService.EVENT_CREATE_ATTACHMENT); + assertThat(createEvents).hasSize(2); + verifyCreateEventsContainsContentId(toBeDeletedContentId, createEvents); + verifyCreateEventsContainsContentId(resultContentId, createEvents); + var deleteEvents = + serviceHandler.getEventContextForEvent(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); + + var deleteContentId = + !resultContentId.equals(toBeDeletedContentId) + ? toBeDeletedContentId + : createEvents.stream() + .filter( + event -> + !resultContentId.equals( + ((AttachmentCreateEventContext) event.context()).getContentId())) + .findFirst() + .orElseThrow() + .context() + .get(Attachments.CONTENT_ID); + + var eventFound = + deleteEvents.stream() + .anyMatch( + event -> + ((AttachmentMarkAsDeletedEventContext) event.context()) + .getContentId() + .equals(deleteContentId)); + assertThat(eventFound).isTrue(); + } + + @Override + protected void verifySingleDeletionEvent(String contentId) { + waitTillExpectedHandlerMessageSize(1); + verifyEventContextEmptyForEvent( + AttachmentService.EVENT_CREATE_ATTACHMENT, AttachmentService.EVENT_READ_ATTACHMENT); + var deleteEvents = + serviceHandler.getEventContextForEvent(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); + assertThat(deleteEvents) + .hasSize(1) + .first() + .satisfies( + event -> { + assertThat(event.context()).isInstanceOf(AttachmentMarkAsDeletedEventContext.class); + var deleteContext = (AttachmentMarkAsDeletedEventContext) event.context(); + assertThat(deleteContext.getContentId()).isEqualTo(contentId); + assertThat(deleteContext.getDeletionUserInfo().getName()).isEqualTo("anonymous"); + assertThat(deleteContext.getDeletionUserInfo().getIsSystemUser()).isFalse(); + }); + } + + @Override + protected void verifySingleReadEvent(String contentId) { + verifyEventContextEmptyForEvent( + AttachmentService.EVENT_CREATE_ATTACHMENT, + AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); + var readContext = serviceHandler.getEventContext(); + assertThat(readContext) + .hasSize(1) + .first() + .satisfies( + event -> { + assertThat(event.event()).isEqualTo(AttachmentService.EVENT_READ_ATTACHMENT); + assertThat(((AttachmentReadEventContext) event.context()).getContentId()) + .isEqualTo(contentId); + }); + } + + @Override + protected void verifyNoAttachmentEventsCalled() { + assertThat(serviceHandler.getEventContext()).isEmpty(); + } + + @Override + protected void verifyEventContextEmptyForEvent(String... events) { + Arrays.stream(events) + .forEach(event -> assertThat(serviceHandler.getEventContextForEvent(event)).isEmpty()); + } + + private Predicate verifyContentIdAndUserInfo( + String itemAttachmentEntityAfterChange) { + return event -> + ((AttachmentMarkAsDeletedEventContext) event.context()) + .getContentId() + .equals(itemAttachmentEntityAfterChange) + && ((AttachmentMarkAsDeletedEventContext) event.context()) + .getDeletionUserInfo() + .getName() + .equals("anonymous") + && Boolean.FALSE.equals( + ((AttachmentMarkAsDeletedEventContext) event.context()) + .getDeletionUserInfo() + .getIsSystemUser()); + } + + private void verifyCreateEventsContainsContentId( + String contentId, List createEvents) { + assertThat( + createEvents.stream() + .anyMatch( + event -> + ((AttachmentCreateEventContext) event.context()) + .getContentId() + .equals(contentId))) + .isTrue(); + } + + private void waitTillExpectedHandlerMessageSize(int expectedSize) { + Awaitility.await() + .atMost(30, TimeUnit.SECONDS) + .pollDelay(1, TimeUnit.SECONDS) + .until( + () -> { + var eventCalls = serviceHandler.getEventContext().size(); + logger.debug( + "Waiting for expected size '{}' in handler context, was '{}'", + expectedSize, + eventCalls); + var numberMatch = eventCalls >= expectedSize; + if (!numberMatch) { + serviceHandler.getEventContext().forEach(event -> logger.info("Event: {}", event)); + } + return numberMatch; + }); + } +} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java new file mode 100644 index 000000000..725c4d775 --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java @@ -0,0 +1,138 @@ +/* + * © 2024-2025 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.nondraftservice; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static org.assertj.core.api.Assertions.assertThat; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.sap.attachments.Attachments; +import com.sap.cds.feature.attachments.integrationtests.common.MalwareScanResultProvider; +import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; +import java.util.concurrent.TimeUnit; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.ActiveProfiles; +import org.wiremock.spring.ConfigureWireMock; +import org.wiremock.spring.EnableWireMock; +import org.wiremock.spring.InjectWireMock; + +@ActiveProfiles(profiles = {Profiles.TEST_HANDLER_DISABLED, Profiles.MALWARE_SCAN_ENABLED}) +@EnableWireMock({@ConfigureWireMock(port = 1111, name = "malware-scanner")}) +class OdataRequestValidationWithoutTestHandlerAndMalwareScannerTest + extends OdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest { + + @InjectWireMock("malware-scanner") + private WireMockServer wiremock; + + @Autowired private MalwareScanResultProvider malwareScanResultProvider; + + @BeforeEach + void setup() { + mockMalwareScanResult(false); + } + + @Override + @AfterEach + void teardown() { + super.teardown(); + wiremock.resetAll(); + } + + @Test + void scannerReturnedMalwareContentCanNotBeRead() throws Exception { + wiremock.resetAll(); + mockMalwareScanResult(true); + + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachment(selectedRoot); + var itemAttachment = getRandomItemAttachment(item); + putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); + clearServiceHandlerContext(); + var selectedItemAfterChange = selectItem(item); + var itemAttachmentAfterChange = getRandomItemAttachment(selectedItemAfterChange); + + var url = + buildNavigationAttachmentUrl(selectedRoot.getId(), item.getId(), itemAttachment.getId()); + var contentUrl = url + "/content"; + waitAndVerifyContentErrorResponse(contentUrl, itemAttachmentAfterChange); + clearServiceHandlerContext(); + verifyAttachmentGetResponse(url, itemAttachmentAfterChange); + } + + @Test + void scannerReturnedErrorContentCanNotBeRead() throws Exception { + wiremock.resetAll(); + wiremock.stubFor( + post(urlEqualTo("/scan")) + .withBasicAuth("test-user", "test-password") + .willReturn(aResponse().withStatus(500))); + + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachment(selectedRoot); + var itemAttachment = getRandomItemAttachment(item); + putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); + clearServiceHandlerContext(); + var selectedItemAfterChange = selectItem(item); + var itemAttachmentAfterChange = getRandomItemAttachment(selectedItemAfterChange); + + var url = + buildNavigationAttachmentUrl(selectedRoot.getId(), item.getId(), itemAttachment.getId()); + var contentUrl = url + "/content"; + waitAndVerifyContentErrorResponse(contentUrl, itemAttachmentAfterChange); + clearServiceHandlerContext(); + verifyAttachmentGetResponse(url, itemAttachmentAfterChange); + } + + private void mockMalwareScanResult(boolean malware) { + wiremock.stubFor( + post(urlEqualTo("/scan")) + .withBasicAuth("test-user", "test-password") + .willReturn( + aResponse() + .withBody(malwareScanResultProvider.buildMalwareScanResult(malware)) + .withStatus(200))); + } + + private void waitAndVerifyContentErrorResponse( + String contentUrl, Attachments itemAttachmentAfterChange) throws Exception { + Awaitility.await() + .atMost(10, TimeUnit.SECONDS) + .until( + () -> { + var response = requestHelper.executeGet(contentUrl); + assertThat(response.getResponse().getStatus()) + .isEqualTo(HttpStatus.METHOD_NOT_ALLOWED.value()); + return response + .getResponse() + .getContentAsString() + .contains("Error text for not clean"); + }); + clearServiceHandlerContext(); + + var response = requestHelper.executeGet(contentUrl); + assertThat(response.getResponse().getStatus()).isEqualTo(HttpStatus.METHOD_NOT_ALLOWED.value()); + assertThat(response.getResponse().getContentAsString()).contains("Error text for not clean"); + verifySingleReadEvent(itemAttachmentAfterChange.getContentId()); + } + + private void verifyAttachmentGetResponse(String url, Attachments itemAttachmentAfterChange) + throws Exception { + var response = requestHelper.executeGet(url); + assertThat(response.getResponse().getStatus()).isEqualTo(HttpStatus.OK.value()); + verifySingleReadEvent(itemAttachmentAfterChange.getContentId()); + } +} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java new file mode 100644 index 000000000..edee93cb5 --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java @@ -0,0 +1,116 @@ +/* + * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.nondraftservice; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.sap.cds.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.integrationtests.constants.Profiles; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles(Profiles.TEST_HANDLER_DISABLED) +class OdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest + extends OdataRequestValidationBase { + + @Test + void serviceHandlerIsNull() { + assertThat(serviceHandler).isNull(); + } + + @Override + protected void executeContentRequestAndValidateContent(String url, String content) + throws Exception { + Awaitility.await() + .atMost(10, TimeUnit.SECONDS) + .until( + () -> { + var response = requestHelper.executeGet(url); + return response.getResponse().getContentAsString().equals(content); + }); + + var response = requestHelper.executeGet(url); + assertThat(response.getResponse().getContentAsString()).isEqualTo(content); + } + + @Override + protected void verifyTwoDeleteEvents( + AttachmentEntity itemAttachmentEntityAfterChange, Attachments itemAttachmentAfterChange) { + // no service handler - nothing to do + } + + @Override + protected void verifyNumberOfEvents(String event, int number) { + // no service handler - nothing to do + } + + @Override + protected void verifyContentId( + Attachments attachmentWithExpectedContent, String attachmentId, String contentId) { + assertThat(attachmentWithExpectedContent.getContentId()).isEqualTo(attachmentId); + } + + @Override + protected void verifyContentAndContentId( + Attachments attachment, String testContent, Attachments itemAttachment) throws IOException { + assertThat(attachment.getContent().readAllBytes()) + .isEqualTo(testContent.getBytes(StandardCharsets.UTF_8)); + assertThat(attachment.getContentId()).isEqualTo(itemAttachment.getId()); + } + + @Override + protected void verifyContentAndContentIdForAttachmentEntity( + AttachmentEntity attachment, String testContent, AttachmentEntity itemAttachment) + throws IOException { + assertThat(attachment.getContent().readAllBytes()) + .isEqualTo(testContent.getBytes(StandardCharsets.UTF_8)); + assertThat(attachment.getContentId()).isEqualTo(itemAttachment.getId()); + } + + @Override + protected void clearServiceHandlerContext() { + // no service handler - nothing to do + } + + @Override + protected void clearServiceHandlerDocuments() { + // no service handler - nothing to do + } + + @Override + protected void verifySingleCreateEvent(String contentId, String content) { + // no service handler - nothing to do + } + + @Override + protected void verifySingleCreateAndUpdateEvent( + String resultContentId, String toBeDeletedContentId, String content) { + // no service handler - nothing to do + } + + @Override + protected void verifySingleDeletionEvent(String contentId) { + // no service handler - nothing to do + } + + @Override + protected void verifySingleReadEvent(String contentId) { + // no service handler - nothing to do + } + + @Override + protected void verifyNoAttachmentEventsCalled() { + // no service handler - nothing to do + } + + @Override + protected void verifyEventContextEmptyForEvent(String... events) { + // no service handler - nothing to do + } +} 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 new file mode 100644 index 000000000..9bc77e763 --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/SizeLimitedAttachmentValidationNonDraftTest.java @@ -0,0 +1,175 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.nondraftservice; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +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.integrationtests.constants.Profiles; +import com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper.AttachmentsBuilder; +import com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper.RootEntityBuilder; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles(Profiles.TEST_HANDLER_DISABLED) +class SizeLimitedAttachmentValidationNonDraftTest extends OdataRequestValidationBase { + + @Test + void uploadContentWithin5MBLimitSucceeds() throws Exception { + // Arrange: Create root with sizeLimitedAttachments + var serviceRoot = buildServiceRootWithSizeLimitedAttachments(); + postServiceRoot(serviceRoot); + + 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"; + requestHelper.setContentType(org.springframework.http.MediaType.APPLICATION_OCTET_STREAM); + requestHelper.executePutWithMatcher(url, content, status().isNoContent()); + } + + @Test + void uploadContentExceeding5MBLimitFails() throws Exception { + // Arrange: Create root with sizeLimitedAttachments + var serviceRoot = buildServiceRootWithSizeLimitedAttachments(); + postServiceRoot(serviceRoot); + + 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"; + 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 + } + + // Helper methods + private Roots buildServiceRootWithSizeLimitedAttachments() { + return RootEntityBuilder.create() + .setTitle("Root with sizeLimitedAttachments") + .addSizeLimitedAttachments( + AttachmentsBuilder.create().setFileName("testFile.txt").setMimeType("text/plain")) + .build(); + } + + 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 result = persistenceService.run(select); + return result.single(Roots.class); + } + + // Required abstract method implementations + @Override + protected void executeContentRequestAndValidateContent(String url, String content) + throws Exception { + Awaitility.await() + .atMost(10, TimeUnit.SECONDS) + .until( + () -> { + var response = requestHelper.executeGet(url); + return response.getResponse().getContentAsString().equals(content); + }); + + var response = requestHelper.executeGet(url); + assertThat(response.getResponse().getContentAsString()).isEqualTo(content); + } + + @Override + protected void verifyTwoDeleteEvents( + AttachmentEntity itemAttachmentEntityAfterChange, Attachments itemAttachmentAfterChange) { + // no service handler - nothing to do + } + + @Override + protected void verifyNumberOfEvents(String event, int number) { + // no service handler - nothing to do + } + + @Override + protected void verifyContentId( + Attachments attachmentWithExpectedContent, String attachmentId, String contentId) { + assertThat(attachmentWithExpectedContent.getContentId()).isEqualTo(attachmentId); + } + + @Override + protected void verifyContentAndContentId( + Attachments attachment, String testContent, Attachments itemAttachment) throws IOException { + assertThat(attachment.getContent().readAllBytes()) + .isEqualTo(testContent.getBytes(StandardCharsets.UTF_8)); + assertThat(attachment.getContentId()).isEqualTo(itemAttachment.getId()); + } + + @Override + protected void verifyContentAndContentIdForAttachmentEntity( + AttachmentEntity attachment, String testContent, AttachmentEntity itemAttachment) + throws IOException { + assertThat(attachment.getContent().readAllBytes()) + .isEqualTo(testContent.getBytes(StandardCharsets.UTF_8)); + assertThat(attachment.getContentId()).isEqualTo(itemAttachment.getId()); + } + + @Override + protected void clearServiceHandlerContext() { + // no service handler - nothing to do + } + + @Override + protected void clearServiceHandlerDocuments() { + // no service handler - nothing to do + } + + @Override + protected void verifySingleCreateEvent(String contentId, String content) { + // no service handler - nothing to do + } + + @Override + protected void verifySingleCreateAndUpdateEvent( + String resultContentId, String toBeDeletedContentId, String content) { + // no service handler - nothing to do + } + + @Override + protected void verifySingleDeletionEvent(String contentId) { + // no service handler - nothing to do + } + + @Override + protected void verifySingleReadEvent(String contentId) { + // no service handler - nothing to do + } + + @Override + protected void verifyNoAttachmentEventsCalled() { + // no service handler - nothing to do + } + + @Override + protected void verifyEventContextEmptyForEvent(String... events) { + // no service handler - nothing to do + } +} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsBuilder.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsBuilder.java new file mode 100644 index 000000000..0f5e1bbfe --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsBuilder.java @@ -0,0 +1,33 @@ +/* + * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper; + +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.sap.attachments.Attachments; + +public class AttachmentsBuilder { + + private Attachments attachment; + + private AttachmentsBuilder() { + attachment = Attachments.create(); + } + + public static AttachmentsBuilder create() { + return new AttachmentsBuilder(); + } + + public AttachmentsBuilder setMimeType(String mimeType) { + attachment.setMimeType(mimeType); + return this; + } + + public AttachmentsBuilder setFileName(String fileName) { + attachment.setFileName(fileName); + return this; + } + + public Attachments build() { + return attachment; + } +} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsEntityBuilder.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsEntityBuilder.java new file mode 100644 index 000000000..2da95a144 --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsEntityBuilder.java @@ -0,0 +1,31 @@ +/* + * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper; + +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.AttachmentEntity; + +public class AttachmentsEntityBuilder { + + private AttachmentEntity attachmentEntity = AttachmentEntity.create(); + + private AttachmentsEntityBuilder() {} + + public static AttachmentsEntityBuilder create() { + return new AttachmentsEntityBuilder(); + } + + public AttachmentsEntityBuilder setMimeType(String mimeType) { + attachmentEntity.setMimeType(mimeType); + return this; + } + + public AttachmentsEntityBuilder setFileName(String fileName) { + attachmentEntity.setFileName(fileName); + return this; + } + + public AttachmentEntity build() { + return attachmentEntity; + } +} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/ItemEntityBuilder.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/ItemEntityBuilder.java new file mode 100644 index 000000000..9b6ab7017 --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/ItemEntityBuilder.java @@ -0,0 +1,44 @@ +/* + * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper; + +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.Items; +import java.util.ArrayList; +import java.util.Arrays; + +public class ItemEntityBuilder { + + private final Items item; + + private ItemEntityBuilder() { + item = Items.create(); + item.setAttachments(new ArrayList<>()); + item.setAttachmentEntities(new ArrayList<>()); + } + + public static ItemEntityBuilder create() { + return new ItemEntityBuilder(); + } + + public ItemEntityBuilder setTitle(String title) { + item.setTitle(title); + return this; + } + + public ItemEntityBuilder addAttachmentEntities(AttachmentsEntityBuilder... attachmentEntities) { + Arrays.stream(attachmentEntities) + .forEach(attachment -> item.getAttachmentEntities().add(attachment.build())); + return this; + } + + public ItemEntityBuilder addAttachments(AttachmentsBuilder... attachmentEntities) { + Arrays.stream(attachmentEntities) + .forEach(attachment -> item.getAttachments().add(attachment.build())); + return this; + } + + public Items build() { + return item; + } +} 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 new file mode 100644 index 000000000..9efc70dfc --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/RootEntityBuilder.java @@ -0,0 +1,50 @@ +/* + * © 2024-2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper; + +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.Roots; +import java.util.ArrayList; +import java.util.Arrays; + +public class RootEntityBuilder { + + private final Roots rootEntity; + + private RootEntityBuilder() { + rootEntity = Roots.create(); + rootEntity.setAttachments(new ArrayList<>()); + rootEntity.setItems(new ArrayList<>()); + rootEntity.setSizeLimitedAttachments(new ArrayList<>()); + } + + public static RootEntityBuilder create() { + return new RootEntityBuilder(); + } + + public RootEntityBuilder setTitle(String title) { + rootEntity.setTitle(title); + return this; + } + + public RootEntityBuilder addAttachments(AttachmentsEntityBuilder... attachments) { + Arrays.stream(attachments) + .forEach(attachment -> rootEntity.getAttachments().add(attachment.build())); + return this; + } + + public RootEntityBuilder addSizeLimitedAttachments(AttachmentsBuilder... attachments) { + Arrays.stream(attachments) + .forEach(attachment -> rootEntity.getSizeLimitedAttachments().add(attachment.build())); + return this; + } + + public RootEntityBuilder addItems(ItemEntityBuilder... items) { + Arrays.stream(items).forEach(item -> rootEntity.getItems().add(item.build())); + return this; + } + + public Roots build() { + return rootEntity; + } +} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandlerTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandlerTest.java new file mode 100644 index 000000000..9dcf8875f --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandlerTest.java @@ -0,0 +1,116 @@ +/* + * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.testhandler; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.sap.cds.services.ServiceException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class TestPersistenceHandlerTest { + + private TestPersistenceHandler testPersistenceHandler; + + @BeforeEach + void setUp() { + testPersistenceHandler = new TestPersistenceHandler(); + } + + @Test + void testReset() { + // Set both flags to true + testPersistenceHandler.setThrowExceptionOnUpdate(true); + testPersistenceHandler.setThrowExceptionOnCreate(true); + + // Reset should set both flags to false + testPersistenceHandler.reset(); + + // Verify no exceptions are thrown after reset + assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnUpdate()); + assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnCreate()); + } + + @Test + void testThrowExceptionOnUpdateWhenEnabled() { + testPersistenceHandler.setThrowExceptionOnUpdate(true); + + ServiceException exception = + assertThrows(ServiceException.class, () -> testPersistenceHandler.throwExceptionOnUpdate()); + + assertTrue(exception.getMessage().contains("Exception on update")); + } + + @Test + void testThrowExceptionOnUpdateWhenDisabled() { + testPersistenceHandler.setThrowExceptionOnUpdate(false); + + assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnUpdate()); + } + + @Test + void testThrowExceptionOnCreateWhenEnabled() { + testPersistenceHandler.setThrowExceptionOnCreate(true); + + ServiceException exception = + assertThrows(ServiceException.class, () -> testPersistenceHandler.throwExceptionOnCreate()); + + assertTrue(exception.getMessage().contains("Exception on create")); + } + + @Test + void testThrowExceptionOnCreateWhenDisabled() { + testPersistenceHandler.setThrowExceptionOnCreate(false); + + assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnCreate()); + } + + @Test + void testSetThrowExceptionOnUpdate() { + // Test setting to true + testPersistenceHandler.setThrowExceptionOnUpdate(true); + assertThrows(ServiceException.class, () -> testPersistenceHandler.throwExceptionOnUpdate()); + + // Test setting to false + testPersistenceHandler.setThrowExceptionOnUpdate(false); + assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnUpdate()); + } + + @Test + void testSetThrowExceptionOnCreate() { + // Test setting to true + testPersistenceHandler.setThrowExceptionOnCreate(true); + assertThrows(ServiceException.class, () -> testPersistenceHandler.throwExceptionOnCreate()); + + // Test setting to false + testPersistenceHandler.setThrowExceptionOnCreate(false); + assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnCreate()); + } + + @Test + void testDefaultBehavior() { + // By default, both flags should be false + assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnUpdate()); + assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnCreate()); + } + + @Test + void testIndependentFlagBehavior() { + // Test that the flags work independently + testPersistenceHandler.setThrowExceptionOnUpdate(true); + testPersistenceHandler.setThrowExceptionOnCreate(false); + + assertThrows(ServiceException.class, () -> testPersistenceHandler.throwExceptionOnUpdate()); + assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnCreate()); + + // Switch them + testPersistenceHandler.setThrowExceptionOnUpdate(false); + testPersistenceHandler.setThrowExceptionOnCreate(true); + + assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnUpdate()); + assertThrows(ServiceException.class, () -> testPersistenceHandler.throwExceptionOnCreate()); + } +} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java new file mode 100644 index 000000000..bc391fe9b --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java @@ -0,0 +1,266 @@ +/* + * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.testhandler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.*; + +import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.MediaData; +import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.StatusCode; +import com.sap.cds.feature.attachments.service.AttachmentService; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentCreateEventContext; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentMarkAsDeletedEventContext; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentReadEventContext; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentRestoreEventContext; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class TestPluginAttachmentsServiceHandlerTest { + + private TestPluginAttachmentsServiceHandler cut; + + @BeforeEach + void setup() { + cut = new TestPluginAttachmentsServiceHandler(); + // Clear any previous test data + cut.clearEventContext(); + cut.clearDocuments(); + } + + @Test + void readIsWorking() { + var context = AttachmentReadEventContext.create(); + context.setContentId("test"); + context.setData(MediaData.create()); + + cut.readAttachment(context); + + assertThat(context.getData().getContent()).isNull(); + } + + @Test + void readWithContentIsWorking() throws IOException { + var createContext = AttachmentCreateEventContext.create(); + createContext.setData(MediaData.create()); + createContext + .getData() + .setContent(new ByteArrayInputStream("test".getBytes(StandardCharsets.UTF_8))); + cut.createAttachment(createContext); + + var context = AttachmentReadEventContext.create(); + context.setContentId(createContext.getContentId()); + context.setData(MediaData.create()); + + cut.readAttachment(context); + + assertThat(context.getData().getContent().readAllBytes()) + .isEqualTo("test".getBytes(StandardCharsets.UTF_8)); + } + + @Test + void dummyTestForDelete() { + var context = AttachmentMarkAsDeletedEventContext.create(); + context.setContentId("test"); + + assertDoesNotThrow(() -> cut.markAttachmentAsDeleted(context)); + } + + @Test + void dummyTestForCreate() throws IOException { + var context = AttachmentCreateEventContext.create(); + context.setData(MediaData.create()); + var stream = mock(InputStream.class); + when(stream.readAllBytes()).thenReturn("test".getBytes(StandardCharsets.UTF_8)); + context.getData().setContent(stream); + + assertDoesNotThrow(() -> cut.createAttachment(context)); + } + + @Test + void dummyTestForRestore() { + var context = AttachmentRestoreEventContext.create(); + context.setRestoreTimestamp(Instant.now()); + + assertDoesNotThrow(() -> cut.restoreAttachment(context)); + } + + @Test + void testCreateAttachmentSetsContentIdAndStatus() throws IOException { + var context = AttachmentCreateEventContext.create(); + context.setData(MediaData.create()); + context + .getData() + .setContent(new ByteArrayInputStream("test content".getBytes(StandardCharsets.UTF_8))); + + cut.createAttachment(context); + + assertNotNull(context.getContentId()); + assertThat(context.getData().getStatus()).isEqualTo(StatusCode.CLEAN); + } + + @Test + void testEventContextTracking() throws IOException { + // Test create event tracking + var createContext = AttachmentCreateEventContext.create(); + createContext.setData(MediaData.create()); + createContext + .getData() + .setContent(new ByteArrayInputStream("test".getBytes(StandardCharsets.UTF_8))); + cut.createAttachment(createContext); + + List createEvents = + cut.getEventContextForEvent(AttachmentService.EVENT_CREATE_ATTACHMENT); + assertThat(createEvents).hasSize(1); + assertThat(createEvents.get(0).event()).isEqualTo(AttachmentService.EVENT_CREATE_ATTACHMENT); + + // Test read event tracking + var readContext = AttachmentReadEventContext.create(); + readContext.setContentId("test-id"); + readContext.setData(MediaData.create()); + cut.readAttachment(readContext); + + List readEvents = + cut.getEventContextForEvent(AttachmentService.EVENT_READ_ATTACHMENT); + assertThat(readEvents).hasSize(1); + assertThat(readEvents.get(0).event()).isEqualTo(AttachmentService.EVENT_READ_ATTACHMENT); + + // Test delete event tracking + var deleteContext = AttachmentMarkAsDeletedEventContext.create(); + deleteContext.setContentId("test-id"); + cut.markAttachmentAsDeleted(deleteContext); + + List deleteEvents = + cut.getEventContextForEvent(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); + assertThat(deleteEvents).hasSize(1); + assertThat(deleteEvents.get(0).event()) + .isEqualTo(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); + + // Test restore event tracking + var restoreContext = AttachmentRestoreEventContext.create(); + restoreContext.setRestoreTimestamp(Instant.now()); + cut.restoreAttachment(restoreContext); + + List restoreEvents = + cut.getEventContextForEvent(AttachmentService.EVENT_RESTORE_ATTACHMENT); + assertThat(restoreEvents).hasSize(1); + assertThat(restoreEvents.get(0).event()).isEqualTo(AttachmentService.EVENT_RESTORE_ATTACHMENT); + } + + @Test + void testGetAllEventContext() throws IOException { + // Create multiple events + var createContext = AttachmentCreateEventContext.create(); + createContext.setData(MediaData.create()); + createContext + .getData() + .setContent(new ByteArrayInputStream("test".getBytes(StandardCharsets.UTF_8))); + cut.createAttachment(createContext); + + var readContext = AttachmentReadEventContext.create(); + readContext.setContentId("test-id"); + readContext.setData(MediaData.create()); + cut.readAttachment(readContext); + + List allEvents = cut.getEventContext(); + assertThat(allEvents).hasSize(2); + } + + @Test + void testClearEventContext() throws IOException { + // Add some events + var context = AttachmentCreateEventContext.create(); + context.setData(MediaData.create()); + context.getData().setContent(new ByteArrayInputStream("test".getBytes(StandardCharsets.UTF_8))); + cut.createAttachment(context); + + assertThat(cut.getEventContext()).hasSize(1); + + // Clear and verify + cut.clearEventContext(); + assertThat(cut.getEventContext()).isEmpty(); + } + + @Test + void testReadWithNullContentId() { + var context = AttachmentReadEventContext.create(); + context.setContentId(null); + context.setData(MediaData.create()); + + cut.readAttachment(context); + + assertThat(context.getData().getContent()).isNull(); + } + + @Test + void testCreateAttachmentWithEmptyContent() throws IOException { + var context = AttachmentCreateEventContext.create(); + context.setData(MediaData.create()); + context.getData().setContent(new ByteArrayInputStream(new byte[0])); + + cut.createAttachment(context); + + assertNotNull(context.getContentId()); + assertThat(context.getData().getStatus()).isEqualTo(StatusCode.CLEAN); + } + + @Test + void testMultipleCreateAndReadOperations() throws IOException { + // Create first attachment + var createContext1 = AttachmentCreateEventContext.create(); + createContext1.setData(MediaData.create()); + createContext1 + .getData() + .setContent(new ByteArrayInputStream("content1".getBytes(StandardCharsets.UTF_8))); + cut.createAttachment(createContext1); + + // Create second attachment + var createContext2 = AttachmentCreateEventContext.create(); + createContext2.setData(MediaData.create()); + createContext2 + .getData() + .setContent(new ByteArrayInputStream("content2".getBytes(StandardCharsets.UTF_8))); + cut.createAttachment(createContext2); + + // Read first attachment + var readContext1 = AttachmentReadEventContext.create(); + readContext1.setContentId(createContext1.getContentId()); + readContext1.setData(MediaData.create()); + cut.readAttachment(readContext1); + + // Read second attachment + var readContext2 = AttachmentReadEventContext.create(); + readContext2.setContentId(createContext2.getContentId()); + readContext2.setData(MediaData.create()); + cut.readAttachment(readContext2); + + // Verify content + assertThat(readContext1.getData().getContent().readAllBytes()) + .isEqualTo("content1".getBytes(StandardCharsets.UTF_8)); + assertThat(readContext2.getData().getContent().readAllBytes()) + .isEqualTo("content2".getBytes(StandardCharsets.UTF_8)); + } + + @Test + void testRestoreWithSpecificTimestamp() { + Instant timestamp = Instant.parse("2024-01-01T12:00:00Z"); + var context = AttachmentRestoreEventContext.create(); + context.setRestoreTimestamp(timestamp); + + cut.restoreAttachment(context); + + List restoreEvents = + cut.getEventContextForEvent(AttachmentService.EVENT_RESTORE_ATTACHMENT); + assertThat(restoreEvents).hasSize(1); + var restoredContext = (AttachmentRestoreEventContext) restoreEvents.get(0).context(); + assertThat(restoredContext.getRestoreTimestamp()).isEqualTo(timestamp); + } +} diff --git a/integration-tests/srv/src/test/resources/application.yaml b/integration-tests/srv/src/test/resources/application.yaml new file mode 100644 index 000000000..54849379b --- /dev/null +++ b/integration-tests/srv/src/test/resources/application.yaml @@ -0,0 +1,15 @@ +cds: + dataSource: + csv: + paths: "../db/src/gen/csv" + +--- +spring: + config: + activate: + on-profile: malware-scan-enabled + +cds: + environment: + local: + defaultEnvPath: "classpath:xsuaa-env.json" diff --git a/integration-tests/srv/src/test/resources/logback-test.xml b/integration-tests/srv/src/test/resources/logback-test.xml new file mode 100644 index 000000000..023662ed8 --- /dev/null +++ b/integration-tests/srv/src/test/resources/logback-test.xml @@ -0,0 +1,21 @@ + + + + + + %date %-5level [%thread] [%marker]: %msg%nopex [%logger] [%mdc{correlation_id}]%n + + + + + + + + + + + + + + + diff --git a/integration-tests/srv/src/test/resources/xsuaa-env.json b/integration-tests/srv/src/test/resources/xsuaa-env.json new file mode 100644 index 000000000..7adbe70b0 --- /dev/null +++ b/integration-tests/srv/src/test/resources/xsuaa-env.json @@ -0,0 +1,30 @@ +{ + "VCAP_SERVICES": { + "malware-scanner": [ + { + "label": "malware-scanner", + "provider": null, + "plan": "clamav", + "name": "dsr-core-malware-scanner", + "tags": [], + "instance_guid": "2fbe12be-569d-473e-ab0a-eb2f1d18c7e3", + "instance_name": "dsr-core-malware-scanner", + "binding_guid": "16ce4d70-1511-45d0-a788-e365b298ca8a", + "binding_name": null, + "credentials": { + "sync_scan_url": "https://test.scanner.com", + "async_scan_url": "", + "uri": "https://test.scanner.com", + "url": "http://localhost:1111", + "username": "test-user", + "password": "test-password" + }, + "syslog_drain_url": null, + "volume_mounts": [] + } + ] + }, + "VCAP_APPLICATION": { + "application_id": "xsapp!t0815" + } +} diff --git a/integration-tests/srv/test-service.cds b/integration-tests/srv/test-service.cds new file mode 100644 index 000000000..ff68a31ff --- /dev/null +++ b/integration-tests/srv/test-service.cds @@ -0,0 +1,27 @@ +using test.data.model as db from '../db/data-model'; + +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' + ]); +} + +annotate db.Roots.mimeValidatedAttachments with { + content @(Core.AcceptableMediaTypes: ['application/pdf']); +} + +service TestService { + entity Roots as projection on db.Roots; + entity AttachmentEntity as projection on db.AttachmentEntity; +} + +service TestDraftService { + @odata.draft.enabled + entity DraftRoots as projection on db.Roots; +} From b02fa9f6f432967cc890fe79fbd611cec478bc5a Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Tue, 31 Mar 2026 11:20:53 +0200 Subject: [PATCH 12/18] fix setup --- integration-tests/mtx-local/package-lock.json | 4144 +++++++++++++++++ integration-tests/srv/pom.xml | 121 - .../integrationtests/Application.java | 15 - .../integrationtests/constants/Profiles.java | 15 - .../testhandler/EventContextHolder.java | 8 - .../testhandler/TestPersistenceHandler.java | 49 - .../TestPluginAttachmentsServiceHandler.java | 120 - .../srv/src/main/resources/application.yaml | 4 - .../srv/src/main/resources/banner.txt | 9 - .../src/main/resources/messages.properties | 1 - .../resources/spotbugs-exclusion-filter.xml | 26 - .../common/JsonToCapMapperTestHelper.java | 21 - .../common/MalwareScanResultProvider.java | 27 - .../common/MockHttpRequestHelper.java | 167 - .../common/TableDataDeleter.java | 21 - .../DraftOdataRequestValidationBase.java | 973 ---- ...aRequestValidationWithTestHandlerTest.java | 288 -- ...thoutTestHandlerAndMalwareScannerTest.java | 114 - ...stHandlerAndWithoutMalwareScannerTest.java | 90 - .../MediaValidatedAttachmentsDraftTest.java | 159 - ...tedAttachmentsSizeValidationDraftTest.java | 202 - ...MediaValidatedAttachmentsNonDraftTest.java | 296 -- .../OdataRequestValidationBase.java | 884 ---- ...aRequestValidationWithTestHandlerTest.java | 245 - ...thoutTestHandlerAndMalwareScannerTest.java | 138 - ...stHandlerAndWithoutMalwareScannerTest.java | 116 - ...mitedAttachmentValidationNonDraftTest.java | 175 - .../helper/AttachmentsBuilder.java | 33 - .../helper/AttachmentsEntityBuilder.java | 31 - .../helper/ItemEntityBuilder.java | 44 - .../helper/RootEntityBuilder.java | 50 - .../TestPersistenceHandlerTest.java | 116 - ...stPluginAttachmentsServiceHandlerTest.java | 266 -- .../srv/src/test/resources/application.yaml | 15 - .../srv/src/test/resources/logback-test.xml | 21 - .../srv/src/test/resources/xsuaa-env.json | 30 - integration-tests/srv/test-service.cds | 27 - 37 files changed, 4144 insertions(+), 4917 deletions(-) create mode 100644 integration-tests/mtx-local/package-lock.json delete mode 100644 integration-tests/srv/pom.xml delete mode 100644 integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/Application.java delete mode 100644 integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/constants/Profiles.java delete mode 100644 integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/EventContextHolder.java delete mode 100644 integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandler.java delete mode 100644 integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandler.java delete mode 100644 integration-tests/srv/src/main/resources/application.yaml delete mode 100644 integration-tests/srv/src/main/resources/banner.txt delete mode 100644 integration-tests/srv/src/main/resources/messages.properties delete mode 100644 integration-tests/srv/src/main/resources/spotbugs-exclusion-filter.xml delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/JsonToCapMapperTestHelper.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MalwareScanResultProvider.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MockHttpRequestHelper.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/TableDataDeleter.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationBase.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithTestHandlerTest.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/MediaValidatedAttachmentsDraftTest.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/SizeLimitedAttachmentsSizeValidationDraftTest.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationBase.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithTestHandlerTest.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/SizeLimitedAttachmentValidationNonDraftTest.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsBuilder.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsEntityBuilder.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/ItemEntityBuilder.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/RootEntityBuilder.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandlerTest.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java delete mode 100644 integration-tests/srv/src/test/resources/application.yaml delete mode 100644 integration-tests/srv/src/test/resources/logback-test.xml delete mode 100644 integration-tests/srv/src/test/resources/xsuaa-env.json delete mode 100644 integration-tests/srv/test-service.cds diff --git a/integration-tests/mtx-local/package-lock.json b/integration-tests/mtx-local/package-lock.json new file mode 100644 index 000000000..3383043ec --- /dev/null +++ b/integration-tests/mtx-local/package-lock.json @@ -0,0 +1,4144 @@ +{ + "name": "mtx-local-integration-tests", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mtx-local-integration-tests", + "version": "0.0.0", + "workspaces": [ + "mtx/sidecar" + ], + "devDependencies": { + "@sap/cds-dk": "^9", + "@sap/cds-mtxs": "^3" + } + }, + "mtx/sidecar": { + "name": "mtx-local-sidecar", + "version": "0.0.0", + "dependencies": { + "@sap/cds": "^9", + "@sap/cds-mtxs": "^3", + "@sap/xssec": "^4", + "express": "^4" + }, + "devDependencies": { + "@cap-js/sqlite": "^2" + } + }, + "mtx/sidecar/node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@cap-js/db-service": { + "version": "2.9.0", + "resolved": "https://int.repositories.cloud.sap/artifactory/api/npm/build-milestones-npm/@cap-js/db-service/-/db-service-2.9.0.tgz", + "integrity": "sha512-WCXhoqezaF6A5I2l0MNZeHKXXtHRNEq7Rp0R89/uccOHQIx0DuU0U9NuJJPV/1G5RGk2QKQ9VBo/KYn+MZuuNQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "generic-pool": "^3.9.0" + }, + "peerDependencies": { + "@sap/cds": ">=9.8" + } + }, + "node_modules/@cap-js/sqlite": { + "version": "2.2.0", + "resolved": "https://int.repositories.cloud.sap/artifactory/api/npm/build-milestones-npm/@cap-js/sqlite/-/sqlite-2.2.0.tgz", + "integrity": "sha512-FPj+uVU/14vtGUl2P/Q8y7XhZbsLgrCav2O5PjHPXnupegjby4sMJkgVNxVHnkyKPFgO/W8uEsq9r5TU9VPx8w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@cap-js/db-service": "^2.9.0", + "better-sqlite3": "^12.0.0" + }, + "peerDependencies": { + "@sap/cds": ">=9.8", + "sql.js": "^1.13.0" + }, + "peerDependenciesMeta": { + "sql.js": { + "optional": true + } + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "license": "MIT", + "peer": true, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@sap/cds": { + "version": "9.8.4", + "resolved": "https://registry.npmjs.org/@sap/cds/-/cds-9.8.4.tgz", + "integrity": "sha512-1cnFdz4Ex4LV15dztDsSLDiilVdJ2mfTqr5A3TGsRd/6cM31SnHhuZ/Ra0FEErodokNS2pwasGZmSfgHVDW8KA==", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@sap/cds-compiler": "^6.4", + "@sap/cds-fiori": "^2", + "express": "^4.22.1 || ^5", + "js-yaml": "^4.1.1" + }, + "bin": { + "cds-deploy": "bin/deploy.js", + "cds-serve": "bin/serve.js" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@eslint/js": "^9 || ^10", + "tar": "^7.5.6" + }, + "peerDependenciesMeta": { + "tar": { + "optional": true + } + } + }, + "node_modules/@sap/cds-compiler": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@sap/cds-compiler/-/cds-compiler-6.8.0.tgz", + "integrity": "sha512-yRoTZcH8DFiP4PcEuIHe42YAaNt51V736+6RRr+U4nbO91sz36L8be2djcE8n760t8IbqLxzZ/UkATiL0fjRmA==", + "license": "SEE LICENSE IN LICENSE", + "bin": { + "cdsc": "bin/cdsc.js", + "cdshi": "bin/cdshi.js", + "cdsse": "bin/cdsse.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@sap/cds-dk": { + "version": "9.8.2", + "resolved": "https://registry.npmjs.org/@sap/cds-dk/-/cds-dk-9.8.2.tgz", + "integrity": "sha512-PGynoQpOCD8Nsr0EEzsEHWg8szYBLVtVd6byxvaaAPYBkNAj2WA+kin6gWaDTiyG3kJa0BSGPk4PCbA2xDoE9A==", + "dev": true, + "hasShrinkwrap": true, + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@cap-js/asyncapi": "^1.0.0", + "@cap-js/openapi": "^1.0.0", + "@sap/cds": ">=8.3", + "@sap/cds-mtxs": ">=2", + "@sap/hdi-deploy": "^5", + "axios": "^1", + "express": "^4.22.1 || ^5", + "hdb": "^2.0.0", + "livereload-js": "^4.0.1", + "mustache": "^4.0.1", + "ws": "^8.4.2", + "xml-js": "^1.6.11", + "yaml": "^2" + }, + "bin": { + "cds": "bin/cds.js", + "cds-ts": "bin/cds-ts.js", + "cds-tsx": "bin/cds-tsx.js" + }, + "optionalDependencies": { + "@cap-js/sqlite": ">=1" + } + }, + "node_modules/@sap/cds-dk/node_modules/@cap-js/asyncapi": { + "version": "1.0.3", + "integrity": "sha512-vZSWKAe+3qfvZDXV5SSFiObGWmqyS9MDyEADb5PLVT8kzO39qGaSDPv/GzI/gwvRfCayGAjU4ThiBKrFA7Gclg==", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "peerDependencies": { + "@sap/cds": ">=7.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/@cap-js/db-service": { + "version": "2.9.0", + "integrity": "sha512-WCXhoqezaF6A5I2l0MNZeHKXXtHRNEq7Rp0R89/uccOHQIx0DuU0U9NuJJPV/1G5RGk2QKQ9VBo/KYn+MZuuNQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "generic-pool": "^3.9.0" + }, + "peerDependencies": { + "@sap/cds": ">=9.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/@cap-js/openapi": { + "version": "1.4.0", + "integrity": "sha512-/LRSwn4SDxAi3qKwl09zoOhEVGaPGlYOPz/0S3UBnaMJVvaLyPiKbbaOtOnrrgulUX5OXt+ujPIQznOsbTzuAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "pluralize": "^8.0.0" + }, + "peerDependencies": { + "@sap/cds": ">=7.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/@cap-js/sqlite": { + "version": "2.2.0", + "integrity": "sha512-FPj+uVU/14vtGUl2P/Q8y7XhZbsLgrCav2O5PjHPXnupegjby4sMJkgVNxVHnkyKPFgO/W8uEsq9r5TU9VPx8w==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@cap-js/db-service": "^2.9.0", + "better-sqlite3": "^12.0.0" + }, + "peerDependencies": { + "@sap/cds": ">=9.8", + "sql.js": "^1.13.0" + }, + "peerDependenciesMeta": { + "sql.js": { + "optional": true + } + } + }, + "node_modules/@sap/cds-dk/node_modules/@eslint/js": { + "version": "10.0.1", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@sap/cds-dk/node_modules/@sap/cds": { + "version": "9.8.3", + "integrity": "sha512-up/kNVCZK8bOTyye1QN2BjMpH+IgcdyhT3U9mS3TqkpmjNE2JXq8Qz1iNAN+nYcXRWJ1XAUNpgebLfuh/49MGQ==", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@sap/cds-compiler": "^6.4", + "@sap/cds-fiori": "^2", + "express": "^4.22.1 || ^5", + "js-yaml": "^4.1.1" + }, + "bin": { + "cds-deploy": "bin/deploy.js", + "cds-serve": "bin/serve.js" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@eslint/js": "^9 || ^10", + "tar": "^7.5.6" + }, + "peerDependenciesMeta": { + "tar": { + "optional": true + } + } + }, + "node_modules/@sap/cds-dk/node_modules/@sap/cds-compiler": { + "version": "6.8.0", + "integrity": "sha512-yRoTZcH8DFiP4PcEuIHe42YAaNt51V736+6RRr+U4nbO91sz36L8be2djcE8n760t8IbqLxzZ/UkATiL0fjRmA==", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "bin": { + "cdsc": "bin/cdsc.js", + "cdshi": "bin/cdshi.js", + "cdsse": "bin/cdsse.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@sap/cds-dk/node_modules/@sap/cds-fiori": { + "version": "2.3.0", + "integrity": "sha512-6oWov+DSpFrSTgxXR0dZhak6aZ/IVRZvaHERMi0EgSTzIJdlvZlpw3Kf18ePMcTrRrtEXwD4RIjKt8pbs0g2Hg==", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "peerDependencies": { + "@sap/cds": ">=8" + } + }, + "node_modules/@sap/cds-dk/node_modules/@sap/cds-mtxs": { + "version": "3.8.1", + "integrity": "sha512-FUm1of08WsZ1O3eur7OGj7ZYp9Gy3RuYEGBZK+zosSPyJ3QrBhBzKwYvbh2DJdbv3P/xuk46NGg5yz914qZxrw==", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@sap/hdi-deploy": "^5" + }, + "bin": { + "cds-mtx": "bin/cds-mtx.js", + "cds-mtx-migrate": "bin/cds-mtx-migrate.js" + }, + "peerDependencies": { + "@sap/cds": "^9" + } + }, + "node_modules/@sap/cds-dk/node_modules/@sap/hdi": { + "version": "4.8.0", + "integrity": "sha512-tkJmY2ffm6mt4/LFwRBihlQkMxNAXa3ngvRe2N/6+qLIsUNdrH/M03S5mkygXq56K+KoVVZYuradajCusMWwsw==", + "dev": true, + "license": "See LICENSE file", + "dependencies": { + "async": "^3.2.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@sap/hana-client": "^2 >= 2.5", + "hdb": "^2 || ^0" + }, + "peerDependenciesMeta": { + "@sap/hana-client": { + "optional": true + }, + "hdb": { + "optional": true + } + } + }, + "node_modules/@sap/cds-dk/node_modules/@sap/hdi-deploy": { + "version": "5.6.1", + "integrity": "sha512-+qQ7qwG8lko303L5yRj2dud/nDAVuVblV/mmzJT44oPbF0Nry18eD2LUS23hFeuxjRa7rYK5YKQ8ffGgWxVrYQ==", + "dev": true, + "license": "See LICENSE file", + "dependencies": { + "@sap/hdi": "^4.8.0", + "@sap/xsenv": "^6.0.0", + "async": "^3.2.6", + "dotenv": "^16.4.5", + "handlebars": "^4.7.8", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=18.x" + }, + "peerDependencies": { + "@sap/hana-client": "^2 >= 2.6", + "hdb": "^2 || ^0" + }, + "peerDependenciesMeta": { + "@sap/hana-client": { + "optional": true + }, + "hdb": { + "optional": true + } + } + }, + "node_modules/@sap/cds-dk/node_modules/@sap/xsenv": { + "version": "6.1.0", + "integrity": "sha512-vlW4Zad3uiDqHtnYdQ0TsEIH8VIO4HmPGDowfBL5dIcHPmeKDISEQ9ibeHL5FkceqvYcXJEQAVZ5/hsHDqlXZg==", + "dev": true, + "license": "SEE LICENSE IN LICENSE file", + "dependencies": { + "debug": "4.4.3", + "node-cache": "^5.1.2", + "verror": "1.10.1" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/accepts": { + "version": "2.0.0", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/argparse": { + "version": "2.0.1", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/@sap/cds-dk/node_modules/assert-plus": { + "version": "1.0.0", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/async": { + "version": "3.2.6", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/asynckit": { + "version": "0.4.0", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/axios": { + "version": "1.13.6", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/base64-js": { + "version": "1.5.1", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/better-sqlite3": { + "version": "12.8.0", + "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, + "node_modules/@sap/cds-dk/node_modules/bindings": { + "version": "1.5.0", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/bl": { + "version": "4.1.0", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/body-parser": { + "version": "2.2.2", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@sap/cds-dk/node_modules/braces": { + "version": "3.0.3", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sap/cds-dk/node_modules/buffer": { + "version": "5.7.1", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/@sap/cds-dk/node_modules/bytes": { + "version": "3.1.2", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/call-bound": { + "version": "1.0.4", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/chownr": { + "version": "1.1.4", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/clone": { + "version": "2.1.2", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/combined-stream": { + "version": "1.0.8", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/content-disposition": { + "version": "1.0.1", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@sap/cds-dk/node_modules/content-type": { + "version": "1.0.5", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/cookie": { + "version": "0.7.2", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/cookie-signature": { + "version": "1.2.2", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/core-util-is": { + "version": "1.0.2", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/debug": { + "version": "4.4.3", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@sap/cds-dk/node_modules/decompress-response": { + "version": "6.0.0", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sap/cds-dk/node_modules/deep-extend": { + "version": "0.6.0", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/delayed-stream": { + "version": "1.0.0", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/depd": { + "version": "2.0.0", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/detect-libc": { + "version": "2.1.2", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sap/cds-dk/node_modules/dotenv": { + "version": "16.6.1", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/@sap/cds-dk/node_modules/dunder-proto": { + "version": "1.0.1", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/ee-first": { + "version": "1.1.1", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/encodeurl": { + "version": "2.0.0", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/end-of-stream": { + "version": "1.4.5", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/es-define-property": { + "version": "1.0.1", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/es-errors": { + "version": "1.3.0", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/es-object-atoms": { + "version": "1.1.1", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/es-set-tostringtag": { + "version": "2.1.0", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/escape-html": { + "version": "1.0.3", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/etag": { + "version": "1.8.1", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/expand-template": { + "version": "2.0.3", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sap/cds-dk/node_modules/express": { + "version": "5.2.1", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@sap/cds-dk/node_modules/extsprintf": { + "version": "1.4.1", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/file-uri-to-path": { + "version": "1.0.0", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/fill-range": { + "version": "7.1.1", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sap/cds-dk/node_modules/finalhandler": { + "version": "2.1.1", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@sap/cds-dk/node_modules/follow-redirects": { + "version": "1.15.11", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/@sap/cds-dk/node_modules/form-data": { + "version": "4.0.5", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@sap/cds-dk/node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/forwarded": { + "version": "0.2.0", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/fresh": { + "version": "2.0.0", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/fs-constants": { + "version": "1.0.0", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/function-bind": { + "version": "1.1.2", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/generic-pool": { + "version": "3.9.0", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@sap/cds-dk/node_modules/get-intrinsic": { + "version": "1.3.0", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/get-proto": { + "version": "1.0.1", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/github-from-package": { + "version": "0.0.0", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/gopd": { + "version": "1.2.0", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/handlebars": { + "version": "4.7.8", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/has-symbols": { + "version": "1.1.0", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/has-tostringtag": { + "version": "1.0.2", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/hasown": { + "version": "2.0.2", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/hdb": { + "version": "2.27.1", + "integrity": "sha512-xYL/W+fq2TyGHyzm8muolQnw8tdh4+2NQ8mQP2FpLSuhfJ8l0jQNSUZoAXic7NfMEan1Jvf8V1L4blwkgTc6+A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "iconv-lite": "0.7.0" + }, + "engines": { + "node": ">= 18" + }, + "optionalDependencies": { + "lz4-wasm-nodejs": "0.9.2" + } + }, + "node_modules/@sap/cds-dk/node_modules/hdb/node_modules/iconv-lite": { + "version": "0.7.0", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@sap/cds-dk/node_modules/http-errors": { + "version": "2.0.1", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@sap/cds-dk/node_modules/iconv-lite": { + "version": "0.7.2", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@sap/cds-dk/node_modules/ieee754": { + "version": "1.2.1", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/inherits": { + "version": "2.0.4", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@sap/cds-dk/node_modules/ini": { + "version": "1.3.8", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/ipaddr.js": { + "version": "1.9.1", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@sap/cds-dk/node_modules/is-number": { + "version": "7.0.0", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/is-promise": { + "version": "4.0.0", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/js-yaml": { + "version": "4.1.1", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@sap/cds-dk/node_modules/livereload-js": { + "version": "4.0.2", + "integrity": "sha512-Fy7VwgQNiOkynYyNBTo3v9hQUhcW5pFAheJN148+DTgpShjsy/22pLHKKwDK5v0kOsZsJBK+6q1PMgLvRmrwFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/lz4-wasm-nodejs": { + "version": "0.9.2", + "integrity": "sha512-hSwgJPS98q/Oe/89Y1OxzeA/UdnASG8GvldRyKa7aZyoAFCC8VPRtViBSava7wWC66WocjUwBpWau2rEmyFPsw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/math-intrinsics": { + "version": "1.1.0", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/media-typer": { + "version": "1.1.0", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/merge-descriptors": { + "version": "2.0.0", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sap/cds-dk/node_modules/micromatch": { + "version": "4.0.8", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@sap/cds-dk/node_modules/mime-db": { + "version": "1.54.0", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/mime-types": { + "version": "3.0.2", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@sap/cds-dk/node_modules/mimic-response": { + "version": "3.1.0", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sap/cds-dk/node_modules/minimist": { + "version": "1.2.8", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/mkdirp-classic": { + "version": "0.5.3", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/ms": { + "version": "2.1.3", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/mustache": { + "version": "4.2.0", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "dev": true, + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, + "node_modules/@sap/cds-dk/node_modules/napi-build-utils": { + "version": "2.0.0", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/negotiator": { + "version": "1.0.0", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/neo-async": { + "version": "2.6.2", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/node-abi": { + "version": "3.89.0", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@sap/cds-dk/node_modules/node-cache": { + "version": "5.1.2", + "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "2.x" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/object-inspect": { + "version": "1.13.4", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/on-finished": { + "version": "2.4.1", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/once": { + "version": "1.4.0", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/@sap/cds-dk/node_modules/parseurl": { + "version": "1.3.3", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/path-to-regexp": { + "version": "8.3.0", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@sap/cds-dk/node_modules/pluralize": { + "version": "8.0.0", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@sap/cds-dk/node_modules/prebuild-install": { + "version": "7.1.3", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@sap/cds-dk/node_modules/proxy-addr": { + "version": "2.0.7", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@sap/cds-dk/node_modules/proxy-from-env": { + "version": "1.1.0", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/pump": { + "version": "3.0.4", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/@sap/cds-dk/node_modules/qs": { + "version": "6.15.0", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/range-parser": { + "version": "1.2.1", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/raw-body": { + "version": "3.0.2", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@sap/cds-dk/node_modules/rc": { + "version": "1.2.8", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/@sap/cds-dk/node_modules/readable-stream": { + "version": "3.6.2", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@sap/cds-dk/node_modules/router": { + "version": "2.2.0", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@sap/cds-dk/node_modules/safe-buffer": { + "version": "5.2.1", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/safer-buffer": { + "version": "2.1.2", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/sax": { + "version": "1.6.0", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/semver": { + "version": "7.7.4", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@sap/cds-dk/node_modules/send": { + "version": "1.2.1", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@sap/cds-dk/node_modules/serve-static": { + "version": "2.2.1", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@sap/cds-dk/node_modules/setprototypeof": { + "version": "1.2.0", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/@sap/cds-dk/node_modules/side-channel": { + "version": "1.1.0", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/side-channel-list": { + "version": "1.0.0", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/side-channel-map": { + "version": "1.0.1", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/side-channel-weakmap": { + "version": "1.0.2", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/simple-concat": { + "version": "1.0.1", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/simple-get": { + "version": "4.0.1", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/source-map": { + "version": "0.6.1", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/statuses": { + "version": "2.0.2", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/string_decoder": { + "version": "1.3.0", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/strip-json-comments": { + "version": "2.0.1", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/tar-fs": { + "version": "2.1.4", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/tar-stream": { + "version": "2.2.0", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sap/cds-dk/node_modules/to-regex-range": { + "version": "5.0.1", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/toidentifier": { + "version": "1.0.1", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/tunnel-agent": { + "version": "0.6.0", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@sap/cds-dk/node_modules/type-is": { + "version": "2.0.1", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/uglify-js": { + "version": "3.19.3", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/unpipe": { + "version": "1.0.0", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/util-deprecate": { + "version": "1.0.2", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/vary": { + "version": "1.1.2", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/verror": { + "version": "1.10.1", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/wordwrap": { + "version": "1.0.0", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/wrappy": { + "version": "1.0.2", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@sap/cds-dk/node_modules/ws": { + "version": "8.20.0", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@sap/cds-dk/node_modules/xml-js": { + "version": "1.6.11", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + }, + "node_modules/@sap/cds-dk/node_modules/yaml": { + "version": "2.8.3", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/@sap/cds-fiori": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sap/cds-fiori/-/cds-fiori-2.3.0.tgz", + "integrity": "sha512-6oWov+DSpFrSTgxXR0dZhak6aZ/IVRZvaHERMi0EgSTzIJdlvZlpw3Kf18ePMcTrRrtEXwD4RIjKt8pbs0g2Hg==", + "license": "SEE LICENSE IN LICENSE", + "peerDependencies": { + "@sap/cds": ">=8" + } + }, + "node_modules/@sap/cds-mtxs": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@sap/cds-mtxs/-/cds-mtxs-3.8.1.tgz", + "integrity": "sha512-FUm1of08WsZ1O3eur7OGj7ZYp9Gy3RuYEGBZK+zosSPyJ3QrBhBzKwYvbh2DJdbv3P/xuk46NGg5yz914qZxrw==", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@sap/hdi-deploy": "^5" + }, + "bin": { + "cds-mtx": "bin/cds-mtx.js", + "cds-mtx-migrate": "bin/cds-mtx-migrate.js" + }, + "peerDependencies": { + "@sap/cds": "^9" + } + }, + "node_modules/@sap/hdi": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@sap/hdi/-/hdi-4.8.0.tgz", + "integrity": "sha512-tkJmY2ffm6mt4/LFwRBihlQkMxNAXa3ngvRe2N/6+qLIsUNdrH/M03S5mkygXq56K+KoVVZYuradajCusMWwsw==", + "license": "See LICENSE file", + "dependencies": { + "async": "^3.2.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@sap/hana-client": "^2 >= 2.5", + "hdb": "^2 || ^0" + }, + "peerDependenciesMeta": { + "@sap/hana-client": { + "optional": true + }, + "hdb": { + "optional": true + } + } + }, + "node_modules/@sap/hdi-deploy": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@sap/hdi-deploy/-/hdi-deploy-5.6.1.tgz", + "integrity": "sha512-+qQ7qwG8lko303L5yRj2dud/nDAVuVblV/mmzJT44oPbF0Nry18eD2LUS23hFeuxjRa7rYK5YKQ8ffGgWxVrYQ==", + "license": "See LICENSE file", + "dependencies": { + "@sap/hdi": "^4.8.0", + "@sap/xsenv": "^6.0.0", + "async": "^3.2.6", + "dotenv": "^16.4.5", + "handlebars": "^4.7.8", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=18.x" + }, + "peerDependencies": { + "@sap/hana-client": "^2 >= 2.6", + "hdb": "^2 || ^0" + }, + "peerDependenciesMeta": { + "@sap/hana-client": { + "optional": true + }, + "hdb": { + "optional": true + } + } + }, + "node_modules/@sap/xsenv": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@sap/xsenv/-/xsenv-6.1.0.tgz", + "integrity": "sha512-vlW4Zad3uiDqHtnYdQ0TsEIH8VIO4HmPGDowfBL5dIcHPmeKDISEQ9ibeHL5FkceqvYcXJEQAVZ5/hsHDqlXZg==", + "license": "SEE LICENSE IN LICENSE file", + "dependencies": { + "debug": "4.4.3", + "node-cache": "^5.1.2", + "verror": "1.10.1" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + } + }, + "node_modules/@sap/xsenv/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@sap/xsenv/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/@sap/xssec": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@sap/xssec/-/xssec-4.13.0.tgz", + "integrity": "sha512-8e+bU+OyAIpAGXQanOopZa5YEK+yHKw84dhhihcCotF40MSNFbVHjQ4xM5hf4QndlqDGfXIuvXmoOMuDATa/gA==", + "license": "SAP DEVELOPER LICENSE AGREEMENT", + "dependencies": { + "debug": "^4.4.3", + "jwt-decode": "^4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sap/xssec/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@sap/xssec/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "12.8.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.8.0.tgz", + "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC" + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/express/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/express/node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/express/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/express/node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/mtx-local-sidecar": { + "resolved": "mtx/sidecar", + "link": true + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.89.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-cache": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", + "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", + "license": "MIT", + "dependencies": { + "clone": "2.x" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/router/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz", + "integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/integration-tests/srv/pom.xml b/integration-tests/srv/pom.xml deleted file mode 100644 index 6a5b6eada..000000000 --- a/integration-tests/srv/pom.xml +++ /dev/null @@ -1,121 +0,0 @@ - - - 4.0.0 - - - com.sap.cds.integration-tests - cds-feature-attachments-integration-tests-parent - ${revision} - - - cds-feature-attachments-integration-tests-srv - jar - - Integration Tests - Service - - - com.sap.cds.feature.attachments.generated - - - - - - - com.sap.cds - cds-starter-spring-boot - - - - - com.sap.cds - cds-adapter-odata-v4 - runtime - - - - org.springframework.security - spring-security-test - test - - - - org.springframework.boot - spring-boot-starter-test - test - - - - com.h2database - h2 - test - - - - org.wiremock.integrations - wiremock-spring-boot - test - - - - - - - - com.sap.cds - cds-maven-plugin - - - cds.clean - - clean - - - - - cds.install-node - - install-node - - - - - cds.resolve - - resolve - - - ${project.basedir}/.. - - - - - cds.build - - cds - - - ${project.basedir}/.. - - build --for java - deploy --to h2 --dry > - "${project.basedir}/src/main/resources/schema.sql" - - - - - - cds.generate - - generate - - - ${project.basedir}/.. - ${generation-package}.integration.test.cds4j - - - - - - - - diff --git a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/Application.java b/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/Application.java deleted file mode 100644 index b0b23079c..000000000 --- a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/Application.java +++ /dev/null @@ -1,15 +0,0 @@ -/* - * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class Application { - - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } -} diff --git a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/constants/Profiles.java b/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/constants/Profiles.java deleted file mode 100644 index 8ac4f8f3e..000000000 --- a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/constants/Profiles.java +++ /dev/null @@ -1,15 +0,0 @@ -/* - * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.constants; - -public final class Profiles { - - public static final String TEST_HANDLER_ENABLED = "test-handler-enabled"; - public static final String TEST_HANDLER_DISABLED = "test-handler-disabled"; - public static final String MALWARE_SCAN_ENABLED = "malware-scan-enabled"; - - private Profiles() { - // prevent instantiation - } -} diff --git a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/EventContextHolder.java b/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/EventContextHolder.java deleted file mode 100644 index 280227a89..000000000 --- a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/EventContextHolder.java +++ /dev/null @@ -1,8 +0,0 @@ -/* - * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.testhandler; - -import com.sap.cds.services.EventContext; - -public record EventContextHolder(String event, EventContext context) {} diff --git a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandler.java b/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandler.java deleted file mode 100644 index 207f49183..000000000 --- a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandler.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.testhandler; - -import static com.sap.cds.services.cds.CqnService.EVENT_CREATE; -import static com.sap.cds.services.cds.CqnService.EVENT_UPDATE; - -import com.sap.cds.services.ServiceException; -import com.sap.cds.services.handler.EventHandler; -import com.sap.cds.services.handler.annotations.Before; -import com.sap.cds.services.handler.annotations.ServiceName; -import com.sap.cds.services.persistence.PersistenceService; -import org.springframework.stereotype.Component; - -@ServiceName(value = "*", type = PersistenceService.class) -@Component -public class TestPersistenceHandler implements EventHandler { - - private volatile boolean throwExceptionOnUpdate = false; - private volatile boolean throwExceptionOnCreate = false; - - @Before(event = EVENT_UPDATE) - public void throwExceptionOnUpdate() { - if (throwExceptionOnUpdate) { - throw new ServiceException("Exception on update"); - } - } - - @Before(event = EVENT_CREATE) - public void throwExceptionOnCreate() { - if (throwExceptionOnCreate) { - throw new ServiceException("Exception on create"); - } - } - - public void reset() { - throwExceptionOnUpdate = false; - throwExceptionOnCreate = false; - } - - public void setThrowExceptionOnUpdate(boolean throwExceptionOnUpdate) { - this.throwExceptionOnUpdate = throwExceptionOnUpdate; - } - - public void setThrowExceptionOnCreate(boolean throwExceptionOnCreate) { - this.throwExceptionOnCreate = throwExceptionOnCreate; - } -} diff --git a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandler.java b/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandler.java deleted file mode 100644 index 341002905..000000000 --- a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandler.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.testhandler; - -import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.StatusCode; -import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; -import com.sap.cds.feature.attachments.service.AttachmentService; -import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentCreateEventContext; -import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentMarkAsDeletedEventContext; -import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentReadEventContext; -import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentRestoreEventContext; -import com.sap.cds.services.handler.EventHandler; -import com.sap.cds.services.handler.annotations.On; -import com.sap.cds.services.handler.annotations.ServiceName; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.slf4j.Marker; -import org.slf4j.MarkerFactory; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@ServiceName(value = "*", type = AttachmentService.class) -@Profile(Profiles.TEST_HANDLER_ENABLED) -@Component -public class TestPluginAttachmentsServiceHandler implements EventHandler { - - private static final Marker marker = MarkerFactory.getMarker("DUMMY_HANDLER"); - private static final Logger logger = - LoggerFactory.getLogger(TestPluginAttachmentsServiceHandler.class); - - private static final Map documents = new ConcurrentHashMap<>(); - private static final List eventContextHolder = - Collections.synchronizedList(new ArrayList<>()); - - @On(event = AttachmentService.EVENT_CREATE_ATTACHMENT) - public void createAttachment(AttachmentCreateEventContext context) throws IOException { - logger.info(marker, "CREATE Attachment called in dummy handler"); - var contentId = UUID.randomUUID().toString(); - documents.put(contentId, context.getData().getContent().readAllBytes()); - context.setContentId(contentId); - context.getData().setStatus(StatusCode.CLEAN); - context.getData().setScannedAt(Instant.now()); - context.setCompleted(); - eventContextHolder.add( - new EventContextHolder(AttachmentService.EVENT_CREATE_ATTACHMENT, context)); - } - - @On(event = AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED) - public void markAttachmentAsDeleted(AttachmentMarkAsDeletedEventContext context) { - logger.info( - marker, - "DELETE Attachment called in dummy handler for document id {}", - context.getContentId()); - context.setCompleted(); - eventContextHolder.add( - new EventContextHolder(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED, context)); - } - - @On(event = AttachmentService.EVENT_READ_ATTACHMENT) - public void readAttachment(AttachmentReadEventContext context) { - logger.info( - marker, - "READ Attachment called in dummy handler for content id {}", - context.getContentId()); - var contentId = context.getContentId(); - var content = contentId != null ? documents.get(contentId) : null; - var stream = content != null ? new ByteArrayInputStream(content) : null; - context.getData().setContent(stream); - context.setCompleted(); - eventContextHolder.add( - new EventContextHolder(AttachmentService.EVENT_READ_ATTACHMENT, context)); - } - - @On(event = AttachmentService.EVENT_RESTORE_ATTACHMENT) - public void restoreAttachment(AttachmentRestoreEventContext context) { - logger.info( - marker, - "RESTORE Attachment called in dummy handler for timestamp {}", - context.getRestoreTimestamp()); - context.setCompleted(); - eventContextHolder.add( - new EventContextHolder(AttachmentService.EVENT_RESTORE_ATTACHMENT, context)); - } - - public List getEventContextForEvent(String event) { - var context = eventContextHolder.stream().filter(e -> e.event().equals(event)).toList(); - if (event.equals(AttachmentService.EVENT_CREATE_ATTACHMENT) && !context.isEmpty()) { - context.forEach( - c -> { - var createContext = (AttachmentCreateEventContext) c.context(); - createContext - .getData() - .setContent(new ByteArrayInputStream(documents.get(createContext.getContentId()))); - }); - } - return context; - } - - public List getEventContext() { - return eventContextHolder; - } - - public void clearEventContext() { - eventContextHolder.clear(); - } - - public void clearDocuments() { - documents.clear(); - } -} diff --git a/integration-tests/srv/src/main/resources/application.yaml b/integration-tests/srv/src/main/resources/application.yaml deleted file mode 100644 index 68a4a6e63..000000000 --- a/integration-tests/srv/src/main/resources/application.yaml +++ /dev/null @@ -1,4 +0,0 @@ ---- -spring: - config.activate.on-profile: default - sql.init.schema-locations: classpath:schema.sql diff --git a/integration-tests/srv/src/main/resources/banner.txt b/integration-tests/srv/src/main/resources/banner.txt deleted file mode 100644 index 875e346ff..000000000 --- a/integration-tests/srv/src/main/resources/banner.txt +++ /dev/null @@ -1,9 +0,0 @@ - __ _ _ _ _ __ - / / /\ | | | | | | | | \ \ - / / / \ | |_ | |_ __ _ ___ | |__ _ __ ___ ___ _ __ | |_ ___ \ \ - < < / /\ \ | __| | __| / _` | / __| | '_ \ | '_ ` _ \ / _ \ | '_ \ | __| / __| > > - \ \ / ____ \ | |_ | |_ | (_| | | (__ | | | | | | | | | | | __/ | | | | | |_ \__ \ / / - \_\ /_/ \_\ \__| \__| \__,_| \___| |_| |_| |_| |_| |_| \___| |_| |_| \__| |___/ /_/ - ================================================================================================= - :: Spring Boot :: ${spring-boot.formatted-version} - diff --git a/integration-tests/srv/src/main/resources/messages.properties b/integration-tests/srv/src/main/resources/messages.properties deleted file mode 100644 index 81680eda5..000000000 --- a/integration-tests/srv/src/main/resources/messages.properties +++ /dev/null @@ -1 +0,0 @@ -not_clean=Error text for not clean \ No newline at end of file diff --git a/integration-tests/srv/src/main/resources/spotbugs-exclusion-filter.xml b/integration-tests/srv/src/main/resources/spotbugs-exclusion-filter.xml deleted file mode 100644 index b5e184082..000000000 --- a/integration-tests/srv/src/main/resources/spotbugs-exclusion-filter.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/JsonToCapMapperTestHelper.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/JsonToCapMapperTestHelper.java deleted file mode 100644 index 5bc8eb32e..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/JsonToCapMapperTestHelper.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.common; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.sap.cds.CdsData; -import com.sap.cds.Struct; -import java.util.HashMap; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -@Component -class JsonToCapMapperTestHelper { - - @Autowired private ObjectMapper objectMapper; - - public CdsData mapResponseToSingleResult(String resultBody) throws Exception { - return Struct.access(objectMapper.readValue(resultBody, HashMap.class)).as(CdsData.class); - } -} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MalwareScanResultProvider.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MalwareScanResultProvider.java deleted file mode 100644 index f36ec06f0..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MalwareScanResultProvider.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.common; - -import org.springframework.stereotype.Component; - -@Component -public class MalwareScanResultProvider { - - public String buildMalwareScanResult(boolean malware) { - return """ - { - \t"malwareDetected": %s, - \t"encryptedContentDetected": false, - \t"scanSize": 68, - \t"finding": "Win.Test.EICAR_HDB-1", - \t"mimeType": "text/plain", - \t"SHA256": "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", - \t"extensions": [ - \t\t"txt" - \t] - } - """ - .formatted(malware); - } -} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MockHttpRequestHelper.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MockHttpRequestHelper.java deleted file mode 100644 index 426e88f2a..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MockHttpRequestHelper.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.common; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.sap.cds.CdsData; -import com.sap.cds.Struct; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.stereotype.Component; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.MvcResult; -import org.springframework.test.web.servlet.ResultMatcher; -import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; - -@Component -public class MockHttpRequestHelper { - - public static final String ODATA_BASE_URL = "/odata/v4/"; - public static final String IF_MATCH = "If-Match"; - - @Autowired private JsonToCapMapperTestHelper mapper; - @Autowired private MockMvc mvc; - - private String contentType = MediaType.APPLICATION_JSON.toString(); - private String accept = MediaType.APPLICATION_JSON.toString(); - - public MvcResult executeGet(String url) throws Exception { - MockHttpServletRequestBuilder requestBuilder = - MockMvcRequestBuilders.get(url).contentType(contentType).accept(accept); - return mvc.perform(requestBuilder).andReturn(); - } - - public String executeGetWithSingleODataResponseAndAssertStatus(String url, HttpStatus status) - throws Exception { - var result = executeGet(url); - assertThat(result.getResponse().getStatus()).isEqualTo(status.value()); - return result.getResponse().getContentAsString(); - } - - public T executeGetWithSingleODataResponseAndAssertStatus( - String url, Class resultType, HttpStatus status) throws Exception { - var resultBody = executeGetWithSingleODataResponseAndAssertStatus(url, status); - return Struct.access(mapper.mapResponseToSingleResult(resultBody)).as(resultType); - } - - public MvcResult executePost(String url, String body) throws Exception { - return mvc.perform( - MockMvcRequestBuilders.post(url).contentType(contentType).accept(accept).content(body)) - .andReturn(); - } - - public MvcResult executePatch(String url, String body) throws Exception { - return executePatch(url, body, "*"); - } - - public MvcResult executePatch(String url, String body, String etag) throws Exception { - return mvc.perform( - MockMvcRequestBuilders.patch(url) - .contentType(contentType) - .accept(accept) - .header(IF_MATCH, etag) - .content(body)) - .andReturn(); - } - - public void executePostWithMatcher(String url, String body, ResultMatcher matcher) - throws Exception { - mvc.perform( - MockMvcRequestBuilders.post(url).contentType(contentType).accept(accept).content(body)) - .andExpect(matcher); - } - - public MvcResult executeDelete(String url) throws Exception { - return executeDelete(url, "*"); - } - - public MvcResult executeDelete(String url, String etag) throws Exception { - return mvc.perform( - MockMvcRequestBuilders.delete(url) - .contentType(contentType) - .accept(accept) - .header(IF_MATCH, etag)) - .andReturn(); - } - - public void executeDeleteWithMatcher(String url, ResultMatcher matcher) throws Exception { - executeDeleteWithMatcher(url, "*", matcher); - } - - public void executeDeleteWithMatcher(String url, String etag, ResultMatcher matcher) - throws Exception { - mvc.perform( - MockMvcRequestBuilders.delete(url) - .contentType(contentType) - .accept(accept) - .header(IF_MATCH, etag)) - .andExpect(matcher); - } - - public CdsData executePostWithODataResponseAndAssertStatusCreated(String url, String body) - throws Exception { - return executePostWithODataResponseAndAssertStatus(url, body, HttpStatus.CREATED); - } - - public void executePatchWithODataResponseAndAssertStatusOk(String url, String body) - throws Exception { - executePatchWithODataResponseAndAssertStatus(url, body, HttpStatus.OK); - } - - public CdsData executePostWithODataResponseAndAssertStatus( - String url, String body, HttpStatus status) throws Exception { - MvcResult result = executePost(url, body); - String resultBody = result.getResponse().getContentAsString(); - assertThat(result.getResponse().getStatus()) - .as("Unexpected HTTP status, with response body " + resultBody) - .isEqualTo(status.value()); - return mapper.mapResponseToSingleResult(resultBody); - } - - public void executePatchWithODataResponseAndAssertStatus( - String url, String body, HttpStatus status) throws Exception { - executePatchWithODataResponseAndAssertStatus(url, body, "*", status); - } - - public void executePatchWithODataResponseAndAssertStatus( - String url, String body, String etag, HttpStatus status) throws Exception { - MvcResult result = executePatch(url, body, etag); - String resultBody = result.getResponse().getContentAsString(); - assertThat(result.getResponse().getStatus()) - .as("Unexpected HTTP status, with response body " + resultBody) - .isEqualTo(status.value()); - } - - public void executePutWithMatcher(String url, byte[] body, ResultMatcher matcher) - throws Exception { - executePutWithMatcher(url, body, "*", matcher); - } - - public void executePutWithMatcher(String url, byte[] body, String etag, ResultMatcher matcher) - throws Exception { - mvc.perform( - MockMvcRequestBuilders.put(url) - .contentType(contentType) - .accept(accept) - .header(IF_MATCH, etag) - .content(body)) - .andExpect(matcher); - } - - public void setContentType(MediaType contentType) { - this.contentType = contentType.toString(); - } - - public void setContentType(String contentType) { - this.contentType = contentType; - } - - public void resetHelper() { - contentType = MediaType.APPLICATION_JSON.toString(); - accept = MediaType.APPLICATION_JSON.toString(); - } -} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/TableDataDeleter.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/TableDataDeleter.java deleted file mode 100644 index a56d44a7c..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/TableDataDeleter.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.common; - -import com.sap.cds.ql.Delete; -import com.sap.cds.services.persistence.PersistenceService; -import java.util.Arrays; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -@Component -public class TableDataDeleter { - - @Autowired private PersistenceService persistenceService; - - public void deleteData(String... entityNames) { - Arrays.stream(entityNames) - .forEach(entityName -> persistenceService.run(Delete.from(entityName))); - } -} 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 deleted file mode 100644 index 843fa2bbc..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationBase.java +++ /dev/null @@ -1,973 +0,0 @@ -/* - * © 2024-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; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import com.sap.cds.Struct; -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.sap.attachments.Attachments; -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testdraftservice.AttachmentEntity; -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testdraftservice.DraftRoots; -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testdraftservice.DraftRoots_; -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testdraftservice.Items; -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testdraftservice.TestDraftService_; -import com.sap.cds.feature.attachments.integrationtests.common.MockHttpRequestHelper; -import com.sap.cds.feature.attachments.integrationtests.common.TableDataDeleter; -import com.sap.cds.feature.attachments.integrationtests.testhandler.TestPersistenceHandler; -import com.sap.cds.feature.attachments.integrationtests.testhandler.TestPluginAttachmentsServiceHandler; -import com.sap.cds.feature.attachments.service.AttachmentService; -import com.sap.cds.ql.Select; -import com.sap.cds.ql.StructuredType; -import com.sap.cds.services.persistence.PersistenceService; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.nio.charset.StandardCharsets; -import java.util.concurrent.TimeUnit; -import org.awaitility.Awaitility; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.HttpStatus; -import org.springframework.test.web.servlet.MvcResult; -import org.springframework.test.web.servlet.ResultMatcher; - -@SpringBootTest -@AutoConfigureMockMvc -abstract class DraftOdataRequestValidationBase { - - 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; - - @AfterEach - void teardown() { - dataDeleter.deleteData( - DraftRoots_.CDS_NAME, DraftRoots_.CDS_NAME + "_drafts", "cds.outbox.Messages"); - requestHelper.resetHelper(); - clearServiceHandlerContext(); - testPersistenceHandler.reset(); - } - - @Test - void deepCreateWorks() throws Exception { - var testContentAttachment = "testContent attachment"; - var testContentAttachmentEntity = "testContent attachmentEntity"; - - var selectedRoot = deepCreateAndActivate(testContentAttachment, testContentAttachmentEntity); - - assertThat(selectedRoot.getIsActiveEntity()).isTrue(); - - var selectedAttachment = selectedRoot.getItems().get(0).getAttachments().get(0); - var selectedAttachmentEntity = selectedRoot.getItems().get(0).getAttachmentEntities().get(0); - - verifyContentId(selectedAttachment.getContentId(), selectedAttachment.getId()); - assertThat(selectedAttachment.getFileName()).isEqualTo("itemAttachment.txt"); - assertThat(selectedAttachment.getMimeType()).contains("text/plain"); - verifyContent(selectedAttachment.getContent(), testContentAttachment); - verifyContentId(selectedAttachmentEntity.getContentId(), selectedAttachmentEntity.getId()); - assertThat(selectedAttachmentEntity.getFileName()).isEqualTo("itemAttachmentEntity.txt"); - assertThat(selectedAttachmentEntity.getMimeType()).contains("image/jpeg"); - verifyContent(selectedAttachmentEntity.getContent(), testContentAttachmentEntity); - verifyOnlyTwoCreateEvents(testContentAttachment, testContentAttachmentEntity); - } - - @Test - void contentCanBeReadFromDraft() throws Exception { - var testContentAttachment = "testContent attachment"; - var testContentAttachmentEntity = "testContent attachmentEntity"; - - var root = deepCreateAndActivate(testContentAttachment, testContentAttachmentEntity); - - var selectedRoot = selectStoredRootData(root); - assertThat(selectedRoot.getItems().get(0).getAttachments()) - .hasSize(1) - .first() - .satisfies(attachment -> verifyContent(attachment.getContent(), testContentAttachment)); - assertThat(selectedRoot.getItems().get(0).getAttachmentEntities()) - .hasSize(1) - .first() - .satisfies( - attachment -> verifyContent(attachment.getContent(), testContentAttachmentEntity)); - 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"; - - Awaitility.await() - .atMost(60, TimeUnit.SECONDS) - .pollDelay(1, TimeUnit.SECONDS) - .pollInterval(2, TimeUnit.SECONDS) - .until( - () -> { - var attachmentResponse = requestHelper.executeGet(attachmentUrl); - var attachmentEntityResponse = requestHelper.executeGet(attachmentEntityUrl); - var attachmentResponseContent = getResponseContent(attachmentResponse); - var attachmentEntityResponseContent = getResponseContent(attachmentEntityResponse); - 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: {}", - attachmentResponseContent, - testContentAttachment, - attachmentEntityResponseContent, - testContentAttachmentEntity); - } - return result; - }); - clearServiceHandlerContext(); - - var attachmentResponse = requestHelper.executeGet(attachmentUrl); - assertThat(attachmentResponse.getResponse().getContentAsString()) - .isEqualTo(testContentAttachment); - var attachmentEntityResponse = requestHelper.executeGet(attachmentEntityUrl); - assertThat(attachmentEntityResponse.getResponse().getContentAsString()) - .isEqualTo(testContentAttachmentEntity); - verifyTwoReadEvents(); - } - - @Test - void deleteAttachmentAndActivateDraft() throws Exception { - 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 attachmentEntityDeleteUrl = getAttachmentEntityBaseUrl(itemAttachmentEntity.getId(), false); - - requestHelper.executeDeleteWithMatcher(attachmentDeleteUrl, status().isNoContent()); - requestHelper.executeDeleteWithMatcher(attachmentEntityDeleteUrl, status().isNoContent()); - verifyNoAttachmentEventsCalled(); - - prepareAndActiveDraft(getRootUrl(selectedRoot.getId(), false)); - - var selectedRootAfterDelete = selectStoredRootData(selectedRoot); - assertThat(selectedRootAfterDelete.getItems().get(0).getAttachments()).isEmpty(); - assertThat(selectedRootAfterDelete.getItems().get(0).getAttachmentEntities()).isEmpty(); - verifyOnlyTwoDeleteEvents(itemAttachment.getContentId(), itemAttachmentEntity.getContentId()); - } - - @Test - void updateAttachmentAndActivateDraft() throws Exception { - 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 changedAttachmentFileName = "changedAttachmentFileName.txt"; - var changedAttachmentEntityFileName = "changedAttachmentEntityFileName.txt"; - - updateFileName( - selectedRoot, - itemAttachment, - itemAttachmentEntity, - changedAttachmentFileName, - changedAttachmentEntityFileName); - - prepareAndActiveDraft(getRootUrl(selectedRoot.getId(), false)); - var selectedRootAfterUpdate = selectStoredRootData(selectedRoot); - assertThat(selectedRootAfterUpdate.getItems().get(0).getAttachments().get(0).getFileName()) - .isEqualTo(changedAttachmentFileName); - assertThat( - selectedRootAfterUpdate.getItems().get(0).getAttachmentEntities().get(0).getFileName()) - .isEqualTo(changedAttachmentEntityFileName); - verifyNoAttachmentEventsCalled(); - } - - @Test - void updateAttachmentAndCancelDraft() throws Exception { - 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 originAttachmentFileName = itemAttachment.getFileName(); - var originAttachmentEntityFileName = itemAttachmentEntity.getFileName(); - - updateFileName( - selectedRoot, - itemAttachment, - itemAttachmentEntity, - "changedAttachmentFileName.txt", - "changedAttachmentEntityFileName.txt"); - - cancelDraft(getRootUrl(selectedRoot.getId(), false)); - var selectedRootAfterUpdate = selectStoredRootData(selectedRoot); - assertThat(selectedRootAfterUpdate.getItems().get(0).getAttachments().get(0).getFileName()) - .isEqualTo(originAttachmentFileName); - assertThat( - selectedRootAfterUpdate.getItems().get(0).getAttachmentEntities().get(0).getFileName()) - .isEqualTo(originAttachmentEntityFileName); - verifyNoAttachmentEventsCalled(); - } - - @Test - void createAttachmentAndActivateDraft() throws Exception { - var selectedRoot = - deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); - clearServiceHandlerContext(); - createNewDraftForExistingRoot(selectedRoot.getId()); - - var itemAttachment = selectedRoot.getItems().get(0); - - var newAttachmentContent = "new attachment content"; - createAttachmentWithContent(newAttachmentContent, itemAttachment.getId()); - var newAttachmentEntityContent = "new attachmentEntity content"; - createAttachmentEntityWithContent(newAttachmentEntityContent, itemAttachment); - - prepareAndActiveDraft(getRootUrl(selectedRoot.getId(), false)); - var selectedRootAfterCreate = selectStoredRootData(selectedRoot); - assertThat(selectedRootAfterCreate.getItems().get(0).getAttachments()).hasSize(2); - assertThat(selectedRootAfterCreate.getItems().get(0).getAttachmentEntities()).hasSize(2); - verifyOnlyTwoCreateEvents(newAttachmentContent, newAttachmentEntityContent); - } - - @Test - void createAttachmentAndCancelDraft() throws Exception { - var selectedRoot = - deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); - clearServiceHandlerContext(); - createNewDraftForExistingRoot(selectedRoot.getId()); - - var itemAttachment = selectedRoot.getItems().get(0); - - var newAttachmentContent = "new attachment content"; - createAttachmentWithContent(newAttachmentContent, itemAttachment.getId()); - var newAttachmentEntityContent = "new attachmentEntity content"; - createAttachmentEntityWithContent(newAttachmentEntityContent, itemAttachment); - - cancelDraft(getRootUrl(selectedRoot.getId(), false)); - var selectedRootAfterCreate = selectStoredRootData(selectedRoot); - assertThat(selectedRootAfterCreate.getItems().get(0).getAttachments()).hasSize(1); - assertThat(selectedRootAfterCreate.getItems().get(0).getAttachmentEntities()).hasSize(1); - verifyTwoCreateAndDeleteEvents(newAttachmentContent, newAttachmentEntityContent); - } - - @Test - void deleteContentInDraft() throws Exception { - var selectedRoot = - deepCreateAndActivate( - "testContent attachment for delete", "testContent attachmentEntity for delete"); - clearServiceHandlerContext(); - createNewDraftForExistingRoot(selectedRoot.getId()); - var itemAttachment = selectedRoot.getItems().get(0).getAttachments().get(0); - var itemAttachmentEntity = selectedRoot.getItems().get(0).getAttachmentEntities().get(0); - - deleteContent(selectedRoot, itemAttachment, itemAttachmentEntity); - verifyNoAttachmentEventsCalled(); - - prepareAndActiveDraft(getRootUrl(selectedRoot.getId(), false)); - var selectedRootAfterDelete = selectStoredRootData(selectedRoot); - verifyContent( - selectedRootAfterDelete.getItems().get(0).getAttachments().get(0).getContent(), null); - verifyContent( - selectedRootAfterDelete.getItems().get(0).getAttachmentEntities().get(0).getContent(), - null); - verifyOnlyTwoDeleteEvents(itemAttachment.getContentId(), itemAttachmentEntity.getContentId()); - } - - @Test - void doNotDeleteContentInCancelledDraft() throws Exception { - 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); - - deleteContent(selectedRoot, itemAttachment, itemAttachmentEntity); - - cancelDraft(getRootUrl(selectedRoot.getId(), false)); - var selectedRootAfterDelete = selectStoredRootData(selectedRoot); - verifyContent( - selectedRootAfterDelete.getItems().get(0).getAttachments().get(0).getContent(), - "testContent attachment"); - verifyContent( - selectedRootAfterDelete.getItems().get(0).getAttachmentEntities().get(0).getContent(), - "testContent attachmentEntity"); - verifyNoAttachmentEventsCalled(); - } - - @Test - void updateContentInDraft() throws Exception { - 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 attachmentContentId = itemAttachment.getContentId(); - var attachmentEntityContentId = itemAttachmentEntity.getContentId(); - - var newAttachmentContent = "new content attachment"; - putNewContentForAttachment( - newAttachmentContent, selectedRoot.getItems().get(0).getId(), itemAttachment.getId()); - var newAttachmentEntityContent = "new content attachmentEntity"; - putNewContentForAttachmentEntity(newAttachmentEntityContent, itemAttachmentEntity.getId()); - - prepareAndActiveDraft(getRootUrl(selectedRoot.getId(), false)); - var selectedRootAfterUpdate = selectStoredRootData(selectedRoot); - verifyContent( - selectedRootAfterUpdate.getItems().get(0).getAttachments().get(0).getContent(), - newAttachmentContent); - verifyContent( - selectedRootAfterUpdate.getItems().get(0).getAttachmentEntities().get(0).getContent(), - newAttachmentEntityContent); - verifyEventContextEmptyForEvent(AttachmentService.EVENT_READ_ATTACHMENT); - verifyTwoUpdateEvents( - newAttachmentContent, - attachmentContentId, - newAttachmentEntityContent, - attachmentEntityContentId); - var selectedRootAfterDeletion = selectStoredRootData(selectedRoot); - assertThat(selectedRootAfterDeletion.getItems().get(0).getAttachments().get(0).getContentId()) - .isNotEmpty(); - assertThat( - selectedRootAfterDeletion - .getItems() - .get(0) - .getAttachmentEntities() - .get(0) - .getContentId()) - .isNotEmpty(); - } - - @Test - void contentCanBeReadForActiveRoot() throws Exception { - var attachmentContent = "attachment Content"; - var attachmentEntityContent = "attachmentEntity Content"; - var selectedRoot = deepCreateAndActivate(attachmentContent, attachmentEntityContent); - clearServiceHandlerContext(); - - readAndValidateActiveContent(selectedRoot, attachmentContent, attachmentEntityContent); - } - - @Test - void noChangesOnAttachmentsContentStillAvailable() throws Exception { - var attachmentContent = "attachment Content"; - var attachmentEntityContent = "attachmentEntity Content"; - var selectedRoot = deepCreateAndActivate(attachmentContent, attachmentEntityContent); - clearServiceHandlerContext(); - createNewDraftForExistingRoot(selectedRoot.getId()); - - var rootUrl = getRootUrl(selectedRoot.getId(), false); - requestHelper.executePatchWithODataResponseAndAssertStatusOk( - rootUrl, "{\"title\":\"some other title\"}"); - - prepareAndActiveDraft(rootUrl); - verifyNoAttachmentEventsCalled(); - - readAndValidateActiveContent(selectedRoot, attachmentContent, attachmentEntityContent); - } - - @Test - void deleteItemAndActivateDraft() throws Exception { - var attachmentContent = "attachment Content"; - var attachmentEntityContent = "attachmentEntity Content"; - var selectedRoot = deepCreateAndActivate(attachmentContent, attachmentEntityContent); - clearServiceHandlerContext(); - createNewDraftForExistingRoot(selectedRoot.getId()); - - var itemUrl = getItemUrl(selectedRoot.getItems().get(0), false); - requestHelper.executeDeleteWithMatcher(itemUrl, status().isNoContent()); - verifyNoAttachmentEventsCalled(); - - prepareAndActiveDraft(getRootUrl(selectedRoot.getId(), false)); - var selectedRootAfterDelete = selectStoredRootData(selectedRoot); - assertThat(selectedRootAfterDelete.getItems()).isEmpty(); - verifyOnlyTwoDeleteEvents( - selectedRoot.getItems().get(0).getAttachments().get(0).getContentId(), - selectedRoot.getItems().get(0).getAttachmentEntities().get(0).getContentId()); - } - - @Test - void deleteItemAndCancelDraft() throws Exception { - var attachmentContent = "attachment Content"; - var attachmentEntityContent = "attachmentEntity Content"; - var selectedRoot = deepCreateAndActivate(attachmentContent, attachmentEntityContent); - clearServiceHandlerContext(); - createNewDraftForExistingRoot(selectedRoot.getId()); - - var itemUrl = getItemUrl(selectedRoot.getItems().get(0), false); - requestHelper.executeDeleteWithMatcher(itemUrl, status().isNoContent()); - - cancelDraft(getRootUrl(selectedRoot.getId(), false)); - var selectedRootAfterDelete = selectStoredRootData(selectedRoot); - assertThat(selectedRootAfterDelete.getItems()).isNotEmpty(); - assertThat(selectedRootAfterDelete.getItems().get(0).getAttachments()).isNotEmpty(); - assertThat(selectedRootAfterDelete.getItems().get(0).getAttachments().get(0).getContentId()) - .isNotEmpty(); - assertThat(selectedRootAfterDelete.getItems().get(0).getAttachmentEntities()).isNotEmpty(); - assertThat( - selectedRootAfterDelete.getItems().get(0).getAttachmentEntities().get(0).getContentId()) - .isNotEmpty(); - verifyNoAttachmentEventsCalled(); - } - - @Test - void noEventsForForDeletedRoot() throws Exception { - var selectedRoot = deepCreateAndActivate("attachmentContent", "attachmentEntityContent"); - clearServiceHandlerContext(); - createNewDraftForExistingRoot(selectedRoot.getId()); - - var rootUrl = getRootUrl(selectedRoot.getId(), true); - requestHelper.executeDeleteWithMatcher(rootUrl, status().isNoContent()); - - var draftPrepareUrl = rootUrl + "/TestDraftService.draftPrepare"; - requestHelper.executePostWithMatcher( - draftPrepareUrl, "{\"SideEffectsQualifier\":\"\"}", status().isNotFound()); - - var select = Select.from(TestDraftService_.DRAFT_ROOTS); - var result = persistenceService.run(select).listOf(DraftRoots.class); - assertThat(result).isEmpty(); - - var attachmentContentId = selectedRoot.getItems().get(0).getAttachments().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"); - clearServiceHandlerContext(); - createNewDraftForExistingRoot(selectedRoot.getId()); - - createNewContentAndValidateEvents(selectedRoot); - - prepareAndActiveDraft(getRootUrl(selectedRoot.getId(), false)); - var selectedRootAfterCreate = selectStoredRootData(selectedRoot); - assertThat(selectedRootAfterCreate.getItems().get(0).getAttachments()).hasSize(2); - assertThat(selectedRootAfterCreate.getItems().get(0).getAttachmentEntities()).hasSize(2); - verifyNoAttachmentEventsCalled(); - } - - @Test - void errorInTransactionAfterCreateCallsDeleteAndNothingForCancel() throws Exception { - var selectedRoot = - deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); - clearServiceHandlerContext(); - createNewDraftForExistingRoot(selectedRoot.getId()); - - createNewContentAndValidateEvents(selectedRoot); - - cancelDraft(getRootUrl(selectedRoot.getId(), false)); - var selectedRootAfterCreate = selectStoredRootData(selectedRoot); - assertThat(selectedRootAfterCreate.getItems().get(0).getAttachments()).hasSize(1); - assertThat(selectedRootAfterCreate.getItems().get(0).getAttachmentEntities()).hasSize(1); - verifyNoAttachmentEventsCalled(); - } - - @Test - void errorInTransactionAfterUpdateCallsDelete() throws Exception { - var attachmentContent = "testContent attachment"; - var attachmentEntityContent = "testContent attachmentEntity"; - var selectedRoot = deepCreateAndActivate(attachmentContent, attachmentEntityContent); - clearServiceHandlerContext(); - createNewDraftForExistingRoot(selectedRoot.getId()); - - var itemAttachment = selectedRoot.getItems().get(0).getAttachments().get(0); - var itemAttachmentEntity = selectedRoot.getItems().get(0).getAttachmentEntities().get(0); - - updateContentWithErrorAndValidateEvents(selectedRoot, itemAttachment, itemAttachmentEntity); - - testPersistenceHandler.reset(); - prepareAndActiveDraft(getRootUrl(selectedRoot.getId(), false)); - verifyNothingHasChangedInDraft(selectedRoot, attachmentContent, attachmentEntityContent); - } - - @Test - void errorInTransactionAfterUpdateCallsDeleteEvenIfDraftIsCancelled() throws Exception { - var attachmentContent = "testContent attachment"; - var attachmentEntityContent = "testContent attachmentEntity"; - var selectedRoot = deepCreateAndActivate(attachmentContent, attachmentEntityContent); - clearServiceHandlerContext(); - createNewDraftForExistingRoot(selectedRoot.getId()); - - var itemAttachment = selectedRoot.getItems().get(0).getAttachments().get(0); - var itemAttachmentEntity = selectedRoot.getItems().get(0).getAttachmentEntities().get(0); - - updateContentWithErrorAndValidateEvents(selectedRoot, itemAttachment, itemAttachmentEntity); - - testPersistenceHandler.reset(); - cancelDraft(getRootUrl(selectedRoot.getId(), false)); - verifyNothingHasChangedInDraft(selectedRoot, attachmentContent, attachmentEntityContent); - } - - @Test - void createAndDeleteAttachmentWorks() throws Exception { - var selectedRoot = - deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); - clearServiceHandlerContext(); - createNewDraftForExistingRoot(selectedRoot.getId()); - - var itemAttachment = selectedRoot.getItems().get(0); - - var newAttachmentContent = "new attachment content"; - createAttachmentWithContent(newAttachmentContent, itemAttachment.getId()); - var newAttachmentEntityContent = "new attachmentEntity content"; - createAttachmentEntityWithContent(newAttachmentEntityContent, itemAttachment); - - var draftRoot = selectStoredRootData(DraftRoots_.CDS_NAME + "_drafts", selectedRoot); - - 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 attachmentEntityDeleteUrl = getAttachmentEntityBaseUrl(newAttachmentEntity.getId(), false); - - requestHelper.executeDeleteWithMatcher(attachmentDeleteUrl, status().isNoContent()); - requestHelper.executeDeleteWithMatcher(attachmentEntityDeleteUrl, status().isNoContent()); - - verifyTwoCreateAndDeleteEvents(newAttachmentContent, newAttachmentEntityContent); - clearServiceHandlerContext(); - - prepareAndActiveDraft(getRootUrl(selectedRoot.getId(), false)); - verifyNoAttachmentEventsCalled(); - } - - protected DraftRoots deepCreateAndActivate( - String testContentAttachment, String testContentAttachmentEntity) throws Exception { - var responseRoot = createNewDraft(); - var rootUrl = updateRoot(responseRoot); - var responseItem = createItem(rootUrl); - createAttachmentWithContent(testContentAttachment, responseItem.getId()); - createAttachmentEntityWithContent(testContentAttachmentEntity, responseItem); - prepareAndActiveDraft(rootUrl); - - return selectStoredRootData(responseRoot); - } - - private DraftRoots createNewDraft() throws Exception { - var responseRootCdsData = - requestHelper.executePostWithODataResponseAndAssertStatusCreated(BASE_ROOT_URL, "{}"); - return Struct.access(responseRootCdsData).as(DraftRoots.class); - } - - private void createNewDraftForExistingRoot(String rootId) throws Exception { - var url = getRootUrl(rootId, true) + "/TestDraftService.draftEdit"; - requestHelper.executePostWithODataResponseAndAssertStatus( - url, "{\"PreserveChanges\":true}", HttpStatus.OK); - } - - private String updateRoot(DraftRoots responseRoot) throws Exception { - responseRoot.setTitle("some title"); - var rootUrl = getRootUrl(responseRoot.getId(), false); - requestHelper.executePatchWithODataResponseAndAssertStatusOk(rootUrl, responseRoot.toJson()); - return rootUrl; - } - - private String getRootUrl(String rootId, boolean isActiveEntity) { - return BASE_ROOT_URL + "(ID=" + rootId + ",IsActiveEntity=" + isActiveEntity + ")"; - } - - 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()); - return Struct.access(responseItemCdsData).as(Items.class); - } - - private void createAttachmentWithContent(String testContentAttachment, String itemId) - throws Exception { - createAttachmentWithContent(testContentAttachment, itemId, status().isNoContent(), false); - } - - private void createAttachmentWithContent( - String testContentAttachment, String itemId, ResultMatcher matcher, boolean withError) - throws Exception { - var responseAttachment = createAttachment(itemId); - if (withError) { - testPersistenceHandler.setThrowExceptionOnUpdate(true); - } - putNewContentForAttachment(testContentAttachment, itemId, responseAttachment.getId(), matcher); - } - - private void putNewContentForAttachment( - String testContentAttachment, String itemId, String attachmentId) throws Exception { - putNewContentForAttachment(testContentAttachment, itemId, attachmentId, status().isNoContent()); - } - - private void putNewContentForAttachment( - String testContentAttachment, String itemId, String attachmentId, ResultMatcher matcher) - throws Exception { - var attachmentPutUrl = getAttachmentBaseUrl(itemId, attachmentId, false) + "/content"; - requestHelper.setContentType("text/plain"); - requestHelper.executePutWithMatcher( - attachmentPutUrl, testContentAttachment.getBytes(StandardCharsets.UTF_8), matcher); - requestHelper.resetHelper(); - } - - private Attachments createAttachment(String itemId) throws Exception { - var itemAttachment = Attachments.create(); - itemAttachment.setFileName("itemAttachment.txt"); - - var attachmentPostUrl = BASE_URL + "Items(ID=" + itemId + ",IsActiveEntity=false)/attachments"; - var responseAttachmentCdsData = - requestHelper.executePostWithODataResponseAndAssertStatusCreated( - attachmentPostUrl, itemAttachment.toJson()); - return Struct.access(responseAttachmentCdsData).as(Attachments.class); - } - - private void createAttachmentEntityWithContent( - String testContentAttachmentEntity, Items responseItem) throws Exception { - createAttachmentEntityWithContent( - testContentAttachmentEntity, responseItem, status().isNoContent(), false); - } - - private void createAttachmentEntityWithContent( - String testContentAttachmentEntity, - Items responseItem, - ResultMatcher matcher, - boolean withError) - throws Exception { - var responseAttachmentEntity = createAttachmentEntity(responseItem); - if (withError) { - testPersistenceHandler.setThrowExceptionOnUpdate(true); - } - putNewContentForAttachmentEntity( - testContentAttachmentEntity, responseAttachmentEntity.getId(), matcher); - } - - private void putNewContentForAttachmentEntity( - String testContentAttachmentEntity, String attachmentId) throws Exception { - putNewContentForAttachmentEntity( - testContentAttachmentEntity, attachmentId, status().isNoContent()); - } - - private void putNewContentForAttachmentEntity( - String testContentAttachmentEntity, String attachmentId, ResultMatcher matcher) - throws Exception { - var attachmentEntityPutUrl = - BASE_URL + "/AttachmentEntity(ID=" + attachmentId + ",IsActiveEntity=false)/content"; - requestHelper.setContentType("image/jpeg"); - requestHelper.executePutWithMatcher( - attachmentEntityPutUrl, - testContentAttachmentEntity.getBytes(StandardCharsets.UTF_8), - matcher); - requestHelper.resetHelper(); - } - - private AttachmentEntity createAttachmentEntity(Items responseItem) throws Exception { - var itemAttachmentEntity = AttachmentEntity.create(); - itemAttachmentEntity.setFileName("itemAttachmentEntity.txt"); - - var attachmentEntityPostUrl = getItemUrl(responseItem, false) + "/attachmentEntities"; - var responseAttachmentEntityCdsData = - requestHelper.executePostWithODataResponseAndAssertStatusCreated( - attachmentEntityPostUrl, itemAttachmentEntity.toJson()); - return Struct.access(responseAttachmentEntityCdsData).as(AttachmentEntity.class); - } - - private String getItemUrl(Items responseItem, boolean isActiveEntity) { - return BASE_URL - + "Items(ID=" - + responseItem.getId() - + ",IsActiveEntity=" - + isActiveEntity - + ")"; - } - - protected String getAttachmentBaseUrl( - String itemId, String attachmentId, boolean isActiveEntity) { - return BASE_URL - + "Items_attachments(up__ID=" - + itemId - + ",ID=" - + attachmentId - + ",IsActiveEntity=" - + isActiveEntity - + ")"; - } - - protected String getAttachmentEntityBaseUrl(String attachmentId, boolean isActiveEntity) { - return BASE_URL - + "AttachmentEntity(ID=" - + attachmentId - + ",IsActiveEntity=" - + isActiveEntity - + ")"; - } - - private void prepareAndActiveDraft(String rootUrl) throws Exception { - var draftPrepareUrl = rootUrl + "/TestDraftService.draftPrepare"; - var draftActivateUrl = rootUrl + "/TestDraftService.draftActivate"; - requestHelper.executePostWithMatcher( - draftPrepareUrl, "{\"SideEffectsQualifier\":\"\"}", status().isOk()); - requestHelper.executePostWithMatcher(draftActivateUrl, "{}", status().isOk()); - } - - private void cancelDraft(String rootUrl) throws Exception { - requestHelper.executeDeleteWithMatcher(rootUrl, status().isNoContent()); - } - - private DraftRoots selectStoredRootData(DraftRoots responseRoot) { - return selectStoredRootData(DraftRoots_.CDS_NAME, 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())); - 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"; - - Awaitility.await() - .atMost(60, TimeUnit.SECONDS) - .pollDelay(1, TimeUnit.SECONDS) - .pollInterval(2, TimeUnit.SECONDS) - .until( - () -> { - var attachmentResponse = requestHelper.executeGet(attachmentUrl); - var attachmentEntityResponse = requestHelper.executeGet(attachmentEntityUrl); - var attachmentContentAsString = attachmentResponse.getResponse().getContentAsString(); - var attachmentEntityContentAsString = - attachmentEntityResponse.getResponse().getContentAsString(); - - var booleanResult = - attachmentContentAsString.equals(attachmentContent) - && attachmentEntityContentAsString.equals(attachmentEntityContent); - - if (!booleanResult) { - logger.info( - "Attachment response content: {}, Attachment Test Content: {}, Attachment Entity response content: {}, Attachment Entity Test Content: {}", - attachmentContentAsString, - attachmentContent, - attachmentEntityContentAsString, - attachmentEntityContent); - } - return booleanResult; - }); - clearServiceHandlerContext(); - - var attachmentResponse = requestHelper.executeGet(attachmentUrl); - var attachmentEntityResponse = requestHelper.executeGet(attachmentEntityUrl); - - assertThat(attachmentResponse.getResponse().getContentAsString()).isEqualTo(attachmentContent); - assertThat(attachmentEntityResponse.getResponse().getContentAsString()) - .isEqualTo(attachmentEntityContent); - verifyTwoReadEvents(); - } - - 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"; - - requestHelper.executeDeleteWithMatcher(attachmentUrl, status().isNoContent()); - requestHelper.executeDeleteWithMatcher(attachmentEntityUrl, status().isNoContent()); - } - - private void updateFileName( - DraftRoots selectedRoot, - Attachments itemAttachment, - AttachmentEntity itemAttachmentEntity, - String changedAttachmentFileName, - String changedAttachmentEntityFileName) - throws Exception { - updateFileName( - selectedRoot, - itemAttachment, - itemAttachmentEntity, - changedAttachmentFileName, - changedAttachmentEntityFileName, - HttpStatus.OK); - } - - private void updateFileName( - DraftRoots selectedRoot, - Attachments itemAttachment, - AttachmentEntity itemAttachmentEntity, - String changedAttachmentFileName, - String changedAttachmentEntityFileName, - HttpStatus httpStatus) - throws Exception { - var attachmentUrl = - getAttachmentBaseUrl(selectedRoot.getItems().get(0).getId(), itemAttachment.getId(), false); - var attachmentEntityUrl = getAttachmentEntityBaseUrl(itemAttachmentEntity.getId(), false); - - requestHelper.executePatchWithODataResponseAndAssertStatus( - attachmentUrl, "{\"fileName\":\"" + changedAttachmentFileName + "\"}", httpStatus); - requestHelper.executePatchWithODataResponseAndAssertStatus( - attachmentEntityUrl, - "{\"fileName\":\"" + changedAttachmentEntityFileName + "\"}", - httpStatus); - } - - private void updateContentWithErrorAndValidateEvents( - DraftRoots selectedRoot, Attachments itemAttachment, AttachmentEntity itemAttachmentEntity) - throws Exception { - testPersistenceHandler.setThrowExceptionOnUpdate(true); - var newAttachmentContent = "new content attachment"; - putNewContentForAttachment( - newAttachmentContent, - selectedRoot.getItems().get(0).getId(), - itemAttachment.getId(), - status().is5xxServerError()); - var newAttachmentEntityContent = "new content attachmentEntity"; - putNewContentForAttachmentEntity( - newAttachmentEntityContent, itemAttachmentEntity.getId(), status().is5xxServerError()); - verifyTwoCreateAndRevertedDeleteEvents(); - clearServiceHandlerContext(); - } - - private void verifyNothingHasChangedInDraft( - DraftRoots selectedRoot, String attachmentContent, String attachmentEntityContent) - throws IOException { - var selectedRootAfterUpdate = selectStoredRootData(selectedRoot); - - verifyContent( - selectedRootAfterUpdate.getItems().get(0).getAttachments().get(0).getContent(), - attachmentContent); - verifyContent( - selectedRootAfterUpdate.getItems().get(0).getAttachmentEntities().get(0).getContent(), - attachmentEntityContent); - verifyNoAttachmentEventsCalled(); - var selectedRootAfterDeletion = selectStoredRootData(selectedRoot); - assertThat(selectedRootAfterDeletion.getItems().get(0).getAttachments().get(0).getContentId()) - .isNotEmpty(); - assertThat( - selectedRootAfterDeletion - .getItems() - .get(0) - .getAttachmentEntities() - .get(0) - .getContentId()) - .isNotEmpty(); - } - - private void createNewContentAndValidateEvents(DraftRoots selectedRoot) throws Exception { - var itemAttachment = selectedRoot.getItems().get(0); - var newAttachmentContent = "new attachment content"; - createAttachmentWithContent( - newAttachmentContent, itemAttachment.getId(), status().is5xxServerError(), true); - testPersistenceHandler.reset(); - var newAttachmentEntityContent = "new attachmentEntity content"; - createAttachmentEntityWithContent( - newAttachmentEntityContent, itemAttachment, status().is5xxServerError(), true); - verifyTwoCreateAndDeleteEvents(newAttachmentContent, newAttachmentEntityContent); - clearServiceHandlerContext(); - testPersistenceHandler.reset(); - } - - private String getResponseContent(MvcResult attachmentResponse) - throws UnsupportedEncodingException { - return attachmentResponse.getResponse().getStatus() == HttpStatus.OK.value() - ? attachmentResponse.getResponse().getContentAsString() - : ""; - } - - protected abstract void verifyContentId(String contentId, String attachmentId); - - protected abstract void verifyContent(InputStream attachment, String testContent) - throws IOException; - - protected abstract void verifyNoAttachmentEventsCalled(); - - protected abstract void clearServiceHandlerContext(); - - protected abstract void verifyEventContextEmptyForEvent(String... events); - - protected abstract void verifyOnlyTwoCreateEvents( - String newAttachmentContent, String newAttachmentEntityContent); - - protected abstract void verifyTwoCreateAndDeleteEvents( - String newAttachmentContent, String newAttachmentEntityContent); - - protected abstract void verifyTwoReadEvents(); - - protected abstract void verifyOnlyTwoDeleteEvents( - String attachmentContentId, String attachmentEntityContentId); - - protected abstract void verifyTwoUpdateEvents( - String newAttachmentContent, - String attachmentContentId, - String newAttachmentEntityContent, - String attachmentEntityContentId); - - protected abstract void verifyTwoCreateAndRevertedDeleteEvents(); -} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithTestHandlerTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithTestHandlerTest.java deleted file mode 100644 index 4ddb6280a..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithTestHandlerTest.java +++ /dev/null @@ -1,288 +0,0 @@ -/* - * © 2024-2024 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; - -import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; -import com.sap.cds.feature.attachments.integrationtests.testhandler.EventContextHolder; -import com.sap.cds.feature.attachments.service.AttachmentService; -import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentCreateEventContext; -import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentMarkAsDeletedEventContext; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.TimeUnit; -import org.awaitility.Awaitility; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.test.context.ActiveProfiles; - -@ActiveProfiles(Profiles.TEST_HANDLER_ENABLED) -class DraftOdataRequestValidationWithTestHandlerTest extends DraftOdataRequestValidationBase { - - private static final Logger logger = - LoggerFactory.getLogger(DraftOdataRequestValidationWithTestHandlerTest.class); - - @Test - void serviceHandlerIsNotEmpty() { - assertThat(serviceHandler).isNotNull(); - verifyNoAttachmentEventsCalled(); - } - - @Override - protected void verifyContentId(String contentId, String attachmentId) { - assertThat(contentId).isNotEmpty().isNotEqualTo(attachmentId); - } - - @Override - protected void verifyContent(InputStream attachment, String testContent) { - assertThat(attachment).isNull(); - } - - @Override - protected void verifyNoAttachmentEventsCalled() { - assertThat(serviceHandler.getEventContext()).isEmpty(); - } - - @Override - protected void clearServiceHandlerContext() { - serviceHandler.clearEventContext(); - } - - @Override - protected void verifyEventContextEmptyForEvent(String... events) { - Arrays.stream(events) - .forEach(event -> assertThat(serviceHandler.getEventContextForEvent(event)).isEmpty()); - } - - @Override - protected void verifyOnlyTwoCreateEvents( - String newAttachmentContent, String newAttachmentEntityContent) { - verifyEventContextEmptyForEvent( - AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED, - AttachmentService.EVENT_READ_ATTACHMENT); - var createEvents = - serviceHandler.getEventContextForEvent(AttachmentService.EVENT_CREATE_ATTACHMENT); - assertThat(createEvents).hasSize(2); - var attachmentContentFound = - isAttachmentContentFoundInCreateEvent(createEvents, newAttachmentContent); - assertThat(attachmentContentFound).isTrue(); - var attachmentEntityContentFound = - isAttachmentContentFoundInCreateEvent(createEvents, newAttachmentEntityContent); - assertThat(attachmentEntityContentFound).isTrue(); - } - - @Override - protected void verifyTwoCreateAndDeleteEvents( - String newAttachmentContent, String newAttachmentEntityContent) { - awaitNumberOfExpectedEvents(4); - verifyEventContextEmptyForEvent(AttachmentService.EVENT_READ_ATTACHMENT); - var createEvents = - serviceHandler.getEventContextForEvent(AttachmentService.EVENT_CREATE_ATTACHMENT); - assertThat(createEvents).hasSize(2); - var attachmentContentFound = - isAttachmentContentFoundInCreateEvent(createEvents, newAttachmentContent); - assertThat(attachmentContentFound).isTrue(); - var attachmentEntityContentFound = - isAttachmentContentFoundInCreateEvent(createEvents, newAttachmentEntityContent); - assertThat(attachmentEntityContentFound).isTrue(); - var deleteEvents = - serviceHandler.getEventContextForEvent(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); - assertThat(deleteEvents).hasSize(2); - deleteEvents.forEach( - event -> { - var deleteContext = (AttachmentMarkAsDeletedEventContext) event.context(); - assertThat(deleteContext.getContentId()).isNotEmpty(); - var createEventFound = - createEvents.stream() - .anyMatch( - createEvent -> { - var createContext = (AttachmentCreateEventContext) createEvent.context(); - return createContext.getContentId().equals(deleteContext.getContentId()); - }); - assertThat(createEventFound).isTrue(); - }); - } - - @Override - protected void verifyTwoReadEvents() { - verifyEventContextEmptyForEvent( - AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED, - AttachmentService.EVENT_CREATE_ATTACHMENT); - var readEvents = - serviceHandler.getEventContextForEvent(AttachmentService.EVENT_READ_ATTACHMENT); - assertThat(readEvents).hasSize(2); - } - - @Override - protected void verifyOnlyTwoDeleteEvents( - String attachmentContentId, String attachmentEntityContentId) { - awaitNumberOfExpectedEvents(2); - verifyEventContextEmptyForEvent( - AttachmentService.EVENT_CREATE_ATTACHMENT, AttachmentService.EVENT_READ_ATTACHMENT); - var deleteEvents = - serviceHandler.getEventContextForEvent(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); - assertThat(deleteEvents).hasSize(2); - verifyDeleteEventContainsContentId(deleteEvents, attachmentContentId); - verifyDeleteEventContainsContentId(deleteEvents, attachmentEntityContentId); - } - - @Override - protected void verifyTwoUpdateEvents( - String newAttachmentContent, - String attachmentContentId, - String newAttachmentEntityContent, - String attachmentEntityContentId) { - awaitNumberOfExpectedEvents(4); - var createEvents = - serviceHandler.getEventContextForEvent(AttachmentService.EVENT_CREATE_ATTACHMENT); - var deleteEvents = - serviceHandler.getEventContextForEvent(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); - assertThat(createEvents).hasSize(2); - verifyCreateEventFound(createEvents, newAttachmentContent); - verifyCreateEventFound(createEvents, newAttachmentEntityContent); - assertThat(deleteEvents).hasSize(2); - verifyDeleteEventContainsContentId(deleteEvents, attachmentContentId); - verifyDeleteEventContainsContentId(deleteEvents, attachmentEntityContentId); - } - - @Override - protected void verifyTwoCreateAndRevertedDeleteEvents() { - awaitNumberOfExpectedEvents(4); - var createEvents = - serviceHandler.getEventContextForEvent(AttachmentService.EVENT_CREATE_ATTACHMENT); - var deleteEvents = - serviceHandler.getEventContextForEvent(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); - assertThat(createEvents).hasSize(2); - assertThat(deleteEvents).hasSize(2); - deleteEvents.forEach( - event -> { - var deleteContext = (AttachmentMarkAsDeletedEventContext) event.context(); - var createEventFound = - createEvents.stream() - .anyMatch( - createEvent -> { - var createContext = (AttachmentCreateEventContext) createEvent.context(); - return createContext.getContentId().equals(deleteContext.getContentId()); - }); - assertThat(createEventFound).isTrue(); - }); - } - - private void awaitNumberOfExpectedEvents(int expectedEvents) { - Awaitility.await() - .atMost(60, TimeUnit.SECONDS) - .pollDelay(1, TimeUnit.SECONDS) - .pollInterval(2, TimeUnit.SECONDS) - .until( - () -> { - var eventCalls = serviceHandler.getEventContext().size(); - logger.info( - "Waiting for expected size '{}' in handler context, was '{}'", - expectedEvents, - eventCalls); - var numberMatch = eventCalls >= expectedEvents; - if (!numberMatch) { - serviceHandler.getEventContext().forEach(event -> logger.info("Event: {}", event)); - } - return numberMatch; - }); - } - - private void verifyCreateEventFound(List createEvents, String newContent) { - var eventContentFound = - createEvents.stream() - .anyMatch( - event -> { - var createContext = (AttachmentCreateEventContext) event.context(); - try { - return Arrays.equals( - createContext.getData().getContent().readAllBytes(), - newContent.getBytes(StandardCharsets.UTF_8)); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - assertThat(eventContentFound).isTrue(); - } - - private boolean isAttachmentContentFoundInCreateEvent( - List createEvents, String newAttachmentContent) { - return createEvents.stream() - .anyMatch( - event -> { - var createContext = (AttachmentCreateEventContext) event.context(); - try { - return Arrays.equals( - createContext.getData().getContent().readAllBytes(), - newAttachmentContent.getBytes(StandardCharsets.UTF_8)); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - } - - private void verifyDeleteEventContainsContentId( - List deleteEvents, String contentId) { - var eventFound = - deleteEvents.stream() - .anyMatch( - event -> { - var deleteContext = (AttachmentMarkAsDeletedEventContext) event.context(); - return deleteContext.getContentId().equals(contentId); - }); - assertThat(eventFound).isTrue(); - } - - // Override flaky tests from base class to disable them. - // These tests are affected by a race condition in the CAP runtime's outbox TaskScheduler - // where the second DELETE event is not processed when two transactions fail in quick succession. - - @Disabled("Flaky due to CAP runtime outbox race condition - second DELETE event not processed") - @Test - @Override - void errorInTransactionAfterCreateCallsDelete() throws Exception { - super.errorInTransactionAfterCreateCallsDelete(); - } - - @Disabled("Flaky due to CAP runtime outbox race condition - second DELETE event not processed") - @Test - @Override - void errorInTransactionAfterCreateCallsDeleteAndNothingForCancel() throws Exception { - super.errorInTransactionAfterCreateCallsDeleteAndNothingForCancel(); - } - - @Disabled("Flaky due to CAP runtime outbox race condition - second DELETE event not processed") - @Test - @Override - void errorInTransactionAfterUpdateCallsDelete() throws Exception { - super.errorInTransactionAfterUpdateCallsDelete(); - } - - @Disabled("Flaky due to CAP runtime outbox race condition - second DELETE event not processed") - @Test - @Override - void errorInTransactionAfterUpdateCallsDeleteEvenIfDraftIsCancelled() throws Exception { - super.errorInTransactionAfterUpdateCallsDeleteEvenIfDraftIsCancelled(); - } - - @Disabled("Flaky due to CAP runtime outbox race condition - second DELETE event not processed") - @Test - @Override - void createAttachmentAndCancelDraft() throws Exception { - super.createAttachmentAndCancelDraft(); - } - - @Disabled("Flaky due to CAP runtime outbox race condition - second DELETE event not processed") - @Test - @Override - void createAndDeleteAttachmentWorks() throws Exception { - super.createAndDeleteAttachmentWorks(); - } -} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java deleted file mode 100644 index 817700d8a..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * © 2024-2025 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.draftservice; - -import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; -import static com.github.tomakehurst.wiremock.client.WireMock.post; -import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; -import static org.assertj.core.api.Assertions.assertThat; - -import com.github.tomakehurst.wiremock.WireMockServer; -import com.sap.cds.feature.attachments.integrationtests.common.MalwareScanResultProvider; -import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; -import java.util.concurrent.TimeUnit; -import org.awaitility.Awaitility; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.context.ActiveProfiles; -import org.wiremock.spring.ConfigureWireMock; -import org.wiremock.spring.EnableWireMock; -import org.wiremock.spring.InjectWireMock; - -@ActiveProfiles(profiles = {Profiles.TEST_HANDLER_DISABLED, Profiles.MALWARE_SCAN_ENABLED}) -@EnableWireMock({@ConfigureWireMock(port = 1111, name = "malware-scanner")}) -class DraftOdataRequestValidationWithoutTestHandlerAndMalwareScannerTest - extends DraftOdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest { - - private static final Logger logger = - LoggerFactory.getLogger( - DraftOdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.class); - - @InjectWireMock("malware-scanner") - private WireMockServer wiremock; - - @Autowired private MalwareScanResultProvider malwareScanResultProvider; - - @BeforeEach - void setup() { - mockMalwareScanResult(false); - } - - @Override - @AfterEach - void teardown() { - super.teardown(); - wiremock.resetAll(); - } - - @Test - void contentCanNotBeReadForActiveRoot() throws Exception { - wiremock.resetAll(); - mockMalwareScanResult(true); - var attachmentContent = "attachment Content"; - var attachmentEntityContent = "attachmentEntity Content"; - var selectedRoot = deepCreateAndActivate(attachmentContent, attachmentEntityContent); - clearServiceHandlerContext(); - - 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) - .pollDelay(1, TimeUnit.SECONDS) - .until( - () -> { - var attachmentResponse = requestHelper.executeGet(attachmentUrl); - var attachmentEntityResponse = requestHelper.executeGet(attachmentEntityUrl); - - var attachmentResponseContent = attachmentResponse.getResponse().getContentAsString(); - var attachmentEntityResponseContent = - attachmentEntityResponse.getResponse().getContentAsString(); - - logger.info( - "Status should contain 'not_clean' for attachment and attachment entity but was: {} for attachment and {} for attachment entity", - attachmentResponseContent, - attachmentEntityResponseContent); - - return attachmentResponseContent.contains("not_clean") - && attachmentEntityResponseContent.contains("not_clean"); - }); - clearServiceHandlerContext(); - - var attachmentResponse = requestHelper.executeGet(attachmentUrl); - var attachmentEntityResponse = requestHelper.executeGet(attachmentEntityUrl); - - assertThat(attachmentResponse.getResponse().getContentAsString()) - .contains("Error text for not clean"); - assertThat(attachmentEntityResponse.getResponse().getContentAsString()) - .contains("Error text for not clean"); - verifyTwoReadEvents(); - } - - private void mockMalwareScanResult(boolean malware) { - wiremock.stubFor( - post(urlEqualTo("/scan")) - .withBasicAuth("test-user", "test-password") - .willReturn( - aResponse() - .withBody(malwareScanResultProvider.buildMalwareScanResult(malware)) - .withStatus(200))); - } -} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java deleted file mode 100644 index 7dce33a89..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * © 2024-2024 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; - -import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.util.Objects; -import org.junit.jupiter.api.Test; -import org.springframework.test.context.ActiveProfiles; - -@ActiveProfiles(Profiles.TEST_HANDLER_DISABLED) -class DraftOdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest - extends DraftOdataRequestValidationBase { - - @Test - void serviceHandlerIsNull() { - assertThat(serviceHandler).isNull(); - } - - @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(StandardCharsets.UTF_8)); - } else { - assertThat(attachment).isNull(); - } - } - - @Override - protected void verifyNoAttachmentEventsCalled() { - // no service handler - nothing to do - } - - @Override - protected void clearServiceHandlerContext() { - // no service handler - nothing to do - } - - @Override - protected void verifyEventContextEmptyForEvent(String... events) { - // no service handler - nothing to do - } - - @Override - protected void verifyOnlyTwoCreateEvents( - String newAttachmentContent, String newAttachmentEntityContent) { - // no service handler - nothing to do - } - - @Override - protected void verifyTwoCreateAndDeleteEvents( - String newAttachmentContent, String newAttachmentEntityContent) { - // no service handler - nothing to do - } - - @Override - protected void verifyTwoReadEvents() { - // no service handler - nothing to do - } - - @Override - protected void verifyOnlyTwoDeleteEvents( - String attachmentContentId, String attachmentEntityContentId) { - // no service handler - nothing to do - } - - @Override - protected void verifyTwoUpdateEvents( - String newAttachmentContent, - String attachmentContentId, - String newAttachmentEntityContent, - String attachmentEntityContentId) { - // no service handler - nothing to do - } - - @Override - protected void verifyTwoCreateAndRevertedDeleteEvents() { - // no service handler - nothing to do - } -} 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 deleted file mode 100644 index f1ddbb49d..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/MediaValidatedAttachmentsDraftTest.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * © 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; -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; -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.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; -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"; - private static final ObjectMapper objectMapper = new ObjectMapper(); - - @BeforeEach - void setup() { - requestHelper.setContentType(MediaType.APPLICATION_JSON); - } - - @ParameterizedTest - @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(); - String metadata = objectMapper.writeValueAsString(Map.of("fileName", fileName)); - - requestHelper.executePostWithMatcher( - buildDraftAttachmentCreationUrl(rootId), metadata, status().is(expectedStatus)); - } - - private String buildDraftAttachmentCreationUrl(String rootId) { - return BASE_ROOT_URL - + "(ID=" - + rootId - + ",IsActiveEntity=false)" - + "/mediaValidatedAttachments"; - } - - @Test - void shouldPass_whenFileNameMissing_inDraft() throws Exception { - String rootId = createDraftRootAndReturnId(); - String metadata = "{}"; - requestHelper.executePostWithMatcher( - buildDraftAttachmentCreationUrl(rootId), metadata, status().isCreated()); - } - - // Helper methods - private String createDraftRootAndReturnId() throws Exception { - CdsData response = - requestHelper.executePostWithODataResponseAndAssertStatusCreated(BASE_ROOT_URL, "{}"); - - DraftRoots draftRoot = Struct.access(response).as(DraftRoots.class); - String payload = objectMapper.writeValueAsString(Map.of("title", "Draft")); - requestHelper.executePatchWithODataResponseAndAssertStatusOk( - getRootUrl(draftRoot.getId(), false), payload); - - 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 deleted file mode 100644 index df8db48a9..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/SizeLimitedAttachmentsSizeValidationDraftTest.java +++ /dev/null @@ -1,202 +0,0 @@ -/* - * © 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; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import com.sap.cds.Struct; -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.sap.attachments.Attachments; -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.List; -import java.util.Objects; -import org.junit.jupiter.api.Test; -import org.springframework.test.context.ActiveProfiles; - -@ActiveProfiles(Profiles.TEST_HANDLER_DISABLED) -class SizeLimitedAttachmentsSizeValidationDraftTest extends DraftOdataRequestValidationBase { - - private static final String BASE_URL = MockHttpRequestHelper.ODATA_BASE_URL + "TestDraftService/"; - private static final String BASE_ROOT_URL = BASE_URL + "DraftRoots"; - - @Test - 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()); - requestHelper.setContentType(org.springframework.http.MediaType.APPLICATION_OCTET_STREAM); - requestHelper.executePutWithMatcher(url, content, status().isNoContent()); - } - - @Test - 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 - } - - @Test - void uploadContentWithinLimitAndActivateDraftSucceeds() throws Exception { - // Arrange: Create draft with sizeLimitedAttachments (no prior activation) - var draftRoot = createNewDraftWithSizeLimitedAttachments(); - var attachment = draftRoot.getSizeLimitedAttachments().get(0); - - // Act: Upload 3MB content (within 5MB limit) - byte[] content = new byte[3 * 1024 * 1024]; // 3MB - var url = buildDraftSizeLimitedAttachmentContentUrl(draftRoot.getId(), attachment.getId()); - requestHelper.setContentType(org.springframework.http.MediaType.APPLICATION_OCTET_STREAM); - requestHelper.executePutWithMatcher(url, content, status().isNoContent()); - - // Assert: Draft activation succeeds - requestHelper.setContentType(org.springframework.http.MediaType.APPLICATION_JSON); - var rootUrl = getRootUrl(draftRoot.getId(), false); - var draftPrepareUrl = rootUrl + "/TestDraftService.draftPrepare"; - var draftActivateUrl = rootUrl + "/TestDraftService.draftActivate"; - requestHelper.executePostWithMatcher( - draftPrepareUrl, "{\"SideEffectsQualifier\":\"\"}", status().isOk()); - requestHelper.executePostWithMatcher(draftActivateUrl, "{}", status().isOk()); - } - - @Test - void uploadContentExceedingLimitOnFirstDraftRejects() throws Exception { - // Arrange: Create draft with sizeLimitedAttachments (no prior activation) - var draftRoot = createNewDraftWithSizeLimitedAttachments(); - var attachment = draftRoot.getSizeLimitedAttachments().get(0); - - // Act & Assert: Upload 6MB content to a brand-new draft attachment fails immediately - 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)); - } - - // Helper methods - private DraftRoots createNewDraftWithSizeLimitedAttachments() throws Exception { - // Create new draft - var responseRootCdsData = - requestHelper.executePostWithODataResponseAndAssertStatusCreated(BASE_ROOT_URL, "{}"); - var draftRoot = Struct.access(responseRootCdsData).as(DraftRoots.class); - - // Update root with title - draftRoot.setTitle("Root with sizeLimitedAttachments"); - var rootUrl = getRootUrl(draftRoot.getId(), false); - requestHelper.executePatchWithODataResponseAndAssertStatusOk(rootUrl, draftRoot.toJson()); - - // Create sizeLimitedAttachment - var attachment = Attachments.create(); - attachment.setFileName("testFile.txt"); - attachment.setMimeType("text/plain"); - var attachmentUrl = rootUrl + "/sizeLimitedAttachments"; - var responseAttachmentCdsData = - requestHelper.executePostWithODataResponseAndAssertStatusCreated( - attachmentUrl, attachment.toJson()); - var createdAttachment = Struct.access(responseAttachmentCdsData).as(Attachments.class); - - // Build result with the attachment - draftRoot.setSizeLimitedAttachments(List.of(createdAttachment)); - return draftRoot; - } - - private String getRootUrl(String rootId, boolean isActiveEntity) { - return BASE_ROOT_URL + "(ID=" + rootId + ",IsActiveEntity=" + isActiveEntity + ")"; - } - - private String buildDraftSizeLimitedAttachmentContentUrl(String rootId, String attachmentId) { - return BASE_ROOT_URL - + "(ID=" - + rootId - + ",IsActiveEntity=false)" - + "/sizeLimitedAttachments(ID=" - + attachmentId - + ",up__ID=" - + rootId - + ",IsActiveEntity=false)" - + "/content"; - } - - // 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() { - // no service handler - nothing to do - } - - @Override - protected void clearServiceHandlerContext() { - // no service handler - nothing to do - } - - @Override - protected void verifyEventContextEmptyForEvent(String... events) { - // no service handler - nothing to do - } - - @Override - protected void verifyOnlyTwoCreateEvents( - String newAttachmentContent, String newAttachmentEntityContent) { - // no service handler - nothing to do - } - - @Override - protected void verifyTwoCreateAndDeleteEvents( - String newAttachmentContent, String newAttachmentEntityContent) { - // no service handler - nothing to do - } - - @Override - protected void verifyTwoReadEvents() { - // no service handler - nothing to do - } - - @Override - protected void verifyOnlyTwoDeleteEvents( - String attachmentContentId, String attachmentEntityContentId) { - // no service handler - nothing to do - } - - @Override - protected void verifyTwoUpdateEvents( - String newAttachmentContent, - String attachmentContentId, - String newAttachmentEntityContent, - String attachmentEntityContentId) { - // no service handler - nothing to do - } - - @Override - protected void verifyTwoCreateAndRevertedDeleteEvents() { - // no service handler - nothing to do - } -} 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 deleted file mode 100644 index 35a3b549b..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java +++ /dev/null @@ -1,296 +0,0 @@ -/* - * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.nondraftservice; - -import static org.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; -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.RootEntityBuilder; -import com.sap.cds.ql.Select; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -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"; - private static final ObjectMapper objectMapper = new ObjectMapper(); - - 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()); - } - - @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()); - } - - @ParameterizedTest - @CsvSource({ - // valid cases - "'test1.jpeg|test2.jpeg',201", - // invalid media types - "'test.pdf',415", - "'test1.jpeg|test2.pdf',415", - // invalid filenames - "'',400", - "' ',400", - // edge cases - "'.gitignore',415" - }) - void shouldValidateMediaTypes_forMultipleAttachments(String fileNames, int expectedStatus) - throws Exception { - String payload = buildPayload(fileNames); - requestHelper.executePostWithMatcher(BASE_URL, payload, status().is(expectedStatus)); - } - - @Test - void shouldAcceptWhenMediaValidatedAttachments_hasNoAttachments() throws Exception { - Map payload = new HashMap<>(); - payload.put("title", "Hello World!"); - payload.put("mediaValidatedAttachments", List.of()); - - String payloadStr = objectMapper.writeValueAsString(payload); - requestHelper.executePostWithMatcher(BASE_URL, payloadStr, status().is(201)); - } - - @Test - void shouldAcceptDeepCreate_whenMixedValidAndAllValidAttachments() throws Exception { - Map payload = new HashMap<>(); - payload.put("title", "Hello World!"); - payload.put( - "mediaValidatedAttachments", - List.of(Map.of("fileName", "test1.jpeg"), Map.of("fileName", "test2.jpeg"))); - - payload.put("mimeValidatedAttachments", List.of(Map.of("fileName", "test3.pdf"))); - - requestHelper.executePostWithMatcher( - BASE_URL, objectMapper.writeValueAsString(payload), status().isCreated()); - } - - @Test - void shouldRejectDeepCreate_whenMixedValidAndInvalidAttachments() throws Exception { - Map payload = new HashMap<>(); - payload.put("title", "Hello World!"); - payload.put( - "mediaValidatedAttachments", - List.of(Map.of("fileName", "test1.pdf"), Map.of("fileName", "test2.jpeg"))); - - payload.put("mimeValidatedAttachments", List.of(Map.of("fileName", "test3.pdf"))); - - requestHelper.executePostWithMatcher( - BASE_URL, objectMapper.writeValueAsString(payload), status().isUnsupportedMediaType()); - } - - private String createRootAndReturnId() throws Exception { - // Build the initial Java object.. Root - Roots serviceRoot = buildServiceRoot(); - - // 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 buildPayload(String fileNames) throws JsonProcessingException { - List> attachments = new ArrayList<>(); - fileNames = fileNames.replaceAll("^'+|'+$", ""); - for (String name : fileNames.split("\\|")) { - attachments.add(Map.of("fileName", name)); - } - Map payload = new HashMap<>(); - payload.put("title", "Hello World!"); - payload.put("mediaValidatedAttachments", attachments); - - return objectMapper.writeValueAsString(payload); - } - - private String createUrl(String rootId, String path) { - return BASE_URL + "(" + rootId + ")" + (path == null || path.isBlank() ? "" : "/" + path); - } - - private String createAttachmentMetadata(String fileName) throws JsonProcessingException { - return objectMapper.writeValueAsString(Map.of("fileName", fileName)); - } - - // helper method - private Roots buildServiceRoot() { - return RootEntityBuilder.create().setTitle("Root").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/OdataRequestValidationBase.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationBase.java deleted file mode 100644 index a4774951f..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationBase.java +++ /dev/null @@ -1,884 +0,0 @@ -/* - * © 2024-2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.nondraftservice; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -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.AttachmentEntity_; -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.Items; -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.Items_; -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.common.TableDataDeleter; -import com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper.AttachmentsBuilder; -import com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper.AttachmentsEntityBuilder; -import com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper.ItemEntityBuilder; -import com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper.RootEntityBuilder; -import com.sap.cds.feature.attachments.integrationtests.testhandler.TestPersistenceHandler; -import com.sap.cds.feature.attachments.integrationtests.testhandler.TestPluginAttachmentsServiceHandler; -import com.sap.cds.feature.attachments.service.AttachmentService; -import com.sap.cds.ql.Select; -import com.sap.cds.ql.StructuredType; -import com.sap.cds.ql.cqn.CqnSelect; -import com.sap.cds.services.persistence.PersistenceService; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Objects; -import org.awaitility.Awaitility; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MvcResult; -import org.springframework.test.web.servlet.ResultMatcher; - -@SpringBootTest -@AutoConfigureMockMvc -abstract class OdataRequestValidationBase { - - protected static final Logger logger = LoggerFactory.getLogger(OdataRequestValidationBase.class); - - @Autowired(required = false) - protected TestPluginAttachmentsServiceHandler serviceHandler; - - @Autowired protected MockHttpRequestHelper requestHelper; - @Autowired protected PersistenceService persistenceService; - @Autowired private TableDataDeleter dataDeleter; - @Autowired private TestPersistenceHandler testPersistenceHandler; - - @AfterEach - void teardown() { - dataDeleter.deleteData(Roots_.CDS_NAME); - clearServiceHandlerContext(); - clearServiceHandlerDocuments(); - requestHelper.resetHelper(); - testPersistenceHandler.reset(); - } - - @Test - void deepCreateWorks() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - verifySelectedRoot(selectedRoot, serviceRoot); - verifyNoAttachmentEventsCalled(); - } - - @Test - void putContentWorksForUrlsWithNavigation() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachment(selectedRoot); - var itemAttachment = getRandomItemAttachment(item); - var content = putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); - var attachment = selectUpdatedAttachmentWithExpand(selectedRoot, itemAttachment); - - verifyContentAndContentId(attachment, content, itemAttachment); - verifySingleCreateEvent(attachment.getContentId(), content); - } - - @Test - void putContentWorksForUrlsWithoutNavigation() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachmentEntity(selectedRoot); - var itemAttachment = getRandomItemAttachmentEntity(item); - - var content = putContentForAttachmentWithoutNavigation(itemAttachment); - var attachment = selectUpdatedAttachment(itemAttachment); - - verifyContentAndContentIdForAttachmentEntity(attachment, content, itemAttachment); - verifySingleCreateEvent(attachment.getContentId(), content); - } - - @Test - void expandReadOfAttachmentsHasNoFilledContent() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachment(selectedRoot); - - var url = buildExpandAttachmentUrl(selectedRoot.getId(), item.getId()); - var responseItem = - requestHelper.executeGetWithSingleODataResponseAndAssertStatus( - url, Items.class, HttpStatus.OK); - - assertThat(responseItem.getAttachments()).hasSameSizeAs(item.getAttachments()); - assertThat(responseItem.getAttachments()) - .allSatisfy( - attachment -> { - assertThat(attachment.getContent()).isNull(); - assertThat(attachment.get("content@mediaContentType")).isNull(); - assertThat(attachment.getContentId()).isNull(); - }); - verifyNoAttachmentEventsCalled(); - } - - @Test - void navigationReadOfAttachmentsHasFilledContent() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachment(selectedRoot); - var itemAttachment = getRandomItemAttachment(item); - var content = putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); - - var url = buildExpandAttachmentUrl(selectedRoot.getId(), item.getId()); - var responseItem = - requestHelper.executeGetWithSingleODataResponseAndAssertStatus( - url, Items.class, HttpStatus.OK); - - assertThat(responseItem.getAttachments()).hasSameSizeAs(item.getAttachments()); - - var attachmentWithExpectedContent = - responseItem.getAttachments().stream() - .filter(attach -> attach.getId().equals(itemAttachment.getId())) - .findAny() - .orElseThrow(); - assertThat(attachmentWithExpectedContent) - .containsEntry("content@mediaContentType", "application/octet-stream;charset=UTF-8") - .containsEntry(Attachments.FILE_NAME, itemAttachment.getFileName()); - assertThat(attachmentWithExpectedContent.getStatus()).isNotEmpty(); - verifyContentId( - attachmentWithExpectedContent, itemAttachment.getId(), itemAttachment.getContentId()); - verifySingleCreateEvent(attachmentWithExpectedContent.getContentId(), content); - } - - @Test - void navigationReadOfAttachmentsReturnsContent() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachment(selectedRoot); - var itemAttachment = getRandomItemAttachment(item); - var content = putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); - clearServiceHandlerContext(); - var selectedItemAfterChange = selectItem(item); - var itemAttachmentAfterChange = getRandomItemAttachment(selectedItemAfterChange); - - var url = - buildNavigationAttachmentUrl(selectedRoot.getId(), item.getId(), itemAttachment.getId()) - + "/content"; - executeContentRequestAndValidateContent(url, content); - verifySingleReadEvent(itemAttachmentAfterChange.getContentId()); - } - - @Test - void navigationDeleteOfContentClears() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachment(selectedRoot); - var itemAttachment = getRandomItemAttachment(item); - putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); - clearServiceHandlerContext(); - var selectedItemAfterChange = selectItem(item); - var itemAttachmentAfterChange = getRandomItemAttachment(selectedItemAfterChange); - - executeDeleteAndCheckNoDataCanBeRead( - buildNavigationAttachmentUrl(selectedRoot.getId(), item.getId(), itemAttachment.getId()), - itemAttachmentAfterChange.getContentId()); - - var expandUrl = buildExpandAttachmentUrl(selectedRoot.getId(), item.getId()); - var responseItem = - requestHelper.executeGetWithSingleODataResponseAndAssertStatus( - expandUrl, Items.class, HttpStatus.OK); - - assertThat(responseItem.getAttachments()).hasSameSizeAs(item.getAttachments()); - assertThat(responseItem.getAttachments()) - .allSatisfy( - attachment -> { - assertThat(attachment.getContent()).isNull(); - assertThat(attachment.get("content@mediaContentType")).isNull(); - assertThat(attachment.getContentId()).isNull(); - }); - verifyNoAttachmentEventsCalled(); - } - - @Test - void navigationDeleteOfAttachmentClearsContentField() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachment(selectedRoot); - var itemAttachment = getRandomItemAttachment(item); - putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); - clearServiceHandlerContext(); - var selectedItemAfterChange = selectItem(item); - var itemAttachmentAfterChange = getRandomItemAttachment(selectedItemAfterChange); - - var url = - buildNavigationAttachmentUrl(selectedRoot.getId(), item.getId(), itemAttachment.getId()); - requestHelper.executeDelete(url); - var expandUrl = buildExpandAttachmentUrl(selectedRoot.getId(), item.getId()); - var responseItem = - requestHelper.executeGetWithSingleODataResponseAndAssertStatus( - expandUrl, Items.class, HttpStatus.OK); - - assertThat(responseItem.getAttachments()).hasSize(1); - assertThat(responseItem.getAttachments()) - .first() - .satisfies( - attachment -> { - assertThat(attachment.getContent()).isNull(); - assertThat(attachment.get("content@mediaContentType")).isNull(); - assertThat(attachment.getContentId()).isNull(); - }); - verifySingleDeletionEvent(itemAttachmentAfterChange.getContentId()); - } - - @Test - void navigationDeleteCallsTwiceReturnsError() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachment(selectedRoot); - var itemAttachment = getRandomItemAttachment(item); - putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); - clearServiceHandlerContext(); - var selectedItemAfterChange = selectItem(item); - var itemAttachmentAfterChange = getRandomItemAttachment(selectedItemAfterChange); - - var url = - buildNavigationAttachmentUrl(selectedRoot.getId(), item.getId(), itemAttachment.getId()); - requestHelper.executeDelete(url); - var result = requestHelper.executeDelete(url); - - assertThat(result.getResponse().getStatus()).isEqualTo(HttpStatus.PRECONDITION_FAILED.value()); - verifySingleDeletionEvent(itemAttachmentAfterChange.getContentId()); - } - - @Test - void directReadOfAttachmentsHasNoContentFilled() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachmentEntity(selectedRoot); - var itemAttachment = getRandomItemAttachmentEntity(item); - - var url = buildDirectAttachmentEntityUrl(itemAttachment.getId()); - var responseAttachment = - requestHelper.executeGetWithSingleODataResponseAndAssertStatus( - url, Attachments.class, HttpStatus.OK); - - assertThat(responseAttachment.get("content@mediaContentType")).isNull(); - assertThat(responseAttachment.getContentId()).isNull(); - assertThat(responseAttachment.getFileName()).isEqualTo(itemAttachment.getFileName()); - verifyNoAttachmentEventsCalled(); - } - - @Test - void directReadOfAttachmentsHasFilledContent() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachmentEntity(selectedRoot); - var itemAttachment = getRandomItemAttachmentEntity(item); - putContentForAttachmentWithoutNavigation(itemAttachment); - clearServiceHandlerContext(); - - var url = buildDirectAttachmentEntityUrl(itemAttachment.getId()); - var responseAttachment = - requestHelper.executeGetWithSingleODataResponseAndAssertStatus( - url, Attachments.class, HttpStatus.OK); - - assertThat(responseAttachment) - .containsEntry("content@mediaContentType", "application/octet-stream;charset=UTF-8") - .containsEntry(Attachments.FILE_NAME, itemAttachment.getFileName()); - verifyContentId(responseAttachment, itemAttachment.getId(), itemAttachment.getContentId()); - verifyNoAttachmentEventsCalled(); - } - - @Test - void directReadOfAttachmentsReturnsContent() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachmentEntity(selectedRoot); - var itemAttachment = getRandomItemAttachmentEntity(item); - var content = putContentForAttachmentWithoutNavigation(itemAttachment); - clearServiceHandlerContext(); - var selectedItemAfterChange = selectItem(item); - var itemAttachmentAfterChange = getRandomItemAttachmentEntity(selectedItemAfterChange); - - var url = buildDirectAttachmentEntityUrl(itemAttachment.getId()) + "/content"; - executeContentRequestAndValidateContent(url, content); - verifySingleReadEvent(itemAttachmentAfterChange.getContentId()); - } - - @Test - void directDeleteOfContentClears() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachmentEntity(selectedRoot); - var itemAttachment = getRandomItemAttachmentEntity(item); - putContentForAttachmentWithoutNavigation(itemAttachment); - clearServiceHandlerContext(); - var selectedItemAfterChange = selectItem(item); - var itemAttachmentAfterChange = getRandomItemAttachmentEntity(selectedItemAfterChange); - - executeDeleteAndCheckNoDataCanBeRead( - buildDirectAttachmentEntityUrl(itemAttachment.getId()), - itemAttachmentAfterChange.getContentId()); - - var expandUrl = buildExpandAttachmentUrl(selectedRoot.getId(), item.getId()); - var responseItem = - requestHelper.executeGetWithSingleODataResponseAndAssertStatus( - expandUrl, Items.class, HttpStatus.OK); - - assertThat(responseItem.getAttachmentEntities()).hasSameSizeAs(item.getAttachmentEntities()); - assertThat(responseItem.getAttachmentEntities()) - .allSatisfy( - attachment -> { - assertThat(attachment.getContent()).isNull(); - assertThat(attachment.get("content@mediaContentType")).isNull(); - assertThat(attachment.getContentId()).isNull(); - }); - verifyNoAttachmentEventsCalled(); - } - - @Test - void directDeleteOfAttachmentClearsContentField() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachmentEntity(selectedRoot); - var itemAttachment = getRandomItemAttachmentEntity(item); - putContentForAttachmentWithoutNavigation(itemAttachment); - clearServiceHandlerContext(); - var selectedItemAfterChange = selectItem(item); - var itemAttachmentAfterChange = getRandomItemAttachmentEntity(selectedItemAfterChange); - - var url = buildDirectAttachmentEntityUrl(itemAttachment.getId()); - requestHelper.executeDelete(url); - var expandUrl = buildExpandAttachmentUrl(selectedRoot.getId(), item.getId()); - var responseItem = - requestHelper.executeGetWithSingleODataResponseAndAssertStatus( - expandUrl, Items.class, HttpStatus.OK); - - assertThat(responseItem.getAttachmentEntities()).isEmpty(); - verifySingleDeletionEvent(itemAttachmentAfterChange.getContentId()); - } - - @Test - void directDeleteCalledTwiceReturnsError() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachmentEntity(selectedRoot); - var itemAttachment = getRandomItemAttachmentEntity(item); - putContentForAttachmentWithoutNavigation(itemAttachment); - clearServiceHandlerContext(); - - var url = buildDirectAttachmentEntityUrl(itemAttachment.getId()); - requestHelper.executeDelete(url); - MvcResult mvcResult = requestHelper.executeDelete(url); - - assertThat(mvcResult.getResponse().getStatus()) - .isEqualTo(HttpStatus.PRECONDITION_FAILED.value()); - if (Objects.nonNull(serviceHandler)) { - Awaitility.await().until(() -> serviceHandler.getEventContext().size() == 1); - verifyNumberOfEvents(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED, 1); - verifyEventContextEmptyForEvent( - AttachmentService.EVENT_CREATE_ATTACHMENT, AttachmentService.EVENT_READ_ATTACHMENT); - } - } - - @Test - void rootDeleteDeletesAllContents() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachmentEntity(selectedRoot); - var itemAttachmentEntity = getRandomItemAttachmentEntity(item); - var itemAttachment = getRandomItemAttachment(item); - - putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); - putContentForAttachmentWithoutNavigation(itemAttachmentEntity); - verifyNumberOfEvents(AttachmentService.EVENT_CREATE_ATTACHMENT, 2); - clearServiceHandlerContext(); - var selectedItemAfterChange = selectItem(item); - var itemAttachmentEntityAfterChange = getRandomItemAttachmentEntity(selectedItemAfterChange); - var itemAttachmentAfterChange = getRandomItemAttachment(selectedItemAfterChange); - - var url = - MockHttpRequestHelper.ODATA_BASE_URL + "TestService/Roots(" + selectedRoot.getId() + ")"; - requestHelper.executeDeleteWithMatcher(url, status().isNoContent()); - - verifyTwoDeleteEvents(itemAttachmentEntityAfterChange, itemAttachmentAfterChange); - } - - @Test - void updateContentWorksForUrlsWithNavigation() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachment(selectedRoot); - var itemAttachment = getRandomItemAttachment(item); - itemAttachment.setNote("note 1"); - putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); - itemAttachment = selectUpdatedAttachmentWithExpand(selectedRoot, itemAttachment); - itemAttachment.setNote("note 2"); - var content = putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); - var attachment = selectUpdatedAttachmentWithExpand(selectedRoot, itemAttachment); - - verifyContentAndContentId(attachment, content, itemAttachment); - verifySingleCreateAndUpdateEvent( - attachment.getContentId(), itemAttachment.getContentId(), content); - } - - @Test - void updateContentWorksForUrlsWithoutNavigation() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachmentEntity(selectedRoot); - var itemAttachment = getRandomItemAttachmentEntity(item); - itemAttachment.setNote("note 1"); - putContentForAttachmentWithoutNavigation(itemAttachment); - itemAttachment = selectUpdatedAttachment(itemAttachment); - itemAttachment.setNote("note 2"); - var content = putContentForAttachmentWithoutNavigation(itemAttachment); - var attachment = selectUpdatedAttachment(itemAttachment); - - verifyContentAndContentIdForAttachmentEntity(attachment, content, itemAttachment); - verifySingleCreateAndUpdateEvent( - attachment.getContentId(), itemAttachment.getContentId(), content); - } - - @Test - void errorInTransactionAfterCreateCallsDelete() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachment(selectedRoot); - var itemAttachment = getRandomItemAttachment(item); - testPersistenceHandler.setThrowExceptionOnUpdate(true); - putContentForAttachmentWithNavigation( - selectedRoot, itemAttachment, status().is5xxServerError()); - var attachment = selectUpdatedAttachmentWithExpand(selectedRoot, itemAttachment); - - assertThat(attachment.getContentId()).isEqualTo(itemAttachment.getContentId()); - assertThat(attachment.getContent()).isEqualTo(itemAttachment.getContent()); - } - - @Test - void updateContentWithErrorsResetsForUrlsWithNavigation() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachment(selectedRoot); - var itemAttachment = getRandomItemAttachment(item); - itemAttachment.setNote("note 1"); - var content = putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); - itemAttachment = selectUpdatedAttachmentWithExpand(selectedRoot, itemAttachment); - itemAttachment.setNote("note 2"); - testPersistenceHandler.setThrowExceptionOnUpdate(true); - putContentForAttachmentWithNavigation( - selectedRoot, itemAttachment, status().is5xxServerError()); - var attachment = selectUpdatedAttachmentWithExpand(selectedRoot, itemAttachment); - - verifyContentAndContentId(attachment, content, itemAttachment); - assertThat(attachment.getContentId()).isEqualTo(itemAttachment.getContentId()); - verifySingleCreateAndUpdateEvent( - attachment.getContentId(), itemAttachment.getContentId(), content); - } - - @Test - void updateContentWithErrorResetsForUrlsWithoutNavigation() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachmentEntity(selectedRoot); - var itemAttachment = getRandomItemAttachmentEntity(item); - itemAttachment.setNote("note 1"); - var content = putContentForAttachmentWithoutNavigation(itemAttachment); - itemAttachment = selectUpdatedAttachment(itemAttachment); - itemAttachment.setNote("note 2"); - testPersistenceHandler.setThrowExceptionOnUpdate(true); - putContentForAttachmentWithoutNavigation(itemAttachment, status().is5xxServerError()); - var attachment = selectUpdatedAttachment(itemAttachment); - - verifyContentAndContentIdForAttachmentEntity(attachment, content, itemAttachment); - assertThat(attachment.getContentId()).isEqualTo(itemAttachment.getContentId()); - verifySingleCreateAndUpdateEvent( - attachment.getContentId(), itemAttachment.getContentId(), content); - } - - @ParameterizedTest - @CsvSource({"status,INFECTED", "contentId,TEST"}) - void statusCannotBeUpdated(String field, String value) throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachmentEntity(selectedRoot); - var itemAttachment = getRandomItemAttachmentEntity(item); - putContentForAttachmentWithoutNavigation(itemAttachment); - itemAttachment.setStatus(value); - var url = buildDirectAttachmentEntityUrl(itemAttachment.getId()); - - requestHelper.resetHelper(); - requestHelper.executePatchWithODataResponseAndAssertStatus( - url, "{\"" + field + "\":\"" + value + "\"}", HttpStatus.OK); - - selectedRoot = selectStoredRootWithDeepData(); - item = getItemWithAttachmentEntity(selectedRoot); - itemAttachment = getRandomItemAttachmentEntity(item); - assertThat(itemAttachment.get(field)).isNotNull().isNotEqualTo(value); - } - - @Test - void wrongEtagCouldNotBeUpdated() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachmentEntity(selectedRoot); - var itemAttachment = getRandomItemAttachmentEntity(item); - - var url = buildDirectAttachmentEntityUrl(itemAttachment.getId()); - requestHelper.executePatchWithODataResponseAndAssertStatus( - url, - "{\"fileName\":\"test_for_change.txt\"}", - "W/\"2024-05-06T15:24:29.657713600Z\"", - HttpStatus.PRECONDITION_FAILED); - - var selectedRootAfterChange = selectStoredRootWithDeepData(); - var itemAfterChange = getItemWithAttachmentEntity(selectedRootAfterChange); - var itemAttachmentAfterChange = getRandomItemAttachmentEntity(itemAfterChange); - assertThat(itemAttachmentAfterChange.getFileName()).isEqualTo(itemAttachment.getFileName()); - } - - @Test - void correctEtagCanBeUpdated() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachmentEntity(selectedRoot); - var itemAttachment = getRandomItemAttachmentEntity(item); - var modifiedAt = itemAttachment.getModifiedAt(); - var eTag = "W/\"" + modifiedAt + "\""; - - var url = buildDirectAttachmentEntityUrl(itemAttachment.getId()); - requestHelper.executePatchWithODataResponseAndAssertStatus( - url, "{\"fileName\":\"test_for_change.txt\"}", eTag, HttpStatus.OK); - - var selectedRootAfterChange = selectStoredRootWithDeepData(); - var itemAfterChange = getItemWithAttachmentEntity(selectedRootAfterChange); - var itemAttachmentAfterChange = getRandomItemAttachmentEntity(itemAfterChange); - assertThat(itemAttachmentAfterChange.getFileName()).isEqualTo("test_for_change.txt"); - } - - protected Items selectItem(Items item) { - var selectedRootAfterContentCreated = selectStoredRootWithDeepData(); - return selectedRootAfterContentCreated.getItems().stream() - .filter(i -> i.getId().equals(item.getId())) - .findAny() - .orElseThrow(); - } - - protected Roots buildServiceRootWithDeepData() { - return RootEntityBuilder.create() - .setTitle("some root title") - .addAttachments( - AttachmentsEntityBuilder.create().setFileName("fileRoot.txt").setMimeType("text/plain")) - .addItems( - ItemEntityBuilder.create() - .setTitle("some item 1 title") - .addAttachments( - AttachmentsBuilder.create() - .setFileName("fileItem1.txt") - .setMimeType("text/plain"), - AttachmentsBuilder.create() - .setFileName("fileItem2.txt") - .setMimeType("text/plain")), - ItemEntityBuilder.create() - .setTitle("some item 2 title") - .addAttachmentEntities( - AttachmentsEntityBuilder.create() - .setFileName("fileItem3.text") - .setMimeType("text/plain")) - .addAttachments( - AttachmentsBuilder.create() - .setFileName("fileItem3.text") - .setMimeType("text/plain"))) - .build(); - } - - protected void postServiceRoot(Roots serviceRoot) throws Exception { - var url = MockHttpRequestHelper.ODATA_BASE_URL + "TestService/Roots"; - requestHelper.executePostWithMatcher(url, serviceRoot.toJson(), status().isCreated()); - } - - protected Roots selectStoredRootWithDeepData() { - CqnSelect select = - Select.from(Roots_.class) - .columns( - StructuredType::_all, - root -> root.attachments().expand(), - root -> - root.items() - .expand( - StructuredType::_all, - item -> item.attachments().expand(), - item -> item.attachmentEntities().expand())); - var result = persistenceService.run(select); - return result.single(Roots.class); - } - - private void verifySelectedRoot(Roots selectedRoot, Roots serviceRoot) { - assertThat(selectedRoot.getId()).isNotEmpty(); - assertThat(selectedRoot.getTitle()).isEqualTo(serviceRoot.getTitle()); - assertThat(selectedRoot.getAttachments()) - .hasSize(1) - .first() - .satisfies( - attachment -> { - assertThat(attachment.getId()).isNotEmpty(); - assertThat(attachment.getFileName()) - .isEqualTo(serviceRoot.getAttachments().get(0).getFileName()); - assertThat(attachment.getMimeType()) - .isEqualTo(serviceRoot.getAttachments().get(0).getMimeType()); - }); - assertThat(selectedRoot.getItems()) - .hasSize(2) - .first() - .satisfies( - item -> { - assertThat(item.getId()).isNotEmpty(); - assertThat(item.getTitle()).isEqualTo(serviceRoot.getItems().get(0).getTitle()); - assertThat(item.getAttachments()).hasSize(2); - }); - assertThat(selectedRoot.getItems().get(1).getId()).isNotEmpty(); - assertThat(selectedRoot.getItems().get(1).getTitle()) - .isEqualTo(serviceRoot.getItems().get(1).getTitle()); - assertThat(selectedRoot.getItems().get(1).getAttachments()).hasSize(1); - } - - protected Attachments getRandomItemAttachment(Items selectedItem) { - return selectedItem.getAttachments().get(0); - } - - protected Attachments getRandomRootSizeLimitedAttachment(Roots selectedRoot) { - return selectedRoot.getSizeLimitedAttachments().get(0); - } - - private AttachmentEntity getRandomItemAttachmentEntity(Items selectedItem) { - return selectedItem.getAttachmentEntities().get(0); - } - - protected Items getItemWithAttachment(Roots selectedRoot) { - return selectedRoot.getItems().stream() - .filter(item -> !item.getAttachments().isEmpty()) - .findAny() - .orElseThrow(); - } - - private Items getItemWithAttachmentEntity(Roots selectedRoot) { - return selectedRoot.getItems().stream() - .filter(item -> !item.getAttachmentEntities().isEmpty()) - .findAny() - .orElseThrow(); - } - - protected String putContentForAttachmentWithNavigation( - Roots selectedRoot, Attachments itemAttachment) throws Exception { - return putContentForAttachmentWithNavigation( - selectedRoot, itemAttachment, status().isNoContent()); - } - - private String putContentForAttachmentWithNavigation( - Roots selectedRoot, Attachments itemAttachment, ResultMatcher matcher) throws Exception { - var selectedItem = - selectedRoot.getItems().stream() - .filter( - item -> - item.getAttachments().stream() - .anyMatch(attach -> attach.getId().equals(itemAttachment.getId()))) - .findAny() - .orElseThrow(); - var url = - buildNavigationAttachmentUrl( - selectedRoot.getId(), selectedItem.getId(), itemAttachment.getId()) - + "/content"; - - var testContent = "testContent" + itemAttachment.getNote(); - requestHelper.setContentType(MediaType.APPLICATION_OCTET_STREAM); - requestHelper.executePutWithMatcher(url, testContent.getBytes(StandardCharsets.UTF_8), matcher); - return testContent; - } - - protected String buildNavigationAttachmentUrl(String rootId, String itemId, String attachmentId) { - return "/odata/v4/TestService/Roots(" - + rootId - + ")/items(" - + itemId - + ")" - + "/attachments(ID=" - + attachmentId - + ",up__ID=" - + itemId - + ")"; - } - - protected String buildNavigationSizeLimitedAttachmentUrl(String rootId, String attachmentId) { - return "/odata/v4/TestService/Roots(" - + rootId - + ")/sizeLimitedAttachments(ID=" - + attachmentId - + ",up__ID=" - + rootId - + ")"; - } - - protected String putContentForSizeLimitedAttachment(Roots selectedRoot, Attachments attachment) - throws Exception { - return putContentForSizeLimitedAttachment(selectedRoot, attachment, status().isNoContent()); - } - - protected String putContentForSizeLimitedAttachment( - Roots selectedRoot, Attachments attachment, ResultMatcher matcher) throws Exception { - var url = - buildNavigationSizeLimitedAttachmentUrl(selectedRoot.getId(), attachment.getId()) - + "/content"; - var testContent = "testContent" + attachment.getNote(); - requestHelper.setContentType(MediaType.APPLICATION_OCTET_STREAM); - requestHelper.executePutWithMatcher(url, testContent.getBytes(StandardCharsets.UTF_8), matcher); - return testContent; - } - - protected String buildExpandAttachmentUrl(String rootId, String itemId) { - return "/odata/v4/TestService/Roots(" - + rootId - + ")/items(" - + itemId - + ")" - + "?$expand=attachments,attachmentEntities"; - } - - private String putContentForAttachmentWithoutNavigation(AttachmentEntity itemAttachment) - throws Exception { - return putContentForAttachmentWithoutNavigation(itemAttachment, status().isNoContent()); - } - - private String putContentForAttachmentWithoutNavigation( - AttachmentEntity itemAttachment, ResultMatcher matcher) throws Exception { - var url = buildDirectAttachmentEntityUrl(itemAttachment.getId()) + "/content"; - var testContent = "testContent" + itemAttachment.getNote(); - requestHelper.setContentType(MediaType.APPLICATION_OCTET_STREAM); - requestHelper.executePutWithMatcher(url, testContent.getBytes(StandardCharsets.UTF_8), matcher); - return testContent; - } - - private String buildDirectAttachmentEntityUrl(String attachmentId) { - return MockHttpRequestHelper.ODATA_BASE_URL - + "TestService/AttachmentEntity(" - + attachmentId - + ")"; - } - - private Attachments selectUpdatedAttachmentWithExpand( - Roots selectedRoot, Attachments itemAttachment) { - CqnSelect attachmentSelect = - Select.from(Items_.class) - .where(a -> a.ID().eq(selectedRoot.getItems().get(0).getId())) - .columns(item -> item.attachments().expand()); - var result = persistenceService.run(attachmentSelect); - var items = result.single(Items.class); - return items.getAttachments().stream() - .filter(attach -> itemAttachment.getId().equals(attach.getId())) - .findAny() - .orElseThrow(); - } - - private AttachmentEntity selectUpdatedAttachment(AttachmentEntity itemAttachment) { - CqnSelect attachmentSelect = - Select.from(AttachmentEntity_.class).where(a -> a.ID().eq(itemAttachment.getId())); - var result = persistenceService.run(attachmentSelect); - return result.single(AttachmentEntity.class); - } - - private void executeDeleteAndCheckNoDataCanBeRead(String baseUrl, String contentId) - throws Exception { - var url = baseUrl + "/content"; - requestHelper.executeDelete(url); - verifySingleDeletionEvent(contentId); - clearServiceHandlerContext(); - var response = requestHelper.executeGet(url); - - assertThat(response.getResponse().getContentLength()).isZero(); - assertThat(response.getResponse().getStatus()).isEqualTo(HttpStatus.NO_CONTENT.value()); - } - - protected abstract void executeContentRequestAndValidateContent(String url, String content) - throws Exception; - - protected abstract void verifyTwoDeleteEvents( - AttachmentEntity itemAttachmentEntityAfterChange, Attachments itemAttachmentAfterChange); - - protected abstract void verifyNumberOfEvents(String event, int number); - - protected abstract void verifyContentId( - Attachments attachmentWithExpectedContent, String attachmentId, String contentId); - - protected abstract void verifyContentAndContentId( - Attachments attachment, String testContent, Attachments itemAttachment) throws IOException; - - protected abstract void verifyContentAndContentIdForAttachmentEntity( - AttachmentEntity attachment, String testContent, AttachmentEntity itemAttachment) - throws IOException; - - protected abstract void clearServiceHandlerContext(); - - protected abstract void clearServiceHandlerDocuments(); - - protected abstract void verifySingleCreateEvent(String contentId, String content); - - protected abstract void verifySingleCreateAndUpdateEvent( - String resultContentId, String toBeDeletedContentId, String content); - - protected abstract void verifySingleDeletionEvent(String contentId); - - protected abstract void verifySingleReadEvent(String contentId); - - protected abstract void verifyNoAttachmentEventsCalled(); - - protected abstract void verifyEventContextEmptyForEvent(String... events); -} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithTestHandlerTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithTestHandlerTest.java deleted file mode 100644 index 94fca3378..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithTestHandlerTest.java +++ /dev/null @@ -1,245 +0,0 @@ -/* - * © 2024-2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.nondraftservice; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.sap.cds.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.integrationtests.constants.Profiles; -import com.sap.cds.feature.attachments.integrationtests.testhandler.EventContextHolder; -import com.sap.cds.feature.attachments.service.AttachmentService; -import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentCreateEventContext; -import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentMarkAsDeletedEventContext; -import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentReadEventContext; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.function.Predicate; -import org.awaitility.Awaitility; -import org.junit.jupiter.api.Test; -import org.springframework.test.context.ActiveProfiles; - -@ActiveProfiles(Profiles.TEST_HANDLER_ENABLED) -class OdataRequestValidationWithTestHandlerTest extends OdataRequestValidationBase { - - @Test - void serviceHandlerAvailable() { - assertThat(serviceHandler).isNotNull(); - } - - @Override - protected void executeContentRequestAndValidateContent(String url, String content) - throws Exception { - var response = requestHelper.executeGet(url); - assertThat(response.getResponse().getContentAsString()).isEqualTo(content); - } - - @Override - protected void verifyTwoDeleteEvents( - AttachmentEntity itemAttachmentEntityAfterChange, Attachments itemAttachmentAfterChange) { - waitTillExpectedHandlerMessageSize(2); - verifyEventContextEmptyForEvent( - AttachmentService.EVENT_READ_ATTACHMENT, AttachmentService.EVENT_CREATE_ATTACHMENT); - var deleteEvents = - serviceHandler.getEventContextForEvent(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); - assertThat(deleteEvents).hasSize(2); - assertThat( - deleteEvents.stream() - .anyMatch( - verifyContentIdAndUserInfo(itemAttachmentEntityAfterChange.getContentId()))) - .isTrue(); - assertThat( - deleteEvents.stream() - .anyMatch(verifyContentIdAndUserInfo(itemAttachmentAfterChange.getContentId()))) - .isTrue(); - } - - @Override - protected void verifyNumberOfEvents(String event, int number) { - assertThat(serviceHandler.getEventContextForEvent(event)).hasSize(number); - } - - @Override - protected void verifyContentId( - Attachments attachmentWithExpectedContent, String attachmentId, String contentId) { - assertThat(attachmentWithExpectedContent.getContentId()).isNotEmpty().isNotEqualTo(contentId); - } - - @Override - protected void verifyContentAndContentId( - Attachments attachment, String content, Attachments itemAttachment) { - assertThat(attachment.getContent()).isNull(); - assertThat(attachment.getContentId()).isNotEmpty().isNotEqualTo(itemAttachment.getId()); - } - - @Override - protected void verifyContentAndContentIdForAttachmentEntity( - AttachmentEntity attachment, String content, AttachmentEntity itemAttachment) { - assertThat(attachment.getContent()).isNull(); - assertThat(attachment.getContentId()).isNotEmpty().isNotEqualTo(itemAttachment.getId()); - } - - @Override - protected void clearServiceHandlerContext() { - serviceHandler.clearEventContext(); - } - - @Override - protected void clearServiceHandlerDocuments() { - serviceHandler.clearDocuments(); - } - - @Override - protected void verifySingleCreateEvent(String contentId, String content) { - verifyEventContextEmptyForEvent( - AttachmentService.EVENT_READ_ATTACHMENT, - AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); - var createEvent = - serviceHandler.getEventContextForEvent(AttachmentService.EVENT_CREATE_ATTACHMENT); - assertThat(createEvent) - .hasSize(1) - .first() - .satisfies( - event -> { - assertThat(event.context()).isInstanceOf(AttachmentCreateEventContext.class); - var createContext = (AttachmentCreateEventContext) event.context(); - assertThat(createContext.getContentId()).isEqualTo(contentId); - assertThat(createContext.getData().getContent().readAllBytes()) - .isEqualTo(content.getBytes(StandardCharsets.UTF_8)); - }); - } - - @Override - protected void verifySingleCreateAndUpdateEvent( - String resultContentId, String toBeDeletedContentId, String content) { - waitTillExpectedHandlerMessageSize(3); - verifyEventContextEmptyForEvent(AttachmentService.EVENT_READ_ATTACHMENT); - var createEvents = - serviceHandler.getEventContextForEvent(AttachmentService.EVENT_CREATE_ATTACHMENT); - assertThat(createEvents).hasSize(2); - verifyCreateEventsContainsContentId(toBeDeletedContentId, createEvents); - verifyCreateEventsContainsContentId(resultContentId, createEvents); - var deleteEvents = - serviceHandler.getEventContextForEvent(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); - - var deleteContentId = - !resultContentId.equals(toBeDeletedContentId) - ? toBeDeletedContentId - : createEvents.stream() - .filter( - event -> - !resultContentId.equals( - ((AttachmentCreateEventContext) event.context()).getContentId())) - .findFirst() - .orElseThrow() - .context() - .get(Attachments.CONTENT_ID); - - var eventFound = - deleteEvents.stream() - .anyMatch( - event -> - ((AttachmentMarkAsDeletedEventContext) event.context()) - .getContentId() - .equals(deleteContentId)); - assertThat(eventFound).isTrue(); - } - - @Override - protected void verifySingleDeletionEvent(String contentId) { - waitTillExpectedHandlerMessageSize(1); - verifyEventContextEmptyForEvent( - AttachmentService.EVENT_CREATE_ATTACHMENT, AttachmentService.EVENT_READ_ATTACHMENT); - var deleteEvents = - serviceHandler.getEventContextForEvent(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); - assertThat(deleteEvents) - .hasSize(1) - .first() - .satisfies( - event -> { - assertThat(event.context()).isInstanceOf(AttachmentMarkAsDeletedEventContext.class); - var deleteContext = (AttachmentMarkAsDeletedEventContext) event.context(); - assertThat(deleteContext.getContentId()).isEqualTo(contentId); - assertThat(deleteContext.getDeletionUserInfo().getName()).isEqualTo("anonymous"); - assertThat(deleteContext.getDeletionUserInfo().getIsSystemUser()).isFalse(); - }); - } - - @Override - protected void verifySingleReadEvent(String contentId) { - verifyEventContextEmptyForEvent( - AttachmentService.EVENT_CREATE_ATTACHMENT, - AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); - var readContext = serviceHandler.getEventContext(); - assertThat(readContext) - .hasSize(1) - .first() - .satisfies( - event -> { - assertThat(event.event()).isEqualTo(AttachmentService.EVENT_READ_ATTACHMENT); - assertThat(((AttachmentReadEventContext) event.context()).getContentId()) - .isEqualTo(contentId); - }); - } - - @Override - protected void verifyNoAttachmentEventsCalled() { - assertThat(serviceHandler.getEventContext()).isEmpty(); - } - - @Override - protected void verifyEventContextEmptyForEvent(String... events) { - Arrays.stream(events) - .forEach(event -> assertThat(serviceHandler.getEventContextForEvent(event)).isEmpty()); - } - - private Predicate verifyContentIdAndUserInfo( - String itemAttachmentEntityAfterChange) { - return event -> - ((AttachmentMarkAsDeletedEventContext) event.context()) - .getContentId() - .equals(itemAttachmentEntityAfterChange) - && ((AttachmentMarkAsDeletedEventContext) event.context()) - .getDeletionUserInfo() - .getName() - .equals("anonymous") - && Boolean.FALSE.equals( - ((AttachmentMarkAsDeletedEventContext) event.context()) - .getDeletionUserInfo() - .getIsSystemUser()); - } - - private void verifyCreateEventsContainsContentId( - String contentId, List createEvents) { - assertThat( - createEvents.stream() - .anyMatch( - event -> - ((AttachmentCreateEventContext) event.context()) - .getContentId() - .equals(contentId))) - .isTrue(); - } - - private void waitTillExpectedHandlerMessageSize(int expectedSize) { - Awaitility.await() - .atMost(30, TimeUnit.SECONDS) - .pollDelay(1, TimeUnit.SECONDS) - .until( - () -> { - var eventCalls = serviceHandler.getEventContext().size(); - logger.debug( - "Waiting for expected size '{}' in handler context, was '{}'", - expectedSize, - eventCalls); - var numberMatch = eventCalls >= expectedSize; - if (!numberMatch) { - serviceHandler.getEventContext().forEach(event -> logger.info("Event: {}", event)); - } - return numberMatch; - }); - } -} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java deleted file mode 100644 index 725c4d775..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * © 2024-2025 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.nondraftservice; - -import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; -import static com.github.tomakehurst.wiremock.client.WireMock.post; -import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; -import static org.assertj.core.api.Assertions.assertThat; - -import com.github.tomakehurst.wiremock.WireMockServer; -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.sap.attachments.Attachments; -import com.sap.cds.feature.attachments.integrationtests.common.MalwareScanResultProvider; -import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; -import java.util.concurrent.TimeUnit; -import org.awaitility.Awaitility; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.test.context.ActiveProfiles; -import org.wiremock.spring.ConfigureWireMock; -import org.wiremock.spring.EnableWireMock; -import org.wiremock.spring.InjectWireMock; - -@ActiveProfiles(profiles = {Profiles.TEST_HANDLER_DISABLED, Profiles.MALWARE_SCAN_ENABLED}) -@EnableWireMock({@ConfigureWireMock(port = 1111, name = "malware-scanner")}) -class OdataRequestValidationWithoutTestHandlerAndMalwareScannerTest - extends OdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest { - - @InjectWireMock("malware-scanner") - private WireMockServer wiremock; - - @Autowired private MalwareScanResultProvider malwareScanResultProvider; - - @BeforeEach - void setup() { - mockMalwareScanResult(false); - } - - @Override - @AfterEach - void teardown() { - super.teardown(); - wiremock.resetAll(); - } - - @Test - void scannerReturnedMalwareContentCanNotBeRead() throws Exception { - wiremock.resetAll(); - mockMalwareScanResult(true); - - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachment(selectedRoot); - var itemAttachment = getRandomItemAttachment(item); - putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); - clearServiceHandlerContext(); - var selectedItemAfterChange = selectItem(item); - var itemAttachmentAfterChange = getRandomItemAttachment(selectedItemAfterChange); - - var url = - buildNavigationAttachmentUrl(selectedRoot.getId(), item.getId(), itemAttachment.getId()); - var contentUrl = url + "/content"; - waitAndVerifyContentErrorResponse(contentUrl, itemAttachmentAfterChange); - clearServiceHandlerContext(); - verifyAttachmentGetResponse(url, itemAttachmentAfterChange); - } - - @Test - void scannerReturnedErrorContentCanNotBeRead() throws Exception { - wiremock.resetAll(); - wiremock.stubFor( - post(urlEqualTo("/scan")) - .withBasicAuth("test-user", "test-password") - .willReturn(aResponse().withStatus(500))); - - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachment(selectedRoot); - var itemAttachment = getRandomItemAttachment(item); - putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); - clearServiceHandlerContext(); - var selectedItemAfterChange = selectItem(item); - var itemAttachmentAfterChange = getRandomItemAttachment(selectedItemAfterChange); - - var url = - buildNavigationAttachmentUrl(selectedRoot.getId(), item.getId(), itemAttachment.getId()); - var contentUrl = url + "/content"; - waitAndVerifyContentErrorResponse(contentUrl, itemAttachmentAfterChange); - clearServiceHandlerContext(); - verifyAttachmentGetResponse(url, itemAttachmentAfterChange); - } - - private void mockMalwareScanResult(boolean malware) { - wiremock.stubFor( - post(urlEqualTo("/scan")) - .withBasicAuth("test-user", "test-password") - .willReturn( - aResponse() - .withBody(malwareScanResultProvider.buildMalwareScanResult(malware)) - .withStatus(200))); - } - - private void waitAndVerifyContentErrorResponse( - String contentUrl, Attachments itemAttachmentAfterChange) throws Exception { - Awaitility.await() - .atMost(10, TimeUnit.SECONDS) - .until( - () -> { - var response = requestHelper.executeGet(contentUrl); - assertThat(response.getResponse().getStatus()) - .isEqualTo(HttpStatus.METHOD_NOT_ALLOWED.value()); - return response - .getResponse() - .getContentAsString() - .contains("Error text for not clean"); - }); - clearServiceHandlerContext(); - - var response = requestHelper.executeGet(contentUrl); - assertThat(response.getResponse().getStatus()).isEqualTo(HttpStatus.METHOD_NOT_ALLOWED.value()); - assertThat(response.getResponse().getContentAsString()).contains("Error text for not clean"); - verifySingleReadEvent(itemAttachmentAfterChange.getContentId()); - } - - private void verifyAttachmentGetResponse(String url, Attachments itemAttachmentAfterChange) - throws Exception { - var response = requestHelper.executeGet(url); - assertThat(response.getResponse().getStatus()).isEqualTo(HttpStatus.OK.value()); - verifySingleReadEvent(itemAttachmentAfterChange.getContentId()); - } -} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java deleted file mode 100644 index edee93cb5..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.nondraftservice; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.sap.cds.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.integrationtests.constants.Profiles; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.concurrent.TimeUnit; -import org.awaitility.Awaitility; -import org.junit.jupiter.api.Test; -import org.springframework.test.context.ActiveProfiles; - -@ActiveProfiles(Profiles.TEST_HANDLER_DISABLED) -class OdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest - extends OdataRequestValidationBase { - - @Test - void serviceHandlerIsNull() { - assertThat(serviceHandler).isNull(); - } - - @Override - protected void executeContentRequestAndValidateContent(String url, String content) - throws Exception { - Awaitility.await() - .atMost(10, TimeUnit.SECONDS) - .until( - () -> { - var response = requestHelper.executeGet(url); - return response.getResponse().getContentAsString().equals(content); - }); - - var response = requestHelper.executeGet(url); - assertThat(response.getResponse().getContentAsString()).isEqualTo(content); - } - - @Override - protected void verifyTwoDeleteEvents( - AttachmentEntity itemAttachmentEntityAfterChange, Attachments itemAttachmentAfterChange) { - // no service handler - nothing to do - } - - @Override - protected void verifyNumberOfEvents(String event, int number) { - // no service handler - nothing to do - } - - @Override - protected void verifyContentId( - Attachments attachmentWithExpectedContent, String attachmentId, String contentId) { - assertThat(attachmentWithExpectedContent.getContentId()).isEqualTo(attachmentId); - } - - @Override - protected void verifyContentAndContentId( - Attachments attachment, String testContent, Attachments itemAttachment) throws IOException { - assertThat(attachment.getContent().readAllBytes()) - .isEqualTo(testContent.getBytes(StandardCharsets.UTF_8)); - assertThat(attachment.getContentId()).isEqualTo(itemAttachment.getId()); - } - - @Override - protected void verifyContentAndContentIdForAttachmentEntity( - AttachmentEntity attachment, String testContent, AttachmentEntity itemAttachment) - throws IOException { - assertThat(attachment.getContent().readAllBytes()) - .isEqualTo(testContent.getBytes(StandardCharsets.UTF_8)); - assertThat(attachment.getContentId()).isEqualTo(itemAttachment.getId()); - } - - @Override - protected void clearServiceHandlerContext() { - // no service handler - nothing to do - } - - @Override - protected void clearServiceHandlerDocuments() { - // no service handler - nothing to do - } - - @Override - protected void verifySingleCreateEvent(String contentId, String content) { - // no service handler - nothing to do - } - - @Override - protected void verifySingleCreateAndUpdateEvent( - String resultContentId, String toBeDeletedContentId, String content) { - // no service handler - nothing to do - } - - @Override - protected void verifySingleDeletionEvent(String contentId) { - // no service handler - nothing to do - } - - @Override - protected void verifySingleReadEvent(String contentId) { - // no service handler - nothing to do - } - - @Override - protected void verifyNoAttachmentEventsCalled() { - // no service handler - nothing to do - } - - @Override - protected void verifyEventContextEmptyForEvent(String... events) { - // no service handler - nothing to do - } -} 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 deleted file mode 100644 index 9bc77e763..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/SizeLimitedAttachmentValidationNonDraftTest.java +++ /dev/null @@ -1,175 +0,0 @@ -/* - * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.nondraftservice; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -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.integrationtests.constants.Profiles; -import com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper.AttachmentsBuilder; -import com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper.RootEntityBuilder; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.concurrent.TimeUnit; -import org.awaitility.Awaitility; -import org.junit.jupiter.api.Test; -import org.springframework.test.context.ActiveProfiles; - -@ActiveProfiles(Profiles.TEST_HANDLER_DISABLED) -class SizeLimitedAttachmentValidationNonDraftTest extends OdataRequestValidationBase { - - @Test - void uploadContentWithin5MBLimitSucceeds() throws Exception { - // Arrange: Create root with sizeLimitedAttachments - var serviceRoot = buildServiceRootWithSizeLimitedAttachments(); - postServiceRoot(serviceRoot); - - 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"; - requestHelper.setContentType(org.springframework.http.MediaType.APPLICATION_OCTET_STREAM); - requestHelper.executePutWithMatcher(url, content, status().isNoContent()); - } - - @Test - void uploadContentExceeding5MBLimitFails() throws Exception { - // Arrange: Create root with sizeLimitedAttachments - var serviceRoot = buildServiceRootWithSizeLimitedAttachments(); - postServiceRoot(serviceRoot); - - 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"; - 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 - } - - // Helper methods - private Roots buildServiceRootWithSizeLimitedAttachments() { - return RootEntityBuilder.create() - .setTitle("Root with sizeLimitedAttachments") - .addSizeLimitedAttachments( - AttachmentsBuilder.create().setFileName("testFile.txt").setMimeType("text/plain")) - .build(); - } - - 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 result = persistenceService.run(select); - return result.single(Roots.class); - } - - // Required abstract method implementations - @Override - protected void executeContentRequestAndValidateContent(String url, String content) - throws Exception { - Awaitility.await() - .atMost(10, TimeUnit.SECONDS) - .until( - () -> { - var response = requestHelper.executeGet(url); - return response.getResponse().getContentAsString().equals(content); - }); - - var response = requestHelper.executeGet(url); - assertThat(response.getResponse().getContentAsString()).isEqualTo(content); - } - - @Override - protected void verifyTwoDeleteEvents( - AttachmentEntity itemAttachmentEntityAfterChange, Attachments itemAttachmentAfterChange) { - // no service handler - nothing to do - } - - @Override - protected void verifyNumberOfEvents(String event, int number) { - // no service handler - nothing to do - } - - @Override - protected void verifyContentId( - Attachments attachmentWithExpectedContent, String attachmentId, String contentId) { - assertThat(attachmentWithExpectedContent.getContentId()).isEqualTo(attachmentId); - } - - @Override - protected void verifyContentAndContentId( - Attachments attachment, String testContent, Attachments itemAttachment) throws IOException { - assertThat(attachment.getContent().readAllBytes()) - .isEqualTo(testContent.getBytes(StandardCharsets.UTF_8)); - assertThat(attachment.getContentId()).isEqualTo(itemAttachment.getId()); - } - - @Override - protected void verifyContentAndContentIdForAttachmentEntity( - AttachmentEntity attachment, String testContent, AttachmentEntity itemAttachment) - throws IOException { - assertThat(attachment.getContent().readAllBytes()) - .isEqualTo(testContent.getBytes(StandardCharsets.UTF_8)); - assertThat(attachment.getContentId()).isEqualTo(itemAttachment.getId()); - } - - @Override - protected void clearServiceHandlerContext() { - // no service handler - nothing to do - } - - @Override - protected void clearServiceHandlerDocuments() { - // no service handler - nothing to do - } - - @Override - protected void verifySingleCreateEvent(String contentId, String content) { - // no service handler - nothing to do - } - - @Override - protected void verifySingleCreateAndUpdateEvent( - String resultContentId, String toBeDeletedContentId, String content) { - // no service handler - nothing to do - } - - @Override - protected void verifySingleDeletionEvent(String contentId) { - // no service handler - nothing to do - } - - @Override - protected void verifySingleReadEvent(String contentId) { - // no service handler - nothing to do - } - - @Override - protected void verifyNoAttachmentEventsCalled() { - // no service handler - nothing to do - } - - @Override - protected void verifyEventContextEmptyForEvent(String... events) { - // no service handler - nothing to do - } -} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsBuilder.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsBuilder.java deleted file mode 100644 index 0f5e1bbfe..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsBuilder.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper; - -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.sap.attachments.Attachments; - -public class AttachmentsBuilder { - - private Attachments attachment; - - private AttachmentsBuilder() { - attachment = Attachments.create(); - } - - public static AttachmentsBuilder create() { - return new AttachmentsBuilder(); - } - - public AttachmentsBuilder setMimeType(String mimeType) { - attachment.setMimeType(mimeType); - return this; - } - - public AttachmentsBuilder setFileName(String fileName) { - attachment.setFileName(fileName); - return this; - } - - public Attachments build() { - return attachment; - } -} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsEntityBuilder.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsEntityBuilder.java deleted file mode 100644 index 2da95a144..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsEntityBuilder.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper; - -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.AttachmentEntity; - -public class AttachmentsEntityBuilder { - - private AttachmentEntity attachmentEntity = AttachmentEntity.create(); - - private AttachmentsEntityBuilder() {} - - public static AttachmentsEntityBuilder create() { - return new AttachmentsEntityBuilder(); - } - - public AttachmentsEntityBuilder setMimeType(String mimeType) { - attachmentEntity.setMimeType(mimeType); - return this; - } - - public AttachmentsEntityBuilder setFileName(String fileName) { - attachmentEntity.setFileName(fileName); - return this; - } - - public AttachmentEntity build() { - return attachmentEntity; - } -} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/ItemEntityBuilder.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/ItemEntityBuilder.java deleted file mode 100644 index 9b6ab7017..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/ItemEntityBuilder.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper; - -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.Items; -import java.util.ArrayList; -import java.util.Arrays; - -public class ItemEntityBuilder { - - private final Items item; - - private ItemEntityBuilder() { - item = Items.create(); - item.setAttachments(new ArrayList<>()); - item.setAttachmentEntities(new ArrayList<>()); - } - - public static ItemEntityBuilder create() { - return new ItemEntityBuilder(); - } - - public ItemEntityBuilder setTitle(String title) { - item.setTitle(title); - return this; - } - - public ItemEntityBuilder addAttachmentEntities(AttachmentsEntityBuilder... attachmentEntities) { - Arrays.stream(attachmentEntities) - .forEach(attachment -> item.getAttachmentEntities().add(attachment.build())); - return this; - } - - public ItemEntityBuilder addAttachments(AttachmentsBuilder... attachmentEntities) { - Arrays.stream(attachmentEntities) - .forEach(attachment -> item.getAttachments().add(attachment.build())); - return this; - } - - public Items build() { - return item; - } -} 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 deleted file mode 100644 index 9efc70dfc..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/RootEntityBuilder.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * © 2024-2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper; - -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.Roots; -import java.util.ArrayList; -import java.util.Arrays; - -public class RootEntityBuilder { - - private final Roots rootEntity; - - private RootEntityBuilder() { - rootEntity = Roots.create(); - rootEntity.setAttachments(new ArrayList<>()); - rootEntity.setItems(new ArrayList<>()); - rootEntity.setSizeLimitedAttachments(new ArrayList<>()); - } - - public static RootEntityBuilder create() { - return new RootEntityBuilder(); - } - - public RootEntityBuilder setTitle(String title) { - rootEntity.setTitle(title); - return this; - } - - public RootEntityBuilder addAttachments(AttachmentsEntityBuilder... attachments) { - Arrays.stream(attachments) - .forEach(attachment -> rootEntity.getAttachments().add(attachment.build())); - return this; - } - - public RootEntityBuilder addSizeLimitedAttachments(AttachmentsBuilder... attachments) { - Arrays.stream(attachments) - .forEach(attachment -> rootEntity.getSizeLimitedAttachments().add(attachment.build())); - return this; - } - - public RootEntityBuilder addItems(ItemEntityBuilder... items) { - Arrays.stream(items).forEach(item -> rootEntity.getItems().add(item.build())); - return this; - } - - public Roots build() { - return rootEntity; - } -} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandlerTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandlerTest.java deleted file mode 100644 index 9dcf8875f..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandlerTest.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.testhandler; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.sap.cds.services.ServiceException; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -class TestPersistenceHandlerTest { - - private TestPersistenceHandler testPersistenceHandler; - - @BeforeEach - void setUp() { - testPersistenceHandler = new TestPersistenceHandler(); - } - - @Test - void testReset() { - // Set both flags to true - testPersistenceHandler.setThrowExceptionOnUpdate(true); - testPersistenceHandler.setThrowExceptionOnCreate(true); - - // Reset should set both flags to false - testPersistenceHandler.reset(); - - // Verify no exceptions are thrown after reset - assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnUpdate()); - assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnCreate()); - } - - @Test - void testThrowExceptionOnUpdateWhenEnabled() { - testPersistenceHandler.setThrowExceptionOnUpdate(true); - - ServiceException exception = - assertThrows(ServiceException.class, () -> testPersistenceHandler.throwExceptionOnUpdate()); - - assertTrue(exception.getMessage().contains("Exception on update")); - } - - @Test - void testThrowExceptionOnUpdateWhenDisabled() { - testPersistenceHandler.setThrowExceptionOnUpdate(false); - - assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnUpdate()); - } - - @Test - void testThrowExceptionOnCreateWhenEnabled() { - testPersistenceHandler.setThrowExceptionOnCreate(true); - - ServiceException exception = - assertThrows(ServiceException.class, () -> testPersistenceHandler.throwExceptionOnCreate()); - - assertTrue(exception.getMessage().contains("Exception on create")); - } - - @Test - void testThrowExceptionOnCreateWhenDisabled() { - testPersistenceHandler.setThrowExceptionOnCreate(false); - - assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnCreate()); - } - - @Test - void testSetThrowExceptionOnUpdate() { - // Test setting to true - testPersistenceHandler.setThrowExceptionOnUpdate(true); - assertThrows(ServiceException.class, () -> testPersistenceHandler.throwExceptionOnUpdate()); - - // Test setting to false - testPersistenceHandler.setThrowExceptionOnUpdate(false); - assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnUpdate()); - } - - @Test - void testSetThrowExceptionOnCreate() { - // Test setting to true - testPersistenceHandler.setThrowExceptionOnCreate(true); - assertThrows(ServiceException.class, () -> testPersistenceHandler.throwExceptionOnCreate()); - - // Test setting to false - testPersistenceHandler.setThrowExceptionOnCreate(false); - assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnCreate()); - } - - @Test - void testDefaultBehavior() { - // By default, both flags should be false - assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnUpdate()); - assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnCreate()); - } - - @Test - void testIndependentFlagBehavior() { - // Test that the flags work independently - testPersistenceHandler.setThrowExceptionOnUpdate(true); - testPersistenceHandler.setThrowExceptionOnCreate(false); - - assertThrows(ServiceException.class, () -> testPersistenceHandler.throwExceptionOnUpdate()); - assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnCreate()); - - // Switch them - testPersistenceHandler.setThrowExceptionOnUpdate(false); - testPersistenceHandler.setThrowExceptionOnCreate(true); - - assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnUpdate()); - assertThrows(ServiceException.class, () -> testPersistenceHandler.throwExceptionOnCreate()); - } -} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java deleted file mode 100644 index bc391fe9b..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java +++ /dev/null @@ -1,266 +0,0 @@ -/* - * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.testhandler; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.Mockito.*; - -import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.MediaData; -import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.StatusCode; -import com.sap.cds.feature.attachments.service.AttachmentService; -import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentCreateEventContext; -import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentMarkAsDeletedEventContext; -import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentReadEventContext; -import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentRestoreEventContext; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.time.Instant; -import java.util.List; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -class TestPluginAttachmentsServiceHandlerTest { - - private TestPluginAttachmentsServiceHandler cut; - - @BeforeEach - void setup() { - cut = new TestPluginAttachmentsServiceHandler(); - // Clear any previous test data - cut.clearEventContext(); - cut.clearDocuments(); - } - - @Test - void readIsWorking() { - var context = AttachmentReadEventContext.create(); - context.setContentId("test"); - context.setData(MediaData.create()); - - cut.readAttachment(context); - - assertThat(context.getData().getContent()).isNull(); - } - - @Test - void readWithContentIsWorking() throws IOException { - var createContext = AttachmentCreateEventContext.create(); - createContext.setData(MediaData.create()); - createContext - .getData() - .setContent(new ByteArrayInputStream("test".getBytes(StandardCharsets.UTF_8))); - cut.createAttachment(createContext); - - var context = AttachmentReadEventContext.create(); - context.setContentId(createContext.getContentId()); - context.setData(MediaData.create()); - - cut.readAttachment(context); - - assertThat(context.getData().getContent().readAllBytes()) - .isEqualTo("test".getBytes(StandardCharsets.UTF_8)); - } - - @Test - void dummyTestForDelete() { - var context = AttachmentMarkAsDeletedEventContext.create(); - context.setContentId("test"); - - assertDoesNotThrow(() -> cut.markAttachmentAsDeleted(context)); - } - - @Test - void dummyTestForCreate() throws IOException { - var context = AttachmentCreateEventContext.create(); - context.setData(MediaData.create()); - var stream = mock(InputStream.class); - when(stream.readAllBytes()).thenReturn("test".getBytes(StandardCharsets.UTF_8)); - context.getData().setContent(stream); - - assertDoesNotThrow(() -> cut.createAttachment(context)); - } - - @Test - void dummyTestForRestore() { - var context = AttachmentRestoreEventContext.create(); - context.setRestoreTimestamp(Instant.now()); - - assertDoesNotThrow(() -> cut.restoreAttachment(context)); - } - - @Test - void testCreateAttachmentSetsContentIdAndStatus() throws IOException { - var context = AttachmentCreateEventContext.create(); - context.setData(MediaData.create()); - context - .getData() - .setContent(new ByteArrayInputStream("test content".getBytes(StandardCharsets.UTF_8))); - - cut.createAttachment(context); - - assertNotNull(context.getContentId()); - assertThat(context.getData().getStatus()).isEqualTo(StatusCode.CLEAN); - } - - @Test - void testEventContextTracking() throws IOException { - // Test create event tracking - var createContext = AttachmentCreateEventContext.create(); - createContext.setData(MediaData.create()); - createContext - .getData() - .setContent(new ByteArrayInputStream("test".getBytes(StandardCharsets.UTF_8))); - cut.createAttachment(createContext); - - List createEvents = - cut.getEventContextForEvent(AttachmentService.EVENT_CREATE_ATTACHMENT); - assertThat(createEvents).hasSize(1); - assertThat(createEvents.get(0).event()).isEqualTo(AttachmentService.EVENT_CREATE_ATTACHMENT); - - // Test read event tracking - var readContext = AttachmentReadEventContext.create(); - readContext.setContentId("test-id"); - readContext.setData(MediaData.create()); - cut.readAttachment(readContext); - - List readEvents = - cut.getEventContextForEvent(AttachmentService.EVENT_READ_ATTACHMENT); - assertThat(readEvents).hasSize(1); - assertThat(readEvents.get(0).event()).isEqualTo(AttachmentService.EVENT_READ_ATTACHMENT); - - // Test delete event tracking - var deleteContext = AttachmentMarkAsDeletedEventContext.create(); - deleteContext.setContentId("test-id"); - cut.markAttachmentAsDeleted(deleteContext); - - List deleteEvents = - cut.getEventContextForEvent(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); - assertThat(deleteEvents).hasSize(1); - assertThat(deleteEvents.get(0).event()) - .isEqualTo(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); - - // Test restore event tracking - var restoreContext = AttachmentRestoreEventContext.create(); - restoreContext.setRestoreTimestamp(Instant.now()); - cut.restoreAttachment(restoreContext); - - List restoreEvents = - cut.getEventContextForEvent(AttachmentService.EVENT_RESTORE_ATTACHMENT); - assertThat(restoreEvents).hasSize(1); - assertThat(restoreEvents.get(0).event()).isEqualTo(AttachmentService.EVENT_RESTORE_ATTACHMENT); - } - - @Test - void testGetAllEventContext() throws IOException { - // Create multiple events - var createContext = AttachmentCreateEventContext.create(); - createContext.setData(MediaData.create()); - createContext - .getData() - .setContent(new ByteArrayInputStream("test".getBytes(StandardCharsets.UTF_8))); - cut.createAttachment(createContext); - - var readContext = AttachmentReadEventContext.create(); - readContext.setContentId("test-id"); - readContext.setData(MediaData.create()); - cut.readAttachment(readContext); - - List allEvents = cut.getEventContext(); - assertThat(allEvents).hasSize(2); - } - - @Test - void testClearEventContext() throws IOException { - // Add some events - var context = AttachmentCreateEventContext.create(); - context.setData(MediaData.create()); - context.getData().setContent(new ByteArrayInputStream("test".getBytes(StandardCharsets.UTF_8))); - cut.createAttachment(context); - - assertThat(cut.getEventContext()).hasSize(1); - - // Clear and verify - cut.clearEventContext(); - assertThat(cut.getEventContext()).isEmpty(); - } - - @Test - void testReadWithNullContentId() { - var context = AttachmentReadEventContext.create(); - context.setContentId(null); - context.setData(MediaData.create()); - - cut.readAttachment(context); - - assertThat(context.getData().getContent()).isNull(); - } - - @Test - void testCreateAttachmentWithEmptyContent() throws IOException { - var context = AttachmentCreateEventContext.create(); - context.setData(MediaData.create()); - context.getData().setContent(new ByteArrayInputStream(new byte[0])); - - cut.createAttachment(context); - - assertNotNull(context.getContentId()); - assertThat(context.getData().getStatus()).isEqualTo(StatusCode.CLEAN); - } - - @Test - void testMultipleCreateAndReadOperations() throws IOException { - // Create first attachment - var createContext1 = AttachmentCreateEventContext.create(); - createContext1.setData(MediaData.create()); - createContext1 - .getData() - .setContent(new ByteArrayInputStream("content1".getBytes(StandardCharsets.UTF_8))); - cut.createAttachment(createContext1); - - // Create second attachment - var createContext2 = AttachmentCreateEventContext.create(); - createContext2.setData(MediaData.create()); - createContext2 - .getData() - .setContent(new ByteArrayInputStream("content2".getBytes(StandardCharsets.UTF_8))); - cut.createAttachment(createContext2); - - // Read first attachment - var readContext1 = AttachmentReadEventContext.create(); - readContext1.setContentId(createContext1.getContentId()); - readContext1.setData(MediaData.create()); - cut.readAttachment(readContext1); - - // Read second attachment - var readContext2 = AttachmentReadEventContext.create(); - readContext2.setContentId(createContext2.getContentId()); - readContext2.setData(MediaData.create()); - cut.readAttachment(readContext2); - - // Verify content - assertThat(readContext1.getData().getContent().readAllBytes()) - .isEqualTo("content1".getBytes(StandardCharsets.UTF_8)); - assertThat(readContext2.getData().getContent().readAllBytes()) - .isEqualTo("content2".getBytes(StandardCharsets.UTF_8)); - } - - @Test - void testRestoreWithSpecificTimestamp() { - Instant timestamp = Instant.parse("2024-01-01T12:00:00Z"); - var context = AttachmentRestoreEventContext.create(); - context.setRestoreTimestamp(timestamp); - - cut.restoreAttachment(context); - - List restoreEvents = - cut.getEventContextForEvent(AttachmentService.EVENT_RESTORE_ATTACHMENT); - assertThat(restoreEvents).hasSize(1); - var restoredContext = (AttachmentRestoreEventContext) restoreEvents.get(0).context(); - assertThat(restoredContext.getRestoreTimestamp()).isEqualTo(timestamp); - } -} diff --git a/integration-tests/srv/src/test/resources/application.yaml b/integration-tests/srv/src/test/resources/application.yaml deleted file mode 100644 index 54849379b..000000000 --- a/integration-tests/srv/src/test/resources/application.yaml +++ /dev/null @@ -1,15 +0,0 @@ -cds: - dataSource: - csv: - paths: "../db/src/gen/csv" - ---- -spring: - config: - activate: - on-profile: malware-scan-enabled - -cds: - environment: - local: - defaultEnvPath: "classpath:xsuaa-env.json" diff --git a/integration-tests/srv/src/test/resources/logback-test.xml b/integration-tests/srv/src/test/resources/logback-test.xml deleted file mode 100644 index 023662ed8..000000000 --- a/integration-tests/srv/src/test/resources/logback-test.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - %date %-5level [%thread] [%marker]: %msg%nopex [%logger] [%mdc{correlation_id}]%n - - - - - - - - - - - - - - - diff --git a/integration-tests/srv/src/test/resources/xsuaa-env.json b/integration-tests/srv/src/test/resources/xsuaa-env.json deleted file mode 100644 index 7adbe70b0..000000000 --- a/integration-tests/srv/src/test/resources/xsuaa-env.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "VCAP_SERVICES": { - "malware-scanner": [ - { - "label": "malware-scanner", - "provider": null, - "plan": "clamav", - "name": "dsr-core-malware-scanner", - "tags": [], - "instance_guid": "2fbe12be-569d-473e-ab0a-eb2f1d18c7e3", - "instance_name": "dsr-core-malware-scanner", - "binding_guid": "16ce4d70-1511-45d0-a788-e365b298ca8a", - "binding_name": null, - "credentials": { - "sync_scan_url": "https://test.scanner.com", - "async_scan_url": "", - "uri": "https://test.scanner.com", - "url": "http://localhost:1111", - "username": "test-user", - "password": "test-password" - }, - "syslog_drain_url": null, - "volume_mounts": [] - } - ] - }, - "VCAP_APPLICATION": { - "application_id": "xsapp!t0815" - } -} diff --git a/integration-tests/srv/test-service.cds b/integration-tests/srv/test-service.cds deleted file mode 100644 index ff68a31ff..000000000 --- a/integration-tests/srv/test-service.cds +++ /dev/null @@ -1,27 +0,0 @@ -using test.data.model as db from '../db/data-model'; - -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' - ]); -} - -annotate db.Roots.mimeValidatedAttachments with { - content @(Core.AcceptableMediaTypes: ['application/pdf']); -} - -service TestService { - entity Roots as projection on db.Roots; - entity AttachmentEntity as projection on db.AttachmentEntity; -} - -service TestDraftService { - @odata.draft.enabled - entity DraftRoots as projection on db.Roots; -} From afd2dcc4c044da1885fca32dbd08d5bfb042ede5 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Tue, 31 Mar 2026 11:22:34 +0200 Subject: [PATCH 13/18] Remove package-lock.json from mtx-local integration tests Also add package-lock.json to .gitignore to prevent accidental commits. --- .gitignore | 1 + integration-tests/mtx-local/package-lock.json | 4144 ----------------- 2 files changed, 1 insertion(+), 4144 deletions(-) delete mode 100644 integration-tests/mtx-local/package-lock.json diff --git a/.gitignore b/.gitignore index 9ca83a529..ca9f60e59 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ target/ .flattened-pom.xml node_modules +package-lock.json ## PMD .pmd diff --git a/integration-tests/mtx-local/package-lock.json b/integration-tests/mtx-local/package-lock.json deleted file mode 100644 index 3383043ec..000000000 --- a/integration-tests/mtx-local/package-lock.json +++ /dev/null @@ -1,4144 +0,0 @@ -{ - "name": "mtx-local-integration-tests", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "mtx-local-integration-tests", - "version": "0.0.0", - "workspaces": [ - "mtx/sidecar" - ], - "devDependencies": { - "@sap/cds-dk": "^9", - "@sap/cds-mtxs": "^3" - } - }, - "mtx/sidecar": { - "name": "mtx-local-sidecar", - "version": "0.0.0", - "dependencies": { - "@sap/cds": "^9", - "@sap/cds-mtxs": "^3", - "@sap/xssec": "^4", - "express": "^4" - }, - "devDependencies": { - "@cap-js/sqlite": "^2" - } - }, - "mtx/sidecar/node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "~1.20.3", - "content-disposition": "~0.5.4", - "content-type": "~1.0.4", - "cookie": "~0.7.1", - "cookie-signature": "~1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.3.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.12", - "proxy-addr": "~2.0.7", - "qs": "~6.14.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "~0.19.0", - "serve-static": "~1.16.2", - "setprototypeof": "1.2.0", - "statuses": "~2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@cap-js/db-service": { - "version": "2.9.0", - "resolved": "https://int.repositories.cloud.sap/artifactory/api/npm/build-milestones-npm/@cap-js/db-service/-/db-service-2.9.0.tgz", - "integrity": "sha512-WCXhoqezaF6A5I2l0MNZeHKXXtHRNEq7Rp0R89/uccOHQIx0DuU0U9NuJJPV/1G5RGk2QKQ9VBo/KYn+MZuuNQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "generic-pool": "^3.9.0" - }, - "peerDependencies": { - "@sap/cds": ">=9.8" - } - }, - "node_modules/@cap-js/sqlite": { - "version": "2.2.0", - "resolved": "https://int.repositories.cloud.sap/artifactory/api/npm/build-milestones-npm/@cap-js/sqlite/-/sqlite-2.2.0.tgz", - "integrity": "sha512-FPj+uVU/14vtGUl2P/Q8y7XhZbsLgrCav2O5PjHPXnupegjby4sMJkgVNxVHnkyKPFgO/W8uEsq9r5TU9VPx8w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@cap-js/db-service": "^2.9.0", - "better-sqlite3": "^12.0.0" - }, - "peerDependencies": { - "@sap/cds": ">=9.8", - "sql.js": "^1.13.0" - }, - "peerDependenciesMeta": { - "sql.js": { - "optional": true - } - } - }, - "node_modules/@eslint/js": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", - "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", - "license": "MIT", - "peer": true, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "eslint": "^10.0.0" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/@sap/cds": { - "version": "9.8.4", - "resolved": "https://registry.npmjs.org/@sap/cds/-/cds-9.8.4.tgz", - "integrity": "sha512-1cnFdz4Ex4LV15dztDsSLDiilVdJ2mfTqr5A3TGsRd/6cM31SnHhuZ/Ra0FEErodokNS2pwasGZmSfgHVDW8KA==", - "license": "SEE LICENSE IN LICENSE", - "dependencies": { - "@sap/cds-compiler": "^6.4", - "@sap/cds-fiori": "^2", - "express": "^4.22.1 || ^5", - "js-yaml": "^4.1.1" - }, - "bin": { - "cds-deploy": "bin/deploy.js", - "cds-serve": "bin/serve.js" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@eslint/js": "^9 || ^10", - "tar": "^7.5.6" - }, - "peerDependenciesMeta": { - "tar": { - "optional": true - } - } - }, - "node_modules/@sap/cds-compiler": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@sap/cds-compiler/-/cds-compiler-6.8.0.tgz", - "integrity": "sha512-yRoTZcH8DFiP4PcEuIHe42YAaNt51V736+6RRr+U4nbO91sz36L8be2djcE8n760t8IbqLxzZ/UkATiL0fjRmA==", - "license": "SEE LICENSE IN LICENSE", - "bin": { - "cdsc": "bin/cdsc.js", - "cdshi": "bin/cdshi.js", - "cdsse": "bin/cdsse.js" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@sap/cds-dk": { - "version": "9.8.2", - "resolved": "https://registry.npmjs.org/@sap/cds-dk/-/cds-dk-9.8.2.tgz", - "integrity": "sha512-PGynoQpOCD8Nsr0EEzsEHWg8szYBLVtVd6byxvaaAPYBkNAj2WA+kin6gWaDTiyG3kJa0BSGPk4PCbA2xDoE9A==", - "dev": true, - "hasShrinkwrap": true, - "license": "SEE LICENSE IN LICENSE", - "dependencies": { - "@cap-js/asyncapi": "^1.0.0", - "@cap-js/openapi": "^1.0.0", - "@sap/cds": ">=8.3", - "@sap/cds-mtxs": ">=2", - "@sap/hdi-deploy": "^5", - "axios": "^1", - "express": "^4.22.1 || ^5", - "hdb": "^2.0.0", - "livereload-js": "^4.0.1", - "mustache": "^4.0.1", - "ws": "^8.4.2", - "xml-js": "^1.6.11", - "yaml": "^2" - }, - "bin": { - "cds": "bin/cds.js", - "cds-ts": "bin/cds-ts.js", - "cds-tsx": "bin/cds-tsx.js" - }, - "optionalDependencies": { - "@cap-js/sqlite": ">=1" - } - }, - "node_modules/@sap/cds-dk/node_modules/@cap-js/asyncapi": { - "version": "1.0.3", - "integrity": "sha512-vZSWKAe+3qfvZDXV5SSFiObGWmqyS9MDyEADb5PLVT8kzO39qGaSDPv/GzI/gwvRfCayGAjU4ThiBKrFA7Gclg==", - "dev": true, - "license": "SEE LICENSE IN LICENSE", - "peerDependencies": { - "@sap/cds": ">=7.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/@cap-js/db-service": { - "version": "2.9.0", - "integrity": "sha512-WCXhoqezaF6A5I2l0MNZeHKXXtHRNEq7Rp0R89/uccOHQIx0DuU0U9NuJJPV/1G5RGk2QKQ9VBo/KYn+MZuuNQ==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "generic-pool": "^3.9.0" - }, - "peerDependencies": { - "@sap/cds": ">=9.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/@cap-js/openapi": { - "version": "1.4.0", - "integrity": "sha512-/LRSwn4SDxAi3qKwl09zoOhEVGaPGlYOPz/0S3UBnaMJVvaLyPiKbbaOtOnrrgulUX5OXt+ujPIQznOsbTzuAw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "pluralize": "^8.0.0" - }, - "peerDependencies": { - "@sap/cds": ">=7.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/@cap-js/sqlite": { - "version": "2.2.0", - "integrity": "sha512-FPj+uVU/14vtGUl2P/Q8y7XhZbsLgrCav2O5PjHPXnupegjby4sMJkgVNxVHnkyKPFgO/W8uEsq9r5TU9VPx8w==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@cap-js/db-service": "^2.9.0", - "better-sqlite3": "^12.0.0" - }, - "peerDependencies": { - "@sap/cds": ">=9.8", - "sql.js": "^1.13.0" - }, - "peerDependenciesMeta": { - "sql.js": { - "optional": true - } - } - }, - "node_modules/@sap/cds-dk/node_modules/@eslint/js": { - "version": "10.0.1", - "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "eslint": "^10.0.0" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/@sap/cds-dk/node_modules/@sap/cds": { - "version": "9.8.3", - "integrity": "sha512-up/kNVCZK8bOTyye1QN2BjMpH+IgcdyhT3U9mS3TqkpmjNE2JXq8Qz1iNAN+nYcXRWJ1XAUNpgebLfuh/49MGQ==", - "dev": true, - "license": "SEE LICENSE IN LICENSE", - "dependencies": { - "@sap/cds-compiler": "^6.4", - "@sap/cds-fiori": "^2", - "express": "^4.22.1 || ^5", - "js-yaml": "^4.1.1" - }, - "bin": { - "cds-deploy": "bin/deploy.js", - "cds-serve": "bin/serve.js" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@eslint/js": "^9 || ^10", - "tar": "^7.5.6" - }, - "peerDependenciesMeta": { - "tar": { - "optional": true - } - } - }, - "node_modules/@sap/cds-dk/node_modules/@sap/cds-compiler": { - "version": "6.8.0", - "integrity": "sha512-yRoTZcH8DFiP4PcEuIHe42YAaNt51V736+6RRr+U4nbO91sz36L8be2djcE8n760t8IbqLxzZ/UkATiL0fjRmA==", - "dev": true, - "license": "SEE LICENSE IN LICENSE", - "bin": { - "cdsc": "bin/cdsc.js", - "cdshi": "bin/cdshi.js", - "cdsse": "bin/cdsse.js" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@sap/cds-dk/node_modules/@sap/cds-fiori": { - "version": "2.3.0", - "integrity": "sha512-6oWov+DSpFrSTgxXR0dZhak6aZ/IVRZvaHERMi0EgSTzIJdlvZlpw3Kf18ePMcTrRrtEXwD4RIjKt8pbs0g2Hg==", - "dev": true, - "license": "SEE LICENSE IN LICENSE", - "peerDependencies": { - "@sap/cds": ">=8" - } - }, - "node_modules/@sap/cds-dk/node_modules/@sap/cds-mtxs": { - "version": "3.8.1", - "integrity": "sha512-FUm1of08WsZ1O3eur7OGj7ZYp9Gy3RuYEGBZK+zosSPyJ3QrBhBzKwYvbh2DJdbv3P/xuk46NGg5yz914qZxrw==", - "dev": true, - "license": "SEE LICENSE IN LICENSE", - "dependencies": { - "@sap/hdi-deploy": "^5" - }, - "bin": { - "cds-mtx": "bin/cds-mtx.js", - "cds-mtx-migrate": "bin/cds-mtx-migrate.js" - }, - "peerDependencies": { - "@sap/cds": "^9" - } - }, - "node_modules/@sap/cds-dk/node_modules/@sap/hdi": { - "version": "4.8.0", - "integrity": "sha512-tkJmY2ffm6mt4/LFwRBihlQkMxNAXa3ngvRe2N/6+qLIsUNdrH/M03S5mkygXq56K+KoVVZYuradajCusMWwsw==", - "dev": true, - "license": "See LICENSE file", - "dependencies": { - "async": "^3.2.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@sap/hana-client": "^2 >= 2.5", - "hdb": "^2 || ^0" - }, - "peerDependenciesMeta": { - "@sap/hana-client": { - "optional": true - }, - "hdb": { - "optional": true - } - } - }, - "node_modules/@sap/cds-dk/node_modules/@sap/hdi-deploy": { - "version": "5.6.1", - "integrity": "sha512-+qQ7qwG8lko303L5yRj2dud/nDAVuVblV/mmzJT44oPbF0Nry18eD2LUS23hFeuxjRa7rYK5YKQ8ffGgWxVrYQ==", - "dev": true, - "license": "See LICENSE file", - "dependencies": { - "@sap/hdi": "^4.8.0", - "@sap/xsenv": "^6.0.0", - "async": "^3.2.6", - "dotenv": "^16.4.5", - "handlebars": "^4.7.8", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=18.x" - }, - "peerDependencies": { - "@sap/hana-client": "^2 >= 2.6", - "hdb": "^2 || ^0" - }, - "peerDependenciesMeta": { - "@sap/hana-client": { - "optional": true - }, - "hdb": { - "optional": true - } - } - }, - "node_modules/@sap/cds-dk/node_modules/@sap/xsenv": { - "version": "6.1.0", - "integrity": "sha512-vlW4Zad3uiDqHtnYdQ0TsEIH8VIO4HmPGDowfBL5dIcHPmeKDISEQ9ibeHL5FkceqvYcXJEQAVZ5/hsHDqlXZg==", - "dev": true, - "license": "SEE LICENSE IN LICENSE file", - "dependencies": { - "debug": "4.4.3", - "node-cache": "^5.1.2", - "verror": "1.10.1" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/accepts": { - "version": "2.0.0", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/argparse": { - "version": "2.0.1", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/@sap/cds-dk/node_modules/assert-plus": { - "version": "1.0.0", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/async": { - "version": "3.2.6", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/asynckit": { - "version": "0.4.0", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/axios": { - "version": "1.13.6", - "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.11", - "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/base64-js": { - "version": "1.5.1", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/better-sqlite3": { - "version": "12.8.0", - "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" - }, - "engines": { - "node": "20.x || 22.x || 23.x || 24.x || 25.x" - } - }, - "node_modules/@sap/cds-dk/node_modules/bindings": { - "version": "1.5.0", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/bl": { - "version": "4.1.0", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/body-parser": { - "version": "2.2.2", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/braces": { - "version": "3.0.3", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sap/cds-dk/node_modules/buffer": { - "version": "5.7.1", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/@sap/cds-dk/node_modules/bytes": { - "version": "3.1.2", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/call-bound": { - "version": "1.0.4", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/chownr": { - "version": "1.1.4", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/clone": { - "version": "2.1.2", - "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/combined-stream": { - "version": "1.0.8", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/content-disposition": { - "version": "1.0.1", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/content-type": { - "version": "1.0.5", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/cookie": { - "version": "0.7.2", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/cookie-signature": { - "version": "1.2.2", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/core-util-is": { - "version": "1.0.2", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/debug": { - "version": "4.4.3", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@sap/cds-dk/node_modules/decompress-response": { - "version": "6.0.0", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@sap/cds-dk/node_modules/deep-extend": { - "version": "0.6.0", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/delayed-stream": { - "version": "1.0.0", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/depd": { - "version": "2.0.0", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/detect-libc": { - "version": "2.1.2", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sap/cds-dk/node_modules/dotenv": { - "version": "16.6.1", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/@sap/cds-dk/node_modules/dunder-proto": { - "version": "1.0.1", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/ee-first": { - "version": "1.1.1", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/encodeurl": { - "version": "2.0.0", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/end-of-stream": { - "version": "1.4.5", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/es-define-property": { - "version": "1.0.1", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/es-errors": { - "version": "1.3.0", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/es-object-atoms": { - "version": "1.1.1", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/es-set-tostringtag": { - "version": "2.1.0", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/escape-html": { - "version": "1.0.3", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/etag": { - "version": "1.8.1", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/expand-template": { - "version": "2.0.3", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "dev": true, - "license": "(MIT OR WTFPL)", - "optional": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/@sap/cds-dk/node_modules/express": { - "version": "5.2.1", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/extsprintf": { - "version": "1.4.1", - "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/file-uri-to-path": { - "version": "1.0.0", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/fill-range": { - "version": "7.1.1", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sap/cds-dk/node_modules/finalhandler": { - "version": "2.1.1", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/follow-redirects": { - "version": "1.15.11", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/@sap/cds-dk/node_modules/form-data": { - "version": "4.0.5", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@sap/cds-dk/node_modules/form-data/node_modules/mime-db": { - "version": "1.52.0", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/form-data/node_modules/mime-types": { - "version": "2.1.35", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/forwarded": { - "version": "0.2.0", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/fresh": { - "version": "2.0.0", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/fs-constants": { - "version": "1.0.0", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/function-bind": { - "version": "1.1.2", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/generic-pool": { - "version": "3.9.0", - "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/@sap/cds-dk/node_modules/get-intrinsic": { - "version": "1.3.0", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/get-proto": { - "version": "1.0.1", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/github-from-package": { - "version": "0.0.0", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/gopd": { - "version": "1.2.0", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/handlebars": { - "version": "4.7.8", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/has-symbols": { - "version": "1.1.0", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/has-tostringtag": { - "version": "1.0.2", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/hasown": { - "version": "2.0.2", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/hdb": { - "version": "2.27.1", - "integrity": "sha512-xYL/W+fq2TyGHyzm8muolQnw8tdh4+2NQ8mQP2FpLSuhfJ8l0jQNSUZoAXic7NfMEan1Jvf8V1L4blwkgTc6+A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "iconv-lite": "0.7.0" - }, - "engines": { - "node": ">= 18" - }, - "optionalDependencies": { - "lz4-wasm-nodejs": "0.9.2" - } - }, - "node_modules/@sap/cds-dk/node_modules/hdb/node_modules/iconv-lite": { - "version": "0.7.0", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/http-errors": { - "version": "2.0.1", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/iconv-lite": { - "version": "0.7.2", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/ieee754": { - "version": "1.2.1", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/inherits": { - "version": "2.0.4", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/@sap/cds-dk/node_modules/ini": { - "version": "1.3.8", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/ipaddr.js": { - "version": "1.9.1", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/@sap/cds-dk/node_modules/is-number": { - "version": "7.0.0", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/is-promise": { - "version": "4.0.0", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/js-yaml": { - "version": "4.1.1", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@sap/cds-dk/node_modules/livereload-js": { - "version": "4.0.2", - "integrity": "sha512-Fy7VwgQNiOkynYyNBTo3v9hQUhcW5pFAheJN148+DTgpShjsy/22pLHKKwDK5v0kOsZsJBK+6q1PMgLvRmrwFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/lz4-wasm-nodejs": { - "version": "0.9.2", - "integrity": "sha512-hSwgJPS98q/Oe/89Y1OxzeA/UdnASG8GvldRyKa7aZyoAFCC8VPRtViBSava7wWC66WocjUwBpWau2rEmyFPsw==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/math-intrinsics": { - "version": "1.1.0", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/media-typer": { - "version": "1.1.0", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/merge-descriptors": { - "version": "2.0.0", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@sap/cds-dk/node_modules/micromatch": { - "version": "4.0.8", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.2", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@sap/cds-dk/node_modules/mime-db": { - "version": "1.54.0", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/mime-types": { - "version": "3.0.2", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/mimic-response": { - "version": "3.1.0", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@sap/cds-dk/node_modules/minimist": { - "version": "1.2.8", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/mkdirp-classic": { - "version": "0.5.3", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/ms": { - "version": "2.1.3", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/mustache": { - "version": "4.2.0", - "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", - "dev": true, - "license": "MIT", - "bin": { - "mustache": "bin/mustache" - } - }, - "node_modules/@sap/cds-dk/node_modules/napi-build-utils": { - "version": "2.0.0", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/negotiator": { - "version": "1.0.0", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/neo-async": { - "version": "2.6.2", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/node-abi": { - "version": "3.89.0", - "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@sap/cds-dk/node_modules/node-cache": { - "version": "5.1.2", - "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone": "2.x" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/object-inspect": { - "version": "1.13.4", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/on-finished": { - "version": "2.4.1", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/once": { - "version": "1.4.0", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/@sap/cds-dk/node_modules/parseurl": { - "version": "1.3.3", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/path-to-regexp": { - "version": "8.3.0", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/pluralize": { - "version": "8.0.0", - "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@sap/cds-dk/node_modules/prebuild-install": { - "version": "7.1.3", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@sap/cds-dk/node_modules/proxy-addr": { - "version": "2.0.7", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/@sap/cds-dk/node_modules/proxy-from-env": { - "version": "1.1.0", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/pump": { - "version": "3.0.4", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/@sap/cds-dk/node_modules/qs": { - "version": "6.15.0", - "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/range-parser": { - "version": "1.2.1", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/raw-body": { - "version": "3.0.2", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/@sap/cds-dk/node_modules/rc": { - "version": "1.2.8", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "optional": true, - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/@sap/cds-dk/node_modules/readable-stream": { - "version": "3.6.2", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@sap/cds-dk/node_modules/router": { - "version": "2.2.0", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@sap/cds-dk/node_modules/safe-buffer": { - "version": "5.2.1", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/safer-buffer": { - "version": "2.1.2", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/sax": { - "version": "1.6.0", - "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=11.0.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/semver": { - "version": "7.7.4", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@sap/cds-dk/node_modules/send": { - "version": "1.2.1", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/serve-static": { - "version": "2.2.1", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/setprototypeof": { - "version": "1.2.0", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true, - "license": "ISC" - }, - "node_modules/@sap/cds-dk/node_modules/side-channel": { - "version": "1.1.0", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/side-channel-list": { - "version": "1.0.0", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/side-channel-map": { - "version": "1.0.1", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/side-channel-weakmap": { - "version": "1.0.2", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/simple-concat": { - "version": "1.0.1", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/simple-get": { - "version": "4.0.1", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/source-map": { - "version": "0.6.1", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/statuses": { - "version": "2.0.2", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/string_decoder": { - "version": "1.3.0", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/strip-json-comments": { - "version": "2.0.1", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/tar-fs": { - "version": "2.1.4", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/tar-stream": { - "version": "2.2.0", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@sap/cds-dk/node_modules/to-regex-range": { - "version": "5.0.1", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/toidentifier": { - "version": "1.0.1", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/tunnel-agent": { - "version": "0.6.0", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@sap/cds-dk/node_modules/type-is": { - "version": "2.0.1", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "dev": true, - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/uglify-js": { - "version": "3.19.3", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/unpipe": { - "version": "1.0.0", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/util-deprecate": { - "version": "1.0.2", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/vary": { - "version": "1.1.2", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/verror": { - "version": "1.10.1", - "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", - "dev": true, - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/wordwrap": { - "version": "1.0.0", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/wrappy": { - "version": "1.0.2", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/@sap/cds-dk/node_modules/ws": { - "version": "8.20.0", - "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/@sap/cds-dk/node_modules/xml-js": { - "version": "1.6.11", - "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "sax": "^1.2.4" - }, - "bin": { - "xml-js": "bin/cli.js" - } - }, - "node_modules/@sap/cds-dk/node_modules/yaml": { - "version": "2.8.3", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", - "dev": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, - "node_modules/@sap/cds-fiori": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@sap/cds-fiori/-/cds-fiori-2.3.0.tgz", - "integrity": "sha512-6oWov+DSpFrSTgxXR0dZhak6aZ/IVRZvaHERMi0EgSTzIJdlvZlpw3Kf18ePMcTrRrtEXwD4RIjKt8pbs0g2Hg==", - "license": "SEE LICENSE IN LICENSE", - "peerDependencies": { - "@sap/cds": ">=8" - } - }, - "node_modules/@sap/cds-mtxs": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@sap/cds-mtxs/-/cds-mtxs-3.8.1.tgz", - "integrity": "sha512-FUm1of08WsZ1O3eur7OGj7ZYp9Gy3RuYEGBZK+zosSPyJ3QrBhBzKwYvbh2DJdbv3P/xuk46NGg5yz914qZxrw==", - "license": "SEE LICENSE IN LICENSE", - "dependencies": { - "@sap/hdi-deploy": "^5" - }, - "bin": { - "cds-mtx": "bin/cds-mtx.js", - "cds-mtx-migrate": "bin/cds-mtx-migrate.js" - }, - "peerDependencies": { - "@sap/cds": "^9" - } - }, - "node_modules/@sap/hdi": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@sap/hdi/-/hdi-4.8.0.tgz", - "integrity": "sha512-tkJmY2ffm6mt4/LFwRBihlQkMxNAXa3ngvRe2N/6+qLIsUNdrH/M03S5mkygXq56K+KoVVZYuradajCusMWwsw==", - "license": "See LICENSE file", - "dependencies": { - "async": "^3.2.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@sap/hana-client": "^2 >= 2.5", - "hdb": "^2 || ^0" - }, - "peerDependenciesMeta": { - "@sap/hana-client": { - "optional": true - }, - "hdb": { - "optional": true - } - } - }, - "node_modules/@sap/hdi-deploy": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/@sap/hdi-deploy/-/hdi-deploy-5.6.1.tgz", - "integrity": "sha512-+qQ7qwG8lko303L5yRj2dud/nDAVuVblV/mmzJT44oPbF0Nry18eD2LUS23hFeuxjRa7rYK5YKQ8ffGgWxVrYQ==", - "license": "See LICENSE file", - "dependencies": { - "@sap/hdi": "^4.8.0", - "@sap/xsenv": "^6.0.0", - "async": "^3.2.6", - "dotenv": "^16.4.5", - "handlebars": "^4.7.8", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=18.x" - }, - "peerDependencies": { - "@sap/hana-client": "^2 >= 2.6", - "hdb": "^2 || ^0" - }, - "peerDependenciesMeta": { - "@sap/hana-client": { - "optional": true - }, - "hdb": { - "optional": true - } - } - }, - "node_modules/@sap/xsenv": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@sap/xsenv/-/xsenv-6.1.0.tgz", - "integrity": "sha512-vlW4Zad3uiDqHtnYdQ0TsEIH8VIO4HmPGDowfBL5dIcHPmeKDISEQ9ibeHL5FkceqvYcXJEQAVZ5/hsHDqlXZg==", - "license": "SEE LICENSE IN LICENSE file", - "dependencies": { - "debug": "4.4.3", - "node-cache": "^5.1.2", - "verror": "1.10.1" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@sap/xsenv/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@sap/xsenv/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/@sap/xssec": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@sap/xssec/-/xssec-4.13.0.tgz", - "integrity": "sha512-8e+bU+OyAIpAGXQanOopZa5YEK+yHKw84dhhihcCotF40MSNFbVHjQ4xM5hf4QndlqDGfXIuvXmoOMuDATa/gA==", - "license": "SAP DEVELOPER LICENSE AGREEMENT", - "dependencies": { - "debug": "^4.4.3", - "jwt-decode": "^4" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@sap/xssec/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@sap/xssec/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/better-sqlite3": { - "version": "12.8.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.8.0.tgz", - "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" - }, - "engines": { - "node": "20.x || 22.x || 23.x || 24.x || 25.x" - } - }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.14.0", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true, - "license": "ISC" - }, - "node_modules/clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "license": "MIT" - }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "dev": true, - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/content-disposition": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/express/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/express/node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/express/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/express/node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/express/node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/extsprintf": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", - "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", - "engines": [ - "node >=0.6.0" - ], - "license": "MIT" - }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "statuses": "~2.0.2", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true, - "license": "MIT" - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/generic-pool": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", - "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "dev": true, - "license": "MIT" - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/handlebars": { - "version": "4.7.9", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", - "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jwt-decode": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", - "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true, - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/mtx-local-sidecar": { - "resolved": "mtx/sidecar", - "link": true - }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "license": "MIT" - }, - "node_modules/node-abi": { - "version": "3.89.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", - "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-cache": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", - "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", - "license": "MIT", - "dependencies": { - "clone": "2.x" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", - "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", - "license": "MIT" - }, - "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/router/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/router/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/router/node_modules/path-to-regexp": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz", - "integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", - "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.1", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "~2.4.1", - "range-parser": "~1.2.1", - "statuses": "~2.0.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/serve-static": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", - "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "~0.19.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/uglify-js": { - "version": "3.19.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "license": "BSD-2-Clause", - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/verror": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", - "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "license": "MIT" - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - } - } -} From 0e38a8aaa5df9b1ae2f7ad531b66e9bb1cd20e77 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Tue, 31 Mar 2026 11:28:53 +0200 Subject: [PATCH 14/18] improvements --- .../system/SubscribeModelTenantsHandler.java | 10 ++- .../MultiTenantAttachmentIsolationTest.java | 2 + .../mt/utils/SubscriptionEndpointClient.java | 2 +- .../attachments/oss/client/AzureClient.java | 44 ++++++--- .../attachments/oss/client/GoogleClient.java | 9 +- .../oss/configuration/Registration.java | 15 ++++ .../handler/OSSAttachmentsServiceHandler.java | 5 +- .../oss/handler/TenantCleanupHandler.java | 1 + .../attachments/oss/client/AWSClientTest.java | 49 ++++++++++ .../oss/configuration/RegistrationTest.java | 89 +++++++++++++++++++ .../OSSAttachmentsServiceHandlerTest.java | 66 ++++++++++++++ .../oss/handler/TenantCleanupHandlerTest.java | 30 +++++++ 12 files changed, 300 insertions(+), 22 deletions(-) diff --git a/integration-tests/mtx-local/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/mt/handlers/system/SubscribeModelTenantsHandler.java b/integration-tests/mtx-local/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/mt/handlers/system/SubscribeModelTenantsHandler.java index 3bed0e2c6..06b260c62 100644 --- a/integration-tests/mtx-local/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/mt/handlers/system/SubscribeModelTenantsHandler.java +++ b/integration-tests/mtx-local/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/mt/handlers/system/SubscribeModelTenantsHandler.java @@ -34,14 +34,15 @@ public void subscribeMockTenants(ApplicationPreparedEventContext context) { if (Boolean.FALSE.equals(multiTenancy.getMock().isEnabled())) { return; } - if (readMockedTenants().isEmpty()) { + List tenants = readMockedTenants(); + if (tenants.isEmpty()) { return; } if (!StringUtils.hasText(multiTenancy.getSidecar().getUrl())) { return; } - readMockedTenants().forEach(this::subscribeTenant); + tenants.forEach(this::subscribeTenant); } @On(event = ApplicationLifecycleService.EVENT_APPLICATION_STOPPED) @@ -51,11 +52,12 @@ public void unsubscribeMockTenants(ApplicationStoppedEventContext context) { if (Boolean.FALSE.equals(multiTenancy.getMock().isEnabled())) { return; } - if (readMockedTenants().isEmpty()) { + List tenants = readMockedTenants(); + if (tenants.isEmpty()) { return; } - readMockedTenants().forEach(this::unsubscribeTenant); + tenants.forEach(this::unsubscribeTenant); } private void subscribeTenant(String tenant) { diff --git a/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/MultiTenantAttachmentIsolationTest.java b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/MultiTenantAttachmentIsolationTest.java index f8cd24b2c..d19bf5ed5 100644 --- a/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/MultiTenantAttachmentIsolationTest.java +++ b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/MultiTenantAttachmentIsolationTest.java @@ -22,6 +22,8 @@ @SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("local-with-tenants") +// TODO: Add tests that upload/download actual binary attachment content across tenants +// to verify storage-level isolation (not just entity-level isolation). class MultiTenantAttachmentIsolationTest { private static final String DOCUMENTS_URL = "/odata/v4/MtTestService/Documents"; diff --git a/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/utils/SubscriptionEndpointClient.java b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/utils/SubscriptionEndpointClient.java index 07fc0c8ff..e78fe0b05 100644 --- a/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/utils/SubscriptionEndpointClient.java +++ b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/utils/SubscriptionEndpointClient.java @@ -30,7 +30,7 @@ public SubscriptionEndpointClient(ObjectMapper objectMapper, MockMvc client) { public void subscribeTenant(String tenant) throws Exception { SubscriptionPayload payload = new SubscriptionPayload(); payload.subscribedTenantId = tenant; - payload.subscribedSubdomain = tenant.concat("sap.com"); + payload.subscribedSubdomain = tenant.concat(".sap.com"); payload.eventType = "CREATE"; client diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AzureClient.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AzureClient.java index a81258823..4f930ac08 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AzureClient.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AzureClient.java @@ -15,6 +15,7 @@ import java.io.InputStream; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import org.slf4j.Logger; @@ -97,23 +98,22 @@ public Future deleteContentByPrefix(String prefix) { () -> { try { ListBlobsOptions options = new ListBlobsOptions().setPrefix(prefix); - List blobNames = new ArrayList<>(); + int batchSize = 1000; + List batch = new ArrayList<>(batchSize); for (BlobItem blobItem : blobContainerClient.listBlobs(options, null)) { - blobNames.add(blobItem.getName()); + batch.add(blobItem.getName()); + if (batch.size() >= batchSize) { + deleteBatch(batch); + batch.clear(); + } } - List> deleteFutures = - blobNames.stream() - .map( - name -> - executor.submit( - () -> { - blobContainerClient.getBlobClient(name).delete(); - return (Void) null; - })) - .toList(); - for (Future f : deleteFutures) { - f.get(); + if (!batch.isEmpty()) { + deleteBatch(batch); } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new ObjectStoreServiceException( + "Interrupted while deleting objects by prefix from the Azure Object Store", e); } catch (RuntimeException e) { throw new ObjectStoreServiceException( "Failed to delete objects by prefix from the Azure Object Store", e); @@ -124,4 +124,20 @@ public Future deleteContentByPrefix(String prefix) { return null; }); } + + private void deleteBatch(List blobNames) throws InterruptedException, ExecutionException { + List> deleteFutures = + blobNames.stream() + .map( + name -> + executor.submit( + () -> { + blobContainerClient.getBlobClient(name).delete(); + return (Void) null; + })) + .toList(); + for (Future f : deleteFutures) { + f.get(); + } + } } diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/GoogleClient.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/GoogleClient.java index e82fb5a64..d41a6767a 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/GoogleClient.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/GoogleClient.java @@ -146,7 +146,14 @@ public Future deleteContentByPrefix(String prefix) { Storage.BlobListOption.prefix(prefix), Storage.BlobListOption.versions(true)); for (Blob blob : blobs.iterateAll()) { - storage.delete(BlobId.of(bucketName, blob.getName(), blob.getGeneration())); + boolean deleted = + storage.delete(BlobId.of(bucketName, blob.getName(), blob.getGeneration())); + if (!deleted) { + logger.warn( + "Failed to delete blob {} (generation {}) during prefix cleanup", + blob.getName(), + blob.getGeneration()); + } } } catch (RuntimeException e) { throw new ObjectStoreServiceException( diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/configuration/Registration.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/configuration/Registration.java index 5a3fff178..2276dd9c0 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/configuration/Registration.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/configuration/Registration.java @@ -12,6 +12,7 @@ import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,6 +29,20 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) { String objectStoreKind = getObjectStoreKind(env); ExecutorService executor = Executors.newCachedThreadPool(); + Runtime.getRuntime() + .addShutdownHook( + new Thread( + () -> { + executor.shutdown(); + try { + if (!executor.awaitTermination(30, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + } catch (InterruptedException e) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } + })); OSSAttachmentsServiceHandler handler = new OSSAttachmentsServiceHandler( bindingOpt.get(), executor, multitenancyEnabled, objectStoreKind); diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java index 1a41d0bb3..3a51830ef 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java @@ -217,8 +217,9 @@ private String getTenant(EventContext context) { return tenant != null ? tenant : "default"; } - private static void validateTenantId(String tenantId) { - if (tenantId.isEmpty() + static void validateTenantId(String tenantId) { + if (tenantId == null + || tenantId.isEmpty() || tenantId.contains("/") || tenantId.contains("\\") || tenantId.contains("..")) { diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandler.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandler.java index f719bb181..b35d7f9e4 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandler.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandler.java @@ -25,6 +25,7 @@ public TenantCleanupHandler(OSClient osClient) { @After(event = DeploymentService.EVENT_UNSUBSCRIBE) void cleanupTenantData(UnsubscribeEventContext context) { String tenantId = context.getTenant(); + OSSAttachmentsServiceHandler.validateTenantId(tenantId); String prefix = tenantId + "/"; try { osClient.deleteContentByPrefix(prefix).get(); diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java index 05e86bff6..b535ce53b 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java @@ -8,7 +8,9 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -297,6 +299,53 @@ void testDeleteContentByPrefixThrowsOnRuntimeException() throws Exception { assertInstanceOf(ObjectStoreServiceException.class, thrown.getCause()); } + @Test + void testDeleteContentByPrefixWithPagination() throws Exception { + AWSClient awsClient = new AWSClient(getDummyBinding(), executor); + + S3Client mockS3Client = mock(S3Client.class); + + // First page: 2 objects, isTruncated=true + S3Object obj1 = S3Object.builder().key("prefix/file1.txt").build(); + S3Object obj2 = S3Object.builder().key("prefix/file2.txt").build(); + + ListObjectsV2Response firstPage = mock(ListObjectsV2Response.class); + when(firstPage.contents()).thenReturn(List.of(obj1, obj2)); + when(firstPage.isTruncated()).thenReturn(true); + when(firstPage.nextContinuationToken()).thenReturn("token1"); + + // Second page: 1 object, isTruncated=false + S3Object obj3 = S3Object.builder().key("prefix/file3.txt").build(); + + ListObjectsV2Response secondPage = mock(ListObjectsV2Response.class); + when(secondPage.contents()).thenReturn(List.of(obj3)); + when(secondPage.isTruncated()).thenReturn(false); + + // First call returns first page, second call (with token) returns second page + when(mockS3Client.listObjectsV2( + argThat((ListObjectsV2Request req) -> req != null && req.continuationToken() == null))) + .thenReturn(firstPage); + when(mockS3Client.listObjectsV2( + argThat( + (ListObjectsV2Request req) -> + req != null && "token1".equals(req.continuationToken())))) + .thenReturn(secondPage); + + DeleteObjectsResponse deleteResponse = mock(DeleteObjectsResponse.class); + when(deleteResponse.hasErrors()).thenReturn(false); + when(deleteResponse.errors()).thenReturn(Collections.emptyList()); + when(mockS3Client.deleteObjects(any(DeleteObjectsRequest.class))).thenReturn(deleteResponse); + + var field = AWSClient.class.getDeclaredField("s3Client"); + field.setAccessible(true); + field.set(awsClient, mockS3Client); + + awsClient.deleteContentByPrefix("prefix/").get(); + + // deleteObjects should be called twice — once per page + verify(mockS3Client, times(2)).deleteObjects(any(DeleteObjectsRequest.class)); + } + private ServiceBinding getDummyBinding() { ServiceBinding binding = mock(ServiceBinding.class); HashMap creds = new HashMap<>(); diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java index 2318424f1..5c8c59498 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java @@ -98,4 +98,93 @@ void testEventHandlersNoBindingDoesNotRegister() { verify(configurer, never()).eventHandler(any()); } + + @Test + void testMtEnabledNonSharedKindRegistersOnlyOSSHandler() { + Registration registration = new Registration(); + CdsRuntimeConfigurer configurer = mock(CdsRuntimeConfigurer.class); + CdsRuntime cdsRuntime = mock(CdsRuntime.class); + CdsEnvironment environment = mock(CdsEnvironment.class); + ServiceBinding binding = mock(ServiceBinding.class); + + Map credentials = new HashMap<>(); + credentials.put("host", "aws.example.com"); + credentials.put("region", "us-east-1"); + credentials.put("access_key_id", "test-access-key"); + credentials.put("secret_access_key", "test-secret-key"); + credentials.put("bucket", "test-bucket"); + + when(configurer.getCdsRuntime()).thenReturn(cdsRuntime); + when(cdsRuntime.getEnvironment()).thenReturn(environment); + when(binding.getServiceName()).thenReturn(Optional.of("objectstore")); + when(binding.getCredentials()).thenReturn(credentials); + when(environment.getServiceBindings()).thenReturn(Stream.of(binding)); + when(environment.getProperty("cds.multitenancy.enabled", Boolean.class, Boolean.FALSE)) + .thenReturn(Boolean.TRUE); + when(environment.getProperty("cds.attachments.objectStore.kind", String.class, null)) + .thenReturn("dedicated"); + + registration.eventHandlers(configurer); + + verify(configurer, times(1)).eventHandler(any(OSSAttachmentsServiceHandler.class)); + verify(configurer, times(1)).eventHandler(any()); + } + + @Test + void testMtEnabledNullKindRegistersOnlyOSSHandler() { + Registration registration = new Registration(); + CdsRuntimeConfigurer configurer = mock(CdsRuntimeConfigurer.class); + CdsRuntime cdsRuntime = mock(CdsRuntime.class); + CdsEnvironment environment = mock(CdsEnvironment.class); + ServiceBinding binding = mock(ServiceBinding.class); + + Map credentials = new HashMap<>(); + credentials.put("host", "aws.example.com"); + credentials.put("region", "us-east-1"); + credentials.put("access_key_id", "test-access-key"); + credentials.put("secret_access_key", "test-secret-key"); + credentials.put("bucket", "test-bucket"); + + when(configurer.getCdsRuntime()).thenReturn(cdsRuntime); + when(cdsRuntime.getEnvironment()).thenReturn(environment); + when(binding.getServiceName()).thenReturn(Optional.of("objectstore")); + when(binding.getCredentials()).thenReturn(credentials); + when(environment.getServiceBindings()).thenReturn(Stream.of(binding)); + when(environment.getProperty("cds.multitenancy.enabled", Boolean.class, Boolean.FALSE)) + .thenReturn(Boolean.TRUE); + + registration.eventHandlers(configurer); + + verify(configurer, times(1)).eventHandler(any(OSSAttachmentsServiceHandler.class)); + verify(configurer, times(1)).eventHandler(any()); + } + + @Test + void testMtDisabledSharedKindRegistersOnlyOSSHandler() { + Registration registration = new Registration(); + CdsRuntimeConfigurer configurer = mock(CdsRuntimeConfigurer.class); + CdsRuntime cdsRuntime = mock(CdsRuntime.class); + CdsEnvironment environment = mock(CdsEnvironment.class); + ServiceBinding binding = mock(ServiceBinding.class); + + Map credentials = new HashMap<>(); + credentials.put("host", "aws.example.com"); + credentials.put("region", "us-east-1"); + credentials.put("access_key_id", "test-access-key"); + credentials.put("secret_access_key", "test-secret-key"); + credentials.put("bucket", "test-bucket"); + + when(configurer.getCdsRuntime()).thenReturn(cdsRuntime); + when(cdsRuntime.getEnvironment()).thenReturn(environment); + when(binding.getServiceName()).thenReturn(Optional.of("objectstore")); + when(binding.getCredentials()).thenReturn(credentials); + when(environment.getServiceBindings()).thenReturn(Stream.of(binding)); + when(environment.getProperty("cds.attachments.objectStore.kind", String.class, null)) + .thenReturn("shared"); + + registration.eventHandlers(configurer); + + verify(configurer, times(1)).eventHandler(any(OSSAttachmentsServiceHandler.class)); + verify(configurer, times(1)).eventHandler(any()); + } } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java index c4350e2e7..538b023a2 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java @@ -714,4 +714,70 @@ void testReadAttachmentHandlesExecutionException() assertThrows(ServiceException.class, () -> handler.readAttachment(context)); verify(context).setCompleted(); } + + @Test + void testReadAttachmentWithMultitenancyBuildsObjectKey() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = + mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); + + var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); + osClientField.setAccessible(true); + osClientField.set(handler, mockOsClient); + var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); + mtField.setAccessible(true); + mtField.set(handler, true); + var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); + kindField.setAccessible(true); + kindField.set(handler, "shared"); + + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + MediaData mockMediaData = mock(MediaData.class); + UserInfo userInfo = mock(UserInfo.class); + + when(context.getContentId()).thenReturn("content123"); + when(context.getData()).thenReturn(mockMediaData); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.getTenant()).thenReturn("myTenant"); + + when(mockOsClient.readContent("myTenant/content123")) + .thenReturn(CompletableFuture.completedFuture(new ByteArrayInputStream("test".getBytes()))); + + handler.readAttachment(context); + + verify(mockOsClient).readContent("myTenant/content123"); + } + + @Test + void testMarkAsDeletedWithMultitenancyBuildsObjectKey() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = + mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); + + var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); + osClientField.setAccessible(true); + osClientField.set(handler, mockOsClient); + var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); + mtField.setAccessible(true); + mtField.set(handler, true); + var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); + kindField.setAccessible(true); + kindField.set(handler, "shared"); + + AttachmentMarkAsDeletedEventContext context = mock(AttachmentMarkAsDeletedEventContext.class); + UserInfo userInfo = mock(UserInfo.class); + + when(context.getContentId()).thenReturn("content123"); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.getTenant()).thenReturn("myTenant"); + + when(mockOsClient.deleteContent("myTenant/content123")) + .thenReturn(CompletableFuture.completedFuture(null)); + + handler.markAttachmentAsDeleted(context); + + verify(mockOsClient).deleteContent("myTenant/content123"); + } } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java index f92e76cdf..e9a0d3d0f 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java @@ -3,13 +3,16 @@ */ package com.sap.cds.feature.attachments.oss.handler; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.sap.cds.feature.attachments.oss.client.OSClient; +import com.sap.cds.services.ServiceException; import com.sap.cds.services.mt.UnsubscribeEventContext; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import org.junit.jupiter.api.Test; class TenantCleanupHandlerTest { @@ -59,4 +62,31 @@ void testCleanupTenantDataHandlesRuntimeException() throws Exception { verify(mockOsClient).deleteContentByPrefix("tenant3/"); } + + @Test + void testCleanupNullTenantThrowsServiceException() { + OSClient mockOsClient = mock(OSClient.class); + UnsubscribeEventContext context = mock(UnsubscribeEventContext.class); + when(context.getTenant()).thenReturn(null); + + TenantCleanupHandler handler = new TenantCleanupHandler(mockOsClient); + assertThrows(ServiceException.class, () -> handler.cleanupTenantData(context)); + } + + @Test + void testCleanupHandlesExecutionException() throws Exception { + OSClient mockOsClient = mock(OSClient.class); + UnsubscribeEventContext context = mock(UnsubscribeEventContext.class); + when(context.getTenant()).thenReturn("tenant4"); + + @SuppressWarnings("unchecked") + CompletableFuture future = mock(CompletableFuture.class); + when(mockOsClient.deleteContentByPrefix("tenant4/")).thenReturn(future); + when(future.get()).thenThrow(new ExecutionException("fail", new RuntimeException("cause"))); + + TenantCleanupHandler handler = new TenantCleanupHandler(mockOsClient); + handler.cleanupTenantData(context); + + verify(mockOsClient).deleteContentByPrefix("tenant4/"); + } } From b3293354780d0243dd8a0341624769ec44279df7 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Tue, 31 Mar 2026 13:00:00 +0200 Subject: [PATCH 15/18] update readme --- .../cds-feature-attachments-oss/README.md | 20 ++++++++++++++++++- .../handler/OSSAttachmentsServiceHandler.java | 6 ++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/storage-targets/cds-feature-attachments-oss/README.md b/storage-targets/cds-feature-attachments-oss/README.md index 78b6e9716..2db6f168e 100644 --- a/storage-targets/cds-feature-attachments-oss/README.md +++ b/storage-targets/cds-feature-attachments-oss/README.md @@ -97,4 +97,22 @@ This artifact provides custom handlers for events from the [AttachmentService](. ### Multitenancy -Multitenancy is not directly supported. All attachments are stored in a flat structure within the provided bucket, which might be shared across tenants. +The plugin supports multi-tenancy scenarios with shared object store instances. + +#### Shared Object Store Instance + +To configure a shared object store instance, set the object store kind to `shared`: + +```yaml +cds: + attachments: + objectStore: + kind: shared +``` + +To ensure tenant isolation when using a shared object store instance, the plugin prefixes object keys with the tenant ID. When a tenant unsubscribes, all objects prefixed with that tenant's ID are automatically cleaned up from the shared bucket. + +#### Separate Object Store Instances + +> [!NOTE] +> Separate object store instances per tenant are not yet supported. This feature is planned for a future release. diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java index 3a51830ef..892238344 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java @@ -139,7 +139,8 @@ void createAttachment(AttachmentCreateEventContext context) { @On void markAttachmentAsDeleted(AttachmentMarkAsDeletedEventContext context) { logger.info( - "OS Attachment Service handler called for marking attachment as deleted with document id {}", + "OS Attachment Service handler called for marking attachment as deleted with document id" + + " {}", context.getContentId()); try { @@ -235,7 +236,8 @@ private static void validateContentId(String contentId) { || contentId.contains("\\") || contentId.contains("..")) { throw new ServiceException( - "Invalid content ID for attachment storage: must not be empty or contain path separators"); + "Invalid content ID for attachment storage: must not be empty or contain path" + + " separators"); } } } From 9f9fb848acafb53fba7e02e2848f19852c6964f9 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Tue, 31 Mar 2026 13:51:26 +0200 Subject: [PATCH 16/18] reduce diff --- .../oss/configuration/RegistrationTest.java | 120 +- .../OSSAttachmentsServiceHandlerTest.java | 1120 ++++++----------- .../oss/handler/TenantCleanupHandlerTest.java | 41 +- 3 files changed, 467 insertions(+), 814 deletions(-) diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java index 5c8c59498..951740ea3 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java @@ -19,60 +19,52 @@ import java.util.Map; import java.util.Optional; import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class RegistrationTest { - @Test - void testEventHandlersRegistersOSSHandler() { - // Arrange - Registration registration = new Registration(); - CdsRuntimeConfigurer configurer = mock(CdsRuntimeConfigurer.class); - CdsRuntime cdsRuntime = mock(CdsRuntime.class); - CdsEnvironment environment = mock(CdsEnvironment.class); - ServiceBinding binding = mock(ServiceBinding.class); + private Registration registration; + private CdsRuntimeConfigurer configurer; + private CdsEnvironment environment; + private ServiceBinding awsBinding; - // Setup valid AWS credentials for the binding + private static ServiceBinding createAwsBinding() { + ServiceBinding binding = mock(ServiceBinding.class); Map credentials = new HashMap<>(); credentials.put("host", "aws.example.com"); credentials.put("region", "us-east-1"); credentials.put("access_key_id", "test-access-key"); credentials.put("secret_access_key", "test-secret-key"); credentials.put("bucket", "test-bucket"); + when(binding.getServiceName()).thenReturn(Optional.of("objectstore")); + when(binding.getCredentials()).thenReturn(credentials); + return binding; + } + @BeforeEach + void setup() { + registration = new Registration(); + configurer = mock(CdsRuntimeConfigurer.class); + CdsRuntime cdsRuntime = mock(CdsRuntime.class); + environment = mock(CdsEnvironment.class); when(configurer.getCdsRuntime()).thenReturn(cdsRuntime); when(cdsRuntime.getEnvironment()).thenReturn(environment); - when(binding.getServiceName()).thenReturn(Optional.of("objectstore")); - when(binding.getCredentials()).thenReturn(credentials); - when(environment.getServiceBindings()).thenReturn(Stream.of(binding)); + awsBinding = createAwsBinding(); + } + + @Test + void testEventHandlersRegistersOSSHandler() { + when(environment.getServiceBindings()).thenReturn(Stream.of(awsBinding)); - // Act registration.eventHandlers(configurer); - // Assert: OSSAttachmentsServiceHandler should be registered verify(configurer).eventHandler(any(OSSAttachmentsServiceHandler.class)); } @Test void testEventHandlersRegistersCleanupHandlerWhenMultitenancyShared() { - Registration registration = new Registration(); - CdsRuntimeConfigurer configurer = mock(CdsRuntimeConfigurer.class); - CdsRuntime cdsRuntime = mock(CdsRuntime.class); - CdsEnvironment environment = mock(CdsEnvironment.class); - ServiceBinding binding = mock(ServiceBinding.class); - - Map credentials = new HashMap<>(); - credentials.put("host", "aws.example.com"); - credentials.put("region", "us-east-1"); - credentials.put("access_key_id", "test-access-key"); - credentials.put("secret_access_key", "test-secret-key"); - credentials.put("bucket", "test-bucket"); - - when(configurer.getCdsRuntime()).thenReturn(cdsRuntime); - when(cdsRuntime.getEnvironment()).thenReturn(environment); - when(binding.getServiceName()).thenReturn(Optional.of("objectstore")); - when(binding.getCredentials()).thenReturn(credentials); - when(environment.getServiceBindings()).thenReturn(Stream.of(binding)); + when(environment.getServiceBindings()).thenReturn(Stream.of(awsBinding)); when(environment.getProperty("cds.multitenancy.enabled", Boolean.class, Boolean.FALSE)) .thenReturn(Boolean.TRUE); when(environment.getProperty("cds.attachments.objectStore.kind", String.class, null)) @@ -85,13 +77,6 @@ void testEventHandlersRegistersCleanupHandlerWhenMultitenancyShared() { @Test void testEventHandlersNoBindingDoesNotRegister() { - Registration registration = new Registration(); - CdsRuntimeConfigurer configurer = mock(CdsRuntimeConfigurer.class); - CdsRuntime cdsRuntime = mock(CdsRuntime.class); - CdsEnvironment environment = mock(CdsEnvironment.class); - - when(configurer.getCdsRuntime()).thenReturn(cdsRuntime); - when(cdsRuntime.getEnvironment()).thenReturn(environment); when(environment.getServiceBindings()).thenReturn(Stream.empty()); registration.eventHandlers(configurer); @@ -101,24 +86,7 @@ void testEventHandlersNoBindingDoesNotRegister() { @Test void testMtEnabledNonSharedKindRegistersOnlyOSSHandler() { - Registration registration = new Registration(); - CdsRuntimeConfigurer configurer = mock(CdsRuntimeConfigurer.class); - CdsRuntime cdsRuntime = mock(CdsRuntime.class); - CdsEnvironment environment = mock(CdsEnvironment.class); - ServiceBinding binding = mock(ServiceBinding.class); - - Map credentials = new HashMap<>(); - credentials.put("host", "aws.example.com"); - credentials.put("region", "us-east-1"); - credentials.put("access_key_id", "test-access-key"); - credentials.put("secret_access_key", "test-secret-key"); - credentials.put("bucket", "test-bucket"); - - when(configurer.getCdsRuntime()).thenReturn(cdsRuntime); - when(cdsRuntime.getEnvironment()).thenReturn(environment); - when(binding.getServiceName()).thenReturn(Optional.of("objectstore")); - when(binding.getCredentials()).thenReturn(credentials); - when(environment.getServiceBindings()).thenReturn(Stream.of(binding)); + when(environment.getServiceBindings()).thenReturn(Stream.of(awsBinding)); when(environment.getProperty("cds.multitenancy.enabled", Boolean.class, Boolean.FALSE)) .thenReturn(Boolean.TRUE); when(environment.getProperty("cds.attachments.objectStore.kind", String.class, null)) @@ -132,24 +100,7 @@ void testMtEnabledNonSharedKindRegistersOnlyOSSHandler() { @Test void testMtEnabledNullKindRegistersOnlyOSSHandler() { - Registration registration = new Registration(); - CdsRuntimeConfigurer configurer = mock(CdsRuntimeConfigurer.class); - CdsRuntime cdsRuntime = mock(CdsRuntime.class); - CdsEnvironment environment = mock(CdsEnvironment.class); - ServiceBinding binding = mock(ServiceBinding.class); - - Map credentials = new HashMap<>(); - credentials.put("host", "aws.example.com"); - credentials.put("region", "us-east-1"); - credentials.put("access_key_id", "test-access-key"); - credentials.put("secret_access_key", "test-secret-key"); - credentials.put("bucket", "test-bucket"); - - when(configurer.getCdsRuntime()).thenReturn(cdsRuntime); - when(cdsRuntime.getEnvironment()).thenReturn(environment); - when(binding.getServiceName()).thenReturn(Optional.of("objectstore")); - when(binding.getCredentials()).thenReturn(credentials); - when(environment.getServiceBindings()).thenReturn(Stream.of(binding)); + when(environment.getServiceBindings()).thenReturn(Stream.of(awsBinding)); when(environment.getProperty("cds.multitenancy.enabled", Boolean.class, Boolean.FALSE)) .thenReturn(Boolean.TRUE); @@ -161,24 +112,7 @@ void testMtEnabledNullKindRegistersOnlyOSSHandler() { @Test void testMtDisabledSharedKindRegistersOnlyOSSHandler() { - Registration registration = new Registration(); - CdsRuntimeConfigurer configurer = mock(CdsRuntimeConfigurer.class); - CdsRuntime cdsRuntime = mock(CdsRuntime.class); - CdsEnvironment environment = mock(CdsEnvironment.class); - ServiceBinding binding = mock(ServiceBinding.class); - - Map credentials = new HashMap<>(); - credentials.put("host", "aws.example.com"); - credentials.put("region", "us-east-1"); - credentials.put("access_key_id", "test-access-key"); - credentials.put("secret_access_key", "test-secret-key"); - credentials.put("bucket", "test-bucket"); - - when(configurer.getCdsRuntime()).thenReturn(cdsRuntime); - when(cdsRuntime.getEnvironment()).thenReturn(environment); - when(binding.getServiceName()).thenReturn(Optional.of("objectstore")); - when(binding.getCredentials()).thenReturn(credentials); - when(environment.getServiceBindings()).thenReturn(Stream.of(binding)); + when(environment.getServiceBindings()).thenReturn(Stream.of(awsBinding)); when(environment.getProperty("cds.attachments.objectStore.kind", String.class, null)) .thenReturn("shared"); diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java index 538b023a2..90ff570da 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java @@ -3,15 +3,18 @@ */ package com.sap.cds.feature.attachments.oss.handler; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.Attachments; import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.MediaData; +import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.StatusCode; import com.sap.cds.feature.attachments.oss.client.OSClient; import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentCreateEventContext; import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentMarkAsDeletedEventContext; @@ -19,24 +22,32 @@ import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentRestoreEventContext; import com.sap.cds.reflect.CdsEntity; import com.sap.cds.services.ServiceException; +import com.sap.cds.services.request.ModifiableUserInfo; import com.sap.cds.services.request.UserInfo; import com.sap.cloud.environment.servicebinding.api.ServiceBinding; import java.io.ByteArrayInputStream; import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.time.Instant; import java.util.Base64; import java.util.HashMap; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; class OSSAttachmentsServiceHandlerTest { - ExecutorService executor = Executors.newCachedThreadPool(); - @Test - void testRestoreAttachmentCallsSetCompleted() { - // Setup a valid AWS binding for the test + private static final ExecutorService executor = Executors.newCachedThreadPool(); + + private OSClient mockOsClient; + private OSSAttachmentsServiceHandler handler; + + private static ServiceBinding createAwsBinding() { ServiceBinding binding = mock(ServiceBinding.class); HashMap creds = new HashMap<>(); creds.put("host", "aws.example.com"); @@ -45,739 +56,446 @@ void testRestoreAttachmentCallsSetCompleted() { creds.put("secret_access_key", "test-secret-key"); creds.put("bucket", "test-bucket"); when(binding.getCredentials()).thenReturn(creds); - - OSSAttachmentsServiceHandler handler = - new OSSAttachmentsServiceHandler(binding, executor, false, null); - AttachmentRestoreEventContext context = mock(AttachmentRestoreEventContext.class); - handler.restoreAttachment(context); - verify(context).setCompleted(); + return binding; } - @Test - void testCreateAttachmentCallsOsClientUploadContent() + private static void injectOsClient(OSSAttachmentsServiceHandler handler, OSClient client) throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - // Mock the handler, but call the real method readAttachment - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); - var field = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); field.setAccessible(true); - field.set(handler, mockOsClient); - - String contentId = "doc123"; - String mimeType = "text/plain"; - String fileName = "file.txt"; - - MediaData mockMediaData = mock(MediaData.class); - var mockEntity = mock(com.sap.cds.reflect.CdsEntity.class); - when(mockEntity.getQualifiedName()).thenReturn(fileName); - - InputStream contentStream = new ByteArrayInputStream("test".getBytes()); - - when(context.getAttachmentEntity()).thenReturn(mockEntity); - when(context.getAttachmentIds()).thenReturn(java.util.Map.of("ID", contentId)); - when(context.getData()).thenReturn(mockMediaData); - when(mockMediaData.getContent()).thenReturn(contentStream); - when(mockMediaData.getMimeType()).thenReturn(mimeType); - when(mockOsClient.uploadContent(any(), anyString(), anyString())) - .thenReturn(CompletableFuture.completedFuture(null)); - - when(context.getContentId()).thenReturn(contentId); - - handler.createAttachment(context); - - verify(mockOsClient).uploadContent(contentStream, contentId, mimeType); - verify(context).setIsInternalStored(false); - verify(context).setContentId(contentId); - verify(context).setCompleted(); + field.set(handler, client); } - @Test - void testReadAttachmentCallsOsClientReadContent() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - // Mock the handler, but call the real method readAttachment - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); - - var field = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - field.setAccessible(true); - field.set(handler, mockOsClient); - - String contentId = "doc123"; - MediaData mockMediaData = mock(MediaData.class); - - when(context.getContentId()).thenReturn(contentId); - when(context.getData()).thenReturn(mockMediaData); - when(mockOsClient.readContent(contentId)) - .thenReturn(CompletableFuture.completedFuture(new ByteArrayInputStream("test".getBytes()))); - - handler.readAttachment(context); - - verify(mockOsClient).readContent(contentId); - verify(mockMediaData).setContent(any(InputStream.class)); - verify(context).setCompleted(); + private static CdsEntity stubEntity(String name) { + CdsEntity entity = mock(CdsEntity.class); + when(entity.getQualifiedName()).thenReturn(name); + return entity; } - @Test - void testReadAttachmentCallsOsClientReadNullContent() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - // Mock the handler, but call the real method readAttachment - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); - - var field = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - field.setAccessible(true); - field.set(handler, mockOsClient); - - String contentId = "doc123"; - MediaData mockMediaData = mock(MediaData.class); - - when(context.getContentId()).thenReturn(contentId); - when(context.getData()).thenReturn(mockMediaData); - when(mockOsClient.readContent(contentId)).thenReturn(CompletableFuture.completedFuture(null)); - - assertThrows(ServiceException.class, () -> handler.readAttachment(context)); - - verify(mockOsClient).readContent(contentId); - verify(context).setCompleted(); + /** + * Creates a real {@link AttachmentCreateEventContext} populated with the given values. + * The only mock used is CdsEntity (a model-level concept not creatable without a full model). + */ + private static AttachmentCreateEventContext createContext( + String contentId, String mimeType, String fileName, byte[] content) { + var ctx = AttachmentCreateEventContext.create(); + ctx.setData(MediaData.create()); + ctx.getData().setContent(new ByteArrayInputStream(content)); + ctx.getData().setMimeType(mimeType); + ctx.getData().setFileName(fileName); + ctx.setAttachmentIds(Map.of(Attachments.ID, contentId)); + ctx.setAttachmentEntity(stubEntity("TestEntity")); + return ctx; } - @Test - void testMarkAttachmentAsDeletedCallsOsClientDeleteContent() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - // Mock the handler, but call the real method readAttachment - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - AttachmentMarkAsDeletedEventContext context = mock(AttachmentMarkAsDeletedEventContext.class); - - var field = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - field.setAccessible(true); - field.set(handler, mockOsClient); - - String contentId = "doc123"; - when(context.getContentId()).thenReturn(contentId); - when(mockOsClient.deleteContent(contentId)).thenReturn(CompletableFuture.completedFuture(null)); - - handler.markAttachmentAsDeleted(context); - - verify(mockOsClient).deleteContent(contentId); - verify(context).setCompleted(); + private static UserInfo userInfoWithTenant(String tenant) { + ModifiableUserInfo userInfo = UserInfo.create(); + userInfo.setTenant(tenant); + return userInfo; } - @Test - void testConstructorHandlesInvalidBase64EncodedPrivateKeyData() { - // Arrange: ServiceBinding with invalid base64EncodedPrivateKeyData (not valid base64) - ServiceBinding binding = mock(ServiceBinding.class); - HashMap creds = new HashMap<>(); - creds.put("base64EncodedPrivateKeyData", "not-a-valid-base64-string"); - when(binding.getCredentials()).thenReturn(creds); - - assertThrows( - ObjectStoreServiceException.class, - () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); + @Nested + class ConstructorTests { + + @Test + void testConstructorHandlesInvalidBase64EncodedPrivateKeyData() { + ServiceBinding binding = mock(ServiceBinding.class); + HashMap creds = new HashMap<>(); + creds.put("base64EncodedPrivateKeyData", "not-a-valid-base64-string"); + when(binding.getCredentials()).thenReturn(creds); + + assertThrows( + ObjectStoreServiceException.class, + () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); + } + + @Test + void testConstructorHandlesValidBase64ButNoGoogleOrGcp() { + String plain = "this is just a dummy string without keywords"; + String base64 = Base64.getEncoder().encodeToString(plain.getBytes(StandardCharsets.UTF_8)); + + ServiceBinding binding = mock(ServiceBinding.class); + HashMap creds = new HashMap<>(); + creds.put("base64EncodedPrivateKeyData", base64); + when(binding.getCredentials()).thenReturn(creds); + + assertThrows( + ObjectStoreServiceException.class, + () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); + } + + @Test + void testConstructorHandlesInValidBase64() { + ServiceBinding binding = mock(ServiceBinding.class); + HashMap creds = new HashMap<>(); + creds.put("base64EncodedPrivateKeyData", "this is just a dummy string without keywords"); + when(binding.getCredentials()).thenReturn(creds); + + assertThrows( + ObjectStoreServiceException.class, + () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); + } + + @Test + void testConstructorHandlesNoValidObjectStoreService() { + ServiceBinding binding = mock(ServiceBinding.class); + HashMap creds = new HashMap<>(); + creds.put("someOtherField", "someValue"); + when(binding.getCredentials()).thenReturn(creds); + + assertThrows( + ObjectStoreServiceException.class, + () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); + } } - @Test - void testConstructorHandlesValidBase64ButNoGoogleOrGcp() { - String plain = "this is just a dummy string without keywords"; - String base64 = - Base64.getEncoder().encodeToString(plain.getBytes(java.nio.charset.StandardCharsets.UTF_8)); + @Nested + class SingleTenantOperations { - ServiceBinding binding = mock(ServiceBinding.class); - HashMap creds = new HashMap<>(); - creds.put("base64EncodedPrivateKeyData", base64); - when(binding.getCredentials()).thenReturn(creds); + @BeforeEach + void setup() throws NoSuchFieldException, IllegalAccessException { + handler = new OSSAttachmentsServiceHandler(createAwsBinding(), executor, false, null); + mockOsClient = mock(OSClient.class); + injectOsClient(handler, mockOsClient); + } - assertThrows( - ObjectStoreServiceException.class, - () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); - } + @Test + void testRestoreAttachmentCallsSetCompleted() { + var context = AttachmentRestoreEventContext.create(); + context.setRestoreTimestamp(Instant.now()); - @Test - void testConstructorHandlesInValidBase64() { - ServiceBinding binding = mock(ServiceBinding.class); - HashMap creds = new HashMap<>(); - creds.put("base64EncodedPrivateKeyData", "this is just a dummy string without keywords"); - when(binding.getCredentials()).thenReturn(creds); + handler.restoreAttachment(context); - assertThrows( - ObjectStoreServiceException.class, - () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); - } + assertThat(context.isCompleted()).isTrue(); + } - @Test - void testConstructorHandlesNoValidObjectStoreService() { - // Arrange: ServiceBinding with no valid object store credentials - ServiceBinding binding = mock(ServiceBinding.class); - HashMap creds = new HashMap<>(); - // No host, container_uri, or base64EncodedPrivateKeyData - creds.put("someOtherField", "someValue"); - when(binding.getCredentials()).thenReturn(creds); + @Test + void testCreateAttachmentUploadsContent() { + when(mockOsClient.uploadContent(any(), anyString(), anyString())) + .thenReturn(CompletableFuture.completedFuture(null)); - assertThrows( - ObjectStoreServiceException.class, - () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); - } + var context = createContext("doc123", "text/plain", "file.txt", "test".getBytes()); - // Helper method to setup common mocks for createAttachment exception tests - private AttachmentCreateEventContext setupCreateAttachmentContext( - OSClient mockOsClient, OSSAttachmentsServiceHandler handler, Exception exceptionToThrow) - throws NoSuchFieldException, - IllegalAccessException, - InterruptedException, - ExecutionException { + handler.createAttachment(context); - var field = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - field.setAccessible(true); - field.set(handler, mockOsClient); - - AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); - MediaData mockMediaData = mock(MediaData.class); - CdsEntity mockEntity = mock(CdsEntity.class); - HashMap attachmentIds = new HashMap<>(); - attachmentIds.put("ID", "test-id"); - - when(context.getAttachmentIds()).thenReturn(attachmentIds); - when(context.getData()).thenReturn(mockMediaData); - when(context.getAttachmentEntity()).thenReturn(mockEntity); - when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); - when(mockMediaData.getFileName()).thenReturn("test.txt"); - when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("test".getBytes())); - when(mockMediaData.getMimeType()).thenReturn("text/plain"); - - @SuppressWarnings("unchecked") - CompletableFuture future = mock(CompletableFuture.class); - when(mockOsClient.uploadContent(any(InputStream.class), anyString(), anyString())) - .thenReturn(future); - when(future.get()).thenThrow(exceptionToThrow); - - return context; - } + verify(mockOsClient).uploadContent(any(InputStream.class), eq("doc123"), eq("text/plain")); + assertThat(context.getIsInternalStored()).isFalse(); + assertThat(context.getContentId()).isEqualTo("doc123"); + assertThat(context.getData().getStatus()).isEqualTo(StatusCode.SCANNING); + assertThat(context.isCompleted()).isTrue(); + } - @Test - void testCreateAttachmentExceptionHandling() - throws NoSuchFieldException, - IllegalAccessException, - InterruptedException, - ExecutionException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - - // Test InterruptedException - AttachmentCreateEventContext context1 = - setupCreateAttachmentContext( - mockOsClient, handler, new InterruptedException("Thread interrupted")); - assertThrows(ServiceException.class, () -> handler.createAttachment(context1)); - verify(context1).setCompleted(); - - // Test ObjectStoreServiceException - AttachmentCreateEventContext context2 = - setupCreateAttachmentContext( - mockOsClient, handler, new ObjectStoreServiceException("Upload failed")); - assertThrows(ServiceException.class, () -> handler.createAttachment(context2)); - verify(context2).setCompleted(); - - // Test ExecutionException - AttachmentCreateEventContext context3 = - setupCreateAttachmentContext( - mockOsClient, handler, new ExecutionException("Upload failed", new RuntimeException())); - assertThrows(ServiceException.class, () -> handler.createAttachment(context3)); - verify(context3).setCompleted(); - } - - // Helper method to setup common mocks for markAttachmentAsDeleted exception tests - private AttachmentMarkAsDeletedEventContext setupMarkAsDeletedContext( - OSClient mockOsClient, OSSAttachmentsServiceHandler handler, Exception exceptionToThrow) - throws NoSuchFieldException, - IllegalAccessException, - InterruptedException, - ExecutionException { - - var field = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - field.setAccessible(true); - field.set(handler, mockOsClient); - - AttachmentMarkAsDeletedEventContext context = mock(AttachmentMarkAsDeletedEventContext.class); - String contentId = "test-content-id"; - - when(context.getContentId()).thenReturn(contentId); - - @SuppressWarnings("unchecked") - CompletableFuture future = mock(CompletableFuture.class); - when(mockOsClient.deleteContent(contentId)).thenReturn(future); - when(future.get()).thenThrow(exceptionToThrow); - - return context; - } - - @Test - void testMarkAttachmentAsDeletedExceptionHandling() - throws NoSuchFieldException, - IllegalAccessException, - InterruptedException, - ExecutionException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - - // Test InterruptedException - AttachmentMarkAsDeletedEventContext context1 = - setupMarkAsDeletedContext( - mockOsClient, handler, new InterruptedException("Thread interrupted")); - assertThrows(ServiceException.class, () -> handler.markAttachmentAsDeleted(context1)); - verify(context1).setCompleted(); - - // Test ObjectStoreServiceException - AttachmentMarkAsDeletedEventContext context2 = - setupMarkAsDeletedContext( - mockOsClient, handler, new ObjectStoreServiceException("Delete failed")); - assertThrows(ServiceException.class, () -> handler.markAttachmentAsDeleted(context2)); - verify(context2).setCompleted(); - - // Test ExecutionException - AttachmentMarkAsDeletedEventContext context3 = - setupMarkAsDeletedContext( - mockOsClient, handler, new ExecutionException("Delete failed", new RuntimeException())); - assertThrows(ServiceException.class, () -> handler.markAttachmentAsDeleted(context3)); - verify(context3).setCompleted(); - } - - @Test - void testReadAttachmentHandlesInterruptedException() - throws NoSuchFieldException, - IllegalAccessException, - InterruptedException, - ExecutionException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); - - var field = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - field.setAccessible(true); - field.set(handler, mockOsClient); - - String contentId = "doc123"; - MediaData mockMediaData = mock(MediaData.class); - - when(context.getContentId()).thenReturn(contentId); - when(context.getData()).thenReturn(mockMediaData); - - @SuppressWarnings("unchecked") - CompletableFuture future = mock(CompletableFuture.class); - when(mockOsClient.readContent(contentId)).thenReturn(future); - when(future.get()).thenThrow(new InterruptedException("Thread interrupted")); - - assertThrows(ServiceException.class, () -> handler.readAttachment(context)); - verify(context).setCompleted(); - } - - @Test - void testCreateAttachmentWithMultitenancyBuildsObjectKey() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - - var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - osClientField.setAccessible(true); - osClientField.set(handler, mockOsClient); - var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); - mtField.setAccessible(true); - mtField.set(handler, true); - var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); - kindField.setAccessible(true); - kindField.set(handler, "shared"); - - AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); - MediaData mockMediaData = mock(MediaData.class); - CdsEntity mockEntity = mock(CdsEntity.class); - UserInfo userInfo = mock(UserInfo.class); - - when(context.getAttachmentEntity()).thenReturn(mockEntity); - when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); - when(context.getAttachmentIds()).thenReturn(java.util.Map.of("ID", "content123")); - when(context.getData()).thenReturn(mockMediaData); - when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("test".getBytes())); - when(mockMediaData.getMimeType()).thenReturn("text/plain"); - when(mockMediaData.getFileName()).thenReturn("file.txt"); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.getTenant()).thenReturn("myTenant"); - - when(mockOsClient.uploadContent(any(), anyString(), anyString())) - .thenReturn(CompletableFuture.completedFuture(null)); - - handler.createAttachment(context); - - // Verify the object key includes tenant prefix - verify(mockOsClient) - .uploadContent(any(), org.mockito.ArgumentMatchers.eq("myTenant/content123"), anyString()); - } - - @Test - void testMultitenancyWithNullTenantThrows() throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - - var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - osClientField.setAccessible(true); - osClientField.set(handler, mockOsClient); - var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); - mtField.setAccessible(true); - mtField.set(handler, true); - var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); - kindField.setAccessible(true); - kindField.set(handler, "shared"); - - AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); - UserInfo userInfo = mock(UserInfo.class); - - when(context.getContentId()).thenReturn("content123"); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.getTenant()).thenReturn(null); - - assertThrows(ServiceException.class, () -> handler.readAttachment(context)); - } + @Test + void testReadAttachmentReadsContent() { + when(mockOsClient.readContent("doc123")) + .thenReturn( + CompletableFuture.completedFuture(new ByteArrayInputStream("test".getBytes()))); - @Test - void testValidateTenantIdWithSlashThrows() throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - - var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - osClientField.setAccessible(true); - osClientField.set(handler, mockOsClient); - var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); - mtField.setAccessible(true); - mtField.set(handler, true); - var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); - kindField.setAccessible(true); - kindField.set(handler, "shared"); - - AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); - UserInfo userInfo = mock(UserInfo.class); - - when(context.getContentId()).thenReturn("content123"); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.getTenant()).thenReturn("tenant/evil"); - - assertThrows(ServiceException.class, () -> handler.readAttachment(context)); - } - - @Test - void testValidateTenantIdWithBackslashThrows() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - - var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - osClientField.setAccessible(true); - osClientField.set(handler, mockOsClient); - var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); - mtField.setAccessible(true); - mtField.set(handler, true); - var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); - kindField.setAccessible(true); - kindField.set(handler, "shared"); - - AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); - UserInfo userInfo = mock(UserInfo.class); - - when(context.getContentId()).thenReturn("content123"); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.getTenant()).thenReturn("tenant\\evil"); - - assertThrows(ServiceException.class, () -> handler.readAttachment(context)); - } + var context = AttachmentReadEventContext.create(); + context.setContentId("doc123"); + context.setData(MediaData.create()); - @Test - void testValidateTenantIdWithDotsThrows() throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - - var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - osClientField.setAccessible(true); - osClientField.set(handler, mockOsClient); - var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); - mtField.setAccessible(true); - mtField.set(handler, true); - var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); - kindField.setAccessible(true); - kindField.set(handler, "shared"); - - AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); - UserInfo userInfo = mock(UserInfo.class); - - when(context.getContentId()).thenReturn("content123"); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.getTenant()).thenReturn("..evil"); - - assertThrows(ServiceException.class, () -> handler.readAttachment(context)); - } - - @Test - void testValidateEmptyTenantIdThrows() throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - - var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - osClientField.setAccessible(true); - osClientField.set(handler, mockOsClient); - var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); - mtField.setAccessible(true); - mtField.set(handler, true); - var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); - kindField.setAccessible(true); - kindField.set(handler, "shared"); - - // Need to mock tenant as empty string but not null (null triggers different path) - AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); - UserInfo userInfo = mock(UserInfo.class); - - when(context.getContentId()).thenReturn("content123"); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.getTenant()).thenReturn(""); - - assertThrows(ServiceException.class, () -> handler.readAttachment(context)); - } - - @Test - void testValidateContentIdWithSlashThrows() throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - - var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - osClientField.setAccessible(true); - osClientField.set(handler, mockOsClient); - var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); - mtField.setAccessible(true); - mtField.set(handler, true); - var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); - kindField.setAccessible(true); - kindField.set(handler, "shared"); - - AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); - UserInfo userInfo = mock(UserInfo.class); - - when(context.getContentId()).thenReturn("content/evil"); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.getTenant()).thenReturn("validTenant"); - - assertThrows(ServiceException.class, () -> handler.readAttachment(context)); - } - - @Test - void testValidateContentIdWithNullThrows() throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - - var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - osClientField.setAccessible(true); - osClientField.set(handler, mockOsClient); - var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); - mtField.setAccessible(true); - mtField.set(handler, true); - var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); - kindField.setAccessible(true); - kindField.set(handler, "shared"); - - AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); - UserInfo userInfo = mock(UserInfo.class); - - when(context.getContentId()).thenReturn(null); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.getTenant()).thenReturn("validTenant"); - - assertThrows(ServiceException.class, () -> handler.readAttachment(context)); - } - - @Test - void testValidateContentIdWithBackslashThrows() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - - var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - osClientField.setAccessible(true); - osClientField.set(handler, mockOsClient); - var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); - mtField.setAccessible(true); - mtField.set(handler, true); - var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); - kindField.setAccessible(true); - kindField.set(handler, "shared"); - - AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); - UserInfo userInfo = mock(UserInfo.class); - - when(context.getContentId()).thenReturn("content\\evil"); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.getTenant()).thenReturn("validTenant"); - - assertThrows(ServiceException.class, () -> handler.readAttachment(context)); - } + handler.readAttachment(context); - @Test - void testValidateContentIdWithDotsThrows() throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - - var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - osClientField.setAccessible(true); - osClientField.set(handler, mockOsClient); - var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); - mtField.setAccessible(true); - mtField.set(handler, true); - var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); - kindField.setAccessible(true); - kindField.set(handler, "shared"); - - AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); - UserInfo userInfo = mock(UserInfo.class); - - when(context.getContentId()).thenReturn("..evil"); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.getTenant()).thenReturn("validTenant"); - - assertThrows(ServiceException.class, () -> handler.readAttachment(context)); - } + verify(mockOsClient).readContent("doc123"); + assertThat(context.getData().getContent()).isNotNull(); + assertThat(context.isCompleted()).isTrue(); + } - @Test - void testValidateEmptyContentIdThrows() throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - - var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - osClientField.setAccessible(true); - osClientField.set(handler, mockOsClient); - var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); - mtField.setAccessible(true); - mtField.set(handler, true); - var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); - kindField.setAccessible(true); - kindField.set(handler, "shared"); - - AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); - UserInfo userInfo = mock(UserInfo.class); - - when(context.getContentId()).thenReturn(""); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.getTenant()).thenReturn("validTenant"); - - assertThrows(ServiceException.class, () -> handler.readAttachment(context)); - } + @Test + void testReadAttachmentWithNullContentThrows() { + when(mockOsClient.readContent("doc123")) + .thenReturn(CompletableFuture.completedFuture(null)); - @Test - void testReadAttachmentHandlesExecutionException() - throws NoSuchFieldException, - IllegalAccessException, - InterruptedException, - ExecutionException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + var context = AttachmentReadEventContext.create(); + context.setContentId("doc123"); + context.setData(MediaData.create()); - var field = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - field.setAccessible(true); - field.set(handler, mockOsClient); + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + assertThat(context.isCompleted()).isTrue(); + } - String contentId = "doc123"; - MediaData mockMediaData = mock(MediaData.class); + @Test + void testMarkAttachmentAsDeletedDeletesContent() { + when(mockOsClient.deleteContent("doc123")) + .thenReturn(CompletableFuture.completedFuture(null)); - when(context.getContentId()).thenReturn(contentId); - when(context.getData()).thenReturn(mockMediaData); + var context = AttachmentMarkAsDeletedEventContext.create(); + context.setContentId("doc123"); - @SuppressWarnings("unchecked") - CompletableFuture future = mock(CompletableFuture.class); - when(mockOsClient.readContent(contentId)).thenReturn(future); - when(future.get()).thenThrow(new ExecutionException("failed", new RuntimeException())); + handler.markAttachmentAsDeleted(context); - assertThrows(ServiceException.class, () -> handler.readAttachment(context)); - verify(context).setCompleted(); + verify(mockOsClient).deleteContent("doc123"); + assertThat(context.isCompleted()).isTrue(); + } } - @Test - void testReadAttachmentWithMultitenancyBuildsObjectKey() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - - var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - osClientField.setAccessible(true); - osClientField.set(handler, mockOsClient); - var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); - mtField.setAccessible(true); - mtField.set(handler, true); - var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); - kindField.setAccessible(true); - kindField.set(handler, "shared"); - - AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); - MediaData mockMediaData = mock(MediaData.class); - UserInfo userInfo = mock(UserInfo.class); - - when(context.getContentId()).thenReturn("content123"); - when(context.getData()).thenReturn(mockMediaData); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.getTenant()).thenReturn("myTenant"); - - when(mockOsClient.readContent("myTenant/content123")) - .thenReturn(CompletableFuture.completedFuture(new ByteArrayInputStream("test".getBytes()))); - - handler.readAttachment(context); - - verify(mockOsClient).readContent("myTenant/content123"); + @Nested + class ExceptionHandling { + + @BeforeEach + void setup() throws NoSuchFieldException, IllegalAccessException { + handler = new OSSAttachmentsServiceHandler(createAwsBinding(), executor, false, null); + mockOsClient = mock(OSClient.class); + injectOsClient(handler, mockOsClient); + } + + @Test + void testCreateAttachmentHandlesInterruptedException() throws Exception { + var context = createContextForUploadException(new InterruptedException("Thread interrupted")); + assertThrows(ServiceException.class, () -> handler.createAttachment(context)); + assertThat(context.isCompleted()).isTrue(); + } + + @Test + void testCreateAttachmentHandlesObjectStoreServiceException() throws Exception { + var context = + createContextForUploadException(new ObjectStoreServiceException("Upload failed")); + assertThrows(ServiceException.class, () -> handler.createAttachment(context)); + assertThat(context.isCompleted()).isTrue(); + } + + @Test + void testCreateAttachmentHandlesExecutionException() throws Exception { + var context = + createContextForUploadException( + new ExecutionException("Upload failed", new RuntimeException())); + assertThrows(ServiceException.class, () -> handler.createAttachment(context)); + assertThat(context.isCompleted()).isTrue(); + } + + @Test + void testMarkAsDeletedHandlesInterruptedException() throws Exception { + var context = + createContextForDeleteException(new InterruptedException("Thread interrupted")); + assertThrows(ServiceException.class, () -> handler.markAttachmentAsDeleted(context)); + assertThat(context.isCompleted()).isTrue(); + } + + @Test + void testMarkAsDeletedHandlesObjectStoreServiceException() throws Exception { + var context = + createContextForDeleteException(new ObjectStoreServiceException("Delete failed")); + assertThrows(ServiceException.class, () -> handler.markAttachmentAsDeleted(context)); + assertThat(context.isCompleted()).isTrue(); + } + + @Test + void testMarkAsDeletedHandlesExecutionException() throws Exception { + var context = + createContextForDeleteException( + new ExecutionException("Delete failed", new RuntimeException())); + assertThrows(ServiceException.class, () -> handler.markAttachmentAsDeleted(context)); + assertThat(context.isCompleted()).isTrue(); + } + + @Test + void testReadAttachmentHandlesInterruptedException() throws Exception { + var context = createContextForReadException(new InterruptedException("Thread interrupted")); + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + assertThat(context.isCompleted()).isTrue(); + } + + @Test + void testReadAttachmentHandlesExecutionException() throws Exception { + var context = + createContextForReadException( + new ExecutionException("failed", new RuntimeException())); + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + assertThat(context.isCompleted()).isTrue(); + } + + private AttachmentCreateEventContext createContextForUploadException(Exception exception) + throws Exception { + @SuppressWarnings("unchecked") + CompletableFuture future = mock(CompletableFuture.class); + when(mockOsClient.uploadContent(any(InputStream.class), anyString(), anyString())) + .thenReturn(future); + when(future.get()).thenThrow(exception); + + return createContext("test-id", "text/plain", "test.txt", "test".getBytes()); + } + + private AttachmentMarkAsDeletedEventContext createContextForDeleteException(Exception exception) + throws Exception { + @SuppressWarnings("unchecked") + CompletableFuture future = mock(CompletableFuture.class); + when(mockOsClient.deleteContent("test-content-id")).thenReturn(future); + when(future.get()).thenThrow(exception); + + var context = AttachmentMarkAsDeletedEventContext.create(); + context.setContentId("test-content-id"); + return context; + } + + private AttachmentReadEventContext createContextForReadException(Exception exception) + throws Exception { + @SuppressWarnings("unchecked") + CompletableFuture future = mock(CompletableFuture.class); + when(mockOsClient.readContent("doc123")).thenReturn(future); + when(future.get()).thenThrow(exception); + + var context = AttachmentReadEventContext.create(); + context.setContentId("doc123"); + context.setData(MediaData.create()); + return context; + } } - @Test - void testMarkAsDeletedWithMultitenancyBuildsObjectKey() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - - var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - osClientField.setAccessible(true); - osClientField.set(handler, mockOsClient); - var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); - mtField.setAccessible(true); - mtField.set(handler, true); - var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); - kindField.setAccessible(true); - kindField.set(handler, "shared"); - - AttachmentMarkAsDeletedEventContext context = mock(AttachmentMarkAsDeletedEventContext.class); - UserInfo userInfo = mock(UserInfo.class); - - when(context.getContentId()).thenReturn("content123"); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.getTenant()).thenReturn("myTenant"); - - when(mockOsClient.deleteContent("myTenant/content123")) - .thenReturn(CompletableFuture.completedFuture(null)); - - handler.markAttachmentAsDeleted(context); - - verify(mockOsClient).deleteContent("myTenant/content123"); + @Nested + class MultitenancyTests { + + @BeforeEach + void setup() throws NoSuchFieldException, IllegalAccessException { + handler = new OSSAttachmentsServiceHandler(createAwsBinding(), executor, true, "shared"); + mockOsClient = mock(OSClient.class); + injectOsClient(handler, mockOsClient); + } + + @Test + void testCreateAttachmentWithMultitenancyBuildsObjectKey() { + when(mockOsClient.uploadContent(any(), anyString(), anyString())) + .thenReturn(CompletableFuture.completedFuture(null)); + + // For multitenancy, getUserInfo() requires a RequestContext, so we mock + // the event context to provide tenant info + CdsEntity entity = stubEntity("TestEntity"); + UserInfo userInfo = userInfoWithTenant("myTenant"); + MediaData data = MediaData.create(); + data.setContent(new ByteArrayInputStream("test".getBytes())); + data.setMimeType("text/plain"); + data.setFileName("file.txt"); + + AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); + when(context.getAttachmentEntity()).thenReturn(entity); + when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, "content123")); + when(context.getData()).thenReturn(data); + when(context.getUserInfo()).thenReturn(userInfo); + + handler.createAttachment(context); + + verify(mockOsClient).uploadContent(any(), eq("myTenant/content123"), anyString()); + } + + @Test + void testReadAttachmentWithMultitenancyBuildsObjectKey() { + when(mockOsClient.readContent("myTenant/content123")) + .thenReturn( + CompletableFuture.completedFuture(new ByteArrayInputStream("test".getBytes()))); + + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + when(context.getContentId()).thenReturn("content123"); + when(context.getData()).thenReturn(MediaData.create()); + when(context.getUserInfo()).thenReturn(userInfoWithTenant("myTenant")); + + handler.readAttachment(context); + + verify(mockOsClient).readContent("myTenant/content123"); + } + + @Test + void testMarkAsDeletedWithMultitenancyBuildsObjectKey() { + when(mockOsClient.deleteContent("myTenant/content123")) + .thenReturn(CompletableFuture.completedFuture(null)); + + AttachmentMarkAsDeletedEventContext context = + mock(AttachmentMarkAsDeletedEventContext.class); + when(context.getContentId()).thenReturn("content123"); + when(context.getUserInfo()).thenReturn(userInfoWithTenant("myTenant")); + + handler.markAttachmentAsDeleted(context); + + verify(mockOsClient).deleteContent("myTenant/content123"); + } + + @Test + void testMultitenancyWithNullTenantThrows() { + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + when(context.getContentId()).thenReturn("content123"); + when(context.getUserInfo()).thenReturn(userInfoWithTenant(null)); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testValidateTenantIdWithSlashThrows() { + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + when(context.getContentId()).thenReturn("content123"); + when(context.getUserInfo()).thenReturn(userInfoWithTenant("tenant/evil")); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testValidateTenantIdWithBackslashThrows() { + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + when(context.getContentId()).thenReturn("content123"); + when(context.getUserInfo()).thenReturn(userInfoWithTenant("tenant\\evil")); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testValidateTenantIdWithDotsThrows() { + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + when(context.getContentId()).thenReturn("content123"); + when(context.getUserInfo()).thenReturn(userInfoWithTenant("..evil")); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testValidateEmptyTenantIdThrows() { + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + when(context.getContentId()).thenReturn("content123"); + when(context.getUserInfo()).thenReturn(userInfoWithTenant("")); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testValidateContentIdWithSlashThrows() { + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + when(context.getContentId()).thenReturn("content/evil"); + when(context.getUserInfo()).thenReturn(userInfoWithTenant("validTenant")); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testValidateContentIdWithNullThrows() { + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + when(context.getContentId()).thenReturn(null); + when(context.getUserInfo()).thenReturn(userInfoWithTenant("validTenant")); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testValidateContentIdWithBackslashThrows() { + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + when(context.getContentId()).thenReturn("content\\evil"); + when(context.getUserInfo()).thenReturn(userInfoWithTenant("validTenant")); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testValidateContentIdWithDotsThrows() { + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + when(context.getContentId()).thenReturn("..evil"); + when(context.getUserInfo()).thenReturn(userInfoWithTenant("validTenant")); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testValidateEmptyContentIdThrows() { + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + when(context.getContentId()).thenReturn(""); + when(context.getUserInfo()).thenReturn(userInfoWithTenant("validTenant")); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } } } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java index e9a0d3d0f..818d3f86d 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java @@ -13,19 +13,28 @@ import com.sap.cds.services.mt.UnsubscribeEventContext; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class TenantCleanupHandlerTest { + private OSClient mockOsClient; + private TenantCleanupHandler handler; + + @BeforeEach + void setup() { + mockOsClient = mock(OSClient.class); + handler = new TenantCleanupHandler(mockOsClient); + } + @Test void testCleanupTenantDataCallsDeleteByPrefix() throws Exception { - OSClient mockOsClient = mock(OSClient.class); - UnsubscribeEventContext context = mock(UnsubscribeEventContext.class); - when(context.getTenant()).thenReturn("tenant1"); + var context = UnsubscribeEventContext.create(); + context.setTenant("tenant1"); + when(mockOsClient.deleteContentByPrefix("tenant1/")) .thenReturn(CompletableFuture.completedFuture(null)); - TenantCleanupHandler handler = new TenantCleanupHandler(mockOsClient); handler.cleanupTenantData(context); verify(mockOsClient).deleteContentByPrefix("tenant1/"); @@ -33,16 +42,14 @@ void testCleanupTenantDataCallsDeleteByPrefix() throws Exception { @Test void testCleanupTenantDataHandlesInterruptedException() throws Exception { - OSClient mockOsClient = mock(OSClient.class); - UnsubscribeEventContext context = mock(UnsubscribeEventContext.class); - when(context.getTenant()).thenReturn("tenant2"); + var context = UnsubscribeEventContext.create(); + context.setTenant("tenant2"); @SuppressWarnings("unchecked") CompletableFuture future = mock(CompletableFuture.class); when(mockOsClient.deleteContentByPrefix("tenant2/")).thenReturn(future); when(future.get()).thenThrow(new InterruptedException("interrupted")); - TenantCleanupHandler handler = new TenantCleanupHandler(mockOsClient); handler.cleanupTenantData(context); verify(mockOsClient).deleteContentByPrefix("tenant2/"); @@ -50,14 +57,12 @@ void testCleanupTenantDataHandlesInterruptedException() throws Exception { @Test void testCleanupTenantDataHandlesRuntimeException() throws Exception { - OSClient mockOsClient = mock(OSClient.class); - UnsubscribeEventContext context = mock(UnsubscribeEventContext.class); - when(context.getTenant()).thenReturn("tenant3"); + var context = UnsubscribeEventContext.create(); + context.setTenant("tenant3"); when(mockOsClient.deleteContentByPrefix("tenant3/")) .thenReturn(CompletableFuture.failedFuture(new RuntimeException("fail"))); - TenantCleanupHandler handler = new TenantCleanupHandler(mockOsClient); handler.cleanupTenantData(context); verify(mockOsClient).deleteContentByPrefix("tenant3/"); @@ -65,26 +70,22 @@ void testCleanupTenantDataHandlesRuntimeException() throws Exception { @Test void testCleanupNullTenantThrowsServiceException() { - OSClient mockOsClient = mock(OSClient.class); - UnsubscribeEventContext context = mock(UnsubscribeEventContext.class); - when(context.getTenant()).thenReturn(null); + var context = UnsubscribeEventContext.create(); + // tenant is null by default - TenantCleanupHandler handler = new TenantCleanupHandler(mockOsClient); assertThrows(ServiceException.class, () -> handler.cleanupTenantData(context)); } @Test void testCleanupHandlesExecutionException() throws Exception { - OSClient mockOsClient = mock(OSClient.class); - UnsubscribeEventContext context = mock(UnsubscribeEventContext.class); - when(context.getTenant()).thenReturn("tenant4"); + var context = UnsubscribeEventContext.create(); + context.setTenant("tenant4"); @SuppressWarnings("unchecked") CompletableFuture future = mock(CompletableFuture.class); when(mockOsClient.deleteContentByPrefix("tenant4/")).thenReturn(future); when(future.get()).thenThrow(new ExecutionException("fail", new RuntimeException("cause"))); - TenantCleanupHandler handler = new TenantCleanupHandler(mockOsClient); handler.cleanupTenantData(context); verify(mockOsClient).deleteContentByPrefix("tenant4/"); From cd4ba8b2c210a9a24036e4bf0087dacc6ac3979d Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Tue, 31 Mar 2026 14:00:22 +0200 Subject: [PATCH 17/18] test improvements --- ...stPluginAttachmentsServiceHandlerTest.java | 9 +- .../attachments/oss/client/AWSClient.java | 8 + .../attachments/oss/client/AzureClient.java | 5 + .../attachments/oss/client/GoogleClient.java | 6 + .../attachments/oss/client/AWSClientTest.java | 107 +++-------- .../oss/client/AzureClientTest.java | 138 +++----------- .../oss/client/GoogleClientTest.java | 179 +++--------------- .../OSSAttachmentsServiceHandlerTest.java | 16 +- 8 files changed, 99 insertions(+), 369 deletions(-) diff --git a/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java index bc391fe9b..26b418ef7 100644 --- a/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java +++ b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java @@ -6,8 +6,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.Mockito.*; - import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.MediaData; import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.StatusCode; import com.sap.cds.feature.attachments.service.AttachmentService; @@ -17,7 +15,6 @@ import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentRestoreEventContext; import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.List; @@ -75,12 +72,10 @@ void dummyTestForDelete() { } @Test - void dummyTestForCreate() throws IOException { + void dummyTestForCreate() { var context = AttachmentCreateEventContext.create(); context.setData(MediaData.create()); - var stream = mock(InputStream.class); - when(stream.readAllBytes()).thenReturn("test".getBytes(StandardCharsets.UTF_8)); - context.getData().setContent(stream); + context.getData().setContent(new ByteArrayInputStream("test".getBytes(StandardCharsets.UTF_8))); assertDoesNotThrow(() -> cut.createAttachment(context)); } diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java index 513a2b738..ec2ed5e7b 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java @@ -63,6 +63,14 @@ public AWSClient(ServiceBinding binding, ExecutorService executor) { logger.info("Initialized AWS S3 client"); } + AWSClient( + S3Client s3Client, S3AsyncClient s3AsyncClient, String bucketName, ExecutorService executor) { + this.s3Client = s3Client; + this.s3AsyncClient = s3AsyncClient; + this.bucketName = bucketName; + this.executor = executor; + } + @Override public Future uploadContent( InputStream content, String completeFileName, String contentType) { diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AzureClient.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AzureClient.java index 4f930ac08..d83c64ee1 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AzureClient.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AzureClient.java @@ -38,6 +38,11 @@ public AzureClient(ServiceBinding binding, ExecutorService executor) { logger.info("Initialized Azure Blob Storage client"); } + AzureClient(BlobContainerClient blobContainerClient, ExecutorService executor) { + this.blobContainerClient = blobContainerClient; + this.executor = executor; + } + @Override public Future uploadContent( InputStream content, String completeFileName, String contentType) { diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/GoogleClient.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/GoogleClient.java index d41a6767a..ece8e91cb 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/GoogleClient.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/GoogleClient.java @@ -54,6 +54,12 @@ public GoogleClient(ServiceBinding binding, ExecutorService executor) { logger.info("Initialized client for Google Cloud Storage with binding: {}", binding); } + GoogleClient(Storage storage, String bucketName, ExecutorService executor) { + this.storage = storage; + this.bucketName = bucketName; + this.executor = executor; + } + @Override public Future uploadContent( InputStream content, String completeFileName, String contentType) { diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java index b535ce53b..b3b55dc39 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java @@ -59,10 +59,9 @@ void testConstructorWithAwsBindingUsesAwsClient() @Test void testReadContent() throws Exception { - AWSClient awsClient = new AWSClient(getDummyBinding(), executor); - - // Mock S3Client to return a dummy InputStream S3Client mockS3Client = mock(S3Client.class); + AWSClient awsClient = new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); + ByteArrayInputStream mockInputStream = new ByteArrayInputStream("test-data".getBytes()); GetObjectResponse mockResponse = mock(GetObjectResponse.class); ResponseInputStream mockResponseInputStream = @@ -70,20 +69,15 @@ void testReadContent() throws Exception { when(mockS3Client.getObject(any(GetObjectRequest.class))).thenReturn(mockResponseInputStream); - var field = AWSClient.class.getDeclaredField("s3Client"); - field.setAccessible(true); - field.set(awsClient, mockS3Client); - InputStream result = awsClient.readContent("test.txt").get(); assertNotNull(result); } @Test void testUploadContent() throws Exception { - AWSClient awsClient = new AWSClient(getDummyBinding(), executor); - - // Mock S3AsyncClient to return a successful PutObjectResponse S3AsyncClient mockAsyncClient = mock(S3AsyncClient.class); + AWSClient awsClient = new AWSClient(mock(S3Client.class), mockAsyncClient, "bucket", executor); + PutObjectResponse mockPutRes = mock(PutObjectResponse.class); SdkHttpResponse mockHttpRes = mock(SdkHttpResponse.class); when(mockHttpRes.isSuccessful()).thenReturn(true); @@ -93,50 +87,33 @@ void testUploadContent() throws Exception { when(mockAsyncClient.putObject(any(PutObjectRequest.class), any(AsyncRequestBody.class))) .thenReturn(successFuture); - var field = AWSClient.class.getDeclaredField("s3AsyncClient"); - field.setAccessible(true); - field.set(awsClient, mockAsyncClient); - - // Should not throw - awsClient .uploadContent(new ByteArrayInputStream("test".getBytes()), "test.txt", "text/plain") .get(); } @Test - void testDeleteContent() throws NoSuchFieldException, IllegalAccessException { - AWSClient awsClient = new AWSClient(getDummyBinding(), executor); - - // Mock S3Client to return a DeleteObjectResponse with successful SdkHttpResponse + void testDeleteContent() { S3Client mockS3Client = mock(S3Client.class); + AWSClient awsClient = new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); + DeleteObjectResponse mockDelRes = mock(DeleteObjectResponse.class); SdkHttpResponse mockHttpRes = mock(SdkHttpResponse.class); when(mockHttpRes.isSuccessful()).thenReturn(true); when(mockDelRes.sdkHttpResponse()).thenReturn(mockHttpRes); when(mockS3Client.deleteObject(any(DeleteObjectRequest.class))).thenReturn(mockDelRes); - var field = AWSClient.class.getDeclaredField("s3Client"); - field.setAccessible(true); - field.set(awsClient, mockS3Client); - assertDoesNotThrow(() -> awsClient.deleteContent("test.txt").get()); } @Test void testReadContentThrows() throws Exception { - AWSClient awsClient = new AWSClient(getDummyBinding(), executor); - - // Mock S3Client to return a dummy InputStream S3Client mockS3Client = mock(S3Client.class); + AWSClient awsClient = new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); when(mockS3Client.getObject(any(GetObjectRequest.class))) .thenThrow(new RuntimeException("Simulated S3 failure")); - var field = AWSClient.class.getDeclaredField("s3Client"); - field.setAccessible(true); - field.set(awsClient, mockS3Client); - ExecutionException thrown = assertThrows(ExecutionException.class, () -> awsClient.readContent("test.txt").get()); assertInstanceOf(ObjectStoreServiceException.class, thrown.getCause()); @@ -144,19 +121,14 @@ void testReadContentThrows() throws Exception { @Test void testUploadContentThrows() throws Exception { - AWSClient awsClient = new AWSClient(getDummyBinding(), executor); - - // Mock S3AsyncClient that always fails S3AsyncClient mockAsyncClient = mock(S3AsyncClient.class); + AWSClient awsClient = new AWSClient(mock(S3Client.class), mockAsyncClient, "bucket", executor); + CompletableFuture failedFuture = new CompletableFuture<>(); failedFuture.completeExceptionally(new RuntimeException("Simulated S3 failure")); when(mockAsyncClient.putObject(any(PutObjectRequest.class), any(AsyncRequestBody.class))) .thenReturn((CompletableFuture) failedFuture); - var field = AWSClient.class.getDeclaredField("s3AsyncClient"); - field.setAccessible(true); - field.set(awsClient, mockAsyncClient); - ExecutionException thrown = assertThrows( ExecutionException.class, @@ -171,18 +143,13 @@ void testUploadContentThrows() throws Exception { @Test void testUploadContentThrowsOnPutResponseNull() throws Exception { - AWSClient awsClient = new AWSClient(getDummyBinding(), executor); - - // Mock S3AsyncClient that returns a null PutObjectResponse S3AsyncClient mockAsyncClient = mock(S3AsyncClient.class); + AWSClient awsClient = new AWSClient(mock(S3Client.class), mockAsyncClient, "bucket", executor); + CompletableFuture nullFuture = CompletableFuture.completedFuture(null); when(mockAsyncClient.putObject(any(PutObjectRequest.class), any(AsyncRequestBody.class))) .thenReturn(nullFuture); - var field = AWSClient.class.getDeclaredField("s3AsyncClient"); - field.setAccessible(true); - field.set(awsClient, mockAsyncClient); - ExecutionException thrown = assertThrows( ExecutionException.class, @@ -197,39 +164,28 @@ void testUploadContentThrowsOnPutResponseNull() throws Exception { @Test void testDeleteContentThrowsOnRuntimeException() throws Exception { - AWSClient awsClient = new AWSClient(getDummyBinding(), executor); - - // Mock S3Client to throw a RuntimeException S3Client mockS3Client = mock(S3Client.class); + AWSClient awsClient = new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); + when(mockS3Client.deleteObject(any(DeleteObjectRequest.class))) .thenThrow(new RuntimeException("Simulated S3 delete failure")); - var field = AWSClient.class.getDeclaredField("s3Client"); - field.setAccessible(true); - field.set(awsClient, mockS3Client); - ExecutionException thrown = assertThrows(ExecutionException.class, () -> awsClient.deleteContent("test.txt").get()); assertInstanceOf(ObjectStoreServiceException.class, thrown.getCause()); } @Test - void testDeleteContentThrowsOnUnsuccessfulResponse() - throws NoSuchFieldException, IllegalAccessException { - AWSClient awsClient = new AWSClient(getDummyBinding(), executor); - - // Mock S3Client to return a DeleteObjectResponse with unsuccessful SdkHttpResponse + void testDeleteContentThrowsOnUnsuccessfulResponse() { S3Client mockS3Client = mock(S3Client.class); + AWSClient awsClient = new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); + DeleteObjectResponse mockDelRes = mock(DeleteObjectResponse.class); SdkHttpResponse mockHttpRes = mock(SdkHttpResponse.class); when(mockHttpRes.isSuccessful()).thenReturn(false); when(mockDelRes.sdkHttpResponse()).thenReturn(mockHttpRes); when(mockS3Client.deleteObject(any(DeleteObjectRequest.class))).thenReturn(mockDelRes); - var field = AWSClient.class.getDeclaredField("s3Client"); - field.setAccessible(true); - field.set(awsClient, mockS3Client); - ExecutionException thrown = assertThrows(ExecutionException.class, () -> awsClient.deleteContent("test.txt").get()); assertInstanceOf(ObjectStoreServiceException.class, thrown.getCause()); @@ -237,9 +193,8 @@ void testDeleteContentThrowsOnUnsuccessfulResponse() @Test void testDeleteContentByPrefix() throws Exception { - AWSClient awsClient = new AWSClient(getDummyBinding(), executor); - S3Client mockS3Client = mock(S3Client.class); + AWSClient awsClient = new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); S3Object obj1 = S3Object.builder().key("prefix/file1.txt").build(); S3Object obj2 = S3Object.builder().key("prefix/file2.txt").build(); @@ -254,10 +209,6 @@ void testDeleteContentByPrefix() throws Exception { when(deleteResponse.errors()).thenReturn(Collections.emptyList()); when(mockS3Client.deleteObjects(any(DeleteObjectsRequest.class))).thenReturn(deleteResponse); - var field = AWSClient.class.getDeclaredField("s3Client"); - field.setAccessible(true); - field.set(awsClient, mockS3Client); - awsClient.deleteContentByPrefix("prefix/").get(); verify(mockS3Client).deleteObjects(any(DeleteObjectsRequest.class)); @@ -265,34 +216,25 @@ void testDeleteContentByPrefix() throws Exception { @Test void testDeleteContentByPrefixEmptyList() throws Exception { - AWSClient awsClient = new AWSClient(getDummyBinding(), executor); - S3Client mockS3Client = mock(S3Client.class); + AWSClient awsClient = new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); ListObjectsV2Response listResponse = mock(ListObjectsV2Response.class); when(listResponse.contents()).thenReturn(Collections.emptyList()); when(listResponse.isTruncated()).thenReturn(false); when(mockS3Client.listObjectsV2(any(ListObjectsV2Request.class))).thenReturn(listResponse); - var field = AWSClient.class.getDeclaredField("s3Client"); - field.setAccessible(true); - field.set(awsClient, mockS3Client); - assertDoesNotThrow(() -> awsClient.deleteContentByPrefix("prefix/").get()); } @Test void testDeleteContentByPrefixThrowsOnRuntimeException() throws Exception { - AWSClient awsClient = new AWSClient(getDummyBinding(), executor); - S3Client mockS3Client = mock(S3Client.class); + AWSClient awsClient = new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); + when(mockS3Client.listObjectsV2(any(ListObjectsV2Request.class))) .thenThrow(new RuntimeException("Simulated failure")); - var field = AWSClient.class.getDeclaredField("s3Client"); - field.setAccessible(true); - field.set(awsClient, mockS3Client); - ExecutionException thrown = assertThrows( ExecutionException.class, () -> awsClient.deleteContentByPrefix("prefix/").get()); @@ -301,9 +243,8 @@ void testDeleteContentByPrefixThrowsOnRuntimeException() throws Exception { @Test void testDeleteContentByPrefixWithPagination() throws Exception { - AWSClient awsClient = new AWSClient(getDummyBinding(), executor); - S3Client mockS3Client = mock(S3Client.class); + AWSClient awsClient = new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); // First page: 2 objects, isTruncated=true S3Object obj1 = S3Object.builder().key("prefix/file1.txt").build(); @@ -336,10 +277,6 @@ void testDeleteContentByPrefixWithPagination() throws Exception { when(deleteResponse.errors()).thenReturn(Collections.emptyList()); when(mockS3Client.deleteObjects(any(DeleteObjectsRequest.class))).thenReturn(deleteResponse); - var field = AWSClient.class.getDeclaredField("s3Client"); - field.setAccessible(true); - field.set(awsClient, mockS3Client); - awsClient.deleteContentByPrefix("prefix/").get(); // deleteObjects should be called twice — once per page diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java index 7f6fe6de2..f1c55570d 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java @@ -27,25 +27,11 @@ class AzureClientTest { ExecutorService executor = Executors.newCachedThreadPool(); @Test - void testReadContent() - throws NoSuchFieldException, - SecurityException, - IllegalArgumentException, - IllegalAccessException, - InterruptedException, - ExecutionException { - AzureClient azureClient = mock(AzureClient.class, CALLS_REAL_METHODS); - - // Mock BlobContainerClient and BlobClient + void testReadContent() throws InterruptedException, ExecutionException { BlobContainerClient mockContainer = mock(BlobContainerClient.class); BlobClient mockBlobClient = mock(BlobClient.class); + AzureClient azureClient = new AzureClient(mockContainer, executor); - var field = AzureClient.class.getDeclaredField("blobContainerClient"); - field.setAccessible(true); - field.set(azureClient, mockContainer); - var executorField = AzureClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(azureClient, executor); when(mockContainer.getBlobClient(anyString())).thenReturn(mockBlobClient); // Should not throw @@ -53,26 +39,15 @@ void testReadContent() } @Test - void testUploadContent() - throws NoSuchFieldException, - IllegalAccessException, - InterruptedException, - ExecutionException { - AzureClient azureClient = mock(AzureClient.class, CALLS_REAL_METHODS); - - // Mock BlobContainerClient and BlockBlobClient + void testUploadContent() throws InterruptedException, ExecutionException { BlobContainerClient mockContainer = mock(BlobContainerClient.class); + BlobClient mockBlobClient = mock(BlobClient.class); BlockBlobClient mockBlockBlob = mock(BlockBlobClient.class); BlobOutputStream mockOutputStream = mock(BlobOutputStream.class); + AzureClient azureClient = new AzureClient(mockContainer, executor); - var field = AzureClient.class.getDeclaredField("blobContainerClient"); - field.setAccessible(true); - field.set(azureClient, mockContainer); - var executorField = AzureClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(azureClient, executor); - when(mockContainer.getBlobClient(anyString())).thenReturn(mock(BlobClient.class)); - when(mockContainer.getBlobClient(anyString()).getBlockBlobClient()).thenReturn(mockBlockBlob); + when(mockContainer.getBlobClient(anyString())).thenReturn(mockBlobClient); + when(mockBlobClient.getBlockBlobClient()).thenReturn(mockBlockBlob); when(mockBlockBlob.getBlobOutputStream()).thenReturn(mockOutputStream); InputStream mockInput = new java.io.ByteArrayInputStream("test-data".getBytes()); @@ -82,23 +57,15 @@ void testUploadContent() } @Test - void testUploadContentThrowsOnIOException() - throws NoSuchFieldException, IllegalAccessException, IOException { - AzureClient azureClient = mock(AzureClient.class, CALLS_REAL_METHODS); - - // Mock BlobContainerClient and BlockBlobClient + void testUploadContentThrowsOnIOException() throws IOException { BlobContainerClient mockContainer = mock(BlobContainerClient.class); + BlobClient mockBlobClient = mock(BlobClient.class); BlockBlobClient mockBlockBlob = mock(BlockBlobClient.class); BlobOutputStream mockOutputStream = mock(BlobOutputStream.class); + AzureClient azureClient = new AzureClient(mockContainer, executor); - var field = AzureClient.class.getDeclaredField("blobContainerClient"); - field.setAccessible(true); - field.set(azureClient, mockContainer); - var executorField = AzureClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(azureClient, executor); - when(mockContainer.getBlobClient(anyString())).thenReturn(mock(BlobClient.class)); - when(mockContainer.getBlobClient(anyString()).getBlockBlobClient()).thenReturn(mockBlockBlob); + when(mockContainer.getBlobClient(anyString())).thenReturn(mockBlobClient); + when(mockBlobClient.getBlockBlobClient()).thenReturn(mockBlockBlob); when(mockBlockBlob.getBlobOutputStream()).thenReturn(mockOutputStream); // Mock InputStream to throw IOException @@ -113,23 +80,11 @@ void testUploadContentThrowsOnIOException() } @Test - void testDeleteContentThrowsOnRuntimeException() - throws NoSuchFieldException, - SecurityException, - IllegalArgumentException, - IllegalAccessException { - AzureClient azureClient = mock(AzureClient.class, CALLS_REAL_METHODS); - - // Mock BlobContainerClient and BlobClient + void testDeleteContentThrowsOnRuntimeException() { BlobContainerClient mockContainer = mock(BlobContainerClient.class); BlobClient mockBlobClient = mock(BlobClient.class); + AzureClient azureClient = new AzureClient(mockContainer, executor); - var field = AzureClient.class.getDeclaredField("blobContainerClient"); - field.setAccessible(true); - field.set(azureClient, mockContainer); - var executorField = AzureClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(azureClient, executor); when(mockContainer.getBlobClient(anyString())).thenReturn(mockBlobClient); // Mock delete to throw RuntimeException @@ -141,25 +96,11 @@ void testDeleteContentThrowsOnRuntimeException() } @Test - void testDeleteContent() - throws NoSuchFieldException, - SecurityException, - IllegalArgumentException, - IllegalAccessException, - InterruptedException, - ExecutionException { - AzureClient azureClient = mock(AzureClient.class, CALLS_REAL_METHODS); - - // Mock BlobContainerClient and BlobClient + void testDeleteContent() throws InterruptedException, ExecutionException { BlobContainerClient mockContainer = mock(BlobContainerClient.class); BlobClient mockBlobClient = mock(BlobClient.class); + AzureClient azureClient = new AzureClient(mockContainer, executor); - var field = AzureClient.class.getDeclaredField("blobContainerClient"); - field.setAccessible(true); - field.set(azureClient, mockContainer); - var executorField = AzureClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(azureClient, executor); when(mockContainer.getBlobClient(anyString())).thenReturn(mockBlobClient); // Should not throw @@ -167,26 +108,14 @@ void testDeleteContent() } @Test - void testReadContentThrowsOnRuntimeException() - throws NoSuchFieldException, - SecurityException, - IllegalArgumentException, - IllegalAccessException { - AzureClient azureClient = mock(AzureClient.class, CALLS_REAL_METHODS); - - // Mock BlobContainerClient and BlobClient + void testReadContentThrowsOnRuntimeException() { BlobContainerClient mockContainer = mock(BlobContainerClient.class); BlobClient mockBlobClient = mock(BlobClient.class); + AzureClient azureClient = new AzureClient(mockContainer, executor); - var field = AzureClient.class.getDeclaredField("blobContainerClient"); - field.setAccessible(true); - field.set(azureClient, mockContainer); - var executorField = AzureClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(azureClient, executor); when(mockContainer.getBlobClient(anyString())).thenReturn(mockBlobClient); - // Mock delete to throw RuntimeException + // Mock openInputStream to throw RuntimeException doThrow(new RuntimeException("Simulated read failure")).when(mockBlobClient).openInputStream(); ExecutionException thrown = @@ -195,22 +124,10 @@ void testReadContentThrowsOnRuntimeException() } @Test - void testDeleteContentByPrefix() - throws NoSuchFieldException, - IllegalAccessException, - InterruptedException, - ExecutionException { - AzureClient azureClient = mock(AzureClient.class, CALLS_REAL_METHODS); - + void testDeleteContentByPrefix() throws InterruptedException, ExecutionException { BlobContainerClient mockContainer = mock(BlobContainerClient.class); BlobClient mockBlobClient = mock(BlobClient.class); - - var field = AzureClient.class.getDeclaredField("blobContainerClient"); - field.setAccessible(true); - field.set(azureClient, mockContainer); - var executorField = AzureClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(azureClient, executor); + AzureClient azureClient = new AzureClient(mockContainer, executor); BlobItem item1 = mock(BlobItem.class); when(item1.getName()).thenReturn("prefix/file1.txt"); @@ -229,18 +146,9 @@ void testDeleteContentByPrefix() } @Test - void testDeleteContentByPrefixThrowsOnRuntimeException() - throws NoSuchFieldException, IllegalAccessException { - AzureClient azureClient = mock(AzureClient.class, CALLS_REAL_METHODS); - + void testDeleteContentByPrefixThrowsOnRuntimeException() { BlobContainerClient mockContainer = mock(BlobContainerClient.class); - - var field = AzureClient.class.getDeclaredField("blobContainerClient"); - field.setAccessible(true); - field.set(azureClient, mockContainer); - var executorField = AzureClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(azureClient, executor); + AzureClient azureClient = new AzureClient(mockContainer, executor); when(mockContainer.listBlobs(any(ListBlobsOptions.class), isNull())) .thenThrow(new RuntimeException("Simulated failure")); diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java index 51147491d..c61af0a4f 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java @@ -54,18 +54,12 @@ void testConstructorThrowsOnInvalidCredentials() { } @Test - void testDeleteContent() - throws NoSuchFieldException, - IllegalArgumentException, - IllegalAccessException, - InterruptedException, - ExecutionException { - GoogleClient googleClient = mock(GoogleClient.class, CALLS_REAL_METHODS); + void testDeleteContent() throws InterruptedException, ExecutionException { + Storage mockStorage = mock(Storage.class); + GoogleClient googleClient = new GoogleClient(mockStorage, "my-bucket", executor); String fileName = "file.txt"; - // Mock storage and paging - Storage mockStorage = mock(Storage.class); Page mockPage = mock(Page.class); Blob mockBlob = mock(Blob.class); when(mockBlob.getName()).thenReturn(fileName); @@ -75,48 +69,19 @@ void testDeleteContent() when(mockStorage.list(anyString(), any(), any())).thenReturn(mockPage); when(mockStorage.delete(any(BlobId.class))).thenReturn(true); - // Inject mock storage and bucketName into googleClient using reflection - var field = GoogleClient.class.getDeclaredField("storage"); - field.setAccessible(true); - field.set(googleClient, mockStorage); - var executorField = GoogleClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(googleClient, executor); - var bucketField = GoogleClient.class.getDeclaredField("bucketName"); - bucketField.setAccessible(true); - bucketField.set(googleClient, "my-bucket"); - // Should not throw googleClient.deleteContent(fileName).get(); } @Test - void testUploadContent() - throws NoSuchFieldException, - IllegalArgumentException, - IllegalAccessException, - InterruptedException, - ExecutionException, - IOException { - GoogleClient googleClient = mock(GoogleClient.class, CALLS_REAL_METHODS); - - // Mock storage and writer + void testUploadContent() throws InterruptedException, ExecutionException, IOException { Storage mockStorage = mock(Storage.class); - WriteChannel mockWriter = mock(WriteChannel.class); + GoogleClient googleClient = new GoogleClient(mockStorage, "my-bucket", executor); - // Inject mock storage and bucketName into googleClient using reflection - var field = GoogleClient.class.getDeclaredField("storage"); - field.setAccessible(true); - field.set(googleClient, mockStorage); - var executorField = GoogleClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(googleClient, executor); - var bucketField = GoogleClient.class.getDeclaredField("bucketName"); - bucketField.setAccessible(true); - bucketField.set(googleClient, "my-bucket"); + WriteChannel mockWriter = mock(WriteChannel.class); when(mockStorage.writer(any(BlobInfo.class))).thenReturn(mockWriter); - when(mockWriter.write(any(java.nio.ByteBuffer.class))).thenReturn(42); // return any int + when(mockWriter.write(any(java.nio.ByteBuffer.class))).thenReturn(42); InputStream input = new java.io.ByteArrayInputStream("test".getBytes()); // Should not throw @@ -124,44 +89,25 @@ void testUploadContent() } @Test - void testReadContent() - throws NoSuchFieldException, - IllegalArgumentException, - IllegalAccessException, - InterruptedException, - ExecutionException { - GoogleClient googleClient = mock(GoogleClient.class, CALLS_REAL_METHODS); - - // Mock storage and read channel + void testReadContent() throws InterruptedException, ExecutionException { Storage mockStorage = mock(Storage.class); + GoogleClient googleClient = new GoogleClient(mockStorage, "my-bucket", executor); + ReadChannel mockReadChannel = mock(ReadChannel.class); when(mockStorage.reader(any(com.google.cloud.storage.BlobId.class))) .thenReturn(mockReadChannel); - // Inject mock storage and bucketName into googleClient using reflection - var field = GoogleClient.class.getDeclaredField("storage"); - field.setAccessible(true); - field.set(googleClient, mockStorage); - var executorField = GoogleClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(googleClient, executor); - var bucketField = GoogleClient.class.getDeclaredField("bucketName"); - bucketField.setAccessible(true); - bucketField.set(googleClient, "my-bucket"); - // Should not throw googleClient.readContent("file.txt").get(); } @Test - void testDeleteContentDoesNotWork() - throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException { - GoogleClient googleClient = mock(GoogleClient.class, CALLS_REAL_METHODS); + void testDeleteContentDoesNotWork() { + Storage mockStorage = mock(Storage.class); + GoogleClient googleClient = new GoogleClient(mockStorage, "my-bucket", executor); String fileName = "file.txt"; - // Mock storage and paging - Storage mockStorage = mock(Storage.class); Page mockPage = mock(Page.class); Blob mockBlob = mock(Blob.class); when(mockBlob.getName()).thenReturn(fileName); @@ -171,41 +117,17 @@ void testDeleteContentDoesNotWork() when(mockStorage.list(anyString(), any(), any())).thenReturn(mockPage); when(mockStorage.delete(any(BlobId.class))).thenReturn(false); - // Inject mock storage and bucketName into googleClient using reflection - var field = GoogleClient.class.getDeclaredField("storage"); - field.setAccessible(true); - field.set(googleClient, mockStorage); - var executorField = GoogleClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(googleClient, executor); - var bucketField = GoogleClient.class.getDeclaredField("bucketName"); - bucketField.setAccessible(true); - bucketField.set(googleClient, "my-bucket"); - ExecutionException thrown = assertThrows(ExecutionException.class, () -> googleClient.deleteContent(fileName).get()); assertInstanceOf(ObjectStoreServiceException.class, thrown.getCause()); } @Test - void testUploadContentThrowsOnIOException() - throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException, IOException { - GoogleClient googleClient = mock(GoogleClient.class, CALLS_REAL_METHODS); - - // Mock storage and writer + void testUploadContentThrowsOnIOException() throws IOException { Storage mockStorage = mock(Storage.class); - WriteChannel mockWriter = mock(WriteChannel.class); + GoogleClient googleClient = new GoogleClient(mockStorage, "my-bucket", executor); - // Inject mock storage and bucketName into googleClient using reflection - var field = GoogleClient.class.getDeclaredField("storage"); - field.setAccessible(true); - field.set(googleClient, mockStorage); - var executorField = GoogleClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(googleClient, executor); - var bucketField = GoogleClient.class.getDeclaredField("bucketName"); - bucketField.setAccessible(true); - bucketField.set(googleClient, "my-bucket"); + WriteChannel mockWriter = mock(WriteChannel.class); when(mockStorage.writer(any(BlobInfo.class))).thenReturn(mockWriter); // Simulate IOException on write @@ -223,21 +145,11 @@ void testUploadContentThrowsOnIOException() } @Test - void testDeleteContentThrowsOnRuntimeException() - throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException { - GoogleClient googleClient = mock(GoogleClient.class, CALLS_REAL_METHODS); - - // Mock storage and blob to throw RuntimeException on delete + void testDeleteContentThrowsOnRuntimeException() { Storage mockStorage = mock(Storage.class); - Blob mockBlob = mock(Blob.class); + GoogleClient googleClient = new GoogleClient(mockStorage, "my-bucket", executor); - // Inject mock storage into googleClient using reflection - var field = GoogleClient.class.getDeclaredField("storage"); - field.setAccessible(true); - field.set(googleClient, mockStorage); - var executorField = GoogleClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(googleClient, executor); + Blob mockBlob = mock(Blob.class); when(mockStorage.get(any(String.class), any(String.class))).thenReturn(mockBlob); doThrow(new RuntimeException("Simulated delete failure")).when(mockBlob).delete(); @@ -248,22 +160,10 @@ void testDeleteContentThrowsOnRuntimeException() } @Test - void testReadContentThrowsOnRuntimeException() - throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException { - GoogleClient googleClient = mock(GoogleClient.class, CALLS_REAL_METHODS); - - // Mock storage and blob to throw RuntimeException on reader + void testReadContentThrowsOnRuntimeException() { Storage mockStorage = mock(Storage.class); + GoogleClient googleClient = new GoogleClient(mockStorage, "my-bucket", executor); - // Inject mock storage into googleClient using reflection - var field = GoogleClient.class.getDeclaredField("storage"); - field.setAccessible(true); - field.set(googleClient, mockStorage); - var executorField = GoogleClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(googleClient, executor); - - // Mock blob.reader() to throw RuntimeException doThrow(new RuntimeException("Simulated read failure")) .when(mockStorage) .reader(any(com.google.cloud.storage.BlobId.class)); @@ -274,14 +174,10 @@ void testReadContentThrowsOnRuntimeException() } @Test - void testDeleteContentByPrefix() - throws NoSuchFieldException, - IllegalAccessException, - InterruptedException, - ExecutionException { - GoogleClient googleClient = mock(GoogleClient.class, CALLS_REAL_METHODS); - + void testDeleteContentByPrefix() throws InterruptedException, ExecutionException { Storage mockStorage = mock(Storage.class); + GoogleClient googleClient = new GoogleClient(mockStorage, "my-bucket", executor); + Page mockPage = mock(Page.class); Blob mockBlob1 = mock(Blob.class); when(mockBlob1.getName()).thenReturn("prefix/file1.txt"); @@ -295,40 +191,19 @@ void testDeleteContentByPrefix() when(mockStorage.list(anyString(), any(), any())).thenReturn(mockPage); when(mockStorage.delete(any(BlobId.class))).thenReturn(true); - var field = GoogleClient.class.getDeclaredField("storage"); - field.setAccessible(true); - field.set(googleClient, mockStorage); - var executorField = GoogleClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(googleClient, executor); - var bucketField = GoogleClient.class.getDeclaredField("bucketName"); - bucketField.setAccessible(true); - bucketField.set(googleClient, "my-bucket"); - googleClient.deleteContentByPrefix("prefix/").get(); verify(mockStorage, times(2)).delete(any(BlobId.class)); } @Test - void testDeleteContentByPrefixThrowsOnRuntimeException() - throws NoSuchFieldException, IllegalAccessException { - GoogleClient googleClient = mock(GoogleClient.class, CALLS_REAL_METHODS); - + void testDeleteContentByPrefixThrowsOnRuntimeException() { Storage mockStorage = mock(Storage.class); + GoogleClient googleClient = new GoogleClient(mockStorage, "my-bucket", executor); + when(mockStorage.list(anyString(), any(), any())) .thenThrow(new RuntimeException("Simulated failure")); - var field = GoogleClient.class.getDeclaredField("storage"); - field.setAccessible(true); - field.set(googleClient, mockStorage); - var executorField = GoogleClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(googleClient, executor); - var bucketField = GoogleClient.class.getDeclaredField("bucketName"); - bucketField.setAccessible(true); - bucketField.set(googleClient, "my-bucket"); - ExecutionException thrown = assertThrows( ExecutionException.class, () -> googleClient.deleteContentByPrefix("prefix/").get()); diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java index 90ff570da..36fb3f75c 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java @@ -73,8 +73,8 @@ private static CdsEntity stubEntity(String name) { } /** - * Creates a real {@link AttachmentCreateEventContext} populated with the given values. - * The only mock used is CdsEntity (a model-level concept not creatable without a full model). + * Creates a real {@link AttachmentCreateEventContext} populated with the given values. The only + * mock used is CdsEntity (a model-level concept not creatable without a full model). */ private static AttachmentCreateEventContext createContext( String contentId, String mimeType, String fileName, byte[] content) { @@ -204,8 +204,7 @@ void testReadAttachmentReadsContent() { @Test void testReadAttachmentWithNullContentThrows() { - when(mockOsClient.readContent("doc123")) - .thenReturn(CompletableFuture.completedFuture(null)); + when(mockOsClient.readContent("doc123")).thenReturn(CompletableFuture.completedFuture(null)); var context = AttachmentReadEventContext.create(); context.setContentId("doc123"); @@ -266,8 +265,7 @@ void testCreateAttachmentHandlesExecutionException() throws Exception { @Test void testMarkAsDeletedHandlesInterruptedException() throws Exception { - var context = - createContextForDeleteException(new InterruptedException("Thread interrupted")); + var context = createContextForDeleteException(new InterruptedException("Thread interrupted")); assertThrows(ServiceException.class, () -> handler.markAttachmentAsDeleted(context)); assertThat(context.isCompleted()).isTrue(); } @@ -299,8 +297,7 @@ void testReadAttachmentHandlesInterruptedException() throws Exception { @Test void testReadAttachmentHandlesExecutionException() throws Exception { var context = - createContextForReadException( - new ExecutionException("failed", new RuntimeException())); + createContextForReadException(new ExecutionException("failed", new RuntimeException())); assertThrows(ServiceException.class, () -> handler.readAttachment(context)); assertThat(context.isCompleted()).isTrue(); } @@ -398,8 +395,7 @@ void testMarkAsDeletedWithMultitenancyBuildsObjectKey() { when(mockOsClient.deleteContent("myTenant/content123")) .thenReturn(CompletableFuture.completedFuture(null)); - AttachmentMarkAsDeletedEventContext context = - mock(AttachmentMarkAsDeletedEventContext.class); + AttachmentMarkAsDeletedEventContext context = mock(AttachmentMarkAsDeletedEventContext.class); when(context.getContentId()).thenReturn("content123"); when(context.getUserInfo()).thenReturn(userInfoWithTenant("myTenant")); From 31c079d2319b899b37cc01f3c103e64a858ce16f Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Tue, 31 Mar 2026 14:00:39 +0200 Subject: [PATCH 18/18] spotless --- ...stPluginAttachmentsServiceHandlerTest.java | 1 + .../attachments/oss/client/AWSClientTest.java | 27 ++++++++++++------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java index 26b418ef7..160e938fd 100644 --- a/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java +++ b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java @@ -6,6 +6,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertNotNull; + import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.MediaData; import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.StatusCode; import com.sap.cds.feature.attachments.service.AttachmentService; diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java index b3b55dc39..2710e2d8f 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java @@ -60,7 +60,8 @@ void testConstructorWithAwsBindingUsesAwsClient() @Test void testReadContent() throws Exception { S3Client mockS3Client = mock(S3Client.class); - AWSClient awsClient = new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); + AWSClient awsClient = + new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); ByteArrayInputStream mockInputStream = new ByteArrayInputStream("test-data".getBytes()); GetObjectResponse mockResponse = mock(GetObjectResponse.class); @@ -95,7 +96,8 @@ void testUploadContent() throws Exception { @Test void testDeleteContent() { S3Client mockS3Client = mock(S3Client.class); - AWSClient awsClient = new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); + AWSClient awsClient = + new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); DeleteObjectResponse mockDelRes = mock(DeleteObjectResponse.class); SdkHttpResponse mockHttpRes = mock(SdkHttpResponse.class); @@ -109,7 +111,8 @@ void testDeleteContent() { @Test void testReadContentThrows() throws Exception { S3Client mockS3Client = mock(S3Client.class); - AWSClient awsClient = new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); + AWSClient awsClient = + new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); when(mockS3Client.getObject(any(GetObjectRequest.class))) .thenThrow(new RuntimeException("Simulated S3 failure")); @@ -165,7 +168,8 @@ void testUploadContentThrowsOnPutResponseNull() throws Exception { @Test void testDeleteContentThrowsOnRuntimeException() throws Exception { S3Client mockS3Client = mock(S3Client.class); - AWSClient awsClient = new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); + AWSClient awsClient = + new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); when(mockS3Client.deleteObject(any(DeleteObjectRequest.class))) .thenThrow(new RuntimeException("Simulated S3 delete failure")); @@ -178,7 +182,8 @@ void testDeleteContentThrowsOnRuntimeException() throws Exception { @Test void testDeleteContentThrowsOnUnsuccessfulResponse() { S3Client mockS3Client = mock(S3Client.class); - AWSClient awsClient = new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); + AWSClient awsClient = + new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); DeleteObjectResponse mockDelRes = mock(DeleteObjectResponse.class); SdkHttpResponse mockHttpRes = mock(SdkHttpResponse.class); @@ -194,7 +199,8 @@ void testDeleteContentThrowsOnUnsuccessfulResponse() { @Test void testDeleteContentByPrefix() throws Exception { S3Client mockS3Client = mock(S3Client.class); - AWSClient awsClient = new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); + AWSClient awsClient = + new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); S3Object obj1 = S3Object.builder().key("prefix/file1.txt").build(); S3Object obj2 = S3Object.builder().key("prefix/file2.txt").build(); @@ -217,7 +223,8 @@ void testDeleteContentByPrefix() throws Exception { @Test void testDeleteContentByPrefixEmptyList() throws Exception { S3Client mockS3Client = mock(S3Client.class); - AWSClient awsClient = new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); + AWSClient awsClient = + new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); ListObjectsV2Response listResponse = mock(ListObjectsV2Response.class); when(listResponse.contents()).thenReturn(Collections.emptyList()); @@ -230,7 +237,8 @@ void testDeleteContentByPrefixEmptyList() throws Exception { @Test void testDeleteContentByPrefixThrowsOnRuntimeException() throws Exception { S3Client mockS3Client = mock(S3Client.class); - AWSClient awsClient = new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); + AWSClient awsClient = + new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); when(mockS3Client.listObjectsV2(any(ListObjectsV2Request.class))) .thenThrow(new RuntimeException("Simulated failure")); @@ -244,7 +252,8 @@ void testDeleteContentByPrefixThrowsOnRuntimeException() throws Exception { @Test void testDeleteContentByPrefixWithPagination() throws Exception { S3Client mockS3Client = mock(S3Client.class); - AWSClient awsClient = new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); + AWSClient awsClient = + new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); // First page: 2 objects, isTruncated=true S3Object obj1 = S3Object.builder().key("prefix/file1.txt").build();