fix: reject invalid keyed mutation intents

This commit is contained in:
DongHyeonka
2026-08-02 01:21:50 +09:00
parent cbcc7b5ed7
commit fa2f699125
3 changed files with 272 additions and 8 deletions
+88 -5
View File
@@ -22,15 +22,19 @@ const createInstalled: InstalledHttpContract<unknown, unknown, unknown> =
TEST_CREATE_HTTP_CONTRACT;
function mutationIntent(
overrides: Readonly<{ intentId?: string; idempotencyKey?: string }> = {},
overrides: Readonly<{
intentId?: string;
operationId?: string;
idempotencyKey?: string | null;
}> = {},
) {
return Object.freeze({
intentId: overrides.intentId ?? "intent-1",
operationId: "TEST_CREATE_ENTITY",
operationId: overrides.operationId ?? "TEST_CREATE_ENTITY",
canonicalInputIdentity: "opaque-input-identity",
...(overrides.idempotencyKey === undefined
? { idempotencyKey: "key-1" }
: { idempotencyKey: overrides.idempotencyKey }),
...(overrides.idempotencyKey === null
? {}
: { idempotencyKey: overrides.idempotencyKey ?? "key-1" }),
createdAtMonotonicMs: 1,
});
}
@@ -61,6 +65,85 @@ async function flushMicrotasks(): Promise<void> {
}
describe("descriptor-driven HTTP execution lifetime", () => {
it.each([
["absent intent", () => undefined],
["empty key", () => mutationIntent({ idempotencyKey: "" })],
["control-character key", () => mutationIntent({ idempotencyKey: "key\u0000private" })],
["over-budget key", () => mutationIntent({ idempotencyKey: "k".repeat(257) })],
[
"wrong operation",
() => mutationIntent({ operationId: "TEST_OTHER_COMMAND" }),
],
])(
"rejects a KEYED command with %s before credentials or fetch",
async (_label, intentFactory) => {
const attachCredentials = vi.fn(() => ({
kind: "READY" as const,
headers: {},
credentials: "omit" as const,
}));
const fetcher = vi.fn();
const executor = createContractHttpExecutor({
baseUrl: "https://api.example/",
maxRetryAttempts: 0,
attachCredentials,
fetcher,
});
const intent = intentFactory();
await expect(
executor.execute(
createInstalled,
{ name: "created" },
{ scope, ...(intent === undefined ? {} : { intent }) },
),
).resolves.toMatchObject({
kind: "CONTRACT_VIOLATION",
violation: {
kind: "MISSING_IDEMPOTENCY_KEY",
operation: "REQUEST",
},
effect: "NOT_STARTED",
});
expect(attachCredentials).not.toHaveBeenCalled();
expect(fetcher).not.toHaveBeenCalled();
},
);
it.each([
["an intent without a key", mutationIntent({ idempotencyKey: null })],
["an intent with a key", mutationIntent()],
])(
"rejects a query carrying %s before credentials or fetch",
async (_label, intent) => {
const attachCredentials = vi.fn(() => ({
kind: "READY" as const,
headers: {},
credentials: "omit" as const,
}));
const fetcher = vi.fn();
const executor = createContractHttpExecutor({
baseUrl: "https://api.example/",
maxRetryAttempts: 0,
attachCredentials,
fetcher,
});
await expect(
executor.execute(installed, { limit: 20 }, { scope, intent }),
).resolves.toMatchObject({
kind: "CONTRACT_VIOLATION",
violation: {
kind: "UNEXPECTED_IDEMPOTENCY_KEY",
operation: "REQUEST",
},
effect: "NOT_STARTED",
});
expect(attachCredentials).not.toHaveBeenCalled();
expect(fetcher).not.toHaveBeenCalled();
},
);
it("reuses one supplied idempotency key across every physical retry", async () => {
const observedKeys: Array<string | null> = [];
let attempt = 0;