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>
8.2 KiB
Vehicles Positive Flow Tests
Task: AZ-577_test_vehicles_positive Name: Vehicles positive tests (FT-P-01..06) Description: Implement xUnit blackbox tests for the 6 happy-path Vehicle CRUD scenarios — create non-default, create default (demotes prior), setDefault, list (no-pagination + Name ASC), filter (case-INSENSITIVE name + exact isDefault), delete with no references. Complexity: 5 points Dependencies: AZ-576_test_infrastructure Component: Blackbox Tests Tracker: AZ-577 Epic: AZ-575
Problem
The /vehicles surface implements two non-obvious invariants that documentation alone cannot guarantee: (1) creating a default vehicle clears any prior default in the same logical step, and (2) the list filter is case-INSENSITIVE on name (the docs said case-sensitive until 2026-05-14 — drift now corrected, but only an executable test can pin the actual code path). Without these tests, a future refactor of VehicleService could silently re-introduce two default rows or a case-sensitive filter and break consumers (autopilot reads the default vehicle on boot).
Outcome
- All six FT-P-01..06 scenarios run against the dockerised
missionsservice via HTTP + Npgsql side-channel and pass. - Each test produces a CSV row with
Category=Blackbox,Traces=AC-1.x,Result=pass, and anExecutionTimeMsunder the documentedMax execution time(5s for create paths, 2s for read/delete). - The list test asserts both shape (
arraynotPaginatedResponse) and ordering (Name ASC). - The filter test asserts case-INSENSITIVE matching for two casings (
BRandbr). - The default-clear invariant is verified via DB count (
is_default=truecount == 1 after every default-creating action).
Scope
Included
- FT-P-01 Create non-default —
POST /vehiclesbody shape + PascalCase response + DB row count. - FT-P-02 Create default demotes prior default —
seed_one_default_vehicleprecondition; assert exactly one default after. - FT-P-03 setDefault promotes existing vehicle —
POST /vehicles/{id}/setDefault; assert clear-then-set via side-channel. - FT-P-04 List unpaginated + Name ASC — assert body is JSON array (not
{Items,Page,…}), assert length and ordering. - FT-P-05 Filter
name=BR&isDefault=truethenname=br&…— assert case-INSENSITIVE substring match againstseed_3_vehicles_2_default. - FT-P-06 Delete with no references —
204+ DB count 0.
Excluded
- FT-N-03 "delete vehicle in use returns 409" lives in Task 13 (negative tests).
- Validation-of-input scenarios (empty
Name, negativeBatteryCapacity, unknownTypeint) are carry-forwards documented intest-data.md§ Data Validation Rules; they are NOT tested here because the spec marks them as "accepted today" — they belong to the Refactor Backlog, not this task. - TOCTOU race on default-vehicle exclusivity (NFT-RES-08) lives in Task 17.
Acceptance Criteria
AC-1: FT-P-01 returns 201 with PascalCase body
Given seed_empty and a JWT with permissions=FL
When POST /vehicles is issued with the documented body
Then response is 201 Created, body parses as Vehicle with PascalCase keys, Id parses as UUID, side-channel SELECT COUNT(*) FROM vehicles WHERE id=<returned> returns 1
AC-2: FT-P-02 demotes prior default
Given seed_one_default_vehicle (prior row P1.is_default=true)
When POST /vehicles { …, IsDefault:true } is issued
Then response is 201, side-channel shows new row is_default=true, row P1.is_default=false, and SELECT COUNT(*) WHERE is_default=true == 1
AC-3: FT-P-03 setDefault clears prior
Given seed_one_default_vehicle plus a non-default row P2
When POST /vehicles/{P2}/setDefault { IsDefault:true } is issued
Then response is 200 with Id==P2, IsDefault==true, and side-channel shows P2.is_default=true, P1.is_default=false, count==1
AC-4: FT-P-04 list is unpaginated and ordered
Given seed_3_vehicles_2_default containing BR-01, BR-02, MQ-9 in any insert order
When GET /vehicles is issued
Then response is 200, body parses as a JSON array (NOT an object with Items), body.length == 3, and [v.Name for v in body] == ["BR-01","BR-02","MQ-9"]
AC-5: FT-P-05 filter is case-INSENSITIVE
Given seed_3_vehicles_2_default
When GET /vehicles?name=BR&isDefault=true AND GET /vehicles?name=br&isDefault=true are issued
Then both responses are 200 with body.length == 1 and body[0].Name == "BR-01"
AC-6: FT-P-06 delete is 204 + row gone
Given one vehicle row with no missions referencing it
When DELETE /vehicles/{id} is issued
Then response is 204 No Content with empty body, and side-channel shows count == 0 for that id
Non-Functional Requirements
Performance
- Each test must complete inside the documented
Max execution timefromblackbox-tests.md(5s for FT-P-01..03, 5s for FT-P-07-style writes, 2s for FT-P-04..06). The xUnit[Trait("max_ms", "5000")]or per-testTimeoutmust reflect this.
Reliability
- Tests share a
[Collection("Vehicles")]xUnit collection and useIClassFixture<DbResetFixture>to TRUNCATE between scenarios. No state must leak between FT-P-01 and FT-P-04.
Blackbox Tests
| AC Ref | Initial Data/Conditions | What to Test | Expected Behavior | NFR References |
|---|---|---|---|---|
| AC-1 | seed_empty, JWT permissions=FL |
POST /vehicles non-default body |
201 + PascalCase Vehicle + DB count 1 |
— |
| AC-2 | seed_one_default_vehicle (P1) |
POST /vehicles { IsDefault:true } |
201 + DB shows count==1 default after |
AC-1.2 invariant |
| AC-3 | seed_one_default_vehicle + extra P2 |
POST /vehicles/{P2}/setDefault |
200 + DB count==1 default; P1 cleared |
AC-1.2 / AC-1.4 |
| AC-4 | seed_3_vehicles_2_default (BR-01,BR-02,MQ-9) |
GET /vehicles shape + order |
200 + array + Name ASC |
AC-1.5 |
| AC-5 | seed_3_vehicles_2_default |
GET /vehicles?name=BR… + ?name=br… |
200 + len 1 + BR-01 for both casings |
AC-1.6 |
| AC-6 | One row, zero missions | DELETE /vehicles/{id} |
204 + DB count 0 |
AC-1.10 |
Constraints
- HTTP only against
http://missions:8080(no project reference toAzaion.Missions.csproj). - Bearer token minted via
https://jwks-mock:8443/signwithpermissions=FL. - DB assertions through the Npgsql side-channel only; marked
[Trait("db_access","seed-or-assert-only")]. - AAA pattern with
// Arrange/// Act/// Assertcomments percoderule.mdc. - PascalCase JSON contract (
PropertyNamingPolicy = null) is part of the SUT contract; the test must NOT silently accept camelCase.
Risks & Mitigation
Risk 1: Tests depend on side-channel SQL that drifts from the SUT migrator
- Risk: If the migrator changes the
vehiclescolumn set, hand-rolledINSERTin the seed fixture breaks. - Mitigation: Seed fixtures use the schema produced by the SUT's own startup migrator —
docker compose upruns first, then the fixture inserts into the already-migrated tables.
Risk 2: Ordering test (AC-4) is flaky if insert order accidentally matches alphabetic order
- Risk: A non-deterministic seed insert could mask a missing
OrderBy. - Mitigation: Seed fixture inserts rows in
[MQ-9, BR-02, BR-01]order (reverse alphabetic) so the test fails if the SUT omits theOrderBy(a => a.Name).
System Under Test Boundary
- Tests drive the product through the public HTTP surface (
http://missions:8080/vehicles*) 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-1 1.1, 1.2, 1.4, 1.5, 1.6, 1.10. - Stubs are allowed ONLY for the external
adminJWT issuer (thejwks-mockcontainer pertests/Azaion.Missions.JwksMock/). - Stubs, fakes, monkeypatches, deterministic fallbacks, or direct imports are NOT allowed for any internal product module — including
VehicleService,VehiclesController,AppDataConnection,DatabaseMigrator,JwtExtensions, orErrorHandlingMiddleware. If any of these is not implemented (e.g., the SUT image hasn't been built), the test MUST fail/block as missing product implementation — it must not pass by replacing the module with a test stub.