Files
ui/tests/mission_planner_weather.test.ts
T
Oleksandr Bezdieniezhnykh b016fd8207 [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>
2026-05-12 04:34:39 +03:00

117 lines
3.4 KiB
TypeScript

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()
})
})