importExisting() {
return this.service.importExisting(this.headers, this.contentTypeUid, this.entryUid, this.params);
}
+ /**
+ * Retrieves all entry variants for this entry.
+ *
+ * Use {@link #addParam(String, Object)} for optional queries such as {@code locale}, {@code include_workflow}.
+ * Branch scope: stack {@value Util#BRANCH} from {@link com.contentstack.cms.Contentstack#stack(String, String, String)}
+ * is forwarded; {@link #addBranch(String)} overrides for this entry only.
+ *
+ * @return Retrofit call for GET …/entries/{entry_uid}/variants
+ * @see Get all entry variants
+ */
+ public Call fetchEntryVariants() {
+ validateCT();
+ validateEntry();
+ return this.service.fetchEntryVariants(this.headers, this.contentTypeUid, this.entryUid, this.params);
+ }
+
+ /**
+ * Retrieves a single entry variant using {@link #headers} for {@value Util#BRANCH} (stack default and/or {@link #addBranch(String)}).
+ *
+ * @param variantUid variant UID path segment
+ * @return Retrofit call for GET …/variants/{variant_uid}
+ * @see #fetchEntryVariant(String, String)
+ */
+ public Call fetchEntryVariant(@NotNull String variantUid) {
+ return fetchEntryVariant(variantUid, null);
+ }
+
+ /**
+ * Retrieves a single entry variant with an optional per-call {@value Util#BRANCH} override.
+ *
+ * When {@code branchUid} is non-blank, it replaces {@value Util#BRANCH} on this request only (stack and {@link #addBranch(String)}
+ * values are not mutated on the entry). When {@code branchUid} is {@code null} or blank, behavior matches {@link #fetchEntryVariant(String)}.
+ * {@link #withAppliedVariantUid(String)} ({@value Util#X_CS_VARIANT_UID}) is unrelated to branch.
+ *
+ * @param variantUid variant UID path segment
+ * @param branchUid optional branch UID or alias for this request only; {@code null} or empty to use entry headers
+ * @return Retrofit call for GET …/variants/{variant_uid}
+ */
+ public Call fetchEntryVariant(@NotNull String variantUid, @Nullable String branchUid) {
+ validateCT();
+ validateEntry();
+ validateVariantUid(variantUid);
+ return this.service.fetchEntryVariant(variantHeadersWithOptionalBranch(branchUid), this.contentTypeUid,
+ this.entryUid, variantUid, this.params);
+ }
+
+ /**
+ * Creates an entry variant. Uses PUT …/variants/{variant_uid} (CMA upsert — same URL as {@link #updateEntryVariant}).
+ *
+ * Branch scope: inherits stack {@value Util#BRANCH}; override with {@link #addBranch(String)} or {@link #addHeader(String, String)}
+ * ({@value Util#BRANCH}) on this entry. Variant personalization header {@value Util#X_CS_VARIANT_UID} is orthogonal.
+ *
+ * @param variantUid variant UID path segment
+ * @param requestBody JSON body per API (typically wraps fields under {@code entry})
+ * @see Create Entry Variant
+ */
+ public Call createEntryVariant(@NotNull String variantUid, @NotNull JSONObject requestBody) {
+ validateCT();
+ validateEntry();
+ validateVariantUid(variantUid);
+ return this.service.createEntryVariant(this.headers, this.contentTypeUid, this.entryUid, variantUid, this.params,
+ requestBody);
+ }
+
+ /**
+ * Updates an entry variant. Same HTTP request shape as create (PUT upsert).
+ *
+ * Branch scope: inherits stack {@value Util#BRANCH}; override with {@link #addBranch(String)} or {@link #addHeader(String, String)}
+ * ({@value Util#BRANCH}) on this entry.
+ *
+ * @see Update Entry Variant
+ */
+ public Call updateEntryVariant(@NotNull String variantUid, @NotNull JSONObject requestBody) {
+ validateCT();
+ validateEntry();
+ validateVariantUid(variantUid);
+ return this.service.updateEntryVariant(this.headers, this.contentTypeUid, this.entryUid, variantUid, this.params,
+ requestBody);
+ }
+
+ /**
+ * Deletes an entry variant.
+ *
+ * Branch scope: inherits stack {@value Util#BRANCH}; override with {@link #addBranch(String)} or {@link #addHeader(String, String)}
+ * ({@value Util#BRANCH}) on this entry.
+ *
+ * @param variantUid variant UID path segment
+ * @return Retrofit call for DELETE …/variants/{variant_uid}
+ */
+ public Call deleteEntryVariant(@NotNull String variantUid) {
+ validateCT();
+ validateEntry();
+ validateVariantUid(variantUid);
+ return this.service.deleteEntryVariant(this.headers, this.contentTypeUid, this.entryUid, variantUid, this.params);
+ }
+
/**
* To Publish an entry request lets you publish an entry either immediately or
* schedule it for a later date/time.
@@ -752,7 +896,25 @@ public Call importExisting() {
public Call publish(@NotNull JSONObject requestBody) {
validateCT();
validateEntry();
- return this.service.publish(this.headers, this.contentTypeUid, this.entryUid, requestBody);
+ return this.service.publish(this.headers, this.contentTypeUid, this.entryUid, this.params, requestBody);
+ }
+
+ /**
+ * Publishes entry variants using the entry publish endpoint with {@code entry.variants} in the body.
+ * Sends header {@value Util#API_VERSION}={@value Util#API_VERSION_ENTRY_VARIANTS_PUBLISH} unless already set on this entry instance.
+ * Use {@link #addParam(String, Object)} for optional {@code locale} query parameter.
+ *
+ * Branch scope: stack {@value Util#BRANCH} is copied into the publish request headers together with {@code api_version};
+ * override with {@link #addBranch(String)} or {@link #addHeader(String, String)} ({@value Util#BRANCH}) on this entry.
+ *
+ * @param requestBody full publish payload including {@code entry}, {@code locale}, etc.
+ */
+ public Call publishEntryVariants(@NotNull JSONObject requestBody) {
+ validateCT();
+ validateEntry();
+ HashMap publishHeaders = new HashMap<>(this.headers);
+ publishHeaders.putIfAbsent(Util.API_VERSION, Util.API_VERSION_ENTRY_VARIANTS_PUBLISH);
+ return this.service.publish(publishHeaders, this.contentTypeUid, this.entryUid, this.params, requestBody);
}
/**
@@ -816,9 +978,23 @@ public Call publishWithReference(@NotNull JSONObject requestBody)
public Call unpublish(@NotNull JSONObject requestBody) {
validateCT();
validateEntry();
- return this.service.unpublish(this.headers, this.contentTypeUid, this.entryUid, requestBody);
+ return this.service.unpublish(this.headers, this.contentTypeUid, this.entryUid, this.params, requestBody);
}
+ /**
+ * Unpublishes entry variants via the entry unpublish endpoint with {@code entry.variants} in the body.
+ * Sends header {@value Util#API_VERSION}={@value Util#API_VERSION_ENTRY_VARIANTS_PUBLISH} unless already set.
+ *
+ * Branch scope: stack {@value Util#BRANCH} is forwarded; override with {@link #addBranch(String)} or {@link #addHeader(String, String)}
+ * ({@value Util#BRANCH}) on this entry.
+ */
+ public Call unpublishEntryVariants(@NotNull JSONObject requestBody) {
+ validateCT();
+ validateEntry();
+ HashMap unpublishHeaders = new HashMap<>(this.headers);
+ unpublishHeaders.putIfAbsent(Util.API_VERSION, Util.API_VERSION_ENTRY_VARIANTS_PUBLISH);
+ return this.service.unpublish(unpublishHeaders, this.contentTypeUid, this.entryUid, this.params, requestBody);
+ }
/**
* Get instance of taxonomy search filter class instance through which we can query on taxonomy based on content type
diff --git a/src/main/java/com/contentstack/cms/stack/EntryService.java b/src/main/java/com/contentstack/cms/stack/EntryService.java
index 8c5c82fb..cfa6e742 100644
--- a/src/main/java/com/contentstack/cms/stack/EntryService.java
+++ b/src/main/java/com/contentstack/cms/stack/EntryService.java
@@ -5,7 +5,6 @@
import retrofit2.Call;
import retrofit2.http.*;
-import java.util.List;
import java.util.Map;
public interface EntryService {
@@ -147,6 +146,7 @@ Call publish(
@HeaderMap Map headers,
@Path("content_type_uid") String contentTypeUid,
@Path("entry_uid") String entryUid,
+ @QueryMap(encoded = true) Map queryParameters,
@Body JSONObject requestBody);
@POST("bulk/publish?x-bulk-action=publish")
@@ -160,8 +160,58 @@ Call unpublish(
@HeaderMap Map headers,
@Path("content_type_uid") String contentTypeUid,
@Path("entry_uid") String entryUid,
+ @QueryMap(encoded = true) Map queryParameters,
@Body JSONObject requestBody);
+ @GET("content_types/{content_type_uid}/entries/{entry_uid}/variants")
+ Call fetchEntryVariants(
+ @HeaderMap Map headers,
+ @Path("content_type_uid") String contentTypeUid,
+ @Path("entry_uid") String entryUid,
+ @QueryMap(encoded = true) Map queryParameters);
+
+ @GET("content_types/{content_type_uid}/entries/{entry_uid}/variants/{variant_uid}")
+ Call fetchEntryVariant(
+ @HeaderMap Map headers,
+ @Path("content_type_uid") String contentTypeUid,
+ @Path("entry_uid") String entryUid,
+ @Path("variant_uid") String variantUid,
+ @QueryMap(encoded = true) Map queryParameters);
+
+ /**
+ * Create entry variant (PUT …/variants/{variant_uid}). Same HTTP contract as update — CMA upserts on this path.
+ */
+ @Headers("Content-Type: application/json")
+ @PUT("content_types/{content_type_uid}/entries/{entry_uid}/variants/{variant_uid}")
+ Call createEntryVariant(
+ @HeaderMap Map headers,
+ @Path("content_type_uid") String contentTypeUid,
+ @Path("entry_uid") String entryUid,
+ @Path("variant_uid") String variantUid,
+ @QueryMap(encoded = true) Map queryParameters,
+ @Body JSONObject requestBody);
+
+ /**
+ * Update entry variant — delegates to {@link #createEntryVariant}; API uses one PUT upsert for both operations.
+ */
+ default Call updateEntryVariant(
+ Map headers,
+ String contentTypeUid,
+ String entryUid,
+ String variantUid,
+ Map queryParameters,
+ JSONObject requestBody) {
+ return createEntryVariant(headers, contentTypeUid, entryUid, variantUid, queryParameters, requestBody);
+ }
+
+ @DELETE("content_types/{content_type_uid}/entries/{entry_uid}/variants/{variant_uid}")
+ Call deleteEntryVariant(
+ @HeaderMap Map headers,
+ @Path("content_type_uid") String contentTypeUid,
+ @Path("entry_uid") String entryUid,
+ @Path("variant_uid") String variantUid,
+ @QueryMap(encoded = true) Map queryParameters);
+
@GET("content_types/{content_type_uid}/entries")
Call filterTaxonomy(
@HeaderMap Map headers,
diff --git a/src/test/java/com/contentstack/cms/TestClient.java b/src/test/java/com/contentstack/cms/TestClient.java
index 2503e39b..c6b1e0e5 100644
--- a/src/test/java/com/contentstack/cms/TestClient.java
+++ b/src/test/java/com/contentstack/cms/TestClient.java
@@ -25,6 +25,26 @@ public class TestClient {
public final static String DEV_HOST = (env.get("dev_host") != null) ? env.get("dev_host").trim() : "api.contentstack.io";
public final static String VARIANT_GROUP_UID = (env.get("variantGroupUid") != null) ? env.get("variantGroupUid")
: "variantGroupUid99999999";
+
+ /** Content type UID for entry-variant tests (default {@code blog}). */
+ public static final String ENTRY_VARIANT_CONTENT_TYPE_UID =
+ env.get("entryVariantContentTypeUid") != null ? env.get("entryVariantContentTypeUid") : "blog";
+
+ /** Stack branch for entry-variant tests (default {@code develop}). */
+ public static final String ENTRY_VARIANT_BRANCH =
+ env.get("entryVariantBranch") != null ? env.get("entryVariantBranch") : "develop";
+
+ /** Locale query param for entry-variant tests (default {@code en-us}). */
+ public static final String ENTRY_VARIANT_LOCALE =
+ env.get("entryVariantLocale") != null ? env.get("entryVariantLocale") : "en-us";
+
+ /**
+ * {@code true} when {@code apiKey} / {@code managementToken} were not loaded from {@code .env} (defaults apply).
+ */
+ public static boolean isUsingDefaultStackCredentials() {
+ return env.get("apiKey") == null || env.get("managementToken") == null;
+ }
+
private static Contentstack instance;
private static Stack stackInstance;
diff --git a/src/test/java/com/contentstack/cms/UnitTestSuite.java b/src/test/java/com/contentstack/cms/UnitTestSuite.java
index 97df7a51..912328f2 100644
--- a/src/test/java/com/contentstack/cms/UnitTestSuite.java
+++ b/src/test/java/com/contentstack/cms/UnitTestSuite.java
@@ -1,6 +1,7 @@
package com.contentstack.cms;
import com.contentstack.cms.core.AuthInterceptorTest;
+import com.contentstack.cms.stack.EntryVariantUnitTest;
import com.contentstack.cms.stack.EnvironmentUnitTest;
import com.contentstack.cms.stack.GlobalFieldUnitTests;
import com.contentstack.cms.stack.LocaleUnitTest;
@@ -25,6 +26,7 @@
ContentstackUnitTest.class,
// Stack module tests (only public classes)
+ EntryVariantUnitTest.class,
EnvironmentUnitTest.class,
GlobalFieldUnitTests.class,
LocaleUnitTest.class,
diff --git a/src/test/java/com/contentstack/cms/stack/APISanityTestSuite.java b/src/test/java/com/contentstack/cms/stack/APISanityTestSuite.java
index 4b027193..14a514f1 100644
--- a/src/test/java/com/contentstack/cms/stack/APISanityTestSuite.java
+++ b/src/test/java/com/contentstack/cms/stack/APISanityTestSuite.java
@@ -22,6 +22,7 @@
GlobalFieldAPITest.class,
VariantGroupAPITest.class,
VariantGroupTest.class,
+ EntryVariantAPITest.class,
ReleaseAPITest.class
})
diff --git a/src/test/java/com/contentstack/cms/stack/EntryVariantAPITest.java b/src/test/java/com/contentstack/cms/stack/EntryVariantAPITest.java
new file mode 100644
index 00000000..05a45f37
--- /dev/null
+++ b/src/test/java/com/contentstack/cms/stack/EntryVariantAPITest.java
@@ -0,0 +1,246 @@
+package com.contentstack.cms.stack;
+
+import com.contentstack.cms.TestClient;
+import com.contentstack.cms.Utils;
+import com.contentstack.cms.core.Util;
+
+import okhttp3.Request;
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.TestMethodOrder;
+
+/**
+ * Request-shape checks for entry-variant endpoints (no HTTP — same style as {@link ContentTypeAPITest}).
+ */
+@Tag("unit")
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+public class EntryVariantAPITest {
+
+ private static final String CREATE_VARIANT_BODY = "entry_variant/create_entry_variant.json";
+ private static final String UPDATE_VARIANT_BODY = "entry_variant/update_entry_variant.json";
+ private static final String PUBLISH_VARIANT_BODY = "entry_variant/publish_entry_variant.json";
+ private static final String UNPUBLISH_VARIANT_BODY = "entry_variant/unpublish_entry_variant.json";
+
+ private final String contentTypeUid = TestClient.ENTRY_VARIANT_CONTENT_TYPE_UID;
+ private final String entryUid = "entry_uid";
+ private final String variantUid = "variant_uid";
+ private final String stackBranch = TestClient.ENTRY_VARIANT_BRANCH == null
+ ? "develop"
+ : TestClient.ENTRY_VARIANT_BRANCH.trim();
+
+ private Stack stack;
+ private int headerCount;
+
+ @BeforeAll
+ void setUp() {
+ /*
+ * Fresh Stack from the client — avoids mutating {@link TestClient#getStack()} singleton headers
+ * (which broke other suites such as {@link ReleaseAPITest} when branch was added globally).
+ */
+ stack = TestClient.getClient().stack(
+ TestClient.API_KEY,
+ TestClient.MANAGEMENT_TOKEN,
+ stackBranch);
+ headerCount = stack.headers.size();
+ }
+
+ private Entry freshEntry() {
+ return stack.contentType(contentTypeUid).entry(entryUid);
+ }
+
+ private String variantPathSuffix() {
+ return "/v3/content_types/" + contentTypeUid + "/entries/" + entryUid + "/variants/" + variantUid;
+ }
+
+ private String variantsCollectionPath() {
+ return "/v3/content_types/" + contentTypeUid + "/entries/" + entryUid + "/variants";
+ }
+
+ /**
+ * Publish/unpublish fixtures omit {@code variants[].uid}; inject placeholder for request body construction.
+ */
+ @SuppressWarnings("unchecked")
+ private static void injectVariantUidIntoVariantsArray(JSONObject body, String uid) {
+ JSONObject ent = (JSONObject) body.get("entry");
+ Assertions.assertNotNull(ent, "Body missing entry");
+ JSONArray variants = (JSONArray) ent.get("variants");
+ Assertions.assertNotNull(variants, "entry.variants missing");
+ Assertions.assertFalse(variants.isEmpty(), "entry.variants empty");
+ JSONObject v0 = (JSONObject) variants.get(0);
+ v0.put("uid", uid);
+ }
+
+ @Test
+ @Order(1)
+ void createEntryVariant_requestShape() {
+ Entry entry = freshEntry();
+ entry.clearParams();
+ entry.addParam("locale", TestClient.ENTRY_VARIANT_LOCALE);
+ JSONObject body = Utils.readJson(CREATE_VARIANT_BODY);
+ Assertions.assertNotNull(body);
+
+ Request req = entry.createEntryVariant(variantUid, body).request();
+ Assertions.assertEquals(headerCount, req.headers().names().size());
+ Assertions.assertEquals("PUT", req.method());
+ Assertions.assertTrue(req.url().isHttps());
+ Assertions.assertEquals(variantPathSuffix(), req.url().encodedPath());
+ Assertions.assertEquals("locale=" + TestClient.ENTRY_VARIANT_LOCALE, req.url().encodedQuery());
+ Assertions.assertEquals(stackBranch, req.header(Util.BRANCH));
+ }
+
+ @Test
+ @Order(2)
+ void updateEntryVariant_sameUrlAndMethodAsCreate() {
+ Entry entry = freshEntry();
+ entry.clearParams();
+ JSONObject createBody = Utils.readJson(CREATE_VARIANT_BODY);
+ JSONObject updateBody = Utils.readJson(UPDATE_VARIANT_BODY);
+ Assertions.assertNotNull(createBody);
+ Assertions.assertNotNull(updateBody);
+
+ Request createReq = entry.createEntryVariant(variantUid, createBody).request();
+ Entry entry2 = freshEntry();
+ Request updateReq = entry2.updateEntryVariant(variantUid, updateBody).request();
+
+ Assertions.assertEquals(createReq.method(), updateReq.method());
+ Assertions.assertEquals(createReq.url(), updateReq.url());
+ }
+
+ @Test
+ @Order(3)
+ void fetchSingleEntryVariant_requestShape() {
+ Entry entry = freshEntry();
+ entry.clearParams();
+ entry.addParam("locale", TestClient.ENTRY_VARIANT_LOCALE);
+ entry.addParam("include_publish_details", true);
+
+ Request req = entry.fetchEntryVariant(variantUid).request();
+ Assertions.assertEquals(headerCount, req.headers().names().size());
+ Assertions.assertEquals("GET", req.method());
+ Assertions.assertTrue(req.url().isHttps());
+ Assertions.assertEquals(variantPathSuffix(), req.url().encodedPath());
+ Assertions.assertNotNull(req.url().encodedQuery());
+ Assertions.assertTrue(req.url().encodedQuery().contains("locale=" + TestClient.ENTRY_VARIANT_LOCALE));
+ Assertions.assertTrue(req.url().encodedQuery().contains("include_publish_details=true"));
+ }
+
+ @Test
+ @Order(4)
+ void fetchAllEntryVariants_requestShape() {
+ Entry entry = freshEntry();
+ entry.clearParams();
+ entry.addParam("locale", TestClient.ENTRY_VARIANT_LOCALE);
+
+ Request req = entry.fetchEntryVariants().request();
+ Assertions.assertEquals(headerCount, req.headers().names().size());
+ Assertions.assertEquals("GET", req.method());
+ Assertions.assertEquals(variantsCollectionPath(), req.url().encodedPath());
+ Assertions.assertEquals("locale=" + TestClient.ENTRY_VARIANT_LOCALE, req.url().encodedQuery());
+ }
+
+ @Test
+ @Order(5)
+ void publishEntryVariants_requestShape() {
+ Entry entry = freshEntry();
+ entry.clearParams();
+ entry.addParam("locale", TestClient.ENTRY_VARIANT_LOCALE);
+
+ JSONObject publishBody = Utils.readJson(PUBLISH_VARIANT_BODY);
+ Assertions.assertNotNull(publishBody);
+ injectVariantUidIntoVariantsArray(publishBody, variantUid);
+
+ Request req = entry.publishEntryVariants(publishBody).request();
+ Assertions.assertEquals("POST", req.method());
+ Assertions.assertTrue(req.url().isHttps());
+ Assertions.assertEquals(
+ "/v3/content_types/" + contentTypeUid + "/entries/" + entryUid + "/publish",
+ req.url().encodedPath());
+ Assertions.assertEquals("locale=" + TestClient.ENTRY_VARIANT_LOCALE, req.url().encodedQuery());
+ Assertions.assertEquals(Util.API_VERSION_ENTRY_VARIANTS_PUBLISH, req.header(Util.API_VERSION));
+ }
+
+ @Test
+ @Order(6)
+ void unpublishEntryVariants_requestShape() {
+ Entry entry = freshEntry();
+ entry.clearParams();
+ entry.addParam("locale", TestClient.ENTRY_VARIANT_LOCALE);
+
+ JSONObject unpublishBody = Utils.readJson(UNPUBLISH_VARIANT_BODY);
+ Assertions.assertNotNull(unpublishBody);
+ injectVariantUidIntoVariantsArray(unpublishBody, variantUid);
+
+ Request req = entry.unpublishEntryVariants(unpublishBody).request();
+ Assertions.assertEquals("POST", req.method());
+ Assertions.assertEquals(
+ "/v3/content_types/" + contentTypeUid + "/entries/" + entryUid + "/unpublish",
+ req.url().encodedPath());
+ Assertions.assertEquals("locale=" + TestClient.ENTRY_VARIANT_LOCALE, req.url().encodedQuery());
+ Assertions.assertEquals(Util.API_VERSION_ENTRY_VARIANTS_PUBLISH, req.header(Util.API_VERSION));
+ }
+
+ @Test
+ @Order(7)
+ void fetchSingleEntryVariant_implicitVsExplicitBranchHeader() {
+ Entry plain = freshEntry();
+ plain.clearParams();
+ plain.addParam("locale", TestClient.ENTRY_VARIANT_LOCALE);
+
+ Request implicit = plain.fetchEntryVariant(variantUid).request();
+ Assertions.assertEquals(stackBranch, implicit.header(Util.BRANCH));
+
+ Entry withPerCall = freshEntry();
+ withPerCall.clearParams();
+ withPerCall.addParam("locale", TestClient.ENTRY_VARIANT_LOCALE);
+ Request explicit = withPerCall.fetchEntryVariant(variantUid, stackBranch).request();
+ Assertions.assertEquals(stackBranch, explicit.header(Util.BRANCH));
+ Assertions.assertEquals(implicit.url(), explicit.url());
+ }
+
+ @Test
+ @Order(8)
+ void fetchSingleEntryVariant_afterAddBranch() {
+ Entry entry = freshEntry();
+ entry.clearParams();
+ entry.addBranch(stackBranch);
+ entry.addParam("locale", TestClient.ENTRY_VARIANT_LOCALE);
+
+ Request req = entry.fetchEntryVariant(variantUid).request();
+ Assertions.assertEquals(stackBranch, req.header(Util.BRANCH));
+ Assertions.assertEquals(variantPathSuffix(), req.url().encodedPath());
+ }
+
+ @Test
+ @Order(9)
+ void fetchSingleEntryVariant_withPerCallBranchOverload() {
+ Entry entry = freshEntry();
+ entry.clearParams();
+ entry.addParam("locale", TestClient.ENTRY_VARIANT_LOCALE);
+
+ Request req = entry.fetchEntryVariant(variantUid, stackBranch).request();
+ Assertions.assertEquals(stackBranch, req.header(Util.BRANCH));
+ Assertions.assertEquals("GET", req.method());
+ Assertions.assertEquals(variantPathSuffix(), req.url().encodedPath());
+ }
+
+ @Test
+ @Order(10)
+ void deleteEntryVariant_requestShape() {
+ Entry entry = freshEntry();
+ entry.clearParams();
+ entry.addParam("locale", TestClient.ENTRY_VARIANT_LOCALE);
+
+ Request req = entry.deleteEntryVariant(variantUid).request();
+ Assertions.assertEquals("DELETE", req.method());
+ Assertions.assertEquals(variantPathSuffix(), req.url().encodedPath());
+ Assertions.assertEquals("locale=" + TestClient.ENTRY_VARIANT_LOCALE, req.url().encodedQuery());
+ }
+}
diff --git a/src/test/java/com/contentstack/cms/stack/EntryVariantUnitTest.java b/src/test/java/com/contentstack/cms/stack/EntryVariantUnitTest.java
new file mode 100644
index 00000000..acb67fe5
--- /dev/null
+++ b/src/test/java/com/contentstack/cms/stack/EntryVariantUnitTest.java
@@ -0,0 +1,235 @@
+package com.contentstack.cms.stack;
+
+import com.contentstack.cms.Contentstack;
+import com.contentstack.cms.TestClient;
+import com.contentstack.cms.Utils;
+import com.contentstack.cms.core.Util;
+import okhttp3.Request;
+import org.json.simple.JSONObject;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Request-shape checks for entry variants (no HTTP — safe without dev18 / prod variant APIs).
+ */
+@Tag("unit")
+public class EntryVariantUnitTest {
+
+ private static final String CREATE_VARIANT_BODY = "entry_variant/create_entry_variant.json";
+ private static final String UPDATE_VARIANT_BODY = "entry_variant/update_entry_variant.json";
+
+ private static Entry entry;
+
+ @BeforeAll
+ static void setup() {
+ Contentstack cms = new Contentstack.Builder().setAuthtoken(TestClient.AUTHTOKEN).build();
+ Stack stack = cms.stack(TestClient.API_KEY, TestClient.MANAGEMENT_TOKEN);
+ entry = stack.contentType("blog").entry("entryUid123");
+ }
+
+ @Test
+ void createEntryVariant_requestIsPutWithVariantSegment() {
+ entry.clearParams();
+ entry.addParam("locale", "en-us");
+ JSONObject body = Utils.readJson(CREATE_VARIANT_BODY);
+ Assertions.assertNotNull(body, "Missing " + CREATE_VARIANT_BODY);
+
+ Request req = entry.createEntryVariant("variantUid456", body).request();
+ Assertions.assertEquals("PUT", req.method());
+ Assertions.assertEquals(
+ "/v3/content_types/blog/entries/entryUid123/variants/variantUid456",
+ req.url().encodedPath());
+ Assertions.assertEquals("locale=en-us", req.url().encodedQuery());
+ }
+
+ @Test
+ void updateEntryVariant_samePathAndMethodAsCreate() {
+ entry.clearParams();
+ JSONObject createBody = Utils.readJson(CREATE_VARIANT_BODY);
+ JSONObject updateBody = Utils.readJson(UPDATE_VARIANT_BODY);
+ Assertions.assertNotNull(createBody, "Missing " + CREATE_VARIANT_BODY);
+ Assertions.assertNotNull(updateBody, "Missing " + UPDATE_VARIANT_BODY);
+
+ Request createReq = entry.createEntryVariant("sameUid", createBody).request();
+ Request updateReq = entry.updateEntryVariant("sameUid", updateBody).request();
+
+ Assertions.assertEquals(createReq.method(), updateReq.method());
+ Assertions.assertEquals(createReq.url(), updateReq.url());
+ }
+
+ @Test
+ void fetchEntryVariants_requestIsGetOnVariantsCollection() {
+ entry.clearParams();
+ Request req = entry.fetchEntryVariants().request();
+ Assertions.assertEquals("GET", req.method());
+ Assertions.assertEquals(
+ "/v3/content_types/blog/entries/entryUid123/variants",
+ req.url().encodedPath());
+ }
+
+ @Test
+ void fetchEntryVariant_requestIsGetOnSingleVariant() {
+ entry.clearParams();
+ Request req = entry.fetchEntryVariant("v1").request();
+ Assertions.assertEquals(
+ "/v3/content_types/blog/entries/entryUid123/variants/v1",
+ req.url().encodedPath());
+ }
+
+ @Test
+ void publishEntryVariants_addsApiVersionHeaderWhenNotAlreadySet() {
+ entry.clearParams();
+ JSONObject body = new JSONObject();
+ Request req = entry.publishEntryVariants(body).request();
+ Assertions.assertEquals(Util.API_VERSION_ENTRY_VARIANTS_PUBLISH, req.header(Util.API_VERSION));
+ }
+
+ @Test
+ void unpublishEntryVariants_addsApiVersionHeaderWhenNotAlreadySet() {
+ entry.clearParams();
+ JSONObject body = new JSONObject();
+ Request req = entry.unpublishEntryVariants(body).request();
+ Assertions.assertEquals(Util.API_VERSION_ENTRY_VARIANTS_PUBLISH, req.header(Util.API_VERSION));
+ }
+
+ @Test
+ void deleteEntryVariant_requestIsDeleteOnVariantPath() {
+ entry.clearParams();
+ Request req = entry.deleteEntryVariant("toRemove").request();
+ Assertions.assertEquals("DELETE", req.method());
+ Assertions.assertEquals(
+ "/v3/content_types/blog/entries/entryUid123/variants/toRemove",
+ req.url().encodedPath());
+ }
+
+ @Test
+ void variantOperations_forwardStackBranchFromThreeArgStack() {
+ Contentstack cms = new Contentstack.Builder().setAuthtoken(TestClient.AUTHTOKEN).build();
+ Stack stack = cms.stack(TestClient.API_KEY, TestClient.MANAGEMENT_TOKEN, "feature-branch");
+ Entry e = stack.contentType("blog").entry("entryUid123");
+ e.clearParams();
+ JSONObject body = Utils.readJson(CREATE_VARIANT_BODY);
+ Assertions.assertNotNull(body, "Missing " + CREATE_VARIANT_BODY);
+
+ Assertions.assertEquals(
+ "feature-branch",
+ e.createEntryVariant("variantUid456", body).request().header(Util.BRANCH));
+ Assertions.assertEquals(
+ "feature-branch",
+ e.fetchEntryVariants().request().header(Util.BRANCH));
+ Assertions.assertEquals(
+ "feature-branch",
+ e.fetchEntryVariant("v1").request().header(Util.BRANCH));
+ Assertions.assertEquals(
+ "feature-branch",
+ e.publishEntryVariants(new JSONObject()).request().header(Util.BRANCH));
+ Assertions.assertEquals(
+ "feature-branch",
+ e.unpublishEntryVariants(new JSONObject()).request().header(Util.BRANCH));
+ }
+
+ @Test
+ void fetchEntryVariant_branchArgumentOverridesStackForThatCallOnly() {
+ Contentstack cms = new Contentstack.Builder().setAuthtoken(TestClient.AUTHTOKEN).build();
+ Stack stack = cms.stack(TestClient.API_KEY, TestClient.MANAGEMENT_TOKEN, "stack-branch");
+ Entry e = stack.contentType("blog").entry("entryUid123");
+ e.clearParams();
+
+ Request overridden = e.fetchEntryVariant("v1", "call-branch").request();
+ Assertions.assertEquals("call-branch", overridden.header(Util.BRANCH));
+
+ Request defaultBranch = e.fetchEntryVariant("v1").request();
+ Assertions.assertEquals("stack-branch", defaultBranch.header(Util.BRANCH));
+
+ Assertions.assertEquals("stack-branch", e.fetchEntryVariant("v1", null).request().header(Util.BRANCH));
+ }
+
+ @Test
+ void fetchEntryVariant_branchArgumentOverridesEntryLevelBranch() {
+ Contentstack cms = new Contentstack.Builder().setAuthtoken(TestClient.AUTHTOKEN).build();
+ Stack stack = cms.stack(TestClient.API_KEY, TestClient.MANAGEMENT_TOKEN, "stack-branch");
+ Entry e = stack.contentType("blog").entry("entryUid123").addBranch("entry-branch");
+ e.clearParams();
+
+ Assertions.assertEquals(
+ "per-call-branch",
+ e.fetchEntryVariant("v1", "per-call-branch").request().header(Util.BRANCH));
+ Assertions.assertEquals(
+ "entry-branch",
+ e.fetchEntryVariant("v1").request().header(Util.BRANCH));
+ }
+
+ @Test
+ void variantOperations_entryAddBranchOverridesStackBranch() {
+ Contentstack cms = new Contentstack.Builder().setAuthtoken(TestClient.AUTHTOKEN).build();
+ Stack stack = cms.stack(TestClient.API_KEY, TestClient.MANAGEMENT_TOKEN, "stack-branch");
+ Entry e = stack.contentType("blog").entry("entryUid123").addBranch("override-branch");
+ e.clearParams();
+ JSONObject body = Utils.readJson(CREATE_VARIANT_BODY);
+ Assertions.assertNotNull(body, "Missing " + CREATE_VARIANT_BODY);
+
+ Request req = e.createEntryVariant("variantUid456", body).request();
+ Assertions.assertEquals("override-branch", req.header(Util.BRANCH));
+ }
+
+ @Test
+ void deleteEntryVariant_forwardsStackBranchHeader() {
+ Contentstack cms = new Contentstack.Builder().setAuthtoken(TestClient.AUTHTOKEN).build();
+ Stack stack = cms.stack(TestClient.API_KEY, TestClient.MANAGEMENT_TOKEN, "branch-for-delete");
+ Entry e = stack.contentType("blog").entry("entryUid123");
+ e.clearParams();
+ Assertions.assertEquals(
+ "branch-for-delete",
+ e.deleteEntryVariant("toRemove").request().header(Util.BRANCH));
+ }
+
+ @Test
+ void updateEntryVariant_forwardsStackBranchHeader() {
+ Contentstack cms = new Contentstack.Builder().setAuthtoken(TestClient.AUTHTOKEN).build();
+ Stack stack = cms.stack(TestClient.API_KEY, TestClient.MANAGEMENT_TOKEN, "branch-for-update");
+ Entry e = stack.contentType("blog").entry("entryUid123");
+ e.clearParams();
+ JSONObject body = Utils.readJson(UPDATE_VARIANT_BODY);
+ Assertions.assertNotNull(body, "Missing " + UPDATE_VARIANT_BODY);
+ Assertions.assertEquals(
+ "branch-for-update",
+ e.updateEntryVariant("uid", body).request().header(Util.BRANCH));
+ }
+
+ @Test
+ void addHeaderBranch_matchesAddBranchForVariantRequests() {
+ Contentstack cms = new Contentstack.Builder().setAuthtoken(TestClient.AUTHTOKEN).build();
+ Stack stack = cms.stack(TestClient.API_KEY, TestClient.MANAGEMENT_TOKEN);
+ Entry viaAddHeader = stack.contentType("blog").entry("e1").addHeader(Util.BRANCH, "hdr-branch");
+ Entry viaAddBranch = stack.contentType("blog").entry("e2").addBranch("hdr-branch");
+ viaAddHeader.clearParams();
+ viaAddBranch.clearParams();
+ JSONObject body = Utils.readJson(CREATE_VARIANT_BODY);
+ Assertions.assertNotNull(body, "Missing " + CREATE_VARIANT_BODY);
+ Assertions.assertEquals(
+ viaAddBranch.createEntryVariant("v", body).request().header(Util.BRANCH),
+ viaAddHeader.createEntryVariant("v", body).request().header(Util.BRANCH));
+ }
+
+ @Test
+ void fetchEntryVariant_blankSecondArgUsesStackBranchLikeNull() {
+ Contentstack cms = new Contentstack.Builder().setAuthtoken(TestClient.AUTHTOKEN).build();
+ Stack stack = cms.stack(TestClient.API_KEY, TestClient.MANAGEMENT_TOKEN, "stack-branch");
+ Entry e = stack.contentType("blog").entry("entryUid123");
+ e.clearParams();
+ Assertions.assertEquals(
+ "stack-branch",
+ e.fetchEntryVariant("v1", "").request().header(Util.BRANCH));
+ }
+
+ @Test
+ void variantOperations_noStackBranch_omitsBranchHeaderWhenNotSetOnEntry() {
+ Contentstack cms = new Contentstack.Builder().setAuthtoken(TestClient.AUTHTOKEN).build();
+ Stack stack = cms.stack(TestClient.API_KEY, TestClient.MANAGEMENT_TOKEN);
+ Entry e = stack.contentType("blog").entry("entryUid123");
+ e.clearParams();
+ Assertions.assertNull(e.fetchEntryVariant("v1").request().header(Util.BRANCH));
+ }
+}
diff --git a/src/test/resources/entry_variant/create_entry_variant.json b/src/test/resources/entry_variant/create_entry_variant.json
new file mode 100644
index 00000000..f8745dad
--- /dev/null
+++ b/src/test/resources/entry_variant/create_entry_variant.json
@@ -0,0 +1,15 @@
+{
+ "entry": {
+ "title": "Blue|v1",
+ "url": "/blue",
+ "single_line": "blue variant v1",
+ "group": [
+ {
+ "single_line": "blue variant group 1"
+ },
+ {
+ "single_line": "blue variant group 2"
+ }
+ ]
+ }
+}
diff --git a/src/test/resources/entry_variant/publish_entry_variant.json b/src/test/resources/entry_variant/publish_entry_variant.json
new file mode 100644
index 00000000..d3680a24
--- /dev/null
+++ b/src/test/resources/entry_variant/publish_entry_variant.json
@@ -0,0 +1,16 @@
+{
+ "entry": {
+ "environments": ["development"],
+ "locales": ["en-us"],
+ "variants": [
+ {
+ "version": 1
+ }
+ ],
+ "variant_rules": {
+ "publish_latest_base": false,
+ "publish_latest_base_conditionally": true
+ }
+ },
+ "locale": "en-us"
+}
diff --git a/src/test/resources/entry_variant/unpublish_entry_variant.json b/src/test/resources/entry_variant/unpublish_entry_variant.json
new file mode 100644
index 00000000..daa27068
--- /dev/null
+++ b/src/test/resources/entry_variant/unpublish_entry_variant.json
@@ -0,0 +1,12 @@
+{
+ "entry": {
+ "environments": ["development"],
+ "locales": ["en-us"],
+ "variants": [
+ {
+ "version": 1
+ }
+ ]
+ },
+ "locale": "en-us"
+}
diff --git a/src/test/resources/entry_variant/update_entry_variant.json b/src/test/resources/entry_variant/update_entry_variant.json
new file mode 100644
index 00000000..412cea49
--- /dev/null
+++ b/src/test/resources/entry_variant/update_entry_variant.json
@@ -0,0 +1,17 @@
+{
+ "entry": {
+ "title": "blue |v2",
+ "url": "/blue",
+ "single_line": "blue variant v2 ",
+ "group": [
+ {
+ "single_line": "Variant 2",
+ "multi_line": "Variant 2 Multi"
+ },
+ {
+ "single_line": "Variant 1",
+ "multi_line": "Variant 1 Multi"
+ }
+ ]
+ }
+}