🪻 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}); 605 606describe("API Endpoints - User Management", () => { 607 describe("DELETE /api/user", () => { 608 serverTest("should delete user account", async () => { 609 // Register user 610 const hashedPassword = await clientHashPassword( 611 TEST_USER.email, 612 TEST_USER.password, 613 ); 614 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 615 method: "POST", 616 headers: { "Content-Type": "application/json" }, 617 body: JSON.stringify({ 618 email: TEST_USER.email, 619 password: hashedPassword, 620 }), 621 }); 622 const sessionCookie = extractSessionCookie(registerResponse); 623 624 // Delete account 625 const response = await authRequest( 626 `${BASE_URL}/api/user`, 627 sessionCookie, 628 { 629 method: "DELETE", 630 }, 631 ); 632 633 expect(response.status).toBe(200); 634 const data = await response.json(); 635 expect(data.success).toBe(true); 636 637 // Verify user is deleted 638 const verifyResponse = await authRequest( 639 `${BASE_URL}/api/auth/me`, 640 sessionCookie, 641 ); 642 expect(verifyResponse.status).toBe(401); 643 }); 644 645 serverTest("should require authentication", async () => { 646 const response = await fetch(`${BASE_URL}/api/user`, { 647 method: "DELETE", 648 }); 649 650 expect(response.status).toBe(401); 651 }); 652 }); 653 654 describe("PUT /api/user/email", () => { 655 serverTest("should update user email", async () => { 656 // Register user 657 const hashedPassword = await clientHashPassword( 658 TEST_USER.email, 659 TEST_USER.password, 660 ); 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 // Update email 672 const newEmail = "newemail@example.com"; 673 const response = await authRequest( 674 `${BASE_URL}/api/user/email`, 675 sessionCookie, 676 { 677 method: "PUT", 678 headers: { "Content-Type": "application/json" }, 679 body: JSON.stringify({ email: newEmail }), 680 }, 681 ); 682 683 expect(response.status).toBe(200); 684 const data = await response.json(); 685 expect(data.success).toBe(true); 686 687 // Verify email updated 688 const meResponse = await authRequest( 689 `${BASE_URL}/api/auth/me`, 690 sessionCookie, 691 ); 692 const meData = await meResponse.json(); 693 expect(meData.email).toBe(newEmail); 694 }); 695 696 serverTest("should reject duplicate email", async () => { 697 // Register two users 698 const hashedPassword1 = await clientHashPassword( 699 TEST_USER.email, 700 TEST_USER.password, 701 ); 702 await fetch(`${BASE_URL}/api/auth/register`, { 703 method: "POST", 704 headers: { "Content-Type": "application/json" }, 705 body: JSON.stringify({ 706 email: TEST_USER.email, 707 password: hashedPassword1, 708 }), 709 }); 710 711 const hashedPassword2 = await clientHashPassword( 712 TEST_USER_2.email, 713 TEST_USER_2.password, 714 ); 715 const user2Response = await fetch(`${BASE_URL}/api/auth/register`, { 716 method: "POST", 717 headers: { "Content-Type": "application/json" }, 718 body: JSON.stringify({ 719 email: TEST_USER_2.email, 720 password: hashedPassword2, 721 }), 722 }); 723 const user2Cookie = extractSessionCookie(user2Response); 724 725 // Try to update user2's email to user1's email 726 const response = await authRequest( 727 `${BASE_URL}/api/user/email`, 728 user2Cookie, 729 { 730 method: "PUT", 731 headers: { "Content-Type": "application/json" }, 732 body: JSON.stringify({ email: TEST_USER.email }), 733 }, 734 ); 735 736 expect(response.status).toBe(400); 737 const data = await response.json(); 738 expect(data.error).toBe("Email already in use"); 739 }); 740 }); 741 742 describe("PUT /api/user/password", () => { 743 serverTest("should update user password", async () => { 744 // Register user 745 const hashedPassword = await clientHashPassword( 746 TEST_USER.email, 747 TEST_USER.password, 748 ); 749 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 750 method: "POST", 751 headers: { "Content-Type": "application/json" }, 752 body: JSON.stringify({ 753 email: TEST_USER.email, 754 password: hashedPassword, 755 }), 756 }); 757 const sessionCookie = extractSessionCookie(registerResponse); 758 759 // Update password 760 const newPassword = await clientHashPassword( 761 TEST_USER.email, 762 "NewPassword123!", 763 ); 764 const response = await authRequest( 765 `${BASE_URL}/api/user/password`, 766 sessionCookie, 767 { 768 method: "PUT", 769 headers: { "Content-Type": "application/json" }, 770 body: JSON.stringify({ password: newPassword }), 771 }, 772 ); 773 774 expect(response.status).toBe(200); 775 const data = await response.json(); 776 expect(data.success).toBe(true); 777 778 // Verify can login with new password 779 const loginResponse = await fetch(`${BASE_URL}/api/auth/login`, { 780 method: "POST", 781 headers: { "Content-Type": "application/json" }, 782 body: JSON.stringify({ 783 email: TEST_USER.email, 784 password: newPassword, 785 }), 786 }); 787 expect(loginResponse.status).toBe(200); 788 }); 789 790 serverTest("should reject invalid password format", async () => { 791 // Register user 792 const hashedPassword = await clientHashPassword( 793 TEST_USER.email, 794 TEST_USER.password, 795 ); 796 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 797 method: "POST", 798 headers: { "Content-Type": "application/json" }, 799 body: JSON.stringify({ 800 email: TEST_USER.email, 801 password: hashedPassword, 802 }), 803 }); 804 const sessionCookie = extractSessionCookie(registerResponse); 805 806 // Try to update with invalid format 807 const response = await authRequest( 808 `${BASE_URL}/api/user/password`, 809 sessionCookie, 810 { 811 method: "PUT", 812 headers: { "Content-Type": "application/json" }, 813 body: JSON.stringify({ password: "short" }), 814 }, 815 ); 816 817 expect(response.status).toBe(400); 818 const data = await response.json(); 819 expect(data.error).toBe("Invalid password format"); 820 }); 821 }); 822 823 describe("PUT /api/user/name", () => { 824 serverTest("should update user name", async () => { 825 // Register user 826 const hashedPassword = await clientHashPassword( 827 TEST_USER.email, 828 TEST_USER.password, 829 ); 830 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 831 method: "POST", 832 headers: { "Content-Type": "application/json" }, 833 body: JSON.stringify({ 834 email: TEST_USER.email, 835 password: hashedPassword, 836 name: TEST_USER.name, 837 }), 838 }); 839 const sessionCookie = extractSessionCookie(registerResponse); 840 841 // Update name 842 const newName = "Updated Name"; 843 const response = await authRequest( 844 `${BASE_URL}/api/user/name`, 845 sessionCookie, 846 { 847 method: "PUT", 848 headers: { "Content-Type": "application/json" }, 849 body: JSON.stringify({ name: newName }), 850 }, 851 ); 852 853 expect(response.status).toBe(200); 854 const data = await response.json(); 855 expect(data.success).toBe(true); 856 857 // Verify name updated 858 const meResponse = await authRequest( 859 `${BASE_URL}/api/auth/me`, 860 sessionCookie, 861 ); 862 const meData = await meResponse.json(); 863 expect(meData.name).toBe(newName); 864 }); 865 866 serverTest("should reject missing name", async () => { 867 // Register user 868 const hashedPassword = await clientHashPassword( 869 TEST_USER.email, 870 TEST_USER.password, 871 ); 872 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 873 method: "POST", 874 headers: { "Content-Type": "application/json" }, 875 body: JSON.stringify({ 876 email: TEST_USER.email, 877 password: hashedPassword, 878 }), 879 }); 880 const sessionCookie = extractSessionCookie(registerResponse); 881 882 const response = await authRequest( 883 `${BASE_URL}/api/user/name`, 884 sessionCookie, 885 { 886 method: "PUT", 887 headers: { "Content-Type": "application/json" }, 888 body: JSON.stringify({}), 889 }, 890 ); 891 892 expect(response.status).toBe(400); 893 }); 894 }); 895 896 describe("PUT /api/user/avatar", () => { 897 serverTest("should update user avatar", async () => { 898 // Register user 899 const hashedPassword = await clientHashPassword( 900 TEST_USER.email, 901 TEST_USER.password, 902 ); 903 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 904 method: "POST", 905 headers: { "Content-Type": "application/json" }, 906 body: JSON.stringify({ 907 email: TEST_USER.email, 908 password: hashedPassword, 909 }), 910 }); 911 const sessionCookie = extractSessionCookie(registerResponse); 912 913 // Update avatar 914 const newAvatar = "👨‍💻"; 915 const response = await authRequest( 916 `${BASE_URL}/api/user/avatar`, 917 sessionCookie, 918 { 919 method: "PUT", 920 headers: { "Content-Type": "application/json" }, 921 body: JSON.stringify({ avatar: newAvatar }), 922 }, 923 ); 924 925 expect(response.status).toBe(200); 926 const data = await response.json(); 927 expect(data.success).toBe(true); 928 929 // Verify avatar updated 930 const meResponse = await authRequest( 931 `${BASE_URL}/api/auth/me`, 932 sessionCookie, 933 ); 934 const meData = await meResponse.json(); 935 expect(meData.avatar).toBe(newAvatar); 936 }); 937 }); 938}); 939 940describe("API Endpoints - Transcriptions", () => { 941 describe("GET /api/transcriptions/health", () => { 942 serverTest( 943 "should return transcription service health status", 944 async () => { 945 const response = await fetch(`${BASE_URL}/api/transcriptions/health`); 946 947 expect(response.status).toBe(200); 948 const data = await response.json(); 949 expect(data).toHaveProperty("available"); 950 expect(typeof data.available).toBe("boolean"); 951 }, 952 ); 953 }); 954 955 describe("GET /api/transcriptions", () => { 956 serverTest("should return user transcriptions", async () => { 957 // Register user 958 const hashedPassword = await clientHashPassword( 959 TEST_USER.email, 960 TEST_USER.password, 961 ); 962 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 963 method: "POST", 964 headers: { "Content-Type": "application/json" }, 965 body: JSON.stringify({ 966 email: TEST_USER.email, 967 password: hashedPassword, 968 }), 969 }); 970 const sessionCookie = extractSessionCookie(registerResponse); 971 972 // Get transcriptions 973 const response = await authRequest( 974 `${BASE_URL}/api/transcriptions`, 975 sessionCookie, 976 ); 977 978 expect(response.status).toBe(200); 979 const data = await response.json(); 980 expect(data.jobs).toBeDefined(); 981 expect(Array.isArray(data.jobs)).toBe(true); 982 }); 983 984 serverTest("should require authentication", async () => { 985 const response = await fetch(`${BASE_URL}/api/transcriptions`); 986 987 expect(response.status).toBe(401); 988 }); 989 }); 990 991 describe("POST /api/transcriptions", () => { 992 serverTest("should upload audio file and start transcription", async () => { 993 // Register user 994 const hashedPassword = await clientHashPassword( 995 TEST_USER.email, 996 TEST_USER.password, 997 ); 998 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 999 method: "POST", 1000 headers: { "Content-Type": "application/json" }, 1001 body: JSON.stringify({ 1002 email: TEST_USER.email, 1003 password: hashedPassword, 1004 }), 1005 }); 1006 const sessionCookie = extractSessionCookie(registerResponse); 1007 1008 // Create a test audio file 1009 const audioBlob = new Blob(["fake audio data"], { type: "audio/mp3" }); 1010 const formData = new FormData(); 1011 formData.append("audio", audioBlob, "test.mp3"); 1012 formData.append("class_name", "Test Class"); 1013 1014 // Upload 1015 const response = await authRequest( 1016 `${BASE_URL}/api/transcriptions`, 1017 sessionCookie, 1018 { 1019 method: "POST", 1020 body: formData, 1021 }, 1022 ); 1023 1024 expect(response.status).toBe(200); 1025 const data = await response.json(); 1026 expect(data.id).toBeDefined(); 1027 expect(data.message).toContain("Upload successful"); 1028 }); 1029 1030 serverTest("should reject non-audio files", async () => { 1031 // Register user 1032 const hashedPassword = await clientHashPassword( 1033 TEST_USER.email, 1034 TEST_USER.password, 1035 ); 1036 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 1037 method: "POST", 1038 headers: { "Content-Type": "application/json" }, 1039 body: JSON.stringify({ 1040 email: TEST_USER.email, 1041 password: hashedPassword, 1042 }), 1043 }); 1044 const sessionCookie = extractSessionCookie(registerResponse); 1045 1046 // Try to upload non-audio file 1047 const textBlob = new Blob(["text file"], { type: "text/plain" }); 1048 const formData = new FormData(); 1049 formData.append("audio", textBlob, "test.txt"); 1050 1051 const response = await authRequest( 1052 `${BASE_URL}/api/transcriptions`, 1053 sessionCookie, 1054 { 1055 method: "POST", 1056 body: formData, 1057 }, 1058 ); 1059 1060 expect(response.status).toBe(400); 1061 }); 1062 1063 serverTest("should reject files exceeding size limit", async () => { 1064 // Register user 1065 const hashedPassword = await clientHashPassword( 1066 TEST_USER.email, 1067 TEST_USER.password, 1068 ); 1069 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 1070 method: "POST", 1071 headers: { "Content-Type": "application/json" }, 1072 body: JSON.stringify({ 1073 email: TEST_USER.email, 1074 password: hashedPassword, 1075 }), 1076 }); 1077 const sessionCookie = extractSessionCookie(registerResponse); 1078 1079 // Create a file larger than 100MB (the actual limit) 1080 const largeBlob = new Blob([new ArrayBuffer(101 * 1024 * 1024)], { 1081 type: "audio/mp3", 1082 }); 1083 const formData = new FormData(); 1084 formData.append("audio", largeBlob, "large.mp3"); 1085 1086 const response = await authRequest( 1087 `${BASE_URL}/api/transcriptions`, 1088 sessionCookie, 1089 { 1090 method: "POST", 1091 body: formData, 1092 }, 1093 ); 1094 1095 expect(response.status).toBe(400); 1096 const data = await response.json(); 1097 expect(data.error).toContain("File size must be less than"); 1098 }); 1099 1100 serverTest("should require authentication", async () => { 1101 const audioBlob = new Blob(["fake audio data"], { type: "audio/mp3" }); 1102 const formData = new FormData(); 1103 formData.append("audio", audioBlob, "test.mp3"); 1104 1105 const response = await fetch(`${BASE_URL}/api/transcriptions`, { 1106 method: "POST", 1107 body: formData, 1108 }); 1109 1110 expect(response.status).toBe(401); 1111 }); 1112 }); 1113}); 1114 1115describe("API Endpoints - Admin", () => { 1116 let adminCookie: string; 1117 let userCookie: string; 1118 let userId: number; 1119 1120 beforeEach(async () => { 1121 if (!serverAvailable) return; 1122 1123 // Create admin user 1124 const adminHash = await clientHashPassword( 1125 TEST_ADMIN.email, 1126 TEST_ADMIN.password, 1127 ); 1128 const adminResponse = await fetch(`${BASE_URL}/api/auth/register`, { 1129 method: "POST", 1130 headers: { "Content-Type": "application/json" }, 1131 body: JSON.stringify({ 1132 email: TEST_ADMIN.email, 1133 password: adminHash, 1134 name: TEST_ADMIN.name, 1135 }), 1136 }); 1137 adminCookie = extractSessionCookie(adminResponse); 1138 1139 // Manually set admin role in database 1140 db.run("UPDATE users SET role = 'admin' WHERE email = ?", [ 1141 TEST_ADMIN.email, 1142 ]); 1143 1144 // Create regular user 1145 const userHash = await clientHashPassword( 1146 TEST_USER.email, 1147 TEST_USER.password, 1148 ); 1149 const userResponse = await fetch(`${BASE_URL}/api/auth/register`, { 1150 method: "POST", 1151 headers: { "Content-Type": "application/json" }, 1152 body: JSON.stringify({ 1153 email: TEST_USER.email, 1154 password: userHash, 1155 name: TEST_USER.name, 1156 }), 1157 }); 1158 userCookie = extractSessionCookie(userResponse); 1159 1160 // Get user ID 1161 const userIdResult = db 1162 .query<{ id: number }, [string]>("SELECT id FROM users WHERE email = ?") 1163 .get(TEST_USER.email); 1164 userId = userIdResult?.id; 1165 }); 1166 1167 describe("GET /api/admin/users", () => { 1168 serverTest("should return all users for admin", async () => { 1169 const response = await authRequest( 1170 `${BASE_URL}/api/admin/users`, 1171 adminCookie, 1172 ); 1173 1174 expect(response.status).toBe(200); 1175 const data = await response.json(); 1176 expect(Array.isArray(data)).toBe(true); 1177 expect(data.length).toBeGreaterThan(0); 1178 }); 1179 1180 serverTest("should reject non-admin users", async () => { 1181 const response = await authRequest( 1182 `${BASE_URL}/api/admin/users`, 1183 userCookie, 1184 ); 1185 1186 expect(response.status).toBe(403); 1187 }); 1188 1189 serverTest("should require authentication", async () => { 1190 const response = await fetch(`${BASE_URL}/api/admin/users`); 1191 1192 expect(response.status).toBe(401); 1193 }); 1194 }); 1195 1196 describe("GET /api/admin/transcriptions", () => { 1197 serverTest("should return all transcriptions for admin", async () => { 1198 const response = await authRequest( 1199 `${BASE_URL}/api/admin/transcriptions`, 1200 adminCookie, 1201 ); 1202 1203 expect(response.status).toBe(200); 1204 const data = await response.json(); 1205 expect(Array.isArray(data)).toBe(true); 1206 }); 1207 1208 serverTest("should reject non-admin users", async () => { 1209 const response = await authRequest( 1210 `${BASE_URL}/api/admin/transcriptions`, 1211 userCookie, 1212 ); 1213 1214 expect(response.status).toBe(403); 1215 }); 1216 }); 1217 1218 describe("DELETE /api/admin/users/:id", () => { 1219 serverTest("should delete user as admin", async () => { 1220 const response = await authRequest( 1221 `${BASE_URL}/api/admin/users/${userId}`, 1222 adminCookie, 1223 { 1224 method: "DELETE", 1225 }, 1226 ); 1227 1228 expect(response.status).toBe(200); 1229 const data = await response.json(); 1230 expect(data.success).toBe(true); 1231 1232 // Verify user is deleted 1233 const verifyResponse = await authRequest( 1234 `${BASE_URL}/api/auth/me`, 1235 userCookie, 1236 ); 1237 expect(verifyResponse.status).toBe(401); 1238 }); 1239 1240 serverTest("should reject non-admin users", async () => { 1241 const response = await authRequest( 1242 `${BASE_URL}/api/admin/users/${userId}`, 1243 userCookie, 1244 { 1245 method: "DELETE", 1246 }, 1247 ); 1248 1249 expect(response.status).toBe(403); 1250 }); 1251 }); 1252 1253 describe("PUT /api/admin/users/:id/role", () => { 1254 serverTest("should update user role as admin", async () => { 1255 const response = await authRequest( 1256 `${BASE_URL}/api/admin/users/${userId}/role`, 1257 adminCookie, 1258 { 1259 method: "PUT", 1260 headers: { "Content-Type": "application/json" }, 1261 body: JSON.stringify({ role: "admin" }), 1262 }, 1263 ); 1264 1265 expect(response.status).toBe(200); 1266 const data = await response.json(); 1267 expect(data.success).toBe(true); 1268 1269 // Verify role updated 1270 const meResponse = await authRequest( 1271 `${BASE_URL}/api/auth/me`, 1272 userCookie, 1273 ); 1274 const meData = await meResponse.json(); 1275 expect(meData.role).toBe("admin"); 1276 }); 1277 1278 serverTest("should reject invalid roles", async () => { 1279 const response = await authRequest( 1280 `${BASE_URL}/api/admin/users/${userId}/role`, 1281 adminCookie, 1282 { 1283 method: "PUT", 1284 headers: { "Content-Type": "application/json" }, 1285 body: JSON.stringify({ role: "superadmin" }), 1286 }, 1287 ); 1288 1289 expect(response.status).toBe(400); 1290 }); 1291 }); 1292 1293 describe("GET /api/admin/users/:id/details", () => { 1294 serverTest("should return user details for admin", async () => { 1295 const response = await authRequest( 1296 `${BASE_URL}/api/admin/users/${userId}/details`, 1297 adminCookie, 1298 ); 1299 1300 expect(response.status).toBe(200); 1301 const data = await response.json(); 1302 expect(data.id).toBe(userId); 1303 expect(data.email).toBe(TEST_USER.email); 1304 expect(data).toHaveProperty("passkeys"); 1305 expect(data).toHaveProperty("sessions"); 1306 }); 1307 1308 serverTest("should reject non-admin users", async () => { 1309 const response = await authRequest( 1310 `${BASE_URL}/api/admin/users/${userId}/details`, 1311 userCookie, 1312 ); 1313 1314 expect(response.status).toBe(403); 1315 }); 1316 }); 1317 1318 describe("PUT /api/admin/users/:id/name", () => { 1319 serverTest("should update user name as admin", async () => { 1320 const newName = "Admin Updated Name"; 1321 const response = await authRequest( 1322 `${BASE_URL}/api/admin/users/${userId}/name`, 1323 adminCookie, 1324 { 1325 method: "PUT", 1326 headers: { "Content-Type": "application/json" }, 1327 body: JSON.stringify({ name: newName }), 1328 }, 1329 ); 1330 1331 expect(response.status).toBe(200); 1332 const data = await response.json(); 1333 expect(data.success).toBe(true); 1334 }); 1335 1336 serverTest("should reject empty names", async () => { 1337 const response = await authRequest( 1338 `${BASE_URL}/api/admin/users/${userId}/name`, 1339 adminCookie, 1340 { 1341 method: "PUT", 1342 headers: { "Content-Type": "application/json" }, 1343 body: JSON.stringify({ name: "" }), 1344 }, 1345 ); 1346 1347 expect(response.status).toBe(400); 1348 }); 1349 }); 1350 1351 describe("PUT /api/admin/users/:id/email", () => { 1352 serverTest("should update user email as admin", async () => { 1353 const newEmail = "newemail@admin.com"; 1354 const response = await authRequest( 1355 `${BASE_URL}/api/admin/users/${userId}/email`, 1356 adminCookie, 1357 { 1358 method: "PUT", 1359 headers: { "Content-Type": "application/json" }, 1360 body: JSON.stringify({ email: newEmail }), 1361 }, 1362 ); 1363 1364 expect(response.status).toBe(200); 1365 const data = await response.json(); 1366 expect(data.success).toBe(true); 1367 }); 1368 1369 serverTest("should reject duplicate emails", async () => { 1370 const response = await authRequest( 1371 `${BASE_URL}/api/admin/users/${userId}/email`, 1372 adminCookie, 1373 { 1374 method: "PUT", 1375 headers: { "Content-Type": "application/json" }, 1376 body: JSON.stringify({ email: TEST_ADMIN.email }), 1377 }, 1378 ); 1379 1380 expect(response.status).toBe(400); 1381 const data = await response.json(); 1382 expect(data.error).toBe("Email already in use"); 1383 }); 1384 }); 1385 1386 describe("GET /api/admin/users/:id/sessions", () => { 1387 serverTest("should return user sessions as admin", async () => { 1388 const response = await authRequest( 1389 `${BASE_URL}/api/admin/users/${userId}/sessions`, 1390 adminCookie, 1391 ); 1392 1393 expect(response.status).toBe(200); 1394 const data = await response.json(); 1395 expect(Array.isArray(data)).toBe(true); 1396 }); 1397 }); 1398 1399 describe("DELETE /api/admin/users/:id/sessions", () => { 1400 serverTest("should delete all user sessions as admin", async () => { 1401 const response = await authRequest( 1402 `${BASE_URL}/api/admin/users/${userId}/sessions`, 1403 adminCookie, 1404 { 1405 method: "DELETE", 1406 }, 1407 ); 1408 1409 expect(response.status).toBe(200); 1410 const data = await response.json(); 1411 expect(data.success).toBe(true); 1412 1413 // Verify sessions are deleted 1414 const verifyResponse = await authRequest( 1415 `${BASE_URL}/api/auth/me`, 1416 userCookie, 1417 ); 1418 expect(verifyResponse.status).toBe(401); 1419 }); 1420 }); 1421}); 1422 1423describe("API Endpoints - Passkeys", () => { 1424 let sessionCookie: string; 1425 1426 beforeEach(async () => { 1427 if (!serverAvailable) return; 1428 1429 // Register user 1430 const hashedPassword = await clientHashPassword( 1431 TEST_USER.email, 1432 TEST_USER.password, 1433 ); 1434 const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, { 1435 method: "POST", 1436 headers: { "Content-Type": "application/json" }, 1437 body: JSON.stringify({ 1438 email: TEST_USER.email, 1439 password: hashedPassword, 1440 }), 1441 }); 1442 sessionCookie = extractSessionCookie(registerResponse); 1443 }); 1444 1445 describe("GET /api/passkeys", () => { 1446 serverTest("should return user passkeys", async () => { 1447 const response = await authRequest( 1448 `${BASE_URL}/api/passkeys`, 1449 sessionCookie, 1450 ); 1451 1452 expect(response.status).toBe(200); 1453 const data = await response.json(); 1454 expect(data.passkeys).toBeDefined(); 1455 expect(Array.isArray(data.passkeys)).toBe(true); 1456 }); 1457 1458 serverTest("should require authentication", async () => { 1459 const response = await fetch(`${BASE_URL}/api/passkeys`); 1460 1461 expect(response.status).toBe(401); 1462 }); 1463 }); 1464 1465 describe("POST /api/passkeys/register/options", () => { 1466 serverTest( 1467 "should return registration options for authenticated user", 1468 async () => { 1469 const response = await authRequest( 1470 `${BASE_URL}/api/passkeys/register/options`, 1471 sessionCookie, 1472 { 1473 method: "POST", 1474 }, 1475 ); 1476 1477 expect(response.status).toBe(200); 1478 const data = await response.json(); 1479 expect(data).toHaveProperty("challenge"); 1480 expect(data).toHaveProperty("rp"); 1481 expect(data).toHaveProperty("user"); 1482 }, 1483 ); 1484 1485 serverTest("should require authentication", async () => { 1486 const response = await fetch( 1487 `${BASE_URL}/api/passkeys/register/options`, 1488 { 1489 method: "POST", 1490 }, 1491 ); 1492 1493 expect(response.status).toBe(401); 1494 }); 1495 }); 1496 1497 describe("POST /api/passkeys/authenticate/options", () => { 1498 serverTest("should return authentication options for email", async () => { 1499 const response = await fetch( 1500 `${BASE_URL}/api/passkeys/authenticate/options`, 1501 { 1502 method: "POST", 1503 headers: { "Content-Type": "application/json" }, 1504 body: JSON.stringify({ email: TEST_USER.email }), 1505 }, 1506 ); 1507 1508 expect(response.status).toBe(200); 1509 const data = await response.json(); 1510 expect(data).toHaveProperty("challenge"); 1511 }); 1512 1513 serverTest("should handle non-existent email", async () => { 1514 const response = await fetch( 1515 `${BASE_URL}/api/passkeys/authenticate/options`, 1516 { 1517 method: "POST", 1518 headers: { "Content-Type": "application/json" }, 1519 body: JSON.stringify({ email: "nonexistent@example.com" }), 1520 }, 1521 ); 1522 1523 // Should still return options for privacy (don't leak user existence) 1524 expect([200, 404]).toContain(response.status); 1525 }); 1526 }); 1527});