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>
9.7 KiB
Missions Positive Flow Tests
Task: AZ-578_test_missions_positive Name: Missions positive tests (FT-P-07..12) Description: Implement xUnit blackbox tests for the 6 happy-path Mission scenarios — create with default CreatedDate, paginated list (PageSize=20, CreatedDate DESC, case-INSENSITIVE name filter), page 2, date-range filter, partial update preserving null fields, and full cascade delete across map_objects/detection/annotations/media/waypoints/missions. Complexity: 5 points Dependencies: AZ-576_test_infrastructure Component: Blackbox Tests Tracker: AZ-578 Epic: AZ-575
Problem
The /missions surface is the project's most consequential read+write path. Three behaviours are easy to silently break: (1) the default CreatedDate = UtcNow when the body omits it (AC-2.1), (2) PaginatedResponse<Mission> envelope with Page,PageSize,TotalCount,Items PascalCase keys + CreatedDate DESC ordering (AC-2.3), and (3) the cascade delete walking every dependency table including DB-only stub tables map_objects, detection, annotations, media (AC-3.1). The cascade is not transaction-wrapped (NFT-RES-01 in Task 16 pins that invariant); the positive scenario here verifies the happy-path walk completes.
Outcome
- All six FT-P-07..12 scenarios run against the dockerised
missionsservice and pass. - Each test produces a CSV row with
Category=Blackbox,Traces=AC-2.xorAC-3.1,Result=pass, within the documentedMax execution time(5s for create, 2s for list/update, 10s for cascade delete). - The pagination test asserts both the envelope shape (
Items, TotalCount, Page, PageSizePascalCase) ANDCreatedDateDESC ordering across all 20 items. - The cascade test compares per-table delete counts against
_docs/00_problem/input_data/expected_results/cascade_F3_walk.jsonviajson_diff.
Scope
Included
- FT-P-07 Mission create with default CreatedDate — assert
|body.CreatedDate - t0| ≤ 5s. - FT-P-08 Mission list default page — envelope shape,
Page==1,PageSize==20,TotalCount==25,Items.length==20,CreatedDateDESC ordering, plus case-INSENSITIVE?name=refilter. - FT-P-09 Mission list page 2 —
Page==2,Items.length==5, UUID-set disjoint from page 1. - FT-P-10 Mission list date range —
?fromDate=&toDate=inclusivity (January 2026 returns 5 of 25). - FT-P-11 Mission partial update —
PUT /missions/{id}withVehicleId:nullpreserves priorVehicleId. - FT-P-12 Mission cascade delete (F3) —
DELETE /missions/{id}walks every dependency table; per-table counts compared againstcascade_F3_walk.json.
Excluded
- FT-N-04 "create mission with non-existent VehicleId returns 400" lives in Task 13.
- FT-N-05 "GET mission 404" lives in Task 13.
- FT-N-06 "cascade delete short-circuits on missing mission (no DELETE issued against dependency tables)" lives in Task 13.
- Cascade NOT-transaction-wrapped invariant (NFT-RES-01) lives in Task 16.
Acceptance Criteria
AC-1: FT-P-07 mission create defaults CreatedDate to UtcNow
Given seed_one_default_vehicle and a JWT with permissions=FL
When the consumer captures t0 = UtcNow then issues POST /missions { Name:"Recon-01", VehicleId:<id>, CreatedDate:null }
Then response is 201, body.CreatedDate parses as UTC, and abs(body.CreatedDate - t0) ≤ 5s
AC-2: FT-P-08 list returns PaginatedResponse with DESC ordering and case-INSENSITIVE name filter
Given seed_25_missions (5 January, 20 February 2026, mix of Recon-* names)
When GET /missions is issued
Then response is 200 with Page==1, PageSize==20, TotalCount==25, Items.length==20, all PascalCase keys, AND for every i ∈ [0..18] Items[i].CreatedDate >= Items[i+1].CreatedDate (strictly DESC ordering)
And when GET /missions?name=re (lowercase) is issued, body.TotalCount > 0 (case-INSENSITIVE substring match against Recon-*)
AC-3: FT-P-09 page 2 returns the remaining 5 items, disjoint from page 1
Given seed_25_missions
When GET /missions?page=2&pageSize=20 is issued
Then response is 200, Page==2, Items.length==5, AND the set of Items[*].Id is disjoint from the page-1 response
AC-4: FT-P-10 date range filter is inclusive of bounds
Given seed_25_missions (5 in January 2026, 20 in February 2026)
When GET /missions?fromDate=2026-01-01T00:00:00Z&toDate=2026-01-31T23:59:59Z is issued
Then response is 200, TotalCount==5, and every Items[i].CreatedDate is within January 2026 UTC
AC-5: FT-P-11 partial update preserves null fields
Given one mission row with known Name="Original" and VehicleId=V1
When PUT /missions/{id} { Name:"Renamed", VehicleId:null } is issued
Then response is 200, body.Name == "Renamed", AND body.VehicleId == V1 (preserved)
AC-6: FT-P-12 cascade delete walks every dependency table
Given fixture_cascade_F3 applied (one mission with 2 waypoints → 2 media → 2 annotations → 2 detection rows + 3 map_objects)
When DELETE /missions/{mid} is issued
Then response is 204, AND side-channel SELECT COUNT(*) returns 0 for map_objects, detection, annotations, media, waypoints, missions rows in the seeded chain
And the per-table counts after deletion match _docs/00_problem/input_data/expected_results/cascade_F3_walk.json via deep JSON diff
Non-Functional Requirements
Performance
- FT-P-07: ≤ 5s. FT-P-08..11: ≤ 2s each. FT-P-12: ≤ 10s (cascade through 5 tables).
Reliability
- FT-P-12 must use
IClassFixture<DbResetFixture>that recreatesfixture_cascade_F3fresh per scenario (the fixture is destructive). FT-P-08..10 shareseed_25_missionsacross the same class.
Blackbox Tests
| AC Ref | Initial Data/Conditions | What to Test | Expected Behavior | NFR References |
|---|---|---|---|---|
| AC-1 | seed_one_default_vehicle |
POST /missions { CreatedDate:null } |
201 + |body.CreatedDate - t0| ≤ 5s |
AC-2.1 |
| AC-2 | seed_25_missions |
GET /missions then GET /missions?name=re |
200 + envelope + DESC + case-INSENSITIVE match |
AC-2.3, AC-8.7 |
| AC-3 | seed_25_missions |
GET /missions?page=2&pageSize=20 |
200 + Page=2 + len 5 + disjoint UUIDs |
AC-2.3 |
| AC-4 | seed_25_missions |
GET /missions?fromDate=…&toDate=… (January window) |
200 + TotalCount=5 + all in window |
AC-2.3 |
| AC-5 | One row with Name=Original, VehicleId=V1 |
PUT /missions/{id} { Name:"Renamed", VehicleId:null } |
200 + Name updated + VehicleId preserved |
AC-2.5 |
| AC-6 | fixture_cascade_F3 |
DELETE /missions/{mid} |
204 + DB counts 0 across 6 tables + cascade_F3_walk.json match |
AC-3.1 |
Constraints
- HTTP only against
http://missions:8080/missions*(no project reference toAzaion.Missions.csproj). - Bearer token minted via
https://jwks-mock:8443/signwithpermissions=FL. - FT-P-12 fixture uses the SQL file at
_docs/00_problem/input_data/expected_results/fixture_cascade_F3.sql(NOT a hand-rolled INSERT — the SQL file is the contract). - Per-table count comparison in FT-P-12 uses
json_diffagainstcascade_F3_walk.json; if the file is missing, the test must fail (not silently pass). - AAA pattern with
// Arrange/// Act/// Assertper test. seed_25_missionsMUST use deterministic UUIDs and deterministicCreatedDatevalues so the disjoint-set assertion in AC-3 and the date-range assertion in AC-4 are reproducible.
Risks & Mitigation
Risk 1: cascade_F3_walk.json drifts from fixture_cascade_F3.sql
- Risk: Updating the seed SQL without updating the walk JSON makes AC-6 silently pass with wrong counts.
- Mitigation: Both files live under the same
expected_results/directory; the test loads the walk JSON at runtime and verifies BOTH that pre-delete counts match the walk'sbeforevalues AND post-delete counts match the walk'saftervalues. A drift fails the "before" assertion first.
Risk 2: AC-2 ordering assertion is flaky if seed CreatedDate values collide
- Risk: Two missions with identical
CreatedDateproduce a tie-breaker-dependent order; the DESC assertion would be deterministic only if the comparator is stable. - Mitigation:
seed_25_missionsSQL assigns distinctCreatedDatevalues spaced ≥ 1 second apart; any future seed change must preserve this invariant.
Risk 3: cascade test pollutes neighbour scenarios
- Risk: F3 fixture deletes rows across 6 tables; if FT-P-12 runs in the same xUnit class as a read-path test, that test sees an empty DB.
- Mitigation: FT-P-12 lives in its own xUnit
[Collection("CascadeF3")]and usesIClassFixture<DbResetFixture>to reset between every scenario in the class.
System Under Test Boundary
- Tests drive the product through the public HTTP surface (
http://missions:8080/missions*) plus the documented DB side-channel for fixture seeding and post-call assertions. Expected outputs are compared against_docs/00_problem/input_data/expected_results/results_report.mdrows AC-2 2.1, 2.3, 2.4, 2.5, 2.7 and AC-3 row 3.1, and against the machine-readable file_docs/00_problem/input_data/expected_results/cascade_F3_walk.jsonfor the cascade walk. - Stubs are allowed ONLY for: the external
adminJWT issuer (jwks-mockcontainer) and the DB-only stub tables formedia,annotations,detection,map_objects(seeded via side-channel SQL because the owning services are out of scope perenvironment.md). - Stubs, fakes, deterministic fallbacks, monkeypatches, or direct imports are NOT allowed for any internal product module — including
MissionService,MissionsController,WaypointService,AppDataConnection,DatabaseMigrator,JwtExtensions, orErrorHandlingMiddleware. If any of these is not implemented, the test MUST fail/block as missing product implementation — it must not pass by replacing the module with a test stub.