mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-06-26 06:11:14 +00:00
[AZ-1074] [AZ-1075] Cycle 9 closeout: security, tests, metrics
Resolve F-AZ1074-1/2 (collection caps, generic gRPC internal errors). Standalone integration compose stack, docs, security audit, perf and retro. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -118,6 +118,14 @@ The cycle-1 (AZ-487) and cycle-2 (AZ-488) code reviews each surfaced an F1 (Low
|
||||
- **Imports from**: Common, DataAccess (uses `IRegionService` / `IRegionRequestQueue` from Common — no compile-time dependency on RegionProcessing)
|
||||
- **Consumed by**: WebApi
|
||||
|
||||
### Shared project: GrpcContracts (cycle 9 — AZ-1074)
|
||||
|
||||
- **Directory**: `SatelliteProvider.GrpcContracts/`
|
||||
- **Purpose**: Canonical protobuf + generated C# stubs for gRPC wire contracts shared by API (server) and IntegrationTests (client).
|
||||
- **Owns**: `SatelliteProvider.GrpcContracts/tile_provision.proto` (`package satellite.v1`; `GrpcServices=Both`)
|
||||
- **ProjectReferences**: none (proto-only project)
|
||||
- **Consumed by**: WebApi (`RouteTileDeliveryGrpcService`), IntegrationTests (`RouteTileDeliveryGrpcTests` via `GrpcTestHelpers`)
|
||||
|
||||
### Component: WebApi
|
||||
|
||||
- **Directory**: `SatelliteProvider.Api/`
|
||||
@@ -130,10 +138,11 @@ The cycle-1 (AZ-487) and cycle-2 (AZ-488) code reviews each surfaced an F1 (Low
|
||||
- `SatelliteProvider.Api/Validators/InventoryRequestValidator.cs` + `TileCoordValidator` (added by AZ-796; FluentValidation rules for `POST /api/satellite/tiles/inventory` — XOR `tiles`/`locationHashes`, per-array cap, slippy-map range checks)
|
||||
- `SatelliteProvider.Api/Validators/GlobalValidatorConfig.cs` (added by AZ-795/AZ-796; idempotent `ApplyOnce()` configures `ValidatorOptions.Global.PropertyNameResolver` so `errors`-map keys are camelCase per `error-shape.md` Inv-4; called from `Program.cs` and from the test assembly's `ModuleInitializer`)
|
||||
- `SatelliteProvider.Api/GlobalExceptionHandler.cs` (added by AZ-795; `IExceptionHandler` registered via `AddExceptionHandler<GlobalExceptionHandler>()`. Intercepts `BadHttpRequestException(JsonException)` from System.Text.Json's strict-parsing path — unknown-member rejection, missing required field via `[JsonRequired]`, JSON type mismatch — and emits `ValidationProblemDetails` with the same `errors[]` map shape that FluentValidation produces. 5xx errors pass through with sanitised body + `correlationId` per AZ-353.)
|
||||
- `SatelliteProvider.Api/Grpc/RouteTileDeliveryGrpcService.cs` (added cycle 9 — AZ-1074; server-streaming gRPC adapter over `IRouteTileDeliveryOrchestrator`)
|
||||
- **Internal**: (none)
|
||||
- **Owns**: `SatelliteProvider.Api/**`
|
||||
- **PackageReferences (added by AZ-487, bumped by AZ-496, then by AZ-500; AZ-795 added FluentValidation)**: `Microsoft.AspNetCore.Authentication.JwtBearer` 10.0.7 (pinned to the same minor patch as `Microsoft.AspNetCore.OpenApi` 10.0.7; AZ-496 bumped both packages from 8.0.21 → 8.0.25 in cycle 3 to close cycle-1 D1 + cycle-2 D3 supply-chain findings, then AZ-500 bumped both 8.0.25 → 10.0.7 in cycle 4 as part of the .NET 8 → .NET 10 migration; AZ-500 also bumped `Swashbuckle.AspNetCore` 6.6.2 → 10.1.7 here to land Microsoft.OpenApi 2.x compat required by ASP.NET Core 10). `FluentValidation` + `FluentValidation.DependencyInjectionExtensions` 12.0.0 added by AZ-795 to back the strict-input-validation epic.
|
||||
- **Imports from**: Common (incl. AZ-488 UAV DTOs + `UavQualityConfig`), DataAccess, TileDownloader (incl. AZ-488 `IUavTileUploadHandler`), RegionProcessing, RouteManagement
|
||||
- **Imports from**: Common (incl. AZ-488 UAV DTOs + `UavQualityConfig`), DataAccess, TileDownloader (incl. AZ-488 `IUavTileUploadHandler`), RegionProcessing, RouteManagement, GrpcContracts (cycle 9)
|
||||
- **Consumed by**: (none — top-level entry point)
|
||||
|
||||
## Shared / Cross-Cutting
|
||||
|
||||
@@ -18,6 +18,11 @@ Application entry point. Configures DI container, sets up middleware, defines mi
|
||||
| POST | `/api/satellite/route` | `CreateRoute` | Create route with intermediate points. AZ-809 (cycle 8) added strict pre-handler validation via `WithValidation<CreateRouteRequest>()`: non-zero `id`, name length ∈ \[1, 200\], description length ≤ 1000, `regionSizeMeters` ∈ \[100, 10000\], `zoomLevel` ∈ \[0, 22\], `points` count ∈ \[2, 500\] with per-point lat/lon range checks, per-polygon NW-of-SE invariants, and the `createTilesZip ⇒ requestMaps` cross-field rule. Deserializer-layer failures (missing `[JsonRequired]` axes, unknown fields, type mismatches) are caught by `GlobalExceptionHandler` and produce the same RFC 7807 envelope. Contract: `_docs/02_document/contracts/api/route-creation.md` v1.0.0 + `_docs/02_document/contracts/api/error-shape.md` v1.0.0. |
|
||||
| GET | `/api/satellite/route/{id}` | `GetRoute` | Get route with all points |
|
||||
|
||||
### gRPC Services (AZ-1074, cycle 9)
|
||||
| Service | RPC | Handler | Description |
|
||||
|---------|-----|---------|-------------|
|
||||
| `RouteTileDelivery` | `DeliverRouteTiles(DeliverRouteTilesRequest) → stream RouteTileEvent` | `RouteTileDeliveryGrpcService` | Server-streaming route tile provisioning for gps-denied-onboard pre-flight cache fill. Reuses `RouteTileDeliveryOrchestrator` + tile download pipeline. Auth: gRPC metadata `authorization: Bearer <JWT>` (same HS256 contract as REST). Proto: `SatelliteProvider.GrpcContracts/tile_provision.proto` (`package satellite.v1`). Contract doc: `_docs/02_document/contracts/c11_tilemanager/tile_provision_grpc.md`. Invalid requests (single waypoint, out-of-range lat/lon/zoom) map to gRPC `InvalidArgument`. |
|
||||
|
||||
### Local Records (defined in Program.cs)
|
||||
- `GetSatelliteTilesResponse`, `SatelliteTile` — MGRS response stubs
|
||||
- `DownloadTileResponse` — tile download response
|
||||
@@ -74,11 +79,12 @@ Application entry point. Configures DI container, sets up middleware, defines mi
|
||||
8. CORS policy: `TilesCors` — configured origins from `CorsConfig:AllowedOrigins`, falls back to allow-any
|
||||
9. JSON options: camelCase, case-insensitive
|
||||
10. **JWT authentication (AZ-487 + AZ-494)**: `AddSatelliteJwt(builder.Configuration)` (extension in `SatelliteProvider.Api.Authentication`) registers `JwtBearer` with `TokenValidationParameters` set per the suite auth contract: signature + lifetime + issuer + audience validation, 30 s clock skew, ≥ 32-byte HMAC key. The `iss` value comes from `JWT_ISSUER` env (fallback `Jwt:Issuer` config); the `aud` value comes from `JWT_AUDIENCE` env (fallback `Jwt:Audience` config). All three values (secret, iss, aud) are fail-fast — the API throws `InvalidOperationException` at startup if any is unset or whitespace-only. Production deploys MUST set the env vars with admin-team-confirmed values; `appsettings.json` ships empty so the fail-fast triggers. `appsettings.Development.json` ships clearly-tagged DEV-ONLY values (`DEV-ONLY-iss-admin-azaion-local` / `DEV-ONLY-aud-satellite-provider`) so local dev works out-of-the-box. Followed by `AddAuthorization` with the `RequiresGpsPermission` policy (AZ-488).
|
||||
11. **Kestrel HTTP/2 (AZ-505)**: `builder.WebHost.ConfigureKestrel(opts => opts.ConfigureEndpointDefaults(lo => lo.Protocols = HttpProtocols.Http1AndHttp2))`. The dev listener is now `https://+:8080` with a self-signed cert (`./certs/api.pfx`, generated idempotently by `scripts/run-tests.sh` and bound via `ASPNETCORE_Kestrel__Certificates__Default__Path` / `__Password` in `docker-compose.yml`). Kestrel needs TLS for HTTP/2 protocol negotiation; ALPN advertises both `h2` and `http/1.1` so HTTP/2-capable clients (browser Leaflet, `HttpClient` with `Version20` + `RequestVersionExact`, httpx `http2=True`) multiplex tile reads on a single TLS connection, and legacy clients fall back to HTTP/1.1. The integration-test container trusts the dev cert via `/usr/local/share/ca-certificates/` + `update-ca-certificates`. AZ-505 AC-5 verifies the multiplex semantics here; production termination is expected at the ingress (Envoy / nginx / ALB) — Kestrel can then drop to HTTP/2 cleartext behind it without changing this code.
|
||||
12. **ProblemDetails + global exception handler (AZ-795, cycle 7)**: `AddProblemDetails()` + `AddExceptionHandler<GlobalExceptionHandler>()` register the uniform RFC 7807 error pipeline. `app.UseExceptionHandler()` (in the middleware chain) routes unhandled exceptions through `GlobalExceptionHandler`, which converts `BadHttpRequestException(JsonException)` (unknown-member rejection, missing-required-field, JSON type mismatch) into `ValidationProblemDetails` with the same `errors[]` map shape that FluentValidation produces. This is the deserializer-layer half of the strict-validation contract — `error-shape.md` v1.0.0 §"Two collaborating pieces of shared infrastructure".
|
||||
13. **Strict JSON parsing (AZ-795, cycle 7)**: `ConfigureHttpJsonOptions` sets `PropertyNamingPolicy = CamelCase`, `PropertyNameCaseInsensitive = true`, `UnmappedMemberHandling = Disallow`, and adds `JsonStringEnumConverter` with camelCase naming. `UnmappedMemberHandling.Disallow` is the key strict-parsing knob: any unknown root or nested field is rejected at the deserializer rather than silently dropped. Catches typos (`{"Z":12}` uppercase, `{"tileZoom":...}` post-rename) that no FluentValidation rule can see after deserialization.
|
||||
14. **FluentValidation registration (AZ-795 + AZ-796, cycle 7)**: `AddValidatorsFromAssemblyContaining<Program>()` auto-registers every `IValidator<T>` in the API assembly (currently `InventoryRequestValidator` + `TileCoordValidator`, AZ-808 `RegionRequestValidator`, AZ-809 `CreateRouteRequestValidator` + `RoutePointValidator` + `GeofencePolygonValidator`, AZ-810 `UavTileBatchMetadataPayloadValidator` + `UavTileMetadataValidator`, AZ-811 `GetTileByLatLonQueryValidator`). `GlobalValidatorConfig.ApplyOnce()` runs the idempotent process-wide config — sets `ValidatorOptions.Global.PropertyNameResolver` so `errors` map keys are camelCase (matches the request body's casing per `error-shape.md` Inv-4). Per-endpoint opt-in via `.WithValidation<T>()` on the JSON-body endpoints — the generic `ValidationEndpointFilter<T>` resolves the validator from DI at request time and returns `Results.ValidationProblem` on failure.
|
||||
15. **AZ-810 multipart validation filter (cycle 8)**: `AddTransient<UavUploadValidationFilter>()` registers the bespoke filter used by `POST /api/satellite/upload`. The endpoint is `multipart/form-data` so the generic `.WithValidation<T>()` JSON-body filter cannot bind; this filter reads the `metadata` form field, deserializes it via the strict global `JsonSerializerOptions`, runs the FluentValidation chain, and enforces the cross-field `items.Count == files.Count` envelope rule. Wired on the endpoint with `.AddEndpointFilter<UavUploadValidationFilter>()` between `.RequireAuthorization(SatellitePermissions.UavUploadPolicy)` and the metadata accept/produces annotations.
|
||||
11. **Kestrel HTTP/2 (AZ-505)**: `builder.WebHost.ConfigureKestrel(opts => opts.ConfigureEndpointDefaults(lo => lo.Protocols = HttpProtocols.Http1AndHttp2))`. The dev listener is now `https://+:8080` with a self-signed cert (`./certs/api.pfx`, generated idempotently by `scripts/run-tests.sh` and bound via `ASPNETCORE_Kestrel__Certificates__Default__Path` / `__Password` in `docker-compose.yml`). Kestrel needs TLS for HTTP/2 protocol negotiation; ALPN advertises both `h2` and `http/1.1` so HTTP/2-capable clients (browser Leaflet, `HttpClient` with `Version20` + `RequestVersionExact`, httpx `http2=True`, gRPC over HTTP/2) multiplex tile reads on a single TLS connection, and legacy clients fall back to HTTP/1.1. The integration-test container trusts the dev cert via `/usr/local/share/ca-certificates/` + `update-ca-certificates`. AZ-505 AC-5 verifies the multiplex semantics here; production termination is expected at the ingress (Envoy / nginx / ALB) — Kestrel can then drop to HTTP/2 cleartext behind it without changing this code.
|
||||
12. **gRPC (AZ-1074, cycle 9)**: `AddGrpc()` + `MapGrpcService<RouteTileDeliveryGrpcService>()`. Shares JWT auth middleware with REST — callers pass `authorization: Bearer <token>` in gRPC metadata. Server-streaming RPC delegates to `IRouteTileDeliveryOrchestrator.DeliverAsync`.
|
||||
13. **ProblemDetails + global exception handler (AZ-795, cycle 7)**: `AddProblemDetails()` + `AddExceptionHandler<GlobalExceptionHandler>()` register the uniform RFC 7807 error pipeline. `app.UseExceptionHandler()` (in the middleware chain) routes unhandled exceptions through `GlobalExceptionHandler`, which converts `BadHttpRequestException(JsonException)` (unknown-member rejection, missing-required-field, JSON type mismatch) into `ValidationProblemDetails` with the same `errors[]` map shape that FluentValidation produces. This is the deserializer-layer half of the strict-validation contract — `error-shape.md` v1.0.0 §"Two collaborating pieces of shared infrastructure".
|
||||
14. **Strict JSON parsing (AZ-795, cycle 7)**: `ConfigureHttpJsonOptions` sets `PropertyNamingPolicy = CamelCase`, `PropertyNameCaseInsensitive = true`, `UnmappedMemberHandling = Disallow`, and adds `JsonStringEnumConverter` with camelCase naming. `UnmappedMemberHandling.Disallow` is the key strict-parsing knob: any unknown root or nested field is rejected at the deserializer rather than silently dropped. Catches typos (`{"Z":12}` uppercase, `{"tileZoom":...}` post-rename) that no FluentValidation rule can see after deserialization.
|
||||
15. **FluentValidation registration (AZ-795 + AZ-796, cycle 7)**: `AddValidatorsFromAssemblyContaining<Program>()` auto-registers every `IValidator<T>` in the API assembly (currently `InventoryRequestValidator` + `TileCoordValidator`, AZ-808 `RegionRequestValidator`, AZ-809 `CreateRouteRequestValidator` + `RoutePointValidator` + `GeofencePolygonValidator`, AZ-810 `UavTileBatchMetadataPayloadValidator` + `UavTileMetadataValidator`, AZ-811 `GetTileByLatLonQueryValidator`). `GlobalValidatorConfig.ApplyOnce()` runs the idempotent process-wide config — sets `ValidatorOptions.Global.PropertyNameResolver` so `errors` map keys are camelCase (matches the request body's casing per `error-shape.md` Inv-4). Per-endpoint opt-in via `.WithValidation<T>()` on the JSON-body endpoints — the generic `ValidationEndpointFilter<T>` resolves the validator from DI at request time and returns `Results.ValidationProblem` on failure.
|
||||
16. **AZ-810 multipart validation filter (cycle 8)**: `AddTransient<UavUploadValidationFilter>()` registers the bespoke filter used by `POST /api/satellite/upload`. The endpoint is `multipart/form-data` so the generic `.WithValidation<T>()` JSON-body filter cannot bind; this filter reads the `metadata` form field, deserializes it via the strict global `JsonSerializerOptions`, runs the FluentValidation chain, and enforces the cross-field `items.Count == files.Count` envelope rule. Wired on the endpoint with `.AddEndpointFilter<UavUploadValidationFilter>()` between `.RequireAuthorization(SatellitePermissions.UavUploadPolicy)` and the metadata accept/produces annotations.
|
||||
|
||||
### Startup
|
||||
1. Database migration via `DatabaseMigrator.RunMigrations()` — throws on failure
|
||||
@@ -128,7 +134,8 @@ NuGet: `Serilog.AspNetCore` (8.0.3 — fallback retained on .NET 10 per AZ-500 R
|
||||
|
||||
## Consumers
|
||||
- HTTP clients (external)
|
||||
- Integration tests (via HTTP)
|
||||
- gRPC clients (gps-denied-onboard C11 — AZ-1074)
|
||||
- Integration tests (via HTTP + gRPC)
|
||||
|
||||
## Data Models
|
||||
Defines several local request/response records that are not shared with other projects.
|
||||
|
||||
@@ -18,6 +18,7 @@ Console application that runs end-to-end integration tests against a live API in
|
||||
- `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 by `TileCoordValidator`.
|
||||
- `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 legacy `tileZoom/tileX/tileY` now yields 400), `TypeMismatch_Returns400`. Each test exercises one of the 9 validation rules end-to-end through `ValidationEndpointFilter<TileInventoryRequest>` + `GlobalExceptionHandler`, asserts HTTP 400 + RFC 7807 `ValidationProblemDetails` shape via the shared `ProblemDetailsAssertions` helper.
|
||||
- `IdempotentPostTests` — pre-existing; cycle 7 adjusted the route-point payload from PascalCase (`Latitude`/`Longitude`) to camelCase (`lat`/`lon`) because the post-AZ-795 `UnmappedMemberHandling.Disallow` would otherwise reject the previously-silently-ignored fields. The `RoutePoint` DTO has carried `JsonPropertyName("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.
|
||||
- `RouteTileDeliveryGrpcTests` (added cycle 9 — AZ-1074/AZ-1075) — `RunHappyPath`, `RunInvalidRequests` (single waypoint / lat out of range / zoom out of range → `InvalidArgument`), `RunBackpressureSafe` (slow consumer preserves JPEG + SHA256), `RunRestConsistency` (REST route CSV tile keys overlap gRPC stream keys). Wired into both smoke and full suites via `Program.cs`.
|
||||
|
||||
### Supporting Classes
|
||||
- `Models.cs` — HTTP response DTOs for deserialization
|
||||
@@ -34,9 +35,11 @@ Console application that runs end-to-end integration tests against a live API in
|
||||
- `IntegrationTestDatabaseReset.cs` (AZ-493) — instance class with a single `EnsureCleanStateAsync()` method that truncates the integration-test target tables in FK-safe order. Guarded via `SatelliteProvider.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 subject `perf-tests` and a `permissions: GPS` claim via the canonical `SatelliteProvider.TestSupport.JwtTokenFactory.Create`; `GenerateUavFixture(args)` writes a 256×256 random-noise JPEG via `SixLabors.ImageSharp` to the path passed on the CLI. Invoked from `scripts/run-performance-tests.sh` via `dotnet <SatelliteProvider.IntegrationTests.dll> --mint-only` and `--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 a `JsonElement` with 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 the `errors` map per `error-shape.md` Inv-2 and optionally checks a specific field path / message substring. Consumed by `TileInventoryValidationTests`; designed to be reused by every future per-endpoint child task under AZ-795.
|
||||
- `GrpcTestHelpers.cs` (added cycle 9 — AZ-1075) — gRPC client factory (`CreateClient` over TLS with dev cert trust), request builders, stream collector, and `ExpectInvalidArgumentAsync` assertion helper. Consumed exclusively by `RouteTileDeliveryGrpcTests`.
|
||||
|
||||
## Internal Logic
|
||||
- Makes HTTP calls to the API at `API_URL` environment variable (default: `http://api:8080`)
|
||||
- Makes HTTP calls to the API at `API_URL` environment variable (default: `https://api:8080` post-AZ-505 TLS)
|
||||
- Makes gRPC calls to the same host via `Grpc.Net.Client` + generated stubs from `SatelliteProvider.GrpcContracts`
|
||||
- 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
|
||||
@@ -45,13 +48,13 @@ Console application that runs end-to-end integration tests against a live API in
|
||||
- `ProjectReference` to `SatelliteProvider.TestSupport` (added by AZ-491; provides `JwtTokenFactory`. Added by AZ-493; provides `IntegrationTestResetGuard`).
|
||||
- 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 `MigrationTests` schema assertions.
|
||||
- NuGet: `Npgsql` 9.0.2 (Postgres client for DB-reset + MigrationTests), `SixLabors.ImageSharp` 3.1.11 (UAV fixture image generation).
|
||||
- ProjectReferences: `SatelliteProvider.Api` (running service for the integration runner), `SatelliteProvider.TestSupport` (canonical `JwtTokenFactory` + `IntegrationTestResetGuard`), `SatelliteProvider.Common` (added by AZ-503 so the `MultiSourceCoexistence_AZ484_Cycle2` seeder can compute `location_hash` via `Uuidv5.Create` instead of duplicating the UUIDv5 algorithm in T-SQL fixtures).
|
||||
- ProjectReferences: `SatelliteProvider.Api` (running service for the integration runner), `SatelliteProvider.TestSupport` (canonical `JwtTokenFactory` + `IntegrationTestResetGuard`), `SatelliteProvider.Common` (added by AZ-503 so the `MultiSourceCoexistence_AZ484_Cycle2` seeder can compute `location_hash` via `Uuidv5.Create` instead of duplicating the UUIDv5 algorithm in T-SQL fixtures), `SatelliteProvider.GrpcContracts` (added cycle 9 — generated gRPC client stubs for `RouteTileDeliveryGrpcTests`).
|
||||
|
||||
## Consumers
|
||||
- `docker-compose.tests.yml` — runs as a container that depends on the API service
|
||||
|
||||
## Configuration
|
||||
- `API_URL` environment variable (set in docker-compose.tests.yml to `http://api:8080`)
|
||||
- `API_URL` environment variable (set in docker-compose.tests.yml to `https://api:8080`)
|
||||
- `INTEGRATION_TESTS_MODE` — `smoke` or `full` (default `full`). Drives `TestRunMode.Smoke`.
|
||||
- `INTEGRATION_KEEP_STATE` — set to `1` or `true` (or pass `--keep-state` to `Program.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).
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# Ripple Log — Cycle 9
|
||||
|
||||
Tasks: AZ-1074, AZ-1075 (gRPC RouteTileDelivery + integration tests)
|
||||
|
||||
- `_docs/02_document/modules/api_program.md` — gRPC service registration + `RouteTileDeliveryGrpcService` (changed by AZ-1074)
|
||||
- `_docs/02_document/modules/tests_integration.md` — `RouteTileDeliveryGrpcTests` + `GrpcTestHelpers` (changed by AZ-1075)
|
||||
- `_docs/02_document/tests/blackbox-tests.md` — BT-32 gRPC scenarios (test-spec sync)
|
||||
- `_docs/02_document/tests/traceability-matrix.md` — AZ-1074/1075 AC rows (test-spec sync)
|
||||
|
||||
No import-graph ripple beyond IntegrationTests → GrpcContracts (direct ProjectReference from task scope).
|
||||
@@ -393,3 +393,22 @@ Cycle 8 extends the AZ-795 shared validation infrastructure (FluentValidation +
|
||||
**AC trace**: AZ-811 AC-1 (5 rules + ProblemDetails shape), AC-2 (happy path), AC-9 (the novel unknown-query-param envelope filter is documented in `_docs/02_document/modules/api_program.md` for reuse).
|
||||
**Notes**: This is the first endpoint to need a generic **unknown-query-param rejection** layer — ASP.NET's default model binder silently ignores unknown query parameters (parallel to `UnmappedMemberHandling.Disallow` for JSON bodies, but no built-in equivalent exists for query strings). The new `UnknownQueryParameterEndpointFilter` introspects the route's declared parameters and rejects any extra keys. Sub-cases `4b` and `4c` exercise this filter: `4b` proves the pre-AZ-811 wire format (`?Latitude=&Longitude=&ZoomLevel=`) that silently fell back to `lat=0, lon=0, zoom=0` now fails fast with HTTP 400 naming all three unknown keys; `4c` proves the same path catches arbitrary hostile / typo keys. The filter is designed for reuse by any future query-param endpoint (AZ-811 AC-9).
|
||||
|
||||
## BT-32: gRPC RouteTileDelivery — Happy Path + Validation + REST Consistency
|
||||
|
||||
**Trigger**: `RouteTileDelivery.DeliverRouteTiles(DeliverRouteTilesRequest)` over gRPC (TLS to `https://api:8080` inside docker-compose; metadata `authorization: Bearer <JWT>`). Contract: `_docs/02_document/contracts/c11_tilemanager/tile_provision_grpc.md` + `SatelliteProvider.GrpcContracts/tile_provision.proto`.
|
||||
**Precondition**: API + Postgres up via `docker-compose.tests.yml`; JWT env vars set; dev TLS cert trusted in integration-tests container.
|
||||
**Expected stream sequence**: `RouteManifest` → zero or more `TileChunk` → `DeliveryComplete` (or `DeliveryError` on failure paths).
|
||||
|
||||
| # | Scenario | Trigger excerpt | Expected | Test method |
|
||||
|---|----------|-----------------|----------|-------------|
|
||||
| pos | Happy path | 2 waypoints (48.276067,37.384458) → (48.270740,37.374029), regionSize=500, zoom=18 | `RouteManifest` with `to_deliver ≥ 0`; at least one `TileChunk` when `to_deliver > 0`; terminal `DeliveryComplete` | `RunHappyPath` |
|
||||
| 1 | Single waypoint | `waypoints` count = 1 | gRPC `StatusCode.InvalidArgument` before stream completes | `RunInvalidRequests` ("Single waypoint route") |
|
||||
| 2 | Lat out of range | first waypoint `lat=91` | gRPC `StatusCode.InvalidArgument` | `RunInvalidRequests` ("Latitude out of range") |
|
||||
| 3 | Zoom out of range | `zoom=99` | gRPC `StatusCode.InvalidArgument` | `RunInvalidRequests` ("Zoom out of allowed range") |
|
||||
| 4 | Backpressure safe | same as happy path; consumer delays 50 ms between stream events | every `TileChunk.jpeg` starts with `FF D8`; `content_sha256` matches SHA-256 of JPEG bytes | `RunBackpressureSafe` |
|
||||
| 5 | REST consistency | REST `POST /api/satellite/route` with `requestMaps=true` for same geometry; then gRPC stream for same corridor | gRPC tile keys `(z,x,y)` overlap REST route CSV tile keys by ≥ 1 | `RunRestConsistency` |
|
||||
|
||||
**Pass criterion**: All sub-cases pass in full integration mode (`INTEGRATION_TESTS_MODE=full`). No `DeliveryError` on happy path / backpressure path. Invalid requests fail with `InvalidArgument` (not `Internal` / `Unknown`). REST overlap count > 0.
|
||||
**AC trace**: AZ-1074 AC-1..AC-4; AZ-1075 AC-1..AC-3.
|
||||
**Notes**: gRPC is additive — REST route endpoints (BT-06..BT-12) remain unchanged. Cache-reuse (AZ-1074 AC-2) is covered structurally by the orchestrator unit tests (`RouteTileDeliveryOrchestratorTests.DeliverAsync_CachedTileOnDisk_EmitsBatchWithoutDownload`) plus the integration happy path reusing tiles seeded by prior REST runs in the same compose volume. Consumer-side tests (gps-denied-onboard AZ-1076) are out of scope.
|
||||
|
||||
|
||||
@@ -263,3 +263,18 @@
|
||||
- AZ-809 ACs 9 + 10 are **advisory** (surfaced for parent-suite team decision, not implemented or tested this cycle). Matrix marks them `◐ advisory (not tested)`. They're recorded so the next cycle / parent-suite review sees them without having to re-discover them from the task spec. AC-9: `RequestRegionRequest.sizeMeters` vs `CreateRouteRequest.regionSizeMeters` naming inconsistency. AC-10: input `points: [{lat, lon}]` vs output `points: [{latitude, longitude}]` round-trip asymmetry on the route endpoint. Either keep + document, or harmonize in a follow-up MAJOR contract bump for both — parent-suite team's call.
|
||||
- AZ-810 AC-9 (no AZ-488 regression) has a **process annotation**: cycle 8's batch_04 report originally claimed AC-9 "verified by tracing each AZ-488 test payload's metadata shape against the new rules" without running the integration suite. That verification was a false-PASS — the suite was actually red on the AZ-488 happy path because `UavUploadTests.NextTestCoordinate()` produced lat > 90° (a pre-existing latent bug masked by the absence of any validator before AZ-810). The bug was fixed by clamping the test-data generator to OSM-valid ranges in commit `b763da3` and AC-9 is now bound to the green full-suite re-run, not to source tracing. Process lesson recorded in `_docs/LESSONS.md` (2026-05-23).
|
||||
- Cycle-update rule check: no NFR conflicts. Range bounds (`lat ∈ [-90, 90]`, `lon ∈ [-180, 180]`, `zoom ∈ [0, 22]`, `tileSizeMeters > 0`) are reaffirmed across all 4 endpoints — they were never previously contested. The error-shape contract (`error-shape.md` v1.0.0 from cycle 7) is reused unchanged.
|
||||
|
||||
| AZ-1074 AC-1 | Happy path streams tiles — `RouteManifest` + ≥1 `TileChunk` + `DeliveryComplete` | BT-32 pos (blackbox); `RouteTileDeliveryGrpcTests.RunHappyPath` (integration) | ✓ |
|
||||
| AZ-1074 AC-2 | Cached tiles served without redundant Google Maps download | `RouteTileDeliveryOrchestratorTests.DeliverAsync_CachedTileOnDisk_EmitsBatchWithoutDownload` (unit); BT-32 pos reuses compose volume cache (integration) | ✓ |
|
||||
| AZ-1074 AC-3 | Invalid route / coordinates rejected with `INVALID_ARGUMENT` | BT-32 sub-cases 1–3 (blackbox); `RouteTileDeliveryGrpcTests.RunInvalidRequests` (integration) | ✓ |
|
||||
| AZ-1074 AC-4 | Slow consumer — tile bytes not corrupted | BT-32 sub-case 4 (blackbox); `RouteTileDeliveryGrpcTests.RunBackpressureSafe` (integration) | ✓ |
|
||||
| AZ-1075 AC-1 | gRPC happy-path passes in docker-compose full run | Full `scripts/run-tests.sh --full` / `docker-compose.tests.yml` (cycle 9 Step 11 — passed) | ✓ |
|
||||
| AZ-1075 AC-2 | Each invalid variant returns expected gRPC status | BT-32 sub-cases 1–3; `RouteTileDeliveryGrpcTests.RunInvalidRequests` | ✓ |
|
||||
| AZ-1075 AC-3 | REST and gRPC tile metadata consistent for same route | BT-32 sub-case 5; `RouteTileDeliveryGrpcTests.RunRestConsistency` | ✓ |
|
||||
|
||||
**Coverage shape notes (Cycle 9 — AZ-1074 + AZ-1075 gRPC RouteTileDelivery):**
|
||||
- Cycle 9 adds the first gRPC blackbox surface alongside the existing REST suite. BT-32 is the binding blackbox spec; integration coverage lives in `RouteTileDeliveryGrpcTests` wired into both smoke and full suites via `Program.cs`.
|
||||
- Proto source of truth moved to `SatelliteProvider.GrpcContracts/tile_provision.proto` (GrpcServices=Both); contract doc at `_docs/02_document/contracts/c11_tilemanager/tile_provision_grpc.md`.
|
||||
- Cycle 9 Step 11 initially failed integration startup due to host port 5433 conflict with sibling project `fleet-viewer-dev-db`. Fixed by making `docker-compose.tests.yml` self-contained (no host port publishing — compose-internal networking only) and pointing `scripts/run-tests.sh` at that file alone for integration runs. Unit count is now 448 (includes orchestrator + gRPC validation tests).
|
||||
- No perf / security NFRs declared in AZ-1074/1075 task specs beyond existing JWT-on-gRPC-metadata (inherits AZ-487/494 invariants). Load testing explicitly excluded.
|
||||
- Cycle-update rule check: no NFR conflicts.
|
||||
|
||||
Reference in New Issue
Block a user