using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using Xunit;
namespace Azaion.Missions.E2E.Helpers;
///
/// Reusable HTTP-shape assertions: PascalCase JSON keys, the
/// { error, traceId } error envelope, paginated-response shape, and
/// expected-status helpers.
///
public static class HttpAssertions
{
public static async Task AssertStatusAsync(HttpResponseMessage response, HttpStatusCode expected)
{
if (response.StatusCode != expected)
{
var body = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
Assert.Fail($"Expected HTTP {(int)expected}; got {(int)response.StatusCode}. Body:\n{body}");
}
}
public static async Task AssertErrorEnvelopeAsync(HttpResponseMessage response)
{
var body = await response.Content.ReadFromJsonAsync().ConfigureAwait(false);
Assert.True(body.TryGetProperty("error", out _), "error-envelope missing 'error' property");
Assert.True(body.TryGetProperty("traceId", out _), "error-envelope missing 'traceId' property");
AssertNoStackLeak(body);
}
public static void AssertNoStackLeak(JsonElement body)
{
// Walk the JSON DOM and fail if any key looks like it leaks server internals.
var leakKeys = new[] { "stack", "stackTrace", "exception", "inner", "trace", "innerException", "type", "details" };
WalkAndAssert(body, leakKeys);
}
private static void WalkAndAssert(JsonElement element, string[] leakKeys)
{
switch (element.ValueKind)
{
case JsonValueKind.Object:
foreach (var prop in element.EnumerateObject())
{
foreach (var leak in leakKeys)
{
if (string.Equals(prop.Name, leak, StringComparison.OrdinalIgnoreCase))
Assert.Fail($"error envelope leaks server internals via key '{prop.Name}'");
}
WalkAndAssert(prop.Value, leakKeys);
}
break;
case JsonValueKind.Array:
foreach (var item in element.EnumerateArray())
WalkAndAssert(item, leakKeys);
break;
}
}
public static AuthenticationHeaderValueLike Bearer(string jwt) => new(jwt);
public sealed record AuthenticationHeaderValueLike(string Jwt)
{
public override string ToString() => $"Bearer {Jwt}";
}
}