import { PrivateKey } from "@bsv/sdk"; import { getAuthToken } from "bitcoin-auth"; import { Hono } from "hono"; import { afterAll, beforeAll, describe, expect, test, vi } from "vitest"; import { getOwnerPubkey, sigmaAuth } from "../../src/middleware/sigma-auth"; describe("Sigma Auth Middleware", () => { let ownerWif: string; let otherWif: string; let app: Hono; beforeAll(() => { // Generate test WIFs ownerWif = PrivateKey.fromRandom().toWif(); otherWif = PrivateKey.fromRandom().toWif(); // Set owner WIF in env process.env.SIGMA_MEMBER_WIF = ownerWif; // Create test app with middleware app = new Hono(); app.use("/protected/*", sigmaAuth); app.get("/protected/test", (c) => c.json({ message: "success" })); app.post("/protected/test", async (c) => { const body = await c.req.json(); return c.json({ message: "success", received: body }); }); }); afterAll(() => { // Clean up env delete process.env.SIGMA_MEMBER_WIF; }); test("getOwnerPubkey derives and caches public key", () => { const pubkey1 = getOwnerPubkey(); const pubkey2 = getOwnerPubkey(); // Should return same instance (cached) expect(pubkey1).toBe(pubkey2); // Should match expected public key const expectedPubkey = PrivateKey.fromWif(ownerWif) .toPublicKey() .toString(); expect(pubkey1).toBe(expectedPubkey); }); test("allows valid owner token without body", async () => { const requestPath = "/protected/test"; const token = getAuthToken({ privateKeyWif: ownerWif, requestPath, }); const response = await app.request(`http://localhost${requestPath}`, { headers: { Authorization: `Bearer ${token}`, }, }); expect(response.status).toBe(200); const data = await response.json(); expect(data).toEqual({ message: "success" }); }); test("allows valid owner token with body", async () => { const requestPath = "/protected/test"; const body = JSON.stringify({ key: "value" }); const token = getAuthToken({ privateKeyWif: ownerWif, requestPath, body, }); const response = await app.request(`http://localhost${requestPath}`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body, }); expect(response.status).toBe(200); const data = await response.json(); expect(data.message).toBe("success"); expect(data.received).toEqual({ key: "value" }); }); test("rejects missing Authorization header", async () => { const requestPath = "/protected/test"; const response = await app.request(`http://localhost${requestPath}`); expect(response.status).toBe(401); const data = await response.json(); expect(data.error).toBe("Missing or invalid Authorization header"); }); test("rejects malformed token", async () => { const requestPath = "/protected/test"; const response = await app.request(`http://localhost${requestPath}`, { headers: { Authorization: "Bearer random-invalid-string", }, }); expect(response.status).toBe(401); const data = await response.json(); expect(data.error).toBe("Invalid token format"); }); test("rejects valid token from wrong identity", async () => { const requestPath = "/protected/test"; const token = getAuthToken({ privateKeyWif: otherWif, // Different identity requestPath, }); const response = await app.request(`http://localhost${requestPath}`, { headers: { Authorization: `Bearer ${token}`, }, }); expect(response.status).toBe(403); const data = await response.json(); expect(data.error).toBe("Forbidden: Identity mismatch"); }); test("rejects expired token", async () => { const requestPath = "/protected/test"; // Create token with timestamp 10 minutes in the past const oldTimestamp = new Date(Date.now() - 10 * 60 * 1000).toISOString(); const token = getAuthToken({ privateKeyWif: ownerWif, requestPath, timestamp: oldTimestamp, }); const response = await app.request(`http://localhost${requestPath}`, { headers: { Authorization: `Bearer ${token}`, }, }); expect(response.status).toBe(401); const data = await response.json(); expect(data.error).toBe("Invalid or expired token"); }); test("returns 500 when parseAuthToken throws", async () => { const parseSpy = vi.spyOn(await import("bitcoin-auth"), "parseAuthToken"); parseSpy.mockImplementation(() => { throw new Error("parse explosion"); }); const requestPath = "/protected/test"; const response = await app.request(`http://localhost${requestPath}`, { headers: { Authorization: "Bearer some-token-value", }, }); expect(response.status).toBe(500); const data = await response.json(); expect(data.error).toBe("Authentication processing error"); parseSpy.mockRestore(); }); test("returns 500 when verifyAuthToken throws", async () => { const verifySpy = vi.spyOn(await import("bitcoin-auth"), "verifyAuthToken"); verifySpy.mockImplementation(() => { throw new Error("verify explosion"); }); const requestPath = "/protected/test"; const token = getAuthToken({ privateKeyWif: ownerWif, requestPath, }); const response = await app.request(`http://localhost${requestPath}`, { headers: { Authorization: `Bearer ${token}`, }, }); expect(response.status).toBe(500); const data = await response.json(); expect(data.error).toBe("Authentication processing error"); verifySpy.mockRestore(); }); });