Files
redefined-designs/frontend/tests/e2e/intake-submit.spec.ts
T
bermudalamb bcecda9122
Linting / lint (pull_request) Successful in 2m27s
SonarQube Analysis / sonarqube (pull_request) Successful in 22m27s
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
2026-08-31 15:42:26 -05:00

77 lines
2.9 KiB
TypeScript

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();
});
});