mirror of
https://github.com/azaion/ui.git
synced 2026-06-22 00:11:11 +00:00
[AZ-501] [AZ-502] Cycle 2 Step 14 security audit + inline fixes
ci/woodpecker/push/build-arm Pipeline failed
ci/woodpecker/push/build-arm Pipeline failed
Security audit (5 phases) → reports under _docs/05_security/. AZ-501 (F-SAST-1, HIGH): Externalize hardcoded Google Geocode key from mission-planner/src/config.ts to VITE_GOOGLE_GEOCODE_KEY via new GeocodeService.ts; fail-soft warn when unset; STC-SEC1D static deny-list gate; +5 unit tests in tests/mission_planner_geocode.test.ts. AZ-502 (F-DEP-1, HIGH): Force vite>=6.4.2 and postcss>=8.5.10 via package.json overrides in both roots; clean reinstall clears all bun audit advisories. Test-spec sync (Step 12) + Update Docs (Step 13) deltas: AC-43, AC-44, NFT-SEC-09b, FT-P-61, FT-N-17, ripple log, batch_12 report. Pending user actions: revoke Google + OWM keys (AC-6 / AZ-499 AC-7). 229 PASS / 13 SKIP / 0 FAIL on static + fast suites. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1470,6 +1470,185 @@ Every test is observed at the SPA's public surface — DOM, ARIA, outbound netwo
|
||||
|
||||
---
|
||||
|
||||
### FT-N-16: mission-planner `getWeatherData` fail-soft when `VITE_OWM_API_KEY` is unset
|
||||
|
||||
**Traces to**: AC-42 (AZ-499 AC-3)
|
||||
**Profile**: fast
|
||||
|
||||
**Input data**: build-time env with `VITE_OWM_API_KEY=""` (or undefined).
|
||||
|
||||
**Steps**:
|
||||
|
||||
| Step | Consumer Action | Expected System Response |
|
||||
|------|----------------|------------------------|
|
||||
| 1 | Spy `globalThis.fetch` | spy installed, no calls yet |
|
||||
| 2 | Stub `import.meta.env.VITE_OWM_API_KEY = ""` and invoke `getWeatherData(50, 30)` | resolves |
|
||||
| 3 | Inspect return value | `=== null` |
|
||||
| 4 | Inspect fetch spy | `mock.calls.length === 0` |
|
||||
|
||||
**Pass criteria**: function returns `null` AND no outbound HTTP request is made when the API key is unset. Mirrors the AZ-448 fail-soft contract on the main SPA.
|
||||
**Max execution time**: 1s (env stub + sync inspection only).
|
||||
**Expected result source**: AZ-499 AC-3 (no `results_report.md` row needed — behavioral test, no input data).
|
||||
|
||||
---
|
||||
|
||||
## Cycle 2 Additions (Phase B Cycle 2 — Self-hosted satellite tiles + mission-planner OWM hardening)
|
||||
|
||||
The scenarios below were appended via `/test-spec` cycle-update mode after Phase B Cycle 2 completed (AZ-498 + AZ-499, batch_11). They use the same template shapes as the original spec. Cross-references: AC-41 (satellite tiles), AC-42 (mission-planner OWM env hardening) are the new global ACs added to `traceability-matrix.md`; the underlying task-spec ACs are AZ-498 AC-1..AC-7, AC-9 and AZ-499 AC-1..AC-6 (AZ-498 AC-8 was dropped with explicit user approval per `_docs/03_implementation/batch_11_report.md`; AZ-499 AC-7 is a manual deliverable, not a test).
|
||||
|
||||
### FT-P-56: Self-hosted satellite tile URL is env-var resolved
|
||||
|
||||
**Traces to**: AC-41 (AZ-498 AC-1, AC-2)
|
||||
**Profile**: fast
|
||||
|
||||
**Input data**: build-time env with `VITE_SATELLITE_TILE_URL` set, unset, or set with a trailing slash.
|
||||
|
||||
**Steps**:
|
||||
|
||||
| Step | Consumer Action | Expected System Response |
|
||||
|------|----------------|------------------------|
|
||||
| 1 | Stub `VITE_SATELLITE_TILE_URL=http://satellite-provider:5100/tiles/{z}/{x}/{y}` and call `getTileUrl()` | returns the env value verbatim |
|
||||
| 2 | Stub `VITE_SATELLITE_TILE_URL=""` and call `getTileUrl()` | returns `DEFAULT_SATELLITE_TILE_URL` (`http://localhost:5100/tiles/{z}/{x}/{y}`) |
|
||||
| 3 | Stub `VITE_SATELLITE_TILE_URL=http://satellite-provider:5100/tiles/{z}/{x}/{y}/` (trailing slash) | returns the value with the trailing slash stripped |
|
||||
| 4 | Mount `<FlightMap>` with the env unset; inspect rendered `<TileLayer>` `data-tile-url` | equals `DEFAULT_SATELLITE_TILE_URL` |
|
||||
|
||||
**Pass criteria**: all four assertions hold. Mirrors the established `getOwmBaseUrl()` / `getApiBase()` env-resolution pattern.
|
||||
**Max execution time**: 2s (jsdom render + four stub variations).
|
||||
**Expected result source**: AZ-498 AC-1, AC-2 (no `results_report.md` row needed — env-var plumbing, no input data fixture).
|
||||
|
||||
---
|
||||
|
||||
### FT-P-57: `<TileLayer crossOrigin="use-credentials">` enables cookie-auth on tile fetches
|
||||
|
||||
**Traces to**: AC-41 (AZ-498 AC-3); E1 (air-gap-friendly bundle); RID R-Reliability for tile auth
|
||||
**Profile**: fast
|
||||
|
||||
**Input data**: `<FlightMap>` and `<MiniMap>` mounted with the default tile URL.
|
||||
|
||||
**Steps**:
|
||||
|
||||
| Step | Consumer Action | Expected System Response |
|
||||
|------|----------------|------------------------|
|
||||
| 1 | Mount `<FlightMap>`; inspect rendered `<TileLayer>` `data-cross-origin` attribute | `=== "use-credentials"` |
|
||||
| 2 | Mount `<MiniMap pointPosition={…}>`; inspect rendered `<TileLayer>` `data-cross-origin` attribute | `=== "use-credentials"` |
|
||||
| 3 | (e2e — gated) Issue `GET <VITE_SATELLITE_TILE_URL substituted with /tiles/1/0/0>` from the rendered map; inspect outbound request | `request.credentials === "include"` (browser attaches the same-origin auth cookie) |
|
||||
|
||||
**Pass criteria**: every `<TileLayer>` the SPA renders carries `crossOrigin="use-credentials"` so the browser sends the satellite-provider cookie on same-origin tile requests. Step 3 e2e is gated by the cross-workspace satellite-provider cookie-auth ticket landing (Step 16 deploy gate).
|
||||
**Max execution time**: 2s for steps 1+2 (fast); e2e step is part of `infrastructure.e2e.ts` — bounded by suite-e2e timeout.
|
||||
**Expected result source**: AZ-498 AC-3 (no `results_report.md` row — DOM-attribute observable).
|
||||
|
||||
---
|
||||
|
||||
### FT-P-58: Classic/satellite map toggle, `mapType` state, and `MiniMap.Props.mapType` are removed
|
||||
|
||||
**Traces to**: AC-41 (AZ-498 AC-4)
|
||||
**Profile**: fast
|
||||
|
||||
**Input data**: `<FlightMap>` mounted with the default tile URL; `<MiniMap>` mounted with only `pointPosition` (no `mapType` prop).
|
||||
|
||||
**Steps**:
|
||||
|
||||
| Step | Consumer Action | Expected System Response |
|
||||
|------|----------------|------------------------|
|
||||
| 1 | Mount `<FlightMap>`; query `screen.queryByRole('button', { name: /satellite|classic/i })` | returns `null` |
|
||||
| 2 | Mount `<FlightMap>`; query `screen.getAllByTestId('tile-layer')` | length `=== 1` (no per-mode branching, single layer) |
|
||||
| 3 | Compile-time check: instantiate `<MiniMap pointPosition={…}>` without `mapType` | TypeScript `tsc --noEmit -p tsconfig.test.json` succeeds (STC-T1) |
|
||||
| 4 | Compile-time check: source-tree grep for any remaining `mapType` reference under `src/features/flights/` | zero hits (compilation error if not — covered by STC-T1) |
|
||||
|
||||
**Pass criteria**: no toggle button, no `mapType` state, `MiniMap.Props` has no `mapType`. Removal is permanent; the `flights.planner.satellite` i18n key was removed from both `en.json` and `ua.json` in lockstep (i18n key parity preserved via STC-FP22).
|
||||
**Max execution time**: 2s (jsdom render + grep).
|
||||
**Expected result source**: AZ-498 AC-4.
|
||||
|
||||
---
|
||||
|
||||
### FT-P-59: e2e harness exercises the new `/tiles/{z}/{x}/{y}` path
|
||||
|
||||
**Traces to**: AC-41 (AZ-498 AC-6); E1 (air-gap)
|
||||
**Profile**: e2e
|
||||
|
||||
**Input data**: suite-e2e compose stack up; `tile-stub` configured at `http://tile-stub:8082/tiles/{z}/{x}/{y}`.
|
||||
|
||||
**Steps**:
|
||||
|
||||
| Step | Consumer Action | Expected System Response |
|
||||
|------|----------------|------------------------|
|
||||
| 1 | `infrastructure.e2e.ts` AC-2 — issue `GET http://tile-stub:8082/tiles/1/0/0` from the playwright runner | HTTP 200, response is a 256×256 image (JPEG) |
|
||||
| 2 | Inspect response headers | `Content-Type: image/jpeg`, `Cache-Control` present, `ETag` present |
|
||||
| 3 | Inspect outbound request from the SPA's `<TileLayer>` | URL matches `^http://tile-stub:8082/tiles/\d+/\d+/\d+$` (NOT `/{z}/{x}/{y}.png`, NOT the legacy `/sat/...` Esri shape) |
|
||||
| 4 | Inspect `EXTERNAL_HOSTS` route guard | OSM and Esri hosts are NOT in the allow-list (removed during cycle 2 cleanup) |
|
||||
|
||||
**Pass criteria**: tile fetch shape matches the satellite-provider contract documented at `_docs/02_document/contracts/satellite-provider/tiles.md`. Note: the same-origin cookie-auth path (cookie attached on the actual fetch) is verified once the cross-workspace satellite-provider cookie-auth ticket lands; until then, the e2e profile uses the `tile-stub` which accepts requests without a cookie.
|
||||
**Max execution time**: bounded by suite-e2e infrastructure-test timeout (per `e2e/tests/infrastructure.e2e.ts`).
|
||||
**Expected result source**: contract at `_docs/02_document/contracts/satellite-provider/tiles.md` v1.0.0; AZ-498 AC-6.
|
||||
|
||||
---
|
||||
|
||||
### FT-P-60: mission-planner `getWeatherData` uses env-resolved key + base URL
|
||||
|
||||
**Traces to**: AC-42 (AZ-499 AC-1, AC-2, AC-4)
|
||||
**Profile**: fast
|
||||
|
||||
**Input data**: build-time env with `VITE_OWM_API_KEY` set + `VITE_OWM_BASE_URL` either set, unset, or set with a trailing slash.
|
||||
|
||||
**Steps**:
|
||||
|
||||
| Step | Consumer Action | Expected System Response |
|
||||
|------|----------------|------------------------|
|
||||
| 1 | Spy `globalThis.fetch` returning a 200 OK with body `{ wind: { speed: 5, deg: 90 } }` | spy installed |
|
||||
| 2 | Stub `VITE_OWM_API_KEY=abc123` + `VITE_OWM_BASE_URL=""`; invoke `getWeatherData(50, 30)` | outbound URL contains `appid=abc123` AND `units=metric` |
|
||||
| 3 | Stub `VITE_OWM_API_KEY=abc123` + `VITE_OWM_BASE_URL=https://example.test/data/2.5`; invoke `getWeatherData(50, 30)` | outbound URL starts with `https://example.test/data/2.5/weather?` |
|
||||
| 4 | Stub `VITE_OWM_API_KEY=abc123` + `VITE_OWM_BASE_URL=https://example.test/data/2.5/` (trailing slash); invoke `getWeatherData(50, 30)` | outbound URL starts with `https://example.test/data/2.5/weather?` (slash stripped) |
|
||||
| 5 | Stub `VITE_OWM_API_KEY=abc123` + `VITE_OWM_BASE_URL=""`; invoke `getWeatherData(50, 30)` | outbound URL starts with `https://api.openweathermap.org/data/2.5/weather?` (default base) |
|
||||
| 6 | Inspect return value on a successful fetch | `=== { windSpeed: 5, windAngle: 90 }` (existing parsed-wind shape preserved) |
|
||||
|
||||
**Pass criteria**: every outbound URL is reconstructed from env vars; the public `getWeatherData(lat, lon)` signature and `WeatherData` return shape are unchanged. Pairs with the AZ-499 NFR-Compatibility constraint.
|
||||
**Max execution time**: 2s (env stubs + fetch-spy assertions; no real network).
|
||||
**Expected result source**: AZ-499 AC-1, AC-2, AC-4 (no `results_report.md` row — env-var plumbing).
|
||||
|
||||
---
|
||||
|
||||
### FT-P-61: mission-planner `geocodeAddress` uses env-resolved Google API key
|
||||
|
||||
**Traces to**: AC-43 (AZ-501 AC-1)
|
||||
**Profile**: fast
|
||||
|
||||
**Input data**: build-time env with `VITE_GOOGLE_GEOCODE_KEY` set to a placeholder string.
|
||||
|
||||
**Steps**:
|
||||
|
||||
| Step | Consumer Action | Expected System Response |
|
||||
|------|----------------|------------------------|
|
||||
| 1 | Spy `globalThis.fetch` returning a 200 OK with body `{ status: 'OK', results: [{ geometry: { location: { lat, lng } } }] }` | spy installed |
|
||||
| 2 | Stub `VITE_GOOGLE_GEOCODE_KEY=env-key-xyz`; invoke `geocodeAddress('Kyiv, Ukraine')` | outbound URL contains `key=env-key-xyz` AND `address=Kyiv%2C%20Ukraine` |
|
||||
| 3 | Inspect return value | `=== { lat, lng }` from the mocked response |
|
||||
|
||||
**Pass criteria**: the outbound URL is reconstructed from the env var; no literal key remains in `mission-planner/src/services/GeocodeService.ts` (defense-in-depth confirmed by STC-SEC1D / NFT-SEC-09b).
|
||||
**Max execution time**: 2s.
|
||||
**Expected result source**: AZ-501 AC-1.
|
||||
|
||||
---
|
||||
|
||||
### FT-N-17: mission-planner `geocodeAddress` fail-soft when `VITE_GOOGLE_GEOCODE_KEY` is unset
|
||||
|
||||
**Traces to**: AC-43 (AZ-501 AC-3)
|
||||
**Profile**: fast
|
||||
|
||||
**Input data**: build-time env with `VITE_GOOGLE_GEOCODE_KEY` empty / undefined.
|
||||
|
||||
**Steps**:
|
||||
|
||||
| Step | Consumer Action | Expected System Response |
|
||||
|------|----------------|------------------------|
|
||||
| 1 | Spy `globalThis.fetch`; spy `console.warn` | spies installed |
|
||||
| 2 | Stub `VITE_GOOGLE_GEOCODE_KEY=''`; invoke `geocodeAddress('anywhere')` | returns `null`; fetch is NOT called; `console.warn` called exactly once with a message containing `VITE_GOOGLE_GEOCODE_KEY` |
|
||||
| 3 | Stub `VITE_GOOGLE_GEOCODE_KEY=env-key-xyz` and force `fetch` to reject with `Error('boom')`; invoke `geocodeAddress('anywhere')` | returns `null`; promise does NOT throw |
|
||||
|
||||
**Pass criteria**: missing-key path is silent-but-warned and never throws; network-error path is silent and never throws — preserves the LeftBoard address-box UX of "Enter does nothing if address is unresolvable".
|
||||
**Max execution time**: 2s.
|
||||
**Expected result source**: AZ-501 AC-3.
|
||||
|
||||
---
|
||||
|
||||
## Notes carried into Phase 3
|
||||
|
||||
- All tests tagged `quarantined` correspond to features either pending a Step 4 fix (e.g., AC-13 i18n detector, AC-21 panel persistence, AC-22 role-gate, AC-26/27 form hygiene, AC-39 split surface, AC-40 tile zoom) or pending Phase B implementation (AC-11 bundle gate, AC-24 SSE refresh, AC-25 async video, AC-40 tile zoom). The test is written so it activates the day the implementation lands; Phase 3 will surface them for downgrade or accept.
|
||||
|
||||
Reference in New Issue
Block a user