Initial import of zappier-edge from zapier monorepo

This commit is contained in:
George Lambert 2026-09-11 13:50:46 -04:00
commit f0d193b221
120 changed files with 19867 additions and 0 deletions

320
tests/portal.test.ts Normal file
View file

@ -0,0 +1,320 @@
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<typeof buildApp>['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 customers 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 customers 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 customers 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);
});
});