mirror of
https://github.com/azaion/ui.git
synced 2026-06-21 08:01:10 +00:00
[AZ-498] [AZ-499] Cycle 2 batch 11: satellite tiles + OWM hardening
AZ-498 — self-hosted satellite tiles + drop classic/satellite toggle: - Single TILE_URL via getTileUrl() (mirrors getOwmBaseUrl/getApiBase pattern from AZ-449/AZ-450); env-var VITE_SATELLITE_TILE_URL with dev default http://localhost:5100/tiles/{z}/{x}/{y}. - FlightMap + MiniMap render one TileLayer with crossOrigin="use-credentials" so Leaflet's <img> tile fetcher attaches the same-origin satellite-provider auth cookie. - ImportMetaEnv + .env.example collapse the prior OSM/Esri pair into one var. The flights.planner.satellite i18n key is removed in lockstep across en.json + ua.json (parity preserved). - E2E harness wired end-to-end: compose passes the new var to azaion-ui; tile-stub serves /tiles/{z}/{x}/{y} with Content-Type=image/jpeg + Cache-Control + ETag matching the contract; infrastructure.e2e.ts AC-2 asserts the new path; dead OSM defenses removed from EXTERNAL_HOSTS route guard. - Fast-profile MSW handlers rewritten for the cookie-auth path shape. - 8 colocated fast tests under src/features/flights/__tests__/. AZ-499 — mission-planner OWM env-var hardening + AZ-482 source-scan gap close: - WeatherService.ts reads VITE_OWM_API_KEY + VITE_OWM_BASE_URL; fail-soft null when key unset (mirrors AZ-448 main-SPA contract). Public signature getWeatherData(lat, lon) preserved. - mission-planner/.env.example + vite-env.d.ts declare both vars. - New owm_key_in_source banned-deps kind scans src/ AND mission-planner/ for the rotated literal; STC-SEC1C row added to scripts/run-tests.sh; check-banned-deps.mjs dispatch extended. - 7 fast tests under tests/mission_planner_weather.test.ts cover AC-1..AC-4 + trailing-slash + happy path + network-error fail-soft. Spec drift (recorded in batch_11_report.md, user-approved Choose B on 2026-05-12): - AZ-498 AC-8 dropped (named tile_split_zoom* files belong to AZ-474 image-annotation surface, not map tiles). - 4 missing files added in-scope (msw tiles handler, tile-stub server, compose env, dead VITE_TILE_BASE_URL replaced). - AZ-499 STC-S6 ID conflict resolved by using STC-SEC1C. Pending USER ACTION (BLOCKING for AZ-499 close): - Revoke OpenWeatherMap key 335799082893fad97fa36118b131f919 at home.openweathermap.org/api_keys; capture evidence on AZ-499. Cross-workspace deploy gate (handled at autodev Step 16, not a Step-10 blocker for AZ-498): - satellite-provider cookie-auth on GET /tiles/{z}/{x}/{y} (separate AZAION ticket on the satellite-provider workspace). Reports: _docs/03_implementation/batch_11_report.md and _docs/03_implementation/reviews/batch_11_review.md (verdict PASS_WITH_WARNINGS — 1 Low, pre-existing trim-trailing-slash duplication across vite roots). Static gates: STC-ARCH-01, STC-ARCH-02, STC-T1, STC-FP22, STC-FP23, STC-SEC1C all PASS post-refactor. +15 fast tests; +1 STC-SEC1C row. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { getWeatherData } from '../mission-planner/src/services/WeatherService'
|
||||
|
||||
// AZ-499 — mission-planner WeatherService env-var hardening.
|
||||
//
|
||||
// Lives under tests/ (Blackbox-Tests-owned) rather than colocated under
|
||||
// mission-planner/ because mission-planner does not have its own runner;
|
||||
// the suite Vitest config already includes mission-planner/src in coverage
|
||||
// and tsconfig.test.json picks up tests/** for type-check (STC-T1).
|
||||
|
||||
type FetchMock = ReturnType<typeof vi.fn>
|
||||
|
||||
describe('AZ-499 — mission-planner getWeatherData (env vars + fail-soft)', () => {
|
||||
let fetchMock: FetchMock
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock = vi.fn(async () =>
|
||||
new Response(JSON.stringify({ wind: { speed: 5, deg: 90 } }), { status: 200 }),
|
||||
)
|
||||
vi.spyOn(globalThis, 'fetch').mockImplementation(fetchMock)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
vi.unstubAllEnvs()
|
||||
})
|
||||
|
||||
it('AC-1: env-var resolved API key reaches the outgoing fetch URL', async () => {
|
||||
// Arrange
|
||||
vi.stubEnv('VITE_OWM_API_KEY', 'abc123')
|
||||
vi.stubEnv('VITE_OWM_BASE_URL', '')
|
||||
|
||||
// Act
|
||||
await getWeatherData(50, 30)
|
||||
|
||||
// Assert
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||
const url = String(fetchMock.mock.calls[0][0])
|
||||
expect(url).toContain('appid=abc123')
|
||||
expect(url).toContain('units=metric')
|
||||
})
|
||||
|
||||
it('AC-2: env-var resolved base URL prefixes the outgoing fetch URL', async () => {
|
||||
// Arrange
|
||||
vi.stubEnv('VITE_OWM_API_KEY', 'abc123')
|
||||
vi.stubEnv('VITE_OWM_BASE_URL', 'https://example.test/data/2.5')
|
||||
|
||||
// Act
|
||||
await getWeatherData(50, 30)
|
||||
|
||||
// Assert
|
||||
const url = String(fetchMock.mock.calls[0][0])
|
||||
expect(url.startsWith('https://example.test/data/2.5/weather?')).toBe(true)
|
||||
})
|
||||
|
||||
it('AC-2: trailing slash on env base URL is stripped', async () => {
|
||||
// Arrange
|
||||
vi.stubEnv('VITE_OWM_API_KEY', 'abc123')
|
||||
vi.stubEnv('VITE_OWM_BASE_URL', 'https://example.test/data/2.5/')
|
||||
|
||||
// Act
|
||||
await getWeatherData(50, 30)
|
||||
|
||||
// Assert
|
||||
const url = String(fetchMock.mock.calls[0][0])
|
||||
expect(url.startsWith('https://example.test/data/2.5/weather?')).toBe(true)
|
||||
})
|
||||
|
||||
it('AC-3: returns null and issues no fetch when VITE_OWM_API_KEY is unset', async () => {
|
||||
// Arrange
|
||||
vi.stubEnv('VITE_OWM_API_KEY', '')
|
||||
|
||||
// Act
|
||||
const result = await getWeatherData(50, 30)
|
||||
|
||||
// Assert
|
||||
expect(result).toBeNull()
|
||||
expect(fetchMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('AC-4: defaults to public OWM base URL when only VITE_OWM_BASE_URL is unset', async () => {
|
||||
// Arrange
|
||||
vi.stubEnv('VITE_OWM_API_KEY', 'abc123')
|
||||
vi.stubEnv('VITE_OWM_BASE_URL', '')
|
||||
|
||||
// Act
|
||||
await getWeatherData(50, 30)
|
||||
|
||||
// Assert
|
||||
const url = String(fetchMock.mock.calls[0][0])
|
||||
expect(url.startsWith('https://api.openweathermap.org/data/2.5/weather?')).toBe(true)
|
||||
})
|
||||
|
||||
it('returns the parsed wind shape on a successful response', async () => {
|
||||
// Arrange
|
||||
vi.stubEnv('VITE_OWM_API_KEY', 'abc123')
|
||||
|
||||
// Act
|
||||
const result = await getWeatherData(50, 30)
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({ windSpeed: 5, windAngle: 90 })
|
||||
})
|
||||
|
||||
it('returns null when fetch rejects (network error fail-soft)', async () => {
|
||||
// Arrange
|
||||
vi.stubEnv('VITE_OWM_API_KEY', 'abc123')
|
||||
fetchMock.mockRejectedValueOnce(new Error('boom'))
|
||||
|
||||
// Act
|
||||
const result = await getWeatherData(50, 30)
|
||||
|
||||
// Assert
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
+19
-13
@@ -1,8 +1,13 @@
|
||||
import { http, HttpResponse } from 'msw'
|
||||
|
||||
// OSM/Esri tile stand-in for the fast profile. Returns a tiny transparent
|
||||
// PNG so `<img>` / Leaflet tile loads succeed in jsdom without exiting the
|
||||
// process.
|
||||
// Satellite-provider tile stand-in for the fast profile (AZ-498).
|
||||
// Returns a tiny transparent PNG so `<img>` / Leaflet tile loads succeed in
|
||||
// jsdom without exiting the process.
|
||||
//
|
||||
// The contract `_docs/02_document/contracts/satellite-provider/tiles.md`
|
||||
// (v1.0.0) freezes the path shape `/tiles/{z}/{x}/{y}` (no `.png` suffix,
|
||||
// `image/jpeg` Content-Type, cookie auth on the same origin). The handler
|
||||
// matches that exact shape and the dev default URL the SPA falls back to.
|
||||
const TILE_PNG = Uint8Array.from([
|
||||
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
|
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4,
|
||||
@@ -11,16 +16,17 @@ const TILE_PNG = Uint8Array.from([
|
||||
0x42, 0x60, 0x82,
|
||||
])
|
||||
|
||||
const tile = () => new HttpResponse(TILE_PNG, { headers: { 'Content-Type': 'image/png' } })
|
||||
const tile = () =>
|
||||
new HttpResponse(TILE_PNG, {
|
||||
headers: {
|
||||
'Content-Type': 'image/jpeg',
|
||||
'Cache-Control': 'public, max-age=86400',
|
||||
'ETag': '"fast-profile-fixture"',
|
||||
},
|
||||
})
|
||||
|
||||
export const tilesHandlers = [
|
||||
// OSM XYZ scheme: {z}/{x}/{y}
|
||||
http.get('https://*.tile.openstreetmap.org/:z/:x/:y.png', tile),
|
||||
http.get('https://tile.openstreetmap.org/:z/:x/:y.png', tile),
|
||||
// Esri ArcGIS satellite scheme: {z}/{y}/{x}
|
||||
http.get('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/:z/:y/:x', tile),
|
||||
// Local tile-stub aliases (e2e parity)
|
||||
http.get('http://tile-stub:8082/:z/:x/:y.png', tile),
|
||||
http.get('http://tile-stub:8082/sat/:z/:y/:x', tile),
|
||||
http.get('/tiles/:z/:x/:y.png', tile),
|
||||
http.get('/tiles/:z/:x/:y', tile),
|
||||
http.get('http://localhost:5100/tiles/:z/:x/:y', tile),
|
||||
http.get('http://tile-stub:8082/tiles/:z/:x/:y', tile),
|
||||
]
|
||||
|
||||
@@ -82,6 +82,14 @@
|
||||
"335799082893fad97fa36118b131f919"
|
||||
]
|
||||
},
|
||||
"owm_key_in_source": {
|
||||
"ac": "NFT-SEC-09 (AC-1, source portion) — OpenWeatherMap key not present in source tree",
|
||||
"scope": "src/ and mission-planner/ (production sources; tests excluded)",
|
||||
"match": "literal",
|
||||
"patterns": [
|
||||
"335799082893fad97fa36118b131f919"
|
||||
]
|
||||
},
|
||||
"alert_calls": {
|
||||
"ac": "NFT-SEC-07 (AZ-466 AC-5) — no alert() in production source",
|
||||
"scope": "src/ and mission-planner/ (production sources; tests excluded)",
|
||||
|
||||
Reference in New Issue
Block a user