This is an automated email from the ASF dual-hosted git repository.

sergehuber pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/unomi.git


The following commit(s) were added to refs/heads/master by this push:
     new bb4faf115 UNOMI-986: Write a one-shot import CSV to the directory of 
its tenant (#867)
bb4faf115 is described below

commit bb4faf115faf0a28fb7e9bc4a94091a7b7e8f319
Author: Jonathan SINOVASSIN-NAIK <[email protected]>
AuthorDate: Sun Sep 27 09:43:39 2026 +0200

    UNOMI-986: Write a one-shot import CSV to the directory of its tenant (#867)
    
    Co-authored-by: Serge Huber <[email protected]>
---
 extensions/router/router-rest/pom.xml              |  5 ++
 .../rest/ImportConfigurationServiceEndPoint.java   | 28 +++++++-
 .../rest/ImportConfigurationOneshotUploadTest.java | 82 ++++++++++++++++++++++
 3 files changed, 112 insertions(+), 3 deletions(-)

diff --git a/extensions/router/router-rest/pom.xml 
b/extensions/router/router-rest/pom.xml
index 5d7f84404..ef2c4b848 100644
--- a/extensions/router/router-rest/pom.xml
+++ b/extensions/router/router-rest/pom.xml
@@ -113,6 +113,11 @@
             <artifactId>junit-jupiter</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <build>
diff --git 
a/extensions/router/router-rest/src/main/java/org/apache/unomi/router/rest/ImportConfigurationServiceEndPoint.java
 
b/extensions/router/router-rest/src/main/java/org/apache/unomi/router/rest/ImportConfigurationServiceEndPoint.java
index cbc7dcec3..8c7e4b1ef 100644
--- 
a/extensions/router/router-rest/src/main/java/org/apache/unomi/router/rest/ImportConfigurationServiceEndPoint.java
+++ 
b/extensions/router/router-rest/src/main/java/org/apache/unomi/router/rest/ImportConfigurationServiceEndPoint.java
@@ -19,8 +19,10 @@ package org.apache.unomi.router.rest;
 import org.apache.cxf.jaxrs.ext.multipart.Attachment;
 import org.apache.cxf.jaxrs.ext.multipart.Multipart;
 import org.apache.cxf.rs.security.cors.CrossOriginResourceSharing;
+import org.apache.unomi.api.ExecutionContext;
 import org.apache.unomi.api.security.UnomiRoles;
 import org.apache.unomi.api.services.ConfigSharingService;
+import org.apache.unomi.api.services.ExecutionContextManager;
 import org.apache.unomi.rest.security.RequiresRole;
 import org.apache.unomi.router.api.ImportConfiguration;
 import org.apache.unomi.router.api.RouterConstants;
@@ -65,10 +67,17 @@ public class ImportConfigurationServiceEndPoint extends 
AbstractConfigurationSer
     @Reference
     protected ConfigSharingService configSharingService;
 
+    @Reference
+    protected ExecutionContextManager executionContextManager;
+
     public void setConfigSharingService(ConfigSharingService 
configSharingService) {
         this.configSharingService = configSharingService;
     }
 
+    public void setExecutionContextManager(ExecutionContextManager 
executionContextManager) {
+        this.executionContextManager = executionContextManager;
+    }
+
     public ImportConfigurationServiceEndPoint() throws KeyStoreException, 
NoSuchAlgorithmException, KeyManagementException {
         LOGGER.info("Initializing import configuration service endpoint...");
     }
@@ -134,13 +143,15 @@ public class ImportConfigurationServiceEndPoint extends 
AbstractConfigurationSer
     /**
      * Uploads a one-shot CSV file for an existing import configuration.
      * <p>
-     * The file is stored under the configured one-shot upload directory as 
{@code {importConfigId}.csv}
-     * for subsequent Camel processing.
+     * The file is stored under the configured one-shot upload directory, in a 
sub-directory named
+     * after the current tenant, as {@code {tenantId}/{importConfigId}.csv}. 
The Camel route reads the
+     * tenant from that directory name.
      *
      * @param importConfigId the import configuration id (multipart field)
      * @param file the CSV file upload (multipart field)
      * @return an empty success response
      * @api.status 200 empty CSV uploaded and ready for processing.
+     * @api.status 400 empty No current tenant context.
      * @api.status 500 empty Error writing the uploaded file.
      * @api.example {}
      */
@@ -150,8 +161,19 @@ public class ImportConfigurationServiceEndPoint extends 
AbstractConfigurationSer
     @Produces(MediaType.APPLICATION_JSON)
     public Response processOneshotImportConfigurationCSV(@Multipart(value = 
"importConfigId") @NotNull @Pattern(regexp = "^[a-zA-Z0-9_.\\-]{1,255}$") 
String importConfigId,
                                                          @Multipart(value = 
"file") Attachment file) {
+        ExecutionContext context = executionContextManager.getCurrentContext();
+        if (context == null || context.getTenantId() == null || 
context.getTenantId().trim().isEmpty()) {
+            LOGGER.warn("One-shot import refused: no current tenant");
+            return Response.status(Response.Status.BAD_REQUEST).build();
+        }
+        String tenantId = context.getTenantId();
         try {
-            java.nio.file.Path path = 
Paths.get(configSharingService.getProperty(RouterConstants.IMPORT_ONESHOT_UPLOAD_DIR)
 + importConfigId + ".csv");
+            // The Camel route reads the tenant from the directory holding the 
file, so the upload
+            // writes it there. Without that directory 
ImportConfigByFileNameProcessor answers
+            // "Invalid or missing tenant ID in path" and drops the file, and 
the import never runs.
+            java.nio.file.Path tenantDir = 
Paths.get(String.valueOf(configSharingService.getProperty(RouterConstants.IMPORT_ONESHOT_UPLOAD_DIR)),
 tenantId);
+            Files.createDirectories(tenantDir);
+            java.nio.file.Path path = tenantDir.resolve(importConfigId + 
".csv");
             Files.deleteIfExists(path);
             InputStream in = file.getObject(InputStream.class);
 
diff --git 
a/extensions/router/router-rest/src/test/java/org/apache/unomi/router/rest/ImportConfigurationOneshotUploadTest.java
 
b/extensions/router/router-rest/src/test/java/org/apache/unomi/router/rest/ImportConfigurationOneshotUploadTest.java
new file mode 100644
index 000000000..86414903d
--- /dev/null
+++ 
b/extensions/router/router-rest/src/test/java/org/apache/unomi/router/rest/ImportConfigurationOneshotUploadTest.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.unomi.router.rest;
+
+import org.apache.cxf.jaxrs.ext.multipart.Attachment;
+import org.apache.unomi.api.ExecutionContext;
+import org.apache.unomi.api.services.ConfigSharingService;
+import org.apache.unomi.api.services.ExecutionContextManager;
+import org.apache.unomi.router.api.RouterConstants;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import javax.ws.rs.core.Response;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * The one-shot upload must land where the Camel route reads it: under the 
current tenant.
+ */
+class ImportConfigurationOneshotUploadTest {
+
+    @TempDir
+    Path uploadDir;
+
+    @Test
+    void writesTheCsvUnderTheCurrentTenantDirectory() throws Exception {
+        ImportConfigurationServiceEndPoint endpoint = endpointWith(new 
ExecutionContext("acme", null, null));
+        Attachment file = mock(Attachment.class);
+        doReturn(new 
ByteArrayInputStream("email\n".getBytes(StandardCharsets.UTF_8)))
+                .when(file).getObject(InputStream.class);
+
+        Response response = 
endpoint.processOneshotImportConfigurationCSV("profile-csv", file);
+
+        assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+        Path written = uploadDir.resolve("acme").resolve("profile-csv.csv");
+        assertEquals("email\n", new String(Files.readAllBytes(written), 
StandardCharsets.UTF_8));
+    }
+
+    @Test
+    void missingContextIsRejectedWithoutWriting() throws Exception {
+        ImportConfigurationServiceEndPoint endpoint = endpointWith(null);
+
+        Response response = 
endpoint.processOneshotImportConfigurationCSV("profile-csv", 
mock(Attachment.class));
+
+        assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), 
response.getStatus());
+        assertFalse(Files.exists(uploadDir.resolve("profile-csv.csv")));
+    }
+
+    private ImportConfigurationServiceEndPoint endpointWith(ExecutionContext 
context) throws Exception {
+        ImportConfigurationServiceEndPoint endpoint = new 
ImportConfigurationServiceEndPoint();
+        ConfigSharingService configSharingService = 
mock(ConfigSharingService.class);
+        
when(configSharingService.getProperty(RouterConstants.IMPORT_ONESHOT_UPLOAD_DIR)).thenReturn(uploadDir.toString());
+        ExecutionContextManager executionContextManager = 
mock(ExecutionContextManager.class);
+        when(executionContextManager.getCurrentContext()).thenReturn(context);
+        endpoint.setConfigSharingService(configSharingService);
+        endpoint.setExecutionContextManager(executionContextManager);
+        return endpoint;
+    }
+}

Reply via email to