Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## [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

## [v0.6.0](https://github.com/contentstack/contentstack-management-dotnet/tree/v0.6.0)
- Enhancement
- Refactor retry policy implementation to improve exception handling and retry logic across various scenarios
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -970,6 +970,92 @@ public async Task Test020_Should_Delete_Release_Async()
}
}

/// <summary>
/// Verifies that Delete Release API succeeds when the SDK does not send Content-Type header (DELETE /releases/{uid}).
/// Creates a release, deletes it without Content-Type, asserts success, then verifies the release is gone.
/// </summary>
[TestMethod]
[DoNotParallelize]
public void Test021_Should_Delete_Release_Without_Content_Type_Header()
{
try
{
var releaseModel = new ReleaseModel
{
Name = _testReleaseName + " Delete Without Content-Type",
Description = _testReleaseDescription + " (Delete without Content-Type header)",
Locked = false,
Archived = false
};

ContentstackResponse createResponse = _stack.Release().Create(releaseModel);
var createResponseJson = createResponse.OpenJObjectResponse();
Assert.IsTrue(createResponse.IsSuccessStatusCode, "Create release must succeed.");
string releaseToDeleteUid = createResponseJson["release"]["uid"].ToString();

ContentstackResponse deleteResponse = _stack.Release(releaseToDeleteUid).Delete();

Assert.IsNotNull(deleteResponse);
Assert.IsTrue(deleteResponse.IsSuccessStatusCode, "Delete release (without Content-Type) must succeed.");

try
{
var fetchResponse = _stack.Release(releaseToDeleteUid).Fetch();
Assert.IsFalse(fetchResponse.IsSuccessStatusCode, "Release must be gone after delete; Fetch should not succeed.");
}
catch (ContentstackErrorException)
{
Assert.IsTrue(true, "Release not found after delete (exception path).");
}
}
catch (Exception e)
{
Assert.Fail($"Delete release without Content-Type header failed: {e.Message}");
}
}

/// <summary>
/// Verifies that Delete Release API (async) succeeds when the SDK does not send Content-Type header (DELETE /releases/{uid}).
/// Creates a release, deletes it without Content-Type, asserts success, then verifies the release is gone.
/// </summary>
[TestMethod]
[DoNotParallelize]
public async Task Test022_Should_Delete_Release_Async_Without_Content_Type_Header()
{
try
{
var releaseModel = new ReleaseModel
{
Name = _testReleaseName + " Delete Async Without Content-Type",
Description = _testReleaseDescription + " (Delete async without Content-Type header)",
Locked = false,
Archived = false
};

ContentstackResponse createResponse = await _stack.Release().CreateAsync(releaseModel);
var createResponseJson = createResponse.OpenJObjectResponse();
Assert.IsTrue(createResponse.IsSuccessStatusCode, "Create release must succeed.");
string releaseToDeleteUid = createResponseJson["release"]["uid"].ToString();

ContentstackResponse deleteResponse = await _stack.Release(releaseToDeleteUid).DeleteAsync();

Assert.IsNotNull(deleteResponse);
Assert.IsTrue(deleteResponse.IsSuccessStatusCode, "Delete release async (without Content-Type) must succeed.");

try
{
var fetchResponse = await _stack.Release(releaseToDeleteUid).FetchAsync();
Assert.IsFalse(fetchResponse.IsSuccessStatusCode, "Release must be gone after delete; Fetch should not succeed.");
}
catch (ContentstackErrorException)
{
Assert.IsTrue(true, "Release not found after delete (exception path).");
}
}
catch (Exception e)
{
Assert.Fail($"Delete release async without Content-Type header failed: {e.Message}");
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
Expand Down Expand Up @@ -279,6 +279,19 @@ public void Return_Value_For_HeaderKey()
Assert.AreEqual("application/json", contentstackService.GetHeaderValue(HeadersKey.ContentTypeHeader));
}

[TestMethod]
public void CreateHttpRequest_Should_Set_Content_Type_When_ShouldSetContentType_Returns_True()
{
var contentstackService = new ContentstackService(serializer);
var config = new ContentstackClientOptions();
config.Authtoken = _fixture.Create<string>();

contentstackService.CreateHttpRequest(new HttpClient(), config);

Assert.IsTrue(contentstackService.Headers.ContainsKey(HeadersKey.ContentTypeHeader));
Assert.AreEqual("application/json", contentstackService.GetHeaderValue(HeadersKey.ContentTypeHeader));
}

[TestMethod]
public void Return_HttpRequest_On_Create_HttpRequest()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
using System;
using System;
using System.Net.Http;
using AutoFixture;
using AutoFixture.AutoMoq;
using Contentstack.Management.Core;
using Contentstack.Management.Core.Services.Models;
using Contentstack.Management.Core.Utils;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;

namespace Contentstack.Management.Core.Unit.Tests.Core.Services.Models
{
[TestClass]
public class FetchDeleteServiceTest
{
private JsonSerializer serializer = JsonSerializer.Create(new JsonSerializerSettings());
private readonly IFixture _fixture = new Fixture()
.Customize(new AutoMoqCustomization());
.Customize(new AutoMoqCustomization());

private static ContentstackClientOptions CreateConfig(IFixture fixture)
{
var config = new ContentstackClientOptions();
config.Authtoken = fixture.Create<string>();
return config;
}

[TestMethod]
public void Should_Throw_On_Null_Serializer()
Expand Down Expand Up @@ -60,5 +70,63 @@ public void Should_Provide_Valid_Param_On_Initialize()
Assert.AreEqual("GET", service.HttpMethod);
Assert.AreEqual(resourcePath, service.ResourcePath);
}

[TestMethod]
public void Delete_Release_Should_Not_Include_Content_Type_Header()
{
var stack = new Management.Core.Models.Stack(null, _fixture.Create<string>());
var service = new FetchDeleteService(serializer, stack, "/releases/release_uid_123", "DELETE");

service.CreateHttpRequest(new HttpClient(), CreateConfig(_fixture));

Assert.IsFalse(service.Headers.ContainsKey(HeadersKey.ContentTypeHeader), "DELETE /releases/{uid} must not include Content-Type header.");
}

[TestMethod]
public void Delete_Release_With_Path_Releases_Only_Should_Not_Include_Content_Type_Header()
{
var stack = new Management.Core.Models.Stack(null, _fixture.Create<string>());
var service = new FetchDeleteService(serializer, stack, "/releases", "DELETE");

service.CreateHttpRequest(new HttpClient(), CreateConfig(_fixture));

Assert.IsFalse(service.Headers.ContainsKey(HeadersKey.ContentTypeHeader));
}

[TestMethod]
public void Delete_Release_Items_Path_Should_Include_Content_Type_Header()
{
var stack = new Management.Core.Models.Stack(null, _fixture.Create<string>());
var service = new FetchDeleteService(serializer, stack, "/releases/release_uid/item", "DELETE");

service.CreateHttpRequest(new HttpClient(), CreateConfig(_fixture));

Assert.IsTrue(service.Headers.ContainsKey(HeadersKey.ContentTypeHeader), "DELETE /releases/{uid}/item (FetchDeleteService) should still set Content-Type.");
Assert.AreEqual("application/json", service.GetHeaderValue(HeadersKey.ContentTypeHeader));
}

[TestMethod]
public void Fetch_Release_Should_Include_Content_Type_Header()
{
var stack = new Management.Core.Models.Stack(null, _fixture.Create<string>());
var service = new FetchDeleteService(serializer, stack, "/releases/release_uid_123", "GET");

service.CreateHttpRequest(new HttpClient(), CreateConfig(_fixture));

Assert.IsTrue(service.Headers.ContainsKey(HeadersKey.ContentTypeHeader));
Assert.AreEqual("application/json", service.GetHeaderValue(HeadersKey.ContentTypeHeader));
}

[TestMethod]
public void Delete_Non_Release_Resource_Should_Include_Content_Type_Header()
{
var stack = new Management.Core.Models.Stack(null, _fixture.Create<string>());
var service = new FetchDeleteService(serializer, stack, "/contenttypes/ct_uid", "DELETE");

service.CreateHttpRequest(new HttpClient(), CreateConfig(_fixture));

Assert.IsTrue(service.Headers.ContainsKey(HeadersKey.ContentTypeHeader));
Assert.AreEqual("application/json", service.GetHeaderValue(HeadersKey.ContentTypeHeader));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System;
using AutoFixture;
using Contentstack.Management.Core;
using Contentstack.Management.Core.Models;
using Contentstack.Management.Core.Queryable;
using Contentstack.Management.Core.Unit.Tests.Mokes;
Expand Down
16 changes: 14 additions & 2 deletions Contentstack.Management.Core/Services/ContentstackService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.IO;
using System.Text;
using System.Net.Http;
Expand Down Expand Up @@ -151,6 +151,15 @@ public bool HasRequestBody()
return HttpMethod == "POST" || HttpMethod == "PUT" || HttpMethod == "PATCH" || HttpMethod == "DELETE";
}

/// <summary>
/// Returns true if the request should include Content-Type: application/json header.
/// Override to skip Content-Type for specific requests (e.g. DELETE /releases).
/// </summary>
protected virtual bool ShouldSetContentType()
{
return true;
}

public virtual IHttpRequest CreateHttpRequest(HttpClient httpClient, ContentstackClientOptions config, bool addAcceptMediaHeader = false, string apiVersion = null)
{
ThrowIfDisposed();
Expand All @@ -162,7 +171,10 @@ public virtual IHttpRequest CreateHttpRequest(HttpClient httpClient, Contentstac
.Add(new MediaTypeWithQualityHeaderValue("image/jpeg"));
}
Uri requestUri = ContentstackUtilities.ComposeUrI(config.GetUri(), this);
Headers["Content-Type"] = "application/json";
if (ShouldSetContentType())
{
Headers["Content-Type"] = "application/json";
}

if (!string.IsNullOrEmpty(this.ManagementToken))
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using Contentstack.Management.Core.Queryable;
using Newtonsoft.Json;
using Contentstack.Management.Core.Utils;
Expand Down Expand Up @@ -29,5 +29,19 @@ internal FetchDeleteService(JsonSerializer serializer, Core.Models.Stack stack,
}
}
#endregion

/// <summary>
/// Skip Content-Type for DELETE /releases (single release delete). Keep it for DELETE /releases/{uid}/item and other requests.
/// </summary>
protected override bool ShouldSetContentType()
{
if (HttpMethod != "DELETE" || string.IsNullOrEmpty(ResourcePath))
return true;
if (!ResourcePath.StartsWith("/releases", StringComparison.Ordinal))
return true;
if (ResourcePath.EndsWith("/item", StringComparison.Ordinal))
return true;
return false;
}
}
}
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<Project>
<PropertyGroup>
<Version>0.6.0</Version>
<Version>0.6.1</Version>
</PropertyGroup>
</Project>
Loading