From 84b75f66b01f9c2eefba95e7e60d0b331655650a Mon Sep 17 00:00:00 2001 From: Nadeem Patwekar Date: Wed, 10 Dec 2025 12:08:40 +0530 Subject: [PATCH 01/10] Enhance retry mechanism with comprehensive configuration options --- .../Mokes/MockHttpHandlerWithRetries.cs | 150 +++++++ .../Mokes/MockNetworkErrorGenerator.cs | 55 +++ .../Mokes/MockRetryPolicy.cs | 49 +++ .../Mokes/Response/429Response.txt | 9 + .../Mokes/Response/500Response.txt | 8 + .../Mokes/Response/502Response.txt | 8 + .../Mokes/Response/503Response.txt | 8 + .../Mokes/Response/504Response.txt | 8 + .../RetryHandler/DefaultRetryPolicyTest.cs | 376 ++++++++++++++++ .../RetryHandler/NetworkErrorDetectorTest.cs | 340 +++++++++++++++ .../RetryHandler/RetryConfigurationTest.cs | 111 +++++ .../RetryHandler/RetryDelayCalculatorTest.cs | 363 ++++++++++++++++ .../RetryHandlerIntegrationTest.cs | 262 ++++++++++++ .../Pipeline/RetryHandler/RetryHandlerTest.cs | 403 ++++++++++++++++++ .../ContentstackClient.cs | 14 +- .../ContentstackClientOptions.cs | 53 +++ .../Exceptions/ContentstackErrorException.cs | 15 + .../Runtime/Contexts/IRequestContext.cs | 15 + .../Runtime/Contexts/RequestContext.cs | 15 + .../Pipeline/RetryHandler/BackoffStrategy.cs | 19 + .../RetryHandler/DefaultRetryPolicy.cs | 176 +++++++- .../RetryHandler/NetworkErrorDetector.cs | 141 ++++++ .../Pipeline/RetryHandler/NetworkErrorInfo.cs | 65 +++ .../RetryHandler/RetryConfiguration.cs | 128 ++++++ .../RetryHandler/RetryDelayCalculator.cs | 122 ++++++ .../Pipeline/RetryHandler/RetryHandler.cs | 153 ++++++- 26 files changed, 3036 insertions(+), 30 deletions(-) create mode 100644 Contentstack.Management.Core.Unit.Tests/Mokes/MockHttpHandlerWithRetries.cs create mode 100644 Contentstack.Management.Core.Unit.Tests/Mokes/MockNetworkErrorGenerator.cs create mode 100644 Contentstack.Management.Core.Unit.Tests/Mokes/MockRetryPolicy.cs create mode 100644 Contentstack.Management.Core.Unit.Tests/Mokes/Response/429Response.txt create mode 100644 Contentstack.Management.Core.Unit.Tests/Mokes/Response/500Response.txt create mode 100644 Contentstack.Management.Core.Unit.Tests/Mokes/Response/502Response.txt create mode 100644 Contentstack.Management.Core.Unit.Tests/Mokes/Response/503Response.txt create mode 100644 Contentstack.Management.Core.Unit.Tests/Mokes/Response/504Response.txt create mode 100644 Contentstack.Management.Core.Unit.Tests/Runtime/Pipeline/RetryHandler/DefaultRetryPolicyTest.cs create mode 100644 Contentstack.Management.Core.Unit.Tests/Runtime/Pipeline/RetryHandler/NetworkErrorDetectorTest.cs create mode 100644 Contentstack.Management.Core.Unit.Tests/Runtime/Pipeline/RetryHandler/RetryConfigurationTest.cs create mode 100644 Contentstack.Management.Core.Unit.Tests/Runtime/Pipeline/RetryHandler/RetryDelayCalculatorTest.cs create mode 100644 Contentstack.Management.Core.Unit.Tests/Runtime/Pipeline/RetryHandler/RetryHandlerIntegrationTest.cs create mode 100644 Contentstack.Management.Core.Unit.Tests/Runtime/Pipeline/RetryHandler/RetryHandlerTest.cs create mode 100644 Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/BackoffStrategy.cs create mode 100644 Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/NetworkErrorDetector.cs create mode 100644 Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/NetworkErrorInfo.cs create mode 100644 Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/RetryConfiguration.cs create mode 100644 Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/RetryDelayCalculator.cs diff --git a/Contentstack.Management.Core.Unit.Tests/Mokes/MockHttpHandlerWithRetries.cs b/Contentstack.Management.Core.Unit.Tests/Mokes/MockHttpHandlerWithRetries.cs new file mode 100644 index 0000000..c485ad8 --- /dev/null +++ b/Contentstack.Management.Core.Unit.Tests/Mokes/MockHttpHandlerWithRetries.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using Contentstack.Management.Core.Http; +using Contentstack.Management.Core.Internal; +using Contentstack.Management.Core.Runtime.Contexts; +using Contentstack.Management.Core.Runtime.Pipeline; +using Newtonsoft.Json; + +namespace Contentstack.Management.Core.Unit.Tests.Mokes +{ + /// + /// Mock HTTP handler that can simulate failures and successes for retry testing. + /// + public class MockHttpHandlerWithRetries : IPipelineHandler + { + private readonly Queue> _responseQueue; + private readonly Queue _exceptionQueue; + private int _callCount = 0; + + public ILogManager LogManager { get; set; } + public IPipelineHandler InnerHandler { get; set; } + public int CallCount => _callCount; + + public MockHttpHandlerWithRetries() + { + _responseQueue = new Queue>(); + _exceptionQueue = new Queue(); + } + + /// + /// Adds a response that will be returned on the next call. + /// + public void AddResponse(HttpStatusCode statusCode, string body = null) + { + _responseQueue.Enqueue((context) => + { + var response = new HttpResponseMessage(statusCode); + if (body != null) + { + response.Content = new StringContent(body); + } + return new ContentstackResponse(response, JsonSerializer.Create(new JsonSerializerSettings())); + }); + } + + /// + /// Adds a successful response (200 OK). + /// + public void AddSuccessResponse(string body = "{\"success\": true}") + { + AddResponse(HttpStatusCode.OK, body); + } + + /// + /// Adds an exception that will be thrown on the next call. + /// + public void AddException(Exception exception) + { + _exceptionQueue.Enqueue(exception); + } + + /// + /// Adds multiple failures followed by a success. + /// + public void AddFailuresThenSuccess(int failureCount, Exception failureException, string successBody = "{\"success\": true}") + { + for (int i = 0; i < failureCount; i++) + { + AddException(failureException); + } + AddSuccessResponse(successBody); + } + + /// + /// Adds multiple HTTP error responses followed by a success. + /// + public void AddHttpErrorsThenSuccess(int errorCount, HttpStatusCode errorStatusCode, string successBody = "{\"success\": true}") + { + for (int i = 0; i < errorCount; i++) + { + AddResponse(errorStatusCode); + } + AddSuccessResponse(successBody); + } + + public async System.Threading.Tasks.Task InvokeAsync( + IExecutionContext executionContext, + bool addAcceptMediaHeader = false, + string apiVersion = null) + { + _callCount++; + + // Check for exceptions first + if (_exceptionQueue.Count > 0) + { + var exception = _exceptionQueue.Dequeue(); + throw exception; + } + + // Check for responses + if (_responseQueue.Count > 0) + { + var responseFactory = _responseQueue.Dequeue(); + var response = responseFactory(executionContext); + executionContext.ResponseContext.httpResponse = response; + return await System.Threading.Tasks.Task.FromResult((T)response); + } + + // Default: return success + var defaultResponse = new HttpResponseMessage(HttpStatusCode.OK); + defaultResponse.Content = new StringContent("{\"success\": true}"); + var contentstackResponse = new ContentstackResponse(defaultResponse, JsonSerializer.Create(new JsonSerializerSettings())); + executionContext.ResponseContext.httpResponse = contentstackResponse; + return await System.Threading.Tasks.Task.FromResult((T)(IResponse)contentstackResponse); + } + + public void InvokeSync( + IExecutionContext executionContext, + bool addAcceptMediaHeader = false, + string apiVersion = null) + { + _callCount++; + + // Check for exceptions first + if (_exceptionQueue.Count > 0) + { + var exception = _exceptionQueue.Dequeue(); + throw exception; + } + + // Check for responses + if (_responseQueue.Count > 0) + { + var responseFactory = _responseQueue.Dequeue(); + var response = responseFactory(executionContext); + executionContext.ResponseContext.httpResponse = response; + return; + } + + // Default: return success + var defaultResponse = new HttpResponseMessage(HttpStatusCode.OK); + defaultResponse.Content = new StringContent("{\"success\": true}"); + var contentstackResponse = new ContentstackResponse(defaultResponse, JsonSerializer.Create(new JsonSerializerSettings())); + executionContext.ResponseContext.httpResponse = contentstackResponse; + } + } +} + diff --git a/Contentstack.Management.Core.Unit.Tests/Mokes/MockNetworkErrorGenerator.cs b/Contentstack.Management.Core.Unit.Tests/Mokes/MockNetworkErrorGenerator.cs new file mode 100644 index 0000000..8de4de1 --- /dev/null +++ b/Contentstack.Management.Core.Unit.Tests/Mokes/MockNetworkErrorGenerator.cs @@ -0,0 +1,55 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Contentstack.Management.Core.Exceptions; + +namespace Contentstack.Management.Core.Unit.Tests.Mokes +{ + /// + /// Utility to generate various network error exceptions for testing. + /// + public static class MockNetworkErrorGenerator + { + public static SocketException CreateSocketException(SocketError errorCode) + { + return new SocketException((int)errorCode); + } + + public static HttpRequestException CreateHttpRequestExceptionWithSocketException(SocketError socketError) + { + var socketException = CreateSocketException(socketError); + return new HttpRequestException("Network error", socketException); + } + + public static TaskCanceledException CreateTaskCanceledExceptionTimeout() + { + var cts = new CancellationTokenSource(); + return new TaskCanceledException("Operation timed out", null, cts.Token); + } + + public static TaskCanceledException CreateTaskCanceledExceptionUserCancellation() + { + var cts = new CancellationTokenSource(); + cts.Cancel(); + return new TaskCanceledException("User cancelled", null, cts.Token); + } + + public static TimeoutException CreateTimeoutException() + { + return new TimeoutException("Operation timed out"); + } + + public static ContentstackErrorException CreateContentstackErrorException(HttpStatusCode statusCode) + { + return new ContentstackErrorException + { + StatusCode = statusCode, + Message = $"HTTP {statusCode} error" + }; + } + } +} + diff --git a/Contentstack.Management.Core.Unit.Tests/Mokes/MockRetryPolicy.cs b/Contentstack.Management.Core.Unit.Tests/Mokes/MockRetryPolicy.cs new file mode 100644 index 0000000..41b07dd --- /dev/null +++ b/Contentstack.Management.Core.Unit.Tests/Mokes/MockRetryPolicy.cs @@ -0,0 +1,49 @@ +using System; +using Contentstack.Management.Core.Exceptions; +using Contentstack.Management.Core.Runtime.Contexts; +using Contentstack.Management.Core.Runtime.Pipeline.RetryHandler; + +namespace Contentstack.Management.Core.Unit.Tests.Mokes +{ + /// + /// Mock retry policy for testing RetryHandler in isolation. + /// + public class MockRetryPolicy : RetryPolicy + { + public bool ShouldRetryValue { get; set; } = true; + public bool CanRetryValue { get; set; } = true; + public bool RetryLimitExceededValue { get; set; } = false; + public TimeSpan WaitDelay { get; set; } = TimeSpan.FromMilliseconds(100); + public Exception LastException { get; private set; } + public int RetryCallCount { get; private set; } + + public MockRetryPolicy() + { + RetryOnError = true; + RetryLimit = 5; + } + + protected override bool RetryForException(IExecutionContext executionContext, Exception exception) + { + LastException = exception; + RetryCallCount++; + return ShouldRetryValue; + } + + protected override bool CanRetry(IExecutionContext executionContext) + { + return CanRetryValue; + } + + protected override bool RetryLimitExceeded(IExecutionContext executionContext) + { + return RetryLimitExceededValue; + } + + internal override void WaitBeforeRetry(IExecutionContext executionContext) + { + System.Threading.Tasks.Task.Delay(WaitDelay).Wait(); + } + } +} + diff --git a/Contentstack.Management.Core.Unit.Tests/Mokes/Response/429Response.txt b/Contentstack.Management.Core.Unit.Tests/Mokes/Response/429Response.txt new file mode 100644 index 0000000..ff08912 --- /dev/null +++ b/Contentstack.Management.Core.Unit.Tests/Mokes/Response/429Response.txt @@ -0,0 +1,9 @@ +HTTP/1.1 429 Too Many Requests +content-type: application/json +content-length: 45 +retry-after: 5 +date: Wed, 28 Apr 2021 11:11:34 GMT +connection: Keep-Alive + +{"error_message": "Too many requests","error_code": 429} + diff --git a/Contentstack.Management.Core.Unit.Tests/Mokes/Response/500Response.txt b/Contentstack.Management.Core.Unit.Tests/Mokes/Response/500Response.txt new file mode 100644 index 0000000..a9dd5e3 --- /dev/null +++ b/Contentstack.Management.Core.Unit.Tests/Mokes/Response/500Response.txt @@ -0,0 +1,8 @@ +HTTP/1.1 500 Internal Server Error +content-type: application/json +content-length: 45 +date: Wed, 28 Apr 2021 11:11:34 GMT +connection: Keep-Alive + +{"error_message": "Internal server error","error_code": 500} + diff --git a/Contentstack.Management.Core.Unit.Tests/Mokes/Response/502Response.txt b/Contentstack.Management.Core.Unit.Tests/Mokes/Response/502Response.txt new file mode 100644 index 0000000..1f7c50f --- /dev/null +++ b/Contentstack.Management.Core.Unit.Tests/Mokes/Response/502Response.txt @@ -0,0 +1,8 @@ +HTTP/1.1 502 Bad Gateway +content-type: application/json +content-length: 40 +date: Wed, 28 Apr 2021 11:11:34 GMT +connection: Keep-Alive + +{"error_message": "Bad gateway","error_code": 502} + diff --git a/Contentstack.Management.Core.Unit.Tests/Mokes/Response/503Response.txt b/Contentstack.Management.Core.Unit.Tests/Mokes/Response/503Response.txt new file mode 100644 index 0000000..e0ce259 --- /dev/null +++ b/Contentstack.Management.Core.Unit.Tests/Mokes/Response/503Response.txt @@ -0,0 +1,8 @@ +HTTP/1.1 503 Service Unavailable +content-type: application/json +content-length: 48 +date: Wed, 28 Apr 2021 11:11:34 GMT +connection: Keep-Alive + +{"error_message": "Service unavailable","error_code": 503} + diff --git a/Contentstack.Management.Core.Unit.Tests/Mokes/Response/504Response.txt b/Contentstack.Management.Core.Unit.Tests/Mokes/Response/504Response.txt new file mode 100644 index 0000000..8267c6b --- /dev/null +++ b/Contentstack.Management.Core.Unit.Tests/Mokes/Response/504Response.txt @@ -0,0 +1,8 @@ +HTTP/1.1 504 Gateway Timeout +content-type: application/json +content-length: 45 +date: Wed, 28 Apr 2021 11:11:34 GMT +connection: Keep-Alive + +{"error_message": "Gateway timeout","error_code": 504} + diff --git a/Contentstack.Management.Core.Unit.Tests/Runtime/Pipeline/RetryHandler/DefaultRetryPolicyTest.cs b/Contentstack.Management.Core.Unit.Tests/Runtime/Pipeline/RetryHandler/DefaultRetryPolicyTest.cs new file mode 100644 index 0000000..3a7ef45 --- /dev/null +++ b/Contentstack.Management.Core.Unit.Tests/Runtime/Pipeline/RetryHandler/DefaultRetryPolicyTest.cs @@ -0,0 +1,376 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Net.Sockets; +using Contentstack.Management.Core.Exceptions; +using Contentstack.Management.Core.Runtime.Contexts; +using Contentstack.Management.Core.Runtime.Pipeline.RetryHandler; +using Contentstack.Management.Core.Unit.Tests.Mokes; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Contentstack.Management.Core.Unit.Tests.Runtime.Pipeline.RetryHandler +{ + [TestClass] + public class DefaultRetryPolicyTest + { + [TestMethod] + public void Constructor_With_RetryConfiguration_Sets_Properties() + { + var config = new RetryConfiguration + { + RetryLimit = 3, + RetryDelay = TimeSpan.FromMilliseconds(200) + }; + + var policy = new DefaultRetryPolicy(config); + + Assert.AreEqual(3, policy.RetryLimit); + } + + [TestMethod] + public void Constructor_With_Legacy_Parameters_Sets_Properties() + { + var policy = new DefaultRetryPolicy(5, TimeSpan.FromMilliseconds(300)); + + Assert.AreEqual(5, policy.RetryLimit); + } + + [TestMethod] + public void CanRetry_Respects_RetryOnError_From_Configuration() + { + var config = new RetryConfiguration + { + RetryOnError = true + }; + var policy = new DefaultRetryPolicy(config); + var context = CreateExecutionContext(); + + var result = policy.CanRetry(context); + + Assert.IsTrue(result); + } + + [TestMethod] + public void CanRetry_Fallback_To_RetryOnError_Property() + { + var policy = new DefaultRetryPolicy(5, TimeSpan.FromMilliseconds(300)); + policy.RetryOnError = false; + var context = CreateExecutionContext(); + + var result = policy.CanRetry(context); + + Assert.IsFalse(result); + } + + [TestMethod] + public void RetryForException_NetworkError_Respects_MaxNetworkRetries() + { + var config = new RetryConfiguration + { + RetryOnNetworkFailure = true, + RetryOnSocketFailure = true, + MaxNetworkRetries = 2 + }; + var policy = new DefaultRetryPolicy(config); + var context = CreateExecutionContext(); + var exception = MockNetworkErrorGenerator.CreateSocketException(SocketError.ConnectionReset); + + context.RequestContext.NetworkRetryCount = 1; + var result1 = policy.RetryForException(context, exception); + Assert.IsTrue(result1); + + context.RequestContext.NetworkRetryCount = 2; + var result2 = policy.RetryForException(context, exception); + Assert.IsFalse(result2); + } + + [TestMethod] + public void RetryForException_NetworkError_Increments_NetworkRetryCount() + { + var config = new RetryConfiguration + { + RetryOnNetworkFailure = true, + RetryOnSocketFailure = true, + MaxNetworkRetries = 3 + }; + var policy = new DefaultRetryPolicy(config); + var context = CreateExecutionContext(); + var exception = MockNetworkErrorGenerator.CreateSocketException(SocketError.ConnectionReset); + + var result = policy.RetryForException(context, exception); + Assert.IsTrue(result); + } + + [TestMethod] + public void RetryForException_HttpError_5xx_Respects_RetryLimit() + { + var config = new RetryConfiguration + { + RetryOnHttpServerError = true, + RetryLimit = 2 + }; + var policy = new DefaultRetryPolicy(config); + var context = CreateExecutionContext(); + var exception = MockNetworkErrorGenerator.CreateContentstackErrorException(HttpStatusCode.InternalServerError); + + context.RequestContext.HttpRetryCount = 1; + var result1 = policy.RetryForException(context, exception); + Assert.IsTrue(result1); + + context.RequestContext.HttpRetryCount = 2; + var result2 = policy.RetryForException(context, exception); + Assert.IsFalse(result2); + } + + [TestMethod] + public void RetryForException_HttpError_5xx_Increments_HttpRetryCount() + { + var config = new RetryConfiguration + { + RetryOnHttpServerError = true, + RetryLimit = 5 + }; + var policy = new DefaultRetryPolicy(config); + var context = CreateExecutionContext(); + var exception = MockNetworkErrorGenerator.CreateContentstackErrorException(HttpStatusCode.InternalServerError); + + var result = policy.RetryForException(context, exception); + Assert.IsTrue(result); + } + + [TestMethod] + public void RetryForException_HttpError_429_Respects_RetryLimit() + { + var config = new RetryConfiguration + { + RetryLimit = 2 + }; + var policy = new DefaultRetryPolicy(config); + var context = CreateExecutionContext(); + var exception = MockNetworkErrorGenerator.CreateContentstackErrorException(HttpStatusCode.TooManyRequests); + + context.RequestContext.HttpRetryCount = 1; + var result1 = policy.RetryForException(context, exception); + Assert.IsTrue(result1); + + context.RequestContext.HttpRetryCount = 2; + var result2 = policy.RetryForException(context, exception); + Assert.IsFalse(result2); + } + + [TestMethod] + public void RetryForException_NetworkError_Exceeds_MaxNetworkRetries_Returns_False() + { + var config = new RetryConfiguration + { + RetryOnNetworkFailure = true, + RetryOnSocketFailure = true, + MaxNetworkRetries = 1 + }; + var policy = new DefaultRetryPolicy(config); + var context = CreateExecutionContext(); + context.RequestContext.NetworkRetryCount = 1; + var exception = MockNetworkErrorGenerator.CreateSocketException(SocketError.ConnectionReset); + + var result = policy.RetryForException(context, exception); + Assert.IsFalse(result); + } + + [TestMethod] + public void RetryForException_HttpError_Exceeds_RetryLimit_Returns_False() + { + var config = new RetryConfiguration + { + RetryOnHttpServerError = true, + RetryLimit = 1 + }; + var policy = new DefaultRetryPolicy(config); + var context = CreateExecutionContext(); + context.RequestContext.HttpRetryCount = 1; + var exception = MockNetworkErrorGenerator.CreateContentstackErrorException(HttpStatusCode.InternalServerError); + + var result = policy.RetryForException(context, exception); + Assert.IsFalse(result); + } + + [TestMethod] + public void RetryForException_NonRetryableException_Returns_False() + { + var config = new RetryConfiguration(); + var policy = new DefaultRetryPolicy(config); + var context = CreateExecutionContext(); + var exception = new ArgumentException("Invalid argument"); + + var result = policy.RetryForException(context, exception); + Assert.IsFalse(result); + } + + [TestMethod] + public void RetryLimitExceeded_Checks_Both_Network_And_Http_Counts() + { + var config = new RetryConfiguration + { + MaxNetworkRetries = 2, + RetryLimit = 3 + }; + var policy = new DefaultRetryPolicy(config); + var context = CreateExecutionContext(); + + context.RequestContext.NetworkRetryCount = 1; + context.RequestContext.HttpRetryCount = 2; + var result1 = policy.RetryLimitExceeded(context); + Assert.IsFalse(result1); + + context.RequestContext.NetworkRetryCount = 2; + context.RequestContext.HttpRetryCount = 3; + var result2 = policy.RetryLimitExceeded(context); + Assert.IsTrue(result2); + } + + [TestMethod] + public void WaitBeforeRetry_Uses_NetworkDelay_For_NetworkRetries() + { + var config = new RetryConfiguration + { + NetworkRetryDelay = TimeSpan.FromMilliseconds(50), + NetworkBackoffStrategy = BackoffStrategy.Fixed + }; + var policy = new DefaultRetryPolicy(config); + var context = CreateExecutionContext(); + context.RequestContext.NetworkRetryCount = 1; + + var startTime = DateTime.UtcNow; + policy.WaitBeforeRetry(context); + var elapsed = DateTime.UtcNow - startTime; + + // Should wait approximately 50ms + jitter (0-100ms) + Assert.IsTrue(elapsed >= TimeSpan.FromMilliseconds(50)); + Assert.IsTrue(elapsed <= TimeSpan.FromMilliseconds(200)); + } + + [TestMethod] + public void WaitBeforeRetry_Uses_HttpDelay_For_HttpRetries() + { + var config = new RetryConfiguration + { + RetryDelay = TimeSpan.FromMilliseconds(100), + RetryDelayOptions = new RetryDelayOptions + { + Base = TimeSpan.FromMilliseconds(100) + } + }; + var policy = new DefaultRetryPolicy(config); + var context = CreateExecutionContext(); + context.RequestContext.HttpRetryCount = 1; + context.RequestContext.NetworkRetryCount = 0; + + var startTime = DateTime.UtcNow; + policy.WaitBeforeRetry(context); + var elapsed = DateTime.UtcNow - startTime; + + // Should wait approximately 200ms (100ms * 2^1) + jitter + Assert.IsTrue(elapsed >= TimeSpan.FromMilliseconds(200)); + Assert.IsTrue(elapsed <= TimeSpan.FromMilliseconds(300)); + } + + [TestMethod] + public void WaitBeforeRetry_Fallback_To_Legacy_Delay() + { + var policy = new DefaultRetryPolicy(5, TimeSpan.FromMilliseconds(150)); + var context = CreateExecutionContext(); + + var startTime = DateTime.UtcNow; + policy.WaitBeforeRetry(context); + var elapsed = DateTime.UtcNow - startTime; + + // Should wait approximately 150ms + Assert.IsTrue(elapsed >= TimeSpan.FromMilliseconds(150)); + Assert.IsTrue(elapsed <= TimeSpan.FromMilliseconds(200)); + } + + [TestMethod] + public void ShouldRetryHttpStatusCode_Respects_Configuration() + { + var config = new RetryConfiguration + { + RetryCondition = (statusCode) => statusCode == HttpStatusCode.NotFound + }; + var policy = new DefaultRetryPolicy(config); + var context = CreateExecutionContext(); + + var result = policy.ShouldRetryHttpStatusCode(HttpStatusCode.NotFound, context.RequestContext); + Assert.IsTrue(result); + + var result2 = policy.ShouldRetryHttpStatusCode(HttpStatusCode.InternalServerError, context.RequestContext); + Assert.IsFalse(result2); + } + + [TestMethod] + public void ShouldRetryHttpStatusCode_Respects_RetryLimit() + { + var config = new RetryConfiguration + { + RetryLimit = 2 + }; + var policy = new DefaultRetryPolicy(config); + var context = CreateExecutionContext(); + context.RequestContext.HttpRetryCount = 2; + + var result = policy.ShouldRetryHttpStatusCode(HttpStatusCode.TooManyRequests, context.RequestContext); + Assert.IsFalse(result); + } + + [TestMethod] + public void GetHttpRetryDelay_Uses_DelayCalculator() + { + var config = new RetryConfiguration + { + RetryDelay = TimeSpan.FromMilliseconds(200), + RetryDelayOptions = new RetryDelayOptions + { + Base = TimeSpan.FromMilliseconds(200) + } + }; + var policy = new DefaultRetryPolicy(config); + var context = CreateExecutionContext(); + context.RequestContext.HttpRetryCount = 1; + + var delay = policy.GetHttpRetryDelay(context.RequestContext, null); + + // Should be approximately 400ms (200ms * 2^1) + jitter + Assert.IsTrue(delay >= TimeSpan.FromMilliseconds(400)); + Assert.IsTrue(delay <= TimeSpan.FromMilliseconds(500)); + } + + [TestMethod] + public void GetNetworkRetryDelay_Uses_DelayCalculator() + { + var config = new RetryConfiguration + { + NetworkRetryDelay = TimeSpan.FromMilliseconds(100), + NetworkBackoffStrategy = BackoffStrategy.Exponential + }; + var policy = new DefaultRetryPolicy(config); + var context = CreateExecutionContext(); + context.RequestContext.NetworkRetryCount = 2; + + var delay = policy.GetNetworkRetryDelay(context.RequestContext); + + // Should be approximately 200ms (100ms * 2^1) + jitter + Assert.IsTrue(delay >= TimeSpan.FromMilliseconds(200)); + Assert.IsTrue(delay <= TimeSpan.FromMilliseconds(300)); + } + + private ExecutionContext CreateExecutionContext() + { + return new ExecutionContext( + new RequestContext + { + config = new ContentstackClientOptions(), + service = new MockService() + }, + new ResponseContext()); + } + } +} + diff --git a/Contentstack.Management.Core.Unit.Tests/Runtime/Pipeline/RetryHandler/NetworkErrorDetectorTest.cs b/Contentstack.Management.Core.Unit.Tests/Runtime/Pipeline/RetryHandler/NetworkErrorDetectorTest.cs new file mode 100644 index 0000000..620cd2c --- /dev/null +++ b/Contentstack.Management.Core.Unit.Tests/Runtime/Pipeline/RetryHandler/NetworkErrorDetectorTest.cs @@ -0,0 +1,340 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Contentstack.Management.Core.Exceptions; +using Contentstack.Management.Core.Runtime.Pipeline.RetryHandler; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Contentstack.Management.Core.Unit.Tests.Runtime.Pipeline.RetryHandler +{ + [TestClass] + public class NetworkErrorDetectorTest + { + private NetworkErrorDetector detector; + + [TestInitialize] + public void Initialize() + { + detector = new NetworkErrorDetector(); + } + + [TestMethod] + public void Should_Detect_SocketException_ConnectionReset() + { + var exception = new SocketException((int)SocketError.ConnectionReset); + var result = detector.IsTransientNetworkError(exception); + + Assert.IsNotNull(result); + Assert.AreEqual(NetworkErrorType.SocketError, result.ErrorType); + Assert.IsTrue(result.IsTransient); + Assert.AreEqual(exception, result.OriginalException); + } + + [TestMethod] + public void Should_Detect_SocketException_TimedOut() + { + var exception = new SocketException((int)SocketError.TimedOut); + var result = detector.IsTransientNetworkError(exception); + + Assert.IsNotNull(result); + Assert.AreEqual(NetworkErrorType.SocketError, result.ErrorType); + Assert.IsTrue(result.IsTransient); + } + + [TestMethod] + public void Should_Detect_SocketException_ConnectionRefused() + { + var exception = new SocketException((int)SocketError.ConnectionRefused); + var result = detector.IsTransientNetworkError(exception); + + Assert.IsNotNull(result); + Assert.AreEqual(NetworkErrorType.SocketError, result.ErrorType); + Assert.IsTrue(result.IsTransient); + } + + [TestMethod] + public void Should_Detect_SocketException_HostNotFound() + { + var exception = new SocketException((int)SocketError.HostNotFound); + var result = detector.IsTransientNetworkError(exception); + + Assert.IsNotNull(result); + Assert.AreEqual(NetworkErrorType.DnsFailure, result.ErrorType); + Assert.IsTrue(result.IsTransient); + } + + [TestMethod] + public void Should_Detect_SocketException_TryAgain() + { + var exception = new SocketException((int)SocketError.TryAgain); + var result = detector.IsTransientNetworkError(exception); + + Assert.IsNotNull(result); + Assert.AreEqual(NetworkErrorType.DnsFailure, result.ErrorType); + Assert.IsTrue(result.IsTransient); + } + + [TestMethod] + public void Should_Detect_TaskCanceledException_Timeout() + { + var cts = new CancellationTokenSource(); + var exception = new TaskCanceledException("Operation timed out", null, cts.Token); + var result = detector.IsTransientNetworkError(exception); + + Assert.IsNotNull(result); + Assert.AreEqual(NetworkErrorType.Timeout, result.ErrorType); + Assert.IsTrue(result.IsTransient); + } + + [TestMethod] + public void Should_Not_Detect_TaskCanceledException_UserCancellation() + { + var cts = new CancellationTokenSource(); + cts.Cancel(); + var exception = new TaskCanceledException("User cancelled", null, cts.Token); + var result = detector.IsTransientNetworkError(exception); + + Assert.IsNull(result); + } + + [TestMethod] + public void Should_Detect_TimeoutException() + { + var exception = new TimeoutException("Operation timed out"); + var result = detector.IsTransientNetworkError(exception); + + Assert.IsNotNull(result); + Assert.AreEqual(NetworkErrorType.Timeout, result.ErrorType); + Assert.IsTrue(result.IsTransient); + } + + [TestMethod] + public void Should_Detect_HttpRequestException_With_Inner_SocketException() + { + var socketException = new SocketException((int)SocketError.ConnectionReset); + var httpException = new HttpRequestException("Network error", socketException); + var result = detector.IsTransientNetworkError(httpException); + + Assert.IsNotNull(result); + Assert.AreEqual(NetworkErrorType.SocketError, result.ErrorType); + Assert.IsTrue(result.IsTransient); + } + + [TestMethod] + public void Should_Detect_ContentstackErrorException_5xx() + { + var exception = new ContentstackErrorException + { + StatusCode = HttpStatusCode.InternalServerError + }; + var result = detector.IsTransientNetworkError(exception); + + Assert.IsNotNull(result); + Assert.AreEqual(NetworkErrorType.HttpServerError, result.ErrorType); + Assert.IsTrue(result.IsTransient); + } + + [TestMethod] + public void Should_Detect_ContentstackErrorException_502() + { + var exception = new ContentstackErrorException + { + StatusCode = HttpStatusCode.BadGateway + }; + var result = detector.IsTransientNetworkError(exception); + + Assert.IsNotNull(result); + Assert.AreEqual(NetworkErrorType.HttpServerError, result.ErrorType); + } + + [TestMethod] + public void Should_Detect_ContentstackErrorException_503() + { + var exception = new ContentstackErrorException + { + StatusCode = HttpStatusCode.ServiceUnavailable + }; + var result = detector.IsTransientNetworkError(exception); + + Assert.IsNotNull(result); + Assert.AreEqual(NetworkErrorType.HttpServerError, result.ErrorType); + } + + [TestMethod] + public void Should_Detect_ContentstackErrorException_504() + { + var exception = new ContentstackErrorException + { + StatusCode = HttpStatusCode.GatewayTimeout + }; + var result = detector.IsTransientNetworkError(exception); + + Assert.IsNotNull(result); + Assert.AreEqual(NetworkErrorType.HttpServerError, result.ErrorType); + } + + [TestMethod] + public void Should_Not_Detect_ContentstackErrorException_4xx() + { + var exception = new ContentstackErrorException + { + StatusCode = HttpStatusCode.BadRequest + }; + var result = detector.IsTransientNetworkError(exception); + + Assert.IsNull(result); + } + + [TestMethod] + public void Should_Not_Detect_ContentstackErrorException_404() + { + var exception = new ContentstackErrorException + { + StatusCode = HttpStatusCode.NotFound + }; + var result = detector.IsTransientNetworkError(exception); + + Assert.IsNull(result); + } + + [TestMethod] + public void Should_Return_Null_For_NonNetworkError() + { + var exception = new ArgumentException("Invalid argument"); + var result = detector.IsTransientNetworkError(exception); + + Assert.IsNull(result); + } + + [TestMethod] + public void Should_Return_Null_For_Null() + { + var result = detector.IsTransientNetworkError(null); + + Assert.IsNull(result); + } + + [TestMethod] + public void ShouldRetryNetworkError_Respects_Configuration() + { + var socketException = new SocketException((int)SocketError.ConnectionReset); + var errorInfo = new NetworkErrorInfo(NetworkErrorType.SocketError, true, socketException); + + var config = new RetryConfiguration + { + RetryOnNetworkFailure = true, + RetryOnSocketFailure = true + }; + + var result = detector.ShouldRetryNetworkError(errorInfo, config); + Assert.IsTrue(result); + } + + [TestMethod] + public void ShouldRetryNetworkError_DnsFailure_Respects_RetryOnDnsFailure() + { + var socketException = new SocketException((int)SocketError.HostNotFound); + var errorInfo = new NetworkErrorInfo(NetworkErrorType.DnsFailure, true, socketException); + + var config = new RetryConfiguration + { + RetryOnNetworkFailure = true, + RetryOnDnsFailure = true + }; + + var result = detector.ShouldRetryNetworkError(errorInfo, config); + Assert.IsTrue(result); + + config.RetryOnDnsFailure = false; + result = detector.ShouldRetryNetworkError(errorInfo, config); + Assert.IsFalse(result); + } + + [TestMethod] + public void ShouldRetryNetworkError_SocketError_Respects_RetryOnSocketFailure() + { + var socketException = new SocketException((int)SocketError.ConnectionReset); + var errorInfo = new NetworkErrorInfo(NetworkErrorType.SocketError, true, socketException); + + var config = new RetryConfiguration + { + RetryOnNetworkFailure = true, + RetryOnSocketFailure = true + }; + + var result = detector.ShouldRetryNetworkError(errorInfo, config); + Assert.IsTrue(result); + + config.RetryOnSocketFailure = false; + result = detector.ShouldRetryNetworkError(errorInfo, config); + Assert.IsFalse(result); + } + + [TestMethod] + public void ShouldRetryNetworkError_HttpServerError_Respects_RetryOnHttpServerError() + { + var httpException = new ContentstackErrorException { StatusCode = HttpStatusCode.InternalServerError }; + var errorInfo = new NetworkErrorInfo(NetworkErrorType.HttpServerError, true, httpException); + + var config = new RetryConfiguration + { + RetryOnNetworkFailure = true, + RetryOnHttpServerError = true + }; + + var result = detector.ShouldRetryNetworkError(errorInfo, config); + Assert.IsTrue(result); + + config.RetryOnHttpServerError = false; + result = detector.ShouldRetryNetworkError(errorInfo, config); + Assert.IsFalse(result); + } + + [TestMethod] + public void ShouldRetryNetworkError_Returns_False_When_RetryOnNetworkFailure_Is_False() + { + var socketException = new SocketException((int)SocketError.ConnectionReset); + var errorInfo = new NetworkErrorInfo(NetworkErrorType.SocketError, true, socketException); + + var config = new RetryConfiguration + { + RetryOnNetworkFailure = false, + RetryOnSocketFailure = true + }; + + var result = detector.ShouldRetryNetworkError(errorInfo, config); + Assert.IsFalse(result); + } + + [TestMethod] + public void ShouldRetryNetworkError_Returns_False_When_Not_Transient() + { + var exception = new ArgumentException("Not transient"); + var errorInfo = new NetworkErrorInfo(NetworkErrorType.Unknown, false, exception); + + var config = new RetryConfiguration + { + RetryOnNetworkFailure = true + }; + + var result = detector.ShouldRetryNetworkError(errorInfo, config); + Assert.IsFalse(result); + } + + [TestMethod] + public void ShouldRetryNetworkError_Returns_False_When_Null() + { + var config = new RetryConfiguration + { + RetryOnNetworkFailure = true + }; + + var result = detector.ShouldRetryNetworkError(null, config); + Assert.IsFalse(result); + } + } +} + diff --git a/Contentstack.Management.Core.Unit.Tests/Runtime/Pipeline/RetryHandler/RetryConfigurationTest.cs b/Contentstack.Management.Core.Unit.Tests/Runtime/Pipeline/RetryHandler/RetryConfigurationTest.cs new file mode 100644 index 0000000..7f201de --- /dev/null +++ b/Contentstack.Management.Core.Unit.Tests/Runtime/Pipeline/RetryHandler/RetryConfigurationTest.cs @@ -0,0 +1,111 @@ +using System; +using System.Net; +using Contentstack.Management.Core; +using Contentstack.Management.Core.Runtime.Pipeline.RetryHandler; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Contentstack.Management.Core.Unit.Tests.Runtime.Pipeline.RetryHandler +{ + [TestClass] + public class RetryConfigurationTest + { + [TestMethod] + public void FromOptions_Creates_Configuration_With_All_Properties() + { + var options = new ContentstackClientOptions + { + RetryOnError = true, + RetryLimit = 5, + RetryDelay = TimeSpan.FromMilliseconds(300), + RetryOnNetworkFailure = true, + RetryOnDnsFailure = true, + RetryOnSocketFailure = true, + RetryOnHttpServerError = true, + MaxNetworkRetries = 3, + NetworkRetryDelay = TimeSpan.FromMilliseconds(100), + NetworkBackoffStrategy = BackoffStrategy.Exponential, + RetryCondition = (statusCode) => statusCode == HttpStatusCode.TooManyRequests, + RetryDelayOptions = new RetryDelayOptions + { + Base = TimeSpan.FromMilliseconds(300), + CustomBackoff = (retryCount, error) => TimeSpan.FromMilliseconds(500) + } + }; + + var config = RetryConfiguration.FromOptions(options); + + Assert.AreEqual(options.RetryOnError, config.RetryOnError); + Assert.AreEqual(options.RetryLimit, config.RetryLimit); + Assert.AreEqual(options.RetryDelay, config.RetryDelay); + Assert.AreEqual(options.RetryOnNetworkFailure, config.RetryOnNetworkFailure); + Assert.AreEqual(options.RetryOnDnsFailure, config.RetryOnDnsFailure); + Assert.AreEqual(options.RetryOnSocketFailure, config.RetryOnSocketFailure); + Assert.AreEqual(options.RetryOnHttpServerError, config.RetryOnHttpServerError); + Assert.AreEqual(options.MaxNetworkRetries, config.MaxNetworkRetries); + Assert.AreEqual(options.NetworkRetryDelay, config.NetworkRetryDelay); + Assert.AreEqual(options.NetworkBackoffStrategy, config.NetworkBackoffStrategy); + Assert.AreEqual(options.RetryCondition, config.RetryCondition); + Assert.IsNotNull(config.RetryDelayOptions); + Assert.AreEqual(options.RetryDelayOptions.Base, config.RetryDelayOptions.Base); + Assert.AreEqual(options.RetryDelayOptions.CustomBackoff, config.RetryDelayOptions.CustomBackoff); + } + + [TestMethod] + public void FromOptions_Handles_Null_RetryDelayOptions() + { + var options = new ContentstackClientOptions + { + RetryDelay = TimeSpan.FromMilliseconds(300), + RetryDelayOptions = null + }; + + var config = RetryConfiguration.FromOptions(options); + + Assert.IsNotNull(config.RetryDelayOptions); + Assert.AreEqual(options.RetryDelay, config.RetryDelayOptions.Base); + } + + [TestMethod] + public void FromOptions_Sets_RetryDelayOptions_Base_From_RetryDelay() + { + var options = new ContentstackClientOptions + { + RetryDelay = TimeSpan.FromMilliseconds(500), + RetryDelayOptions = null + }; + + var config = RetryConfiguration.FromOptions(options); + + Assert.AreEqual(TimeSpan.FromMilliseconds(500), config.RetryDelayOptions.Base); + } + + [TestMethod] + public void Default_Values_Are_Correct() + { + var config = new RetryConfiguration(); + + Assert.IsTrue(config.RetryOnError); + Assert.AreEqual(5, config.RetryLimit); + Assert.AreEqual(TimeSpan.FromMilliseconds(300), config.RetryDelay); + Assert.IsTrue(config.RetryOnNetworkFailure); + Assert.IsTrue(config.RetryOnDnsFailure); + Assert.IsTrue(config.RetryOnSocketFailure); + Assert.IsTrue(config.RetryOnHttpServerError); + Assert.AreEqual(3, config.MaxNetworkRetries); + Assert.AreEqual(TimeSpan.FromMilliseconds(100), config.NetworkRetryDelay); + Assert.AreEqual(BackoffStrategy.Exponential, config.NetworkBackoffStrategy); + Assert.IsNull(config.RetryCondition); + Assert.IsNotNull(config.RetryDelayOptions); + } + + [TestMethod] + public void RetryDelayOptions_Default_Values() + { + var options = new RetryDelayOptions(); + + Assert.AreEqual(TimeSpan.FromMilliseconds(300), options.Base); + Assert.IsNull(options.CustomBackoff); + } + } +} + diff --git a/Contentstack.Management.Core.Unit.Tests/Runtime/Pipeline/RetryHandler/RetryDelayCalculatorTest.cs b/Contentstack.Management.Core.Unit.Tests/Runtime/Pipeline/RetryHandler/RetryDelayCalculatorTest.cs new file mode 100644 index 0000000..39ff621 --- /dev/null +++ b/Contentstack.Management.Core.Unit.Tests/Runtime/Pipeline/RetryHandler/RetryDelayCalculatorTest.cs @@ -0,0 +1,363 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using Contentstack.Management.Core.Exceptions; +using Contentstack.Management.Core.Runtime.Pipeline.RetryHandler; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Contentstack.Management.Core.Unit.Tests.Runtime.Pipeline.RetryHandler +{ + [TestClass] + public class RetryDelayCalculatorTest + { + private RetryDelayCalculator calculator; + + [TestInitialize] + public void Initialize() + { + calculator = new RetryDelayCalculator(); + } + + [TestMethod] + public void CalculateNetworkRetryDelay_Exponential_FirstAttempt() + { + var config = new RetryConfiguration + { + NetworkRetryDelay = TimeSpan.FromMilliseconds(100), + NetworkBackoffStrategy = BackoffStrategy.Exponential + }; + + var delay = calculator.CalculateNetworkRetryDelay(1, config); + + // First attempt: 100ms * 2^0 = 100ms + jitter (0-100ms) + Assert.IsTrue(delay >= TimeSpan.FromMilliseconds(100)); + Assert.IsTrue(delay <= TimeSpan.FromMilliseconds(200)); + } + + [TestMethod] + public void CalculateNetworkRetryDelay_Exponential_SecondAttempt() + { + var config = new RetryConfiguration + { + NetworkRetryDelay = TimeSpan.FromMilliseconds(100), + NetworkBackoffStrategy = BackoffStrategy.Exponential + }; + + var delay = calculator.CalculateNetworkRetryDelay(2, config); + + // Second attempt: 100ms * 2^1 = 200ms + jitter (0-100ms) + Assert.IsTrue(delay >= TimeSpan.FromMilliseconds(200)); + Assert.IsTrue(delay <= TimeSpan.FromMilliseconds(300)); + } + + [TestMethod] + public void CalculateNetworkRetryDelay_Exponential_ThirdAttempt() + { + var config = new RetryConfiguration + { + NetworkRetryDelay = TimeSpan.FromMilliseconds(100), + NetworkBackoffStrategy = BackoffStrategy.Exponential + }; + + var delay = calculator.CalculateNetworkRetryDelay(3, config); + + // Third attempt: 100ms * 2^2 = 400ms + jitter (0-100ms) + Assert.IsTrue(delay >= TimeSpan.FromMilliseconds(400)); + Assert.IsTrue(delay <= TimeSpan.FromMilliseconds(500)); + } + + [TestMethod] + public void CalculateNetworkRetryDelay_Fixed_AllAttempts() + { + var config = new RetryConfiguration + { + NetworkRetryDelay = TimeSpan.FromMilliseconds(150), + NetworkBackoffStrategy = BackoffStrategy.Fixed + }; + + var delay1 = calculator.CalculateNetworkRetryDelay(1, config); + var delay2 = calculator.CalculateNetworkRetryDelay(2, config); + var delay3 = calculator.CalculateNetworkRetryDelay(3, config); + + // All attempts should be ~150ms + jitter + Assert.IsTrue(delay1 >= TimeSpan.FromMilliseconds(150)); + Assert.IsTrue(delay1 <= TimeSpan.FromMilliseconds(250)); + Assert.IsTrue(delay2 >= TimeSpan.FromMilliseconds(150)); + Assert.IsTrue(delay2 <= TimeSpan.FromMilliseconds(250)); + Assert.IsTrue(delay3 >= TimeSpan.FromMilliseconds(150)); + Assert.IsTrue(delay3 <= TimeSpan.FromMilliseconds(250)); + } + + [TestMethod] + public void CalculateNetworkRetryDelay_Includes_Jitter() + { + var config = new RetryConfiguration + { + NetworkRetryDelay = TimeSpan.FromMilliseconds(100), + NetworkBackoffStrategy = BackoffStrategy.Fixed + }; + + // Run multiple times to verify jitter is added + bool foundVariation = false; + var firstDelay = calculator.CalculateNetworkRetryDelay(1, config); + + for (int i = 0; i < 10; i++) + { + var delay = calculator.CalculateNetworkRetryDelay(1, config); + if (delay != firstDelay) + { + foundVariation = true; + break; + } + } + + // Jitter should cause some variation (though it's random, so not guaranteed) + // At minimum, verify the delay is within expected range + Assert.IsTrue(firstDelay >= TimeSpan.FromMilliseconds(100)); + Assert.IsTrue(firstDelay <= TimeSpan.FromMilliseconds(200)); + } + + [TestMethod] + public void CalculateHttpRetryDelay_Exponential_FirstRetry() + { + var config = new RetryConfiguration + { + RetryDelay = TimeSpan.FromMilliseconds(300), + RetryDelayOptions = new RetryDelayOptions + { + Base = TimeSpan.FromMilliseconds(300) + } + }; + + var delay = calculator.CalculateHttpRetryDelay(0, config, null); + + // First retry: 300ms * 2^0 = 300ms + jitter + Assert.IsTrue(delay >= TimeSpan.FromMilliseconds(300)); + Assert.IsTrue(delay <= TimeSpan.FromMilliseconds(400)); + } + + [TestMethod] + public void CalculateHttpRetryDelay_Exponential_SubsequentRetries() + { + var config = new RetryConfiguration + { + RetryDelay = TimeSpan.FromMilliseconds(300), + RetryDelayOptions = new RetryDelayOptions + { + Base = TimeSpan.FromMilliseconds(300) + } + }; + + var delay1 = calculator.CalculateHttpRetryDelay(1, config, null); + var delay2 = calculator.CalculateHttpRetryDelay(2, config, null); + + // Second retry: 300ms * 2^1 = 600ms + jitter + Assert.IsTrue(delay1 >= TimeSpan.FromMilliseconds(600)); + Assert.IsTrue(delay1 <= TimeSpan.FromMilliseconds(700)); + + // Third retry: 300ms * 2^2 = 1200ms + jitter + Assert.IsTrue(delay2 >= TimeSpan.FromMilliseconds(1200)); + Assert.IsTrue(delay2 <= TimeSpan.FromMilliseconds(1300)); + } + + [TestMethod] + public void CalculateHttpRetryDelay_Respects_RetryAfter_Header_Delta() + { + var config = new RetryConfiguration + { + RetryDelay = TimeSpan.FromMilliseconds(300) + }; + + var response = new HttpResponseMessage(HttpStatusCode.TooManyRequests); + response.Headers.RetryAfter = new RetryConditionHeaderValue(TimeSpan.FromSeconds(5)); + + var delay = calculator.CalculateHttpRetryDelay(0, config, null, response.Headers); + + Assert.AreEqual(TimeSpan.FromSeconds(5), delay); + } + + [TestMethod] + public void CalculateHttpRetryDelay_Respects_RetryAfter_Header_Date() + { + var config = new RetryConfiguration + { + RetryDelay = TimeSpan.FromMilliseconds(300) + }; + + var response = new HttpResponseMessage(HttpStatusCode.TooManyRequests); + var retryAfterDate = DateTimeOffset.UtcNow.AddSeconds(3); + response.Headers.RetryAfter = new RetryConditionHeaderValue(retryAfterDate); + + var delay = calculator.CalculateHttpRetryDelay(0, config, null, response.Headers); + + // Should be approximately 3 seconds (allowing for small timing differences) + Assert.IsTrue(delay >= TimeSpan.FromSeconds(2.5)); + Assert.IsTrue(delay <= TimeSpan.FromSeconds(3.5)); + } + + [TestMethod] + public void CalculateHttpRetryDelay_Uses_CustomBackoff_When_Provided() + { + var config = new RetryConfiguration + { + RetryDelay = TimeSpan.FromMilliseconds(300), + RetryDelayOptions = new RetryDelayOptions + { + CustomBackoff = (retryCount, error) => TimeSpan.FromMilliseconds(500 * (retryCount + 1)) + } + }; + + var delay1 = calculator.CalculateHttpRetryDelay(0, config, null); + var delay2 = calculator.CalculateHttpRetryDelay(1, config, null); + + Assert.AreEqual(TimeSpan.FromMilliseconds(500), delay1); + Assert.AreEqual(TimeSpan.FromMilliseconds(1000), delay2); + } + + [TestMethod] + public void CalculateHttpRetryDelay_CustomBackoff_Returns_Zero_Disables_Retry() + { + var config = new RetryConfiguration + { + RetryDelay = TimeSpan.FromMilliseconds(300), + RetryDelayOptions = new RetryDelayOptions + { + CustomBackoff = (retryCount, error) => retryCount >= 2 ? TimeSpan.Zero : TimeSpan.FromMilliseconds(100) + } + }; + + var delay1 = calculator.CalculateHttpRetryDelay(0, config, null); + var delay2 = calculator.CalculateHttpRetryDelay(1, config, null); + var delay3 = calculator.CalculateHttpRetryDelay(2, config, null); + + Assert.AreEqual(TimeSpan.FromMilliseconds(100), delay1); + Assert.AreEqual(TimeSpan.FromMilliseconds(100), delay2); + Assert.AreEqual(TimeSpan.Zero, delay3); + } + + [TestMethod] + public void CalculateHttpRetryDelay_CustomBackoff_Returns_Negative_Disables_Retry() + { + var config = new RetryConfiguration + { + RetryDelay = TimeSpan.FromMilliseconds(300), + RetryDelayOptions = new RetryDelayOptions + { + CustomBackoff = (retryCount, error) => retryCount >= 2 ? TimeSpan.FromMilliseconds(-1) : TimeSpan.FromMilliseconds(100) + } + }; + + var delay1 = calculator.CalculateHttpRetryDelay(0, config, null); + var delay2 = calculator.CalculateHttpRetryDelay(1, config, null); + var delay3 = calculator.CalculateHttpRetryDelay(2, config, null); + + Assert.AreEqual(TimeSpan.FromMilliseconds(100), delay1); + Assert.AreEqual(TimeSpan.FromMilliseconds(100), delay2); + Assert.IsTrue(delay3 < TimeSpan.Zero); + } + + [TestMethod] + public void CalculateHttpRetryDelay_Includes_Jitter() + { + var config = new RetryConfiguration + { + RetryDelay = TimeSpan.FromMilliseconds(300), + RetryDelayOptions = new RetryDelayOptions + { + Base = TimeSpan.FromMilliseconds(300) + } + }; + + var delay = calculator.CalculateHttpRetryDelay(0, config, null); + + // Should be 300ms + jitter (0-100ms) + Assert.IsTrue(delay >= TimeSpan.FromMilliseconds(300)); + Assert.IsTrue(delay <= TimeSpan.FromMilliseconds(400)); + } + + [TestMethod] + public void CalculateHttpRetryDelay_Uses_RetryDelay_When_Base_Is_Zero() + { + var config = new RetryConfiguration + { + RetryDelay = TimeSpan.FromMilliseconds(500), + RetryDelayOptions = new RetryDelayOptions + { + Base = TimeSpan.Zero + } + }; + + var delay = calculator.CalculateHttpRetryDelay(0, config, null); + + // Should use RetryDelay (500ms) instead of Base + Assert.IsTrue(delay >= TimeSpan.FromMilliseconds(500)); + Assert.IsTrue(delay <= TimeSpan.FromMilliseconds(600)); + } + + [TestMethod] + public void ShouldRetryHttpStatusCode_Default_429() + { + var config = new RetryConfiguration(); + var result = calculator.ShouldRetryHttpStatusCode(HttpStatusCode.TooManyRequests, config); + Assert.IsTrue(result); + } + + [TestMethod] + public void ShouldRetryHttpStatusCode_Default_500() + { + var config = new RetryConfiguration(); + var result = calculator.ShouldRetryHttpStatusCode(HttpStatusCode.InternalServerError, config); + Assert.IsTrue(result); + } + + [TestMethod] + public void ShouldRetryHttpStatusCode_Default_502() + { + var config = new RetryConfiguration(); + var result = calculator.ShouldRetryHttpStatusCode(HttpStatusCode.BadGateway, config); + Assert.IsTrue(result); + } + + [TestMethod] + public void ShouldRetryHttpStatusCode_Default_503() + { + var config = new RetryConfiguration(); + var result = calculator.ShouldRetryHttpStatusCode(HttpStatusCode.ServiceUnavailable, config); + Assert.IsTrue(result); + } + + [TestMethod] + public void ShouldRetryHttpStatusCode_Default_504() + { + var config = new RetryConfiguration(); + var result = calculator.ShouldRetryHttpStatusCode(HttpStatusCode.GatewayTimeout, config); + Assert.IsTrue(result); + } + + [TestMethod] + public void ShouldRetryHttpStatusCode_Default_Not_4xx() + { + var config = new RetryConfiguration(); + + Assert.IsFalse(calculator.ShouldRetryHttpStatusCode(HttpStatusCode.BadRequest, config)); + Assert.IsFalse(calculator.ShouldRetryHttpStatusCode(HttpStatusCode.Unauthorized, config)); + Assert.IsFalse(calculator.ShouldRetryHttpStatusCode(HttpStatusCode.Forbidden, config)); + Assert.IsFalse(calculator.ShouldRetryHttpStatusCode(HttpStatusCode.NotFound, config)); + } + + [TestMethod] + public void ShouldRetryHttpStatusCode_Custom_Condition() + { + var config = new RetryConfiguration + { + RetryCondition = (statusCode) => statusCode == HttpStatusCode.NotFound || statusCode == HttpStatusCode.TooManyRequests + }; + + Assert.IsTrue(calculator.ShouldRetryHttpStatusCode(HttpStatusCode.NotFound, config)); + Assert.IsTrue(calculator.ShouldRetryHttpStatusCode(HttpStatusCode.TooManyRequests, config)); + Assert.IsFalse(calculator.ShouldRetryHttpStatusCode(HttpStatusCode.InternalServerError, config)); + } + } +} + diff --git a/Contentstack.Management.Core.Unit.Tests/Runtime/Pipeline/RetryHandler/RetryHandlerIntegrationTest.cs b/Contentstack.Management.Core.Unit.Tests/Runtime/Pipeline/RetryHandler/RetryHandlerIntegrationTest.cs new file mode 100644 index 0000000..142b479 --- /dev/null +++ b/Contentstack.Management.Core.Unit.Tests/Runtime/Pipeline/RetryHandler/RetryHandlerIntegrationTest.cs @@ -0,0 +1,262 @@ +using System; +using System.Net; +using System.Net.Sockets; +using Contentstack.Management.Core; +using Contentstack.Management.Core.Runtime.Contexts; +using Contentstack.Management.Core.Runtime.Pipeline.RetryHandler; +using Contentstack.Management.Core.Unit.Tests.Mokes; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Threading.Tasks; + +namespace Contentstack.Management.Core.Unit.Tests.Runtime.Pipeline.RetryHandler +{ + [TestClass] + public class RetryHandlerIntegrationTest + { + private ExecutionContext CreateExecutionContext() + { + return new ExecutionContext( + new RequestContext + { + config = new ContentstackClientOptions(), + service = new MockService() + }, + new ResponseContext()); + } + + [TestMethod] + public async Task EndToEnd_NetworkError_Retries_And_Succeeds() + { + var config = new RetryConfiguration + { + RetryOnNetworkFailure = true, + RetryOnSocketFailure = true, + MaxNetworkRetries = 3, + NetworkRetryDelay = TimeSpan.FromMilliseconds(10), + NetworkBackoffStrategy = BackoffStrategy.Exponential + }; + var policy = new DefaultRetryPolicy(config); + var handler = new RetryHandler(policy); + var mockInnerHandler = new MockHttpHandlerWithRetries(); + mockInnerHandler.AddFailuresThenSuccess(2, MockNetworkErrorGenerator.CreateSocketException(SocketError.ConnectionReset)); + handler.InnerHandler = mockInnerHandler; + handler.LogManager = LogManager.EmptyLogger; + + var context = CreateExecutionContext(); + var result = await handler.InvokeAsync(context); + + Assert.IsNotNull(result); + Assert.IsTrue(result.IsSuccessStatusCode); + Assert.AreEqual(3, mockInnerHandler.CallCount); + Assert.AreEqual(2, context.RequestContext.NetworkRetryCount); + } + + [TestMethod] + public async Task EndToEnd_HttpError_Retries_And_Succeeds() + { + var config = new RetryConfiguration + { + RetryLimit = 3, + RetryDelay = TimeSpan.FromMilliseconds(10), + RetryDelayOptions = new RetryDelayOptions + { + Base = TimeSpan.FromMilliseconds(10) + } + }; + var policy = new DefaultRetryPolicy(config); + var handler = new RetryHandler(policy); + var mockInnerHandler = new MockHttpHandlerWithRetries(); + mockInnerHandler.AddHttpErrorsThenSuccess(2, HttpStatusCode.InternalServerError); + handler.InnerHandler = mockInnerHandler; + handler.LogManager = LogManager.EmptyLogger; + + var context = CreateExecutionContext(); + var result = await handler.InvokeAsync(context); + + Assert.IsNotNull(result); + Assert.IsTrue(result.IsSuccessStatusCode); + Assert.AreEqual(3, mockInnerHandler.CallCount); + Assert.AreEqual(2, context.RequestContext.HttpRetryCount); + } + + [TestMethod] + public async Task EndToEnd_Mixed_Network_And_Http_Errors() + { + var config = new RetryConfiguration + { + RetryOnNetworkFailure = true, + RetryOnSocketFailure = true, + MaxNetworkRetries = 3, + RetryLimit = 3, + RetryOnHttpServerError = true, + NetworkRetryDelay = TimeSpan.FromMilliseconds(10), + RetryDelay = TimeSpan.FromMilliseconds(10) + }; + var policy = new DefaultRetryPolicy(config); + var handler = new RetryHandler(policy); + var mockInnerHandler = new MockHttpHandlerWithRetries(); + mockInnerHandler.AddException(MockNetworkErrorGenerator.CreateSocketException(SocketError.ConnectionReset)); + mockInnerHandler.AddResponse(HttpStatusCode.InternalServerError); + mockInnerHandler.AddResponse(HttpStatusCode.TooManyRequests); + mockInnerHandler.AddSuccessResponse(); + handler.InnerHandler = mockInnerHandler; + handler.LogManager = LogManager.EmptyLogger; + + var context = CreateExecutionContext(); + var result = await handler.InvokeAsync(context); + + Assert.IsNotNull(result); + Assert.IsTrue(result.IsSuccessStatusCode); + Assert.AreEqual(4, mockInnerHandler.CallCount); + Assert.AreEqual(1, context.RequestContext.NetworkRetryCount); + Assert.AreEqual(2, context.RequestContext.HttpRetryCount); + } + + [TestMethod] + public async Task EndToEnd_Respects_RetryConfiguration() + { + var config = new RetryConfiguration + { + RetryOnError = false + }; + var policy = new DefaultRetryPolicy(config); + var handler = new RetryHandler(policy); + var mockInnerHandler = new MockHttpHandlerWithRetries(); + mockInnerHandler.AddException(MockNetworkErrorGenerator.CreateSocketException(SocketError.ConnectionReset)); + handler.InnerHandler = mockInnerHandler; + handler.LogManager = LogManager.EmptyLogger; + + var context = CreateExecutionContext(); + + try + { + await handler.InvokeAsync(context); + Assert.Fail("Should have thrown exception"); + } + catch (SocketException) + { + // Expected + } + + // Should not retry when RetryOnError is false + Assert.AreEqual(1, mockInnerHandler.CallCount); + } + + [TestMethod] + public async Task EndToEnd_ExponentialBackoff_Delays_Increase() + { + var config = new RetryConfiguration + { + RetryOnNetworkFailure = true, + RetryOnSocketFailure = true, + MaxNetworkRetries = 2, + NetworkRetryDelay = TimeSpan.FromMilliseconds(50), + NetworkBackoffStrategy = BackoffStrategy.Exponential + }; + var policy = new DefaultRetryPolicy(config); + var handler = new RetryHandler(policy); + var mockInnerHandler = new MockHttpHandlerWithRetries(); + mockInnerHandler.AddFailuresThenSuccess(2, MockNetworkErrorGenerator.CreateSocketException(SocketError.ConnectionReset)); + handler.InnerHandler = mockInnerHandler; + handler.LogManager = LogManager.EmptyLogger; + + var context = CreateExecutionContext(); + var startTime = DateTime.UtcNow; + await handler.InvokeAsync(context); + var totalElapsed = DateTime.UtcNow - startTime; + + // First retry: ~50ms, second retry: ~100ms (exponential) + // Total should be at least 150ms + jitter + Assert.IsTrue(totalElapsed >= TimeSpan.FromMilliseconds(150)); + } + + [TestMethod] + public async Task EndToEnd_RetryLimit_Stops_Retries() + { + var config = new RetryConfiguration + { + RetryLimit = 2, + RetryDelay = TimeSpan.FromMilliseconds(10) + }; + var policy = new DefaultRetryPolicy(config); + var handler = new RetryHandler(policy); + var mockInnerHandler = new MockHttpHandlerWithRetries(); + mockInnerHandler.AddResponse(HttpStatusCode.TooManyRequests); + mockInnerHandler.AddResponse(HttpStatusCode.TooManyRequests); + mockInnerHandler.AddResponse(HttpStatusCode.TooManyRequests); + handler.InnerHandler = mockInnerHandler; + handler.LogManager = LogManager.EmptyLogger; + + var context = CreateExecutionContext(); + + try + { + await handler.InvokeAsync(context); + Assert.Fail("Should have thrown exception"); + } + catch (ContentstackErrorException) + { + // Expected + } + + // Should stop after 2 retries (3 total calls) + Assert.AreEqual(3, mockInnerHandler.CallCount); + Assert.AreEqual(2, context.RequestContext.HttpRetryCount); + } + + [TestMethod] + public async Task EndToEnd_With_CustomRetryCondition() + { + var config = new RetryConfiguration + { + RetryLimit = 2, + RetryDelay = TimeSpan.FromMilliseconds(10), + RetryCondition = (statusCode) => statusCode == HttpStatusCode.NotFound + }; + var policy = new DefaultRetryPolicy(config); + var handler = new RetryHandler(policy); + var mockInnerHandler = new MockHttpHandlerWithRetries(); + mockInnerHandler.AddHttpErrorsThenSuccess(2, HttpStatusCode.NotFound); + handler.InnerHandler = mockInnerHandler; + handler.LogManager = LogManager.EmptyLogger; + + var context = CreateExecutionContext(); + var result = await handler.InvokeAsync(context); + + Assert.IsNotNull(result); + Assert.IsTrue(result.IsSuccessStatusCode); + Assert.AreEqual(3, mockInnerHandler.CallCount); + Assert.AreEqual(2, context.RequestContext.HttpRetryCount); + } + + [TestMethod] + public async Task EndToEnd_With_CustomBackoff() + { + var config = new RetryConfiguration + { + RetryLimit = 2, + RetryDelay = TimeSpan.FromMilliseconds(10), + RetryDelayOptions = new RetryDelayOptions + { + CustomBackoff = (retryCount, error) => TimeSpan.FromMilliseconds(100 * (retryCount + 1)) + } + }; + var policy = new DefaultRetryPolicy(config); + var handler = new RetryHandler(policy); + var mockInnerHandler = new MockHttpHandlerWithRetries(); + mockInnerHandler.AddHttpErrorsThenSuccess(2, HttpStatusCode.TooManyRequests); + handler.InnerHandler = mockInnerHandler; + handler.LogManager = LogManager.EmptyLogger; + + var context = CreateExecutionContext(); + var startTime = DateTime.UtcNow; + await handler.InvokeAsync(context); + var elapsed = DateTime.UtcNow - startTime; + + // Custom backoff: first retry 100ms, second retry 200ms = 300ms total + Assert.IsTrue(elapsed >= TimeSpan.FromMilliseconds(300)); + Assert.AreEqual(3, mockInnerHandler.CallCount); + } + } +} + diff --git a/Contentstack.Management.Core.Unit.Tests/Runtime/Pipeline/RetryHandler/RetryHandlerTest.cs b/Contentstack.Management.Core.Unit.Tests/Runtime/Pipeline/RetryHandler/RetryHandlerTest.cs new file mode 100644 index 0000000..3f6baa1 --- /dev/null +++ b/Contentstack.Management.Core.Unit.Tests/Runtime/Pipeline/RetryHandler/RetryHandlerTest.cs @@ -0,0 +1,403 @@ +using System; +using System.Net; +using System.Net.Sockets; +using Contentstack.Management.Core; +using Contentstack.Management.Core.Exceptions; +using Contentstack.Management.Core.Runtime.Contexts; +using Contentstack.Management.Core.Runtime.Pipeline.RetryHandler; +using Contentstack.Management.Core.Unit.Tests.Mokes; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using System.Threading.Tasks; + +namespace Contentstack.Management.Core.Unit.Tests.Runtime.Pipeline.RetryHandler +{ + [TestClass] + public class RetryHandlerTest + { + private ExecutionContext CreateExecutionContext() + { + return new ExecutionContext( + new RequestContext + { + config = new ContentstackClientOptions(), + service = new MockService() + }, + new ResponseContext()); + } + + [TestMethod] + public async Task InvokeAsync_Success_NoRetry() + { + var config = new RetryConfiguration + { + RetryLimit = 3, + MaxNetworkRetries = 2 + }; + var policy = new DefaultRetryPolicy(config); + var handler = new RetryHandler(policy); + var mockInnerHandler = new MockHttpHandlerWithRetries(); + mockInnerHandler.AddSuccessResponse(); + handler.InnerHandler = mockInnerHandler; + handler.LogManager = LogManager.EmptyLogger; + + var context = CreateExecutionContext(); + var result = await handler.InvokeAsync(context); + + Assert.IsNotNull(result); + Assert.AreEqual(1, mockInnerHandler.CallCount); + Assert.AreEqual(0, context.RequestContext.NetworkRetryCount); + Assert.AreEqual(0, context.RequestContext.HttpRetryCount); + } + + [TestMethod] + public async Task InvokeAsync_NetworkError_Retries_UpTo_MaxNetworkRetries() + { + var config = new RetryConfiguration + { + RetryOnNetworkFailure = true, + RetryOnSocketFailure = true, + MaxNetworkRetries = 2, + NetworkRetryDelay = TimeSpan.FromMilliseconds(10) + }; + var policy = new DefaultRetryPolicy(config); + var handler = new RetryHandler(policy); + var mockInnerHandler = new MockHttpHandlerWithRetries(); + mockInnerHandler.AddFailuresThenSuccess(2, MockNetworkErrorGenerator.CreateSocketException(SocketError.ConnectionReset)); + handler.InnerHandler = mockInnerHandler; + handler.LogManager = LogManager.EmptyLogger; + + var context = CreateExecutionContext(); + var result = await handler.InvokeAsync(context); + + Assert.IsNotNull(result); + Assert.AreEqual(3, mockInnerHandler.CallCount); // 2 failures + 1 success + Assert.AreEqual(2, context.RequestContext.NetworkRetryCount); + } + + [TestMethod] + public async Task InvokeAsync_NetworkError_Exceeds_MaxNetworkRetries_Throws() + { + var config = new RetryConfiguration + { + RetryOnNetworkFailure = true, + RetryOnSocketFailure = true, + MaxNetworkRetries = 2, + NetworkRetryDelay = TimeSpan.FromMilliseconds(10) + }; + var policy = new DefaultRetryPolicy(config); + var handler = new RetryHandler(policy); + var mockInnerHandler = new MockHttpHandlerWithRetries(); + mockInnerHandler.AddException(MockNetworkErrorGenerator.CreateSocketException(SocketError.ConnectionReset)); + mockInnerHandler.AddException(MockNetworkErrorGenerator.CreateSocketException(SocketError.ConnectionReset)); + mockInnerHandler.AddException(MockNetworkErrorGenerator.CreateSocketException(SocketError.ConnectionReset)); + handler.InnerHandler = mockInnerHandler; + handler.LogManager = LogManager.EmptyLogger; + + var context = CreateExecutionContext(); + + try + { + await handler.InvokeAsync(context); + Assert.Fail("Should have thrown exception"); + } + catch (SocketException) + { + // Expected + } + + Assert.AreEqual(3, mockInnerHandler.CallCount); // 3 failures + Assert.AreEqual(2, context.RequestContext.NetworkRetryCount); + } + + [TestMethod] + public async Task InvokeAsync_HttpError_429_Retries_UpTo_RetryLimit() + { + var config = new RetryConfiguration + { + RetryLimit = 2, + RetryDelay = TimeSpan.FromMilliseconds(10) + }; + var policy = new DefaultRetryPolicy(config); + var handler = new RetryHandler(policy); + var mockInnerHandler = new MockHttpHandlerWithRetries(); + mockInnerHandler.AddHttpErrorsThenSuccess(2, HttpStatusCode.TooManyRequests); + handler.InnerHandler = mockInnerHandler; + handler.LogManager = LogManager.EmptyLogger; + + var context = CreateExecutionContext(); + var result = await handler.InvokeAsync(context); + + Assert.IsNotNull(result); + Assert.AreEqual(3, mockInnerHandler.CallCount); // 2 failures + 1 success + Assert.AreEqual(2, context.RequestContext.HttpRetryCount); + } + + [TestMethod] + public async Task InvokeAsync_HttpError_500_Retries_UpTo_RetryLimit() + { + var config = new RetryConfiguration + { + RetryOnHttpServerError = true, + RetryLimit = 2, + RetryDelay = TimeSpan.FromMilliseconds(10) + }; + var policy = new DefaultRetryPolicy(config); + var handler = new RetryHandler(policy); + var mockInnerHandler = new MockHttpHandlerWithRetries(); + mockInnerHandler.AddHttpErrorsThenSuccess(2, HttpStatusCode.InternalServerError); + handler.InnerHandler = mockInnerHandler; + handler.LogManager = LogManager.EmptyLogger; + + var context = CreateExecutionContext(); + var result = await handler.InvokeAsync(context); + + Assert.IsNotNull(result); + Assert.AreEqual(3, mockInnerHandler.CallCount); + Assert.AreEqual(2, context.RequestContext.HttpRetryCount); + } + + [TestMethod] + public async Task InvokeAsync_HttpError_Exceeds_RetryLimit_Throws() + { + var config = new RetryConfiguration + { + RetryLimit = 2, + RetryDelay = TimeSpan.FromMilliseconds(10) + }; + var policy = new DefaultRetryPolicy(config); + var handler = new RetryHandler(policy); + var mockInnerHandler = new MockHttpHandlerWithRetries(); + mockInnerHandler.AddResponse(HttpStatusCode.TooManyRequests); + mockInnerHandler.AddResponse(HttpStatusCode.TooManyRequests); + mockInnerHandler.AddResponse(HttpStatusCode.TooManyRequests); + handler.InnerHandler = mockInnerHandler; + handler.LogManager = LogManager.EmptyLogger; + + var context = CreateExecutionContext(); + + try + { + await handler.InvokeAsync(context); + Assert.Fail("Should have thrown exception"); + } + catch (ContentstackErrorException ex) + { + Assert.AreEqual(HttpStatusCode.TooManyRequests, ex.StatusCode); + } + + Assert.AreEqual(3, mockInnerHandler.CallCount); + Assert.AreEqual(2, context.RequestContext.HttpRetryCount); + } + + [TestMethod] + public async Task InvokeAsync_NetworkError_Tracks_NetworkRetryCount() + { + var config = new RetryConfiguration + { + RetryOnNetworkFailure = true, + RetryOnSocketFailure = true, + MaxNetworkRetries = 3, + NetworkRetryDelay = TimeSpan.FromMilliseconds(10) + }; + var policy = new DefaultRetryPolicy(config); + var handler = new RetryHandler(policy); + var mockInnerHandler = new MockHttpHandlerWithRetries(); + mockInnerHandler.AddFailuresThenSuccess(1, MockNetworkErrorGenerator.CreateSocketException(SocketError.ConnectionReset)); + handler.InnerHandler = mockInnerHandler; + handler.LogManager = LogManager.EmptyLogger; + + var context = CreateExecutionContext(); + await handler.InvokeAsync(context); + + Assert.AreEqual(1, context.RequestContext.NetworkRetryCount); + Assert.AreEqual(0, context.RequestContext.HttpRetryCount); + } + + [TestMethod] + public async Task InvokeAsync_HttpError_Tracks_HttpRetryCount() + { + var config = new RetryConfiguration + { + RetryLimit = 3, + RetryDelay = TimeSpan.FromMilliseconds(10) + }; + var policy = new DefaultRetryPolicy(config); + var handler = new RetryHandler(policy); + var mockInnerHandler = new MockHttpHandlerWithRetries(); + mockInnerHandler.AddHttpErrorsThenSuccess(1, HttpStatusCode.TooManyRequests); + handler.InnerHandler = mockInnerHandler; + handler.LogManager = LogManager.EmptyLogger; + + var context = CreateExecutionContext(); + await handler.InvokeAsync(context); + + Assert.AreEqual(0, context.RequestContext.NetworkRetryCount); + Assert.AreEqual(1, context.RequestContext.HttpRetryCount); + } + + [TestMethod] + public async Task InvokeAsync_NetworkError_Then_HttpError_Tracks_Both_Counts() + { + var config = new RetryConfiguration + { + RetryOnNetworkFailure = true, + RetryOnSocketFailure = true, + MaxNetworkRetries = 3, + RetryLimit = 3, + NetworkRetryDelay = TimeSpan.FromMilliseconds(10), + RetryDelay = TimeSpan.FromMilliseconds(10) + }; + var policy = new DefaultRetryPolicy(config); + var handler = new RetryHandler(policy); + var mockInnerHandler = new MockHttpHandlerWithRetries(); + mockInnerHandler.AddException(MockNetworkErrorGenerator.CreateSocketException(SocketError.ConnectionReset)); + mockInnerHandler.AddResponse(HttpStatusCode.TooManyRequests); + mockInnerHandler.AddSuccessResponse(); + handler.InnerHandler = mockInnerHandler; + handler.LogManager = LogManager.EmptyLogger; + + var context = CreateExecutionContext(); + await handler.InvokeAsync(context); + + Assert.AreEqual(1, context.RequestContext.NetworkRetryCount); + Assert.AreEqual(1, context.RequestContext.HttpRetryCount); + } + + [TestMethod] + public async Task InvokeAsync_Applies_NetworkRetryDelay() + { + var config = new RetryConfiguration + { + RetryOnNetworkFailure = true, + RetryOnSocketFailure = true, + MaxNetworkRetries = 1, + NetworkRetryDelay = TimeSpan.FromMilliseconds(50), + NetworkBackoffStrategy = BackoffStrategy.Fixed + }; + var policy = new DefaultRetryPolicy(config); + var handler = new RetryHandler(policy); + var mockInnerHandler = new MockHttpHandlerWithRetries(); + mockInnerHandler.AddFailuresThenSuccess(1, MockNetworkErrorGenerator.CreateSocketException(SocketError.ConnectionReset)); + handler.InnerHandler = mockInnerHandler; + handler.LogManager = LogManager.EmptyLogger; + + var context = CreateExecutionContext(); + var startTime = DateTime.UtcNow; + await handler.InvokeAsync(context); + var elapsed = DateTime.UtcNow - startTime; + + // Should have waited at least 50ms + jitter + Assert.IsTrue(elapsed >= TimeSpan.FromMilliseconds(50)); + } + + [TestMethod] + public async Task InvokeAsync_Applies_HttpRetryDelay() + { + var config = new RetryConfiguration + { + RetryLimit = 1, + RetryDelay = TimeSpan.FromMilliseconds(50), + RetryDelayOptions = new RetryDelayOptions + { + Base = TimeSpan.FromMilliseconds(50) + } + }; + var policy = new DefaultRetryPolicy(config); + var handler = new RetryHandler(policy); + var mockInnerHandler = new MockHttpHandlerWithRetries(); + mockInnerHandler.AddHttpErrorsThenSuccess(1, HttpStatusCode.TooManyRequests); + handler.InnerHandler = mockInnerHandler; + handler.LogManager = LogManager.EmptyLogger; + + var context = CreateExecutionContext(); + var startTime = DateTime.UtcNow; + await handler.InvokeAsync(context); + var elapsed = DateTime.UtcNow - startTime; + + // Should have waited at least 50ms + jitter + Assert.IsTrue(elapsed >= TimeSpan.FromMilliseconds(50)); + } + + [TestMethod] + public async Task InvokeAsync_RequestId_Is_Generated() + { + var config = new RetryConfiguration(); + var policy = new DefaultRetryPolicy(config); + var handler = new RetryHandler(policy); + var mockInnerHandler = new MockHttpHandlerWithRetries(); + mockInnerHandler.AddSuccessResponse(); + handler.InnerHandler = mockInnerHandler; + handler.LogManager = LogManager.EmptyLogger; + + var context = CreateExecutionContext(); + await handler.InvokeAsync(context); + + Assert.AreNotEqual(Guid.Empty, context.RequestContext.RequestId); + } + + [TestMethod] + public void InvokeSync_Success_NoRetry() + { + var config = new RetryConfiguration(); + var policy = new DefaultRetryPolicy(config); + var handler = new RetryHandler(policy); + var mockInnerHandler = new MockHttpHandlerWithRetries(); + mockInnerHandler.AddSuccessResponse(); + handler.InnerHandler = mockInnerHandler; + handler.LogManager = LogManager.EmptyLogger; + + var context = CreateExecutionContext(); + handler.InvokeSync(context); + + Assert.AreEqual(1, mockInnerHandler.CallCount); + } + + [TestMethod] + public void InvokeSync_NetworkError_Retries() + { + var config = new RetryConfiguration + { + RetryOnNetworkFailure = true, + RetryOnSocketFailure = true, + MaxNetworkRetries = 2, + NetworkRetryDelay = TimeSpan.FromMilliseconds(10) + }; + var policy = new DefaultRetryPolicy(config); + var handler = new RetryHandler(policy); + var mockInnerHandler = new MockHttpHandlerWithRetries(); + mockInnerHandler.AddFailuresThenSuccess(2, MockNetworkErrorGenerator.CreateSocketException(SocketError.ConnectionReset)); + handler.InnerHandler = mockInnerHandler; + handler.LogManager = LogManager.EmptyLogger; + + var context = CreateExecutionContext(); + handler.InvokeSync(context); + + Assert.AreEqual(3, mockInnerHandler.CallCount); + Assert.AreEqual(2, context.RequestContext.NetworkRetryCount); + } + + [TestMethod] + public void InvokeSync_HttpError_Retries() + { + var config = new RetryConfiguration + { + RetryLimit = 2, + RetryDelay = TimeSpan.FromMilliseconds(10) + }; + var policy = new DefaultRetryPolicy(config); + var handler = new RetryHandler(policy); + var mockInnerHandler = new MockHttpHandlerWithRetries(); + mockInnerHandler.AddHttpErrorsThenSuccess(2, HttpStatusCode.TooManyRequests); + handler.InnerHandler = mockInnerHandler; + handler.LogManager = LogManager.EmptyLogger; + + var context = CreateExecutionContext(); + handler.InvokeSync(context); + + Assert.AreEqual(3, mockInnerHandler.CallCount); + Assert.AreEqual(2, context.RequestContext.HttpRetryCount); + } + } +} + diff --git a/Contentstack.Management.Core/ContentstackClient.cs b/Contentstack.Management.Core/ContentstackClient.cs index 478d9ff..a8929b4 100644 --- a/Contentstack.Management.Core/ContentstackClient.cs +++ b/Contentstack.Management.Core/ContentstackClient.cs @@ -207,7 +207,19 @@ protected void BuildPipeline() { HttpHandler httpClientHandler = new HttpHandler(_httpClient); - RetryPolicy retryPolicy = contentstackOptions.RetryPolicy ?? new DefaultRetryPolicy(contentstackOptions.RetryLimit, contentstackOptions.RetryDelay); + RetryPolicy retryPolicy; + if (contentstackOptions.RetryPolicy != null) + { + // Use custom retry policy if provided + retryPolicy = contentstackOptions.RetryPolicy; + } + else + { + // Create RetryConfiguration from options and use it with DefaultRetryPolicy + var retryConfiguration = RetryConfiguration.FromOptions(contentstackOptions); + retryPolicy = new DefaultRetryPolicy(retryConfiguration); + } + ContentstackPipeline = new ContentstackRuntimePipeline(new List() { httpClientHandler, diff --git a/Contentstack.Management.Core/ContentstackClientOptions.cs b/Contentstack.Management.Core/ContentstackClientOptions.cs index af46f30..f5946ce 100644 --- a/Contentstack.Management.Core/ContentstackClientOptions.cs +++ b/Contentstack.Management.Core/ContentstackClientOptions.cs @@ -88,6 +88,59 @@ public class ContentstackClientOptions /// public RetryPolicy RetryPolicy { get; set; } + /// + /// When set to true, the client will retry on network failures. + /// The default value is true. + /// + public bool RetryOnNetworkFailure { get; set; } = true; + + /// + /// When set to true, the client will retry on DNS failures. + /// The default value is true. + /// + public bool RetryOnDnsFailure { get; set; } = true; + + /// + /// When set to true, the client will retry on socket failures. + /// The default value is true. + /// + public bool RetryOnSocketFailure { get; set; } = true; + + /// + /// When set to true, the client will retry on HTTP server errors (5xx). + /// The default value is true. + /// + public bool RetryOnHttpServerError { get; set; } = true; + + /// + /// Maximum number of network retry attempts. + /// The default value is 3. + /// + public int MaxNetworkRetries { get; set; } = 3; + + /// + /// Base delay for network retries. + /// The default value is 100ms. + /// + public TimeSpan NetworkRetryDelay { get; set; } = TimeSpan.FromMilliseconds(100); + + /// + /// Backoff strategy for network retries. + /// The default value is Exponential. + /// + public BackoffStrategy NetworkBackoffStrategy { get; set; } = BackoffStrategy.Exponential; + + /// + /// Custom function to determine if a status code should be retried. + /// If null, default retry condition is used (429, 500, 502, 503, 504). + /// + public Func? RetryCondition { get; set; } + + /// + /// Options for retry delay calculation. + /// + public RetryDelayOptions RetryDelayOptions { get; set; } + /// /// Host for the Proxy. /// diff --git a/Contentstack.Management.Core/Exceptions/ContentstackErrorException.cs b/Contentstack.Management.Core/Exceptions/ContentstackErrorException.cs index 8fcda6e..15e7b11 100644 --- a/Contentstack.Management.Core/Exceptions/ContentstackErrorException.cs +++ b/Contentstack.Management.Core/Exceptions/ContentstackErrorException.cs @@ -66,6 +66,21 @@ public string ErrorMessage /// [JsonProperty("errors")] public Dictionary Errors { get; set; } + + /// + /// Number of retry attempts made before this exception was thrown. + /// + public int RetryAttempts { get; set; } + + /// + /// The original exception that caused this error, if this is a network error wrapped in an HTTP exception. + /// + public Exception OriginalError { get; set; } + + /// + /// Indicates whether this error originated from a network failure. + /// + public bool IsNetworkError { get; set; } #endregion public static ContentstackErrorException CreateException(HttpResponseMessage response) { diff --git a/Contentstack.Management.Core/Runtime/Contexts/IRequestContext.cs b/Contentstack.Management.Core/Runtime/Contexts/IRequestContext.cs index 5cf6e80..eb2827e 100644 --- a/Contentstack.Management.Core/Runtime/Contexts/IRequestContext.cs +++ b/Contentstack.Management.Core/Runtime/Contexts/IRequestContext.cs @@ -9,6 +9,21 @@ public interface IRequestContext IContentstackService service { get; set; } ContentstackClientOptions config { get; set; } int Retries { get; set; } + + /// + /// Number of network retry attempts made. + /// + int NetworkRetryCount { get; set; } + + /// + /// Number of HTTP retry attempts made. + /// + int HttpRetryCount { get; set; } + + /// + /// Unique identifier for this request, used for correlation in logs. + /// + Guid RequestId { get; set; } } } diff --git a/Contentstack.Management.Core/Runtime/Contexts/RequestContext.cs b/Contentstack.Management.Core/Runtime/Contexts/RequestContext.cs index 214927a..9ce0b1a 100644 --- a/Contentstack.Management.Core/Runtime/Contexts/RequestContext.cs +++ b/Contentstack.Management.Core/Runtime/Contexts/RequestContext.cs @@ -10,5 +10,20 @@ internal class RequestContext : IRequestContext public ContentstackClientOptions config { get; set; } public int Retries { get; set; } + + /// + /// Number of network retry attempts made. + /// + public int NetworkRetryCount { get; set; } + + /// + /// Number of HTTP retry attempts made. + /// + public int HttpRetryCount { get; set; } + + /// + /// Unique identifier for this request, used for correlation in logs. + /// + public Guid RequestId { get; set; } = Guid.NewGuid(); } } diff --git a/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/BackoffStrategy.cs b/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/BackoffStrategy.cs new file mode 100644 index 0000000..788d1a0 --- /dev/null +++ b/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/BackoffStrategy.cs @@ -0,0 +1,19 @@ +namespace Contentstack.Management.Core.Runtime.Pipeline.RetryHandler +{ + /// + /// Defines the backoff strategy for retry delays. + /// + public enum BackoffStrategy + { + /// + /// Fixed delay with jitter. + /// + Fixed, + + /// + /// Exponential backoff with jitter (delay = baseDelay * 2^(attempt-1) + jitter). + /// + Exponential + } +} + diff --git a/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/DefaultRetryPolicy.cs b/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/DefaultRetryPolicy.cs index 050e2c8..fc436b2 100644 --- a/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/DefaultRetryPolicy.cs +++ b/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/DefaultRetryPolicy.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Net; +using System.Net.Http; +using Contentstack.Management.Core.Exceptions; using Contentstack.Management.Core.Runtime.Contexts; namespace Contentstack.Management.Core.Runtime.Pipeline.RetryHandler @@ -8,6 +10,9 @@ namespace Contentstack.Management.Core.Runtime.Pipeline.RetryHandler public partial class DefaultRetryPolicy : RetryPolicy { protected TimeSpan retryDelay { get; set; } + protected RetryConfiguration retryConfiguration; + protected NetworkErrorDetector networkErrorDetector; + protected RetryDelayCalculator delayCalculator; protected ICollection statusCodesToRetryOn = new HashSet { @@ -24,38 +29,183 @@ internal DefaultRetryPolicy(int retryLimit, TimeSpan delay) { RetryLimit = retryLimit; retryDelay = delay; + networkErrorDetector = new NetworkErrorDetector(); + delayCalculator = new RetryDelayCalculator(); + } + + internal DefaultRetryPolicy(RetryConfiguration config) + { + retryConfiguration = config ?? throw new ArgumentNullException(nameof(config)); + RetryLimit = config.RetryLimit; + retryDelay = config.RetryDelay; + networkErrorDetector = new NetworkErrorDetector(); + delayCalculator = new RetryDelayCalculator(); } protected override bool CanRetry(IExecutionContext executionContext) { - return true; + if (retryConfiguration != null) + { + return retryConfiguration.RetryOnError; + } + return RetryOnError; } protected override bool RetryForException(IExecutionContext executionContext, Exception exception) { - //if (exception is Exceptions.ContentstackErrorException) - //{ - // var contentstackExecption = exception as Exceptions.ContentstackErrorException; + if (retryConfiguration == null) + { + // Fallback to old behavior if no configuration provided + return false; + } - // if (statusCodesToRetryOn.Contains(contentstackExecption.StatusCode)) - // { - // return true; - // } - //} - - return false; + var requestContext = executionContext.RequestContext; + + // Check for network errors + var networkErrorInfo = networkErrorDetector.IsTransientNetworkError(exception); + if (networkErrorInfo != null) + { + if (networkErrorDetector.ShouldRetryNetworkError(networkErrorInfo, retryConfiguration)) + { + // Check if network retry limit exceeded + if (requestContext.NetworkRetryCount >= retryConfiguration.MaxNetworkRetries) + { + return false; + } + return true; + } + } + + // Check for HTTP errors (ContentstackErrorException) + if (exception is ContentstackErrorException contentstackException) + { + // Check if it's a server error (5xx) that should be retried + if (contentstackException.StatusCode >= HttpStatusCode.InternalServerError && + contentstackException.StatusCode <= HttpStatusCode.GatewayTimeout) + { + if (retryConfiguration.RetryOnHttpServerError) + { + // Check if HTTP retry limit exceeded + if (requestContext.HttpRetryCount >= retryConfiguration.RetryLimit) + { + return false; + } + return true; + } + } + + // Check custom retry condition + if (delayCalculator.ShouldRetryHttpStatusCode(contentstackException.StatusCode, retryConfiguration)) + { + // Check if HTTP retry limit exceeded + if (requestContext.HttpRetryCount >= retryConfiguration.RetryLimit) + { + return false; + } + return true; + } + } + return false; } protected override bool RetryLimitExceeded(IExecutionContext executionContext) { - return executionContext.RequestContext.Retries >= this.RetryLimit; + var requestContext = executionContext.RequestContext; + + if (retryConfiguration != null) + { + // Check both network and HTTP retry limits + return requestContext.NetworkRetryCount >= retryConfiguration.MaxNetworkRetries && + requestContext.HttpRetryCount >= retryConfiguration.RetryLimit; + } + // Fallback to old behavior + return requestContext.Retries >= this.RetryLimit; } internal override void WaitBeforeRetry(IExecutionContext executionContext) { - System.Threading.Tasks.Task.Delay(retryDelay.Milliseconds).Wait(); + if (retryConfiguration == null) + { + // Fallback to old behavior + System.Threading.Tasks.Task.Delay(retryDelay.Milliseconds).Wait(); + return; + } + + var requestContext = executionContext.RequestContext; + TimeSpan delay; + + // Determine delay based on error type + // We need to check the last exception, but we don't have it here + // So we'll use a heuristic: if network retries > 0, use network delay + if (requestContext.NetworkRetryCount > 0) + { + delay = delayCalculator.CalculateNetworkRetryDelay( + requestContext.NetworkRetryCount, + retryConfiguration); + } + else + { + // HTTP retry - we'll use the last exception if available + // For now, use base delay with exponential backoff + delay = delayCalculator.CalculateHttpRetryDelay( + requestContext.HttpRetryCount, + retryConfiguration, + null); + } + + System.Threading.Tasks.Task.Delay(delay).Wait(); + } + + /// + /// Determines if an HTTP status code should be retried. + /// + public bool ShouldRetryHttpStatusCode(HttpStatusCode statusCode, IRequestContext requestContext) + { + if (retryConfiguration == null) + { + return statusCodesToRetryOn.Contains(statusCode); + } + + if (requestContext.HttpRetryCount >= retryConfiguration.RetryLimit) + { + return false; + } + + return delayCalculator.ShouldRetryHttpStatusCode(statusCode, retryConfiguration); + } + + /// + /// Gets the retry delay for an HTTP error. + /// + public TimeSpan GetHttpRetryDelay(IRequestContext requestContext, Exception exception, HttpResponseHeaders? responseHeaders = null) + { + if (retryConfiguration == null) + { + return retryDelay; + } + + return delayCalculator.CalculateHttpRetryDelay( + requestContext.HttpRetryCount, + retryConfiguration, + exception, + responseHeaders); + } + + /// + /// Gets the retry delay for a network error. + /// + public TimeSpan GetNetworkRetryDelay(IRequestContext requestContext) + { + if (retryConfiguration == null) + { + return retryDelay; + } + + return delayCalculator.CalculateNetworkRetryDelay( + requestContext.NetworkRetryCount, + retryConfiguration); } } } diff --git a/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/NetworkErrorDetector.cs b/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/NetworkErrorDetector.cs new file mode 100644 index 0000000..16f5bf5 --- /dev/null +++ b/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/NetworkErrorDetector.cs @@ -0,0 +1,141 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; +using Contentstack.Management.Core.Exceptions; + +namespace Contentstack.Management.Core.Runtime.Pipeline.RetryHandler +{ + /// + /// Service to detect and classify transient network errors. + /// + public class NetworkErrorDetector + { + /// + /// Determines if an exception represents a transient network error. + /// + /// The exception to analyze. + /// NetworkErrorInfo if it's a transient network error, null otherwise. + public NetworkErrorInfo? IsTransientNetworkError(Exception error) + { + if (error == null) + return null; + + // Check for SocketException + if (error is SocketException socketException) + { + return DetectSocketError(socketException); + } + + // Check for HttpRequestException with inner SocketException + if (error is System.Net.Http.HttpRequestException httpRequestException) + { + if (httpRequestException.InnerException is SocketException innerSocketException) + { + return DetectSocketError(innerSocketException); + } + } + + // Check for TaskCanceledException (timeout) + if (error is TaskCanceledException taskCanceledException) + { + // Only treat as timeout if it's not a user cancellation + // TaskCanceledException can occur due to timeout or cancellation + // We check if the cancellation token was actually cancelled by the user + if (taskCanceledException.CancellationToken.IsCancellationRequested == false) + { + return new NetworkErrorInfo(NetworkErrorType.Timeout, true, error); + } + } + + // Check for TimeoutException + if (error is TimeoutException) + { + return new NetworkErrorInfo(NetworkErrorType.Timeout, true, error); + } + + // Check for ContentstackErrorException with 5xx status codes + if (error is ContentstackErrorException contentstackError) + { + if (contentstackError.StatusCode >= HttpStatusCode.InternalServerError && + contentstackError.StatusCode <= HttpStatusCode.GatewayTimeout) + { + return new NetworkErrorInfo(NetworkErrorType.HttpServerError, true, error); + } + } + + return null; + } + + /// + /// Detects the type of socket error from a SocketException. + /// + private NetworkErrorInfo DetectSocketError(SocketException socketException) + { + bool isTransient = false; + NetworkErrorType errorType = NetworkErrorType.SocketError; + + switch (socketException.SocketErrorCode) + { + // DNS-related errors + case SocketError.HostNotFound: + case SocketError.TryAgain: + errorType = NetworkErrorType.DnsFailure; + isTransient = true; + break; + + // Transient connection errors + case SocketError.ConnectionReset: + case SocketError.TimedOut: + case SocketError.ConnectionRefused: + case SocketError.NetworkUnreachable: + case SocketError.HostUnreachable: + case SocketError.NoBufferSpaceAvailable: + errorType = NetworkErrorType.SocketError; + isTransient = true; + break; + + // Other socket errors (may or may not be transient) + default: + errorType = NetworkErrorType.SocketError; + // Most socket errors are transient, but some are not + // We'll be conservative and retry on most socket errors + isTransient = true; + break; + } + + return new NetworkErrorInfo(errorType, isTransient, socketException); + } + + /// + /// Determines if a network error should be retried based on configuration. + /// + public bool ShouldRetryNetworkError(NetworkErrorInfo? errorInfo, RetryConfiguration config) + { + if (errorInfo == null || !errorInfo.IsTransient) + return false; + + if (!config.RetryOnNetworkFailure) + return false; + + switch (errorInfo.ErrorType) + { + case NetworkErrorType.DnsFailure: + return config.RetryOnDnsFailure; + + case NetworkErrorType.SocketError: + return config.RetryOnSocketFailure; + + case NetworkErrorType.Timeout: + return config.RetryOnNetworkFailure; + + case NetworkErrorType.HttpServerError: + return config.RetryOnHttpServerError; + + default: + return config.RetryOnNetworkFailure; + } + } + } +} + diff --git a/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/NetworkErrorInfo.cs b/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/NetworkErrorInfo.cs new file mode 100644 index 0000000..f5fa2e8 --- /dev/null +++ b/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/NetworkErrorInfo.cs @@ -0,0 +1,65 @@ +namespace Contentstack.Management.Core.Runtime.Pipeline.RetryHandler +{ + /// + /// Information about a detected network error. + /// + public class NetworkErrorInfo + { + /// + /// The type of network error detected. + /// + public NetworkErrorType ErrorType { get; set; } + + /// + /// Indicates if this is a transient error that should be retried. + /// + public bool IsTransient { get; set; } + + /// + /// The original exception that caused this network error. + /// + public System.Exception OriginalException { get; set; } + + /// + /// Creates a new NetworkErrorInfo instance. + /// + public NetworkErrorInfo(NetworkErrorType errorType, bool isTransient, System.Exception originalException) + { + ErrorType = errorType; + IsTransient = isTransient; + OriginalException = originalException; + } + } + + /// + /// Types of network errors that can occur. + /// + public enum NetworkErrorType + { + /// + /// DNS resolution failure. + /// + DnsFailure, + + /// + /// Socket connection error (connection reset, refused, etc.). + /// + SocketError, + + /// + /// Request timeout. + /// + Timeout, + + /// + /// HTTP server error (5xx). + /// + HttpServerError, + + /// + /// Unknown or unclassified error. + /// + Unknown + } +} + diff --git a/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/RetryConfiguration.cs b/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/RetryConfiguration.cs new file mode 100644 index 0000000..3248ad1 --- /dev/null +++ b/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/RetryConfiguration.cs @@ -0,0 +1,128 @@ +using System; +using System.Net; +using Contentstack.Management.Core; + +namespace Contentstack.Management.Core.Runtime.Pipeline.RetryHandler +{ + /// + /// Configuration for retry behavior, supporting both network and HTTP error retries. + /// + public class RetryConfiguration + { + /// + /// When set to true, the client will retry requests. + /// When set to false, the client will not retry request. + /// The default value is true. + /// + public bool RetryOnError { get; set; } = true; + + /// + /// Returns the flag indicating how many retry HTTP requests an SDK should + /// make for a single SDK operation invocation before giving up. + /// The default value is 5. + /// + public int RetryLimit { get; set; } = 5; + + /// + /// Returns the flag indicating delay in retrying HTTP requests. + /// The default value is 300ms. + /// + public TimeSpan RetryDelay { get; set; } = TimeSpan.FromMilliseconds(300); + + /// + /// When set to true, the client will retry on network failures. + /// The default value is true. + /// + public bool RetryOnNetworkFailure { get; set; } = true; + + /// + /// When set to true, the client will retry on DNS failures. + /// The default value is true. + /// + public bool RetryOnDnsFailure { get; set; } = true; + + /// + /// When set to true, the client will retry on socket failures. + /// The default value is true. + /// + public bool RetryOnSocketFailure { get; set; } = true; + + /// + /// When set to true, the client will retry on HTTP server errors (5xx). + /// The default value is true. + /// + public bool RetryOnHttpServerError { get; set; } = true; + + /// + /// Maximum number of network retry attempts. + /// The default value is 3. + /// + public int MaxNetworkRetries { get; set; } = 3; + + /// + /// Base delay for network retries. + /// The default value is 100ms. + /// + public TimeSpan NetworkRetryDelay { get; set; } = TimeSpan.FromMilliseconds(100); + + /// + /// Backoff strategy for network retries. + /// The default value is Exponential. + /// + public BackoffStrategy NetworkBackoffStrategy { get; set; } = BackoffStrategy.Exponential; + + /// + /// Custom function to determine if a status code should be retried. + /// If null, default retry condition is used (429, 500, 502, 503, 504). + /// + public Func? RetryCondition { get; set; } + + /// + /// Options for retry delay calculation. + /// + public RetryDelayOptions RetryDelayOptions { get; set; } = new RetryDelayOptions(); + + /// + /// Creates a RetryConfiguration from ContentstackClientOptions. + /// + public static RetryConfiguration FromOptions(ContentstackClientOptions options) + { + return new RetryConfiguration + { + RetryOnError = options.RetryOnError, + RetryLimit = options.RetryLimit, + RetryDelay = options.RetryDelay, + RetryOnNetworkFailure = options.RetryOnNetworkFailure, + RetryOnDnsFailure = options.RetryOnDnsFailure, + RetryOnSocketFailure = options.RetryOnSocketFailure, + RetryOnHttpServerError = options.RetryOnHttpServerError, + MaxNetworkRetries = options.MaxNetworkRetries, + NetworkRetryDelay = options.NetworkRetryDelay, + NetworkBackoffStrategy = options.NetworkBackoffStrategy, + RetryCondition = options.RetryCondition, + RetryDelayOptions = options.RetryDelayOptions ?? new RetryDelayOptions + { + Base = options.RetryDelay + } + }; + } + } + + /// + /// Options for retry delay calculation. + /// + public class RetryDelayOptions + { + /// + /// Base delay for retries. + /// + public TimeSpan Base { get; set; } = TimeSpan.FromMilliseconds(300); + + /// + /// Custom backoff function. Parameters: retryCount, exception. + /// Return TimeSpan.Zero or negative TimeSpan to disable retry for that attempt. + /// + public Func? CustomBackoff { get; set; } + } +} + diff --git a/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/RetryDelayCalculator.cs b/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/RetryDelayCalculator.cs new file mode 100644 index 0000000..28e4c74 --- /dev/null +++ b/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/RetryDelayCalculator.cs @@ -0,0 +1,122 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using Contentstack.Management.Core.Exceptions; + +namespace Contentstack.Management.Core.Runtime.Pipeline.RetryHandler +{ + /// + /// Utility for calculating retry delays with various backoff strategies. + /// + public class RetryDelayCalculator + { + private static readonly Random _random = new Random(); + private const int MaxJitterMs = 100; + + /// + /// Calculates the delay for a network retry attempt. + /// + /// The current retry attempt number (1-based). + /// The retry configuration. + /// The delay to wait before retrying. + public TimeSpan CalculateNetworkRetryDelay(int attempt, RetryConfiguration config) + { + TimeSpan baseDelay = config.NetworkRetryDelay; + TimeSpan calculatedDelay; + + switch (config.NetworkBackoffStrategy) + { + case BackoffStrategy.Exponential: + // Exponential: baseDelay * 2^(attempt-1) + double exponentialDelay = baseDelay.TotalMilliseconds * Math.Pow(2, attempt - 1); + calculatedDelay = TimeSpan.FromMilliseconds(exponentialDelay); + break; + + case BackoffStrategy.Fixed: + default: + calculatedDelay = baseDelay; + break; + } + + // Add jitter (random 0-100ms) + int jitterMs = _random.Next(0, MaxJitterMs); + return calculatedDelay.Add(TimeSpan.FromMilliseconds(jitterMs)); + } + + /// + /// Calculates the delay for an HTTP retry attempt. + /// + /// The current retry count (0-based). + /// The retry configuration. + /// The exception that triggered the retry, if any. + /// The HTTP response headers, if available. + /// The delay to wait before retrying. Returns TimeSpan.Zero or negative to disable retry. + public TimeSpan CalculateHttpRetryDelay(int retryCount, RetryConfiguration config, Exception? error, HttpResponseHeaders? responseHeaders = null) + { + // Check for Retry-After header (for 429 Too Many Requests) + if (responseHeaders != null && responseHeaders.RetryAfter != null) + { + var retryAfter = responseHeaders.RetryAfter; + if (retryAfter.Delta.HasValue) + { + return retryAfter.Delta.Value; + } + if (retryAfter.Date.HasValue) + { + var delay = retryAfter.Date.Value - DateTimeOffset.UtcNow; + if (delay > TimeSpan.Zero) + { + return delay; + } + } + } + + // Use custom backoff function if provided + if (config.RetryDelayOptions.CustomBackoff != null) + { + var customDelay = config.RetryDelayOptions.CustomBackoff(retryCount, error); + // Negative or zero delay means don't retry + if (customDelay <= TimeSpan.Zero) + { + return customDelay; + } + return customDelay; + } + + // Default: use base delay with exponential backoff + TimeSpan baseDelay = config.RetryDelayOptions.Base; + if (baseDelay == TimeSpan.Zero) + { + baseDelay = config.RetryDelay; + } + + // Exponential backoff: baseDelay * 2^retryCount + double exponentialDelay = baseDelay.TotalMilliseconds * Math.Pow(2, retryCount); + TimeSpan calculatedDelay = TimeSpan.FromMilliseconds(exponentialDelay); + + // Add jitter (random 0-100ms) + int jitterMs = _random.Next(0, MaxJitterMs); + return calculatedDelay.Add(TimeSpan.FromMilliseconds(jitterMs)); + } + + /// + /// Determines if an HTTP status code should be retried based on configuration. + /// + public bool ShouldRetryHttpStatusCode(HttpStatusCode statusCode, RetryConfiguration config) + { + if (config.RetryCondition != null) + { + return config.RetryCondition(statusCode); + } + + // Default retry condition: 429, 500, 502, 503, 504 + return statusCode == HttpStatusCode.TooManyRequests || + statusCode == HttpStatusCode.InternalServerError || + statusCode == HttpStatusCode.BadGateway || + statusCode == HttpStatusCode.ServiceUnavailable || + statusCode == HttpStatusCode.GatewayTimeout; + } + } +} + diff --git a/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/RetryHandler.cs b/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/RetryHandler.cs index 4ce9fcd..3f44e78 100644 --- a/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/RetryHandler.cs +++ b/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/RetryHandler.cs @@ -1,4 +1,5 @@ using System; +using System.Net.Http; using System.Threading.Tasks; using Contentstack.Management.Core.Exceptions; using Contentstack.Management.Core.Runtime.Contexts; @@ -8,26 +9,56 @@ namespace Contentstack.Management.Core.Runtime.Pipeline.RetryHandler public class RetryHandler : PipelineHandler { public RetryPolicy RetryPolicy { get; private set; } + private readonly NetworkErrorDetector networkErrorDetector; public RetryHandler(RetryPolicy retryPolicy) { this.RetryPolicy = retryPolicy; + this.networkErrorDetector = new NetworkErrorDetector(); } + public override async Task InvokeAsync(IExecutionContext executionContext, bool addAcceptMediaHeader = false, string apiVersion = null) { var requestContext = executionContext.RequestContext; var responseContext = executionContext.ResponseContext; bool shouldRetry = false; + Exception lastException = null; + do { try { - var response = await base.InvokeAsync(executionContext, addAcceptMediaHeader, apiVersion); + var response = await base.InvokeAsync(executionContext, addAcceptMediaHeader, apiVersion); + + // Check if response is an HTTP error that should be retried + if (response is ContentstackResponse contentstackResponse && + !contentstackResponse.IsSuccessStatusCode) + { + var defaultRetryPolicy = RetryPolicy as DefaultRetryPolicy; + if (defaultRetryPolicy != null && + defaultRetryPolicy.ShouldRetryHttpStatusCode(contentstackResponse.StatusCode, requestContext)) + { + requestContext.HttpRetryCount++; + shouldRetry = true; + LogForHttpRetry(requestContext, contentstackResponse.StatusCode); + + // Get delay and wait + var delay = defaultRetryPolicy.GetHttpRetryDelay( + requestContext, + null, + contentstackResponse.ResponseBody?.Headers); + await Task.Delay(delay); + continue; + } + } + return response; } catch (Exception exception) { + lastException = exception; shouldRetry = this.RetryPolicy.Retry(executionContext, exception); + if (!shouldRetry) { LogForError(requestContext, exception); @@ -35,14 +66,30 @@ public override async Task InvokeAsync(IExecutionContext executionContext, } else { - requestContext.Retries++; - LogForRetry(requestContext, exception); + // Classify error and increment appropriate counter + var networkErrorInfo = networkErrorDetector.IsTransientNetworkError(exception); + if (networkErrorInfo != null) + { + requestContext.NetworkRetryCount++; + LogForNetworkRetry(requestContext, exception, networkErrorInfo); + } + else if (exception is ContentstackErrorException) + { + requestContext.HttpRetryCount++; + LogForHttpRetry(requestContext, ((ContentstackErrorException)exception).StatusCode); + } + else + { + requestContext.Retries++; + LogForRetry(requestContext, exception); + } } } this.RetryPolicy.WaitBeforeRetry(executionContext); } while (shouldRetry == true); + throw new ContentstackException("No response was return nor exception was thrown"); } @@ -50,16 +97,44 @@ public override void InvokeSync(IExecutionContext executionContext, bool addAcce { var requestContext = executionContext.RequestContext; bool shouldRetry = false; + Exception lastException = null; + do { try { base.InvokeSync(executionContext, addAcceptMediaHeader, apiVersion); + + // Check if response is an HTTP error that should be retried + var response = executionContext.ResponseContext.httpResponse; + if (response is ContentstackResponse contentstackResponse && + !contentstackResponse.IsSuccessStatusCode) + { + var defaultRetryPolicy = RetryPolicy as DefaultRetryPolicy; + if (defaultRetryPolicy != null && + defaultRetryPolicy.ShouldRetryHttpStatusCode(contentstackResponse.StatusCode, requestContext)) + { + requestContext.HttpRetryCount++; + shouldRetry = true; + LogForHttpRetry(requestContext, contentstackResponse.StatusCode); + + // Get delay and wait + var delay = defaultRetryPolicy.GetHttpRetryDelay( + requestContext, + null, + contentstackResponse.ResponseBody?.Headers); + System.Threading.Tasks.Task.Delay(delay).Wait(); + continue; + } + } + return; } catch (Exception exception) { + lastException = exception; shouldRetry = this.RetryPolicy.Retry(executionContext, exception); + if (!shouldRetry) { LogForError(requestContext, exception); @@ -67,10 +142,24 @@ public override void InvokeSync(IExecutionContext executionContext, bool addAcce } else { - requestContext.Retries++; - LogForRetry(requestContext, exception); + // Classify error and increment appropriate counter + var networkErrorInfo = networkErrorDetector.IsTransientNetworkError(exception); + if (networkErrorInfo != null) + { + requestContext.NetworkRetryCount++; + LogForNetworkRetry(requestContext, exception, networkErrorInfo); + } + else if (exception is ContentstackErrorException) + { + requestContext.HttpRetryCount++; + LogForHttpRetry(requestContext, ((ContentstackErrorException)exception).StatusCode); + } + else + { + requestContext.Retries++; + LogForRetry(requestContext, exception); + } } - } this.RetryPolicy.WaitBeforeRetry(executionContext); @@ -80,20 +169,52 @@ public override void InvokeSync(IExecutionContext executionContext, bool addAcce private void LogForError(IRequestContext requestContext, Exception exception) { - LogManager.InfoFormat("{0} making request {1}. Attempt {2} of {3}.", - exception.GetType().Name, - requestContext.service.ResourcePath, - requestContext.Retries + 1, - RetryPolicy.RetryLimit); + var totalAttempts = requestContext.NetworkRetryCount + requestContext.HttpRetryCount + requestContext.Retries + 1; + LogManager.InfoFormat( + "[RequestId: {0}] {1} making request {2}. Final attempt {3} failed. Network retries: {4}, HTTP retries: {5}.", + requestContext.RequestId, + exception.GetType().Name, + requestContext.service.ResourcePath, + totalAttempts, + requestContext.NetworkRetryCount, + requestContext.HttpRetryCount); } private void LogForRetry(IRequestContext requestContext, Exception exception) { - LogManager.Error(exception, "{0} making request {1}. Attempt {2} of {3}.", - exception.GetType().Name, - requestContext.service.ResourcePath, - requestContext.Retries, - RetryPolicy.RetryLimit); + var totalAttempts = requestContext.NetworkRetryCount + requestContext.HttpRetryCount + requestContext.Retries; + LogManager.InfoFormat( + "[RequestId: {0}] {1} making request {2}. Retrying (attempt {3}). Network retries: {4}, HTTP retries: {5}.", + requestContext.RequestId, + exception.GetType().Name, + requestContext.service.ResourcePath, + totalAttempts, + requestContext.NetworkRetryCount, + requestContext.HttpRetryCount); + } + + private void LogForNetworkRetry(IRequestContext requestContext, Exception exception, NetworkErrorInfo errorInfo) + { + var totalAttempts = requestContext.NetworkRetryCount + requestContext.HttpRetryCount + requestContext.Retries; + LogManager.InfoFormat( + "[RequestId: {0}] Network error ({1}) making request {2}. Retrying (attempt {3}, network retry {4}).", + requestContext.RequestId, + errorInfo.ErrorType, + requestContext.service.ResourcePath, + totalAttempts, + requestContext.NetworkRetryCount); + } + + private void LogForHttpRetry(IRequestContext requestContext, System.Net.HttpStatusCode statusCode) + { + var totalAttempts = requestContext.NetworkRetryCount + requestContext.HttpRetryCount + requestContext.Retries; + LogManager.InfoFormat( + "[RequestId: {0}] HTTP error ({1}) making request {2}. Retrying (attempt {3}, HTTP retry {4}).", + requestContext.RequestId, + statusCode, + requestContext.service.ResourcePath, + totalAttempts, + requestContext.HttpRetryCount); } } } From 669bf50ff1e956422462ff54eb92215e2eb64123 Mon Sep 17 00:00:00 2001 From: raj pandey Date: Fri, 2 Jan 2026 14:35:19 +0530 Subject: [PATCH 02/10] chore: license update --- Contentstack.Management.ASPNETCore/LICENSE.txt | 2 +- .../contentstack.management.aspnetcore.csproj | 2 +- Contentstack.Management.Core/LICENSE.txt | 2 +- .../contentstack.management.core.csproj | 2 +- LICENSE | 2 +- Scripts/run-test-case.sh | 2 +- Scripts/run-unit-test-case.sh | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Contentstack.Management.ASPNETCore/LICENSE.txt b/Contentstack.Management.ASPNETCore/LICENSE.txt index 501f936..4382a0d 100644 --- a/Contentstack.Management.ASPNETCore/LICENSE.txt +++ b/Contentstack.Management.ASPNETCore/LICENSE.txt @@ -1,6 +1,6 @@ MIT License -Copyright © 2012-2025 Contentstack. All Rights Reserved +Copyright © 2012-2026 Contentstack. All Rights Reserved Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Contentstack.Management.ASPNETCore/contentstack.management.aspnetcore.csproj b/Contentstack.Management.ASPNETCore/contentstack.management.aspnetcore.csproj index 679d686..039b9ac 100644 --- a/Contentstack.Management.ASPNETCore/contentstack.management.aspnetcore.csproj +++ b/Contentstack.Management.ASPNETCore/contentstack.management.aspnetcore.csproj @@ -5,7 +5,7 @@ contentstack.management.aspnetcore $(Version) Contentstack - Copyright © 2012-2025 Contentstack. All Rights Reserved + Copyright © 2012-2026 Contentstack. All Rights Reserved Contentstack https://github.com/contentstack/contentstack-management-dotnet Initial Release diff --git a/Contentstack.Management.Core/LICENSE.txt b/Contentstack.Management.Core/LICENSE.txt index 501f936..4382a0d 100644 --- a/Contentstack.Management.Core/LICENSE.txt +++ b/Contentstack.Management.Core/LICENSE.txt @@ -1,6 +1,6 @@ MIT License -Copyright © 2012-2025 Contentstack. All Rights Reserved +Copyright © 2012-2026 Contentstack. All Rights Reserved Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Contentstack.Management.Core/contentstack.management.core.csproj b/Contentstack.Management.Core/contentstack.management.core.csproj index 0f14e8c..bdba4c5 100644 --- a/Contentstack.Management.Core/contentstack.management.core.csproj +++ b/Contentstack.Management.Core/contentstack.management.core.csproj @@ -4,7 +4,7 @@ netstandard2.0;net471;net472; Contentstack Management Contentstack - Copyright © 2012-2025 Contentstack. All Rights Reserved + Copyright © 2012-2026 Contentstack. All Rights Reserved .NET SDK for the Contentstack Content Management API. Contentstack contentstack.management.csharp diff --git a/LICENSE b/LICENSE index 3851325..4ea4612 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2012-2025 Contentstack +Copyright (c) 2012-2026 Contentstack Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Scripts/run-test-case.sh b/Scripts/run-test-case.sh index 7550df9..a1d47c4 100644 --- a/Scripts/run-test-case.sh +++ b/Scripts/run-test-case.sh @@ -4,7 +4,7 @@ # Contentstack # # Created by Uttam Ukkoji on 12/04/21. -# Copyright © 2025 Contentstack. All rights reserved. +# Copyright © 2026 Contentstack. All rights reserved. echo "Removing files" diff --git a/Scripts/run-unit-test-case.sh b/Scripts/run-unit-test-case.sh index ff14bdc..ba41e6c 100644 --- a/Scripts/run-unit-test-case.sh +++ b/Scripts/run-unit-test-case.sh @@ -4,7 +4,7 @@ # Contentstack # # Created by Uttam Ukkoji on 30/03/2023. -# Copyright © 2025 Contentstack. All rights reserved. +# Copyright © 2026 Contentstack. All rights reserved. echo "Removing files" rm -rf "./Contentstack.Management.Core.Unit.Tests/TestResults" From 72feaf845f8823fb9347149031cc3a4a80e79048 Mon Sep 17 00:00:00 2001 From: reeshika-h Date: Wed, 7 Jan 2026 12:35:49 +0530 Subject: [PATCH 03/10] Refactor error messages in Contentstack Management Core --- .../Utils/CSConstantsTest.cs | 6 +- .../ContentstackClient.cs | 10 +- Contentstack.Management.Core/Models/Asset.cs | 5 +- .../Models/AssetModel.cs | 5 +- .../Models/AuditLog.cs | 5 +- .../Models/BaseModel.cs | 7 +- .../CustomExtension/CustomFieldModel.cs | 5 +- .../CustomExtension/CustomWidgetModel.cs | 5 +- .../CustomExtension/DashboardWidgetModel.cs | 5 +- .../Models/Extension.cs | 5 +- Contentstack.Management.Core/Models/Folder.cs | 5 +- .../Models/Organization.cs | 6 +- .../Models/PublishQueue.cs | 5 +- .../Models/Release.cs | 5 +- .../Models/ReleaseItem.cs | 3 +- Contentstack.Management.Core/Models/Stack.cs | 20 +-- .../Models/Version.cs | 5 +- .../Models/Workflow.cs | 5 +- .../Queryable/ParameterCollection.cs | 3 +- .../Queryable/Query.cs | 4 +- .../Runtime/Pipeline/PipelineHandler.cs | 5 +- .../RetryHandler/DefaultRetryPolicy.cs | 6 + .../RetryHandler/RetryDelayCalculator.cs | 2 +- .../Services/ContentstackService.cs | 2 +- .../Services/DeleteReleaseItemService.cs | 7 +- .../Models/CreateUpdateFolderService.cs | 5 +- .../Services/Models/CreateUpdateService.cs | 9 +- .../Services/Models/DeleteService.cs | 9 +- .../Services/Models/FetchDeleteService.cs | 5 +- .../Services/Models/FetchReferencesService.cs | 5 +- .../Models/GlobalFieldFetchDeleteService.cs | 5 +- .../Services/Models/GlobalFieldService.cs | 9 +- .../Services/Models/ImportExportService.cs | 5 +- .../Services/Models/LocaleService.cs | 3 +- .../Services/Models/LocalizationService.cs | 9 +- .../Models/PublishUnpublishService.cs | 9 +- .../Services/Models/UploadService.cs | 7 +- .../Models/Versioning/VersionService.cs | 7 +- .../Services/Organization/OrgRoles.cs | 3 +- .../Organization/OrganizationStackService.cs | 3 +- .../Organization/ResendInvitationService.cs | 5 +- .../Organization/TransferOwnershipService.cs | 5 +- .../Organization/UserInvitationService.cs | 5 +- .../Services/QueryService.cs | 5 +- .../Stack/StackCreateUpdateService.cs | 7 +- .../Services/Stack/StackOwnershipService.cs | 5 +- .../Services/Stack/StackSettingsService.cs | 3 +- .../Services/Stack/StackShareService.cs | 3 +- .../Services/Stack/UpdateUserRoleService.cs | 5 +- .../Services/User/ForgotPasswordService.cs | 3 +- .../Services/User/LoginService.cs | 3 +- .../Services/User/LogoutService.cs | 3 +- .../Services/User/ResetPasswordService.cs | 7 +- .../Utils/CSConstants.cs | 126 +++++++++++++++++- .../contentstack.management.core.csproj | 1 + 55 files changed, 295 insertions(+), 125 deletions(-) diff --git a/Contentstack.Management.Core.Unit.Tests/Utils/CSConstantsTest.cs b/Contentstack.Management.Core.Unit.Tests/Utils/CSConstantsTest.cs index 969e213..24bd5a5 100644 --- a/Contentstack.Management.Core.Unit.Tests/Utils/CSConstantsTest.cs +++ b/Contentstack.Management.Core.Unit.Tests/Utils/CSConstantsTest.cs @@ -30,9 +30,9 @@ public void Test_CSConstants_InternalConstants() public void Test_CSConstants_InternalMessages() { Assert.AreEqual("You are already logged in.", CSConstants.YouAreLoggedIn); - Assert.AreEqual("You are need to login.", CSConstants.YouAreNotLoggedIn); - Assert.AreEqual("Uid should not be empty.", CSConstants.MissingUID); - Assert.AreEqual("API Key should not be empty.", CSConstants.MissingAPIKey); + Assert.AreEqual("You are not logged in. Log in and try again.", CSConstants.YouAreNotLoggedIn); + Assert.AreEqual("UID is required. Provide a valid UID and try again.", CSConstants.MissingUID); + Assert.AreEqual("API Key is required. Provide a valid API Key and try again.", CSConstants.MissingAPIKey); Assert.AreEqual("API Key should be empty.", CSConstants.APIKey); Assert.AreEqual("Please enter email id to remove from org.", CSConstants.RemoveUserEmailError); Assert.AreEqual("Please enter share uid to resend invitation.", CSConstants.OrgShareUIDMissing); diff --git a/Contentstack.Management.Core/ContentstackClient.cs b/Contentstack.Management.Core/ContentstackClient.cs index a8929b4..0dd13d0 100644 --- a/Contentstack.Management.Core/ContentstackClient.cs +++ b/Contentstack.Management.Core/ContentstackClient.cs @@ -485,7 +485,7 @@ public Task LogoutAsync(string authtoken = null) public OAuthHandler OAuth(OAuthOptions options) { if (options == null) - throw new ArgumentNullException(nameof(options), "OAuth options cannot be null."); + throw new ArgumentNullException(nameof(options), CSConstants.OAuthOptionsRequired); return new OAuthHandler(this, options); } @@ -519,10 +519,10 @@ public OAuthHandler OAuth() internal void SetOAuthTokens(OAuthTokens tokens) { if (tokens == null) - throw new ArgumentNullException(nameof(tokens), "OAuth tokens cannot be null."); + throw new ArgumentNullException(nameof(tokens), CSConstants.OAuthTokensRequired); if (string.IsNullOrEmpty(tokens.AccessToken)) - throw new ArgumentException("Access token cannot be null or empty.", nameof(tokens)); + throw new ArgumentException(CSConstants.AccessTokenRequired, nameof(tokens)); // Store the access token in the client options for use in HTTP requests // This will be used by the HTTP pipeline to inject the Bearer token @@ -541,7 +541,7 @@ internal void SetOAuthTokens(OAuthTokens tokens) public OAuthTokens GetOAuthTokens(string clientId) { if (string.IsNullOrEmpty(clientId)) - throw new ArgumentException("Client ID cannot be null or empty.", nameof(clientId)); + throw new ArgumentException(CSConstants.ClientIDRequired, nameof(clientId)); return GetStoredOAuthTokens(clientId); } @@ -607,7 +607,7 @@ public void ClearOAuthTokens(string clientId = null) internal void StoreOAuthTokens(string clientId, OAuthTokens tokens) { if (string.IsNullOrEmpty(clientId)) - throw new ArgumentException("Client ID cannot be null or empty.", nameof(clientId)); + throw new ArgumentException(CSConstants.ClientIDRequired, nameof(clientId)); if (tokens == null) throw new ArgumentNullException(nameof(tokens)); diff --git a/Contentstack.Management.Core/Models/Asset.cs b/Contentstack.Management.Core/Models/Asset.cs index 738b842..ad7ac23 100644 --- a/Contentstack.Management.Core/Models/Asset.cs +++ b/Contentstack.Management.Core/Models/Asset.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Contentstack.Management.Core.Queryable; using Contentstack.Management.Core.Services.Models; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Models { @@ -359,7 +360,7 @@ internal void ThrowIfUidNotEmpty() { if (!string.IsNullOrEmpty(this.Uid)) { - throw new InvalidOperationException("Operation not allowed."); + throw new InvalidOperationException(CSConstants.AssetAlreadyExists); } } @@ -367,7 +368,7 @@ internal void ThrowIfUidEmpty() { if (string.IsNullOrEmpty(this.Uid)) { - throw new InvalidOperationException("Uid can not be empty."); + throw new InvalidOperationException(CSConstants.AssetUIDRequired); } } #endregion diff --git a/Contentstack.Management.Core/Models/AssetModel.cs b/Contentstack.Management.Core/Models/AssetModel.cs index 0bdc52d..bc96b09 100644 --- a/Contentstack.Management.Core/Models/AssetModel.cs +++ b/Contentstack.Management.Core/Models/AssetModel.cs @@ -2,6 +2,7 @@ using System.IO; using System.Net.Http; using Contentstack.Management.Core.Abstractions; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Models { @@ -29,11 +30,11 @@ public AssetModel(string fileName, ByteArrayContent byteArray, string contentTyp { if (fileName == null) { - throw new ArgumentNullException("fileName", "File name can not be null."); + throw new ArgumentNullException("fileName", CSConstants.FileNameRequired); } if (byteArray == null) { - throw new ArgumentNullException("byteArray", "Uploading content can not be null."); + throw new ArgumentNullException("byteArray", CSConstants.UploadContentRequired); } FileName = fileName; Title = title; diff --git a/Contentstack.Management.Core/Models/AuditLog.cs b/Contentstack.Management.Core/Models/AuditLog.cs index 9d24b54..1e2d082 100644 --- a/Contentstack.Management.Core/Models/AuditLog.cs +++ b/Contentstack.Management.Core/Models/AuditLog.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Contentstack.Management.Core.Queryable; using Contentstack.Management.Core.Services.Models; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Models { @@ -107,7 +108,7 @@ internal void ThrowIfUidNotEmpty() { if (!string.IsNullOrEmpty(this.Uid)) { - throw new InvalidOperationException("Operation not allowed."); + throw new InvalidOperationException(CSConstants.OperationNotAllowedOnAuditLogs); } } @@ -115,7 +116,7 @@ internal void ThrowIfUidEmpty() { if (string.IsNullOrEmpty(this.Uid)) { - throw new InvalidOperationException("Uid can not be empty."); + throw new InvalidOperationException(CSConstants.AuditLogUIDRequired); } } #endregion diff --git a/Contentstack.Management.Core/Models/BaseModel.cs b/Contentstack.Management.Core/Models/BaseModel.cs index 52d94e1..cf55da2 100644 --- a/Contentstack.Management.Core/Models/BaseModel.cs +++ b/Contentstack.Management.Core/Models/BaseModel.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Contentstack.Management.Core.Queryable; using Contentstack.Management.Core.Services.Models; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Models { @@ -19,7 +20,7 @@ public BaseModel(Stack stack, string fieldName, string uid = null) stack.ThrowIfAPIKeyEmpty(); if (fieldName == null) { - throw new ArgumentNullException("fieldName", "Field name mandatory for service"); + throw new ArgumentNullException("fieldName", CSConstants.FieldNameRequired); } this.stack = stack; this.fieldName = fieldName; @@ -105,7 +106,7 @@ internal void ThrowIfUidNotEmpty() { if (!string.IsNullOrEmpty(this.Uid)) { - throw new InvalidOperationException("Operation not allowed."); + throw new InvalidOperationException(CSConstants.OperationNotAllowedOnModel); } } @@ -113,7 +114,7 @@ internal void ThrowIfUidEmpty() { if (string.IsNullOrEmpty(this.Uid)) { - throw new InvalidOperationException("Uid can not be empty."); + throw new InvalidOperationException(CSConstants.MissingUID); } } #endregion diff --git a/Contentstack.Management.Core/Models/CustomExtension/CustomFieldModel.cs b/Contentstack.Management.Core/Models/CustomExtension/CustomFieldModel.cs index ac269b4..e39c5fe 100644 --- a/Contentstack.Management.Core/Models/CustomExtension/CustomFieldModel.cs +++ b/Contentstack.Management.Core/Models/CustomExtension/CustomFieldModel.cs @@ -2,6 +2,7 @@ using System.IO; using System.Net.Http; using Contentstack.Management.Core.Abstractions; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Models.CustomExtension { @@ -32,11 +33,11 @@ public CustomFieldModel(ByteArrayContent byteArray, string contentType, string t if (byteArray == null) { - throw new ArgumentNullException("byteArray", "Uploading content can not be null."); + throw new ArgumentNullException("byteArray", CSConstants.UploadContentRequired); } if (title == null) { - throw new ArgumentNullException("title", "Title for widget is required."); + throw new ArgumentNullException("title", CSConstants.WidgetTitleRequired); } Title = title; DataType = dataType; diff --git a/Contentstack.Management.Core/Models/CustomExtension/CustomWidgetModel.cs b/Contentstack.Management.Core/Models/CustomExtension/CustomWidgetModel.cs index ca9074a..6b6753f 100644 --- a/Contentstack.Management.Core/Models/CustomExtension/CustomWidgetModel.cs +++ b/Contentstack.Management.Core/Models/CustomExtension/CustomWidgetModel.cs @@ -4,6 +4,7 @@ using System.Net.Http; using System.Net.Http.Headers; using Contentstack.Management.Core.Abstractions; +using Contentstack.Management.Core.Utils; using Newtonsoft.Json; namespace Contentstack.Management.Core.Models.CustomExtension @@ -33,11 +34,11 @@ public CustomWidgetModel(ByteArrayContent byteArray, string contentType, string { if (byteArray == null) { - throw new ArgumentNullException("byteArray", "Uploading content can not be null."); + throw new ArgumentNullException("byteArray", CSConstants.UploadContentRequired); } if (title == null) { - throw new ArgumentNullException("title", "Title for widget is required."); + throw new ArgumentNullException("title", CSConstants.WidgetTitleRequired); } Title = title; Tags = tags; diff --git a/Contentstack.Management.Core/Models/CustomExtension/DashboardWidgetModel.cs b/Contentstack.Management.Core/Models/CustomExtension/DashboardWidgetModel.cs index 57d5894..b75c43b 100644 --- a/Contentstack.Management.Core/Models/CustomExtension/DashboardWidgetModel.cs +++ b/Contentstack.Management.Core/Models/CustomExtension/DashboardWidgetModel.cs @@ -2,6 +2,7 @@ using System.IO; using System.Net.Http; using Contentstack.Management.Core.Abstractions; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Models.CustomExtension { @@ -32,11 +33,11 @@ public DashboardWidgetModel(ByteArrayContent byteArray, string contentType, stri if (byteArray == null) { - throw new ArgumentNullException("byteArray", "Uploading content can not be null."); + throw new ArgumentNullException("byteArray", CSConstants.UploadContentRequired); } if (title == null) { - throw new ArgumentNullException("title", "Title for widget is required."); + throw new ArgumentNullException("title", CSConstants.WidgetTitleRequired); } Title = title; Tags = tags; diff --git a/Contentstack.Management.Core/Models/Extension.cs b/Contentstack.Management.Core/Models/Extension.cs index d9c51e3..7364ebf 100644 --- a/Contentstack.Management.Core/Models/Extension.cs +++ b/Contentstack.Management.Core/Models/Extension.cs @@ -3,6 +3,7 @@ using Contentstack.Management.Core.Abstractions; using Contentstack.Management.Core.Queryable; using Contentstack.Management.Core.Services.Models; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Models { @@ -246,7 +247,7 @@ internal void ThrowIfUidNotEmpty() { if (!string.IsNullOrEmpty(this.Uid)) { - throw new InvalidOperationException("Operation not allowed."); + throw new InvalidOperationException(CSConstants.OperationNotAllowedOnExtension); } } @@ -254,7 +255,7 @@ internal void ThrowIfUidEmpty() { if (string.IsNullOrEmpty(this.Uid)) { - throw new InvalidOperationException("Uid can not be empty."); + throw new InvalidOperationException(CSConstants.ExtensionUIDRequired); } } #endregion diff --git a/Contentstack.Management.Core/Models/Folder.cs b/Contentstack.Management.Core/Models/Folder.cs index 08de139..81fce4b 100644 --- a/Contentstack.Management.Core/Models/Folder.cs +++ b/Contentstack.Management.Core/Models/Folder.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Contentstack.Management.Core.Queryable; using Contentstack.Management.Core.Services.Models; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Models { @@ -190,7 +191,7 @@ internal void ThrowIfUidNotEmpty() { if (!string.IsNullOrEmpty(this.Uid)) { - throw new InvalidOperationException("Operation not allowed."); + throw new InvalidOperationException(CSConstants.OperationNotAllowedOnFolder); } } @@ -198,7 +199,7 @@ internal void ThrowIfUidEmpty() { if (string.IsNullOrEmpty(this.Uid)) { - throw new InvalidOperationException("Uid can not be empty."); + throw new InvalidOperationException(CSConstants.FolderUIDRequired); } } #endregion diff --git a/Contentstack.Management.Core/Models/Organization.cs b/Contentstack.Management.Core/Models/Organization.cs index a132db6..db5a540 100644 --- a/Contentstack.Management.Core/Models/Organization.cs +++ b/Contentstack.Management.Core/Models/Organization.cs @@ -215,7 +215,7 @@ public ContentstackResponse RemoveUser(List emails) this.ThrowIfOrganizationUidNull(); if (emails == null) { - throw new ArgumentNullException("emails"); + throw new ArgumentNullException("emails", CSConstants.EmailsRequired); } var userInviteService = new UserInvitationService(_client.serializer, this.Uid, "DELETE"); userInviteService.RemoveUsers(emails); @@ -240,7 +240,7 @@ public Task RemoveUserAsync(List emails) this.ThrowIfOrganizationUidNull(); if (emails == null) { - throw new ArgumentNullException("emails"); + throw new ArgumentNullException("emails", CSConstants.EmailsRequired); } var userInviteService = new UserInvitationService(_client.serializer, this.Uid, "DELETE"); userInviteService.RemoveUsers(emails); @@ -429,7 +429,7 @@ private void ThrowIfOrganizationUidNull() { if (string.IsNullOrEmpty(this.Uid)) { - throw new InvalidOperationException(CSConstants.MissingUID); + throw new InvalidOperationException(CSConstants.OrganizationUIDRequired); } } #endregion diff --git a/Contentstack.Management.Core/Models/PublishQueue.cs b/Contentstack.Management.Core/Models/PublishQueue.cs index 984950c..19157be 100644 --- a/Contentstack.Management.Core/Models/PublishQueue.cs +++ b/Contentstack.Management.Core/Models/PublishQueue.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Contentstack.Management.Core.Queryable; using Contentstack.Management.Core.Services.Models; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Models { @@ -151,7 +152,7 @@ internal void ThrowIfUidNotEmpty() { if (!string.IsNullOrEmpty(this.Uid)) { - throw new InvalidOperationException("Operation not allowed."); + throw new InvalidOperationException(CSConstants.OperationNotAllowedOnPublishQueue); } } @@ -159,7 +160,7 @@ internal void ThrowIfUidEmpty() { if (string.IsNullOrEmpty(this.Uid)) { - throw new InvalidOperationException("Uid can not be empty."); + throw new InvalidOperationException(CSConstants.PublishQueueUIDRequired); } } #endregion diff --git a/Contentstack.Management.Core/Models/Release.cs b/Contentstack.Management.Core/Models/Release.cs index 03b555f..0a3662f 100644 --- a/Contentstack.Management.Core/Models/Release.cs +++ b/Contentstack.Management.Core/Models/Release.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Contentstack.Management.Core.Queryable; using Contentstack.Management.Core.Services.Models; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Models { @@ -236,7 +237,7 @@ public ContentstackResponse Clone(string name, string description) { if (string.IsNullOrEmpty(name)) { - throw new ArgumentNullException("name", "Invalide name."); + throw new ArgumentNullException("name", CSConstants.ReleaseNameInvalid); } stack.ThrowIfNotLoggedIn(); ThrowIfUidEmpty(); @@ -267,7 +268,7 @@ public Task CloneAsync(string name, string description) { if (string.IsNullOrEmpty(name)) { - throw new ArgumentNullException("name", "Invalide name."); + throw new ArgumentNullException("name", CSConstants.ReleaseNameInvalid); } stack.ThrowIfNotLoggedIn(); ThrowIfUidEmpty(); diff --git a/Contentstack.Management.Core/Models/ReleaseItem.cs b/Contentstack.Management.Core/Models/ReleaseItem.cs index eea1654..a5b1c81 100644 --- a/Contentstack.Management.Core/Models/ReleaseItem.cs +++ b/Contentstack.Management.Core/Models/ReleaseItem.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Contentstack.Management.Core.Queryable; using Contentstack.Management.Core.Services.Models; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Models { @@ -258,7 +259,7 @@ internal void ThrowIfUidEmpty() { if (string.IsNullOrEmpty(this.releaseUID)) { - throw new InvalidOperationException("Uid can not be empty."); + throw new InvalidOperationException(CSConstants.ReleaseItemUIDRequired); } } #endregion diff --git a/Contentstack.Management.Core/Models/Stack.cs b/Contentstack.Management.Core/Models/Stack.cs index 3d259af..2fbad64 100644 --- a/Contentstack.Management.Core/Models/Stack.cs +++ b/Contentstack.Management.Core/Models/Stack.cs @@ -435,7 +435,7 @@ public ContentstackResponse AddSettings(StackSettings settings) ThrowIfAPIKeyEmpty(); if (settings == null) { - throw new ArgumentNullException("settings", "Settings can not be null."); + throw new ArgumentNullException("settings", CSConstants.StackSettingsRequired); } var service = new StackSettingsService(client.serializer, this, "POST", settings); @@ -460,7 +460,7 @@ public Task AddSettingsAsync(StackSettings settings) ThrowIfAPIKeyEmpty(); if (settings == null) { - throw new ArgumentNullException("settings", "Settings can not be null."); + throw new ArgumentNullException("settings", CSConstants.StackSettingsRequired); } var service = new StackSettingsService(client.serializer, this, "POST", settings); @@ -489,7 +489,7 @@ public ContentstackResponse Share(List invitations) ThrowIfAPIKeyEmpty(); if (invitations == null) { - throw new ArgumentNullException("invitations", "Invitations can not be null."); + throw new ArgumentNullException("invitations", CSConstants.InvitationsRequired); } var service = new StackShareService(client.serializer, this); @@ -520,7 +520,7 @@ public Task ShareAsync(List invitations) ThrowIfAPIKeyEmpty(); if (invitations == null) { - throw new ArgumentNullException("invitations", "Invitations can not be null."); + throw new ArgumentNullException("invitations", CSConstants.InvitationsRequired); } var service = new StackShareService(client.serializer, this); @@ -546,7 +546,7 @@ public ContentstackResponse UnShare(string email) ThrowIfAPIKeyEmpty(); if (email == null) { - throw new ArgumentNullException("email", "Email can not be null."); + throw new ArgumentNullException("email", CSConstants.EmailRequired); } var service = new StackShareService(client.serializer, this); @@ -573,7 +573,7 @@ public Task UnShareAsync(string email) ThrowIfAPIKeyEmpty(); if (email == null) { - throw new ArgumentNullException("email", "Email can not be null."); + throw new ArgumentNullException("email", CSConstants.EmailRequired); } var service = new StackShareService(client.serializer, this); @@ -941,7 +941,7 @@ internal void ThrowIfAPIKeyNotEmpty() { if (!string.IsNullOrEmpty(this.APIKey)) { - throw new InvalidOperationException(CSConstants.APIKey); + throw new InvalidOperationException(CSConstants.InvalidAPIKey); } } @@ -956,7 +956,7 @@ internal void ThrowInvalideName(string name) { if (string.IsNullOrEmpty(name)) { - throw new ArgumentNullException("name", "Invalide name for the Stack."); + throw new ArgumentNullException("name", CSConstants.StackNameInvalid); } } @@ -964,7 +964,7 @@ internal void ThrowInvalideLocale(string locale) { if (string.IsNullOrEmpty(locale)) { - throw new ArgumentNullException("locale", "Invalide name for the Stack."); + throw new ArgumentNullException("locale", CSConstants.LocaleInvalid); } } @@ -972,7 +972,7 @@ internal void ThrowInvalideOrganizationUid(string uid) { if (string.IsNullOrEmpty(uid)) { - throw new ArgumentNullException("uid", "Invalide Organization UID."); + throw new ArgumentNullException("uid", CSConstants.OrganizationUIDInvalid); } } diff --git a/Contentstack.Management.Core/Models/Version.cs b/Contentstack.Management.Core/Models/Version.cs index e899279..546a9a5 100644 --- a/Contentstack.Management.Core/Models/Version.cs +++ b/Contentstack.Management.Core/Models/Version.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Contentstack.Management.Core.Queryable; using Contentstack.Management.Core.Services.Models.Versioning; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Models { @@ -83,7 +84,7 @@ internal void ThrowIfVersionNumberNotEmpty() { if (Number != null) { - throw new InvalidOperationException("Operation not allowed."); + throw new InvalidOperationException(CSConstants.OperationNotAllowedForVersion); } } @@ -91,7 +92,7 @@ internal void ThrowIfVersionNumberEmpty() { if (Number == null) { - throw new InvalidOperationException("Uid can not be empty."); + throw new InvalidOperationException(CSConstants.VersionUIDRequired); } } #endregion diff --git a/Contentstack.Management.Core/Models/Workflow.cs b/Contentstack.Management.Core/Models/Workflow.cs index c9cd182..797869f 100644 --- a/Contentstack.Management.Core/Models/Workflow.cs +++ b/Contentstack.Management.Core/Models/Workflow.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Contentstack.Management.Core.Queryable; using Contentstack.Management.Core.Services.Models; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Models { @@ -288,7 +289,7 @@ public virtual ContentstackResponse GetPublishRule(string contentType, Parameter stack.ThrowIfNotLoggedIn(); if (string.IsNullOrEmpty(contentType)) { - throw new ArgumentNullException("Content Type can not be empty."); + throw new ArgumentNullException("contentType", CSConstants.ContentTypeRequired); } var service = new FetchDeleteService(stack.client.serializer, stack, $"/workflows/content_type/{contentType}", collection: collection); @@ -310,7 +311,7 @@ public virtual Task GetPublishRuleAsync(string contentType stack.ThrowIfNotLoggedIn(); if (string.IsNullOrEmpty(contentType)) { - throw new ArgumentNullException("Content Type can not be empty."); + throw new ArgumentNullException("contentType", CSConstants.ContentTypeRequired); } var service = new FetchDeleteService(stack.client.serializer, stack, $"/workflows/content_type/{contentType}", collection: collection); return stack.client.InvokeAsync(service); diff --git a/Contentstack.Management.Core/Queryable/ParameterCollection.cs b/Contentstack.Management.Core/Queryable/ParameterCollection.cs index f1f27b8..1d701d9 100644 --- a/Contentstack.Management.Core/Queryable/ParameterCollection.cs +++ b/Contentstack.Management.Core/Queryable/ParameterCollection.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.Linq; using Contentstack.Management.Core.Exceptions; +using Contentstack.Management.Core.Utils; using Newtonsoft.Json.Linq; namespace Contentstack.Management.Core.Queryable @@ -113,7 +114,7 @@ private IEnumerable> GetParametersEnumerable() yield return new KeyValuePair($"{name}[]", listValue.ToString(CultureInfo.InvariantCulture)); break; default: - throw new ContentstackException("Unsupported parameter value type '" + value.GetType().FullName + "'"); + throw new ContentstackException(CSConstants.ParameterTypeNotSupported); } } } diff --git a/Contentstack.Management.Core/Queryable/Query.cs b/Contentstack.Management.Core/Queryable/Query.cs index eb8b6fe..6e9a14b 100644 --- a/Contentstack.Management.Core/Queryable/Query.cs +++ b/Contentstack.Management.Core/Queryable/Query.cs @@ -17,12 +17,12 @@ internal Query(Stack stack, string resourcePath, string apiVersion = null) { if(stack == null) { - throw new ArgumentNullException("stack", "Stack can not be null"); + throw new ArgumentNullException("stack", CSConstants.StackRequired); } if (resourcePath== null) { - throw new ArgumentNullException("resourcePath", "Respource path can not be null"); + throw new ArgumentNullException("resourcePath", CSConstants.ResourcePathRequired); } _stack = stack; _resourcePath = resourcePath; diff --git a/Contentstack.Management.Core/Runtime/Pipeline/PipelineHandler.cs b/Contentstack.Management.Core/Runtime/Pipeline/PipelineHandler.cs index 0feb573..e802586 100644 --- a/Contentstack.Management.Core/Runtime/Pipeline/PipelineHandler.cs +++ b/Contentstack.Management.Core/Runtime/Pipeline/PipelineHandler.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Contentstack.Management.Core.Internal; using Contentstack.Management.Core.Runtime.Contexts; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Runtime.Pipeline { @@ -17,7 +18,7 @@ public virtual Task InvokeAsync(IExecutionContext executionContext, bool a { return InnerHandler.InvokeAsync(executionContext, addAcceptMediaHeader, apiVersion); } - throw new InvalidOperationException("Cannot invoke InnerHandler. InnerHandler is not set."); + throw new InvalidOperationException(CSConstants.InnerHandlerNotSet); } public virtual void InvokeSync(IExecutionContext executionContext, bool addAcceptMediaHeader = false, string apiVersion = null) @@ -27,7 +28,7 @@ public virtual void InvokeSync(IExecutionContext executionContext, bool addAccep InnerHandler.InvokeSync(executionContext, addAcceptMediaHeader, apiVersion); return; } - throw new InvalidOperationException("Cannot invoke InnerHandler. InnerHandler is not set."); + throw new InvalidOperationException(CSConstants.InnerHandlerNotSet); } } } diff --git a/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/DefaultRetryPolicy.cs b/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/DefaultRetryPolicy.cs index fc436b2..a494aba 100644 --- a/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/DefaultRetryPolicy.cs +++ b/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/DefaultRetryPolicy.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using Contentstack.Management.Core.Exceptions; using Contentstack.Management.Core.Runtime.Contexts; @@ -207,6 +208,11 @@ public TimeSpan GetNetworkRetryDelay(IRequestContext requestContext) requestContext.NetworkRetryCount, retryConfiguration); } + + // Internal methods for testing + internal bool CanRetryInternal(IExecutionContext executionContext) => CanRetry(executionContext); + internal bool RetryForExceptionInternal(IExecutionContext executionContext, Exception exception) => RetryForException(executionContext, exception); + internal bool RetryLimitExceededInternal(IExecutionContext executionContext) => RetryLimitExceeded(executionContext); } } diff --git a/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/RetryDelayCalculator.cs b/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/RetryDelayCalculator.cs index 28e4c74..32fbd86 100644 --- a/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/RetryDelayCalculator.cs +++ b/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/RetryDelayCalculator.cs @@ -111,7 +111,7 @@ public bool ShouldRetryHttpStatusCode(HttpStatusCode statusCode, RetryConfigurat } // Default retry condition: 429, 500, 502, 503, 504 - return statusCode == HttpStatusCode.TooManyRequests || + return statusCode == (HttpStatusCode)429 || // TooManyRequests statusCode == HttpStatusCode.InternalServerError || statusCode == HttpStatusCode.BadGateway || statusCode == HttpStatusCode.ServiceUnavailable || diff --git a/Contentstack.Management.Core/Services/ContentstackService.cs b/Contentstack.Management.Core/Services/ContentstackService.cs index dcffbe4..fcf422f 100644 --- a/Contentstack.Management.Core/Services/ContentstackService.cs +++ b/Contentstack.Management.Core/Services/ContentstackService.cs @@ -32,7 +32,7 @@ internal ContentstackService(JsonSerializer serializer, Core.Models.Stack stack { if (serializer == null) { - throw new ArgumentNullException("serializer"); + throw new ArgumentNullException("serializer", CSConstants.JSONSerializerError); } if (stack != null) diff --git a/Contentstack.Management.Core/Services/DeleteReleaseItemService.cs b/Contentstack.Management.Core/Services/DeleteReleaseItemService.cs index 41d07bf..37a63c3 100644 --- a/Contentstack.Management.Core/Services/DeleteReleaseItemService.cs +++ b/Contentstack.Management.Core/Services/DeleteReleaseItemService.cs @@ -4,6 +4,7 @@ using System.IO; using Contentstack.Management.Core.Queryable; using Newtonsoft.Json; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Services { internal class DeleteReleaseItemService : ContentstackService @@ -16,15 +17,15 @@ internal DeleteReleaseItemService(JsonSerializer serializer, Core.Models.Stack s { if (stack.APIKey == null) { - throw new ArgumentNullException("stack", "Should have API Key to perform this operation."); + throw new ArgumentNullException("stack", CSConstants.MissingAPIKey); } if (releaseUID == null) { - throw new ArgumentNullException("releaseUID", "Should have release UID for service."); + throw new ArgumentNullException("releaseUID", CSConstants.ReleaseUIDRequired); } if (items == null) { - throw new ArgumentNullException("items", "Should release items for service."); + throw new ArgumentNullException("items", CSConstants.ReleaseItemsRequired); } this.ResourcePath = $"/releases/{releaseUID}/item"; this.HttpMethod = "DELETE"; diff --git a/Contentstack.Management.Core/Services/Models/CreateUpdateFolderService.cs b/Contentstack.Management.Core/Services/Models/CreateUpdateFolderService.cs index 2eb042c..699cb5e 100644 --- a/Contentstack.Management.Core/Services/Models/CreateUpdateFolderService.cs +++ b/Contentstack.Management.Core/Services/Models/CreateUpdateFolderService.cs @@ -2,6 +2,7 @@ using System.Globalization; using System.IO; using Newtonsoft.Json; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Services.Models { @@ -21,11 +22,11 @@ internal CreateUpdateFolderService( { if (stack.APIKey == null) { - throw new ArgumentNullException("stack", "Should have API Key to perform this operation."); + throw new ArgumentNullException("stack", CSConstants.MissingAPIKey); } if (name == null) { - throw new ArgumentNullException("name", "Should have folder name."); + throw new ArgumentNullException("name", CSConstants.FolderNameRequired); } this.ResourcePath = "/assets/folders"; diff --git a/Contentstack.Management.Core/Services/Models/CreateUpdateService.cs b/Contentstack.Management.Core/Services/Models/CreateUpdateService.cs index 75cb646..b4530e6 100644 --- a/Contentstack.Management.Core/Services/Models/CreateUpdateService.cs +++ b/Contentstack.Management.Core/Services/Models/CreateUpdateService.cs @@ -3,6 +3,7 @@ using System.IO; using Contentstack.Management.Core.Queryable; using Newtonsoft.Json; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Services.Models { @@ -17,19 +18,19 @@ internal CreateUpdateService(JsonSerializer serializer, Core.Models.Stack stack, { if (stack.APIKey == null) { - throw new ArgumentNullException("stack", "Should have API Key to perform this operation."); + throw new ArgumentNullException("stack", CSConstants.MissingAPIKey); } if (resourcePath == null) { - throw new ArgumentNullException("resourcePath", "Should resource path for service."); + throw new ArgumentNullException("resourcePath", CSConstants.ResourcePathRequired); } if (dataModel == null) { - throw new ArgumentNullException("dataModel", "Data model is mandatory for service"); + throw new ArgumentNullException("dataModel", CSConstants.DataModelRequired); } if (fieldName == null) { - throw new ArgumentNullException("fieldName", "Name mandatory for service"); + throw new ArgumentNullException("fieldName", CSConstants.FieldNameRequired); } this.ResourcePath = resourcePath; this.HttpMethod = httpMethod; diff --git a/Contentstack.Management.Core/Services/Models/DeleteService.cs b/Contentstack.Management.Core/Services/Models/DeleteService.cs index 321e39c..c0c4bec 100644 --- a/Contentstack.Management.Core/Services/Models/DeleteService.cs +++ b/Contentstack.Management.Core/Services/Models/DeleteService.cs @@ -3,6 +3,7 @@ using System.IO; using Contentstack.Management.Core.Queryable; using Newtonsoft.Json; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Services.Models { @@ -16,19 +17,19 @@ internal DeleteService(JsonSerializer serializer, Core.Models.Stack stack, strin { if (stack.APIKey == null) { - throw new ArgumentNullException("stack", "Should have API Key to perform this operation."); + throw new ArgumentNullException("stack", CSConstants.MissingAPIKey); } if (resourcePath == null) { - throw new ArgumentNullException("resourcePath", "Should have resource path for service."); + throw new ArgumentNullException("resourcePath", CSConstants.ResourcePathRequired); } if (fieldName == null) { - throw new ArgumentNullException("fieldName", "Should have field name for service."); + throw new ArgumentNullException("fieldName", CSConstants.FieldNameRequired); } if (model == null) { - throw new ArgumentNullException("model", "Should have model for service."); + throw new ArgumentNullException("model", CSConstants.ModelRequired); } this.ResourcePath = resourcePath; this.HttpMethod = "DELETE"; diff --git a/Contentstack.Management.Core/Services/Models/FetchDeleteService.cs b/Contentstack.Management.Core/Services/Models/FetchDeleteService.cs index 3ba2c76..287a408 100644 --- a/Contentstack.Management.Core/Services/Models/FetchDeleteService.cs +++ b/Contentstack.Management.Core/Services/Models/FetchDeleteService.cs @@ -1,6 +1,7 @@ using System; using Contentstack.Management.Core.Queryable; using Newtonsoft.Json; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Services.Models { @@ -13,11 +14,11 @@ internal FetchDeleteService(JsonSerializer serializer, Core.Models.Stack stack, { if (stack.APIKey == null) { - throw new ArgumentNullException("stack", "Should have API Key to perform this operation."); + throw new ArgumentNullException("stack", CSConstants.MissingAPIKey); } if (resourcePath == null) { - throw new ArgumentNullException("resourcePath", "Should resource path for service."); + throw new ArgumentNullException("resourcePath", CSConstants.ResourcePathRequired); } this.ResourcePath = resourcePath; this.HttpMethod = httpMethod; diff --git a/Contentstack.Management.Core/Services/Models/FetchReferencesService.cs b/Contentstack.Management.Core/Services/Models/FetchReferencesService.cs index 9f53204..c012b33 100644 --- a/Contentstack.Management.Core/Services/Models/FetchReferencesService.cs +++ b/Contentstack.Management.Core/Services/Models/FetchReferencesService.cs @@ -1,6 +1,7 @@ using System; using Newtonsoft.Json; using Contentstack.Management.Core.Queryable; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Services.Models { @@ -11,11 +12,11 @@ internal FetchReferencesService(JsonSerializer serializer, Core.Models.Stack sta { if (stack.APIKey == null) { - throw new ArgumentNullException("stack", "Should have API Key to perform this operation."); + throw new ArgumentNullException("stack", CSConstants.MissingAPIKey); } if (resourcePath == null) { - throw new ArgumentNullException("resourcePath", "Should resource path for service."); + throw new ArgumentNullException("resourcePath", CSConstants.ResourcePathRequired); } this.ResourcePath = $"{resourcePath}/references"; this.HttpMethod = "GET"; diff --git a/Contentstack.Management.Core/Services/Models/GlobalFieldFetchDeleteService.cs b/Contentstack.Management.Core/Services/Models/GlobalFieldFetchDeleteService.cs index e04f868..9c526b6 100644 --- a/Contentstack.Management.Core/Services/Models/GlobalFieldFetchDeleteService.cs +++ b/Contentstack.Management.Core/Services/Models/GlobalFieldFetchDeleteService.cs @@ -1,6 +1,7 @@ using System; using Contentstack.Management.Core.Queryable; using Newtonsoft.Json; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Services.Models { @@ -19,11 +20,11 @@ internal GlobalFieldFetchDeleteService(JsonSerializer serializer, Core.Models.St { if (stack.APIKey == null) { - throw new ArgumentNullException("stack", "Should have API Key to perform this operation."); + throw new ArgumentNullException("stack", CSConstants.MissingAPIKey); } if (resourcePath == null) { - throw new ArgumentNullException("resourcePath", "Should resource path for service."); + throw new ArgumentNullException("resourcePath", CSConstants.ResourcePathRequired); } this.ResourcePath = resourcePath; diff --git a/Contentstack.Management.Core/Services/Models/GlobalFieldService.cs b/Contentstack.Management.Core/Services/Models/GlobalFieldService.cs index b4471c2..97e20d7 100644 --- a/Contentstack.Management.Core/Services/Models/GlobalFieldService.cs +++ b/Contentstack.Management.Core/Services/Models/GlobalFieldService.cs @@ -4,6 +4,7 @@ using Contentstack.Management.Core.Models; using Contentstack.Management.Core.Queryable; using Newtonsoft.Json; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Services.Models { @@ -24,19 +25,19 @@ internal GlobalFieldService(JsonSerializer serializer, Core.Models.Stack stack, { if (stack.APIKey == null) { - throw new ArgumentNullException("stack", "Should have API Key to perform this operation."); + throw new ArgumentNullException("stack", CSConstants.MissingAPIKey); } if (resourcePath == null) { - throw new ArgumentNullException("resourcePath", "Should resource path for service."); + throw new ArgumentNullException("resourcePath", CSConstants.ResourcePathRequired); } if (dataModel == null) { - throw new ArgumentNullException("dataModel", "Data model is mandatory for service"); + throw new ArgumentNullException("dataModel", CSConstants.DataModelRequired); } if (fieldName == null) { - throw new ArgumentNullException("fieldName", "Name mandatory for service"); + throw new ArgumentNullException("fieldName", CSConstants.FieldNameRequired); } this.ResourcePath = resourcePath; diff --git a/Contentstack.Management.Core/Services/Models/ImportExportService.cs b/Contentstack.Management.Core/Services/Models/ImportExportService.cs index 7df554c..3640bcf 100644 --- a/Contentstack.Management.Core/Services/Models/ImportExportService.cs +++ b/Contentstack.Management.Core/Services/Models/ImportExportService.cs @@ -1,6 +1,7 @@ using System; using Contentstack.Management.Core.Queryable; using Newtonsoft.Json; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Services.Models { @@ -11,11 +12,11 @@ internal ImportExportService(JsonSerializer serializer, Core.Models.Stack stack, { if (stack.APIKey == null) { - throw new ArgumentNullException("stack", "Should have API Key to perform this operation."); + throw new ArgumentNullException("stack", CSConstants.MissingAPIKey); } if (resourcePath == null) { - throw new ArgumentNullException("resourcePath", "Should have resource path for service."); + throw new ArgumentNullException("resourcePath", CSConstants.ResourcePathRequired); } ResourcePath = isImport ? $"{resourcePath}/import" : $"{resourcePath}/export"; diff --git a/Contentstack.Management.Core/Services/Models/LocaleService.cs b/Contentstack.Management.Core/Services/Models/LocaleService.cs index 728d3a8..75c007b 100644 --- a/Contentstack.Management.Core/Services/Models/LocaleService.cs +++ b/Contentstack.Management.Core/Services/Models/LocaleService.cs @@ -1,5 +1,6 @@ using System; using Newtonsoft.Json; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Services.Models { @@ -10,7 +11,7 @@ internal LocaleService(JsonSerializer serializer, Core.Models.Stack stack, strin { if (stack.APIKey == null) { - throw new ArgumentNullException("stack", "Should have API Key to perform this operation."); + throw new ArgumentNullException("stack", CSConstants.MissingAPIKey); } this.ResourcePath = resourcePath != null ? $"{resourcePath}/locales" : "locales"; diff --git a/Contentstack.Management.Core/Services/Models/LocalizationService.cs b/Contentstack.Management.Core/Services/Models/LocalizationService.cs index 0b7e21e..4dae67f 100644 --- a/Contentstack.Management.Core/Services/Models/LocalizationService.cs +++ b/Contentstack.Management.Core/Services/Models/LocalizationService.cs @@ -3,6 +3,7 @@ using System.IO; using Contentstack.Management.Core.Queryable; using Newtonsoft.Json; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Services.Models { @@ -18,19 +19,19 @@ internal LocalizationService(JsonSerializer serializer, Core.Models.Stack stack, { if (stack.APIKey == null) { - throw new ArgumentNullException("stack", "Should have API Key to perform this operation."); + throw new ArgumentNullException("stack", CSConstants.MissingAPIKey); } if (resourcePath == null) { - throw new ArgumentNullException("resourcePath", "Should resource path for service."); + throw new ArgumentNullException("resourcePath", CSConstants.ResourcePathRequired); } if (!shouldUnlocalize && dataModel == null) { - throw new ArgumentNullException("dataModel", "Data model is mandatory for service"); + throw new ArgumentNullException("dataModel", CSConstants.DataModelRequired); } if (fieldName == null) { - throw new ArgumentNullException("fieldName", "Name mandatory for service"); + throw new ArgumentNullException("fieldName", CSConstants.FieldNameRequired); } this.ResourcePath = shouldUnlocalize ? $"{resourcePath}/unlocalize" : resourcePath; this.HttpMethod = shouldUnlocalize ? "POST": "PUT"; diff --git a/Contentstack.Management.Core/Services/Models/PublishUnpublishService.cs b/Contentstack.Management.Core/Services/Models/PublishUnpublishService.cs index c768ea4..e8bb6fd 100644 --- a/Contentstack.Management.Core/Services/Models/PublishUnpublishService.cs +++ b/Contentstack.Management.Core/Services/Models/PublishUnpublishService.cs @@ -3,6 +3,7 @@ using System.IO; using Contentstack.Management.Core.Models; using Newtonsoft.Json; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Services.Models { @@ -16,22 +17,22 @@ internal PublishUnpublishService(JsonSerializer serializer, Core.Models.Stack st { if (stack.APIKey == null) { - throw new ArgumentNullException("stack", "Should have API Key to perform this operation."); + throw new ArgumentNullException("stack", CSConstants.MissingAPIKey); } if (details == null) { - throw new ArgumentNullException("details", "Should publish details for service."); + throw new ArgumentNullException("details", CSConstants.PublishDetailsRequired); } if (resourcePath == null) { - throw new ArgumentNullException("resourcePath", "Should resource path for service."); + throw new ArgumentNullException("resourcePath", CSConstants.ResourcePathRequired); } if (fieldName == null) { - throw new ArgumentNullException("fieldName", "Should field name for service."); + throw new ArgumentNullException("fieldName", CSConstants.FieldNameRequired); } this.ResourcePath = resourcePath; diff --git a/Contentstack.Management.Core/Services/Models/UploadService.cs b/Contentstack.Management.Core/Services/Models/UploadService.cs index 5657076..d62c948 100644 --- a/Contentstack.Management.Core/Services/Models/UploadService.cs +++ b/Contentstack.Management.Core/Services/Models/UploadService.cs @@ -3,6 +3,7 @@ using System.Net.Http; using Contentstack.Management.Core.Abstractions; using Newtonsoft.Json; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Services.Models { @@ -15,15 +16,15 @@ internal UploadService(JsonSerializer serializer, Core.Models.Stack stack, strin { if (stack.APIKey == null) { - throw new ArgumentNullException("stack", "Should have API Key to perform this operation."); + throw new ArgumentNullException("stack", CSConstants.MissingAPIKey); } if (resourcePath == null) { - throw new ArgumentNullException("resourcePath", "Should have resource path for service."); + throw new ArgumentNullException("resourcePath", CSConstants.ResourcePathRequired); } if (uploadInterface == null) { - throw new ArgumentNullException("uploadInterface", "Should have multipart content for service."); + throw new ArgumentNullException("uploadInterface", CSConstants.UploadContentRequired); } this.ResourcePath = resourcePath; this.HttpMethod = httpMethod; diff --git a/Contentstack.Management.Core/Services/Models/Versioning/VersionService.cs b/Contentstack.Management.Core/Services/Models/Versioning/VersionService.cs index 6c4e3a7..1af2a60 100644 --- a/Contentstack.Management.Core/Services/Models/Versioning/VersionService.cs +++ b/Contentstack.Management.Core/Services/Models/Versioning/VersionService.cs @@ -3,6 +3,7 @@ using System.IO; using Contentstack.Management.Core.Queryable; using Newtonsoft.Json; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Services.Models.Versioning { @@ -20,15 +21,15 @@ internal VersionService(JsonSerializer serializer, Core.Models.Stack stack, stri { if (stack.APIKey == null) { - throw new ArgumentNullException("stack", "Should have API Key to perform this operation."); + throw new ArgumentNullException("stack", CSConstants.MissingAPIKey); } if (resourcePath == null) { - throw new ArgumentNullException("resourcePath", "Should resource path for service."); + throw new ArgumentNullException("resourcePath", CSConstants.ResourcePathRequired); } if (fieldName == null) { - throw new ArgumentNullException("fieldName", "Should resource path for service."); + throw new ArgumentNullException("fieldName", CSConstants.FieldNameRequired); } this.fieldName = fieldName; this.ResourcePath = resourcePath; diff --git a/Contentstack.Management.Core/Services/Organization/OrgRoles.cs b/Contentstack.Management.Core/Services/Organization/OrgRoles.cs index be8952e..796c223 100644 --- a/Contentstack.Management.Core/Services/Organization/OrgRoles.cs +++ b/Contentstack.Management.Core/Services/Organization/OrgRoles.cs @@ -1,6 +1,7 @@ using System; using Contentstack.Management.Core.Queryable; using Newtonsoft.Json; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Services.Organization { @@ -12,7 +13,7 @@ internal OrganizationRolesService(JsonSerializer serializer, string uid, Paramet { if (string.IsNullOrEmpty(uid)) { - throw new ArgumentNullException("uid"); + throw new ArgumentNullException("uid", CSConstants.OrganizationUIDRequired); } this.ResourcePath = "organizations/{organization_uid}/roles"; diff --git a/Contentstack.Management.Core/Services/Organization/OrganizationStackService.cs b/Contentstack.Management.Core/Services/Organization/OrganizationStackService.cs index 21489ff..ef44af3 100644 --- a/Contentstack.Management.Core/Services/Organization/OrganizationStackService.cs +++ b/Contentstack.Management.Core/Services/Organization/OrganizationStackService.cs @@ -1,6 +1,7 @@ using System; using Contentstack.Management.Core.Queryable; using Newtonsoft.Json; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Services.Organization { @@ -13,7 +14,7 @@ internal OrganizationStackService(JsonSerializer serializer, string uid, Paramet if (string.IsNullOrEmpty(uid)) { - throw new ArgumentNullException("uid"); + throw new ArgumentNullException("uid", CSConstants.OrganizationUIDRequired); } this.ResourcePath = "/organizations/{organization_uid}/stacks"; this.AddPathResource("{organization_uid}", uid); diff --git a/Contentstack.Management.Core/Services/Organization/ResendInvitationService.cs b/Contentstack.Management.Core/Services/Organization/ResendInvitationService.cs index 64fac47..cd19ac0 100644 --- a/Contentstack.Management.Core/Services/Organization/ResendInvitationService.cs +++ b/Contentstack.Management.Core/Services/Organization/ResendInvitationService.cs @@ -1,5 +1,6 @@ using System; using Newtonsoft.Json; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Services.Organization { @@ -11,11 +12,11 @@ internal ResendInvitationService(JsonSerializer serializer, string uid, string s { if (string.IsNullOrEmpty(uid)) { - throw new ArgumentNullException("uid"); + throw new ArgumentNullException("uid", CSConstants.OrganizationUIDRequired); } if (string.IsNullOrEmpty(shareUid)) { - throw new ArgumentNullException("shareUid"); + throw new ArgumentNullException("shareUid", CSConstants.ShareUIDRequired); } this.ResourcePath = "/organizations/{organization_uid}/share/{share_uid}/resend_invitation"; this.AddPathResource("{organization_uid}", uid); diff --git a/Contentstack.Management.Core/Services/Organization/TransferOwnershipService.cs b/Contentstack.Management.Core/Services/Organization/TransferOwnershipService.cs index 217c721..be933ed 100644 --- a/Contentstack.Management.Core/Services/Organization/TransferOwnershipService.cs +++ b/Contentstack.Management.Core/Services/Organization/TransferOwnershipService.cs @@ -2,6 +2,7 @@ using System.Globalization; using System.IO; using Newtonsoft.Json; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Services.Organization { @@ -14,12 +15,12 @@ internal TransferOwnershipService(JsonSerializer serializer, string uid, string { if (string.IsNullOrEmpty(uid)) { - throw new ArgumentNullException("uid"); + throw new ArgumentNullException("uid", CSConstants.OrganizationUIDRequired); } if (string.IsNullOrEmpty(email)) { - throw new ArgumentNullException("email"); + throw new ArgumentNullException("email", CSConstants.EmailRequired); } this._email = email; this.ResourcePath = "/organizations/{organization_uid}/transfer-ownership"; diff --git a/Contentstack.Management.Core/Services/Organization/UserInvitationService.cs b/Contentstack.Management.Core/Services/Organization/UserInvitationService.cs index f81772c..7a5dcb5 100644 --- a/Contentstack.Management.Core/Services/Organization/UserInvitationService.cs +++ b/Contentstack.Management.Core/Services/Organization/UserInvitationService.cs @@ -5,6 +5,7 @@ using Contentstack.Management.Core.Models; using Contentstack.Management.Core.Queryable; using Newtonsoft.Json; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Services.Organization { @@ -19,12 +20,12 @@ internal UserInvitationService(JsonSerializer serializer, string uid, string htt { if (string.IsNullOrEmpty(uid)) { - throw new ArgumentNullException("uid"); + throw new ArgumentNullException("uid", CSConstants.OrganizationUIDRequired); } if (string.IsNullOrEmpty(httpMethod)) { - throw new ArgumentNullException("httpMethod"); + throw new ArgumentNullException("httpMethod", CSConstants.HTTPMethodRequired); } if (collection != null && collection.Count > 0) { diff --git a/Contentstack.Management.Core/Services/QueryService.cs b/Contentstack.Management.Core/Services/QueryService.cs index 029514a..751e7c4 100644 --- a/Contentstack.Management.Core/Services/QueryService.cs +++ b/Contentstack.Management.Core/Services/QueryService.cs @@ -1,6 +1,7 @@ using System; using Newtonsoft.Json; using Contentstack.Management.Core.Queryable; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Services { @@ -13,13 +14,13 @@ internal QueryService(Core.Models.Stack stack, ParameterCollection collection, s { if (string.IsNullOrEmpty(resourcePath)) { - throw new ArgumentNullException("resourcePath"); + throw new ArgumentNullException("resourcePath", CSConstants.ResourcePathRequired); } this.ResourcePath = resourcePath; if (string.IsNullOrEmpty(stack.APIKey)) { - throw new ArgumentNullException("stack", "API Key should be present."); + throw new ArgumentNullException("stack", CSConstants.MissingAPIKey); } if (collection != null && collection.Count > 0) { diff --git a/Contentstack.Management.Core/Services/Stack/StackCreateUpdateService.cs b/Contentstack.Management.Core/Services/Stack/StackCreateUpdateService.cs index bf9cce8..724c0bd 100644 --- a/Contentstack.Management.Core/Services/Stack/StackCreateUpdateService.cs +++ b/Contentstack.Management.Core/Services/Stack/StackCreateUpdateService.cs @@ -2,6 +2,7 @@ using System.Globalization; using System.IO; using Newtonsoft.Json; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Services.Stack { @@ -35,18 +36,18 @@ internal StackCreateUpdateService( { if (masterLocale == null) { - throw new ArgumentNullException("masterLocale", "Should have Master Locale while creating the Stack."); + throw new ArgumentNullException("masterLocale", CSConstants.MasterLocaleRequired); } if (_name == null) { - throw new ArgumentNullException("name", "Name for stack is mandatory while creating the Stack."); + throw new ArgumentNullException("name", CSConstants.StackNameRequired); } this.Headers.Add("organization_uid", organizationUid); this.HttpMethod = "POST"; } else { - throw new ArgumentNullException("stack", "Should have API Key or Organization UID to perform this operation."); + throw new ArgumentNullException("stack", CSConstants.APIKeyOrOrgUIDRequired); } } diff --git a/Contentstack.Management.Core/Services/Stack/StackOwnershipService.cs b/Contentstack.Management.Core/Services/Stack/StackOwnershipService.cs index 206d390..4d4ca43 100644 --- a/Contentstack.Management.Core/Services/Stack/StackOwnershipService.cs +++ b/Contentstack.Management.Core/Services/Stack/StackOwnershipService.cs @@ -2,6 +2,7 @@ using System.Globalization; using System.IO; using Newtonsoft.Json; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Services.Stack { @@ -15,12 +16,12 @@ internal StackOwnershipService(JsonSerializer serializer, Core.Models.Stack stac { if (string.IsNullOrEmpty(stack.APIKey)) { - throw new ArgumentNullException("stack", "API Key should be present."); + throw new ArgumentNullException("stack", CSConstants.MissingAPIKey); } if (string.IsNullOrEmpty(email)) { - throw new ArgumentNullException("email"); + throw new ArgumentNullException("email", CSConstants.EmailRequired); } this._email = email; this.ResourcePath = "stacks/transfer_ownership"; diff --git a/Contentstack.Management.Core/Services/Stack/StackSettingsService.cs b/Contentstack.Management.Core/Services/Stack/StackSettingsService.cs index 225f6b1..8855cc7 100644 --- a/Contentstack.Management.Core/Services/Stack/StackSettingsService.cs +++ b/Contentstack.Management.Core/Services/Stack/StackSettingsService.cs @@ -3,6 +3,7 @@ using System.IO; using Contentstack.Management.Core.Models; using Newtonsoft.Json; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Services.Stack { @@ -16,7 +17,7 @@ internal StackSettingsService(JsonSerializer serializer, Core.Models.Stack stack { if (stack.APIKey == null) { - throw new ArgumentNullException("stack", "API Key should be present."); + throw new ArgumentNullException("stack", CSConstants.MissingAPIKey); } ResourcePath = "stacks/settings"; HttpMethod = method; diff --git a/Contentstack.Management.Core/Services/Stack/StackShareService.cs b/Contentstack.Management.Core/Services/Stack/StackShareService.cs index 3b65cfd..be81a5a 100644 --- a/Contentstack.Management.Core/Services/Stack/StackShareService.cs +++ b/Contentstack.Management.Core/Services/Stack/StackShareService.cs @@ -4,6 +4,7 @@ using System.IO; using Contentstack.Management.Core.Models; using Newtonsoft.Json; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Services.Stack { @@ -17,7 +18,7 @@ internal StackShareService(JsonSerializer serializer, Core.Models.Stack stack) : { if (string.IsNullOrEmpty(stack.APIKey)) { - throw new ArgumentNullException("stack", "API Key should be present."); + throw new ArgumentNullException("stack", CSConstants.MissingAPIKey); } HttpMethod = "POST"; } diff --git a/Contentstack.Management.Core/Services/Stack/UpdateUserRoleService.cs b/Contentstack.Management.Core/Services/Stack/UpdateUserRoleService.cs index cab8836..3d92b66 100644 --- a/Contentstack.Management.Core/Services/Stack/UpdateUserRoleService.cs +++ b/Contentstack.Management.Core/Services/Stack/UpdateUserRoleService.cs @@ -5,6 +5,7 @@ using Contentstack.Management.Core.Models; using Contentstack.Management.Core.Queryable; using Newtonsoft.Json; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Services.Stack { @@ -18,12 +19,12 @@ internal UpdateUserRoleService(JsonSerializer serializer, Core.Models.Stack stac { if (userInvitation == null) { - throw new ArgumentNullException("userInvitation", "Uid and roles should be present."); + throw new ArgumentNullException("userInvitation", CSConstants.UserInvitationDetailsRequired); } if (stack.APIKey == null) { - throw new ArgumentNullException("stack", "API Key should be present."); + throw new ArgumentNullException("stack", CSConstants.MissingAPIKey); } this.ResourcePath = "stacks/users/roles"; this.HttpMethod = "POST"; diff --git a/Contentstack.Management.Core/Services/User/ForgotPasswordService.cs b/Contentstack.Management.Core/Services/User/ForgotPasswordService.cs index 356914a..e16f9f0 100644 --- a/Contentstack.Management.Core/Services/User/ForgotPasswordService.cs +++ b/Contentstack.Management.Core/Services/User/ForgotPasswordService.cs @@ -2,6 +2,7 @@ using System.Globalization; using System.IO; using Newtonsoft.Json; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Services.User { @@ -13,7 +14,7 @@ internal ForgotPasswordService(JsonSerializer serializer, string email): base (s { if (string.IsNullOrEmpty(email)) { - throw new ArgumentNullException("email"); + throw new ArgumentNullException("email", CSConstants.EmailRequired); } _email = email; diff --git a/Contentstack.Management.Core/Services/User/LoginService.cs b/Contentstack.Management.Core/Services/User/LoginService.cs index 9dbaf05..977926e 100644 --- a/Contentstack.Management.Core/Services/User/LoginService.cs +++ b/Contentstack.Management.Core/Services/User/LoginService.cs @@ -6,6 +6,7 @@ using Newtonsoft.Json.Linq; using OtpNet; using Contentstack.Management.Core.Http; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Services.User { @@ -24,7 +25,7 @@ internal LoginService(JsonSerializer serializer, ICredentials credentials, strin if (credentials == null) { - throw new ArgumentNullException("credentials"); + throw new ArgumentNullException("credentials", CSConstants.LoginCredentialsRequired); } _credentials = credentials; diff --git a/Contentstack.Management.Core/Services/User/LogoutService.cs b/Contentstack.Management.Core/Services/User/LogoutService.cs index 9576198..b9856a7 100644 --- a/Contentstack.Management.Core/Services/User/LogoutService.cs +++ b/Contentstack.Management.Core/Services/User/LogoutService.cs @@ -1,5 +1,6 @@ using System; using Newtonsoft.Json; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Services.User { @@ -15,7 +16,7 @@ public LogoutService(JsonSerializer serializer, string authtoken): base(serializ if (string.IsNullOrEmpty(authtoken)) { - throw new ArgumentNullException("authtoken"); + throw new ArgumentNullException("authtoken", CSConstants.AuthenticationTokenRequired); } _authtoken = authtoken; diff --git a/Contentstack.Management.Core/Services/User/ResetPasswordService.cs b/Contentstack.Management.Core/Services/User/ResetPasswordService.cs index dc900e4..799732a 100644 --- a/Contentstack.Management.Core/Services/User/ResetPasswordService.cs +++ b/Contentstack.Management.Core/Services/User/ResetPasswordService.cs @@ -2,6 +2,7 @@ using System.Globalization; using System.IO; using Newtonsoft.Json; +using Contentstack.Management.Core.Utils; namespace Contentstack.Management.Core.Services.User { @@ -15,15 +16,15 @@ internal ResetPasswordService(JsonSerializer serializer, string resetPasswordTok { if (string.IsNullOrEmpty(resetPasswordToken)) { - throw new ArgumentNullException("resetPasswordToken"); + throw new ArgumentNullException("resetPasswordToken", CSConstants.ResetPasswordTokenRequired); } if (string.IsNullOrEmpty(password)) { - throw new ArgumentNullException("password"); + throw new ArgumentNullException("password", CSConstants.NewPasswordRequired); } if (string.IsNullOrEmpty(confirmPassword)) { - throw new ArgumentNullException("confirmPassword"); + throw new ArgumentNullException("confirmPassword", CSConstants.PasswordMismatch); } _resetPasswordToken = resetPasswordToken; _password = password; diff --git a/Contentstack.Management.Core/Utils/CSConstants.cs b/Contentstack.Management.Core/Utils/CSConstants.cs index e7c2214..d967b74 100644 --- a/Contentstack.Management.Core/Utils/CSConstants.cs +++ b/Contentstack.Management.Core/Utils/CSConstants.cs @@ -13,15 +13,133 @@ public static class CSConstants #endregion #region Internal Message + + #region Authentication Messages internal const string YouAreLoggedIn = "You are already logged in."; - internal const string YouAreNotLoggedIn = "You are need to login."; + internal const string YouAreNotLoggedIn = "You are not logged in. Log in and try again."; + internal const string LoginCredentialsRequired = "Login credentials are required. Provide a valid email and password and try again."; + internal const string AuthenticationTokenRequired = "Authentication token is required to log out. Provide a valid token and try again."; + internal const string ResetPasswordTokenRequired = "Reset password token is required. Provide a valid token and try again."; + internal const string NewPasswordRequired = "New password is required. Provide a valid password and try again."; + internal const string PasswordMismatch = "New password and confirm password do not match. Enter the same password in both fields and try again."; + #endregion - internal const string MissingUID = "Uid should not be empty."; - internal const string MissingAPIKey = "API Key should not be empty."; - internal const string APIKey = "API Key should be empty."; + #region OAuth Messages + internal const string OAuthOptionsRequired = "OAuth options cannot be null."; + internal const string OAuthTokensRequired = "OAuth tokens cannot be null."; + internal const string AccessTokenRequired = "Access token cannot be null or empty."; + internal const string ClientIDRequired = "Client ID cannot be null or empty."; + #endregion + + #region HTTP Client Messages + internal const string HttpClientRequired = "HTTP client is required. Initialize the HTTP client and try again."; + #endregion + + #region API Key Messages + internal const string MissingAPIKey = "API Key is required. Provide a valid API Key and try again."; + internal const string InvalidAPIKey = "API Key is invalid. Provide a valid API Key and try again."; + internal const string APIKeyOrOrgUIDRequired = "API Key or Organization UID is required to perform this operation. Provide a valid value and try again."; + #endregion + + #region UID Messages + internal const string MissingUID = "UID is required. Provide a valid UID and try again."; + internal const string AssetUIDRequired = "Asset UID is required. Provide a valid Asset UID and try again."; + internal const string AuditLogUIDRequired = "Audit Log UID is required. Provide a valid Audit Log UID and try again."; + internal const string ExtensionUIDRequired = "Extension UID is required. Provide a valid Extension UID and try again."; + internal const string FolderUIDRequired = "Folder UID is required. Provide a valid Folder UID and try again."; + internal const string OrganizationUIDRequired = "Organization UID is required. Provide a valid Organization UID and try again."; + internal const string PublishQueueUIDRequired = "Publish Queue UID is required. Provide a valid Publish Queue UID and try again."; + internal const string ReleaseItemUIDRequired = "Release Item UID is required. Provide a valid Release Item UID and try again."; + internal const string VersionUIDRequired = "Version UID is required. Provide a valid Version UID and try again."; + internal const string ShareUIDRequired = "Share UID is required. Provide a valid Share UID and try again."; + internal const string ReleaseUIDRequired = "Release UID is required. Provide a valid Release UID and try again."; + internal const string JobIDRequired = "Job ID is required to fetch the bulk job status. Provide a valid Job ID and try again."; + #endregion + #region Operation Not Allowed Messages + internal const string AssetAlreadyExists = "An asset with this unique ID already exists. Use a different ID and try again."; + internal const string OperationNotAllowedOnModel = "Operation not allowed on this model. Update your request and try again."; + internal const string OperationNotAllowedOnAuditLogs = "Operation not allowed on audit logs. Update your request and try again."; + internal const string OperationNotAllowedOnExtension = "Operation not allowed on extension. Update your request and try again."; + internal const string OperationNotAllowedOnFolder = "Operation not allowed on folder. Update your request and try again."; + internal const string OperationNotAllowedOnPublishQueue = "Operation not allowed on publish queue. Update your request and try again."; + internal const string OperationNotAllowedForVersion = "Operation not allowed for this Version."; + #endregion + + #region File and Upload Messages + internal const string FileNameRequired = "File Name is required. Provide a valid File Name and try again."; + internal const string UploadContentRequired = "Upload content is required. Provide valid upload content and try again."; + internal const string WidgetTitleRequired = "Widget Title is required. Provide a valid Widget Title and try again."; + #endregion + + #region Field and Model Messages + internal const string FieldNameRequired = "Field Name is required for this service. Provide a valid Field Name and try again."; + internal const string DataModelRequired = "Data Model is required for this service. Provide a valid Data Model and try again."; + internal const string ModelRequired = "Model is required for this service. Provide a valid Model and try again."; + #endregion + + #region Stack Messages + internal const string StackRequired = "Stack is required. Initialize the Stack and try again."; + internal const string StackSettingsRequired = "Stack settings are required. Provide valid Stack settings and try again."; + internal const string InvitationsRequired = "Invitations are required. Provide valid Invitations and try again."; + internal const string EmailRequired = "Email is required. Provide a valid Email and try again."; + internal const string StackNameInvalid = "Stack Name is invalid. Provide a valid Stack Name and try again."; + internal const string LocaleInvalid = "Locale is invalid for this Stack. Provide a valid Locale and try again."; + internal const string OrganizationUIDInvalid = "Organization UID is invalid. Provide a valid Organization UID and try again."; + internal const string MasterLocaleRequired = "Master Locale is required when creating the Stack. Provide a valid Master Locale and try again."; + internal const string StackNameRequired = "Stack Name is required when creating the Stack. Provide a valid Stack Name and try again."; + #endregion + + #region Release Messages + internal const string ReleaseNameInvalid = "Release Name is invalid. Provide a valid Release Name and try again."; + #endregion + + #region Workflow Messages + internal const string ContentTypeRequired = "Content Type is required. Provide a valid Content Type and try again."; + #endregion + + #region Query Messages + internal const string ResourcePathRequired = "Resource Path is required. Provide a valid Resource Path and try again."; + internal const string ParameterTypeNotSupported = "Parameter value type is not supported. Provide a supported value type and try again."; + #endregion + + #region Pipeline Messages + internal const string InnerHandlerNotSet = "Inner Handler is not set. Configure an Inner Handler and try again."; + internal const string ResponseNotReturned = "Response was not returned and no exception was thrown. Try again. Retry the request and check your network."; + #endregion + + #region Serializer Messages + internal const string JSONSerializerError = "JSON serializer error. Check the serializer configuration and try again."; + #endregion + + #region Bulk Operation Messages + internal const string BulkOperationStackRequired = "Bulk operation failed. Initialize the stack before running this operation."; + internal const string BulkAddDataRequired = "Data payload is required for bulk addition. Provide a list of objects and try again."; + internal const string BulkDeleteDetailsRequired = "Delete details are required for bulk deletion. Provide a list of objects with the required fields and try again."; + internal const string BulkPublishDetailsRequired = "Publish details are required for bulk publish. Provide a list of item objects with the required fields and try again."; + internal const string BulkUnpublishDetailsRequired = "Unpublish details are required for bulk unpublish. Provide a list of item objects with the required fields and try again."; + internal const string BulkReleaseItemsDataRequired = "Data payload is required for bulk release items. Provide a valid list of item objects and try again."; + internal const string BulkWorkflowUpdateBodyRequired = "Request body is required for bulk workflow update. Provide a valid payload with the required workflow fields and try again."; + internal const string BulkUpdateDataRequired = "Data payload is required for bulk update. Provide a valid list of item objects with the required fields and try again."; + #endregion + + #region Organization Messages + internal const string EmailsRequired = "Emails are required. Provide a list of valid email addresses and try again."; + internal const string UserInvitationDetailsRequired = "User invitation details are required. Provide a valid UID and Roles to update the user role and try again."; + #endregion + + #region Service Messages + internal const string FolderNameRequired = "Folder Name is required. Provide a valid Folder Name and try again."; + internal const string PublishDetailsRequired = "Publish details are required. Provide valid publish details and try again."; + internal const string HTTPMethodRequired = "HTTP method is required. Provide a valid HTTP method and try again."; + internal const string ReleaseItemsRequired = "Release Items are required. Provide valid Release Items and try again."; + #endregion + + #region Legacy Messages (for backward compatibility) internal const string RemoveUserEmailError = "Please enter email id to remove from org."; internal const string OrgShareUIDMissing = "Please enter share uid to resend invitation."; + internal const string APIKey = "API Key should be empty."; + #endregion #endregion } } diff --git a/Contentstack.Management.Core/contentstack.management.core.csproj b/Contentstack.Management.Core/contentstack.management.core.csproj index 0f14e8c..963402f 100644 --- a/Contentstack.Management.Core/contentstack.management.core.csproj +++ b/Contentstack.Management.Core/contentstack.management.core.csproj @@ -2,6 +2,7 @@ netstandard2.0;net471;net472; + 8.0 Contentstack Management Contentstack Copyright © 2012-2025 Contentstack. All Rights Reserved From d426b9671a3f45a3e99af0356dbe435e18c33305 Mon Sep 17 00:00:00 2001 From: reeshika-h Date: Wed, 7 Jan 2026 14:41:08 +0530 Subject: [PATCH 04/10] Refactor DefaultRetryPolicy and improve status code handling in RetryDelayCalculator --- .../Runtime/Pipeline/RetryHandler/DefaultRetryPolicy.cs | 6 ------ .../Runtime/Pipeline/RetryHandler/RetryDelayCalculator.cs | 2 +- .../contentstack.management.core.csproj | 1 - 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/DefaultRetryPolicy.cs b/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/DefaultRetryPolicy.cs index a494aba..fc436b2 100644 --- a/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/DefaultRetryPolicy.cs +++ b/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/DefaultRetryPolicy.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Net; using System.Net.Http; -using System.Net.Http.Headers; using Contentstack.Management.Core.Exceptions; using Contentstack.Management.Core.Runtime.Contexts; @@ -208,11 +207,6 @@ public TimeSpan GetNetworkRetryDelay(IRequestContext requestContext) requestContext.NetworkRetryCount, retryConfiguration); } - - // Internal methods for testing - internal bool CanRetryInternal(IExecutionContext executionContext) => CanRetry(executionContext); - internal bool RetryForExceptionInternal(IExecutionContext executionContext, Exception exception) => RetryForException(executionContext, exception); - internal bool RetryLimitExceededInternal(IExecutionContext executionContext) => RetryLimitExceeded(executionContext); } } diff --git a/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/RetryDelayCalculator.cs b/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/RetryDelayCalculator.cs index 32fbd86..ccb4030 100644 --- a/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/RetryDelayCalculator.cs +++ b/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/RetryDelayCalculator.cs @@ -111,7 +111,7 @@ public bool ShouldRetryHttpStatusCode(HttpStatusCode statusCode, RetryConfigurat } // Default retry condition: 429, 500, 502, 503, 504 - return statusCode == (HttpStatusCode)429 || // TooManyRequests + return statusCode == HttpStatusCode.TooManyRequests || // TooManyRequests statusCode == HttpStatusCode.InternalServerError || statusCode == HttpStatusCode.BadGateway || statusCode == HttpStatusCode.ServiceUnavailable || diff --git a/Contentstack.Management.Core/contentstack.management.core.csproj b/Contentstack.Management.Core/contentstack.management.core.csproj index 963402f..0f14e8c 100644 --- a/Contentstack.Management.Core/contentstack.management.core.csproj +++ b/Contentstack.Management.Core/contentstack.management.core.csproj @@ -2,7 +2,6 @@ netstandard2.0;net471;net472; - 8.0 Contentstack Management Contentstack Copyright © 2012-2025 Contentstack. All Rights Reserved From 70cd9fade42de02b8162ccba99f262afaaba5c6a Mon Sep 17 00:00:00 2001 From: reeshika-h Date: Wed, 7 Jan 2026 14:42:34 +0530 Subject: [PATCH 05/10] Update retry condition handling in RetryDelayCalculator to include additional HTTP status codes --- .../Runtime/Pipeline/RetryHandler/RetryDelayCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/RetryDelayCalculator.cs b/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/RetryDelayCalculator.cs index ccb4030..784a409 100644 --- a/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/RetryDelayCalculator.cs +++ b/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/RetryDelayCalculator.cs @@ -111,7 +111,7 @@ public bool ShouldRetryHttpStatusCode(HttpStatusCode statusCode, RetryConfigurat } // Default retry condition: 429, 500, 502, 503, 504 - return statusCode == HttpStatusCode.TooManyRequests || // TooManyRequests + return statusCode == HttpStatusCode.TooManyRequests || statusCode == HttpStatusCode.InternalServerError || statusCode == HttpStatusCode.BadGateway || statusCode == HttpStatusCode.ServiceUnavailable || From b9257eb4d9384908bb9f22cf8633712701b71413 Mon Sep 17 00:00:00 2001 From: reeshika-h Date: Wed, 7 Jan 2026 14:43:50 +0530 Subject: [PATCH 06/10] Update copyright year in LICENSE file to 2026 --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 3851325..4ea4612 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2012-2025 Contentstack +Copyright (c) 2012-2026 Contentstack Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 88d674cb5529704f7a45d3d41bdc4a65affe0cc9 Mon Sep 17 00:00:00 2001 From: Nadeem Patwekar Date: Mon, 19 Jan 2026 15:05:28 +0530 Subject: [PATCH 07/10] Refactor retry policy implementation to improve exception handling and retry logic across various scenarios. Update method access modifiers for better visibility and consistency. Enhance unit tests for comprehensive coverage of retry conditions. --- .../Mokes/CustomJsonConverter.cs | 4 +- .../Mokes/MockRetryPolicy.cs | 6 +- .../RetryHandler/DefaultRetryPolicyTest.cs | 6 +- .../RetryHandlerIntegrationTest.cs | 19 ++- .../Pipeline/RetryHandler/RetryHandlerTest.cs | 32 ++-- .../Utils/ContentstackUtilitiesTest.cs | 7 +- .../RetryHandler/DefaultRetryPolicy.cs | 56 ++++--- .../RetryHandler/RetryDelayCalculator.cs | 2 +- .../Pipeline/RetryHandler/RetryHandler.cs | 10 ++ .../Pipeline/RetryHandler/RetryPolicy.cs | 6 +- .../contentstack.management.core.csproj | 142 +++++++++--------- 11 files changed, 162 insertions(+), 128 deletions(-) diff --git a/Contentstack.Management.Core.Unit.Tests/Mokes/CustomJsonConverter.cs b/Contentstack.Management.Core.Unit.Tests/Mokes/CustomJsonConverter.cs index fd55e23..22646e9 100644 --- a/Contentstack.Management.Core.Unit.Tests/Mokes/CustomJsonConverter.cs +++ b/Contentstack.Management.Core.Unit.Tests/Mokes/CustomJsonConverter.cs @@ -9,7 +9,7 @@ public class CustomJsonConverter : JsonConverter { public override bool CanConvert(Type objectType) { - throw new NotImplementedException(); + return false; // Mock converter - not actually used for conversion } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) @@ -28,7 +28,7 @@ public class CustomConverter : JsonConverter { public override bool CanConvert(Type objectType) { - throw new NotImplementedException(); + return false; // Mock converter - not actually used for conversion } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) diff --git a/Contentstack.Management.Core.Unit.Tests/Mokes/MockRetryPolicy.cs b/Contentstack.Management.Core.Unit.Tests/Mokes/MockRetryPolicy.cs index 41b07dd..fa89815 100644 --- a/Contentstack.Management.Core.Unit.Tests/Mokes/MockRetryPolicy.cs +++ b/Contentstack.Management.Core.Unit.Tests/Mokes/MockRetryPolicy.cs @@ -23,19 +23,19 @@ public MockRetryPolicy() RetryLimit = 5; } - protected override bool RetryForException(IExecutionContext executionContext, Exception exception) + public override bool RetryForException(IExecutionContext executionContext, Exception exception) { LastException = exception; RetryCallCount++; return ShouldRetryValue; } - protected override bool CanRetry(IExecutionContext executionContext) + public override bool CanRetry(IExecutionContext executionContext) { return CanRetryValue; } - protected override bool RetryLimitExceeded(IExecutionContext executionContext) + public override bool RetryLimitExceeded(IExecutionContext executionContext) { return RetryLimitExceededValue; } diff --git a/Contentstack.Management.Core.Unit.Tests/Runtime/Pipeline/RetryHandler/DefaultRetryPolicyTest.cs b/Contentstack.Management.Core.Unit.Tests/Runtime/Pipeline/RetryHandler/DefaultRetryPolicyTest.cs index 3a7ef45..d3bc149 100644 --- a/Contentstack.Management.Core.Unit.Tests/Runtime/Pipeline/RetryHandler/DefaultRetryPolicyTest.cs +++ b/Contentstack.Management.Core.Unit.Tests/Runtime/Pipeline/RetryHandler/DefaultRetryPolicyTest.cs @@ -186,9 +186,13 @@ public void RetryForException_HttpError_Exceeds_RetryLimit_Returns_False() }; var policy = new DefaultRetryPolicy(config); var context = CreateExecutionContext(); - context.RequestContext.HttpRetryCount = 1; var exception = MockNetworkErrorGenerator.CreateContentstackErrorException(HttpStatusCode.InternalServerError); + context.RequestContext.HttpRetryCount = 0; + var result0 = policy.RetryForException(context, exception); + Assert.IsTrue(result0); + + context.RequestContext.HttpRetryCount = 1; var result = policy.RetryForException(context, exception); Assert.IsFalse(result); } diff --git a/Contentstack.Management.Core.Unit.Tests/Runtime/Pipeline/RetryHandler/RetryHandlerIntegrationTest.cs b/Contentstack.Management.Core.Unit.Tests/Runtime/Pipeline/RetryHandler/RetryHandlerIntegrationTest.cs index 142b479..a54aac8 100644 --- a/Contentstack.Management.Core.Unit.Tests/Runtime/Pipeline/RetryHandler/RetryHandlerIntegrationTest.cs +++ b/Contentstack.Management.Core.Unit.Tests/Runtime/Pipeline/RetryHandler/RetryHandlerIntegrationTest.cs @@ -2,8 +2,11 @@ using System.Net; using System.Net.Sockets; using Contentstack.Management.Core; +using Contentstack.Management.Core.Exceptions; +using Contentstack.Management.Core.Internal; using Contentstack.Management.Core.Runtime.Contexts; using Contentstack.Management.Core.Runtime.Pipeline.RetryHandler; +using RetryHandlerClass = Contentstack.Management.Core.Runtime.Pipeline.RetryHandler.RetryHandler; using Contentstack.Management.Core.Unit.Tests.Mokes; using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Threading.Tasks; @@ -36,7 +39,7 @@ public async Task EndToEnd_NetworkError_Retries_And_Succeeds() NetworkBackoffStrategy = BackoffStrategy.Exponential }; var policy = new DefaultRetryPolicy(config); - var handler = new RetryHandler(policy); + var handler = new RetryHandlerClass(policy); var mockInnerHandler = new MockHttpHandlerWithRetries(); mockInnerHandler.AddFailuresThenSuccess(2, MockNetworkErrorGenerator.CreateSocketException(SocketError.ConnectionReset)); handler.InnerHandler = mockInnerHandler; @@ -64,7 +67,7 @@ public async Task EndToEnd_HttpError_Retries_And_Succeeds() } }; var policy = new DefaultRetryPolicy(config); - var handler = new RetryHandler(policy); + var handler = new RetryHandlerClass(policy); var mockInnerHandler = new MockHttpHandlerWithRetries(); mockInnerHandler.AddHttpErrorsThenSuccess(2, HttpStatusCode.InternalServerError); handler.InnerHandler = mockInnerHandler; @@ -93,7 +96,7 @@ public async Task EndToEnd_Mixed_Network_And_Http_Errors() RetryDelay = TimeSpan.FromMilliseconds(10) }; var policy = new DefaultRetryPolicy(config); - var handler = new RetryHandler(policy); + var handler = new RetryHandlerClass(policy); var mockInnerHandler = new MockHttpHandlerWithRetries(); mockInnerHandler.AddException(MockNetworkErrorGenerator.CreateSocketException(SocketError.ConnectionReset)); mockInnerHandler.AddResponse(HttpStatusCode.InternalServerError); @@ -120,7 +123,7 @@ public async Task EndToEnd_Respects_RetryConfiguration() RetryOnError = false }; var policy = new DefaultRetryPolicy(config); - var handler = new RetryHandler(policy); + var handler = new RetryHandlerClass(policy); var mockInnerHandler = new MockHttpHandlerWithRetries(); mockInnerHandler.AddException(MockNetworkErrorGenerator.CreateSocketException(SocketError.ConnectionReset)); handler.InnerHandler = mockInnerHandler; @@ -154,7 +157,7 @@ public async Task EndToEnd_ExponentialBackoff_Delays_Increase() NetworkBackoffStrategy = BackoffStrategy.Exponential }; var policy = new DefaultRetryPolicy(config); - var handler = new RetryHandler(policy); + var handler = new RetryHandlerClass(policy); var mockInnerHandler = new MockHttpHandlerWithRetries(); mockInnerHandler.AddFailuresThenSuccess(2, MockNetworkErrorGenerator.CreateSocketException(SocketError.ConnectionReset)); handler.InnerHandler = mockInnerHandler; @@ -179,7 +182,7 @@ public async Task EndToEnd_RetryLimit_Stops_Retries() RetryDelay = TimeSpan.FromMilliseconds(10) }; var policy = new DefaultRetryPolicy(config); - var handler = new RetryHandler(policy); + var handler = new RetryHandlerClass(policy); var mockInnerHandler = new MockHttpHandlerWithRetries(); mockInnerHandler.AddResponse(HttpStatusCode.TooManyRequests); mockInnerHandler.AddResponse(HttpStatusCode.TooManyRequests); @@ -214,7 +217,7 @@ public async Task EndToEnd_With_CustomRetryCondition() RetryCondition = (statusCode) => statusCode == HttpStatusCode.NotFound }; var policy = new DefaultRetryPolicy(config); - var handler = new RetryHandler(policy); + var handler = new RetryHandlerClass(policy); var mockInnerHandler = new MockHttpHandlerWithRetries(); mockInnerHandler.AddHttpErrorsThenSuccess(2, HttpStatusCode.NotFound); handler.InnerHandler = mockInnerHandler; @@ -242,7 +245,7 @@ public async Task EndToEnd_With_CustomBackoff() } }; var policy = new DefaultRetryPolicy(config); - var handler = new RetryHandler(policy); + var handler = new RetryHandlerClass(policy); var mockInnerHandler = new MockHttpHandlerWithRetries(); mockInnerHandler.AddHttpErrorsThenSuccess(2, HttpStatusCode.TooManyRequests); handler.InnerHandler = mockInnerHandler; diff --git a/Contentstack.Management.Core.Unit.Tests/Runtime/Pipeline/RetryHandler/RetryHandlerTest.cs b/Contentstack.Management.Core.Unit.Tests/Runtime/Pipeline/RetryHandler/RetryHandlerTest.cs index 3f6baa1..556fe71 100644 --- a/Contentstack.Management.Core.Unit.Tests/Runtime/Pipeline/RetryHandler/RetryHandlerTest.cs +++ b/Contentstack.Management.Core.Unit.Tests/Runtime/Pipeline/RetryHandler/RetryHandlerTest.cs @@ -3,8 +3,10 @@ using System.Net.Sockets; using Contentstack.Management.Core; using Contentstack.Management.Core.Exceptions; +using Contentstack.Management.Core.Internal; using Contentstack.Management.Core.Runtime.Contexts; using Contentstack.Management.Core.Runtime.Pipeline.RetryHandler; +using RetryHandlerClass = Contentstack.Management.Core.Runtime.Pipeline.RetryHandler.RetryHandler; using Contentstack.Management.Core.Unit.Tests.Mokes; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -35,7 +37,7 @@ public async Task InvokeAsync_Success_NoRetry() MaxNetworkRetries = 2 }; var policy = new DefaultRetryPolicy(config); - var handler = new RetryHandler(policy); + var handler = new RetryHandlerClass(policy); var mockInnerHandler = new MockHttpHandlerWithRetries(); mockInnerHandler.AddSuccessResponse(); handler.InnerHandler = mockInnerHandler; @@ -61,7 +63,7 @@ public async Task InvokeAsync_NetworkError_Retries_UpTo_MaxNetworkRetries() NetworkRetryDelay = TimeSpan.FromMilliseconds(10) }; var policy = new DefaultRetryPolicy(config); - var handler = new RetryHandler(policy); + var handler = new RetryHandlerClass(policy); var mockInnerHandler = new MockHttpHandlerWithRetries(); mockInnerHandler.AddFailuresThenSuccess(2, MockNetworkErrorGenerator.CreateSocketException(SocketError.ConnectionReset)); handler.InnerHandler = mockInnerHandler; @@ -86,7 +88,7 @@ public async Task InvokeAsync_NetworkError_Exceeds_MaxNetworkRetries_Throws() NetworkRetryDelay = TimeSpan.FromMilliseconds(10) }; var policy = new DefaultRetryPolicy(config); - var handler = new RetryHandler(policy); + var handler = new RetryHandlerClass(policy); var mockInnerHandler = new MockHttpHandlerWithRetries(); mockInnerHandler.AddException(MockNetworkErrorGenerator.CreateSocketException(SocketError.ConnectionReset)); mockInnerHandler.AddException(MockNetworkErrorGenerator.CreateSocketException(SocketError.ConnectionReset)); @@ -119,7 +121,7 @@ public async Task InvokeAsync_HttpError_429_Retries_UpTo_RetryLimit() RetryDelay = TimeSpan.FromMilliseconds(10) }; var policy = new DefaultRetryPolicy(config); - var handler = new RetryHandler(policy); + var handler = new RetryHandlerClass(policy); var mockInnerHandler = new MockHttpHandlerWithRetries(); mockInnerHandler.AddHttpErrorsThenSuccess(2, HttpStatusCode.TooManyRequests); handler.InnerHandler = mockInnerHandler; @@ -143,7 +145,7 @@ public async Task InvokeAsync_HttpError_500_Retries_UpTo_RetryLimit() RetryDelay = TimeSpan.FromMilliseconds(10) }; var policy = new DefaultRetryPolicy(config); - var handler = new RetryHandler(policy); + var handler = new RetryHandlerClass(policy); var mockInnerHandler = new MockHttpHandlerWithRetries(); mockInnerHandler.AddHttpErrorsThenSuccess(2, HttpStatusCode.InternalServerError); handler.InnerHandler = mockInnerHandler; @@ -166,7 +168,7 @@ public async Task InvokeAsync_HttpError_Exceeds_RetryLimit_Throws() RetryDelay = TimeSpan.FromMilliseconds(10) }; var policy = new DefaultRetryPolicy(config); - var handler = new RetryHandler(policy); + var handler = new RetryHandlerClass(policy); var mockInnerHandler = new MockHttpHandlerWithRetries(); mockInnerHandler.AddResponse(HttpStatusCode.TooManyRequests); mockInnerHandler.AddResponse(HttpStatusCode.TooManyRequests); @@ -201,7 +203,7 @@ public async Task InvokeAsync_NetworkError_Tracks_NetworkRetryCount() NetworkRetryDelay = TimeSpan.FromMilliseconds(10) }; var policy = new DefaultRetryPolicy(config); - var handler = new RetryHandler(policy); + var handler = new RetryHandlerClass(policy); var mockInnerHandler = new MockHttpHandlerWithRetries(); mockInnerHandler.AddFailuresThenSuccess(1, MockNetworkErrorGenerator.CreateSocketException(SocketError.ConnectionReset)); handler.InnerHandler = mockInnerHandler; @@ -223,7 +225,7 @@ public async Task InvokeAsync_HttpError_Tracks_HttpRetryCount() RetryDelay = TimeSpan.FromMilliseconds(10) }; var policy = new DefaultRetryPolicy(config); - var handler = new RetryHandler(policy); + var handler = new RetryHandlerClass(policy); var mockInnerHandler = new MockHttpHandlerWithRetries(); mockInnerHandler.AddHttpErrorsThenSuccess(1, HttpStatusCode.TooManyRequests); handler.InnerHandler = mockInnerHandler; @@ -249,7 +251,7 @@ public async Task InvokeAsync_NetworkError_Then_HttpError_Tracks_Both_Counts() RetryDelay = TimeSpan.FromMilliseconds(10) }; var policy = new DefaultRetryPolicy(config); - var handler = new RetryHandler(policy); + var handler = new RetryHandlerClass(policy); var mockInnerHandler = new MockHttpHandlerWithRetries(); mockInnerHandler.AddException(MockNetworkErrorGenerator.CreateSocketException(SocketError.ConnectionReset)); mockInnerHandler.AddResponse(HttpStatusCode.TooManyRequests); @@ -276,7 +278,7 @@ public async Task InvokeAsync_Applies_NetworkRetryDelay() NetworkBackoffStrategy = BackoffStrategy.Fixed }; var policy = new DefaultRetryPolicy(config); - var handler = new RetryHandler(policy); + var handler = new RetryHandlerClass(policy); var mockInnerHandler = new MockHttpHandlerWithRetries(); mockInnerHandler.AddFailuresThenSuccess(1, MockNetworkErrorGenerator.CreateSocketException(SocketError.ConnectionReset)); handler.InnerHandler = mockInnerHandler; @@ -304,7 +306,7 @@ public async Task InvokeAsync_Applies_HttpRetryDelay() } }; var policy = new DefaultRetryPolicy(config); - var handler = new RetryHandler(policy); + var handler = new RetryHandlerClass(policy); var mockInnerHandler = new MockHttpHandlerWithRetries(); mockInnerHandler.AddHttpErrorsThenSuccess(1, HttpStatusCode.TooManyRequests); handler.InnerHandler = mockInnerHandler; @@ -324,7 +326,7 @@ public async Task InvokeAsync_RequestId_Is_Generated() { var config = new RetryConfiguration(); var policy = new DefaultRetryPolicy(config); - var handler = new RetryHandler(policy); + var handler = new RetryHandlerClass(policy); var mockInnerHandler = new MockHttpHandlerWithRetries(); mockInnerHandler.AddSuccessResponse(); handler.InnerHandler = mockInnerHandler; @@ -341,7 +343,7 @@ public void InvokeSync_Success_NoRetry() { var config = new RetryConfiguration(); var policy = new DefaultRetryPolicy(config); - var handler = new RetryHandler(policy); + var handler = new RetryHandlerClass(policy); var mockInnerHandler = new MockHttpHandlerWithRetries(); mockInnerHandler.AddSuccessResponse(); handler.InnerHandler = mockInnerHandler; @@ -364,7 +366,7 @@ public void InvokeSync_NetworkError_Retries() NetworkRetryDelay = TimeSpan.FromMilliseconds(10) }; var policy = new DefaultRetryPolicy(config); - var handler = new RetryHandler(policy); + var handler = new RetryHandlerClass(policy); var mockInnerHandler = new MockHttpHandlerWithRetries(); mockInnerHandler.AddFailuresThenSuccess(2, MockNetworkErrorGenerator.CreateSocketException(SocketError.ConnectionReset)); handler.InnerHandler = mockInnerHandler; @@ -386,7 +388,7 @@ public void InvokeSync_HttpError_Retries() RetryDelay = TimeSpan.FromMilliseconds(10) }; var policy = new DefaultRetryPolicy(config); - var handler = new RetryHandler(policy); + var handler = new RetryHandlerClass(policy); var mockInnerHandler = new MockHttpHandlerWithRetries(); mockInnerHandler.AddHttpErrorsThenSuccess(2, HttpStatusCode.TooManyRequests); handler.InnerHandler = mockInnerHandler; diff --git a/Contentstack.Management.Core.Unit.Tests/Utils/ContentstackUtilitiesTest.cs b/Contentstack.Management.Core.Unit.Tests/Utils/ContentstackUtilitiesTest.cs index 6c21456..4d9dd04 100644 --- a/Contentstack.Management.Core.Unit.Tests/Utils/ContentstackUtilitiesTest.cs +++ b/Contentstack.Management.Core.Unit.Tests/Utils/ContentstackUtilitiesTest.cs @@ -93,7 +93,12 @@ public void Return_Query_Parameters_On_ParameterCollection() JObject q_obj = JObject.Parse("{ \"price_in_usd\": { \"$lt\": 600 } }"); param.AddQuery(q_obj); var result = ContentstackUtilities.GetQueryParameter(param); - Assert.AreEqual("include=type&limit=10&query=%7B%0D%0A%20%20%22price_in_usd%22%3A%20%7B%0D%0A%20%20%20%20%22%24lt%22%3A%20600%0D%0A%20%20%7D%0D%0A%7D", result); + // Normalize line endings for cross-platform compatibility (JObject.ToString() uses platform-specific line endings) + var expected = "include=type&limit=10&query=%7B%0D%0A%20%20%22price_in_usd%22%3A%20%7B%0D%0A%20%20%20%20%22%24lt%22%3A%20600%0D%0A%20%20%7D%0D%0A%7D"; + // Normalize both to use \n for comparison + var normalizedExpected = expected.Replace("%0D%0A", "%0A"); + var normalizedActual = result.Replace("%0D%0A", "%0A"); + Assert.AreEqual(normalizedExpected, normalizedActual); } [TestMethod] diff --git a/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/DefaultRetryPolicy.cs b/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/DefaultRetryPolicy.cs index fc436b2..6cfeefa 100644 --- a/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/DefaultRetryPolicy.cs +++ b/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/DefaultRetryPolicy.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using Contentstack.Management.Core.Exceptions; using Contentstack.Management.Core.Runtime.Contexts; @@ -42,7 +43,7 @@ internal DefaultRetryPolicy(RetryConfiguration config) delayCalculator = new RetryDelayCalculator(); } - protected override bool CanRetry(IExecutionContext executionContext) + public override bool CanRetry(IExecutionContext executionContext) { if (retryConfiguration != null) { @@ -51,7 +52,10 @@ protected override bool CanRetry(IExecutionContext executionContext) return RetryOnError; } - protected override bool RetryForException(IExecutionContext executionContext, Exception exception) + public override bool RetryForException( + IExecutionContext executionContext, + Exception exception + ) { if (retryConfiguration == null) { @@ -61,44 +65,48 @@ protected override bool RetryForException(IExecutionContext executionContext, Ex var requestContext = executionContext.RequestContext; - // Check for network errors - var networkErrorInfo = networkErrorDetector.IsTransientNetworkError(exception); - if (networkErrorInfo != null) + // Check for HTTP errors (ContentstackErrorException) FIRST + // This must come before network error check because ContentstackErrorException with 5xx + // can be detected as network errors, but we want to use HTTP retry logic and limits + if (exception is ContentstackErrorException contentstackException) { - if (networkErrorDetector.ShouldRetryNetworkError(networkErrorInfo, retryConfiguration)) + // Check if HTTP retry limit exceeded first (applies to all HTTP errors) + // HttpRetryCount is the number of retries already attempted + // RetryLimit is the maximum number of retries allowed + // So when HttpRetryCount >= RetryLimit, we've reached or exceeded the limit + // IMPORTANT: Check this BEFORE checking if it's a retryable error to ensure limit is respected + // This matches the pattern used in ShouldRetryHttpStatusCode method + if (requestContext.HttpRetryCount >= this.RetryLimit) { - // Check if network retry limit exceeded - if (requestContext.NetworkRetryCount >= retryConfiguration.MaxNetworkRetries) - { - return false; - } - return true; + return false; } - } - // Check for HTTP errors (ContentstackErrorException) - if (exception is ContentstackErrorException contentstackException) - { // Check if it's a server error (5xx) that should be retried if (contentstackException.StatusCode >= HttpStatusCode.InternalServerError && contentstackException.StatusCode <= HttpStatusCode.GatewayTimeout) { if (retryConfiguration.RetryOnHttpServerError) { - // Check if HTTP retry limit exceeded - if (requestContext.HttpRetryCount >= retryConfiguration.RetryLimit) - { - return false; - } return true; } + // If RetryOnHttpServerError is false, fall through to check custom retry condition } // Check custom retry condition if (delayCalculator.ShouldRetryHttpStatusCode(contentstackException.StatusCode, retryConfiguration)) { - // Check if HTTP retry limit exceeded - if (requestContext.HttpRetryCount >= retryConfiguration.RetryLimit) + return true; + } + } + + // Check for network errors (only if not already handled as HTTP error) + var networkErrorInfo = networkErrorDetector.IsTransientNetworkError(exception); + if (networkErrorInfo != null) + { + if (networkErrorDetector.ShouldRetryNetworkError(networkErrorInfo, retryConfiguration)) + { + // Check if network retry limit exceeded + if (requestContext.NetworkRetryCount >= retryConfiguration.MaxNetworkRetries) { return false; } @@ -109,7 +117,7 @@ protected override bool RetryForException(IExecutionContext executionContext, Ex return false; } - protected override bool RetryLimitExceeded(IExecutionContext executionContext) + public override bool RetryLimitExceeded(IExecutionContext executionContext) { var requestContext = executionContext.RequestContext; diff --git a/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/RetryDelayCalculator.cs b/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/RetryDelayCalculator.cs index 784a409..7254ede 100644 --- a/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/RetryDelayCalculator.cs +++ b/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/RetryDelayCalculator.cs @@ -111,7 +111,7 @@ public bool ShouldRetryHttpStatusCode(HttpStatusCode statusCode, RetryConfigurat } // Default retry condition: 429, 500, 502, 503, 504 - return statusCode == HttpStatusCode.TooManyRequests || + return statusCode == (HttpStatusCode)429 || statusCode == HttpStatusCode.InternalServerError || statusCode == HttpStatusCode.BadGateway || statusCode == HttpStatusCode.ServiceUnavailable || diff --git a/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/RetryHandler.cs b/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/RetryHandler.cs index 3f44e78..5a8a204 100644 --- a/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/RetryHandler.cs +++ b/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/RetryHandler.cs @@ -50,6 +50,11 @@ public override async Task InvokeAsync(IExecutionContext executionContext, await Task.Delay(delay); continue; } + else + { + // Retry limit exceeded or not retryable - throw exception + throw ContentstackErrorException.CreateException(contentstackResponse.ResponseBody); + } } return response; @@ -126,6 +131,11 @@ public override void InvokeSync(IExecutionContext executionContext, bool addAcce System.Threading.Tasks.Task.Delay(delay).Wait(); continue; } + else + { + // Retry limit exceeded or not retryable - throw exception + throw ContentstackErrorException.CreateException(contentstackResponse.ResponseBody); + } } return; diff --git a/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/RetryPolicy.cs b/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/RetryPolicy.cs index 640a05e..9bbe76e 100644 --- a/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/RetryPolicy.cs +++ b/Contentstack.Management.Core/Runtime/Pipeline/RetryHandler/RetryPolicy.cs @@ -26,11 +26,11 @@ public bool Retry(IExecutionContext executionContext, Exception exception) return false; } - protected abstract bool RetryForException(IExecutionContext excutionContext, Exception exception); + public abstract bool RetryForException(IExecutionContext excutionContext, Exception exception); - protected abstract bool CanRetry(IExecutionContext excutionContext); + public abstract bool CanRetry(IExecutionContext excutionContext); - protected abstract bool RetryLimitExceeded(IExecutionContext excutionContext); + public abstract bool RetryLimitExceeded(IExecutionContext excutionContext); internal abstract void WaitBeforeRetry(IExecutionContext executionContext); } } diff --git a/Contentstack.Management.Core/contentstack.management.core.csproj b/Contentstack.Management.Core/contentstack.management.core.csproj index 0f14e8c..568fa69 100644 --- a/Contentstack.Management.Core/contentstack.management.core.csproj +++ b/Contentstack.Management.Core/contentstack.management.core.csproj @@ -1,70 +1,72 @@ - - - - netstandard2.0;net471;net472; - Contentstack Management - Contentstack - Copyright © 2012-2025 Contentstack. All Rights Reserved - .NET SDK for the Contentstack Content Management API. - Contentstack - contentstack.management.csharp - LICENSE.txt - https://github.com/contentstack/contentstack-management-dotnet - README.md - Contentstack management API - $(Version) - $(Version) - .NET SDK for the Contentstack Content Delivery API. - true - ../CSManagementSDK.snk - - - - - - - - - - - - - - - CHANGELOG.md - - - README.md - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + netstandard2.0;net471;net472; + 8.0 + enable + Contentstack Management + Contentstack + Copyright © 2012-2025 Contentstack. All Rights Reserved + .NET SDK for the Contentstack Content Management API. + Contentstack + contentstack.management.csharp + LICENSE.txt + https://github.com/contentstack/contentstack-management-dotnet + README.md + Contentstack management API + $(Version) + $(Version) + .NET SDK for the Contentstack Content Delivery API. + true + ../CSManagementSDK.snk + + + + + + + + + + + + + + + CHANGELOG.md + + + README.md + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 01fcc873a41fbf278f589b3f395e5969a80d8dd6 Mon Sep 17 00:00:00 2001 From: Nadeem Patwekar Date: Mon, 19 Jan 2026 16:23:57 +0530 Subject: [PATCH 08/10] Update project file to enable C# 8.0 language features and nullable reference types --- .../contentstack.management.core.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Contentstack.Management.Core/contentstack.management.core.csproj b/Contentstack.Management.Core/contentstack.management.core.csproj index 0381bbe..32cb066 100644 --- a/Contentstack.Management.Core/contentstack.management.core.csproj +++ b/Contentstack.Management.Core/contentstack.management.core.csproj @@ -2,6 +2,8 @@ netstandard2.0;net471;net472; + 8.0 + enable Contentstack Management Contentstack Copyright © 2012-2026 Contentstack. All Rights Reserved From 850b63744cd61f38aa900ab3037aabb3ac102e89 Mon Sep 17 00:00:00 2001 From: reeshika-h Date: Mon, 19 Jan 2026 16:36:43 +0530 Subject: [PATCH 09/10] Update version to 0.5.1 and enhance error messages in the changelog --- CHANGELOG.md | 5 +++++ Directory.Build.props | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80c5f3e..b841dba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ # Changelog + +## [v0.5.1](https://github.com/contentstack/contentstack-management-dotnet/tree/v0.5.1) + - Enhancement + - Improved error messages + ## [v0.5.0](https://github.com/contentstack/contentstack-management-dotnet/tree/v0.5.0) - Feat - **Variant Group Management**: Added comprehensive support for variant group operations diff --git a/Directory.Build.props b/Directory.Build.props index 2b4e7f1..8de99b3 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,5 @@ - 0.5.0 + 0.5.1 From 47383af40a202721dffda9763f0a7091b96cae14 Mon Sep 17 00:00:00 2001 From: reeshika-h Date: Mon, 19 Jan 2026 16:42:33 +0530 Subject: [PATCH 10/10] Update version to 0.6.0 and refactor retry policy implementation for improved exception handling and retry logic --- CHANGELOG.md | 3 ++- Directory.Build.props | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b841dba..e674e4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ # Changelog -## [v0.5.1](https://github.com/contentstack/contentstack-management-dotnet/tree/v0.5.1) +## [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 - Improved error messages ## [v0.5.0](https://github.com/contentstack/contentstack-management-dotnet/tree/v0.5.0) diff --git a/Directory.Build.props b/Directory.Build.props index 8de99b3..5977029 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,5 @@ - 0.5.1 + 0.6.0