using System.Net; using System.Net.Http.Headers; using System.Text.Json; using Azaion.Missions.E2E.Fixtures; using Azaion.Missions.E2E.Helpers; using Xunit; namespace Azaion.Missions.E2E.Tests.Missions; /// /// FT-P-12 — mission cascade delete walks every dependency table. /// Owns its own xUnit collection (CascadeF3) because the F3 fixture /// is destructive and must run with a fresh DB per scenario. /// Compares per-table counts against /// _docs/00_problem/input_data/expected_results/cascade_F3_walk.json /// via deep JSON diff (results_report.md row 3.1). /// [Collection("CascadeF3")] [Trait("Category", "Blackbox")] [Trait("db_access", "seed-or-assert-only")] public sealed class CascadeF3Tests : TestBase, IClassFixture { public CascadeF3Tests(CascadeF3Fixture _) { /* fixture seeds the DB. */ } [Fact] [Trait("Traces", "AC-3.1")] [Trait("max_ms", "10000")] public async Task FT_P_12_mission_cascade_walks_every_dependency_table() { // Arrange — load the canonical walk JSON to assert pre-state and post-state. var walkJson = JsonDocument.Parse(File.ReadAllText( Path.Combine( Environment.GetEnvironmentVariable("FIXTURE_SQL_DIR") ?? "/app/fixtures", "..", // expected_results/.. == input_data "expected_results", "cascade_F3_walk.json"))); var preState = walkJson.RootElement.GetProperty("expected_per_table_pre_state_for_safety_check"); // Refresh the F3 fixture into a known state — IClassFixture seeds once // per class, but we want a clean walk for this single scenario. DbResetFixture.ResetDatabase(TestEnvironment.DbSideChannel); StubSchema.EnsureCreated(); Seeds.Apply(FixtureSql.Load("fixture_cascade_F3")); // Sanity-check the pre-state — if the seed fixture failed silently, the // post-state assertions would trivially pass and mask the failure. Assert.Equal(preState.GetProperty("missions").GetInt32(), (int)DbAssertions.TableRowCount("missions")); Assert.Equal(preState.GetProperty("waypoints").GetInt32(), (int)DbAssertions.TableRowCount("waypoints")); Assert.Equal(preState.GetProperty("map_objects").GetInt32(), (int)DbAssertions.TableRowCount("map_objects")); Assert.Equal(preState.GetProperty("media").GetInt32(), (int)DbAssertions.TableRowCount("media")); Assert.Equal(preState.GetProperty("annotations").GetInt32(), (int)DbAssertions.TableRowCount("annotations")); Assert.Equal(preState.GetProperty("detection").GetInt32(), (int)DbAssertions.TableRowCount("detection")); var token = await Tokens.MintDefaultAsync(); // Act using var http = new HttpRequestMessage( HttpMethod.Delete, $"/missions/{CascadeF3Fixture.MissionId}"); http.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Jwt); using var response = await Missions.SendAsync(http); // Assert await HttpAssertions.AssertStatusAsync(response, HttpStatusCode.NoContent); var bodyLength = (await response.Content.ReadAsByteArrayAsync()).Length; Assert.Equal(0, bodyLength); // The walk JSON pins per-table post-state filters; assert each one. var postState = walkJson.RootElement.GetProperty("expected_per_table_post_state"); AssertCount("missions", "id = '22222222-0000-0000-0000-000000000001'", 0); AssertCount("waypoints", "mission_id = '22222222-0000-0000-0000-000000000001'", 0); AssertCount("map_objects", "mission_id = '22222222-0000-0000-0000-000000000001'", 0); AssertCount("media", "id IN ('media-fixture-001', 'media-fixture-002')", 0); AssertCount("annotations", "id IN ('anno-fixture-001', 'anno-fixture-002')", 0); AssertCount("detection", "annotation_id IN ('anno-fixture-001', 'anno-fixture-002')", 0); // Sanity: the walk JSON has the same expectations we just asserted — fail // loudly if the JSON is out of sync with the in-source filters. Assert.Equal(0, postState.GetProperty("missions").GetProperty("expected_count").GetInt32()); } private static void AssertCount(string table, string filterSql, long expected) { if (!table.All(c => char.IsLetterOrDigit(c) || c == '_')) throw new ArgumentException($"unsafe table identifier '{table}'", nameof(table)); var actual = DbAssertions.ScalarCount($"SELECT COUNT(*) FROM {table} WHERE {filterSql}"); Assert.Equal(expected, actual); } }