fix: reject invalid keyed mutation intents
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user