import { describe, it, expect, vi, beforeEach } from 'vitest'; import { createBillingClient, BillingApiError } from './index.js'; import type { BillingClient } from './index.js'; function mockFetch(status: number, body: unknown) { return vi.fn().mockResolvedValue({ ok: status >= 200 && status < 300, status, json: () => Promise.resolve(body), }); } describe('createBillingClient', () => { let client: BillingClient; const config = { baseUrl: 'http://localhost:4003/api', productId: 'notelett', getAccessToken: () => 'test-token', }; beforeEach(() => { client = createBillingClient(config); }); it('listPlans — returns plans array', async () => { const plans = [{ name: 'free', displayName: 'Free', price: 0 }]; globalThis.fetch = mockFetch(200, { plans }); const result = await client.listPlans(); expect(result).toEqual(plans); expect(globalThis.fetch).toHaveBeenCalledWith( 'http://localhost:4003/api/plans', expect.objectContaining({ method: 'GET' }) ); }); it('getPlan — returns single plan', async () => { const plan = { name: 'pro', displayName: 'Pro', price: 9.99 }; globalThis.fetch = mockFetch(200, plan); const result = await client.getPlan('pro'); expect(result).toEqual(plan); }); it('getSubscription — returns subscription', async () => { const sub = { id: 'sub_1', plan: 'free', status: 'active' }; globalThis.fetch = mockFetch(200, sub); const result = await client.getSubscription(); expect(result).toEqual(sub); }); it('getSubscription — returns null on 404', async () => { globalThis.fetch = mockFetch(404, { message: 'Not found' }); const result = await client.getSubscription(); expect(result).toBeNull(); }); it('changePlan — sends plan in body', async () => { const sub = { id: 'sub_1', plan: 'pro', status: 'active' }; globalThis.fetch = mockFetch(200, sub); const result = await client.changePlan('pro'); expect(result.plan).toBe('pro'); expect(globalThis.fetch).toHaveBeenCalledWith( 'http://localhost:4003/api/subscriptions/me/change-plan', expect.objectContaining({ method: 'POST', body: JSON.stringify({ plan: 'pro' }), }) ); }); it('cancelSubscription — POST to cancel endpoint', async () => { const sub = { id: 'sub_1', plan: 'pro', cancelAtPeriodEnd: true }; globalThis.fetch = mockFetch(200, sub); const result = await client.cancelSubscription(); expect(result.cancelAtPeriodEnd).toBe(true); }); it('resumeSubscription — POST to resume endpoint', async () => { const sub = { id: 'sub_1', plan: 'pro', cancelAtPeriodEnd: false }; globalThis.fetch = mockFetch(200, sub); const result = await client.resumeSubscription(); expect(result.cancelAtPeriodEnd).toBe(false); }); it('listPayments — returns payments array', async () => { const payments = [{ id: 'pay_1', amount: 999, status: 'succeeded' }]; globalThis.fetch = mockFetch(200, { payments }); const result = await client.listPayments(); expect(result).toEqual(payments); }); it('getUsage — returns usage summary', async () => { const usage = { tokensUsed: 500, tokensIncluded: 10000, tokensRemaining: 9500, percentUsed: 5 }; globalThis.fetch = mockFetch(200, usage); const result = await client.getUsage(); expect(result.tokensRemaining).toBe(9500); }); it('sends auth header and product-id header', async () => { globalThis.fetch = mockFetch(200, { plans: [] }); await client.listPlans(); const call = (globalThis.fetch as ReturnType).mock.calls[0]; const headers = call[1].headers; expect(headers['Authorization']).toBe('Bearer test-token'); expect(headers['x-product-id']).toBe('notelett'); }); it('throws BillingApiError on non-ok response', async () => { globalThis.fetch = mockFetch(403, { message: 'Forbidden' }); await expect(client.changePlan('enterprise')).rejects.toThrow(BillingApiError); }); it('works without access token', async () => { const noAuthClient = createBillingClient({ ...config, getAccessToken: () => null }); globalThis.fetch = mockFetch(200, { plans: [] }); await noAuthClient.listPlans(); const headers = (globalThis.fetch as ReturnType).mock.calls[0][1].headers; expect(headers['Authorization']).toBeUndefined(); }); });