From a583e226645885a9bb3c0a5c7d703cc7cee250c5 Mon Sep 17 00:00:00 2001 From: Mazin Date: Wed, 10 Dec 2025 10:23:13 +0100 Subject: [PATCH 01/11] feat: add support for multiple file types in catalog uploads * validates .csv, .json and .jsonl * derive the file extension from the uploaded file. --- .../io/constructor/client/ConstructorIO.java | 80 ++++++++++++++++--- 1 file changed, 68 insertions(+), 12 deletions(-) diff --git a/constructorio-client/src/main/java/io/constructor/client/ConstructorIO.java b/constructorio-client/src/main/java/io/constructor/client/ConstructorIO.java index 0ac7968a..c60ad280 100644 --- a/constructorio-client/src/main/java/io/constructor/client/ConstructorIO.java +++ b/constructorio-client/src/main/java/io/constructor/client/ConstructorIO.java @@ -2031,6 +2031,50 @@ protected static String getResponseBody(Response response) throws ConstructorExc throw new ConstructorException(errorMessage, errorCode); } + /** + * Validates and extracts the file extension from a File object for catalog uploads. + * Only .csv, .json, and .jsonl extensions are supported. + * + * @param file the File object containing the actual file + * @param fileName the logical file name (items, variations, item_groups) + * @return the validated file extension (including the dot, e.g., ".csv", ".json", or ".jsonl") + * @throws ConstructorException if the file extension is invalid or missing + */ + private static String getValidatedFileExtension(File file, String fileName) + throws ConstructorException { + if (file == null) { + throw new ConstructorException( + "Invalid file for '" + fileName + "': file cannot be null."); + } + + String actualFileName = file.getName(); + if (actualFileName == null || actualFileName.isEmpty()) { + throw new ConstructorException( + "Invalid file for '" + fileName + "': file name cannot be empty."); + } + + int lastDotIndex = actualFileName.lastIndexOf('.'); + if (lastDotIndex == -1 || lastDotIndex == actualFileName.length() - 1) { + throw new ConstructorException( + "Invalid file for '" + + fileName + + "': file must have .csv, .json, or .jsonl extension. Found: " + + actualFileName); + } + + String extension = actualFileName.substring(lastDotIndex).toLowerCase(); + + if (!extension.equals(".csv") && !extension.equals(".json") && !extension.equals(".jsonl")) { + throw new ConstructorException( + "Invalid file type for '" + + fileName + + "': file must have .csv, .json, or .jsonl extension. Found: " + + actualFileName); + } + + return extension; + } + /** * Grabs the version number (hard coded ATM) * @@ -2308,9 +2352,12 @@ protected static JSONArray transformItemsAPIV2Response(JSONArray results) { /** * Send a full catalog to replace the current one (sync) * - * @param req the catalog request - * @return a string of JSON - * @throws ConstructorException if the request is invalid. + * Supports CSV, JSON, and JSONL file formats. The file type is automatically + * detected from the file extension (.csv, .json, or .jsonl). + * + * @param req the catalog request containing files with .csv, .json, or .jsonl extensions + * @return a string of JSON containing task information + * @throws ConstructorException if the request is invalid or file extensions are not supported */ public String replaceCatalog(CatalogRequest req) throws ConstructorException { try { @@ -2336,10 +2383,11 @@ public String replaceCatalog(CatalogRequest req) throws ConstructorException { for (Map.Entry entry : files.entrySet()) { String fileName = entry.getKey(); File file = entry.getValue(); + String fileExtension = getValidatedFileExtension(file, fileName); multipartBuilder.addFormDataPart( fileName, - fileName + ".csv", + fileName + fileExtension, RequestBody.create(MediaType.parse("application/octet-stream"), file)); } } @@ -2365,9 +2413,12 @@ public String replaceCatalog(CatalogRequest req) throws ConstructorException { /** * Send a partial catalog to update specific items (delta) * - * @param req the catalog request - * @return a string of JSON - * @throws ConstructorException if the request is invalid. + * Supports CSV, JSON, and JSONL file formats. The file type is automatically + * detected from the file extension (.csv, .json, or .jsonl). + * + * @param req the catalog request containing files with .csv, .json, or .jsonl extensions + * @return a string of JSON containing task information + * @throws ConstructorException if the request is invalid or file extensions are not supported */ public String updateCatalog(CatalogRequest req) throws ConstructorException { try { @@ -2393,10 +2444,11 @@ public String updateCatalog(CatalogRequest req) throws ConstructorException { for (Map.Entry entry : files.entrySet()) { String fileName = entry.getKey(); File file = entry.getValue(); + String fileExtension = getValidatedFileExtension(file, fileName); multipartBuilder.addFormDataPart( fileName, - fileName + ".csv", + fileName + fileExtension, RequestBody.create(MediaType.parse("application/octet-stream"), file)); } } @@ -2423,9 +2475,12 @@ public String updateCatalog(CatalogRequest req) throws ConstructorException { /** * Send a patch delta catalog to update specific items (delta) * - * @param req the catalog request - * @return a string of JSON - * @throws ConstructorException if the request is invalid. + * Supports CSV, JSON, and JSONL file formats. The file type is automatically + * detected from the file extension (.csv, .json, or .jsonl). + * + * @param req the catalog request containing files with .csv, .json, or .jsonl extensions + * @return a string of JSON containing task information + * @throws ConstructorException if the request is invalid or file extensions are not supported */ public String patchCatalog(CatalogRequest req) throws ConstructorException { try { @@ -2456,10 +2511,11 @@ public String patchCatalog(CatalogRequest req) throws ConstructorException { for (Map.Entry entry : files.entrySet()) { String fileName = entry.getKey(); File file = entry.getValue(); + String fileExtension = getValidatedFileExtension(file, fileName); multipartBuilder.addFormDataPart( fileName, - fileName + ".csv", + fileName + fileExtension, RequestBody.create(MediaType.parse("application/octet-stream"), file)); } } From 7384c92d4804ce236a5bce4f04d34bbb65c19c9e Mon Sep 17 00:00:00 2001 From: Mazin Date: Wed, 10 Dec 2025 10:23:28 +0100 Subject: [PATCH 02/11] chore: added relevant tests --- .../client/ConstructorIOCatalogTest.java | 209 ++++++++++++++++++ .../src/test/resources/invalid/items | 1 + .../src/test/resources/invalid/items.txt | 1 + .../src/test/resources/json/items.json | 23 ++ .../src/test/resources/json/variations.json | 18 ++ .../test/resources/jsonl/item_groups.jsonl | 2 + .../src/test/resources/jsonl/items.jsonl | 3 + .../src/test/resources/jsonl/variations.jsonl | 3 + 8 files changed, 260 insertions(+) create mode 100644 constructorio-client/src/test/resources/invalid/items create mode 100644 constructorio-client/src/test/resources/invalid/items.txt create mode 100644 constructorio-client/src/test/resources/json/items.json create mode 100644 constructorio-client/src/test/resources/json/variations.json create mode 100644 constructorio-client/src/test/resources/jsonl/item_groups.jsonl create mode 100644 constructorio-client/src/test/resources/jsonl/items.jsonl create mode 100644 constructorio-client/src/test/resources/jsonl/variations.jsonl diff --git a/constructorio-client/src/test/java/io/constructor/client/ConstructorIOCatalogTest.java b/constructorio-client/src/test/java/io/constructor/client/ConstructorIOCatalogTest.java index 260b5513..a6e388cc 100644 --- a/constructorio-client/src/test/java/io/constructor/client/ConstructorIOCatalogTest.java +++ b/constructorio-client/src/test/java/io/constructor/client/ConstructorIOCatalogTest.java @@ -405,4 +405,213 @@ public void PatchCatalogWithItemsAndVariationsAndItemGroupsFilesShouldReturnTask assertTrue("task_id exists", jsonObj.has("task_id") == true); assertTrue("task_status_path exists", jsonObj.has("task_status_path") == true); } + + // JSONL Format Tests + + @Test + public void ReplaceCatalogWithJsonlItemsFileShouldReturnTaskInfo() throws Exception { + ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); + Map files = new HashMap(); + + files.put("items", new File("src/test/resources/jsonl/items.jsonl")); + + CatalogRequest req = new CatalogRequest(files, "Products"); + String response = constructor.replaceCatalog(req); + JSONObject jsonObj = new JSONObject(response); + + assertTrue("task_id exists", jsonObj.has("task_id") == true); + assertTrue("task_status_path exists", jsonObj.has("task_status_path") == true); + } + + @Test + public void UpdateCatalogWithJsonlItemsFileShouldReturnTaskInfo() throws Exception { + ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); + Map files = new HashMap(); + + files.put("items", new File("src/test/resources/jsonl/items.jsonl")); + + CatalogRequest req = new CatalogRequest(files, "Products"); + String response = constructor.updateCatalog(req); + JSONObject jsonObj = new JSONObject(response); + + assertTrue("task_id exists", jsonObj.has("task_id") == true); + assertTrue("task_status_path exists", jsonObj.has("task_status_path") == true); + } + + @Test + public void PatchCatalogWithJsonlItemsFileShouldReturnTaskInfo() throws Exception { + ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); + Map files = new HashMap(); + + files.put("items", new File("src/test/resources/jsonl/items.jsonl")); + + CatalogRequest req = new CatalogRequest(files, "Products"); + String response = constructor.patchCatalog(req); + JSONObject jsonObj = new JSONObject(response); + + assertTrue("task_id exists", jsonObj.has("task_id") == true); + assertTrue("task_status_path exists", jsonObj.has("task_status_path") == true); + } + + // Invalid Extension Tests + + @Test + public void ReplaceCatalogWithInvalidExtensionShouldError() throws Exception { + ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); + Map files = new HashMap(); + + files.put("items", new File("src/test/resources/invalid/items.txt")); + + CatalogRequest req = new CatalogRequest(files, "Products"); + + thrown.expect(ConstructorException.class); + thrown.expectMessage("Invalid file type for 'items'"); + thrown.expectMessage("must have .csv or .jsonl extension"); + constructor.replaceCatalog(req); + } + + @Test + public void UpdateCatalogWithNoExtensionShouldError() throws Exception { + ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); + Map files = new HashMap(); + + files.put("items", new File("src/test/resources/invalid/items")); + + CatalogRequest req = new CatalogRequest(files, "Products"); + + thrown.expect(ConstructorException.class); + thrown.expectMessage("Invalid file for 'items'"); + thrown.expectMessage("must have .csv or .jsonl extension"); + constructor.updateCatalog(req); + } + + @Test + public void PatchCatalogWithInvalidExtensionShouldError() throws Exception { + ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); + Map files = new HashMap(); + + files.put("items", new File("src/test/resources/invalid/items.txt")); + + CatalogRequest req = new CatalogRequest(files, "Products"); + + thrown.expect(ConstructorException.class); + thrown.expectMessage("Invalid file type for 'items'"); + thrown.expectMessage("must have .csv or .jsonl extension"); + constructor.patchCatalog(req); + } + + // Edge Case Tests + + @Test + public void ReplaceCatalogWithMixedFileTypesShouldSucceed() throws Exception { + ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); + Map files = new HashMap(); + + files.put("items", new File("src/test/resources/csv/items.csv")); + files.put("variations", new File("src/test/resources/jsonl/variations.jsonl")); + + CatalogRequest req = new CatalogRequest(files, "Products"); + String response = constructor.replaceCatalog(req); + JSONObject jsonObj = new JSONObject(response); + + assertTrue("task_id exists", jsonObj.has("task_id") == true); + assertTrue("task_status_path exists", jsonObj.has("task_status_path") == true); + } + + @Test + public void UpdateCatalogWithJsonlVariationsAndItemGroupsShouldSucceed() throws Exception { + ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); + Map files = new HashMap(); + + files.put("variations", new File("src/test/resources/jsonl/variations.jsonl")); + files.put("item_groups", new File("src/test/resources/jsonl/item_groups.jsonl")); + + CatalogRequest req = new CatalogRequest(files, "Products"); + String response = constructor.updateCatalog(req); + JSONObject jsonObj = new JSONObject(response); + + assertTrue("task_id exists", jsonObj.has("task_id") == true); + assertTrue("task_status_path exists", jsonObj.has("task_status_path") == true); + } + + @Test + public void PatchCatalogWithAllJsonlFilesShouldSucceed() throws Exception { + ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); + Map files = new HashMap(); + + files.put("items", new File("src/test/resources/jsonl/items.jsonl")); + files.put("variations", new File("src/test/resources/jsonl/variations.jsonl")); + files.put("item_groups", new File("src/test/resources/jsonl/item_groups.jsonl")); + + CatalogRequest req = new CatalogRequest(files, "Products"); + String response = constructor.patchCatalog(req); + JSONObject jsonObj = new JSONObject(response); + + assertTrue("task_id exists", jsonObj.has("task_id") == true); + assertTrue("task_status_path exists", jsonObj.has("task_status_path") == true); + } + + // JSON Format Tests + + @Test + public void ReplaceCatalogWithJsonItemsFileShouldReturnTaskInfo() throws Exception { + ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); + Map files = new HashMap(); + + files.put("items", new File("src/test/resources/json/items.json")); + + CatalogRequest req = new CatalogRequest(files, "Products"); + String response = constructor.replaceCatalog(req); + JSONObject jsonObj = new JSONObject(response); + + assertTrue("task_id exists", jsonObj.has("task_id") == true); + assertTrue("task_status_path exists", jsonObj.has("task_status_path") == true); + } + + @Test + public void UpdateCatalogWithJsonItemsFileShouldReturnTaskInfo() throws Exception { + ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); + Map files = new HashMap(); + + files.put("items", new File("src/test/resources/json/items.json")); + + CatalogRequest req = new CatalogRequest(files, "Products"); + String response = constructor.updateCatalog(req); + JSONObject jsonObj = new JSONObject(response); + + assertTrue("task_id exists", jsonObj.has("task_id") == true); + assertTrue("task_status_path exists", jsonObj.has("task_status_path") == true); + } + + @Test + public void PatchCatalogWithJsonItemsFileShouldReturnTaskInfo() throws Exception { + ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); + Map files = new HashMap(); + + files.put("items", new File("src/test/resources/json/items.json")); + + CatalogRequest req = new CatalogRequest(files, "Products"); + String response = constructor.patchCatalog(req); + JSONObject jsonObj = new JSONObject(response); + + assertTrue("task_id exists", jsonObj.has("task_id") == true); + assertTrue("task_status_path exists", jsonObj.has("task_status_path") == true); + } + + @Test + public void ReplaceCatalogWithMixedCsvJsonJsonlShouldSucceed() throws Exception { + ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); + Map files = new HashMap(); + + files.put("items", new File("src/test/resources/csv/items.csv")); + files.put("variations", new File("src/test/resources/json/variations.json")); + files.put("item_groups", new File("src/test/resources/jsonl/item_groups.jsonl")); + + CatalogRequest req = new CatalogRequest(files, "Products"); + String response = constructor.replaceCatalog(req); + JSONObject jsonObj = new JSONObject(response); + + assertTrue("task_id exists", jsonObj.has("task_id") == true); + assertTrue("task_status_path exists", jsonObj.has("task_status_path") == true); + } } diff --git a/constructorio-client/src/test/resources/invalid/items b/constructorio-client/src/test/resources/invalid/items new file mode 100644 index 00000000..a992b237 --- /dev/null +++ b/constructorio-client/src/test/resources/invalid/items @@ -0,0 +1 @@ +This file has no extension for testing validation. diff --git a/constructorio-client/src/test/resources/invalid/items.txt b/constructorio-client/src/test/resources/invalid/items.txt new file mode 100644 index 00000000..887c87ec --- /dev/null +++ b/constructorio-client/src/test/resources/invalid/items.txt @@ -0,0 +1 @@ +This is a text file with invalid extension for catalog upload testing. diff --git a/constructorio-client/src/test/resources/json/items.json b/constructorio-client/src/test/resources/json/items.json new file mode 100644 index 00000000..69b7f47f --- /dev/null +++ b/constructorio-client/src/test/resources/json/items.json @@ -0,0 +1,23 @@ +[ + { + "id": "item1", + "value": "Product 1", + "data": { + "url": "https://example.com/product1" + } + }, + { + "id": "item2", + "value": "Product 2", + "data": { + "url": "https://example.com/product2" + } + }, + { + "id": "item3", + "value": "Product 3", + "data": { + "url": "https://example.com/product3" + } + } +] diff --git a/constructorio-client/src/test/resources/json/variations.json b/constructorio-client/src/test/resources/json/variations.json new file mode 100644 index 00000000..45fb9621 --- /dev/null +++ b/constructorio-client/src/test/resources/json/variations.json @@ -0,0 +1,18 @@ +[ + { + "id": "var1", + "item_id": "item1", + "value": "Product 1 Variation A", + "data": { + "color": "red" + } + }, + { + "id": "var2", + "item_id": "item1", + "value": "Product 1 Variation B", + "data": { + "color": "blue" + } + } +] diff --git a/constructorio-client/src/test/resources/jsonl/item_groups.jsonl b/constructorio-client/src/test/resources/jsonl/item_groups.jsonl new file mode 100644 index 00000000..2809502e --- /dev/null +++ b/constructorio-client/src/test/resources/jsonl/item_groups.jsonl @@ -0,0 +1,2 @@ +{"id":"group1","value":"Group 1","data":{"parent_id":"root"}} +{"id":"group2","value":"Group 2","data":{"parent_id":"root"}} diff --git a/constructorio-client/src/test/resources/jsonl/items.jsonl b/constructorio-client/src/test/resources/jsonl/items.jsonl new file mode 100644 index 00000000..b3c5f58b --- /dev/null +++ b/constructorio-client/src/test/resources/jsonl/items.jsonl @@ -0,0 +1,3 @@ +{"id":"item1","value":"Product 1","data":{"url":"https://example.com/product1"}} +{"id":"item2","value":"Product 2","data":{"url":"https://example.com/product2"}} +{"id":"item3","value":"Product 3","data":{"url":"https://example.com/product3"}} diff --git a/constructorio-client/src/test/resources/jsonl/variations.jsonl b/constructorio-client/src/test/resources/jsonl/variations.jsonl new file mode 100644 index 00000000..5b17177f --- /dev/null +++ b/constructorio-client/src/test/resources/jsonl/variations.jsonl @@ -0,0 +1,3 @@ +{"id":"var1","item_id":"item1","value":"Product 1 Variation A","data":{"color":"red"}} +{"id":"var2","item_id":"item1","value":"Product 1 Variation B","data":{"color":"blue"}} +{"id":"var3","item_id":"item2","value":"Product 2 Variation A","data":{"color":"green"}} From 9ec73c416672a379ce3e7fee982f817059be8f28 Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Mon, 29 Dec 2025 21:33:32 +0300 Subject: [PATCH 03/11] Update tests --- .../io/constructor/client/ConstructorIOCatalogTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/constructorio-client/src/test/java/io/constructor/client/ConstructorIOCatalogTest.java b/constructorio-client/src/test/java/io/constructor/client/ConstructorIOCatalogTest.java index a6e388cc..7ef3c295 100644 --- a/constructorio-client/src/test/java/io/constructor/client/ConstructorIOCatalogTest.java +++ b/constructorio-client/src/test/java/io/constructor/client/ConstructorIOCatalogTest.java @@ -466,7 +466,7 @@ public void ReplaceCatalogWithInvalidExtensionShouldError() throws Exception { thrown.expect(ConstructorException.class); thrown.expectMessage("Invalid file type for 'items'"); - thrown.expectMessage("must have .csv or .jsonl extension"); + thrown.expectMessage("must have .csv, .json, or .jsonl extension"); constructor.replaceCatalog(req); } @@ -481,7 +481,7 @@ public void UpdateCatalogWithNoExtensionShouldError() throws Exception { thrown.expect(ConstructorException.class); thrown.expectMessage("Invalid file for 'items'"); - thrown.expectMessage("must have .csv or .jsonl extension"); + thrown.expectMessage("must have .csv, .json, or .jsonl extension"); constructor.updateCatalog(req); } @@ -496,7 +496,7 @@ public void PatchCatalogWithInvalidExtensionShouldError() throws Exception { thrown.expect(ConstructorException.class); thrown.expectMessage("Invalid file type for 'items'"); - thrown.expectMessage("must have .csv or .jsonl extension"); + thrown.expectMessage("must have .csv, .json, or .jsonl extension"); constructor.patchCatalog(req); } From ce6d7b65b10861f458600f06194bc412bf439240 Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Mon, 29 Dec 2025 21:40:13 +0300 Subject: [PATCH 04/11] Utilize set --- .../io/constructor/client/ConstructorIO.java | 16 ++++++++--- .../client/ConstructorIOCatalogTest.java | 27 ++++++++++++++++--- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/constructorio-client/src/main/java/io/constructor/client/ConstructorIO.java b/constructorio-client/src/main/java/io/constructor/client/ConstructorIO.java index c60ad280..96a150e8 100644 --- a/constructorio-client/src/main/java/io/constructor/client/ConstructorIO.java +++ b/constructorio-client/src/main/java/io/constructor/client/ConstructorIO.java @@ -9,8 +9,10 @@ import java.util.Arrays; import java.util.Base64; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.TimeUnit; import okhttp3.Call; import okhttp3.Callback; @@ -32,6 +34,10 @@ /** Constructor.io Client */ public class ConstructorIO { + /** Valid file extensions for catalog uploads */ + private static final Set VALID_CATALOG_EXTENSIONS = + new HashSet<>(Arrays.asList(".csv", ".json", ".jsonl")); + /** the HTTP client used by all instances */ private static OkHttpClient client = new OkHttpClient.Builder() @@ -2058,17 +2064,21 @@ private static String getValidatedFileExtension(File file, String fileName) throw new ConstructorException( "Invalid file for '" + fileName - + "': file must have .csv, .json, or .jsonl extension. Found: " + + "': file must have " + + VALID_CATALOG_EXTENSIONS + + " extension. Found: " + actualFileName); } String extension = actualFileName.substring(lastDotIndex).toLowerCase(); - if (!extension.equals(".csv") && !extension.equals(".json") && !extension.equals(".jsonl")) { + if (!VALID_CATALOG_EXTENSIONS.contains(extension)) { throw new ConstructorException( "Invalid file type for '" + fileName - + "': file must have .csv, .json, or .jsonl extension. Found: " + + "': file must have " + + VALID_CATALOG_EXTENSIONS + + " extension. Found: " + actualFileName); } diff --git a/constructorio-client/src/test/java/io/constructor/client/ConstructorIOCatalogTest.java b/constructorio-client/src/test/java/io/constructor/client/ConstructorIOCatalogTest.java index 7ef3c295..7805be5d 100644 --- a/constructorio-client/src/test/java/io/constructor/client/ConstructorIOCatalogTest.java +++ b/constructorio-client/src/test/java/io/constructor/client/ConstructorIOCatalogTest.java @@ -466,7 +466,9 @@ public void ReplaceCatalogWithInvalidExtensionShouldError() throws Exception { thrown.expect(ConstructorException.class); thrown.expectMessage("Invalid file type for 'items'"); - thrown.expectMessage("must have .csv, .json, or .jsonl extension"); + thrown.expectMessage(".csv"); + thrown.expectMessage(".json"); + thrown.expectMessage(".jsonl"); constructor.replaceCatalog(req); } @@ -481,7 +483,9 @@ public void UpdateCatalogWithNoExtensionShouldError() throws Exception { thrown.expect(ConstructorException.class); thrown.expectMessage("Invalid file for 'items'"); - thrown.expectMessage("must have .csv, .json, or .jsonl extension"); + thrown.expectMessage(".csv"); + thrown.expectMessage(".json"); + thrown.expectMessage(".jsonl"); constructor.updateCatalog(req); } @@ -496,12 +500,29 @@ public void PatchCatalogWithInvalidExtensionShouldError() throws Exception { thrown.expect(ConstructorException.class); thrown.expectMessage("Invalid file type for 'items'"); - thrown.expectMessage("must have .csv, .json, or .jsonl extension"); + thrown.expectMessage(".csv"); + thrown.expectMessage(".json"); + thrown.expectMessage(".jsonl"); constructor.patchCatalog(req); } // Edge Case Tests + @Test + public void ReplaceCatalogWithNullFileShouldError() throws Exception { + ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); + Map files = new HashMap(); + + files.put("items", null); + + CatalogRequest req = new CatalogRequest(files, "Products"); + + thrown.expect(ConstructorException.class); + thrown.expectMessage("Invalid file for 'items'"); + thrown.expectMessage("file cannot be null"); + constructor.replaceCatalog(req); + } + @Test public void ReplaceCatalogWithMixedFileTypesShouldSucceed() throws Exception { ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); From 66b49529be95918b1064424c61380bf1a2012d25 Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Mon, 29 Dec 2025 21:44:29 +0300 Subject: [PATCH 05/11] Use LinkedHashSet --- .../main/java/io/constructor/client/ConstructorIO.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/constructorio-client/src/main/java/io/constructor/client/ConstructorIO.java b/constructorio-client/src/main/java/io/constructor/client/ConstructorIO.java index 96a150e8..bc56f4e2 100644 --- a/constructorio-client/src/main/java/io/constructor/client/ConstructorIO.java +++ b/constructorio-client/src/main/java/io/constructor/client/ConstructorIO.java @@ -9,7 +9,7 @@ import java.util.Arrays; import java.util.Base64; import java.util.HashMap; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -36,7 +36,7 @@ public class ConstructorIO { /** Valid file extensions for catalog uploads */ private static final Set VALID_CATALOG_EXTENSIONS = - new HashSet<>(Arrays.asList(".csv", ".json", ".jsonl")); + new LinkedHashSet<>(Arrays.asList(".csv", ".json", ".jsonl")); /** the HTTP client used by all instances */ private static OkHttpClient client = @@ -2039,12 +2039,11 @@ protected static String getResponseBody(Response response) throws ConstructorExc /** * Validates and extracts the file extension from a File object for catalog uploads. - * Only .csv, .json, and .jsonl extensions are supported. * * @param file the File object containing the actual file * @param fileName the logical file name (items, variations, item_groups) - * @return the validated file extension (including the dot, e.g., ".csv", ".json", or ".jsonl") - * @throws ConstructorException if the file extension is invalid or missing + * @return the validated file extension (including the dot) + * @throws ConstructorException if the file extension is not in VALID_CATALOG_EXTENSIONS */ private static String getValidatedFileExtension(File file, String fileName) throws ConstructorException { From 936d1f45a4935197020d1d9dedd046933a5f09d1 Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Mon, 29 Dec 2025 21:57:17 +0300 Subject: [PATCH 06/11] Use generators instead of static files --- .../client/ConstructorIOCatalogTest.java | 74 ++++-- .../java/io/constructor/client/Utils.java | 230 ++++++++++++++++++ .../src/test/resources/invalid/items | 1 - .../src/test/resources/invalid/items.txt | 1 - .../src/test/resources/json/items.json | 23 -- .../src/test/resources/json/variations.json | 18 -- .../test/resources/jsonl/item_groups.jsonl | 2 - .../src/test/resources/jsonl/items.jsonl | 3 - .../src/test/resources/jsonl/variations.jsonl | 3 - 9 files changed, 284 insertions(+), 71 deletions(-) delete mode 100644 constructorio-client/src/test/resources/invalid/items delete mode 100644 constructorio-client/src/test/resources/invalid/items.txt delete mode 100644 constructorio-client/src/test/resources/json/items.json delete mode 100644 constructorio-client/src/test/resources/json/variations.json delete mode 100644 constructorio-client/src/test/resources/jsonl/item_groups.jsonl delete mode 100644 constructorio-client/src/test/resources/jsonl/items.jsonl delete mode 100644 constructorio-client/src/test/resources/jsonl/variations.jsonl diff --git a/constructorio-client/src/test/java/io/constructor/client/ConstructorIOCatalogTest.java b/constructorio-client/src/test/java/io/constructor/client/ConstructorIOCatalogTest.java index 7805be5d..c427799c 100644 --- a/constructorio-client/src/test/java/io/constructor/client/ConstructorIOCatalogTest.java +++ b/constructorio-client/src/test/java/io/constructor/client/ConstructorIOCatalogTest.java @@ -22,10 +22,18 @@ public class ConstructorIOCatalogTest { private File itemsFile = new File("src/test/resources/csv/items.csv"); private File variationsFile = new File("src/test/resources/csv/variations.csv"); private File itemGroupsFile = new File("src/test/resources/csv/item_groups.csv"); - private String baseUrl = - "https://raw.githubusercontent.com/Constructor-io/integration-examples/main/catalog/"; + private String baseUrl = "https://raw.githubusercontent.com/Constructor-io/integration-examples/main/catalog/"; - @Rule public ExpectedException thrown = ExpectedException.none(); + private File jsonItemsFile; + private File jsonVariationsFile; + private File jsonlItemsFile; + private File jsonlVariationsFile; + private File jsonlItemGroupsFile; + private File invalidExtensionFile; + private File noExtensionFile; + + @Rule + public ExpectedException thrown = ExpectedException.none(); @Before public void init() throws Exception { @@ -37,14 +45,40 @@ public void init() throws Exception { URL itemGroupsUrl = new URL(baseUrl + "item_groups.csv"); FileUtils.copyURLToFile(itemGroupsUrl, itemGroupsFile); + + // Generate JSON/JSONL/invalid files + jsonItemsFile = Utils.createItemsJsonFile(3); + jsonVariationsFile = Utils.createVariationsJsonFile(2); + jsonlItemsFile = Utils.createItemsJsonlFile(3); + jsonlVariationsFile = Utils.createVariationsJsonlFile(3); + jsonlItemGroupsFile = Utils.createItemGroupsJsonlFile(2); + invalidExtensionFile = Utils.createInvalidExtensionFile(); + noExtensionFile = Utils.createNoExtensionFile(); } @After public void teardown() throws Exception { + // Clean up CSV files itemsFile.delete(); variationsFile.delete(); itemGroupsFile.delete(); csvFolder.delete(); + + // Clean up generated files + if (jsonItemsFile != null) + jsonItemsFile.delete(); + if (jsonVariationsFile != null) + jsonVariationsFile.delete(); + if (jsonlItemsFile != null) + jsonlItemsFile.delete(); + if (jsonlVariationsFile != null) + jsonlVariationsFile.delete(); + if (jsonlItemGroupsFile != null) + jsonlItemGroupsFile.delete(); + if (invalidExtensionFile != null) + invalidExtensionFile.delete(); + if (noExtensionFile != null) + noExtensionFile.delete(); } @Test @@ -413,7 +447,7 @@ public void ReplaceCatalogWithJsonlItemsFileShouldReturnTaskInfo() throws Except ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); Map files = new HashMap(); - files.put("items", new File("src/test/resources/jsonl/items.jsonl")); + files.put("items", jsonlItemsFile); CatalogRequest req = new CatalogRequest(files, "Products"); String response = constructor.replaceCatalog(req); @@ -428,7 +462,7 @@ public void UpdateCatalogWithJsonlItemsFileShouldReturnTaskInfo() throws Excepti ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); Map files = new HashMap(); - files.put("items", new File("src/test/resources/jsonl/items.jsonl")); + files.put("items", jsonlItemsFile); CatalogRequest req = new CatalogRequest(files, "Products"); String response = constructor.updateCatalog(req); @@ -443,7 +477,7 @@ public void PatchCatalogWithJsonlItemsFileShouldReturnTaskInfo() throws Exceptio ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); Map files = new HashMap(); - files.put("items", new File("src/test/resources/jsonl/items.jsonl")); + files.put("items", jsonlItemsFile); CatalogRequest req = new CatalogRequest(files, "Products"); String response = constructor.patchCatalog(req); @@ -460,7 +494,7 @@ public void ReplaceCatalogWithInvalidExtensionShouldError() throws Exception { ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); Map files = new HashMap(); - files.put("items", new File("src/test/resources/invalid/items.txt")); + files.put("items", invalidExtensionFile); CatalogRequest req = new CatalogRequest(files, "Products"); @@ -477,7 +511,7 @@ public void UpdateCatalogWithNoExtensionShouldError() throws Exception { ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); Map files = new HashMap(); - files.put("items", new File("src/test/resources/invalid/items")); + files.put("items", noExtensionFile); CatalogRequest req = new CatalogRequest(files, "Products"); @@ -494,7 +528,7 @@ public void PatchCatalogWithInvalidExtensionShouldError() throws Exception { ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); Map files = new HashMap(); - files.put("items", new File("src/test/resources/invalid/items.txt")); + files.put("items", invalidExtensionFile); CatalogRequest req = new CatalogRequest(files, "Products"); @@ -529,7 +563,7 @@ public void ReplaceCatalogWithMixedFileTypesShouldSucceed() throws Exception { Map files = new HashMap(); files.put("items", new File("src/test/resources/csv/items.csv")); - files.put("variations", new File("src/test/resources/jsonl/variations.jsonl")); + files.put("variations", jsonlVariationsFile); CatalogRequest req = new CatalogRequest(files, "Products"); String response = constructor.replaceCatalog(req); @@ -544,8 +578,8 @@ public void UpdateCatalogWithJsonlVariationsAndItemGroupsShouldSucceed() throws ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); Map files = new HashMap(); - files.put("variations", new File("src/test/resources/jsonl/variations.jsonl")); - files.put("item_groups", new File("src/test/resources/jsonl/item_groups.jsonl")); + files.put("variations", jsonlVariationsFile); + files.put("item_groups", jsonlItemGroupsFile); CatalogRequest req = new CatalogRequest(files, "Products"); String response = constructor.updateCatalog(req); @@ -560,9 +594,9 @@ public void PatchCatalogWithAllJsonlFilesShouldSucceed() throws Exception { ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); Map files = new HashMap(); - files.put("items", new File("src/test/resources/jsonl/items.jsonl")); - files.put("variations", new File("src/test/resources/jsonl/variations.jsonl")); - files.put("item_groups", new File("src/test/resources/jsonl/item_groups.jsonl")); + files.put("items", jsonlItemsFile); + files.put("variations", jsonlVariationsFile); + files.put("item_groups", jsonlItemGroupsFile); CatalogRequest req = new CatalogRequest(files, "Products"); String response = constructor.patchCatalog(req); @@ -579,7 +613,7 @@ public void ReplaceCatalogWithJsonItemsFileShouldReturnTaskInfo() throws Excepti ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); Map files = new HashMap(); - files.put("items", new File("src/test/resources/json/items.json")); + files.put("items", jsonItemsFile); CatalogRequest req = new CatalogRequest(files, "Products"); String response = constructor.replaceCatalog(req); @@ -594,7 +628,7 @@ public void UpdateCatalogWithJsonItemsFileShouldReturnTaskInfo() throws Exceptio ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); Map files = new HashMap(); - files.put("items", new File("src/test/resources/json/items.json")); + files.put("items", jsonItemsFile); CatalogRequest req = new CatalogRequest(files, "Products"); String response = constructor.updateCatalog(req); @@ -609,7 +643,7 @@ public void PatchCatalogWithJsonItemsFileShouldReturnTaskInfo() throws Exception ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); Map files = new HashMap(); - files.put("items", new File("src/test/resources/json/items.json")); + files.put("items", jsonItemsFile); CatalogRequest req = new CatalogRequest(files, "Products"); String response = constructor.patchCatalog(req); @@ -625,8 +659,8 @@ public void ReplaceCatalogWithMixedCsvJsonJsonlShouldSucceed() throws Exception Map files = new HashMap(); files.put("items", new File("src/test/resources/csv/items.csv")); - files.put("variations", new File("src/test/resources/json/variations.json")); - files.put("item_groups", new File("src/test/resources/jsonl/item_groups.jsonl")); + files.put("variations", jsonVariationsFile); + files.put("item_groups", jsonlItemGroupsFile); CatalogRequest req = new CatalogRequest(files, "Products"); String response = constructor.replaceCatalog(req); diff --git a/constructorio-client/src/test/java/io/constructor/client/Utils.java b/constructorio-client/src/test/java/io/constructor/client/Utils.java index e5e43484..1ab47d5b 100644 --- a/constructorio-client/src/test/java/io/constructor/client/Utils.java +++ b/constructorio-client/src/test/java/io/constructor/client/Utils.java @@ -1,5 +1,8 @@ package io.constructor.client; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -113,4 +116,231 @@ public static void enableHTTPLogging() { OkHttpClient newClient = client.newBuilder().addInterceptor(logger).build(); ConstructorIO.setHttpClient(newClient); } + + // ==================== File Generation Utilities ==================== + + /** + * Creates a JSON string for an item. + * + * @param id the item ID + * @param value the item display value + * @param url the item URL + * @return JSON string representation + */ + private static String itemToJson(String id, String value, String url) { + return String.format( + "{\"id\":\"%s\",\"value\":\"%s\",\"data\":{\"url\":\"%s\"}}", id, value, url); + } + + /** + * Creates a JSON string for a variation. + * + * @param id the variation ID + * @param itemId the parent item ID + * @param value the variation display value + * @param url the variation URL + * @return JSON string representation + */ + private static String variationToJson(String id, String itemId, String value, String url) { + return String.format( + "{\"id\":\"%s\",\"item_id\":\"%s\",\"value\":\"%s\",\"data\":{\"url\":\"%s\"}}", + id, itemId, value, url); + } + + /** + * Creates a JSON string for an item group. + * + * @param id the group ID + * @param value the group display value + * @param parentId the parent group ID + * @return JSON string representation + */ + private static String itemGroupToJson(String id, String value, String parentId) { + return String.format( + "{\"id\":\"%s\",\"value\":\"%s\",\"data\":{\"parent_id\":\"%s\"}}", + id, value, parentId); + } + + /** + * Generates a unique ID for test data. + * + * @param prefix the prefix for the ID + * @return a unique ID string + */ + private static String generateId(String prefix) { + return prefix + UUID.randomUUID().toString().substring(0, 8); + } + + /** + * Creates a temporary JSON file containing an array of items. + * + * @param count the number of items to generate + * @return a temporary File with .json extension + * @throws IOException if file creation fails + */ + public static File createItemsJsonFile(int count) throws IOException { + File file = File.createTempFile("items", ".json"); + file.deleteOnExit(); + + StringBuilder sb = new StringBuilder(); + sb.append("[\n"); + for (int i = 0; i < count; i++) { + String id = generateId("item"); + String value = "Product " + (i + 1); + String url = "https://example.com/" + id; + sb.append(" ").append(itemToJson(id, value, url)); + if (i < count - 1) { + sb.append(","); + } + sb.append("\n"); + } + sb.append("]"); + + try (FileWriter writer = new FileWriter(file)) { + writer.write(sb.toString()); + } + return file; + } + + /** + * Creates a temporary JSONL file containing items (one per line). + * + * @param count the number of items to generate + * @return a temporary File with .jsonl extension + * @throws IOException if file creation fails + */ + public static File createItemsJsonlFile(int count) throws IOException { + File file = File.createTempFile("items", ".jsonl"); + file.deleteOnExit(); + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < count; i++) { + String id = generateId("item"); + String value = "Product " + (i + 1); + String url = "https://example.com/" + id; + sb.append(itemToJson(id, value, url)).append("\n"); + } + + try (FileWriter writer = new FileWriter(file)) { + writer.write(sb.toString()); + } + return file; + } + + /** + * Creates a temporary JSON file containing an array of variations. + * + * @param count the number of variations to generate + * @return a temporary File with .json extension + * @throws IOException if file creation fails + */ + public static File createVariationsJsonFile(int count) throws IOException { + File file = File.createTempFile("variations", ".json"); + file.deleteOnExit(); + + StringBuilder sb = new StringBuilder(); + sb.append("[\n"); + for (int i = 0; i < count; i++) { + String id = generateId("var"); + String itemId = "item" + ((i % 3) + 1); // Rotate through item1, item2, item3 + String value = "Variation " + (i + 1); + String url = "https://example.com/" + id; + sb.append(" ").append(variationToJson(id, itemId, value, url)); + if (i < count - 1) { + sb.append(","); + } + sb.append("\n"); + } + sb.append("]"); + + try (FileWriter writer = new FileWriter(file)) { + writer.write(sb.toString()); + } + return file; + } + + /** + * Creates a temporary JSONL file containing variations (one per line). + * + * @param count the number of variations to generate + * @return a temporary File with .jsonl extension + * @throws IOException if file creation fails + */ + public static File createVariationsJsonlFile(int count) throws IOException { + File file = File.createTempFile("variations", ".jsonl"); + file.deleteOnExit(); + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < count; i++) { + String id = generateId("var"); + String itemId = "item" + ((i % 3) + 1); // Rotate through item1, item2, item3 + String value = "Variation " + (i + 1); + String url = "https://example.com/" + id; + sb.append(variationToJson(id, itemId, value, url)).append("\n"); + } + + try (FileWriter writer = new FileWriter(file)) { + writer.write(sb.toString()); + } + return file; + } + + /** + * Creates a temporary JSONL file containing item groups (one per line). + * + * @param count the number of item groups to generate + * @return a temporary File with .jsonl extension + * @throws IOException if file creation fails + */ + public static File createItemGroupsJsonlFile(int count) throws IOException { + File file = File.createTempFile("item_groups", ".jsonl"); + file.deleteOnExit(); + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < count; i++) { + String id = generateId("group"); + String value = "Group " + (i + 1); + String parentId = "root"; + sb.append(itemGroupToJson(id, value, parentId)).append("\n"); + } + + try (FileWriter writer = new FileWriter(file)) { + writer.write(sb.toString()); + } + return file; + } + + /** + * Creates a temporary file with an invalid .txt extension for testing validation. + * + * @return a temporary File with .txt extension + * @throws IOException if file creation fails + */ + public static File createInvalidExtensionFile() throws IOException { + File file = File.createTempFile("items", ".txt"); + file.deleteOnExit(); + + try (FileWriter writer = new FileWriter(file)) { + writer.write("This is a text file with invalid extension for catalog upload testing."); + } + return file; + } + + /** + * Creates a temporary file with no extension for testing validation. + * + * @return a temporary File with no extension + * @throws IOException if file creation fails + */ + public static File createNoExtensionFile() throws IOException { + File tempFile = File.createTempFile("items", ".tmp"); + File noExtFile = new File(tempFile.getParent(), "items_" + UUID.randomUUID().toString().substring(0, 8)); + tempFile.renameTo(noExtFile); + noExtFile.deleteOnExit(); + + try (FileWriter writer = new FileWriter(noExtFile)) { + writer.write("This file has no extension for testing validation."); + } + return noExtFile; + } } diff --git a/constructorio-client/src/test/resources/invalid/items b/constructorio-client/src/test/resources/invalid/items deleted file mode 100644 index a992b237..00000000 --- a/constructorio-client/src/test/resources/invalid/items +++ /dev/null @@ -1 +0,0 @@ -This file has no extension for testing validation. diff --git a/constructorio-client/src/test/resources/invalid/items.txt b/constructorio-client/src/test/resources/invalid/items.txt deleted file mode 100644 index 887c87ec..00000000 --- a/constructorio-client/src/test/resources/invalid/items.txt +++ /dev/null @@ -1 +0,0 @@ -This is a text file with invalid extension for catalog upload testing. diff --git a/constructorio-client/src/test/resources/json/items.json b/constructorio-client/src/test/resources/json/items.json deleted file mode 100644 index 69b7f47f..00000000 --- a/constructorio-client/src/test/resources/json/items.json +++ /dev/null @@ -1,23 +0,0 @@ -[ - { - "id": "item1", - "value": "Product 1", - "data": { - "url": "https://example.com/product1" - } - }, - { - "id": "item2", - "value": "Product 2", - "data": { - "url": "https://example.com/product2" - } - }, - { - "id": "item3", - "value": "Product 3", - "data": { - "url": "https://example.com/product3" - } - } -] diff --git a/constructorio-client/src/test/resources/json/variations.json b/constructorio-client/src/test/resources/json/variations.json deleted file mode 100644 index 45fb9621..00000000 --- a/constructorio-client/src/test/resources/json/variations.json +++ /dev/null @@ -1,18 +0,0 @@ -[ - { - "id": "var1", - "item_id": "item1", - "value": "Product 1 Variation A", - "data": { - "color": "red" - } - }, - { - "id": "var2", - "item_id": "item1", - "value": "Product 1 Variation B", - "data": { - "color": "blue" - } - } -] diff --git a/constructorio-client/src/test/resources/jsonl/item_groups.jsonl b/constructorio-client/src/test/resources/jsonl/item_groups.jsonl deleted file mode 100644 index 2809502e..00000000 --- a/constructorio-client/src/test/resources/jsonl/item_groups.jsonl +++ /dev/null @@ -1,2 +0,0 @@ -{"id":"group1","value":"Group 1","data":{"parent_id":"root"}} -{"id":"group2","value":"Group 2","data":{"parent_id":"root"}} diff --git a/constructorio-client/src/test/resources/jsonl/items.jsonl b/constructorio-client/src/test/resources/jsonl/items.jsonl deleted file mode 100644 index b3c5f58b..00000000 --- a/constructorio-client/src/test/resources/jsonl/items.jsonl +++ /dev/null @@ -1,3 +0,0 @@ -{"id":"item1","value":"Product 1","data":{"url":"https://example.com/product1"}} -{"id":"item2","value":"Product 2","data":{"url":"https://example.com/product2"}} -{"id":"item3","value":"Product 3","data":{"url":"https://example.com/product3"}} diff --git a/constructorio-client/src/test/resources/jsonl/variations.jsonl b/constructorio-client/src/test/resources/jsonl/variations.jsonl deleted file mode 100644 index 5b17177f..00000000 --- a/constructorio-client/src/test/resources/jsonl/variations.jsonl +++ /dev/null @@ -1,3 +0,0 @@ -{"id":"var1","item_id":"item1","value":"Product 1 Variation A","data":{"color":"red"}} -{"id":"var2","item_id":"item1","value":"Product 1 Variation B","data":{"color":"blue"}} -{"id":"var3","item_id":"item2","value":"Product 2 Variation A","data":{"color":"green"}} From 7c470c030edd7f3f70e8cff810b6c0b7207278ae Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Mon, 29 Dec 2025 22:02:45 +0300 Subject: [PATCH 07/11] Lint and cleanup --- .../io/constructor/client/ConstructorIO.java | 2 +- .../client/ConstructorIOCatalogTest.java | 1 - .../java/io/constructor/client/Utils.java | 36 +++++++++---------- 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/constructorio-client/src/main/java/io/constructor/client/ConstructorIO.java b/constructorio-client/src/main/java/io/constructor/client/ConstructorIO.java index bc56f4e2..6996f417 100644 --- a/constructorio-client/src/main/java/io/constructor/client/ConstructorIO.java +++ b/constructorio-client/src/main/java/io/constructor/client/ConstructorIO.java @@ -2042,7 +2042,7 @@ protected static String getResponseBody(Response response) throws ConstructorExc * * @param file the File object containing the actual file * @param fileName the logical file name (items, variations, item_groups) - * @return the validated file extension (including the dot) + * @return the validated file extension (including the dot, e.g., ".csv", ".json", or ".jsonl") * @throws ConstructorException if the file extension is not in VALID_CATALOG_EXTENSIONS */ private static String getValidatedFileExtension(File file, String fileName) diff --git a/constructorio-client/src/test/java/io/constructor/client/ConstructorIOCatalogTest.java b/constructorio-client/src/test/java/io/constructor/client/ConstructorIOCatalogTest.java index c427799c..631637b3 100644 --- a/constructorio-client/src/test/java/io/constructor/client/ConstructorIOCatalogTest.java +++ b/constructorio-client/src/test/java/io/constructor/client/ConstructorIOCatalogTest.java @@ -58,7 +58,6 @@ public void init() throws Exception { @After public void teardown() throws Exception { - // Clean up CSV files itemsFile.delete(); variationsFile.delete(); itemGroupsFile.delete(); diff --git a/constructorio-client/src/test/java/io/constructor/client/Utils.java b/constructorio-client/src/test/java/io/constructor/client/Utils.java index 1ab47d5b..e5e79075 100644 --- a/constructorio-client/src/test/java/io/constructor/client/Utils.java +++ b/constructorio-client/src/test/java/io/constructor/client/Utils.java @@ -79,20 +79,19 @@ public static ConstructorVariation createProductVariation(String itemId) { /** * @param statusCode the http status code - * @param bodyText the body + * @param bodyText the body * @return an HTTP response */ public static Response createResponse(int statusCode, String bodyText) { Request request = new Request.Builder().url("https://example.com").build(); ResponseBody body = ResponseBody.create(bodyType, bodyText); - Response response = - new Response.Builder() - .request(request) - .protocol(Protocol.HTTP_1_1) - .code(statusCode) - .body(body) - .message("") - .build(); + Response response = new Response.Builder() + .request(request) + .protocol(Protocol.HTTP_1_1) + .code(statusCode) + .body(body) + .message("") + .build(); return response; } @@ -117,14 +116,12 @@ public static void enableHTTPLogging() { ConstructorIO.setHttpClient(newClient); } - // ==================== File Generation Utilities ==================== - /** * Creates a JSON string for an item. * - * @param id the item ID + * @param id the item ID * @param value the item display value - * @param url the item URL + * @param url the item URL * @return JSON string representation */ private static String itemToJson(String id, String value, String url) { @@ -135,10 +132,10 @@ private static String itemToJson(String id, String value, String url) { /** * Creates a JSON string for a variation. * - * @param id the variation ID + * @param id the variation ID * @param itemId the parent item ID - * @param value the variation display value - * @param url the variation URL + * @param value the variation display value + * @param url the variation URL * @return JSON string representation */ private static String variationToJson(String id, String itemId, String value, String url) { @@ -150,8 +147,8 @@ private static String variationToJson(String id, String itemId, String value, St /** * Creates a JSON string for an item group. * - * @param id the group ID - * @param value the group display value + * @param id the group ID + * @param value the group display value * @param parentId the parent group ID * @return JSON string representation */ @@ -311,7 +308,8 @@ public static File createItemGroupsJsonlFile(int count) throws IOException { } /** - * Creates a temporary file with an invalid .txt extension for testing validation. + * Creates a temporary file with an invalid .txt extension for testing + * validation. * * @return a temporary File with .txt extension * @throws IOException if file creation fails From f09e4a0cb41fef41d1d0efbb037e009a096dd5e2 Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Mon, 29 Dec 2025 22:14:55 +0300 Subject: [PATCH 08/11] Utilize existing functions --- .../java/io/constructor/client/Utils.java | 103 ++++++------------ 1 file changed, 35 insertions(+), 68 deletions(-) diff --git a/constructorio-client/src/test/java/io/constructor/client/Utils.java b/constructorio-client/src/test/java/io/constructor/client/Utils.java index e5e79075..a7a89348 100644 --- a/constructorio-client/src/test/java/io/constructor/client/Utils.java +++ b/constructorio-client/src/test/java/io/constructor/client/Utils.java @@ -1,14 +1,17 @@ package io.constructor.client; +import com.google.gson.Gson; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.UUID; import okhttp3.MediaType; import okhttp3.OkHttpClient; @@ -116,46 +119,26 @@ public static void enableHTTPLogging() { ConstructorIO.setHttpClient(newClient); } - /** - * Creates a JSON string for an item. - * - * @param id the item ID - * @param value the item display value - * @param url the item URL - * @return JSON string representation - */ - private static String itemToJson(String id, String value, String url) { - return String.format( - "{\"id\":\"%s\",\"value\":\"%s\",\"data\":{\"url\":\"%s\"}}", id, value, url); - } - - /** - * Creates a JSON string for a variation. - * - * @param id the variation ID - * @param itemId the parent item ID - * @param value the variation display value - * @param url the variation URL - * @return JSON string representation - */ - private static String variationToJson(String id, String itemId, String value, String url) { - return String.format( - "{\"id\":\"%s\",\"item_id\":\"%s\",\"value\":\"%s\",\"data\":{\"url\":\"%s\"}}", - id, itemId, value, url); - } + private static final Gson gson = new Gson(); /** * Creates a JSON string for an item group. * * @param id the group ID - * @param value the group display value + * @param name the group display name * @param parentId the parent group ID * @return JSON string representation */ - private static String itemGroupToJson(String id, String value, String parentId) { - return String.format( - "{\"id\":\"%s\",\"value\":\"%s\",\"data\":{\"parent_id\":\"%s\"}}", - id, value, parentId); + private static String itemGroupToJson(String id, String name, String parentId) { + Map dataMap = new HashMap(); + dataMap.put("parent_id", parentId); + + Map group = new HashMap(); + group.put("id", id); + group.put("name", name); + group.put("data", dataMap); + + return gson.toJson(group); } /** @@ -170,6 +153,7 @@ private static String generateId(String prefix) { /** * Creates a temporary JSON file containing an array of items. + * Uses createProductItem() to generate realistic test data. * * @param count the number of items to generate * @return a temporary File with .json extension @@ -179,28 +163,21 @@ public static File createItemsJsonFile(int count) throws IOException { File file = File.createTempFile("items", ".json"); file.deleteOnExit(); - StringBuilder sb = new StringBuilder(); - sb.append("[\n"); + List> items = new ArrayList>(); for (int i = 0; i < count; i++) { - String id = generateId("item"); - String value = "Product " + (i + 1); - String url = "https://example.com/" + id; - sb.append(" ").append(itemToJson(id, value, url)); - if (i < count - 1) { - sb.append(","); - } - sb.append("\n"); + ConstructorItem item = createProductItem(); + items.add(item.toMap()); } - sb.append("]"); try (FileWriter writer = new FileWriter(file)) { - writer.write(sb.toString()); + writer.write(gson.toJson(items)); } return file; } /** * Creates a temporary JSONL file containing items (one per line). + * Uses createProductItem() to generate realistic test data. * * @param count the number of items to generate * @return a temporary File with .jsonl extension @@ -212,10 +189,8 @@ public static File createItemsJsonlFile(int count) throws IOException { StringBuilder sb = new StringBuilder(); for (int i = 0; i < count; i++) { - String id = generateId("item"); - String value = "Product " + (i + 1); - String url = "https://example.com/" + id; - sb.append(itemToJson(id, value, url)).append("\n"); + ConstructorItem item = createProductItem(); + sb.append(gson.toJson(item.toMap())).append("\n"); } try (FileWriter writer = new FileWriter(file)) { @@ -226,6 +201,7 @@ public static File createItemsJsonlFile(int count) throws IOException { /** * Creates a temporary JSON file containing an array of variations. + * Uses createProductVariation() to generate realistic test data. * * @param count the number of variations to generate * @return a temporary File with .json extension @@ -235,29 +211,22 @@ public static File createVariationsJsonFile(int count) throws IOException { File file = File.createTempFile("variations", ".json"); file.deleteOnExit(); - StringBuilder sb = new StringBuilder(); - sb.append("[\n"); + List> variations = new ArrayList>(); for (int i = 0; i < count; i++) { - String id = generateId("var"); - String itemId = "item" + ((i % 3) + 1); // Rotate through item1, item2, item3 - String value = "Variation " + (i + 1); - String url = "https://example.com/" + id; - sb.append(" ").append(variationToJson(id, itemId, value, url)); - if (i < count - 1) { - sb.append(","); - } - sb.append("\n"); + String itemId = "item" + ((i % 3) + 1); + ConstructorVariation variation = createProductVariation(itemId); + variations.add(variation.toMap()); } - sb.append("]"); try (FileWriter writer = new FileWriter(file)) { - writer.write(sb.toString()); + writer.write(gson.toJson(variations)); } return file; } /** * Creates a temporary JSONL file containing variations (one per line). + * Uses createProductVariation() to generate realistic test data. * * @param count the number of variations to generate * @return a temporary File with .jsonl extension @@ -269,11 +238,9 @@ public static File createVariationsJsonlFile(int count) throws IOException { StringBuilder sb = new StringBuilder(); for (int i = 0; i < count; i++) { - String id = generateId("var"); - String itemId = "item" + ((i % 3) + 1); // Rotate through item1, item2, item3 - String value = "Variation " + (i + 1); - String url = "https://example.com/" + id; - sb.append(variationToJson(id, itemId, value, url)).append("\n"); + String itemId = "item" + ((i % 3) + 1); + ConstructorVariation variation = createProductVariation(itemId); + sb.append(gson.toJson(variation.toMap())).append("\n"); } try (FileWriter writer = new FileWriter(file)) { @@ -296,9 +263,9 @@ public static File createItemGroupsJsonlFile(int count) throws IOException { StringBuilder sb = new StringBuilder(); for (int i = 0; i < count; i++) { String id = generateId("group"); - String value = "Group " + (i + 1); + String name = "Group " + (i + 1); String parentId = "root"; - sb.append(itemGroupToJson(id, value, parentId)).append("\n"); + sb.append(itemGroupToJson(id, name, parentId)).append("\n"); } try (FileWriter writer = new FileWriter(file)) { From 34c677b520fcc3ff6fa54addc9168707dbc40cbd Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Mon, 29 Dec 2025 22:32:52 +0300 Subject: [PATCH 09/11] Cleanup --- .../client/ConstructorIOCatalogTest.java | 47 ++++++++++++++++ .../java/io/constructor/client/Utils.java | 54 +++++-------------- 2 files changed, 61 insertions(+), 40 deletions(-) diff --git a/constructorio-client/src/test/java/io/constructor/client/ConstructorIOCatalogTest.java b/constructorio-client/src/test/java/io/constructor/client/ConstructorIOCatalogTest.java index 631637b3..d80a1362 100644 --- a/constructorio-client/src/test/java/io/constructor/client/ConstructorIOCatalogTest.java +++ b/constructorio-client/src/test/java/io/constructor/client/ConstructorIOCatalogTest.java @@ -539,6 +539,23 @@ public void PatchCatalogWithInvalidExtensionShouldError() throws Exception { constructor.patchCatalog(req); } + @Test + public void UpdateCatalogWithInvalidExtensionShouldError() throws Exception { + ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); + Map files = new HashMap(); + + files.put("items", invalidExtensionFile); + + CatalogRequest req = new CatalogRequest(files, "Products"); + + thrown.expect(ConstructorException.class); + thrown.expectMessage("Invalid file type for 'items'"); + thrown.expectMessage(".csv"); + thrown.expectMessage(".json"); + thrown.expectMessage(".jsonl"); + constructor.updateCatalog(req); + } + // Edge Case Tests @Test @@ -556,6 +573,36 @@ public void ReplaceCatalogWithNullFileShouldError() throws Exception { constructor.replaceCatalog(req); } + @Test + public void UpdateCatalogWithNullFileShouldError() throws Exception { + ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); + Map files = new HashMap(); + + files.put("items", null); + + CatalogRequest req = new CatalogRequest(files, "Products"); + + thrown.expect(ConstructorException.class); + thrown.expectMessage("Invalid file for 'items'"); + thrown.expectMessage("file cannot be null"); + constructor.updateCatalog(req); + } + + @Test + public void PatchCatalogWithNullFileShouldError() throws Exception { + ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); + Map files = new HashMap(); + + files.put("items", null); + + CatalogRequest req = new CatalogRequest(files, "Products"); + + thrown.expect(ConstructorException.class); + thrown.expectMessage("Invalid file for 'items'"); + thrown.expectMessage("file cannot be null"); + constructor.patchCatalog(req); + } + @Test public void ReplaceCatalogWithMixedFileTypesShouldSucceed() throws Exception { ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); diff --git a/constructorio-client/src/test/java/io/constructor/client/Utils.java b/constructorio-client/src/test/java/io/constructor/client/Utils.java index a7a89348..3648876c 100644 --- a/constructorio-client/src/test/java/io/constructor/client/Utils.java +++ b/constructorio-client/src/test/java/io/constructor/client/Utils.java @@ -121,36 +121,6 @@ public static void enableHTTPLogging() { private static final Gson gson = new Gson(); - /** - * Creates a JSON string for an item group. - * - * @param id the group ID - * @param name the group display name - * @param parentId the parent group ID - * @return JSON string representation - */ - private static String itemGroupToJson(String id, String name, String parentId) { - Map dataMap = new HashMap(); - dataMap.put("parent_id", parentId); - - Map group = new HashMap(); - group.put("id", id); - group.put("name", name); - group.put("data", dataMap); - - return gson.toJson(group); - } - - /** - * Generates a unique ID for test data. - * - * @param prefix the prefix for the ID - * @return a unique ID string - */ - private static String generateId(String prefix) { - return prefix + UUID.randomUUID().toString().substring(0, 8); - } - /** * Creates a temporary JSON file containing an array of items. * Uses createProductItem() to generate realistic test data. @@ -262,10 +232,15 @@ public static File createItemGroupsJsonlFile(int count) throws IOException { StringBuilder sb = new StringBuilder(); for (int i = 0; i < count; i++) { - String id = generateId("group"); - String name = "Group " + (i + 1); - String parentId = "root"; - sb.append(itemGroupToJson(id, name, parentId)).append("\n"); + Map dataMap = new HashMap(); + dataMap.put("parent_id", "root"); + + Map group = new HashMap(); + group.put("id", "group" + UUID.randomUUID().toString().substring(0, 8)); + group.put("name", "Group " + (i + 1)); + group.put("data", dataMap); + + sb.append(gson.toJson(group)).append("\n"); } try (FileWriter writer = new FileWriter(file)) { @@ -298,14 +273,13 @@ public static File createInvalidExtensionFile() throws IOException { * @throws IOException if file creation fails */ public static File createNoExtensionFile() throws IOException { - File tempFile = File.createTempFile("items", ".tmp"); - File noExtFile = new File(tempFile.getParent(), "items_" + UUID.randomUUID().toString().substring(0, 8)); - tempFile.renameTo(noExtFile); - noExtFile.deleteOnExit(); + String tmpDir = System.getProperty("java.io.tmpdir"); + File file = new File(tmpDir, "items_" + UUID.randomUUID().toString().substring(0, 8)); + file.deleteOnExit(); - try (FileWriter writer = new FileWriter(noExtFile)) { + try (FileWriter writer = new FileWriter(file)) { writer.write("This file has no extension for testing validation."); } - return noExtFile; + return file; } } From 6d6925dc1984cd35f3938eabfb68214420c46eb9 Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Tue, 30 Dec 2025 17:16:47 +0300 Subject: [PATCH 10/11] Lint --- .../io/constructor/client/ConstructorIO.java | 12 +++---- .../client/ConstructorIOCatalogTest.java | 27 ++++++-------- .../java/io/constructor/client/Utils.java | 36 +++++++++---------- 3 files changed, 34 insertions(+), 41 deletions(-) diff --git a/constructorio-client/src/main/java/io/constructor/client/ConstructorIO.java b/constructorio-client/src/main/java/io/constructor/client/ConstructorIO.java index 6996f417..f8ea8a42 100644 --- a/constructorio-client/src/main/java/io/constructor/client/ConstructorIO.java +++ b/constructorio-client/src/main/java/io/constructor/client/ConstructorIO.java @@ -2361,8 +2361,8 @@ protected static JSONArray transformItemsAPIV2Response(JSONArray results) { /** * Send a full catalog to replace the current one (sync) * - * Supports CSV, JSON, and JSONL file formats. The file type is automatically - * detected from the file extension (.csv, .json, or .jsonl). + *

Supports CSV, JSON, and JSONL file formats. The file type is automatically detected from + * the file extension (.csv, .json, or .jsonl). * * @param req the catalog request containing files with .csv, .json, or .jsonl extensions * @return a string of JSON containing task information @@ -2422,8 +2422,8 @@ public String replaceCatalog(CatalogRequest req) throws ConstructorException { /** * Send a partial catalog to update specific items (delta) * - * Supports CSV, JSON, and JSONL file formats. The file type is automatically - * detected from the file extension (.csv, .json, or .jsonl). + *

Supports CSV, JSON, and JSONL file formats. The file type is automatically detected from + * the file extension (.csv, .json, or .jsonl). * * @param req the catalog request containing files with .csv, .json, or .jsonl extensions * @return a string of JSON containing task information @@ -2484,8 +2484,8 @@ public String updateCatalog(CatalogRequest req) throws ConstructorException { /** * Send a patch delta catalog to update specific items (delta) * - * Supports CSV, JSON, and JSONL file formats. The file type is automatically - * detected from the file extension (.csv, .json, or .jsonl). + *

Supports CSV, JSON, and JSONL file formats. The file type is automatically detected from + * the file extension (.csv, .json, or .jsonl). * * @param req the catalog request containing files with .csv, .json, or .jsonl extensions * @return a string of JSON containing task information diff --git a/constructorio-client/src/test/java/io/constructor/client/ConstructorIOCatalogTest.java b/constructorio-client/src/test/java/io/constructor/client/ConstructorIOCatalogTest.java index d80a1362..4db2eeac 100644 --- a/constructorio-client/src/test/java/io/constructor/client/ConstructorIOCatalogTest.java +++ b/constructorio-client/src/test/java/io/constructor/client/ConstructorIOCatalogTest.java @@ -22,7 +22,8 @@ public class ConstructorIOCatalogTest { private File itemsFile = new File("src/test/resources/csv/items.csv"); private File variationsFile = new File("src/test/resources/csv/variations.csv"); private File itemGroupsFile = new File("src/test/resources/csv/item_groups.csv"); - private String baseUrl = "https://raw.githubusercontent.com/Constructor-io/integration-examples/main/catalog/"; + private String baseUrl = + "https://raw.githubusercontent.com/Constructor-io/integration-examples/main/catalog/"; private File jsonItemsFile; private File jsonVariationsFile; @@ -32,8 +33,7 @@ public class ConstructorIOCatalogTest { private File invalidExtensionFile; private File noExtensionFile; - @Rule - public ExpectedException thrown = ExpectedException.none(); + @Rule public ExpectedException thrown = ExpectedException.none(); @Before public void init() throws Exception { @@ -64,20 +64,13 @@ public void teardown() throws Exception { csvFolder.delete(); // Clean up generated files - if (jsonItemsFile != null) - jsonItemsFile.delete(); - if (jsonVariationsFile != null) - jsonVariationsFile.delete(); - if (jsonlItemsFile != null) - jsonlItemsFile.delete(); - if (jsonlVariationsFile != null) - jsonlVariationsFile.delete(); - if (jsonlItemGroupsFile != null) - jsonlItemGroupsFile.delete(); - if (invalidExtensionFile != null) - invalidExtensionFile.delete(); - if (noExtensionFile != null) - noExtensionFile.delete(); + if (jsonItemsFile != null) jsonItemsFile.delete(); + if (jsonVariationsFile != null) jsonVariationsFile.delete(); + if (jsonlItemsFile != null) jsonlItemsFile.delete(); + if (jsonlVariationsFile != null) jsonlVariationsFile.delete(); + if (jsonlItemGroupsFile != null) jsonlItemGroupsFile.delete(); + if (invalidExtensionFile != null) invalidExtensionFile.delete(); + if (noExtensionFile != null) noExtensionFile.delete(); } @Test diff --git a/constructorio-client/src/test/java/io/constructor/client/Utils.java b/constructorio-client/src/test/java/io/constructor/client/Utils.java index 3648876c..077afaf8 100644 --- a/constructorio-client/src/test/java/io/constructor/client/Utils.java +++ b/constructorio-client/src/test/java/io/constructor/client/Utils.java @@ -82,19 +82,20 @@ public static ConstructorVariation createProductVariation(String itemId) { /** * @param statusCode the http status code - * @param bodyText the body + * @param bodyText the body * @return an HTTP response */ public static Response createResponse(int statusCode, String bodyText) { Request request = new Request.Builder().url("https://example.com").build(); ResponseBody body = ResponseBody.create(bodyType, bodyText); - Response response = new Response.Builder() - .request(request) - .protocol(Protocol.HTTP_1_1) - .code(statusCode) - .body(body) - .message("") - .build(); + Response response = + new Response.Builder() + .request(request) + .protocol(Protocol.HTTP_1_1) + .code(statusCode) + .body(body) + .message("") + .build(); return response; } @@ -122,8 +123,8 @@ public static void enableHTTPLogging() { private static final Gson gson = new Gson(); /** - * Creates a temporary JSON file containing an array of items. - * Uses createProductItem() to generate realistic test data. + * Creates a temporary JSON file containing an array of items. Uses createProductItem() to + * generate realistic test data. * * @param count the number of items to generate * @return a temporary File with .json extension @@ -146,8 +147,8 @@ public static File createItemsJsonFile(int count) throws IOException { } /** - * Creates a temporary JSONL file containing items (one per line). - * Uses createProductItem() to generate realistic test data. + * Creates a temporary JSONL file containing items (one per line). Uses createProductItem() to + * generate realistic test data. * * @param count the number of items to generate * @return a temporary File with .jsonl extension @@ -170,8 +171,8 @@ public static File createItemsJsonlFile(int count) throws IOException { } /** - * Creates a temporary JSON file containing an array of variations. - * Uses createProductVariation() to generate realistic test data. + * Creates a temporary JSON file containing an array of variations. Uses + * createProductVariation() to generate realistic test data. * * @param count the number of variations to generate * @return a temporary File with .json extension @@ -195,8 +196,8 @@ public static File createVariationsJsonFile(int count) throws IOException { } /** - * Creates a temporary JSONL file containing variations (one per line). - * Uses createProductVariation() to generate realistic test data. + * Creates a temporary JSONL file containing variations (one per line). Uses + * createProductVariation() to generate realistic test data. * * @param count the number of variations to generate * @return a temporary File with .jsonl extension @@ -250,8 +251,7 @@ public static File createItemGroupsJsonlFile(int count) throws IOException { } /** - * Creates a temporary file with an invalid .txt extension for testing - * validation. + * Creates a temporary file with an invalid .txt extension for testing validation. * * @return a temporary File with .txt extension * @throws IOException if file creation fails From fe1d2cf4a9017001b6bae66a6ae4c049d7183349 Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Wed, 31 Dec 2025 15:36:52 +0300 Subject: [PATCH 11/11] Address comments --- .../client/ConstructorIOCatalogTest.java | 128 ++++++++++++------ .../java/io/constructor/client/Utils.java | 30 ++++ 2 files changed, 119 insertions(+), 39 deletions(-) diff --git a/constructorio-client/src/test/java/io/constructor/client/ConstructorIOCatalogTest.java b/constructorio-client/src/test/java/io/constructor/client/ConstructorIOCatalogTest.java index 4db2eeac..39f27a49 100644 --- a/constructorio-client/src/test/java/io/constructor/client/ConstructorIOCatalogTest.java +++ b/constructorio-client/src/test/java/io/constructor/client/ConstructorIOCatalogTest.java @@ -16,6 +16,8 @@ public class ConstructorIOCatalogTest { + private static final String PRODUCTS_SECTION = "Products"; + private String token = System.getenv("TEST_API_TOKEN"); private String apiKey = System.getenv("TEST_CATALOG_API_KEY"); private File csvFolder = new File("src/test/resources/csv"); @@ -27,6 +29,7 @@ public class ConstructorIOCatalogTest { private File jsonItemsFile; private File jsonVariationsFile; + private File jsonItemGroupsFile; private File jsonlItemsFile; private File jsonlVariationsFile; private File jsonlItemGroupsFile; @@ -49,6 +52,7 @@ public void init() throws Exception { // Generate JSON/JSONL/invalid files jsonItemsFile = Utils.createItemsJsonFile(3); jsonVariationsFile = Utils.createVariationsJsonFile(2); + jsonItemGroupsFile = Utils.createItemGroupsJsonFile(2); jsonlItemsFile = Utils.createItemsJsonlFile(3); jsonlVariationsFile = Utils.createVariationsJsonlFile(3); jsonlItemGroupsFile = Utils.createItemGroupsJsonlFile(2); @@ -66,6 +70,7 @@ public void teardown() throws Exception { // Clean up generated files if (jsonItemsFile != null) jsonItemsFile.delete(); if (jsonVariationsFile != null) jsonVariationsFile.delete(); + if (jsonItemGroupsFile != null) jsonItemGroupsFile.delete(); if (jsonlItemsFile != null) jsonlItemsFile.delete(); if (jsonlVariationsFile != null) jsonlVariationsFile.delete(); if (jsonlItemGroupsFile != null) jsonlItemGroupsFile.delete(); @@ -77,7 +82,7 @@ public void teardown() throws Exception { public void ReplaceCatalogWithNoFilesShouldError() throws Exception { ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); Map files = new HashMap(); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); thrown.expect(ConstructorException.class); thrown.expectMessage( @@ -92,7 +97,7 @@ public void ReplaceCatalogWithItemsFileShouldReturnTaskInfo() throws Exception { files.put("items", new File("src/test/resources/csv/items.csv")); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); String response = constructor.replaceCatalog(req); JSONObject jsonObj = new JSONObject(response); @@ -107,7 +112,7 @@ public void ReplaceCatalogWithItemsAndNotificationEmailShouldReturnTaskInfo() th files.put("items", new File("src/test/resources/csv/items.csv")); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); req.setNotificationEmail("test@constructor.io"); @@ -125,7 +130,7 @@ public void ReplaceCatalogWithItemsAndSectionShouldReturnTaskInfo() throws Excep files.put("items", new File("src/test/resources/csv/items.csv")); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); req.setSection("Content"); @@ -142,7 +147,7 @@ public void ReplaceCatalogWithItemsAndForceShouldReturnTaskInfo() throws Excepti Map files = new HashMap(); files.put("items", new File("src/test/resources/csv/items.csv")); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); req.setForce(true); @@ -161,7 +166,7 @@ public void ReplaceCatalogWithItemsAndVariationsFilesShouldReturnTaskInfo() thro files.put("items", new File("src/test/resources/csv/items.csv")); files.put("variations", new File("src/test/resources/csv/variations.csv")); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); String response = constructor.replaceCatalog(req); JSONObject jsonObj = new JSONObject(response); @@ -179,7 +184,7 @@ public void ReplaceCatalogWithItemsAndVariationsAndItemGroupsFilesShouldReturnTa files.put("variations", new File("src/test/resources/csv/variations.csv")); files.put("item_groups", new File("src/test/resources/csv/item_groups.csv")); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); String response = constructor.replaceCatalog(req); JSONObject jsonObj = new JSONObject(response); @@ -191,7 +196,7 @@ public void ReplaceCatalogWithItemsAndVariationsAndItemGroupsFilesShouldReturnTa public void UpdateCatalogWithNoFilesShouldError() throws Exception { ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); Map files = new HashMap(); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); thrown.expect(ConstructorException.class); thrown.expectMessage( @@ -206,7 +211,7 @@ public void UpdateCatalogWithItemsFileShouldReturnTaskInfo() throws Exception { files.put("items", new File("src/test/resources/csv/items.csv")); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); String response = constructor.updateCatalog(req); JSONObject jsonObj = new JSONObject(response); @@ -221,7 +226,7 @@ public void UpdateCatalogWithItemsAndNotificationEmailShouldReturnTaskInfo() thr files.put("items", new File("src/test/resources/csv/items.csv")); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); req.setNotificationEmail("test@constructor.io"); @@ -239,7 +244,7 @@ public void UpdateCatalogWithItemsAndSectionShouldReturnTaskInfo() throws Except files.put("items", new File("src/test/resources/csv/items.csv")); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); req.setSection("Content"); @@ -256,7 +261,7 @@ public void UpdateCatalogWithItemsAndForceShouldReturnTaskInfo() throws Exceptio Map files = new HashMap(); files.put("items", new File("src/test/resources/csv/items.csv")); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); req.setForce(true); @@ -275,7 +280,7 @@ public void UpdateCatalogWithItemsAndVariationsFilesShouldReturnTaskInfo() throw files.put("items", new File("src/test/resources/csv/items.csv")); files.put("variations", new File("src/test/resources/csv/variations.csv")); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); String response = constructor.updateCatalog(req); JSONObject jsonObj = new JSONObject(response); @@ -293,7 +298,7 @@ public void UpdateCatalogWithItemsAndVariationsAndItemGroupsFilesShouldReturnTas files.put("variations", new File("src/test/resources/csv/variations.csv")); files.put("item_groups", new File("src/test/resources/csv/item_groups.csv")); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); String response = constructor.updateCatalog(req); JSONObject jsonObj = new JSONObject(response); @@ -305,7 +310,7 @@ public void UpdateCatalogWithItemsAndVariationsAndItemGroupsFilesShouldReturnTas public void PatchCatalogWithNoFilesShouldError() throws Exception { ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); Map files = new HashMap(); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); thrown.expect(ConstructorException.class); thrown.expectMessage( @@ -320,7 +325,7 @@ public void PatchCatalogWithItemsFileShouldReturnTaskInfo() throws Exception { files.put("items", new File("src/test/resources/csv/items.csv")); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); String response = constructor.patchCatalog(req); JSONObject jsonObj = new JSONObject(response); @@ -335,7 +340,7 @@ public void PatchCatalogWithItemsAndNotificationEmailShouldReturnTaskInfo() thro files.put("items", new File("src/test/resources/csv/items.csv")); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); req.setNotificationEmail("test@constructor.io"); @@ -353,7 +358,7 @@ public void PatchCatalogWithItemsAndSectionShouldReturnTaskInfo() throws Excepti files.put("items", new File("src/test/resources/csv/items.csv")); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); req.setSection("Content"); @@ -370,7 +375,7 @@ public void PatchCatalogWithItemsAndForceShouldReturnTaskInfo() throws Exception Map files = new HashMap(); files.put("items", new File("src/test/resources/csv/items.csv")); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); req.setForce(true); @@ -387,7 +392,7 @@ public void PatchCatalogWithItemsAndOnMissingShouldReturnTaskInfo() throws Excep Map files = new HashMap(); files.put("items", new File("src/test/resources/csv/items.csv")); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); req.setOnMissing(CatalogRequest.OnMissing.CREATE); @@ -406,7 +411,7 @@ public void PatchCatalogWithItemsAndVariationsFilesShouldReturnTaskInfo() throws files.put("items", new File("src/test/resources/csv/items.csv")); files.put("variations", new File("src/test/resources/csv/variations.csv")); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); String response = constructor.patchCatalog(req); JSONObject jsonObj = new JSONObject(response); @@ -424,7 +429,7 @@ public void PatchCatalogWithItemsAndVariationsAndItemGroupsFilesShouldReturnTask files.put("variations", new File("src/test/resources/csv/variations.csv")); files.put("item_groups", new File("src/test/resources/csv/item_groups.csv")); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); String response = constructor.patchCatalog(req); JSONObject jsonObj = new JSONObject(response); @@ -441,7 +446,7 @@ public void ReplaceCatalogWithJsonlItemsFileShouldReturnTaskInfo() throws Except files.put("items", jsonlItemsFile); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); String response = constructor.replaceCatalog(req); JSONObject jsonObj = new JSONObject(response); @@ -456,7 +461,7 @@ public void UpdateCatalogWithJsonlItemsFileShouldReturnTaskInfo() throws Excepti files.put("items", jsonlItemsFile); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); String response = constructor.updateCatalog(req); JSONObject jsonObj = new JSONObject(response); @@ -471,7 +476,7 @@ public void PatchCatalogWithJsonlItemsFileShouldReturnTaskInfo() throws Exceptio files.put("items", jsonlItemsFile); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); String response = constructor.patchCatalog(req); JSONObject jsonObj = new JSONObject(response); @@ -488,7 +493,7 @@ public void ReplaceCatalogWithInvalidExtensionShouldError() throws Exception { files.put("items", invalidExtensionFile); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); thrown.expect(ConstructorException.class); thrown.expectMessage("Invalid file type for 'items'"); @@ -505,7 +510,7 @@ public void UpdateCatalogWithNoExtensionShouldError() throws Exception { files.put("items", noExtensionFile); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); thrown.expect(ConstructorException.class); thrown.expectMessage("Invalid file for 'items'"); @@ -522,7 +527,7 @@ public void PatchCatalogWithInvalidExtensionShouldError() throws Exception { files.put("items", invalidExtensionFile); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); thrown.expect(ConstructorException.class); thrown.expectMessage("Invalid file type for 'items'"); @@ -539,7 +544,7 @@ public void UpdateCatalogWithInvalidExtensionShouldError() throws Exception { files.put("items", invalidExtensionFile); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); thrown.expect(ConstructorException.class); thrown.expectMessage("Invalid file type for 'items'"); @@ -558,7 +563,7 @@ public void ReplaceCatalogWithNullFileShouldError() throws Exception { files.put("items", null); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); thrown.expect(ConstructorException.class); thrown.expectMessage("Invalid file for 'items'"); @@ -573,7 +578,7 @@ public void UpdateCatalogWithNullFileShouldError() throws Exception { files.put("items", null); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); thrown.expect(ConstructorException.class); thrown.expectMessage("Invalid file for 'items'"); @@ -588,7 +593,7 @@ public void PatchCatalogWithNullFileShouldError() throws Exception { files.put("items", null); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); thrown.expect(ConstructorException.class); thrown.expectMessage("Invalid file for 'items'"); @@ -604,7 +609,7 @@ public void ReplaceCatalogWithMixedFileTypesShouldSucceed() throws Exception { files.put("items", new File("src/test/resources/csv/items.csv")); files.put("variations", jsonlVariationsFile); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); String response = constructor.replaceCatalog(req); JSONObject jsonObj = new JSONObject(response); @@ -620,7 +625,7 @@ public void UpdateCatalogWithJsonlVariationsAndItemGroupsShouldSucceed() throws files.put("variations", jsonlVariationsFile); files.put("item_groups", jsonlItemGroupsFile); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); String response = constructor.updateCatalog(req); JSONObject jsonObj = new JSONObject(response); @@ -637,7 +642,7 @@ public void PatchCatalogWithAllJsonlFilesShouldSucceed() throws Exception { files.put("variations", jsonlVariationsFile); files.put("item_groups", jsonlItemGroupsFile); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); String response = constructor.patchCatalog(req); JSONObject jsonObj = new JSONObject(response); @@ -654,7 +659,7 @@ public void ReplaceCatalogWithJsonItemsFileShouldReturnTaskInfo() throws Excepti files.put("items", jsonItemsFile); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); String response = constructor.replaceCatalog(req); JSONObject jsonObj = new JSONObject(response); @@ -669,7 +674,7 @@ public void UpdateCatalogWithJsonItemsFileShouldReturnTaskInfo() throws Exceptio files.put("items", jsonItemsFile); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); String response = constructor.updateCatalog(req); JSONObject jsonObj = new JSONObject(response); @@ -684,7 +689,52 @@ public void PatchCatalogWithJsonItemsFileShouldReturnTaskInfo() throws Exception files.put("items", jsonItemsFile); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); + String response = constructor.patchCatalog(req); + JSONObject jsonObj = new JSONObject(response); + + assertTrue("task_id exists", jsonObj.has("task_id") == true); + assertTrue("task_status_path exists", jsonObj.has("task_status_path") == true); + } + + @Test + public void ReplaceCatalogWithJsonItemGroupsFileShouldReturnTaskInfo() throws Exception { + ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); + Map files = new HashMap(); + + files.put("item_groups", jsonItemGroupsFile); + + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); + String response = constructor.replaceCatalog(req); + JSONObject jsonObj = new JSONObject(response); + + assertTrue("task_id exists", jsonObj.has("task_id") == true); + assertTrue("task_status_path exists", jsonObj.has("task_status_path") == true); + } + + @Test + public void UpdateCatalogWithJsonItemGroupsFileShouldReturnTaskInfo() throws Exception { + ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); + Map files = new HashMap(); + + files.put("item_groups", jsonItemGroupsFile); + + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); + String response = constructor.updateCatalog(req); + JSONObject jsonObj = new JSONObject(response); + + assertTrue("task_id exists", jsonObj.has("task_id") == true); + assertTrue("task_status_path exists", jsonObj.has("task_status_path") == true); + } + + @Test + public void PatchCatalogWithJsonItemGroupsFileShouldReturnTaskInfo() throws Exception { + ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); + Map files = new HashMap(); + + files.put("item_groups", jsonItemGroupsFile); + + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); String response = constructor.patchCatalog(req); JSONObject jsonObj = new JSONObject(response); @@ -701,7 +751,7 @@ public void ReplaceCatalogWithMixedCsvJsonJsonlShouldSucceed() throws Exception files.put("variations", jsonVariationsFile); files.put("item_groups", jsonlItemGroupsFile); - CatalogRequest req = new CatalogRequest(files, "Products"); + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); String response = constructor.replaceCatalog(req); JSONObject jsonObj = new JSONObject(response); diff --git a/constructorio-client/src/test/java/io/constructor/client/Utils.java b/constructorio-client/src/test/java/io/constructor/client/Utils.java index 077afaf8..fd036b78 100644 --- a/constructorio-client/src/test/java/io/constructor/client/Utils.java +++ b/constructorio-client/src/test/java/io/constructor/client/Utils.java @@ -220,6 +220,36 @@ public static File createVariationsJsonlFile(int count) throws IOException { return file; } + /** + * Creates a temporary JSON file containing an array of item groups. + * + * @param count the number of item groups to generate + * @return a temporary File with .json extension + * @throws IOException if file creation fails + */ + public static File createItemGroupsJsonFile(int count) throws IOException { + File file = File.createTempFile("item_groups", ".json"); + file.deleteOnExit(); + + List> groups = new ArrayList>(); + for (int i = 0; i < count; i++) { + Map dataMap = new HashMap(); + dataMap.put("parent_id", "root"); + + Map group = new HashMap(); + group.put("id", "group" + UUID.randomUUID().toString().substring(0, 8)); + group.put("name", "Group " + (i + 1)); + group.put("data", dataMap); + + groups.add(group); + } + + try (FileWriter writer = new FileWriter(file)) { + writer.write(gson.toJson(groups)); + } + return file; + } + /** * Creates a temporary JSONL file containing item groups (one per line). *