test(server): add fresh-SQLite test harness + anti-fraud core suites

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
This commit is contained in:
2026-06-21 15:20:38 +02:00
parent 3ed785c33e
commit 0985b86fa7
9 changed files with 268 additions and 4 deletions
+77
View File
@@ -0,0 +1,77 @@
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();
});
});