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