Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion contentstack/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
18 changes: 18 additions & 0 deletions contentstack/src/main/java/com/contentstack/sdk/Asset.java
Original file line number Diff line number Diff line change
Expand Up @@ -616,4 +616,22 @@ public Asset includeBranch() {
return this;
}

/**
* <p>
* <br><br><b>Example :</b><br>
* <pre class="prettyprint">
* Asset asset = asset.setLocale("en-hi");
* </pre>
* </p>
*/
public Asset setLocale(String locale) {
if (locale != null) {
try {
urlQueries.put("locale", locale);
} catch (JSONException e) {
Log.e(TAG, e.getLocalizedMessage());
}
}
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
117 changes: 47 additions & 70 deletions contentstack/src/test/java/com/contentstack/sdk/TestContentType.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<String, Object> 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<String, Object> getLocalHeader(ContentType contentType) throws Exception {
Expand Down Expand Up @@ -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<Error> 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<Error> 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<String, Object> 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);
}
Expand Down Expand Up @@ -593,4 +562,12 @@ public void testGetUrlParamsNullOrEmptyReturnsNull() throws Exception {
HashMap<String, Object> 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<String> keys() {
throw new RuntimeException("boom");
}
}
}
Loading