diff --git a/CHANGELOG.md b/CHANGELOG.md index 00590d50..b3b05840 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +### Version: 2.28.0 +#### Date: May-15-2026 + +##### Feat: +- Entry Variants Branch Support + - Added support for passing an optional `branch` parameter to the `.Variant()` method in both `Entry` and `Query` classes. + - If the branch parameter is null or empty, it automatically falls back to the Stack's configured branch or "main". + - Added comprehensive unit and integration tests for Entry and Query variant branch logic. + ### Version: 2.27.0 #### Date: Apr-23-2026 diff --git a/Contentstack.Core.Tests/Integration/VariantsTests/EntryVariantsComprehensiveTest.cs b/Contentstack.Core.Tests/Integration/VariantsTests/EntryVariantsComprehensiveTest.cs index ce04a10c..204ce6b7 100644 --- a/Contentstack.Core.Tests/Integration/VariantsTests/EntryVariantsComprehensiveTest.cs +++ b/Contentstack.Core.Tests/Integration/VariantsTests/EntryVariantsComprehensiveTest.cs @@ -24,6 +24,96 @@ public EntryVariantsComprehensiveTest(ITestOutputHelper output) : base(output) #region Basic Variant Operations + [Fact(DisplayName = "Entry Operations - Variant With Invalid Branch Throws Exception")] + public async Task Variant_WithInvalidBranch_ThrowsException() + { + // Arrange + LogArrange("Creating client and setting up test data"); + LogContext("ContentType", TestDataHelper.ComplexContentTypeUid); + LogContext("EntryUid", TestDataHelper.ComplexEntryUid); + LogContext("VariantUid", TestDataHelper.VariantUid); + LogContext("Branch", "invalid_branch_name_123"); + + var client = CreateClient(); + + // Act & Assert + LogAct("Fetching entry with variant and INVALID branch using .Variant() method"); + + var exception = await Assert.ThrowsAsync(async () => + { + await client + .ContentType(TestDataHelper.ComplexContentTypeUid) + .Entry(TestDataHelper.ComplexEntryUid) + .Variant(TestDataHelper.VariantUid, "invalid_branch_name_123") + .Fetch(); + }); + + LogAssert("Verifying exception was thrown"); + TestAssert.NotNull(exception); + } + + [Fact(DisplayName = "Entry Operations - Variant With Valid Branch Returns Results")] + public async Task Variant_WithValidBranch_ReturnsResults() + { + // Arrange + LogArrange("Creating client and setting up test data"); + LogContext("ContentType", TestDataHelper.ComplexContentTypeUid); + LogContext("EntryUid", TestDataHelper.ComplexEntryUid); + LogContext("VariantUid", TestDataHelper.VariantUid); + LogContext("Branch", "development"); + + var client = CreateClient(); + + // Act + LogAct("Fetching entry with variant and valid branch using .Variant() method"); + + var entry = await client + .ContentType(TestDataHelper.ComplexContentTypeUid) + .Entry(TestDataHelper.ComplexEntryUid) + .Variant(TestDataHelper.VariantUid, "development") + .Fetch(); + + // Assert + LogAssert("Verifying entry properties"); + TestAssert.NotNull(entry); + TestAssert.NotNull(entry.Uid); + } + + [Fact(DisplayName = "Entry Operations - Variant With Null Or Empty Branch Falls Back To Stack Branch Or Main")] + public async Task Variant_WithNullOrEmptyBranch_FallsBackToStackBranchOrMain() + { + // Arrange + LogArrange("Creating client and setting up test data"); + LogContext("ContentType", TestDataHelper.ComplexContentTypeUid); + LogContext("EntryUid", TestDataHelper.ComplexEntryUid); + LogContext("VariantUid", TestDataHelper.VariantUid); + + var client = CreateClient(); + + // Act + LogAct("Fetching entry with variant and null/empty branch using .Variant() method"); + + var entryWithNullBranch = await client + .ContentType(TestDataHelper.ComplexContentTypeUid) + .Entry(TestDataHelper.ComplexEntryUid) + .Variant(TestDataHelper.VariantUid, null) + .Fetch(); + + var entryWithEmptyBranch = await client + .ContentType(TestDataHelper.ComplexContentTypeUid) + .Entry(TestDataHelper.ComplexEntryUid) + .Variant(TestDataHelper.VariantUid, " ") + .Fetch(); + + // Assert + LogAssert("Verifying entry properties"); + TestAssert.NotNull(entryWithNullBranch); + TestAssert.NotNull(entryWithNullBranch.Uid); + + TestAssert.NotNull(entryWithEmptyBranch); + TestAssert.NotNull(entryWithEmptyBranch.Uid); + } + [Fact(DisplayName = "Entry Operations - Variant Fetch With Variant Method Returns Variant Content")] public async Task Variant_FetchWithVariantMethod_ReturnsVariantContent() { @@ -221,6 +311,92 @@ public async Task Variant_MultipleVariants_UsingList() #region Query with Variants + [Fact(DisplayName = "Entry Operations - Variant Query With Invalid Branch Throws Exception")] + public async Task Variant_Query_WithInvalidBranch_ThrowsException() + { + // Arrange + LogArrange("Setting up query operation with invalid branch"); + LogContext("ContentType", TestDataHelper.ComplexContentTypeUid); + LogContext("VariantUid", TestDataHelper.VariantUid); + LogContext("Branch", "invalid_branch_name_123"); + + var client = CreateClient(); + var query = client.ContentType(TestDataHelper.ComplexContentTypeUid).Query(); + + // Act & Assert + LogAct("Executing query with variant and INVALID branch"); + + query.Variant(TestDataHelper.VariantUid, "invalid_branch_name_123"); + query.Limit(5); + + var exception = await Assert.ThrowsAsync(async () => + { + await query.Find(); + }); + + LogAssert("Verifying exception was thrown"); + TestAssert.NotNull(exception); + } + + [Fact(DisplayName = "Entry Operations - Variant Query With Valid Branch Returns Results")] + public async Task Variant_Query_WithValidBranch_ReturnsResults() + { + // Arrange + LogArrange("Setting up query operation with valid branch"); + LogContext("ContentType", TestDataHelper.ComplexContentTypeUid); + LogContext("VariantUid", TestDataHelper.VariantUid); + LogContext("Branch", "development"); + + var client = CreateClient(); + var query = client.ContentType(TestDataHelper.ComplexContentTypeUid).Query(); + + // Act + LogAct("Executing query with variant and valid branch"); + + query.Variant(TestDataHelper.VariantUid, "development"); + query.Limit(5); + var result = await query.Find(); + + // Assert + LogAssert("Verifying response"); + + TestAssert.NotNull(result); + TestAssert.NotNull(result.Items); + } + + [Fact(DisplayName = "Entry Operations - Variant Query With Null Or Empty Branch Falls Back To Stack Branch Or Main")] + public async Task Variant_Query_WithNullOrEmptyBranch_FallsBackToStackBranchOrMain() + { + // Arrange + LogArrange("Setting up query operation with null/empty branch"); + LogContext("ContentType", TestDataHelper.ComplexContentTypeUid); + LogContext("VariantUid", TestDataHelper.VariantUid); + + var client = CreateClient(); + + // Act + LogAct("Executing queries with variant and null/empty branch"); + + var queryWithNullBranch = client.ContentType(TestDataHelper.ComplexContentTypeUid).Query(); + queryWithNullBranch.Variant(TestDataHelper.VariantUid, null); + queryWithNullBranch.Limit(1); + var resultWithNullBranch = await queryWithNullBranch.Find(); + + var queryWithEmptyBranch = client.ContentType(TestDataHelper.ComplexContentTypeUid).Query(); + queryWithEmptyBranch.Variant(TestDataHelper.VariantUid, " "); + queryWithEmptyBranch.Limit(1); + var resultWithEmptyBranch = await queryWithEmptyBranch.Find(); + + // Assert + LogAssert("Verifying response"); + + TestAssert.NotNull(resultWithNullBranch); + TestAssert.NotNull(resultWithNullBranch.Items); + + TestAssert.NotNull(resultWithEmptyBranch); + TestAssert.NotNull(resultWithEmptyBranch.Items); + } + [Fact(DisplayName = "Entry Operations - Variant Query With Variant Method")] public async Task Variant_Query_WithVariantMethod() { diff --git a/Contentstack.Core.Unit.Tests/EntryUnitTests.cs b/Contentstack.Core.Unit.Tests/EntryUnitTests.cs index 859cd063..b3451f4a 100644 --- a/Contentstack.Core.Unit.Tests/EntryUnitTests.cs +++ b/Contentstack.Core.Unit.Tests/EntryUnitTests.cs @@ -2745,6 +2745,95 @@ public void Entry_Fetch_WithCachePolicyNotSet_Setup_VerifiesDefault() } #endregion + + #region Variant Tests + + private ContentstackClient GetMockClient(string stackBranch = null) + { + var options = new ContentstackOptions + { + ApiKey = "dummy_api_key", + DeliveryToken = "dummy_delivery_token", + Environment = "dummy_environment", + Branch = stackBranch + }; + return new ContentstackClient(options); + } + + [Fact(DisplayName = "Entry Operations - Variant With Branch Sets Branch Header")] + public void Entry_Variant_WithBranch_SetsBranchHeader() + { + // Arrange + var client = GetMockClient("main"); + var entry = client.ContentType("dummy_content_type").Entry("dummy_entry_uid"); + + // Act + entry.Variant("variant_1", "development"); + + // Assert + Assert.True(entry._Headers.ContainsKey("x-cs-variant-uid")); + Assert.Equal("variant_1", entry._Headers["x-cs-variant-uid"]); + + Assert.True(entry._Headers.ContainsKey("branch")); + Assert.Equal("development", entry._Headers["branch"]); + } + + [Fact(DisplayName = "Entry Operations - Variant With Null Branch Falls Back To Stack Branch")] + public void Entry_Variant_WithNullBranch_FallsBackToStackBranch() + { + // Arrange + var client = GetMockClient("stack_branch"); + var entry = client.ContentType("dummy_content_type").Entry("dummy_entry_uid"); + + // Act + entry.Variant("variant_1", null); + + // Assert + Assert.True(entry._Headers.ContainsKey("x-cs-variant-uid")); + Assert.Equal("variant_1", entry._Headers["x-cs-variant-uid"]); + + Assert.True(entry._Headers.ContainsKey("branch")); + Assert.Equal("stack_branch", entry._Headers["branch"]); + } + + [Fact(DisplayName = "Entry Operations - Variant With Empty Branch Falls Back To Main If Stack Branch Is Null")] + public void Entry_Variant_WithEmptyBranch_FallsBackToMainIfStackBranchIsNull() + { + // Arrange + var client = GetMockClient(null); + var entry = client.ContentType("dummy_content_type").Entry("dummy_entry_uid"); + + // Act + entry.Variant("variant_1", " "); + + // Assert + Assert.True(entry._Headers.ContainsKey("x-cs-variant-uid")); + Assert.Equal("variant_1", entry._Headers["x-cs-variant-uid"]); + + Assert.True(entry._Headers.ContainsKey("branch")); + Assert.Equal("main", entry._Headers["branch"]); + } + + [Fact(DisplayName = "Entry Operations - Variant With Multiple Variants And Branch Sets Headers")] + public void Entry_Variant_WithMultipleVariantsAndBranch_SetsHeaders() + { + // Arrange + var client = GetMockClient("main"); + var entry = client.ContentType("dummy_content_type").Entry("dummy_entry_uid"); + var variants = new List { "variant_1", "variant_2" }; + + // Act + entry.Variant(variants, "feature_branch"); + + // Assert + Assert.True(entry._Headers.ContainsKey("x-cs-variant-uid")); + Assert.Equal("variant_1,variant_2", entry._Headers["x-cs-variant-uid"]); + + Assert.True(entry._Headers.ContainsKey("branch")); + Assert.Equal("feature_branch", entry._Headers["branch"]); + } + + #endregion } } diff --git a/Contentstack.Core.Unit.Tests/QueryUnitTests.cs b/Contentstack.Core.Unit.Tests/QueryUnitTests.cs index 4e1c4ff3..3f3a32f1 100644 --- a/Contentstack.Core.Unit.Tests/QueryUnitTests.cs +++ b/Contentstack.Core.Unit.Tests/QueryUnitTests.cs @@ -2632,6 +2632,107 @@ public void Query_WithNullUrlQueries_Setup_HandlesGracefully() } #endregion + + #region Variant Tests + + private ContentstackClient GetMockClient(string stackBranch = null) + { + var options = new ContentstackOptions + { + ApiKey = "dummy_api_key", + DeliveryToken = "dummy_delivery_token", + Environment = "dummy_environment", + Branch = stackBranch + }; + return new ContentstackClient(options); + } + + [Fact(DisplayName = "Query Operations - Variant With Branch Sets Branch Header")] + public void Query_Variant_WithBranch_SetsBranchHeader() + { + // Arrange + var client = GetMockClient("main"); + var query = client.ContentType("dummy_content_type").Query(); + + // Act + query.Variant("variant_1", "development"); + + // Assert + var headersField = typeof(Query).GetField("_Headers", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + var headers = (Dictionary)headersField.GetValue(query); + + Assert.True(headers.ContainsKey("x-cs-variant-uid")); + Assert.Equal("variant_1", headers["x-cs-variant-uid"]); + + Assert.True(headers.ContainsKey("branch")); + Assert.Equal("development", headers["branch"]); + } + + [Fact(DisplayName = "Query Operations - Variant With Null Branch Falls Back To Stack Branch")] + public void Query_Variant_WithNullBranch_FallsBackToStackBranch() + { + // Arrange + var client = GetMockClient("stack_branch"); + var query = client.ContentType("dummy_content_type").Query(); + + // Act + query.Variant("variant_1", null); + + // Assert + var headersField = typeof(Query).GetField("_Headers", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + var headers = (Dictionary)headersField.GetValue(query); + + Assert.True(headers.ContainsKey("x-cs-variant-uid")); + Assert.Equal("variant_1", headers["x-cs-variant-uid"]); + + Assert.True(headers.ContainsKey("branch")); + Assert.Equal("stack_branch", headers["branch"]); + } + + [Fact(DisplayName = "Query Operations - Variant With Empty Branch Falls Back To Main If Stack Branch Is Null")] + public void Query_Variant_WithEmptyBranch_FallsBackToMainIfStackBranchIsNull() + { + // Arrange + var client = GetMockClient(null); + var query = client.ContentType("dummy_content_type").Query(); + + // Act + query.Variant("variant_1", " "); + + // Assert + var headersField = typeof(Query).GetField("_Headers", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + var headers = (Dictionary)headersField.GetValue(query); + + Assert.True(headers.ContainsKey("x-cs-variant-uid")); + Assert.Equal("variant_1", headers["x-cs-variant-uid"]); + + Assert.True(headers.ContainsKey("branch")); + Assert.Equal("main", headers["branch"]); + } + + [Fact(DisplayName = "Query Operations - Variant With Multiple Variants And Branch Sets Headers")] + public void Query_Variant_WithMultipleVariantsAndBranch_SetsHeaders() + { + // Arrange + var client = GetMockClient("main"); + var query = client.ContentType("dummy_content_type").Query(); + var variants = new List { "variant_1", "variant_2" }; + + // Act + query.Variant(variants, "feature_branch"); + + // Assert + var headersField = typeof(Query).GetField("_Headers", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + var headers = (Dictionary)headersField.GetValue(query); + + Assert.True(headers.ContainsKey("x-cs-variant-uid")); + Assert.Equal("variant_1,variant_2", headers["x-cs-variant-uid"]); + + Assert.True(headers.ContainsKey("branch")); + Assert.Equal("feature_branch", headers["branch"]); + } + + #endregion } } diff --git a/Contentstack.Core/Models/Entry.cs b/Contentstack.Core/Models/Entry.cs index 70d1948d..8e63275a 100644 --- a/Contentstack.Core/Models/Entry.cs +++ b/Contentstack.Core/Models/Entry.cs @@ -418,9 +418,13 @@ public void RemoveHeader(string key) /// }); /// /// - public Entry Variant(string variant_header) + public Entry Variant(string variant_header, string branch = null) { this.SetHeader("x-cs-variant-uid", variant_header); + string branchToUse = string.IsNullOrWhiteSpace(branch) + ? (this.ContentTypeInstance?.StackInstance?.Config?.Branch ?? "main") + : branch; + this.SetHeader("branch", branchToUse); return this; } @@ -443,9 +447,13 @@ public Entry Variant(string variant_header) /// }); /// /// - public Entry Variant(List variant_headers) + public Entry Variant(List variant_headers, string branch = null) { this.SetHeader("x-cs-variant-uid", string.Join(",", variant_headers)); + string branchToUse = string.IsNullOrWhiteSpace(branch) + ? (this.ContentTypeInstance?.StackInstance?.Config?.Branch ?? "main") + : branch; + this.SetHeader("branch", branchToUse); return this; } diff --git a/Contentstack.Core/Models/Query.cs b/Contentstack.Core/Models/Query.cs index 09dcc9c8..b852b07b 100644 --- a/Contentstack.Core/Models/Query.cs +++ b/Contentstack.Core/Models/Query.cs @@ -1733,9 +1733,13 @@ public Query SetCachePolicy(CachePolicy cachePolicy) /// }); /// /// - public Query Variant(string variant_header) + public Query Variant(string variant_header, string branch = null) { this.SetHeader("x-cs-variant-uid", variant_header); + string branchToUse = string.IsNullOrWhiteSpace(branch) + ? (this.ContentTypeInstance?.StackInstance?.Config?.Branch ?? "main") + : branch; + this.SetHeader("branch", branchToUse); return this; } @@ -1757,9 +1761,13 @@ public Query Variant(string variant_header) /// }); /// /// - public Query Variant(List variant_headers) + public Query Variant(List variant_headers, string branch = null) { this.SetHeader("x-cs-variant-uid", string.Join(",", variant_headers)); + string branchToUse = string.IsNullOrWhiteSpace(branch) + ? (this.ContentTypeInstance?.StackInstance?.Config?.Branch ?? "main") + : branch; + this.SetHeader("branch", branchToUse); return this; } diff --git a/Directory.Build.props b/Directory.Build.props index b5bbd620..79401e83 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,5 @@ - 2.27.0 + 2.28.0