Openstatus www.openstatus.dev
at 4c0f4c00a38753a5d0dfd7e7b7b7706dec6f1503 194 lines 6.9 kB view raw
1import { describe, expect, it } from "bun:test"; 2import { 3 generateApiKey, 4 hashApiKey, 5 shouldUpdateLastUsed, 6 verifyApiKeyHash, 7} from "./api-key"; 8 9describe("API Key Utilities", () => { 10 describe("generateApiKey", () => { 11 it("should generate a token with correct format", async () => { 12 const { token, prefix, hash } = await generateApiKey(); 13 14 // Token should start with "os_" and be 35 chars total (os_ + 32 hex) 15 expect(token).toMatch(/^os_[a-f0-9]{32}$/); 16 expect(token.length).toBe(35); 17 }); 18 19 it("should generate a prefix with correct format", async () => { 20 const { prefix } = await generateApiKey(); 21 22 // Prefix should be "os_" + 8 chars = 11 chars total 23 expect(prefix).toMatch(/^os_[a-f0-9]{8}$/); 24 expect(prefix.length).toBe(11); 25 }); 26 27 it("should generate unique tokens", async () => { 28 const key1 = await generateApiKey(); 29 const key2 = await generateApiKey(); 30 31 expect(key1.token).not.toBe(key2.token); 32 expect(key1.hash).not.toBe(key2.hash); 33 }); 34 35 it("should generate prefix from token start", async () => { 36 const { token, prefix } = await generateApiKey(); 37 38 expect(token.slice(0, 11)).toBe(prefix); 39 }); 40 }); 41 42 describe("hashApiKey", () => { 43 it("should generate hash that can verify the token", async () => { 44 const { token, hash } = await generateApiKey(); 45 46 expect(await verifyApiKeyHash(token, hash)).toBe(true); 47 }); 48 }); 49 50 describe("hashApiKey", () => { 51 it("should generate different hashes for different tokens", async () => { 52 const hash1 = await hashApiKey("os_token1"); 53 const hash2 = await hashApiKey("os_token2"); 54 55 expect(hash1).not.toBe(hash2); 56 }); 57 58 it("should generate a valid bcrypt hash", async () => { 59 const hash = await hashApiKey("os_test_token"); 60 61 // Bcrypt hashes start with $2a$, $2b$, or $2y$ 62 expect(hash).toMatch(/^\$2[aby]\$/); 63 }); 64 65 it("should generate hash that can verify the original token", async () => { 66 const token = "os_test_token_12345"; 67 const hash = await hashApiKey(token); 68 69 expect(await verifyApiKeyHash(token, hash)).toBe(true); 70 }); 71 72 it("should generate different hashes for same token on multiple calls", async () => { 73 const token = "os_same_token"; 74 const hash1 = await hashApiKey(token); 75 const hash2 = await hashApiKey(token); 76 77 // bcrypt uses salt, so same input produces different hashes 78 expect(hash1).not.toBe(hash2); 79 // But both should verify the token 80 expect(await verifyApiKeyHash(token, hash1)).toBe(true); 81 expect(await verifyApiKeyHash(token, hash2)).toBe(true); 82 }); 83 }); 84 85 describe("verifyApiKeyHash", () => { 86 it("should return true for valid bcrypt hash with correct token", async () => { 87 const token = "os_valid_token_12345"; 88 const hash = await hashApiKey(token); 89 90 expect(await verifyApiKeyHash(token, hash)).toBe(true); 91 }); 92 93 it("should return false for valid bcrypt hash with wrong token", async () => { 94 const correctToken = "os_correct_token"; 95 const wrongToken = "os_wrong_token"; 96 const hash = await hashApiKey(correctToken); 97 98 expect(await verifyApiKeyHash(wrongToken, hash)).toBe(false); 99 }); 100 101 it("should return false for non-bcrypt hash format", async () => { 102 const token = "os_test_token"; 103 const invalidHash = "not_a_bcrypt_hash"; 104 105 expect(await verifyApiKeyHash(token, invalidHash)).toBe(false); 106 }); 107 108 it("should return false for SHA-256 hash format", async () => { 109 const token = "os_test_token"; 110 // SHA-256 hashes are 64 hex characters 111 const sha256Hash = 112 "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"; 113 114 expect(await verifyApiKeyHash(token, sha256Hash)).toBe(false); 115 }); 116 117 it("should return false for empty hash", async () => { 118 const token = "os_test_token"; 119 120 expect(await verifyApiKeyHash(token, "")).toBe(false); 121 }); 122 123 it("should return false for empty token with valid hash", async () => { 124 const hash = await hashApiKey("os_some_token"); 125 126 expect(await verifyApiKeyHash("", hash)).toBe(false); 127 }); 128 129 it("should handle bcrypt hashes with different cost factors", async () => { 130 const token = "os_test_token"; 131 const hash = await hashApiKey(token); 132 133 // Should work regardless of the $2a$, $2b$, or $2y$ variant 134 expect(await verifyApiKeyHash(token, hash)).toBe(true); 135 }); 136 }); 137 138 describe("shouldUpdateLastUsed", () => { 139 it("should return true when lastUsedAt is null", () => { 140 expect(shouldUpdateLastUsed(null)).toBe(true); 141 }); 142 143 it("should return true when enough time has passed", () => { 144 const sixMinutesAgo = new Date(Date.now() - 6 * 60 * 1000); 145 expect(shouldUpdateLastUsed(sixMinutesAgo, 5)).toBe(true); 146 }); 147 148 it("should return false when not enough time has passed", () => { 149 const twoMinutesAgo = new Date(Date.now() - 2 * 60 * 1000); 150 expect(shouldUpdateLastUsed(twoMinutesAgo, 5)).toBe(false); 151 }); 152 153 it("should respect custom debounce period", () => { 154 const threeMinutesAgo = new Date(Date.now() - 3 * 60 * 1000); 155 expect(shouldUpdateLastUsed(threeMinutesAgo, 2)).toBe(true); 156 expect(shouldUpdateLastUsed(threeMinutesAgo, 4)).toBe(false); 157 }); 158 159 it("should return false when just updated", () => { 160 const justNow = new Date(); 161 expect(shouldUpdateLastUsed(justNow, 5)).toBe(false); 162 }); 163 164 it("should handle boundary case at exact debounce time", () => { 165 const exactlyFiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000); 166 // At exactly the debounce time, it should not update (needs to be > not >=) 167 expect(shouldUpdateLastUsed(exactlyFiveMinutesAgo, 5)).toBe(false); 168 }); 169 170 it("should handle boundary case just after debounce time", () => { 171 const justOverFiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000 - 1); 172 expect(shouldUpdateLastUsed(justOverFiveMinutesAgo, 5)).toBe(true); 173 }); 174 175 it("should use default debounce of 5 minutes when not specified", () => { 176 const fourMinutesAgo = new Date(Date.now() - 4 * 60 * 1000); 177 const sixMinutesAgo = new Date(Date.now() - 6 * 60 * 1000); 178 179 expect(shouldUpdateLastUsed(fourMinutesAgo)).toBe(false); 180 expect(shouldUpdateLastUsed(sixMinutesAgo)).toBe(true); 181 }); 182 183 it("should handle zero debounce period", () => { 184 const oneSecondAgo = new Date(Date.now() - 1000); 185 expect(shouldUpdateLastUsed(oneSecondAgo, 0)).toBe(true); 186 }); 187 188 it("should handle very long debounce periods", () => { 189 const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000); 190 expect(shouldUpdateLastUsed(oneHourAgo, 120)).toBe(false); // 2 hours 191 expect(shouldUpdateLastUsed(oneHourAgo, 30)).toBe(true); // 30 minutes 192 }); 193 }); 194});