Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


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

## [v0.7.0](https://github.com/contentstack/contentstack-management-dotnet/tree/v0.7.0)
- Feat
- **Bulk publish/unpublish: query parameters (DX-3233)**
- `skip_workflow_stage_check` and `approvals` are now sent as query parameters instead of headers for bulk publish and bulk unpublish
- Unit tests updated to assert on `QueryResources` for these flags (BulkPublishServiceTest, BulkUnpublishServiceTest, BulkOperationServicesTest)
- Integration tests: bulk publish with skipWorkflowStage and approvals (Test003a), bulk unpublish with skipWorkflowStage and approvals (Test004a), and helper `EnsureBulkTestContentTypeAndEntriesAsync()` so bulk tests can run in any order

## [v0.6.1](https://github.com/contentstack/contentstack-management-dotnet/tree/v0.6.1) (2026-02-02)
- Fix
- Release DELETE request no longer includes Content-Type header to comply with API requirements
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<TargetFramework>net7.0</TargetFramework>

<IsPackable>false</IsPackable>
<ReleaseVersion>$(Version)</ReleaseVersion>
<ReleaseVersion>0.1.3</ReleaseVersion>

<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>../CSManagementSDK.snk</AssemblyOriginatorKeyFile>
Expand All @@ -24,6 +24,7 @@
<PackageReference Include="AutoFixture.AutoMoq" Version="4.18.1" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.3.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
</ItemGroup>

<ItemGroup>
Expand All @@ -35,11 +36,6 @@
<ItemGroup>
<EmbeddedResource Include="Mock\*.json" />
</ItemGroup>
<ItemGroup>
<Content Include="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<None Remove="Microsoft.AspNetCore.Http" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Contentstack.Management.Core.Exceptions;
using Contentstack.Management.Core.Models;
using Contentstack.Management.Core.Models.Fields;
using Contentstack.Management.Core.Tests.Model;
Expand All @@ -21,6 +23,17 @@ public class Contentstack015_BulkOperationTest
private string _testReleaseUid = "bulk_test_release";
private List<EntryInfo> _createdEntries = new List<EntryInfo>();

/// <summary>
/// Fails the test with a clear message from ContentstackErrorException or generic exception.
/// </summary>
private static void FailWithError(string operation, Exception ex)
{
if (ex is ContentstackErrorException cex)
Assert.Fail($"{operation} failed. HTTP {(int)cex.StatusCode} ({cex.StatusCode}). ErrorCode: {cex.ErrorCode}. Message: {cex.ErrorMessage ?? cex.Message}");
else
Assert.Fail($"{operation} failed: {ex.Message}");
}

[TestInitialize]
public async Task Initialize()
{
Expand Down Expand Up @@ -196,6 +209,197 @@ public async Task Test004_Should_Perform_Bulk_Unpublish_Operation()
}
}

[TestMethod]
[DoNotParallelize]
public async Task Test003a_Should_Perform_Bulk_Publish_With_SkipWorkflowStage_And_Approvals()
{
try
{
await EnsureBulkTestContentTypeAndEntriesAsync();

List<EntryInfo> availableEntries = await FetchExistingEntries();
Assert.IsTrue(availableEntries.Count > 0, "No entries available for bulk operation");

List<string> availableEnvironments = await GetAvailableEnvironments();

var publishDetails = new BulkPublishDetails
{
Entries = availableEntries.Select(e => new BulkPublishEntry
{
Uid = e.Uid,
ContentType = _contentTypeUid,
Version = e.Version,
Locale = "en-us"
}).ToList(),
Locales = new List<string> { "en-us" },
Environments = availableEnvironments
};

ContentstackResponse response = _stack.BulkOperation().Publish(publishDetails, skipWorkflowStage: true, approvals: true);

Assert.IsNotNull(response);
Assert.IsTrue(response.IsSuccessStatusCode, $"Bulk publish failed with status {(int)response.StatusCode} ({response.StatusCode}).");
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode, $"Expected 200 OK, got {(int)response.StatusCode}.");

var responseJson = response.OpenJObjectResponse();
Assert.IsNotNull(responseJson);
}
catch (Exception ex)
{
FailWithError("Bulk publish with skipWorkflowStage and approvals", ex);
}
}

[TestMethod]
[DoNotParallelize]
public async Task Test004a_Should_Perform_Bulk_Unpublish_With_SkipWorkflowStage_And_Approvals()
{
try
{
await EnsureBulkTestContentTypeAndEntriesAsync();

List<EntryInfo> availableEntries = await FetchExistingEntries();
Assert.IsTrue(availableEntries.Count > 0, "No entries available for bulk operation");

List<string> availableEnvironments = await GetAvailableEnvironments();

var unpublishDetails = new BulkPublishDetails
{
Entries = availableEntries.Select(e => new BulkPublishEntry
{
Uid = e.Uid,
ContentType = _contentTypeUid,
Version = e.Version,
Locale = "en-us"
}).ToList(),
Locales = new List<string> { "en-us" },
Environments = availableEnvironments
};

ContentstackResponse response = _stack.BulkOperation().Unpublish(unpublishDetails, skipWorkflowStage: true, approvals: true);

Assert.IsNotNull(response);
Assert.IsTrue(response.IsSuccessStatusCode, $"Bulk unpublish failed with status {(int)response.StatusCode} ({response.StatusCode}).");
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode, $"Expected 200 OK, got {(int)response.StatusCode}.");

var responseJson = response.OpenJObjectResponse();
Assert.IsNotNull(responseJson);
}
catch (Exception ex)
{
FailWithError("Bulk unpublish with skipWorkflowStage and approvals", ex);
}
}

[TestMethod]
[DoNotParallelize]
public async Task Test003b_Should_Perform_Bulk_Publish_With_ApiVersion_3_2()
{
try
{
await EnsureBulkTestContentTypeAndEntriesAsync();

List<EntryInfo> availableEntries = await FetchExistingEntries();
Assert.IsTrue(availableEntries.Count > 0, "No entries available for bulk operation");

List<string> availableEnvironments = await GetAvailableEnvironments();

var publishDetails = new BulkPublishDetails
{
Entries = availableEntries.Select(e => new BulkPublishEntry
{
Uid = e.Uid,
ContentType = _contentTypeUid,
Version = e.Version,
Locale = "en-us"
}).ToList(),
Locales = new List<string> { "en-us" },
Environments = availableEnvironments
};

ContentstackResponse response = _stack.BulkOperation().Publish(publishDetails, skipWorkflowStage: true, approvals: true, apiVersion: "3.2");

Assert.IsNotNull(response);
Assert.IsTrue(response.IsSuccessStatusCode, $"Bulk publish with api_version 3.2 failed with status {(int)response.StatusCode} ({response.StatusCode}).");
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode, $"Expected 200 OK, got {(int)response.StatusCode}.");

var responseJson = response.OpenJObjectResponse();
Assert.IsNotNull(responseJson);
}
catch (Exception ex)
{
FailWithError("Bulk publish with api_version 3.2", ex);
}
}

[TestMethod]
[DoNotParallelize]
public async Task Test004b_Should_Perform_Bulk_Unpublish_With_ApiVersion_3_2()
{
try
{
await EnsureBulkTestContentTypeAndEntriesAsync();

List<EntryInfo> availableEntries = await FetchExistingEntries();
Assert.IsTrue(availableEntries.Count > 0, "No entries available for bulk operation");

List<string> availableEnvironments = await GetAvailableEnvironments();

var unpublishDetails = new BulkPublishDetails
{
Entries = availableEntries.Select(e => new BulkPublishEntry
{
Uid = e.Uid,
ContentType = _contentTypeUid,
Version = e.Version,
Locale = "en-us"
}).ToList(),
Locales = new List<string> { "en-us" },
Environments = availableEnvironments
};

ContentstackResponse response = _stack.BulkOperation().Unpublish(unpublishDetails, skipWorkflowStage: true, approvals: true, apiVersion: "3.2");

Assert.IsNotNull(response);
Assert.IsTrue(response.IsSuccessStatusCode, $"Bulk unpublish with api_version 3.2 failed with status {(int)response.StatusCode} ({response.StatusCode}).");
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode, $"Expected 200 OK, got {(int)response.StatusCode}.");

var responseJson = response.OpenJObjectResponse();
Assert.IsNotNull(responseJson);
}
catch (Exception ex)
{
FailWithError("Bulk unpublish with api_version 3.2", ex);
}
}

[TestMethod]
[DoNotParallelize]
public void Test004c_Should_Return_Error_When_Bulk_Unpublish_With_Invalid_Data()
{
var invalidDetails = new BulkPublishDetails
{
Entries = new List<BulkPublishEntry>(),
Locales = new List<string> { "en-us" },
Environments = new List<string> { "non_existent_environment_uid" }
};

try
{
_stack.BulkOperation().Unpublish(invalidDetails);
Assert.Fail("Expected ContentstackErrorException was not thrown.");
}
catch (ContentstackErrorException ex)
{
Assert.IsFalse(ex.StatusCode >= HttpStatusCode.OK && (int)ex.StatusCode < 300, "Expected non-success status code.");
Assert.IsNotNull(ex.ErrorMessage ?? ex.Message, "Error message should be present.");
}
catch (Exception ex)
{
FailWithError("Bulk unpublish with invalid data (negative test)", ex);
}
}

[TestMethod]
[DoNotParallelize]
public async Task Test005_Should_Perform_Bulk_Release_Operations()
Expand Down Expand Up @@ -570,6 +774,72 @@ private async Task<List<string>> GetAvailableEnvironments()
}
}

/// <summary>
/// Ensures bulk_test_content_type exists and has at least one entry so bulk tests can run in any order.
/// </summary>
private async Task EnsureBulkTestContentTypeAndEntriesAsync()
{
try
{
bool contentTypeExists = false;
try
{
ContentstackResponse ctResponse = _stack.ContentType(_contentTypeUid).Fetch();
contentTypeExists = ctResponse.IsSuccessStatusCode;
}
catch
{
// Content type not found
}

if (!contentTypeExists)
{
await CreateTestEnvironment();
await CreateTestRelease();
var contentModelling = new ContentModelling
{
Title = "bulk_test_content_type",
Uid = _contentTypeUid,
Schema = new List<Field>
{
new TextboxField
{
DisplayName = "Title",
Uid = "title",
DataType = "text",
Mandatory = true,
Unique = false,
Multiple = false
}
}
};
_stack.ContentType().Create(contentModelling);
}

// Ensure at least one entry exists
List<EntryInfo> existing = await FetchExistingEntries();
if (existing == null || existing.Count == 0)
{
var entry = new SimpleEntry { Title = "Bulk test entry" };
ContentstackResponse createResponse = _stack.ContentType(_contentTypeUid).Entry().Create(entry);
var responseJson = createResponse.OpenJObjectResponse();
if (createResponse.IsSuccessStatusCode && responseJson["entry"] != null && responseJson["entry"]["uid"] != null)
{
_createdEntries.Add(new EntryInfo
{
Uid = responseJson["entry"]["uid"].ToString(),
Title = responseJson["entry"]["title"]?.ToString() ?? "Bulk test entry",
Version = responseJson["entry"]["_version"] != null ? (int)responseJson["entry"]["_version"] : 1
});
}
}
}
catch (Exception)
{
// Caller will handle if entries are still missing
}
}

private async Task<List<EntryInfo>> FetchExistingEntries()
{
try
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,25 +48,25 @@ public void Should_Create_Service_With_Valid_Parameters()
}

[TestMethod]
public void Should_Set_Skip_Workflow_Stage_Header_When_True()
public void Should_Set_Skip_Workflow_Stage_Query_Parameter_When_True()
{
var details = new BulkPublishDetails();
var service = new BulkPublishService(serializer, new Management.Core.Models.Stack(null), details, skipWorkflowStage: true);

Assert.IsNotNull(service);
Assert.IsTrue(service.Headers.ContainsKey("skip_workflow_stage_check"));
Assert.AreEqual("true", service.Headers["skip_workflow_stage_check"]);
Assert.IsTrue(service.QueryResources.ContainsKey("skip_workflow_stage_check"));
Assert.AreEqual("true", service.QueryResources["skip_workflow_stage_check"]);
}

[TestMethod]
public void Should_Set_Approvals_Header_When_True()
public void Should_Set_Approvals_Query_Parameter_When_True()
{
var details = new BulkPublishDetails();
var service = new BulkPublishService(serializer, new Management.Core.Models.Stack(null), details, approvals: true);

Assert.IsNotNull(service);
Assert.IsTrue(service.Headers.ContainsKey("approvals"));
Assert.AreEqual("true", service.Headers["approvals"]);
Assert.IsTrue(service.QueryResources.ContainsKey("approvals"));
Assert.AreEqual("true", service.QueryResources["approvals"]);
}

[TestMethod]
Expand Down
Loading
Loading