🪻 distributed transcription service thistle.dunkirk.sh
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add a dedicated test mode

+222 -132
+2 -2
package.json
··· 6 6 "scripts": { 7 7 "dev": "bun run src/index.ts --hot", 8 8 "clean": "rm -rf transcripts uploads thistle.db", 9 - "test": "bun test", 10 - "test:integration": "bun test src/index.test.ts", 9 + "test": "NODE_ENV=test bun test", 10 + "test:integration": "NODE_ENV=test bun test src/index.test.ts", 11 11 "ngrok": "ngrok http 3000 --domain casual-renewing-reptile.ngrok-free.app" 12 12 }, 13 13 "devDependencies": {
+5 -1
src/db/schema.ts
··· 1 1 import { Database } from "bun:sqlite"; 2 2 3 - export const db = new Database("thistle.db"); 3 + // Use test database when NODE_ENV is test 4 + const dbPath = process.env.NODE_ENV === "test" ? "thistle.test.db" : "thistle.db"; 5 + export const db = new Database(dbPath); 6 + 7 + console.log(`[Database] Using database: ${dbPath}`); 4 8 5 9 // Schema version tracking 6 10 db.run(`
+171 -117
src/index.test.ts
··· 6 6 expect, 7 7 test, 8 8 } from "bun:test"; 9 - import db from "./db/schema"; 10 9 import { hashPasswordClient } from "./lib/client-auth"; 10 + import type { Subprocess } from "bun"; 11 11 12 - // Test server URL - uses port 3001 for testing to avoid conflicts 12 + // Test server configuration 13 13 const TEST_PORT = 3001; 14 14 const BASE_URL = `http://localhost:${TEST_PORT}`; 15 + const TEST_DB_PATH = "./thistle.test.db"; 15 16 16 - // Check if server is available 17 - let serverAvailable = false; 17 + // Test server process 18 + let serverProcess: Subprocess | null = null; 18 19 19 20 beforeAll(async () => { 21 + // Clean up any existing test database 20 22 try { 21 - const response = await fetch(`${BASE_URL}/api/health`, { 22 - signal: AbortSignal.timeout(1000), 23 - }); 24 - serverAvailable = response.ok || response.status === 404; 23 + await import("node:fs/promises").then((fs) => fs.unlink(TEST_DB_PATH)); 25 24 } catch { 26 - console.warn( 27 - `\n⚠️ Test server not running on port ${TEST_PORT}. Start it with:\n PORT=${TEST_PORT} bun run src/index.ts\n Then run tests in another terminal.\n`, 28 - ); 29 - serverAvailable = false; 25 + // Ignore if doesn't exist 30 26 } 27 + 28 + // Start test server as subprocess 29 + serverProcess = Bun.spawn(["bun", "run", "src/index.ts"], { 30 + env: { 31 + ...process.env, 32 + NODE_ENV: "test", 33 + PORT: TEST_PORT.toString(), 34 + SKIP_EMAILS: "true", 35 + SKIP_POLAR_SYNC: "true", 36 + // Dummy env vars to pass startup validation (won't be used due to SKIP_EMAILS/SKIP_POLAR_SYNC) 37 + MAILCHANNELS_API_KEY: "test-key", 38 + DKIM_PRIVATE_KEY: "test-key", 39 + LLM_API_KEY: "test-key", 40 + LLM_API_BASE_URL: "https://test.com", 41 + LLM_MODEL: "test-model", 42 + POLAR_ACCESS_TOKEN: "test-token", 43 + POLAR_ORGANIZATION_ID: "test-org", 44 + POLAR_PRODUCT_ID: "test-product", 45 + POLAR_SUCCESS_URL: "http://localhost:3001/success", 46 + POLAR_WEBHOOK_SECRET: "test-webhook-secret", 47 + ORIGIN: "http://localhost:3001", 48 + }, 49 + stdout: "pipe", 50 + stderr: "pipe", 51 + }); 52 + 53 + // Log server output for debugging 54 + const stdoutReader = serverProcess.stdout.getReader(); 55 + const stderrReader = serverProcess.stderr.getReader(); 56 + const decoder = new TextDecoder(); 57 + 58 + (async () => { 59 + try { 60 + while (true) { 61 + const { value, done } = await stdoutReader.read(); 62 + if (done) break; 63 + const text = decoder.decode(value); 64 + console.log("[SERVER OUT]", text.trim()); 65 + } 66 + } catch {} 67 + })(); 68 + 69 + (async () => { 70 + try { 71 + while (true) { 72 + const { value, done } = await stderrReader.read(); 73 + if (done) break; 74 + const text = decoder.decode(value); 75 + console.error("[SERVER ERR]", text.trim()); 76 + } 77 + } catch {} 78 + })(); 79 + 80 + // Wait for server to be ready 81 + let retries = 30; 82 + let ready = false; 83 + while (retries > 0 && !ready) { 84 + try { 85 + const response = await fetch(`${BASE_URL}/api/health`, { 86 + signal: AbortSignal.timeout(1000), 87 + }); 88 + if (response.ok) { 89 + ready = true; 90 + break; 91 + } 92 + } catch { 93 + // Server not ready yet 94 + } 95 + await new Promise((resolve) => setTimeout(resolve, 500)); 96 + retries--; 97 + } 98 + 99 + if (!ready) { 100 + throw new Error("Test server failed to start within 15 seconds"); 101 + } 102 + 103 + console.log(`✓ Test server running on port ${TEST_PORT}`); 104 + }); 105 + 106 + afterAll(async () => { 107 + // Kill test server 108 + if (serverProcess) { 109 + serverProcess.kill(); 110 + await new Promise((resolve) => setTimeout(resolve, 1000)); 111 + } 112 + 113 + // Clean up test database 114 + try { 115 + await import("node:fs/promises").then((fs) => fs.unlink(TEST_DB_PATH)); 116 + } catch { 117 + // Ignore if doesn't exist 118 + } 119 + 120 + console.log("✓ Test server stopped and test database cleaned up"); 31 121 }); 32 122 33 123 // Test user credentials ··· 81 171 }); 82 172 } 83 173 84 - // Cleanup helpers 85 - function cleanupTestData() { 86 - // Delete test users and their related data (cascade will handle most of it) 87 - // Include 'newemail%' to catch users whose emails were updated during tests 88 - db.run( 89 - "DELETE FROM sessions WHERE user_id IN (SELECT id FROM users WHERE email LIKE 'test%' OR email LIKE 'admin@%' OR email LIKE 'newemail%')", 90 - ); 91 - db.run( 92 - "DELETE FROM passkeys WHERE user_id IN (SELECT id FROM users WHERE email LIKE 'test%' OR email LIKE 'admin@%' OR email LIKE 'newemail%')", 93 - ); 94 - db.run( 95 - "DELETE FROM transcriptions WHERE user_id IN (SELECT id FROM users WHERE email LIKE 'test%' OR email LIKE 'admin@%' OR email LIKE 'newemail%')", 96 - ); 97 - db.run( 98 - "DELETE FROM users WHERE email LIKE 'test%' OR email LIKE 'admin@%' OR email LIKE 'newemail%'", 99 - ); 100 - 101 - // Clear ALL rate limit data to prevent accumulation across tests 102 - // (IP-based rate limits don't contain test/admin in the key) 103 - db.run("DELETE FROM rate_limit_attempts"); 104 - } 105 - 106 - beforeEach(() => { 107 - if (serverAvailable) { 108 - cleanupTestData(); 109 - } 110 - }); 111 - 112 - afterAll(() => { 113 - if (serverAvailable) { 114 - cleanupTestData(); 115 - } 116 - }); 117 - 118 - // Helper to skip tests if server is not available 119 - function serverTest(name: string, fn: () => void | Promise<void>) { 120 - test(name, async () => { 121 - if (!serverAvailable) { 122 - console.log(`⏭️ Skipping: ${name} (server not running)`); 123 - return; 124 - } 125 - await fn(); 126 - }); 127 - } 174 + // All tests run against a fresh database, no cleanup needed 128 175 129 176 describe("API Endpoints - Authentication", () => { 130 177 describe("POST /api/auth/register", () => { 131 - serverTest("should register a new user successfully", async () => { 178 + test("should register a new user successfully", async () => { 132 179 const hashedPassword = await clientHashPassword( 133 180 TEST_USER.email, 134 181 TEST_USER.password, ··· 144 191 }), 145 192 }); 146 193 194 + if (response.status !== 200) { 195 + const error = await response.json(); 196 + console.error("Registration failed:", response.status, error); 197 + } 198 + 147 199 expect(response.status).toBe(200); 200 + 201 + // Extract session before consuming response body 202 + const sessionCookie = extractSessionCookie(response); 203 + 148 204 const data = await response.json(); 149 205 expect(data.user).toBeDefined(); 150 206 expect(data.user.email).toBe(TEST_USER.email); 151 - expect(extractSessionCookie(response)).toBeTruthy(); 207 + expect(sessionCookie).toBeTruthy(); 152 208 }); 153 209 154 - serverTest("should reject registration with missing email", async () => { 210 + test("should reject registration with missing email", async () => { 155 211 const response = await fetch(`${BASE_URL}/api/auth/register`, { 156 212 method: "POST", 157 213 headers: { "Content-Type": "application/json" }, ··· 165 221 expect(data.error).toBe("Email and password required"); 166 222 }); 167 223 168 - serverTest( 224 + test( 169 225 "should reject registration with invalid password format", 170 226 async () => { 171 227 const response = await fetch(`${BASE_URL}/api/auth/register`, { ··· 183 239 }, 184 240 ); 185 241 186 - serverTest("should reject duplicate email registration", async () => { 242 + test("should reject duplicate email registration", async () => { 187 243 const hashedPassword = await clientHashPassword( 188 244 TEST_USER.email, 189 245 TEST_USER.password, ··· 216 272 expect(data.error).toBe("Email already registered"); 217 273 }); 218 274 219 - serverTest("should enforce rate limiting on registration", async () => { 275 + test("should enforce rate limiting on registration", async () => { 220 276 const hashedPassword = await clientHashPassword( 221 277 "test@example.com", 222 278 "password", ··· 246 302 }); 247 303 248 304 describe("POST /api/auth/login", () => { 249 - serverTest("should login successfully with valid credentials", async () => { 305 + test("should login successfully with valid credentials", async () => { 250 306 // Register user first 251 307 const hashedPassword = await clientHashPassword( 252 308 TEST_USER.email, ··· 279 335 expect(extractSessionCookie(response)).toBeTruthy(); 280 336 }); 281 337 282 - serverTest("should reject login with invalid credentials", async () => { 338 + test("should reject login with invalid credentials", async () => { 283 339 // Register user first 284 340 const hashedPassword = await clientHashPassword( 285 341 TEST_USER.email, ··· 313 369 expect(data.error).toBe("Invalid email or password"); 314 370 }); 315 371 316 - serverTest("should reject login with missing fields", async () => { 372 + test("should reject login with missing fields", async () => { 317 373 const response = await fetch(`${BASE_URL}/api/auth/login`, { 318 374 method: "POST", 319 375 headers: { "Content-Type": "application/json" }, ··· 327 383 expect(data.error).toBe("Email and password required"); 328 384 }); 329 385 330 - serverTest("should enforce rate limiting on login attempts", async () => { 386 + test("should enforce rate limiting on login attempts", async () => { 331 387 const hashedPassword = await clientHashPassword( 332 388 TEST_USER.email, 333 389 TEST_USER.password, ··· 357 413 }); 358 414 359 415 describe("POST /api/auth/logout", () => { 360 - serverTest("should logout successfully", async () => { 416 + test("should logout successfully", async () => { 361 417 // Register and login 362 418 const hashedPassword = await clientHashPassword( 363 419 TEST_USER.email, ··· 391 447 expect(setCookie).toContain("Max-Age=0"); 392 448 }); 393 449 394 - serverTest("should logout even without valid session", async () => { 450 + test("should logout even without valid session", async () => { 395 451 const response = await fetch(`${BASE_URL}/api/auth/logout`, { 396 452 method: "POST", 397 453 }); ··· 403 459 }); 404 460 405 461 describe("GET /api/auth/me", () => { 406 - serverTest( 462 + test( 407 463 "should return current user info when authenticated", 408 464 async () => { 409 465 // Register user ··· 436 492 }, 437 493 ); 438 494 439 - serverTest("should return 401 when not authenticated", async () => { 495 + test("should return 401 when not authenticated", async () => { 440 496 const response = await fetch(`${BASE_URL}/api/auth/me`); 441 497 442 498 expect(response.status).toBe(401); ··· 444 500 expect(data.error).toBe("Not authenticated"); 445 501 }); 446 502 447 - serverTest("should return 401 with invalid session", async () => { 503 + test("should return 401 with invalid session", async () => { 448 504 const response = await authRequest( 449 505 `${BASE_URL}/api/auth/me`, 450 506 "invalid-session", ··· 459 515 460 516 describe("API Endpoints - Session Management", () => { 461 517 describe("GET /api/sessions", () => { 462 - serverTest("should return user sessions", async () => { 518 + test("should return user sessions", async () => { 463 519 // Register user 464 520 const hashedPassword = await clientHashPassword( 465 521 TEST_USER.email, ··· 490 546 expect(data.sessions[0]).toHaveProperty("user_agent"); 491 547 }); 492 548 493 - serverTest("should require authentication", async () => { 549 + test("should require authentication", async () => { 494 550 const response = await fetch(`${BASE_URL}/api/sessions`); 495 551 496 552 expect(response.status).toBe(401); ··· 498 554 }); 499 555 500 556 describe("DELETE /api/sessions", () => { 501 - serverTest("should delete specific session", async () => { 557 + test("should delete specific session", async () => { 502 558 // Register user and create multiple sessions 503 559 const hashedPassword = await clientHashPassword( 504 560 TEST_USER.email, ··· 557 613 expect(verifyResponse.status).toBe(401); 558 614 }); 559 615 560 - serverTest("should not delete another user's session", async () => { 616 + test("should not delete another user's session", async () => { 561 617 // Register two users 562 618 const hashedPassword1 = await clientHashPassword( 563 619 TEST_USER.email, ··· 601 657 expect(response.status).toBe(404); 602 658 }); 603 659 604 - serverTest("should not delete current session", async () => { 660 + test("should not delete current session", async () => { 605 661 // Register user 606 662 const hashedPassword = await clientHashPassword( 607 663 TEST_USER.email, ··· 637 693 638 694 describe("API Endpoints - User Management", () => { 639 695 describe("DELETE /api/user", () => { 640 - serverTest("should delete user account", async () => { 696 + test("should delete user account", async () => { 641 697 // Register user 642 698 const hashedPassword = await clientHashPassword( 643 699 TEST_USER.email, ··· 674 730 expect(verifyResponse.status).toBe(401); 675 731 }); 676 732 677 - serverTest("should require authentication", async () => { 733 + test("should require authentication", async () => { 678 734 const response = await fetch(`${BASE_URL}/api/user`, { 679 735 method: "DELETE", 680 736 }); ··· 684 740 }); 685 741 686 742 describe("PUT /api/user/email", () => { 687 - serverTest("should update user email", async () => { 743 + test("should update user email", async () => { 688 744 // Register user 689 745 const hashedPassword = await clientHashPassword( 690 746 TEST_USER.email, ··· 725 781 expect(meData.email).toBe(newEmail); 726 782 }); 727 783 728 - serverTest("should reject duplicate email", async () => { 784 + test("should reject duplicate email", async () => { 729 785 // Register two users 730 786 const hashedPassword1 = await clientHashPassword( 731 787 TEST_USER.email, ··· 772 828 }); 773 829 774 830 describe("PUT /api/user/password", () => { 775 - serverTest("should update user password", async () => { 831 + test("should update user password", async () => { 776 832 // Register user 777 833 const hashedPassword = await clientHashPassword( 778 834 TEST_USER.email, ··· 819 875 expect(loginResponse.status).toBe(200); 820 876 }); 821 877 822 - serverTest("should reject invalid password format", async () => { 878 + test("should reject invalid password format", async () => { 823 879 // Register user 824 880 const hashedPassword = await clientHashPassword( 825 881 TEST_USER.email, ··· 853 909 }); 854 910 855 911 describe("PUT /api/user/name", () => { 856 - serverTest("should update user name", async () => { 912 + test("should update user name", async () => { 857 913 // Register user 858 914 const hashedPassword = await clientHashPassword( 859 915 TEST_USER.email, ··· 895 951 expect(meData.name).toBe(newName); 896 952 }); 897 953 898 - serverTest("should reject missing name", async () => { 954 + test("should reject missing name", async () => { 899 955 // Register user 900 956 const hashedPassword = await clientHashPassword( 901 957 TEST_USER.email, ··· 926 982 }); 927 983 928 984 describe("PUT /api/user/avatar", () => { 929 - serverTest("should update user avatar", async () => { 985 + test("should update user avatar", async () => { 930 986 // Register user 931 987 const hashedPassword = await clientHashPassword( 932 988 TEST_USER.email, ··· 971 1027 972 1028 describe("API Endpoints - Health", () => { 973 1029 describe("GET /api/health", () => { 974 - serverTest( 1030 + test( 975 1031 "should return service health status with details", 976 1032 async () => { 977 1033 const response = await fetch(`${BASE_URL}/api/health`); ··· 991 1047 992 1048 describe("API Endpoints - Transcriptions", () => { 993 1049 describe("GET /api/transcriptions", () => { 994 - serverTest("should return user transcriptions", async () => { 1050 + test("should return user transcriptions", async () => { 995 1051 // Register user 996 1052 const hashedPassword = await clientHashPassword( 997 1053 TEST_USER.email, ··· 1019 1075 expect(Array.isArray(data.jobs)).toBe(true); 1020 1076 }); 1021 1077 1022 - serverTest("should require authentication", async () => { 1078 + test("should require authentication", async () => { 1023 1079 const response = await fetch(`${BASE_URL}/api/transcriptions`); 1024 1080 1025 1081 expect(response.status).toBe(401); ··· 1027 1083 }); 1028 1084 1029 1085 describe("POST /api/transcriptions", () => { 1030 - serverTest("should upload audio file and start transcription", async () => { 1086 + test("should upload audio file and start transcription", async () => { 1031 1087 // Register user 1032 1088 const hashedPassword = await clientHashPassword( 1033 1089 TEST_USER.email, ··· 1065 1121 expect(data.message).toContain("Upload successful"); 1066 1122 }); 1067 1123 1068 - serverTest("should reject non-audio files", async () => { 1124 + test("should reject non-audio files", async () => { 1069 1125 // Register user 1070 1126 const hashedPassword = await clientHashPassword( 1071 1127 TEST_USER.email, ··· 1098 1154 expect(response.status).toBe(400); 1099 1155 }); 1100 1156 1101 - serverTest("should reject files exceeding size limit", async () => { 1157 + test("should reject files exceeding size limit", async () => { 1102 1158 // Register user 1103 1159 const hashedPassword = await clientHashPassword( 1104 1160 TEST_USER.email, ··· 1135 1191 expect(data.error).toContain("File size must be less than"); 1136 1192 }); 1137 1193 1138 - serverTest("should require authentication", async () => { 1194 + test("should require authentication", async () => { 1139 1195 const audioBlob = new Blob(["fake audio data"], { type: "audio/mp3" }); 1140 1196 const formData = new FormData(); 1141 1197 formData.append("audio", audioBlob, "test.mp3"); ··· 1156 1212 let userId: number; 1157 1213 1158 1214 beforeEach(async () => { 1159 - if (!serverAvailable) return; 1160 1215 1161 1216 // Create admin user 1162 1217 const adminHash = await clientHashPassword( ··· 1203 1258 }); 1204 1259 1205 1260 describe("GET /api/admin/users", () => { 1206 - serverTest("should return all users for admin", async () => { 1261 + test("should return all users for admin", async () => { 1207 1262 const response = await authRequest( 1208 1263 `${BASE_URL}/api/admin/users`, 1209 1264 adminCookie, ··· 1215 1270 expect(data.length).toBeGreaterThan(0); 1216 1271 }); 1217 1272 1218 - serverTest("should reject non-admin users", async () => { 1273 + test("should reject non-admin users", async () => { 1219 1274 const response = await authRequest( 1220 1275 `${BASE_URL}/api/admin/users`, 1221 1276 userCookie, ··· 1224 1279 expect(response.status).toBe(403); 1225 1280 }); 1226 1281 1227 - serverTest("should require authentication", async () => { 1282 + test("should require authentication", async () => { 1228 1283 const response = await fetch(`${BASE_URL}/api/admin/users`); 1229 1284 1230 1285 expect(response.status).toBe(401); ··· 1232 1287 }); 1233 1288 1234 1289 describe("GET /api/admin/transcriptions", () => { 1235 - serverTest("should return all transcriptions for admin", async () => { 1290 + test("should return all transcriptions for admin", async () => { 1236 1291 const response = await authRequest( 1237 1292 `${BASE_URL}/api/admin/transcriptions`, 1238 1293 adminCookie, ··· 1243 1298 expect(Array.isArray(data)).toBe(true); 1244 1299 }); 1245 1300 1246 - serverTest("should reject non-admin users", async () => { 1301 + test("should reject non-admin users", async () => { 1247 1302 const response = await authRequest( 1248 1303 `${BASE_URL}/api/admin/transcriptions`, 1249 1304 userCookie, ··· 1254 1309 }); 1255 1310 1256 1311 describe("DELETE /api/admin/users/:id", () => { 1257 - serverTest("should delete user as admin", async () => { 1312 + test("should delete user as admin", async () => { 1258 1313 const response = await authRequest( 1259 1314 `${BASE_URL}/api/admin/users/${userId}`, 1260 1315 adminCookie, ··· 1275 1330 expect(verifyResponse.status).toBe(401); 1276 1331 }); 1277 1332 1278 - serverTest("should reject non-admin users", async () => { 1333 + test("should reject non-admin users", async () => { 1279 1334 const response = await authRequest( 1280 1335 `${BASE_URL}/api/admin/users/${userId}`, 1281 1336 userCookie, ··· 1289 1344 }); 1290 1345 1291 1346 describe("PUT /api/admin/users/:id/role", () => { 1292 - serverTest("should update user role as admin", async () => { 1347 + test("should update user role as admin", async () => { 1293 1348 const response = await authRequest( 1294 1349 `${BASE_URL}/api/admin/users/${userId}/role`, 1295 1350 adminCookie, ··· 1313 1368 expect(meData.role).toBe("admin"); 1314 1369 }); 1315 1370 1316 - serverTest("should reject invalid roles", async () => { 1371 + test("should reject invalid roles", async () => { 1317 1372 const response = await authRequest( 1318 1373 `${BASE_URL}/api/admin/users/${userId}/role`, 1319 1374 adminCookie, ··· 1329 1384 }); 1330 1385 1331 1386 describe("GET /api/admin/users/:id/details", () => { 1332 - serverTest("should return user details for admin", async () => { 1387 + test("should return user details for admin", async () => { 1333 1388 const response = await authRequest( 1334 1389 `${BASE_URL}/api/admin/users/${userId}/details`, 1335 1390 adminCookie, ··· 1343 1398 expect(data).toHaveProperty("sessions"); 1344 1399 }); 1345 1400 1346 - serverTest("should reject non-admin users", async () => { 1401 + test("should reject non-admin users", async () => { 1347 1402 const response = await authRequest( 1348 1403 `${BASE_URL}/api/admin/users/${userId}/details`, 1349 1404 userCookie, ··· 1354 1409 }); 1355 1410 1356 1411 describe("PUT /api/admin/users/:id/name", () => { 1357 - serverTest("should update user name as admin", async () => { 1412 + test("should update user name as admin", async () => { 1358 1413 const newName = "Admin Updated Name"; 1359 1414 const response = await authRequest( 1360 1415 `${BASE_URL}/api/admin/users/${userId}/name`, ··· 1371 1426 expect(data.success).toBe(true); 1372 1427 }); 1373 1428 1374 - serverTest("should reject empty names", async () => { 1429 + test("should reject empty names", async () => { 1375 1430 const response = await authRequest( 1376 1431 `${BASE_URL}/api/admin/users/${userId}/name`, 1377 1432 adminCookie, ··· 1387 1442 }); 1388 1443 1389 1444 describe("PUT /api/admin/users/:id/email", () => { 1390 - serverTest("should update user email as admin", async () => { 1445 + test("should update user email as admin", async () => { 1391 1446 const newEmail = "newemail@admin.com"; 1392 1447 const response = await authRequest( 1393 1448 `${BASE_URL}/api/admin/users/${userId}/email`, ··· 1404 1459 expect(data.success).toBe(true); 1405 1460 }); 1406 1461 1407 - serverTest("should reject duplicate emails", async () => { 1462 + test("should reject duplicate emails", async () => { 1408 1463 const response = await authRequest( 1409 1464 `${BASE_URL}/api/admin/users/${userId}/email`, 1410 1465 adminCookie, ··· 1422 1477 }); 1423 1478 1424 1479 describe("GET /api/admin/users/:id/sessions", () => { 1425 - serverTest("should return user sessions as admin", async () => { 1480 + test("should return user sessions as admin", async () => { 1426 1481 const response = await authRequest( 1427 1482 `${BASE_URL}/api/admin/users/${userId}/sessions`, 1428 1483 adminCookie, ··· 1435 1490 }); 1436 1491 1437 1492 describe("DELETE /api/admin/users/:id/sessions", () => { 1438 - serverTest("should delete all user sessions as admin", async () => { 1493 + test("should delete all user sessions as admin", async () => { 1439 1494 const response = await authRequest( 1440 1495 `${BASE_URL}/api/admin/users/${userId}/sessions`, 1441 1496 adminCookie, ··· 1462 1517 let sessionCookie: string; 1463 1518 1464 1519 beforeEach(async () => { 1465 - if (!serverAvailable) return; 1466 1520 1467 1521 // Register user 1468 1522 const hashedPassword = await clientHashPassword( ··· 1481 1535 }); 1482 1536 1483 1537 describe("GET /api/passkeys", () => { 1484 - serverTest("should return user passkeys", async () => { 1538 + test("should return user passkeys", async () => { 1485 1539 const response = await authRequest( 1486 1540 `${BASE_URL}/api/passkeys`, 1487 1541 sessionCookie, ··· 1493 1547 expect(Array.isArray(data.passkeys)).toBe(true); 1494 1548 }); 1495 1549 1496 - serverTest("should require authentication", async () => { 1550 + test("should require authentication", async () => { 1497 1551 const response = await fetch(`${BASE_URL}/api/passkeys`); 1498 1552 1499 1553 expect(response.status).toBe(401); ··· 1501 1555 }); 1502 1556 1503 1557 describe("POST /api/passkeys/register/options", () => { 1504 - serverTest( 1558 + test( 1505 1559 "should return registration options for authenticated user", 1506 1560 async () => { 1507 1561 const response = await authRequest( ··· 1520 1574 }, 1521 1575 ); 1522 1576 1523 - serverTest("should require authentication", async () => { 1577 + test("should require authentication", async () => { 1524 1578 const response = await fetch( 1525 1579 `${BASE_URL}/api/passkeys/register/options`, 1526 1580 { ··· 1533 1587 }); 1534 1588 1535 1589 describe("POST /api/passkeys/authenticate/options", () => { 1536 - serverTest("should return authentication options for email", async () => { 1590 + test("should return authentication options for email", async () => { 1537 1591 const response = await fetch( 1538 1592 `${BASE_URL}/api/passkeys/authenticate/options`, 1539 1593 { ··· 1548 1602 expect(data).toHaveProperty("challenge"); 1549 1603 }); 1550 1604 1551 - serverTest("should handle non-existent email", async () => { 1605 + test("should handle non-existent email", async () => { 1552 1606 const response = await fetch( 1553 1607 `${BASE_URL}/api/passkeys/authenticate/options`, 1554 1608 {
+8 -1
src/index.ts
··· 173 173 userId: number, 174 174 email: string, 175 175 ): Promise<void> { 176 + // Skip Polar sync in test mode 177 + if (process.env.NODE_ENV === "test" || process.env.SKIP_POLAR_SYNC === "true") { 178 + return; 179 + } 180 + 176 181 try { 177 182 const { polar } = await import("./lib/polar"); 178 183 ··· 299 304 ); 300 305 301 306 const server = Bun.serve({ 302 - port: process.env.PORT ? Number.parseInt(process.env.PORT, 10) : 3000, 307 + port: process.env.NODE_ENV === "test" 308 + ? 3001 309 + : (process.env.PORT ? Number.parseInt(process.env.PORT, 10) : 3000), 303 310 idleTimeout: 120, // 120 seconds for SSE connections 304 311 routes: { 305 312 "/": indexHTML,
+12 -9
src/lib/email-verification.test.ts
··· 27 27 }); 28 28 29 29 test("creates verification token", () => { 30 - const token = createEmailVerificationToken(userId); 31 - expect(token).toBeDefined(); 32 - expect(typeof token).toBe("string"); 33 - expect(token.length).toBeGreaterThan(0); 30 + const result = createEmailVerificationToken(userId); 31 + expect(result).toBeDefined(); 32 + expect(typeof result).toBe("object"); 33 + expect(typeof result.code).toBe("string"); 34 + expect(typeof result.token).toBe("string"); 35 + expect(typeof result.sentAt).toBe("number"); 36 + expect(result.code.length).toBe(6); 34 37 }); 35 38 36 39 test("verifies valid token", () => { 37 - const token = createEmailVerificationToken(userId); 40 + const { token } = createEmailVerificationToken(userId); 38 41 const result = verifyEmailToken(token); 39 42 40 43 expect(result).not.toBeNull(); ··· 50 53 }); 51 54 52 55 test("token is one-time use", () => { 53 - const token = createEmailVerificationToken(userId); 56 + const { token } = createEmailVerificationToken(userId); 54 57 55 58 // First use succeeds 56 59 const firstResult = verifyEmailToken(token); ··· 62 65 }); 63 66 64 67 test("rejects expired token", () => { 65 - const token = createEmailVerificationToken(userId); 68 + const { token } = createEmailVerificationToken(userId); 66 69 67 70 // Manually expire the token 68 71 db.run( ··· 75 78 }); 76 79 77 80 test("replaces existing token when creating new one", () => { 78 - const token1 = createEmailVerificationToken(userId); 79 - const token2 = createEmailVerificationToken(userId); 81 + const { token: token1 } = createEmailVerificationToken(userId); 82 + const { token: token2 } = createEmailVerificationToken(userId); 80 83 81 84 // First token should be invalidated 82 85 expect(verifyEmailToken(token1)).toBeNull();
+6
src/lib/email.ts
··· 25 25 * Send an email via MailChannels 26 26 */ 27 27 export async function sendEmail(options: SendEmailOptions): Promise<void> { 28 + // Skip sending emails in test mode 29 + if (process.env.NODE_ENV === "test" || process.env.SKIP_EMAILS === "true") { 30 + console.log(`[Email] SKIPPED (test mode): "${options.subject}" to ${typeof options.to === "string" ? options.to : options.to.email}`); 31 + return; 32 + } 33 + 28 34 const fromEmail = process.env.SMTP_FROM_EMAIL || "noreply@thistle.app"; 29 35 const fromName = process.env.SMTP_FROM_NAME || "Thistle"; 30 36 const dkimDomain = process.env.DKIM_DOMAIN || "thistle.app";
+2 -2
src/lib/subscription-routes.test.ts
··· 29 29 headers: { Cookie: sessionCookie }, 30 30 }); 31 31 32 - expect(response.status).toBe(500); 32 + expect(response.status).toBe(403); 33 33 const data = await response.json(); 34 34 expect(data.error).toContain("subscription"); 35 35 }); ··· 74 74 body: formData, 75 75 }); 76 76 77 - expect(response.status).toBe(500); 77 + expect(response.status).toBe(403); 78 78 const data = await response.json(); 79 79 expect(data.error).toContain("subscription"); 80 80 });
+10
src/lib/vtt-cleaner.test.ts
··· 55 55 56 56 test("cleanVTT preserves empty VTT", async () => { 57 57 const emptyVTT = "WEBVTT\n\n"; 58 + 59 + // Save and remove API key to avoid burning tokens 60 + const originalKey = process.env.LLM_API_KEY; 61 + delete process.env.LLM_API_KEY; 62 + 58 63 const result = await cleanVTT("test-empty", emptyVTT); 59 64 60 65 expect(result).toBe(emptyVTT); 66 + 67 + // Restore original key 68 + if (originalKey) { 69 + process.env.LLM_API_KEY = originalKey; 70 + } 61 71 }); 62 72 63 73 // AI integration test - skip by default to avoid burning credits
+6
src/lib/vtt-cleaner.ts
··· 338 338 `[VTTCleaner] Processing ${segments.length} segments for ${transcriptionId}`, 339 339 ); 340 340 341 + // Check if API key is available, return original if not 342 + if (!process.env.LLM_API_KEY) { 343 + console.warn("[VTTCleaner] LLM_API_KEY not set, returning original VTT"); 344 + return vttContent; 345 + } 346 + 341 347 // Validated at startup 342 348 const apiKey = process.env.LLM_API_KEY as string; 343 349 const apiBaseUrl = process.env.LLM_API_BASE_URL as string;