🪻 distributed transcription service thistle.dunkirk.sh
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #1 from taciturnaxolotl/claude/testing-mi1tzj9gixd7ot87-0182T4BTqLQ4az8GrAqEru97

authored by

Kieran Klukas and committed by
GitHub
6bd6ac5d 2cedf8bf

+1417 -3
+3 -1
package.json
··· 5 5 "private": true, 6 6 "scripts": { 7 7 "dev": "bun run src/index.ts --hot", 8 - "clean": "rm -rf transcripts uploads thistle.db" 8 + "clean": "rm -rf transcripts uploads thistle.db", 9 + "test": "bun test", 10 + "test:integration": "bun test src/index.test.ts" 9 11 }, 10 12 "devDependencies": { 11 13 "@biomejs/biome": "^2.3.2",
+129
src/index.test.README.md
··· 1 + # API Integration Tests 2 + 3 + This file (`src/index.test.ts`) contains comprehensive integration tests for all API endpoints in the Thistle application. 4 + 5 + ## Running the Tests 6 + 7 + ### Option 1: Manual Server Start (Recommended for Development) 8 + 9 + 1. Start the test server in one terminal: 10 + ```bash 11 + PORT=3001 bun run src/index.ts 12 + ``` 13 + 14 + 2. Run the integration tests in another terminal: 15 + ```bash 16 + bun test src/index.test.ts 17 + ``` 18 + 19 + ### Option 2: Run All Tests 20 + 21 + To run all tests (both unit and integration): 22 + ```bash 23 + bun test 24 + ``` 25 + 26 + **Note**: Integration tests will be skipped if the test server is not running on port 3001. 27 + 28 + ## Test Coverage 29 + 30 + The integration tests cover the following endpoint groups: 31 + 32 + ### Authentication Endpoints 33 + - `POST /api/auth/register` - User registration with validation and rate limiting 34 + - `POST /api/auth/login` - User login with rate limiting 35 + - `POST /api/auth/logout` - User logout 36 + - `GET /api/auth/me` - Get current user information 37 + 38 + ### Session Management 39 + - `GET /api/sessions` - List user sessions 40 + - `DELETE /api/sessions` - Delete specific session 41 + 42 + ### User Management 43 + - `DELETE /api/user` - Delete user account 44 + - `PUT /api/user/email` - Update user email 45 + - `PUT /api/user/password` - Update user password 46 + - `PUT /api/user/name` - Update user name 47 + - `PUT /api/user/avatar` - Update user avatar 48 + 49 + ### Passkey Management 50 + - `POST /api/passkeys/register/options` - Get passkey registration options 51 + - `POST /api/passkeys/register/verify` - Verify and create passkey 52 + - `POST /api/passkeys/authenticate/options` - Get authentication options 53 + - `POST /api/passkeys/authenticate/verify` - Verify and authenticate with passkey 54 + - `GET /api/passkeys` - List user passkeys 55 + - `PUT /api/passkeys/:id` - Update passkey name 56 + - `DELETE /api/passkeys/:id` - Delete passkey 57 + 58 + ### Transcription Endpoints 59 + - `GET /api/transcriptions/health` - Check transcription service health 60 + - `GET /api/transcriptions` - List user transcriptions 61 + - `POST /api/transcriptions` - Upload audio file and start transcription 62 + - `GET /api/transcriptions/:id` - Get transcription details 63 + - `GET /api/transcriptions/:id/audio` - Get audio file with range support 64 + - `GET /api/transcriptions/:id/stream` - SSE stream for transcription updates 65 + 66 + ### Admin Endpoints 67 + - `GET /api/admin/users` - List all users 68 + - `GET /api/admin/users/:id/details` - Get user details 69 + - `DELETE /api/admin/users/:id` - Delete user 70 + - `PUT /api/admin/users/:id/role` - Update user role 71 + - `PUT /api/admin/users/:id/name` - Update user name 72 + - `PUT /api/admin/users/:id/email` - Update user email 73 + - `PUT /api/admin/users/:id/password` - Update user password 74 + - `GET /api/admin/users/:id/sessions` - List user sessions 75 + - `DELETE /api/admin/users/:id/sessions` - Delete all user sessions 76 + - `DELETE /api/admin/users/:id/sessions/:sessionId` - Delete specific session 77 + - `DELETE /api/admin/users/:id/passkeys/:passkeyId` - Delete user passkey 78 + - `GET /api/admin/transcriptions` - List all transcriptions 79 + - `GET /api/admin/transcriptions/:id/details` - Get transcription details 80 + - `DELETE /api/admin/transcriptions/:id` - Delete transcription 81 + 82 + ## Test Features 83 + 84 + - **Automatic cleanup**: Test data is cleaned up before and after each test 85 + - **Rate limit testing**: Validates rate limiting on sensitive endpoints 86 + - **Authorization testing**: Ensures proper authentication and authorization 87 + - **Validation testing**: Checks input validation and error handling 88 + - **Security testing**: Tests for common vulnerabilities 89 + - **File upload testing**: Validates file type and size restrictions 90 + 91 + ## Test Database 92 + 93 + Tests use the same database as development. Test users and data are identified by email patterns (`test%`, `admin@%`) and are automatically cleaned up after tests run. 94 + 95 + ## Continuous Integration 96 + 97 + For CI/CD pipelines, you can use a background server: 98 + 99 + ```bash 100 + # Start server in background 101 + PORT=3001 bun run src/index.ts & 102 + SERVER_PID=$! 103 + 104 + # Wait for server to be ready 105 + sleep 2 106 + 107 + # Run tests 108 + bun test src/index.test.ts 109 + 110 + # Kill server 111 + kill $SERVER_PID 112 + ``` 113 + 114 + ## Troubleshooting 115 + 116 + ### Tests are being skipped 117 + - Make sure the test server is running on port 3001 118 + - Check that there are no port conflicts 119 + - Verify the server started successfully (check console output) 120 + 121 + ### Tests are failing with connection errors 122 + - Ensure no firewall is blocking localhost connections 123 + - Try increasing the timeout in the `beforeAll` hook 124 + - Check that the database is accessible 125 + 126 + ### Rate limit tests are flaky 127 + - Rate limits are shared across test runs 128 + - Clean test data between runs: `rm thistle.db` 129 + - Or adjust rate limit test expectations
+1283
src/index.test.ts
··· 1 + import { describe, expect, test, beforeAll, afterAll, beforeEach } from "bun:test"; 2 + import db from "./db/schema"; 3 + import { hashPasswordClient } from "./lib/client-auth"; 4 + 5 + // Test server URL - uses port 3001 for testing to avoid conflicts 6 + const TEST_PORT = 3001; 7 + const BASE_URL = `http://localhost:${TEST_PORT}`; 8 + 9 + // Check if server is available 10 + let serverAvailable = false; 11 + 12 + beforeAll(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 27 + const TEST_USER = { 28 + email: "test@example.com", 29 + password: "TestPassword123!", 30 + name: "Test User", 31 + }; 32 + 33 + const TEST_ADMIN = { 34 + email: "admin@example.com", 35 + password: "AdminPassword123!", 36 + name: "Admin User", 37 + }; 38 + 39 + const 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 46 + async function clientHashPassword(email: string, password: string): Promise<string> { 47 + return await hashPasswordClient(password, email); 48 + } 49 + 50 + // Helper to extract session cookie 51 + function 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 59 + function 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 74 + function 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 + 87 + beforeEach(() => { 88 + if (serverAvailable) { 89 + cleanupTestData(); 90 + } 91 + }); 92 + 93 + afterAll(() => { 94 + if (serverAvailable) { 95 + cleanupTestData(); 96 + } 97 + }); 98 + 99 + // Helper to skip tests if server is not available 100 + function 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 + 110 + describe("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 + 398 + describe("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 + 515 + describe("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 + 775 + describe("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 + 918 + describe("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 + 1197 + describe("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 + });
+2 -2
src/index.ts
··· 102 102 setInterval(() => whisperService.cleanupStaleFiles(), 24 * 60 * 60 * 1000); 103 103 104 104 const server = Bun.serve({ 105 - port: 3000, 105 + port: process.env.PORT ? Number.parseInt(process.env.PORT, 10) : 3000, 106 106 idleTimeout: 120, // 120 seconds for SSE connections 107 107 routes: { 108 108 "/": indexHTML, ··· 981 981 } 982 982 983 983 if (file.size > MAX_FILE_SIZE) { 984 - throw ValidationErrors.fileTooLarge("25MB"); 984 + throw ValidationErrors.fileTooLarge("100MB"); 985 985 } 986 986 987 987 // Generate unique filename