From 1ed558d4b31fbe0ed028027db91d9718dfdb5fa7 Mon Sep 17 00:00:00 2001 From: reeshika-h Date: Wed, 24 Jun 2026 14:26:01 +0530 Subject: [PATCH] feat: add asset scanning tests and update header assertions in AssetUnitTest and AssetAPITest --- .../com/contentstack/cms/UnitTestSuite.java | 2 + .../contentstack/cms/stack/AssetAPITest.java | 314 +++++++++++++++--- .../contentstack/cms/stack/AssetUnitTest.java | 125 ++++++- 3 files changed, 377 insertions(+), 64 deletions(-) diff --git a/src/test/java/com/contentstack/cms/UnitTestSuite.java b/src/test/java/com/contentstack/cms/UnitTestSuite.java index 6c514051..9a54d743 100644 --- a/src/test/java/com/contentstack/cms/UnitTestSuite.java +++ b/src/test/java/com/contentstack/cms/UnitTestSuite.java @@ -2,6 +2,7 @@ import com.contentstack.cms.core.AuthInterceptorTest; import com.contentstack.cms.core.EndpointTest; +import com.contentstack.cms.stack.AssetUnitTest; import com.contentstack.cms.stack.EnvironmentUnitTest; import com.contentstack.cms.stack.GlobalFieldUnitTests; import com.contentstack.cms.stack.LocaleUnitTest; @@ -27,6 +28,7 @@ EndpointTest.class, // Stack module tests (only public classes) + AssetUnitTest.class, EnvironmentUnitTest.class, GlobalFieldUnitTests.class, LocaleUnitTest.class, diff --git a/src/test/java/com/contentstack/cms/stack/AssetAPITest.java b/src/test/java/com/contentstack/cms/stack/AssetAPITest.java index e1090709..34da4a33 100644 --- a/src/test/java/com/contentstack/cms/stack/AssetAPITest.java +++ b/src/test/java/com/contentstack/cms/stack/AssetAPITest.java @@ -5,18 +5,14 @@ import okhttp3.Request; import okhttp3.ResponseBody; import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; import org.junit.jupiter.api.*; import retrofit2.Response; import java.io.IOException; import java.util.HashMap; import java.util.Objects; -import com.contentstack.cms.stack.FileUploader; -import org.junit.jupiter.api.Test; -import java.io.File; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertEquals; @Tag("API") class AssetAPITest { @@ -51,7 +47,8 @@ void testFindAssets() { Assertions.assertEquals(3, request.headers().size()); Assertions.assertTrue(request.isHttps(), "always works on https"); Assertions.assertEquals("GET", request.method(), "works with GET call"); - Assertions.assertEquals("https", request.url().scheme(), "the scheme should be https"); Assertions.assertEquals(443, request.url().port(), "port should be 443"); + Assertions.assertEquals("https", request.url().scheme(), "the scheme should be https"); + Assertions.assertEquals(443, request.url().port(), "port should be 443"); Assertions.assertTrue(request.url().pathSegments().contains("v3"), "the first segment of url should be v3"); Assertions.assertTrue(request.url().pathSegments().contains("assets"), "url segment should contain assets"); Assertions.assertFalse(Objects.requireNonNull(request.url().query()).isEmpty(), @@ -68,66 +65,59 @@ void testFetch() { asset.addParam("include_publish_details", true); asset.addParam("environment", "production"); asset.addParam("relative_urls", false); - - // Headers api_key(required) & authorization(required) asset.addHeader("api_key", API_KEY); asset.addHeader("authorization", MANAGEMENT_TOKEN); asset.addHeader("authtoken", AUTHTOKEN); - // Create Asset Instance to find all assets Request request = asset.fetch().request(); Assertions.assertEquals(3, request.headers().size()); Assertions.assertTrue(request.isHttps(), "always works on https"); Assertions.assertEquals("GET", request.method(), "works with GET call"); - Assertions.assertEquals("https", request.url().scheme(), "the scheme should be https"); Assertions.assertEquals(443, request.url().port(), "port should be 443"); + Assertions.assertEquals("https", request.url().scheme(), "the scheme should be https"); + Assertions.assertEquals(443, request.url().port(), "port should be 443"); Assertions.assertTrue(request.url().pathSegments().contains("v3"), "the first segment of url should be v3"); Assertions.assertTrue(request.url().pathSegments().contains("assets"), "url segment should contain assets"); Assertions.assertFalse(Objects.requireNonNull(request.url().query()).isEmpty(), "query params should not be empty"); - } @Order(3) @Test void testGetAssetByFolderUid() throws IOException { asset.clearParams(); - // Headers api_key(required) & authorization(required) asset.addHeader("api_key", API_KEY); asset.addHeader("authorization", MANAGEMENT_TOKEN); - // Create Asset Instance to find all assets Response resp = asset.byFolderUid(_uid).execute(); Assertions.assertEquals(5, resp.raw().request().headers().size()); Assertions.assertTrue(resp.raw().request().headers().names().contains("api_key")); Assertions.assertTrue(resp.raw().request().headers().names().contains("authorization")); Assertions.assertTrue(resp.raw().request().isHttps(), "always works on https"); Assertions.assertEquals("GET", resp.raw().request().method(), "works with GET call"); - Assertions.assertEquals("https", resp.raw().request().url().scheme(), "the scheme should be https"); Assertions.assertEquals(443, resp.raw().request().url().port(), "port should be 443"); + Assertions.assertEquals("https", resp.raw().request().url().scheme(), "the scheme should be https"); + Assertions.assertEquals(443, resp.raw().request().url().port(), "port should be 443"); Assertions.assertTrue(resp.raw().request().url().pathSegments().contains("v3"), "the first segment of url should be v3"); Assertions.assertTrue(resp.raw().request().url().pathSegments().contains("assets"), "url segment should contain assets"); Assertions.assertFalse(Objects.requireNonNull(resp.raw().request().url().query()).isEmpty(), "query params should not be empty"); - } @Test void testAssetSubFolder() throws IOException { asset.clearParams(); - // Headers api_key(required) & authorization(required) asset.addHeader("api_key", API_KEY); asset.addHeader("authtoken", AUTHTOKEN); - // Create Asset Instance to find all assets Request request = asset.subfolder(_uid, true).request(); Assertions.assertTrue(request.headers().names().contains("api_key")); Assertions.assertTrue(request.headers().names().contains("authtoken")); Assertions.assertTrue(request.isHttps(), "always works on https"); Assertions.assertEquals("GET", request.method(), "works with GET call"); - Assertions.assertEquals("https", request.url().scheme(), "the scheme should be https"); Assertions.assertEquals(443, request.url().port(), "port should be 443"); + Assertions.assertEquals("https", request.url().scheme(), "the scheme should be https"); + Assertions.assertEquals(443, request.url().port(), "port should be 443"); Assertions.assertTrue(request.url().pathSegments().contains("v3"), "the first segment of url should be v3"); Assertions.assertTrue(request.url().pathSegments().contains("assets"), "url segment should contain assets"); Assertions.assertFalse(Objects.requireNonNull(request.url().query()).isEmpty(), "query params should not be empty"); - } @Test @@ -142,18 +132,17 @@ void testAssetUpload() { String filePath = "/Users/shaileshmishra/Desktop/image.jpeg"; String description = "The calender has been placed to assets by shaileshmishra"; Request request = asset.uploadAsset(filePath, description).request(); - // The assertions Assertions.assertEquals(3, request.headers().size()); Assertions.assertTrue(request.headers().names().contains("api_key")); Assertions.assertTrue(request.headers().names().contains("authtoken")); Assertions.assertTrue(request.isHttps(), "always works on https"); Assertions.assertEquals("POST", request.method(), "works with GET call"); - Assertions.assertEquals("https", request.url().scheme(), "the scheme should be https"); Assertions.assertEquals(443, request.url().port(), "port should be 443"); + Assertions.assertEquals("https", request.url().scheme(), "the scheme should be https"); + Assertions.assertEquals(443, request.url().port(), "port should be 443"); Assertions.assertTrue(request.url().pathSegments().contains("v3"), "the first segment of url should be v3"); Assertions.assertTrue(request.url().pathSegments().contains("assets"), "url segment should contain assets"); Assertions.assertFalse(Objects.requireNonNull(request.url().query()).isEmpty(), "query params should not be empty"); - } @Test @@ -161,30 +150,26 @@ void testAssetUpload() { void testAssetReplace() throws IOException { asset.clearParams(); asset = client.stack().asset(_uid); - asset.addParam("include_branch", true); asset.addParam("relative_urls", false); asset.addParam("environment", "production"); - // Headers api_key(required) & authorization(required) asset.addHeader("api_key", API_KEY); asset.addHeader("authorization", MANAGEMENT_TOKEN); - // Create Asset Instance to find all assets String filePath = "/Users/shaileshmishra/Downloads/calendar.png"; Response resp = asset.replace(filePath, "Assets created by ***REMOVED***").execute(); - // The assertions Assertions.assertEquals(6, resp.raw().request().headers().size()); Assertions.assertTrue(resp.raw().request().headers().names().contains("api_key")); Assertions.assertTrue(resp.raw().request().headers().names().contains("authtoken")); Assertions.assertTrue(resp.raw().request().isHttps(), "always works on https"); Assertions.assertEquals("PUT", resp.raw().request().method(), "works with GET call"); - Assertions.assertEquals("https", resp.raw().request().url().scheme(), "the scheme should be https"); Assertions.assertEquals(443, resp.raw().request().url().port(), "port should be 443"); + Assertions.assertEquals("https", resp.raw().request().url().scheme(), "the scheme should be https"); + Assertions.assertEquals(443, resp.raw().request().url().port(), "port should be 443"); Assertions.assertTrue(resp.raw().request().url().pathSegments().contains("v3"), "the first segment of url should be v3"); Assertions.assertTrue(resp.raw().request().url().pathSegments().contains("assets"), "url segment should contain assets"); Assertions.assertFalse(Objects.requireNonNull(resp.raw().request().url().query()).isEmpty(), "query params should not be empty"); - } @Test @@ -198,13 +183,13 @@ void testAssetGeneratePermanentUrl() throws IOException { subBody.put("permanent_url", "https://api.contentstack.io/v3/assets/stack_api_key/asset_UID/sample-slug.jpeg"); body.put("asset", body); Response resp = asset.generatePermanentUrl(body).execute(); - // The assertions Assertions.assertEquals(6, resp.raw().request().headers().size()); Assertions.assertTrue(resp.raw().request().headers().names().contains("api_key")); Assertions.assertTrue(resp.raw().request().headers().names().contains("authtoken")); Assertions.assertTrue(resp.raw().request().isHttps(), "always works on https"); Assertions.assertEquals("PUT", resp.raw().request().method(), "works with GET call"); - Assertions.assertEquals("https", resp.raw().request().url().scheme(), "the scheme should be https"); Assertions.assertEquals(443, resp.raw().request().url().port(), "port should be 443"); + Assertions.assertEquals("https", resp.raw().request().url().scheme(), "the scheme should be https"); + Assertions.assertEquals(443, resp.raw().request().url().port(), "port should be 443"); Assertions.assertTrue(resp.raw().request().url().pathSegments().contains("v3"), "the first segment of url should be v3"); Assertions.assertTrue(resp.raw().request().url().pathSegments().contains("assets"), @@ -216,24 +201,21 @@ void testAssetGeneratePermanentUrl() throws IOException { @Test void testAssetDownloadPermanentUrl() throws IOException { asset = client.stack().asset(_uid); - // Headers api_key(required) & authorization(required) asset.addHeader("api_key", API_KEY); asset.addHeader("authorization", MANAGEMENT_TOKEN); asset.addHeader("authtoken", AUTHTOKEN); - // Create Asset Instance to find all assets Request request = asset.getPermanentUrl("www.google.com/search").request(); - // The assertions Assertions.assertEquals(3, request.headers().size()); Assertions.assertTrue(request.headers().names().contains("api_key")); Assertions.assertTrue(request.headers().names().contains("authtoken")); Assertions.assertTrue(request.isHttps(), "always works on https"); Assertions.assertEquals("GET", request.method(), "works with GET call"); - Assertions.assertEquals("https", request.url().scheme(), "the scheme should be https"); Assertions.assertEquals(443, request.url().port(), "port should be 443"); + Assertions.assertEquals("https", request.url().scheme(), "the scheme should be https"); + Assertions.assertEquals(443, request.url().port(), "port should be 443"); Assertions.assertTrue(request.url().pathSegments().contains("v3"), "the first segment of url should be v3"); Assertions.assertTrue(request.url().pathSegments().contains("assets"), "url segment should contain assets"); } - @Test void testAssetUploadWithMultipleParams() throws IOException { String description = "The calender has been placed to assets by ***REMOVED***"; @@ -259,36 +241,44 @@ void testFetchSingleAssetPojo() throws IOException { .addParam("include_count", true); Request request = asset.fetchAsPojo().request(); Assertions.assertEquals("GET", request.method()); - Assertions.assertEquals("https", request.url().scheme()); Assertions.assertEquals(443, request.url().port()); + Assertions.assertEquals("https", request.url().scheme()); + Assertions.assertEquals(443, request.url().port()); Assertions.assertTrue(request.url().pathSegments().contains("v3")); - Assertions.assertTrue(request.url().pathSegments().contains("assets")); } + Assertions.assertTrue(request.url().pathSegments().contains("assets")); + } @Test void testFetchAllAssetsPojo() throws IOException { asset = client.stack().asset(); Request request = asset.findAsPojo().request(); Assertions.assertEquals("GET", request.method()); - Assertions.assertEquals("https", request.url().scheme()); Assertions.assertEquals(443, request.url().port()); + Assertions.assertEquals("https", request.url().scheme()); + Assertions.assertEquals(443, request.url().port()); Assertions.assertTrue(request.url().pathSegments().contains("v3")); - Assertions.assertTrue(request.url().pathSegments().contains("assets")); } + Assertions.assertTrue(request.url().pathSegments().contains("assets")); + } @Test void testFetchAssetsByFolderUidPojo() throws IOException { asset = client.stack().asset(); Request request = asset.byFolderUidAsPojo("folder_uid").request(); Assertions.assertEquals("GET", request.method()); - Assertions.assertEquals("https", request.url().scheme()); Assertions.assertEquals(443, request.url().port()); + Assertions.assertEquals("https", request.url().scheme()); + Assertions.assertEquals(443, request.url().port()); Assertions.assertTrue(request.url().pathSegments().contains("v3")); - Assertions.assertTrue(request.url().pathSegments().contains("assets")); } + Assertions.assertTrue(request.url().pathSegments().contains("assets")); + } @Test void testFetchAssetsBySubFolderUidPojo() throws IOException { asset = client.stack().asset(); Request request = asset.subfolderAsPojo("subfolder_uid", true).request(); Assertions.assertEquals("GET", request.method()); - Assertions.assertEquals("https", request.url().scheme()); Assertions.assertEquals(443, request.url().port()); + Assertions.assertEquals("https", request.url().scheme()); + Assertions.assertEquals(443, request.url().port()); Assertions.assertTrue(request.url().pathSegments().contains("v3")); - Assertions.assertTrue(request.url().pathSegments().contains("assets")); } + Assertions.assertTrue(request.url().pathSegments().contains("assets")); + } @Test void testFetchSingleFolderByNamePojo() { @@ -299,25 +289,245 @@ void testFetchSingleFolderByNamePojo() { asset.addParam("query", queryContent); Request request = asset.getSingleFolderByNameAsPojo().request(); Assertions.assertEquals("GET", request.method()); - Assertions.assertEquals("https", request.url().scheme()); Assertions.assertEquals(443, request.url().port()); + Assertions.assertEquals("https", request.url().scheme()); + Assertions.assertEquals(443, request.url().port()); Assertions.assertTrue(request.url().pathSegments().contains("v3")); - Assertions.assertTrue(request.url().pathSegments().contains("assets")); } + Assertions.assertTrue(request.url().pathSegments().contains("assets")); + } @Test void testFetchSubfoldersByParentFolderPojo() { asset = client.stack().asset(); HashMap queryContent = new HashMap<>(); - queryContent.put("is_dir", true); + queryContent.put("is_dir", true); asset.addParam("folder", "test_folder"); - asset.addParam("include_folders", true); + asset.addParam("include_folders", true); asset.addParam("query", queryContent); queryContent.put("parent_uid", "parent_uid"); asset.addParam("query", queryContent); Request request = asset.getSubfolderAsPojo().request(); Assertions.assertEquals("GET", request.method()); - Assertions.assertEquals("https", request.url().scheme()); Assertions.assertEquals(443, request.url().port()); + Assertions.assertEquals("https", request.url().scheme()); + Assertions.assertEquals(443, request.url().port()); Assertions.assertTrue(request.url().pathSegments().contains("v3")); - Assertions.assertTrue(request.url().pathSegments().contains("assets")); } + Assertions.assertTrue(request.url().pathSegments().contains("assets")); + } + + // ==================== Asset Scanning API Tests ==================== + static String uploadedAssetUid; + + @Test + @Order(10) + void testUploadAssetIncludesScanStatusPending() throws IOException, ParseException { + Asset uploadAsset = client.stack(API_KEY, MANAGEMENT_TOKEN).asset(); + uploadAsset.addParam("include_asset_scan_status", true); + uploadAsset.addHeader("api_key", API_KEY); + uploadAsset.addHeader("authorization", MANAGEMENT_TOKEN); + Response response = uploadAsset.uploadAsset("src/test/resources/asset.png", "asset scan test upload").execute(); + Assertions.assertTrue(response.isSuccessful(), "Expected 201 on upload"); + Assertions.assertNotNull(response.body(), "Response body should not be null"); + String body = response.body().string(); + Assertions.assertTrue(body.contains("_asset_scan_status"), + "Upload response should include _asset_scan_status when param is set"); + Assertions.assertTrue(body.contains("pending"), + "Newly uploaded asset scan status should be pending"); + JSONParser parser = new JSONParser(); + JSONObject json = (JSONObject) parser.parse(body); + JSONObject assetObj = (JSONObject) json.get("asset"); + if (assetObj != null) { + uploadedAssetUid = (String) assetObj.get("uid"); + } + Assertions.assertNotNull(uploadedAssetUid, "Could not extract UID of uploaded asset"); + } + + @Test + @Order(11) + void testFindAssetsWithScanStatus() throws IOException { + Assumptions.assumeTrue(uploadedAssetUid != null, "Skipping: no uploaded asset from testUploadAssetIncludesScanStatusPending"); + asset.clearParams(); + asset.addParam("include_asset_scan_status", true); + asset.addHeader("api_key", API_KEY); + asset.addHeader("authorization", MANAGEMENT_TOKEN); + Response response = asset.find().execute(); + Assertions.assertTrue(response.isSuccessful(), "Expected 200"); + Assertions.assertNotNull(response.body()); + String body = response.body().string(); + Assertions.assertTrue(body.contains("_asset_scan_status"), + "Response should include _asset_scan_status field when param is set"); + Assertions.assertTrue(body.contains(uploadedAssetUid), + "Uploaded asset UID should appear in the list response"); + } + + @Test + @Order(12) + void testFetchAssetWithScanStatus() throws IOException { + Assumptions.assumeTrue(uploadedAssetUid != null, "Skipping: no uploaded asset from testUploadAssetIncludesScanStatusPending"); + Asset scanAsset = client.stack(API_KEY, MANAGEMENT_TOKEN).asset(uploadedAssetUid); + scanAsset.addParam("include_asset_scan_status", true); + scanAsset.addHeader("api_key", API_KEY); + scanAsset.addHeader("authorization", MANAGEMENT_TOKEN); + Response response = scanAsset.fetch().execute(); + Assertions.assertTrue(response.raw().request().url().toString().contains("include_asset_scan_status=true")); + Assertions.assertTrue(response.isSuccessful(), "Expected 200 for asset UID: " + uploadedAssetUid); + Assertions.assertNotNull(response.body()); + String body = response.body().string(); + Assertions.assertTrue(body.contains("_asset_scan_status"), + "Response should include _asset_scan_status for the uploaded asset"); + } + + @Test + @Order(13) + void testFindAssetsAllScanStatusesAreValid() throws IOException { + Asset listAsset = client.stack(API_KEY, MANAGEMENT_TOKEN).asset(); + listAsset.addParam("include_asset_scan_status", true); + listAsset.addHeader("api_key", API_KEY); + listAsset.addHeader("authorization", MANAGEMENT_TOKEN); + Response response = listAsset.find().execute(); + Assertions.assertTrue(response.isSuccessful(), "Expected 200"); + Assertions.assertNotNull(response.body()); + String body = response.body().string(); + Assertions.assertTrue(body.contains("_asset_scan_status"), + "Response should include _asset_scan_status when param is set"); + boolean hasValidStatus = body.contains("\"pending\"") || body.contains("\"clean\"") + || body.contains("\"not_scanned\"") || body.contains("\"quarantined\""); + Assertions.assertTrue(hasValidStatus, + "Every _asset_scan_status value must be one of: pending, clean, not_scanned, quarantined"); + } + + @Test + @Order(14) + void testPublishAssetWithApiVersionHeader() throws IOException { + Assumptions.assumeTrue(uploadedAssetUid != null, "Skipping: no uploaded asset from testUploadAssetIncludesScanStatusPending"); + Asset scanAsset = client.stack(API_KEY, MANAGEMENT_TOKEN).asset(uploadedAssetUid); + scanAsset.addHeader("api_key", API_KEY); + scanAsset.addHeader("authorization", MANAGEMENT_TOKEN); + scanAsset.addHeader("api_version", "3.2"); + JSONObject publishBody = new JSONObject(); + JSONObject publishDetails = new JSONObject(); + publishDetails.put("locales", new String[]{"en-us"}); + publishDetails.put("environments", new String[]{"development"}); + publishBody.put("asset", publishDetails); + Response response = scanAsset.publish(publishBody).execute(); + Assertions.assertEquals("3.2", response.raw().request().header("api_version"), + "api_version: 3.2 header must be present on publish for CDA-side scan validation"); + Assertions.assertTrue(response.isSuccessful(), "Expected 200 — publish always returns success regardless of scan status"); + Assertions.assertNotNull(response.body()); + String body = response.body().string(); + Assertions.assertTrue(body.contains("Asset sent for publishing"), + "Publish response notice should confirm asset was sent for publishing"); + } + + @Test + @Order(15) + void testFetchUploadedAssetScanStatusIsValid() throws IOException, ParseException { + Assumptions.assumeTrue(uploadedAssetUid != null, "Skipping: no uploaded asset from testUploadAssetIncludesScanStatusPending"); + Asset scanAsset = client.stack(API_KEY, MANAGEMENT_TOKEN).asset(uploadedAssetUid); + scanAsset.addParam("include_asset_scan_status", true); + scanAsset.addHeader("api_key", API_KEY); + scanAsset.addHeader("authorization", MANAGEMENT_TOKEN); + Response response = scanAsset.fetch().execute(); + Assertions.assertTrue(response.isSuccessful(), "Expected 200 for asset UID: " + uploadedAssetUid); + Assertions.assertNotNull(response.body()); + String body = response.body().string(); + Assertions.assertTrue(body.contains("_asset_scan_status"), + "Response should include _asset_scan_status when param is set"); + JSONParser parser = new JSONParser(); + JSONObject json = (JSONObject) parser.parse(body); + JSONObject assetObj = (JSONObject) json.get("asset"); + Assertions.assertNotNull(assetObj, "Response should contain an 'asset' object"); + String status = (String) assetObj.get("_asset_scan_status"); + Assertions.assertNotNull(status, "_asset_scan_status should be present in asset object"); + java.util.Set validStatuses = java.util.Set.of("pending", "clean", "not_scanned", "quarantined"); + Assertions.assertTrue(validStatuses.contains(status), + "_asset_scan_status value '" + status + "' must be one of: pending, clean, not_scanned, quarantined"); + } + + @Test + @Order(16) + void testFindAssetsWithoutScanStatusParamFieldAbsent() throws IOException { + Asset listAsset = client.stack(API_KEY, MANAGEMENT_TOKEN).asset(); + listAsset.addHeader("api_key", API_KEY); + listAsset.addHeader("authorization", MANAGEMENT_TOKEN); + Response response = listAsset.find().execute(); + Assertions.assertTrue(response.isSuccessful(), "Expected 200"); + Assertions.assertNotNull(response.body()); + String body = response.body().string(); + Assertions.assertFalse(body.contains("_asset_scan_status"), + "_asset_scan_status must be absent when include_asset_scan_status param is not passed"); + } + + @Test + @Order(17) + void testFetchAssetWithoutScanStatusParamFieldAbsent() throws IOException { + Assumptions.assumeTrue(uploadedAssetUid != null, "Skipping: no uploaded asset from testUploadAssetIncludesScanStatusPending"); + Asset scanAsset = client.stack(API_KEY, MANAGEMENT_TOKEN).asset(uploadedAssetUid); + scanAsset.addHeader("api_key", API_KEY); + scanAsset.addHeader("authorization", MANAGEMENT_TOKEN); + Response response = scanAsset.fetch().execute(); + Assertions.assertTrue(response.isSuccessful(), "Expected 200 for asset UID: " + uploadedAssetUid); + Assertions.assertNotNull(response.body()); + String body = response.body().string(); + Assertions.assertFalse(body.contains("_asset_scan_status"), + "_asset_scan_status must be absent when include_asset_scan_status param is not passed"); + } + + @Test + @Order(18) + void testPublishWithoutApiVersionHeaderIsAbsent() throws IOException { + Assumptions.assumeTrue(uploadedAssetUid != null, "Skipping: no uploaded asset from testUploadAssetIncludesScanStatusPending"); + Asset scanAsset = client.stack(API_KEY, MANAGEMENT_TOKEN).asset(uploadedAssetUid); + scanAsset.addHeader("api_key", API_KEY); + scanAsset.addHeader("authorization", MANAGEMENT_TOKEN); + JSONObject publishBody = new JSONObject(); + JSONObject publishDetails = new JSONObject(); + publishDetails.put("locales", new String[]{"en-us"}); + publishDetails.put("environments", new String[]{"development"}); + publishBody.put("asset", publishDetails); + Response response = scanAsset.publish(publishBody).execute(); + Assertions.assertNull(response.raw().request().header("api_version"), + "api_version header must be absent when not explicitly added"); + } + + @Test + @Order(19) + void testUploadWithoutScanStatusParamFieldAbsent() throws IOException { + Asset uploadAsset = client.stack(API_KEY, MANAGEMENT_TOKEN).asset(); + uploadAsset.addHeader("api_key", API_KEY); + uploadAsset.addHeader("authorization", MANAGEMENT_TOKEN); + Response response = uploadAsset.uploadAsset("src/test/resources/asset.png", "negative scan test upload").execute(); + Assertions.assertTrue(response.isSuccessful(), "Expected 201 on upload"); + Assertions.assertNotNull(response.body()); + String body = response.body().string(); + Assertions.assertFalse(body.contains("_asset_scan_status"), + "_asset_scan_status must be absent in upload response when param is not passed"); + try { + JSONParser parser = new JSONParser(); + JSONObject json = (JSONObject) parser.parse(body); + JSONObject assetObj = (JSONObject) json.get("asset"); + if (assetObj != null) { + String negativeTestUid = (String) assetObj.get("uid"); + if (negativeTestUid != null) { + Asset toDelete = client.stack(API_KEY, MANAGEMENT_TOKEN).asset(negativeTestUid); + toDelete.addHeader("api_key", API_KEY); + toDelete.addHeader("authorization", MANAGEMENT_TOKEN); + toDelete.delete().execute(); + } + } + } catch (ParseException ignored) {} + } + + @Test + @Order(20) + void testDeleteUploadedScanAsset() throws IOException { + Assumptions.assumeTrue(uploadedAssetUid != null, "Skipping: no uploaded asset to clean up"); + Asset uploadedAsset = client.stack(API_KEY, MANAGEMENT_TOKEN).asset(uploadedAssetUid); + uploadedAsset.addHeader("api_key", API_KEY); + uploadedAsset.addHeader("authorization", MANAGEMENT_TOKEN); + Response response = uploadedAsset.delete().execute(); + Assertions.assertTrue(response.isSuccessful(), "Expected 200 on delete for asset UID: " + uploadedAssetUid); + } + + // ==================== End Asset Scanning API Tests ==================== @Test @Disabled("disabled to avoid unnecessary asset creation, Tested working fine") @@ -327,8 +537,8 @@ void uploadFile() throws Exception { Asset asset = stack.asset(); String fileName = "/Users/reeshika.hosmani/Downloads/surf-svgrepo-com.svg", parentFolder = "bltd1150f1f7d9411e5", title = "Vacation icon"; String[] tags = {"icon"}; - Response response = asset.uploadAsset(fileName,parentFolder,title,"",tags).execute(); - if(response.isSuccessful()){ + Response response = asset.uploadAsset(fileName, parentFolder, title, "", tags).execute(); + if (response.isSuccessful()) { System.out.println("uploaded asset successfully:" + response.body().string()); } else { System.out.println("Error in uploading" + response.errorBody().string()); diff --git a/src/test/java/com/contentstack/cms/stack/AssetUnitTest.java b/src/test/java/com/contentstack/cms/stack/AssetUnitTest.java index f9c2ad71..0798745a 100644 --- a/src/test/java/com/contentstack/cms/stack/AssetUnitTest.java +++ b/src/test/java/com/contentstack/cms/stack/AssetUnitTest.java @@ -10,9 +10,10 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; +import java.util.Objects; @Tag("unit") -class AssetUnitTest { +public class AssetUnitTest { protected static String AUTHTOKEN = TestClient.AUTHTOKEN; protected static String API_KEY = TestClient.API_KEY; @@ -66,7 +67,7 @@ void testAssetSingle() { Request resp = asset.fetch().request(); Assertions.assertTrue(resp.isHttps()); Assertions.assertEquals("GET", resp.method()); - Assertions.assertEquals(2, resp.headers().size()); + Assertions.assertEquals(3, resp.headers().size()); Collection matcher = new ArrayList<>(); matcher.add("api_key"); matcher.add("authorization"); @@ -146,7 +147,7 @@ void testAssetReplace() { .request(); Assertions.assertTrue(resp.isHttps()); Assertions.assertEquals("PUT", resp.method()); - Assertions.assertEquals(2, resp.headers().size()); + Assertions.assertEquals(3, resp.headers().size()); Collection matcher = new ArrayList<>(); matcher.add("api_key"); matcher.add("authorization"); @@ -196,7 +197,7 @@ void testAssetDelete() { Request resp = asset.delete().request(); Assertions.assertTrue(resp.isHttps()); Assertions.assertEquals("DELETE", resp.method()); - Assertions.assertEquals(2, resp.headers().size()); + Assertions.assertEquals(3, resp.headers().size()); Collection matcher = new ArrayList<>(); matcher.add("api_key"); matcher.add("authorization"); @@ -210,7 +211,7 @@ void testAssetRteInformation() { Request resp = asset.rteInformation().request(); Assertions.assertTrue(resp.isHttps()); Assertions.assertEquals("GET", resp.method()); - Assertions.assertEquals(2, resp.headers().size()); + Assertions.assertEquals(3, resp.headers().size()); Collection matcher = new ArrayList<>(); matcher.add("api_key"); matcher.add("authorization"); @@ -228,7 +229,7 @@ void testAssetSetVersionName() { Request resp = asset.setVersionName(2, body).request(); Assertions.assertTrue(resp.isHttps()); Assertions.assertEquals("POST", resp.method()); - Assertions.assertEquals(2, resp.headers().size()); + Assertions.assertEquals(3, resp.headers().size()); Collection matcher = new ArrayList<>(); matcher.add("api_key"); matcher.add("authorization"); @@ -261,7 +262,7 @@ void testAssetDeleteVersionName() { Request resp = asset.deleteVersionName(2).request(); Assertions.assertTrue(resp.isHttps()); Assertions.assertEquals("DELETE", resp.method()); - Assertions.assertEquals(2, resp.headers().size()); + Assertions.assertEquals(3, resp.headers().size()); Collection matcher = new ArrayList<>(); matcher.add("api_key"); matcher.add("authorization"); @@ -365,7 +366,7 @@ void testAssetUnpublish() { Request resp = asset.unpublish(_body).request(); Assertions.assertTrue(resp.isHttps()); Assertions.assertEquals("POST", resp.method()); - Assertions.assertEquals(2, resp.headers().size()); + Assertions.assertEquals(3, resp.headers().size()); Collection matcher = new ArrayList<>(); matcher.add("api_key"); matcher.add("authorization"); @@ -382,7 +383,7 @@ void testAssetSingleFolder() { Request resp = assetFolder.fetch().request(); Assertions.assertTrue(resp.isHttps()); Assertions.assertEquals("GET", resp.method()); - Assertions.assertEquals(0, resp.headers().size()); + Assertions.assertEquals(3, resp.headers().size()); Collection matcher = new ArrayList<>(); matcher.add("api_key"); matcher.add("authorization"); @@ -443,7 +444,7 @@ void testAssetCreateFolder() { Request resp = asset.folder().create(_body).request(); Assertions.assertTrue(resp.isHttps()); Assertions.assertEquals("POST", resp.method()); - Assertions.assertEquals(0, resp.headers().size()); + Assertions.assertEquals(3, resp.headers().size()); Collection matcher = new ArrayList<>(); matcher.add("api_key"); matcher.add("authorization"); @@ -465,7 +466,7 @@ void testAssetUpdateFolder() { Request resp = asset.folder(_uid).update(_body).request(); Assertions.assertTrue(resp.isHttps()); Assertions.assertEquals("PUT", resp.method()); - Assertions.assertEquals(0, resp.headers().size()); + Assertions.assertEquals(2, resp.headers().size()); Collection matcher = new ArrayList<>(); matcher.add("api_key"); matcher.add("authorization"); @@ -480,7 +481,7 @@ void testAssetDeleteFolder() { Request resp = asset.folder(_uid).delete().request(); Assertions.assertTrue(resp.isHttps()); Assertions.assertEquals("DELETE", resp.method()); - Assertions.assertEquals(0, resp.headers().size()); + Assertions.assertEquals(2, resp.headers().size()); Collection matcher = new ArrayList<>(); matcher.add("api_key"); matcher.add("authorization"); @@ -489,6 +490,106 @@ void testAssetDeleteFolder() { resp.url().query()); Assertions.assertNull(resp.body()); } + // ==================== Asset Scanning Tests ==================== + // Feature: Asset Scanning (plan-based; enabled by default for orgs with damAccess) + // Possible _asset_scan_status values: pending | clean | quarantined | not_scanned + // not_scanned — returned for assets uploaded before scanning was enabled on the stack. + // API tests against a scan-enabled stack are in AssetAPITest. + // ============================================================== + + @Test + void testFetchIncludesScanStatusParam() { + asset.clearParams(); + asset.addParam("include_asset_scan_status", true); + Request resp = asset.fetch().request(); + Assertions.assertEquals("GET", resp.method()); + Assertions.assertEquals("/v3/assets/" + _uid, resp.url().encodedPath()); + Assertions.assertTrue( + Objects.requireNonNull(resp.url().query()).contains("include_asset_scan_status=true")); + Assertions.assertNull(resp.body()); + } + + @Test + void testFindIncludesScanStatusParam() { + asset.clearParams(); + asset.addParam("include_asset_scan_status", true); + Request resp = asset.find().request(); + Assertions.assertEquals("GET", resp.method()); + Assertions.assertEquals("/v3/assets", resp.url().encodedPath()); + Assertions.assertTrue( + Objects.requireNonNull(resp.url().query()).contains("include_asset_scan_status=true")); + Assertions.assertNull(resp.body()); + } + + @Test + void testUploadIncludesScanStatusParam() { + asset.clearParams(); + asset.addParam("include_asset_scan_status", true); + String filePath = "src/test/resources/asset.png"; + Request resp = asset.uploadAsset(filePath, "test upload for scan status").request(); + Assertions.assertEquals("POST", resp.method()); + Assertions.assertEquals("/v3/assets", resp.url().encodedPath()); + Assertions.assertTrue( + Objects.requireNonNull(resp.url().query()).contains("include_asset_scan_status=true")); + Assertions.assertNotNull(resp.body()); + } + + @Test + void testFetchWithoutScanStatusParamExcludesField() { + // Omitting the param means _asset_scan_status is not requested. + // Legacy assets (not_scanned) and new assets alike will not include the field. + asset.clearParams(); + Request resp = asset.fetch().request(); + String query = resp.url().query(); + Assertions.assertTrue(query == null || !query.contains("include_asset_scan_status"), + "include_asset_scan_status must be absent when not explicitly set"); + Assertions.assertNull(resp.body()); + } + + @Test + void testScanStatusParamCoexistsWithOtherParams() { + asset.clearParams(); + asset.addParam("include_asset_scan_status", true); + asset.addParam("include_publish_details", true); + asset.addParam("environment", "production"); + Request resp = asset.fetch().request(); + String query = Objects.requireNonNull(resp.url().query()); + Assertions.assertTrue(query.contains("include_asset_scan_status=true")); + Assertions.assertTrue(query.contains("include_publish_details=true")); + Assertions.assertTrue(query.contains("environment=production")); + } + + @Test + void testPublishIncludesApiVersionHeader() { + // api_version: 3.2 is required for CDA-side scan validation on publish. + // Without it, quarantined assets incorrectly appear as published in the UI. + asset.clearParams(); + asset.addHeader("api_version", "3.2"); + JSONObject body = new JSONObject(); + Request resp = asset.publish(body).request(); + Assertions.assertEquals("POST", resp.method()); + Assertions.assertEquals("/v3/assets/" + _uid + "/publish", resp.url().encodedPath()); + Assertions.assertEquals("3.2", resp.header("api_version")); + Assertions.assertNotNull(resp.body()); + } + + @Test + void testApiVersionHeaderScopedToPublish() { + // api_version: 3.2 must be set explicitly per-call and must not be a global header. + // Setting it globally would affect all API calls and could break other functionality. + HashMap headers = new HashMap<>(); + headers.put(Util.API_KEY, API_KEY); + headers.put(Util.AUTHORIZATION, MANAGEMENT_TOKEN); + Asset freshAsset = TestClient.getClient().stack(headers).asset(_uid); + freshAsset.clearParams(); + Request resp = freshAsset.fetch().request(); + Assertions.assertEquals("GET", resp.method()); + Assertions.assertNull(resp.header("api_version"), + "api_version header must not be present on fetch unless explicitly added"); + } + + // ==================== End Asset Scanning Tests ==================== + @Test void testAssetValidate() { asset = TestClient.getStack().asset();