From ed485d8bdece5568ca77936d72b85fa2e3256f14 Mon Sep 17 00:00:00 2001 From: lorentz Date: Fri, 10 Jul 2026 17:32:20 -0400 Subject: [PATCH] test(10-01): add failing tests for Pax8Client token exchange + auth-proof call --- lib/services/pax8-client.test.ts | 108 +++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 lib/services/pax8-client.test.ts diff --git a/lib/services/pax8-client.test.ts b/lib/services/pax8-client.test.ts new file mode 100644 index 0000000..8adc001 --- /dev/null +++ b/lib/services/pax8-client.test.ts @@ -0,0 +1,108 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { Pax8Client } from './pax8-client'; + +const SECRET = 'super-secret-value-should-never-leak'; + +function makeFetchMock(opts: { + tokenOk?: boolean; + tokenStatus?: number; + tokenBody?: unknown; + companiesBody?: unknown; +}) { + const { + tokenOk = true, + tokenStatus = 200, + tokenBody = { access_token: 'tok', token_type: 'Bearer', expires_in: 86400 }, + companiesBody = { content: [{ id: 'c1', name: 'Acme' }], page: { size: 10, totalElements: 1, totalPages: 1, number: 0 } }, + } = opts; + + const calls: Array<{ url: string; init?: RequestInit }> = []; + + const fetchMock = vi.fn(async (url: string, init?: RequestInit) => { + calls.push({ url, init }); + + if (url.includes('/token')) { + return { + ok: tokenOk, + status: tokenStatus, + json: async () => tokenBody, + text: async () => `token error status ${tokenStatus}`, + } as unknown as Response; + } + + return { + ok: true, + status: 200, + json: async () => companiesBody, + text: async () => '', + } as unknown as Response; + }); + + return { fetchMock, calls }; +} + +describe('Pax8Client', () => { + beforeEach(() => { + vi.unstubAllGlobals(); + }); + + it('getToken() POSTs a JSON body with grant_type, client_id, client_secret, audience', async () => { + const { fetchMock, calls } = makeFetchMock({}); + vi.stubGlobal('fetch', fetchMock); + + const client = new Pax8Client({ clientId: 'id1', clientSecret: SECRET }); + await client.listCompanies(); + + const tokenCall = calls.find(c => c.url.includes('/token')); + expect(tokenCall).toBeDefined(); + expect((tokenCall!.init!.headers as Record)['Content-Type']).toBe('application/json'); + + const parsedBody = JSON.parse(tokenCall!.init!.body as string); + expect(parsedBody.grant_type).toBe('client_credentials'); + expect(parsedBody.client_id).toBe('id1'); + expect(parsedBody.client_secret).toBe(SECRET); + expect(parsedBody.audience).toBe('https://api.pax8.com'); + }); + + it('reuses the cached token without a second fetch within the cache window', async () => { + const { fetchMock, calls } = makeFetchMock({}); + vi.stubGlobal('fetch', fetchMock); + + const client = new Pax8Client({ clientId: 'id1', clientSecret: SECRET }); + await client.listCompanies(); + await client.listCompanies(); + + const tokenCalls = calls.filter(c => c.url.includes('/token')); + expect(tokenCalls).toHaveLength(1); + expect(fetchMock).toHaveBeenCalledTimes(3); // 1 token + 2 companies + }); + + it('throws on a not-ok token response, including the status but never the secret', async () => { + const { fetchMock } = makeFetchMock({ tokenOk: false, tokenStatus: 401 }); + vi.stubGlobal('fetch', fetchMock); + + const client = new Pax8Client({ clientId: 'id1', clientSecret: SECRET }); + + await expect(client.listCompanies()).rejects.toThrow(/401/); + await expect(client.listCompanies()).rejects.not.toThrow(new RegExp(SECRET)); + }); + + it('listCompanies() sends Authorization: Bearer and returns the parsed envelope', async () => { + const companiesBody = { + content: [{ id: 'c1', name: 'Acme Co' }], + page: { size: 10, totalElements: 1, totalPages: 1, number: 0 }, + }; + const { fetchMock, calls } = makeFetchMock({ companiesBody }); + vi.stubGlobal('fetch', fetchMock); + + const client = new Pax8Client({ clientId: 'id1', clientSecret: SECRET }); + const result = await client.listCompanies(); + + const companiesCall = calls.find(c => c.url.includes('/companies')); + expect(companiesCall).toBeDefined(); + const authHeader = (companiesCall!.init!.headers as Record)['Authorization']; + expect(authHeader).toMatch(/^Bearer /); + + expect(result).toEqual(companiesBody); + }); +});