Files
ui/tests/mission_planner_geocode.test.ts
T
Oleksandr Bezdieniezhnykh f7dd6c98d8
ci/woodpecker/push/build-arm Pipeline failed
[AZ-501] [AZ-502] Cycle 2 Step 14 security audit + inline fixes
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>
2026-05-12 05:31:11 +03:00

102 lines
3.3 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { geocodeAddress } from '../mission-planner/src/services/GeocodeService'
// AZ-501 — mission-planner GeocodeService env-var hardening (mirrors AZ-499).
//
// Lives under tests/ rather than colocated under mission-planner/ for the
// same reason as mission_planner_weather.test.ts: mission-planner has no
// runner of its own; the suite Vitest config covers mission-planner/src.
type FetchMock = ReturnType<typeof vi.fn>
const okResponse = (lat: number, lng: number) =>
new Response(
JSON.stringify({
status: 'OK',
results: [{ geometry: { location: { lat, lng } } }],
}),
{ status: 200 },
)
describe('AZ-501 — mission-planner geocodeAddress (env vars + fail-soft)', () => {
let fetchMock: FetchMock
let warnSpy: ReturnType<typeof vi.spyOn>
beforeEach(() => {
fetchMock = vi.fn(async () => okResponse(50.45, 30.52))
vi.spyOn(globalThis, 'fetch').mockImplementation(fetchMock)
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
})
afterEach(() => {
vi.restoreAllMocks()
vi.unstubAllEnvs()
})
it('AC-1: env-var resolved API key reaches the outgoing fetch URL', async () => {
// Arrange
vi.stubEnv('VITE_GOOGLE_GEOCODE_KEY', 'env-key-xyz')
// Act
const result = await geocodeAddress('Kyiv, Ukraine')
// Assert
expect(fetchMock).toHaveBeenCalledTimes(1)
const url = String(fetchMock.mock.calls[0][0])
expect(url).toContain('key=env-key-xyz')
expect(url).toContain('address=Kyiv%2C%20Ukraine')
expect(result).toEqual({ lat: 50.45, lng: 30.52 })
})
it('AC-3: returns null, issues no fetch, and warns when VITE_GOOGLE_GEOCODE_KEY is unset', async () => {
// Arrange
vi.stubEnv('VITE_GOOGLE_GEOCODE_KEY', '')
// Act
const result = await geocodeAddress('Kyiv, Ukraine')
// Assert
expect(result).toBeNull()
expect(fetchMock).not.toHaveBeenCalled()
expect(warnSpy).toHaveBeenCalledTimes(1)
expect(String(warnSpy.mock.calls[0][0])).toContain('VITE_GOOGLE_GEOCODE_KEY')
})
it('AC-3: still returns null and does not throw when fetch rejects (network error fail-soft)', async () => {
// Arrange
vi.stubEnv('VITE_GOOGLE_GEOCODE_KEY', 'env-key-xyz')
fetchMock.mockRejectedValueOnce(new Error('boom'))
// Act
const result = await geocodeAddress('Kyiv, Ukraine')
// Assert
expect(result).toBeNull()
})
it('returns null when the response status is non-OK (e.g. ZERO_RESULTS)', async () => {
// Arrange
vi.stubEnv('VITE_GOOGLE_GEOCODE_KEY', 'env-key-xyz')
fetchMock.mockResolvedValueOnce(
new Response(JSON.stringify({ status: 'ZERO_RESULTS', results: [] }), {
status: 200,
}),
)
// Act
const result = await geocodeAddress('nowhere-place-12345')
// Assert
expect(result).toBeNull()
})
it('AC-4 (defense-in-depth): no live key string is hardcoded in the service module', async () => {
// Assert: importing GeocodeService.ts must NOT bring along the previously
// hardcoded literal. Done as a sanity assertion on the resolved URL when
// no env var is present — ensures we cannot accidentally leak a fallback.
vi.stubEnv('VITE_GOOGLE_GEOCODE_KEY', '')
await geocodeAddress('test')
expect(fetchMock).not.toHaveBeenCalled()
})
})