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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 8 additions & 11 deletions src/Teapot.Web.Tests/TestCase.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
12 changes: 10 additions & 2 deletions src/Teapot.Web.Tests/TestCases.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<int, TeapotStatusCodeMetadata> 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<TestCase> StatusCodesWithRetryAfter =>
All
.Where(x => RetryAfterStatusCodes.Contains((HttpStatusCode)x.Key))
.Select(Map);
}
110 changes: 65 additions & 45 deletions src/Teapot.Web.Tests/UnitTests/CustomHttpStatusCodeResultTests.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -36,81 +36,101 @@ 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));
});
_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<string, StringValues> 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));
}
}
2 changes: 1 addition & 1 deletion src/Teapot.Web/CustomHttpStatusCodeResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down
9 changes: 7 additions & 2 deletions src/Teapot.Web/Models/TeapotStatusCodeMetadataCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ public TeapotStatusCodeMetadataCollection(
Description = "Moved Permanently",
IncludeHeaders = new Dictionary<string, string>
{
{"Location", "https://httpstat.us"}
{"Location", "https://httpstat.us"},
{"Retry-After", "5"}
}
});
Add(302, new TeapotStatusCodeMetadata
Expand Down Expand Up @@ -314,7 +315,11 @@ public TeapotStatusCodeMetadataCollection(
});
Add(503, new TeapotStatusCodeMetadata
{
Description = "Service Unavailable"
Description = "Service Unavailable",
IncludeHeaders = new Dictionary<string, string>
{
{"Retry-After", "5"}
}
});
Add(504, new TeapotStatusCodeMetadata
{
Expand Down