🪻 distributed transcription service thistle.dunkirk.sh
1
fork

Configure Feed

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

chore: fix biome lint issues

- Sort imports in index.test.ts
- Auto-format schema.ts, email.ts, vtt-cleaner.test.ts

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.5 via Crush <crush@charm.land>

+80 -71
+2 -1
src/db/schema.ts
··· 1 1 import { Database } from "bun:sqlite"; 2 2 3 3 // Use test database when NODE_ENV is test 4 - const dbPath = process.env.NODE_ENV === "test" ? "thistle.test.db" : "thistle.db"; 4 + const dbPath = 5 + process.env.NODE_ENV === "test" ? "thistle.test.db" : "thistle.db"; 5 6 export const db = new Database(dbPath); 6 7 7 8 console.log(`[Database] Using database: ${dbPath}`);
+72 -66
src/index.test.ts
··· 6 6 expect, 7 7 test, 8 8 } from "bun:test"; 9 - import { hashPasswordClient } from "./lib/client-auth"; 10 9 import type { Subprocess } from "bun"; 10 + import { hashPasswordClient } from "./lib/client-auth"; 11 11 12 12 // Test server configuration 13 13 const TEST_PORT = 3001; ··· 54 54 const stdoutReader = serverProcess.stdout.getReader(); 55 55 const stderrReader = serverProcess.stderr.getReader(); 56 56 const decoder = new TextDecoder(); 57 - 57 + 58 58 (async () => { 59 59 try { 60 60 while (true) { ··· 65 65 } 66 66 } catch {} 67 67 })(); 68 - 68 + 69 69 (async () => { 70 70 try { 71 71 while (true) { ··· 123 123 // Clear database between each test 124 124 beforeEach(async () => { 125 125 const db = require("bun:sqlite").Database.open(TEST_DB_PATH); 126 - 126 + 127 127 // Delete all data from tables (preserve schema) 128 128 db.run("DELETE FROM rate_limit_attempts"); 129 129 db.run("DELETE FROM email_change_tokens"); ··· 138 138 db.run("DELETE FROM classes"); 139 139 db.run("DELETE FROM class_waitlist"); 140 140 db.run("DELETE FROM users WHERE id != 0"); // Keep ghost user 141 - 141 + 142 142 db.close(); 143 143 }); 144 144 ··· 194 194 } 195 195 196 196 // Helper to register a user, verify email, and get session via login 197 - async function registerAndLogin(user: { email: string; password: string; name?: string }): Promise<string> { 197 + async function registerAndLogin(user: { 198 + email: string; 199 + password: string; 200 + name?: string; 201 + }): Promise<string> { 198 202 const hashedPassword = await clientHashPassword(user.email, user.password); 199 203 200 204 // Register the user ··· 242 246 // Helper to add active subscription to a user 243 247 function addSubscription(userEmail: string): void { 244 248 const db = require("bun:sqlite").Database.open(TEST_DB_PATH); 245 - const user = db.query("SELECT id FROM users WHERE email = ?").get(userEmail) as { id: number }; 249 + const user = db 250 + .query("SELECT id FROM users WHERE email = ?") 251 + .get(userEmail) as { id: number }; 246 252 if (!user) { 247 253 db.close(); 248 254 throw new Error(`User ${userEmail} not found`); 249 255 } 250 - 256 + 251 257 db.run( 252 258 "INSERT INTO subscriptions (id, user_id, customer_id, status) VALUES (?, ?, ?, ?)", 253 - [`test-sub-${user.id}`, user.id, `test-customer-${user.id}`, "active"] 259 + [`test-sub-${user.id}`, user.id, `test-customer-${user.id}`, "active"], 254 260 ); 255 261 db.close(); 256 262 } ··· 281 287 } 282 288 283 289 expect(response.status).toBe(201); 284 - 290 + 285 291 const data = await response.json(); 286 292 expect(data.user).toBeDefined(); 287 293 expect(data.user.email).toBe(TEST_USER.email); ··· 302 308 expect(data.error).toBe("Email and password required"); 303 309 }); 304 310 305 - test( 306 - "should reject registration with invalid password format", 307 - async () => { 308 - const response = await fetch(`${BASE_URL}/api/auth/register`, { 309 - method: "POST", 310 - headers: { "Content-Type": "application/json" }, 311 - body: JSON.stringify({ 312 - email: TEST_USER.email, 313 - password: "short", 314 - }), 315 - }); 311 + test("should reject registration with invalid password format", async () => { 312 + const response = await fetch(`${BASE_URL}/api/auth/register`, { 313 + method: "POST", 314 + headers: { "Content-Type": "application/json" }, 315 + body: JSON.stringify({ 316 + email: TEST_USER.email, 317 + password: "short", 318 + }), 319 + }); 316 320 317 - expect(response.status).toBe(400); 318 - const data = await response.json(); 319 - expect(data.error).toBe("Invalid password format"); 320 - }, 321 - ); 321 + expect(response.status).toBe(400); 322 + const data = await response.json(); 323 + expect(data.error).toBe("Invalid password format"); 324 + }); 322 325 323 326 test("should reject duplicate email registration", async () => { 324 327 const hashedPassword = await clientHashPassword( ··· 473 476 474 477 // Manually complete the email change in the database (simulating verification) 475 478 const db = require("bun:sqlite").Database.open(TEST_DB_PATH); 476 - const tokenData = db.query("SELECT user_id, new_email FROM email_change_tokens ORDER BY created_at DESC LIMIT 1").get() as { user_id: number, new_email: string }; 477 - db.run("UPDATE users SET email = ?, email_verified = 1 WHERE id = ?", [tokenData.new_email, tokenData.user_id]); 478 - db.run("DELETE FROM email_change_tokens WHERE user_id = ?", [tokenData.user_id]); 479 + const tokenData = db 480 + .query( 481 + "SELECT user_id, new_email FROM email_change_tokens ORDER BY created_at DESC LIMIT 1", 482 + ) 483 + .get() as { user_id: number; new_email: string }; 484 + db.run("UPDATE users SET email = ?, email_verified = 1 WHERE id = ?", [ 485 + tokenData.new_email, 486 + tokenData.user_id, 487 + ]); 488 + db.run("DELETE FROM email_change_tokens WHERE user_id = ?", [ 489 + tokenData.user_id, 490 + ]); 479 491 db.close(); 480 492 481 493 // Verify email updated ··· 646 658 647 659 describe("API Endpoints - Health", () => { 648 660 describe("GET /api/health", () => { 649 - test( 650 - "should return service health status with details", 651 - async () => { 652 - const response = await fetch(`${BASE_URL}/api/health`); 661 + test("should return service health status with details", async () => { 662 + const response = await fetch(`${BASE_URL}/api/health`); 653 663 654 - expect(response.status).toBe(200); 655 - const data = await response.json(); 656 - expect(data).toHaveProperty("status"); 657 - expect(data).toHaveProperty("timestamp"); 658 - expect(data).toHaveProperty("services"); 659 - expect(data.services).toHaveProperty("database"); 660 - expect(data.services).toHaveProperty("whisper"); 661 - expect(data.services).toHaveProperty("storage"); 662 - }, 663 - ); 664 + expect(response.status).toBe(200); 665 + const data = await response.json(); 666 + expect(data).toHaveProperty("status"); 667 + expect(data).toHaveProperty("timestamp"); 668 + expect(data).toHaveProperty("services"); 669 + expect(data.services).toHaveProperty("database"); 670 + expect(data.services).toHaveProperty("whisper"); 671 + expect(data.services).toHaveProperty("storage"); 672 + }); 664 673 }); 665 674 }); 666 675 ··· 669 678 test("should return user transcriptions", async () => { 670 679 // Register and login 671 680 const sessionCookie = await registerAndLogin(TEST_USER); 672 - 681 + 673 682 // Add subscription 674 683 addSubscription(TEST_USER.email); 675 684 ··· 696 705 test("should upload audio file and start transcription", async () => { 697 706 // Register and login 698 707 const sessionCookie = await registerAndLogin(TEST_USER); 699 - 708 + 700 709 // Add subscription 701 710 addSubscription(TEST_USER.email); 702 711 ··· 725 734 test("should reject non-audio files", async () => { 726 735 // Register and login 727 736 const sessionCookie = await registerAndLogin(TEST_USER); 728 - 737 + 729 738 // Add subscription 730 739 addSubscription(TEST_USER.email); 731 740 ··· 749 758 test("should reject files exceeding size limit", async () => { 750 759 // Register and login 751 760 const sessionCookie = await registerAndLogin(TEST_USER); 752 - 761 + 753 762 // Add subscription 754 763 addSubscription(TEST_USER.email); 755 764 ··· 797 806 beforeEach(async () => { 798 807 // Create admin user 799 808 adminCookie = await registerAndLogin(TEST_ADMIN); 800 - 809 + 801 810 // Manually set admin role in database 802 811 const db = require("bun:sqlite").Database.open(TEST_DB_PATH); 803 812 db.run("UPDATE users SET role = 'admin' WHERE email = ?", [ ··· 812 821 .query<{ id: number }, [string]>("SELECT id FROM users WHERE email = ?") 813 822 .get(TEST_USER.email); 814 823 userId = userIdResult?.id; 815 - 824 + 816 825 db.close(); 817 826 }); 818 827 ··· 1095 1104 }); 1096 1105 1097 1106 describe("POST /api/passkeys/register/options", () => { 1098 - test( 1099 - "should return registration options for authenticated user", 1100 - async () => { 1101 - const response = await authRequest( 1102 - `${BASE_URL}/api/passkeys/register/options`, 1103 - sessionCookie, 1104 - { 1105 - method: "POST", 1106 - }, 1107 - ); 1107 + test("should return registration options for authenticated user", async () => { 1108 + const response = await authRequest( 1109 + `${BASE_URL}/api/passkeys/register/options`, 1110 + sessionCookie, 1111 + { 1112 + method: "POST", 1113 + }, 1114 + ); 1108 1115 1109 - expect(response.status).toBe(200); 1110 - const data = await response.json(); 1111 - expect(data).toHaveProperty("challenge"); 1112 - expect(data).toHaveProperty("rp"); 1113 - expect(data).toHaveProperty("user"); 1114 - }, 1115 - ); 1116 + expect(response.status).toBe(200); 1117 + const data = await response.json(); 1118 + expect(data).toHaveProperty("challenge"); 1119 + expect(data).toHaveProperty("rp"); 1120 + expect(data).toHaveProperty("user"); 1121 + }); 1116 1122 1117 1123 test("should require authentication", async () => { 1118 1124 const response = await fetch(
+3 -1
src/lib/email.ts
··· 27 27 export async function sendEmail(options: SendEmailOptions): Promise<void> { 28 28 // Skip sending emails in test mode 29 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}`); 30 + console.log( 31 + `[Email] SKIPPED (test mode): "${options.subject}" to ${typeof options.to === "string" ? options.to : options.to.email}`, 32 + ); 31 33 return; 32 34 } 33 35
+3 -3
src/lib/vtt-cleaner.test.ts
··· 55 55 56 56 test("cleanVTT preserves empty VTT", async () => { 57 57 const emptyVTT = "WEBVTT\n\n"; 58 - 58 + 59 59 // Save and remove API key to avoid burning tokens 60 60 const originalKey = process.env.LLM_API_KEY; 61 61 delete process.env.LLM_API_KEY; 62 - 62 + 63 63 const result = await cleanVTT("test-empty", emptyVTT); 64 64 65 65 expect(result).toBe(emptyVTT); 66 - 66 + 67 67 // Restore original key 68 68 if (originalKey) { 69 69 process.env.LLM_API_KEY = originalKey;