// Client HTTP behavior via an injected mock fetch (no network). import { test } from 'node:test'; import assert from 'node:assert/strict'; import { ParkSmarterClient, ParkSmarterApiError, MemoryTokenStore } from '../dist/index.js'; /** Build a fake fetch that records calls and returns canned responses. */ function mockFetch(handler) { const calls = []; const fetchImpl = async (url, init) => { calls.push({ url: String(url), init }); const { status = 200, body = {} } = handler({ url: String(url), init, calls }) ?? {}; const text = typeof body === 'string' ? body : JSON.stringify(body); return { status, ok: status >= 200 && status < 300, text: async () => text, headers: new Map(), }; }; return { fetchImpl, calls }; } test('every request carries Application_Token and a fresh X-Request-Id', async () => { const { fetchImpl, calls } = mockFetch(() => ({ body: { Response: { Status: 'Success' } } })); const ps = new ParkSmarterClient({ environment: 'prodv2', fetchImpl }); await ps.getApplicationValidity(); const h = calls[0].init.headers; assert.ok(h['Application_Token'], 'Application_Token present'); assert.match(h['X-Request-Id'], /[0-9a-f]{8}-[0-9a-f]{4}/i, 'X-Request-Id is a UUID'); }); test('single-estimate query serializes the ParkSmarter param names', async () => { const { fetchImpl, calls } = mockFetch(() => ({ body: { ParkingDetail: {}, MinTime: 5, MaxTime: 33, Response: { Status: 'Success' } }, })); const ps = new ParkSmarterClient({ environment: 'prodv2', fetchImpl }); await ps.getParkingEstimateSingle({ zoneId: 113165, spaceId: 329522, customerId: 217, vehicleId: 42, durationInMinutes: 30, creditCardId: 0, }); const url = calls[0].url; assert.match(url, /ZoneID=113165/); assert.match(url, /SpaceID=329522/); assert.match(url, /CustomerID=217/); assert.match(url, /ParkingDuration=30/); assert.match(url, /VehicleID=42/); }); test('a non-2xx response throws ParkSmarterApiError carrying the status', async () => { const { fetchImpl } = mockFetch(() => ({ status: 500, body: { message: 'boom' } })); const ps = new ParkSmarterClient({ environment: 'prodv2', fetchImpl }); await assert.rejects( () => ps.getApplicationValidity(), (e) => e instanceof ParkSmarterApiError && e.status === 500, ); }); test('rolling Auth_Token from the Response envelope is stored and re-sent', async () => { const tokens = new MemoryTokenStore(); const { fetchImpl, calls } = mockFetch(() => ({ body: { Response: { Status: 'Success', Auth_Token: 'REFRESHED123' } }, })); const ps = new ParkSmarterClient({ environment: 'prodv2', fetchImpl, tokens }); await ps.getUserDetail(); await ps.getUserDetail(); assert.equal(calls[1].init.headers['Auth_Token'], 'REFRESHED123'); assert.equal(await tokens.getAuthToken(), 'REFRESHED123'); }); test('a 401 clears the auth token and invokes onUnauthorized', async () => { const tokens = new MemoryTokenStore(); await tokens.setAuthToken('OLD'); let unauthorized = 0; const { fetchImpl } = mockFetch(() => ({ status: 401, body: { Response: { Status: 'Error' } } })); const ps = new ParkSmarterClient({ environment: 'prodv2', fetchImpl, tokens, onUnauthorized: () => { unauthorized += 1; }, }); await assert.rejects(() => ps.getUserDetail()); assert.equal(unauthorized, 1); assert.equal(await tokens.getAuthToken(), null); });