🪻 distributed transcription service thistle.dunkirk.sh
1import { describe, expect, test, beforeAll, afterAll, beforeEach } from "bun:test"; 2import db from "./db/schema"; 3import { hashPasswordClient } from "./lib/client-auth"; 4 5// Test server URL - uses port 3001 for testing to avoid conflicts 6const TEST_PORT = 3001; 7const BASE_URL = `http://localhost:${TEST_PORT}`; 8 9// Check if server is available 10let serverAvailable = false; 11 12beforeAll(async () => { 13 try { 14 const response = await fetch(`${BASE_URL}/api/transcriptions/health`, { 15 signal: AbortSignal.timeout(1000), 16 }); 17 serverAvailable = response.ok || response.status === 404; 18 } catch { 19 console.warn( 20 `\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` 21 ); 22 serverAvailable = false; 23 } 24}); 25 26// Test user credentials 27const TEST_USER = { 28 email: "test@example.com", 29 password: "TestPassword123!", 30 name: "Test User", 31}; 32 33const TEST_ADMIN = { 34 email: "admin@example.com", 35 password: "AdminPassword123!", 36 name: "Admin User", 37}; 38 39const TEST_USER_2 = { 40 email: "test2@example.com", 41 password: "TestPassword456!", 42 name: "Test User 2", 43}; 44 45// Helper to hash passwords like the client would 46async function clientHashPassword(email: string, password: string): Promise<string> { 47 return await hashPasswordClient(password, email); 48} 49 50// Helper to extract session cookie 51function extractSessionCookie(response: Response): string | null { 52 const setCookie = response.headers.get("set-cookie"); 53 if (!setCookie) return null; 54 const match = setCookie.match(/session=([^;]+)/); 55 return match ? match[1] : null; 56} 57 58// Helper to make authenticated requests 59function authRequest( 60 url: string, 61 sessionCookie: string, 62 options: RequestInit = {}, 63): Promise<Response> { 64 return fetch(url, { 65 ...options, 66 headers: { 67 ...options.headers, 68 Cookie: `session=${sessionCookie}`, 69 }, 70 }); 71} 72 73// Cleanup helpers 74function cleanupTestData() { 75 // Delete test users and their related data (cascade will handle most of it) 76 db.run("DELETE FROM sessions WHERE user_id IN (SELECT id FROM users WHERE email LIKE 'test%' OR email LIKE 'admin@%')"); 77 db.run("DELETE FROM passkeys WHERE user_id IN (SELECT id FROM users WHERE email LIKE 'test%' OR email LIKE 'admin@%')"); 78 db.run("DELETE FROM transcriptions WHERE user_id IN (SELECT id FROM users WHERE email LIKE 'test%' OR email LIKE 'admin@%')"); 79 db.run("DELETE FROM users WHERE email LIKE 'test%' OR email LIKE 'admin@%'"); 80 81 // Clear ALL rate limit data to prevent accumulation across tests 82 // (IP-based rate limits don't contain test/admin in the key) 83 db.run("DELETE FROM rate_limit_attempts"); 84} 85 86beforeEach(() => { 87 if (serverAvailable) { 88 cleanupTestData(); 89 } 90}); 91 92afterAll(() => { 93 if (serverAvailable) { 94 cleanupTestData(); 95 } 96}); 97 98// Helper to skip tests if server is not available 99function serverTest(name: string, fn: () => void | Promise<void>) { 100 test(name, async () => { 101 if (!serverAvailable) { 102 console.log(`⏭️ Skipping: ${name} (server not running)`); 103 return; 104 } 105 await fn(); 106 }); 107} 108 109describe("API Endpoints - Authentication", () => { 110 describe("POST /api/auth/register", () => { 111 serverTest("should register a new user successfully", async () => { 112 const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 113 114 const response = await fetch(`${BASE_URL}/api/auth/register`, { 115 method: "POST", 116 headers: { "Content-Type": "application/json" }, 117 body: JSON.stringify({ 118 email: TEST_USER.email, 119 password: hashedPassword, 120 name: TEST_USER.name, 121 }), 122 }); 123 124 expect(response.status).toBe(200); 125 const data = await response.json(); 126 expect(data.user).toBeDefined(); 127 expect(data.user.email).toBe(TEST_USER.email); 128 expect(extractSessionCookie(response)).toBeTruthy(); 129 }); 130 131 serverTest("should reject registration with missing email", async () => { 132 const response = await fetch(`${BASE_URL}/api/auth/register`, { 133 method: "POST", 134 headers: { "Content-Type": "application/json" }, 135 body: JSON.stringify({ 136 password: "hashedpassword123456", 137 }), 138 }); 139 140 expect(response.status).toBe(400); 141 const data = await response.json(); 142 expect(data.error).toBe("Email and password required"); 143 }); 144 145 serverTest("should reject registration with invalid password format", async () => { 146 const response = await fetch(`${BASE_URL}/api/auth/register`, { 147 method: "POST", 148 headers: { "Content-Type": "application/json" }, 149 body: JSON.stringify({ 150 email: TEST_USER.email, 151 password: "short", 152 }), 153 }); 154 155 expect(response.status).toBe(400); 156 const data = await response.json(); 157 expect(data.error).toBe("Invalid password format"); 158 }); 159 160 serverTest("should reject duplicate email registration", async () => { 161 const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 162 163 // First registration 164 await fetch(`${BASE_URL}/api/auth/register`, { 165 method: "POST", 166 headers: { "Content-Type": "application/json" }, 167 body: JSON.stringify({ 168 email: TEST_USER.email, 169 password: hashedPassword, 170 name: TEST_USER.name, 171 }), 172 }); 173 174 // Duplicate registration 175 const response = await fetch(`${BASE_URL}/api/auth/register`, { 176 method: "POST", 177 headers: { "Content-Type": "application/json" }, 178 body: JSON.stringify({ 179 email: TEST_USER.email, 180 password: hashedPassword, 181 name: TEST_USER.name, 182 }), 183 }); 184 185 expect(response.status).toBe(400); 186 const data = await response.json(); 187 expect(data.error).toBe("Email already registered"); 188 }); 189 190 serverTest("should enforce rate limiting on registration", async () => { 191 const hashedPassword = await clientHashPassword("test@example.com", "password"); 192 193 // Make registration attempts until rate limit is hit (limit is 5 per hour) 194 let rateLimitHit = false; 195 for (let i = 0; i < 10; i++) { 196 const response = await fetch(`${BASE_URL}/api/auth/register`, { 197 method: "POST", 198 headers: { "Content-Type": "application/json" }, 199 body: JSON.stringify({ 200 email: `test${i}@example.com`, 201 password: hashedPassword, 202 }), 203 }); 204 205 if (response.status === 429) { 206 rateLimitHit = true; 207 break; 208 } 209 } 210 211 // Verify that rate limiting was triggered 212 expect(rateLimitHit).toBe(true); 213 }); 214 }); 215 216 describe("POST /api/auth/login", () => { 217 serverTest("should login successfully with valid credentials", async () => { 218 // Register user first 219 const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 220 await fetch(`${BASE_URL}/api/auth/register`, { 221 method: "POST", 222 headers: { "Content-Type": "application/json" }, 223 body: JSON.stringify({ 224 email: TEST_USER.email, 225 password: hashedPassword, 226 name: TEST_USER.name, 227 }), 228 }); 229 230 // Login 231 const response = await fetch(`${BASE_URL}/api/auth/login`, { 232 method: "POST", 233 headers: { "Content-Type": "application/json" }, 234 body: JSON.stringify({ 235 email: TEST_USER.email, 236 password: hashedPassword, 237 }), 238 }); 239 240 expect(response.status).toBe(200); 241 const data = await response.json(); 242 expect(data.user).toBeDefined(); 243 expect(data.user.email).toBe(TEST_USER.email); 244 expect(extractSessionCookie(response)).toBeTruthy(); 245 }); 246 247 serverTest("should reject login with invalid credentials", async () => { 248 // Register user first 249 const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 250 await fetch(`${BASE_URL}/api/auth/register`, { 251 method: "POST", 252 headers: { "Content-Type": "application/json" }, 253 body: JSON.stringify({ 254 email: TEST_USER.email, 255 password: hashedPassword, 256 }), 257 }); 258 259 // Login with wrong password 260 const wrongPassword = await clientHashPassword(TEST_USER.email, "WrongPassword123!"); 261 const response = await fetch(`${BASE_URL}/api/auth/login`, { 262 method: "POST", 263 headers: { "Content-Type": "application/json" }, 264 body: JSON.stringify({ 265 email: TEST_USER.email, 266 password: wrongPassword, 267 }), 268 }); 269 270 expect(response.status).toBe(401); 271 const data = await response.json(); 272 expect(data.error).toBe("Invalid email or password"); 273 }); 274 275 serverTest("should reject login with missing fields", async () => { 276 const response = await fetch(`${BASE_URL}/api/auth/login`, { 277 method: "POST", 278 headers: { "Content-Type": "application/json" }, 279 body: JSON.stringify({ 280 email: TEST_USER.email, 281 }), 282 }); 283 284 expect(response.status).toBe(400); 285 const data = await response.json(); 286 expect(data.error).toBe("Email and password required"); 287 }); 288 289 serverTest("should enforce rate limiting on login attempts", async () => { 290 const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 291 292 // Make 11 login attempts (limit is 10 per 15 minutes per IP) 293 let rateLimitHit = false; 294 for (let i = 0; i < 11; i++) { 295 const response = await fetch(`${BASE_URL}/api/auth/login`, { 296 method: "POST", 297 headers: { "Content-Type": "application/json" }, 298 body: JSON.stringify({ 299 email: TEST_USER.email, 300 password: hashedPassword, 301 }), 302 }); 303 304 if (response.status === 429) { 305 rateLimitHit = true; 306 break; 307 } 308 } 309 310 // Verify that rate limiting was triggered 311 expect(rateLimitHit).toBe(true); 312 }); 313 }); 314 315 describe("POST /api/auth/logout", () => { 316 serverTest("should logout successfully", async () => { 317 // Register and login 318 const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 319 const loginResponse = await fetch(`${BASE_URL}/api/auth/register`, { 320 method: "POST", 321 headers: { "Content-Type": "application/json" }, 322 body: JSON.stringify({ 323 email: TEST_USER.email, 324 password: hashedPassword, 325 }), 326 }); 327 const sessionCookie = extractSessionCookie(loginResponse)!; 328 329 // Logout 330 const response = await authRequest(`${BASE_URL}/api/auth/logout`, sessionCookie, { 331 method: "POST", 332 }); 333 334 expect(response.status).toBe(200); 335 const data = await response.json(); 336 expect(data.success).toBe(true); 337 338 // Verify cookie is cleared 339 const setCookie = response.headers.get("set-cookie"); 340 expect(setCookie).toContain("Max-Age=0"); 341 }); 342 343 serverTest("should logout even without valid session", async () => { 344 const response = await fetch(`${BASE_URL}/api/auth/logout`, { 345 method: "POST", 346 }); 347 348 expect(response.status).toBe(200); 349 const data = await response.json(); 350 expect(data.success).toBe(true); 351 }); 352 }); 353 354 describe("GET /api/auth/me", () => { 355 serverTest("should return current user info when authenticated", async () => { 356 // Register user 357 const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 358 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 359 method: "POST", 360 headers: { "Content-Type": "application/json" }, 361 body: JSON.stringify({ 362 email: TEST_USER.email, 363 password: hashedPassword, 364 name: TEST_USER.name, 365 }), 366 }); 367 const sessionCookie = extractSessionCookie(registerResponse)!; 368 369 // Get current user 370 const response = await authRequest(`${BASE_URL}/api/auth/me`, sessionCookie); 371 372 expect(response.status).toBe(200); 373 const data = await response.json(); 374 expect(data.email).toBe(TEST_USER.email); 375 expect(data.name).toBe(TEST_USER.name); 376 expect(data.role).toBeDefined(); 377 }); 378 379 serverTest("should return 401 when not authenticated", async () => { 380 const response = await fetch(`${BASE_URL}/api/auth/me`); 381 382 expect(response.status).toBe(401); 383 const data = await response.json(); 384 expect(data.error).toBe("Not authenticated"); 385 }); 386 387 serverTest("should return 401 with invalid session", async () => { 388 const response = await authRequest(`${BASE_URL}/api/auth/me`, "invalid-session"); 389 390 expect(response.status).toBe(401); 391 const data = await response.json(); 392 expect(data.error).toBe("Invalid session"); 393 }); 394 }); 395}); 396 397describe("API Endpoints - Session Management", () => { 398 describe("GET /api/sessions", () => { 399 serverTest("should return user sessions", async () => { 400 // Register user 401 const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 402 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 403 method: "POST", 404 headers: { "Content-Type": "application/json" }, 405 body: JSON.stringify({ 406 email: TEST_USER.email, 407 password: hashedPassword, 408 }), 409 }); 410 const sessionCookie = extractSessionCookie(registerResponse)!; 411 412 // Get sessions 413 const response = await authRequest(`${BASE_URL}/api/sessions`, sessionCookie); 414 415 expect(response.status).toBe(200); 416 const data = await response.json(); 417 expect(data.sessions).toBeDefined(); 418 expect(data.sessions.length).toBeGreaterThan(0); 419 expect(data.sessions[0]).toHaveProperty("id"); 420 expect(data.sessions[0]).toHaveProperty("ip_address"); 421 expect(data.sessions[0]).toHaveProperty("user_agent"); 422 }); 423 424 serverTest("should require authentication", async () => { 425 const response = await fetch(`${BASE_URL}/api/sessions`); 426 427 expect(response.status).toBe(401); 428 }); 429 }); 430 431 describe("DELETE /api/sessions", () => { 432 serverTest("should delete specific session", async () => { 433 // Register user and create multiple sessions 434 const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 435 const session1Response = await fetch(`${BASE_URL}/api/auth/register`, { 436 method: "POST", 437 headers: { "Content-Type": "application/json" }, 438 body: JSON.stringify({ 439 email: TEST_USER.email, 440 password: hashedPassword, 441 }), 442 }); 443 const session1Cookie = extractSessionCookie(session1Response)!; 444 445 const session2Response = await fetch(`${BASE_URL}/api/auth/login`, { 446 method: "POST", 447 headers: { "Content-Type": "application/json" }, 448 body: JSON.stringify({ 449 email: TEST_USER.email, 450 password: hashedPassword, 451 }), 452 }); 453 const session2Cookie = extractSessionCookie(session2Response)!; 454 455 // Get sessions list 456 const sessionsResponse = await authRequest(`${BASE_URL}/api/sessions`, session1Cookie); 457 const sessionsData = await sessionsResponse.json(); 458 const targetSessionId = sessionsData.sessions.find( 459 (s: any) => s.id === session2Cookie 460 )?.id; 461 462 // Delete session 2 463 const response = await authRequest(`${BASE_URL}/api/sessions`, session1Cookie, { 464 method: "DELETE", 465 headers: { "Content-Type": "application/json" }, 466 body: JSON.stringify({ sessionId: targetSessionId }), 467 }); 468 469 expect(response.status).toBe(200); 470 const data = await response.json(); 471 expect(data.success).toBe(true); 472 473 // Verify session 2 is deleted 474 const verifyResponse = await authRequest(`${BASE_URL}/api/auth/me`, session2Cookie); 475 expect(verifyResponse.status).toBe(401); 476 }); 477 478 serverTest("should not delete another user's session", async () => { 479 // Register two users 480 const hashedPassword1 = await clientHashPassword(TEST_USER.email, TEST_USER.password); 481 const user1Response = await fetch(`${BASE_URL}/api/auth/register`, { 482 method: "POST", 483 headers: { "Content-Type": "application/json" }, 484 body: JSON.stringify({ 485 email: TEST_USER.email, 486 password: hashedPassword1, 487 }), 488 }); 489 const user1Cookie = extractSessionCookie(user1Response)!; 490 491 const hashedPassword2 = await clientHashPassword(TEST_USER_2.email, TEST_USER_2.password); 492 const user2Response = await fetch(`${BASE_URL}/api/auth/register`, { 493 method: "POST", 494 headers: { "Content-Type": "application/json" }, 495 body: JSON.stringify({ 496 email: TEST_USER_2.email, 497 password: hashedPassword2, 498 }), 499 }); 500 const user2Cookie = extractSessionCookie(user2Response)!; 501 502 // Try to delete user2's session using user1's credentials 503 const response = await authRequest(`${BASE_URL}/api/sessions`, user1Cookie, { 504 method: "DELETE", 505 headers: { "Content-Type": "application/json" }, 506 body: JSON.stringify({ sessionId: user2Cookie }), 507 }); 508 509 expect(response.status).toBe(404); 510 }); 511 }); 512}); 513 514describe("API Endpoints - User Management", () => { 515 describe("DELETE /api/user", () => { 516 serverTest("should delete user account", async () => { 517 // Register user 518 const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 519 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 520 method: "POST", 521 headers: { "Content-Type": "application/json" }, 522 body: JSON.stringify({ 523 email: TEST_USER.email, 524 password: hashedPassword, 525 }), 526 }); 527 const sessionCookie = extractSessionCookie(registerResponse)!; 528 529 // Delete account 530 const response = await authRequest(`${BASE_URL}/api/user`, sessionCookie, { 531 method: "DELETE", 532 }); 533 534 expect(response.status).toBe(200); 535 const data = await response.json(); 536 expect(data.success).toBe(true); 537 538 // Verify user is deleted 539 const verifyResponse = await authRequest(`${BASE_URL}/api/auth/me`, sessionCookie); 540 expect(verifyResponse.status).toBe(401); 541 }); 542 543 serverTest("should require authentication", async () => { 544 const response = await fetch(`${BASE_URL}/api/user`, { 545 method: "DELETE", 546 }); 547 548 expect(response.status).toBe(401); 549 }); 550 }); 551 552 describe("PUT /api/user/email", () => { 553 serverTest("should update user email", async () => { 554 // Register user 555 const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 556 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 557 method: "POST", 558 headers: { "Content-Type": "application/json" }, 559 body: JSON.stringify({ 560 email: TEST_USER.email, 561 password: hashedPassword, 562 }), 563 }); 564 const sessionCookie = extractSessionCookie(registerResponse)!; 565 566 // Update email 567 const newEmail = "newemail@example.com"; 568 const response = await authRequest(`${BASE_URL}/api/user/email`, sessionCookie, { 569 method: "PUT", 570 headers: { "Content-Type": "application/json" }, 571 body: JSON.stringify({ email: newEmail }), 572 }); 573 574 expect(response.status).toBe(200); 575 const data = await response.json(); 576 expect(data.success).toBe(true); 577 578 // Verify email updated 579 const meResponse = await authRequest(`${BASE_URL}/api/auth/me`, sessionCookie); 580 const meData = await meResponse.json(); 581 expect(meData.email).toBe(newEmail); 582 }); 583 584 serverTest("should reject duplicate email", async () => { 585 // Register two users 586 const hashedPassword1 = await clientHashPassword(TEST_USER.email, TEST_USER.password); 587 await fetch(`${BASE_URL}/api/auth/register`, { 588 method: "POST", 589 headers: { "Content-Type": "application/json" }, 590 body: JSON.stringify({ 591 email: TEST_USER.email, 592 password: hashedPassword1, 593 }), 594 }); 595 596 const hashedPassword2 = await clientHashPassword(TEST_USER_2.email, TEST_USER_2.password); 597 const user2Response = await fetch(`${BASE_URL}/api/auth/register`, { 598 method: "POST", 599 headers: { "Content-Type": "application/json" }, 600 body: JSON.stringify({ 601 email: TEST_USER_2.email, 602 password: hashedPassword2, 603 }), 604 }); 605 const user2Cookie = extractSessionCookie(user2Response)!; 606 607 // Try to update user2's email to user1's email 608 const response = await authRequest(`${BASE_URL}/api/user/email`, user2Cookie, { 609 method: "PUT", 610 headers: { "Content-Type": "application/json" }, 611 body: JSON.stringify({ email: TEST_USER.email }), 612 }); 613 614 expect(response.status).toBe(400); 615 const data = await response.json(); 616 expect(data.error).toBe("Email already in use"); 617 }); 618 }); 619 620 describe("PUT /api/user/password", () => { 621 serverTest("should update user password", async () => { 622 // Register user 623 const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 624 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 625 method: "POST", 626 headers: { "Content-Type": "application/json" }, 627 body: JSON.stringify({ 628 email: TEST_USER.email, 629 password: hashedPassword, 630 }), 631 }); 632 const sessionCookie = extractSessionCookie(registerResponse)!; 633 634 // Update password 635 const newPassword = await clientHashPassword(TEST_USER.email, "NewPassword123!"); 636 const response = await authRequest(`${BASE_URL}/api/user/password`, sessionCookie, { 637 method: "PUT", 638 headers: { "Content-Type": "application/json" }, 639 body: JSON.stringify({ password: newPassword }), 640 }); 641 642 expect(response.status).toBe(200); 643 const data = await response.json(); 644 expect(data.success).toBe(true); 645 646 // Verify can login with new password 647 const loginResponse = await fetch(`${BASE_URL}/api/auth/login`, { 648 method: "POST", 649 headers: { "Content-Type": "application/json" }, 650 body: JSON.stringify({ 651 email: TEST_USER.email, 652 password: newPassword, 653 }), 654 }); 655 expect(loginResponse.status).toBe(200); 656 }); 657 658 serverTest("should reject invalid password format", async () => { 659 // Register user 660 const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 661 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 662 method: "POST", 663 headers: { "Content-Type": "application/json" }, 664 body: JSON.stringify({ 665 email: TEST_USER.email, 666 password: hashedPassword, 667 }), 668 }); 669 const sessionCookie = extractSessionCookie(registerResponse)!; 670 671 // Try to update with invalid format 672 const response = await authRequest(`${BASE_URL}/api/user/password`, sessionCookie, { 673 method: "PUT", 674 headers: { "Content-Type": "application/json" }, 675 body: JSON.stringify({ password: "short" }), 676 }); 677 678 expect(response.status).toBe(400); 679 const data = await response.json(); 680 expect(data.error).toBe("Invalid password format"); 681 }); 682 }); 683 684 describe("PUT /api/user/name", () => { 685 serverTest("should update user name", async () => { 686 // Register user 687 const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 688 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 689 method: "POST", 690 headers: { "Content-Type": "application/json" }, 691 body: JSON.stringify({ 692 email: TEST_USER.email, 693 password: hashedPassword, 694 name: TEST_USER.name, 695 }), 696 }); 697 const sessionCookie = extractSessionCookie(registerResponse)!; 698 699 // Update name 700 const newName = "Updated Name"; 701 const response = await authRequest(`${BASE_URL}/api/user/name`, sessionCookie, { 702 method: "PUT", 703 headers: { "Content-Type": "application/json" }, 704 body: JSON.stringify({ name: newName }), 705 }); 706 707 expect(response.status).toBe(200); 708 const data = await response.json(); 709 expect(data.success).toBe(true); 710 711 // Verify name updated 712 const meResponse = await authRequest(`${BASE_URL}/api/auth/me`, sessionCookie); 713 const meData = await meResponse.json(); 714 expect(meData.name).toBe(newName); 715 }); 716 717 serverTest("should reject missing name", async () => { 718 // Register user 719 const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 720 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 721 method: "POST", 722 headers: { "Content-Type": "application/json" }, 723 body: JSON.stringify({ 724 email: TEST_USER.email, 725 password: hashedPassword, 726 }), 727 }); 728 const sessionCookie = extractSessionCookie(registerResponse)!; 729 730 const response = await authRequest(`${BASE_URL}/api/user/name`, sessionCookie, { 731 method: "PUT", 732 headers: { "Content-Type": "application/json" }, 733 body: JSON.stringify({}), 734 }); 735 736 expect(response.status).toBe(400); 737 }); 738 }); 739 740 describe("PUT /api/user/avatar", () => { 741 serverTest("should update user avatar", async () => { 742 // Register user 743 const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 744 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 745 method: "POST", 746 headers: { "Content-Type": "application/json" }, 747 body: JSON.stringify({ 748 email: TEST_USER.email, 749 password: hashedPassword, 750 }), 751 }); 752 const sessionCookie = extractSessionCookie(registerResponse)!; 753 754 // Update avatar 755 const newAvatar = "👨‍💻"; 756 const response = await authRequest(`${BASE_URL}/api/user/avatar`, sessionCookie, { 757 method: "PUT", 758 headers: { "Content-Type": "application/json" }, 759 body: JSON.stringify({ avatar: newAvatar }), 760 }); 761 762 expect(response.status).toBe(200); 763 const data = await response.json(); 764 expect(data.success).toBe(true); 765 766 // Verify avatar updated 767 const meResponse = await authRequest(`${BASE_URL}/api/auth/me`, sessionCookie); 768 const meData = await meResponse.json(); 769 expect(meData.avatar).toBe(newAvatar); 770 }); 771 }); 772}); 773 774describe("API Endpoints - Transcriptions", () => { 775 describe("GET /api/transcriptions/health", () => { 776 serverTest("should return transcription service health status", async () => { 777 const response = await fetch(`${BASE_URL}/api/transcriptions/health`); 778 779 expect(response.status).toBe(200); 780 const data = await response.json(); 781 expect(data).toHaveProperty("available"); 782 expect(typeof data.available).toBe("boolean"); 783 }); 784 }); 785 786 describe("GET /api/transcriptions", () => { 787 serverTest("should return user transcriptions", async () => { 788 // Register user 789 const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 790 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 791 method: "POST", 792 headers: { "Content-Type": "application/json" }, 793 body: JSON.stringify({ 794 email: TEST_USER.email, 795 password: hashedPassword, 796 }), 797 }); 798 const sessionCookie = extractSessionCookie(registerResponse)!; 799 800 // Get transcriptions 801 const response = await authRequest(`${BASE_URL}/api/transcriptions`, sessionCookie); 802 803 expect(response.status).toBe(200); 804 const data = await response.json(); 805 expect(data.jobs).toBeDefined(); 806 expect(Array.isArray(data.jobs)).toBe(true); 807 }); 808 809 serverTest("should require authentication", async () => { 810 const response = await fetch(`${BASE_URL}/api/transcriptions`); 811 812 expect(response.status).toBe(401); 813 }); 814 }); 815 816 describe("POST /api/transcriptions", () => { 817 serverTest("should upload audio file and start transcription", async () => { 818 // Register user 819 const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 820 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 821 method: "POST", 822 headers: { "Content-Type": "application/json" }, 823 body: JSON.stringify({ 824 email: TEST_USER.email, 825 password: hashedPassword, 826 }), 827 }); 828 const sessionCookie = extractSessionCookie(registerResponse)!; 829 830 // Create a test audio file 831 const audioBlob = new Blob(["fake audio data"], { type: "audio/mp3" }); 832 const formData = new FormData(); 833 formData.append("audio", audioBlob, "test.mp3"); 834 formData.append("class_name", "Test Class"); 835 836 // Upload 837 const response = await authRequest(`${BASE_URL}/api/transcriptions`, sessionCookie, { 838 method: "POST", 839 body: formData, 840 }); 841 842 expect(response.status).toBe(200); 843 const data = await response.json(); 844 expect(data.id).toBeDefined(); 845 expect(data.message).toContain("Upload successful"); 846 }); 847 848 serverTest("should reject non-audio files", async () => { 849 // Register user 850 const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 851 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 852 method: "POST", 853 headers: { "Content-Type": "application/json" }, 854 body: JSON.stringify({ 855 email: TEST_USER.email, 856 password: hashedPassword, 857 }), 858 }); 859 const sessionCookie = extractSessionCookie(registerResponse)!; 860 861 // Try to upload non-audio file 862 const textBlob = new Blob(["text file"], { type: "text/plain" }); 863 const formData = new FormData(); 864 formData.append("audio", textBlob, "test.txt"); 865 866 const response = await authRequest(`${BASE_URL}/api/transcriptions`, sessionCookie, { 867 method: "POST", 868 body: formData, 869 }); 870 871 expect(response.status).toBe(400); 872 }); 873 874 serverTest("should reject files exceeding size limit", async () => { 875 // Register user 876 const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 877 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 878 method: "POST", 879 headers: { "Content-Type": "application/json" }, 880 body: JSON.stringify({ 881 email: TEST_USER.email, 882 password: hashedPassword, 883 }), 884 }); 885 const sessionCookie = extractSessionCookie(registerResponse)!; 886 887 // Create a file larger than 100MB (the actual limit) 888 const largeBlob = new Blob([new ArrayBuffer(101 * 1024 * 1024)], { type: "audio/mp3" }); 889 const formData = new FormData(); 890 formData.append("audio", largeBlob, "large.mp3"); 891 892 const response = await authRequest(`${BASE_URL}/api/transcriptions`, sessionCookie, { 893 method: "POST", 894 body: formData, 895 }); 896 897 expect(response.status).toBe(400); 898 const data = await response.json(); 899 expect(data.error).toContain("File size must be less than"); 900 }); 901 902 serverTest("should require authentication", async () => { 903 const audioBlob = new Blob(["fake audio data"], { type: "audio/mp3" }); 904 const formData = new FormData(); 905 formData.append("audio", audioBlob, "test.mp3"); 906 907 const response = await fetch(`${BASE_URL}/api/transcriptions`, { 908 method: "POST", 909 body: formData, 910 }); 911 912 expect(response.status).toBe(401); 913 }); 914 }); 915}); 916 917describe("API Endpoints - Admin", () => { 918 let adminCookie: string; 919 let userCookie: string; 920 let userId: number; 921 922 beforeEach(async () => { 923 if (!serverAvailable) return; 924 925 // Create admin user 926 const adminHash = await clientHashPassword(TEST_ADMIN.email, TEST_ADMIN.password); 927 const adminResponse = await fetch(`${BASE_URL}/api/auth/register`, { 928 method: "POST", 929 headers: { "Content-Type": "application/json" }, 930 body: JSON.stringify({ 931 email: TEST_ADMIN.email, 932 password: adminHash, 933 name: TEST_ADMIN.name, 934 }), 935 }); 936 adminCookie = extractSessionCookie(adminResponse)!; 937 938 // Manually set admin role in database 939 db.run("UPDATE users SET role = 'admin' WHERE email = ?", [TEST_ADMIN.email]); 940 941 // Create regular user 942 const userHash = await clientHashPassword(TEST_USER.email, TEST_USER.password); 943 const userResponse = await fetch(`${BASE_URL}/api/auth/register`, { 944 method: "POST", 945 headers: { "Content-Type": "application/json" }, 946 body: JSON.stringify({ 947 email: TEST_USER.email, 948 password: userHash, 949 name: TEST_USER.name, 950 }), 951 }); 952 userCookie = extractSessionCookie(userResponse)!; 953 954 // Get user ID 955 const userIdResult = db.query<{ id: number }, [string]>( 956 "SELECT id FROM users WHERE email = ?" 957 ).get(TEST_USER.email); 958 userId = userIdResult!.id; 959 }); 960 961 describe("GET /api/admin/users", () => { 962 serverTest("should return all users for admin", async () => { 963 const response = await authRequest(`${BASE_URL}/api/admin/users`, adminCookie); 964 965 expect(response.status).toBe(200); 966 const data = await response.json(); 967 expect(Array.isArray(data)).toBe(true); 968 expect(data.length).toBeGreaterThan(0); 969 }); 970 971 serverTest("should reject non-admin users", async () => { 972 const response = await authRequest(`${BASE_URL}/api/admin/users`, userCookie); 973 974 expect(response.status).toBe(403); 975 }); 976 977 serverTest("should require authentication", async () => { 978 const response = await fetch(`${BASE_URL}/api/admin/users`); 979 980 expect(response.status).toBe(401); 981 }); 982 }); 983 984 describe("GET /api/admin/transcriptions", () => { 985 serverTest("should return all transcriptions for admin", async () => { 986 const response = await authRequest(`${BASE_URL}/api/admin/transcriptions`, adminCookie); 987 988 expect(response.status).toBe(200); 989 const data = await response.json(); 990 expect(Array.isArray(data)).toBe(true); 991 }); 992 993 serverTest("should reject non-admin users", async () => { 994 const response = await authRequest(`${BASE_URL}/api/admin/transcriptions`, userCookie); 995 996 expect(response.status).toBe(403); 997 }); 998 }); 999 1000 describe("DELETE /api/admin/users/:id", () => { 1001 serverTest("should delete user as admin", async () => { 1002 const response = await authRequest( 1003 `${BASE_URL}/api/admin/users/${userId}`, 1004 adminCookie, 1005 { 1006 method: "DELETE", 1007 } 1008 ); 1009 1010 expect(response.status).toBe(200); 1011 const data = await response.json(); 1012 expect(data.success).toBe(true); 1013 1014 // Verify user is deleted 1015 const verifyResponse = await authRequest(`${BASE_URL}/api/auth/me`, userCookie); 1016 expect(verifyResponse.status).toBe(401); 1017 }); 1018 1019 serverTest("should reject non-admin users", async () => { 1020 const response = await authRequest( 1021 `${BASE_URL}/api/admin/users/${userId}`, 1022 userCookie, 1023 { 1024 method: "DELETE", 1025 } 1026 ); 1027 1028 expect(response.status).toBe(403); 1029 }); 1030 }); 1031 1032 describe("PUT /api/admin/users/:id/role", () => { 1033 serverTest("should update user role as admin", async () => { 1034 const response = await authRequest( 1035 `${BASE_URL}/api/admin/users/${userId}/role`, 1036 adminCookie, 1037 { 1038 method: "PUT", 1039 headers: { "Content-Type": "application/json" }, 1040 body: JSON.stringify({ role: "admin" }), 1041 } 1042 ); 1043 1044 expect(response.status).toBe(200); 1045 const data = await response.json(); 1046 expect(data.success).toBe(true); 1047 1048 // Verify role updated 1049 const meResponse = await authRequest(`${BASE_URL}/api/auth/me`, userCookie); 1050 const meData = await meResponse.json(); 1051 expect(meData.role).toBe("admin"); 1052 }); 1053 1054 serverTest("should reject invalid roles", async () => { 1055 const response = await authRequest( 1056 `${BASE_URL}/api/admin/users/${userId}/role`, 1057 adminCookie, 1058 { 1059 method: "PUT", 1060 headers: { "Content-Type": "application/json" }, 1061 body: JSON.stringify({ role: "superadmin" }), 1062 } 1063 ); 1064 1065 expect(response.status).toBe(400); 1066 }); 1067 }); 1068 1069 describe("GET /api/admin/users/:id/details", () => { 1070 serverTest("should return user details for admin", async () => { 1071 const response = await authRequest( 1072 `${BASE_URL}/api/admin/users/${userId}/details`, 1073 adminCookie 1074 ); 1075 1076 expect(response.status).toBe(200); 1077 const data = await response.json(); 1078 expect(data.id).toBe(userId); 1079 expect(data.email).toBe(TEST_USER.email); 1080 expect(data).toHaveProperty("passkeys"); 1081 expect(data).toHaveProperty("sessions"); 1082 }); 1083 1084 serverTest("should reject non-admin users", async () => { 1085 const response = await authRequest( 1086 `${BASE_URL}/api/admin/users/${userId}/details`, 1087 userCookie 1088 ); 1089 1090 expect(response.status).toBe(403); 1091 }); 1092 }); 1093 1094 describe("PUT /api/admin/users/:id/name", () => { 1095 serverTest("should update user name as admin", async () => { 1096 const newName = "Admin Updated Name"; 1097 const response = await authRequest( 1098 `${BASE_URL}/api/admin/users/${userId}/name`, 1099 adminCookie, 1100 { 1101 method: "PUT", 1102 headers: { "Content-Type": "application/json" }, 1103 body: JSON.stringify({ name: newName }), 1104 } 1105 ); 1106 1107 expect(response.status).toBe(200); 1108 const data = await response.json(); 1109 expect(data.success).toBe(true); 1110 }); 1111 1112 serverTest("should reject empty names", async () => { 1113 const response = await authRequest( 1114 `${BASE_URL}/api/admin/users/${userId}/name`, 1115 adminCookie, 1116 { 1117 method: "PUT", 1118 headers: { "Content-Type": "application/json" }, 1119 body: JSON.stringify({ name: "" }), 1120 } 1121 ); 1122 1123 expect(response.status).toBe(400); 1124 }); 1125 }); 1126 1127 describe("PUT /api/admin/users/:id/email", () => { 1128 serverTest("should update user email as admin", async () => { 1129 const newEmail = "newemail@admin.com"; 1130 const response = await authRequest( 1131 `${BASE_URL}/api/admin/users/${userId}/email`, 1132 adminCookie, 1133 { 1134 method: "PUT", 1135 headers: { "Content-Type": "application/json" }, 1136 body: JSON.stringify({ email: newEmail }), 1137 } 1138 ); 1139 1140 expect(response.status).toBe(200); 1141 const data = await response.json(); 1142 expect(data.success).toBe(true); 1143 }); 1144 1145 serverTest("should reject duplicate emails", async () => { 1146 const response = await authRequest( 1147 `${BASE_URL}/api/admin/users/${userId}/email`, 1148 adminCookie, 1149 { 1150 method: "PUT", 1151 headers: { "Content-Type": "application/json" }, 1152 body: JSON.stringify({ email: TEST_ADMIN.email }), 1153 } 1154 ); 1155 1156 expect(response.status).toBe(400); 1157 const data = await response.json(); 1158 expect(data.error).toBe("Email already in use"); 1159 }); 1160 }); 1161 1162 describe("GET /api/admin/users/:id/sessions", () => { 1163 serverTest("should return user sessions as admin", async () => { 1164 const response = await authRequest( 1165 `${BASE_URL}/api/admin/users/${userId}/sessions`, 1166 adminCookie 1167 ); 1168 1169 expect(response.status).toBe(200); 1170 const data = await response.json(); 1171 expect(Array.isArray(data)).toBe(true); 1172 }); 1173 }); 1174 1175 describe("DELETE /api/admin/users/:id/sessions", () => { 1176 serverTest("should delete all user sessions as admin", async () => { 1177 const response = await authRequest( 1178 `${BASE_URL}/api/admin/users/${userId}/sessions`, 1179 adminCookie, 1180 { 1181 method: "DELETE", 1182 } 1183 ); 1184 1185 expect(response.status).toBe(200); 1186 const data = await response.json(); 1187 expect(data.success).toBe(true); 1188 1189 // Verify sessions are deleted 1190 const verifyResponse = await authRequest(`${BASE_URL}/api/auth/me`, userCookie); 1191 expect(verifyResponse.status).toBe(401); 1192 }); 1193 }); 1194}); 1195 1196describe("API Endpoints - Passkeys", () => { 1197 let sessionCookie: string; 1198 1199 beforeEach(async () => { 1200 if (!serverAvailable) return; 1201 1202 // Register user 1203 const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password); 1204 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 1205 method: "POST", 1206 headers: { "Content-Type": "application/json" }, 1207 body: JSON.stringify({ 1208 email: TEST_USER.email, 1209 password: hashedPassword, 1210 }), 1211 }); 1212 sessionCookie = extractSessionCookie(registerResponse)!; 1213 }); 1214 1215 describe("GET /api/passkeys", () => { 1216 serverTest("should return user passkeys", async () => { 1217 const response = await authRequest(`${BASE_URL}/api/passkeys`, sessionCookie); 1218 1219 expect(response.status).toBe(200); 1220 const data = await response.json(); 1221 expect(data.passkeys).toBeDefined(); 1222 expect(Array.isArray(data.passkeys)).toBe(true); 1223 }); 1224 1225 serverTest("should require authentication", async () => { 1226 const response = await fetch(`${BASE_URL}/api/passkeys`); 1227 1228 expect(response.status).toBe(401); 1229 }); 1230 }); 1231 1232 describe("POST /api/passkeys/register/options", () => { 1233 serverTest("should return registration options for authenticated user", async () => { 1234 const response = await authRequest( 1235 `${BASE_URL}/api/passkeys/register/options`, 1236 sessionCookie, 1237 { 1238 method: "POST", 1239 } 1240 ); 1241 1242 expect(response.status).toBe(200); 1243 const data = await response.json(); 1244 expect(data).toHaveProperty("challenge"); 1245 expect(data).toHaveProperty("rp"); 1246 expect(data).toHaveProperty("user"); 1247 }); 1248 1249 serverTest("should require authentication", async () => { 1250 const response = await fetch(`${BASE_URL}/api/passkeys/register/options`, { 1251 method: "POST", 1252 }); 1253 1254 expect(response.status).toBe(401); 1255 }); 1256 }); 1257 1258 describe("POST /api/passkeys/authenticate/options", () => { 1259 serverTest("should return authentication options for email", async () => { 1260 const response = await fetch(`${BASE_URL}/api/passkeys/authenticate/options`, { 1261 method: "POST", 1262 headers: { "Content-Type": "application/json" }, 1263 body: JSON.stringify({ email: TEST_USER.email }), 1264 }); 1265 1266 expect(response.status).toBe(200); 1267 const data = await response.json(); 1268 expect(data).toHaveProperty("challenge"); 1269 }); 1270 1271 serverTest("should handle non-existent email", async () => { 1272 const response = await fetch(`${BASE_URL}/api/passkeys/authenticate/options`, { 1273 method: "POST", 1274 headers: { "Content-Type": "application/json" }, 1275 body: JSON.stringify({ email: "nonexistent@example.com" }), 1276 }); 1277 1278 // Should still return options for privacy (don't leak user existence) 1279 expect([200, 404]).toContain(response.status); 1280 }); 1281 }); 1282});