mirror of
https://github.com/azaion/missions.git
synced 2026-06-22 17:11: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,114 @@
|
||||
# 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=<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 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<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 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.
|
||||
Reference in New Issue
Block a user