From cdb399a9c17d6ed8f2cba4babee84a18eed5f64c Mon Sep 17 00:00:00 2001 From: maaz519 Date: Fri, 3 Jul 2026 02:27:20 +0530 Subject: [PATCH] feat(iios): idempotency smoke + P9 slice-6 verification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit smoke-idempotency.mjs: dup POST /v1/support/tickets with the same Idempotency-Key replays the same ticket, same key + different body → 409, and a second tenant reusing the key gets its own ticket (isolation). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../scripts/smoke-idempotency.mjs | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 packages/iios-service/scripts/smoke-idempotency.mjs diff --git a/packages/iios-service/scripts/smoke-idempotency.mjs b/packages/iios-service/scripts/smoke-idempotency.mjs new file mode 100644 index 0000000..bfb5b9a --- /dev/null +++ b/packages/iios-service/scripts/smoke-idempotency.mjs @@ -0,0 +1,64 @@ +// P9 slice-6 idempotency-ledger smoke: a retried command with the same Idempotency-Key +// replays the same ticket (no duplicate side-effect); the same key with a different body +// is a 409; two tenants may reuse the same key independently (KG-02/KG-06). +// Requires the service running with IIOS_DEV_TOKENS=1. +import 'dotenv/config'; + +const SERVICE = process.env.SMOKE_URL ?? 'http://localhost:3200'; +const APP_ID = 'portal-demo'; +const assert = (c, m) => { if (!c) { console.error('✗', m); process.exit(1); } console.log('✓', m); }; + +async function devToken(userId, orgId) { + const r = await fetch(`${SERVICE}/v1/dev/token`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ appId: APP_ID, userId, name: userId, orgId }), + }); + if (!r.ok) throw new Error(`devToken ${r.status} (service with IIOS_DEV_TOKENS=1?)`); + return (await r.json()).token; +} +function post(token, path, body, idempotencyKey) { + return fetch(`${SERVICE}${path}`, { + method: 'POST', + headers: { + 'content-type': 'application/json', + authorization: `Bearer ${token}`, + ...(idempotencyKey ? { 'idempotency-key': idempotencyKey } : {}), + }, + body: JSON.stringify(body), + }); +} +async function json(r, ctx) { + if (!r.ok) throw new Error(`${ctx} ${r.status}: ${await r.text()}`); + return r.json(); +} + +const A = await devToken('idem-a', 'org_idem_A'); +const B = await devToken('idem-b', 'org_idem_B'); +const KEY = 'k-smoke-1'; + +// A creates a ticket under KEY. +const t1 = await json(await post(A, '/v1/support/tickets', { subject: 'X' }, KEY), 'A create'); +assert(t1.id, `A created ticket ${t1.id}`); + +// Same key + same body → the SAME ticket (replay, no re-execution). +const t1again = await json(await post(A, '/v1/support/tickets', { subject: 'X' }, KEY), 'A replay'); +assert(t1again.id === t1.id, `same key + same body replays the same ticket id (${t1again.id})`); + +// Same key + a DIFFERENT body → 409 conflict. +const conflict = await post(A, '/v1/support/tickets', { subject: 'Y' }, KEY); +assert(conflict.status === 409, `same key + different body → 409 (got ${conflict.status})`); + +// Tenant B reuses the SAME key → its own ticket (cross-tenant isolation). +const tB = await json(await post(B, '/v1/support/tickets', { subject: 'X' }, KEY), 'B create'); +assert(tB.id && tB.id !== t1.id, `tenant B's same-key create is independent (${tB.id} ≠ ${t1.id})`); + +// A's ticket list contains t1 exactly once (no duplicate side-effect from the replay/conflict). +const list = await json( + await fetch(`${SERVICE}/v1/support/tickets`, { headers: { authorization: `Bearer ${A}` } }), + 'A list', +); +assert(list.filter((t) => t.id === t1.id).length === 1, 'A sees the ticket exactly once (no duplicate)'); + +console.log('\nP9 idempotency smoke: PASS'); +process.exit(0);