diff --git a/lib/services/pax8-client.test.ts b/lib/services/pax8-client.test.ts index 8adc001..10f781b 100644 --- a/lib/services/pax8-client.test.ts +++ b/lib/services/pax8-client.test.ts @@ -41,6 +41,62 @@ function makeFetchMock(opts: { return { fetchMock, calls }; } +/** + * Multi-page fetch mock for the listAll* pagination helpers. Keys page + * bodies by URL substring (e.g. '/subscriptions') and by the `page=N` query + * param, returning one page envelope per (resource, page) pair. + */ +function makeMultiPageFetchMock(opts: { + resource: string; // e.g. 'subscriptions', 'products', 'companies' + pages: Array<{ content: unknown[]; totalPages: number }>; + tokenBody?: unknown; +}) { + const { + resource, + pages, + tokenBody = { access_token: 'tok', token_type: 'Bearer', expires_in: 86400 }, + } = 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: true, + status: 200, + json: async () => tokenBody, + text: async () => '', + } as unknown as Response; + } + + if (url.includes(`/${resource}`)) { + const match = url.match(/page=(\d+)/); + const pageNum = match ? parseInt(match[1], 10) : 0; + const p = pages[pageNum]; + return { + ok: true, + status: 200, + json: async () => ({ + content: p.content, + page: { size: 200, totalElements: pages.reduce((n, pg) => n + pg.content.length, 0), totalPages: p.totalPages, number: pageNum }, + }), + text: async () => '', + } as unknown as Response; + } + + return { + ok: true, + status: 200, + json: async () => ({ content: [], page: { size: 200, totalElements: 0, totalPages: 1, number: 0 } }), + text: async () => '', + } as unknown as Response; + }); + + return { fetchMock, calls }; +} + describe('Pax8Client', () => { beforeEach(() => { vi.unstubAllGlobals(); @@ -105,4 +161,81 @@ describe('Pax8Client', () => { expect(result).toEqual(companiesBody); }); + + it('listAllSubscriptions() concatenates content across all pages in order', async () => { + const pages = [ + { content: [{ id: 's1' }, { id: 's2' }], totalPages: 2 }, + { content: [{ id: 's3' }], totalPages: 2 }, + ]; + const { fetchMock, calls } = makeMultiPageFetchMock({ resource: 'subscriptions', pages }); + vi.stubGlobal('fetch', fetchMock); + + const client = new Pax8Client({ clientId: 'id1', clientSecret: SECRET }); + const result = await client.listAllSubscriptions(); + + expect(result).toEqual([{ id: 's1' }, { id: 's2' }, { id: 's3' }]); + + const subCalls = calls.filter(c => c.url.includes('/subscriptions')); + expect(subCalls).toHaveLength(2); + for (const c of subCalls) { + expect(c.url).toContain('size=200'); + } + }); + + it('listAllSubscriptions() stops after a single page when totalPages is 1 (no infinite loop)', async () => { + const pages = [{ content: [{ id: 's1' }], totalPages: 1 }]; + const { fetchMock, calls } = makeMultiPageFetchMock({ resource: 'subscriptions', pages }); + vi.stubGlobal('fetch', fetchMock); + + const client = new Pax8Client({ clientId: 'id1', clientSecret: SECRET }); + const result = await client.listAllSubscriptions(); + + expect(result).toEqual([{ id: 's1' }]); + expect(calls.filter(c => c.url.includes('/subscriptions'))).toHaveLength(1); + }); + + it('listAllProducts() concatenates content across all pages in order', async () => { + const pages = [ + { content: [{ id: 'p1' }], totalPages: 2 }, + { content: [{ id: 'p2' }], totalPages: 2 }, + ]; + const { fetchMock } = makeMultiPageFetchMock({ resource: 'products', pages }); + vi.stubGlobal('fetch', fetchMock); + + const client = new Pax8Client({ clientId: 'id1', clientSecret: SECRET }); + const result = await client.listAllProducts(); + + expect(result).toEqual([{ id: 'p1' }, { id: 'p2' }]); + }); + + it('listAllCompanies() concatenates content across all pages in order', async () => { + const pages = [ + { content: [{ id: 'c1' }], totalPages: 2 }, + { content: [{ id: 'c2' }], totalPages: 2 }, + ]; + const { fetchMock } = makeMultiPageFetchMock({ resource: 'companies', pages }); + vi.stubGlobal('fetch', fetchMock); + + const client = new Pax8Client({ clientId: 'id1', clientSecret: SECRET }); + const result = await client.listAllCompanies(); + + expect(result).toEqual([{ id: 'c1' }, { id: 'c2' }]); + }); + + it('every request the listAll* helpers issue is a GET with an Authorization: Bearer header; none use a mutating method', async () => { + const pages = [{ content: [{ id: 's1' }], totalPages: 1 }]; + const { fetchMock, calls } = makeMultiPageFetchMock({ resource: 'subscriptions', pages }); + vi.stubGlobal('fetch', fetchMock); + + const client = new Pax8Client({ clientId: 'id1', clientSecret: SECRET }); + await client.listAllSubscriptions(); + + const dataCalls = calls.filter(c => !c.url.includes('/token')); + expect(dataCalls.length).toBeGreaterThan(0); + for (const c of dataCalls) { + expect(c.init?.method === undefined || c.init?.method === 'GET').toBe(true); + const authHeader = (c.init!.headers as Record)['Authorization']; + expect(authHeader).toMatch(/^Bearer /); + } + }); });