Files
redefined-designs/backend/tests/unit/readId.test.ts
T
bermudalambandClaude Opus 5 be6a2fe5bd fix(admin): answer 404 for an item id that does not exist (#207)
Three routes in admin.ts answered a miss with a success. PUT /items/:id ran an UPDATE that matched nothing, committed happily, selected nothing back and replied 200 with an empty body — a success the admin client could do nothing with, and no record anywhere that the item was not found. mark-sold and mark-available did the same. The create route beside them has always used requireRow for exactly this, which is why this reads as an oversight rather than a decision.

A garbage id was worse in a different direction. Number('abc') is NaN, the driver sends it to Postgres as the text "NaN", Postgres raises 22P02 for an integer column, and the catch turned that into a 500 — so a caller asking for an item that cannot exist was told the server broke. Both now answer 404, because from the caller's side "/items/abc" identifies no item in exactly the way "/items/999999" does.

readId is shared rather than repeated, and rejects zero, negatives and fractions as well as text: every id in this schema is a positive serial, so anything else identifies nothing.

mark-sold now notifies favouriters only after the row is known to exist, so nobody is told about a sale that did not happen.

The issue asked for the same shape to be checked across the other admin routes. It was: unpublish already looks the item up and 404s, and the tags and categories PUT routes both do an existence check before their UPDATE, so their rows[0] is guaranteed. items.ts already guards the public read. These three were the only ones lying about a miss.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 08:44:13 -05:00

22 lines
581 B
TypeScript

import { readId } from '../../src/utils';
describe('readId', () => {
it('reads a positive integer', () => {
expect(readId('7')).toBe(7);
expect(readId('999999')).toBe(999999);
});
// Each of these used to be sent to Postgres as text, raising 22P02 for an
// integer column and surfacing to the caller as a 500 (#207).
it.each(['abc', '', ' ', '1.5', '-1', '0', 'NaN', '1e5abc'])(
'refuses %p',
(value) => {
expect(readId(value)).toBeNull();
}
);
it('refuses a missing param', () => {
expect(readId(undefined)).toBeNull();
});
});