From 3e4eac475ff542bdd1da60a72c2bb7d9bf6860a8 Mon Sep 17 00:00:00 2001 From: OMpawar-21 Date: Tue, 4 Aug 2026 12:20:06 +0530 Subject: [PATCH] feat(DX-9678): add taxonomy publish/unpublish/unlocalize and term unlocalize MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds six new methods to the .NET management SDK taxonomy/terms surfaces: - Taxonomy.Publish() / Unpublish() — POST /taxonomies/publish|unpublish (collection-level) - Taxonomy.Unlocalize() — DELETE /taxonomies/{uid}?locale=... (instance-level) - Term.Unlocalize() — DELETE /taxonomies/{uid}/terms/{uid}?locale=... - Taxonomy.Localize() / Term.Localize() already existed; Unlocalize completes the pair New: TaxonomyPublishDetails model (with Items list), TaxonomyPublishService (serializes body directly without field wrapper, unlike PublishUnpublishService). All sync + async variants covered. 46 unit tests pass. Co-Authored-By: Claude Sonnet 4.6 --- .../Models/TaxonomyTest.cs | 103 ++++++++++++++++++ .../Models/TermTest.cs | 52 +++++++++ .../Models/Taxonomy.cs | 70 ++++++++++++ .../Models/TaxonomyPublishDetails.cs | 32 ++++++ Contentstack.Management.Core/Models/Term.cs | 26 +++++ .../Services/Models/TaxonomyPublishService.cs | 34 ++++++ 6 files changed, 317 insertions(+) create mode 100644 Contentstack.Management.Core/Models/TaxonomyPublishDetails.cs create mode 100644 Contentstack.Management.Core/Services/Models/TaxonomyPublishService.cs diff --git a/Contentstack.Management.Core.Unit.Tests/Models/TaxonomyTest.cs b/Contentstack.Management.Core.Unit.Tests/Models/TaxonomyTest.cs index 66f7240..1b824b3 100644 --- a/Contentstack.Management.Core.Unit.Tests/Models/TaxonomyTest.cs +++ b/Contentstack.Management.Core.Unit.Tests/Models/TaxonomyTest.cs @@ -194,5 +194,108 @@ public void Localize_When_Api_Returns_400_Returns_Unsuccessful_Response() Assert.IsFalse(response.IsSuccessStatusCode); Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode); } + + [TestMethod] + public void Publish_Throws_When_Uid_Is_Set() + { + var details = new TaxonomyPublishDetails { Locales = new System.Collections.Generic.List { "en-us" } }; + Assert.ThrowsException(() => _stack.Taxonomy(_fixture.Create()).Publish(details)); + Assert.ThrowsExceptionAsync(() => _stack.Taxonomy(_fixture.Create()).PublishAsync(details)); + } + + [TestMethod] + public void Unpublish_Throws_When_Uid_Is_Set() + { + var details = new TaxonomyPublishDetails { Locales = new System.Collections.Generic.List { "en-us" } }; + Assert.ThrowsException(() => _stack.Taxonomy(_fixture.Create()).Unpublish(details)); + Assert.ThrowsExceptionAsync(() => _stack.Taxonomy(_fixture.Create()).UnpublishAsync(details)); + } + + [TestMethod] + public void Unlocalize_Throws_When_Uid_Is_Empty() + { + Assert.ThrowsException(() => _stack.Taxonomy().Unlocalize("hi-in")); + Assert.ThrowsExceptionAsync(() => _stack.Taxonomy().UnlocalizeAsync("hi-in")); + } + + [TestMethod] + public void Should_Publish_Taxonomy() + { + var details = new TaxonomyPublishDetails + { + Locales = new System.Collections.Generic.List { "en-us" }, + Environments = new System.Collections.Generic.List { "production" }, + Items = new System.Collections.Generic.List { new TaxonomyPublishItem { Uid = "tax_uid" } } + }; + ContentstackResponse response = _stack.Taxonomy().Publish(details); + + Assert.AreEqual(_contentstackResponse.OpenResponse(), response.OpenResponse()); + Assert.AreEqual(_contentstackResponse.OpenJsonObjectResponse().ToString(), response.OpenJsonObjectResponse().ToString()); + } + + [TestMethod] + public async System.Threading.Tasks.Task Should_Publish_Taxonomy_Async() + { + var details = new TaxonomyPublishDetails + { + Locales = new System.Collections.Generic.List { "en-us" }, + Environments = new System.Collections.Generic.List { "production" }, + Items = new System.Collections.Generic.List { new TaxonomyPublishItem { Uid = "tax_uid" } } + }; + ContentstackResponse response = await _stack.Taxonomy().PublishAsync(details); + + Assert.AreEqual(_contentstackResponse.OpenResponse(), response.OpenResponse()); + Assert.AreEqual(_contentstackResponse.OpenJsonObjectResponse().ToString(), response.OpenJsonObjectResponse().ToString()); + } + + [TestMethod] + public void Should_Unpublish_Taxonomy() + { + var details = new TaxonomyPublishDetails + { + Locales = new System.Collections.Generic.List { "en-us" }, + Environments = new System.Collections.Generic.List { "production" }, + Items = new System.Collections.Generic.List { new TaxonomyPublishItem { Uid = "tax_uid" } } + }; + ContentstackResponse response = _stack.Taxonomy().Unpublish(details); + + Assert.AreEqual(_contentstackResponse.OpenResponse(), response.OpenResponse()); + Assert.AreEqual(_contentstackResponse.OpenJsonObjectResponse().ToString(), response.OpenJsonObjectResponse().ToString()); + } + + [TestMethod] + public async System.Threading.Tasks.Task Should_Unpublish_Taxonomy_Async() + { + var details = new TaxonomyPublishDetails + { + Locales = new System.Collections.Generic.List { "en-us" }, + Environments = new System.Collections.Generic.List { "production" }, + Items = new System.Collections.Generic.List { new TaxonomyPublishItem { Uid = "tax_uid" } } + }; + ContentstackResponse response = await _stack.Taxonomy().UnpublishAsync(details); + + Assert.AreEqual(_contentstackResponse.OpenResponse(), response.OpenResponse()); + Assert.AreEqual(_contentstackResponse.OpenJsonObjectResponse().ToString(), response.OpenJsonObjectResponse().ToString()); + } + + [TestMethod] + public void Should_Unlocalize_Taxonomy() + { + string uid = _fixture.Create(); + ContentstackResponse response = _stack.Taxonomy(uid).Unlocalize("hi-in"); + + Assert.AreEqual(_contentstackResponse.OpenResponse(), response.OpenResponse()); + Assert.AreEqual(_contentstackResponse.OpenJsonObjectResponse().ToString(), response.OpenJsonObjectResponse().ToString()); + } + + [TestMethod] + public async System.Threading.Tasks.Task Should_Unlocalize_Taxonomy_Async() + { + string uid = _fixture.Create(); + ContentstackResponse response = await _stack.Taxonomy(uid).UnlocalizeAsync("hi-in"); + + Assert.AreEqual(_contentstackResponse.OpenResponse(), response.OpenResponse()); + Assert.AreEqual(_contentstackResponse.OpenJsonObjectResponse().ToString(), response.OpenJsonObjectResponse().ToString()); + } } } diff --git a/Contentstack.Management.Core.Unit.Tests/Models/TermTest.cs b/Contentstack.Management.Core.Unit.Tests/Models/TermTest.cs index 7191318..d1c82ca 100644 --- a/Contentstack.Management.Core.Unit.Tests/Models/TermTest.cs +++ b/Contentstack.Management.Core.Unit.Tests/Models/TermTest.cs @@ -174,5 +174,57 @@ public void Localize_Throws_When_Term_Uid_Is_Empty() Assert.ThrowsException(() => _stack.Taxonomy(taxonomyUid).Terms().Localize(_fixture.Create())); Assert.ThrowsExceptionAsync(() => _stack.Taxonomy(taxonomyUid).Terms().LocalizeAsync(_fixture.Create())); } + + [TestMethod] + public void Unlocalize_Throws_When_Term_Uid_Is_Empty() + { + string taxonomyUid = _fixture.Create(); + Assert.ThrowsException(() => _stack.Taxonomy(taxonomyUid).Terms().Unlocalize("hi-in")); + Assert.ThrowsExceptionAsync(() => _stack.Taxonomy(taxonomyUid).Terms().UnlocalizeAsync("hi-in")); + } + + [TestMethod] + public void Should_Unlocalize_Term() + { + string taxonomyUid = _fixture.Create(); + string termUid = _fixture.Create(); + ContentstackResponse response = _stack.Taxonomy(taxonomyUid).Terms(termUid).Unlocalize("hi-in"); + + Assert.AreEqual(_contentstackResponse.OpenResponse(), response.OpenResponse()); + Assert.AreEqual(_contentstackResponse.OpenJsonObjectResponse().ToString(), response.OpenJsonObjectResponse().ToString()); + } + + [TestMethod] + public async System.Threading.Tasks.Task Should_Unlocalize_Term_Async() + { + string taxonomyUid = _fixture.Create(); + string termUid = _fixture.Create(); + ContentstackResponse response = await _stack.Taxonomy(taxonomyUid).Terms(termUid).UnlocalizeAsync("hi-in"); + + Assert.AreEqual(_contentstackResponse.OpenResponse(), response.OpenResponse()); + Assert.AreEqual(_contentstackResponse.OpenJsonObjectResponse().ToString(), response.OpenJsonObjectResponse().ToString()); + } + + [TestMethod] + public void Should_Localize_Term() + { + string taxonomyUid = _fixture.Create(); + string termUid = _fixture.Create(); + ContentstackResponse response = _stack.Taxonomy(taxonomyUid).Terms(termUid).Localize(_fixture.Create()); + + Assert.AreEqual(_contentstackResponse.OpenResponse(), response.OpenResponse()); + Assert.AreEqual(_contentstackResponse.OpenJsonObjectResponse().ToString(), response.OpenJsonObjectResponse().ToString()); + } + + [TestMethod] + public async System.Threading.Tasks.Task Should_Localize_Term_Async() + { + string taxonomyUid = _fixture.Create(); + string termUid = _fixture.Create(); + ContentstackResponse response = await _stack.Taxonomy(taxonomyUid).Terms(termUid).LocalizeAsync(_fixture.Create()); + + Assert.AreEqual(_contentstackResponse.OpenResponse(), response.OpenResponse()); + Assert.AreEqual(_contentstackResponse.OpenJsonObjectResponse().ToString(), response.OpenJsonObjectResponse().ToString()); + } } } diff --git a/Contentstack.Management.Core/Models/Taxonomy.cs b/Contentstack.Management.Core/Models/Taxonomy.cs index 3614312..1daa969 100644 --- a/Contentstack.Management.Core/Models/Taxonomy.cs +++ b/Contentstack.Management.Core/Models/Taxonomy.cs @@ -160,6 +160,76 @@ public Task LocalizeAsync(TaxonomyModel model, ParameterCo return stack.client.InvokeAsync, ContentstackResponse>(service); } + /// + /// Publish one or more taxonomies. POST /taxonomies/publish (collection-level, no uid required). + /// + public ContentstackResponse Publish(TaxonomyPublishDetails details, ParameterCollection? collection = null) + { + stack.ThrowIfNotLoggedIn(); + ThrowIfUidNotEmpty(); + var service = new TaxonomyPublishService(stack, details, "/taxonomies/publish", collection); + return stack.client.InvokeSync(service); + } + + /// + /// Publish one or more taxonomies asynchronously. + /// + public Task PublishAsync(TaxonomyPublishDetails details, ParameterCollection? collection = null) + { + stack.ThrowIfNotLoggedIn(); + ThrowIfUidNotEmpty(); + var service = new TaxonomyPublishService(stack, details, "/taxonomies/publish", collection); + return stack.client.InvokeAsync(service); + } + + /// + /// Unpublish one or more taxonomies. POST /taxonomies/unpublish (collection-level, no uid required). + /// + public ContentstackResponse Unpublish(TaxonomyPublishDetails details, ParameterCollection? collection = null) + { + stack.ThrowIfNotLoggedIn(); + ThrowIfUidNotEmpty(); + var service = new TaxonomyPublishService(stack, details, "/taxonomies/unpublish", collection); + return stack.client.InvokeSync(service); + } + + /// + /// Unpublish one or more taxonomies asynchronously. + /// + public Task UnpublishAsync(TaxonomyPublishDetails details, ParameterCollection? collection = null) + { + stack.ThrowIfNotLoggedIn(); + ThrowIfUidNotEmpty(); + var service = new TaxonomyPublishService(stack, details, "/taxonomies/unpublish", collection); + return stack.client.InvokeAsync(service); + } + + /// + /// Unlocalize a taxonomy from the given locale. DELETE /taxonomies/{uid}?locale=... + /// + public ContentstackResponse Unlocalize(string locale, ParameterCollection? collection = null) + { + stack.ThrowIfNotLoggedIn(); + ThrowIfUidEmpty(); + var coll = collection ?? new ParameterCollection(); + coll.Add("locale", locale); + var service = new FetchDeleteService(stack, resourcePath, "DELETE", coll); + return stack.client.InvokeSync(service); + } + + /// + /// Unlocalize a taxonomy asynchronously. + /// + public Task UnlocalizeAsync(string locale, ParameterCollection? collection = null) + { + stack.ThrowIfNotLoggedIn(); + ThrowIfUidEmpty(); + var coll = collection ?? new ParameterCollection(); + coll.Add("locale", locale); + var service = new FetchDeleteService(stack, resourcePath, "DELETE", coll); + return stack.client.InvokeAsync(service); + } + /// /// Import taxonomy. POST /taxonomies/import with multipart form (taxonomy file). /// diff --git a/Contentstack.Management.Core/Models/TaxonomyPublishDetails.cs b/Contentstack.Management.Core/Models/TaxonomyPublishDetails.cs new file mode 100644 index 0000000..9a26506 --- /dev/null +++ b/Contentstack.Management.Core/Models/TaxonomyPublishDetails.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Contentstack.Management.Core.Models +{ + /// + /// Details for publishing or unpublishing one or more taxonomies. + /// + public class TaxonomyPublishDetails + { + [JsonPropertyName("locales")] + public List? Locales { get; set; } + + [JsonPropertyName("environments")] + public List? Environments { get; set; } + + [JsonPropertyName("scheduled_at")] + public string? ScheduledAt { get; set; } + + [JsonPropertyName("items")] + public List? Items { get; set; } + } + + /// + /// Identifies a taxonomy to publish or unpublish by UID. + /// + public class TaxonomyPublishItem + { + [JsonPropertyName("uid")] + public string Uid { get; set; } = null!; + } +} diff --git a/Contentstack.Management.Core/Models/Term.cs b/Contentstack.Management.Core/Models/Term.cs index 4b00ecc..3ccee73 100644 --- a/Contentstack.Management.Core/Models/Term.cs +++ b/Contentstack.Management.Core/Models/Term.cs @@ -204,6 +204,32 @@ public Task LocalizeAsync(TermModel model, ParameterCollec return stack.client.InvokeAsync, ContentstackResponse>(service); } + /// + /// Unlocalize a term from the given locale. DELETE /taxonomies/{uid}/terms/{term_uid}?locale=... + /// + public ContentstackResponse Unlocalize(string locale, ParameterCollection? collection = null) + { + stack.ThrowIfNotLoggedIn(); + ThrowIfUidEmpty(); + var coll = collection ?? new ParameterCollection(); + coll.Add("locale", locale); + var service = new FetchDeleteService(stack, resourcePath, "DELETE", coll); + return stack.client.InvokeSync(service); + } + + /// + /// Unlocalize a term asynchronously. + /// + public Task UnlocalizeAsync(string locale, ParameterCollection? collection = null) + { + stack.ThrowIfNotLoggedIn(); + ThrowIfUidEmpty(); + var coll = collection ?? new ParameterCollection(); + coll.Add("locale", locale); + var service = new FetchDeleteService(stack, resourcePath, "DELETE", coll); + return stack.client.InvokeAsync(service); + } + /// /// Search terms across all taxonomies. GET /taxonomies/$all/terms with typeahead query param. Callable only when no specific term UID is set. /// diff --git a/Contentstack.Management.Core/Services/Models/TaxonomyPublishService.cs b/Contentstack.Management.Core/Services/Models/TaxonomyPublishService.cs new file mode 100644 index 0000000..db1cd0a --- /dev/null +++ b/Contentstack.Management.Core/Services/Models/TaxonomyPublishService.cs @@ -0,0 +1,34 @@ +using System; +using System.Text.Json; +using Contentstack.Management.Core.Models; +using Contentstack.Management.Core.Queryable; +using Contentstack.Management.Core.Utils; + +namespace Contentstack.Management.Core.Services.Models +{ + internal class TaxonomyPublishService : ContentstackService + { + private readonly TaxonomyPublishDetails _details; + + internal TaxonomyPublishService(Core.Models.Stack stack, TaxonomyPublishDetails details, string resourcePath, ParameterCollection? collection = null, JsonSerializerOptions? stjOptions = null) + : base(stjOptions ?? stack?.client?.SerializerOptions ?? new JsonSerializerOptions(), stack: stack, collection: collection) + { + if (stack!.APIKey == null) + throw new ArgumentNullException("stack", CSConstants.MissingAPIKey); + if (details == null) + throw new ArgumentNullException("details", CSConstants.PublishDetailsRequired); + if (resourcePath == null) + throw new ArgumentNullException("resourcePath", CSConstants.ResourcePathRequired); + + this.ResourcePath = resourcePath; + this.HttpMethod = "POST"; + _details = details; + } + + public override void ContentBody() + { + string jsonString = JsonSerializer.Serialize(_details, SerializerOptions); + this.ByteContent = System.Text.Encoding.UTF8.GetBytes(jsonString); + } + } +}