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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## [v1.2.0](https://github.com/contentstack/contentstack-management-dotnet/tree/v1.2.0)(2026-10-28)

- **New**
- **Branch create: optional environment list**: `BranchModel.PublishConfig` (`BranchPublishConfigModel`, with `Environments` as a list of environment names, not UIDs) lets `Branch.Create` / `CreateAsync` clone only the listed environments. `[]` clones none; leaving `PublishConfig` unset clones all environments, as before. The list is sent unchanged; an unknown name or a mix rejects with the API's 422 error (`ContentstackErrorException`, message, error code and errors unchanged) and no branch is created.
- **Branch fetch**: a fetched `BranchModel` carries `PublishConfig.Environments` as the API returns it; it is `null` when the API omits it.
- **Test**
- Unit tests for the branch create body with two names, `[]` and unset (through `Create` and `CreateAsync`), a mocked 422 rejection, and fetched branch deserialization.
- Integration tests for create with two names, `[]`, an unknown name and a mix (written; not run in this change).

## [v1.1.1](https://github.com/contentstack/contentstack-management-dotnet/tree/v1.1.1)(2026-07-30)

- **Fix**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using Contentstack.Management.Core.Exceptions;
using Contentstack.Management.Core.Models;
Expand All @@ -24,6 +27,16 @@ public class Contentstack005_BranchTest
private const string TestBranchUidAsync = "dotnet_int_br_a"; // max 15 chars, alphanumeric+underscore
private const string NonExistentBranchUid = "dotnet_no_br";

// publish_config.environments: names (not UIDs) that must exist on the test stack.
// Set these to two real environment names on the stack before running the live suite.
private const string EnvironmentOne = "development";
private const string EnvironmentTwo = "staging";
private const string UnknownEnvironment = "dotnet_no_env_99999";
private const string EnvTwoBranchUid = "dotnet_env_two"; // max 15 chars, alphanumeric+underscore
private const string EnvNoneBranchUid = "dotnet_env_none"; // max 15 chars, alphanumeric+underscore
private const string EnvUnknownBranchUid = "dotnet_env_unk"; // max 15 chars, alphanumeric+underscore
private const string EnvMixBranchUid = "dotnet_env_mix"; // max 15 chars, alphanumeric+underscore

[ClassInitialize]
public static void ClassInitialize(TestContext context)
{
Expand All @@ -34,13 +47,21 @@ public static void ClassInitialize(TestContext context)
// Best-effort cleanup of any branches left over from a previous test run
TryDeleteBranch(TestBranchUid);
TryDeleteBranch(TestBranchUidAsync);
TryDeleteBranch(EnvTwoBranchUid);
TryDeleteBranch(EnvNoneBranchUid);
TryDeleteBranch(EnvUnknownBranchUid);
TryDeleteBranch(EnvMixBranchUid);
}

[ClassCleanup]
public static void ClassCleanup()
{
TryDeleteBranch(_createdBranchUid);
TryDeleteBranch(_createdBranchUidAsync);
TryDeleteBranch(EnvTwoBranchUid);
TryDeleteBranch(EnvNoneBranchUid);
TryDeleteBranch(EnvUnknownBranchUid);
TryDeleteBranch(EnvMixBranchUid);

try { _client?.Logout(); } catch { }
_client = null;
Expand Down Expand Up @@ -504,8 +525,132 @@ public void Test025_Should_Fail_Create_Duplicate_Branch()
}
}

// ---- Create with publish_config.environments ---------------------------

[TestMethod]
[DoNotParallelize]
public void Test026_Should_Create_Branch_With_Two_Environment_Names()
{
TestOutputLogger.LogContext("TestScenario", "Branch_Create_TwoEnvironments");
try
{
var model = NewEnvironmentBranch(EnvTwoBranchUid, EnvironmentOne, EnvironmentTwo);
ContentstackResponse response = _stack.Branch().Create(model);
AssertLogger.IsNotNull(response.OpenJsonObjectResponse(), "response");
AssertEnvironmentsEqual(response, "Branch_Create_TwoEnvironments", EnvironmentOne, EnvironmentTwo);
}
catch (Exception e)
{
AssertLogger.Fail(e.Message);
}
}

[TestMethod]
[DoNotParallelize]
public void Test027_Should_Create_Branch_With_Empty_Environment_List()
{
TestOutputLogger.LogContext("TestScenario", "Branch_Create_EmptyEnvironments");
try
{
var model = NewEnvironmentBranch(EnvNoneBranchUid);
ContentstackResponse response = _stack.Branch().Create(model);
AssertLogger.IsNotNull(response.OpenJsonObjectResponse(), "response");
// [] must read back as an empty list (present, not null), so assert it is not null first
AssertEnvironmentsEqual(response, "Branch_Create_EmptyEnvironments");
}
catch (Exception e)
{
AssertLogger.Fail(e.Message);
}
}

[TestMethod]
[DoNotParallelize]
public void Test028_Should_Reject_Create_With_Unknown_Environment_Name()
{
TestOutputLogger.LogContext("TestScenario", "Branch_Create_UnknownEnvironment");
var model = NewEnvironmentBranch(EnvUnknownBranchUid, UnknownEnvironment);
try
{
_stack.Branch().Create(model);
AssertLogger.Fail("Expected API error for an unknown environment name");
}
catch (ContentstackErrorException ex)
{
AssertEnvironmentRejected(ex, EnvUnknownBranchUid, "Branch_Create_UnknownEnvironment");
}
}

[TestMethod]
[DoNotParallelize]
public void Test029_Should_Reject_Create_With_Mix_Of_Valid_And_Unknown_Environment_Names()
{
TestOutputLogger.LogContext("TestScenario", "Branch_Create_MixedEnvironments");
var model = NewEnvironmentBranch(EnvMixBranchUid, EnvironmentOne, UnknownEnvironment);
try
{
_stack.Branch().Create(model);
AssertLogger.Fail("Expected API error for a mix of valid and unknown environment names");
}
catch (ContentstackErrorException ex)
{
AssertEnvironmentRejected(ex, EnvMixBranchUid, "Branch_Create_MixedEnvironments");
}
}

// ---- Helper methods ---------------------------------------------------

private static BranchModel NewEnvironmentBranch(string uid, params string[] environments)
{
return new BranchModel
{
Uid = uid,
Source = SourceBranch,
PublishConfig = new BranchPublishConfigModel { Environments = new List<string>(environments) }
};
}

private static List<string> EnvironmentsOf(ContentstackResponse response)
{
JsonNode node = response.OpenJsonObjectResponse()["branch"]?["publish_config"]?["environments"];
return node?.AsArray().Select(n => n!.GetValue<string>()).ToList();
}

private static void AssertEnvironmentsEqual(ContentstackResponse response, string assertionName, params string[] expected)
{
List<string> names = EnvironmentsOf(response);
AssertLogger.IsNotNull(names, "publish_config.environments");
AssertLogger.IsTrue(
names.SequenceEqual(expected),
$"Expected environments [{string.Join(", ", expected)}] but got [{string.Join(", ", names)}]",
assertionName);
}

private static void AssertEnvironmentRejected(ContentstackErrorException ex, string uid, string assertionName)
{
AssertLogger.IsTrue(
ex.StatusCode == (HttpStatusCode)422,
$"Expected 422, got {(int)ex.StatusCode} ({ex.StatusCode})",
assertionName);
AssertLogger.IsTrue(
ex.Errors != null && ex.Errors.ContainsKey("publish_config.environments"),
"Expected the API error to be keyed publish_config.environments",
assertionName);

// No branch may be created when the request is rejected
bool exists;
try
{
_stack.Branch(uid).Fetch();
exists = true;
}
catch (ContentstackErrorException)
{
exists = false;
}
AssertLogger.IsTrue(!exists, "A rejected create must not leave a branch behind", assertionName);
}

private static void AssertBranchApiError(ContentstackErrorException ex, string assertionName)
{
AssertLogger.IsTrue(
Expand Down
Loading
Loading