mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 23:18:56 -07:00
Radicade Public Code
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using RadicadeServer.Middleware;
|
||||
using RadicadeServer.RecNet.Responses;
|
||||
using Xunit;
|
||||
|
||||
namespace RadicadeServer.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// The failure envelope, pinned end to end: a bodyless 4xx or 5xx must leave the pipeline
|
||||
/// carrying `{ "success": false, "error": "<why>" }` under its own status.
|
||||
///
|
||||
/// This is the wire contract the client's error path reads. The 2023-09-06 client takes the
|
||||
/// body's `error` as a string and asserts on it (`ArgumentException: Error message cannot be
|
||||
/// null`), and it raises that inside the caller's task continuation - so an empty body does not
|
||||
/// surface as a visible failure, it silently cancels whatever the caller was doing. Player.log
|
||||
/// shows the crash behind every bodyless 404 this server used to serve: `/clubs/club/home/me`
|
||||
/// and `/econ/api/subscriptionseasons/v1/seasons/current` (both deliberate 404s) and the 404 a
|
||||
/// private dorm answers when the caller is not its owner (docs/2023-0831-CLIENT-SWITCH.md,
|
||||
/// invite section).
|
||||
/// </summary>
|
||||
public class FailureEnvelopeTests
|
||||
{
|
||||
private static async Task<(int Status, string ContentType, string Body)> Run(RequestDelegate next)
|
||||
{
|
||||
var context = new DefaultHttpContext
|
||||
{
|
||||
// WriteAsJsonAsync resolves its serializer options from here; a bare DefaultHttpContext
|
||||
// has no service provider.
|
||||
RequestServices = new ServiceCollection().BuildServiceProvider(),
|
||||
};
|
||||
context.Response.Body = new MemoryStream();
|
||||
|
||||
await new FailureEnvelopeMiddleware(next).InvokeAsync(context);
|
||||
|
||||
context.Response.Body.Position = 0;
|
||||
using var reader = new StreamReader(context.Response.Body, Encoding.UTF8);
|
||||
return (context.Response.StatusCode, context.Response.ContentType ?? string.Empty, await reader.ReadToEndAsync());
|
||||
}
|
||||
|
||||
private static RequestDelegate BareStatus(int status) => context =>
|
||||
{
|
||||
context.Response.StatusCode = status;
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public async Task ABareNotFoundAnswersTheFailureEnvelope()
|
||||
{
|
||||
var (status, contentType, body) = await Run(BareStatus(StatusCodes.Status404NotFound));
|
||||
|
||||
Assert.Equal(StatusCodes.Status404NotFound, status);
|
||||
Assert.StartsWith("application/json", contentType);
|
||||
|
||||
var json = JsonSerializer.Deserialize<JsonElement>(body);
|
||||
Assert.False(json.GetProperty("success").GetBoolean());
|
||||
Assert.Equal("not found", json.GetProperty("error").GetString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(400, "bad request")]
|
||||
[InlineData(401, "unauthorized")]
|
||||
[InlineData(403, "forbidden")]
|
||||
[InlineData(405, "method not allowed")]
|
||||
[InlineData(413, "request too large")]
|
||||
[InlineData(500, "internal server error")]
|
||||
[InlineData(503, "service unavailable")]
|
||||
public async Task EveryBodylessFailureStatusIsSpelled(int statusCode, string expected)
|
||||
{
|
||||
var (status, _, body) = await Run(BareStatus(statusCode));
|
||||
|
||||
Assert.Equal(statusCode, status);
|
||||
var json = JsonSerializer.Deserialize<JsonElement>(body);
|
||||
Assert.False(json.GetProperty("success").GetBoolean());
|
||||
Assert.Equal(expected, json.GetProperty("error").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AnUnknownFailureStatusStillCarriesANonNullError()
|
||||
{
|
||||
// 418 and friends: the wording is nobody's contract, but a NULL error is, and the client
|
||||
// asserts on it whatever the status was.
|
||||
var (_, _, body) = await Run(BareStatus(418));
|
||||
|
||||
var json = JsonSerializer.Deserialize<JsonElement>(body);
|
||||
Assert.False(json.GetProperty("success").GetBoolean());
|
||||
Assert.False(string.IsNullOrWhiteSpace(json.GetProperty("error").GetString()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AnAnswerThatAlreadyHasABodyIsLeftAlone()
|
||||
{
|
||||
var (status, _, body) = await Run(async context =>
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status404NotFound;
|
||||
await context.Response.WriteAsJsonAsync(new Failure("No such room"));
|
||||
});
|
||||
|
||||
Assert.Equal(StatusCodes.Status404NotFound, status);
|
||||
Assert.Equal("No such room", JsonSerializer.Deserialize<JsonElement>(body).GetProperty("error").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SuccessfulAndEmptyAnswersAreLeftAlone()
|
||||
{
|
||||
var (ok, okContentType, okBody) = await Run(BareStatus(StatusCodes.Status200OK));
|
||||
Assert.Equal(StatusCodes.Status200OK, ok);
|
||||
Assert.Empty(okBody);
|
||||
Assert.Empty(okContentType);
|
||||
|
||||
var (noContent, _, noContentBody) = await Run(BareStatus(StatusCodes.Status204NoContent));
|
||||
Assert.Equal(StatusCodes.Status204NoContent, noContent);
|
||||
Assert.Empty(noContentBody);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpgradesAndRedirectsAreLeftAlone()
|
||||
{
|
||||
// 101 is a WebSocket taking over the response: writing into it would corrupt the socket,
|
||||
// and the hub is on this pipeline.
|
||||
var (upgrade, _, upgradeBody) = await Run(BareStatus(101));
|
||||
Assert.Equal(101, upgrade);
|
||||
Assert.Empty(upgradeBody);
|
||||
|
||||
var (redirect, _, redirectBody) = await Run(BareStatus(StatusCodes.Status302Found));
|
||||
Assert.Equal(StatusCodes.Status302Found, redirect);
|
||||
Assert.Empty(redirectBody);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheFailureEnvelopeNeverSpellsAnEmptyError()
|
||||
{
|
||||
// `error` is what the client turns into an exception message; blank is as unusable as null.
|
||||
var json = JsonSerializer.Serialize(new Failure(""));
|
||||
Assert.Equal("request failed", JsonSerializer.Deserialize<JsonElement>(json).GetProperty("error").GetString());
|
||||
|
||||
Assert.Equal("not found", FailureEnvelopeMiddleware.Why(StatusCodes.Status404NotFound));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user