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