Files
Oleksandr Bezdieniezhnykh 6b2c2d998e [AZ-577] [AZ-578] [AZ-579] [AZ-580] Implement E2E test batch 2
Adds 26 blackbox tests (FT-P-01..18, FT-N-01..08) covering full AC
matrices for Vehicles/Missions/Waypoints/Health/Errors. Three
spec-vs-code carry-forwards documented in batch_02_report.md and
pinned with [Trait("carry_forward", ...)].

Shared scaffolding: ApiDtos.cs, AssertProblemEnvelopeAsync helper,
Seeds.cs, StubSchema.cs, CascadeF3/F4 fixtures, PostgresStopStart
fixture (gated by COMPOSE_RESTART_ENABLED). Removes the 4 placeholder
Sanity.cs files (now superseded). docker-compose.test.yml gains the
expected_results volume mount + FIXTURE_SQL_DIR for the consumer.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-15 08:28:37 +03:00

104 lines
4.1 KiB
C#

using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using Xunit;
namespace Azaion.Missions.E2E.Helpers;
/// <summary>
/// Reusable HTTP-shape assertions: PascalCase JSON keys, the
/// <c>{ error, traceId }</c> error envelope, paginated-response shape, and
/// expected-status helpers.
/// </summary>
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<JsonElement>().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);
}
/// <summary>
/// Asserts the {statusCode, message} envelope produced by
/// <c>ErrorHandlingMiddleware</c>. The envelope uses camelCase keys
/// because the middleware emits an anonymous object literal — see
/// _docs/02_document/components/06_http_conventions/description.md.
/// </summary>
public static async Task<ProblemDto> AssertProblemEnvelopeAsync(
HttpResponseMessage response,
HttpStatusCode expectedStatus)
{
await AssertStatusAsync(response, expectedStatus).ConfigureAwait(false);
var body = await response.Content.ReadFromJsonAsync<JsonElement>().ConfigureAwait(false);
Assert.True(body.TryGetProperty("statusCode", out var statusEl),
"problem envelope missing 'statusCode' property");
Assert.True(body.TryGetProperty("message", out var messageEl),
"problem envelope missing 'message' property");
Assert.Equal((int)expectedStatus, statusEl.GetInt32());
var message = messageEl.GetString();
Assert.False(string.IsNullOrEmpty(message),
"problem envelope 'message' must be non-empty");
AssertNoStackLeak(body);
// Reject any extra keys to pin the envelope contract — the spec says
// EXACTLY these two keys (results_report.md row 1.8 + AC-8.6).
var extraKeys = body.EnumerateObject()
.Select(p => p.Name)
.Where(n => n is not ("statusCode" or "message"))
.ToArray();
Assert.True(extraKeys.Length == 0,
$"problem envelope has unexpected extra keys: {string.Join(",", extraKeys)}");
return new ProblemDto(statusEl.GetInt32(), message!);
}
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}";
}
}