mirror of
https://github.com/azaion/missions.git
synced 2026-06-22 18:01: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,77 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using Azaion.Missions.E2E.Fixtures;
|
||||
using Azaion.Missions.E2E.Helpers;
|
||||
using Xunit;
|
||||
|
||||
namespace Azaion.Missions.E2E.Tests.Waypoints;
|
||||
|
||||
/// <summary>
|
||||
/// FT-P-18 — waypoint cascade delete is scoped to one waypoint; the sibling
|
||||
/// waypoint's chain remains intact. Owns its own xUnit collection because
|
||||
/// the F4 fixture is destructive.
|
||||
/// Traces: AC-4.5.
|
||||
/// </summary>
|
||||
[Collection("CascadeF4")]
|
||||
[Trait("Category", "Blackbox")]
|
||||
[Trait("db_access", "seed-or-assert-only")]
|
||||
public sealed class CascadeF4Tests : TestBase, IClassFixture<CascadeF4Fixture>
|
||||
{
|
||||
public CascadeF4Tests(CascadeF4Fixture _) { /* fixture seeds the DB. */ }
|
||||
|
||||
[Fact]
|
||||
[Trait("Traces", "AC-4.5")]
|
||||
[Trait("max_ms", "10000")]
|
||||
public async Task FT_P_18_waypoint_cascade_scoped_to_one_waypoint_sibling_intact()
|
||||
{
|
||||
// Arrange — refresh the F4 fixture into a deterministic state.
|
||||
DbResetFixture.ResetDatabase(TestEnvironment.DbSideChannel);
|
||||
StubSchema.EnsureCreated();
|
||||
Seeds.Apply(FixtureSql.Load("fixture_cascade_F4"));
|
||||
|
||||
// Pre-state safety check (cascade_F4_walk.json
|
||||
// expected_per_table_pre_state_for_safety_check).
|
||||
Assert.Equal(2, DbAssertions.TableRowCount("waypoints"));
|
||||
Assert.Equal(2, DbAssertions.TableRowCount("media"));
|
||||
Assert.Equal(2, DbAssertions.TableRowCount("annotations"));
|
||||
Assert.Equal(2, DbAssertions.TableRowCount("detection"));
|
||||
|
||||
var token = await Tokens.MintDefaultAsync();
|
||||
|
||||
// Act
|
||||
using var http = new HttpRequestMessage(
|
||||
HttpMethod.Delete,
|
||||
$"/missions/{CascadeF4Fixture.MissionId}/waypoints/{CascadeF4Fixture.TargetWaypointId}");
|
||||
http.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Jwt);
|
||||
using var response = await Missions.SendAsync(http);
|
||||
|
||||
// Assert — target chain gone.
|
||||
await HttpAssertions.AssertStatusAsync(response, HttpStatusCode.NoContent);
|
||||
Assert.Equal(0L, DbAssertions.ScalarCount(
|
||||
"SELECT COUNT(*) FROM waypoints WHERE id = @id",
|
||||
("id", CascadeF4Fixture.TargetWaypointId)));
|
||||
Assert.Equal(0L, DbAssertions.ScalarCount(
|
||||
"SELECT COUNT(*) FROM media WHERE id = @id",
|
||||
("id", CascadeF4Fixture.TargetMediaId)));
|
||||
Assert.Equal(0L, DbAssertions.ScalarCount(
|
||||
"SELECT COUNT(*) FROM annotations WHERE id = @id",
|
||||
("id", CascadeF4Fixture.TargetAnnotationId)));
|
||||
Assert.Equal(0L, DbAssertions.ScalarCount(
|
||||
"SELECT COUNT(*) FROM detection WHERE annotation_id = @id",
|
||||
("id", CascadeF4Fixture.TargetAnnotationId)));
|
||||
|
||||
// Sibling chain intact.
|
||||
Assert.Equal(1L, DbAssertions.ScalarCount(
|
||||
"SELECT COUNT(*) FROM waypoints WHERE id = @id",
|
||||
("id", CascadeF4Fixture.SiblingWaypointId)));
|
||||
Assert.Equal(1L, DbAssertions.ScalarCount(
|
||||
"SELECT COUNT(*) FROM media WHERE id = @id",
|
||||
("id", CascadeF4Fixture.SiblingMediaId)));
|
||||
Assert.Equal(1L, DbAssertions.ScalarCount(
|
||||
"SELECT COUNT(*) FROM annotations WHERE id = @id",
|
||||
("id", CascadeF4Fixture.SiblingAnnotationId)));
|
||||
Assert.Equal(1L, DbAssertions.ScalarCount(
|
||||
"SELECT COUNT(*) FROM detection WHERE annotation_id = @id",
|
||||
("id", CascadeF4Fixture.SiblingAnnotationId)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using Azaion.Missions.E2E.Fixtures;
|
||||
using Azaion.Missions.E2E.Helpers;
|
||||
using Xunit;
|
||||
|
||||
namespace Azaion.Missions.E2E.Tests.Waypoints;
|
||||
|
||||
/// <summary>
|
||||
/// FT-N-07 — waypoint operation against a missing mission must surface as
|
||||
/// a 404 with the standard envelope (results_report.md row 4.1 / AC-4.2).
|
||||
/// </summary>
|
||||
[Collection("Waypoints")]
|
||||
[Trait("Category", "Blackbox")]
|
||||
[Trait("db_access", "seed-or-assert-only")]
|
||||
public sealed class NegativeTests : TestBase, IClassFixture<DbResetFixture>
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Traces", "AC-4.2")]
|
||||
[Trait("max_ms", "2000")]
|
||||
[Trait("carry_forward", "AC-4.2")]
|
||||
public async Task FT_N_07_waypoint_list_against_missing_mission_returns_empty_array_today()
|
||||
{
|
||||
// CARRY-FORWARD: spec says 404 with problem envelope (AZ-580 AC-7
|
||||
// and results_report.md row 4.1). Today the SUT
|
||||
// (WaypointService.GetWaypoints) does NOT validate parent existence
|
||||
// — it returns an empty list which the controller wraps as 200 []. Per
|
||||
// /autodev batch 2 user choice, this test asserts the CODE shape.
|
||||
// Flip to 404+envelope expectation when the divergence is closed.
|
||||
|
||||
// Arrange
|
||||
DbResetFixture.ResetDatabase(TestEnvironment.DbSideChannel);
|
||||
var token = await Tokens.MintDefaultAsync();
|
||||
var randomMissionId = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
using var http = new HttpRequestMessage(
|
||||
HttpMethod.Get, $"/missions/{randomMissionId}/waypoints");
|
||||
http.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Jwt);
|
||||
using var response = await Missions.SendAsync(http);
|
||||
|
||||
// Assert
|
||||
await HttpAssertions.AssertStatusAsync(response, HttpStatusCode.OK);
|
||||
var raw = await response.Content.ReadAsStringAsync();
|
||||
Assert.Equal("[]", raw.Trim());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using Azaion.Missions.E2E.Fixtures;
|
||||
using Azaion.Missions.E2E.Helpers;
|
||||
using Xunit;
|
||||
|
||||
namespace Azaion.Missions.E2E.Tests.Waypoints;
|
||||
|
||||
/// <summary>
|
||||
/// FT-P-13..15 — waypoint happy-path scenarios. FT-P-18 (cascade delete) is
|
||||
/// in <see cref="CascadeF4Tests"/>, FT-P-16/17 (health) are in
|
||||
/// <c>Tests/Health/HealthTests.cs</c>.
|
||||
/// Traces: AC-4.3 / AC-4 (data_parameters § 2.3) / AC-4.4.
|
||||
/// </summary>
|
||||
[Collection("Waypoints")]
|
||||
[Trait("Category", "Blackbox")]
|
||||
[Trait("db_access", "seed-or-assert-only")]
|
||||
public sealed class PositiveTests : TestBase, IClassFixture<DbResetFixture>
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Traces", "AC-4.3")]
|
||||
[Trait("max_ms", "2000")]
|
||||
public async Task FT_P_13_waypoint_list_is_ordered_by_order_num_asc()
|
||||
{
|
||||
// Arrange
|
||||
DbResetFixture.ResetDatabase(TestEnvironment.DbSideChannel);
|
||||
Seeds.Apply(Seeds.FiveWaypointsUnordered.Sql);
|
||||
var token = await Tokens.MintDefaultAsync();
|
||||
|
||||
// Act
|
||||
using var http = new HttpRequestMessage(
|
||||
HttpMethod.Get, $"/missions/{Seeds.FiveWaypointsUnordered.MissionId}/waypoints");
|
||||
http.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Jwt);
|
||||
using var response = await Missions.SendAsync(http);
|
||||
|
||||
// Assert
|
||||
await HttpAssertions.AssertStatusAsync(response, HttpStatusCode.OK);
|
||||
var raw = await response.Content.ReadAsStringAsync();
|
||||
using var doc = JsonDocument.Parse(raw);
|
||||
Assert.Equal(JsonValueKind.Array, doc.RootElement.ValueKind);
|
||||
|
||||
var waypoints = JsonSerializer.Deserialize<List<WaypointDto>>(raw)
|
||||
?? throw new InvalidOperationException($"could not deserialize array: {raw}");
|
||||
Assert.Equal(5, waypoints.Count);
|
||||
Assert.Equal(new[] { 1, 2, 3, 4, 5 },
|
||||
waypoints.Select(w => w.OrderNum).ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Traces", "AC-4")]
|
||||
[Trait("max_ms", "2000")]
|
||||
[Trait("carry_forward", "waypoint-response-flat-vs-nested-geo")]
|
||||
public async Task FT_P_14_create_waypoint_echoes_lat_lon_and_does_not_auto_convert_to_mgrs()
|
||||
{
|
||||
// CARRY-FORWARD: the canonical task spec (AZ-579 AC-2) says the
|
||||
// response body has nested "GeoPoint:{Lat,Lon,Mgrs}". The actual SUT
|
||||
// (Database/Entities/Waypoint.cs + Controllers/MissionsController.cs)
|
||||
// returns the LinqToDB entity directly, which has flat Lat/Lon/Mgrs
|
||||
// columns — there is no GeoPoint object in the response. Per /autodev
|
||||
// batch 2 user choice we assert the CODE shape (flat) here. Flip when
|
||||
// the spec/code divergence is closed.
|
||||
|
||||
// Arrange
|
||||
DbResetFixture.ResetDatabase(TestEnvironment.DbSideChannel);
|
||||
Seeds.Apply(Seeds.FiveWaypointsUnordered.Sql);
|
||||
var missionId = Seeds.FiveWaypointsUnordered.MissionId;
|
||||
var token = await Tokens.MintDefaultAsync();
|
||||
|
||||
// Act
|
||||
using var http = new HttpRequestMessage(HttpMethod.Post, $"/missions/{missionId}/waypoints")
|
||||
{
|
||||
Content = JsonContent.Create(new
|
||||
{
|
||||
GeoPoint = new { Lat = 50.45m, Lon = 30.52m, Mgrs = (string?)null },
|
||||
WaypointSource = 0,
|
||||
WaypointObjective = 0,
|
||||
OrderNum = 99,
|
||||
Height = 120m
|
||||
})
|
||||
};
|
||||
http.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Jwt);
|
||||
using var response = await Missions.SendAsync(http);
|
||||
|
||||
// Assert
|
||||
await HttpAssertions.AssertStatusAsync(response, HttpStatusCode.Created);
|
||||
var waypoint = await response.Content.ReadFromJsonAsync<WaypointDto>() ?? throw new InvalidOperationException("waypoint body deserialized to null");
|
||||
Assert.Equal(50.45m, waypoint.Lat);
|
||||
Assert.Equal(30.52m, waypoint.Lon);
|
||||
Assert.Null(waypoint.Mgrs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Traces", "AC-4.4")]
|
||||
[Trait("max_ms", "2000")]
|
||||
[Trait("carry_forward", "waypoint-response-flat-vs-nested-geo")]
|
||||
public async Task FT_P_15_waypoint_update_is_full_overwrite_height_zero_geofields_cleared()
|
||||
{
|
||||
// CARRY-FORWARD: same flat-vs-nested divergence as FT-P-14. The "full
|
||||
// overwrite" semantic IS pinned: send Height:0 and assert the prior
|
||||
// Height:120 is replaced; send geo nullable fields and assert they
|
||||
// become null.
|
||||
|
||||
// Arrange
|
||||
DbResetFixture.ResetDatabase(TestEnvironment.DbSideChannel);
|
||||
Seeds.Apply(Seeds.FiveWaypointsUnordered.Sql);
|
||||
var missionId = Seeds.FiveWaypointsUnordered.MissionId;
|
||||
|
||||
var targetWaypoint = await GetSeededWaypointAsync(missionId);
|
||||
// Sanity check the seed shape — the original Height for a seed row
|
||||
// is 100/110/120/130/140; pick whichever waypoint has Height==120.
|
||||
var token = await Tokens.MintDefaultAsync();
|
||||
|
||||
// Act
|
||||
using var http = new HttpRequestMessage(
|
||||
HttpMethod.Put,
|
||||
$"/missions/{missionId}/waypoints/{targetWaypoint.Id}")
|
||||
{
|
||||
Content = JsonContent.Create(new
|
||||
{
|
||||
GeoPoint = (object?)null,
|
||||
WaypointSource = 1,
|
||||
WaypointObjective = 1,
|
||||
OrderNum = targetWaypoint.OrderNum + 100,
|
||||
Height = 0m
|
||||
})
|
||||
};
|
||||
http.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Jwt);
|
||||
using var response = await Missions.SendAsync(http);
|
||||
|
||||
// Assert
|
||||
await HttpAssertions.AssertStatusAsync(response, HttpStatusCode.OK);
|
||||
var updated = await response.Content.ReadFromJsonAsync<WaypointDto>() ?? throw new InvalidOperationException("waypoint body deserialized to null");
|
||||
Assert.Equal(0m, updated.Height);
|
||||
Assert.Equal(targetWaypoint.OrderNum + 100, updated.OrderNum);
|
||||
Assert.Null(updated.Lat);
|
||||
Assert.Null(updated.Lon);
|
||||
Assert.Null(updated.Mgrs);
|
||||
Assert.Equal(1, updated.WaypointSource);
|
||||
Assert.Equal(1, updated.WaypointObjective);
|
||||
}
|
||||
|
||||
private async Task<WaypointDto> GetSeededWaypointAsync(Guid missionId)
|
||||
{
|
||||
var token = await Tokens.MintDefaultAsync();
|
||||
using var http = new HttpRequestMessage(HttpMethod.Get, $"/missions/{missionId}/waypoints");
|
||||
http.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Jwt);
|
||||
using var resp = await Missions.SendAsync(http);
|
||||
resp.EnsureSuccessStatusCode();
|
||||
var list = await resp.Content.ReadFromJsonAsync<List<WaypointDto>>() ?? throw new InvalidOperationException("waypoints list deserialized to null");
|
||||
return list.First(w => w.OrderNum == 1);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Azaion.Missions.E2E.Tests.Waypoints;
|
||||
|
||||
/// <summary>
|
||||
/// Discovery-only smoke test for the Waypoints category. Real Waypoints
|
||||
/// scenarios (FT-P-13..15, FT-P-18, FT-N-07) land in AZ-579.
|
||||
/// </summary>
|
||||
public sealed class Sanity
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", "Blackbox")]
|
||||
[Trait("Traces", "AC-3")]
|
||||
public void Discovery_smoke_test_runs()
|
||||
{
|
||||
// Arrange
|
||||
const int sentinel = 1;
|
||||
// Act
|
||||
var result = sentinel + 0;
|
||||
// Assert
|
||||
Assert.Equal(1, result);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user