import { describe, expect, it, vi } from "vitest"; import { API_KEY_PREFIX, extractKeyPrefix, generateApiKey, hashApiKey, isValidApiKeyFormat, } from "./api-key.js"; import { EdgeFunctionApiKeyValidator } from "./api-key-validator.js"; describe("API Key Generation", () => { describe("generateApiKey", () => { it("generates a key with the correct prefix", () => { const apiKey = generateApiKey(); expect(apiKey.startsWith(API_KEY_PREFIX)).toBe(true); }); it("generates a key with 32 hex characters after prefix", () => { const apiKey = generateApiKey(); const keyPart = apiKey.slice(API_KEY_PREFIX.length); expect(keyPart.length).toBe(32); expect(/^[0-9a-f]+$/i.test(keyPart)).toBe(true); }); it("generates unique keys on each call", () => { const apiKey1 = generateApiKey(); const apiKey2 = generateApiKey(); expect(apiKey1).not.toBe(apiKey2); }); it("generates keys with total length of prefix + 32 characters", () => { const apiKey = generateApiKey(); expect(apiKey.length).toBe(API_KEY_PREFIX.length + 32); }); }); describe("extractKeyPrefix", () => { it("extracts first 8 characters after prefix", () => { const apiKey = "imemio_0cee07c39ef7ba7d8e23e760929ce7bc"; const prefix = extractKeyPrefix(apiKey); expect(prefix).toBe("0cee07c3"); }); it("throws error for invalid key format", () => { expect(() => extractKeyPrefix("invalid_key")).toThrow( "Invalid API key format", ); }); it("throws error for key without imemio prefix", () => { expect(() => extractKeyPrefix("sk_0cee07c39ef7ba7d")).toThrow( "Invalid API key format", ); }); }); describe("hashApiKey", () => { it("returns a 64 character hex string", async () => { const apiKey = "imemio_0cee07c39ef7ba7d8e23e760929ce7bc"; const hash = await hashApiKey(apiKey); expect(hash.length).toBe(64); expect(/^[0-9a-f]+$/i.test(hash)).toBe(true); }); it("returns consistent hash for same input", async () => { const apiKey = "imemio_0cee07c39ef7ba7d8e23e760929ce7bc"; const hash1 = await hashApiKey(apiKey); const hash2 = await hashApiKey(apiKey); expect(hash1).toBe(hash2); }); it("returns different hashes for different inputs", async () => { const apiKey1 = "imemio_0cee07c39ef7ba7d8e23e760929ce7bc"; const apiKey2 = "imemio_1111111111111111111111111111111a"; const hash1 = await hashApiKey(apiKey1); const hash2 = await hashApiKey(apiKey2); expect(hash1).not.toBe(hash2); }); }); describe("isValidApiKeyFormat", () => { it("returns true for valid API key", () => { const apiKey = "imemio_0cee07c39ef7ba7d8e23e760929ce7bc"; expect(isValidApiKeyFormat(apiKey)).toBe(true); }); it("returns false for key without prefix", () => { const apiKey = "0cee07c39ef7ba7d8e23e760929ce7bc"; expect(isValidApiKeyFormat(apiKey)).toBe(false); }); it("returns false for key with wrong prefix", () => { const apiKey = "sk_0cee07c39ef7ba7d8e23e760929ce7bc"; expect(isValidApiKeyFormat(apiKey)).toBe(false); }); it("returns false for key with too few hex characters", () => { const apiKey = "imemio_0cee07c3"; expect(isValidApiKeyFormat(apiKey)).toBe(false); }); it("returns false for key with too many hex characters", () => { const apiKey = "imemio_0cee07c39ef7ba7d8e23e760929ce7bc00"; expect(isValidApiKeyFormat(apiKey)).toBe(false); }); it("returns false for key with non-hex characters", () => { const apiKey = "imemio_0cee07c39ef7ba7d8e23e760929cexyz"; expect(isValidApiKeyFormat(apiKey)).toBe(false); }); it("accepts uppercase hex characters", () => { const apiKey = "imemio_0CEE07C39EF7BA7D8E23E760929CE7BC"; expect(isValidApiKeyFormat(apiKey)).toBe(true); }); }); }); describe("EdgeFunctionApiKeyValidator", () => { const mockSupabaseUrl = "https://test.supabase.co"; const mockSupabaseAnonKey = "test-anon-key"; describe("validate", () => { it("returns valid result with userId and apiKeyId on success", async () => { const mockResponse = { ok: true, json: vi.fn().mockResolvedValue({ userId: "user-123", apiKeyId: "key-456", }), }; global.fetch = vi.fn().mockResolvedValue(mockResponse); const validator = new EdgeFunctionApiKeyValidator( mockSupabaseUrl, mockSupabaseAnonKey, ); const result = await validator.validate( "imemio_0cee07c39ef7ba7d8e23e760929ce7bc", ); expect(result.valid).toBe(true); if (result.valid) { expect(result.userId).toBe("user-123"); expect(result.apiKeyId).toBe("key-456"); } }); it("sends correct request to Edge Function", async () => { const mockResponse = { ok: true, json: vi.fn().mockResolvedValue({ userId: "user-123", apiKeyId: "key-456", }), }; global.fetch = vi.fn().mockResolvedValue(mockResponse); const validator = new EdgeFunctionApiKeyValidator( mockSupabaseUrl, mockSupabaseAnonKey, ); await validator.validate("imemio_0cee07c39ef7ba7d8e23e760929ce7bc"); expect(fetch).toHaveBeenCalledWith( `${mockSupabaseUrl}/functions/v1/validate-api-key`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${mockSupabaseAnonKey}`, }, body: JSON.stringify({ apiKey: "imemio_0cee07c39ef7ba7d8e23e760929ce7bc", }), }, ); }); it("returns invalid result with error on non-ok response", async () => { const mockResponse = { ok: false, json: vi.fn().mockResolvedValue({ error: "Invalid or revoked API key", }), }; global.fetch = vi.fn().mockResolvedValue(mockResponse); const validator = new EdgeFunctionApiKeyValidator( mockSupabaseUrl, mockSupabaseAnonKey, ); const result = await validator.validate("imemio_invalid"); expect(result.valid).toBe(false); if (!result.valid) { expect(result.error).toBe("Invalid or revoked API key"); } }); it("returns default error message when response has no error field", async () => { const mockResponse = { ok: false, json: vi.fn().mockResolvedValue({}), }; global.fetch = vi.fn().mockResolvedValue(mockResponse); const validator = new EdgeFunctionApiKeyValidator( mockSupabaseUrl, mockSupabaseAnonKey, ); const result = await validator.validate("imemio_invalid"); expect(result.valid).toBe(false); if (!result.valid) { expect(result.error).toBe("Validation failed"); } }); it("returns invalid result on network error", async () => { global.fetch = vi.fn().mockRejectedValue(new Error("Network error")); const validator = new EdgeFunctionApiKeyValidator( mockSupabaseUrl, mockSupabaseAnonKey, ); const result = await validator.validate( "imemio_0cee07c39ef7ba7d8e23e760929ce7bc", ); expect(result.valid).toBe(false); if (!result.valid) { expect(result.error).toBe("Failed to validate API key"); } }); }); });