Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
edddd85
feat: Added branch support in entry variants
OMpawar-21 Jul 13, 2026
df8af7b
Potential fix for pull request finding
OMpawar-21 Jul 14, 2026
44aeae2
Potential fix for pull request finding
OMpawar-21 Jul 14, 2026
6c4ffbd
Potential fix for pull request finding
OMpawar-21 Jul 14, 2026
b23f36a
Potential fix for pull request finding
OMpawar-21 Jul 14, 2026
1a69925
Potential fix for pull request finding
OMpawar-21 Jul 14, 2026
a75c310
fix: Fix stale header reference in MockHttpHandler and tighten branch…
OMpawar-21 Jul 14, 2026
73cd66a
Revert "fix: Fix stale header reference in MockHttpHandler and tighte…
OMpawar-21 Jul 14, 2026
7bdcf1c
Revert "Potential fix for pull request finding"
OMpawar-21 Jul 14, 2026
8444c8d
Revert "Potential fix for pull request finding"
OMpawar-21 Jul 14, 2026
a519969
Revert "Potential fix for pull request finding"
OMpawar-21 Jul 14, 2026
8fb24d1
Revert "Potential fix for pull request finding"
OMpawar-21 Jul 14, 2026
f08194a
Revert "Potential fix for pull request finding"
OMpawar-21 Jul 14, 2026
2430f8c
Address Copilot review findings for EntryVariant publish and test rel…
OMpawar-21 Jul 14, 2026
6d44968
Fix async-over-sync antipattern in retry handler and flaky timing test
OMpawar-21 Jul 14, 2026
3d192b7
Fix flaky DateTime.UtcNow timing assertions in DefaultRetryPolicyTest
OMpawar-21 Jul 14, 2026
4ae14d6
Fix Test083 skipping when branch already exists from a prior run
OMpawar-21 Jul 15, 2026
20c9c2e
Update release date for v1.1.0
OMpawar-21 Jul 16, 2026
File filter

Filter by extension

Filter by extension

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

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

- **New**
- **Branch override for Entry Variants**: `Entry.Variant(uid?, branchUid?)` accepts an optional `branchUid` so a single variant call (`Find`, `Create`, `Update`, `Fetch`, `Delete`) can target a branch other than the `Stack`'s configured one, by overriding the `branch` request header. Passing `null`/whitespace falls back to the Stack's own branch.
- **Publish/Unpublish for Entry Variants**: `EntryVariant.Publish`/`PublishAsync` and `EntryVariant.Unpublish`/`UnpublishAsync`, matching the same `PublishUnpublishDetails` contract as `Entry.Publish`/`Unpublish`, and respecting the same branch override.

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

- **Breaking Change**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3481,6 +3481,194 @@ await AssertLogger.ThrowsContentstackErrorAsync(async () =>
}

#endregion

#region L — Branch Override Tests

private const string BranchOverrideUid = "dotnet_variant_br";

private void TryDeleteBranch(string uid)
{
if (string.IsNullOrEmpty(uid)) return;
var force = new global::Contentstack.Management.Core.Queryable.ParameterCollection();
force.Add("force", true);
try { _stack.Branch(uid).Delete(force); } catch { }
}

[TestMethod]
[DoNotParallelize]
public void Test083_Should_Create_Branch_For_Variant_Override_Tests()
{
if (string.IsNullOrEmpty(_entryUid) || string.IsNullOrEmpty(_variantUid))
{
Assert.Inconclusive("Setup not completed. Ensure Test001 runs first.");
return;
}

TestOutputLogger.LogContext("TestScenario", "VariantBranchOverride_Setup");

TryDeleteBranch(BranchOverrideUid);

try
{
var model = new BranchModel { Uid = BranchOverrideUid, Source = "main" };
ContentstackResponse response = _stack.Branch().Create(model);
AssertLogger.IsNotNull(response.OpenJsonObjectResponse(), "response");
}
catch (ContentstackErrorException cex) when (
cex.StatusCode == HttpStatusCode.Conflict ||
cex.StatusCode == (HttpStatusCode)422)
{
// Branch already exists from a previous run — that is fine for our purposes.
Console.WriteLine($"Branch '{BranchOverrideUid}' already exists (HTTP {(int)cex.StatusCode}); continuing.");
}
catch (Exception ex)
{
Assert.Inconclusive("Could not create a branch for branch-override tests (branching may not be enabled on this stack): " + ex.Message);
}
}

[TestMethod]
[DoNotParallelize]
public void Test084_Should_Fetch_Variant_On_Explicit_Branch()
{
if (string.IsNullOrEmpty(_entryUid) || string.IsNullOrEmpty(_variantUid))
{
Assert.Inconclusive("Setup not completed. Ensure Test001 runs first.");
return;
}

TestOutputLogger.LogContext("TestScenario", "VariantBranchOverride_Fetch");

try
{
// The variant/entry may not have been synced onto the new branch yet, so we only
// assert that the SDK successfully issues the request with the branch override —
// not that the API necessarily has matching content on that branch.
var response = _stack.ContentType(_contentTypeUid).Entry(_entryUid).Variant(_variantUid, BranchOverrideUid).Fetch();
Console.WriteLine("Fetch on explicit branch response: " + response.OpenResponse());
AssertLogger.IsTrue(
response.IsSuccessStatusCode,
"Expected a 2xx response when fetching a variant with a valid branch override (errors would have thrown ContentstackErrorException)",
"FetchVariantOnExplicitBranch");
Comment thread
OMpawar-21 marked this conversation as resolved.
}
catch (ContentstackErrorException cex)
{
Console.WriteLine("Fetch on explicit branch failed (acceptable if content hasn't synced to the branch yet): " + cex.Message);
}
Comment thread
OMpawar-21 marked this conversation as resolved.
}

[TestMethod]
[DoNotParallelize]
public async Task Test085_Should_Fetch_Variant_On_Explicit_Branch_Async()
{
if (string.IsNullOrEmpty(_entryUid) || string.IsNullOrEmpty(_variantUid))
{
Assert.Inconclusive("Setup not completed. Ensure Test001 runs first.");
return;
}

TestOutputLogger.LogContext("TestScenario", "VariantBranchOverride_FetchAsync");

try
{
var response = await _stack.ContentType(_contentTypeUid).Entry(_entryUid).Variant(_variantUid, BranchOverrideUid).FetchAsync();
Console.WriteLine("FetchAsync on explicit branch response: " + response.OpenResponse());
AssertLogger.IsTrue(
response.IsSuccessStatusCode,
"Expected a 2xx response when fetching a variant with a valid branch override (errors would have thrown ContentstackErrorException)",
"FetchVariantOnExplicitBranchAsync");
Comment thread
OMpawar-21 marked this conversation as resolved.
}
catch (ContentstackErrorException cex)
{
Console.WriteLine("FetchAsync on explicit branch failed (acceptable if content hasn't synced to the branch yet): " + cex.Message);
}
Comment thread
OMpawar-21 marked this conversation as resolved.
}

[TestMethod]
[DoNotParallelize]
public async Task Test086_Should_Fail_To_Fetch_Variant_On_Invalid_Branch()
{
if (string.IsNullOrEmpty(_entryUid) || string.IsNullOrEmpty(_variantUid))
{
Assert.Inconclusive("Setup not completed. Ensure Test001 runs first.");
return;
}

TestOutputLogger.LogContext("TestScenario", "VariantBranchOverride_InvalidBranch");

await AssertLogger.ThrowsContentstackErrorAsync(async () =>
{
var response = await _stack.ContentType(_contentTypeUid).Entry(_entryUid).Variant(_variantUid, "definitely_invalid_branch").FetchAsync();
if (!response.IsSuccessStatusCode)
{
throw new ContentstackErrorException
{
StatusCode = response.StatusCode,
ErrorMessage = "Invalid branch UID"
};
}
}, "FetchVariantOnInvalidBranch", HttpStatusCode.NotFound, (HttpStatusCode)422, HttpStatusCode.BadRequest, HttpStatusCode.Unauthorized, HttpStatusCode.Forbidden);
}

[TestMethod]
[DoNotParallelize]
public async Task Test087_Should_Fallback_To_Stack_Branch_When_BranchUid_Is_Blank()
{
if (string.IsNullOrEmpty(_entryUid) || string.IsNullOrEmpty(_variantUid))
{
Assert.Inconclusive("Setup not completed. Ensure Test001 runs first.");
return;
}

TestOutputLogger.LogContext("TestScenario", "VariantBranchOverride_BlankFallback");

// A blank/whitespace branchUid must behave identically to omitting it entirely —
// i.e. fall back to the Stack's configured branch (main, in these tests).
var withoutOverride = await _stack.ContentType(_contentTypeUid).Entry(_entryUid).Variant().FindAsync();
var withBlankOverride = await _stack.ContentType(_contentTypeUid).Entry(_entryUid).Variant(branchUid: " ").FindAsync();

Assert.AreEqual(withoutOverride.IsSuccessStatusCode, withBlankOverride.IsSuccessStatusCode,
"A blank branchUid should fall back to the Stack's branch, matching the no-override request");
}

[TestMethod]
[DoNotParallelize]
public async Task Test088_Should_Publish_Variant_With_Explicit_Branch_Override()
{
if (string.IsNullOrEmpty(_entryUid) || string.IsNullOrEmpty(_variantUid))
{
Assert.Inconclusive("Setup not completed. Ensure Test001 runs first.");
return;
}

TestOutputLogger.LogContext("TestScenario", "VariantBranchOverride_Publish");

var publishDetails = new PublishUnpublishDetails
{
Locales = new List<string> { "en-us" },
Environments = new List<string> { "development" }
};

try
{
var response = await _stack.ContentType(_contentTypeUid).Entry(_entryUid).Variant(_variantUid, BranchOverrideUid).PublishAsync(publishDetails, "en-us");
Console.WriteLine("Publish with branch override response: " + response.OpenResponse());
}
catch (Exception ex)
{
Console.WriteLine("Publish with branch override failed (often due to missing 'development' environment on the branch). Continuing. Exception: " + ex.Message);
}
Comment thread
Copilot marked this conversation as resolved.
}

[TestMethod]
[DoNotParallelize]
public void Test089_Should_Cleanup_Branch_For_Variant_Override_Tests()
{
TestOutputLogger.LogContext("TestScenario", "VariantBranchOverride_Cleanup");
TryDeleteBranch(BranchOverrideUid);
}

#endregion
}

/// <summary>
Expand Down
165 changes: 164 additions & 1 deletion Contentstack.Management.Core.Unit.Tests/Models/EntryVariantTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ public class EntryVariantTest
private Stack _stack;
private readonly IFixture _fixture = new Fixture();
private ContentstackResponse _contentstackResponse;
private MockHttpHandler _mockHttpHandler;

[TestInitialize]
public void initialize()
{
var client = new ContentstackClient();
_contentstackResponse = MockResponse.CreateContentstackResponse("MockResponse.txt");
client.ContentstackPipeline.ReplaceHandler(new MockHttpHandler(_contentstackResponse));
_mockHttpHandler = new MockHttpHandler(_contentstackResponse);
client.ContentstackPipeline.ReplaceHandler(_mockHttpHandler);
client.contentstackOptions.Authtoken = _fixture.Create<string>();
_stack = new Stack(client, _fixture.Create<string>());
}
Expand Down Expand Up @@ -147,5 +149,166 @@ public void Should_Delete_EntryVariant()

Assert.AreEqual(_contentstackResponse.OpenResponse(), response.OpenResponse());
}

[TestMethod]
public void Initialize_EntryVariant_With_BranchUid()
{
var ctUid = _fixture.Create<string>();
var entryUid = _fixture.Create<string>();
var branchUid = _fixture.Create<string>();

EntryVariant variant = new EntryVariant(_stack, ctUid, entryUid, branchUid: branchUid);

Assert.AreEqual(branchUid, variant.branchUid);
}

[TestMethod]
public void Should_Override_Branch_Header_On_Find_When_BranchUid_Provided()
{
var ctUid = _fixture.Create<string>();
var entryUid = _fixture.Create<string>();
var branchUid = _fixture.Create<string>();
EntryVariant variant = new EntryVariant(_stack, ctUid, entryUid, branchUid: branchUid);

variant.Find();

Assert.AreEqual(branchUid, _mockHttpHandler.LastRequestHeaders["branch"]);
}

[TestMethod]
public void Should_Override_Branch_Header_On_Create_When_BranchUid_Provided()
{
var ctUid = _fixture.Create<string>();
var entryUid = _fixture.Create<string>();
var uid = _fixture.Create<string>();
var branchUid = _fixture.Create<string>();
EntryVariant variant = new EntryVariant(_stack, ctUid, entryUid, uid, branchUid);
var model = new { entry = new { banner_color = "Navy Blue" } };

variant.Create(model);

Assert.AreEqual(branchUid, _mockHttpHandler.LastRequestHeaders["branch"]);
}

[TestMethod]
public void Should_Override_Branch_Header_On_Fetch_When_BranchUid_Provided()
{
var ctUid = _fixture.Create<string>();
var entryUid = _fixture.Create<string>();
var uid = _fixture.Create<string>();
var branchUid = _fixture.Create<string>();
EntryVariant variant = new EntryVariant(_stack, ctUid, entryUid, uid, branchUid);

variant.Fetch();

Assert.AreEqual(branchUid, _mockHttpHandler.LastRequestHeaders["branch"]);
}

[TestMethod]
public void Should_Override_Branch_Header_On_Delete_When_BranchUid_Provided()
{
var ctUid = _fixture.Create<string>();
var entryUid = _fixture.Create<string>();
var uid = _fixture.Create<string>();
var branchUid = _fixture.Create<string>();
EntryVariant variant = new EntryVariant(_stack, ctUid, entryUid, uid, branchUid);

variant.Delete();

Assert.AreEqual(branchUid, _mockHttpHandler.LastRequestHeaders["branch"]);
}

[TestMethod]
public void Should_Fallback_To_Stack_Branch_When_BranchUid_Is_Empty()
{
var stackBranchUid = _fixture.Create<string>();
var client = new ContentstackClient();
var mockHttpHandler = new MockHttpHandler(_contentstackResponse);
client.ContentstackPipeline.ReplaceHandler(mockHttpHandler);
client.contentstackOptions.Authtoken = _fixture.Create<string>();
var stack = new Stack(client, _fixture.Create<string>(), branchUid: stackBranchUid);

var ctUid = _fixture.Create<string>();
var entryUid = _fixture.Create<string>();
EntryVariant variant = new EntryVariant(stack, ctUid, entryUid, branchUid: " ");

variant.Find();

Assert.AreEqual(stackBranchUid, mockHttpHandler.LastRequestHeaders["branch"]);
}

[TestMethod]
public void Should_Publish_EntryVariant()
{
var ctUid = _fixture.Create<string>();
var entryUid = _fixture.Create<string>();
var uid = _fixture.Create<string>();
EntryVariant variant = new EntryVariant(_stack, ctUid, entryUid, uid);
var details = new PublishUnpublishDetails { Locales = new System.Collections.Generic.List<string> { "en-us" }, Version = 1 };

ContentstackResponse response = variant.Publish(details);

Assert.AreEqual(_contentstackResponse.OpenResponse(), response.OpenResponse());
Assert.IsTrue(details.Variants.Exists(v => v.Uid == uid));
}

[TestMethod]
public async System.Threading.Tasks.Task Should_Publish_EntryVariant_Async()
{
var ctUid = _fixture.Create<string>();
var entryUid = _fixture.Create<string>();
var uid = _fixture.Create<string>();
EntryVariant variant = new EntryVariant(_stack, ctUid, entryUid, uid);
var details = new PublishUnpublishDetails();

ContentstackResponse response = await variant.PublishAsync(details);

Assert.AreEqual(_contentstackResponse.OpenResponse(), response.OpenResponse());
Assert.IsTrue(details.Variants.Exists(v => v.Uid == uid));
}

[TestMethod]
public void Should_Unpublish_EntryVariant()
{
var ctUid = _fixture.Create<string>();
var entryUid = _fixture.Create<string>();
var uid = _fixture.Create<string>();
EntryVariant variant = new EntryVariant(_stack, ctUid, entryUid, uid);
var details = new PublishUnpublishDetails();

ContentstackResponse response = variant.Unpublish(details);

Assert.AreEqual(_contentstackResponse.OpenResponse(), response.OpenResponse());
Assert.IsTrue(details.Variants.Exists(v => v.Uid == uid));
}

[TestMethod]
public async System.Threading.Tasks.Task Should_Unpublish_EntryVariant_Async()
{
var ctUid = _fixture.Create<string>();
var entryUid = _fixture.Create<string>();
var uid = _fixture.Create<string>();
EntryVariant variant = new EntryVariant(_stack, ctUid, entryUid, uid);
var details = new PublishUnpublishDetails();

ContentstackResponse response = await variant.UnpublishAsync(details);

Assert.AreEqual(_contentstackResponse.OpenResponse(), response.OpenResponse());
Assert.IsTrue(details.Variants.Exists(v => v.Uid == uid));
}

[TestMethod]
public void Should_Override_Branch_Header_On_Publish_When_BranchUid_Provided()
{
var ctUid = _fixture.Create<string>();
var entryUid = _fixture.Create<string>();
var uid = _fixture.Create<string>();
var branchUid = _fixture.Create<string>();
EntryVariant variant = new EntryVariant(_stack, ctUid, entryUid, uid, branchUid);

variant.Publish(new PublishUnpublishDetails());

Assert.AreEqual(branchUid, _mockHttpHandler.LastRequestHeaders["branch"]);
}
}
}
Loading
Loading