mirror of
https://github.com/azaion/missions.git
synced 2026-06-22 17:11:07 +00:00
[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>
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Azaion.Missions.E2E.Helpers;
|
||||
|
||||
// Wire DTOs used to deserialize responses from the missions service. Property
|
||||
// names are PascalCase because the SUT serializes its entity types as-is (no
|
||||
// JsonNamingPolicy override is configured in Program.cs — see
|
||||
// _docs/02_document/components/06_http_conventions/description.md Notes #1).
|
||||
// JsonPropertyName is set explicitly so a future global camelCase migration
|
||||
// (ADR-002 carry-forward) breaks these tests loudly instead of silently.
|
||||
|
||||
public sealed record VehicleDto(
|
||||
[property: JsonPropertyName("Id")] Guid Id,
|
||||
[property: JsonPropertyName("Type")] int Type,
|
||||
[property: JsonPropertyName("Model")] string Model,
|
||||
[property: JsonPropertyName("Name")] string Name,
|
||||
[property: JsonPropertyName("FuelType")] int FuelType,
|
||||
[property: JsonPropertyName("BatteryCapacity")] decimal BatteryCapacity,
|
||||
[property: JsonPropertyName("EngineConsumption")] decimal EngineConsumption,
|
||||
[property: JsonPropertyName("EngineConsumptionIdle")] decimal EngineConsumptionIdle,
|
||||
[property: JsonPropertyName("IsDefault")] bool IsDefault);
|
||||
|
||||
public sealed record MissionDto(
|
||||
[property: JsonPropertyName("Id")] Guid Id,
|
||||
[property: JsonPropertyName("CreatedDate")] DateTime CreatedDate,
|
||||
[property: JsonPropertyName("Name")] string Name,
|
||||
[property: JsonPropertyName("VehicleId")] Guid VehicleId);
|
||||
|
||||
// Waypoint response is FLAT (Lat/Lon/Mgrs at top level, NOT nested in a
|
||||
// GeoPoint object) because the SUT returns the LinqToDB entity directly via
|
||||
// `Ok(waypoint)` and the entity stores those columns flat. The request DTO
|
||||
// nests them under GeoPoint, but the response does not — see
|
||||
// _docs/02_document/modules/controller_missions.md and Database/Entities/Waypoint.cs.
|
||||
public sealed record WaypointDto(
|
||||
[property: JsonPropertyName("Id")] Guid Id,
|
||||
[property: JsonPropertyName("MissionId")] Guid MissionId,
|
||||
[property: JsonPropertyName("Lat")] decimal? Lat,
|
||||
[property: JsonPropertyName("Lon")] decimal? Lon,
|
||||
[property: JsonPropertyName("Mgrs")] string? Mgrs,
|
||||
[property: JsonPropertyName("WaypointSource")] int WaypointSource,
|
||||
[property: JsonPropertyName("WaypointObjective")] int WaypointObjective,
|
||||
[property: JsonPropertyName("OrderNum")] int OrderNum,
|
||||
[property: JsonPropertyName("Height")] decimal Height);
|
||||
|
||||
public sealed record PaginatedResponseDto<T>(
|
||||
[property: JsonPropertyName("Items")] List<T> Items,
|
||||
[property: JsonPropertyName("TotalCount")] int TotalCount,
|
||||
[property: JsonPropertyName("Page")] int Page,
|
||||
[property: JsonPropertyName("PageSize")] int PageSize);
|
||||
|
||||
// Error envelope produced by ErrorHandlingMiddleware. The middleware uses an
|
||||
// anonymous object literal (`new { statusCode = ..., message = ... }`) so the
|
||||
// wire shape IS camelCase even though the rest of the API is PascalCase.
|
||||
public sealed record ProblemDto(
|
||||
[property: JsonPropertyName("statusCode")] int StatusCode,
|
||||
[property: JsonPropertyName("message")] string Message);
|
||||
@@ -29,6 +29,42 @@ public static class HttpAssertions
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user