/* RFC-0968: Unit tests for handover authorization logic — signing, verification, file IO, expiry, hash determinism, forgery detection, and bordbuch duplicate rejection. Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only. RFC-0968: initial handover unit tests. */ import { test, expect, describe, beforeEach, afterEach } from "vitest"; import { mkdtempSync, writeFileSync, mkdirSync, rmSync, existsSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { computeAuthorizationHash, signAuthorization, verifyAuthorization, resolveAuthorizationPath, readAuthorization, writeAuthorization, removeAuthorization, isAuthorizationExpired, type HandoverAuthorizationV1, } from "./handover.ts"; import { generateKeyPair, toHex } from "@warpgogol/werkstatt-engine/signing"; import { appendBordbuchEntry, readBordbuch } from "../bordbuch/bordbuch-io.ts"; let tmpDir: string; let werkstattRoot: string; beforeEach(() => { tmpDir = mkdtempSync(join(tmpdir(), "handover-test-")); werkstattRoot = join(tmpDir, "werkstatt"); mkdirSync(werkstattRoot, { recursive: true }); }); afterEach(() => { rmSync(tmpDir, { recursive: true, force: true }); }); function setupSystem(systemId: string): string { const cacheDir = join(tmpDir, "systems-cache", systemId); mkdirSync(cacheDir, { recursive: true }); mkdirSync(join(cacheDir, "missions"), { recursive: true }); mkdirSync(join(cacheDir, "bordbuch"), { recursive: true }); writeFileSync( join(cacheDir, "system-config.yaml"), [ `schemaVersion: system-config/v1`, `id: ${systemId}`, `cosmicStar: Vega`, `mirrors:`, ` - path: ../systems-cache/${systemId}`, ` storageType: non-bare`, ` - path: ../systems-git/${systemId}.git`, ` storageType: bare`, `pinnedPlatform: 6.0.0`, `status: active`, `registeredAt: "2026-01-01T00:00:00Z"`, `notes: ""`, ``, ].join("\n"), ); return cacheDir; } function makePayload(overrides?: Partial): HandoverAuthorizationV1 { const now = new Date(); const expires = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000); return { schema: "handover-authorization/v1", systemId: "test-site", sender: { identity: "human:sender", publicKey: "aabbccdd", }, recipient: { identity: "human:recipient", publicKey: "eeff0011", }, passportHash: "sha256:abcdef", bordbuchHead: "sha256:head123", authorizedAt: now.toISOString(), expiresAt: expires.toISOString(), ...overrides, }; } describe("RFC-0968: handover authorization core logic", () => { test("signAuthorization produces valid signed authorization", async () => { const keyPair = await generateKeyPair(); const payload = makePayload({ sender: { identity: "human:sender", publicKey: toHex(keyPair.publicKey) }, }); const doc = await signAuthorization(payload, keyPair.privateKey); expect(doc.authorizationHash).toMatch(/^sha256:/); expect(doc.signature).toMatch(/^[0-9a-f]+$/); expect(doc.payload).toEqual(payload); }); test("verifyAuthorization succeeds with correct sender public key", async () => { const keyPair = await generateKeyPair(); const payload = makePayload({ sender: { identity: "human:sender", publicKey: toHex(keyPair.publicKey) }, }); const doc = await signAuthorization(payload, keyPair.privateKey); const result = await verifyAuthorization(doc, toHex(keyPair.publicKey)); expect(result.valid).toBe(true); expect(result.errors).toHaveLength(0); }); test("forgery test: tampered authorization payload fails verification", async () => { const keyPair = await generateKeyPair(); const payload = makePayload({ sender: { identity: "human:sender", publicKey: toHex(keyPair.publicKey) }, }); const doc = await signAuthorization(payload, keyPair.privateKey); // Tamper with the payload const tampered: typeof doc = { ...doc, payload: { ...doc.payload, recipient: { identity: "human:attacker", publicKey: "deadbeef" } }, }; const result = await verifyAuthorization(tampered, toHex(keyPair.publicKey)); expect(result.valid).toBe(false); expect(result.errors.length).toBeGreaterThan(0); }); test("verifyAuthorization fails with wrong sender public key", async () => { const keyPair = await generateKeyPair(); const wrongKeyPair = await generateKeyPair(); const payload = makePayload({ sender: { identity: "human:sender", publicKey: toHex(keyPair.publicKey) }, }); const doc = await signAuthorization(payload, keyPair.privateKey); const result = await verifyAuthorization(doc, toHex(wrongKeyPair.publicKey)); expect(result.valid).toBe(false); expect(result.errors.some((e) => e.includes("signature verification failed"))).toBe(true); }); test("authorization hash determinism: same payload produces same hash", () => { const payload1 = makePayload(); // makePayload() calls new Date() per invocation — two calls can straddle a // millisecond boundary, so share the timestamps to get identical payloads. const payload2 = makePayload({ authorizedAt: payload1.authorizedAt, expiresAt: payload1.expiresAt, }); expect(computeAuthorizationHash(payload1)).toBe(computeAuthorizationHash(payload2)); }); test("authorization hash changes when payload changes", () => { const payload1 = makePayload(); const payload2 = makePayload({ systemId: "different-site" }); expect(computeAuthorizationHash(payload1)).not.toBe(computeAuthorizationHash(payload2)); }); test("isAuthorizationExpired returns false for future expiry", () => { const future = new Date(Date.now() + 60_000).toISOString(); expect(isAuthorizationExpired(future)).toBe(false); }); test("isAuthorizationExpired returns true for past expiry", () => { const past = new Date(Date.now() - 60_000).toISOString(); expect(isAuthorizationExpired(past)).toBe(true); }); }); describe("RFC-0968: handover authorization file IO", () => { test("writeAuthorization + readAuthorization round-trip", async () => { const systemId = "test-site"; setupSystem(systemId); const keyPair = await generateKeyPair(); const payload = makePayload({ systemId, sender: { identity: "human:sender", publicKey: toHex(keyPair.publicKey) }, }); const doc = await signAuthorization(payload, keyPair.privateKey); await writeAuthorization(werkstattRoot, systemId, doc); const read = await readAuthorization(werkstattRoot, systemId); expect(read).not.toBeNull(); expect(read!.authorizationHash).toBe(doc.authorizationHash); expect(read!.payload.systemId).toBe(systemId); }); test("readAuthorization returns null when file does not exist", async () => { setupSystem("test-site"); const result = await readAuthorization(werkstattRoot, "test-site"); expect(result).toBeNull(); }); test("removeAuthorization removes the file and returns true", async () => { const systemId = "test-site"; setupSystem(systemId); const keyPair = await generateKeyPair(); const payload = makePayload({ systemId }); const doc = await signAuthorization(payload, keyPair.privateKey); await writeAuthorization(werkstattRoot, systemId, doc); expect(existsSync(resolveAuthorizationPath(werkstattRoot, systemId))).toBe(true); const removed = await removeAuthorization(werkstattRoot, systemId); expect(removed).toBe(true); expect(existsSync(resolveAuthorizationPath(werkstattRoot, systemId))).toBe(false); }); test("removeAuthorization returns false when file does not exist", async () => { setupSystem("test-site"); const removed = await removeAuthorization(werkstattRoot, "test-site"); expect(removed).toBe(false); }); }); describe("RFC-0968: bordbuch duplicate handover rejection", () => { test("appendBordbuchEntry rejects duplicate handover with same authorizationHash", async () => { const systemId = "test-site"; setupSystem(systemId); const metadata = { from: "human:sender", to: "human:recipient", authorizationHash: "sha256:auth123", newPassportHash: "sha256:passport456", }; await appendBordbuchEntry( werkstattRoot, systemId, "handover", "handover from sender to recipient", "human:recipient", { missionId: null, releaseId: null, metadata: metadata as Record }, ); await expect( appendBordbuchEntry( werkstattRoot, systemId, "handover", "handover from sender to recipient", "human:recipient", { missionId: null, releaseId: null, metadata: metadata as Record }, ), ).rejects.toThrow(/duplicate handover/); }); test("appendBordbuchEntry allows handover with different authorizationHash", async () => { const systemId = "test-site"; setupSystem(systemId); const metadata1 = { from: "human:sender", to: "human:recipient", authorizationHash: "sha256:auth1", newPassportHash: "sha256:passport1", }; const metadata2 = { from: "human:recipient", to: "human:third", authorizationHash: "sha256:auth2", newPassportHash: "sha256:passport2", }; await appendBordbuchEntry( werkstattRoot, systemId, "handover", "first handover", "human:recipient", { missionId: null, releaseId: null, metadata: metadata1 as Record }, ); await appendBordbuchEntry( werkstattRoot, systemId, "handover", "second handover", "human:third", { missionId: null, releaseId: null, metadata: metadata2 as Record }, ); const entries = await readBordbuch(werkstattRoot, systemId); expect(entries).toHaveLength(2); expect(entries[0].kind).toBe("handover"); expect(entries[1].kind).toBe("handover"); }); test("bordbuch chain integrity: handover entry has correct previousHash", async () => { const systemId = "test-site"; setupSystem(systemId); await appendBordbuchEntry( werkstattRoot, systemId, "mission-open", "mission opened", "human:operator", { missionId: "test-site-m000001", releaseId: null }, ); const metadata = { from: "human:sender", to: "human:recipient", authorizationHash: "sha256:auth123", newPassportHash: "sha256:passport456", }; const entry = await appendBordbuchEntry( werkstattRoot, systemId, "handover", "handover event", "human:recipient", { missionId: null, releaseId: null, metadata: metadata as Record }, ); const entries = await readBordbuch(werkstattRoot, systemId); expect(entries).toHaveLength(2); expect(entry.previousHash).toBe(entries[0].hash); expect(entry.kind).toBe("handover"); expect((entry.metadata as Record).authorizationHash).toBe("sha256:auth123"); }); });