diff --git a/CHANGELOG.md b/CHANGELOG.md
index 62ca689..b2fde24 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## [v0.8.0](https://github.com/contentstack/contentstack-management-dotnet/tree/v0.8.0)
+ - Feat
+ - **Entry Variant support**
+ - `EntryVariant` model for create, fetch, find, update, and delete on entry variant endpoints
+ - `Entry.Variant(uid)` to access variant operations for a given entry
+ - Publish with variants: `PublishVariant`, `PublishVariantRules`, and `Variants` / `VariantRules` on `PublishUnpublishDetails`; serialization updated in `PublishUnpublishService`
+ - Unit tests for `EntryVariant` and publish payload serialization; integration tests (`Contentstack021_EntryVariantTest`) for Product Banner lifecycle and negative cases
+
## [v0.7.0](https://github.com/contentstack/contentstack-management-dotnet/tree/v0.7.0)
- Feat
- **Bulk publish/unpublish: query parameters (DX-3233)**
diff --git a/Contentstack.Management.Core.Tests/IntegrationTest/Contentstack015_BulkOperationTest.cs b/Contentstack.Management.Core.Tests/IntegrationTest/Contentstack015_BulkOperationTest.cs
index 9c61f86..339ecba 100644
--- a/Contentstack.Management.Core.Tests/IntegrationTest/Contentstack015_BulkOperationTest.cs
+++ b/Contentstack.Management.Core.Tests/IntegrationTest/Contentstack015_BulkOperationTest.cs
@@ -1789,6 +1789,9 @@ public class SimpleEntry : IEntry
{
[JsonProperty(propertyName: "title")]
public string Title { get; set; }
+
+ [JsonProperty(propertyName: "_variant")]
+ public object Variant { get; set; }
}
public class EntryInfo
diff --git a/Contentstack.Management.Core.Tests/IntegrationTest/Contentstack018_EnvironmentTest.cs b/Contentstack.Management.Core.Tests/IntegrationTest/Contentstack018_EnvironmentTest.cs
new file mode 100644
index 0000000..d763b2a
--- /dev/null
+++ b/Contentstack.Management.Core.Tests/IntegrationTest/Contentstack018_EnvironmentTest.cs
@@ -0,0 +1,471 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+using Contentstack.Management.Core.Exceptions;
+using Contentstack.Management.Core.Models;
+using Contentstack.Management.Core.Tests.Helpers;
+using Contentstack.Management.Core.Tests.Model;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Newtonsoft.Json.Linq;
+
+namespace Contentstack.Management.Core.Tests.IntegrationTest
+{
+ [TestClass]
+ [DoNotParallelize]
+ public class Contentstack018_EnvironmentTest
+ {
+ ///
+ /// Name that should not exist on any stack (for negative-path tests).
+ ///
+ private const string NonExistentEnvironmentName = "nonexistent_environment_name";
+
+ private static ContentstackClient _client;
+ private Stack _stack;
+
+ [ClassInitialize]
+ public static void ClassInitialize(TestContext context)
+ {
+ _client = Contentstack.CreateAuthenticatedClient();
+ }
+
+ [ClassCleanup]
+ public static void ClassCleanup()
+ {
+ try { _client?.Logout(); } catch { }
+ _client = null;
+ }
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ StackResponse response = StackResponse.getStack(_client.serializer);
+ _stack = _client.Stack(response.Stack.APIKey);
+ }
+
+ private static EnvironmentModel BuildModel(string uniqueName)
+ {
+ return new EnvironmentModel
+ {
+ Name = uniqueName,
+ Urls = new List
+ {
+ new LocalesUrl
+ {
+ Locale = "en-us",
+ Url = "https://example.com"
+ }
+ },
+ DeployContent = true
+ };
+ }
+
+ private static string ParseEnvironmentName(ContentstackResponse response)
+ {
+ var jo = response.OpenJObjectResponse();
+ return jo?["environment"]?["name"]?.ToString();
+ }
+
+ private void SafeDelete(string environmentName)
+ {
+ if (string.IsNullOrEmpty(environmentName))
+ {
+ return;
+ }
+
+ try
+ {
+ _stack.Environment(environmentName).Delete();
+ }
+ catch
+ {
+ // Best-effort cleanup; ignore if already deleted or API error
+ }
+ }
+
+ private static bool EnvironmentsArrayContainsName(JArray environments, string name)
+ {
+ if (environments == null || string.IsNullOrEmpty(name))
+ {
+ return false;
+ }
+
+ return environments.Any(e => e["name"]?.ToString() == name);
+ }
+
+ #region A — Sync happy path
+
+ [TestMethod]
+ public void Test001_Should_Create_Environment_Sync()
+ {
+ TestOutputLogger.LogContext("TestScenario", "Test001_Should_Create_Environment_Sync");
+ string environmentName = null;
+ string name = $"env_sync_create_{Guid.NewGuid():N}";
+ try
+ {
+ var model = BuildModel(name);
+ ContentstackResponse response = _stack.Environment().Create(model);
+
+ AssertLogger.IsTrue(response.IsSuccessStatusCode, "Create environment should succeed", "CreateSyncSuccess");
+ environmentName = ParseEnvironmentName(response);
+ AssertLogger.IsNotNull(environmentName, "environment name");
+ AssertLogger.AreEqual(name, environmentName, "Parsed name should match request", "ParsedEnvironmentName");
+
+ var jo = response.OpenJObjectResponse();
+ AssertLogger.AreEqual(name, jo["environment"]?["name"]?.ToString(), "Response name should match", "EnvironmentName");
+ }
+ finally
+ {
+ SafeDelete(environmentName ?? name);
+ }
+ }
+
+ [TestMethod]
+ public void Test002_Should_Fetch_Environment_Sync()
+ {
+ TestOutputLogger.LogContext("TestScenario", "Test002_Should_Fetch_Environment_Sync");
+ string environmentName = null;
+ string name = $"env_sync_fetch_{Guid.NewGuid():N}";
+ try
+ {
+ ContentstackResponse createResponse = _stack.Environment().Create(BuildModel(name));
+ AssertLogger.IsTrue(createResponse.IsSuccessStatusCode, "Create should succeed", "CreateForFetch");
+ environmentName = ParseEnvironmentName(createResponse);
+ AssertLogger.IsNotNull(environmentName, "name after create");
+ AssertLogger.AreEqual(name, environmentName, "Parsed name should match create request", "CreateNameMatch");
+
+ string expectedUid = createResponse.OpenJObjectResponse()?["environment"]?["uid"]?.ToString();
+
+ ContentstackResponse fetchResponse = _stack.Environment(name).Fetch();
+ AssertLogger.IsTrue(fetchResponse.IsSuccessStatusCode, "Fetch should succeed", "FetchSyncSuccess");
+
+ var env = fetchResponse.OpenJObjectResponse()?["environment"];
+ AssertLogger.AreEqual(name, env?["name"]?.ToString(), "Fetched name should match", "FetchedName");
+ AssertLogger.AreEqual(expectedUid, env?["uid"]?.ToString(), "Fetched uid should match create response", "FetchedUid");
+ }
+ finally
+ {
+ SafeDelete(environmentName ?? name);
+ }
+ }
+
+ [TestMethod]
+ public void Test003_Should_Query_Environments_Sync()
+ {
+ TestOutputLogger.LogContext("TestScenario", "Test003_Should_Query_Environments_Sync");
+ string environmentName = null;
+ string name = $"env_sync_query_{Guid.NewGuid():N}";
+ try
+ {
+ ContentstackResponse createResponse = _stack.Environment().Create(BuildModel(name));
+ AssertLogger.IsTrue(createResponse.IsSuccessStatusCode, "Create should succeed", "CreateForQuery");
+ environmentName = ParseEnvironmentName(createResponse);
+ AssertLogger.IsNotNull(environmentName, "name after create");
+
+ ContentstackResponse queryResponse = _stack.Environment().Query().Find();
+ AssertLogger.IsTrue(queryResponse.IsSuccessStatusCode, "Query Find should succeed", "QueryFindSuccess");
+
+ var environments = queryResponse.OpenJObjectResponse()?["environments"] as JArray;
+ AssertLogger.IsNotNull(environments, "environments array");
+ AssertLogger.IsTrue(
+ EnvironmentsArrayContainsName(environments, name),
+ "Query result should contain created environment name",
+ "ContainsName");
+ }
+ finally
+ {
+ SafeDelete(environmentName ?? name);
+ }
+ }
+
+ [TestMethod]
+ public void Test004_Should_Update_Environment_Sync()
+ {
+ TestOutputLogger.LogContext("TestScenario", "Test004_Should_Update_Environment_Sync");
+ string environmentNameForCleanup = null;
+ string originalName = $"env_sync_update_{Guid.NewGuid():N}";
+ string updatedName = $"{originalName}_updated";
+ try
+ {
+ ContentstackResponse createResponse = _stack.Environment().Create(BuildModel(originalName));
+ AssertLogger.IsTrue(createResponse.IsSuccessStatusCode, "Create should succeed", "CreateForUpdate");
+ string createdName = ParseEnvironmentName(createResponse);
+ AssertLogger.IsNotNull(createdName, "name after create");
+ AssertLogger.AreEqual(originalName, createdName, "Parsed name should match create request", "CreateNameMatch");
+
+ var updateModel = BuildModel(updatedName);
+ ContentstackResponse updateResponse = _stack.Environment(originalName).Update(updateModel);
+ AssertLogger.IsTrue(updateResponse.IsSuccessStatusCode, "Update should succeed", "UpdateSyncSuccess");
+
+ environmentNameForCleanup = updatedName;
+
+ ContentstackResponse fetchResponse = _stack.Environment(updatedName).Fetch();
+ AssertLogger.IsTrue(fetchResponse.IsSuccessStatusCode, "Fetch after update should succeed", "FetchAfterUpdate");
+ var env = fetchResponse.OpenJObjectResponse()?["environment"];
+ AssertLogger.AreEqual(updatedName, env?["name"]?.ToString(), "Name should reflect update", "UpdatedName");
+ }
+ finally
+ {
+ SafeDelete(environmentNameForCleanup ?? originalName);
+ }
+ }
+
+ [TestMethod]
+ public void Test005_Should_Delete_Environment_Sync()
+ {
+ TestOutputLogger.LogContext("TestScenario", "Test005_Should_Delete_Environment_Sync");
+ string environmentName = null;
+ string name = $"env_sync_delete_{Guid.NewGuid():N}";
+ try
+ {
+ ContentstackResponse createResponse = _stack.Environment().Create(BuildModel(name));
+ AssertLogger.IsTrue(createResponse.IsSuccessStatusCode, "Create should succeed", "CreateForDelete");
+ environmentName = ParseEnvironmentName(createResponse);
+ AssertLogger.IsNotNull(environmentName, "name after create");
+
+ ContentstackResponse deleteResponse = _stack.Environment(name).Delete();
+ AssertLogger.IsTrue(deleteResponse.IsSuccessStatusCode, "Delete should succeed", "DeleteSyncSuccess");
+
+ AssertLogger.ThrowsContentstackError(
+ () => _stack.Environment(name).Fetch(),
+ "FetchAfterDelete",
+ HttpStatusCode.NotFound,
+ (HttpStatusCode)422);
+
+ environmentName = null;
+ }
+ finally
+ {
+ SafeDelete(environmentName ?? name);
+ }
+ }
+
+ #endregion
+
+ #region B — Async happy path
+
+ [TestMethod]
+ public async Task Test006_Should_Create_Environment_Async()
+ {
+ TestOutputLogger.LogContext("TestScenario", "Test006_Should_Create_Environment_Async");
+ string environmentName = null;
+ string name = $"env_async_create_{Guid.NewGuid():N}";
+ try
+ {
+ var model = BuildModel(name);
+ ContentstackResponse response = await _stack.Environment().CreateAsync(model);
+
+ AssertLogger.IsTrue(response.IsSuccessStatusCode, "CreateAsync should succeed", "CreateAsyncSuccess");
+ environmentName = ParseEnvironmentName(response);
+ AssertLogger.IsNotNull(environmentName, "environment name");
+ AssertLogger.AreEqual(name, environmentName, "Parsed name should match request", "ParsedEnvironmentName");
+
+ var jo = response.OpenJObjectResponse();
+ AssertLogger.AreEqual(name, jo["environment"]?["name"]?.ToString(), "Response name should match", "EnvironmentName");
+ }
+ finally
+ {
+ SafeDelete(environmentName ?? name);
+ }
+ }
+
+ [TestMethod]
+ public async Task Test007_Should_Fetch_Environment_Async()
+ {
+ TestOutputLogger.LogContext("TestScenario", "Test007_Should_Fetch_Environment_Async");
+ string environmentName = null;
+ string name = $"env_async_fetch_{Guid.NewGuid():N}";
+ try
+ {
+ ContentstackResponse createResponse = await _stack.Environment().CreateAsync(BuildModel(name));
+ AssertLogger.IsTrue(createResponse.IsSuccessStatusCode, "Create should succeed", "CreateForFetchAsync");
+ environmentName = ParseEnvironmentName(createResponse);
+ AssertLogger.IsNotNull(environmentName, "name after create");
+ AssertLogger.AreEqual(name, environmentName, "Parsed name should match create request", "CreateNameMatch");
+
+ string expectedUid = createResponse.OpenJObjectResponse()?["environment"]?["uid"]?.ToString();
+
+ ContentstackResponse fetchResponse = await _stack.Environment(name).FetchAsync();
+ AssertLogger.IsTrue(fetchResponse.IsSuccessStatusCode, "FetchAsync should succeed", "FetchAsyncSuccess");
+
+ var env = fetchResponse.OpenJObjectResponse()?["environment"];
+ AssertLogger.AreEqual(name, env?["name"]?.ToString(), "Fetched name should match", "FetchedName");
+ AssertLogger.AreEqual(expectedUid, env?["uid"]?.ToString(), "Fetched uid should match create response", "FetchedUid");
+ }
+ finally
+ {
+ SafeDelete(environmentName ?? name);
+ }
+ }
+
+ [TestMethod]
+ public async Task Test008_Should_Query_Environments_Async()
+ {
+ TestOutputLogger.LogContext("TestScenario", "Test008_Should_Query_Environments_Async");
+ string environmentName = null;
+ string name = $"env_async_query_{Guid.NewGuid():N}";
+ try
+ {
+ ContentstackResponse createResponse = await _stack.Environment().CreateAsync(BuildModel(name));
+ AssertLogger.IsTrue(createResponse.IsSuccessStatusCode, "Create should succeed", "CreateForQueryAsync");
+ environmentName = ParseEnvironmentName(createResponse);
+ AssertLogger.IsNotNull(environmentName, "name after create");
+
+ ContentstackResponse queryResponse = await _stack.Environment().Query().FindAsync();
+ AssertLogger.IsTrue(queryResponse.IsSuccessStatusCode, "Query FindAsync should succeed", "QueryFindAsyncSuccess");
+
+ var environments = queryResponse.OpenJObjectResponse()?["environments"] as JArray;
+ AssertLogger.IsNotNull(environments, "environments array");
+ AssertLogger.IsTrue(
+ EnvironmentsArrayContainsName(environments, name),
+ "Query result should contain created environment name",
+ "ContainsName");
+ }
+ finally
+ {
+ SafeDelete(environmentName ?? name);
+ }
+ }
+
+ [TestMethod]
+ public async Task Test009_Should_Update_Environment_Async()
+ {
+ TestOutputLogger.LogContext("TestScenario", "Test009_Should_Update_Environment_Async");
+ string environmentNameForCleanup = null;
+ string originalName = $"env_async_update_{Guid.NewGuid():N}";
+ string updatedName = $"{originalName}_updated";
+ try
+ {
+ ContentstackResponse createResponse = await _stack.Environment().CreateAsync(BuildModel(originalName));
+ AssertLogger.IsTrue(createResponse.IsSuccessStatusCode, "Create should succeed", "CreateForUpdateAsync");
+ string createdName = ParseEnvironmentName(createResponse);
+ AssertLogger.IsNotNull(createdName, "name after create");
+ AssertLogger.AreEqual(originalName, createdName, "Parsed name should match create request", "CreateNameMatch");
+
+ var updateModel = BuildModel(updatedName);
+ ContentstackResponse updateResponse = await _stack.Environment(originalName).UpdateAsync(updateModel);
+ AssertLogger.IsTrue(updateResponse.IsSuccessStatusCode, "UpdateAsync should succeed", "UpdateAsyncSuccess");
+
+ environmentNameForCleanup = updatedName;
+
+ ContentstackResponse fetchResponse = await _stack.Environment(updatedName).FetchAsync();
+ AssertLogger.IsTrue(fetchResponse.IsSuccessStatusCode, "FetchAsync after update should succeed", "FetchAsyncAfterUpdate");
+ var env = fetchResponse.OpenJObjectResponse()?["environment"];
+ AssertLogger.AreEqual(updatedName, env?["name"]?.ToString(), "Name should reflect update", "UpdatedName");
+ }
+ finally
+ {
+ SafeDelete(environmentNameForCleanup ?? originalName);
+ }
+ }
+
+ [TestMethod]
+ public async Task Test010_Should_Delete_Environment_Async()
+ {
+ TestOutputLogger.LogContext("TestScenario", "Test010_Should_Delete_Environment_Async");
+ string environmentName = null;
+ string name = $"env_async_delete_{Guid.NewGuid():N}";
+ try
+ {
+ ContentstackResponse createResponse = await _stack.Environment().CreateAsync(BuildModel(name));
+ AssertLogger.IsTrue(createResponse.IsSuccessStatusCode, "Create should succeed", "CreateForDeleteAsync");
+ environmentName = ParseEnvironmentName(createResponse);
+ AssertLogger.IsNotNull(environmentName, "name after create");
+
+ ContentstackResponse deleteResponse = await _stack.Environment(name).DeleteAsync();
+ AssertLogger.IsTrue(deleteResponse.IsSuccessStatusCode, "DeleteAsync should succeed", "DeleteAsyncSuccess");
+
+ await AssertLogger.ThrowsContentstackErrorAsync(
+ async () => await _stack.Environment(name).FetchAsync(),
+ "FetchAsyncAfterDelete",
+ HttpStatusCode.NotFound,
+ (HttpStatusCode)422);
+
+ environmentName = null;
+ }
+ finally
+ {
+ SafeDelete(environmentName ?? name);
+ }
+ }
+
+ #endregion
+
+ #region C — Sync negative path
+
+ [TestMethod]
+ public void Test011_Should_Fail_Fetch_NonExistent_Environment_Sync()
+ {
+ TestOutputLogger.LogContext("TestScenario", "Test011_Should_Fail_Fetch_NonExistent_Environment_Sync");
+ AssertLogger.ThrowsContentstackError(
+ () => _stack.Environment(NonExistentEnvironmentName).Fetch(),
+ "FetchNonExistentSync",
+ HttpStatusCode.NotFound,
+ (HttpStatusCode)422);
+ }
+
+ [TestMethod]
+ public void Test012_Should_Fail_Update_NonExistent_Environment_Sync()
+ {
+ TestOutputLogger.LogContext("TestScenario", "Test012_Should_Fail_Update_NonExistent_Environment_Sync");
+ var model = BuildModel($"env_nonexistent_update_{Guid.NewGuid():N}");
+ AssertLogger.ThrowsContentstackError(
+ () => _stack.Environment(NonExistentEnvironmentName).Update(model),
+ "UpdateNonExistentSync",
+ HttpStatusCode.NotFound,
+ (HttpStatusCode)422);
+ }
+
+ [TestMethod]
+ public void Test013_Should_Fail_Delete_NonExistent_Environment_Sync()
+ {
+ TestOutputLogger.LogContext("TestScenario", "Test013_Should_Fail_Delete_NonExistent_Environment_Sync");
+ AssertLogger.ThrowsContentstackError(
+ () => _stack.Environment(NonExistentEnvironmentName).Delete(),
+ "DeleteNonExistentSync",
+ HttpStatusCode.NotFound,
+ (HttpStatusCode)422);
+ }
+
+ #endregion
+
+ #region D — Async negative path
+
+ [TestMethod]
+ public async Task Test014_Should_Fail_Fetch_NonExistent_Environment_Async()
+ {
+ TestOutputLogger.LogContext("TestScenario", "Test014_Should_Fail_Fetch_NonExistent_Environment_Async");
+ await AssertLogger.ThrowsContentstackErrorAsync(
+ async () => await _stack.Environment(NonExistentEnvironmentName).FetchAsync(),
+ "FetchNonExistentAsync",
+ HttpStatusCode.NotFound,
+ (HttpStatusCode)422);
+ }
+
+ [TestMethod]
+ public async Task Test015_Should_Fail_Update_NonExistent_Environment_Async()
+ {
+ TestOutputLogger.LogContext("TestScenario", "Test015_Should_Fail_Update_NonExistent_Environment_Async");
+ var model = BuildModel($"env_nonexistent_update_async_{Guid.NewGuid():N}");
+ await AssertLogger.ThrowsContentstackErrorAsync(
+ async () => await _stack.Environment(NonExistentEnvironmentName).UpdateAsync(model),
+ "UpdateNonExistentAsync",
+ HttpStatusCode.NotFound,
+ (HttpStatusCode)422);
+ }
+
+ [TestMethod]
+ public async Task Test016_Should_Fail_Delete_NonExistent_Environment_Async()
+ {
+ TestOutputLogger.LogContext("TestScenario", "Test016_Should_Fail_Delete_NonExistent_Environment_Async");
+ await AssertLogger.ThrowsContentstackErrorAsync(
+ async () => await _stack.Environment(NonExistentEnvironmentName).DeleteAsync(),
+ "DeleteNonExistentAsync",
+ HttpStatusCode.NotFound,
+ (HttpStatusCode)422);
+ }
+
+ #endregion
+ }
+}
diff --git a/Contentstack.Management.Core.Tests/IntegrationTest/Contentstack019_RoleTest.cs b/Contentstack.Management.Core.Tests/IntegrationTest/Contentstack019_RoleTest.cs
new file mode 100644
index 0000000..816ae7b
--- /dev/null
+++ b/Contentstack.Management.Core.Tests/IntegrationTest/Contentstack019_RoleTest.cs
@@ -0,0 +1,459 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+using Contentstack.Management.Core.Models;
+using Contentstack.Management.Core.Tests.Helpers;
+using Contentstack.Management.Core.Tests.Model;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Newtonsoft.Json.Linq;
+
+namespace Contentstack.Management.Core.Tests.IntegrationTest
+{
+ [TestClass]
+ [DoNotParallelize]
+ public class Contentstack019_RoleTest
+ {
+ ///
+ /// UID that should not exist on any stack (for negative-path tests).
+ ///
+ private const string NonExistentRoleUid = "blt0000000000000000";
+
+ private static ContentstackClient _client;
+ private Stack _stack;
+
+ [ClassInitialize]
+ public static void ClassInitialize(TestContext context)
+ {
+ _client = Contentstack.CreateAuthenticatedClient();
+ }
+
+ [ClassCleanup]
+ public static void ClassCleanup()
+ {
+ try { _client?.Logout(); } catch { }
+ _client = null;
+ }
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ StackResponse response = StackResponse.getStack(_client.serializer);
+ _stack = _client.Stack(response.Stack.APIKey);
+ }
+
+ ///
+ /// Minimal role payload: branch rule on default branch "main".
+ ///
+ private static RoleModel BuildMinimalRoleModel(string uniqueName)
+ {
+ return new RoleModel
+ {
+ Name = uniqueName,
+ Description = "Integration test role",
+ DeployContent = true,
+ Rules = new List
+ {
+ new BranchRules
+ {
+ Branches = new List { "main" }
+ }
+ }
+ };
+ }
+
+ private static string ParseRoleUid(ContentstackResponse response)
+ {
+ var jo = response.OpenJObjectResponse();
+ return jo?["role"]?["uid"]?.ToString();
+ }
+
+ private void SafeDelete(string roleUid)
+ {
+ if (string.IsNullOrEmpty(roleUid))
+ {
+ return;
+ }
+
+ try
+ {
+ _stack.Role(roleUid).Delete();
+ }
+ catch
+ {
+ // Best-effort cleanup; ignore if already deleted or API error
+ }
+ }
+
+ private static bool RolesArrayContainsUid(JArray roles, string uid)
+ {
+ if (roles == null || string.IsNullOrEmpty(uid))
+ {
+ return false;
+ }
+
+ return roles.Any(r => r["uid"]?.ToString() == uid);
+ }
+
+ #region A — Sync happy path
+
+ [TestMethod]
+ public void Test001_Should_Create_Role_Sync()
+ {
+ TestOutputLogger.LogContext("TestScenario", "Test001_Should_Create_Role_Sync");
+ string roleUid = null;
+ string name = $"role_sync_create_{Guid.NewGuid():N}";
+ try
+ {
+ var model = BuildMinimalRoleModel(name);
+ ContentstackResponse response = _stack.Role().Create(model);
+
+ AssertLogger.IsTrue(response.IsSuccessStatusCode, "Create role should succeed", "CreateSyncSuccess");
+ roleUid = ParseRoleUid(response);
+ AssertLogger.IsNotNull(roleUid, "role uid");
+
+ var jo = response.OpenJObjectResponse();
+ AssertLogger.AreEqual(name, jo["role"]?["name"]?.ToString(), "Response name should match", "RoleName");
+ }
+ finally
+ {
+ SafeDelete(roleUid);
+ }
+ }
+
+ [TestMethod]
+ public void Test002_Should_Fetch_Role_Sync()
+ {
+ TestOutputLogger.LogContext("TestScenario", "Test002_Should_Fetch_Role_Sync");
+ string roleUid = null;
+ string name = $"role_sync_fetch_{Guid.NewGuid():N}";
+ try
+ {
+ ContentstackResponse createResponse = _stack.Role().Create(BuildMinimalRoleModel(name));
+ AssertLogger.IsTrue(createResponse.IsSuccessStatusCode, "Create should succeed", "CreateForFetch");
+ roleUid = ParseRoleUid(createResponse);
+ AssertLogger.IsNotNull(roleUid, "uid after create");
+
+ ContentstackResponse fetchResponse = _stack.Role(roleUid).Fetch();
+ AssertLogger.IsTrue(fetchResponse.IsSuccessStatusCode, "Fetch should succeed", "FetchSyncSuccess");
+
+ var role = fetchResponse.OpenJObjectResponse()?["role"];
+ AssertLogger.AreEqual(name, role?["name"]?.ToString(), "Fetched name should match", "FetchedName");
+ AssertLogger.AreEqual(roleUid, role?["uid"]?.ToString(), "Fetched uid should match", "FetchedUid");
+ }
+ finally
+ {
+ SafeDelete(roleUid);
+ }
+ }
+
+ [TestMethod]
+ public void Test003_Should_Query_Roles_Sync()
+ {
+ TestOutputLogger.LogContext("TestScenario", "Test003_Should_Query_Roles_Sync");
+ string roleUid = null;
+ string name = $"role_sync_query_{Guid.NewGuid():N}";
+ try
+ {
+ ContentstackResponse createResponse = _stack.Role().Create(BuildMinimalRoleModel(name));
+ AssertLogger.IsTrue(createResponse.IsSuccessStatusCode, "Create should succeed", "CreateForQuery");
+ roleUid = ParseRoleUid(createResponse);
+ AssertLogger.IsNotNull(roleUid, "uid after create");
+
+ ContentstackResponse queryResponse = _stack.Role().Query().Find();
+ AssertLogger.IsTrue(queryResponse.IsSuccessStatusCode, "Query Find should succeed", "QueryFindSuccess");
+
+ var roles = queryResponse.OpenJObjectResponse()?["roles"] as JArray;
+ AssertLogger.IsNotNull(roles, "roles array");
+ AssertLogger.IsTrue(
+ RolesArrayContainsUid(roles, roleUid),
+ "Query result should contain created role uid",
+ "ContainsUid");
+ }
+ finally
+ {
+ SafeDelete(roleUid);
+ }
+ }
+
+ [TestMethod]
+ public void Test004_Should_Update_Role_Sync()
+ {
+ TestOutputLogger.LogContext("TestScenario", "Test004_Should_Update_Role_Sync");
+ string roleUid = null;
+ string originalName = $"role_sync_update_{Guid.NewGuid():N}";
+ string updatedName = $"{originalName}_updated";
+ try
+ {
+ ContentstackResponse createResponse = _stack.Role().Create(BuildMinimalRoleModel(originalName));
+ AssertLogger.IsTrue(createResponse.IsSuccessStatusCode, "Create should succeed", "CreateForUpdate");
+ roleUid = ParseRoleUid(createResponse);
+ AssertLogger.IsNotNull(roleUid, "uid after create");
+
+ var updateModel = BuildMinimalRoleModel(updatedName);
+ ContentstackResponse updateResponse = _stack.Role(roleUid).Update(updateModel);
+ AssertLogger.IsTrue(updateResponse.IsSuccessStatusCode, "Update should succeed", "UpdateSyncSuccess");
+
+ ContentstackResponse fetchResponse = _stack.Role(roleUid).Fetch();
+ AssertLogger.IsTrue(fetchResponse.IsSuccessStatusCode, "Fetch after update should succeed", "FetchAfterUpdate");
+ var role = fetchResponse.OpenJObjectResponse()?["role"];
+ AssertLogger.AreEqual(updatedName, role?["name"]?.ToString(), "Name should reflect update", "UpdatedName");
+ }
+ finally
+ {
+ SafeDelete(roleUid);
+ }
+ }
+
+ [TestMethod]
+ public void Test005_Should_Delete_Role_Sync()
+ {
+ TestOutputLogger.LogContext("TestScenario", "Test005_Should_Delete_Role_Sync");
+ string roleUid = null;
+ string name = $"role_sync_delete_{Guid.NewGuid():N}";
+ try
+ {
+ ContentstackResponse createResponse = _stack.Role().Create(BuildMinimalRoleModel(name));
+ AssertLogger.IsTrue(createResponse.IsSuccessStatusCode, "Create should succeed", "CreateForDelete");
+ roleUid = ParseRoleUid(createResponse);
+ AssertLogger.IsNotNull(roleUid, "uid after create");
+
+ ContentstackResponse deleteResponse = _stack.Role(roleUid).Delete();
+ AssertLogger.IsTrue(deleteResponse.IsSuccessStatusCode, "Delete should succeed", "DeleteSyncSuccess");
+
+ AssertLogger.ThrowsContentstackError(
+ () => _stack.Role(roleUid).Fetch(),
+ "FetchAfterDelete",
+ HttpStatusCode.NotFound,
+ (HttpStatusCode)422);
+
+ roleUid = null;
+ }
+ finally
+ {
+ SafeDelete(roleUid);
+ }
+ }
+
+ #endregion
+
+ #region B — Async happy path
+
+ [TestMethod]
+ public async Task Test006_Should_Create_Role_Async()
+ {
+ TestOutputLogger.LogContext("TestScenario", "Test006_Should_Create_Role_Async");
+ string roleUid = null;
+ string name = $"role_async_create_{Guid.NewGuid():N}";
+ try
+ {
+ var model = BuildMinimalRoleModel(name);
+ ContentstackResponse response = await _stack.Role().CreateAsync(model);
+
+ AssertLogger.IsTrue(response.IsSuccessStatusCode, "CreateAsync should succeed", "CreateAsyncSuccess");
+ roleUid = ParseRoleUid(response);
+ AssertLogger.IsNotNull(roleUid, "role uid");
+
+ var jo = response.OpenJObjectResponse();
+ AssertLogger.AreEqual(name, jo["role"]?["name"]?.ToString(), "Response name should match", "RoleName");
+ }
+ finally
+ {
+ SafeDelete(roleUid);
+ }
+ }
+
+ [TestMethod]
+ public async Task Test007_Should_Fetch_Role_Async()
+ {
+ TestOutputLogger.LogContext("TestScenario", "Test007_Should_Fetch_Role_Async");
+ string roleUid = null;
+ string name = $"role_async_fetch_{Guid.NewGuid():N}";
+ try
+ {
+ ContentstackResponse createResponse = await _stack.Role().CreateAsync(BuildMinimalRoleModel(name));
+ AssertLogger.IsTrue(createResponse.IsSuccessStatusCode, "Create should succeed", "CreateForFetchAsync");
+ roleUid = ParseRoleUid(createResponse);
+ AssertLogger.IsNotNull(roleUid, "uid after create");
+
+ ContentstackResponse fetchResponse = await _stack.Role(roleUid).FetchAsync();
+ AssertLogger.IsTrue(fetchResponse.IsSuccessStatusCode, "FetchAsync should succeed", "FetchAsyncSuccess");
+
+ var role = fetchResponse.OpenJObjectResponse()?["role"];
+ AssertLogger.AreEqual(name, role?["name"]?.ToString(), "Fetched name should match", "FetchedName");
+ AssertLogger.AreEqual(roleUid, role?["uid"]?.ToString(), "Fetched uid should match", "FetchedUid");
+ }
+ finally
+ {
+ SafeDelete(roleUid);
+ }
+ }
+
+ [TestMethod]
+ public async Task Test008_Should_Query_Roles_Async()
+ {
+ TestOutputLogger.LogContext("TestScenario", "Test008_Should_Query_Roles_Async");
+ string roleUid = null;
+ string name = $"role_async_query_{Guid.NewGuid():N}";
+ try
+ {
+ ContentstackResponse createResponse = await _stack.Role().CreateAsync(BuildMinimalRoleModel(name));
+ AssertLogger.IsTrue(createResponse.IsSuccessStatusCode, "Create should succeed", "CreateForQueryAsync");
+ roleUid = ParseRoleUid(createResponse);
+ AssertLogger.IsNotNull(roleUid, "uid after create");
+
+ ContentstackResponse queryResponse = await _stack.Role().Query().FindAsync();
+ AssertLogger.IsTrue(queryResponse.IsSuccessStatusCode, "Query FindAsync should succeed", "QueryFindAsyncSuccess");
+
+ var roles = queryResponse.OpenJObjectResponse()?["roles"] as JArray;
+ AssertLogger.IsNotNull(roles, "roles array");
+ AssertLogger.IsTrue(
+ RolesArrayContainsUid(roles, roleUid),
+ "Query result should contain created role uid",
+ "ContainsUid");
+ }
+ finally
+ {
+ SafeDelete(roleUid);
+ }
+ }
+
+ [TestMethod]
+ public async Task Test009_Should_Update_Role_Async()
+ {
+ TestOutputLogger.LogContext("TestScenario", "Test009_Should_Update_Role_Async");
+ string roleUid = null;
+ string originalName = $"role_async_update_{Guid.NewGuid():N}";
+ string updatedName = $"{originalName}_updated";
+ try
+ {
+ ContentstackResponse createResponse = await _stack.Role().CreateAsync(BuildMinimalRoleModel(originalName));
+ AssertLogger.IsTrue(createResponse.IsSuccessStatusCode, "Create should succeed", "CreateForUpdateAsync");
+ roleUid = ParseRoleUid(createResponse);
+ AssertLogger.IsNotNull(roleUid, "uid after create");
+
+ var updateModel = BuildMinimalRoleModel(updatedName);
+ ContentstackResponse updateResponse = await _stack.Role(roleUid).UpdateAsync(updateModel);
+ AssertLogger.IsTrue(updateResponse.IsSuccessStatusCode, "UpdateAsync should succeed", "UpdateAsyncSuccess");
+
+ ContentstackResponse fetchResponse = await _stack.Role(roleUid).FetchAsync();
+ AssertLogger.IsTrue(fetchResponse.IsSuccessStatusCode, "FetchAsync after update should succeed", "FetchAsyncAfterUpdate");
+ var role = fetchResponse.OpenJObjectResponse()?["role"];
+ AssertLogger.AreEqual(updatedName, role?["name"]?.ToString(), "Name should reflect update", "UpdatedName");
+ }
+ finally
+ {
+ SafeDelete(roleUid);
+ }
+ }
+
+ [TestMethod]
+ public async Task Test010_Should_Delete_Role_Async()
+ {
+ TestOutputLogger.LogContext("TestScenario", "Test010_Should_Delete_Role_Async");
+ string roleUid = null;
+ string name = $"role_async_delete_{Guid.NewGuid():N}";
+ try
+ {
+ ContentstackResponse createResponse = await _stack.Role().CreateAsync(BuildMinimalRoleModel(name));
+ AssertLogger.IsTrue(createResponse.IsSuccessStatusCode, "Create should succeed", "CreateForDeleteAsync");
+ roleUid = ParseRoleUid(createResponse);
+ AssertLogger.IsNotNull(roleUid, "uid after create");
+
+ ContentstackResponse deleteResponse = await _stack.Role(roleUid).DeleteAsync();
+ AssertLogger.IsTrue(deleteResponse.IsSuccessStatusCode, "DeleteAsync should succeed", "DeleteAsyncSuccess");
+
+ await AssertLogger.ThrowsContentstackErrorAsync(
+ async () => await _stack.Role(roleUid).FetchAsync(),
+ "FetchAsyncAfterDelete",
+ HttpStatusCode.NotFound,
+ (HttpStatusCode)422);
+
+ roleUid = null;
+ }
+ finally
+ {
+ SafeDelete(roleUid);
+ }
+ }
+
+ #endregion
+
+ #region C — Sync negative path
+
+ [TestMethod]
+ public void Test011_Should_Fail_Fetch_NonExistent_Role_Sync()
+ {
+ TestOutputLogger.LogContext("TestScenario", "Test011_Should_Fail_Fetch_NonExistent_Role_Sync");
+ AssertLogger.ThrowsContentstackError(
+ () => _stack.Role(NonExistentRoleUid).Fetch(),
+ "FetchNonExistentSync",
+ HttpStatusCode.NotFound,
+ (HttpStatusCode)422);
+ }
+
+ [TestMethod]
+ public void Test012_Should_Fail_Update_NonExistent_Role_Sync()
+ {
+ TestOutputLogger.LogContext("TestScenario", "Test012_Should_Fail_Update_NonExistent_Role_Sync");
+ var model = BuildMinimalRoleModel($"role_nonexistent_update_{Guid.NewGuid():N}");
+ AssertLogger.ThrowsContentstackError(
+ () => _stack.Role(NonExistentRoleUid).Update(model),
+ "UpdateNonExistentSync",
+ HttpStatusCode.NotFound,
+ (HttpStatusCode)422);
+ }
+
+ [TestMethod]
+ public void Test013_Should_Fail_Delete_NonExistent_Role_Sync()
+ {
+ TestOutputLogger.LogContext("TestScenario", "Test013_Should_Fail_Delete_NonExistent_Role_Sync");
+ AssertLogger.ThrowsContentstackError(
+ () => _stack.Role(NonExistentRoleUid).Delete(),
+ "DeleteNonExistentSync",
+ HttpStatusCode.NotFound,
+ (HttpStatusCode)422);
+ }
+
+ #endregion
+
+ #region D — Async negative path
+
+ [TestMethod]
+ public async Task Test014_Should_Fail_Fetch_NonExistent_Role_Async()
+ {
+ TestOutputLogger.LogContext("TestScenario", "Test014_Should_Fail_Fetch_NonExistent_Role_Async");
+ await AssertLogger.ThrowsContentstackErrorAsync(
+ async () => await _stack.Role(NonExistentRoleUid).FetchAsync(),
+ "FetchNonExistentAsync",
+ HttpStatusCode.NotFound,
+ (HttpStatusCode)422);
+ }
+
+ [TestMethod]
+ public async Task Test015_Should_Fail_Update_NonExistent_Role_Async()
+ {
+ TestOutputLogger.LogContext("TestScenario", "Test015_Should_Fail_Update_NonExistent_Role_Async");
+ var model = BuildMinimalRoleModel($"role_nonexistent_update_async_{Guid.NewGuid():N}");
+ await AssertLogger.ThrowsContentstackErrorAsync(
+ async () => await _stack.Role(NonExistentRoleUid).UpdateAsync(model),
+ "UpdateNonExistentAsync",
+ HttpStatusCode.NotFound,
+ (HttpStatusCode)422);
+ }
+
+ [TestMethod]
+ public async Task Test016_Should_Fail_Delete_NonExistent_Role_Async()
+ {
+ TestOutputLogger.LogContext("TestScenario", "Test016_Should_Fail_Delete_NonExistent_Role_Async");
+ await AssertLogger.ThrowsContentstackErrorAsync(
+ async () => await _stack.Role(NonExistentRoleUid).DeleteAsync(),
+ "DeleteNonExistentAsync",
+ HttpStatusCode.NotFound,
+ (HttpStatusCode)422);
+ }
+
+ #endregion
+ }
+}
diff --git a/Contentstack.Management.Core.Tests/IntegrationTest/Contentstack021_EntryVariantTest.cs b/Contentstack.Management.Core.Tests/IntegrationTest/Contentstack021_EntryVariantTest.cs
new file mode 100644
index 0000000..fcd5992
--- /dev/null
+++ b/Contentstack.Management.Core.Tests/IntegrationTest/Contentstack021_EntryVariantTest.cs
@@ -0,0 +1,505 @@
+using System;
+using System.Collections.Generic;
+using Contentstack.Management.Core.Models;
+using Contentstack.Management.Core.Models.Fields;
+using Contentstack.Management.Core.Tests.Helpers;
+using Contentstack.Management.Core.Tests.Model;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Contentstack.Management.Core.Abstractions;
+
+namespace Contentstack.Management.Core.Tests.IntegrationTest
+{
+ public class ProductBannerEntry : IEntry
+ {
+ [JsonProperty("title")]
+ public string Title { get; set; }
+
+ [JsonProperty("banner_title")]
+ public string BannerTitle { get; set; }
+
+ [JsonProperty("banner_color")]
+ public string BannerColor { get; set; }
+ }
+
+ [TestClass]
+ public class Contentstack021_EntryVariantTest
+ {
+ private static ContentstackClient _client;
+ private Stack _stack;
+ private string _contentTypeUid = "product_banner";
+ private static string _entryUid;
+ private static string _variantUid;
+ private static string _variantGroupUid;
+
+ [ClassInitialize]
+ public static void ClassInitialize(TestContext context)
+ {
+ _client = Contentstack.CreateAuthenticatedClient();
+ }
+
+ [ClassCleanup]
+ public static void ClassCleanup()
+ {
+ try { _client?.Logout(); } catch { }
+ _client = null;
+ }
+
+ [TestInitialize]
+ public void TestInitialize()
+ {
+ // Read the API key from appSettings.json
+ string apiKey = Contentstack.Config["Contentstack:Stack:api_key"];
+
+ // Optional: Fallback to stackApiKey.txt if it's missing in appSettings.json
+ if (string.IsNullOrEmpty(apiKey))
+ {
+ StackResponse response = StackResponse.getStack(_client.serializer);
+ apiKey = response.Stack.APIKey;
+ }
+
+ _stack = _client.Stack(apiKey);
+ }
+
+ [TestMethod]
+ [DoNotParallelize]
+ public async System.Threading.Tasks.Task Test001_Ensure_Setup_Data()
+ {
+ TestOutputLogger.LogContext("TestScenario", "ProductBannerVariantLifecycle_Setup");
+
+ // 1. Ensure Variant Group exists
+ var collection = new global::Contentstack.Management.Core.Queryable.ParameterCollection();
+ collection.Add("include_variant_info", "true");
+ collection.Add("include_variant_count", "true");
+
+ var vgResponse = await _stack.VariantGroup().FindAsync(collection);
+ Console.WriteLine("Variant Groups Response: " + vgResponse.OpenResponse());
+
+ var vgJObject = vgResponse.OpenJObjectResponse();
+ var groups = vgJObject["variant_groups"] as JArray;
+
+ if (groups == null || groups.Count == 0)
+ {
+ Assert.Inconclusive("No variant groups found in the stack. Create one to run EntryVariant tests. Response was: " + vgResponse.OpenResponse());
+ return;
+ }
+
+ _variantGroupUid = groups[0]["uid"]?.ToString();
+
+ var variantsArray = groups[0]["variants"] as JArray;
+ if (variantsArray != null && variantsArray.Count > 0)
+ {
+ _variantUid = variantsArray[0]["uid"]?.ToString();
+ }
+ else
+ {
+ var variantUids = groups[0]["variant_uids"] as JArray;
+ if (variantUids != null && variantUids.Count > 0)
+ {
+ _variantUid = variantUids[0].ToString();
+ }
+ }
+
+ if (string.IsNullOrEmpty(_variantUid))
+ {
+ // Fallback to demo UIDs if none are returned by the API so the test doesn't skip
+ _variantUid = "cs2082f36d4099af4e";
+ Console.WriteLine("Warning: The variant group had no variants. Using a hardcoded variant UID for testing: " + _variantUid);
+ }
+
+ TestOutputLogger.LogContext("VariantGroup", _variantGroupUid);
+ TestOutputLogger.LogContext("Variant", _variantUid);
+
+ // 2. Ensure Content Type exists
+ ContentstackResponse ctFetchResponse = _stack.ContentType(_contentTypeUid).Fetch();
+ if (!ctFetchResponse.IsSuccessStatusCode)
+ {
+ var contentModelling = new ContentModelling
+ {
+ Title = "Product Banner",
+ Uid = _contentTypeUid,
+ Schema = new List
+ {
+ new TextboxField
+ {
+ DisplayName = "Title",
+ Uid = "title",
+ DataType = "text",
+ Mandatory = true
+ },
+ new TextboxField
+ {
+ DisplayName = "Banner Title",
+ Uid = "banner_title",
+ DataType = "text"
+ },
+ new TextboxField
+ {
+ DisplayName = "Banner Color",
+ Uid = "banner_color",
+ DataType = "text"
+ }
+ }
+ };
+
+ ContentstackResponse createCtResponse = _stack.ContentType().Create(contentModelling);
+ if (!createCtResponse.IsSuccessStatusCode)
+ {
+ Assert.Fail("Failed to create content type: " + createCtResponse.OpenResponse());
+ }
+ }
+
+ // 3. Link Content Type to Variant Group
+ try
+ {
+ var linkResponse = await _stack.VariantGroup(_variantGroupUid).LinkContentTypesAsync(new List { _contentTypeUid });
+ if (!linkResponse.IsSuccessStatusCode)
+ {
+ Console.WriteLine("Warning: LinkContentTypesAsync failed, but continuing as it might already be linked. Error: " + linkResponse.OpenResponse());
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("Warning: LinkContentTypesAsync threw an exception. It might be due to an SDK endpoint bug. Continuing. Exception: " + ex.Message);
+ }
+
+ // 4. Ensure Base Entry exists
+ var queryResp = await _stack.ContentType(_contentTypeUid).Entry().Query().FindAsync();
+ var entriesArray = queryResp.OpenJObjectResponse()["entries"] as JArray;
+
+ if (entriesArray != null && entriesArray.Count > 0)
+ {
+ _entryUid = entriesArray[0]["uid"]?.ToString();
+ }
+ else
+ {
+ var entryData = new ProductBannerEntry
+ {
+ Title = "Test Banner",
+ BannerTitle = "Original Title",
+ BannerColor = "Original Color"
+ };
+
+ var entryResponse = await _stack.ContentType(_contentTypeUid).Entry().CreateAsync(entryData);
+ Assert.IsTrue(entryResponse.IsSuccessStatusCode, "Should create base entry: " + entryResponse.OpenResponse());
+ var entryObj = entryResponse.OpenJObjectResponse()["entry"];
+ _entryUid = entryObj["uid"]?.ToString();
+ }
+
+ Assert.IsNotNull(_entryUid, "Entry UID should not be null");
+
+ TestOutputLogger.LogContext("Entry", _entryUid);
+ }
+
+ [TestMethod]
+ [DoNotParallelize]
+ public async System.Threading.Tasks.Task Test002_Should_Create_Entry_Variant()
+ {
+ if (string.IsNullOrEmpty(_entryUid) || string.IsNullOrEmpty(_variantUid))
+ {
+ Assert.Inconclusive("Setup not completed. Ensure Test001 runs first.");
+ return;
+ }
+
+ TestOutputLogger.LogContext("TestScenario", "ProductBannerVariantLifecycle_Create");
+
+ var variantData = new
+ {
+ banner_color = "Navy Blue",
+ _variant = new
+ {
+ _change_set = new[] { "banner_color" },
+ _order = new string[] { }
+ }
+ };
+
+ var createVariantResponse = await _stack.ContentType(_contentTypeUid).Entry(_entryUid).Variant(_variantUid).CreateAsync(variantData);
+ Assert.IsTrue(createVariantResponse.IsSuccessStatusCode, "Should create entry variant. " + createVariantResponse.OpenResponse());
+ }
+
+ [TestMethod]
+ [DoNotParallelize]
+ public async System.Threading.Tasks.Task Test003_Should_Fetch_Entry_Variants()
+ {
+ if (string.IsNullOrEmpty(_entryUid))
+ {
+ Assert.Inconclusive("Setup not completed. Ensure Test001 runs first.");
+ return;
+ }
+
+ TestOutputLogger.LogContext("TestScenario", "ProductBannerVariantLifecycle_Fetch");
+
+ var fetchVariantsResponse = await _stack.ContentType(_contentTypeUid).Entry(_entryUid).Variant().FindAsync();
+ Assert.IsTrue(fetchVariantsResponse.IsSuccessStatusCode, "Should fetch all variants for entry");
+ }
+
+ [TestMethod]
+ [DoNotParallelize]
+ public async System.Threading.Tasks.Task Test004_Should_Publish_Entry_With_Variants()
+ {
+ if (string.IsNullOrEmpty(_entryUid) || string.IsNullOrEmpty(_variantUid))
+ {
+ Assert.Inconclusive("Setup not completed. Ensure Test001 runs first.");
+ return;
+ }
+
+ TestOutputLogger.LogContext("TestScenario", "ProductBannerVariantLifecycle_Publish");
+
+ var publishDetails = new PublishUnpublishDetails
+ {
+ Locales = new List { "en-us" },
+ Environments = new List { "development" },
+ Variants = new List
+ {
+ new PublishVariant { Uid = _variantUid, Version = 1 }
+ },
+ VariantRules = new PublishVariantRules
+ {
+ PublishLatestBase = true,
+ PublishLatestBaseConditionally = false
+ }
+ };
+
+ try
+ {
+ var publishResponse = await _stack.ContentType(_contentTypeUid).Entry(_entryUid).PublishAsync(publishDetails, "en-us");
+ if (!publishResponse.IsSuccessStatusCode)
+ {
+ Console.WriteLine("Publish failed (often due to missing 'development' environment). Response: " + publishResponse.OpenResponse());
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("Publish threw exception (often due to missing 'development' environment). Continuing. Exception: " + ex.Message);
+ }
+ }
+
+ [TestMethod]
+ [DoNotParallelize]
+ public async System.Threading.Tasks.Task Test005_Should_Delete_Entry_Variant()
+ {
+ if (string.IsNullOrEmpty(_entryUid) || string.IsNullOrEmpty(_variantUid))
+ {
+ Assert.Inconclusive("Setup not completed. Ensure Test001 runs first.");
+ return;
+ }
+
+ TestOutputLogger.LogContext("TestScenario", "ProductBannerVariantLifecycle_Delete");
+
+ var deleteVariantResponse = await _stack.ContentType(_contentTypeUid).Entry(_entryUid).Variant(_variantUid).DeleteAsync();
+ Assert.IsTrue(deleteVariantResponse.IsSuccessStatusCode, "Should delete entry variant");
+ }
+ [TestMethod]
+ [DoNotParallelize]
+ public async System.Threading.Tasks.Task Test006_Should_Fail_To_Create_Variant_For_Invalid_Entry()
+ {
+ if (string.IsNullOrEmpty(_variantUid))
+ {
+ Assert.Inconclusive("Setup not completed. Ensure Test001 runs first.");
+ return;
+ }
+
+ TestOutputLogger.LogContext("TestScenario", "ProductBannerVariantLifecycle_Create_Negative");
+
+ var invalidEntryUid = "blt_invalid_entry_uid";
+ var variantData = new { banner_color = "Navy Blue", _variant = new { _change_set = new[] { "banner_color" } } };
+
+ try
+ {
+ await _stack.ContentType(_contentTypeUid).Entry(invalidEntryUid).Variant(_variantUid).CreateAsync(variantData);
+ Assert.Fail("Creating a variant for an invalid entry should have thrown an exception.");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("Successfully caught expected exception: " + ex.Message);
+ }
+ }
+
+ [TestMethod]
+ [DoNotParallelize]
+ public async System.Threading.Tasks.Task Test007_Should_Fail_To_Fetch_Invalid_Variant()
+ {
+ if (string.IsNullOrEmpty(_entryUid))
+ {
+ Assert.Inconclusive("Setup not completed. Ensure Test001 runs first.");
+ return;
+ }
+
+ TestOutputLogger.LogContext("TestScenario", "ProductBannerVariantLifecycle_Fetch_Negative");
+
+ var invalidVariantUid = "cs_invalid_variant_123";
+
+ try
+ {
+ await _stack.ContentType(_contentTypeUid).Entry(_entryUid).Variant(invalidVariantUid).FetchAsync();
+ Assert.Fail("Fetching an invalid variant should have thrown an exception.");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("Successfully caught expected exception: " + ex.Message);
+ }
+ }
+
+ [TestMethod]
+ [DoNotParallelize]
+ public async System.Threading.Tasks.Task Test008_Should_Fail_To_Delete_Invalid_Variant()
+ {
+ if (string.IsNullOrEmpty(_entryUid))
+ {
+ Assert.Inconclusive("Setup not completed. Ensure Test001 runs first.");
+ return;
+ }
+
+ TestOutputLogger.LogContext("TestScenario", "ProductBannerVariantLifecycle_Delete_Negative");
+
+ var invalidVariantUid = "cs_invalid_variant_123";
+
+ try
+ {
+ await _stack.ContentType(_contentTypeUid).Entry(_entryUid).Variant(invalidVariantUid).DeleteAsync();
+ Assert.Fail("Deleting an invalid variant should have thrown an exception.");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("Successfully caught expected exception: " + ex.Message);
+ }
+ }
+
+ [TestMethod]
+ [DoNotParallelize]
+ public async System.Threading.Tasks.Task Test009_Should_Fail_To_Publish_With_Invalid_Variant()
+ {
+ if (string.IsNullOrEmpty(_entryUid))
+ {
+ Assert.Inconclusive("Setup not completed. Ensure Test001 runs first.");
+ return;
+ }
+
+ TestOutputLogger.LogContext("TestScenario", "ProductBannerVariantLifecycle_Publish_Negative");
+
+ var invalidPublishDetails = new PublishUnpublishDetails
+ {
+ Locales = new List { "en-us" },
+ Environments = new List { "development" },
+ Variants = new List
+ {
+ new PublishVariant { Uid = "cs_invalid_variant_123", Version = 1 }
+ }
+ };
+
+ try
+ {
+ await _stack.ContentType(_contentTypeUid).Entry(_entryUid).PublishAsync(invalidPublishDetails, "en-us");
+ Assert.Fail("Publishing an entry with invalid variant details should have thrown an exception.");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("Successfully caught expected exception: " + ex.Message);
+ }
+ }
+
+ [TestMethod]
+ [DoNotParallelize]
+ public async System.Threading.Tasks.Task Test010_Should_Fail_To_Create_Variant_Without_ChangeSet()
+ {
+ if (string.IsNullOrEmpty(_entryUid) || string.IsNullOrEmpty(_variantUid))
+ {
+ Assert.Inconclusive("Setup not completed. Ensure Test001 runs first.");
+ return;
+ }
+
+ TestOutputLogger.LogContext("TestScenario", "ProductBannerVariantLifecycle_Create_NoChangeSet_Negative");
+
+ var variantDataMissingChangeSet = new
+ {
+ banner_color = "Red",
+ _variant = new
+ {
+ // missing _change_set array which the API requires
+ _order = new string[] { }
+ }
+ };
+
+ try
+ {
+ await _stack.ContentType(_contentTypeUid).Entry(_entryUid).Variant(_variantUid).CreateAsync(variantDataMissingChangeSet);
+ Assert.Fail("Creating an entry variant without _change_set metadata should have thrown an exception.");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("Successfully caught expected exception: " + ex.Message);
+ }
+ }
+
+ [TestMethod]
+ [DoNotParallelize]
+ public async System.Threading.Tasks.Task Test011_Should_Fail_To_Publish_Variant_To_Invalid_Environment()
+ {
+ if (string.IsNullOrEmpty(_entryUid) || string.IsNullOrEmpty(_variantUid))
+ {
+ Assert.Inconclusive("Setup not completed. Ensure Test001 runs first.");
+ return;
+ }
+
+ TestOutputLogger.LogContext("TestScenario", "ProductBannerVariantLifecycle_Publish_Env_Negative");
+
+ var publishDetails = new PublishUnpublishDetails
+ {
+ Locales = new List { "en-us" },
+ Environments = new List { "non_existent_environment_123" },
+ Variants = new List
+ {
+ new PublishVariant { Uid = _variantUid, Version = 1 }
+ }
+ };
+
+ try
+ {
+ await _stack.ContentType(_contentTypeUid).Entry(_entryUid).PublishAsync(publishDetails, "en-us");
+ Assert.Fail("Publishing an entry variant to an invalid environment should have thrown an exception.");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("Successfully caught expected exception: " + ex.Message);
+ }
+ }
+
+ [TestMethod]
+ [DoNotParallelize]
+ public async System.Threading.Tasks.Task Test012_Should_Fail_To_Create_Variant_With_Unlinked_Content_Type()
+ {
+ if (string.IsNullOrEmpty(_variantUid))
+ {
+ Assert.Inconclusive("Setup not completed. Ensure Test001 runs first.");
+ return;
+ }
+
+ TestOutputLogger.LogContext("TestScenario", "ProductBannerVariantLifecycle_Unlinked_CT_Negative");
+
+ var dummyContentTypeUid = "unlinked_dummy_ct";
+
+ // To be thorough, this test usually creates a dummy Content Type, creates an entry in it,
+ // and tries to create a variant when it hasn't been linked to a variant group.
+ // But since creating full schema is tedious, we can assert that trying to use a non-existent
+ // or unlinked dummy content type for variants will be rejected by the API.
+
+ var invalidVariantData = new
+ {
+ title = "Dummy",
+ _variant = new { _change_set = new[] { "title" } }
+ };
+
+ try
+ {
+ // Tries to perform variant creation on a content type that has no variants linked
+ await _stack.ContentType(dummyContentTypeUid).Entry("blt_dummy_entry").Variant(_variantUid).CreateAsync(invalidVariantData);
+ Assert.Fail("Attempting to create variants for an unlinked content type should have thrown an error.");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("Successfully caught expected exception: " + ex.Message);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Contentstack.Management.Core.Unit.Tests/Core/Services/Models/PublishUnpublishServiceTest.cs b/Contentstack.Management.Core.Unit.Tests/Core/Services/Models/PublishUnpublishServiceTest.cs
index 8e4018a..7e4dba7 100644
--- a/Contentstack.Management.Core.Unit.Tests/Core/Services/Models/PublishUnpublishServiceTest.cs
+++ b/Contentstack.Management.Core.Unit.Tests/Core/Services/Models/PublishUnpublishServiceTest.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Text;
using AutoFixture;
@@ -83,6 +83,8 @@ public void Should_Create_Content_Body()
var resourcePath = _fixture.Create();
var fieldName = _fixture.Create();
var details = _fixture.Create();
+ details.Variants = null;
+ details.VariantRules = null;
PublishUnpublishService service = new PublishUnpublishService(
serializer,
new Management.Core.Models.Stack(null, apiKey),
@@ -111,6 +113,8 @@ public void Should_Create_Content_Body_with_Locale()
var resourcePath = _fixture.Create();
var fieldName = _fixture.Create();
var details = _fixture.Create();
+ details.Variants = null;
+ details.VariantRules = null;
var locale = _fixture.Create();
PublishUnpublishService service = new PublishUnpublishService(
serializer,
@@ -178,5 +182,40 @@ public void Should_Create_Blank_Locale_and_Environment_Content_Body()
Assert.AreEqual(resourcePath, service.ResourcePath);
Assert.AreEqual($"{{\"{fieldName}\": {{}}}}", Encoding.Default.GetString(service.ByteContent));
}
+ [TestMethod]
+ public void Should_Create_Content_Body_With_Variants()
+ {
+ var apiKey = "api_key";
+ var resourcePath = "/publish";
+ var fieldName = "entry";
+ var details = new PublishUnpublishDetails()
+ {
+ Locales = new List { "en-us" },
+ Environments = new List { "development" },
+ Variants = new List
+ {
+ new PublishVariant { Uid = "cs123", Version = 1 }
+ },
+ VariantRules = new PublishVariantRules
+ {
+ PublishLatestBase = true,
+ PublishLatestBaseConditionally = false
+ }
+ };
+ PublishUnpublishService service = new PublishUnpublishService(
+ serializer,
+ new Management.Core.Models.Stack(null, apiKey),
+ details,
+ resourcePath,
+ fieldName);
+ service.ContentBody();
+
+ string expectedJson = "{\"entry\":{\"locales\":[\"en-us\"],\"environments\":[\"development\"],\"variants\":[{\"uid\":\"cs123\",\"version\":1}],\"variant_rules\":{\"publish_latest_base\":true,\"publish_latest_base_conditionally\":false}}}";
+
+ Assert.IsNotNull(service);
+ Assert.AreEqual("POST", service.HttpMethod);
+ Assert.AreEqual(resourcePath, service.ResourcePath);
+ Assert.AreEqual(expectedJson, Encoding.Default.GetString(service.ByteContent));
+ }
}
}
diff --git a/Contentstack.Management.Core.Unit.Tests/Models/ContentModel/EntryModel.cs b/Contentstack.Management.Core.Unit.Tests/Models/ContentModel/EntryModel.cs
index 41bb545..7619ff4 100644
--- a/Contentstack.Management.Core.Unit.Tests/Models/ContentModel/EntryModel.cs
+++ b/Contentstack.Management.Core.Unit.Tests/Models/ContentModel/EntryModel.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using Contentstack.Management.Core.Abstractions;
using Newtonsoft.Json;
diff --git a/Contentstack.Management.Core.Unit.Tests/Models/EntryTest.cs b/Contentstack.Management.Core.Unit.Tests/Models/EntryTest.cs
index af3c1fd..bcf4bfe 100644
--- a/Contentstack.Management.Core.Unit.Tests/Models/EntryTest.cs
+++ b/Contentstack.Management.Core.Unit.Tests/Models/EntryTest.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.IO;
using AutoFixture;
diff --git a/Contentstack.Management.Core.Unit.Tests/Models/EntryVariantTest.cs b/Contentstack.Management.Core.Unit.Tests/Models/EntryVariantTest.cs
new file mode 100644
index 0000000..92ec3b6
--- /dev/null
+++ b/Contentstack.Management.Core.Unit.Tests/Models/EntryVariantTest.cs
@@ -0,0 +1,151 @@
+using System;
+using AutoFixture;
+using Contentstack.Management.Core.Models;
+using Contentstack.Management.Core.Queryable;
+using Contentstack.Management.Core.Unit.Tests.Mokes;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Contentstack.Management.Core.Unit.Tests.Models
+{
+ [TestClass]
+ public class EntryVariantTest
+ {
+ private Stack _stack;
+ private readonly IFixture _fixture = new Fixture();
+ private ContentstackResponse _contentstackResponse;
+
+ [TestInitialize]
+ public void initialize()
+ {
+ var client = new ContentstackClient();
+ _contentstackResponse = MockResponse.CreateContentstackResponse("MockResponse.txt");
+ client.ContentstackPipeline.ReplaceHandler(new MockHttpHandler(_contentstackResponse));
+ client.contentstackOptions.Authtoken = _fixture.Create();
+ _stack = new Stack(client, _fixture.Create());
+ }
+
+ [TestMethod]
+ public void Initialize_EntryVariant()
+ {
+ var ctUid = _fixture.Create();
+ var entryUid = _fixture.Create();
+
+ EntryVariant variant = new EntryVariant(_stack, ctUid, entryUid);
+
+ Assert.IsNull(variant.Uid);
+ Assert.AreEqual($"/content_types/{ctUid}/entries/{entryUid}/variants", variant.resourcePath);
+ }
+
+ [TestMethod]
+ public void Initialize_EntryVariant_With_Uid()
+ {
+ var ctUid = _fixture.Create();
+ var entryUid = _fixture.Create();
+ var uid = _fixture.Create();
+
+ EntryVariant variant = new EntryVariant(_stack, ctUid, entryUid, uid);
+
+ Assert.AreEqual(uid, variant.Uid);
+ Assert.AreEqual($"/content_types/{ctUid}/entries/{entryUid}/variants/{uid}", variant.resourcePath);
+ }
+
+ [TestMethod]
+ public void Should_Throw_ArgumentNullException_On_Null_Stack()
+ {
+ var ctUid = _fixture.Create();
+ var entryUid = _fixture.Create();
+
+ Assert.ThrowsException(() => new EntryVariant(null, ctUid, entryUid));
+ }
+
+ [TestMethod]
+ public void Should_Find_EntryVariants()
+ {
+ var ctUid = _fixture.Create();
+ var entryUid = _fixture.Create();
+ EntryVariant variant = new EntryVariant(_stack, ctUid, entryUid);
+
+ ContentstackResponse response = variant.Find();
+
+ Assert.AreEqual(_contentstackResponse.OpenResponse(), response.OpenResponse());
+ Assert.AreEqual(
+ _contentstackResponse.OpenJObjectResponse().ToString(),
+ response.OpenJObjectResponse().ToString()
+ );
+ }
+
+ [TestMethod]
+ public async System.Threading.Tasks.Task Should_Find_EntryVariants_Async()
+ {
+ var ctUid = _fixture.Create();
+ var entryUid = _fixture.Create();
+ EntryVariant variant = new EntryVariant(_stack, ctUid, entryUid);
+
+ ContentstackResponse response = await variant.FindAsync();
+
+ Assert.AreEqual(_contentstackResponse.OpenResponse(), response.OpenResponse());
+ Assert.AreEqual(
+ _contentstackResponse.OpenJObjectResponse().ToString(),
+ response.OpenJObjectResponse().ToString()
+ );
+ }
+
+ [TestMethod]
+ public void Should_Create_EntryVariant()
+ {
+ var ctUid = _fixture.Create();
+ var entryUid = _fixture.Create();
+ var uid = _fixture.Create();
+ EntryVariant variant = new EntryVariant(_stack, ctUid, entryUid, uid);
+
+ var model = new { entry = new { banner_color = "Navy Blue" } };
+
+ ContentstackResponse response = variant.Create(model);
+
+ Assert.AreEqual(_contentstackResponse.OpenResponse(), response.OpenResponse());
+ }
+
+ [TestMethod]
+ public async System.Threading.Tasks.Task Should_Create_EntryVariant_Async()
+ {
+ var ctUid = _fixture.Create();
+ var entryUid = _fixture.Create();
+ var uid = _fixture.Create();
+ EntryVariant variant = new EntryVariant(_stack, ctUid, entryUid, uid);
+
+ var model = new { entry = new { banner_color = "Navy Blue" } };
+
+ ContentstackResponse response = await variant.CreateAsync(model);
+
+ Assert.AreEqual(_contentstackResponse.OpenResponse(), response.OpenResponse());
+ }
+
+ [TestMethod]
+ public void Should_Update_EntryVariant()
+ {
+ var ctUid = _fixture.Create();
+ var entryUid = _fixture.Create();
+ var uid = _fixture.Create();
+ EntryVariant variant = new EntryVariant(_stack, ctUid, entryUid, uid);
+
+ var model = new { entry = new { banner_color = "Red" } };
+
+ ContentstackResponse response = variant.Update(model);
+
+ Assert.AreEqual(_contentstackResponse.OpenResponse(), response.OpenResponse());
+ }
+
+ [TestMethod]
+ public void Should_Delete_EntryVariant()
+ {
+ var ctUid = _fixture.Create();
+ var entryUid = _fixture.Create();
+ var uid = _fixture.Create();
+ EntryVariant variant = new EntryVariant(_stack, ctUid, entryUid, uid);
+
+ ContentstackResponse response = variant.Delete();
+
+ Assert.AreEqual(_contentstackResponse.OpenResponse(), response.OpenResponse());
+ }
+ }
+}
\ No newline at end of file
diff --git a/Contentstack.Management.Core/Abstractions/IEntry.cs b/Contentstack.Management.Core/Abstractions/IEntry.cs
index 265a0b7..bd37c45 100644
--- a/Contentstack.Management.Core/Abstractions/IEntry.cs
+++ b/Contentstack.Management.Core/Abstractions/IEntry.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using Newtonsoft.Json;
namespace Contentstack.Management.Core.Abstractions
diff --git a/Contentstack.Management.Core/Models/Entry.cs b/Contentstack.Management.Core/Models/Entry.cs
index 7df09db..26a8f70 100644
--- a/Contentstack.Management.Core/Models/Entry.cs
+++ b/Contentstack.Management.Core/Models/Entry.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
@@ -12,9 +12,12 @@ namespace Contentstack.Management.Core.Models
{
public class Entry: BaseModel
{
+ internal string contentTypeUid;
+
internal Entry(Stack stack, string contentTyppe, string uid)
: base(stack, "entry", uid)
{
+ contentTypeUid = contentTyppe;
resourcePath = uid == null ? $"/content_types/{contentTyppe}/entries" : $"/content_types/{contentTyppe}/entries/{uid}";
}
@@ -34,6 +37,19 @@ public Query Query()
return new Query(stack, resourcePath);
}
+ ///
+ /// The Variant on Entry will allow to fetch, create, update or delete entry variants.
+ ///
+ /// The UID of the variant.
+ /// The
+ public EntryVariant Variant(string uid = null)
+ {
+ stack.ThrowIfNotLoggedIn();
+ ThrowIfUidEmpty();
+
+ return new EntryVariant(stack, contentTypeUid, Uid, uid);
+ }
+
///
/// The Version on Entry will allow to fetch all version, delete specific version or naming the asset version.
///
diff --git a/Contentstack.Management.Core/Models/EntryVariant.cs b/Contentstack.Management.Core/Models/EntryVariant.cs
new file mode 100644
index 0000000..2e04e5e
--- /dev/null
+++ b/Contentstack.Management.Core/Models/EntryVariant.cs
@@ -0,0 +1,203 @@
+using System;
+using System.Threading.Tasks;
+using Contentstack.Management.Core.Queryable;
+using Contentstack.Management.Core.Services;
+using Contentstack.Management.Core.Services.Models;
+
+namespace Contentstack.Management.Core.Models
+{
+ ///
+ /// Represents the Entry Variant sub-resource.
+ ///
+ public class EntryVariant
+ {
+ internal Stack stack;
+ internal string resourcePath;
+
+ ///
+ /// Gets the UID of the variant.
+ ///
+ public string Uid { get; private set; }
+
+ #region Constructor
+ internal EntryVariant(Stack stack, string contentTypeUid, string entryUid, string uid = null)
+ {
+ if (stack == null)
+ {
+ throw new ArgumentNullException("stack", "Stack cannot be null.");
+ }
+
+ stack.ThrowIfAPIKeyEmpty();
+
+ this.stack = stack;
+ this.Uid = uid;
+
+ string basePath = $"/content_types/{contentTypeUid}/entries/{entryUid}/variants";
+ this.resourcePath = uid == null ? basePath : $"{basePath}/{uid}";
+ }
+ #endregion
+
+ #region Public Methods
+
+ ///
+ /// Finds all variants for an entry.
+ ///
+ /// Query parameters.
+ /// The .
+ public ContentstackResponse Find(ParameterCollection collection = null)
+ {
+ stack.ThrowIfNotLoggedIn();
+ ThrowIfUidNotEmpty();
+
+ var service = new QueryService(
+ stack,
+ collection ?? new ParameterCollection(),
+ resourcePath
+ );
+ return stack.client.InvokeSync(service);
+ }
+
+ ///
+ /// Finds all variants for an entry asynchronously.
+ ///
+ /// Query parameters.
+ /// The Task.
+ public Task FindAsync(ParameterCollection collection = null)
+ {
+ stack.ThrowIfNotLoggedIn();
+ ThrowIfUidNotEmpty();
+
+ var service = new QueryService(
+ stack,
+ collection ?? new ParameterCollection(),
+ resourcePath
+ );
+ return stack.client.InvokeAsync(service);
+ }
+
+ ///
+ /// Creates a variant for an entry.
+ ///
+ /// The variant entry data including _variant metadata.
+ /// Query parameters.
+ /// The .
+ public ContentstackResponse Create(object model, ParameterCollection collection = null)
+ {
+ stack.ThrowIfNotLoggedIn();
+ ThrowIfUidEmpty();
+
+ var service = new CreateUpdateService