diff --git a/Contentstack.Core.Tests/Helpers/TestDataHelper.cs b/Contentstack.Core.Tests/Helpers/TestDataHelper.cs index 9499df5..b1ea3b1 100644 --- a/Contentstack.Core.Tests/Helpers/TestDataHelper.cs +++ b/Contentstack.Core.Tests/Helpers/TestDataHelper.cs @@ -117,19 +117,49 @@ static TestDataHelper() #endregion #region Taxonomy - + /// /// Gets the taxonomy term for USA state (e.g., "california") /// - public static string TaxUsaState => + public static string TaxUsaState => GetRequiredConfig("TAX_USA_STATE"); - + /// /// Gets the taxonomy term for India state (e.g., "maharashtra") /// - public static string TaxIndiaState => + public static string TaxIndiaState => GetRequiredConfig("TAX_INDIA_STATE"); - + + /// + /// API key for the taxonomy-publish test stack (gadgets). + /// + public static string TaxPublishApiKey => + GetRequiredConfig("TAX_PUBLISH_API_KEY"); + + /// + /// Delivery token for the taxonomy-publish test stack. + /// + public static string TaxPublishDeliveryToken => + GetRequiredConfig("TAX_PUBLISH_DELIVERY_TOKEN"); + + /// + /// Environment for the taxonomy-publish test stack. + /// + public static string TaxPublishEnvironment => + GetRequiredConfig("TAX_PUBLISH_ENVIRONMENT"); + + /// + /// UID of the published taxonomy to use in localization tests (e.g. "gadgets"). + /// + public static string TaxPublishTaxonomyUid => + GetRequiredConfig("TAX_PUBLISH_TAXONOMY_UID"); + + /// + /// Locale code used for localized taxonomy/term delivery tests (e.g. "hi-in"). + /// + public static string TaxPublishLocale => + GetRequiredConfig("TAX_PUBLISH_LOCALE"); + #endregion #region Live Preview diff --git a/Contentstack.Core.Tests/Integration/Taxonomy/TaxonomyLocalisationRunner.cs b/Contentstack.Core.Tests/Integration/Taxonomy/TaxonomyLocalisationRunner.cs new file mode 100644 index 0000000..2d09a79 --- /dev/null +++ b/Contentstack.Core.Tests/Integration/Taxonomy/TaxonomyLocalisationRunner.cs @@ -0,0 +1,177 @@ +using System; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; +using Contentstack.Core.Configuration; +using Contentstack.Core.Models; +using Contentstack.Core.Tests.Helpers; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Contentstack.Core.Tests.Integration.Taxonomy +{ + /// + /// End-to-end runner for all taxonomy localisation CDA calls. + /// Mirrors the TypeScript smoke-test script — runs every endpoint in sequence, + /// prints the SDK call, real HTTP request (URL + headers), and full JSON response. + /// Run with: + /// dotnet test --filter "FullyQualifiedName~TaxonomyLocalisationRunner" --logger "console;verbosity=detailed" + /// + [Trait("Category", "TaxonomyLocalisationRunner")] + public class TaxonomyLocalisationRunner + { + private readonly ITestOutputHelper _out; + + private const string ApiKey = "blt168147f34138ebac"; + private const string DeliveryToken = "cs91c6d782d3c9ca953c0a2685"; + private const string Environment = "dev"; + private const string TaxonomyUid = "gadgets"; + private const string Locale = "hi-in"; + + public TaxonomyLocalisationRunner(ITestOutputHelper output) + { + _out = output; + } + + private ContentstackClient CreateClient() + { + var options = new ContentstackOptions + { + ApiKey = ApiKey, + DeliveryToken = DeliveryToken, + Environment = Environment + }; + return new ContentstackClient(options); + } + + private void Section(int num, string title) + { + var line = new string('─', 60); + _out.WriteLine(""); + _out.WriteLine(line); + _out.WriteLine($" {num}. {title}"); + _out.WriteLine(line); + } + + private void PrintResponse(string label, object data) + { + _out.WriteLine($"✅ {label}"); + _out.WriteLine(JsonConvert.SerializeObject(data, Formatting.Indented)); + } + + private void PrintError(string label, Exception ex) + { + _out.WriteLine($"❌ {label}"); + _out.WriteLine($" {ex.Message}"); + } + + [Fact(DisplayName = "TaxonomyLocalisationRunner - Full end-to-end run of all CDA localisation calls")] + public async Task Run_All_TaxonomyLocalisation_Calls() + { + var client = CreateClient(); + string firstTermUid = null; + + // ── 1. GET /v3/taxonomies/gadgets?environment=dev ───────────────── + Section(1, $"Fetch taxonomy uid={TaxonomyUid} (master locale)"); + _out.WriteLine($"SDK : client.Taxonomies(\"{TaxonomyUid}\").Fetch()"); + try + { + var result = await client.Taxonomies(TaxonomyUid).Fetch(); + PrintResponse($"GET /taxonomies/{TaxonomyUid}", result); + } + catch (Exception ex) { PrintError($"GET /taxonomies/{TaxonomyUid}", ex); } + + // ── 2. GET /v3/taxonomies/gadgets?environment=dev&locale=hi-in ──── + Section(2, $"Fetch taxonomy uid={TaxonomyUid} locale={Locale}"); + _out.WriteLine($"SDK : client.Taxonomies(\"{TaxonomyUid}\").Fetch(\"{Locale}\")"); + try + { + var result = await client.Taxonomies(TaxonomyUid).Fetch(Locale); + PrintResponse($"GET /taxonomies/{TaxonomyUid}?locale={Locale}", result); + } + catch (Exception ex) { PrintError($"GET /taxonomies/{TaxonomyUid}?locale={Locale}", ex); } + + // ── 3. GET /v3/taxonomies/gadgets/terms?environment=dev&locale=hi-in + Section(3, $"Find terms taxonomy={TaxonomyUid} locale={Locale}"); + _out.WriteLine($"SDK : client.Taxonomies(\"{TaxonomyUid}\").Terms().SetLocale(\"{Locale}\").Find()"); + try + { + var result = await client.Taxonomies(TaxonomyUid).Terms() + .SetLocale(Locale) + .Find(); + PrintResponse($"GET /taxonomies/{TaxonomyUid}/terms?locale={Locale}", result); + } + catch (Exception ex) { PrintError($"GET /taxonomies/{TaxonomyUid}/terms?locale={Locale}", ex); } + + // ── 4. GET /v3/taxonomies/gadgets/terms?locale=hi-in&include_fallback=true + Section(4, $"Find terms taxonomy={TaxonomyUid} locale={Locale} include_fallback=true"); + _out.WriteLine($"SDK : client.Taxonomies(\"{TaxonomyUid}\").Terms().SetLocale(\"{Locale}\").IncludeFallback().Find()"); + try + { + var result = await client.Taxonomies(TaxonomyUid).Terms() + .SetLocale(Locale) + .IncludeFallback() + .Find(); + PrintResponse($"GET /taxonomies/{TaxonomyUid}/terms?locale={Locale}&include_fallback=true", result); + + // Grab first term uid for subsequent calls + var terms = result?.Items; + foreach (var t in terms ?? System.Array.Empty()) + { + firstTermUid = t?["uid"]?.ToString(); + if (!string.IsNullOrEmpty(firstTermUid)) break; + } + } + catch (Exception ex) { PrintError($"GET /taxonomies/{TaxonomyUid}/terms with fallback", ex); } + + if (string.IsNullOrEmpty(firstTermUid)) + { + _out.WriteLine(""); + _out.WriteLine("⚠️ No term UID found — skipping single-term calls."); + return; + } + + _out.WriteLine($"\n📌 Using term uid = \"{firstTermUid}\" for single-term calls."); + + // ── 5. GET /v3/taxonomies/gadgets/terms/:uid?locale=hi-in ───────── + Section(5, $"Fetch single term uid={firstTermUid} locale={Locale}"); + _out.WriteLine($"SDK : client.Taxonomies(\"{TaxonomyUid}\").Term(\"{firstTermUid}\").Fetch(\"{Locale}\")"); + try + { + var result = await client.Taxonomies(TaxonomyUid).Term(firstTermUid).Fetch(Locale); + PrintResponse($"GET /taxonomies/{TaxonomyUid}/terms/{firstTermUid}?locale={Locale}", result); + } + catch (Exception ex) { PrintError($"GET /taxonomies/{TaxonomyUid}/terms/{firstTermUid}?locale={Locale}", ex); } + + // ── 6. GET /v3/taxonomies/gadgets/terms/:uid/locales ────────────── + Section(6, $"Term locales uid={firstTermUid}"); + _out.WriteLine($"SDK : client.Taxonomies(\"{TaxonomyUid}\").Term(\"{firstTermUid}\").Locales()"); + try + { + var result = await client.Taxonomies(TaxonomyUid).Term(firstTermUid).Locales(); + PrintResponse($"GET /taxonomies/{TaxonomyUid}/terms/{firstTermUid}/locales", result); + } + catch (Exception ex) { PrintError($"GET /taxonomies/{TaxonomyUid}/terms/{firstTermUid}/locales", ex); } + + // ── 7. GET /v3/taxonomies/gadgets/terms/:uid/ancestors ──────────── + Section(7, $"Term ancestors uid={firstTermUid}"); + _out.WriteLine($"SDK : client.Taxonomies(\"{TaxonomyUid}\").Term(\"{firstTermUid}\").Ancestors()"); + try + { + var result = await client.Taxonomies(TaxonomyUid).Term(firstTermUid).Ancestors(); + PrintResponse($"GET /taxonomies/{TaxonomyUid}/terms/{firstTermUid}/ancestors", result); + } + catch (Exception ex) { PrintError($"GET /taxonomies/{TaxonomyUid}/terms/{firstTermUid}/ancestors", ex); } + + // ── 8. GET /v3/taxonomies/gadgets/terms/:uid/descendants ────────── + Section(8, $"Term descendants uid={firstTermUid}"); + _out.WriteLine($"SDK : client.Taxonomies(\"{TaxonomyUid}\").Term(\"{firstTermUid}\").Descendants()"); + try + { + var result = await client.Taxonomies(TaxonomyUid).Term(firstTermUid).Descendants(); + PrintResponse($"GET /taxonomies/{TaxonomyUid}/terms/{firstTermUid}/descendants", result); + } + catch (Exception ex) { PrintError($"GET /taxonomies/{TaxonomyUid}/terms/{firstTermUid}/descendants", ex); } + } + } +} diff --git a/Contentstack.Core.Tests/Integration/Taxonomy/TaxonomyLocalisationTest.cs b/Contentstack.Core.Tests/Integration/Taxonomy/TaxonomyLocalisationTest.cs new file mode 100644 index 0000000..1b768ef --- /dev/null +++ b/Contentstack.Core.Tests/Integration/Taxonomy/TaxonomyLocalisationTest.cs @@ -0,0 +1,272 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; +using Contentstack.Core.Configuration; +using Contentstack.Core.Models; +using Contentstack.Core.Internals; +using Contentstack.Core.Tests.Helpers; + +namespace Contentstack.Core.Tests.Integration.Taxonomy +{ + /// + /// Integration tests for localized taxonomy and term delivery (CDA). + /// Covers: Taxonomy.Fetch(locale), TermQuery.SetLocale/IncludeFallback/Find, + /// Term.Fetch(locale), Term.Locales, Term.Ancestors, Term.Descendants. + /// Stack: gadgets taxonomy, locales en-us / hi-in. + /// + [Trait("Category", "TaxonomyLocalisation")] + public class TaxonomyLocalisationTest : IntegrationTestBase + { + public TaxonomyLocalisationTest(ITestOutputHelper output) : base(output) { } + + /// + /// Creates a client scoped to the gadgets taxonomy-publish test stack. + /// Uses the default CDN host (no custom host override needed). + /// + private ContentstackClient CreateGadgetsClient() + { + var options = new ContentstackOptions + { + ApiKey = TestDataHelper.TaxPublishApiKey, + DeliveryToken = TestDataHelper.TaxPublishDeliveryToken, + Environment = TestDataHelper.TaxPublishEnvironment + }; + var client = new ContentstackClient(options); + client.Plugins.Add(new RequestLoggingPlugin(TestOutput)); + return client; + } + + // ── 1. Fetch localized taxonomy ────────────────────────────────────── + + [Fact(DisplayName = "TaxPublish - Fetch taxonomy returns object with uid and name")] + public async Task Fetch_Taxonomy_MasterLocale_ReturnsValidObject() + { + LogArrange("Fetching taxonomy without locale (master)"); + LogContext("TaxonomyUid", TestDataHelper.TaxPublishTaxonomyUid); + + var client = CreateGadgetsClient(); + + LogAct("Calling Taxonomies(uid).Fetch()"); + var result = await client + .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid) + .Fetch(); + + LogAssert("Verifying response"); + Assert.NotNull(result); + Assert.NotNull(result["uid"]?.ToString()); + Assert.Equal(TestDataHelper.TaxPublishTaxonomyUid, result["uid"]?.ToString()); + } + + [Fact(DisplayName = "TaxPublish - Fetch taxonomy with locale returns localized name")] + public async Task Fetch_Taxonomy_WithLocale_ReturnsLocalizedName() + { + LogArrange("Fetching taxonomy with locale"); + LogContext("TaxonomyUid", TestDataHelper.TaxPublishTaxonomyUid); + LogContext("Locale", TestDataHelper.TaxPublishLocale); + + var client = CreateGadgetsClient(); + + LogAct("Calling Taxonomies(uid).Fetch(locale)"); + var result = await client + .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid) + .Fetch(TestDataHelper.TaxPublishLocale); + + LogAssert("Verifying response"); + Assert.NotNull(result); + Assert.NotNull(result["uid"]?.ToString()); + // The name should differ from the master locale (en-us) value + Assert.NotNull(result["name"]?.ToString()); + } + + // ── 2. Find all taxonomies ──────────────────────────────────────────── + + [Fact(DisplayName = "TaxPublish - Find all taxonomies returns non-empty collection")] + public async Task Find_AllTaxonomies_ReturnsCollection() + { + LogArrange("Fetching all published taxonomies"); + + var client = CreateGadgetsClient(); + + LogAct("Calling Taxonomies().Find()"); + var result = await client.Taxonomies().Find(); + + LogAssert("Verifying response"); + Assert.NotNull(result); + Assert.NotNull(result.Items); + } + + // ── 3. Find terms with locale ───────────────────────────────────────── + + [Fact(DisplayName = "TaxPublish - Find terms with locale returns localized terms")] + public async Task Find_Terms_WithLocale_ReturnsLocalizedTerms() + { + LogArrange("Finding terms with locale"); + LogContext("TaxonomyUid", TestDataHelper.TaxPublishTaxonomyUid); + LogContext("Locale", TestDataHelper.TaxPublishLocale); + + var client = CreateGadgetsClient(); + + LogAct("Calling Terms().SetLocale(locale).Find()"); + var result = await client + .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid) + .Terms() + .SetLocale(TestDataHelper.TaxPublishLocale) + .Find(); + + LogAssert("Verifying response"); + Assert.NotNull(result); + Assert.NotNull(result.Items); + } + + [Fact(DisplayName = "TaxPublish - Find terms with locale and fallback returns terms")] + public async Task Find_Terms_WithLocaleAndFallback_ReturnsTerms() + { + LogArrange("Finding terms with locale and include_fallback"); + LogContext("TaxonomyUid", TestDataHelper.TaxPublishTaxonomyUid); + LogContext("Locale", TestDataHelper.TaxPublishLocale); + + var client = CreateGadgetsClient(); + + LogAct("Calling Terms().SetLocale(locale).IncludeFallback().Find()"); + var result = await client + .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid) + .Terms() + .SetLocale(TestDataHelper.TaxPublishLocale) + .IncludeFallback() + .Find(); + + LogAssert("Verifying response"); + Assert.NotNull(result); + Assert.NotNull(result.Items); + // Fallback means we should get at least as many terms as without fallback + Assert.True(result.Items.Any()); + } + + // ── 4–7. Single term methods ────────────────────────────────────────── + + /// + /// Fetches the first available term UID from the gadgets taxonomy to use in subsequent tests. + /// + private async Task GetFirstTermUidAsync(ContentstackClient client) + { + var terms = await client + .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid) + .Terms() + .SetLocale(TestDataHelper.TaxPublishLocale) + .IncludeFallback() + .Find(); + + return terms?.Items?.FirstOrDefault()?["uid"]?.ToString(); + } + + [Fact(DisplayName = "TaxPublish - Fetch single term with locale returns localized term")] + public async Task Fetch_SingleTerm_WithLocale_ReturnsLocalizedTerm() + { + var client = CreateGadgetsClient(); + var termUid = await GetFirstTermUidAsync(client); + + if (string.IsNullOrEmpty(termUid)) + { + Output.WriteLine("No term UID found — skipping test."); + return; + } + + LogArrange("Fetching single term with locale"); + LogContext("TaxonomyUid", TestDataHelper.TaxPublishTaxonomyUid); + LogContext("TermUid", termUid); + LogContext("Locale", TestDataHelper.TaxPublishLocale); + + LogAct("Calling Term(termUid).Fetch(locale)"); + var result = await client + .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid) + .Term(termUid) + .Fetch(TestDataHelper.TaxPublishLocale); + + LogAssert("Verifying response"); + Assert.NotNull(result); + Assert.NotNull(result["uid"]?.ToString()); + Assert.Equal(termUid, result["uid"]?.ToString()); + } + + [Fact(DisplayName = "TaxPublish - Term.Locales returns locales collection")] + public async Task Term_Locales_ReturnsLocalesCollection() + { + var client = CreateGadgetsClient(); + var termUid = await GetFirstTermUidAsync(client); + + if (string.IsNullOrEmpty(termUid)) + { + Output.WriteLine("No term UID found — skipping test."); + return; + } + + LogArrange("Fetching all locales for a term"); + LogContext("TaxonomyUid", TestDataHelper.TaxPublishTaxonomyUid); + LogContext("TermUid", termUid); + + LogAct("Calling Term(termUid).Locales()"); + var result = await client + .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid) + .Term(termUid) + .Locales(); + + LogAssert("Verifying response"); + Assert.NotNull(result); + } + + [Fact(DisplayName = "TaxPublish - Term.Ancestors returns ancestors collection")] + public async Task Term_Ancestors_ReturnsAncestorsCollection() + { + var client = CreateGadgetsClient(); + var termUid = await GetFirstTermUidAsync(client); + + if (string.IsNullOrEmpty(termUid)) + { + Output.WriteLine("No term UID found — skipping test."); + return; + } + + LogArrange("Fetching ancestors for a term"); + LogContext("TaxonomyUid", TestDataHelper.TaxPublishTaxonomyUid); + LogContext("TermUid", termUid); + + LogAct("Calling Term(termUid).Ancestors()"); + var result = await client + .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid) + .Term(termUid) + .Ancestors(); + + LogAssert("Verifying response"); + Assert.NotNull(result); + } + + [Fact(DisplayName = "TaxPublish - Term.Descendants returns descendants collection")] + public async Task Term_Descendants_ReturnsDescendantsCollection() + { + var client = CreateGadgetsClient(); + var termUid = await GetFirstTermUidAsync(client); + + if (string.IsNullOrEmpty(termUid)) + { + Output.WriteLine("No term UID found — skipping test."); + return; + } + + LogArrange("Fetching descendants for a term"); + LogContext("TaxonomyUid", TestDataHelper.TaxPublishTaxonomyUid); + LogContext("TermUid", termUid); + + LogAct("Calling Term(termUid).Descendants()"); + var result = await client + .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid) + .Term(termUid) + .Descendants(); + + LogAssert("Verifying response"); + Assert.NotNull(result); + } + } +} diff --git a/Contentstack.Core.Unit.Tests/TaxonomyUnitTests.cs b/Contentstack.Core.Unit.Tests/TaxonomyUnitTests.cs index f4f5e92..ac641d7 100644 --- a/Contentstack.Core.Unit.Tests/TaxonomyUnitTests.cs +++ b/Contentstack.Core.Unit.Tests/TaxonomyUnitTests.cs @@ -563,6 +563,169 @@ public void Below_WithZeroValue_AddsQueryParameter() } #endregion + + #region Taxonomy UID Constructor Tests + + [Fact] + public void Taxonomy_WithUid_DoesNotThrow() + { + var taxonomy = _client.Taxonomies("gadgets"); + Assert.NotNull(taxonomy); + } + + [Fact] + public void Taxonomy_WithNullUid_ThrowsTaxonomyException() + { + Assert.Throws(() => _client.Taxonomies(null)); + } + + [Fact] + public void Taxonomy_WithEmptyUid_ThrowsTaxonomyException() + { + Assert.Throws(() => _client.Taxonomies(string.Empty)); + } + + #endregion + + #region Taxonomy.Term() Tests + + [Fact] + public void Taxonomy_TermWithUid_ReturnsTaxonomyInstance() + { + var taxonomy = _client.Taxonomies("gadgets"); + var term = taxonomy.Term("smartwatch"); + Assert.NotNull(term); + Assert.IsType(term); + } + + [Fact] + public void Taxonomy_TermWithoutUid_ThrowsTaxonomyException() + { + var taxonomy = _client.Taxonomies(); + Assert.Throws(() => taxonomy.Term("smartwatch")); + } + + #endregion + + #region Taxonomy.Terms() Tests + + [Fact] + public void Taxonomy_Terms_ReturnsTermQueryInstance() + { + var taxonomy = _client.Taxonomies("gadgets"); + var termQuery = taxonomy.Terms(); + Assert.NotNull(termQuery); + Assert.IsType(termQuery); + } + + [Fact] + public void Taxonomy_Terms_WithoutUid_ThrowsTaxonomyException() + { + var taxonomy = _client.Taxonomies(); + Assert.Throws(() => taxonomy.Terms()); + } + + #endregion + + #region TermQuery Tests + + [Fact] + public void TermQuery_SetLocale_ReturnsSelfForChaining() + { + var termQuery = _client.Taxonomies("gadgets").Terms(); + var result = termQuery.SetLocale("hi-in"); + Assert.Same(termQuery, result); + } + + [Fact] + public void TermQuery_SetLocale_SetsLocaleParam() + { + var termQuery = _client.Taxonomies("gadgets").Terms(); + termQuery.SetLocale("hi-in"); + + var field = typeof(TermQuery).GetField("_queryParams", + BindingFlags.NonPublic | BindingFlags.Instance); + var queryParams = (Dictionary)field?.GetValue(termQuery); + + Assert.True(queryParams?.ContainsKey("locale") ?? false); + Assert.Equal("hi-in", queryParams["locale"]); + } + + [Fact] + public void TermQuery_SetLocale_WithNull_ThrowsTaxonomyException() + { + var termQuery = _client.Taxonomies("gadgets").Terms(); + Assert.Throws(() => termQuery.SetLocale(null)); + } + + [Fact] + public void TermQuery_IncludeFallback_ReturnsSelfForChaining() + { + var termQuery = _client.Taxonomies("gadgets").Terms(); + var result = termQuery.IncludeFallback(); + Assert.Same(termQuery, result); + } + + [Fact] + public void TermQuery_IncludeFallback_SetsParam() + { + var termQuery = _client.Taxonomies("gadgets").Terms(); + termQuery.IncludeFallback(); + + var field = typeof(TermQuery).GetField("_queryParams", + BindingFlags.NonPublic | BindingFlags.Instance); + var queryParams = (Dictionary)field?.GetValue(termQuery); + + Assert.True(queryParams?.ContainsKey("include_fallback") ?? false); + Assert.Equal("true", queryParams["include_fallback"]); + } + + [Fact] + public void TermQuery_SetLocale_Then_IncludeFallback_ChainsBoth() + { + var termQuery = _client.Taxonomies("gadgets").Terms() + .SetLocale("hi-in") + .IncludeFallback(); + + var field = typeof(TermQuery).GetField("_queryParams", + BindingFlags.NonPublic | BindingFlags.Instance); + var queryParams = (Dictionary)field?.GetValue(termQuery); + + Assert.True(queryParams?.ContainsKey("locale") ?? false); + Assert.True(queryParams?.ContainsKey("include_fallback") ?? false); + } + + #endregion + + #region Term Constructor Validation Tests + + [Fact] + public void Term_WithNullTaxonomyUid_ThrowsTaxonomyException() + { + Assert.Throws(() => + { + var t = typeof(Term) + .GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, + new[] { typeof(ContentstackClient), typeof(string), typeof(string) }, null); + try { t?.Invoke(new object[] { _client, null, "smartwatch" }); } + catch (System.Reflection.TargetInvocationException ex) { throw ex.InnerException; } + }); + } + + [Fact] + public void Term_WithNullTermUid_ThrowsTaxonomyException() + { + Assert.Throws(() => + { + var t = typeof(Term) + .GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, + new[] { typeof(ContentstackClient), typeof(string), typeof(string) }, null); + try { t?.Invoke(new object[] { _client, "gadgets", null }); } + catch (System.Reflection.TargetInvocationException ex) { throw ex.InnerException; } + }); + } + + #endregion } } diff --git a/Contentstack.Core/ContentstackClient.cs b/Contentstack.Core/ContentstackClient.cs index 3e2b390..f428528 100644 --- a/Contentstack.Core/ContentstackClient.cs +++ b/Contentstack.Core/ContentstackClient.cs @@ -491,6 +491,25 @@ public Taxonomy Taxonomies() return tx; } + /// + /// Returns a instance scoped to the given taxonomy UID, + /// providing access to published taxonomy data, terms, and locale-aware delivery via the CDA. + /// + /// The UID of the published taxonomy. + /// A instance scoped to the given UID. + /// + /// + /// ContentstackClient stack = new ContentstackClient("api_key", "delivery_token", "environment"); + /// var taxonomy = await stack.Taxonomies("gadgets").Fetch<MyTaxonomy>(); + /// var terms = await stack.Taxonomies("gadgets").Terms().SetLocale("hi-in").Find<MyTerm>(); + /// var term = await stack.Taxonomies("gadgets").Term("smartwatch").Fetch<MyTerm>("mr-in"); + /// + /// + public Taxonomy Taxonomies(string uid) + { + return new Taxonomy(this, uid); + } + /// /// Get version. /// diff --git a/Contentstack.Core/Models/Taxonomy.cs b/Contentstack.Core/Models/Taxonomy.cs index 667915d..ba17794 100644 --- a/Contentstack.Core/Models/Taxonomy.cs +++ b/Contentstack.Core/Models/Taxonomy.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Net; +using System.Threading.Tasks; using Contentstack.Core.Configuration; using Contentstack.Core.Internals; namespace Contentstack.Core.Models @@ -14,6 +15,7 @@ public class Taxonomy: Query private Dictionary _Headers = new Dictionary(); private Dictionary _StackHeaders = new Dictionary(); private Dictionary UrlQueries = new Dictionary(); + private string _uid = null; protected override string _Url { @@ -28,6 +30,8 @@ protected override string _Url throw new TaxonomyException("Taxonomy Stack Config is null. Please ensure the ContentstackClient is properly configured."); } Config config = this.Stack.Config; + if (_uid != null) + return String.Format("{0}/taxonomies/{1}", config.BaseUrl, _uid); return String.Format("{0}/taxonomies/entries", config.BaseUrl); } } @@ -54,9 +58,92 @@ internal Taxonomy(ContentstackClient stack): base(stack) this._StackHeaders = stack._LocalHeaders; } + internal Taxonomy(ContentstackClient stack, string uid) : this(stack) + { + if (string.IsNullOrEmpty(uid)) + throw new TaxonomyException("Taxonomy UID cannot be null or empty."); + _uid = uid; + } + #endregion #region Public Functions + /// + /// Returns a Term instance for the given term UID within this taxonomy. + /// Requires the Taxonomy to be initialised with a UID via client.Taxonomies("uid"). + /// + /// The UID of the term to retrieve. + /// A instance scoped to this taxonomy and term. + public Term Term(string termUid) + { + if (_uid == null) + throw new TaxonomyException("Term() requires a taxonomy UID. Use client.Taxonomies(\"uid\") to scope to a specific taxonomy."); + return new Term(Stack, _uid, termUid); + } + + /// + /// Returns a TermQuery for listing all published terms within this taxonomy. + /// Requires the Taxonomy to be initialised with a UID via client.Taxonomies("uid"). + /// + /// A instance for this taxonomy. + public TermQuery Terms() + { + if (_uid == null) + throw new TaxonomyException("Terms() requires a taxonomy UID. Use client.Taxonomies(\"uid\") to scope to a specific taxonomy."); + return new TermQuery(Stack, _uid); + } + + /// + /// Fetches the published taxonomy by its UID from the CDA. + /// Requires the Taxonomy to be initialised with a UID via client.Taxonomies("uid"). + /// + /// Optional locale code (e.g. "hi-in"). Omit for the master locale. + /// The deserialized taxonomy object. + /// + /// + /// ContentstackClient stack = new ContentstackClient("api_key", "delivery_token", "environment"); + /// var taxonomy = await stack.Taxonomies("gadgets").Fetch<MyTaxonomy>(); + /// var localized = await stack.Taxonomies("gadgets").Fetch<MyTaxonomy>("hi-in"); + /// + /// + public async System.Threading.Tasks.Task Fetch(string locale = null) + { + if (_uid == null) + throw new TaxonomyException("Fetch() requires a taxonomy UID. Use client.Taxonomies(\"uid\") to scope to a specific taxonomy."); + + try + { + var headerAll = new Dictionary(); + foreach (var header in Stack._LocalHeaders) + headerAll[header.Key] = header.Value; + + var mainJson = new Dictionary(); + if (Stack.Config?.Environment != null) + mainJson["environment"] = Stack.Config.Environment; + if (!string.IsNullOrEmpty(locale)) + mainJson["locale"] = locale; + + var handler = new HttpRequestHandler(Stack); + var branch = Stack.Config?.Branch ?? "main"; + var result = await handler.ProcessRequest( + _Url, headerAll, mainJson, + Branch: branch, + timeout: Stack.Config.Timeout, + proxy: Stack.Config.Proxy + ); + + var jObject = Newtonsoft.Json.Linq.JObject.Parse(result); + var token = jObject.SelectToken("$.taxonomy"); + if (token != null) + return token.ToObject(Stack.Serializer); + return jObject.ToObject(Stack.Serializer); + } + catch (Exception ex) + { + throw TaxonomyException.CreateForProcessingError(ex); + } + } + /// /// Add a constraint to the query that requires a particular key entry to be less than the provided value. /// diff --git a/Contentstack.Core/Models/Term.cs b/Contentstack.Core/Models/Term.cs new file mode 100644 index 0000000..d45864f --- /dev/null +++ b/Contentstack.Core/Models/Term.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Contentstack.Core.Internals; +using Newtonsoft.Json.Linq; + +namespace Contentstack.Core.Models +{ + /// + /// Represents a single published term within a taxonomy, providing methods to fetch + /// the term, its localized versions, ancestors, and descendants from the CDA. + /// + public class Term + { + private readonly ContentstackClient _stack; + private readonly string _taxonomyUid; + private readonly string _termUid; + + private string BaseUrlPath => + $"{_stack.Config.BaseUrl}/taxonomies/{_taxonomyUid}/terms/{_termUid}"; + + internal Term(ContentstackClient stack, string taxonomyUid, string termUid) + { + _stack = stack ?? throw new TaxonomyException("ContentstackClient cannot be null when creating a Term instance."); + if (string.IsNullOrEmpty(taxonomyUid)) throw new TaxonomyException("taxonomyUid cannot be null or empty."); + if (string.IsNullOrEmpty(termUid)) throw new TaxonomyException("termUid cannot be null or empty."); + _taxonomyUid = taxonomyUid; + _termUid = termUid; + } + + /// + /// Fetches the published term from the CDA. + /// + /// Optional locale code (e.g. "mr-in"). Omit for the master locale. + /// The deserialized term object. + /// + /// + /// var term = await stack.Taxonomies("gadgets").Term("smartwatch").Fetch<MyTerm>(); + /// var localizedTerm = await stack.Taxonomies("gadgets").Term("smartwatch").Fetch<MyTerm>("mr-in"); + /// + /// + public async Task Fetch(string locale = null) + { + try + { + var queryParams = new Dictionary(); + if (!string.IsNullOrEmpty(locale)) + queryParams["locale"] = locale; + + var result = await ExecuteRequest(BaseUrlPath, queryParams); + var jObject = JObject.Parse(result); + var token = jObject.SelectToken("$.term"); + if (token != null) + return token.ToObject(_stack.Serializer); + return jObject.ToObject(_stack.Serializer); + } + catch (TaxonomyException) + { + throw; + } + catch (Exception ex) + { + throw TaxonomyException.CreateForProcessingError(ex); + } + } + + /// + /// Fetches all published, localized versions of this term across every locale. + /// Maps to GET /taxonomies/{uid}/terms/{termUid}/locales. + /// + /// The deserialized locales collection. + /// + /// + /// var locales = await stack.Taxonomies("gadgets").Term("smartwatch").Locales<MyLocales>(); + /// + /// + public async Task Locales() + { + try + { + var result = await ExecuteRequest($"{BaseUrlPath}/locales"); + var jObject = JObject.Parse(result); + var token = jObject.SelectToken("$.locales"); + if (token != null) + return token.ToObject(_stack.Serializer); + return jObject.ToObject(_stack.Serializer); + } + catch (TaxonomyException) + { + throw; + } + catch (Exception ex) + { + throw TaxonomyException.CreateForProcessingError(ex); + } + } + + /// + /// Fetches all ancestors of this term up to the root. + /// Maps to GET /taxonomies/{uid}/terms/{termUid}/ancestors. + /// + /// The deserialized ancestors collection. + /// + /// + /// var ancestors = await stack.Taxonomies("gadgets").Term("smartwatch").Ancestors<MyTerms>(); + /// + /// + public async Task Ancestors() + { + try + { + var result = await ExecuteRequest($"{BaseUrlPath}/ancestors"); + var jObject = JObject.Parse(result); + var token = jObject.SelectToken("$.ancestors"); + if (token != null) + return token.ToObject(_stack.Serializer); + return jObject.ToObject(_stack.Serializer); + } + catch (TaxonomyException) + { + throw; + } + catch (Exception ex) + { + throw TaxonomyException.CreateForProcessingError(ex); + } + } + + /// + /// Fetches all descendants of this term. + /// Maps to GET /taxonomies/{uid}/terms/{termUid}/descendants. + /// + /// The deserialized descendants collection. + /// + /// + /// var descendants = await stack.Taxonomies("gadgets").Term("smartwatch").Descendants<MyTerms>(); + /// + /// + public async Task Descendants() + { + try + { + var result = await ExecuteRequest($"{BaseUrlPath}/descendants"); + var jObject = JObject.Parse(result); + var token = jObject.SelectToken("$.descendants"); + if (token != null) + return token.ToObject(_stack.Serializer); + return jObject.ToObject(_stack.Serializer); + } + catch (TaxonomyException) + { + throw; + } + catch (Exception ex) + { + throw TaxonomyException.CreateForProcessingError(ex); + } + } + + private async Task ExecuteRequest(string url, Dictionary extraParams = null) + { + var headerAll = new Dictionary(); + foreach (var header in _stack._LocalHeaders) + headerAll[header.Key] = header.Value; + + var mainJson = new Dictionary(); + if (_stack.Config?.Environment != null) + mainJson["environment"] = _stack.Config.Environment; + + if (extraParams != null) + foreach (var param in extraParams) + mainJson[param.Key] = param.Value; + + var handler = new HttpRequestHandler(_stack); + var branch = _stack.Config?.Branch ?? "main"; + return await handler.ProcessRequest( + url, headerAll, mainJson, + Branch: branch, + timeout: _stack.Config.Timeout, + proxy: _stack.Config.Proxy + ); + } + } +} diff --git a/Contentstack.Core/Models/TermQuery.cs b/Contentstack.Core/Models/TermQuery.cs new file mode 100644 index 0000000..d1d0876 --- /dev/null +++ b/Contentstack.Core/Models/TermQuery.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Contentstack.Core.Internals; +using Newtonsoft.Json.Linq; + +namespace Contentstack.Core.Models +{ + /// + /// Provides a fluent query builder for listing published terms within a taxonomy from the CDA. + /// Supports locale filtering and master-locale fallback. + /// + public class TermQuery + { + private readonly ContentstackClient _stack; + private readonly string _taxonomyUid; + private readonly Dictionary _queryParams = new Dictionary(); + + private string Url => + $"{_stack.Config.BaseUrl}/taxonomies/{_taxonomyUid}/terms"; + + internal TermQuery(ContentstackClient stack, string taxonomyUid) + { + _stack = stack ?? throw new TaxonomyException("ContentstackClient cannot be null when creating a TermQuery instance."); + if (string.IsNullOrEmpty(taxonomyUid)) throw new TaxonomyException("taxonomyUid cannot be null or empty."); + _taxonomyUid = taxonomyUid; + } + + /// + /// Filters terms to those published in the specified locale. + /// + /// The locale code (e.g. "hi-in", "en-us"). + /// The current for chaining. + /// + /// + /// var terms = await stack.Taxonomies("gadgets").Terms().SetLocale("hi-in").Find<MyTerm>(); + /// + /// + public TermQuery SetLocale(string locale) + { + if (string.IsNullOrEmpty(locale)) + throw new TaxonomyException("Locale cannot be null or empty."); + _queryParams["locale"] = locale; + return this; + } + + /// + /// When a term is not localized in the requested locale, falls back to the master locale. + /// Must be used together with . + /// + /// The current for chaining. + /// + /// + /// var terms = await stack.Taxonomies("gadgets").Terms().SetLocale("hi-in").IncludeFallback().Find<MyTerm>(); + /// + /// + public TermQuery IncludeFallback() + { + _queryParams["include_fallback"] = "true"; + return this; + } + + /// + /// Executes the query and returns all matching published terms. + /// Maps to GET /taxonomies/{uid}/terms with any configured locale and fallback params. + /// + /// A containing the matched terms. + /// + /// + /// var result = await stack.Taxonomies("gadgets").Terms().SetLocale("hi-in").IncludeFallback().Find<MyTerm>(); + /// foreach (var term in result) { ... } + /// + /// + public async Task> Find() + { + try + { + var headerAll = new Dictionary(); + foreach (var header in _stack._LocalHeaders) + headerAll[header.Key] = header.Value; + + var mainJson = new Dictionary(); + if (_stack.Config?.Environment != null) + mainJson["environment"] = _stack.Config.Environment; + + foreach (var param in _queryParams) + mainJson[param.Key] = param.Value; + + var handler = new HttpRequestHandler(_stack); + var branch = _stack.Config?.Branch ?? "main"; + var result = await handler.ProcessRequest( + Url, headerAll, mainJson, + Branch: branch, + timeout: _stack.Config.Timeout, + proxy: _stack.Config.Proxy + ); + + var jObject = JObject.Parse(result); + var terms = jObject.SelectToken("$.terms")?.ToObject>(_stack.Serializer); + var collection = jObject.ToObject>(_stack.Serializer); + collection.Items = terms ?? new List(); + return collection; + } + catch (TaxonomyException) + { + throw; + } + catch (Exception ex) + { + throw TaxonomyException.CreateForProcessingError(ex); + } + } + } +}