[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:
Oleksandr Bezdieniezhnykh
2026-05-15 08:28:37 +03:00
parent 3c5354e56c
commit 6b2c2d998e
29 changed files with 1951 additions and 95 deletions
@@ -0,0 +1,134 @@
# Validation + 404 + Authz Negative Tests
**Task**: AZ-580_test_validation_authz_negative
**Name**: Functional negative tests (FT-N-01..08)
**Description**: Implement xUnit blackbox tests for the 8 negative scenarios — case-insensitive filter no-match, 404 for missing GET vehicle/mission/waypoint-parent, 409 for delete-vehicle-in-use, 400 for create-mission-with-bogus-VehicleId (carry-forward divergence), cascade short-circuit on missing mission (no dependency DELETEs issued), and the generic 500 redacted-body + stacktrace-in-log contract.
**Complexity**: 3 points
**Dependencies**: AZ-576_test_infrastructure
**Component**: Blackbox Tests
**Tracker**: AZ-580
**Epic**: AZ-575
## Problem
The negative-path contract is what protects clients from undefined behaviour: every documented failure must produce a predictable status code + `{ statusCode, message }` envelope, and no failure mode may silently mutate state. Three behaviors are especially load-bearing: (1) `DELETE /missions/{missing}` must 404 *before* any dependency-table DELETE issues — otherwise a typo'd UUID could remove rows from `map_objects` belonging to a different mission (AC-3.2); (2) `DELETE /vehicles/{used}` must 409 and leave the row in place (AC-1.8); (3) the generic 500 must redact internals — `Internal server error` body, full stack only in container logs (AC-8.6, AC-10.3).
## Outcome
- All eight FT-N-01..08 scenarios run against the dockerised `missions` service and pass.
- Each test produces a CSV row with `Category=Blackbox` (negative subset; `Traces=AC-1.6, AC-1.7, AC-1.8, AC-2.2, AC-2.4, AC-3.2, AC-4.2, AC-8.6, AC-10.3`), `Result=pass`.
- The 500 test asserts BOTH that the body is exactly `{ "statusCode":500, "message":"Internal server error" }` AND that the container log emitted an `"Unhandled exception"` line within 2s.
- FT-N-06 asserts via `pg_stat_statements` (or post-request log scrape) that NO `DELETE FROM map_objects/waypoints/media/annotations/detection` SQL ran during the 404 request — the existence check short-circuits before the cascade.
- FT-N-04 explicitly pins the documented spec-divergence (returns 400 today, spec wants 404); test must include a comment marking it as a carry-forward to revisit when the divergence is closed.
## Scope
### Included
- FT-N-01 Vehicle name filter no-match — `?name=ZZ` and `?name=zz` against `seed_3_vehicles_2_default` both return `body.length == 0`.
- FT-N-02 GET vehicle 404 — random UUID returns `{ statusCode:404, message:… }`.
- FT-N-03 Delete vehicle in use 409 — row not deleted afterwards.
- FT-N-04 Create mission with bogus VehicleId returns 400 today (CARRY-FORWARD comment).
- FT-N-05 GET mission 404 — envelope shape.
- FT-N-06 Cascade short-circuit — 404 + zero DELETE SQL issued.
- FT-N-07 Waypoint operation against missing mission — 404.
- FT-N-08 Generic 500 — redacted body + stacktrace in log.
### Excluded
- 401 / 403 auth-failure paths (NFT-SEC-01..06) live in Task 14.
- 400/422 spec-divergence carry-forwards that are NOT executable today (input validation for empty `Name`, negative `BatteryCapacity`, unknown `Type` int) are documented as Refactor Backlog items in `tests/blackbox-tests.md` and are NOT in scope here.
## Acceptance Criteria
**AC-1: FT-N-01 vehicle filter no-match returns empty array for both casings**
Given `seed_3_vehicles_2_default` (`BR-01, BR-02, MQ-9`)
When `GET /vehicles?name=ZZ` then `GET /vehicles?name=zz` are issued
Then both responses are `200` with `body.length == 0`
**AC-2: FT-N-02 GET vehicle 404 returns the standard envelope**
Given any DB state and a valid JWT
When `GET /vehicles/{random uuid}` is issued
Then response is `404` with body parsing to JSON object having EXACTLY the keys `statusCode` and `message`, and `statusCode == 404`
**AC-3: FT-N-03 delete in-use vehicle returns 409 and leaves row**
Given one vehicle and ≥ 1 mission referencing it
When `DELETE /vehicles/{id}` is issued
Then response is `409` with envelope `{ statusCode:409, message:<non-empty> }`, and side-channel `SELECT COUNT(*) FROM vehicles WHERE id={id}` returns `1`
**AC-4: FT-N-04 create mission with bogus VehicleId returns 400 today (carry-forward)**
Given `seed_empty`
When `POST /missions { Name:"x", VehicleId:<random uuid>, CreatedDate:null }` is issued
Then response is `400` with envelope (carry-forward: spec wants 404; the test must include a `// CARRY-FORWARD: expected to flip to 404 when AC-2.2 divergence is closed` comment)
And side-channel `SELECT COUNT(*) FROM missions` returns `0`
**AC-5: FT-N-05 GET mission 404 returns the standard envelope**
Given any DB state and a valid JWT
When `GET /missions/{random uuid}` is issued
Then response is `404` with envelope `{ statusCode:404, message:<non-empty> }`
**AC-6: FT-N-06 cascade short-circuit issues zero dependency-table DELETEs**
Given `fixture_cascade_F3` (seeded chain rooted at `mid`) and a `postgres-test` started with `log_statement=all`
When `DELETE /missions/{mid'}` (random UUID, not `mid`) is issued
Then response is `404`, side-channel `SELECT COUNT(*) FROM map_objects` is unchanged, AND the `postgres-test` log (or `pg_stat_statements`) shows NO `DELETE FROM map_objects/waypoints/media/annotations/detection` SQL emitted by the request connection
**AC-7: FT-N-07 waypoint operation against missing mission returns 404**
Given any DB state and a valid JWT
When `GET /missions/{random uuid}/waypoints` is issued
Then response is `404` with envelope `{ statusCode:404, message:<non-empty> }`
**AC-8: FT-N-08 generic 500 redacts body, stacktrace lands in log**
Given side-channel has executed `DROP TABLE vehicles CASCADE`
When `GET /vehicles/{any uuid}` is issued with JWT `FL`
Then response is `500` with body EXACTLY `{ "statusCode":500, "message":"Internal server error" }`
And `docker logs missions-sut` contains an `"Unhandled exception"` line emitted ≤ 2s after the request timestamp, containing the exception type name (`PostgresException` or similar)
## Non-Functional Requirements
**Performance**
- FT-N-01..05, FT-N-07: ≤ 2s each. FT-N-06: ≤ 5s. FT-N-08: ≤ 5s (allow log scrape).
**Reliability**
- FT-N-06 requires `postgres-test` to be started with `log_statement=all` (`command: ["postgres", "-c", "log_statement=all"]` overlay in `docker-compose.test.yml`, OR `ALTER SYSTEM SET` via side-channel in the fixture). The test must FAIL if logging is not enabled — not silently pass.
- FT-N-08 is destructive (drops the `vehicles` table). It MUST run in its own xUnit `[Collection("ErrorEnvelope500")]` with `ComposeRestartFixture` teardown (full `down -v && up -d`).
## Blackbox Tests
| AC Ref | Initial Data/Conditions | What to Test | Expected Behavior | NFR References |
|--------|------------------------|-------------|-------------------|----------------|
| AC-1 | `seed_3_vehicles_2_default` | `?name=ZZ` then `?name=zz` | `200` + `body.length == 0` for both | AC-1.6 |
| AC-2 | any | `GET /vehicles/{random}` | `404` + envelope | AC-1.7, AC-8.2 |
| AC-3 | Vehicle + mission referencing it | `DELETE /vehicles/{id}` | `409` + row preserved | AC-1.8, AC-8.5 |
| AC-4 | `seed_empty` | `POST /missions { VehicleId:<random> }` | `400` (today) + no row written + carry-forward comment | AC-2.2 |
| AC-5 | any | `GET /missions/{random}` | `404` + envelope | AC-2.4, AC-8.2 |
| AC-6 | `fixture_cascade_F3` + PG logging on | `DELETE /missions/{random}` | `404` + zero dependency-table DELETE SQL | AC-3.2 |
| AC-7 | any | `GET /missions/{random}/waypoints` | `404` + envelope | AC-4.2 |
| AC-8 | side-channel DROPped vehicles | `GET /vehicles/{any}` | `500` + redacted body + stacktrace logged within 2s | AC-8.6, AC-10.3 |
## Constraints
- HTTP only against `http://missions:8080`; bearer token via `https://jwks-mock:8443/sign` with `permissions=FL`.
- FT-N-06 requires Postgres logging mode `log_statement=all`; the fixture must verify (via `SHOW log_statement`) that logging is on BEFORE running the test — fail in Arrange if not.
- FT-N-08 fixture teardown must restart the compose stack (`down -v && up -d`); subsequent tests would otherwise hit a missing table.
- AAA pattern with `// Arrange` / `// Act` / `// Assert` per test.
- Carry-forward comments (FT-N-04) are required so future spec-vs-code work knows where to update.
## Risks & Mitigation
**Risk 1: FT-N-06 false-pass when PG logging is off**
- *Risk*: If `postgres-test` runs without `log_statement=all`, the "no DELETE issued" assertion trivially passes — the log is empty.
- *Mitigation*: Arrange phase runs `SHOW log_statement` via side-channel and fails fast if the result is not `"all"`. The compose overlay setting this MUST be loaded.
**Risk 2: FT-N-08 leaves the SUT in a broken state**
- *Risk*: After `DROP TABLE vehicles CASCADE`, every subsequent test against `/vehicles` returns 500 until the migrator re-creates the table on next startup.
- *Mitigation*: Fixture runs `docker compose -f docker-compose.test.yml down -v && up -d` in teardown; subsequent tests wait for `missions` to reach `healthy`.
**Risk 3: FT-N-04 expectation flips silently when spec divergence closes**
- *Risk*: When the spec-aligned 404 lands, this test will fail with a status mismatch — and the test author needs context to know it's intentional.
- *Mitigation*: The test includes a `// CARRY-FORWARD: AC-2.2 — expected to flip to 404 when bogus-VehicleId divergence is closed` source-level comment AND `[Trait("carry_forward", "AC-2.2")]` so a future filter can find it.
## System Under Test Boundary
- Tests drive the product through the public HTTP surface (`http://missions:8080/{vehicles,missions}*`) plus the documented DB side-channel for fixture seeding, post-call assertions, and (for FT-N-06) reading `pg_stat_statements` / Postgres log lines, and (for FT-N-08) reading `docker logs missions-sut`. Expected outputs are compared against `_docs/00_problem/input_data/expected_results/results_report.md` rows AC-1 1.7, 1.8, 1.9; AC-2 2.2, 2.6; AC-3 3.2; AC-4 4.1; AC-8 8.7; AC-10 10.1.
- Stubs are allowed ONLY for: the external `admin` JWT issuer (`jwks-mock` container) and the DB-only stub tables for `media`, `annotations`, `detection`, `map_objects` (seeded via side-channel SQL).
- Stubs, fakes, deterministic fallbacks, monkeypatches, or direct imports are NOT allowed for any internal product module — including `VehicleService`, `MissionService`, `WaypointService`, the controllers, `ErrorHandlingMiddleware`, `AppDataConnection`, `DatabaseMigrator`, or `JwtExtensions`. 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.