import { expect, test } from "bun:test"; import { createAuthHeader, generateSecret, signMessage, verifyAuthHeader, verifySignature, } from "./auth"; // Secret generation tests test("generateSecret creates a 32-byte base64 secret", () => { const secret = generateSecret(); // Base64 encoding of 32 bytes should be 44 characters (with padding) expect(secret.length).toBe(44); // Should be valid base64 const decoded = Buffer.from(secret, "base64"); expect(decoded.length).toBe(32); }); test("generateSecret creates unique secrets", () => { const secret1 = generateSecret(); const secret2 = generateSecret(); const secret3 = generateSecret(); expect(secret1).not.toBe(secret2); expect(secret2).not.toBe(secret3); expect(secret1).not.toBe(secret3); }); // Sign/verify tests test("signMessage creates a signature", async () => { const secret = generateSecret(); const message = "test message"; const signature = await signMessage(secret, message); expect(signature).toBeTruthy(); expect(signature.length).toBeGreaterThan(0); // HMAC-SHA256 produces 32 bytes, base64 encoded is 44 chars expect(signature.length).toBe(44); }); test("verifySignature succeeds with correct secret and message", async () => { const secret = generateSecret(); const message = "test message"; const signature = await signMessage(secret, message); const valid = await verifySignature(secret, message, signature); expect(valid).toBe(true); }); test("verifySignature fails with wrong secret", async () => { const secret1 = generateSecret(); const secret2 = generateSecret(); const message = "test message"; const signature = await signMessage(secret1, message); const valid = await verifySignature(secret2, message, signature); expect(valid).toBe(false); }); test("verifySignature fails with wrong message", async () => { const secret = generateSecret(); const message1 = "test message"; const message2 = "different message"; const signature = await signMessage(secret, message1); const valid = await verifySignature(secret, message2, signature); expect(valid).toBe(false); }); test("verifySignature fails with tampered signature", async () => { const secret = generateSecret(); const message = "test message"; const signature = await signMessage(secret, message); // Tamper with the signature const tamperedSignature = `${signature.slice(0, -1)}X`; const valid = await verifySignature(secret, message, tamperedSignature); expect(valid).toBe(false); }); test("verifySignature fails with invalid base64 signature", async () => { const secret = generateSecret(); const message = "test message"; const valid = await verifySignature(secret, message, "not-valid-base64!!!"); expect(valid).toBe(false); }); // Auth header tests test("createAuthHeader creates header with timestamp and signature", async () => { const secret = generateSecret(); const timestamp = Date.now(); const header = await createAuthHeader(secret, timestamp); const parts = header.split(":"); expect(parts.length).toBe(2); expect(parts[0]).toBe(timestamp.toString()); expect(parts[1]?.length).toBe(44); // HMAC-SHA256 base64 length }); test("verifyAuthHeader succeeds with valid header", async () => { const secret = generateSecret(); const timestamp = Date.now(); const header = await createAuthHeader(secret, timestamp); const valid = await verifyAuthHeader(secret, header); expect(valid).toBe(true); }); test("verifyAuthHeader fails with wrong secret", async () => { const secret1 = generateSecret(); const secret2 = generateSecret(); const timestamp = Date.now(); const header = await createAuthHeader(secret1, timestamp); const valid = await verifyAuthHeader(secret2, header); expect(valid).toBe(false); }); test("verifyAuthHeader fails with tampered timestamp", async () => { const secret = generateSecret(); const timestamp = Date.now(); const header = await createAuthHeader(secret, timestamp); // Tamper with timestamp const parts = header.split(":"); const tamperedHeader = `${Number.parseInt(parts[0] || "0", 10) + 1000}:${parts[1]}`; const valid = await verifyAuthHeader(secret, tamperedHeader); expect(valid).toBe(false); }); test("verifyAuthHeader fails with tampered signature", async () => { const secret = generateSecret(); const timestamp = Date.now(); const header = await createAuthHeader(secret, timestamp); // Tamper with signature const parts = header.split(":"); const tamperedHeader = `${parts[0]}:${parts[1]?.slice(0, -1)}X`; const valid = await verifyAuthHeader(secret, tamperedHeader); expect(valid).toBe(false); }); test("verifyAuthHeader fails with expired timestamp", async () => { const secret = generateSecret(); // Timestamp from 10 minutes ago const timestamp = Date.now() - 10 * 60 * 1000; const header = await createAuthHeader(secret, timestamp); // Default maxAge is 5 minutes, so this should fail const valid = await verifyAuthHeader(secret, header); expect(valid).toBe(false); }); test("verifyAuthHeader succeeds with timestamp within maxAge", async () => { const secret = generateSecret(); // Timestamp from 3 minutes ago const timestamp = Date.now() - 3 * 60 * 1000; const header = await createAuthHeader(secret, timestamp); // Default maxAge is 5 minutes, so this should succeed const valid = await verifyAuthHeader(secret, header); expect(valid).toBe(true); }); test("verifyAuthHeader respects custom maxAge", async () => { const secret = generateSecret(); // Timestamp from 2 minutes ago const timestamp = Date.now() - 2 * 60 * 1000; const header = await createAuthHeader(secret, timestamp); // Should fail with 1 minute maxAge const valid1 = await verifyAuthHeader(secret, header, 1 * 60 * 1000); expect(valid1).toBe(false); // Should succeed with 3 minute maxAge const valid2 = await verifyAuthHeader(secret, header, 3 * 60 * 1000); expect(valid2).toBe(true); }); test("verifyAuthHeader rejects future timestamps beyond clock skew", async () => { const secret = generateSecret(); // Timestamp from 2 minutes in the future const timestamp = Date.now() + 2 * 60 * 1000; const header = await createAuthHeader(secret, timestamp); const valid = await verifyAuthHeader(secret, header); expect(valid).toBe(false); }); test("verifyAuthHeader allows small future timestamps (clock skew)", async () => { const secret = generateSecret(); // Timestamp from 30 seconds in the future (within 1 minute tolerance) const timestamp = Date.now() + 30 * 1000; const header = await createAuthHeader(secret, timestamp); const valid = await verifyAuthHeader(secret, header); expect(valid).toBe(true); }); test("verifyAuthHeader fails with malformed header (no colon)", async () => { const secret = generateSecret(); const valid = await verifyAuthHeader(secret, "malformed-header"); expect(valid).toBe(false); }); test("verifyAuthHeader fails with malformed header (extra colons)", async () => { const secret = generateSecret(); const timestamp = Date.now(); const header = await createAuthHeader(secret, timestamp); const malformed = `${header}:extra`; const valid = await verifyAuthHeader(secret, malformed); expect(valid).toBe(false); }); test("verifyAuthHeader fails with empty timestamp", async () => { const secret = generateSecret(); const valid = await verifyAuthHeader(secret, ":signature"); expect(valid).toBe(false); }); test("verifyAuthHeader fails with empty signature", async () => { const secret = generateSecret(); const timestamp = Date.now(); const valid = await verifyAuthHeader(secret, `${timestamp}:`); expect(valid).toBe(false); }); test("verifyAuthHeader fails with non-numeric timestamp", async () => { const secret = generateSecret(); const signature = await signMessage(secret, "not-a-number"); const valid = await verifyAuthHeader(secret, `not-a-number:${signature}`); expect(valid).toBe(false); });