import request from 'supertest'; import { totp } from '../src/accounts'; import { buildApp } from '../src/app'; import { PaymentClient } from '../src/portal'; const devPayments: PaymentClient = { reload: async (_customer, amountCents) => ({ mode: 'dev' as const, creditedCents: amountCents }), }; function portalApp() { return buildApp({ payments: devPayments, qr: async (uri) => `data:image/png;base64,fake-qr-for:${uri}`, }); } async function signup(app: ReturnType['app'], email = 'ada@example.com') { const res = await request(app) .post('/portal/api/signup') .send({ name: 'Ada', email, password: 'super-secret-1' }); expect(res.status).toBe(201); return res.body as { token: string; customer: { id: string; apiKey: string } }; } describe('portal signup + login', () => { it('signs up a new customer on the free tier with an API key', async () => { const { app, customers } = portalApp(); const { token, customer } = await signup(app); expect(customer.apiKey).toMatch(/^key-/); const stored = customers.findByEmail('ada@example.com'); expect(stored?.tierId).toBe('free'); expect(stored?.passwordHash).toMatch(/^scrypt:/); const me = await request(app).get('/portal/api/me').set('authorization', `Bearer ${token}`); expect(me.status).toBe(200); expect(me.body.tierId).toBe('free'); expect(me.body.balanceCents).toBe(0); }); it('never leaks passwordHash or totpSecret through the API', async () => { const { app } = portalApp(); const { token } = await signup(app); const me = await request(app).get('/portal/api/me').set('authorization', `Bearer ${token}`); expect(me.body.passwordHash).toBeUndefined(); expect(me.body.totpSecret).toBeUndefined(); }); it('rejects weak passwords and duplicate emails', async () => { const { app } = portalApp(); const weak = await request(app) .post('/portal/api/signup') .send({ name: 'Ada', email: 'ada@example.com', password: 'short' }); expect(weak.status).toBe(400); await signup(app); const dup = await request(app) .post('/portal/api/signup') .send({ name: 'Other', email: 'ada@example.com', password: 'super-secret-2' }); expect(dup.status).toBe(409); }); it('claims an admin-created customer by email on first signup', async () => { const { app, customers } = portalApp(); const ada = customers.findByApiKey('key-ada')!; customers.save({ ...ada, email: 'ada@example.com' }); const { customer } = await signup(app); expect(customer.id).toBe('cust_1'); // same customer, now with a password expect(customers.findByApiKey('key-ada')?.passwordHash).toMatch(/^scrypt:/); }); it('logs in with email + password and rejects bad credentials', async () => { const { app } = portalApp(); await signup(app); const bad = await request(app) .post('/portal/api/login') .send({ email: 'ada@example.com', password: 'wrong-password' }); expect(bad.status).toBe(401); const ok = await request(app) .post('/portal/api/login') .send({ email: 'ada@example.com', password: 'super-secret-1' }); expect(ok.status).toBe(200); expect(ok.body.token).toBeTruthy(); }); it('requires a session for /me (401 without token)', async () => { const { app } = portalApp(); expect((await request(app).get('/portal/api/me')).status).toBe(401); }); }); describe('portal scoping', () => { it('lists only the signed-in customer’s invoices', async () => { const { app, customers, invoices } = portalApp(); const { token, customer } = await signup(app); invoices.save({ id: 'INV-2026-07-0001', customerId: customer.id, period: '2026-07', status: 'issued', lines: [{ endpointId: 'transform', calls: 10, cents: 30 }], totalCents: 30, creditCents: 0, billableCents: 30, billingType: 'stripe', }); invoices.save({ id: 'INV-2026-07-0002', customerId: 'cust_2', period: '2026-07', status: 'issued', lines: [], totalCents: 999, creditCents: 0, billableCents: 999, billingType: 'stripe', }); const res = await request(app) .get('/portal/api/invoices') .set('authorization', `Bearer ${token}`); expect(res.body.invoices.map((i: { id: string }) => i.id)).toEqual(['INV-2026-07-0001']); expect(customers.list()).toHaveLength(4); // 3 seeds + signup }); it('returns 404 for another customer’s invoice (json and html)', async () => { const { app, invoices } = portalApp(); const { token } = await signup(app); invoices.save({ id: 'INV-2026-07-0009', customerId: 'cust_2', period: '2026-07', status: 'issued', lines: [], totalCents: 999, creditCents: 0, billableCents: 999, billingType: 'stripe', }); const json = await request(app) .get('/portal/api/invoices/INV-2026-07-0009') .set('authorization', `Bearer ${token}`); expect(json.status).toBe(404); const html = await request(app) .get('/portal/api/invoices/INV-2026-07-0009?format=html') .set('authorization', `Bearer ${token}`); expect(html.status).toBe(404); }); it('serves the customer’s own invoice as printable HTML', async () => { const { app, invoices } = portalApp(); const { token, customer } = await signup(app); invoices.save({ id: 'INV-2026-07-0001', customerId: customer.id, period: '2026-07', status: 'issued', lines: [{ endpointId: 'transform', calls: 10, cents: 30 }], totalCents: 30, creditCents: 0, billableCents: 30, billingType: 'stripe', }); const res = await request(app) .get('/portal/api/invoices/INV-2026-07-0001?format=html') .set('authorization', `Bearer ${token}`); expect(res.status).toBe(200); expect(res.text).toContain('INV-2026-07-0001'); }); }); describe('portal api key + usage', () => { it('regenerates the API key; the old key stops working', async () => { const { app } = portalApp(); const { token, customer } = await signup(app); const oldKey = customer.apiKey; const res = await request(app) .post('/portal/api/api-key') .set('authorization', `Bearer ${token}`); expect(res.status).toBe(200); expect(res.body.apiKey).not.toBe(oldKey); const stale = await request(app).get('/v1/status').set('x-api-key', oldKey); expect(stale.status).toBe(401); const fresh = await request(app).get('/v1/status').set('x-api-key', res.body.apiKey); expect(fresh.status).toBe(200); }); it('reports month-to-date usage with the tier credit applied', async () => { const { app, usage, customers } = portalApp(); const { token, customer } = await signup(app); usage.record({ customerId: customer.id, endpointId: 'transform', cents: 40, metadataBytes: 0, attachmentBytes: 0, timestamp: new Date(), }); const res = await request(app).get('/portal/api/usage').set('authorization', `Bearer ${token}`); expect(res.status).toBe(200); expect(res.body.totalCents).toBe(40); expect(res.body.includedCents).toBe(40); // free tier credit 100 covers it expect(res.body.billableCents).toBe(0); expect(customers.findByEmail('ada@example.com')).toBeTruthy(); }); }); describe('portal 2FA', () => { it('setup → enable → login requires the TOTP code', async () => { const { app, customers } = portalApp(); const { token } = await signup(app); const setup = await request(app) .post('/portal/api/2fa/setup') .set('authorization', `Bearer ${token}`); expect(setup.status).toBe(200); expect(setup.body.secret).toMatch(/^[A-Z2-7]{32}$/); expect(setup.body.uri).toContain('otpauth://totp/'); expect(setup.body.qr).toMatch(/^data:image\/png;base64,/); // Enabling with a wrong code fails. const badEnable = await request(app) .post('/portal/api/2fa/enable') .set('authorization', `Bearer ${token}`) .send({ code: '000000' }); expect(badEnable.status).toBe(400); const code = totp(setup.body.secret, Date.now()); const enable = await request(app) .post('/portal/api/2fa/enable') .set('authorization', `Bearer ${token}`) .send({ code }); expect(enable.status).toBe(200); expect(customers.findByEmail('ada@example.com')?.totpEnabled).toBe(true); // Password alone no longer suffices. const noCode = await request(app) .post('/portal/api/login') .send({ email: 'ada@example.com', password: 'super-secret-1' }); expect(noCode.status).toBe(401); expect(noCode.body.error).toBe('totp_required'); const withCode = await request(app) .post('/portal/api/login') .send({ email: 'ada@example.com', password: 'super-secret-1', totpCode: totp(setup.body.secret, Date.now()), }); expect(withCode.status).toBe(200); }); it('disables 2FA with a valid code', async () => { const { app, customers } = portalApp(); const { token } = await signup(app); const setup = await request(app) .post('/portal/api/2fa/setup') .set('authorization', `Bearer ${token}`); await request(app) .post('/portal/api/2fa/enable') .set('authorization', `Bearer ${token}`) .send({ code: totp(setup.body.secret, Date.now()) }); const disable = await request(app) .post('/portal/api/2fa/disable') .set('authorization', `Bearer ${token}`) .send({ code: totp(setup.body.secret, Date.now()) }); expect(disable.status).toBe(200); const stored = customers.findByEmail('ada@example.com'); expect(stored?.totpEnabled).toBe(false); expect(stored?.totpSecret).toBeUndefined(); }); }); describe('portal billing', () => { it('reloads the prepaid balance via the payment client', async () => { const { app } = portalApp(); const { token } = await signup(app); const res = await request(app) .post('/portal/api/reload') .set('authorization', `Bearer ${token}`) .send({ amountCents: 2500 }); expect(res.status).toBe(200); expect(res.body.balanceCents).toBe(2500); expect(res.body.mode).toBe('dev'); const me = await request(app).get('/portal/api/me').set('authorization', `Bearer ${token}`); expect(me.body.balanceCents).toBe(2500); }); it('rejects out-of-range reload amounts', async () => { const { app } = portalApp(); const { token } = await signup(app); for (const amountCents of [0, 50, -100, 2_000_000, 12.5]) { const res = await request(app) .post('/portal/api/reload') .set('authorization', `Bearer ${token}`) .send({ amountCents }); expect(res.status).toBe(400); } }); it('serves the live rate card and tiers for the pricing page', async () => { const { app } = portalApp(); const { token } = await signup(app); const res = await request(app) .get('/portal/api/pricing') .set('authorization', `Bearer ${token}`); expect(res.status).toBe(200); expect(res.body.rateCard.endpoints.transform).toBeTruthy(); expect(res.body.tiers.map((t: { id: string }) => t.id)).toEqual( expect.arrayContaining(['free', 'pro', 'business']), ); }); it('toggles email invoicing', async () => { const { app } = portalApp(); const { token } = await signup(app); const res = await request(app) .put('/portal/api/email-invoicing') .set('authorization', `Bearer ${token}`) .send({ enabled: true }); expect(res.status).toBe(200); const me = await request(app).get('/portal/api/me').set('authorization', `Bearer ${token}`); expect(me.body.emailInvoicing).toBe(true); }); });