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