fix(intake): stop a throttled sender being told their link is dead (#222)
Adding e2e specs for the submission page found a defect in the page they were written for, which is what they were for. One limiter counted page loads and submissions against the same twenty-per-quarter-hour allowance, so a sender working through a box of stock ran out after ten items — the exact person the feature exists for, and the exact case the limiter's own comment said must not be refused. The comment said refusing them costs a consignment while the number quietly did it. Worse, the page could not tell a 429 from a 404. `fetchIntakeLink` treated any non-OK response as "no link", so a throttled sender was told "This link is not active" and sent to ask for a replacement — which could not have helped, because the problem was their address and a minute of patience. Two conditions needing opposite reactions were sharing a message. Now two limiters, because the two requests cost different things. Reading a link hits one indexed row and writes nothing, so that allowance is generous at 120: someone re-reading the form or losing their signal should never be told to wait. Submitting writes up to six files, so that is the one worth bounding, at 30 — more than anyone photographing items can manage and far less than a script would want. The page gains a third state. Unknown, revoked and used-up still collapse into one "not active" card, because whether a link exists is not something a stranger needs to learn. Throttled is deliberately kept apart from them, since "wait a moment" and "go and ask for another link" are opposite instructions. Measured rather than assumed, on a freshly started process both times: before, 25 page loads produced 14 rejections; after, 40 produce none. The first attempt at that measurement was wrong and worth recording — the restart had failed with EADDRINUSE, so it read 30 of 30 against the old process's already-exhausted store. The two specs now pass in a full parallel run alongside everything else. They are scoped the way #241 asks: unique run ids, assertions naming only this run's rows, nothing asserted about the table as a whole. Backend: 284 integration, 309 unit. Frontend: build clean, lint unchanged at 2 pre-existing warnings. Ref #222, #241
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
import { test, expect, createAdminContext, uniqueSuffix } from './fixtures';
|
||||
|
||||
/**
|
||||
* The public submission page (#222).
|
||||
*
|
||||
* Every fixture here carries a run id and every assertion names only what this
|
||||
* run created. The suite is fullyParallel against one shared database, so a
|
||||
* spec that asserts on anything catalogue-wide is asserting on other specs too
|
||||
* (#241).
|
||||
*/
|
||||
const RUN = `i${uniqueSuffix()}`;
|
||||
|
||||
let token = '';
|
||||
|
||||
test.beforeAll(async ({ playwright }) => {
|
||||
const api = await createAdminContext(playwright);
|
||||
|
||||
const res = await api.post('/api/admin/upload-links', {
|
||||
data: { label: `Intake spec ${RUN}` }
|
||||
});
|
||||
expect(res.status(), 'creating the upload link').toBe(201);
|
||||
token = (await res.json()).token;
|
||||
|
||||
await api.dispose();
|
||||
});
|
||||
|
||||
test.describe('Sending in an item through a link', () => {
|
||||
test('shows the form for a link that works', async ({ page }) => {
|
||||
await page.goto(`/submit/${token}`);
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Send in an item' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Choose photos' })).toBeVisible();
|
||||
});
|
||||
|
||||
// Nothing to send is not a submission, and the server would refuse it — but
|
||||
// the sender should not have to find that out by pressing the button.
|
||||
test('will not send until a photo is chosen', async ({ page }) => {
|
||||
await page.goto(`/submit/${token}`);
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Send' })).toBeDisabled();
|
||||
});
|
||||
|
||||
// One state for every refusal, matching the server's single 404. Saying which
|
||||
// kind of dead a link is would tell a stranger whether one they guessed at
|
||||
// exists.
|
||||
test('explains an unusable link without saying which kind', async ({ page }) => {
|
||||
await page.goto('/submit/not-a-real-token');
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'This link is not active' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Choose photos' })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('accepts a photo and says it arrived', async ({ page }) => {
|
||||
await page.goto(`/submit/${token}`);
|
||||
|
||||
// A real 1x1 PNG, so the server's magic-byte check sees what it expects
|
||||
// rather than a buffer that merely starts correctly.
|
||||
const png = Buffer.from(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==',
|
||||
'base64'
|
||||
);
|
||||
await page.setInputFiles('input[type="file"]', {
|
||||
name: `${RUN}.png`,
|
||||
mimeType: 'image/png',
|
||||
buffer: png
|
||||
});
|
||||
await page.getByLabel('Anything you know about this item').fill(`Stoneware ${RUN}`);
|
||||
|
||||
await page.getByRole('button', { name: 'Send' }).click();
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Thank you — it arrived' })).toBeVisible();
|
||||
// The wording matters: it is what stops a sender wondering why their item
|
||||
// is not on the site.
|
||||
await expect(page.getByText(/Nothing is listed for sale until/)).toBeVisible();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user