diff --git a/CHANGELOG.md b/CHANGELOG.md index 83882ab3..168be0f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # CHANGELOG +## Version 4.2.0 + +### Date: 02-Mar-2026 + +- Added asset localisation support + ## Version 4.1.0 ### Date: 15-Sept-2025 diff --git a/LICENSE b/LICENSE index c7d34c7c..0c2b6003 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2012 - 2025 Contentstack +Copyright (c) 2012 - 2026 Contentstack Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/build.gradle b/build.gradle index 91743a3f..77660ab8 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - jacoco_version = '0.8.8' + jacoco_version = '0.8.12' agp_version = '8.2.1' } repositories { diff --git a/contentstack/build.gradle b/contentstack/build.gradle index 25ab9afb..f0ad9685 100755 --- a/contentstack/build.gradle +++ b/contentstack/build.gradle @@ -1,17 +1,27 @@ plugins { id "com.android.library" id "com.vanniktech.maven.publish" version "0.33.0" + id 'jacoco' } ext { PUBLISH_GROUP_ID = 'com.contentstack.sdk' PUBLISH_ARTIFACT_ID = 'android' - PUBLISH_VERSION = '4.1.0' + PUBLISH_VERSION = '4.2.0' } android { namespace "com.contentstack.sdk" compileSdk 34 // Using latest stable Android SDK version + + // SDK compiles to Java 17 for JaCoCo compatibility + // But can be built with Java 21 - tests use Java 17 toolchain + compileOptions { + coreLibraryDesugaringEnabled true + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + buildFeatures { buildConfig true } @@ -30,10 +40,15 @@ android { } testOptions { - unitTests.all { - // jacoco { - // includeNoLocationClasses = true - // } + unitTests { + includeAndroidResources = true + returnDefaultValues = true + all { + jacoco { + includeNoLocationClasses = true + excludes = ['jdk.internal.*'] + } + } } } // signing { @@ -109,6 +124,8 @@ dependencies { def multidex = "2.0.1" def volley = "1.2.1" def junit = "4.13.2" + def mockito = "5.2.0" + def mockitoKotlin = "2.2.0" configurations.configureEach { resolutionStrategy.force 'com.android.support:support-annotations:23.1.0' } implementation fileTree(include: ['*.jar'], dir: 'libs') implementation "com.android.volley:volley:$volley" @@ -116,11 +133,28 @@ dependencies { // For AGP 7.4+ coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' + + // Unit Testing Dependencies testImplementation 'junit:junit:4.13.2' + testImplementation "org.mockito:mockito-core:$mockito" + testImplementation "org.mockito:mockito-inline:$mockito" + testImplementation 'org.mockito:mockito-android:5.2.0' + testImplementation 'org.robolectric:robolectric:4.15' // Updated to fix security vulnerabilities + testImplementation 'androidx.test:core:1.5.0' + testImplementation 'androidx.test:runner:1.5.2' + testImplementation 'androidx.test.ext:junit:1.1.5' + testImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0' + testImplementation 'org.json:json:20231013' + // PowerMock for advanced mocking + testImplementation 'org.powermock:powermock-module-junit4:2.0.9' + testImplementation 'org.powermock:powermock-api-mockito2:2.0.9' + testImplementation 'org.powermock:powermock-core:2.0.9' + + // Android Test Dependencies androidTestImplementation 'androidx.test:core:1.5.0' - testImplementation 'org.robolectric:robolectric:4.6.1' - - androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0', { + androidTestImplementation 'androidx.test:runner:1.5.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation('androidx.test.espresso:espresso-core:3.5.1', { exclude group: 'com.android.support', module: 'support-annotations' }) @@ -200,22 +234,170 @@ mavenPublishing { } } +jacoco { + toolVersion = "0.8.12" +} + tasks.register('jacocoTestReport', JacocoReport) { - dependsOn('testDebugUnitTest', 'createDebugCoverageReport') + dependsOn('testDebugUnitTest') + reports { + xml.required = true html.required = true + csv.required = false + + xml.outputLocation = file("${buildDir}/reports/jacoco/jacocoTestReport/jacocoTestReport.xml") + html.outputLocation = file("${buildDir}/reports/jacoco/jacocoTestReport/html") } + + def excludePatterns = [ + '**/R.class', + '**/R$*.class', + '**/BuildConfig.*', + '**/Manifest*.*', + '**/*Test*.*', + 'android/**/*.*', + '**/*$ViewInjector*.*', + '**/*$ViewBinder*.*', + '**/Lambda$*.class', + '**/Lambda.class', + '**/*Lambda.class', + '**/*Lambda*.class', + '**/*_MembersInjector.class', + '**/Dagger*Component*.*', + '**/*Module_*Factory.class', + '**/AutoValue_*.*', + '**/*JavascriptBridge.class', + '**/package-info.class', + '**/TestActivity.class', + // External library exclusions + '**/okhttp/**', + '**/okio/**', + '**/txtmark/**', + '**/retrofit2/**', + '**/volley/**', + '**/CSConnectionRequest.class', + // Exclude callback interfaces and their anonymous implementations + '**/SyncResultCallBack.class', + '**/Stack$*.class' + ] + + sourceDirectories.setFrom(files([ + "${project.projectDir}/src/main/java" + ])) + + classDirectories.setFrom(files([ + fileTree(dir: "${buildDir}/intermediates/javac/debug", excludes: excludePatterns), + fileTree(dir: "${buildDir}/tmp/kotlin-classes/debug", excludes: excludePatterns) + ])) + + executionData.setFrom(fileTree(buildDir).include([ + "outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec", + "jacoco/testDebugUnitTest.exec" + ])) } -// Configure jacocoTestReport after evaluation when classDirectories is available -project.afterEvaluate { - tasks.named('jacocoTestReport', JacocoReport) { - classDirectories.setFrom(files(classDirectories.files.collect { - fileTree(dir: it, exclude: [ - '**com/contentstack/okhttp**', - '**com/contentstack/okio**', - '**com/contentstack/txtmark**' - ]) - })) +// Combined coverage report for both unit and instrumentation tests +tasks.register('jacocoCombinedReport', JacocoReport) { + // This task can run after both test types complete + // Make it depend on both if they're being run + group = "Reporting" + description = "Generate Jacoco coverage reports for both unit and instrumentation tests" + + reports { + xml.required = true + html.required = true + csv.required = false + + xml.outputLocation = file("${buildDir}/reports/jacoco/jacocoCombinedReport/jacocoCombinedReport.xml") + html.outputLocation = file("${buildDir}/reports/jacoco/jacocoCombinedReport/html") } + + def excludePatterns = [ + '**/R.class', + '**/R$*.class', + '**/BuildConfig.*', + '**/Manifest*.*', + '**/*Test*.*', + 'android/**/*.*', + '**/*$ViewInjector*.*', + '**/*$ViewBinder*.*', + '**/Lambda$*.class', + '**/Lambda.class', + '**/*Lambda.class', + '**/*Lambda*.class', + '**/*_MembersInjector.class', + '**/Dagger*Component*.*', + '**/*Module_*Factory.class', + '**/AutoValue_*.*', + '**/*JavascriptBridge.class', + '**/package-info.class', + '**/TestActivity.class', + // External library exclusions + '**/okhttp/**', + '**/okio/**', + '**/txtmark/**', + '**/retrofit2/**', + '**/volley/**', + '**/CSConnectionRequest.class', + // Exclude callback interfaces and their anonymous implementations + '**/SyncResultCallBack.class', + '**/Stack$*.class' + ] + + sourceDirectories.setFrom(files([ + "${project.projectDir}/src/main/java" + ])) + + classDirectories.setFrom(files([ + fileTree(dir: "${buildDir}/intermediates/javac/debug", excludes: excludePatterns), + fileTree(dir: "${buildDir}/tmp/kotlin-classes/debug", excludes: excludePatterns) + ])) + + // Collect execution data from both unit tests and instrumentation tests + executionData.setFrom(fileTree(buildDir).include([ + // Unit test coverage + "outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec", + "jacoco/testDebugUnitTest.exec", + // Instrumentation test coverage + "outputs/code_coverage/debugAndroidTest/connected/**/*.ec" + ])) +} + +tasks.register('jacocoTestCoverageVerification', JacocoCoverageVerification) { + dependsOn('testDebugUnitTest') + + def excludePatterns = [ + '**/R.class', + '**/R$*.class', + '**/BuildConfig.*', + '**/Manifest*.*', + '**/*Test*.*', + 'android/**/*.*', + '**/package-info.class', + '**/TestActivity.class', + '**/CSConnectionRequest.class', + // Exclude callback interfaces and their anonymous implementations + '**/SyncResultCallBack.class', + '**/Stack$*.class' + ] + + sourceDirectories.setFrom(files([ + "${project.projectDir}/src/main/java" + ])) + + classDirectories.setFrom(files([ + fileTree(dir: "${buildDir}/intermediates/javac/debug", excludes: excludePatterns), + fileTree(dir: "${buildDir}/tmp/kotlin-classes/debug", excludes: excludePatterns) + ])) + + executionData.setFrom(fileTree(buildDir).include([ + "outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec", + "jacoco/testDebugUnitTest.exec" + ])) +} + +// Make check task depend on coverage verification +tasks.named('check') { + dependsOn('jacocoTestReport', 'jacocoTestCoverageVerification') } \ No newline at end of file diff --git a/contentstack/src/androidTest/java/com/contentstack/sdk/AssetTestCase.java b/contentstack/src/androidTest/java/com/contentstack/sdk/AssetTestCase.java index ce358ec1..1f807b6e 100644 --- a/contentstack/src/androidTest/java/com/contentstack/sdk/AssetTestCase.java +++ b/contentstack/src/androidTest/java/com/contentstack/sdk/AssetTestCase.java @@ -124,6 +124,22 @@ public void onCompletion(ResponseType responseType, Error error) { latch.await(5, TimeUnit.SECONDS); } + @Test + public void test_setLocale_fetch() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + final Asset asset = stack.asset(assetUid); + asset.setLocale("en-us"); + asset.fetch(new FetchResultCallback() { + @Override + public void onCompletion(ResponseType responseType, Error error) { + assertNotNull(asset.getAssetUid()); + latch.countDown(); + } + }); + latch.await(5, TimeUnit.SECONDS); + assertEquals("Query was not completed in time", 0, latch.getCount()); + } + @Test public void test_include_branch() { final Asset asset = stack.asset(assetUid); diff --git a/contentstack/src/main/java/com/contentstack/sdk/Asset.java b/contentstack/src/main/java/com/contentstack/sdk/Asset.java index c135e2a6..a30eb371 100755 --- a/contentstack/src/main/java/com/contentstack/sdk/Asset.java +++ b/contentstack/src/main/java/com/contentstack/sdk/Asset.java @@ -616,4 +616,22 @@ public Asset includeBranch() { return this; } + /** + *
+ *
Example :
+ *
+ * Asset asset = asset.setLocale("en-hi");
+ *
+ *
+ */
+ public Asset setLocale(String locale) {
+ if (locale != null) {
+ try {
+ urlQueries.put("locale", locale);
+ } catch (JSONException e) {
+ Log.e(TAG, e.getLocalizedMessage());
+ }
+ }
+ return this;
+ }
}
diff --git a/contentstack/src/main/java/com/contentstack/sdk/TestActivity.java b/contentstack/src/main/java/com/contentstack/sdk/TestActivity.java
deleted file mode 100755
index c4c516c6..00000000
--- a/contentstack/src/main/java/com/contentstack/sdk/TestActivity.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.contentstack.sdk;
-
-/**
- * @author Contentstack.com, Inc
- */
-
-//public class TestActivity extends AppCompatActivity {
-//
-// @Override
-// protected void onCreate(@Nullable Bundle savedInstanceState) {
-// super.onCreate(savedInstanceState);
-// }
-//}
diff --git a/contentstack/src/test/java/com/contentstack/sdk/ExampleUnitTest.java b/contentstack/src/test/java/com/contentstack/sdk/ExampleUnitTest.java
deleted file mode 100644
index e7748124..00000000
--- a/contentstack/src/test/java/com/contentstack/sdk/ExampleUnitTest.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.contentstack.sdk;
-
-import static org.junit.Assert.assertEquals;
-
-import org.junit.Test;
-
-public class ExampleUnitTest {
- @Test
- public void defaultTest() {
- assertEquals(4, 2 + 2);
- }
-
-}
\ No newline at end of file
diff --git a/contentstack/src/test/java/com/contentstack/sdk/TestAssetAdvanced.java b/contentstack/src/test/java/com/contentstack/sdk/TestAssetAdvanced.java
new file mode 100644
index 00000000..fb3bd0a9
--- /dev/null
+++ b/contentstack/src/test/java/com/contentstack/sdk/TestAssetAdvanced.java
@@ -0,0 +1,735 @@
+package com.contentstack.sdk;
+
+import android.content.Context;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.HashMap;
+
+import static org.junit.Assert.*;
+/**
+ * Comprehensive tests for Asset class to improve coverage.
+ */
+@RunWith(RobolectricTestRunner.class)
+public class TestAssetAdvanced {
+
+ private Context context;
+ private Stack stack;
+ private Asset asset;
+
+ @Before
+ public void setUp() throws Exception {
+ context = ApplicationProvider.getApplicationContext();
+ Config config = new Config();
+ config.setHost("cdn.contentstack.io");
+
+ stack = Contentstack.stack(context, "test_api_key", "test_delivery_token", "test_env", config);
+ asset = stack.asset("test_asset_uid");
+ }
+
+ // ==================== CONSTRUCTOR Tests ====================
+
+ @Test
+ public void testAssetCreation() {
+ assertNotNull(asset);
+ }
+
+ @Test
+ public void testAssetCreationWithUid() {
+ Asset assetWithUid = stack.asset("specific_asset_uid");
+ assertNotNull(assetWithUid);
+ }
+
+ // ==================== CONFIGURE Tests ====================
+
+ @Test
+ public void testConfigureWithValidJSON() throws JSONException {
+ JSONObject assetJson = new JSONObject();
+ assetJson.put("uid", "asset123");
+ assetJson.put("content_type", "image/jpeg");
+ assetJson.put("file_size", "1024");
+ assetJson.put("filename", "test.jpg");
+ assetJson.put("url", "https://example.com/test.jpg");
+
+ Asset result = asset.configure(assetJson);
+ assertNotNull(result);
+ assertSame(asset, result); // Should return same instance
+ }
+
+ @Test
+ public void testConfigureWithMinimalJSON() throws JSONException {
+ JSONObject assetJson = new JSONObject();
+ assetJson.put("uid", "asset_minimal");
+
+ Asset result = asset.configure(assetJson);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void testConfigureWithEmptyJSON() throws JSONException {
+ JSONObject emptyJson = new JSONObject();
+ Asset result = asset.configure(emptyJson);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void testConfigureMultipleTimes() throws JSONException {
+ JSONObject json1 = new JSONObject();
+ json1.put("uid", "asset1");
+ json1.put("filename", "file1.jpg");
+
+ JSONObject json2 = new JSONObject();
+ json2.put("uid", "asset2");
+ json2.put("filename", "file2.jpg");
+
+ asset.configure(json1);
+ Asset result = asset.configure(json2); // Reconfigure
+
+ assertNotNull(result);
+ }
+
+ // ==================== HEADER Tests ====================
+
+ @Test
+ public void testSetHeader() {
+ asset.setHeader("custom-header", "custom-value");
+ assertNotNull(asset);
+ }
+
+ @Test
+ public void testSetHeaderMultiple() {
+ asset.setHeader("header1", "value1");
+ asset.setHeader("header2", "value2");
+ asset.setHeader("header3", "value3");
+ assertNotNull(asset);
+ }
+
+ @Test
+ public void testSetHeaderWithNull() {
+ asset.setHeader(null, "value");
+ asset.setHeader("key", null);
+ assertNotNull(asset);
+ }
+
+ @Test
+ public void testSetHeaderWithEmptyStrings() {
+ asset.setHeader("", "value");
+ asset.setHeader("key", "");
+ assertNotNull(asset);
+ }
+
+ @Test
+ public void testRemoveHeader() {
+ asset.setHeader("test-header", "test-value");
+ asset.removeHeader("test-header");
+ assertNotNull(asset);
+ }
+
+ @Test
+ public void testRemoveHeaderThatDoesntExist() {
+ asset.removeHeader("non-existent-header");
+ assertNotNull(asset);
+ }
+
+ @Test
+ public void testRemoveHeaderWithNull() {
+ asset.removeHeader(null);
+ assertNotNull(asset);
+ }
+
+ @Test
+ public void testRemoveHeaderWithEmptyString() {
+ asset.removeHeader("");
+ assertNotNull(asset);
+ }
+
+ // ==================== ADD PARAM Tests ====================
+
+ @Test
+ public void testAddParam() {
+ Asset result = asset.addParam("include_dimension", "true");
+ assertNotNull(result);
+ assertSame(asset, result);
+ }
+
+ @Test
+ public void testAddParamMultiple() {
+ asset.addParam("param1", "value1");
+ asset.addParam("param2", "value2");
+ assertNotNull(asset);
+ }
+
+ @Test
+ public void testAddParamWithNull() {
+ Asset result = asset.addParam(null, "value");
+ assertNotNull(result);
+
+ result = asset.addParam("key", null);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void testAddParamWithEmptyStrings() {
+ asset.addParam("", "value");
+ asset.addParam("key", "");
+ assertNotNull(asset);
+ }
+
+ @Test
+ public void testAddParamOverwrite() {
+ asset.addParam("dimension", "true");
+ asset.addParam("dimension", "false"); // Overwrite
+ assertNotNull(asset);
+ }
+
+ // ==================== SET LOCALE Tests ====================
+
+ @Test
+ public void testSetLocale() {
+ Asset result = asset.setLocale("en-us");
+ assertNotNull(result);
+ assertSame(asset, result);
+ assertEquals("en-us", asset.urlQueries.optString("locale", ""));
+ }
+
+ @Test
+ public void testSetLocaleReturnsThis() {
+ Asset result = asset.setLocale("en-hi");
+ assertSame(asset, result);
+ }
+
+ @Test
+ public void testSetLocaleWithNull() {
+ asset.setLocale("en-us");
+ Asset result = asset.setLocale(null);
+ assertSame(asset, result);
+ assertEquals("en-us", asset.urlQueries.optString("locale", ""));
+ }
+
+ @Test
+ public void testSetLocaleChainedWithFetch() {
+ asset.setLocale("en-us").includeFallback();
+ assertTrue(asset.urlQueries.has("locale"));
+ assertEquals("en-us", asset.urlQueries.optString("locale", ""));
+ }
+
+ // ==================== GET METHODS Tests ====================
+
+ @Test
+ public void testGetAssetUid() throws JSONException {
+ JSONObject assetJson = new JSONObject();
+ assetJson.put("uid", "test_uid_123");
+
+ asset.configure(assetJson);
+ String uid = asset.getAssetUid();
+
+ assertNotNull(uid);
+ assertEquals("test_uid_123", uid);
+ }
+
+ @Test
+ public void testGetAssetUidBeforeConfigure() {
+ // Asset uid should be set from constructor
+ String uid = asset.getAssetUid();
+ assertNotNull(uid);
+ assertEquals("test_asset_uid", uid);
+ }
+
+ @Test
+ public void testGetFileType() throws JSONException {
+ JSONObject assetJson = new JSONObject();
+ assetJson.put("content_type", "image/png");
+
+ asset.configure(assetJson);
+ String fileType = asset.getFileType();
+
+ assertNotNull(fileType);
+ assertEquals("image/png", fileType);
+ }
+
+ @Test
+ public void testGetFileTypeBeforeConfigure() {
+ String fileType = asset.getFileType();
+ // Should return null or empty before configuration
+ assertTrue(fileType == null || fileType.isEmpty());
+ }
+
+ @Test
+ public void testGetFileSize() throws JSONException {
+ JSONObject assetJson = new JSONObject();
+ assetJson.put("file_size", "2048");
+
+ asset.configure(assetJson);
+ String fileSize = asset.getFileSize();
+
+ assertNotNull(fileSize);
+ assertEquals("2048", fileSize);
+ }
+
+ @Test
+ public void testGetFileName() throws JSONException {
+ JSONObject assetJson = new JSONObject();
+ assetJson.put("filename", "my_image.jpg");
+
+ asset.configure(assetJson);
+ String fileName = asset.getFileName();
+
+ assertNotNull(fileName);
+ assertEquals("my_image.jpg", fileName);
+ }
+
+ @Test
+ public void testGetUrl() throws JSONException {
+ JSONObject assetJson = new JSONObject();
+ assetJson.put("url", "https://cdn.example.com/asset.jpg");
+
+ asset.configure(assetJson);
+ String url = asset.getUrl();
+
+ assertNotNull(url);
+ assertEquals("https://cdn.example.com/asset.jpg", url);
+ }
+
+ @Test
+ public void testToJSON() throws JSONException {
+ JSONObject assetJson = new JSONObject();
+ assetJson.put("uid", "json_test");
+ assetJson.put("title", "Test Asset");
+
+ asset.configure(assetJson);
+ JSONObject result = asset.toJSON();
+
+ assertNotNull(result);
+ }
+
+ @Test
+ public void testGetTags() throws JSONException {
+ String[] tags = asset.getTags();
+ // Tags should be null or empty array before configuration
+ assertTrue(tags == null || tags.length == 0);
+ }
+
+ // ==================== SET TAGS Tests ====================
+
+ @Test
+ public void testSetTags() {
+ String[] tags = {"tag1", "tag2", "tag3"};
+ asset.setTags(tags);
+
+ String[] result = asset.getTags();
+ assertNotNull(result);
+ assertEquals(3, result.length);
+ assertEquals("tag1", result[0]);
+ }
+
+ @Test
+ public void testSetTagsWithNull() {
+ asset.setTags(null);
+ String[] result = asset.getTags();
+ assertTrue(result == null || result.length == 0);
+ }
+
+ @Test
+ public void testSetTagsWithEmptyArray() {
+ asset.setTags(new String[]{});
+ String[] result = asset.getTags();
+ assertTrue(result == null || result.length == 0);
+ }
+
+ @Test
+ public void testSetTagsOverwrite() {
+ asset.setTags(new String[]{"old1", "old2"});
+ asset.setTags(new String[]{"new1", "new2", "new3"});
+
+ String[] result = asset.getTags();
+ assertNotNull(result);
+ assertEquals(3, result.length);
+ }
+
+ // ==================== CACHE POLICY Tests ====================
+
+ @Test
+ public void testSetCachePolicyNetworkOnly() {
+ asset.setCachePolicy(CachePolicy.NETWORK_ONLY);
+ assertNotNull(asset);
+ }
+
+ @Test
+ public void testSetCachePolicyCacheOnly() {
+ asset.setCachePolicy(CachePolicy.CACHE_ONLY);
+ assertNotNull(asset);
+ }
+
+ @Test
+ public void testSetCachePolicyCacheElseNetwork() {
+ asset.setCachePolicy(CachePolicy.CACHE_ELSE_NETWORK);
+ assertNotNull(asset);
+ }
+
+ @Test
+ public void testSetCachePolicyCacheThenNetwork() {
+ asset.setCachePolicy(CachePolicy.CACHE_THEN_NETWORK);
+ assertNotNull(asset);
+ }
+
+ @Test
+ public void testSetCachePolicyNetworkElseCache() {
+ asset.setCachePolicy(CachePolicy.NETWORK_ELSE_CACHE);
+ assertNotNull(asset);
+ }
+
+ @Test
+ public void testSetCachePolicyIgnoreCache() {
+ asset.setCachePolicy(CachePolicy.IGNORE_CACHE);
+ assertNotNull(asset);
+ }
+
+ // ==================== GET URL Tests ====================
+
+ @Test
+ public void testGetUrlWithoutConfiguration() {
+ String url = asset.getUrl();
+ // URL should be returned (might be null or empty without configuration)
+ // This tests method doesn't throw exception
+ }
+
+ @Test
+ public void testGetUrlWithConfiguration() throws JSONException {
+ JSONObject assetJson = new JSONObject();
+ assetJson.put("url", "https://cdn.example.com/image.png");
+
+ asset.configure(assetJson);
+ String url = asset.getUrl();
+ assertNotNull(url);
+ }
+
+ // ==================== METHOD CHAINING Tests ====================
+
+ @Test
+ public void testMethodChaining() {
+ asset.setHeader("custom-header", "value");
+ Asset result = asset.addParam("dimension", "true");
+ asset.setCachePolicy(CachePolicy.NETWORK_ONLY);
+
+ assertNotNull(result);
+ }
+
+ // ==================== COMPLEX SCENARIOS Tests ====================
+
+ @Test
+ public void testCompleteAssetWorkflow() throws JSONException {
+ JSONObject assetData = new JSONObject();
+ assetData.put("uid", "complete_asset");
+ assetData.put("content_type", "image/jpeg");
+ assetData.put("file_size", "1048576");
+ assetData.put("filename", "photo.jpg");
+ assetData.put("url", "https://cdn.example.com/photo.jpg");
+ assetData.put("title", "My Photo");
+
+ asset.configure(assetData);
+ asset.setHeader("api-version", "v3");
+ asset.addParam("include_dimension", "true");
+ asset.setCachePolicy(CachePolicy.CACHE_ELSE_NETWORK);
+
+ assertEquals("complete_asset", asset.getAssetUid());
+ assertEquals("image/jpeg", asset.getFileType());
+ assertEquals("1048576", asset.getFileSize());
+ assertEquals("photo.jpg", asset.getFileName());
+ assertNotNull(asset.getUrl());
+ }
+
+ @Test
+ public void testReconfigureAsset() throws JSONException {
+ JSONObject firstConfig = new JSONObject();
+ firstConfig.put("uid", "asset_v1");
+ firstConfig.put("filename", "version1.jpg");
+
+ asset.configure(firstConfig);
+ assertEquals("asset_v1", asset.getAssetUid());
+
+ JSONObject secondConfig = new JSONObject();
+ secondConfig.put("uid", "asset_v2");
+ secondConfig.put("filename", "version2.jpg");
+
+ asset.configure(secondConfig);
+ assertEquals("asset_v2", asset.getAssetUid());
+ }
+
+ @Test
+ public void testAssetWithSpecialCharacters() throws JSONException {
+ JSONObject assetJson = new JSONObject();
+ assetJson.put("uid", "asset_with_特殊字符");
+ assetJson.put("filename", "file with spaces & special.jpg");
+ assetJson.put("url", "https://example.com/path/to/file%20name.jpg");
+
+ asset.configure(assetJson);
+ assertNotNull(asset.getAssetUid());
+ assertNotNull(asset.getFileName());
+ assertNotNull(asset.getUrl());
+ }
+
+ @Test
+ public void testAssetWithVeryLargeFileSize() throws JSONException {
+ JSONObject assetJson = new JSONObject();
+ assetJson.put("file_size", "10737418240"); // 10GB
+
+ asset.configure(assetJson);
+ assertEquals("10737418240", asset.getFileSize());
+ }
+
+ @Test
+ public void testAssetGetUrl() throws JSONException {
+ JSONObject assetJson = new JSONObject();
+ assetJson.put("uid", "image_asset");
+ assetJson.put("content_type", "image/png");
+ assetJson.put("url", "https://cdn.example.com/image.png");
+
+ asset.configure(assetJson);
+
+ String url = asset.getUrl();
+ assertNotNull(url);
+ }
+
+ @Test
+ public void testAssetWithAllSupportedContentTypes() throws JSONException {
+ String[] contentTypes = {
+ "image/jpeg", "image/png", "image/gif", "image/webp",
+ "video/mp4", "video/mpeg", "video/quicktime",
+ "audio/mp3", "audio/mpeg", "audio/wav",
+ "application/pdf", "application/zip", "text/plain"
+ };
+
+ for (String contentType : contentTypes) {
+ JSONObject assetJson = new JSONObject();
+ assetJson.put("content_type", contentType);
+ asset.configure(assetJson);
+ assertEquals(contentType, asset.getFileType());
+ }
+ }
+
+ // ==================== DATE GETTER TESTS ====================
+
+ @Test
+ public void testGetCreateAt() throws JSONException {
+ JSONObject assetJson = new JSONObject();
+ assetJson.put("uid", "date_test");
+ assetJson.put("created_at", "2023-01-15T10:30:00.000Z");
+
+ asset.configure(assetJson);
+
+ java.util.Calendar calendar = asset.getCreateAt();
+ assertNotNull(calendar);
+ }
+
+ @Test
+ public void testGetUpdateAt() throws JSONException {
+ JSONObject assetJson = new JSONObject();
+ assetJson.put("uid", "date_test");
+ assetJson.put("updated_at", "2023-06-20T14:45:30.000Z");
+
+ asset.configure(assetJson);
+
+ java.util.Calendar calendar = asset.getUpdateAt();
+ assertNotNull(calendar);
+ }
+
+ @Test
+ public void testGetDeleteAt() throws JSONException {
+ JSONObject assetJson = new JSONObject();
+ assetJson.put("uid", "date_test");
+ assetJson.put("deleted_at", "2023-12-31T23:59:59.000Z");
+
+ asset.configure(assetJson);
+
+ java.util.Calendar calendar = asset.getDeleteAt();
+ assertNotNull(calendar);
+ }
+
+ @Test
+ public void testGetDeleteAtWhenNull() throws JSONException {
+ JSONObject assetJson = new JSONObject();
+ assetJson.put("uid", "date_test");
+ // No deleted_at field
+
+ asset.configure(assetJson);
+
+ java.util.Calendar calendar = asset.getDeleteAt();
+ assertNull(calendar);
+ }
+
+ // ==================== USER GETTER TESTS ====================
+
+ @Test
+ public void testGetCreatedBy() throws JSONException {
+ JSONObject assetJson = new JSONObject();
+ assetJson.put("uid", "user_test");
+ assetJson.put("created_by", "user_creator_123");
+
+ asset.configure(assetJson);
+
+ String createdBy = asset.getCreatedBy();
+ assertEquals("user_creator_123", createdBy);
+ }
+
+ @Test
+ public void testGetUpdatedBy() throws JSONException {
+ JSONObject assetJson = new JSONObject();
+ assetJson.put("uid", "user_test");
+ assetJson.put("updated_by", "user_updater_456");
+
+ asset.configure(assetJson);
+
+ String updatedBy = asset.getUpdatedBy();
+ assertEquals("user_updater_456", updatedBy);
+ }
+
+ @Test
+ public void testGetDeletedBy() throws JSONException {
+ JSONObject assetJson = new JSONObject();
+ assetJson.put("uid", "user_test");
+ assetJson.put("deleted_by", "user_deleter_789");
+
+ asset.configure(assetJson);
+
+ String deletedBy = asset.getDeletedBy();
+ assertEquals("user_deleter_789", deletedBy);
+ }
+
+ @Test
+ public void testGetDeletedByWhenEmpty() throws JSONException {
+ JSONObject assetJson = new JSONObject();
+ assetJson.put("uid", "user_test");
+ // No deleted_by field
+
+ asset.configure(assetJson);
+
+ String deletedBy = asset.getDeletedBy();
+ assertEquals("", deletedBy);
+ }
+
+ // ==================== COMPREHENSIVE CONFIGURATION TESTS ====================
+
+ @Test
+ public void testConfigureWithAllDateFields() throws JSONException {
+ JSONObject assetJson = new JSONObject();
+ assetJson.put("uid", "complete_date_test");
+ assetJson.put("created_at", "2023-01-01T00:00:00.000Z");
+ assetJson.put("updated_at", "2023-06-15T12:30:00.000Z");
+ assetJson.put("deleted_at", "2023-12-31T23:59:59.000Z");
+ assetJson.put("created_by", "creator_user");
+ assetJson.put("updated_by", "updater_user");
+ assetJson.put("deleted_by", "deleter_user");
+
+ asset.configure(assetJson);
+
+ // Verify all date fields
+ assertNotNull(asset.getCreateAt());
+ assertNotNull(asset.getUpdateAt());
+ assertNotNull(asset.getDeleteAt());
+
+ // Verify all user fields
+ assertEquals("creator_user", asset.getCreatedBy());
+ assertEquals("updater_user", asset.getUpdatedBy());
+ assertEquals("deleter_user", asset.getDeletedBy());
+ }
+
+ @Test
+ public void testConfigureWithMissingDateFields() throws JSONException {
+ JSONObject assetJson = new JSONObject();
+ assetJson.put("uid", "minimal_date_test");
+ // No date or user fields
+
+ asset.configure(assetJson);
+
+ // deleted_at should be null when not provided
+ assertNull(asset.getDeleteAt());
+
+ // deleted_by should be empty string when not provided
+ assertEquals("", asset.getDeletedBy());
+ }
+
+ @Test
+ public void testGettersWithCompleteAssetData() throws JSONException {
+ JSONObject completeData = new JSONObject();
+ completeData.put("uid", "complete_asset");
+ completeData.put("content_type", "image/jpeg");
+ completeData.put("file_size", "3145728");
+ completeData.put("filename", "complete_image.jpg");
+ completeData.put("url", "https://cdn.example.com/complete_image.jpg");
+ completeData.put("created_at", "2023-03-15T08:20:00.000Z");
+ completeData.put("updated_at", "2023-09-20T16:45:00.000Z");
+ completeData.put("created_by", "blt_creator");
+ completeData.put("updated_by", "blt_updater");
+
+ asset.configure(completeData);
+
+ // Test all getters
+ assertEquals("complete_asset", asset.getAssetUid());
+ assertEquals("image/jpeg", asset.getFileType());
+ assertEquals("3145728", asset.getFileSize());
+ assertEquals("complete_image.jpg", asset.getFileName());
+ assertEquals("https://cdn.example.com/complete_image.jpg", asset.getUrl());
+ assertNotNull(asset.getCreateAt());
+ assertNotNull(asset.getUpdateAt());
+ assertNull(asset.getDeleteAt());
+ assertEquals("blt_creator", asset.getCreatedBy());
+ assertEquals("blt_updater", asset.getUpdatedBy());
+ assertEquals("", asset.getDeletedBy());
+ assertNotNull(asset.toJSON());
+ }
+
+ @Test
+ public void testDateFieldsWithDifferentFormats() throws JSONException {
+ JSONObject assetJson = new JSONObject();
+ assetJson.put("uid", "date_format_test");
+ assetJson.put("created_at", "2023-01-01T00:00:00.000Z");
+ assetJson.put("updated_at", "2023-12-31T23:59:59.999Z");
+
+ asset.configure(assetJson);
+
+ assertNotNull(asset.getCreateAt());
+ assertNotNull(asset.getUpdateAt());
+ }
+
+ // ==================== INCLUDE METHOD TESTS ====================
+
+ @Test
+ public void testIncludeDimension() {
+ Asset result = asset.includeDimension();
+ assertNotNull(result);
+ assertSame(asset, result);
+ }
+
+ @Test
+ public void testIncludeFallback() {
+ Asset result = asset.includeFallback();
+ assertNotNull(result);
+ assertSame(asset, result);
+ }
+
+ @Test
+ public void testIncludeBranch() {
+ Asset result = asset.includeBranch();
+ assertNotNull(result);
+ assertSame(asset, result);
+ }
+
+ @Test
+ public void testMultipleIncludesCombined() {
+ Asset result = asset
+ .includeDimension()
+ .includeFallback()
+ .includeBranch();
+
+ assertNotNull(result);
+ assertSame(asset, result);
+ }
+}
+
diff --git a/contentstack/src/test/java/com/contentstack/sdk/TestAssetLibraryAdvanced.java b/contentstack/src/test/java/com/contentstack/sdk/TestAssetLibraryAdvanced.java
new file mode 100644
index 00000000..40adb2b0
--- /dev/null
+++ b/contentstack/src/test/java/com/contentstack/sdk/TestAssetLibraryAdvanced.java
@@ -0,0 +1,2202 @@
+package com.contentstack.sdk;
+
+import android.content.Context;
+import android.util.ArrayMap;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.robolectric.RobolectricTestRunner;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import static org.junit.Assert.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.*;
+
+/**
+ * Comprehensive tests for AssetLibrary class to improve coverage.
+ */
+@RunWith(RobolectricTestRunner.class)
+public class TestAssetLibraryAdvanced {
+
+ private Context context;
+ private Stack stack;
+ private AssetLibrary assetLibrary;
+
+ @Before
+ public void setUp() throws Exception {
+ context = ApplicationProvider.getApplicationContext();
+ Config config = new Config();
+ config.setHost("cdn.contentstack.io");
+
+ stack = Contentstack.stack(context, "test_api_key", "test_delivery_token", "test_env", config);
+ assetLibrary = stack.assetLibrary();
+ }
+
+ // ==================== CONSTRUCTOR Tests ====================
+
+ @Test
+ public void testAssetLibraryCreation() {
+ assertNotNull(assetLibrary);
+ }
+
+ // ==================== HEADER Tests ====================
+
+ @Test
+ public void testSetHeader() {
+ assetLibrary.setHeader("custom-header", "custom-value");
+ assertNotNull(assetLibrary);
+ }
+
+ @Test
+ public void testSetHeaderWithNull() {
+ assetLibrary.setHeader(null, "value");
+ assetLibrary.setHeader("key", null);
+ assertNotNull(assetLibrary);
+ }
+
+ @Test
+ public void testSetHeaderWithEmptyStrings() {
+ assetLibrary.setHeader("", "value");
+ assetLibrary.setHeader("key", "");
+ assertNotNull(assetLibrary);
+ }
+
+ @Test
+ public void testSetHeaderMultiple() {
+ assetLibrary.setHeader("header1", "value1");
+ assetLibrary.setHeader("header2", "value2");
+ assetLibrary.setHeader("header3", "value3");
+ assertNotNull(assetLibrary);
+ }
+
+ @Test
+ public void testRemoveHeader() {
+ assetLibrary.setHeader("test-header", "test-value");
+ assetLibrary.removeHeader("test-header");
+ assertNotNull(assetLibrary);
+ }
+
+ @Test
+ public void testRemoveHeaderWithNull() {
+ assetLibrary.removeHeader(null);
+ assertNotNull(assetLibrary);
+ }
+
+ @Test
+ public void testRemoveHeaderWithEmptyString() {
+ assetLibrary.removeHeader("");
+ assertNotNull(assetLibrary);
+ }
+
+ // ==================== SORT Tests ====================
+
+ @Test
+ public void testSortAscending() {
+ AssetLibrary result = assetLibrary.sort("created_at", AssetLibrary.ORDERBY.ASCENDING);
+ assertNotNull(result);
+ assertSame(assetLibrary, result);
+ }
+
+ @Test
+ public void testSortDescending() {
+ AssetLibrary result = assetLibrary.sort("updated_at", AssetLibrary.ORDERBY.DESCENDING);
+ assertNotNull(result);
+ assertSame(assetLibrary, result);
+ }
+
+ @Test
+ public void testSortWithNullKey() {
+ AssetLibrary result = assetLibrary.sort(null, AssetLibrary.ORDERBY.ASCENDING);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void testSortMultipleTimes() {
+ assetLibrary.sort("field1", AssetLibrary.ORDERBY.ASCENDING);
+ AssetLibrary result = assetLibrary.sort("field2", AssetLibrary.ORDERBY.DESCENDING);
+ assertNotNull(result);
+ }
+
+ // ==================== INCLUDE COUNT Tests ====================
+
+ @Test
+ public void testIncludeCount() {
+ AssetLibrary result = assetLibrary.includeCount();
+ assertNotNull(result);
+ assertSame(assetLibrary, result);
+ }
+
+ @Test
+ public void testIncludeCountMultipleTimes() {
+ assetLibrary.includeCount();
+ AssetLibrary result = assetLibrary.includeCount();
+ assertNotNull(result);
+ }
+
+ // ==================== INCLUDE RELATIVE URL Tests ====================
+
+ @Test
+ public void testIncludeRelativeUrl() {
+ AssetLibrary result = assetLibrary.includeRelativeUrl();
+ assertNotNull(result);
+ assertSame(assetLibrary, result);
+ }
+
+ @Test
+ public void testIncludeRelativeUrlMultipleTimes() {
+ assetLibrary.includeRelativeUrl();
+ AssetLibrary result = assetLibrary.includeRelativeUrl();
+ assertNotNull(result);
+ }
+
+ // ==================== INCLUDE METADATA Tests ====================
+
+ @Test
+ public void testIncludeMetadata() {
+ AssetLibrary result = assetLibrary.includeMetadata();
+ assertNotNull(result);
+ assertSame(assetLibrary, result);
+ }
+
+ @Test
+ public void testIncludeMetadataMultipleTimes() {
+ assetLibrary.includeMetadata();
+ AssetLibrary result = assetLibrary.includeMetadata();
+ assertNotNull(result);
+ }
+
+ // ==================== WHERE Tests ====================
+
+ @Test
+ public void testWhere() {
+ AssetLibrary result = assetLibrary.where("content_type", "image/jpeg");
+ assertNotNull(result);
+ assertSame(assetLibrary, result);
+ }
+
+ @Test
+ public void testWhereWithNull() {
+ AssetLibrary result = assetLibrary.where(null, "value");
+ assertNotNull(result);
+
+ result = assetLibrary.where("key", null);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void testWhereMultiple() {
+ assetLibrary.where("content_type", "image/png");
+ AssetLibrary result = assetLibrary.where("file_size", "1024");
+ assertNotNull(result);
+ }
+
+ // ==================== CACHE POLICY Tests ====================
+
+ @Test
+ public void testSetCachePolicyNetworkOnly() {
+ assetLibrary.setCachePolicy(CachePolicy.NETWORK_ONLY);
+ assertNotNull(assetLibrary);
+ }
+
+ @Test
+ public void testSetCachePolicyCacheOnly() {
+ assetLibrary.setCachePolicy(CachePolicy.CACHE_ONLY);
+ assertNotNull(assetLibrary);
+ }
+
+ @Test
+ public void testSetCachePolicyCacheElseNetwork() {
+ assetLibrary.setCachePolicy(CachePolicy.CACHE_ELSE_NETWORK);
+ assertNotNull(assetLibrary);
+ }
+
+ @Test
+ public void testSetCachePolicyCacheThenNetwork() {
+ assetLibrary.setCachePolicy(CachePolicy.CACHE_THEN_NETWORK);
+ assertNotNull(assetLibrary);
+ }
+
+ @Test
+ public void testSetCachePolicyNetworkElseCache() {
+ assetLibrary.setCachePolicy(CachePolicy.NETWORK_ELSE_CACHE);
+ assertNotNull(assetLibrary);
+ }
+
+ @Test
+ public void testSetCachePolicyIgnoreCache() {
+ assetLibrary.setCachePolicy(CachePolicy.IGNORE_CACHE);
+ assertNotNull(assetLibrary);
+ }
+
+ // ==================== CONFIGURATION Tests ====================
+
+ @Test
+ public void testConfigurationCombination() {
+ assetLibrary.setHeader("test-header", "test-value");
+ assetLibrary.setCachePolicy(CachePolicy.NETWORK_ONLY);
+ assetLibrary.includeCount();
+ assertNotNull(assetLibrary);
+ }
+
+ // ==================== METHOD CHAINING Tests ====================
+
+ @Test
+ public void testMethodChaining() {
+ AssetLibrary result = assetLibrary
+ .sort("created_at", AssetLibrary.ORDERBY.ASCENDING)
+ .includeCount()
+ .includeRelativeUrl()
+ .includeMetadata()
+ .where("content_type", "image/jpeg");
+
+ assertNotNull(result);
+ assertSame(assetLibrary, result);
+ }
+
+ @Test
+ public void testComplexChaining() {
+ assetLibrary.setHeader("api-version", "v3");
+ assetLibrary.setCachePolicy(CachePolicy.CACHE_ELSE_NETWORK);
+
+ assetLibrary
+ .sort("updated_at", AssetLibrary.ORDERBY.DESCENDING)
+ .includeCount()
+ .includeRelativeUrl()
+ .where("title", "My Asset");
+
+ assertNotNull(assetLibrary);
+ }
+
+ // ==================== MULTIPLE OPERATIONS Tests ====================
+
+ @Test
+ public void testMultipleSortOperations() {
+ assetLibrary
+ .sort("created_at", AssetLibrary.ORDERBY.ASCENDING)
+ .sort("file_size", AssetLibrary.ORDERBY.DESCENDING);
+
+ assertNotNull(assetLibrary);
+ }
+
+ @Test
+ public void testMultipleWhereOperations() {
+ assetLibrary
+ .where("content_type", "image/png")
+ .where("file_size", "2048")
+ .where("title", "Test");
+
+ assertNotNull(assetLibrary);
+ }
+
+ @Test
+ public void testAllIncludeOptions() {
+ assetLibrary
+ .includeCount()
+ .includeRelativeUrl()
+ .includeMetadata();
+
+ assertNotNull(assetLibrary);
+ }
+
+ // ==================== EDGE CASES Tests ====================
+
+ @Test
+ public void testEmptyStringValues() {
+ assetLibrary
+ .sort("", AssetLibrary.ORDERBY.ASCENDING)
+ .where("", "");
+
+ assertNotNull(assetLibrary);
+ }
+
+ @Test
+ public void testSpecialCharacters() {
+ assetLibrary.where("title", "Asset &