Compose-ready workspace: packages/zappier (rate card, portal, Stripe), packages/verae-zapier-middleware (timestamp + NATS), packages/verae-zapier (CLI app), vendor/zapier-platform, and research/zapier vendor corpus. Gate 0 structure checks pass. Product code and research are not yet wired.
86 lines
3.2 KiB
TypeScript
86 lines
3.2 KiB
TypeScript
import express from 'express';
|
|
import request from 'supertest';
|
|
import { apiKeyAuth, Customer, InMemoryCustomerRepo } from '../src/auth';
|
|
import { meter } from '../src/meter';
|
|
import { DEFAULT_PRICING } from '../src/pricing';
|
|
import { InMemoryUsageRepo } from '../src/usage';
|
|
|
|
const FREE_ADA: Customer = { id: 'cust_1', name: 'Ada', tierId: 'free', apiKey: 'key-ada' };
|
|
|
|
function buildApp(seed: Customer[] = [FREE_ADA]) {
|
|
const customers = new InMemoryCustomerRepo(seed);
|
|
const usage = new InMemoryUsageRepo();
|
|
const app = express();
|
|
app.use(express.json());
|
|
app.use(apiKeyAuth(customers));
|
|
app.post('/transform', meter('transform', usage, DEFAULT_PRICING), (req, res) =>
|
|
res.json({ quote: res.locals.quote }),
|
|
);
|
|
app.post('/storage', meter('storage', usage, DEFAULT_PRICING), (req, res) =>
|
|
res.json({ quote: res.locals.quote }),
|
|
);
|
|
app.post('/experimental', meter('experimental', usage, DEFAULT_PRICING), (req, res) =>
|
|
res.json({ quote: res.locals.quote }),
|
|
);
|
|
return { app, usage };
|
|
}
|
|
|
|
describe('meter middleware', () => {
|
|
it('quotes a fixed endpoint at the multiplied tier price and records usage', async () => {
|
|
const { app, usage } = buildApp();
|
|
const res = await request(app)
|
|
.post('/transform')
|
|
.set('x-api-key', 'key-ada')
|
|
.send({ text: 'hi' });
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.quote.totalCents).toBe(4); // free tier, multiplier 1
|
|
const entries = usage.listFor('cust_1');
|
|
expect(entries).toHaveLength(1);
|
|
expect(entries[0].endpointId).toBe('transform');
|
|
expect(entries[0].cents).toBe(4);
|
|
});
|
|
|
|
it('charges per KB of metadata on variable endpoints', async () => {
|
|
const { app } = buildApp();
|
|
const res = await request(app)
|
|
.post('/storage')
|
|
.set('x-api-key', 'key-ada')
|
|
.send({ metadata: { note: 'x'.repeat(2048) } });
|
|
// JSON of metadata is 2059 bytes -> 3 KB -> list 10 + 3 * 1 = 13
|
|
expect(res.body.quote.totalCents).toBe(13);
|
|
});
|
|
|
|
it('applies a per-customer multiplier override', async () => {
|
|
const { app } = buildApp([
|
|
{ id: 'cust_vip', name: 'Vip', tierId: 'free', apiKey: 'key-vip', multiplierOverride: 0.5 },
|
|
]);
|
|
const res = await request(app)
|
|
.post('/transform')
|
|
.set('x-api-key', 'key-vip')
|
|
.send({ text: 'hi' });
|
|
expect(res.body.quote.totalCents).toBe(2); // list 4 x override 0.5
|
|
});
|
|
|
|
it('returns 401 when no customer is attached', async () => {
|
|
const usage = new InMemoryUsageRepo();
|
|
const app = express();
|
|
app.use(express.json());
|
|
app.post('/transform', meter('transform', usage, DEFAULT_PRICING), (req, res) =>
|
|
res.json({}),
|
|
);
|
|
const res = await request(app).post('/transform').send({ text: 'hi' });
|
|
expect(res.status).toBe(401);
|
|
expect(usage.listFor('cust_1')).toHaveLength(0);
|
|
});
|
|
|
|
it('returns 403 for an endpoint with no price rule on the caller tier', async () => {
|
|
const { app, usage } = buildApp();
|
|
const res = await request(app)
|
|
.post('/experimental')
|
|
.set('x-api-key', 'key-ada')
|
|
.send({});
|
|
expect(res.status).toBe(403);
|
|
expect(res.body.error).toMatch(/No price rule/);
|
|
expect(usage.listFor('cust_1')).toHaveLength(0);
|
|
});
|
|
});
|