diff --git a/SatelliteProvider.Api/Grpc/RouteTileDeliveryGrpcService.cs b/SatelliteProvider.Api/Grpc/RouteTileDeliveryGrpcService.cs index 8a33e80..1f4944c 100644 --- a/SatelliteProvider.Api/Grpc/RouteTileDeliveryGrpcService.cs +++ b/SatelliteProvider.Api/Grpc/RouteTileDeliveryGrpcService.cs @@ -54,7 +54,7 @@ public sealed class RouteTileDeliveryGrpcService : RouteTileDelivery.RouteTileDe catch (Exception ex) { _logger.LogError(ex, "Route tile delivery failed for route {RouteId}", routeId); - await WriteErrorAsync(responseStream, "INTERNAL_ERROR", ex.Message, retryable: true, context.CancellationToken); + await WriteErrorAsync(responseStream, "INTERNAL_ERROR", "An internal error occurred.", retryable: true, context.CancellationToken); } } diff --git a/SatelliteProvider.Services.RouteManagement/TileProvision/RouteTileDeliveryOrchestrator.cs b/SatelliteProvider.Services.RouteManagement/TileProvision/RouteTileDeliveryOrchestrator.cs index 884191d..02ab636 100644 --- a/SatelliteProvider.Services.RouteManagement/TileProvision/RouteTileDeliveryOrchestrator.cs +++ b/SatelliteProvider.Services.RouteManagement/TileProvision/RouteTileDeliveryOrchestrator.cs @@ -214,6 +214,10 @@ public sealed class RouteTileDeliveryOrchestrator await sink.WriteCompleteAsync(delivered, skippedByClient, serverFiltered, cancellationToken); } + private const int MaxWaypoints = 500; + private const int MaxGeofencePolygons = 50; + private const int MaxClientTiles = 5000; + private void ValidateJob(RouteTileDeliveryJob job) { if (job.RouteId == Guid.Empty) @@ -226,6 +230,27 @@ public sealed class RouteTileDeliveryOrchestrator throw new ArgumentException("Route must have at least 2 waypoints", nameof(job)); } + if (job.Waypoints.Count > MaxWaypoints) + { + throw new ArgumentException( + $"waypoints must contain at most {MaxWaypoints} entries", + nameof(job)); + } + + if (job.GeofenceVertices.Count > MaxGeofencePolygons) + { + throw new ArgumentException( + $"geofences must contain at most {MaxGeofencePolygons} polygons", + nameof(job)); + } + + if (job.ClientTiles.Count > MaxClientTiles) + { + throw new ArgumentException( + $"client_tiles must contain at most {MaxClientTiles} entries", + nameof(job)); + } + for (var i = 0; i < job.Waypoints.Count; i++) { var (lat, lon) = job.Waypoints[i]; diff --git a/SatelliteProvider.Tests/RouteTileDeliveryOrchestratorTests.cs b/SatelliteProvider.Tests/RouteTileDeliveryOrchestratorTests.cs index 9e13fa4..6a473b1 100644 --- a/SatelliteProvider.Tests/RouteTileDeliveryOrchestratorTests.cs +++ b/SatelliteProvider.Tests/RouteTileDeliveryOrchestratorTests.cs @@ -37,6 +37,29 @@ public class RouteTileDeliveryOrchestratorTests .WithMessage("*lat must be between -90 and 90*"); } + [Fact] + public async Task DeliverAsync_TooManyWaypoints_Throws() + { + var expander = new RouteTileExpander( + new RoutePointGraphBuilder(Options.Create(new ProcessingConfig { MaxRoutePointSpacingMeters = 200 })), + new GeofenceGridCalculator()); + var orchestrator = CreateOrchestrator(expander, Mock.Of(), Mock.Of()); + var waypoints = Enumerable.Range(0, 501).Select(i => (47.0 + i * 0.0001, 37.0)).ToList(); + var job = new RouteTileDeliveryJob( + Guid.NewGuid(), + waypoints, + 400, + 18, + [], + false, + []); + + var act = () => orchestrator.DeliverAsync(job, new RecordingSink(), CancellationToken.None); + + await act.Should().ThrowAsync() + .WithMessage("*at most 500*"); + } + [Fact] public async Task DeliverAsync_AllTilesSkippedByClient_EmitsManifestAndCompleteWithZeroDelivered() { diff --git a/_docs/02_document/module-layout.md b/_docs/02_document/module-layout.md index 4754e9d..c92e369 100644 --- a/_docs/02_document/module-layout.md +++ b/_docs/02_document/module-layout.md @@ -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()`. 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 diff --git a/_docs/02_document/modules/api_program.md b/_docs/02_document/modules/api_program.md index 7ffd577..3d19fb6 100644 --- a/_docs/02_document/modules/api_program.md +++ b/_docs/02_document/modules/api_program.md @@ -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()`: 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 ` (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()` 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()` auto-registers every `IValidator` 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()` on the JSON-body endpoints — the generic `ValidationEndpointFilter` resolves the validator from DI at request time and returns `Results.ValidationProblem` on failure. -15. **AZ-810 multipart validation filter (cycle 8)**: `AddTransient()` registers the bespoke filter used by `POST /api/satellite/upload`. The endpoint is `multipart/form-data` so the generic `.WithValidation()` 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()` 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()`. Shares JWT auth middleware with REST — callers pass `authorization: Bearer ` in gRPC metadata. Server-streaming RPC delegates to `IRouteTileDeliveryOrchestrator.DeliverAsync`. +13. **ProblemDetails + global exception handler (AZ-795, cycle 7)**: `AddProblemDetails()` + `AddExceptionHandler()` 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()` auto-registers every `IValidator` 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()` on the JSON-body endpoints — the generic `ValidationEndpointFilter` resolves the validator from DI at request time and returns `Results.ValidationProblem` on failure. +16. **AZ-810 multipart validation filter (cycle 8)**: `AddTransient()` registers the bespoke filter used by `POST /api/satellite/upload`. The endpoint is `multipart/form-data` so the generic `.WithValidation()` 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()` 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. diff --git a/_docs/02_document/modules/tests_integration.md b/_docs/02_document/modules/tests_integration.md index 49fb8a9..70ab4ba 100644 --- a/_docs/02_document/modules/tests_integration.md +++ b/_docs/02_document/modules/tests_integration.md @@ -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` + `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 --mint-only` and `--gen-uav-fixture `. - `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). diff --git a/_docs/02_document/ripple_log_cycle9.md b/_docs/02_document/ripple_log_cycle9.md new file mode 100644 index 0000000..796008c --- /dev/null +++ b/_docs/02_document/ripple_log_cycle9.md @@ -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). diff --git a/_docs/02_document/tests/blackbox-tests.md b/_docs/02_document/tests/blackbox-tests.md index 459606e..4a49b5e 100644 --- a/_docs/02_document/tests/blackbox-tests.md +++ b/_docs/02_document/tests/blackbox-tests.md @@ -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 `). 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. + diff --git a/_docs/02_document/tests/traceability-matrix.md b/_docs/02_document/tests/traceability-matrix.md index 1fc59ca..e3b4c44 100644 --- a/_docs/02_document/tests/traceability-matrix.md +++ b/_docs/02_document/tests/traceability-matrix.md @@ -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. diff --git a/_docs/03_implementation/deploy_cycle9.md b/_docs/03_implementation/deploy_cycle9.md new file mode 100644 index 0000000..ee0c1e1 --- /dev/null +++ b/_docs/03_implementation/deploy_cycle9.md @@ -0,0 +1,73 @@ +# Deploy Report — Cycle 9 (AZ-1074 + AZ-1075) + +**Date**: 2026-06-25 +**Cycle**: 9 +**Scope**: gRPC RouteTileDelivery streaming service (AZ-1074) + integration test coverage (AZ-1075). + +## What is shipping + +### Code changes (uncommitted on `dev` at report time) + +| Area | Change | +|------|--------| +| `SatelliteProvider.GrpcContracts/` | New shared project — `tile_provision.proto`, GrpcServices=Both | +| `SatelliteProvider.Api/Grpc/RouteTileDeliveryGrpcService.cs` | `[Authorize]` streaming RPC; generic internal error to client | +| `SatelliteProvider.Api/Program.cs` | `AddGrpc` (16 MiB recv / 64 MiB send); `MapGrpcService` after auth | +| `RouteTileDeliveryOrchestrator` | Collection caps (500/50/5000); lat/lon validation | +| Integration tests | `RouteTileDeliveryGrpcTests`, `GrpcTestHelpers`; standalone `docker-compose.tests.yml` | +| Docs / security | Cycle-9 module docs, traceability, security reports + hardening | + +### Database migrations + +**None.** + +### Configuration changes + +| Setting | Change | +|---------|--------| +| New env vars | **None** — gRPC uses existing JWT auth | +| `docker-compose.tests.yml` | Self-contained test stack (no host port publish) | +| Container image | Same base `mcr.microsoft.com/dotnet/aspnet:10.0`; Dockerfile copies GrpcContracts | + +### Contract changes (consumer-visible) + +| Contract | Change | Consumer action | +|----------|--------|-----------------| +| gRPC `RouteTileDelivery.DeliverRouteTiles` | **New** — server-streaming tile delivery | Clients import `SatelliteProvider.GrpcContracts`; Bearer/metadata auth required | +| REST endpoints | Unchanged wire shape | No action | + +## Verification gates passed in this cycle + +| Gate | Result | Evidence | +|------|--------|----------| +| Step 11 — Functional tests | **PASS** | 448 unit + integration exit 0 (`scripts/run-tests.sh --full`) | +| Step 12 — Test-Spec Sync | **PASS** | BT-32 gRPC scenarios; traceability matrix updated | +| Step 13 — Update Docs | **PASS** | `api_program.md`, `tests_integration.md`, `module-layout.md`, ripple log | +| Step 14 — Security Audit | **PASS** (delta) | `security_report_cycle9.md`; F-AZ1074-1/2 resolved in follow-up | +| Step 15 — Performance Test | **PASS** | `perf_2026-06-25_cycle9.md` — 8/8 REST scenarios, exit 0 | + +## Outstanding carry-overs (unchanged) + +- F-AZ795-1, F-AZ795-2 — REST error message sanitization +- D-AZ795-1 — FluentValidation 12.0.0 → 12.1.1 +- D2-cy4 — test SDK transitive advisory + +## Operator runbook + +1. **Commit and push** cycle-9 changes to `origin/dev`; confirm Woodpecker `01-test` green. +2. **No migration** — deploy new image only. +3. **Smoke-test** after deploy: + - REST: existing endpoints (401 without JWT, 200 with JWT) + - gRPC: `DeliverRouteTiles` with valid JWT — expect manifest + tile chunks or InvalidArgument on bad input +4. **Ingress**: ensure HTTP/2 (gRPC) is forwarded if clients connect through ingress; dev stack uses TLS on 18980. + +## Recommended follow-up PBIs + +| Estimate | Title | +|----------|-------| +| 3 SP | PT-10 gRPC stream latency / backpressure perf scenario | +| 2 SP | REST error sanitizer pass (F-AZ795-1/2) | +| 1 SP | FluentValidation 12.0.0 → 12.1.1 (D-AZ795-1) | +| 1 SP | Document postgres port conflict workaround for parallel dev stacks | + +**Verdict**: Cleared for Step 16.5 (Release) pending operator commit/push and environment selection. diff --git a/_docs/05_security/dependency_scan_cycle9.md b/_docs/05_security/dependency_scan_cycle9.md new file mode 100644 index 0000000..a129045 --- /dev/null +++ b/_docs/05_security/dependency_scan_cycle9.md @@ -0,0 +1,38 @@ +# Dependency Scan (Cycle 9) + +**Date**: 2026-06-25 +**Mode**: Delta scan +**Scope**: Cycle-9 delta over cycle-8 (`dependency_scan_cycle8.md`). Surface = AZ-1074/AZ-1075 gRPC RouteTileDelivery + `SatelliteProvider.GrpcContracts`. +**Method**: `dotnet list SatelliteProvider.sln package --vulnerable --include-transitive` via Docker SDK 10.0 image + manifest diff on new/changed csproj files. + +## Cycle-9 Package Manifest Diff + +| csproj | Cycle 8 baseline | Cycle 9 change | +|--------|------------------|----------------| +| `SatelliteProvider.Api/SatelliteProvider.Api.csproj` | unchanged | **+1** `Grpc.AspNetCore` 2.71.0 | +| `SatelliteProvider.GrpcContracts/SatelliteProvider.GrpcContracts.csproj` | **NEW** | `Google.Protobuf` 3.31.1, `Grpc.AspNetCore` 2.71.0, `Grpc.Tools` 2.71.0 (PrivateAssets) | +| All other csproj | unchanged | **+0** | + +## Vulnerable Package Scan (2026-06-25) + +| Project | Finding | Severity | Notes | +|---------|---------|----------|-------| +| `SatelliteProvider.Api` | none | — | Includes new `Grpc.AspNetCore` 2.71.0 — clean | +| `SatelliteProvider.GrpcContracts` | none | — | New project — clean | +| `SatelliteProvider.IntegrationTests` | transitive `Microsoft.IdentityModel.JsonWebTokens` 7.0.3, `System.IdentityModel.Tokens.Jwt` 7.0.3 | Moderate | GHSA-59j7-ghrg-fj52 — **test-runtime only** (pre-existing; unchanged by cycle 9) | +| `SatelliteProvider.TestSupport` | same JWT packages 7.0.3 | Moderate | test-runtime only — pre-existing | + +## Cycle-9 Findings + +**No new dependency CVEs** from the gRPC package additions. Grpc.AspNetCore 2.71.0 / Google.Protobuf 3.31.1 report clean against NuGet advisory feed at scan time. + +## Carry-overs + +- **D-AZ795-1** (Low): FluentValidation 12.0.0 → 12.1.1 hardening — still open +- **D2-cy4** (Medium, test-runtime): `Microsoft.NET.Test.Sdk` transitive — still open + +## Verdict + +**PASS** (cycle-9 delta) — zero new CVEs in production/runtime packages. + +Cumulative: **PASS_WITH_WARNINGS** — D2-cy4 + D-AZ795-1 carry-overs unchanged. diff --git a/_docs/05_security/infrastructure_review_cycle9.md b/_docs/05_security/infrastructure_review_cycle9.md new file mode 100644 index 0000000..4c147c9 --- /dev/null +++ b/_docs/05_security/infrastructure_review_cycle9.md @@ -0,0 +1,27 @@ +# Infrastructure & Configuration Review (Cycle 9) + +**Date**: 2026-06-25 +**Mode**: Delta scan +**Scope**: Cycle-9 infrastructure changes only. + +| File | Change | Security relevance | +|------|--------|-------------------| +| `docker-compose.tests.yml` | Rewritten as self-contained stack; **no host port publishing** for postgres/api | **Positive** — avoids port conflicts; reduces accidental exposure of test DB/API to host network | +| `scripts/run-tests.sh` | Integration runs use `docker-compose.tests.yml` only | Aligns with above | +| `SatelliteProvider.Api/Dockerfile` | Added `GrpcContracts` csproj COPY | Build-order only; no new secrets | +| `SatelliteProvider.IntegrationTests/Dockerfile` | `linux/amd64` platform; `aspnet:10.0` runtime for Grpc.AspNetCore | Protoc/build stability; no new exposed ports | +| `docker-compose.yml` (dev) | Unchanged | Host ports 5433/18980 still published for local dev — pre-existing | +| CI/CD, `.env`, `appsettings.*` | Unchanged | — | + +## Container checks (carried forward) + +| Check | Status | +|-------|--------| +| Non-root user in API image | Still runs as root (pre-existing; not cycle-9 regression) | +| Secrets in build args | None | +| Dev TLS cert gitignored | `./certs/` — unchanged | +| JWT via env vars | Unchanged | + +## Verdict + +**PASS** (cycle-9 delta) — test harness change improves isolation; no new misconfiguration. diff --git a/_docs/05_security/owasp_review_cycle9.md b/_docs/05_security/owasp_review_cycle9.md new file mode 100644 index 0000000..a717384 --- /dev/null +++ b/_docs/05_security/owasp_review_cycle9.md @@ -0,0 +1,22 @@ +# OWASP Top 10 Review (Cycle 9) + +**Date**: 2026-06-25 +**Framework**: OWASP Top 10:2021 +**Scope**: Cycle-9 gRPC delta (AZ-1074/AZ-1075) + +| Category | Status (cycle-9 delta) | Notes | +|----------|------------------------|-------| +| A01 — Broken Access Control | **PASS** | `[Authorize]` on gRPC service; anonymous calls rejected (integration tests cover JWT baseline) | +| A02 — Cryptographic Failures | **N/A** | TLS via Kestrel dev cert / production ingress — unchanged pattern from AZ-505 | +| A03 — Injection | **PASS** | No new string-built SQL; tile coords validated before expand | +| A04 — Insecure Design | **PASS (post-follow-up)** | F-AZ1074-1 unbounded collections **resolved** — caps aligned with REST | +| A05 — Security Misconfiguration | **PASS** | gRPC message size limits set; test compose no longer publishes DB port to host | +| A06 — Vulnerable Components | **PASS_WITH_WARNINGS** | New Grpc.AspNetCore 2.71.0 clean; D-AZ795-1 + D2-cy4 carry-overs | +| A07 — Auth Failures | **PASS** | Same JWT contract as REST; gRPC metadata `Authorization: Bearer` | +| A08 — Data Integrity Failures | **N/A** | No CI/CD or signing changes | +| A09 — Logging Failures | **PASS_WITH_WARNINGS** | F-AZ1074-2 **resolved**; F-AZ795-1/F-AZ795-2 REST carry-overs still open | +| A10 — SSRF | **N/A** | No URL inputs in gRPC contract | + +## Verdict + +**PASS_WITH_WARNINGS** cumulative (REST carry-overs). Cycle-9 delta: **PASS** after Step-14 follow-up fixes. diff --git a/_docs/05_security/security_report_cycle9.md b/_docs/05_security/security_report_cycle9.md new file mode 100644 index 0000000..a186a0c --- /dev/null +++ b/_docs/05_security/security_report_cycle9.md @@ -0,0 +1,60 @@ +# Security Audit Report (Cycle 9) + +**Date**: 2026-06-25 +**Scope**: Cycle-9 delta — AZ-1074 (gRPC RouteTileDelivery service) + AZ-1075 (integration tests) + `SatelliteProvider.GrpcContracts`. +**Trigger**: `/autodev` Step 14 — user chose **A) Run security audit**. +**Verdict (cycle-9 delta, post-follow-up)**: **PASS** — 0 Medium open, 1 Low resolved in follow-up, 0 new Critical/High. +**Verdict (cumulative)**: **PASS_WITH_WARNINGS** — cycle-4/7/8 carry-overs unchanged (D2-cy4, D-AZ795-1, F-AZ795-1, F-AZ795-2, F-AZ810-1, F-AZ810-2). + +## Summary + +| Severity | Cycle 9 at audit | Post Step-14 follow-up | Cumulative open | +|----------|------------------|------------------------|-----------------| +| Critical | 0 | 0 | 0 | +| High | 0 | 0 | 0 | +| Medium | 1 (F-AZ1074-1) | **0 — RESOLVED** | 1 (D2-cy4 test-runtime) | +| Low | 1 (F-AZ1074-2) | **0 — RESOLVED** | 5+ (cycle 7–8 carry-overs) | + +## OWASP Top 10:2021 (cycle-9 delta) + +See `owasp_review_cycle9.md` — all categories PASS or N/A after follow-up. + +## Findings + +| # | Severity | Category | Location | Title | Status | +|---|----------|----------|----------|-------|--------| +| F-AZ1074-1 | Medium | Insecure Design (A04) | `RouteTileDeliveryOrchestrator.ValidateJob` | Unbounded gRPC waypoints/geofences/client_tiles | **RESOLVED** — caps 500/50/5000 | +| F-AZ1074-2 | Low | Information Disclosure (A09) | `RouteTileDeliveryGrpcService` catch-all | `ex.Message` in `DeliveryError` | **RESOLVED** — generic client message | + +### F-AZ1074-1 detail (RESOLVED) + +Aligned gRPC collection caps with REST: `waypoints ≤ 500`, `geofences ≤ 50`, `client_tiles ≤ 5000`. InvalidArgument via existing `ArgumentException` → `RpcException` mapping. + +### F-AZ1074-2 detail (RESOLVED) + +Internal errors now return `"An internal error occurred."` on the wire; `LogError` retains full exception. + +## Carry-overs (unchanged) + +- **F-AZ795-1, F-AZ795-2, F-AZ810-1, F-AZ810-2** — REST information-disclosure / time-handling (cycle 7–8) +- **D-AZ795-1** — FluentValidation 12.0.0 → 12.1.1 +- **D2-cy4** — test SDK transitive (Medium, test-runtime only) + +## Recommendations + +### Immediate +- None blocking cycle 9 ship. + +### Short-term (cycle 10+) +- Sanitise REST `GlobalExceptionHandler` + `UavUploadValidationFilter` (F-AZ795-1 / F-AZ810-1) in one ticket. +- Bump FluentValidation 12.0.0 → 12.1.1 (D-AZ795-1). + +### Long-term +- Consider `region_size_meters` upper bound on gRPC path (REST uses 10_000 m cap) — advisory parity, not release-blocking. + +## Artifacts + +- `dependency_scan_cycle9.md` +- `static_analysis_cycle9.md` +- `owasp_review_cycle9.md` +- `infrastructure_review_cycle9.md` diff --git a/_docs/05_security/static_analysis_cycle9.md b/_docs/05_security/static_analysis_cycle9.md new file mode 100644 index 0000000..ff3a414 --- /dev/null +++ b/_docs/05_security/static_analysis_cycle9.md @@ -0,0 +1,48 @@ +# Static Analysis (Cycle 9) + +**Date**: 2026-06-25 +**Mode**: Delta scan +**Scope**: AZ-1074 + AZ-1075 gRPC surface. Cycle-8 baseline remains authoritative for REST validators. + +**Files in scope**: +- `SatelliteProvider.Api/Grpc/RouteTileDeliveryGrpcService.cs` (new) +- `SatelliteProvider.Api/Program.cs` (`AddGrpc`, `MapGrpcService`, message size limits) +- `SatelliteProvider.Services.RouteManagement/TileProvision/RouteTileDeliveryOrchestrator.cs` (validation hardening) +- `SatelliteProvider.GrpcContracts/tile_provision.proto` + generated stubs +- `SatelliteProvider.IntegrationTests/RouteTileDeliveryGrpcTests.cs`, `GrpcTestHelpers.cs` +- `SatelliteProvider.IntegrationTests/Dockerfile` (linux/amd64, aspnet runtime) +- `docker-compose.tests.yml` (self-contained test stack) + +**Method**: End-to-end read of new files; grep for hardcoded secrets; trace auth middleware order; compare gRPC validation bounds vs REST `CreateRouteRequestValidator`. + +## Findings + +### F-AZ1074-1 — Unbounded gRPC request collections enable authenticated DoS (Medium / A04) — **RESOLVED in cycle 9 (Step-14 follow-up)** + +- **Location**: `RouteTileDeliveryOrchestrator.ValidateJob` (pre-fix). +- **Description**: `DeliverRouteTiles` accepted unbounded `waypoints`, `geofences`, and `client_tiles` protobuf repeated fields. REST `POST /api/satellite/route` caps `points` at 500 and `geofences.polygons` at 50 (cycle-8 F-AZ809-1 fix); gRPC had no equivalent caps before cycle 9 Step 14. +- **Impact**: Medium. Auth-gated (`[Authorize]` on `RouteTileDeliveryGrpcService`; JWT metadata required). Authenticated operator could force large CPU/memory work in `RouteTileExpander.Expand` and `ClientTileCatalog.IndexByZxy`. +- **Resolution**: Added `MaxWaypoints = 500`, `MaxGeofencePolygons = 50`, `MaxClientTiles = 5000` (inventory cap parity) to `ValidateJob`. Unit test `DeliverAsync_TooManyWaypoints_Throws` added. + +### F-AZ1074-2 — Internal exception message echoed to gRPC client (Low / A09) — **RESOLVED in cycle 9 (Step-14 follow-up)** + +- **Location**: `RouteTileDeliveryGrpcService.cs:55-58` (pre-fix). +- **Description**: Generic `catch (Exception)` wrote `ex.Message` into stream `DeliveryError.Message` — parallel to cycle-7 F-AZ795-1 (REST ProblemDetails path). +- **Impact**: Low. Auth-gated. Could leak internal exception text to authenticated clients. +- **Resolution**: Client message replaced with generic `"An internal error occurred."`; full exception still logged server-side. + +## Pass areas (cycle-9 delta) + +| Area | Result | +|------|--------| +| SQL injection | N/A — no new raw SQL | +| Hardcoded secrets | None in new files | +| gRPC auth | `[Authorize]` + `UseAuthentication`/`UseAuthorization` before `MapGrpcService` | +| JWT on gRPC | Integration tests pass Bearer token via metadata — matches REST contract | +| Message size limits | `MaxReceiveMessageSize = 16 MiB`, `MaxSendMessageSize = 64 MiB` configured | +| Protobuf parsing | Bounded by Kestrel/gRPC message limits; collection caps added post-audit | +| Test fixtures | `GrpcTestHelpers` uses env-resolved JWT via `JwtTestHelpers.MintAuthenticated` — no embedded secrets | + +## Verdict + +**PASS_WITH_WARNINGS** at audit time (1 Medium open → **resolved in Step-14 follow-up**). Post-fix delta: **PASS** for cycle-9 new code. diff --git a/_docs/06_metrics/perf_2026-06-25_cycle9.md b/_docs/06_metrics/perf_2026-06-25_cycle9.md new file mode 100644 index 0000000..e599673 --- /dev/null +++ b/_docs/06_metrics/perf_2026-06-25_cycle9.md @@ -0,0 +1,53 @@ +# Perf Run — Cycle 9 (AZ-1074 + AZ-1075) + +**Date**: 2026-06-25T14:18Z +**Run label**: cycle9 — full default-parameter run after gRPC RouteTileDelivery (AZ-1074) + integration tests (AZ-1075) and Step-14 security hardening (collection caps + generic internal error message). +**Trigger**: autodev existing-code Step 15 (Performance Test gate). User chose **A) Run performance tests**. +**Runner**: `scripts/run-performance-tests.sh` (default params: `PERF_REPEAT_COUNT=20`, `PERF_UAV_BATCH_SIZE=10`). Single run, exit 0. +**System under test**: `docker compose -f docker-compose.yml -f docker-compose.perf.yml up -d --build` — postgres without host port publish (5433 conflict with sibling `fleet-viewer-dev-db`); api on `https://localhost:18980` (TLS+ALPN, dev cert `./certs/api.crt` trusted via `--cacert`). +**Build**: `SatelliteProvider.IntegrationTests` Release built on host; 0 errors / 15 warnings (carried-over NU1902 IdentityModel + CA2227). +**JWT**: minted by `SatelliteProvider.IntegrationTests --mint-only`; 4 h lifetime, 341 bytes. + +## Results + +| # | Scenario | Verdict | Observed | Threshold | Source | +|---|----------|---------|----------|-----------|--------| +| PT-01 | Tile download (cold) | **PASS** | 1260 ms | ≤ 30000 ms | performance-tests.md | +| PT-02 | Cached tile retrieval | **PASS** | 220 ms | ≤ 500 ms | performance-tests.md | +| PT-03 | Region 200 m / z18 | **PASS** | 2317 ms | ≤ 60000 ms | performance-tests.md | +| PT-04 | Region 500 m / z18 + stitch | **PASS** | 2192 ms | ≤ 120000 ms | performance-tests.md | +| PT-05 | 5 concurrent regions | **PASS** | 2383 ms | ≤ 300000 ms | performance-tests.md | +| PT-06 | Route creation (2 points) | **PASS** | 244 ms | ≤ 5000 ms | performance-tests.md | +| PT-07 | Region distribution (N=20, cold + warm) | **PASS** | cold p50=2115 ms, p95=2156 ms · warm p50=45 ms, p95=79 ms | warm p95 < cold p95 | AZ-484 / AZ-492 | +| PT-08 | UAV batch upload (batch=10, N=20) | **PASS** | batch p50=90 ms, p95=179 ms; per-item proxy p95=17 ms; accepted=200, rejected=0, failed=0 | batch p95 ≤ 2000 ms | AZ-488 | + +**Raw verdict: 8 Pass · 0 Warn · 0 Fail · 0 Unverified** + +## Cycle-9 delta (gRPC) + +No gRPC scenarios in `performance-tests.md` yet (PT-01..PT-08 are REST-only). The new `DeliverRouteTiles` streaming RPC shares `RouteTileDeliveryOrchestrator` with REST tile delivery; PT-06 exercises route creation latency on the same orchestration path. Step-14 caps (500 waypoints / 50 geofences / 5000 client tiles) are O(1) count checks — invisible at REST perf resolution. + +**Unverified (not blocking)**: gRPC stream latency / backpressure under load — candidate PT-10 for a future cycle if acceptance criteria add streaming NFRs. + +## Trend vs cycle 8 + +| Scenario | Cycle 8 | Cycle 9 | Δ | Notes | +|----------|---------|---------|---|-------| +| PT-01 cold | 885 ms | 1260 ms | +375 ms | noise (tile likely cached on disk; not true cold Google Maps) | +| PT-02 cached | 244 ms | 220 ms | -24 ms | noise | +| PT-03 region 200 m | 99 ms | 2317 ms | +2218 ms | cold-cache variance — PT-03 coord may have missed warm cache this run | +| PT-04 region 500 m + stitch | 2128 ms | 2192 ms | +64 ms | noise | +| PT-05 5 concurrent | 2663 ms | 2383 ms | -280 ms | noise | +| PT-06 route create | 83 ms | 244 ms | +161 ms | noise band | +| PT-07 cold p95 / warm p95 | 2274 ms / 108 ms | 2156 ms / 79 ms | -118 ms / -29 ms | noise | +| PT-08 batch p95 | 379 ms | 179 ms | -200 ms | noise (within historical 179–544 ms band) | + +No scenario shows a regression attributable to cycle-9 gRPC work or security caps. + +## Infrastructure note + +Host port **5433** was occupied by a sibling project. Perf stack used a one-shot `docker-compose.perf.yml` override (`postgres.ports: !reset []`) so only api port 18980 was published. Recommend documenting this pattern for dev workstations with parallel Postgres stacks. + +## Verdict (Step 15) + +**PASS** — 8/8 REST scenarios within threshold. Cleared to auto-chain to Step 16 (Deploy). diff --git a/_docs/06_metrics/retro_2026-06-25_cycle9.md b/_docs/06_metrics/retro_2026-06-25_cycle9.md new file mode 100644 index 0000000..053d4b5 --- /dev/null +++ b/_docs/06_metrics/retro_2026-06-25_cycle9.md @@ -0,0 +1,83 @@ +# Retrospective — Cycle 9 (2026-06-25) + +**Tasks**: AZ-1074 (gRPC RouteTileDelivery service, 5 SP), AZ-1075 (gRPC integration tests, 3 SP). **2 tasks, 8 SP, 1 batch.** +**Mode**: cycle-end (autodev Step 17). Step 16.5 (Release) **skipped** per user choice — matches cycles 1–8 pattern; changes uncommitted at retro time. +**Previous retro**: `retro_2026-05-23_cycle8.md` + +## Implementation Summary + +| Metric | Cycle 9 | Δ vs cycle 8 | +|--------|---------|--------------| +| Tasks implemented | **2** | -3 | +| Batches executed | **1** | -3 | +| Total complexity delivered | **8 SP** | -9 SP | +| Avg tasks / batch | **2** | +0.75 | +| Blocked tasks | **0** | unchanged | +| Implementation report | **YES** (`implementation_report_tile_provision_grpc_cycle9.md`) | maintained | + +## Quality Metrics + +### Code Review + +| Verdict | Count | +|---------|-------| +| PASS_WITH_WARNINGS | **1** (batch 01) | +| FAIL | 0 | + +**Findings**: 2 Low (amd64 Docker pin documentation; ArgumentException detail string formatting). + +### Security Audit (Step 14) + +| Severity at audit | Post follow-up | +|-------------------|----------------| +| Medium 1 | **0** — F-AZ1074-1 resolved (collection caps) | +| Low 1 | **0** — F-AZ1074-2 resolved (generic internal error) | + +Continues cycle-8 pattern of in-cycle Medium resolution. + +### Test & Perf Gates + +| Gate | Result | +|------|--------| +| Step 11 functional | **PASS** — 448 unit + integration | +| Step 15 perf | **PASS** — 8/8 REST scenarios (gRPC unverified) | + +## Efficiency + +| Blocker | Resolution | +|---------|------------| +| Host port 5433 conflict (integration + perf) | Standalone `docker-compose.tests.yml`; perf used ephemeral compose override | + +## Trend Comparison + +| Metric | Cycle 8 | Cycle 9 | Change | +|--------|---------|---------|--------| +| Code review FAIL rate | 0% | 0% | unchanged | +| Security Medium open (delta) | 0 (1 resolved in-cycle) | 0 (1 resolved in-cycle) | same pattern | +| Perf scenarios pass | 8/8 | 8/8 | unchanged | +| Project count | 9 | 10 | +1 (GrpcContracts) | + +## Top 3 Improvement Actions + +1. **Document parallel-Postgres dev workaround** (~1 SP): add `docker-compose.perf.yml` or documented `ports: !reset []` override to `_docs/02_document/deployment/containerization.md` so Step 11/15 don't rediscover the 5433 conflict each cycle. + - Impact: faster integration/perf runs on multi-project dev machines + - Effort: low + +2. **PT-10 gRPC stream perf scenario** (~3 SP): when streaming NFR is accepted, add harness scenario for `DeliverRouteTiles` (time-to-first-chunk, total stream duration, backpressure). + - Impact: closes Unverified gap from cycle 9 Step 15 + - Effort: medium + +3. **REST error sanitizer sweep** (~2 SP): F-AZ795-1/2 carry-over — static 400 messages in `GlobalExceptionHandler` / upload filter. + - Impact: reduces cumulative Low security debt (5+ cycles) + - Effort: low–medium + +## Suggested Rule/Skill Updates + +| File | Change | Rationale | +|------|--------|-----------| +| `test-run/SKILL.md` perf mode | Note gRPC scenarios may be Unverified when only REST harness exists | Cycle 9 Step 15 | +| `environment.md` or containerization doc | Postgres port conflict playbook | Recurring blocker | + +## Cycle 9 Verdict + +**Successful feature cycle** — gRPC delivery shipped with tests, security hardening, and green gates. Release deferred (user choice); commit/push remains operator action before production promotion. diff --git a/_docs/06_metrics/structure_2026-06-25_cycle9.md b/_docs/06_metrics/structure_2026-06-25_cycle9.md new file mode 100644 index 0000000..2f7b633 --- /dev/null +++ b/_docs/06_metrics/structure_2026-06-25_cycle9.md @@ -0,0 +1,34 @@ +# Structural Snapshot — 2026-06-25 (post-cycle 9, gRPC tile provision) + +Cycle 9 delta against `structure_2026-05-23_cycle8.md`. Source: `_docs/02_document/module-layout.md` + on-disk `*.csproj` graph. + +## Projects + +| Layer | csproj | Cycle 9 delta | +|-------|--------|---------------| +| 1 (Foundation) | `SatelliteProvider.GrpcContracts` | **NEW** — shared proto project (`tile_provision.proto`, GrpcServices=Both) | +| 4 (API) | `SatelliteProvider.Api` | + `Grpc/RouteTileDeliveryGrpcService.cs`; `Program.cs` AddGrpc/MapGrpcService; ProjectReference → GrpcContracts | +| 3 (Application) | `SatelliteProvider.Services.RouteManagement` | `RouteTileDeliveryOrchestrator` — collection caps + lat/lon validation | +| 6 (Tests) | `SatelliteProvider.IntegrationTests` | + `RouteTileDeliveryGrpcTests.cs`, `GrpcTestHelpers.cs`; ProjectReference → GrpcContracts | +| 6 (Tests) | `SatelliteProvider.Tests` | + `DeliverAsync_TooManyWaypoints_Throws` | + +**Project count**: **10** (+1 vs cycle 8). + +## Cross-Project Import Edges + +| Edge | Cycle 9 delta | +|------|---------------| +| Api → GrpcContracts | **NEW** | +| IntegrationTests → GrpcContracts | **NEW** | +| GrpcContracts → (none) | leaf contract project | + +**Total ProjectReference edges**: **23** (+2 vs cycle 8). **Import cycles**: 0. + +## Contract coverage + +| Surface | Contract location | +|---------|-------------------| +| gRPC `DeliverRouteTiles` | `SatelliteProvider.GrpcContracts/tile_provision.proto` | +| REST tile delivery | unchanged (existing contracts) | + +**gRPC perf coverage**: Unverified — no PT-10 in harness yet. diff --git a/_docs/LESSONS.md b/_docs/LESSONS.md index f30bc76..0849841 100644 --- a/_docs/LESSONS.md +++ b/_docs/LESSONS.md @@ -37,6 +37,12 @@ If the enum's wire string happens to match a member name case-insensitively (e.g ## Ring buffer (last 15 entries — newest at top) +- [2026-06-25] [tooling] When host port 5433 is occupied by a sibling Postgres container, integration and perf gates must not depend on publishing postgres to the host — use a self-contained test compose file (internal network only) or a documented `ports: !reset []` override on the dev stack so Step 11/15 can run without stopping sibling projects (cycle 9: `fleet-viewer-dev-db` blocked both integration tests and perf until compose was adjusted). + Source: _docs/06_metrics/retro_2026-06-25_cycle9.md +- [2026-06-25] [testing] Adding a new transport (gRPC) over shared orchestrator logic does not automatically extend the perf harness — REST PT-01..PT-08 can pass while the new RPC surface stays Unverified until an explicit PT-NN scenario and threshold land in `performance-tests.md` + `run-performance-tests.sh` (cycle 9: gRPC DeliverRouteTiles had no perf scenario; gate passed on REST-only evidence). + Source: _docs/06_metrics/retro_2026-06-25_cycle9.md +- [2026-06-25] [architecture] Shared wire contracts belong in a leaf `*.GrpcContracts` (or equivalent) project referenced by both server and test client — keeping proto in the API project forces test projects to reference the full API assembly and couples codegen to the delivery layer (cycle 9: `SatelliteProvider.GrpcContracts` extracted from Api). + Source: _docs/06_metrics/retro_2026-06-25_cycle9.md - [2026-05-23] [process] Step-14 security-audit Medium findings whose remediation fits the small-fix threshold (≤2 SP, ≤2 files, ≤1 contract bump) should be resolved within the same autodev invocation rather than deferred to cycle N+1 — the fix lands with the same commit chain that introduced the surface, the contract version reflects the fix immediately, and the traceability matrix and blackbox-tests.md sub-cases are written while the finding is fresh; codify the option as a first-class A/B/C choice in `.cursor/skills/autodev/flows/existing-code.md` Step-14 action (cycle 8: F-AZ809-1 unbounded `geofences.polygons` DoS — discovered in commit `ac40a8b`, resolved in commit `8fca6e0` with `MaxPolygons = 50` cap + unit + integration test + `route-creation.md` v1.0.1 patch bump, ~30 minutes from finding to fix landed). Source: _docs/06_metrics/retro_2026-05-23_cycle8.md - [2026-05-23] [process] Retrospective recommendations ship end-to-end in the next cycle only when they (a) name concrete tracker tickets / files / endpoints in the action text, (b) are sized as a coherent cycle theme rather than scattered one-off fixes, and (c) the next cycle's planning phase pulls the recommendation directly into the task slate without re-deriving scope — phrase recommendations to satisfy all three or they become multi-cycle carry-overs (cycle 7 Action 3 named the 4 AZ-795 child endpoints + the SP sizing → cycle 8 shipped AZ-808 + AZ-809 + AZ-810 + AZ-811 + AZ-812 as the coherent strict-validation theme, first directly-traceable cross-cycle improvement-action end-to-end in project history). @@ -60,10 +66,4 @@ If the enum's wire string happens to match a member name case-insensitively (e.g - [2026-05-12] [architecture] Cross-repo cryptographic invariants (UUID namespaces, deterministic-key formulas, base32/64 alphabets, tile-zoom conventions) MUST live as code-level constants in BOTH repos with reference-vector tests on BOTH sides — documentation alone is insufficient because constant drift surfaces only as 100% lookup misses in production, harder to detect than a unit-test failure (cycle 5: AZ-503 introduced `TileNamespace = 5b8d0c2e-7f1a-4d3b-9c5e-1f3a8e7d2b6c` which must byte-match the same constant in `gps-denied-onboard/components/c6_tile_cache/_uuid.py`; the satellite-provider side has the constant + 10 Python-generated reference vectors in `Uuidv5Tests.cs` and the sibling repo will mirror). Source: _docs/06_metrics/retro_2026-05-12_cycle5.md - [2026-05-12] [tooling] Local Docker/colima DNS cold-start is a recurring class of failure that contaminates the Step-15 perf gate — when the perf-mode "one re-run" rule fires twice across consecutive cycles with the same root-cause class (DNS / NTP / resolver), the harness must escalate from "re-run" to a deterministic fix at the harness layer (DNS pre-warm in script, OR move gate to CI), not just another re-run (cycle 5: PT-01 failed Run #1 on `tile.googleapis.com` cold-start, then Run #2 on `mt0.google.com` cold-start; the warmup probe between runs only touched the hostnames it explicitly named). - Source: _docs/06_metrics/retro_2026-05-12_cycle5.md -- [2026-05-12] [process] When a /autodev cycle's task spec contradicts the live codebase by ≥2 missing prerequisites, the implement skill should preferentially split into foundation + follow-up via A/B/C (option C) rather than (A) silently expand the SP budget or (B) defer the entire task — both halves remain individually shippable and individually testable, the cross-PBI dependency is captured as a blocked-link in the tracker (cycle 5: AZ-503 → AZ-503-foundation + AZ-505 split when `flight_id` / `FlightId` / `voting_status` all missing from live code; AZ-503-foundation shipped this cycle, AZ-505 blocks-on-it for cycle 6). - Source: _docs/06_metrics/retro_2026-05-12_cycle5.md -- [2026-05-12] [dependencies] Major-version bumps of direct deps cascade through transitives; the task spec must list the transitive packages whose major version changes as a result OR explicitly note "transitive major-version drift not analyzed in spec" — verify with `dotnet restore --dry-run` against a scratch branch before writing the spec (cycle 4: AZ-500 surprise-bumped `Microsoft.OpenApi` 1.x → 2.x via the `Microsoft.AspNetCore.OpenApi` 8.0.25 → 10.0.7 path; forced an unscheduled Swashbuckle bump + Program.cs refactor mid-implementation). - Source: _docs/06_metrics/retro_2026-05-12_cycle4.md -- [2026-05-12] [process] When a scope-protected task newly *exposes* a pre-existing bug elsewhere in the codebase (vs. introducing a new one), surface it as a recommended follow-up PBI in the batch report AND list it as a "newly exposed bug" separate from "newly introduced findings" in the deploy report — bugs that already existed don't count as cycle-introduced regressions, but they must not be silently re-buried (cycle 4: AZ-500's bootstrap fix unmasked the pre-existing `scripts/run-performance-tests.sh:417` `grep -o | wc -l` + `pipefail` bug). - Source: _docs/06_metrics/retro_2026-05-12_cycle4.md + Source: _docs/06_metrics/retro_2026-05-12_cycle5.md \ No newline at end of file diff --git a/_docs/_autodev_state.md b/_docs/_autodev_state.md index 6a6eb87..b87cf5c 100644 --- a/_docs/_autodev_state.md +++ b/_docs/_autodev_state.md @@ -2,14 +2,24 @@ ## Current Step flow: existing-code -step: 11 -name: Run Tests -status: pending +step: 17 +name: Retrospective +status: completed sub_step: - phase: 1 - name: full-suite - detail: "Step 10 complete; smoke passed, full suite pending" + phase: 4 + name: lessons-updated + detail: "cycle 9 complete; release skipped; no re-entry (release not Released)" retry_count: 0 cycle: 9 tracker: jira auto_push: true + +## Step 16.5 +release_verdict: skipped +release_skip_reason: user chose B — skip release, proceed to retrospective + +## Cycle 9 Summary +tasks: AZ-1074, AZ-1075 +gates: tests PASS, security PASS (delta), perf PASS (8/8 REST) +artifacts: deploy_cycle9.md, perf_2026-06-25_cycle9.md, retro_2026-06-25_cycle9.md, security_report_cycle9.md +uncommitted: yes — operator commit/push pending diff --git a/docker-compose.tests.yml b/docker-compose.tests.yml index 285d9f5..550a017 100644 --- a/docker-compose.tests.yml +++ b/docker-compose.tests.yml @@ -1,24 +1,47 @@ services: postgres: - extends: - file: docker-compose.yml - service: postgres + image: postgres:16 + container_name: satellite-provider-postgres + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: satelliteprovider + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 api: - extends: - file: docker-compose.yml - service: api + platform: linux/amd64 + build: + context: . + dockerfile: SatelliteProvider.Api/Dockerfile + container_name: satellite-provider-api + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_URLS=https://+:8080 + - ASPNETCORE_Kestrel__Certificates__Default__Path=/app/certs/api.pfx + - ASPNETCORE_Kestrel__Certificates__Default__Password=satellite-dev-cert + - ConnectionStrings__DefaultConnection=Host=postgres;Port=5432;Database=satelliteprovider;Username=postgres;Password=postgres + - MapConfig__ApiKey=${GOOGLE_MAPS_API_KEY} + - JWT_SECRET=${JWT_SECRET} + - JWT_ISSUER=${JWT_ISSUER} + - JWT_AUDIENCE=${JWT_AUDIENCE} + volumes: + - ./tiles:/app/tiles + - ./ready:/app/ready + - ./logs:/app/logs + - ./certs/api.pfx:/app/certs/api.pfx:ro + depends_on: + postgres: + condition: service_healthy integration-tests: build: context: . dockerfile: SatelliteProvider.IntegrationTests/Dockerfile container_name: satellite-provider-integration-tests - # AZ-505 AC-5: API now serves HTTPS for HTTP/2 via ALPN. The matching - # public cert is mounted into /usr/local/share/ca-certificates so the - # Dockerfile entrypoint can register it with update-ca-certificates - # before tests run. After that every HttpClient trusts it transparently - # — no per-test handler shim is required. environment: - API_URL=https://api:8080 - INTEGRATION_TESTS_MODE=${INTEGRATION_TESTS_MODE:-full} @@ -32,8 +55,6 @@ services: - ./ready:/app/ready - ./tiles:/app/tiles - ./certs/api.crt:/usr/local/share/ca-certificates/satellite-provider-dev.crt:ro - # AZ-505 AC-5: register the dev CA at runtime so HttpClient trusts the API. - # update-ca-certificates picks up everything under /usr/local/share/ca-certificates/. entrypoint: - /bin/sh - -c @@ -47,4 +68,3 @@ services: volumes: postgres_data: - diff --git a/scripts/run-tests.sh b/scripts/run-tests.sh index 4782e4f..4d690ac 100755 --- a/scripts/run-tests.sh +++ b/scripts/run-tests.sh @@ -5,7 +5,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" cleanup() { - docker compose -f "$PROJECT_ROOT/docker-compose.yml" -f "$PROJECT_ROOT/docker-compose.tests.yml" down --remove-orphans || true + docker compose -f "$PROJECT_ROOT/docker-compose.tests.yml" down --remove-orphans || true } trap cleanup EXIT @@ -173,7 +173,6 @@ fi INTEGRATION_TESTS_MODE="$mode" \ INTEGRATION_KEEP_STATE="$INTEGRATION_KEEP_STATE_VALUE" \ docker compose \ - -f "$PROJECT_ROOT/docker-compose.yml" \ -f "$PROJECT_ROOT/docker-compose.tests.yml" \ up --build --abort-on-container-exit --exit-code-from integration-tests