Files
TREK/server/tests/unit/nest/maps.google-provider.test.ts
Maurice 2c3fb15263 style(server): format the files the health lanes added or moved
Prettier on every new or moved file that was not formatted, so lint:format
holds them; the files taken off the format baseline are the ones that moved
or were formatted on the way.
2026-10-10 20:16:04 +02:00

76 lines
3.4 KiB
TypeScript

/**
* GooglePlacesProvider / GooglePlacesClient: the Google half of the keyed
* places-provider seam, on its own. The search, details and photo flows are
* covered through MapsService in maps.service.test.ts; this pins what the
* provider adds as a unit: the interface's reverse answer, the counted
* transport and the credential each request carries.
*/
import type { GoogleQuotaService } from '../../../src/nest/google-quota/google-quota.service';
import { GooglePlacesClient } from '../../../src/nest/maps/providers/google-places.provider';
import { afterEach, describe, expect, it, vi } from 'vitest';
const quota = () => ({ exhausted: vi.fn(async () => false), record: vi.fn(async () => {}) });
function client(q = quota()) {
return { q, client: new GooglePlacesClient(q as unknown as GoogleQuotaService) };
}
const ok = (body: unknown) => new Response(JSON.stringify(body), { status: 200 });
afterEach(() => {
vi.unstubAllGlobals();
delete process.env.PLACES_API_BASE;
});
describe('GooglePlacesProvider', () => {
it('GPROV-001: is never asked to reverse-geocode, and says so with the interface miss', async () => {
const fetchMock = vi.fn();
vi.stubGlobal('fetch', fetchMock);
const { client: c } = client();
expect(await c.provider({ key: 'k', source: 'instance', userId: 1 }).reverse()).toBeNull();
expect(fetchMock).not.toHaveBeenCalled();
});
it('GPROV-002: a text search sends the credential it was built with and counts the call', async () => {
const fetchMock = vi.fn(async (_url: string, _init?: RequestInit) =>
ok({ places: [{ id: 'ChIJ1', displayName: { text: 'Dom' } }] }),
);
vi.stubGlobal('fetch', fetchMock);
const { q, client: c } = client();
const places = await c
.provider({ key: 'secret', source: 'instance', userId: 7 })
.searchText('dom', 'de', { lat: 50, lng: 7, radius: 900000 });
expect(places).toEqual([expect.objectContaining({ google_place_id: 'ChIJ1', name: 'Dom', source: 'google' })]);
expect(q.record).toHaveBeenCalledTimes(1);
const [url, init] = fetchMock.mock.calls[0];
expect(url).toBe('https://places.googleapis.com/v1/places:searchText');
expect((init?.headers as Record<string, string>)['X-Goog-Api-Key']).toBe('secret');
// Google caps the bias circle at 50 km and answers a wider one with a 400.
expect(JSON.parse(String(init?.body)).locationBias.circle.radius).toBe(50000);
});
it('GPROV-003: PLACES_API_BASE moves the call to the operator gateway, path untouched', async () => {
process.env.PLACES_API_BASE = 'https://gateway.example.test/places///';
const fetchMock = vi.fn(async (_url: string, _init?: RequestInit) => ok({ editorialSummary: { text: 'Hi' } }));
vi.stubGlobal('fetch', fetchMock);
const { client: c } = client();
expect(await c.fetchEditorialSummary('ChIJ1', 'k')).toBe('Hi');
expect(fetchMock.mock.calls[0][0]).toBe('https://gateway.example.test/places/v1/places/ChIJ1?languageCode=en');
});
it('GPROV-004: a details error keeps Google message and status', async () => {
vi.stubGlobal(
'fetch',
vi.fn(async () => new Response(JSON.stringify({ error: { message: 'API key not valid' } }), { status: 403 })),
);
const { client: c } = client();
await expect(c.provider({ key: 'k', source: null, userId: 0 }).placeDetailsExpanded('ChIJ1')).rejects.toMatchObject(
{
message: 'API key not valid',
status: 403,
},
);
});
});