0985b86fa7
Foundation for testing every service. Adds @parking/db/testing — createTestDb() spins a fresh in-memory SQLite and applies the real Drizzle migrations, so server tests run against the production schema with zero live-DB risk. Wires Vitest into apps/server (test script + config; test signing keys via env) and adds the first Phase-1 suites against the anti-fraud core: - signer.test.ts (10): sign/verify round-trip, tamper + forgery rejection, malformed-signature guard, determinism, keyId rotation (buildVerifier). - event-log.test.ts (12): monotonic index, prevHash linkage, payload-in-signature, append serialization, and verifyChain() catching every tamper class — edited payload, deleted row (index gap), broken prevHash, unknown keyId — plus canonicalize byte-stability. Also stops *.test.ts leaking into shipped dist/ (tsconfig exclude in server + shared; shared had been emitting compiled tests all along). server 22/22, shared 87/87 green. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
78 lines
3.0 KiB
TypeScript
78 lines
3.0 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { SoftwareSigner, buildSigner, buildVerifier } from "./signer.js";
|
|
|
|
// The signer is half of the anti-fraud chain (the other half is event-log's hashing).
|
|
// These tests pin: a sign/verify round-trip, rejection of any tamper, constant-time
|
|
// length handling, and the keyId rotation contract that lets one chain span keys.
|
|
|
|
describe("SoftwareSigner", () => {
|
|
it("verifies its own signature (round-trip)", () => {
|
|
const s = new SoftwareSigner("a-test-secret-key");
|
|
const sig = s.sign("hello world");
|
|
expect(s.verify("hello world", sig)).toBe(true);
|
|
});
|
|
|
|
it("rejects a signature over different content (tamper-evidence)", () => {
|
|
const s = new SoftwareSigner("a-test-secret-key");
|
|
const sig = s.sign("amount=100");
|
|
// Flip the signed content — the whole point of signing the payload.
|
|
expect(s.verify("amount=9999", sig)).toBe(false);
|
|
});
|
|
|
|
it("rejects a signature made under a different key (forgery)", () => {
|
|
const real = new SoftwareSigner("the-real-host-key");
|
|
const forger = new SoftwareSigner("an-attacker-guess");
|
|
const forged = forger.sign("amount=100");
|
|
expect(real.verify("amount=100", forged)).toBe(false);
|
|
});
|
|
|
|
it("rejects a malformed / wrong-length signature without throwing", () => {
|
|
const s = new SoftwareSigner("a-test-secret-key");
|
|
// timingSafeEqual throws on length mismatch; verify() must guard it.
|
|
expect(() => s.verify("x", "deadbeef")).not.toThrow();
|
|
expect(s.verify("x", "deadbeef")).toBe(false);
|
|
expect(s.verify("x", "")).toBe(false);
|
|
});
|
|
|
|
it("is deterministic — same key + payload yields the same signature", () => {
|
|
const a = new SoftwareSigner("k").sign("p");
|
|
const b = new SoftwareSigner("k").sign("p");
|
|
expect(a).toBe(b);
|
|
});
|
|
|
|
it("defaults to the v2 keyId", () => {
|
|
expect(new SoftwareSigner("k").keyId).toBe("sw-hmac-v2");
|
|
});
|
|
});
|
|
|
|
describe("buildSigner", () => {
|
|
// vitest.config.ts sets EVENT_SIGNING_KEY + JWT_SECRET for the whole run.
|
|
it("prefers EVENT_SIGNING_KEY (keyId sw-hmac-v2)", () => {
|
|
const s = buildSigner();
|
|
expect(s.keyId).toBe("sw-hmac-v2");
|
|
const sig = s.sign("x");
|
|
expect(s.verify("x", sig)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("buildVerifier (key rotation)", () => {
|
|
it("returns a working verifier for the configured v2 key", () => {
|
|
const v = buildVerifier("sw-hmac-v2");
|
|
expect(v).toBeDefined();
|
|
const signer = new SoftwareSigner(process.env.EVENT_SIGNING_KEY!, "sw-hmac-v2");
|
|
expect(v!.verify("x", signer.sign("x"))).toBe(true);
|
|
});
|
|
|
|
it("resolves the jwtfallback key when present", () => {
|
|
const v = buildVerifier("sw-hmac-jwtfallback");
|
|
expect(v).toBeDefined();
|
|
const signer = new SoftwareSigner(process.env.JWT_SECRET!, "sw-hmac-jwtfallback");
|
|
expect(v!.verify("x", signer.sign("x"))).toBe(true);
|
|
});
|
|
|
|
it("returns undefined for an unknown keyId (key gone, not a false tamper)", () => {
|
|
expect(buildVerifier("atecc608-slot0")).toBeUndefined();
|
|
expect(buildVerifier("nonsense")).toBeUndefined();
|
|
});
|
|
});
|