# 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 `missions` service via HTTP + Npgsql side-channel and pass. - Each test produces a CSV row with `Category=Blackbox`, `Traces=AC-1.x`, `Result=pass`, and an `ExecutionTimeMs` under the documented `Max execution time` (5s for create paths, 2s for read/delete). - The list test asserts both shape (`array` not `PaginatedResponse`) and ordering (`Name ASC`). - The filter test asserts case-INSENSITIVE matching for two casings (`BR` and `br`). - The default-clear invariant is verified via DB count (`is_default=true` count == 1 after every default-creating action). ## Scope ### Included - FT-P-01 Create non-default — `POST /vehicles` body shape + PascalCase response + DB row count. - FT-P-02 Create default demotes prior default — `seed_one_default_vehicle` precondition; 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=true` then `name=br&…` — assert case-INSENSITIVE substring match against `seed_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`, negative `BatteryCapacity`, unknown `Type` int) are carry-forwards documented in `test-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=` 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 time` from `blackbox-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-test `Timeout` must reflect this. **Reliability** - Tests share a `[Collection("Vehicles")]` xUnit collection and use `IClassFixture` 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 to `Azaion.Missions.csproj`). - Bearer token minted via `https://jwks-mock:8443/sign` with `permissions=FL`. - DB assertions through the Npgsql side-channel only; marked `[Trait("db_access","seed-or-assert-only")]`. - AAA pattern with `// Arrange` / `// Act` / `// Assert` comments per `coderule.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 `vehicles` column set, hand-rolled `INSERT` in the seed fixture breaks. - *Mitigation*: Seed fixtures use the schema produced by the SUT's own startup migrator — `docker compose up` runs 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 the `OrderBy(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.md` rows AC-1 1.1, 1.2, 1.4, 1.5, 1.6, 1.10. - Stubs are allowed ONLY for the external `admin` JWT issuer (the `jwks-mock` container per `tests/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`, or `ErrorHandlingMiddleware`. 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.