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/Contentstack020_WorkflowTest.cs b/Contentstack.Management.Core.Tests/IntegrationTest/Contentstack020_WorkflowTest.cs new file mode 100644 index 0000000..a992ee9 --- /dev/null +++ b/Contentstack.Management.Core.Tests/IntegrationTest/Contentstack020_WorkflowTest.cs @@ -0,0 +1,1193 @@ +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.Queryable; +using Contentstack.Management.Core.Tests.Helpers; +using Contentstack.Management.Core.Tests.Model; +using Contentstack.Management.Core.Utils; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Contentstack.Management.Core.Tests.IntegrationTest +{ + /// + /// Workflow integration tests covering CRUD operations, publish rules, and error scenarios. + /// API requires 2–20 workflow stages per workflow; helpers enforce that minimum. + /// Tests are independent with unique naming to avoid conflicts. Cleanup is best-effort to maintain stack state. + /// + [TestClass] + [DoNotParallelize] + public class Contentstack020_WorkflowTest + { + private static ContentstackClient _client; + private Stack _stack; + + // Test resource tracking for cleanup + private List _createdWorkflowUids = new List(); + private List _createdPublishRuleUids = new List(); + private string _testEnvironmentUid; + + [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); + } + + [TestCleanup] + public void Cleanup() + { + // Best-effort cleanup of created resources + CleanupCreatedResources(); + } + + /// + /// Fails the test with a clear message from ContentstackErrorException or generic exception. + /// + private static void FailWithError(string operation, Exception ex) + { + if (ex is ContentstackErrorException cex) + AssertLogger.Fail($"{operation} failed. HTTP {(int)cex.StatusCode} ({cex.StatusCode}). ErrorCode: {cex.ErrorCode}. Message: {cex.ErrorMessage ?? cex.Message}"); + else + AssertLogger.Fail($"{operation} failed: {ex.Message}"); + } + + /// + /// API may return 404 or 422 for missing or invalid workflow UIDs depending on endpoint. + /// + private static void AssertMissingWorkflowStatus(HttpStatusCode statusCode, string assertionName) + { + AssertLogger.IsTrue( + statusCode == HttpStatusCode.NotFound || statusCode == HttpStatusCode.UnprocessableEntity, + $"Expected 404 or 422 for missing workflow, got {(int)statusCode} ({statusCode})", + assertionName); + } + + /// + /// Creates a test workflow model with specified name and stage count. + /// Contentstack API requires between 2 and 20 workflow stages. + /// + private WorkflowModel CreateTestWorkflowModel(string name, int stageCount = 2) + { + if (stageCount < 2 || stageCount > 20) + throw new ArgumentOutOfRangeException(nameof(stageCount), "API requires workflow_stages count between 2 and 20."); + var stages = GenerateTestStages(stageCount); + return new WorkflowModel + { + Name = name, + Enabled = true, + Branches = new List { "main" }, + ContentTypes = new List { "$all" }, + AdminUsers = new Dictionary { ["users"] = new List() }, + WorkflowStages = stages + }; + } + + /// + /// Generates test workflow stages with unique names and standard configurations. + /// + private List GenerateTestStages(int count) + { + var stages = new List(); + var colors = new[] { "#fe5cfb", "#3688bf", "#28a745", "#ffc107", "#dc3545" }; + + for (int i = 0; i < count; i++) + { + var sysAcl = new Dictionary + { + ["roles"] = new Dictionary { ["uids"] = new List() }, + ["users"] = new Dictionary { ["uids"] = new List { "$all" } }, + ["others"] = new Dictionary() + }; + + stages.Add(new WorkflowStage + { + Name = $"Test Stage {i + 1}", + Color = colors[i % colors.Length], + SystemACL = sysAcl, + NextAvailableStages = new List { "$all" }, + AllStages = true, + AllUsers = true, + SpecificStages = false, + SpecificUsers = false, + EntryLock = "$none" + }); + } + return stages; + } + + /// + /// Creates a test publish rule model for the given workflow and stage UIDs. + /// + private PublishRuleModel CreateTestPublishRuleModel(string workflowUid, string stageUid, string environmentUid) + { + return new PublishRuleModel + { + WorkflowUid = workflowUid, + WorkflowStageUid = stageUid, + Environment = environmentUid, + Branches = new List { "main" }, + ContentTypes = new List { "$all" }, + Locales = new List { "en-us" }, + Actions = new List(), + Approvers = new Approvals { Users = new List(), Roles = new List() }, + DisableApproval = false + }; + } + + /// + /// Ensures a test environment exists for publish rule tests. + /// + private async Task EnsureTestEnvironmentAsync() + { + if (!string.IsNullOrEmpty(_testEnvironmentUid)) + return; + + try + { + // Try to find existing environments first + ContentstackResponse envResponse = _stack.Environment().Query().Find(); + if (envResponse.IsSuccessStatusCode) + { + var envJson = envResponse.OpenJObjectResponse(); + var environments = envJson["environments"] as JArray; + if (environments != null && environments.Count > 0) + { + _testEnvironmentUid = environments[0]["uid"]?.ToString(); + return; + } + } + + // Create test environment if none exist + var environmentModel = new EnvironmentModel + { + Name = $"test_workflow_env_{Guid.NewGuid():N}", + Urls = new List + { + new LocalesUrl + { + Url = "https://test-workflow-environment.example.com", + Locale = "en-us" + } + } + }; + + ContentstackResponse response = _stack.Environment().Create(environmentModel); + if (response.IsSuccessStatusCode) + { + var responseJson = response.OpenJObjectResponse(); + _testEnvironmentUid = responseJson["environment"]?["uid"]?.ToString(); + } + } + catch (Exception) + { + // Environment creation failed - tests will skip or use fallback + } + } + + /// + /// Returns the UID of the first content type on the stack, or null if the query fails. + /// GetPublishRule(contentType) requires a real content-type UID; $all is valid on workflows/publish rules but not in that path. + /// + private string TryGetFirstContentTypeUidFromStack() + { + try + { + ContentstackResponse response = _stack.ContentType().Query().Find(); + if (!response.IsSuccessStatusCode) + return null; + var model = response.OpenTResponse(); + return model?.Modellings?.FirstOrDefault()?.Uid; + } + catch + { + return null; + } + } + + /// + /// Best-effort cleanup of created test resources. + /// + private void CleanupCreatedResources() + { + // Cleanup publish rules first (they depend on workflows) + foreach (var ruleUid in _createdPublishRuleUids.ToList()) + { + try + { + _stack.Workflow().PublishRule(ruleUid).Delete(); + _createdPublishRuleUids.Remove(ruleUid); + } + catch + { + // Ignore cleanup failures + } + } + + // Then cleanup workflows + foreach (var workflowUid in _createdWorkflowUids.ToList()) + { + try + { + _stack.Workflow(workflowUid).Delete(); + _createdWorkflowUids.Remove(workflowUid); + } + catch + { + // Ignore cleanup failures + } + } + } + + // ==== HAPPY PATH TESTS (001-015) ==== + + [TestMethod] + [DoNotParallelize] + public void Test001_Should_Create_Workflow_With_Minimum_Required_Stages() + { + TestOutputLogger.LogContext("TestScenario", "CreateWorkflowWithMinimumRequiredStages"); + try + { + // Arrange — API enforces min 2 stages (max 20) + string workflowName = $"test_min_stages_workflow_{Guid.NewGuid():N}"; + var workflowModel = CreateTestWorkflowModel(workflowName, 2); + + // Act + ContentstackResponse response = _stack.Workflow().Create(workflowModel); + var responseJson = response.OpenJObjectResponse(); + + // Assert + AssertLogger.IsNotNull(response, "workflowCreateResponse"); + AssertLogger.IsTrue(response.IsSuccessStatusCode, $"Workflow create failed with status {(int)response.StatusCode}", "workflowCreateSuccess"); + AssertLogger.IsNotNull(responseJson["workflow"], "workflowObject"); + AssertLogger.IsNotNull(responseJson["workflow"]["uid"], "workflowUid"); + + string workflowUid = responseJson["workflow"]["uid"].ToString(); + _createdWorkflowUids.Add(workflowUid); + TestOutputLogger.LogContext("WorkflowUid", workflowUid); + + var stages = responseJson["workflow"]["workflow_stages"] as JArray; + AssertLogger.AreEqual(2, stages?.Count, "Expected exactly 2 stages (API minimum)", "stageCount"); + AssertLogger.AreEqual(workflowName, responseJson["workflow"]["name"]?.ToString(), "workflowName"); + } + catch (Exception ex) + { + FailWithError("Create workflow with minimum required stages", ex); + } + } + + [TestMethod] + [DoNotParallelize] + public void Test002_Should_Create_Workflow_With_Multiple_Stages() + { + TestOutputLogger.LogContext("TestScenario", "CreateWorkflowWithMultipleStages"); + try + { + // Arrange + string workflowName = $"test_multi_stage_workflow_{Guid.NewGuid():N}"; + var workflowModel = CreateTestWorkflowModel(workflowName, 3); + + // Act + ContentstackResponse response = _stack.Workflow().Create(workflowModel); + var responseJson = response.OpenJObjectResponse(); + + // Assert + AssertLogger.IsNotNull(response, "workflowCreateResponse"); + AssertLogger.IsTrue(response.IsSuccessStatusCode, $"Workflow create failed with status {(int)response.StatusCode}", "workflowCreateSuccess"); + AssertLogger.IsNotNull(responseJson["workflow"], "workflowObject"); + + string workflowUid = responseJson["workflow"]["uid"].ToString(); + _createdWorkflowUids.Add(workflowUid); + TestOutputLogger.LogContext("WorkflowUid", workflowUid); + + var stages = responseJson["workflow"]["workflow_stages"] as JArray; + AssertLogger.AreEqual(3, stages?.Count, "Expected exactly 3 stages", "stageCount"); + + // Verify all stages were created with correct names + for (int i = 0; i < 3; i++) + { + AssertLogger.AreEqual($"Test Stage {i + 1}", stages[i]["name"]?.ToString(), $"stage{i + 1}Name"); + } + } + catch (Exception ex) + { + FailWithError("Create workflow with multiple stages", ex); + } + } + + [TestMethod] + [DoNotParallelize] + public void Test003_Should_Fetch_Single_Workflow_By_Uid() + { + TestOutputLogger.LogContext("TestScenario", "FetchSingleWorkflowByUid"); + try + { + // Arrange - Create a workflow first + string workflowName = $"test_fetch_workflow_{Guid.NewGuid():N}"; + var workflowModel = CreateTestWorkflowModel(workflowName, 2); + + ContentstackResponse createResponse = _stack.Workflow().Create(workflowModel); + var createJson = createResponse.OpenJObjectResponse(); + string workflowUid = createJson["workflow"]["uid"].ToString(); + _createdWorkflowUids.Add(workflowUid); + + // Act + ContentstackResponse response = _stack.Workflow(workflowUid).Fetch(); + var responseJson = response.OpenJObjectResponse(); + + // Assert + AssertLogger.IsNotNull(response, "workflowFetchResponse"); + AssertLogger.IsTrue(response.IsSuccessStatusCode, $"Workflow fetch failed with status {(int)response.StatusCode}", "workflowFetchSuccess"); + AssertLogger.IsNotNull(responseJson["workflow"], "workflowObject"); + AssertLogger.AreEqual(workflowUid, responseJson["workflow"]["uid"]?.ToString(), "workflowUid"); + AssertLogger.AreEqual(workflowName, responseJson["workflow"]["name"]?.ToString(), "workflowName"); + + var stages = responseJson["workflow"]["workflow_stages"] as JArray; + AssertLogger.AreEqual(2, stages?.Count, "Expected 2 stages", "stageCount"); + TestOutputLogger.LogContext("FetchedWorkflowUid", workflowUid); + } + catch (Exception ex) + { + FailWithError("Fetch single workflow by UID", ex); + } + } + + [TestMethod] + [DoNotParallelize] + public void Test004_Should_Fetch_All_Workflows() + { + TestOutputLogger.LogContext("TestScenario", "FetchAllWorkflows"); + try + { + // Act + ContentstackResponse response = _stack.Workflow().FindAll(); + var responseJson = response.OpenJObjectResponse(); + + // Assert + AssertLogger.IsNotNull(response, "workflowFindAllResponse"); + AssertLogger.IsTrue(response.IsSuccessStatusCode, $"Workflow FindAll failed with status {(int)response.StatusCode}", "workflowFindAllSuccess"); + + // Response should contain workflows array (even if empty) + var workflows = (responseJson["workflows"] as JArray) ?? (responseJson["workflow"] as JArray); + AssertLogger.IsNotNull(workflows, "workflowsArray"); + + TestOutputLogger.LogContext("WorkflowCount", workflows.Count.ToString()); + } + catch (Exception ex) + { + FailWithError("Fetch all workflows", ex); + } + } + + [TestMethod] + [DoNotParallelize] + public void Test005_Should_Update_Workflow_Properties() + { + TestOutputLogger.LogContext("TestScenario", "UpdateWorkflowProperties"); + try + { + // Arrange - Create a workflow first + string originalName = $"test_update_workflow_{Guid.NewGuid():N}"; + var workflowModel = CreateTestWorkflowModel(originalName, 2); + + ContentstackResponse createResponse = _stack.Workflow().Create(workflowModel); + var createJson = createResponse.OpenJObjectResponse(); + string workflowUid = createJson["workflow"]["uid"].ToString(); + _createdWorkflowUids.Add(workflowUid); + + // Prepare update + string updatedName = $"updated_workflow_{Guid.NewGuid():N}"; + workflowModel.Name = updatedName; + workflowModel.Enabled = false; // Change enabled status + + // Act + ContentstackResponse response = _stack.Workflow(workflowUid).Update(workflowModel); + var responseJson = response.OpenJObjectResponse(); + + // Assert + AssertLogger.IsNotNull(response, "workflowUpdateResponse"); + AssertLogger.IsTrue(response.IsSuccessStatusCode, $"Workflow update failed with status {(int)response.StatusCode}", "workflowUpdateSuccess"); + AssertLogger.IsNotNull(responseJson["workflow"], "workflowObject"); + AssertLogger.AreEqual(updatedName, responseJson["workflow"]["name"]?.ToString(), "updatedWorkflowName"); + AssertLogger.AreEqual(false, responseJson["workflow"]["enabled"]?.Value(), "updatedEnabledStatus"); + + TestOutputLogger.LogContext("UpdatedWorkflowUid", workflowUid); + } + catch (Exception ex) + { + FailWithError("Update workflow properties", ex); + } + } + + [TestMethod] + [DoNotParallelize] + public void Test006_Should_Add_New_Stage_To_Existing_Workflow() + { + TestOutputLogger.LogContext("TestScenario", "AddNewStageToExistingWorkflow"); + try + { + // Arrange - Create with 2 stages (API minimum), then add a third + string workflowName = $"test_add_stage_workflow_{Guid.NewGuid():N}"; + var workflowModel = CreateTestWorkflowModel(workflowName, 2); + + ContentstackResponse createResponse = _stack.Workflow().Create(workflowModel); + var createJson = createResponse.OpenJObjectResponse(); + string workflowUid = createJson["workflow"]["uid"].ToString(); + _createdWorkflowUids.Add(workflowUid); + + workflowModel.WorkflowStages = GenerateTestStages(3); + + // Act + ContentstackResponse response = _stack.Workflow(workflowUid).Update(workflowModel); + var responseJson = response.OpenJObjectResponse(); + + // Assert + AssertLogger.IsNotNull(response, "workflowUpdateResponse"); + AssertLogger.IsTrue(response.IsSuccessStatusCode, $"Workflow update failed with status {(int)response.StatusCode}", "workflowUpdateSuccess"); + + var stages = responseJson["workflow"]["workflow_stages"] as JArray; + AssertLogger.AreEqual(3, stages?.Count, "Expected 3 stages after update", "stageCount"); + + TestOutputLogger.LogContext("WorkflowWithNewStageUid", workflowUid); + } + catch (Exception ex) + { + FailWithError("Add new stage to existing workflow", ex); + } + } + + [TestMethod] + [DoNotParallelize] + public void Test007_Should_Enable_Workflow_Successfully() + { + TestOutputLogger.LogContext("TestScenario", "EnableWorkflowSuccessfully"); + try + { + // Arrange - Create a disabled workflow + string workflowName = $"test_enable_workflow_{Guid.NewGuid():N}"; + var workflowModel = CreateTestWorkflowModel(workflowName, 2); + workflowModel.Enabled = false; + + ContentstackResponse createResponse = _stack.Workflow().Create(workflowModel); + var createJson = createResponse.OpenJObjectResponse(); + string workflowUid = createJson["workflow"]["uid"].ToString(); + _createdWorkflowUids.Add(workflowUid); + + // Act + ContentstackResponse response = _stack.Workflow(workflowUid).Enable(); + + // Assert + AssertLogger.IsNotNull(response, "workflowEnableResponse"); + AssertLogger.IsTrue(response.IsSuccessStatusCode, $"Workflow enable failed with status {(int)response.StatusCode}", "workflowEnableSuccess"); + + TestOutputLogger.LogContext("EnabledWorkflowUid", workflowUid); + } + catch (Exception ex) + { + FailWithError("Enable workflow", ex); + } + } + + [TestMethod] + [DoNotParallelize] + public void Test008_Should_Disable_Workflow_Successfully() + { + TestOutputLogger.LogContext("TestScenario", "DisableWorkflowSuccessfully"); + try + { + // Arrange - Create an enabled workflow + string workflowName = $"test_disable_workflow_{Guid.NewGuid():N}"; + var workflowModel = CreateTestWorkflowModel(workflowName, 2); + workflowModel.Enabled = true; + + ContentstackResponse createResponse = _stack.Workflow().Create(workflowModel); + var createJson = createResponse.OpenJObjectResponse(); + string workflowUid = createJson["workflow"]["uid"].ToString(); + _createdWorkflowUids.Add(workflowUid); + + // Act + ContentstackResponse response = _stack.Workflow(workflowUid).Disable(); + + // Assert + AssertLogger.IsNotNull(response, "workflowDisableResponse"); + AssertLogger.IsTrue(response.IsSuccessStatusCode, $"Workflow disable failed with status {(int)response.StatusCode}", "workflowDisableSuccess"); + + TestOutputLogger.LogContext("DisabledWorkflowUid", workflowUid); + } + catch (Exception ex) + { + FailWithError("Disable workflow", ex); + } + } + + [TestMethod] + [DoNotParallelize] + public async Task Test009_Should_Create_Publish_Rule_For_Workflow_Stage() + { + TestOutputLogger.LogContext("TestScenario", "CreatePublishRuleForWorkflowStage"); + try + { + // Arrange - Create workflow and ensure environment exists + await EnsureTestEnvironmentAsync(); + AssertLogger.IsFalse(string.IsNullOrEmpty(_testEnvironmentUid), "Test environment is required for publish rule tests", "testEnvironmentUid"); + + string workflowName = $"test_publish_rule_workflow_{Guid.NewGuid():N}"; + var workflowModel = CreateTestWorkflowModel(workflowName, 2); + + ContentstackResponse workflowResponse = _stack.Workflow().Create(workflowModel); + var workflowJson = workflowResponse.OpenJObjectResponse(); + string workflowUid = workflowJson["workflow"]["uid"].ToString(); + _createdWorkflowUids.Add(workflowUid); + + var stages = workflowJson["workflow"]["workflow_stages"] as JArray; + string stageUid = stages[1]["uid"].ToString(); // Use second stage + + // Create publish rule + var publishRuleModel = CreateTestPublishRuleModel(workflowUid, stageUid, _testEnvironmentUid); + + // Act + ContentstackResponse response = _stack.Workflow().PublishRule().Create(publishRuleModel); + var responseJson = response.OpenJObjectResponse(); + + // Assert + AssertLogger.IsNotNull(response, "publishRuleCreateResponse"); + AssertLogger.IsTrue(response.IsSuccessStatusCode, $"Publish rule create failed with status {(int)response.StatusCode}", "publishRuleCreateSuccess"); + AssertLogger.IsNotNull(responseJson["publishing_rule"], "publishingRuleObject"); + + string publishRuleUid = responseJson["publishing_rule"]["uid"].ToString(); + _createdPublishRuleUids.Add(publishRuleUid); + + AssertLogger.AreEqual(workflowUid, responseJson["publishing_rule"]["workflow"]?.ToString(), "publishRuleWorkflowUid"); + AssertLogger.AreEqual(stageUid, responseJson["publishing_rule"]["workflow_stage"]?.ToString(), "publishRuleStageUid"); + + TestOutputLogger.LogContext("PublishRuleUid", publishRuleUid); + } + catch (Exception ex) + { + FailWithError("Create publish rule for workflow stage", ex); + } + } + + [TestMethod] + [DoNotParallelize] + public void Test010_Should_Fetch_All_Publish_Rules() + { + TestOutputLogger.LogContext("TestScenario", "FetchAllPublishRules"); + try + { + // Act + ContentstackResponse response = _stack.Workflow().PublishRule().FindAll(); + var responseJson = response.OpenJObjectResponse(); + + // Assert + AssertLogger.IsNotNull(response, "publishRuleFindAllResponse"); + AssertLogger.IsTrue(response.IsSuccessStatusCode, $"Publish rule FindAll failed with status {(int)response.StatusCode}", "publishRuleFindAllSuccess"); + + // Response should contain publishing_rules array (even if empty) + var rules = (responseJson["publishing_rules"] as JArray) ?? (responseJson["publishing_rule"] as JArray); + AssertLogger.IsNotNull(rules, "publishingRulesArray"); + + TestOutputLogger.LogContext("PublishRuleCount", rules.Count.ToString()); + } + catch (Exception ex) + { + FailWithError("Fetch all publish rules", ex); + } + } + + [TestMethod] + [DoNotParallelize] + public async Task Test011_Should_Get_Publish_Rules_By_Content_Type() + { + TestOutputLogger.LogContext("TestScenario", "GetPublishRulesByContentType"); + try + { + // Arrange - Create workflow and publish rule first + await EnsureTestEnvironmentAsync(); + AssertLogger.IsFalse(string.IsNullOrEmpty(_testEnvironmentUid), "Test environment is required", "testEnvironmentUid"); + + string contentTypeUid = TryGetFirstContentTypeUidFromStack(); + AssertLogger.IsFalse(string.IsNullOrEmpty(contentTypeUid), "Stack must expose at least one content type for GetPublishRule by content type", "contentTypeUid"); + + string workflowName = $"test_content_type_rule_workflow_{Guid.NewGuid():N}"; + var workflowModel = CreateTestWorkflowModel(workflowName, 2); + + ContentstackResponse workflowResponse = _stack.Workflow().Create(workflowModel); + var workflowJson = workflowResponse.OpenJObjectResponse(); + string workflowUid = workflowJson["workflow"]["uid"].ToString(); + _createdWorkflowUids.Add(workflowUid); + + var stages = workflowJson["workflow"]["workflow_stages"] as JArray; + string stageUid = stages[0]["uid"].ToString(); + + var publishRuleModel = CreateTestPublishRuleModel(workflowUid, stageUid, _testEnvironmentUid); + publishRuleModel.ContentTypes = new List { contentTypeUid }; + + ContentstackResponse ruleResponse = _stack.Workflow().PublishRule().Create(publishRuleModel); + var ruleJson = ruleResponse.OpenJObjectResponse(); + string publishRuleUid = ruleJson["publishing_rule"]["uid"].ToString(); + _createdPublishRuleUids.Add(publishRuleUid); + + // Act + var collection = new ParameterCollection(); + ContentstackResponse response = _stack.Workflow(workflowUid).GetPublishRule(contentTypeUid, collection); + + // Assert + AssertLogger.IsNotNull(response, "getPublishRuleResponse"); + AssertLogger.IsTrue(response.IsSuccessStatusCode, $"Get publish rule by content type failed with status {(int)response.StatusCode}", "getPublishRuleSuccess"); + + TestOutputLogger.LogContext("ContentTypeFilter", contentTypeUid); + } + catch (Exception ex) + { + FailWithError("Get publish rules by content type", ex); + } + } + + [TestMethod] + [DoNotParallelize] + public async Task Test012_Should_Update_Publish_Rule() + { + TestOutputLogger.LogContext("TestScenario", "UpdatePublishRule"); + try + { + // Arrange - Create workflow and publish rule first + await EnsureTestEnvironmentAsync(); + AssertLogger.IsFalse(string.IsNullOrEmpty(_testEnvironmentUid), "Test environment is required", "testEnvironmentUid"); + + string workflowName = $"test_update_rule_workflow_{Guid.NewGuid():N}"; + var workflowModel = CreateTestWorkflowModel(workflowName, 2); + + ContentstackResponse workflowResponse = _stack.Workflow().Create(workflowModel); + var workflowJson = workflowResponse.OpenJObjectResponse(); + string workflowUid = workflowJson["workflow"]["uid"].ToString(); + _createdWorkflowUids.Add(workflowUid); + + var stages = workflowJson["workflow"]["workflow_stages"] as JArray; + string stageUid = stages[0]["uid"].ToString(); + + var publishRuleModel = CreateTestPublishRuleModel(workflowUid, stageUid, _testEnvironmentUid); + ContentstackResponse ruleResponse = _stack.Workflow().PublishRule().Create(publishRuleModel); + var ruleJson = ruleResponse.OpenJObjectResponse(); + string publishRuleUid = ruleJson["publishing_rule"]["uid"].ToString(); + _createdPublishRuleUids.Add(publishRuleUid); + + // Update the publish rule (locales must exist on the stack; integration stack typically has en-us) + publishRuleModel.DisableApproval = true; + publishRuleModel.Locales = new List { "en-us" }; + + // Act + ContentstackResponse response = _stack.Workflow().PublishRule(publishRuleUid).Update(publishRuleModel); + var responseJson = response.OpenJObjectResponse(); + + // Assert + AssertLogger.IsNotNull(response, "publishRuleUpdateResponse"); + AssertLogger.IsTrue(response.IsSuccessStatusCode, $"Publish rule update failed with status {(int)response.StatusCode}", "publishRuleUpdateSuccess"); + AssertLogger.AreEqual(true, responseJson["publishing_rule"]["disable_approver_publishing"]?.Value(), "updatedDisableApproval"); + + TestOutputLogger.LogContext("UpdatedPublishRuleUid", publishRuleUid); + } + catch (Exception ex) + { + FailWithError("Update publish rule", ex); + } + } + + [TestMethod] + [DoNotParallelize] + public void Test013_Should_Fetch_Workflows_With_Include_Parameters() + { + TestOutputLogger.LogContext("TestScenario", "FetchWorkflowsWithIncludeParameters"); + try + { + // Act + var collection = new ParameterCollection(); + collection.Add("include_count", "true"); + collection.Add("include_publish_details", "true"); + + ContentstackResponse response = _stack.Workflow().FindAll(collection); + var responseJson = response.OpenJObjectResponse(); + + // Assert + AssertLogger.IsNotNull(response, "workflowFindAllWithIncludeResponse"); + AssertLogger.IsTrue(response.IsSuccessStatusCode, $"Workflow FindAll with include failed with status {(int)response.StatusCode}", "workflowFindAllWithIncludeSuccess"); + + var workflows = (responseJson["workflows"] as JArray) ?? (responseJson["workflow"] as JArray); + AssertLogger.IsNotNull(workflows, "workflowsArray"); + + TestOutputLogger.LogContext("IncludeParameters", "include_count,include_publish_details"); + } + catch (Exception ex) + { + FailWithError("Fetch workflows with include parameters", ex); + } + } + + [TestMethod] + [DoNotParallelize] + public void Test014_Should_Fetch_Workflows_With_Pagination() + { + TestOutputLogger.LogContext("TestScenario", "FetchWorkflowsWithPagination"); + try + { + // Act + var collection = new ParameterCollection(); + collection.Add("limit", "5"); + collection.Add("skip", "0"); + + ContentstackResponse response = _stack.Workflow().FindAll(collection); + var responseJson = response.OpenJObjectResponse(); + + // Assert + AssertLogger.IsNotNull(response, "workflowPaginationResponse"); + AssertLogger.IsTrue(response.IsSuccessStatusCode, $"Workflow FindAll with pagination failed with status {(int)response.StatusCode}", "workflowPaginationSuccess"); + + var workflows = (responseJson["workflows"] as JArray) ?? (responseJson["workflow"] as JArray); + AssertLogger.IsNotNull(workflows, "workflowsArray"); + + TestOutputLogger.LogContext("PaginationParams", "limit=5,skip=0"); + } + catch (Exception ex) + { + FailWithError("Fetch workflows with pagination", ex); + } + } + + [TestMethod] + [DoNotParallelize] + public async Task Test015_Should_Delete_Publish_Rule_Successfully() + { + TestOutputLogger.LogContext("TestScenario", "DeletePublishRuleSuccessfully"); + try + { + // Arrange - Create workflow and publish rule first + await EnsureTestEnvironmentAsync(); + AssertLogger.IsFalse(string.IsNullOrEmpty(_testEnvironmentUid), "Test environment is required", "testEnvironmentUid"); + + string workflowName = $"test_delete_rule_workflow_{Guid.NewGuid():N}"; + var workflowModel = CreateTestWorkflowModel(workflowName, 2); + + ContentstackResponse workflowResponse = _stack.Workflow().Create(workflowModel); + var workflowJson = workflowResponse.OpenJObjectResponse(); + string workflowUid = workflowJson["workflow"]["uid"].ToString(); + _createdWorkflowUids.Add(workflowUid); + + var stages = workflowJson["workflow"]["workflow_stages"] as JArray; + string stageUid = stages[0]["uid"].ToString(); + + var publishRuleModel = CreateTestPublishRuleModel(workflowUid, stageUid, _testEnvironmentUid); + ContentstackResponse ruleResponse = _stack.Workflow().PublishRule().Create(publishRuleModel); + var ruleJson = ruleResponse.OpenJObjectResponse(); + string publishRuleUid = ruleJson["publishing_rule"]["uid"].ToString(); + + // Act + ContentstackResponse response = _stack.Workflow().PublishRule(publishRuleUid).Delete(); + + // Assert + AssertLogger.IsNotNull(response, "publishRuleDeleteResponse"); + AssertLogger.IsTrue(response.IsSuccessStatusCode, $"Publish rule delete failed with status {(int)response.StatusCode}", "publishRuleDeleteSuccess"); + + TestOutputLogger.LogContext("DeletedPublishRuleUid", publishRuleUid); + + // Remove from cleanup list since it's already deleted + _createdPublishRuleUids.Remove(publishRuleUid); + } + catch (Exception ex) + { + FailWithError("Delete publish rule", ex); + } + } + + // ==== NEGATIVE PATH TESTS (101-110) ==== + + [TestMethod] + [DoNotParallelize] + public void Test101_Should_Fail_Create_Workflow_With_Missing_Name() + { + TestOutputLogger.LogContext("TestScenario", "FailCreateWorkflowWithMissingName"); + try + { + // Arrange - Create workflow model without name + var workflowModel = new WorkflowModel + { + Name = null, // Missing required field + Enabled = true, + Branches = new List { "main" }, + ContentTypes = new List { "$all" }, + WorkflowStages = GenerateTestStages(2) + }; + + // Act & Assert + AssertLogger.ThrowsException(() => + { + ContentstackResponse response = _stack.Workflow().Create(workflowModel); + if (!response.IsSuccessStatusCode) + { + // Parse error details and throw exception for validation + throw new ContentstackErrorException { StatusCode = response.StatusCode, ErrorMessage = "Validation failed" }; + } + }, "createWorkflowWithMissingName"); + + TestOutputLogger.LogContext("ValidationError", "MissingName"); + } + catch (ContentstackErrorException cex) + { + // Expected validation error + AssertLogger.IsTrue((int)cex.StatusCode >= 400 && (int)cex.StatusCode < 500, "Expected 4xx status code for validation error", "validationErrorStatusCode"); + TestOutputLogger.LogContext("ExpectedValidationError", cex.Message); + } + catch (Exception ex) + { + FailWithError("Expected validation error for missing workflow name", ex); + } + } + + [TestMethod] + [DoNotParallelize] + public void Test102_Should_Fail_Create_Workflow_With_Invalid_Stage_Data() + { + TestOutputLogger.LogContext("TestScenario", "FailCreateWorkflowWithInvalidStageData"); + try + { + // Arrange - Create workflow with invalid stage configuration + var workflowModel = new WorkflowModel + { + Name = $"test_invalid_stage_workflow_{Guid.NewGuid():N}", + Enabled = true, + Branches = new List { "main" }, + ContentTypes = new List { "$all" }, + WorkflowStages = new List + { + new WorkflowStage + { + Name = null, // Invalid: missing stage name + Color = "invalid_color", // Invalid color format + SystemACL = null // Missing ACL + } + } + }; + + // Act + ContentstackResponse response = _stack.Workflow().Create(workflowModel); + + // Assert - Should fail with validation error + AssertLogger.IsFalse(response.IsSuccessStatusCode, "Expected workflow creation to fail with invalid stage data", "invalidStageCreationFailed"); + AssertLogger.IsTrue((int)response.StatusCode >= 400 && (int)response.StatusCode < 500, "Expected 4xx status code", "validationErrorStatusCode"); + + TestOutputLogger.LogContext("ValidationError", "InvalidStageData"); + } + catch (Exception ex) + { + // Some validation errors might be thrown as exceptions + if (ex is ContentstackErrorException cex) + { + AssertLogger.IsTrue((int)cex.StatusCode >= 400 && (int)cex.StatusCode < 500, "Expected 4xx status code for validation error", "validationErrorStatusCode"); + TestOutputLogger.LogContext("ExpectedValidationError", cex.Message); + } + else + { + FailWithError("Expected validation error for invalid stage data", ex); + } + } + } + + [TestMethod] + [DoNotParallelize] + public void Test103_Should_Fail_Create_Duplicate_Workflow_Name() + { + TestOutputLogger.LogContext("TestScenario", "FailCreateDuplicateWorkflowName"); + try + { + // Arrange - Create first workflow + string duplicateName = $"test_duplicate_workflow_{Guid.NewGuid():N}"; + var workflowModel1 = CreateTestWorkflowModel(duplicateName, 2); + + ContentstackResponse response1 = _stack.Workflow().Create(workflowModel1); + AssertLogger.IsTrue(response1.IsSuccessStatusCode, "First workflow creation should succeed", "firstWorkflowCreated"); + + var responseJson1 = response1.OpenJObjectResponse(); + string workflowUid1 = responseJson1["workflow"]["uid"].ToString(); + _createdWorkflowUids.Add(workflowUid1); + + // Create second workflow with same name + var workflowModel2 = CreateTestWorkflowModel(duplicateName, 2); + + // Act & assert — duplicate name may return non-success response or throw ContentstackErrorException (422) + try + { + ContentstackResponse response2 = _stack.Workflow().Create(workflowModel2); + AssertLogger.IsFalse(response2.IsSuccessStatusCode, "Expected duplicate workflow creation to fail", "duplicateWorkflowCreationFailed"); + AssertLogger.IsTrue((int)response2.StatusCode == 409 || (int)response2.StatusCode == 422, "Expected 409 Conflict or 422 Unprocessable Entity", "conflictErrorStatusCode"); + } + catch (ContentstackErrorException cex) + { + AssertLogger.IsTrue((int)cex.StatusCode == 409 || (int)cex.StatusCode == 422, "Expected 409 Conflict or 422 Unprocessable Entity", "conflictErrorStatusCode"); + } + + TestOutputLogger.LogContext("ConflictError", "DuplicateWorkflowName"); + } + catch (Exception ex) + { + FailWithError("Expected conflict error for duplicate workflow name", ex); + } + } + + [TestMethod] + [DoNotParallelize] + public void Test104_Should_Fail_Fetch_NonExistent_Workflow() + { + TestOutputLogger.LogContext("TestScenario", "FailFetchNonExistentWorkflow"); + try + { + // Arrange + string nonExistentUid = $"non_existent_workflow_{Guid.NewGuid():N}"; + + // Act + ContentstackResponse response = _stack.Workflow(nonExistentUid).Fetch(); + + // Assert — API often returns 422 for invalid/missing workflow UID + AssertLogger.IsFalse(response.IsSuccessStatusCode, "Expected fetch to fail for non-existent workflow", "fetchNonExistentFailed"); + AssertMissingWorkflowStatus(response.StatusCode, "missingWorkflowStatusCode"); + + TestOutputLogger.LogContext("NotFoundOrUnprocessable", nonExistentUid); + } + catch (ContentstackErrorException cex) + { + AssertMissingWorkflowStatus(cex.StatusCode, "missingWorkflowStatusCode"); + TestOutputLogger.LogContext("ExpectedNotFoundError", cex.Message); + } + catch (Exception ex) + { + FailWithError("Expected error for non-existent workflow fetch", ex); + } + } + + [TestMethod] + [DoNotParallelize] + public void Test105_Should_Fail_Update_NonExistent_Workflow() + { + TestOutputLogger.LogContext("TestScenario", "FailUpdateNonExistentWorkflow"); + try + { + // Arrange + string nonExistentUid = $"non_existent_workflow_{Guid.NewGuid():N}"; + var workflowModel = CreateTestWorkflowModel("update_test", 2); + + // Act + ContentstackResponse response = _stack.Workflow(nonExistentUid).Update(workflowModel); + + // Assert + AssertLogger.IsFalse(response.IsSuccessStatusCode, "Expected update to fail for non-existent workflow", "updateNonExistentFailed"); + AssertMissingWorkflowStatus(response.StatusCode, "missingWorkflowStatusCode"); + + TestOutputLogger.LogContext("NotFoundOrUnprocessable", nonExistentUid); + } + catch (ContentstackErrorException cex) + { + AssertMissingWorkflowStatus(cex.StatusCode, "missingWorkflowStatusCode"); + TestOutputLogger.LogContext("ExpectedNotFoundError", cex.Message); + } + catch (Exception ex) + { + FailWithError("Expected error for non-existent workflow update", ex); + } + } + + [TestMethod] + [DoNotParallelize] + public void Test106_Should_Fail_Enable_NonExistent_Workflow() + { + TestOutputLogger.LogContext("TestScenario", "FailEnableNonExistentWorkflow"); + try + { + // Arrange + string nonExistentUid = $"non_existent_workflow_{Guid.NewGuid():N}"; + + // Act + ContentstackResponse response = _stack.Workflow(nonExistentUid).Enable(); + + // Assert + AssertLogger.IsFalse(response.IsSuccessStatusCode, "Expected enable to fail for non-existent workflow", "enableNonExistentFailed"); + AssertMissingWorkflowStatus(response.StatusCode, "missingWorkflowStatusCode"); + + TestOutputLogger.LogContext("NotFoundOrUnprocessable", nonExistentUid); + } + catch (ContentstackErrorException cex) + { + AssertMissingWorkflowStatus(cex.StatusCode, "missingWorkflowStatusCode"); + TestOutputLogger.LogContext("ExpectedNotFoundError", cex.Message); + } + catch (Exception ex) + { + FailWithError("Expected error for non-existent workflow enable", ex); + } + } + + [TestMethod] + [DoNotParallelize] + public async Task Test107_Should_Fail_Create_Publish_Rule_Invalid_Workflow_Reference() + { + TestOutputLogger.LogContext("TestScenario", "FailCreatePublishRuleInvalidWorkflowReference"); + try + { + // Arrange + await EnsureTestEnvironmentAsync(); + AssertLogger.IsFalse(string.IsNullOrEmpty(_testEnvironmentUid), "Test environment is required", "testEnvironmentUid"); + + string invalidWorkflowUid = $"invalid_workflow_{Guid.NewGuid():N}"; + string invalidStageUid = $"invalid_stage_{Guid.NewGuid():N}"; + + var publishRuleModel = CreateTestPublishRuleModel(invalidWorkflowUid, invalidStageUid, _testEnvironmentUid); + + // Act + ContentstackResponse response = _stack.Workflow().PublishRule().Create(publishRuleModel); + + // Assert + AssertLogger.IsFalse(response.IsSuccessStatusCode, "Expected publish rule creation to fail with invalid workflow reference", "invalidReferenceCreationFailed"); + AssertLogger.IsTrue((int)response.StatusCode >= 400 && (int)response.StatusCode < 500, "Expected 4xx status code", "validationErrorStatusCode"); + + TestOutputLogger.LogContext("ValidationError", "InvalidWorkflowReference"); + } + catch (Exception ex) + { + if (ex is ContentstackErrorException cex) + { + AssertLogger.IsTrue((int)cex.StatusCode >= 400 && (int)cex.StatusCode < 500, "Expected 4xx status code for validation error", "validationErrorStatusCode"); + TestOutputLogger.LogContext("ExpectedValidationError", cex.Message); + } + else + { + FailWithError("Expected validation error for invalid workflow reference", ex); + } + } + } + + [TestMethod] + [DoNotParallelize] + public async Task Test108_Should_Allow_Delete_Workflow_With_Active_Publish_Rules() + { + TestOutputLogger.LogContext("TestScenario", "DeleteWorkflowWithActivePublishRules"); + try + { + // Arrange - Create workflow and publish rule + await EnsureTestEnvironmentAsync(); + AssertLogger.IsFalse(string.IsNullOrEmpty(_testEnvironmentUid), "Test environment is required", "testEnvironmentUid"); + + string workflowName = $"test_delete_with_rules_workflow_{Guid.NewGuid():N}"; + var workflowModel = CreateTestWorkflowModel(workflowName, 2); + + ContentstackResponse workflowResponse = _stack.Workflow().Create(workflowModel); + var workflowJson = workflowResponse.OpenJObjectResponse(); + string workflowUid = workflowJson["workflow"]["uid"].ToString(); + _createdWorkflowUids.Add(workflowUid); + + var stages = workflowJson["workflow"]["workflow_stages"] as JArray; + string stageUid = stages[0]["uid"].ToString(); + + var publishRuleModel = CreateTestPublishRuleModel(workflowUid, stageUid, _testEnvironmentUid); + ContentstackResponse ruleResponse = _stack.Workflow().PublishRule().Create(publishRuleModel); + var ruleJson = ruleResponse.OpenJObjectResponse(); + string publishRuleUid = ruleJson["publishing_rule"]["uid"].ToString(); + _createdPublishRuleUids.Add(publishRuleUid); + + // Act — Management API allows deleting the workflow while publish rules still reference it; cleanup removes rules first + ContentstackResponse response = _stack.Workflow(workflowUid).Delete(); + + // Assert + AssertLogger.IsNotNull(response, "workflowDeleteResponse"); + AssertLogger.IsTrue(response.IsSuccessStatusCode, $"Workflow delete failed with status {(int)response.StatusCode}", "workflowDeleteSuccess"); + _createdWorkflowUids.Remove(workflowUid); + + TestOutputLogger.LogContext("DeletedWorkflowWithPublishRules", workflowUid); + } + catch (Exception ex) + { + FailWithError("Delete workflow with active publish rules", ex); + } + } + + [TestMethod] + [DoNotParallelize] + public void Test109_Should_Fail_Workflow_Operations_Without_Authentication() + { + TestOutputLogger.LogContext("TestScenario", "FailWorkflowOperationsWithoutAuthentication"); + try + { + // Arrange - Create unauthenticated client + var unauthenticatedClient = new ContentstackClient(); + var unauthenticatedStack = unauthenticatedClient.Stack("dummy_api_key"); + + // Act & Assert — SDK throws InvalidOperationException when not logged in (before HTTP) + AssertLogger.ThrowsException(() => + { + unauthenticatedStack.Workflow().FindAll(); + }, "unauthenticatedWorkflowOperation"); + + TestOutputLogger.LogContext("AuthenticationError", "NotLoggedIn"); + } + catch (Exception ex) + { + FailWithError("Unauthenticated workflow operation", ex); + } + } + + [TestMethod] + [DoNotParallelize] + public void Test110_Should_Delete_Workflow_Successfully_After_Cleanup() + { + TestOutputLogger.LogContext("TestScenario", "DeleteWorkflowSuccessfullyAfterCleanup"); + try + { + // Arrange - Create a simple workflow + string workflowName = $"test_final_delete_workflow_{Guid.NewGuid():N}"; + var workflowModel = CreateTestWorkflowModel(workflowName, 2); + + ContentstackResponse createResponse = _stack.Workflow().Create(workflowModel); + var createJson = createResponse.OpenJObjectResponse(); + string workflowUid = createJson["workflow"]["uid"].ToString(); + + // Act + ContentstackResponse response = _stack.Workflow(workflowUid).Delete(); + + // Assert + AssertLogger.IsNotNull(response, "workflowDeleteResponse"); + AssertLogger.IsTrue(response.IsSuccessStatusCode, $"Workflow delete failed with status {(int)response.StatusCode}", "workflowDeleteSuccess"); + + TestOutputLogger.LogContext("DeletedWorkflowUid", workflowUid); + + // Verify deletion — fetch may return error response or throw ContentstackErrorException (e.g. 422) + try + { + ContentstackResponse fetchResponse = _stack.Workflow(workflowUid).Fetch(); + AssertMissingWorkflowStatus(fetchResponse.StatusCode, "workflowNotFoundAfterDelete"); + } + catch (ContentstackErrorException cex) + { + AssertMissingWorkflowStatus(cex.StatusCode, "workflowNotFoundAfterDelete"); + } + } + catch (Exception ex) + { + FailWithError("Delete workflow after cleanup", ex); + } + } + } +} \ No newline at end of file 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(stack.client.serializer, stack, resourcePath, model, "entry", "PUT", collection: collection); + return stack.client.InvokeSync(service); + } + + /// + /// Creates a variant for an entry asynchronously. + /// + /// The variant entry data including _variant metadata. + /// Query parameters. + /// The Task. + public Task CreateAsync(object model, ParameterCollection collection = null) + { + stack.ThrowIfNotLoggedIn(); + ThrowIfUidEmpty(); + + var service = new CreateUpdateService(stack.client.serializer, stack, resourcePath, model, "entry", "PUT", collection: collection); + return stack.client.InvokeAsync, ContentstackResponse>(service); + } + + /// + /// Updates a variant for an entry. + /// + /// The variant entry data including _variant metadata. + /// Query parameters. + /// The . + public ContentstackResponse Update(object model, ParameterCollection collection = null) + { + return Create(model, collection); + } + + /// + /// Updates a variant for an entry asynchronously. + /// + /// The variant entry data including _variant metadata. + /// Query parameters. + /// The Task. + public Task UpdateAsync(object model, ParameterCollection collection = null) + { + return CreateAsync(model, collection); + } + + /// + /// Fetches a specific variant. + /// + /// Query parameters. + /// The . + public ContentstackResponse Fetch(ParameterCollection collection = null) + { + stack.ThrowIfNotLoggedIn(); + ThrowIfUidEmpty(); + + var service = new FetchDeleteService(stack.client.serializer, stack, resourcePath, collection: collection); + return stack.client.InvokeSync(service); + } + + /// + /// Fetches a specific variant asynchronously. + /// + /// Query parameters. + /// The Task. + public Task FetchAsync(ParameterCollection collection = null) + { + stack.ThrowIfNotLoggedIn(); + ThrowIfUidEmpty(); + + var service = new FetchDeleteService(stack.client.serializer, stack, resourcePath, collection: collection); + return stack.client.InvokeAsync(service); + } + + /// + /// Deletes a specific variant. + /// + /// Query parameters. + /// The . + public ContentstackResponse Delete(ParameterCollection collection = null) + { + stack.ThrowIfNotLoggedIn(); + ThrowIfUidEmpty(); + + var service = new FetchDeleteService(stack.client.serializer, stack, resourcePath, "DELETE", collection: collection); + return stack.client.InvokeSync(service); + } + + /// + /// Deletes a specific variant asynchronously. + /// + /// Query parameters. + /// The Task. + public Task DeleteAsync(ParameterCollection collection = null) + { + stack.ThrowIfNotLoggedIn(); + ThrowIfUidEmpty(); + + var service = new FetchDeleteService(stack.client.serializer, stack, resourcePath, "DELETE", collection: collection); + return stack.client.InvokeAsync(service); + } + + internal void ThrowIfUidNotEmpty() + { + if (!string.IsNullOrEmpty(this.Uid)) + { + throw new InvalidOperationException("Operation not allowed with a specified UID."); + } + } + + internal void ThrowIfUidEmpty() + { + if (string.IsNullOrEmpty(this.Uid)) + { + throw new InvalidOperationException("UID is required for this operation."); + } + } + #endregion + } +} \ No newline at end of file diff --git a/Contentstack.Management.Core/Models/PublishUnpublishDetails.cs b/Contentstack.Management.Core/Models/PublishUnpublishDetails.cs index 19003ea..f86be13 100644 --- a/Contentstack.Management.Core/Models/PublishUnpublishDetails.cs +++ b/Contentstack.Management.Core/Models/PublishUnpublishDetails.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Newtonsoft.Json; @@ -10,6 +10,10 @@ public class PublishUnpublishDetails public List Environments { get; set; } + public List Variants { get; set; } + + public PublishVariantRules VariantRules { get; set; } + public int? Version { get; set; } public string ScheduledAt { get; set; } diff --git a/Contentstack.Management.Core/Models/PublishVariant.cs b/Contentstack.Management.Core/Models/PublishVariant.cs new file mode 100644 index 0000000..b9ac08d --- /dev/null +++ b/Contentstack.Management.Core/Models/PublishVariant.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace Contentstack.Management.Core.Models +{ + public class PublishVariant + { + [JsonProperty("uid")] + public string Uid { get; set; } + + [JsonProperty("version")] + public int? Version { get; set; } + } +} diff --git a/Contentstack.Management.Core/Models/PublishVariantRules.cs b/Contentstack.Management.Core/Models/PublishVariantRules.cs new file mode 100644 index 0000000..4e89570 --- /dev/null +++ b/Contentstack.Management.Core/Models/PublishVariantRules.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace Contentstack.Management.Core.Models +{ + public class PublishVariantRules + { + [JsonProperty("publish_latest_base")] + public bool? PublishLatestBase { get; set; } + + [JsonProperty("publish_latest_base_conditionally")] + public bool? PublishLatestBaseConditionally { get; set; } + } +} diff --git a/Contentstack.Management.Core/Models/Workflow.cs b/Contentstack.Management.Core/Models/Workflow.cs index 9647670..21b44f5 100644 --- a/Contentstack.Management.Core/Models/Workflow.cs +++ b/Contentstack.Management.Core/Models/Workflow.cs @@ -235,7 +235,7 @@ public virtual ContentstackResponse Enable() stack.ThrowIfNotLoggedIn(); ThrowIfUidEmpty(); - var service = new FetchDeleteService(stack.client.serializer, stack, $"{resourcePath}/disable"); + var service = new FetchDeleteService(stack.client.serializer, stack, $"{resourcePath}/enable"); return stack.client.InvokeSync(service); } @@ -254,7 +254,7 @@ public virtual Task EnableAsync() stack.ThrowIfNotLoggedIn(); ThrowIfUidEmpty(); - var service = new FetchDeleteService(stack.client.serializer, stack, $"{resourcePath}/disable"); + var service = new FetchDeleteService(stack.client.serializer, stack, $"{resourcePath}/enable"); return stack.client.InvokeAsync(service); } diff --git a/Contentstack.Management.Core/Services/Models/PublishUnpublishService.cs b/Contentstack.Management.Core/Services/Models/PublishUnpublishService.cs index e8bb6fd..83cb838 100644 --- a/Contentstack.Management.Core/Services/Models/PublishUnpublishService.cs +++ b/Contentstack.Management.Core/Services/Models/PublishUnpublishService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Globalization; using System.IO; using Contentstack.Management.Core.Models; @@ -69,6 +69,46 @@ public override void ContentBody() writer.WriteEndArray(); } + + if (details.Variants != null && details.Variants.Count > 0) + { + writer.WritePropertyName("variants"); + writer.WriteStartArray(); + foreach (var variant in details.Variants) + { + writer.WriteStartObject(); + if (variant.Uid != null) + { + writer.WritePropertyName("uid"); + writer.WriteValue(variant.Uid); + } + if (variant.Version.HasValue) + { + writer.WritePropertyName("version"); + writer.WriteValue(variant.Version.Value); + } + writer.WriteEndObject(); + } + writer.WriteEndArray(); + } + + if (details.VariantRules != null) + { + writer.WritePropertyName("variant_rules"); + writer.WriteStartObject(); + if (details.VariantRules.PublishLatestBase.HasValue) + { + writer.WritePropertyName("publish_latest_base"); + writer.WriteValue(details.VariantRules.PublishLatestBase.Value); + } + if (details.VariantRules.PublishLatestBaseConditionally.HasValue) + { + writer.WritePropertyName("publish_latest_base_conditionally"); + writer.WriteValue(details.VariantRules.PublishLatestBaseConditionally.Value); + } + writer.WriteEndObject(); + } + writer.WriteEndObject(); if (details.Version!=null) diff --git a/Directory.Build.props b/Directory.Build.props index d79e191..b135ba9 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,5 @@ - 0.7.0 + 0.8.0