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..f8ea8a42 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.LinkedHashSet; 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 LinkedHashSet<>(Arrays.asList(".csv", ".json", ".jsonl")); + /** the HTTP client used by all instances */ private static OkHttpClient client = new OkHttpClient.Builder() @@ -2031,6 +2037,53 @@ 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. + * + * @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 not in VALID_CATALOG_EXTENSIONS + */ + 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 " + + VALID_CATALOG_EXTENSIONS + + " extension. Found: " + + actualFileName); + } + + String extension = actualFileName.substring(lastDotIndex).toLowerCase(); + + if (!VALID_CATALOG_EXTENSIONS.contains(extension)) { + throw new ConstructorException( + "Invalid file type for '" + + fileName + + "': file must have " + + VALID_CATALOG_EXTENSIONS + + " extension. Found: " + + actualFileName); + } + + return extension; + } + /** * Grabs the version number (hard coded ATM) * @@ -2308,9 +2361,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 +2392,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 +2422,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 +2453,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 +2484,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 +2520,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)); } } 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..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"); @@ -25,6 +27,15 @@ public class ConstructorIOCatalogTest { private String baseUrl = "https://raw.githubusercontent.com/Constructor-io/integration-examples/main/catalog/"; + private File jsonItemsFile; + private File jsonVariationsFile; + private File jsonItemGroupsFile; + private File jsonlItemsFile; + private File jsonlVariationsFile; + private File jsonlItemGroupsFile; + private File invalidExtensionFile; + private File noExtensionFile; + @Rule public ExpectedException thrown = ExpectedException.none(); @Before @@ -37,6 +48,16 @@ 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); + jsonItemGroupsFile = Utils.createItemGroupsJsonFile(2); + jsonlItemsFile = Utils.createItemsJsonlFile(3); + jsonlVariationsFile = Utils.createVariationsJsonlFile(3); + jsonlItemGroupsFile = Utils.createItemGroupsJsonlFile(2); + invalidExtensionFile = Utils.createInvalidExtensionFile(); + noExtensionFile = Utils.createNoExtensionFile(); } @After @@ -45,13 +66,23 @@ public void teardown() throws Exception { variationsFile.delete(); itemGroupsFile.delete(); csvFolder.delete(); + + // 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(); + if (invalidExtensionFile != null) invalidExtensionFile.delete(); + if (noExtensionFile != null) noExtensionFile.delete(); } @Test 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( @@ -66,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); @@ -81,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"); @@ -99,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"); @@ -116,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); @@ -135,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); @@ -153,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); @@ -165,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( @@ -180,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); @@ -195,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"); @@ -213,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"); @@ -230,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); @@ -249,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); @@ -267,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); @@ -279,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( @@ -294,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); @@ -309,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"); @@ -327,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"); @@ -344,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); @@ -361,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); @@ -380,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); @@ -398,11 +429,333 @@ 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); + + 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", jsonlItemsFile); + + 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 UpdateCatalogWithJsonlItemsFileShouldReturnTaskInfo() throws Exception { + ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); + Map files = new HashMap(); + + files.put("items", jsonlItemsFile); + + 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 PatchCatalogWithJsonlItemsFileShouldReturnTaskInfo() throws Exception { + ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); + Map files = new HashMap(); + + files.put("items", jsonlItemsFile); + + 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); + } + + // Invalid Extension Tests + + @Test + public void ReplaceCatalogWithInvalidExtensionShouldError() throws Exception { + ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); + Map files = new HashMap(); + + files.put("items", invalidExtensionFile); + + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); + + thrown.expect(ConstructorException.class); + thrown.expectMessage("Invalid file type for 'items'"); + thrown.expectMessage(".csv"); + thrown.expectMessage(".json"); + thrown.expectMessage(".jsonl"); + constructor.replaceCatalog(req); + } + + @Test + public void UpdateCatalogWithNoExtensionShouldError() throws Exception { + ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); + Map files = new HashMap(); + + files.put("items", noExtensionFile); + + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); + + thrown.expect(ConstructorException.class); + thrown.expectMessage("Invalid file for 'items'"); + thrown.expectMessage(".csv"); + thrown.expectMessage(".json"); + thrown.expectMessage(".jsonl"); + constructor.updateCatalog(req); + } + + @Test + public void PatchCatalogWithInvalidExtensionShouldError() throws Exception { + ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); + Map files = new HashMap(); + + files.put("items", invalidExtensionFile); + + CatalogRequest req = new CatalogRequest(files, PRODUCTS_SECTION); + + thrown.expect(ConstructorException.class); + thrown.expectMessage("Invalid file type for 'items'"); + thrown.expectMessage(".csv"); + thrown.expectMessage(".json"); + thrown.expectMessage(".jsonl"); + 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_SECTION); + + 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 + 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_SECTION); + + thrown.expect(ConstructorException.class); + thrown.expectMessage("Invalid file for 'items'"); + thrown.expectMessage("file cannot be null"); + 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_SECTION); + + 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_SECTION); + + 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); + Map files = new HashMap(); + + files.put("items", new File("src/test/resources/csv/items.csv")); + files.put("variations", jsonlVariationsFile); + + 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 UpdateCatalogWithJsonlVariationsAndItemGroupsShouldSucceed() throws Exception { + ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); + Map files = new HashMap(); + + files.put("variations", jsonlVariationsFile); + files.put("item_groups", jsonlItemGroupsFile); + + 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 PatchCatalogWithAllJsonlFilesShouldSucceed() throws Exception { + ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); + Map files = new HashMap(); + + files.put("items", jsonlItemsFile); + files.put("variations", jsonlVariationsFile); + files.put("item_groups", jsonlItemGroupsFile); + + 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); + } + + // JSON Format Tests + + @Test + public void ReplaceCatalogWithJsonItemsFileShouldReturnTaskInfo() throws Exception { + ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); + Map files = new HashMap(); + + files.put("items", jsonItemsFile); + + 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 UpdateCatalogWithJsonItemsFileShouldReturnTaskInfo() throws Exception { + ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); + Map files = new HashMap(); + + files.put("items", jsonItemsFile); + + 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 PatchCatalogWithJsonItemsFileShouldReturnTaskInfo() throws Exception { + ConstructorIO constructor = new ConstructorIO(token, apiKey, true, null); + Map files = new HashMap(); + + files.put("items", jsonItemsFile); + + 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); + + 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", jsonVariationsFile); + files.put("item_groups", jsonlItemGroupsFile); + + 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); + } } 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..fd036b78 100644 --- a/constructorio-client/src/test/java/io/constructor/client/Utils.java +++ b/constructorio-client/src/test/java/io/constructor/client/Utils.java @@ -1,11 +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; @@ -113,4 +119,197 @@ public static void enableHTTPLogging() { OkHttpClient newClient = client.newBuilder().addInterceptor(logger).build(); ConstructorIO.setHttpClient(newClient); } + + private static final Gson gson = new Gson(); + + /** + * 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 + * @throws IOException if file creation fails + */ + public static File createItemsJsonFile(int count) throws IOException { + File file = File.createTempFile("items", ".json"); + file.deleteOnExit(); + + List> items = new ArrayList>(); + for (int i = 0; i < count; i++) { + ConstructorItem item = createProductItem(); + items.add(item.toMap()); + } + + try (FileWriter writer = new FileWriter(file)) { + 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 + * @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++) { + ConstructorItem item = createProductItem(); + sb.append(gson.toJson(item.toMap())).append("\n"); + } + + try (FileWriter writer = new FileWriter(file)) { + writer.write(sb.toString()); + } + return file; + } + + /** + * 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 + * @throws IOException if file creation fails + */ + public static File createVariationsJsonFile(int count) throws IOException { + File file = File.createTempFile("variations", ".json"); + file.deleteOnExit(); + + List> variations = new ArrayList>(); + for (int i = 0; i < count; i++) { + String itemId = "item" + ((i % 3) + 1); + ConstructorVariation variation = createProductVariation(itemId); + variations.add(variation.toMap()); + } + + try (FileWriter writer = new FileWriter(file)) { + 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 + * @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 itemId = "item" + ((i % 3) + 1); + ConstructorVariation variation = createProductVariation(itemId); + sb.append(gson.toJson(variation.toMap())).append("\n"); + } + + try (FileWriter writer = new FileWriter(file)) { + writer.write(sb.toString()); + } + 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). + * + * @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++) { + 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)) { + 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 { + 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(file)) { + writer.write("This file has no extension for testing validation."); + } + return file; + } }