Some checks failed
CI / Frontend Lint (push) Has been cancelled
CI / Frontend Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Frontend E2E Tests (push) Has been cancelled
CI / Orchestrator Build (push) Has been cancelled
CI / Contracts Compile (push) Has been cancelled
CI / Contracts Test (push) Has been cancelled
Security Scan / Dependency Vulnerability Scan (push) Has been cancelled
Security Scan / OWASP ZAP Scan (push) Has been cancelled
150 lines
5.2 KiB
TypeScript
150 lines
5.2 KiB
TypeScript
import { describe, it, expect, beforeEach, jest } from "@jest/globals";
|
|
|
|
type Row = {
|
|
id: string;
|
|
plan_id: string;
|
|
type: string;
|
|
actor: string | null;
|
|
payload: Record<string, unknown>;
|
|
payload_hash: string;
|
|
prev_hash: string | null;
|
|
signature: string;
|
|
created_at: string;
|
|
};
|
|
|
|
const rows: Row[] = [];
|
|
let idSeq = 0;
|
|
|
|
jest.mock("../../src/db/postgres", () => ({
|
|
query: async (sql: string, params: unknown[] = []) => {
|
|
if (sql.startsWith("SELECT signature")) {
|
|
const planId = params[0] as string;
|
|
const matches = rows.filter((r) => r.plan_id === planId);
|
|
if (matches.length === 0) return [];
|
|
return [{ signature: matches[matches.length - 1].signature }];
|
|
}
|
|
if (sql.startsWith("INSERT INTO events")) {
|
|
const [plan_id, type, actor, payloadJson, payload_hash, prev_hash, signature] =
|
|
params as [string, string, string | null, string, string, string | null, string];
|
|
const rec: Row = {
|
|
id: `evt-${++idSeq}`,
|
|
plan_id,
|
|
type,
|
|
actor,
|
|
payload: JSON.parse(payloadJson),
|
|
payload_hash,
|
|
prev_hash,
|
|
signature,
|
|
created_at: new Date(Date.now() + idSeq).toISOString(),
|
|
};
|
|
rows.push(rec);
|
|
return [rec];
|
|
}
|
|
if (sql.startsWith("SELECT id, plan_id")) {
|
|
const planId = params[0] as string;
|
|
return rows.filter((r) => r.plan_id === planId);
|
|
}
|
|
return [];
|
|
},
|
|
}));
|
|
|
|
import { publish, getEventsForPlan, verifyChain, EVENT_TYPES } from "../../src/services/eventBus";
|
|
|
|
describe("Event Bus", () => {
|
|
beforeEach(() => {
|
|
rows.length = 0;
|
|
idSeq = 0;
|
|
});
|
|
|
|
it("EVENT_TYPES covers all arch §7.2 categories", () => {
|
|
expect(EVENT_TYPES).toContain("transaction.created");
|
|
expect(EVENT_TYPES).toContain("transaction.committed");
|
|
expect(EVENT_TYPES).toContain("transaction.aborted");
|
|
expect(EVENT_TYPES).toContain("payment.settled");
|
|
expect(EVENT_TYPES).toContain("instrument.dispatched");
|
|
expect(EVENT_TYPES.length).toBe(15);
|
|
});
|
|
|
|
it("publish persists with payload_hash, prev_hash=null, and signature", async () => {
|
|
const rec = await publish({
|
|
planId: "p-1",
|
|
type: "transaction.created",
|
|
actor: "coordinator",
|
|
payload: { foo: 1 },
|
|
});
|
|
expect(rec.id).toMatch(/evt-/);
|
|
expect(rec.prev_hash).toBeNull();
|
|
expect(rec.payload_hash).toMatch(/^[0-9a-f]{64}$/);
|
|
expect(rec.signature).toMatch(/^[0-9a-f]{64}$/);
|
|
expect(rec.payload).toEqual({ foo: 1 });
|
|
});
|
|
|
|
it("prev_hash chains consecutive events for the same plan", async () => {
|
|
const a = await publish({ planId: "p-1", type: "transaction.created" });
|
|
const b = await publish({ planId: "p-1", type: "participants.authorized" });
|
|
const c = await publish({ planId: "p-1", type: "preconditions.satisfied" });
|
|
expect(a.prev_hash).toBeNull();
|
|
expect(b.prev_hash).toBe(a.signature);
|
|
expect(c.prev_hash).toBe(b.signature);
|
|
});
|
|
|
|
it("events are isolated per plan_id", async () => {
|
|
const a1 = await publish({ planId: "p-1", type: "transaction.created" });
|
|
const b1 = await publish({ planId: "p-2", type: "transaction.created" });
|
|
expect(a1.prev_hash).toBeNull();
|
|
expect(b1.prev_hash).toBeNull();
|
|
});
|
|
|
|
it("verifyChain returns ok for an untampered chain", async () => {
|
|
await publish({ planId: "p-1", type: "transaction.created" });
|
|
await publish({ planId: "p-1", type: "transaction.prepared" });
|
|
await publish({ planId: "p-1", type: "transaction.committed" });
|
|
const result = await verifyChain("p-1");
|
|
expect(result.ok).toBe(true);
|
|
});
|
|
|
|
it("verifyChain detects payload tampering", async () => {
|
|
await publish({ planId: "p-1", type: "transaction.created", payload: { amount: 100 } });
|
|
await publish({ planId: "p-1", type: "transaction.committed" });
|
|
rows[0].payload = { amount: 999_999 }; // tamper
|
|
const result = await verifyChain("p-1");
|
|
expect(result.ok).toBe(false);
|
|
if (!result.ok) {
|
|
expect(result.brokenAt).toBe(0);
|
|
expect(result.reason).toBe("payload_hash mismatch");
|
|
}
|
|
});
|
|
|
|
it("verifyChain detects signature tampering", async () => {
|
|
await publish({ planId: "p-1", type: "transaction.created" });
|
|
await publish({ planId: "p-1", type: "transaction.committed" });
|
|
rows[1].signature = "0".repeat(64); // tamper
|
|
const result = await verifyChain("p-1");
|
|
expect(result.ok).toBe(false);
|
|
if (!result.ok) {
|
|
expect(result.brokenAt).toBe(1);
|
|
}
|
|
});
|
|
|
|
it("verifyChain detects broken prev_hash link", async () => {
|
|
await publish({ planId: "p-1", type: "transaction.created" });
|
|
await publish({ planId: "p-1", type: "transaction.committed" });
|
|
rows[1].prev_hash = "0".repeat(64);
|
|
const result = await verifyChain("p-1");
|
|
expect(result.ok).toBe(false);
|
|
if (!result.ok) {
|
|
expect(result.reason).toBe("prev_hash mismatch");
|
|
}
|
|
});
|
|
|
|
it("getEventsForPlan returns events in chronological order", async () => {
|
|
await publish({ planId: "p-1", type: "transaction.created" });
|
|
await publish({ planId: "p-1", type: "transaction.prepared" });
|
|
const events = await getEventsForPlan("p-1");
|
|
expect(events.map((e) => e.type)).toEqual([
|
|
"transaction.created",
|
|
"transaction.prepared",
|
|
]);
|
|
});
|
|
});
|