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,72 @@
|
||||
import { test, expect, uniqueSuffix } from './fixtures';
|
||||
|
||||
/**
|
||||
* Issuing and revoking upload links from the admin (#222).
|
||||
*
|
||||
* Every link is labelled with a fresh run id and every assertion is scoped to
|
||||
* that row. The suite is fullyParallel against one shared database, so an
|
||||
* assertion on the table as a whole would be an assertion on whatever other
|
||||
* specs happen to be doing (#241).
|
||||
*/
|
||||
|
||||
test.describe('Managing upload links', () => {
|
||||
test('issues a link, shows its token once, and lists it bounded', async ({ page, admin }) => {
|
||||
const label = `Sarah ${uniqueSuffix()}`;
|
||||
|
||||
await admin.goto();
|
||||
await page.getByRole('tab', { name: 'Upload links' }).click();
|
||||
|
||||
await page.getByLabel('Link label').fill(label);
|
||||
await page.getByRole('button', { name: 'Create link' }).click();
|
||||
|
||||
// Shown exactly once. The server keeps only a digest, so there is no
|
||||
// reveal to come back to — the copy has to say so.
|
||||
await expect(page.getByText('Copy this link now')).toBeVisible();
|
||||
await expect(page.getByText(/\/submit\//)).toBeVisible();
|
||||
await expect(page.getByText(/cannot be shown again/)).toBeVisible();
|
||||
|
||||
const row = page.getByRole('row', { name: new RegExp(label) });
|
||||
await expect(row).toBeVisible();
|
||||
// The default cap, not unlimited. An unbounded link should be asked for.
|
||||
await expect(row.getByText('0 of 25')).toBeVisible();
|
||||
await expect(row.getByText('Active')).toBeVisible();
|
||||
});
|
||||
|
||||
test('revokes a link, naming the action on the confirm', async ({ page, admin }) => {
|
||||
const label = `Temporary ${uniqueSuffix()}`;
|
||||
|
||||
await admin.goto();
|
||||
await page.getByRole('tab', { name: 'Upload links' }).click();
|
||||
await page.getByLabel('Link label').fill(label);
|
||||
await page.getByRole('button', { name: 'Create link' }).click();
|
||||
|
||||
const row = page.getByRole('row', { name: new RegExp(label) });
|
||||
await row.getByRole('button', { name: 'Revoke' }).click();
|
||||
|
||||
// The confirm names what it does rather than saying OK, which is what the
|
||||
// rest of this admin's destructive actions do.
|
||||
await page.getByRole('tooltip').getByRole('button', { name: 'Revoke' }).click();
|
||||
|
||||
await expect(row.getByText('Revoked')).toBeVisible();
|
||||
// The row stays: what arrived through the link is kept, and the record of
|
||||
// where it came from with it.
|
||||
await expect(row).toBeVisible();
|
||||
});
|
||||
|
||||
// Blank-means-unlimited would make the least deliberate action produce the
|
||||
// least bounded link, so unlimited is a checkbox rather than an empty field.
|
||||
test('makes unlimited a deliberate choice', async ({ page, admin }) => {
|
||||
const label = `Always on ${uniqueSuffix()}`;
|
||||
|
||||
await admin.goto();
|
||||
await page.getByRole('tab', { name: 'Upload links' }).click();
|
||||
await page.getByLabel('Link label').fill(label);
|
||||
await page.getByText('No limit').click();
|
||||
await page.getByRole('button', { name: 'Create link' }).click();
|
||||
|
||||
const row = page.getByRole('row', { name: new RegExp(label) });
|
||||
await expect(row).toBeVisible();
|
||||
// A bare count rather than "0 of N".
|
||||
await expect(row.getByText('0 of', { exact: false })).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user