Files
annotations/_docs/02_document/tests/blackbox-tests.md
T
Oleksandr Bezdieniezhnykh 03f879206e docs+src: complete Steps 1-3 outcomes + auth re-sync baseline
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>
2026-05-14 20:19:05 +03:00

20 KiB

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