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/contentstack/build.gradle b/contentstack/build.gradle index f7a3ed44..f0ad9685 100755 --- a/contentstack/build.gradle +++ b/contentstack/build.gradle @@ -7,7 +7,7 @@ plugins { ext { PUBLISH_GROUP_ID = 'com.contentstack.sdk' PUBLISH_ARTIFACT_ID = 'android' - PUBLISH_VERSION = '4.1.0' + PUBLISH_VERSION = '4.2.0' } android { 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/test/java/com/contentstack/sdk/TestAssetAdvanced.java b/contentstack/src/test/java/com/contentstack/sdk/TestAssetAdvanced.java index 8b12c8a9..fb3bd0a9 100644 --- a/contentstack/src/test/java/com/contentstack/sdk/TestAssetAdvanced.java +++ b/contentstack/src/test/java/com/contentstack/sdk/TestAssetAdvanced.java @@ -189,6 +189,37 @@ public void testAddParamOverwrite() { 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 diff --git a/contentstack/src/test/java/com/contentstack/sdk/TestContentType.java b/contentstack/src/test/java/com/contentstack/sdk/TestContentType.java index c008c10d..c0409588 100644 --- a/contentstack/src/test/java/com/contentstack/sdk/TestContentType.java +++ b/contentstack/src/test/java/com/contentstack/sdk/TestContentType.java @@ -14,8 +14,10 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.HashMap; +import java.util.Iterator; import java.util.Map; -import static org.mockito.Mockito.*; +import java.util.concurrent.atomic.AtomicReference; + @RunWith(RobolectricTestRunner.class) @Config(sdk = 28, manifest = Config.NONE) public class TestContentType { @@ -336,27 +338,8 @@ private ContentType createBareContentType(String contentTypeUid) { return new ContentType(contentTypeUid); } - private ContentType createContentTypeWithStackAndHeaders(String contentTypeUid) throws Exception { - ContentType contentType = new ContentType(contentTypeUid); - - // mock Stack and inject a stackHeader / localHeader field if present - Stack mockStack = mock(Stack.class); - - // We will inject "localHeader" field into Stack if it exists - try { - Field localHeaderField = Stack.class.getDeclaredField("localHeader"); - localHeaderField.setAccessible(true); - ArrayMap stackHeaders = new ArrayMap<>(); - stackHeaders.put("environment", "prod-env"); - stackHeaders.put("stackKey", "stackVal"); - localHeaderField.set(mockStack, stackHeaders); - } catch (NoSuchFieldException ignored) { - // If Stack doesn't have localHeader, getHeader will just use localHeader or - // null. - } - - contentType.setStackInstance(mockStack); - return contentType; + private ContentType createContentTypeWithStackAndHeaders(String contentTypeUid) { + return stack.contentType(contentTypeUid); } private ArrayMap getLocalHeader(ContentType contentType) throws Exception { @@ -484,82 +467,68 @@ public void testQueryHasFormHeaderNonNull() throws Exception { @Test public void testFetchWithEmptyContentTypeNameCallsOnRequestFail() throws Exception { ContentType contentType = createBareContentType(""); - - // make sure stackInstance is not null - contentType.setStackInstance(mock(Stack.class)); - - ContentTypesCallback callback = mock(ContentTypesCallback.class); + contentType.setStackInstance(stack); + + final AtomicReference errorRef = new AtomicReference<>(); + ContentTypesCallback callback = new ContentTypesCallback() { + @Override + public void onCompletion(ContentTypesModel contentTypesModel, Error error) { + if (error != null) { + errorRef.set(error); + } + } + }; contentType.fetch(new JSONObject(), callback); - verify(callback).onRequestFail(eq(ResponseType.UNKNOWN), any(Error.class)); + assertNotNull(errorRef.get()); } @Test public void testFetchExceptionCallsOnRequestFail() throws Exception { ContentType contentType = createBareContentType("blog"); - contentType.setStackInstance(mock(Stack.class)); + contentType.setStackInstance(stack); - // Force an exception by using bad JSONObject for params - JSONObject badParams = mock(JSONObject.class); - when(badParams.keys()).thenThrow(new RuntimeException("boom")); + final AtomicReference errorRef = new AtomicReference<>(); + ContentTypesCallback callback = new ContentTypesCallback() { + @Override + public void onCompletion(ContentTypesModel contentTypesModel, Error error) { + if (error != null) { + errorRef.set(error); + } + } + }; - ContentTypesCallback callback = mock(ContentTypesCallback.class); + contentType.fetch(new ThrowingJSONObject(), callback); - contentType.fetch(badParams, callback); - - verify(callback).onRequestFail(eq(ResponseType.UNKNOWN), any(Error.class)); + assertNotNull(errorRef.get()); } @Test public void testFetchNullParamsAndEnvironmentHeader() throws Exception { ContentType contentType = createBareContentType("blog"); + contentType.setStackInstance(stack); - // Create a fake Stack with environment in its localHeader (so getHeader picks - // it) - Stack mockStack = mock(Stack.class); - - // Inject stack.localHeader if it exists - try { - Field localHeaderField = Stack.class.getDeclaredField("localHeader"); - localHeaderField.setAccessible(true); - ArrayMap stackHeaders = new ArrayMap<>(); - stackHeaders.put("environment", "prod-env"); - localHeaderField.set(mockStack, stackHeaders); - } catch (NoSuchFieldException ignored) { - } + ContentTypesCallback callback = new ContentTypesCallback() { + @Override + public void onCompletion(ContentTypesModel contentTypesModel, Error error) {} + }; - // Inject VERSION field if exists so URL is built properly (not strictly - // necessary for coverage) - try { - Field versionField = Stack.class.getDeclaredField("VERSION"); - versionField.setAccessible(true); - versionField.set(mockStack, "v3"); - } catch (NoSuchFieldException ignored) { - } - - contentType.setStackInstance(mockStack); - - ContentTypesCallback callback = mock(ContentTypesCallback.class); - - // this will hit: - // if (params == null) params = new JSONObject(); - // then iterate keys (none) - // then add environment if headers contains it contentType.fetch(null, callback); - - // We don't verify callback interactions here; this is just to cover branches. } @Test public void testFetchNormalCallDoesNotCrash() throws Exception { ContentType contentType = createBareContentType("blog"); - contentType.setStackInstance(mock(Stack.class)); + contentType.setStackInstance(stack); JSONObject params = new JSONObject(); params.put("limit", 3); - ContentTypesCallback callback = mock(ContentTypesCallback.class); + ContentTypesCallback callback = new ContentTypesCallback() { + @Override + public void onCompletion(ContentTypesModel contentTypesModel, Error error) {} + }; contentType.fetch(params, callback); } @@ -593,4 +562,12 @@ public void testGetUrlParamsNullOrEmptyReturnsNull() throws Exception { HashMap resultEmpty = invokeGetUrlParams(contentType, empty); assertNull(resultEmpty); } + + /** JSONObject that throws when keys() is called – used to trigger exception path in fetch() without Mockito. */ + private static class ThrowingJSONObject extends JSONObject { + @Override + public Iterator keys() { + throw new RuntimeException("boom"); + } + } } \ No newline at end of file