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
2 changes: 1 addition & 1 deletion src/Teapot.Web.Tests/IntegrationTests/StatusCodeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public async Task ResponseWithContent([Values] TestCase testCase)
[TestCaseSource(typeof(TestCases), nameof(TestCases.StatusCodesWithContent))]
public async Task ResponseWithContentSuppressedViaQs([Values] TestCase testCase)
{
string uri = $"/{testCase.Code}?{nameof(CustomHttpStatusCodeResult.SuppressBody)}=true";
string uri = $"/{testCase.Code}?{nameof(ResponseOptions.SuppressBody)}=true";
using HttpRequestMessage httpRequest = new(httpMethod, uri);
using HttpResponseMessage response = await _httpClient.SendAsync(httpRequest);
Assert.That((int)response.StatusCode, Is.EqualTo(testCase.Code));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public void Setup()
[TestCaseSource(typeof(TestCases), nameof(TestCases.StatusCodesAll))]
public async Task Response_Is_Correct(TestCase testCase)
{
CustomHttpStatusCodeResult target = new(testCase.Code, testCase.TeapotStatusCodeMetadata, null, null, []);
CustomHttpStatusCodeResult target = new(new ResponseOptions(testCase.Code, metadata: testCase.TeapotStatusCodeMetadata));

await target.ExecuteAsync(_httpContext);
Assert.Multiple(() =>
Expand Down Expand Up @@ -68,7 +68,7 @@ public async Task Response_Is_Correct(TestCase testCase)
[TestCaseSource(typeof(TestCases), nameof(TestCases.StatusCodesAll))]
public async Task Response_Json_Is_Correct(TestCase testCase)
{
CustomHttpStatusCodeResult target = new(testCase.Code, testCase.TeapotStatusCodeMetadata, null, null, []);
CustomHttpStatusCodeResult target = new(new ResponseOptions(testCase.Code, metadata: testCase.TeapotStatusCodeMetadata));

_httpContext.Request.Headers.Accept = "application/json";

Expand Down Expand Up @@ -103,7 +103,7 @@ 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, testCase.TeapotStatusCodeMetadata, null, null, []);
CustomHttpStatusCodeResult target = new(new ResponseOptions(testCase.Code, metadata: testCase.TeapotStatusCodeMetadata));

await target.ExecuteAsync(_httpContext);
Assert.Multiple(() =>
Expand All @@ -129,7 +129,7 @@ public async Task Response_Retry_After_Single_Header(TestCase testCase)
{ "Retry-After", new StringValues("42") }
};

CustomHttpStatusCodeResult target = new(testCase.Code, testCase.TeapotStatusCodeMetadata, sleep: null, suppressBody: null, customHeaders);
CustomHttpStatusCodeResult target = new(new ResponseOptions(testCase.Code, metadata: testCase.TeapotStatusCodeMetadata, customHeaders: customHeaders));
await target.ExecuteAsync(_httpContext);
Assert.That(_httpContext.Response.Headers.RetryAfter, Has.Count.EqualTo(1));
}
Expand Down
22 changes: 12 additions & 10 deletions src/Teapot.Web.Tests/UnitTests/SleepTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,17 @@ public void Setup() {
}

[Test]
public void SleepReadFromQuery() {
public void SleepReadFromQuery()
{
Mock<HttpRequest> request = HttpRequestHelper.GenerateMockRequest();
IResult result = StatusExtensions.HandleStatusRequestAsync(200, Sleep, null, null, request.Object, _statusCodes);
IResult result = StatusExtensions.CommonHandleStatusRequestAsync(new ResponseOptions(200, sleep: Sleep), null, request.Object, _statusCodes);

Assert.Multiple(() => {
Assert.Multiple(() =>
{
Assert.That(result, Is.InstanceOf<CustomHttpStatusCodeResult>());

CustomHttpStatusCodeResult r = (CustomHttpStatusCodeResult)result;
Assert.That(r.Sleep, Is.EqualTo(Sleep));
Assert.That(r.Options.Sleep, Is.EqualTo(Sleep));
});
}

Expand All @@ -40,13 +42,13 @@ public void SleepReadFromHeader()
Mock<HttpRequest> request = HttpRequestHelper.GenerateMockRequest();
request.Object.Headers.Append(StatusExtensions.SLEEP_HEADER, Sleep.ToString());

IResult result = StatusExtensions.HandleStatusRequestAsync(200, null, null, null, request.Object, _statusCodes);
IResult result = StatusExtensions.CommonHandleStatusRequestAsync(new ResponseOptions(200), null, request.Object, _statusCodes);

Assert.Multiple(() => {
Assert.That(result, Is.InstanceOf<CustomHttpStatusCodeResult>());

CustomHttpStatusCodeResult r = (CustomHttpStatusCodeResult)result;
Assert.That(r.Sleep, Is.EqualTo(Sleep));
Assert.That(r.Options.Sleep, Is.EqualTo(Sleep));
});
}

Expand All @@ -55,13 +57,13 @@ public void SleepReadFromQSTakesPriorityHeader() {
Mock<HttpRequest> request = HttpRequestHelper.GenerateMockRequest();
request.Object.Headers.Append(StatusExtensions.SLEEP_HEADER, Sleep.ToString());

IResult result = StatusExtensions.HandleStatusRequestAsync(200, Sleep * 2, null, null, request.Object, _statusCodes);
IResult result = StatusExtensions.CommonHandleStatusRequestAsync(new ResponseOptions(200, sleep:Sleep * 2), null, request.Object, _statusCodes);

Assert.Multiple(() => {
Assert.That(result, Is.InstanceOf<CustomHttpStatusCodeResult>());

CustomHttpStatusCodeResult r = (CustomHttpStatusCodeResult)result;
Assert.That(r.Sleep, Is.EqualTo(Sleep * 2));
Assert.That(r.Options.Sleep, Is.EqualTo(Sleep * 2));
});
}

Expand All @@ -71,14 +73,14 @@ public void BadSleepHeaderIgnored()
Mock<HttpRequest> request = HttpRequestHelper.GenerateMockRequest();
request.Object.Headers.Append(StatusExtensions.SLEEP_HEADER, "invalid");

IResult result = StatusExtensions.HandleStatusRequestAsync(200, null, null, null, request.Object, _statusCodes);
IResult result = StatusExtensions.CommonHandleStatusRequestAsync(new ResponseOptions(200), null, request.Object, _statusCodes);

Assert.Multiple(() =>
{
Assert.That(result, Is.InstanceOf<CustomHttpStatusCodeResult>());

CustomHttpStatusCodeResult r = (CustomHttpStatusCodeResult)result;
Assert.That(r.Sleep, Is.Null);
Assert.That(r.Options.Sleep, Is.Null);
});
}

Expand Down
16 changes: 8 additions & 8 deletions src/Teapot.Web.Tests/UnitTests/SuppressBodyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ public void Setup()
public void SuppressBodyReadFromQuery(bool? suppressBody)
{
Mock<HttpRequest> request = HttpRequestHelper.GenerateMockRequest();
IResult result = StatusExtensions.HandleStatusRequestAsync(200, null, suppressBody, null, request.Object, _statusCodes);
IResult result = StatusExtensions.CommonHandleStatusRequestAsync(new ResponseOptions(200, suppressBody:suppressBody), null, request.Object, _statusCodes);

Assert.Multiple(() =>
{
Assert.That(result, Is.InstanceOf<CustomHttpStatusCodeResult>());

CustomHttpStatusCodeResult r = (CustomHttpStatusCodeResult)result;
Assert.That(r.SuppressBody, Is.EqualTo(suppressBody));
Assert.That(r.Options.SuppressBody, Is.EqualTo(suppressBody));
});
}

Expand All @@ -48,7 +48,7 @@ public void SuppressBodyReadFromHeader(string? suppressBody)
Mock<HttpRequest> request = HttpRequestHelper.GenerateMockRequest();
request.Object.Headers.Append(StatusExtensions.SUPPRESS_BODY_HEADER, suppressBody);

IResult result = StatusExtensions.HandleStatusRequestAsync(200, null, null, null, request.Object, _statusCodes);
IResult result = StatusExtensions.CommonHandleStatusRequestAsync(new ResponseOptions(200), null, request.Object, _statusCodes);

Assert.Multiple(() =>
{
Expand All @@ -60,7 +60,7 @@ public void SuppressBodyReadFromHeader(string? suppressBody)
string { Length: > 0 } stringValue => bool.Parse(stringValue),
_ => null
};
Assert.That(r.SuppressBody, Is.EqualTo(expectedValue));
Assert.That(r.Options.SuppressBody, Is.EqualTo(expectedValue));
});
}

Expand All @@ -76,7 +76,7 @@ public void SuppressBodyReadFromQSTakesPriorityHeader(string? headerValue, bool?
Mock<HttpRequest> request = HttpRequestHelper.GenerateMockRequest();
request.Object.Headers.Append(StatusExtensions.SUPPRESS_BODY_HEADER, headerValue);

IResult result = StatusExtensions.HandleStatusRequestAsync(200, null, queryStringValue, null, request.Object, _statusCodes);
IResult result = StatusExtensions.CommonHandleStatusRequestAsync(new ResponseOptions(200, suppressBody: queryStringValue), null, request.Object, _statusCodes);

Assert.Multiple(() =>
{
Expand All @@ -88,7 +88,7 @@ public void SuppressBodyReadFromQSTakesPriorityHeader(string? headerValue, bool?
string { Length: > 0 } stringValue => bool.Parse(stringValue),
_ => null
};
Assert.That(r.SuppressBody, Is.EqualTo(expectedValue));
Assert.That(r.Options.SuppressBody, Is.EqualTo(expectedValue));
});
}

Expand All @@ -98,14 +98,14 @@ public void BadSuppressBodyHeaderIgnored()
Mock<HttpRequest> request = HttpRequestHelper.GenerateMockRequest();
request.Object.Headers.Append(StatusExtensions.SUPPRESS_BODY_HEADER, "invalid");

IResult result = StatusExtensions.HandleStatusRequestAsync(200, null, null, null, request.Object, _statusCodes);
IResult result = StatusExtensions.CommonHandleStatusRequestAsync(new ResponseOptions(200),null, request.Object, _statusCodes);

Assert.Multiple(() =>
{
Assert.That(result, Is.InstanceOf<CustomHttpStatusCodeResult>());

CustomHttpStatusCodeResult r = (CustomHttpStatusCodeResult)result;
Assert.That(r.SuppressBody, Is.Null);
Assert.That(r.Options.SuppressBody, Is.Null);
});
}
}
76 changes: 55 additions & 21 deletions src/Teapot.Web/CustomHttpStatusCodeResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,51 +7,50 @@
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Teapot.Web.Models;

namespace Teapot.Web;

public class CustomHttpStatusCodeResult(
int statusCode,
TeapotStatusCodeMetadata metadata,
int? sleep,
bool? suppressBody,
Dictionary<string, StringValues> customResponseHeaders) : IResult
public class CustomHttpStatusCodeResult(ResponseOptions options) : IResult
{
private const int SLEEP_MIN = 0;
private const int SLEEP_MAX = 5 * 60 * 1000; // 5 mins in milliseconds
internal static readonly string[] onlySingleHeader = ["Location", "Retry-After"];

private static readonly MediaTypeHeaderValue jsonMimeType = new("application/json");

public int? Sleep => sleep;

public bool? SuppressBody => suppressBody;
public ResponseOptions Options => options;

public async Task ExecuteAsync(HttpContext context)
{
await DoSleep(Sleep);
await DoSleep(Options.Sleep);

if (Options.AbortBeforeHeaders == true)
{
context.Abort();
return;
}


context.Response.StatusCode = statusCode;
context.Response.StatusCode = Options.StatusCode;

if (!string.IsNullOrEmpty(metadata.Description))
if (!string.IsNullOrEmpty(Options.Metadata.Description))
{
IHttpResponseFeature? httpResponseFeature = context.Features.Get<IHttpResponseFeature>();
if (httpResponseFeature is not null)
{
httpResponseFeature.ReasonPhrase = metadata.Description;
httpResponseFeature.ReasonPhrase = Options.Metadata.Description;
}
}

if (metadata.IncludeHeaders is not null)
if (Options.Metadata.IncludeHeaders is not null)
{
foreach ((string header, string values) in metadata.IncludeHeaders)
foreach ((string header, string values) in Options.Metadata.IncludeHeaders)
{
context.Response.Headers.Append(header, values);
}
}

foreach ((string header, StringValues values) in customResponseHeaders)
foreach ((string header, StringValues values) in Options.CustomHeaders)
{
if (onlySingleHeader.Contains(header))
{
Expand All @@ -63,7 +62,7 @@ public async Task ExecuteAsync(HttpContext context)
}
}

if (metadata.ExcludeBody || suppressBody == true)
if (Options.Metadata.ExcludeBody || Options.SuppressBody == true)
{
//remove Content-Length and Content-Type when there isn't any body
context.Response.Headers.Remove("Content-Length");
Expand All @@ -75,14 +74,49 @@ public async Task ExecuteAsync(HttpContext context)

(string body, string contentType) = acceptTypes.Contains(jsonMimeType) switch
{
true => (JsonSerializer.Serialize(new { code = statusCode, description = metadata.Body ?? metadata.Description }), "application/json"),
false => (metadata.Body ?? $"{statusCode} {metadata.Description}", "text/plain")
true => (JsonSerializer.Serialize(new { code = Options.StatusCode, description = Options.Metadata.Body ?? Options.Metadata.Description }), "application/json"),
false => (Options.Metadata.Body ?? $"{Options.StatusCode} {Options.Metadata.Description}", "text/plain")
};

context.Response.ContentType = contentType;
context.Response.ContentLength = body.Length;
await context.Response.WriteAsync(body);

await context.Response.StartAsync();

await DoSleep(Options.SleepAfterHeaders);

if (Options.AbortAfterHeaders == true)
{
await DoSleep(10);
context.Abort();
return;
}

if (Options.AbortDuringBody == true)
{
await context.Response.WriteAsync(body.Substring(0, 1));
await context.Response.Body.FlushAsync();
await DoSleep(10);
context.Abort();
return;
}

if (Options.DribbleBody == true)
{
for (int i = 0; i < body.Length; i++)
{
await context.Response.WriteAsync(body[i..(i + 1)]);
if (i < 20)
{
await context.Response.Body.FlushAsync();
await DoSleep(10);
}
}
}
else
{
await context.Response.WriteAsync(body);
}
}
}

Expand Down
42 changes: 42 additions & 0 deletions src/Teapot.Web/ResponseOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using Microsoft.Extensions.Primitives;
using System.Collections.Generic;
using Teapot.Web.Models;

namespace Teapot.Web;

public record class ResponseOptions
{
public ResponseOptions(int statusCode,
int? sleep = null,
int? sleepAfterHeaders = null,
bool? abortBeforeHeaders = null,
bool? abortAfterHeaders = null,
bool? abortDuringBody = null,
bool? suppressBody = null,
bool? dribbleBody = null,
TeapotStatusCodeMetadata? metadata = null,
Dictionary<string, StringValues>? customHeaders = null)
{
StatusCode = statusCode;
Sleep = sleep;
SleepAfterHeaders = sleepAfterHeaders;
AbortBeforeHeaders = abortBeforeHeaders;
AbortAfterHeaders = abortAfterHeaders;
AbortDuringBody = abortDuringBody;
SuppressBody = suppressBody;
DribbleBody = dribbleBody;
CustomHeaders = customHeaders ?? new Dictionary<string, StringValues>();
Metadata = metadata ?? new();
}

public int StatusCode { get; set; }
public int? Sleep { get; set; }
public int? SleepAfterHeaders { get; set; }
public bool? SuppressBody { get; set; }
public bool? AbortBeforeHeaders { get; set; }
public bool? AbortAfterHeaders { get; set; }
public bool? AbortDuringBody { get; set; }
public bool? DribbleBody { get; set; }
public Dictionary<string, StringValues> CustomHeaders { get; set; }
public TeapotStatusCodeMetadata Metadata { get; set; }
}
Loading
Loading