Skip to content

Commit 1c08403

Browse files
committed
test(runtime): pin both /data doors refusing a version token that can never carry a version
Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU Co-authored-by: Claude <noreply@anthropic.com>
1 parent 589ff57 commit 1c08403

2 files changed

Lines changed: 136 additions & 1 deletion

File tree

‎packages/runtime/src/domains/data-delete-expected-version.test.ts‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,64 @@ describe('[#23265] the two doors refuse a stale token and a repeated one with th
314314
}, 60_000);
315315
});
316316

317+
// ── A token that can never carry a version ────────────────────────────────────
318+
319+
/**
320+
* [#23492] Both doors read `?expectedVersion ?? If-Match` and used to forward
321+
* a token only when it was truthy, so `?expectedVersion=` (empty) hid a stale
322+
* `If-Match` and the delete was APPLIED, `200`, on both doors (measured on
323+
* this rig before the fix, the row read back gone). Now a token that can never
324+
* carry a version is refused with the protocol's own answer to the
325+
* quoted-empty tag `""`, `400 VALIDATION_FAILED`, before `deleteData` is
326+
* asked; maintainer decision #20 ① rules that shape refused, neither read as
327+
* "no token" nor answered `409`. `data-update-expected-version.test.ts` pins
328+
* the body spellings on `PATCH`.
329+
*/
330+
const UNUSABLE: ReadonlyArray<readonly [string, DeleteOptions]> = [
331+
['an empty ?expectedVersion beside a stale If-Match', { query: { expectedVersion: '' }, headers: { 'If-Match': STALE } }],
332+
['an empty ?expectedVersion and no If-Match', { query: { expectedVersion: '' } }],
333+
['a blank ?expectedVersion beside a stale If-Match', { query: { expectedVersion: ' ' }, headers: { 'If-Match': STALE } }],
334+
['an empty If-Match and no ?expectedVersion', { headers: { 'If-Match': '' } }],
335+
];
336+
337+
const refused = (a: Answer) =>
338+
expect({ status: a.status, code: a.code }, JSON.stringify(a)).toEqual({ status: 400, code: 'VALIDATION_FAILED' });
339+
340+
for (const [doorName, boot] of DOORS) {
341+
describe(`[#23492] ${doorName}: DELETE /data/:object/:id refuses a token that can never carry a version`, () => {
342+
for (const [shape, opts] of UNUSABLE) {
343+
it(`${shape}: 400 VALIDATION_FAILED, deleteData never asked, the row kept`, async () => {
344+
const door = await boot();
345+
refused(await door.delete(opts));
346+
expect(door.forwarded()).toEqual([]);
347+
expect(await door.present()).toBe(true);
348+
}, 60_000);
349+
}
350+
351+
it('the refusal is the protocol\'s own answer to the quoted-empty tag: the same status and code', async () => {
352+
const quoted = await boot();
353+
const protocolAnswer = await quoted.delete({ query: { expectedVersion: '""' } });
354+
// The protocol, not the door, refused this one: the token reached it.
355+
expect(quoted.forwarded()).toEqual(['""']);
356+
const empty = await boot();
357+
const doorAnswer = await empty.delete({ query: { expectedVersion: '' } });
358+
expect({ status: doorAnswer.status, code: doorAnswer.code })
359+
.toEqual({ status: protocolAnswer.status, code: protocolAnswer.code });
360+
}, 60_000);
361+
});
362+
}
363+
364+
describe('[#23492] the two doors refuse each token that can never carry a version alike', () => {
365+
for (const [shape, opts] of UNUSABLE) {
366+
it(`${shape}: the same status, code and sentence`, async () => {
367+
const answers: Answer[] = [];
368+
for (const [, boot] of DOORS) answers.push(await (await boot()).delete(opts));
369+
const [rest, catchAllAnswer] = answers;
370+
expect(catchAllAnswer).toEqual(rest);
371+
}, 60_000);
372+
}
373+
});
374+
317375
// ── The dispatcher's fallback deleter ─────────────────────────────────────────
318376

319377
/**

‎packages/runtime/src/domains/data-update-expected-version.test.ts‎

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,8 @@ for (const [doorName, boot] of DOORS) {
294294
* row. What either door does with them is `RestServer`'s reading, not this
295295
* file's: a token is the body key when it is not nullish (else `If-Match`),
296296
* the key is stripped from the patch whatever its value, and a token is
297-
* forwarded as its string only when it is truthy.
297+
* forwarded as its string unless it can never carry a version, which is
298+
* refused (#23492, pinned in its own block below).
298299
*/
299300
const EDGE_SHAPES: ReadonlyArray<readonly [string, (version: string) => PatchOptions]> = [
300301
['a stale token', () => ({ body: { name: AFTER, expectedVersion: STALE } })],
@@ -334,6 +335,82 @@ describe('[#23462] the two doors answer each token shape with the same status, c
334335
}, 60_000);
335336
});
336337

338+
// ── A token that can never carry a version ────────────────────────────────────
339+
340+
/**
341+
* [#23492] Both doors read `bodyVersion ?? If-Match` and used to forward a
342+
* token only when it was truthy. `''` and `false` are not nullish, so the
343+
* stale `If-Match` beside them was never read, and not truthy, so no token was
344+
* forwarded: the update was APPLIED, `200`, on both doors (measured on this
345+
* rig before the fix, the row read back as written). Now a token in a token
346+
* position that can never carry a version is refused with the protocol's own
347+
* answer to the quoted-empty tag `""`, `400 VALIDATION_FAILED`, before
348+
* `updateData` is asked; maintainer decision #20 ① rules that shape refused,
349+
* neither read as "no token" nor answered `409`.
350+
*/
351+
const UNUSABLE: ReadonlyArray<readonly [string, PatchOptions]> = [
352+
["a body expectedVersion of '' beside a stale If-Match", { body: { name: AFTER, expectedVersion: '' }, headers: { 'If-Match': STALE } }],
353+
["a body expectedVersion of '' and no If-Match", { body: { name: AFTER, expectedVersion: '' } }],
354+
['a body expectedVersion of false beside a stale If-Match', { body: { name: AFTER, expectedVersion: false }, headers: { 'If-Match': STALE } }],
355+
['a body expectedVersion of false and no If-Match', { body: { name: AFTER, expectedVersion: false } }],
356+
['a blank body expectedVersion beside a stale If-Match', { body: { name: AFTER, expectedVersion: ' ' }, headers: { 'If-Match': STALE } }],
357+
['a body expectedVersion of true', { body: { name: AFTER, expectedVersion: true } }],
358+
['a body expectedVersion that is an array, beside a stale If-Match', { body: { name: AFTER, expectedVersion: [] }, headers: { 'If-Match': STALE } }],
359+
['an empty If-Match and no body token', { body: { name: AFTER }, headers: { 'If-Match': '' } }],
360+
];
361+
362+
const refused = (a: Answer) =>
363+
expect({ status: a.status, code: a.code }, JSON.stringify(a)).toEqual({ status: 400, code: 'VALIDATION_FAILED' });
364+
365+
for (const [doorName, boot] of DOORS) {
366+
describe(`[#23492] ${doorName}: PATCH /data/:object/:id refuses a token that can never carry a version`, () => {
367+
for (const [shape, opts] of UNUSABLE) {
368+
it(`${shape}: 400 VALIDATION_FAILED, updateData never asked, the row unchanged`, async () => {
369+
const door = await boot();
370+
refused(await door.patch(opts));
371+
expect(door.forwarded()).toEqual([]);
372+
expect(await door.name()).toBe(BEFORE);
373+
}, 60_000);
374+
}
375+
376+
it('the refusal is the protocol\'s own answer to the quoted-empty tag: the same status and code', async () => {
377+
const quoted = await boot();
378+
const protocolAnswer = await quoted.patch({ body: { name: AFTER, expectedVersion: '""' } });
379+
// The protocol, not the door, refused this one: the token reached it.
380+
expect(quoted.forwarded().map((f) => f.expectedVersion)).toEqual(['""']);
381+
const empty = await boot();
382+
const doorAnswer = await empty.patch({ body: { name: AFTER, expectedVersion: '' } });
383+
expect({ status: doorAnswer.status, code: doorAnswer.code })
384+
.toEqual({ status: protocolAnswer.status, code: protocolAnswer.code });
385+
}, 60_000);
386+
387+
it('control: a null body token is no token, so a stale If-Match beside it is judged and refused 409', async () => {
388+
const door = await boot();
389+
conflict(await door.patch({ body: { name: AFTER, expectedVersion: null }, headers: { 'If-Match': STALE } }));
390+
expect(door.forwarded().map((f) => f.expectedVersion)).toEqual([STALE]);
391+
expect(await door.name()).toBe(BEFORE);
392+
}, 60_000);
393+
394+
it('control: a body token of 0 is a number, judged as "0" (refused 409), never dropped', async () => {
395+
const door = await boot();
396+
conflict(await door.patch({ body: { name: AFTER, expectedVersion: 0 }, headers: { 'If-Match': door.version } }));
397+
expect(door.forwarded()).toEqual([{ expectedVersion: '0', data: { name: AFTER } }]);
398+
expect(await door.name()).toBe(BEFORE);
399+
}, 60_000);
400+
});
401+
}
402+
403+
describe('[#23492] the two doors refuse each token that can never carry a version alike', () => {
404+
for (const [shape, opts] of UNUSABLE) {
405+
it(`${shape}: the same status, code and sentence`, async () => {
406+
const answers: Answer[] = [];
407+
for (const [, boot] of DOORS) answers.push(await (await boot()).patch(opts));
408+
const [rest, catchAllAnswer] = answers;
409+
expect(catchAllAnswer).toEqual(rest);
410+
}, 60_000);
411+
}
412+
});
413+
337414
// ── The dispatcher's fallback updater ─────────────────────────────────────────
338415

339416
/**

0 commit comments

Comments
 (0)