master-zapier-plan-draft/packages/zappier/tests/app.test.ts
George Lambert b814501441 Milestone 1: Add Numbers Zapier app, /v1/add, developer setup guide
packages/verae-activate is pushable with zapier-platform 19.1.0: local
Add Numbers (number1+number2=sum) needs no hosted API. zapier-platform
validate is clean (0 warnings). zappier POST /v1/add is a free metered
endpoint for the optional hosted path.

Tests: verae-activate 7/7, zappier 176/176, verae-zapier 5/5, middleware
gate 1 11/11. Morning steps: docs/04-activate/SETUP-ZAPIER-DEVELOPER.md

Learned: perform runs on Zapier cloud so arithmetic needs no public URL;
CLI and zapier-platform-core majors must match; do not mix zapier-sdk.
2026-09-09 02:41:34 -04:00

127 lines
4.6 KiB
TypeScript

import request from 'supertest';
import { buildApp } from '../src/app';
const KEY = 'key-ada'; // seeded free-tier customer
describe('Zappier API', () => {
it('rejects calls without an API key', async () => {
const { app } = buildApp();
const res = await request(app).get('/v1/status');
expect(res.status).toBe(401);
});
it('GET /v1/status is free', async () => {
const { app } = buildApp();
const res = await request(app).get('/v1/status').set('x-api-key', KEY);
expect(res.status).toBe(200);
expect(res.body.status).toBe('ok');
expect(res.body.quote.totalCents).toBe(0);
});
it('POST /v1/add returns the sum and is free', async () => {
const { app } = buildApp();
const res = await request(app)
.post('/v1/add')
.set('x-api-key', KEY)
.send({ number1: 2, number2: 3 });
expect(res.status).toBe(200);
expect(res.body.sum).toBe(5);
expect(res.body.number1).toBe(2);
expect(res.body.number2).toBe(3);
expect(res.body.quote.totalCents).toBe(0);
});
it('POST /v1/add rejects missing fields', async () => {
const { app } = buildApp();
const res = await request(app).post('/v1/add').set('x-api-key', KEY).send({ number1: 1 });
expect(res.status).toBe(400);
});
it('POST /v1/transform uppercases text at the multiplied fixed price', async () => {
const { app } = buildApp();
const res = await request(app)
.post('/v1/transform')
.set('x-api-key', KEY)
.send({ text: 'hello' });
expect(res.status).toBe(200);
expect(res.body.output).toBe('HELLO');
expect(res.body.quote.totalCents).toBe(4); // list 4 x free-tier multiplier 1
});
it('rejects a request that violates the OpenAPI schema with 400', async () => {
const { app } = buildApp();
const res = await request(app)
.post('/v1/transform')
.set('x-api-key', KEY)
.send({ wrong: 1 });
expect(res.status).toBe(400);
});
it('POST /v1/storage stores metadata plus attachments and quotes by size', async () => {
const { app } = buildApp();
const res = await request(app)
.post('/v1/storage')
.set('x-api-key', KEY)
.field('metadata', JSON.stringify({ title: 'report' }))
.attach('attachments', Buffer.alloc(1024 * 1024), 'one.bin')
.attach('attachments', Buffer.alloc(1024 * 1024), 'two.bin');
expect(res.status).toBe(200);
expect(res.body.id).toBeTruthy();
// metadata string is 20 bytes -> 1 KB; 2 MB attachments
// list 10 + 1 * 1 + 2 * 50 = 111, free-tier multiplier 1
expect(res.body.quote.totalCents).toBe(111);
});
it('POST /v1/storage with malformed metadata JSON returns 400 and charges nothing', async () => {
const { app } = buildApp();
const res = await request(app)
.post('/v1/storage')
.set('x-api-key', KEY)
.field('metadata', '{not json');
expect(res.status).toBe(400);
expect(res.body.error).toMatch(/invalid metadata JSON/);
const usageRes = await request(app).get('/v1/usage').set('x-api-key', KEY);
expect(usageRes.status).toBe(200);
expect(usageRes.body.totalCents).toBe(0);
});
it('GET /v1/storage lists the caller items newest first', async () => {
const { app } = buildApp();
await request(app)
.post('/v1/storage')
.set('x-api-key', KEY)
.field('metadata', JSON.stringify({ title: 'a' }));
await request(app)
.post('/v1/storage')
.set('x-api-key', KEY)
.field('metadata', JSON.stringify({ title: 'b' }));
const res = await request(app).get('/v1/storage').set('x-api-key', KEY);
expect(res.status).toBe(200);
expect(res.body.items).toHaveLength(2);
expect(res.body.items[0].metadata.title).toBe('b');
});
it('GET /v1/usage returns the caller summary with monthly credit applied', async () => {
const { app } = buildApp();
await request(app)
.post('/v1/transform')
.set('x-api-key', KEY)
.send({ text: 'x' });
const res = await request(app).get('/v1/usage').set('x-api-key', KEY);
expect(res.status).toBe(200);
expect(res.body.customerId).toBe('cust_1');
expect(res.body.calls).toBe(1);
expect(res.body.totalCents).toBe(4);
expect(res.body.includedCents).toBe(4); // free-tier credit (100) covers it
expect(res.body.billableCents).toBe(0);
});
it('charges less for a business-tier customer', async () => {
const { app } = buildApp();
const res = await request(app)
.post('/v1/transform')
.set('x-api-key', 'key-linus')
.send({ text: 'hello' });
expect(res.body.quote.totalCents).toBe(1); // list 4 x business multiplier 0.25
});
});