mirror of
https://github.com/azaion/annotations.git
synced 2026-06-21 15:31:06 +00:00
03f879206e
This commit captures everything produced during autodev existing-code Steps 1 (Document), 2 (Architecture Baseline Scan), and 3 (Test Spec), together with the targeted auth + CORS re-sync triggered on 2026-05-14 when codebase drift was detected at Step 4 entry. None of this work was previously committed. Step 1 (Document) — 50+ _docs/02_document/ files: problem, solution, architecture, system flows, glossary, module-layout, per-component specs (01..06), modules, deployment, diagrams, data model, FINAL report, verification log, discovery. Step 2 (Architecture Baseline) — architecture_compliance_baseline.md. Verdict PASS_WITH_WARNINGS (0 Critical, 0 High, 1 Medium, 2 Low). No High/Critical findings; auto-chained to Step 3 per existing-code flow. Step 3 (Test Spec) — _docs/02_document/tests/* (67 scenarios across blackbox, security, resilience, resource-limit, performance), plus e2e/docker-compose.test.yml, e2e/seed/run.sh, scripts/run-tests.sh, scripts/run-performance-tests.sh. Coverage 88% over the active scope (40 of 45 items covered, 6 RB-deferred, 5 documented-as-uncovered). Targeted auth + CORS re-sync — replaces the deleted in-house token issuer with a JWKS-verifier model. AuthController and TokenService removed; JwtExtensions switched from HS256 symmetric to ES256 over admin's JWKS. ConfigurationResolver and CorsConfigurationValidator added under src/Infrastructure/. ADR-002 and ADR-006 retired; SEC-01, SEC-02, SEC-03 marked Closed. One new testability risk recorded in architecture.md Open Risks Section 6 (JWKS HTTPS gating). Source changes: - src/Auth/JwtExtensions.cs (modified) — ES256, JWKS, alg pinning - src/Program.cs (modified) — DI wiring for ConfigurationResolver and CorsConfigurationValidator - src/Controllers/AuthController.cs (deleted) — no in-service issuance - src/Services/TokenService.cs (deleted) — same - src/Infrastructure/ConfigurationResolver.cs (new) - src/Infrastructure/CorsConfigurationValidator.cs (new) - .env.example (new) — required env var documentation - .gitignore (updated) Cross-repo coordination: _docs/cross-repo/flights_h1_h2_h3_change_spec captures the change-spec for downstream services that consumed the now deleted /auth endpoints. Co-authored-by: Cursor <cursoragent@cursor.com>
573 lines
20 KiB
Markdown
573 lines
20 KiB
Markdown
# Blackbox Tests
|
|
|
|
## Positive Scenarios
|
|
|
|
### FT-P-01: Annotation create — single detection, small image
|
|
|
|
**Summary**: A `POST /annotations` with a small frame and one synthetic detection persists the row, writes the YOLO label file, and returns the persisted DTO.
|
|
**Traces to**: AC-F-01, AC-F-03, AC-F-04
|
|
**Category**: Annotation lifecycle — Create
|
|
|
|
**Preconditions**:
|
|
- SUT healthy (`/health` returns 200)
|
|
- DB clean (no rows in `annotations`, `detection`, `media`, `annotations_queue_records`)
|
|
- Runner has minted an ES256 token with the `ANN` claim (see `test-data.md` → "Bearer token harness")
|
|
|
|
**Input data**: `image_small.jpg` + `F1_001_request.json` (1 detection, `class_num=10` Plane, normalized bbox)
|
|
|
|
**Steps**:
|
|
|
|
| Step | Consumer Action | Expected System Response |
|
|
|------|----------------|------------------------|
|
|
| 1 | `POST /annotations` with the request body | HTTP 200; body matches `AnnotationDto` schema; `body.id =~ /^[0-9a-f]{32}$/`; `body.detections.length == 1` |
|
|
| 2 | Out-of-band: assert `<images_dir>/<id>.jpg` exists with same bytes as `image_small.jpg` | file present, byte-for-byte match |
|
|
| 3 | Out-of-band: assert `<images_dir>/<id>.txt` exists with one line `10 0.45 0.32 0.08 0.12` (or whatever the request supplied, formatted) | file present, line matches regex `^10 \d+\.\d+ \d+\.\d+ \d+\.\d+ \d+\.\d+$` |
|
|
| 4 | `GET /annotations/{id}` | HTTP 200; same body as step 1 |
|
|
|
|
**Expected outcome**: the persisted entity round-trips through `GET /annotations/{id}` byte-for-byte, the image file is on disk, and the label file format is YOLO.
|
|
**Max execution time**: 5s
|
|
|
|
---
|
|
|
|
### FT-P-02: Annotation create — idempotency on identical re-POST
|
|
|
|
**Summary**: Re-POSTing the same image bytes + same detections does not create a new row; the second response carries the same `id`.
|
|
**Traces to**: AC-F-01, AC-F-02
|
|
|
|
**Preconditions**:
|
|
- FT-P-01 has just succeeded, so an annotation for `image_small.jpg` already exists.
|
|
|
|
**Input data**: same as FT-P-01
|
|
|
|
**Steps**:
|
|
|
|
| Step | Consumer Action | Expected System Response |
|
|
|------|----------------|------------------------|
|
|
| 1 | `POST /annotations` with the same body | HTTP 200; `body.id == <id from FT-P-01>` |
|
|
| 2 | Out-of-band: count rows in `annotations WHERE id = <id>` | `count == 1` |
|
|
|
|
**Expected outcome**: idempotent write — same hash → same id → same row.
|
|
**Max execution time**: 5s
|
|
|
|
---
|
|
|
|
### FT-P-03: Annotation create — empty scene, 0 detections
|
|
|
|
**Summary**: An empty-scene image with 0 detections creates an annotation row with no detection rows; the YOLO label file is empty.
|
|
**Traces to**: AC-F-03 (label-file format with 0 detections)
|
|
|
|
**Preconditions**: clean state.
|
|
|
|
**Input data**: `image_empty_scene.jpg` + `F1_003_request.json` (0 detections)
|
|
|
|
**Steps**:
|
|
|
|
| Step | Consumer Action | Expected System Response |
|
|
|------|----------------|------------------------|
|
|
| 1 | `POST /annotations` | HTTP 200; `body.detections.length == 0` |
|
|
| 2 | Out-of-band: read `<images_dir>/<id>.txt` | file exists; content is empty (0 bytes) or whitespace-only |
|
|
| 3 | Out-of-band: count rows in `detection WHERE annotation_id = <id>` | `count == 0` |
|
|
|
|
**Expected outcome**: persisted annotation with empty detections; label file present and empty.
|
|
**Max execution time**: 5s
|
|
|
|
---
|
|
|
|
### FT-P-04: Annotation create — dense scene, 5 mixed-class detections
|
|
|
|
**Summary**: A dense frame with 5 detections across multiple seeded classes persists 5 detection rows, writes a 5-line YOLO label, and returns a DTO with all 5 detections.
|
|
**Traces to**: AC-F-03, AC-F-04
|
|
|
|
**Preconditions**: clean state.
|
|
|
|
**Input data**: `image_dense01.jpg` + `F1_004_request.json` (5 detections, class_num ∈ {0=ArmorVehicle, 1=Truck, 2=Vehicle, 9=Smoke, 10=Plane})
|
|
|
|
**Steps**:
|
|
|
|
| Step | Consumer Action | Expected System Response |
|
|
|------|----------------|------------------------|
|
|
| 1 | `POST /annotations` | HTTP 200; `body.detections.length == 5` |
|
|
| 2 | Out-of-band: read `<images_dir>/<id>.txt` | exactly 5 lines; each matches `^\d+ \d+\.\d+ \d+\.\d+ \d+\.\d+ \d+\.\d+$` |
|
|
| 3 | Compare line set against expected | line set equals the 5 detections in `F1_004_request.json` (order may differ — test uses set equality) |
|
|
|
|
**Expected outcome**: 5 detections round-trip through both DB and YOLO label.
|
|
**Max execution time**: 5s
|
|
|
|
---
|
|
|
|
### FT-P-05: Annotation listing — paginated read
|
|
|
|
**Summary**: After several creates, `GET /annotations` returns a paginated list with the correct shape and count.
|
|
**Traces to**: AC-F-04 (read path)
|
|
|
|
**Preconditions**: FT-P-01..FT-P-04 have run; 4 annotations exist.
|
|
|
|
**Steps**:
|
|
|
|
| Step | Consumer Action | Expected System Response |
|
|
|------|----------------|------------------------|
|
|
| 1 | `GET /annotations?limit=10` | HTTP 200; `body.length == 4`; each item conforms to `AnnotationListItem` schema |
|
|
| 2 | `GET /annotations?limit=2&offset=0` | HTTP 200; `body.length == 2` |
|
|
| 3 | `GET /annotations?limit=2&offset=2` | HTTP 200; `body.length == 2`; ids disjoint from step 2's response |
|
|
|
|
**Expected outcome**: paginated read works; results are stable across paging windows.
|
|
**Max execution time**: 5s
|
|
|
|
---
|
|
|
|
### FT-P-06: Annotation detail by id
|
|
|
|
**Summary**: `GET /annotations/{id}` returns the full DTO including detections.
|
|
**Traces to**: AC-F-04
|
|
|
|
**Preconditions**: FT-P-04 has run.
|
|
|
|
**Steps**:
|
|
|
|
| Step | Consumer Action | Expected System Response |
|
|
|------|----------------|------------------------|
|
|
| 1 | `GET /annotations/<id from FT-P-04>` | HTTP 200; body matches `AnnotationDto`; `body.detections.length == 5` |
|
|
|
|
**Max execution time**: 3s
|
|
|
|
---
|
|
|
|
### FT-P-07: SSE delivery — event for new annotation
|
|
|
|
**Summary**: A subscriber connected to `/annotations/events?missionId=<m>` receives the lifecycle event for a `POST /annotations` against that mission within 1 second.
|
|
**Traces to**: AC-F-10
|
|
|
|
**Preconditions**: clean state.
|
|
|
|
**Steps**:
|
|
|
|
| Step | Consumer Action | Expected System Response |
|
|
|------|----------------|------------------------|
|
|
| 1 | Open SSE connection to `/annotations/events?missionId=<m>` | HTTP 200; `Content-Type: text/event-stream` |
|
|
| 2 | `POST /annotations` against mission `<m>` | HTTP 200 |
|
|
| 3 | Read next event from the SSE stream | event arrives within 1000ms; `event.data` parses as `AnnotationEventDto`; `event.operation == "Created"`; `event.annotationId == <id from step 2>` |
|
|
|
|
**Expected outcome**: real-time delivery of the lifecycle event.
|
|
**Max execution time**: 10s
|
|
|
|
---
|
|
|
|
### FT-P-08: Outbox row on create
|
|
|
|
**Summary**: A successful `POST /annotations` inserts exactly one row into `annotations_queue_records` with `operation == 10` (Created).
|
|
**Traces to**: AC-F-12 (outbox drain), AC-F-05 (`[after RB-01]` for non-Created paths)
|
|
|
|
**Preconditions**: clean state; RabbitMQ broker reachable but the test does not consume from the stream yet.
|
|
|
|
**Steps**:
|
|
|
|
| Step | Consumer Action | Expected System Response |
|
|
|------|----------------|------------------------|
|
|
| 1 | `POST /annotations` (any valid payload) | HTTP 200 |
|
|
| 2 | Out-of-band: `SELECT COUNT(*) FROM annotations_queue_records WHERE annotation_id = <id> AND operation = 10` immediately after step 1 | `count == 1` (within 500ms — outbox insert happens before the response returns) |
|
|
|
|
**Max execution time**: 5s
|
|
|
|
---
|
|
|
|
### FT-P-09: Stream message round-trip
|
|
|
|
**Summary**: After the outbox drain interval, a message arrives on the `azaion-annotations` stream that decodes to the documented schema.
|
|
**Traces to**: AC-F-12
|
|
|
|
**Preconditions**: FT-P-08 just succeeded; outbox row present.
|
|
|
|
**Steps**:
|
|
|
|
| Step | Consumer Action | Expected System Response |
|
|
|------|----------------|------------------------|
|
|
| 1 | Connect a stream consumer to `azaion-annotations` at offset `next` | consumer alive |
|
|
| 2 | Wait up to `drain_interval + 2s` for one message | one message arrives |
|
|
| 3 | gzip-decompress + MessagePack-deserialize the body | object matches the documented stream schema |
|
|
| 4 | Out-of-band: re-query `annotations_queue_records WHERE annotation_id = <id>` | `count == 0` (drainer deleted the row) |
|
|
|
|
**Max execution time**: 30s (depends on configured drain interval)
|
|
|
|
---
|
|
|
|
### FT-P-10: Media single upload
|
|
|
|
**Summary**: `POST /media` (multipart) persists the file and a media row.
|
|
**Traces to**: AC-F-20
|
|
|
|
**Steps**:
|
|
|
|
| Step | Consumer Action | Expected System Response |
|
|
|------|----------------|------------------------|
|
|
| 1 | `POST /media` with `image_small.jpg`, `mediaType=Image`, `waypointId=<m>` (multipart) | HTTP 200; body matches `MediaListItem` schema |
|
|
| 2 | Out-of-band: `<media_dir>/<media_id>.jpg` exists | file present |
|
|
|
|
**Max execution time**: 5s
|
|
|
|
---
|
|
|
|
### FT-P-11: Media batch upload
|
|
|
|
**Summary**: `POST /media/batch` with N files persists N rows + N files in one request.
|
|
**Traces to**: AC-F-21
|
|
|
|
**Steps**:
|
|
|
|
| Step | Consumer Action | Expected System Response |
|
|
|------|----------------|------------------------|
|
|
| 1 | `POST /media/batch` with 3 distinct files (`image_small`, `image_dense01`, `image_dense02`) | HTTP 200; `body.length == 3`; 3 distinct media ids |
|
|
| 2 | Out-of-band: 3 distinct files exist on disk | 3 files present |
|
|
|
|
**Max execution time**: 10s
|
|
|
|
---
|
|
|
|
### FT-P-12: Bearer token verification — happy path
|
|
|
|
**Summary**: A request bearing an ES256 access token whose `iss`, `aud`, signature, and `exp` are all valid is accepted by every authenticated endpoint reached.
|
|
**Traces to**: AC-F-50
|
|
|
|
**Preconditions**: A test-only ES256 key pair is published at the `JWT_JWKS_URL` fetched by the service at boot (see `test-data.md` → "Bearer token harness"). The runner mints an access token signed with the matching private key.
|
|
|
|
**Steps**:
|
|
|
|
| Step | Consumer Action | Expected System Response |
|
|
|------|----------------|------------------------|
|
|
| 1 | `GET /annotations` with `Authorization: Bearer <ES256 token, iss=$JWT_ISSUER, aud=$JWT_AUDIENCE, exp=now+5m, ANN claim present>` | HTTP 200; valid `PaginatedResponse<AnnotationListItem>` body |
|
|
|
|
**Max execution time**: 3s
|
|
|
|
---
|
|
|
|
### FT-P-13: Bearer token verification — alg pinning
|
|
|
|
**Summary**: A token signed with `alg=HS256` (using the public ES256 key as the HMAC secret) is rejected — `JwtExtensions.AddJwtAuth` pins `ValidAlgorithms = [EcdsaSha256]`.
|
|
**Traces to**: AC-F-50
|
|
|
|
**Preconditions**: Same harness as FT-P-12. Runner additionally produces a forged HS256 token using the public ES256 key bytes as the HMAC key.
|
|
|
|
**Steps**:
|
|
|
|
| Step | Consumer Action | Expected System Response |
|
|
|------|----------------|------------------------|
|
|
| 1 | `GET /annotations` with `Authorization: Bearer <forged HS256 token>` | HTTP 401; error envelope |
|
|
|
|
**Max execution time**: 3s
|
|
|
|
---
|
|
|
|
### FT-P-14: Detection class catalog read
|
|
|
|
**Summary**: `GET /classes` returns the 19 seeded classes with stable ids.
|
|
**Traces to**: AC-F-41
|
|
|
|
**Steps**:
|
|
|
|
| Step | Consumer Action | Expected System Response |
|
|
|------|----------------|------------------------|
|
|
| 1 | `GET /classes` | HTTP 200; `body.length == 19`; ids `[0..18]` present (set equality); entry where `id==9` has `name=="Smoke"`; entry where `id==10` has `name=="Plane"` |
|
|
|
|
**Max execution time**: 3s
|
|
|
|
---
|
|
|
|
### FT-P-15: Directory settings → PathResolver invariant
|
|
|
|
**Summary**: `PUT /settings/directories` updates the values; the next annotation create writes to the new path.
|
|
**Traces to**: AC-F-40
|
|
|
|
**Preconditions**: ADM JWT in hand. Volume mounts include both old and new paths so the SUT can write to either.
|
|
|
|
**Steps**:
|
|
|
|
| Step | Consumer Action | Expected System Response |
|
|
|------|----------------|------------------------|
|
|
| 1 | `GET /settings/directories` | HTTP 200; record current `imagesDir` |
|
|
| 2 | `PUT /settings/directories` with `imagesDir = /data/images-alt` | HTTP 200 |
|
|
| 3 | `GET /settings/directories` | HTTP 200; `imagesDir == "/data/images-alt"` |
|
|
| 4 | `POST /annotations` for a fresh image | HTTP 200; out-of-band: image lands at `/data/images-alt/<id>.jpg`, NOT at the original `imagesDir` |
|
|
|
|
**Expected outcome**: `pathResolver.Reset()` has fired and the next write uses the new directory.
|
|
**Max execution time**: 10s
|
|
|
|
---
|
|
|
|
### FT-P-16: Dataset filter by status
|
|
|
|
**Summary**: `GET /dataset?status=10` (Pending) returns only Pending rows.
|
|
**Traces to**: AC-F-30
|
|
|
|
**Preconditions**: FT-P-04 just ran; one Pending annotation exists.
|
|
|
|
**Steps**:
|
|
|
|
| Step | Consumer Action | Expected System Response |
|
|
|------|----------------|------------------------|
|
|
| 1 | `GET /dataset?status=10` | HTTP 200; every item in `body` has `status == 10` |
|
|
|
|
**Max execution time**: 3s
|
|
|
|
---
|
|
|
|
### FT-P-17: Dataset class distribution
|
|
|
|
**Summary**: `GET /dataset/class-distribution` returns counts grouped by class with the expected shape.
|
|
**Traces to**: AC-F-30 (read path), AC-F-41 (class metadata)
|
|
|
|
**Steps**:
|
|
|
|
| Step | Consumer Action | Expected System Response |
|
|
|------|----------------|------------------------|
|
|
| 1 | `GET /dataset/class-distribution` after FT-P-04 (5 detections of mixed classes) | HTTP 200; body is an array; entry for `classNum=10` has `count >= 1`; sum of all `count` values equals total detection rows |
|
|
|
|
**Max execution time**: 3s
|
|
|
|
---
|
|
|
|
### FT-P-18: Dataset bulk status
|
|
|
|
**Summary**: `POST /dataset/status/bulk` flips status atomically on N rows.
|
|
**Traces to**: AC-F-31
|
|
|
|
**Preconditions**: 2+ Pending annotations from FT-P-01 and FT-P-04 (now FT-P-04 has 1; need at least 2).
|
|
|
|
**Steps**:
|
|
|
|
| Step | Consumer Action | Expected System Response |
|
|
|------|----------------|------------------------|
|
|
| 1 | `POST /dataset/status/bulk` with `{annotationIds: [<id1>, <id2>], status: 20}` | HTTP 200 |
|
|
| 2 | `GET /annotations/<id1>` | HTTP 200; `body.status == 20` |
|
|
| 3 | `GET /annotations/<id2>` | HTTP 200; `body.status == 20` |
|
|
|
|
**Max execution time**: 5s
|
|
|
|
---
|
|
|
|
### FT-P-19: Health check
|
|
|
|
**Summary**: `GET /health` returns 200 with low latency at any time post-boot.
|
|
**Traces to**: AC-F-54
|
|
|
|
**Steps**:
|
|
|
|
| Step | Consumer Action | Expected System Response |
|
|
|------|----------------|------------------------|
|
|
| 1 | `GET /health` | HTTP 200 within 200ms |
|
|
|
|
**Max execution time**: 2s
|
|
|
|
---
|
|
|
|
### FT-P-20: Migrator idempotence
|
|
|
|
**Summary**: Restarting the SUT against the same DB makes 0 schema changes.
|
|
**Traces to**: AC-N-02
|
|
|
|
**Preconditions**: SUT booted once; DB schema captured (e.g., `pg_dump --schema-only`).
|
|
|
|
**Steps**:
|
|
|
|
| Step | Consumer Action | Expected System Response |
|
|
|------|----------------|------------------------|
|
|
| 1 | Capture schema-only dump → `dump_a.sql` | non-empty dump |
|
|
| 2 | `docker compose restart annotations` | SUT comes back healthy |
|
|
| 3 | Capture schema-only dump → `dump_b.sql` | non-empty dump |
|
|
| 4 | Diff `dump_a.sql` and `dump_b.sql` | zero meaningful differences (whitespace / SERIAL counters tolerated) |
|
|
|
|
**Max execution time**: 30s
|
|
|
|
---
|
|
|
|
### FT-P-21 `[after RB-01]`: Lifecycle event on update
|
|
|
|
**Summary**: `PUT /annotations/{id}` emits an `Updated` SSE event AND inserts an outbox row.
|
|
**Traces to**: AC-F-05 (post RB-01)
|
|
|
|
**Note**: this test stays disabled (skipped with reason `"awaiting RB-01"`) until the refactor lands.
|
|
|
|
---
|
|
|
|
### FT-P-22 `[after RB-01]`: Lifecycle event on delete + soft-delete file relocation
|
|
|
|
**Summary**: `DELETE /annotations/{id}` flips status to `40`, relocates files to `deleted_dir`, and emits a `Deleted` SSE event + outbox row.
|
|
**Traces to**: AC-F-06, AC-F-07
|
|
|
|
**Note**: skipped until RB-01 + RB-08 land.
|
|
|
|
---
|
|
|
|
## Negative Scenarios
|
|
|
|
### FT-N-01: Create without image bytes
|
|
|
|
**Summary**: `POST /annotations` with no `image` field is rejected.
|
|
**Traces to**: AC-F-04 (negative)
|
|
|
|
**Steps**:
|
|
|
|
| Step | Consumer Action | Expected System Response |
|
|
|------|----------------|------------------------|
|
|
| 1 | `POST /annotations` with body missing `image` | HTTP 400 or 422; error envelope |
|
|
|
|
**Max execution time**: 3s
|
|
|
|
---
|
|
|
|
### FT-N-02: Create without mediaType
|
|
|
|
**Summary**: Missing required enum field is rejected.
|
|
|
|
| Step | Consumer Action | Expected System Response |
|
|
|------|----------------|------------------------|
|
|
| 1 | `POST /annotations` with no `mediaType` | HTTP 400 or 422; error envelope |
|
|
|
|
**Max execution time**: 3s
|
|
|
|
---
|
|
|
|
### FT-N-03: Create without ANN policy
|
|
|
|
**Summary**: A token with policy `DATASET` cannot create annotations.
|
|
**Traces to**: AC-F-52
|
|
|
|
| Step | Consumer Action | Expected System Response |
|
|
|------|----------------|------------------------|
|
|
| 1 | `POST /annotations` with an ES256 token carrying only the `DATASET` claim | HTTP 403; error envelope |
|
|
|
|
**Max execution time**: 3s
|
|
|
|
---
|
|
|
|
### FT-N-04: Create unauthenticated
|
|
|
|
**Summary**: Missing `Authorization` header → 401.
|
|
|
|
| Step | Consumer Action | Expected System Response |
|
|
|------|----------------|------------------------|
|
|
| 1 | `POST /annotations` with no `Authorization` header | HTTP 401; error envelope |
|
|
|
|
**Max execution time**: 3s
|
|
|
|
---
|
|
|
|
### FT-N-05: Out-of-range bbox value (current lenient behavior)
|
|
|
|
**Summary**: `centerX = 1.5` is accepted today; the test asserts the **current** behavior. Will flip to expecting 400/422 after SEC-05 lands.
|
|
**Traces to**: documented gap in `security_approach.md` SEC-05
|
|
|
|
| Step | Consumer Action | Expected System Response |
|
|
|------|----------------|------------------------|
|
|
| 1 | `POST /annotations` with `detections[0].centerX = 1.5` | HTTP 200 today (lenient); test will be inverted post-SEC-05 |
|
|
|
|
**Max execution time**: 3s
|
|
|
|
---
|
|
|
|
### FT-N-06: GET nonexistent annotation
|
|
|
|
| Step | Consumer Action | Expected System Response |
|
|
|------|----------------|------------------------|
|
|
| 1 | `GET /annotations/00000000000000000000000000000000` | HTTP 404; error envelope; `error.code` matches `/not.?found/i` |
|
|
|
|
**Max execution time**: 3s
|
|
|
|
---
|
|
|
|
### FT-N-07: Filter by unknown mission
|
|
|
|
| Step | Consumer Action | Expected System Response |
|
|
|------|----------------|------------------------|
|
|
| 1 | `GET /annotations?missionId=<unknown-guid>` | HTTP 200; `body.length == 0` |
|
|
|
|
**Max execution time**: 3s
|
|
|
|
---
|
|
|
|
### FT-N-08: SSE without auth
|
|
|
|
| Step | Consumer Action | Expected System Response |
|
|
|------|----------------|------------------------|
|
|
| 1 | Open SSE to `/annotations/events?missionId=<m>` with no `Authorization` | HTTP 401 on connection establishment |
|
|
|
|
**Max execution time**: 3s
|
|
|
|
---
|
|
|
|
### FT-N-09: Bearer token — expired
|
|
|
|
| Step | Consumer Action | Expected System Response |
|
|
|------|----------------|------------------------|
|
|
| 1 | `GET /annotations` with `Authorization: Bearer <token with exp=now-1m, otherwise valid>` | HTTP 401; error envelope |
|
|
|
|
**Max execution time**: 3s
|
|
|
|
---
|
|
|
|
### FT-N-10: Bearer token — wrong issuer
|
|
|
|
| Step | Consumer Action | Expected System Response |
|
|
|------|----------------|------------------------|
|
|
| 1 | `GET /annotations` with `Authorization: Bearer <token with iss="https://other.example.com" but otherwise valid>` | HTTP 401; error envelope |
|
|
|
|
**Max execution time**: 3s
|
|
|
|
---
|
|
|
|
### FT-N-11: Bearer token — wrong audience
|
|
|
|
| Step | Consumer Action | Expected System Response |
|
|
|------|----------------|------------------------|
|
|
| 1 | `GET /annotations` with `Authorization: Bearer <token with aud="some-other-service" but otherwise valid>` | HTTP 401; error envelope |
|
|
|
|
**Max execution time**: 3s
|
|
|
|
---
|
|
|
|
### FT-N-12: Mutating settings without ADM
|
|
|
|
| Step | Consumer Action | Expected System Response |
|
|
|------|----------------|------------------------|
|
|
| 1 | `PUT /settings/system` with an ES256 token carrying only the `ANN` claim | HTTP 403; error envelope |
|
|
|
|
**Max execution time**: 3s
|
|
|
|
---
|
|
|
|
### FT-N-13: PUT directories without ADM
|
|
|
|
| Step | Consumer Action | Expected System Response |
|
|
|------|----------------|------------------------|
|
|
| 1 | `PUT /settings/directories` with non-ADM JWT | HTTP 403; error envelope |
|
|
|
|
**Max execution time**: 3s
|
|
|
|
---
|
|
|
|
### FT-N-14: Media upload missing waypoint
|
|
|
|
| Step | Consumer Action | Expected System Response |
|
|
|------|----------------|------------------------|
|
|
| 1 | `POST /media` multipart without `waypointId` | HTTP 400 or 422; error envelope |
|
|
|
|
**Max execution time**: 3s
|
|
|
|
---
|
|
|
|
### FT-N-15: Media upload without ANN
|
|
|
|
| Step | Consumer Action | Expected System Response |
|
|
|------|----------------|------------------------|
|
|
| 1 | `POST /media` with non-ANN JWT | HTTP 403; error envelope |
|
|
|
|
**Max execution time**: 3s
|
|
|
|
---
|
|
|
|
### FT-N-16: Bulk status with empty list
|
|
|
|
| Step | Consumer Action | Expected System Response |
|
|
|------|----------------|------------------------|
|
|
| 1 | `POST /dataset/status/bulk` with `annotationIds: []` | HTTP 400; error envelope (verified: `DatasetService.BulkUpdateStatus` throws `ArgumentException`) |
|
|
|
|
**Max execution time**: 3s
|