# 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: }`, 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:, 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: }` **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: }` **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: }` | `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.