diff --git a/src/Teapot.Web.Tests/TestCase.cs b/src/Teapot.Web.Tests/TestCase.cs index c491d4e..8c9f6dc 100644 --- a/src/Teapot.Web.Tests/TestCase.cs +++ b/src/Teapot.Web.Tests/TestCase.cs @@ -1,24 +1,21 @@ using System.Text.Json.Serialization; +using Teapot.Web.Models; namespace Teapot.Web.Tests; -public class TestCase +public class TestCase(int code, string description, string? body, TeapotStatusCodeMetadata teapotStatusCodeMetadata) { - public TestCase(int code, string description, string? body) - { - Code = code; - Description = description; - Body = body ?? $"{Code} {Description}"; - } - [JsonPropertyName("code")] - public int Code { get; } + public int Code => code; [JsonPropertyName("description")] - public string Description { get; } + public string Description => description; + + [JsonIgnore] + public string? Body => body ?? $"{Code} {Description}"; [JsonIgnore] - public string? Body { get; } + public TeapotStatusCodeMetadata TeapotStatusCodeMetadata { get; } = teapotStatusCodeMetadata; public override bool Equals(object? obj) => obj is TestCase code && Code == code.Code; diff --git a/src/Teapot.Web.Tests/TestCases.cs b/src/Teapot.Web.Tests/TestCases.cs index 567a872..dbe51b5 100644 --- a/src/Teapot.Web.Tests/TestCases.cs +++ b/src/Teapot.Web.Tests/TestCases.cs @@ -35,11 +35,19 @@ public class TestCases private static TestCase Map(HttpStatusCode code) { int key = (int)code; - return new(key, All[key].Description, All[key].Body); + return new(key, All[key].Description, All[key].Body, All[key]); } private static TestCase Map(KeyValuePair code) { - return new(code.Key, code.Value.Description, code.Value.Body); + return new(code.Key, code.Value.Description, code.Value.Body, code.Value); } + + + private static readonly HttpStatusCode[] RetryAfterStatusCodes = [MovedPermanently, TooManyRequests, ServiceUnavailable]; + + public static IEnumerable StatusCodesWithRetryAfter => + All + .Where(x => RetryAfterStatusCodes.Contains((HttpStatusCode)x.Key)) + .Select(Map); } diff --git a/src/Teapot.Web.Tests/UnitTests/CustomHttpStatusCodeResultTests.cs b/src/Teapot.Web.Tests/UnitTests/CustomHttpStatusCodeResultTests.cs index dfd88e1..c40f485 100644 --- a/src/Teapot.Web.Tests/UnitTests/CustomHttpStatusCodeResultTests.cs +++ b/src/Teapot.Web.Tests/UnitTests/CustomHttpStatusCodeResultTests.cs @@ -1,8 +1,8 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; using System.Text.Json; -using Teapot.Web.Models; namespace Teapot.Web.Tests.UnitTests; @@ -36,71 +36,78 @@ public void Setup() } [TestCaseSource(typeof(TestCases), nameof(TestCases.StatusCodesAll))] - public async Task Response_Is_Correct(TestCase testCase) { - TeapotStatusCodeMetadata statusCodeResult = new() - { - Description = testCase.Description - }; - - CustomHttpStatusCodeResult target = new(testCase.Code, statusCodeResult, null, null, new()); + public async Task Response_Is_Correct(TestCase testCase) + { + CustomHttpStatusCodeResult target = new(testCase.Code, testCase.TeapotStatusCodeMetadata, null, null, []); await target.ExecuteAsync(_httpContext); - Assert.Multiple(() => { + Assert.Multiple(() => + { Assert.That(_httpContext.Response.StatusCode, Is.EqualTo(testCase.Code)); - Assert.That(_httpContext.Response.ContentType, Is.EqualTo("text/plain")); + Assert.That(_httpContext.Response.ContentType, Is.EqualTo(testCase.TeapotStatusCodeMetadata.ExcludeBody ? null : "text/plain")); Assert.That(_httpResponseFeature.ReasonPhrase, Is.EqualTo(testCase.Description)); }); - _body.Position = 0; - StreamReader sr = new(_body); - string body = sr.ReadToEnd(); - Assert.Multiple(() => { - Assert.That(body, Is.EqualTo(testCase.ToString())); - Assert.That(_httpContext.Response.ContentLength, Is.EqualTo(body.Length)); - }); - } - [TestCaseSource(typeof(TestCases), nameof(TestCases.StatusCodesAll))] - public async Task Response_Json_Is_Correct(TestCase testCase) { - TeapotStatusCodeMetadata statusCodeResult = new() + if (testCase.TeapotStatusCodeMetadata.ExcludeBody) { - Description = testCase.Description - }; + Assert.That(_httpContext.Response.ContentLength, Is.Null); + } + else + { + _body.Position = 0; + StreamReader sr = new(_body); + string body = sr.ReadToEnd(); + Assert.Multiple(() => + { + Assert.That(body, Is.EqualTo(testCase.Body)); + Assert.That(_httpContext.Response.ContentLength, Is.EqualTo(body?.Length)); + }); + } + } - CustomHttpStatusCodeResult target = new(testCase.Code, statusCodeResult, null, null, new()); + [TestCaseSource(typeof(TestCases), nameof(TestCases.StatusCodesAll))] + public async Task Response_Json_Is_Correct(TestCase testCase) + { + CustomHttpStatusCodeResult target = new(testCase.Code, testCase.TeapotStatusCodeMetadata, null, null, []); _httpContext.Request.Headers.Accept = "application/json"; await target.ExecuteAsync(_httpContext); - Assert.Multiple(() => { + Assert.Multiple(() => + { Assert.That(_httpContext.Response.StatusCode, Is.EqualTo(testCase.Code)); - Assert.That(_httpContext.Response.ContentType, Is.EqualTo("application/json")); + Assert.That(_httpContext.Response.ContentType, Is.EqualTo(testCase.TeapotStatusCodeMetadata.ExcludeBody ? null : "application/json")); Assert.That(_httpResponseFeature.ReasonPhrase, Is.EqualTo(testCase.Description)); }); - _body.Position = 0; - StreamReader sr = new(_body); - string body = sr.ReadToEnd(); - string expectedBody = JsonSerializer.Serialize(testCase); - Assert.Multiple(() => { - Assert.That(body, Is.EqualTo(expectedBody)); - Assert.That(_httpContext.Response.ContentLength, Is.EqualTo(body.Length)); - }); + if (testCase.TeapotStatusCodeMetadata.ExcludeBody) + { + Assert.That(_httpContext.Response.ContentLength, Is.Null); + } + else + { + _body.Position = 0; + StreamReader sr = new(_body); + string body = sr.ReadToEnd(); + string expectedBody = JsonSerializer.Serialize(new { code = testCase.Code, description = testCase.TeapotStatusCodeMetadata.Body ?? testCase.TeapotStatusCodeMetadata.Description }); + Assert.Multiple(() => + { + Assert.That(body, Is.EqualTo(expectedBody)); + Assert.That(_httpContext.Response.ContentLength, Is.EqualTo(body.Length)); + }); + } } [TestCaseSource(typeof(TestCases), nameof(TestCases.StatusCodesNoContent))] - public async Task Response_No_Content(TestCase testCase) { - TeapotStatusCodeMetadata statusCodeResult = new() - { - Description = testCase.Description, - ExcludeBody = true - }; - - _httpContext.Response.Headers["Content-Type"] = "text/plain"; + public async Task Response_No_Content(TestCase testCase) + { + _httpContext.Response.Headers.ContentType = "text/plain"; _httpContext.Response.Headers["Content-Length"] = "42"; - CustomHttpStatusCodeResult target = new(testCase.Code, statusCodeResult, null, null, new()); + CustomHttpStatusCodeResult target = new(testCase.Code, testCase.TeapotStatusCodeMetadata, null, null, []); await target.ExecuteAsync(_httpContext); - Assert.Multiple(() => { + Assert.Multiple(() => + { Assert.That(_httpContext.Response.StatusCode, Is.EqualTo(testCase.Code)); Assert.That(_httpContext.Response.ContentType, Is.Null); Assert.That(_httpResponseFeature.ReasonPhrase, Is.EqualTo(testCase.Description)); @@ -108,9 +115,22 @@ public async Task Response_No_Content(TestCase testCase) { _body.Position = 0; StreamReader sr = new(_body); string body = sr.ReadToEnd(); - Assert.Multiple(() => { + Assert.Multiple(() => + { Assert.That(body, Is.Empty); Assert.That(_httpContext.Response.ContentLength, Is.Null); }); } + + [TestCaseSource(typeof(TestCases), nameof(TestCases.StatusCodesWithRetryAfter))] + public async Task Response_Retry_After_Single_Header(TestCase testCase) + { + Dictionary customHeaders = new() { + { "Retry-After", new StringValues("42") } + }; + + CustomHttpStatusCodeResult target = new(testCase.Code, testCase.TeapotStatusCodeMetadata, sleep: null, suppressBody: null, customHeaders); + await target.ExecuteAsync(_httpContext); + Assert.That(_httpContext.Response.Headers.RetryAfter, Has.Count.EqualTo(1)); + } } \ No newline at end of file diff --git a/src/Teapot.Web/CustomHttpStatusCodeResult.cs b/src/Teapot.Web/CustomHttpStatusCodeResult.cs index 7259d36..fdbbf97 100644 --- a/src/Teapot.Web/CustomHttpStatusCodeResult.cs +++ b/src/Teapot.Web/CustomHttpStatusCodeResult.cs @@ -20,7 +20,7 @@ public class CustomHttpStatusCodeResult( { private const int SLEEP_MIN = 0; private const int SLEEP_MAX = 5 * 60 * 1000; // 5 mins in milliseconds - private static readonly string[] onlySingleHeader = ["Location"]; + internal static readonly string[] onlySingleHeader = ["Location", "Retry-After"]; private static readonly MediaTypeHeaderValue jsonMimeType = new("application/json"); diff --git a/src/Teapot.Web/Models/TeapotStatusCodeMetadataCollection.cs b/src/Teapot.Web/Models/TeapotStatusCodeMetadataCollection.cs index 013be44..aaeb8a2 100644 --- a/src/Teapot.Web/Models/TeapotStatusCodeMetadataCollection.cs +++ b/src/Teapot.Web/Models/TeapotStatusCodeMetadataCollection.cs @@ -115,7 +115,8 @@ public TeapotStatusCodeMetadataCollection( Description = "Moved Permanently", IncludeHeaders = new Dictionary { - {"Location", "https://httpstat.us"} + {"Location", "https://httpstat.us"}, + {"Retry-After", "5"} } }); Add(302, new TeapotStatusCodeMetadata @@ -314,7 +315,11 @@ public TeapotStatusCodeMetadataCollection( }); Add(503, new TeapotStatusCodeMetadata { - Description = "Service Unavailable" + Description = "Service Unavailable", + IncludeHeaders = new Dictionary + { + {"Retry-After", "5"} + } }); Add(504, new TeapotStatusCodeMetadata {