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}"; } }