diff --git a/CHANGELOG.md b/CHANGELOG.md
index ee2230a..62ca689 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/Contentstack.Management.Core.Tests/Contentstack.Management.Core.Tests.csproj b/Contentstack.Management.Core.Tests/Contentstack.Management.Core.Tests.csproj
index f8be953..74cce31 100644
--- a/Contentstack.Management.Core.Tests/Contentstack.Management.Core.Tests.csproj
+++ b/Contentstack.Management.Core.Tests/Contentstack.Management.Core.Tests.csproj
@@ -24,6 +24,7 @@
+
@@ -35,14 +36,17 @@
+
-
+
PreserveNewest
+
+
diff --git a/Contentstack.Management.Core.Tests/IntegrationTest/Contentstack015_BulkOperationTest.cs b/Contentstack.Management.Core.Tests/IntegrationTest/Contentstack015_BulkOperationTest.cs
index 9cbc4f0..562239d 100644
--- a/Contentstack.Management.Core.Tests/IntegrationTest/Contentstack015_BulkOperationTest.cs
+++ b/Contentstack.Management.Core.Tests/IntegrationTest/Contentstack015_BulkOperationTest.cs
@@ -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;
@@ -12,6 +14,11 @@
namespace Contentstack.Management.Core.Tests.IntegrationTest
{
+ ///
+ /// Bulk operation integration tests. ClassInitialize ensures environment (find or create "bulk_test_env"), then finds or creates workflow "workflow_test" (2 stages: New stage 1, New stage 2) and publish rule (Stage 2) once.
+ /// Tests are independent. Four workflow-based tests assign entries to Stage 1/Stage 2 then run bulk unpublish/publish with/without version and params.
+ /// No cleanup so you can verify workflow, publish rules, and entry allotment in the UI.
+ ///
[TestClass]
public class Contentstack015_BulkOperationTest
{
@@ -21,25 +28,313 @@ public class Contentstack015_BulkOperationTest
private string _testReleaseUid = "bulk_test_release";
private List _createdEntries = new List();
+ // Workflow and publishing rule for bulk tests (static so one create/delete across all test instances)
+ private static string _bulkTestWorkflowUid;
+ private static string _bulkTestWorkflowStageUid; // Stage 2 (Complete) – used by publish rule and backward compat
+ private static string _bulkTestWorkflowStage1Uid; // Stage 1 (Review)
+ private static string _bulkTestWorkflowStage2Uid; // Stage 2 (Complete) – selected in publishing rule
+ private static string _bulkTestPublishRuleUid;
+ private static string _bulkTestEnvironmentUid; // Environment used for workflow/publish rule (ensured in ClassInitialize or Test000b/000c)
+ private static string _bulkTestWorkflowSetupError; // Reason workflow setup failed (so workflow_tests can show it)
+
+ ///
+ /// Fails the test with a clear message from ContentstackErrorException or generic exception.
+ ///
+ 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}");
+ }
+
+ ///
+ /// Asserts that the workflow and both stages were created in ClassInitialize. Call at the start of workflow-based tests so they fail clearly when setup failed.
+ ///
+ private static void AssertWorkflowCreated()
+ {
+ string reason = string.IsNullOrEmpty(_bulkTestWorkflowSetupError) ? "Check auth and stack permissions for workflow create." : _bulkTestWorkflowSetupError;
+ Assert.IsFalse(string.IsNullOrEmpty(_bulkTestWorkflowUid), "Workflow was not created in ClassInitialize. " + reason);
+ Assert.IsFalse(string.IsNullOrEmpty(_bulkTestWorkflowStage1Uid), "Workflow Stage 1 (New stage 1) was not set. " + reason);
+ Assert.IsFalse(string.IsNullOrEmpty(_bulkTestWorkflowStage2Uid), "Workflow Stage 2 (New stage 2) was not set. " + reason);
+ }
+
+ ///
+ /// Returns a Stack instance for the test run (used by ClassInitialize/ClassCleanup).
+ ///
+ private static Stack GetStack()
+ {
+ StackResponse response = StackResponse.getStack(Contentstack.Client.serializer);
+ return Contentstack.Client.Stack(response.Stack.APIKey);
+ }
+
+ [ClassInitialize]
+ public static void ClassInitialize(TestContext context)
+ {
+ try
+ {
+ Stack stack = GetStack();
+ EnsureBulkTestWorkflowAndPublishingRuleAsync(stack).GetAwaiter().GetResult();
+ }
+ catch (Exception)
+ {
+ // Workflow/publish rule setup failed (e.g. auth, plan limits); tests can still run without them
+ }
+ }
+
+ [ClassCleanup]
+ public static void ClassCleanup()
+ {
+ // Intentionally no cleanup: workflow, publish rules, and entries are left so you can verify them in the UI.
+ }
+
[TestInitialize]
public async Task Initialize()
{
StackResponse response = StackResponse.getStack(Contentstack.Client.serializer);
_stack = Contentstack.Client.Stack(response.Stack.APIKey);
-
- // Create a test environment for bulk operations
- //await CreateTestEnvironment();
- //await CreateTestRelease();
+
+ // Create test environment and release for bulk operations (for new stack)
+ try
+ {
+ await CreateTestEnvironment();
+ }
+ catch (ContentstackErrorException ex)
+ {
+ // Environment may already exist on this stack; no action needed
+ Console.WriteLine($"[Initialize] CreateTestEnvironment skipped: HTTP {(int)ex.StatusCode} ({ex.StatusCode}). ErrorCode: {ex.ErrorCode}. Message: {ex.ErrorMessage ?? ex.Message}");
+ }
+
+ try
+ {
+ await CreateTestRelease();
+ }
+ catch (ContentstackErrorException ex)
+ {
+ // Release may already exist on this stack; no action needed
+ Console.WriteLine($"[Initialize] CreateTestRelease skipped: HTTP {(int)ex.StatusCode} ({ex.StatusCode}). ErrorCode: {ex.ErrorCode}. Message: {ex.ErrorMessage ?? ex.Message}");
+ }
+
+ // Ensure workflow (and bulk env) is initialized when running on a new stack
+ if (string.IsNullOrEmpty(_bulkTestWorkflowUid))
+ {
+ try
+ {
+ EnsureBulkTestWorkflowAndPublishingRuleAsync(_stack).GetAwaiter().GetResult();
+ }
+ catch (Exception ex)
+ {
+ // Workflow setup failed (e.g. auth, plan limits); record the reason so workflow-based tests can surface it
+ _bulkTestWorkflowSetupError = ex is ContentstackErrorException cex
+ ? $"HTTP {(int)cex.StatusCode} ({cex.StatusCode}). ErrorCode: {cex.ErrorCode}. Message: {cex.ErrorMessage ?? cex.Message}"
+ : ex.Message;
+ Console.WriteLine($"[Initialize] Workflow setup failed: {_bulkTestWorkflowSetupError}");
+ }
+ }
}
+ [TestMethod]
+ [DoNotParallelize]
+ public void Test000a_Should_Create_Workflow_With_Two_Stages()
+ {
+ try
+ {
+ const string workflowName = "workflow_test";
+
+ // Check if a workflow with the same name already exists (e.g. from a previous test run)
+ try
+ {
+ ContentstackResponse listResponse = _stack.Workflow().FindAll();
+ if (listResponse.IsSuccessStatusCode)
+ {
+ var listJson = listResponse.OpenJObjectResponse();
+ var existing = (listJson["workflows"] as JArray) ?? (listJson["workflow"] as JArray);
+ if (existing != null)
+ {
+ foreach (var wf in existing)
+ {
+ if (wf["name"]?.ToString() == workflowName && wf["uid"] != null)
+ {
+ _bulkTestWorkflowUid = wf["uid"].ToString();
+ var existingStages = wf["workflow_stages"] as JArray;
+ if (existingStages != null && existingStages.Count >= 2)
+ {
+ _bulkTestWorkflowStage1Uid = existingStages[0]["uid"]?.ToString();
+ _bulkTestWorkflowStage2Uid = existingStages[1]["uid"]?.ToString();
+ _bulkTestWorkflowStageUid = _bulkTestWorkflowStage2Uid;
+ Assert.IsNotNull(_bulkTestWorkflowStage1Uid, "Stage 1 UID null in existing workflow.");
+ Assert.IsNotNull(_bulkTestWorkflowStage2Uid, "Stage 2 UID null in existing workflow.");
+ return; // Already exists with stages – nothing more to do
+ }
+ }
+ }
+ }
+ }
+ }
+ catch { /* If listing fails, proceed to create */ }
+
+ var sysAcl = new Dictionary
+ {
+ ["roles"] = new Dictionary { ["uids"] = new List() },
+ ["users"] = new Dictionary { ["uids"] = new List { "$all" } },
+ ["others"] = new Dictionary()
+ };
+
+ var workflowModel = new WorkflowModel
+ {
+ Name = workflowName,
+ Enabled = true,
+ Branches = new List { "main" },
+ ContentTypes = new List { "$all" },
+ AdminUsers = new Dictionary { ["users"] = new List