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