From 89210b1d619fe8606bf18a43dcb8db24a51c9b9c Mon Sep 17 00:00:00 2001 From: samyuktaprabhu <33195453+samyuktaprabhu@users.noreply.github.com> Date: Mon, 20 Jul 2026 15:47:08 +0200 Subject: [PATCH 1/4] feat: implement multi-tenancy for Document AI plugin --- .../DocumentAiServiceConfiguration.java | 18 ++- .../handlers/DocumentSubmissionHandler.java | 14 +++ .../handlers/ExtractionPollingHandler.java | 37 +++++- .../DefaultDocumentAiProcessingService.java | 4 +- .../service/DocumentAiProcessingService.java | 2 +- .../service/ExtractionServiceImpl.java | 3 +- .../client/DefaultDocumentAiClient.java | 19 ++- .../service/client/DocumentAiClient.java | 4 +- .../DocumentAiServiceConfigurationTest.java | 51 ++++++++ .../DocumentSubmissionHandlerTest.java | 10 +- .../ExtractionPollingHandlerTest.java | 116 ++++++++++++++++-- ...efaultDocumentAiProcessingServiceTest.java | 15 ++- .../service/ExtractionServiceImplTest.java | 14 +-- .../client/DefaultDocumentAiClientTest.java | 68 ++++++++-- .../aicore/itest/BaseIntegrationTest.java | 2 +- .../AbstractDocumentAiTest.java | 7 +- samples/bookshop/app/index.html | 4 +- 17 files changed, 330 insertions(+), 58 deletions(-) diff --git a/cds-feature-sap-document-ai/src/main/java/com/sap/cds/feature/documentai/configuration/DocumentAiServiceConfiguration.java b/cds-feature-sap-document-ai/src/main/java/com/sap/cds/feature/documentai/configuration/DocumentAiServiceConfiguration.java index 1917294..aa2958d 100644 --- a/cds-feature-sap-document-ai/src/main/java/com/sap/cds/feature/documentai/configuration/DocumentAiServiceConfiguration.java +++ b/cds-feature-sap-document-ai/src/main/java/com/sap/cds/feature/documentai/configuration/DocumentAiServiceConfiguration.java @@ -12,6 +12,8 @@ import com.sap.cds.feature.documentai.service.client.DocumentAiClient; import com.sap.cds.services.ServiceCatalog; import com.sap.cds.services.environment.CdsEnvironment; +import com.sap.cds.services.environment.CdsProperties; +import com.sap.cds.services.mt.DeploymentService; import com.sap.cds.services.outbox.OutboxService; import com.sap.cds.services.persistence.PersistenceService; import com.sap.cds.services.runtime.CdsRuntime; @@ -82,6 +84,7 @@ public void services(CdsRuntimeConfigurer configurer) { public void eventHandlers(CdsRuntimeConfigurer configurer) { CdsRuntime runtime = configurer.getCdsRuntime(); ServiceCatalog serviceCatalog = runtime.getServiceCatalog(); + boolean multiTenancyEnabled = detectMultiTenancy(runtime); // framework-managed dependency PersistenceService persistenceService = @@ -123,7 +126,8 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) { documentAiClient, outboxService, runtime, - pollDelay)); + pollDelay, + multiTenancyEnabled)); } } @@ -176,4 +180,16 @@ static DocumentAiClient buildDocumentAi(CdsEnvironment environment) { return null; } } + + static boolean detectMultiTenancy(CdsRuntime runtime) { + CdsProperties props = runtime.getEnvironment().getCdsProperties(); + String sidecarUrl = props.getMultiTenancy().getSidecar().getUrl(); + if (sidecarUrl != null && !sidecarUrl.isBlank()) { + return true; + } + return runtime + .getServiceCatalog() + .getService(DeploymentService.class, DeploymentService.DEFAULT_NAME) + != null; + } } diff --git a/cds-feature-sap-document-ai/src/main/java/com/sap/cds/feature/documentai/handlers/DocumentSubmissionHandler.java b/cds-feature-sap-document-ai/src/main/java/com/sap/cds/feature/documentai/handlers/DocumentSubmissionHandler.java index 2e2ea40..e5cda7b 100644 --- a/cds-feature-sap-document-ai/src/main/java/com/sap/cds/feature/documentai/handlers/DocumentSubmissionHandler.java +++ b/cds-feature-sap-document-ai/src/main/java/com/sap/cds/feature/documentai/handlers/DocumentSubmissionHandler.java @@ -5,6 +5,8 @@ import com.sap.cds.feature.documentai.generated.cds4j.sap.document.ai.documentaiservice.DocumentExtraction; import com.sap.cds.feature.documentai.generated.cds4j.sap.document.ai.documentaiservice.DocumentExtractionContext; +import com.sap.cds.feature.documentai.generated.cds4j.sap.document.ai.documentaiservice.DocumentExtractionResult; +import com.sap.cds.feature.documentai.generated.cds4j.sap.document.ai.documentaiservice.DocumentExtractionResultContext; import com.sap.cds.feature.documentai.service.ExtractionService; import com.sap.cds.feature.documentai.service.model.ExtractionResult; import com.sap.cds.services.cds.ApplicationService; @@ -61,10 +63,22 @@ public void onDocumentExtraction(DocumentExtractionContext context) { if (result.status() == ExtractionResult.Status.FAILED) { logger.error("[sap-document-ai] Extraction failed for fileName={}", event.getFileName()); + emitExtractionResult(context, result.internalJobId(), null); } else if (result.status() == ExtractionResult.Status.PENDING) { logger.warn("[sap-document-ai] Document AI unavailable, left as PENDING"); } context.setCompleted(); } + + private void emitExtractionResult( + DocumentExtractionContext context, String jobId, String extractionResult) { + DocumentExtractionResult eventData = DocumentExtractionResult.create(); + eventData.setJobId(jobId); + eventData.setExtractionResult(extractionResult); + DocumentExtractionResultContext eventContext = DocumentExtractionResultContext.create(); + eventContext.setData(eventData); + context.getService().emit(eventContext); + logger.info("[sap-document-ai] Emitted DocumentExtractionResult for jobId={}", jobId); + } } diff --git a/cds-feature-sap-document-ai/src/main/java/com/sap/cds/feature/documentai/handlers/ExtractionPollingHandler.java b/cds-feature-sap-document-ai/src/main/java/com/sap/cds/feature/documentai/handlers/ExtractionPollingHandler.java index 0ba4a70..74a18b6 100644 --- a/cds-feature-sap-document-ai/src/main/java/com/sap/cds/feature/documentai/handlers/ExtractionPollingHandler.java +++ b/cds-feature-sap-document-ai/src/main/java/com/sap/cds/feature/documentai/handlers/ExtractionPollingHandler.java @@ -22,9 +22,13 @@ import com.sap.cds.services.outbox.OutboxService; import com.sap.cds.services.outbox.Schedule; import com.sap.cds.services.persistence.PersistenceService; +import com.sap.cds.services.request.RequestContext; import com.sap.cds.services.runtime.CdsRuntime; import java.time.Duration; import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -60,6 +64,7 @@ public class ExtractionPollingHandler implements EventHandler { private static final Logger logger = LoggerFactory.getLogger(ExtractionPollingHandler.class); + private final boolean multiTenancyEnabled; private final PersistenceService persistenceService; private final ExtractionService extractionService; private final DocumentAiClient documentAiClient; @@ -81,13 +86,15 @@ public ExtractionPollingHandler( DocumentAiClient documentAiClient, OutboxService outboxService, CdsRuntime runtime, - Duration pollDelay) { + Duration pollDelay, + boolean multiTenancyEnabled) { this.persistenceService = persistenceService; this.extractionService = extractionService; this.documentAiClient = documentAiClient; this.outboxService = outboxService; this.runtime = runtime; this.pollDelay = pollDelay; + this.multiTenancyEnabled = multiTenancyEnabled; } /** @@ -117,8 +124,17 @@ public void pollExtractionJobs(OutboxMessageEventContext context) { return; } - for (ExtractionJob job : activeJobs) { - processJob(job); + if (multiTenancyEnabled) { + Map> byTenant = + activeJobs.stream() + .collect(Collectors.groupingBy(j -> j.getTenantId() != null ? j.getTenantId() : "")); + byTenant.forEach( + (key, jobs) -> { + String tenantId = key.isEmpty() ? null : key; + processJobsForTenant(tenantId, jobs); + }); + } else { + activeJobs.forEach(job -> processJob(job, null)); } if (outboxService != null) { @@ -133,7 +149,7 @@ public void pollExtractionJobs(OutboxMessageEventContext context) { context.setCompleted(); } - private void processJob(ExtractionJob job) { + private void processJob(ExtractionJob job, String tenantId) { String jobId = job.getId(); String dieJobId = job.getDocumentAiJobId(); @@ -143,7 +159,7 @@ private void processJob(ExtractionJob job) { } try { - ExtractionData result = documentAiClient.getJobResult(dieJobId); + ExtractionData result = documentAiClient.getJobResult(dieJobId, tenantId); ExtractionStatus newStatus = mapDieStatus(result.dieStatus()); if (newStatus == null) { @@ -170,6 +186,17 @@ private void processJob(ExtractionJob job) { } } + private void processJobsForTenant(String tenantId, List jobs) { + if (tenantId == null) { + jobs.forEach(job -> processJob(job, null)); + return; + } + runtime + .requestContext() + .systemUser(tenantId) + .run((Consumer) ctx -> jobs.forEach(job -> processJob(job, tenantId))); + } + private void emitExtractionCompleted(String jobId, String extractionResult) { ApplicationService documentAiService = runtime diff --git a/cds-feature-sap-document-ai/src/main/java/com/sap/cds/feature/documentai/service/DefaultDocumentAiProcessingService.java b/cds-feature-sap-document-ai/src/main/java/com/sap/cds/feature/documentai/service/DefaultDocumentAiProcessingService.java index ec4a73f..0f418d6 100644 --- a/cds-feature-sap-document-ai/src/main/java/com/sap/cds/feature/documentai/service/DefaultDocumentAiProcessingService.java +++ b/cds-feature-sap-document-ai/src/main/java/com/sap/cds/feature/documentai/service/DefaultDocumentAiProcessingService.java @@ -26,9 +26,9 @@ public DefaultDocumentAiProcessingService(DocumentAiClient documentAiClient) { } @Override - public String processDocument(String jobId, DocumentInput documentInput) { + public String processDocument(String jobId, DocumentInput documentInput, String tenantId) { try { - String documentAiJobId = documentAiClient.submitDocument(documentInput); + String documentAiJobId = documentAiClient.submitDocument(documentInput, tenantId); return documentAiJobId; } catch (Exception e) { throw new DocumentAiException.Processing("Failed to process document for jobId=" + jobId, e); diff --git a/cds-feature-sap-document-ai/src/main/java/com/sap/cds/feature/documentai/service/DocumentAiProcessingService.java b/cds-feature-sap-document-ai/src/main/java/com/sap/cds/feature/documentai/service/DocumentAiProcessingService.java index 3972446..46c65fd 100644 --- a/cds-feature-sap-document-ai/src/main/java/com/sap/cds/feature/documentai/service/DocumentAiProcessingService.java +++ b/cds-feature-sap-document-ai/src/main/java/com/sap/cds/feature/documentai/service/DocumentAiProcessingService.java @@ -30,5 +30,5 @@ public interface DocumentAiProcessingService { * @throws com.sap.cds.feature.documentai.service.exceptions.DocumentAiException if submission or * response parsing fails */ - String processDocument(String jobId, DocumentInput documentInput); + String processDocument(String jobId, DocumentInput documentInput, String tenantId); } diff --git a/cds-feature-sap-document-ai/src/main/java/com/sap/cds/feature/documentai/service/ExtractionServiceImpl.java b/cds-feature-sap-document-ai/src/main/java/com/sap/cds/feature/documentai/service/ExtractionServiceImpl.java index 2216619..b39ec19 100644 --- a/cds-feature-sap-document-ai/src/main/java/com/sap/cds/feature/documentai/service/ExtractionServiceImpl.java +++ b/cds-feature-sap-document-ai/src/main/java/com/sap/cds/feature/documentai/service/ExtractionServiceImpl.java @@ -114,7 +114,8 @@ public void updateExtractionResult( private ExtractionResult performExtraction( String jobId, String fileName, DocumentInput documentInput, String tenantId) { try { - String documentAiJobId = documentAiProcessingService.processDocument(jobId, documentInput); + String documentAiJobId = + documentAiProcessingService.processDocument(jobId, documentInput, tenantId); updateExtractionJob(jobId, SUBMITTED, documentAiJobId, null); schedulePolling(); return new ExtractionResult(jobId, Status.SUCCESS, documentAiJobId); diff --git a/cds-feature-sap-document-ai/src/main/java/com/sap/cds/feature/documentai/service/client/DefaultDocumentAiClient.java b/cds-feature-sap-document-ai/src/main/java/com/sap/cds/feature/documentai/service/client/DefaultDocumentAiClient.java index 21057fa..3fa0483 100644 --- a/cds-feature-sap-document-ai/src/main/java/com/sap/cds/feature/documentai/service/client/DefaultDocumentAiClient.java +++ b/cds-feature-sap-document-ai/src/main/java/com/sap/cds/feature/documentai/service/client/DefaultDocumentAiClient.java @@ -44,6 +44,7 @@ public class DefaultDocumentAiClient implements DocumentAiClient { private static final ObjectMapper objectMapper = new ObjectMapper(); private static final String DOCUMENT_AI_API_PATH = "document-information-extraction/v1"; public static final String DOCUMENT_JOBS = "/document/jobs"; + public static final String CLIENT_ID_PARAM = "clientId"; public static final String EXTRACTED_VALUES_TRUE = "?extractedValues=true"; private final HttpDestination destination; private final HttpClient httpClient; @@ -59,17 +60,20 @@ public DefaultDocumentAiClient(HttpDestination destination, HttpClient httpClien } @Override - public String submitDocument(DocumentInput documentInput) { - URI submitUri = buildUri(DOCUMENT_AI_API_PATH + DOCUMENT_JOBS); + public String submitDocument(DocumentInput documentInput, String tenantId) { + URI submitUri = buildUriWithClientId(DOCUMENT_AI_API_PATH + DOCUMENT_JOBS, tenantId); HttpPost request = buildSubmitRequest(documentInput, submitUri); String body = executeRequest(request, submitUri); return extractJobId(body); } @Override - public ExtractionData getJobResult(String dieJobId) { - URI uri = - buildUri(DOCUMENT_AI_API_PATH + DOCUMENT_JOBS + "/" + dieJobId + EXTRACTED_VALUES_TRUE); + public ExtractionData getJobResult(String dieJobId, String tenantId) { + String path = DOCUMENT_AI_API_PATH + DOCUMENT_JOBS + "/" + dieJobId + EXTRACTED_VALUES_TRUE; + if (tenantId != null) { + path = path + "&" + CLIENT_ID_PARAM + "=" + tenantId; + } + URI uri = buildUri(path); logger.info("[sap-document-ai] Polling DIE for dieJobId={}", dieJobId); HttpGet request = new HttpGet(uri); String body = executeRequest(request, uri); @@ -82,6 +86,11 @@ private URI buildUri(String path) { return URI.create(prefix).resolve(path); } + private URI buildUriWithClientId(String path, String tenantId) { + String fullPath = (tenantId != null) ? path + "?" + CLIENT_ID_PARAM + "=" + tenantId : path; + return buildUri(fullPath); + } + private HttpPost buildSubmitRequest(DocumentInput documentInput, URI submitUri) { logger.info( "[sap-document-ai] Submitting document to DIE at url={}, fileName={}, mimeType={}", diff --git a/cds-feature-sap-document-ai/src/main/java/com/sap/cds/feature/documentai/service/client/DocumentAiClient.java b/cds-feature-sap-document-ai/src/main/java/com/sap/cds/feature/documentai/service/client/DocumentAiClient.java index cd327c4..277ca96 100644 --- a/cds-feature-sap-document-ai/src/main/java/com/sap/cds/feature/documentai/service/client/DocumentAiClient.java +++ b/cds-feature-sap-document-ai/src/main/java/com/sap/cds/feature/documentai/service/client/DocumentAiClient.java @@ -22,7 +22,7 @@ public interface DocumentAiClient { * @throws com.sap.cds.feature.documentai.service.exceptions.DocumentAiException if the HTTP call * fails or the response cannot be parsed */ - String submitDocument(DocumentInput documentInput); + String submitDocument(DocumentInput documentInput, String tenantId); /** * Polls the DIE service for the current status and result of a previously submitted job. @@ -32,5 +32,5 @@ public interface DocumentAiClient { * @throws com.sap.cds.feature.documentai.service.exceptions.DocumentAiException if the HTTP call * fails or the response cannot be parsed */ - ExtractionData getJobResult(String dieJobId); + ExtractionData getJobResult(String dieJobId, String tenantId); } diff --git a/cds-feature-sap-document-ai/src/test/java/com/sap/cds/feature/documentai/configuration/DocumentAiServiceConfigurationTest.java b/cds-feature-sap-document-ai/src/test/java/com/sap/cds/feature/documentai/configuration/DocumentAiServiceConfigurationTest.java index fbfade8..06746fc 100644 --- a/cds-feature-sap-document-ai/src/test/java/com/sap/cds/feature/documentai/configuration/DocumentAiServiceConfigurationTest.java +++ b/cds-feature-sap-document-ai/src/test/java/com/sap/cds/feature/documentai/configuration/DocumentAiServiceConfigurationTest.java @@ -15,7 +15,9 @@ import com.sap.cds.services.Service; import com.sap.cds.services.ServiceCatalog; import com.sap.cds.services.environment.CdsEnvironment; +import com.sap.cds.services.environment.CdsProperties; import com.sap.cds.services.handler.EventHandler; +import com.sap.cds.services.mt.DeploymentService; import com.sap.cds.services.persistence.PersistenceService; import com.sap.cds.services.runtime.CdsRuntime; import com.sap.cds.services.runtime.CdsRuntimeConfigurer; @@ -38,6 +40,9 @@ class DocumentAiServiceConfigurationTest { @Mock ServiceCatalog serviceCatalog; @Mock PersistenceService persistenceService; @Mock CdsEnvironment environment; + @Mock CdsProperties cdsProperties; + @Mock CdsProperties.MultiTenancy multiTenancy; + @Mock CdsProperties.MultiTenancy.Sidecar sidecar; DocumentAiServiceConfiguration registration; @@ -66,6 +71,13 @@ void eventHandlersRegistersDocumentSubmissionHandler() { .thenReturn(3); when(serviceCatalog.getService(PersistenceService.class, PersistenceService.DEFAULT_NAME)) .thenReturn(persistenceService); + // stub MT detection + when(environment.getCdsProperties()).thenReturn(cdsProperties); + when(cdsProperties.getMultiTenancy()).thenReturn(multiTenancy); + when(multiTenancy.getSidecar()).thenReturn(sidecar); + when(sidecar.getUrl()).thenReturn(null); + when(serviceCatalog.getService(DeploymentService.class, DeploymentService.DEFAULT_NAME)) + .thenReturn(null); registration.services(configurer); registration.eventHandlers(configurer); @@ -108,4 +120,43 @@ void buildDocumentAi_bindingFound_destinationCreationFails_returnsNull() { assertThat(result).isNull(); } } + + @Test + void detectMultiTenancy_withSidecarUrl_returnsTrue() { + when(cdsRuntime.getEnvironment()).thenReturn(environment); + when(environment.getCdsProperties()).thenReturn(cdsProperties); + when(cdsProperties.getMultiTenancy()).thenReturn(multiTenancy); + when(multiTenancy.getSidecar()).thenReturn(sidecar); + when(sidecar.getUrl()).thenReturn("https://sidecar.example.com"); + + assertThat(DocumentAiServiceConfiguration.detectMultiTenancy(cdsRuntime)).isTrue(); + } + + @Test + void detectMultiTenancy_withDeploymentService_returnsTrue() { + when(cdsRuntime.getEnvironment()).thenReturn(environment); + when(cdsRuntime.getServiceCatalog()).thenReturn(serviceCatalog); + when(environment.getCdsProperties()).thenReturn(cdsProperties); + when(cdsProperties.getMultiTenancy()).thenReturn(multiTenancy); + when(multiTenancy.getSidecar()).thenReturn(sidecar); + when(sidecar.getUrl()).thenReturn(null); + when(serviceCatalog.getService(DeploymentService.class, DeploymentService.DEFAULT_NAME)) + .thenReturn(mock(DeploymentService.class)); + + assertThat(DocumentAiServiceConfiguration.detectMultiTenancy(cdsRuntime)).isTrue(); + } + + @Test + void detectMultiTenancy_noIndicators_returnsFalse() { + when(cdsRuntime.getEnvironment()).thenReturn(environment); + when(cdsRuntime.getServiceCatalog()).thenReturn(serviceCatalog); + when(environment.getCdsProperties()).thenReturn(cdsProperties); + when(cdsProperties.getMultiTenancy()).thenReturn(multiTenancy); + when(multiTenancy.getSidecar()).thenReturn(sidecar); + when(sidecar.getUrl()).thenReturn(null); + when(serviceCatalog.getService(DeploymentService.class, DeploymentService.DEFAULT_NAME)) + .thenReturn(null); + + assertThat(DocumentAiServiceConfiguration.detectMultiTenancy(cdsRuntime)).isFalse(); + } } diff --git a/cds-feature-sap-document-ai/src/test/java/com/sap/cds/feature/documentai/handlers/DocumentSubmissionHandlerTest.java b/cds-feature-sap-document-ai/src/test/java/com/sap/cds/feature/documentai/handlers/DocumentSubmissionHandlerTest.java index 90be694..74fa4e6 100644 --- a/cds-feature-sap-document-ai/src/test/java/com/sap/cds/feature/documentai/handlers/DocumentSubmissionHandlerTest.java +++ b/cds-feature-sap-document-ai/src/test/java/com/sap/cds/feature/documentai/handlers/DocumentSubmissionHandlerTest.java @@ -9,8 +9,10 @@ import com.sap.cds.feature.documentai.generated.cds4j.sap.document.ai.documentaiservice.DocumentExtraction; import com.sap.cds.feature.documentai.generated.cds4j.sap.document.ai.documentaiservice.DocumentExtractionContext; +import com.sap.cds.feature.documentai.generated.cds4j.sap.document.ai.documentaiservice.DocumentExtractionResultContext; import com.sap.cds.feature.documentai.service.ExtractionService; import com.sap.cds.feature.documentai.service.model.ExtractionResult; +import com.sap.cds.services.cds.ApplicationService; import com.sap.cds.services.request.UserInfo; import java.io.ByteArrayInputStream; import java.io.InputStream; @@ -30,6 +32,7 @@ class DocumentSubmissionHandlerTest { @Mock ExtractionService extractionService; @Mock DocumentExtractionContext eventContext; @Mock UserInfo userInfo; + @Mock ApplicationService applicationService; DocumentSubmissionHandler handler; @@ -72,19 +75,20 @@ void onDocumentExtraction_logsPendingWhenServiceUnavailable() { handler.onDocumentExtraction(eventContext); - verify(extractionService).triggerExtraction(any(), any(), any(), any(), any()); + verify(eventContext, never()).getService(); } @Test - void onDocumentExtraction_logsFailedWhenExtractionFails() { + void onDocumentExtraction_emitsResultEventWhenExtractionFails() { when(eventContext.getUserInfo()).thenReturn(userInfo); when(userInfo.getTenant()).thenReturn(TENANT_ID); when(eventContext.getData()).thenReturn(createEvent()); when(extractionService.triggerExtraction(any(), any(), any(), any(), any())) .thenReturn(new ExtractionResult("job-123", ExtractionResult.Status.FAILED, null)); + when(eventContext.getService()).thenReturn(applicationService); handler.onDocumentExtraction(eventContext); - verify(extractionService).triggerExtraction(any(), any(), any(), any(), any()); + verify(applicationService).emit(any(DocumentExtractionResultContext.class)); } } diff --git a/cds-feature-sap-document-ai/src/test/java/com/sap/cds/feature/documentai/handlers/ExtractionPollingHandlerTest.java b/cds-feature-sap-document-ai/src/test/java/com/sap/cds/feature/documentai/handlers/ExtractionPollingHandlerTest.java index 610dc8b..47adbd6 100644 --- a/cds-feature-sap-document-ai/src/test/java/com/sap/cds/feature/documentai/handlers/ExtractionPollingHandlerTest.java +++ b/cds-feature-sap-document-ai/src/test/java/com/sap/cds/feature/documentai/handlers/ExtractionPollingHandlerTest.java @@ -22,8 +22,10 @@ import com.sap.cds.services.outbox.Schedule; import com.sap.cds.services.persistence.PersistenceService; import com.sap.cds.services.runtime.CdsRuntime; +import com.sap.cds.services.runtime.RequestContextRunner; import java.time.Duration; import java.util.List; +import java.util.function.Consumer; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -46,6 +48,7 @@ class ExtractionPollingHandlerTest { @Mock ApplicationService documentAiService; @Mock OutboxMessageEventContext context; @Mock Result queryResult; + @Mock RequestContextRunner requestContextRunner; ExtractionPollingHandler handler; @@ -58,7 +61,8 @@ void setUp() { documentAiClient, outboxService, runtime, - Duration.ofSeconds(ExtractionPollingHandler.DEFAULT_POLL_INTERVAL_SECONDS)); + Duration.ofSeconds(ExtractionPollingHandler.DEFAULT_POLL_INTERVAL_SECONDS), + false); } private void mockEmit() { @@ -81,7 +85,7 @@ void pollStopsAndSetsCompletedWhenNoActiveJobs() { @Test void pollReschedulesWhenActiveJobsExist() { mockActiveJob(DIE_JOB_ID); - when(documentAiClient.getJobResult(DIE_JOB_ID)) + when(documentAiClient.getJobResult(DIE_JOB_ID, null)) .thenReturn(new ExtractionData(DIE_JOB_ID, "RUNNING", null)); handler.pollExtractionJobs(context); @@ -97,13 +101,13 @@ void pollSkipsJobWithNoDieJobId() { handler.pollExtractionJobs(context); - verify(documentAiClient, never()).getJobResult(any()); + verify(documentAiClient, never()).getJobResult(any(), any()); } @Test void pollDoesNotUpdateStatusWhenDieReturnsPending() { mockActiveJob(DIE_JOB_ID); - when(documentAiClient.getJobResult(DIE_JOB_ID)) + when(documentAiClient.getJobResult(DIE_JOB_ID, null)) .thenReturn(new ExtractionData(DIE_JOB_ID, "PENDING", null)); handler.pollExtractionJobs(context); @@ -114,7 +118,7 @@ void pollDoesNotUpdateStatusWhenDieReturnsPending() { @Test void pollUpdatesStatusToRunningWithoutEmittingEvent() { mockActiveJob(DIE_JOB_ID); - when(documentAiClient.getJobResult(DIE_JOB_ID)) + when(documentAiClient.getJobResult(DIE_JOB_ID, null)) .thenReturn(new ExtractionData(DIE_JOB_ID, "RUNNING", null)); handler.pollExtractionJobs(context); @@ -127,7 +131,7 @@ void pollUpdatesStatusToRunningWithoutEmittingEvent() { @Test void pollUpdatesStatusToFailedWithoutEmittingEvent() { mockActiveJob(DIE_JOB_ID); - when(documentAiClient.getJobResult(DIE_JOB_ID)) + when(documentAiClient.getJobResult(DIE_JOB_ID, null)) .thenReturn(new ExtractionData(DIE_JOB_ID, "FAILED", null)); handler.pollExtractionJobs(context); @@ -141,7 +145,7 @@ void pollUpdatesStatusToFailedWithoutEmittingEvent() { void pollUpdatesStatusToDoneAndEmitsEvent() { mockEmit(); mockActiveJob(DIE_JOB_ID); - when(documentAiClient.getJobResult(DIE_JOB_ID)) + when(documentAiClient.getJobResult(DIE_JOB_ID, null)) .thenReturn(new ExtractionData(DIE_JOB_ID, "DONE", RAW_RESULT)); handler.pollExtractionJobs(context); @@ -164,8 +168,9 @@ void pollContinuesToNextJobWhenOneThrows() { when(persistenceService.run(any(CqnSelect.class))).thenReturn(queryResult); when(queryResult.listOf(ExtractionJob.class)).thenReturn(List.of(failingJob, goodJob)); - when(documentAiClient.getJobResult("die-fail")).thenThrow(new RuntimeException("timeout")); - when(documentAiClient.getJobResult(DIE_JOB_ID)) + when(documentAiClient.getJobResult("die-fail", null)) + .thenThrow(new RuntimeException("timeout")); + when(documentAiClient.getJobResult(DIE_JOB_ID, null)) .thenReturn(new ExtractionData(DIE_JOB_ID, "RUNNING", null)); handler.pollExtractionJobs(context); @@ -175,6 +180,99 @@ void pollContinuesToNextJobWhenOneThrows() { verify(context).setCompleted(); } + @Test + @SuppressWarnings("unchecked") + void poll_multiTenancy_groupsJobsByTenant() { + ExtractionPollingHandler mtHandler = + new ExtractionPollingHandler( + persistenceService, + extractionService, + documentAiClient, + outboxService, + runtime, + Duration.ofSeconds(ExtractionPollingHandler.DEFAULT_POLL_INTERVAL_SECONDS), + true); + + ExtractionJob jobA = ExtractionJob.create(); + jobA.setId("job-a"); + jobA.setDocumentAiJobId("die-a"); + jobA.setTenantId("tenant-a"); + + ExtractionJob jobB = ExtractionJob.create(); + jobB.setId("job-b"); + jobB.setDocumentAiJobId("die-b"); + jobB.setTenantId("tenant-b"); + + when(persistenceService.run(any(CqnSelect.class))).thenReturn(queryResult); + when(queryResult.listOf(ExtractionJob.class)).thenReturn(List.of(jobA, jobB)); + when(documentAiClient.getJobResult(any(), any())) + .thenReturn(new ExtractionData("die-x", "RUNNING", null)); + + when(runtime.requestContext()).thenReturn(requestContextRunner); + when(requestContextRunner.systemUser(any())).thenReturn(requestContextRunner); + doAnswer( + inv -> { + inv.getArgument(0, Consumer.class).accept(null); + return null; + }) + .when(requestContextRunner) + .run(any(Consumer.class)); + + mtHandler.pollExtractionJobs(context); + + verify(runtime.requestContext()).systemUser("tenant-a"); + verify(runtime.requestContext()).systemUser("tenant-b"); + verify(documentAiClient).getJobResult("die-a", "tenant-a"); + verify(documentAiClient).getJobResult("die-b", "tenant-b"); + } + + @Test + @SuppressWarnings("unchecked") + void poll_multiTenancy_nullTenantId_processedWithoutContextSwitch() { + ExtractionPollingHandler mtHandler = + new ExtractionPollingHandler( + persistenceService, + extractionService, + documentAiClient, + outboxService, + runtime, + Duration.ofSeconds(ExtractionPollingHandler.DEFAULT_POLL_INTERVAL_SECONDS), + true); + + ExtractionJob job = ExtractionJob.create(); + job.setId(JOB_ID); + job.setDocumentAiJobId(DIE_JOB_ID); + // no tenantId set — null + + when(persistenceService.run(any(CqnSelect.class))).thenReturn(queryResult); + when(queryResult.listOf(ExtractionJob.class)).thenReturn(List.of(job)); + when(documentAiClient.getJobResult(DIE_JOB_ID, null)) + .thenReturn(new ExtractionData(DIE_JOB_ID, "RUNNING", null)); + + mtHandler.pollExtractionJobs(context); + + verify(runtime, never()).requestContext(); + verify(documentAiClient).getJobResult(DIE_JOB_ID, null); + } + + @Test + void poll_singleTenant_doesNotCallRequestContext() { + // handler is constructed with multiTenancyEnabled=false in setUp() + ExtractionJob job = ExtractionJob.create(); + job.setId(JOB_ID); + job.setDocumentAiJobId(DIE_JOB_ID); + job.setTenantId("tenant-a"); // has a tenantId, but MT is disabled + + when(persistenceService.run(any(CqnSelect.class))).thenReturn(queryResult); + when(queryResult.listOf(ExtractionJob.class)).thenReturn(List.of(job)); + when(documentAiClient.getJobResult(DIE_JOB_ID, null)) + .thenReturn(new ExtractionData(DIE_JOB_ID, "RUNNING", null)); + + handler.pollExtractionJobs(context); + + verify(runtime, never()).requestContext(); + } + private void mockActiveJob(String dieJobId) { ExtractionJob job = ExtractionJob.create(); job.setId(JOB_ID); diff --git a/cds-feature-sap-document-ai/src/test/java/com/sap/cds/feature/documentai/service/DefaultDocumentAiProcessingServiceTest.java b/cds-feature-sap-document-ai/src/test/java/com/sap/cds/feature/documentai/service/DefaultDocumentAiProcessingServiceTest.java index 212a68c..eea31f1 100644 --- a/cds-feature-sap-document-ai/src/test/java/com/sap/cds/feature/documentai/service/DefaultDocumentAiProcessingServiceTest.java +++ b/cds-feature-sap-document-ai/src/test/java/com/sap/cds/feature/documentai/service/DefaultDocumentAiProcessingServiceTest.java @@ -32,7 +32,7 @@ class DefaultDocumentAiProcessingServiceTest { @BeforeEach void setUp() { documentAiClient = Mockito.mock(DocumentAiClient.class); - Mockito.when(documentAiClient.submitDocument(any())).thenReturn(MOCK_RESULT); + Mockito.when(documentAiClient.submitDocument(any(), any())).thenReturn(MOCK_RESULT); service = new DefaultDocumentAiProcessingService(documentAiClient); documentInput = new DocumentInput( @@ -56,15 +56,22 @@ void isAvailableReturnsFalseWhenClientNull() { // ------- processDocument() ------- @Test void processDocumentCompletesWithoutException() { - Assertions.assertThatCode(() -> service.processDocument(JOB_1, documentInput)) + Assertions.assertThatCode(() -> service.processDocument(JOB_1, documentInput, null)) .doesNotThrowAnyException(); } @Test void processDocumentThrowsWhenSubmitDocumentFails() { - Mockito.when(documentAiClient.submitDocument(any())) + Mockito.when(documentAiClient.submitDocument(any(), any())) .thenThrow(new RuntimeException("submit failed")); - Assertions.assertThatThrownBy(() -> service.processDocument(JOB_1, documentInput)) + Assertions.assertThatThrownBy(() -> service.processDocument(JOB_1, documentInput, null)) .isInstanceOf(DocumentAiException.Processing.class); } + + @Test + void processDocumentPassesTenantIdToClient() { + service.processDocument(JOB_1, documentInput, "tenant-a"); + + Mockito.verify(documentAiClient).submitDocument(any(), Mockito.eq("tenant-a")); + } } diff --git a/cds-feature-sap-document-ai/src/test/java/com/sap/cds/feature/documentai/service/ExtractionServiceImplTest.java b/cds-feature-sap-document-ai/src/test/java/com/sap/cds/feature/documentai/service/ExtractionServiceImplTest.java index 3f05fad..c46b123 100644 --- a/cds-feature-sap-document-ai/src/test/java/com/sap/cds/feature/documentai/service/ExtractionServiceImplTest.java +++ b/cds-feature-sap-document-ai/src/test/java/com/sap/cds/feature/documentai/service/ExtractionServiceImplTest.java @@ -73,7 +73,7 @@ void triggerExtractionCreatesJobWithCorrectFields() { mockAllDatabaseCalls(); Result statusResult = resultWithJobStatus(PENDING); lenient().when(persistenceService.run(any(CqnSelect.class))).thenReturn(statusResult); - when(documentAiProcessingService.processDocument(any(), any())).thenReturn(DIE_JOB_ID); + when(documentAiProcessingService.processDocument(any(), any(), any())).thenReturn(DIE_JOB_ID); extractionService.triggerExtraction(TEST_PDF, CONTENT_TYPE, contentStream(), null, TENANT_1); @@ -91,7 +91,7 @@ void triggerExtractionSubmitsDocumentAndUpdatesStatusToSubmitted() { mockAllDatabaseCalls(); Result statusResult = resultWithJobStatus(PENDING); lenient().when(persistenceService.run(any(CqnSelect.class))).thenReturn(statusResult); - when(documentAiProcessingService.processDocument(any(), any())).thenReturn(DIE_JOB_ID); + when(documentAiProcessingService.processDocument(any(), any(), any())).thenReturn(DIE_JOB_ID); ExtractionResult result = extractionService.triggerExtraction( @@ -111,7 +111,7 @@ void triggerExtractionDoesNotThrowWhenOutboxIsNull() { mockAllDatabaseCalls(); Result statusResult = resultWithJobStatus(PENDING); lenient().when(persistenceService.run(any(CqnSelect.class))).thenReturn(statusResult); - when(documentAiProcessingService.processDocument(any(), any())).thenReturn(DIE_JOB_ID); + when(documentAiProcessingService.processDocument(any(), any(), any())).thenReturn(DIE_JOB_ID); ExtractionResult result = extractionService.triggerExtraction( @@ -128,7 +128,7 @@ void triggerExtractionMarksJobFailedOnProcessingError() { lenient().when(persistenceService.run(any(CqnSelect.class))).thenReturn(statusResult); doThrow(new RuntimeException("simulated failure")) .when(documentAiProcessingService) - .processDocument(any(), any()); + .processDocument(any(), any(), any()); ExtractionResult result = extractionService.triggerExtraction( @@ -144,7 +144,7 @@ void triggerExtractionReturnSuccessOnConcurrentUpdate() { mockAllDatabaseCalls(); Result statusResult = resultWithJobStatus(PENDING); lenient().when(persistenceService.run(any(CqnSelect.class))).thenReturn(statusResult); - when(documentAiProcessingService.processDocument(any(), any())).thenReturn(DIE_JOB_ID); + when(documentAiProcessingService.processDocument(any(), any(), any())).thenReturn(DIE_JOB_ID); Result zeroRowResult = mock(Result.class); when(zeroRowResult.rowCount()).thenReturn(0L); when(persistenceService.run(any(CqnUpdate.class))).thenReturn(zeroRowResult); @@ -164,7 +164,7 @@ void triggerExtractionThrowsOnInvalidStatusTransition() { lenient().when(persistenceService.run(any(CqnSelect.class))).thenReturn(statusResult); doThrow(new IllegalStatusTransitionException("invalid transition")) .when(documentAiProcessingService) - .processDocument(any(), any()); + .processDocument(any(), any(), any()); assertThrows( IllegalStatusTransitionException.class, @@ -180,7 +180,7 @@ void updateStatusWithSameStateSkipsUpdate() { mockInsertDatabaseCalls(); Result statusResult = resultWithJobStatus(SUBMITTED); lenient().when(persistenceService.run(any(CqnSelect.class))).thenReturn(statusResult); - when(documentAiProcessingService.processDocument(any(), any())).thenReturn(DIE_JOB_ID); + when(documentAiProcessingService.processDocument(any(), any(), any())).thenReturn(DIE_JOB_ID); extractionService.triggerExtraction(TEST_PDF, CONTENT_TYPE, contentStream(), null, TENANT_1); diff --git a/cds-feature-sap-document-ai/src/test/java/com/sap/cds/feature/documentai/service/client/DefaultDocumentAiClientTest.java b/cds-feature-sap-document-ai/src/test/java/com/sap/cds/feature/documentai/service/client/DefaultDocumentAiClientTest.java index 9a2226c..9508865 100644 --- a/cds-feature-sap-document-ai/src/test/java/com/sap/cds/feature/documentai/service/client/DefaultDocumentAiClientTest.java +++ b/cds-feature-sap-document-ai/src/test/java/com/sap/cds/feature/documentai/service/client/DefaultDocumentAiClientTest.java @@ -59,7 +59,7 @@ void setUp() { void submitDocumentReturnsJobIdOnSuccess() throws IOException { mockHttpResponse(200, "{\"id\":\"" + JOB_ID + "\"}"); - String result = client.submitDocument(documentInput); + String result = client.submitDocument(documentInput, null); assertThat(result).isEqualTo(JOB_ID); } @@ -68,7 +68,7 @@ void submitDocumentReturnsJobIdOnSuccess() throws IOException { void submitDocumentThrowsRequestExceptionOnNon2xxResponse() throws IOException { mockHttpResponse(400, "Bad Request"); - assertThatThrownBy(() -> client.submitDocument(documentInput)) + assertThatThrownBy(() -> client.submitDocument(documentInput, null)) .isInstanceOf(DocumentAiException.Request.class) .hasMessageContaining("400"); } @@ -78,7 +78,7 @@ void submitDocumentThrowsConnectivityExceptionOnIoFailure() throws IOException { when(httpClient.execute(any(HttpUriRequestBase.class), any(HttpClientResponseHandler.class))) .thenThrow(new IOException("timeout")); - assertThatThrownBy(() -> client.submitDocument(documentInput)) + assertThatThrownBy(() -> client.submitDocument(documentInput, null)) .isInstanceOf(DocumentAiException.Connectivity.class) .hasMessageContaining(BASE_URL); } @@ -87,7 +87,7 @@ void submitDocumentThrowsConnectivityExceptionOnIoFailure() throws IOException { void submitDocumentThrowsWhenResponseHasNoIdField() throws IOException { mockHttpResponse(200, "{\"status\":\"ok\"}"); - assertThatThrownBy(() -> client.submitDocument(documentInput)) + assertThatThrownBy(() -> client.submitDocument(documentInput, null)) .isInstanceOf(DocumentAiException.Processing.class) .hasMessageContaining("Unexpected DIE response"); } @@ -96,7 +96,7 @@ void submitDocumentThrowsWhenResponseHasNoIdField() throws IOException { void submitDocumentThrowsWhenResponseIsNotValidJson() throws IOException { mockHttpResponse(200, "not-json{{{{"); - assertThatThrownBy(() -> client.submitDocument(documentInput)) + assertThatThrownBy(() -> client.submitDocument(documentInput, null)) .isInstanceOf(DocumentAiException.Processing.class) .hasMessageContaining("Failed to parse DIE response"); } @@ -106,7 +106,7 @@ void getJobResultReturnsStatusAndRawBody() throws IOException { String responseBody = "{\"id\":\"" + JOB_ID + "\",\"status\":\"DONE\",\"extraction\":{}}"; mockHttpResponse(200, responseBody); - ExtractionData result = client.getJobResult(JOB_ID); + ExtractionData result = client.getJobResult(JOB_ID, null); assertThat(result.dieJobId()).isEqualTo(JOB_ID); assertThat(result.dieStatus()).isEqualTo("DONE"); @@ -117,7 +117,7 @@ void getJobResultReturnsStatusAndRawBody() throws IOException { void getJobResultThrowsWhenStatusFieldMissing() throws IOException { mockHttpResponse(200, "{\"id\":\"" + JOB_ID + "\",\"extraction\":{}}"); - assertThatThrownBy(() -> client.getJobResult(JOB_ID)) + assertThatThrownBy(() -> client.getJobResult(JOB_ID, null)) .isInstanceOf(DocumentAiException.Processing.class) .hasMessageContaining("missing 'status' field"); } @@ -126,7 +126,7 @@ void getJobResultThrowsWhenStatusFieldMissing() throws IOException { void getJobResultThrowsRequestExceptionOnNon2xxResponse() throws IOException { mockHttpResponse(404, "Not Found"); - assertThatThrownBy(() -> client.getJobResult(JOB_ID)) + assertThatThrownBy(() -> client.getJobResult(JOB_ID, null)) .isInstanceOf(DocumentAiException.Request.class) .hasMessageContaining("404"); } @@ -136,7 +136,7 @@ void getJobResultThrowsConnectivityExceptionOnIoFailure() throws IOException { when(httpClient.execute(any(HttpUriRequestBase.class), any(HttpClientResponseHandler.class))) .thenThrow(new IOException("timeout")); - assertThatThrownBy(() -> client.getJobResult(JOB_ID)) + assertThatThrownBy(() -> client.getJobResult(JOB_ID, null)) .isInstanceOf(DocumentAiException.Connectivity.class); } @@ -144,7 +144,7 @@ void getJobResultThrowsConnectivityExceptionOnIoFailure() throws IOException { void getJobResultThrowsWhenResponseIsNotValidJson() throws IOException { mockHttpResponse(200, "not-json{{{{"); - assertThatThrownBy(() -> client.getJobResult(JOB_ID)) + assertThatThrownBy(() -> client.getJobResult(JOB_ID, null)) .isInstanceOf(DocumentAiException.Processing.class) .hasMessageContaining("Failed to parse DIE job result response"); } @@ -155,7 +155,7 @@ void submitDocumentUsesOctetStreamWhenMimeTypeIsNull() throws IOException { new DocumentInput("invoice.pdf", null, new ByteArrayInputStream("bytes".getBytes()), null); mockHttpResponse(200, "{\"id\":\"" + JOB_ID + "\"}"); - String result = client.submitDocument(documentInput); + String result = client.submitDocument(documentInput, null); assertThat(result).isEqualTo(JOB_ID); } @@ -176,7 +176,7 @@ void submitDocumentUsesEmptyJsonWhenOptionsIsNull() throws IOException { return handler.handleResponse(response); }); - client.submitDocument(documentInput); + client.submitDocument(documentInput, null); ByteArrayOutputStream buffer = new ByteArrayOutputStream(); requestCaptor.getValue().getEntity().writeTo(buffer); @@ -184,6 +184,50 @@ void submitDocumentUsesEmptyJsonWhenOptionsIsNull() throws IOException { assertThat(requestBody).contains("{}").contains("options"); } + @Test + void submitDocument_withTenantId_appendsClientIdQueryParam() throws Exception { + mockHttpResponse(200, "{\"id\":\"" + JOB_ID + "\"}"); + + client.submitDocument(documentInput, "tenant-a"); + + ArgumentCaptor captor = ArgumentCaptor.forClass(HttpUriRequestBase.class); + verify(httpClient).execute(captor.capture(), any(HttpClientResponseHandler.class)); + assertThat(captor.getValue().getUri().toString()).contains("?clientId=tenant-a"); + } + + @Test + void submitDocument_withNullTenantId_noClientIdQueryParam() throws Exception { + mockHttpResponse(200, "{\"id\":\"" + JOB_ID + "\"}"); + + client.submitDocument(documentInput, null); + + ArgumentCaptor captor = ArgumentCaptor.forClass(HttpUriRequestBase.class); + verify(httpClient).execute(captor.capture(), any(HttpClientResponseHandler.class)); + assertThat(captor.getValue().getUri().toString()).doesNotContain("clientId"); + } + + @Test + void getJobResult_withTenantId_appendsClientIdQueryParam() throws Exception { + mockHttpResponse(200, "{\"id\":\"" + JOB_ID + "\",\"status\":\"DONE\"}"); + + client.getJobResult(JOB_ID, "tenant-a"); + + ArgumentCaptor captor = ArgumentCaptor.forClass(HttpUriRequestBase.class); + verify(httpClient).execute(captor.capture(), any(HttpClientResponseHandler.class)); + assertThat(captor.getValue().getUri().toString()).contains("&clientId=tenant-a"); + } + + @Test + void getJobResult_withNullTenantId_noClientIdQueryParam() throws Exception { + mockHttpResponse(200, "{\"id\":\"" + JOB_ID + "\",\"status\":\"DONE\"}"); + + client.getJobResult(JOB_ID, null); + + ArgumentCaptor captor = ArgumentCaptor.forClass(HttpUriRequestBase.class); + verify(httpClient).execute(captor.capture(), any(HttpClientResponseHandler.class)); + assertThat(captor.getValue().getUri().toString()).doesNotContain("clientId"); + } + @SuppressWarnings("unchecked") private void mockHttpResponse(int statusCode, String body) throws IOException { when(response.getCode()).thenReturn(statusCode); diff --git a/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/BaseIntegrationTest.java b/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/BaseIntegrationTest.java index 19a1372..3e8761d 100644 --- a/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/BaseIntegrationTest.java +++ b/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/BaseIntegrationTest.java @@ -22,8 +22,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; import org.springframework.test.web.servlet.MockMvc; @SpringBootTest diff --git a/integration-tests/spring/src/test/java/com/sap/cds/feature/documentai/integrationtest/AbstractDocumentAiTest.java b/integration-tests/spring/src/test/java/com/sap/cds/feature/documentai/integrationtest/AbstractDocumentAiTest.java index 36f2b7f..b982eb9 100644 --- a/integration-tests/spring/src/test/java/com/sap/cds/feature/documentai/integrationtest/AbstractDocumentAiTest.java +++ b/integration-tests/spring/src/test/java/com/sap/cds/feature/documentai/integrationtest/AbstractDocumentAiTest.java @@ -46,7 +46,8 @@ void runPollCycle( pollingClient(jobResultFn), null, cdsRuntime, - Duration.ZERO); + Duration.ZERO, + false); OutboxMessageEventContext ctx = EventContext.create(OutboxMessageEventContext.class, ExtractionPollingHandler.POLL_EVENT); @@ -56,12 +57,12 @@ void runPollCycle( private DocumentAiClient pollingClient(Function jobResultFn) { return new DocumentAiClient() { @Override - public String submitDocument(DocumentInput input) { + public String submitDocument(DocumentInput input, String tenantId) { throw new UnsupportedOperationException("Submission is not supported by this test client."); } @Override - public ExtractionData getJobResult(String dieJobId) { + public ExtractionData getJobResult(String dieJobId, String tenantId) { return jobResultFn.apply(dieJobId); } }; diff --git a/samples/bookshop/app/index.html b/samples/bookshop/app/index.html index 70f6315..f4b337b 100644 --- a/samples/bookshop/app/index.html +++ b/samples/bookshop/app/index.html @@ -14,8 +14,8 @@ }; - - + - - +