rea/tests/replayMachine.test.ts

245 lines
6.9 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { replayMachineSchema } from "../src/domain/replayMachine.js";
const loginMachine = () => ({
initial_state: "waiting_for_login",
states: [
{ name: "waiting_for_login" },
{ name: "waiting_for_api" },
{ name: "complete", terminal: true },
],
transitions: [
{
id: "login",
from: "waiting_for_login",
to: "waiting_for_api",
priority: 10,
trigger: { protocol: "http", method: "POST", path: "/login" },
captures: [
{
variable: "token",
value: { source: "request_json", path: ["token"] },
sensitive: true,
},
],
actions: [{ type: "http_response", status: 204, headers: {}, body: "" }],
max_uses: 1,
},
{
id: "api",
from: "waiting_for_api",
to: "complete",
priority: 10,
trigger: { protocol: "http", method: "GET", path: "/api" },
guards: [
{
variable: "token",
value: { source: "request_header", name: "authorization" },
},
],
actions: [
{
type: "http_response",
status: 200,
headers: {},
body: "done",
},
],
max_uses: 1,
},
],
max_transitions: 2,
});
describe("replay machine", () => {
it("accepts a bounded, reachable, data-only login machine", () => {
expect(replayMachineSchema.parse(loginMachine())).toMatchObject({
initial_state: "waiting_for_login",
max_transitions: 2,
});
});
it.each([
[
"unreachable state",
(machine: ReturnType<typeof loginMachine>) =>
machine.states.push({ name: "orphan", terminal: false }),
"unreachable",
],
[
"ambiguous transition",
(machine: ReturnType<typeof loginMachine>) => {
const transition = machine.transitions[0];
if (transition === undefined) throw new TypeError("missing transition");
machine.transitions.push({ ...transition, id: "login_2" });
},
"ambiguous",
],
[
"outgoing terminal transition",
(machine: ReturnType<typeof loginMachine>) => {
const transition = machine.transitions[0];
if (transition === undefined) throw new TypeError("missing transition");
machine.transitions.push({
...transition,
id: "restart",
from: "complete",
to: "waiting_for_login",
trigger: { protocol: "http", method: "POST", path: "/restart" },
});
},
"terminal",
],
[
"unknown guard variable",
(machine: ReturnType<typeof loginMachine>) => {
const guard = machine.transitions[1]?.guards?.[0];
if (guard === undefined) throw new TypeError("missing guard");
guard.variable = "missing";
},
"executable path before use",
],
[
"missing terminal state",
(machine: ReturnType<typeof loginMachine>) => {
const state = machine.states[2];
if (state === undefined) throw new TypeError("missing state");
state.terminal = false;
},
"terminal state",
],
])("rejects %s", (_name, mutate, message) => {
const machine = loginMachine();
mutate(machine);
expect(() => replayMachineSchema.parse(machine)).toThrow(message);
});
it("canonicalizes methods and headers while refusing persisted credential matchers", () => {
const canonical = {
...loginMachine(),
transitions: loginMachine().transitions.map((transition, index) =>
index === 0
? {
...transition,
trigger: {
protocol: "http",
method: "post",
path: "/login",
headers: { "X-Request-ID": "fixture" },
},
}
: transition,
),
};
expect(
replayMachineSchema.parse(canonical).transitions[0]?.trigger,
).toMatchObject({
method: "POST",
headers: { "x-request-id": "fixture" },
});
const credentialMatcher = {
...loginMachine(),
transitions: loginMachine().transitions.map((transition, index) =>
index === 0
? {
...transition,
trigger: {
protocol: "http",
method: "POST",
path: "/login",
headers: { Authorization: "test-auth-token" },
},
}
: transition,
),
};
expect(() => replayMachineSchema.parse(credentialMatcher)).toThrow(
/secret alias/u,
);
});
it("rejects unknown fields at nested machine boundaries", () => {
const candidate = loginMachine();
const action = candidate.transitions[0]?.actions[0];
if (action === undefined) throw new TypeError("missing action");
expect(() =>
replayMachineSchema.parse({
...candidate,
transitions: [
{
...candidate.transitions[0],
actions: [{ ...action, unexpected: true }],
},
candidate.transitions[1],
],
}),
).toThrow(/Unrecognized key/u);
});
it("reports duplicate transition IDs as structured validation", () => {
const candidate = loginMachine();
const first = candidate.transitions[0];
const second = candidate.transitions[1];
if (first === undefined || second === undefined)
throw new TypeError("missing transition");
expect(
replayMachineSchema.safeParse({
...candidate,
transitions: [first, { ...second, id: first.id }],
}),
).toMatchObject({ success: false });
});
it("rejects guards whose capture is not available before the transition", () => {
const candidate = loginMachine();
const first = candidate.transitions[0];
if (first === undefined) throw new TypeError("missing transition");
first.guards = [
{
variable: "token",
value: { source: "request_header", name: "authorization" },
},
];
expect(() => replayMachineSchema.parse(candidate)).toThrow(
/executable path before use/u,
);
});
it("rejects ambiguous header triggers regardless of key order", () => {
const candidate = loginMachine();
const first = candidate.transitions[0];
if (first === undefined) throw new TypeError("missing transition");
const withHeaders = {
...first,
trigger: {
protocol: "http" as const,
method: "POST",
path: "/login",
headers: { accept: "json", "x-mode": "test" },
},
};
expect(() =>
replayMachineSchema.parse({
...candidate,
transitions: [
withHeaders,
{
...withHeaders,
id: "login_duplicate",
trigger: {
...withHeaders.trigger,
headers: { "x-mode": "test", accept: "json" },
},
},
candidate.transitions[1],
],
}),
).toThrow(/ambiguous/u);
});
});