Step 12 (Test-Spec Sync): adds BT-27 for the AZ-796 9-rule validation surface and 12 cycle-7 AC rows + Coverage Summary update to traceability-matrix.md. Step 13 (Update Docs): module-layout + module docs for the new SatelliteProvider.Api/Validators namespace + GlobalExceptionHandler + updated TileInventory DTO; tests_unit + tests_integration document the new InventoryRequestValidatorTests (16 unit tests covering all 9 rules) + TileInventoryValidationTests (16 integration tests) + ProblemDetailsAssertions support; glossary entries for Validation Problem Details / FluentValidation / Unmapped Member Handling; system-flows F8 (Tile Inventory Bulk Lookup) expanded with deserializer + validator gates and a 13-row Validation Surface table; data_parameters § Tile Inventory documents the v2 input schema + constraints; ripple_log_cycle7 captures the doc-side ripple decisions. Step 14 (Security Audit): 5-phase audit ran; verdict PASS_WITH_WARNINGS (3 Low findings — D-AZ795-1 FluentValidation 12.0.0 -> 12.1.1 recommended bump, F-AZ795-1 JsonException.Message leak in 400 detail, F-AZ795-2 BadHttpRequestException.Message leak). No Critical / High; auth runs before validation (confirmed in Program.cs); two NuGet additions (FluentValidation 12.0.0 + .DependencyInjectionExtensions 12.0.0) both CVE-clean. Per-phase reports plus consolidated security_report_cycle7.md. Step 15 (Performance Test): docker compose stack used for perf run, scripts/run-performance-tests.sh exited 0 with 8/8 scenarios PASS (second consecutive clean exit-0); added PT-09 cycle-7 smoke probe (v2 z/x/y schema, 2500-tile all-miss batch) measuring min=27ms median=44ms p95=73ms max=86ms (13.7x under AZ-505 AC-4 1000ms budget). PT-07/08 improvements traced to the cycle-6 TLS handshake-overhead identification, not application-side change. Co-authored-by: Cursor <cursoragent@cursor.com>
14 KiB
Module: Tests/SatelliteProvider.IntegrationTests
Purpose
Console application that runs end-to-end integration tests against a live API instance. Designed to run in Docker alongside the API and PostgreSQL containers.
Public Interface
Test Classes
TileTests— tile download via lat/lon endpointRegionTests— region request → polling → completion flowBasicRouteTests— route creation with intermediate pointsComplexRouteTests— routes with geofencingExtendedRouteTests— routes withrequestMaps: trueand tile ZIP creationMigrationTests— direct PostgreSQL schema/index validation (no HTTP). AZ-484 cycle added:BackfillUpdateAssignsGoogleMapsAndCapturedAt_AZ484_AC4,MultiSourceInsertCoexistsUnderNewIndex_AZ484_AC1,MostRecentAcrossSourcesSelection_AZ484_AC2,SameSourceUpsertReplacesPreviousRow_AZ484_AC3(latter four use temp tables to keep production data untouched). AZ-503 (cycle 5) added:Az503ColumnsExistAndLocationHashIsNotNull(asserts the 4 new columns +location_hash NOT NULL),Az503NewUniqueIndexCoversIntegerKeyAndFlightId(assertsidx_tiles_unique_identitycolumns +COALESCE(flight_id, ...)predicate),Az503LocationHashBackfillIsDeterministic(computespg_temp.uuidv5("18/12345/23456")and compares byte-for-byte against the C#Uuidv5.Createoutput on 3 sampled live rows); the AZ-484 supersession test was renamed toAz503MigrationSupersedesAz484UniqueIndexand assertsidx_tiles_unique_location_sourceno longer exists.JwtIntegrationTests(added by AZ-487 cycle 2; helpers consolidated by AZ-491 cycle 3; iss/aud scenarios added by AZ-494 cycle 3) —AnonymousRequest_To_AnyEndpoint_Returns401,ExpiredToken_Returns401,InvalidSignature_Returns401,ValidToken_Returns200_OnHealthyEndpoint,WrongIssuer_Returns401(AZ-494 AC-1),WrongAudience_Returns401(AZ-494 AC-2),SwaggerDocument_AdvertisesBearerSecurityScheme. HS256 token minting lives in the sharedSatelliteProvider.TestSupport.JwtTokenFactory(consumed viaProjectReference); runner-specific concerns (JwtTestHelpers.ResolveSecretOrThrow/ResolveIssuerOrThrow/ResolveAudienceOrThrow,MintAuthenticated/MintExpiredconvenience wrappers that auto-fill iss+aud from env,AttachDefaultAuthorization,DefaultSubject = "integration-tests") remain in this project. The test runner setsJWT_SECRET+JWT_ISSUER+JWT_AUDIENCEon the API container and attaches a Bearer token (with matching iss/aud) to every existing test's HTTP requests so the pre-cycle-2 suite continues to pass.UavUploadTests(added by AZ-488, cycle 2; coordinate-counter promoted to defense-in-depth by AZ-493 cycle 3; AZ-503 cycle 5 added 2 more tests) —HappyPathSingleItem_PersistsRow,MixedBatch_ReturnsPerItemResults,MultiSourceCoexistence_AZ484_Cycle2,SameSourceUpsert_AZ484_Cycle2,NoToken_Returns401,ValidTokenWithoutGpsPermission_Returns403,OversizedBatch_Returns400, plus AZ-503:MultiFlightUavRowsCoexist_AZ503_AC3(two flights at the same cell → two rows, onelocation_hash, twofile_paths under./tiles/uav/{flight_id}/...) andFloatRoundingDoesNotBreakIdempotence_AZ503_AC4(two uploads with float-distinctlatituderecomputed fromTileToWorldPoscollapse to a single row because the conflict key is integer-only). The AZ-503 migration madelocation_hash NOT NULL, so the cycle-2MultiSourceCoexistence_AZ484_Cycle2seeder was updated to computelocation_hashviaUuidv5.Create(canonical name"{zoom}/0/0") before the raw SQLINSERT— this required adding aProjectReferencefromSatelliteProvider.IntegrationTeststoSatelliteProvider.Common. The wall-clock-seeded_coordinateCounteris retained as a belt-and-suspenders safeguard alongside the AZ-493 startup DB-reset (below) — if a developer runs with--keep-state, or the DB-reset path is skipped for any reason, the wall-clock seed still spreads coordinates across runs so the unique index does not collide.StubAndErrorContractTests(existing) — updated in cycle 2 to drop the legacyStubUpload_Returns501expectation since AZ-488 implemented the endpoint.TileInventoryTests(added cycle 6 — AZ-505) —OrderingAndPresentAbsentShaping_AC1,LeafletReadReturnsMostRecentViaLocationHash_AC2,ValidationRejectsBothPopulated_AC6,ValidationRejectsNeitherPopulated_AC6,ValidationRejectsOversizedBatch_AC6,UnauthenticatedRequestReturns401_AC6,PerformanceBudget_AC4(full-suite only). Tests are cycle-7-stable — they use the post-AZ-794{z, x, y}wire shape and a minor x/y reduction was applied in cycle 7 to keep the synthetic coords within the z=18 slippy bounds enforced byTileCoordValidator.TileInventoryValidationTests(added cycle 7 — AZ-796) — 16 tests:HappyPath_Returns200,EmptyBody_Returns400,NeitherPopulated_Returns400,BothPopulated_Returns400,EmptyTilesArray_Returns400,TilesOverCap_Returns400,MissingZ_Returns400WithFieldPath,MissingXAndY_Returns400,ZoomOutOfRange_Returns400WithFieldPath,XBeyondZoomBounds_Returns400,YBeyondZoomBounds_Returns400,NegativeAxis_Returns400,UnknownRootField_Returns400,UnknownNestedField_Returns400,OldV1FieldName_Returns400(AZ-794 + AZ-796 intersection — exact AZ-777 Phase 1 reproducer body, asserts legacytileZoom/tileX/tileYnow yields 400),TypeMismatch_Returns400. Each test exercises one of the 9 validation rules end-to-end throughValidationEndpointFilter<TileInventoryRequest>+GlobalExceptionHandler, asserts HTTP 400 + RFC 7807ValidationProblemDetailsshape via the sharedProblemDetailsAssertionshelper.IdempotentPostTests— pre-existing; cycle 7 adjusted the route-point payload from PascalCase (Latitude/Longitude) to camelCase (lat/lon) because the post-AZ-795UnmappedMemberHandling.Disallowwould otherwise reject the previously-silently-ignored fields. TheRoutePointDTO has carriedJsonPropertyName("lat"/"lon")since AZ-309; cycle 7's strict JSON parsing exposed the test was sending the wrong shape and getting away with it via the pre-cycle-7 permissive deserializer.
Supporting Classes
Models.cs— HTTP response DTOs for deserializationRouteTestHelpers.cs— shared utilities (wait-for-completion polling, geofence polygon builders, test data)Program.cs— test runner entry point (handles--smoke/--fullmode selection,--keep-stateopt-out flag, default-token issuance viaJwtTokenFactory, the AZ-493 DB-reset hook, and the AZ-492--mint-only/--gen-uav-fixtureperf-bootstrap subcommands that short-circuit before any HTTP / DB setup)JwtTestHelpers.cs— runner-side JWT concerns:ResolveSecretOrThrowreadsJWT_SECRETenv var with size validationResolveIssuerOrThrow/ResolveAudienceOrThrow(AZ-494) readJWT_ISSUER/JWT_AUDIENCEwith fail-fast contractMintAuthenticated(secret, …)(AZ-494) auto-fills iss/aud from env and delegates toJwtTokenFactory.Create; acceptsoverrideIssuer/overrideAudiencefor negative-AC scenarios (WrongIssuer_Returns401/WrongAudience_Returns401)MintExpired(secret, …)(AZ-494) mirrorsMintAuthenticatedfor the expired-token fixtureAttachDefaultAuthorizationputs a Bearer token on the sharedHttpClientDefaultSubject = "integration-tests"is the canonical runner subject value- Token minting lives in the shared
SatelliteProvider.TestSupport.JwtTokenFactory(AZ-491) — runner-side concerns (env reads, HttpClient mutation, the iss/aud-aware mint wrapper) deliberately stay here.
IntegrationTestDatabaseReset.cs(AZ-493) — instance class with a singleEnsureCleanStateAsync()method that truncates the integration-test target tables in FK-safe order. Guarded viaSatelliteProvider.TestSupport.IntegrationTestResetGuard(env + Host allowlist) so it cannot run against a non-test database.PerfBootstrap.cs(AZ-492) — static helpers for the perf harness bootstrap subcommands.MintToken()mints a 4-hour HS256 token with subjectperf-testsand apermissions: GPSclaim via the canonicalSatelliteProvider.TestSupport.JwtTokenFactory.Create;GenerateUavFixture(args)writes a 256×256 random-noise JPEG viaSixLabors.ImageSharpto the path passed on the CLI. Invoked fromscripts/run-performance-tests.shviadotnet <SatelliteProvider.IntegrationTests.dll> --mint-onlyand--gen-uav-fixture <path>.ProblemDetailsAssertions.cs(added cycle 7 — AZ-795) — shared static helpers for asserting RFC 7807 ProblemDetails bodies on integration-test responses.ReadProblemDetailsAsync(HttpResponseMessage, label)deserialises the response body into aJsonElementwith helpful failure messages when the content-type / shape doesn't match.AssertProblemDetails(problem, expectedStatus, label)asserts the base ProblemDetails shape (type,title,status).AssertValidationProblem(problem, expectedStatus, label, expectedErrorPath?, expectedErrorContains?)extends the base assertion to require theerrorsmap pererror-shape.mdInv-2 and optionally checks a specific field path / message substring. Consumed byTileInventoryValidationTests; designed to be reused by every future per-endpoint child task under AZ-795.
Internal Logic
- Makes HTTP calls to the API at
API_URLenvironment variable (default:http://api:8080) - Tests are methods called sequentially from
Program.cs(not xUnit — plain console app) - Poll-based waiting for async operations (region/route completion)
- Validates response structure, status transitions, file creation
Dependencies
ProjectReferencetoSatelliteProvider.TestSupport(added by AZ-491; providesJwtTokenFactory. Added by AZ-493; providesIntegrationTestResetGuard).- Communicates with the API exclusively via HTTP for end-to-end tests; communicates with PostgreSQL directly only via the dedicated DB-reset hook + the existing
MigrationTestsschema assertions. - NuGet:
Npgsql9.0.2 (Postgres client for DB-reset + MigrationTests),SixLabors.ImageSharp3.1.11 (UAV fixture image generation). - ProjectReferences:
SatelliteProvider.Api(running service for the integration runner),SatelliteProvider.TestSupport(canonicalJwtTokenFactory+IntegrationTestResetGuard),SatelliteProvider.Common(added by AZ-503 so theMultiSourceCoexistence_AZ484_Cycle2seeder can computelocation_hashviaUuidv5.Createinstead of duplicating the UUIDv5 algorithm in T-SQL fixtures).
Consumers
docker-compose.tests.yml— runs as a container that depends on the API service
Configuration
API_URLenvironment variable (set in docker-compose.tests.yml tohttp://api:8080)INTEGRATION_TESTS_MODE—smokeorfull(defaultfull). DrivesTestRunMode.Smoke.INTEGRATION_KEEP_STATE— set to1ortrue(or pass--keep-statetoProgram.cs/scripts/run-tests.sh) to skip the AZ-493 DB-reset hook. Useful for debugging a failed run.ASPNETCORE_ENVIRONMENT=Testing— guard for the DB-reset hook. The reset refuses to run unless this is set (see Reliability § Test isolation below).JWT_SECRET— shared HMAC secret with the API container; must be ≥ 32 bytes (UTF-8).JWT_ISSUER— expectedissclaim, must match the API container (AZ-494). Fail-fast at startup if unset.JWT_AUDIENCE— expectedaudclaim, must match the API container (AZ-494). Fail-fast at startup if unset.DB_CONNECTION_STRING— Npgsql connection string; the reset hook additionally requires the Host to be in the allowed-host list (postgres,localhost,127.0.0.1).
Reliability
Test isolation (AZ-493)
Program.cs runs IntegrationTestDatabaseReset.EnsureCleanStateAsync() at startup, before any test class executes. The hook truncates route_regions, route_points, routes, regions, tiles (in that FK-safe order, with RESTART IDENTITY CASCADE) so each run starts from a known empty state. The Postgres named volume in docker-compose.yml is intentionally persisted across docker-compose down cycles for fast iteration; the AZ-493 reset hook is what gives back per-run isolation in spite of that.
Two guards protect against accidental truncate against a non-test database:
ASPNETCORE_ENVIRONMENTMUST equalTesting(case-insensitive). Set bydocker-compose.tests.yml; absent in production / dev.DB_CONNECTION_STRINGHost MUST be one ofpostgres,localhost,127.0.0.1. Set bydocker-compose.tests.ymland developer machines; a remote-host connection string is rejected even with the env guard satisfied.
Both guards are pure-string checks in SatelliteProvider.TestSupport.IntegrationTestResetGuard — unit-tested in SatelliteProvider.Tests/TestSupport/IntegrationTestResetGuardTests.cs. Failure of either guard surfaces a clear InvalidOperationException and exits the runner with code 1.
To debug leftover state from a failed run, opt out of the reset:
- CLI:
./scripts/run-tests.sh --full --keep-state - Direct:
INTEGRATION_KEEP_STATE=1 docker compose ... up - In the runner Main:
dotnet run --project SatelliteProvider.IntegrationTests -- --keep-state
Adding new tables
If a new task adds a table that integration tests insert into AND that table participates in foreign-key relationships with tiles / regions / routes, update IntegrationTestDatabaseReset.TruncateOrder to include the new table in FK-safe order. The current order assumes the AZ-484 + AZ-488 schema; future migrations that introduce new FK chains need a corresponding order revision. The CASCADE clause is a safety net but is not a substitute for an explicit order — the order is the audit trail for "what does an integration-test runner see at startup".
External Integrations
- HTTP to the SatelliteProvider API
- Reads output files from mounted
./ready/and./tiles/volumes
Security
None.
Tests
This IS the integration test suite.