import request from 'supertest'; import app from '../../src/app'; import { pool } from '../../src/db'; import { resetDb, closeDb } from './setup/testDb'; import { resetAlertThrottleForTests } from '../../src/intake/abuseAlert'; const PNG = Buffer.from( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64' ); beforeEach(async () => { await resetDb(); resetAlertThrottleForTests(); }); afterAll(async () => { await pool.end(); await closeDb(); }); async function makeLink(): Promise { const res = await request(app) .post('/api/admin/upload-links') .send({ label: 'ceiling spec', email: 'sarah@example.com' }); expect(res.status).toBe(201); return res.body.token; } /** Fills the window with drafts, as though earlier submissions had arrived. */ async function fillWindow(count: number): Promise { for (let i = 0; i < count; i++) { const { rows } = await pool.query<{ id: number }>( `INSERT INTO items (name, status) VALUES ('filler', 'pending') RETURNING id` ); await pool.query(`INSERT INTO item_drafts (item_id) VALUES ($1)`, [rows[0]!.id]); } } async function setCeiling(value: number): Promise { await pool.query( `INSERT INTO admin_settings (key, value, updated_at) VALUES ('intake_daily_ceiling', $1, now()) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now()`, [String(value)] ); } const submit = (token: string) => request(app).post(`/api/intake/${token}`).attach('images', PNG, 'a.png'); const draftCount = async (): Promise => (await pool.query<{ c: number }>(`SELECT count(*)::int AS c FROM item_drafts`)).rows[0]?.c ?? 0; describe('the submission ceiling', () => { it('accepts a submission below the ceiling', async () => { await setCeiling(5); const token = await makeLink(); expect((await submit(token)).status).toBe(201); }); it('refuses with 503 once the ceiling is reached', async () => { await setCeiling(2); await fillWindow(2); const token = await makeLink(); const res = await submit(token); expect(res.status).toBe(503); expect(res.body.error).toMatch(/later/i); }); // Nothing the sender did is wrong and the condition clears by itself, so this // must not read as a rejection of them or of their link. it('leaves the link usable, so it works again when there is room', async () => { await setCeiling(1); await fillWindow(1); const token = await makeLink(); expect((await submit(token)).status).toBe(503); await setCeiling(50); expect((await submit(token)).status).toBe(201); }); // A refused submission must write nothing. The check is ordered ahead of // uploadImages for the same reason requireUsableLink is. it('stores no item when it refuses', async () => { await setCeiling(1); await fillWindow(1); const token = await makeLink(); await submit(token); expect(await draftCount()).toBe(1); }); /** * The constraint that matters most. Intake being throttled is an * inconvenience; the shop being unable to add its own stock is an outage. */ it('never applies to the admin upload path', async () => { await setCeiling(1); await fillWindow(5); const res = await request(app) .post('/api/admin/items') .field('name', 'admin adds stock') .field('price', '42.00') .attach('images', PNG, 'a.png'); // 200, not 201 — this route answers with the created item rather than a // bare created status, unlike the intake route. expect(res.status).toBe(200); }); it('accepts again after the window is reset', async () => { await setCeiling(1); await fillWindow(1); const token = await makeLink(); expect((await submit(token)).status).toBe(503); const reset = await request(app).post('/api/admin/upload-links/reset-ceiling'); expect(reset.status).toBe(200); expect((await submit(token)).status).toBe(201); }); // A reset forgives; it does not erase. Those submissions are real and their // items are in the review queue. it('keeps the submissions it counted after a reset', async () => { await fillWindow(3); await request(app).post('/api/admin/upload-links/reset-ceiling'); expect(await draftCount()).toBe(3); }); // reset-ceiling is declared above /:id/revoke, so Express must not read it as // an id and try to revoke a link called "reset-ceiling". it('does not collide with the revoke route', async () => { const res = await request(app).post('/api/admin/upload-links/reset-ceiling'); expect(res.body).toEqual({ reset: true }); }); });