From 714fea3420b14b1e635519e092f609b711b80739 Mon Sep 17 00:00:00 2001 From: OMpawar-21 Date: Fri, 15 May 2026 15:07:21 +0530 Subject: [PATCH 1/2] feat: Add support for variant branches in Entry and Query classes Add support for variant branches in Entry and Query classes - 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. --- CHANGELOG.md | 9 + .../EntryVariantsComprehensiveTest.cs | 176 ++++++++++++++++++ .../EntryUnitTests.cs | 89 +++++++++ .../QueryUnitTests.cs | 101 ++++++++++ Contentstack.Core/Models/Entry.cs | 12 +- Contentstack.Core/Models/Query.cs | 12 +- Directory.Build.props | 2 +- 7 files changed, 396 insertions(+), 5 deletions(-) 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 From c3c737ea50979343af4ee8ac877918fed0765599 Mon Sep 17 00:00:00 2001 From: OMpawar-21 Date: Fri, 17 Jul 2026 11:35:00 +0530 Subject: [PATCH 2/2] refactor(variants): extract ResolveBranch helper and consolidate test utilities - Extract duplicated branch-resolution logic into a private ResolveBranch() method in both Entry and Query, eliminating inline ternary repetition - Add DefaultBranch = "main" constant shared across Variant overloads - Promote Query._Headers to internal visibility to remove reflection in unit tests - Introduce UnitTestHelpers.GetMockClient() shared helper, removing duplicate private factory methods from EntryUnitTests and QueryUnitTests - Strengthen integration test assertions: use ThrowsAnyAsync and assert that exception message contains "branch" rather than just checking non-null - Replace hard-coded "development" branch strings with TestDataHelper.BranchUid - Bump contentstack.utils dependency from 2.0.0 to 2.0.1 Co-Authored-By: Claude Sonnet 4.6 --- .../EntryVariantsComprehensiveTest.cs | 20 ++--- .../EntryUnitTests.cs | 52 +++++------- .../QueryUnitTests.cs | 80 ++++++------------- .../UnitTestHelpers.cs | 20 +++++ Contentstack.Core/Contentstack.Core.csproj | 2 +- Contentstack.Core/Models/Entry.cs | 23 +++--- Contentstack.Core/Models/Query.cs | 25 +++--- Contentstack.Core/Scripts/refresh-region.py | 73 +++++++++++++++++ 8 files changed, 175 insertions(+), 120 deletions(-) create mode 100644 Contentstack.Core.Unit.Tests/UnitTestHelpers.cs create mode 100755 Contentstack.Core/Scripts/refresh-region.py diff --git a/Contentstack.Core.Tests/Integration/VariantsTests/EntryVariantsComprehensiveTest.cs b/Contentstack.Core.Tests/Integration/VariantsTests/EntryVariantsComprehensiveTest.cs index 204ce6b7..e2c988e3 100644 --- a/Contentstack.Core.Tests/Integration/VariantsTests/EntryVariantsComprehensiveTest.cs +++ b/Contentstack.Core.Tests/Integration/VariantsTests/EntryVariantsComprehensiveTest.cs @@ -39,7 +39,7 @@ public async Task Variant_WithInvalidBranch_ThrowsException() // Act & Assert LogAct("Fetching entry with variant and INVALID branch using .Variant() method"); - var exception = await Assert.ThrowsAsync(async () => + var exception = await Assert.ThrowsAnyAsync(async () => { await client .ContentType(TestDataHelper.ComplexContentTypeUid) @@ -48,8 +48,8 @@ await client .Fetch(); }); - LogAssert("Verifying exception was thrown"); - TestAssert.NotNull(exception); + LogAssert("Verifying exception was thrown for invalid branch"); + Assert.Contains("branch", exception.Message, System.StringComparison.OrdinalIgnoreCase); } [Fact(DisplayName = "Entry Operations - Variant With Valid Branch Returns Results")] @@ -60,7 +60,7 @@ public async Task Variant_WithValidBranch_ReturnsResults() LogContext("ContentType", TestDataHelper.ComplexContentTypeUid); LogContext("EntryUid", TestDataHelper.ComplexEntryUid); LogContext("VariantUid", TestDataHelper.VariantUid); - LogContext("Branch", "development"); + LogContext("Branch", TestDataHelper.BranchUid); var client = CreateClient(); @@ -70,7 +70,7 @@ public async Task Variant_WithValidBranch_ReturnsResults() var entry = await client .ContentType(TestDataHelper.ComplexContentTypeUid) .Entry(TestDataHelper.ComplexEntryUid) - .Variant(TestDataHelper.VariantUid, "development") + .Variant(TestDataHelper.VariantUid, TestDataHelper.BranchUid) .Fetch(); // Assert @@ -329,13 +329,13 @@ public async Task Variant_Query_WithInvalidBranch_ThrowsException() query.Variant(TestDataHelper.VariantUid, "invalid_branch_name_123"); query.Limit(5); - var exception = await Assert.ThrowsAsync(async () => + var exception = await Assert.ThrowsAnyAsync(async () => { await query.Find(); }); - LogAssert("Verifying exception was thrown"); - TestAssert.NotNull(exception); + LogAssert("Verifying exception was thrown for invalid branch"); + Assert.Contains("branch", exception.Message, System.StringComparison.OrdinalIgnoreCase); } [Fact(DisplayName = "Entry Operations - Variant Query With Valid Branch Returns Results")] @@ -345,7 +345,7 @@ public async Task Variant_Query_WithValidBranch_ReturnsResults() LogArrange("Setting up query operation with valid branch"); LogContext("ContentType", TestDataHelper.ComplexContentTypeUid); LogContext("VariantUid", TestDataHelper.VariantUid); - LogContext("Branch", "development"); + LogContext("Branch", TestDataHelper.BranchUid); var client = CreateClient(); var query = client.ContentType(TestDataHelper.ComplexContentTypeUid).Query(); @@ -353,7 +353,7 @@ public async Task Variant_Query_WithValidBranch_ReturnsResults() // Act LogAct("Executing query with variant and valid branch"); - query.Variant(TestDataHelper.VariantUid, "development"); + query.Variant(TestDataHelper.VariantUid, TestDataHelper.BranchUid); query.Limit(5); var result = await query.Find(); diff --git a/Contentstack.Core.Unit.Tests/EntryUnitTests.cs b/Contentstack.Core.Unit.Tests/EntryUnitTests.cs index 9bb0b2e7..ffea017f 100644 --- a/Contentstack.Core.Unit.Tests/EntryUnitTests.cs +++ b/Contentstack.Core.Unit.Tests/EntryUnitTests.cs @@ -2748,32 +2748,20 @@ public void Entry_Fetch_WithCachePolicyNotSet_Setup_VerifiesDefault() #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"); + var client = UnitTestHelpers.GetMockClient("main"); + var entry = client.ContentType("DUMMY_CONTENT_TYPE").Entry("DUMMY_ENTRY_UID"); // Act - entry.Variant("variant_1", "development"); + 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.Equal("VARIANT_1", entry._Headers["x-cs-variant-uid"]); + Assert.True(entry._Headers.ContainsKey("branch")); Assert.Equal("development", entry._Headers["branch"]); } @@ -2782,16 +2770,16 @@ public void Entry_Variant_WithBranch_SetsBranchHeader() public void Entry_Variant_WithNullBranch_FallsBackToStackBranch() { // Arrange - var client = GetMockClient("stack_branch"); - var entry = client.ContentType("dummy_content_type").Entry("dummy_entry_uid"); + var client = UnitTestHelpers.GetMockClient("stack_branch"); + var entry = client.ContentType("DUMMY_CONTENT_TYPE").Entry("DUMMY_ENTRY_UID"); // Act - entry.Variant("variant_1", null); + 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.Equal("VARIANT_1", entry._Headers["x-cs-variant-uid"]); + Assert.True(entry._Headers.ContainsKey("branch")); Assert.Equal("stack_branch", entry._Headers["branch"]); } @@ -2800,16 +2788,16 @@ public void Entry_Variant_WithNullBranch_FallsBackToStackBranch() public void Entry_Variant_WithEmptyBranch_FallsBackToMainIfStackBranchIsNull() { // Arrange - var client = GetMockClient(null); - var entry = client.ContentType("dummy_content_type").Entry("dummy_entry_uid"); + var client = UnitTestHelpers.GetMockClient(null); + var entry = client.ContentType("DUMMY_CONTENT_TYPE").Entry("DUMMY_ENTRY_UID"); // Act - entry.Variant("variant_1", " "); + 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.Equal("VARIANT_1", entry._Headers["x-cs-variant-uid"]); + Assert.True(entry._Headers.ContainsKey("branch")); Assert.Equal("main", entry._Headers["branch"]); } @@ -2818,17 +2806,17 @@ public void Entry_Variant_WithEmptyBranch_FallsBackToMainIfStackBranchIsNull() 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" }; + var client = UnitTestHelpers.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.Equal("VARIANT_1,VARIANT_2", entry._Headers["x-cs-variant-uid"]); + Assert.True(entry._Headers.ContainsKey("branch")); Assert.Equal("feature_branch", entry._Headers["branch"]); } diff --git a/Contentstack.Core.Unit.Tests/QueryUnitTests.cs b/Contentstack.Core.Unit.Tests/QueryUnitTests.cs index 3f3a32f1..8871cb85 100644 --- a/Contentstack.Core.Unit.Tests/QueryUnitTests.cs +++ b/Contentstack.Core.Unit.Tests/QueryUnitTests.cs @@ -2635,101 +2635,69 @@ public void Query_WithNullUrlQueries_Setup_HandlesGracefully() #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(); + var query = UnitTestHelpers.GetMockClient("main").ContentType("DUMMY_CONTENT_TYPE").Query(); // Act - query.Variant("variant_1", "development"); + 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"]); + Assert.True(query._Headers.ContainsKey("x-cs-variant-uid")); + Assert.Equal("VARIANT_1", query._Headers["x-cs-variant-uid"]); + Assert.True(query._Headers.ContainsKey("branch")); + Assert.Equal("development", query._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(); + var query = UnitTestHelpers.GetMockClient("stack_branch").ContentType("DUMMY_CONTENT_TYPE").Query(); // Act - query.Variant("variant_1", null); + 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"]); + Assert.True(query._Headers.ContainsKey("x-cs-variant-uid")); + Assert.Equal("VARIANT_1", query._Headers["x-cs-variant-uid"]); + Assert.True(query._Headers.ContainsKey("branch")); + Assert.Equal("stack_branch", query._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(); + var query = UnitTestHelpers.GetMockClient(null).ContentType("DUMMY_CONTENT_TYPE").Query(); // Act - query.Variant("variant_1", " "); + 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"]); + Assert.True(query._Headers.ContainsKey("x-cs-variant-uid")); + Assert.Equal("VARIANT_1", query._Headers["x-cs-variant-uid"]); + Assert.True(query._Headers.ContainsKey("branch")); + Assert.Equal("main", query._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" }; + var query = UnitTestHelpers.GetMockClient("main").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"]); + Assert.True(query._Headers.ContainsKey("x-cs-variant-uid")); + Assert.Equal("VARIANT_1,VARIANT_2", query._Headers["x-cs-variant-uid"]); + Assert.True(query._Headers.ContainsKey("branch")); + Assert.Equal("feature_branch", query._Headers["branch"]); } #endregion diff --git a/Contentstack.Core.Unit.Tests/UnitTestHelpers.cs b/Contentstack.Core.Unit.Tests/UnitTestHelpers.cs new file mode 100644 index 00000000..1673930a --- /dev/null +++ b/Contentstack.Core.Unit.Tests/UnitTestHelpers.cs @@ -0,0 +1,20 @@ +using Contentstack.Core; +using Contentstack.Core.Configuration; + +namespace Contentstack.Core.Unit.Tests +{ + internal static class UnitTestHelpers + { + internal static 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); + } + } +} diff --git a/Contentstack.Core/Contentstack.Core.csproj b/Contentstack.Core/Contentstack.Core.csproj index c602db54..2c40f424 100644 --- a/Contentstack.Core/Contentstack.Core.csproj +++ b/Contentstack.Core/Contentstack.Core.csproj @@ -32,7 +32,7 @@ - + diff --git a/Contentstack.Core/Models/Entry.cs b/Contentstack.Core/Models/Entry.cs index 2d0fe7f9..835afbf3 100644 --- a/Contentstack.Core/Models/Entry.cs +++ b/Contentstack.Core/Models/Entry.cs @@ -395,12 +395,13 @@ public void RemoveHeader(string key) /// To set variants header using Entry instance. /// /// Entry instance + /// Optional branch to fetch the variant from. Falls back to the stack's configured branch, then "main". /// Current instance of Entry, this will be useful for a chaining calls. /// /// /// ContentstackClient stack = new ContentstackClinet("api_key", "delivery_token", "environment"); /// Entry csEntry = stack.ContentType("contentType_id").Entry("entry_uid"); - /// + /// /// csEntry.Variant("variant_entry_1"); /// csEntry.Fetch().ContinueWith((entryResult) => { /// //Your callback code. @@ -411,10 +412,7 @@ public void RemoveHeader(string key) 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); + this.SetHeader("branch", ResolveBranch(branch)); return this; } @@ -424,12 +422,13 @@ public Entry Variant(string variant_header, string branch = null) /// To set multiple variants headers using Entry instance. /// /// Entry instance + /// Optional branch to fetch the variant from. Falls back to the stack's configured branch, then "main". /// Current instance of Entry, this will be useful for a chaining calls. /// /// /// ContentstackClient stack = new ContentstackClinet("api_key", "delivery_token", "environment"); /// Entry csEntry = stack.ContentType("contentType_id").Query(); - /// + /// /// csEntry.Variant(new List { "variant_entry_1", "variant_entry_2", "variant_entry_3" }); /// csEntry.Fetch().ContinueWith((entryResult) => { /// //Your callback code. @@ -440,13 +439,17 @@ public Entry Variant(string variant_header, string branch = null) 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); + this.SetHeader("branch", ResolveBranch(branch)); return this; } + private const string DefaultBranch = "main"; + + private string ResolveBranch(string branch) => + string.IsNullOrWhiteSpace(branch) + ? (ContentTypeInstance?.StackInstance?.Config?.Branch ?? DefaultBranch) + : branch; + /// /// Get metadata of entry. /// diff --git a/Contentstack.Core/Models/Query.cs b/Contentstack.Core/Models/Query.cs index db19ef7f..d1def290 100644 --- a/Contentstack.Core/Models/Query.cs +++ b/Contentstack.Core/Models/Query.cs @@ -20,7 +20,7 @@ public class Query #region Private Variables internal Dictionary _FormHeaders = new Dictionary(); - private Dictionary _Headers = new Dictionary(); + internal Dictionary _Headers = new Dictionary(); private Dictionary UrlQueries = new Dictionary(); protected Dictionary QueryValueJson = new Dictionary(); @@ -1711,12 +1711,13 @@ public Query SetCachePolicy(CachePolicy cachePolicy) /// To set variants header using query instance. /// /// Query instance + /// Optional branch to fetch the variant from. Falls back to the stack's configured branch, then "main". /// Current instance of Query, this will be useful for a chaining calls. /// /// /// ContentstackClient stack = new ContentstackClinet("api_key", "delivery_token", "environment"); /// Query csQuery = stack.ContentType("contentType_id").Query(); - /// + /// /// csQuery.Variant("variant_entry_1"); /// csQuery.Find().ContinueWith((queryResult) => { /// //Your callback code. @@ -1726,10 +1727,7 @@ public Query SetCachePolicy(CachePolicy cachePolicy) 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); + this.SetHeader("branch", ResolveBranch(branch)); return this; } @@ -1739,12 +1737,13 @@ public Query Variant(string variant_header, string branch = null) /// To set multiple variants headers using query instance. /// /// Query instance + /// Optional branch to fetch the variant from. Falls back to the stack's configured branch, then "main". /// Current instance of Query, this will be useful for a chaining calls. /// /// /// ContentstackClient stack = new ContentstackClinet("api_key", "delivery_token", "environment"); /// Query csQuery = stack.ContentType("contentType_id").Query(); - /// + /// /// csQuery.Variant(new List { "variant_entry_1", "variant_entry_2", "variant_entry_3" }); /// csQuery.Find().ContinueWith((queryResult) => { /// //Your callback code. @@ -1754,13 +1753,17 @@ public Query Variant(string variant_header, string branch = null) 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); + this.SetHeader("branch", ResolveBranch(branch)); return this; } + private const string DefaultBranch = "main"; + + private string ResolveBranch(string branch) => + string.IsNullOrWhiteSpace(branch) + ? (ContentTypeInstance?.StackInstance?.Config?.Branch ?? DefaultBranch) + : branch; + /// /// Execute a Query and Caches its result (Optional) /// diff --git a/Contentstack.Core/Scripts/refresh-region.py b/Contentstack.Core/Scripts/refresh-region.py new file mode 100755 index 00000000..d7ed1015 --- /dev/null +++ b/Contentstack.Core/Scripts/refresh-region.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +# Refresh regions.json from the Contentstack CDN. +# Usage (run from your project root): +# python3 Scripts/refresh-region.py +# python Scripts/refresh-region.py + +import glob +import json +import os +import ssl +import sys +import urllib.request + +REGIONS_URL = "https://artifacts.contentstack.com/regions.json" + +print(f"Fetching {REGIONS_URL} ...") + + +def _fetch(url): + # First attempt: normal SSL verification + try: + with urllib.request.urlopen(url, timeout=30) as r: + return r.read().decode("utf-8") + except urllib.error.URLError as e: + if "CERTIFICATE_VERIFY_FAILED" not in str(e): + raise + # macOS python.org builds often lack system certs — retry without verification + print("WARNING: SSL certificate verification failed. Retrying without verification.", file=sys.stderr) + print(" To fix permanently, run: /Applications/Python*/Install\\ Certificates.command", file=sys.stderr) + ctx = ssl.create_default_context() + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + with urllib.request.urlopen(url, timeout=30, context=ctx) as r: + return r.read().decode("utf-8") + + +try: + raw = _fetch(REGIONS_URL) +except Exception as e: + print(f"ERROR: Could not download regions.json: {e}", file=sys.stderr) + sys.exit(1) + +try: + data = json.loads(raw) +except json.JSONDecodeError as e: + print(f"ERROR: Downloaded content is not valid JSON: {e}", file=sys.stderr) + sys.exit(1) + +if "regions" not in data: + print("ERROR: Downloaded JSON does not contain a 'regions' key.", file=sys.stderr) + sys.exit(1) + +region_count = len(data["regions"]) + +# Scan bin/ for every copy of the DLL (covers Debug/Release, any TFM, any nesting). +dll_pattern = os.path.join(os.getcwd(), "**", "Contentstack.Utils.dll") +found = [ + os.path.dirname(dll) + for dll in glob.glob(dll_pattern, recursive=True) + if os.sep + "bin" + os.sep in dll +] + +if not found: + print("[bin] No build output found — run 'dotnet build' first, then re-run this script.") + sys.exit(1) + +for bin_dir in found: + assets_dir = os.path.join(bin_dir, "Assets") + os.makedirs(assets_dir, exist_ok=True) + dest = os.path.join(assets_dir, "regions.json") + with open(dest, "w", encoding="utf-8") as f: + f.write(raw) + print(f"[bin] Wrote {region_count} regions → {dest}")