···
1
+
import { describe, expect, test, beforeAll, afterAll, beforeEach } from "bun:test";
2
+
import db from "./db/schema";
3
+
import { hashPasswordClient } from "./lib/client-auth";
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}`;
9
+
// Check if server is available
10
+
let serverAvailable = false;
12
+
beforeAll(async () => {
14
+
const response = await fetch(`${BASE_URL}/api/transcriptions/health`, {
15
+
signal: AbortSignal.timeout(1000),
17
+
serverAvailable = response.ok || response.status === 404;
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`
22
+
serverAvailable = false;
26
+
// Test user credentials
28
+
email: "test@example.com",
29
+
password: "TestPassword123!",
33
+
const TEST_ADMIN = {
34
+
email: "admin@example.com",
35
+
password: "AdminPassword123!",
39
+
const TEST_USER_2 = {
40
+
email: "test2@example.com",
41
+
password: "TestPassword456!",
42
+
name: "Test User 2",
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);
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;
58
+
// Helper to make authenticated requests
59
+
function authRequest(
61
+
sessionCookie: string,
62
+
options: RequestInit = {},
63
+
): Promise<Response> {
68
+
Cookie: `session=${sessionCookie}`,
74
+
function cleanupTestData() {
75
+
// Delete test users and their related data (cascade will handle most of it)
76
+
db.run("DELETE FROM sessions WHERE user_id IN (SELECT id FROM users WHERE email LIKE 'test%' OR email LIKE 'admin@%')");
77
+
db.run("DELETE FROM passkeys WHERE user_id IN (SELECT id FROM users WHERE email LIKE 'test%' OR email LIKE 'admin@%')");
78
+
db.run("DELETE FROM transcriptions WHERE user_id IN (SELECT id FROM users WHERE email LIKE 'test%' OR email LIKE 'admin@%')");
79
+
db.run("DELETE FROM users WHERE email LIKE 'test%' OR email LIKE 'admin@%'");
81
+
// Clear rate limit data
82
+
db.run("DELETE FROM rate_limits WHERE identifier LIKE 'test%' OR identifier LIKE 'admin%'");
86
+
if (serverAvailable) {
92
+
if (serverAvailable) {
97
+
// Helper to skip tests if server is not available
98
+
function serverTest(name: string, fn: () => void | Promise<void>) {
99
+
test(name, async () => {
100
+
if (!serverAvailable) {
101
+
console.log(`⏭️ Skipping: ${name} (server not running)`);
108
+
describe("API Endpoints - Authentication", () => {
109
+
describe("POST /api/auth/register", () => {
110
+
serverTest("should register a new user successfully", async () => {
111
+
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
113
+
const response = await fetch(`${BASE_URL}/api/auth/register`, {
115
+
headers: { "Content-Type": "application/json" },
116
+
body: JSON.stringify({
117
+
email: TEST_USER.email,
118
+
password: hashedPassword,
119
+
name: TEST_USER.name,
123
+
expect(response.status).toBe(200);
124
+
const data = await response.json();
125
+
expect(data.user).toBeDefined();
126
+
expect(data.user.email).toBe(TEST_USER.email);
127
+
expect(extractSessionCookie(response)).toBeTruthy();
130
+
serverTest("should reject registration with missing email", async () => {
131
+
const response = await fetch(`${BASE_URL}/api/auth/register`, {
133
+
headers: { "Content-Type": "application/json" },
134
+
body: JSON.stringify({
135
+
password: "hashedpassword123456",
139
+
expect(response.status).toBe(400);
140
+
const data = await response.json();
141
+
expect(data.error).toBe("Email and password required");
144
+
serverTest("should reject registration with invalid password format", async () => {
145
+
const response = await fetch(`${BASE_URL}/api/auth/register`, {
147
+
headers: { "Content-Type": "application/json" },
148
+
body: JSON.stringify({
149
+
email: TEST_USER.email,
154
+
expect(response.status).toBe(400);
155
+
const data = await response.json();
156
+
expect(data.error).toBe("Invalid password format");
159
+
serverTest("should reject duplicate email registration", async () => {
160
+
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
162
+
// First registration
163
+
await fetch(`${BASE_URL}/api/auth/register`, {
165
+
headers: { "Content-Type": "application/json" },
166
+
body: JSON.stringify({
167
+
email: TEST_USER.email,
168
+
password: hashedPassword,
169
+
name: TEST_USER.name,
173
+
// Duplicate registration
174
+
const response = await fetch(`${BASE_URL}/api/auth/register`, {
176
+
headers: { "Content-Type": "application/json" },
177
+
body: JSON.stringify({
178
+
email: TEST_USER.email,
179
+
password: hashedPassword,
180
+
name: TEST_USER.name,
184
+
expect(response.status).toBe(400);
185
+
const data = await response.json();
186
+
expect(data.error).toBe("Email already registered");
189
+
serverTest("should enforce rate limiting on registration", async () => {
190
+
const hashedPassword = await clientHashPassword("test@example.com", "password");
192
+
// Make 6 registration attempts (limit is 5 per hour)
193
+
for (let i = 0; i < 6; i++) {
194
+
const response = await fetch(`${BASE_URL}/api/auth/register`, {
196
+
headers: { "Content-Type": "application/json" },
197
+
body: JSON.stringify({
198
+
email: `test${i}@example.com`,
199
+
password: hashedPassword,
204
+
expect(response.status).toBeLessThan(429);
206
+
expect(response.status).toBe(429);
212
+
describe("POST /api/auth/login", () => {
213
+
serverTest("should login successfully with valid credentials", async () => {
214
+
// Register user first
215
+
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
216
+
await fetch(`${BASE_URL}/api/auth/register`, {
218
+
headers: { "Content-Type": "application/json" },
219
+
body: JSON.stringify({
220
+
email: TEST_USER.email,
221
+
password: hashedPassword,
222
+
name: TEST_USER.name,
227
+
const response = await fetch(`${BASE_URL}/api/auth/login`, {
229
+
headers: { "Content-Type": "application/json" },
230
+
body: JSON.stringify({
231
+
email: TEST_USER.email,
232
+
password: hashedPassword,
236
+
expect(response.status).toBe(200);
237
+
const data = await response.json();
238
+
expect(data.user).toBeDefined();
239
+
expect(data.user.email).toBe(TEST_USER.email);
240
+
expect(extractSessionCookie(response)).toBeTruthy();
243
+
serverTest("should reject login with invalid credentials", async () => {
244
+
// Register user first
245
+
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
246
+
await fetch(`${BASE_URL}/api/auth/register`, {
248
+
headers: { "Content-Type": "application/json" },
249
+
body: JSON.stringify({
250
+
email: TEST_USER.email,
251
+
password: hashedPassword,
255
+
// Login with wrong password
256
+
const wrongPassword = await clientHashPassword(TEST_USER.email, "WrongPassword123!");
257
+
const response = await fetch(`${BASE_URL}/api/auth/login`, {
259
+
headers: { "Content-Type": "application/json" },
260
+
body: JSON.stringify({
261
+
email: TEST_USER.email,
262
+
password: wrongPassword,
266
+
expect(response.status).toBe(401);
267
+
const data = await response.json();
268
+
expect(data.error).toBe("Invalid email or password");
271
+
serverTest("should reject login with missing fields", async () => {
272
+
const response = await fetch(`${BASE_URL}/api/auth/login`, {
274
+
headers: { "Content-Type": "application/json" },
275
+
body: JSON.stringify({
276
+
email: TEST_USER.email,
280
+
expect(response.status).toBe(400);
281
+
const data = await response.json();
282
+
expect(data.error).toBe("Email and password required");
285
+
serverTest("should enforce rate limiting on login attempts", async () => {
286
+
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
288
+
// Make 11 login attempts (limit is 10 per 15 minutes per IP)
289
+
for (let i = 0; i < 11; i++) {
290
+
const response = await fetch(`${BASE_URL}/api/auth/login`, {
292
+
headers: { "Content-Type": "application/json" },
293
+
body: JSON.stringify({
294
+
email: TEST_USER.email,
295
+
password: hashedPassword,
300
+
expect(response.status).toBeLessThan(429);
302
+
expect(response.status).toBe(429);
308
+
describe("POST /api/auth/logout", () => {
309
+
serverTest("should logout successfully", async () => {
310
+
// Register and login
311
+
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
312
+
const loginResponse = await fetch(`${BASE_URL}/api/auth/register`, {
314
+
headers: { "Content-Type": "application/json" },
315
+
body: JSON.stringify({
316
+
email: TEST_USER.email,
317
+
password: hashedPassword,
320
+
const sessionCookie = extractSessionCookie(loginResponse)!;
323
+
const response = await authRequest(`${BASE_URL}/api/auth/logout`, sessionCookie, {
327
+
expect(response.status).toBe(200);
328
+
const data = await response.json();
329
+
expect(data.success).toBe(true);
331
+
// Verify cookie is cleared
332
+
const setCookie = response.headers.get("set-cookie");
333
+
expect(setCookie).toContain("Max-Age=0");
336
+
serverTest("should logout even without valid session", async () => {
337
+
const response = await fetch(`${BASE_URL}/api/auth/logout`, {
341
+
expect(response.status).toBe(200);
342
+
const data = await response.json();
343
+
expect(data.success).toBe(true);
347
+
describe("GET /api/auth/me", () => {
348
+
serverTest("should return current user info when authenticated", async () => {
350
+
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
351
+
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
353
+
headers: { "Content-Type": "application/json" },
354
+
body: JSON.stringify({
355
+
email: TEST_USER.email,
356
+
password: hashedPassword,
357
+
name: TEST_USER.name,
360
+
const sessionCookie = extractSessionCookie(registerResponse)!;
362
+
// Get current user
363
+
const response = await authRequest(`${BASE_URL}/api/auth/me`, sessionCookie);
365
+
expect(response.status).toBe(200);
366
+
const data = await response.json();
367
+
expect(data.email).toBe(TEST_USER.email);
368
+
expect(data.name).toBe(TEST_USER.name);
369
+
expect(data.role).toBeDefined();
372
+
serverTest("should return 401 when not authenticated", async () => {
373
+
const response = await fetch(`${BASE_URL}/api/auth/me`);
375
+
expect(response.status).toBe(401);
376
+
const data = await response.json();
377
+
expect(data.error).toBe("Not authenticated");
380
+
serverTest("should return 401 with invalid session", async () => {
381
+
const response = await authRequest(`${BASE_URL}/api/auth/me`, "invalid-session");
383
+
expect(response.status).toBe(401);
384
+
const data = await response.json();
385
+
expect(data.error).toBe("Invalid session");
390
+
describe("API Endpoints - Session Management", () => {
391
+
describe("GET /api/sessions", () => {
392
+
serverTest("should return user sessions", async () => {
394
+
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
395
+
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
397
+
headers: { "Content-Type": "application/json" },
398
+
body: JSON.stringify({
399
+
email: TEST_USER.email,
400
+
password: hashedPassword,
403
+
const sessionCookie = extractSessionCookie(registerResponse)!;
406
+
const response = await authRequest(`${BASE_URL}/api/sessions`, sessionCookie);
408
+
expect(response.status).toBe(200);
409
+
const data = await response.json();
410
+
expect(data.sessions).toBeDefined();
411
+
expect(data.sessions.length).toBeGreaterThan(0);
412
+
expect(data.sessions[0]).toHaveProperty("id");
413
+
expect(data.sessions[0]).toHaveProperty("ip_address");
414
+
expect(data.sessions[0]).toHaveProperty("user_agent");
417
+
serverTest("should require authentication", async () => {
418
+
const response = await fetch(`${BASE_URL}/api/sessions`);
420
+
expect(response.status).toBe(401);
424
+
describe("DELETE /api/sessions", () => {
425
+
serverTest("should delete specific session", async () => {
426
+
// Register user and create multiple sessions
427
+
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
428
+
const session1Response = await fetch(`${BASE_URL}/api/auth/register`, {
430
+
headers: { "Content-Type": "application/json" },
431
+
body: JSON.stringify({
432
+
email: TEST_USER.email,
433
+
password: hashedPassword,
436
+
const session1Cookie = extractSessionCookie(session1Response)!;
438
+
const session2Response = await fetch(`${BASE_URL}/api/auth/login`, {
440
+
headers: { "Content-Type": "application/json" },
441
+
body: JSON.stringify({
442
+
email: TEST_USER.email,
443
+
password: hashedPassword,
446
+
const session2Cookie = extractSessionCookie(session2Response)!;
448
+
// Get sessions list
449
+
const sessionsResponse = await authRequest(`${BASE_URL}/api/sessions`, session1Cookie);
450
+
const sessionsData = await sessionsResponse.json();
451
+
const targetSessionId = sessionsData.sessions.find(
452
+
(s: any) => s.id === session2Cookie
455
+
// Delete session 2
456
+
const response = await authRequest(`${BASE_URL}/api/sessions`, session1Cookie, {
458
+
headers: { "Content-Type": "application/json" },
459
+
body: JSON.stringify({ sessionId: targetSessionId }),
462
+
expect(response.status).toBe(200);
463
+
const data = await response.json();
464
+
expect(data.success).toBe(true);
466
+
// Verify session 2 is deleted
467
+
const verifyResponse = await authRequest(`${BASE_URL}/api/auth/me`, session2Cookie);
468
+
expect(verifyResponse.status).toBe(401);
471
+
serverTest("should not delete another user's session", async () => {
472
+
// Register two users
473
+
const hashedPassword1 = await clientHashPassword(TEST_USER.email, TEST_USER.password);
474
+
const user1Response = await fetch(`${BASE_URL}/api/auth/register`, {
476
+
headers: { "Content-Type": "application/json" },
477
+
body: JSON.stringify({
478
+
email: TEST_USER.email,
479
+
password: hashedPassword1,
482
+
const user1Cookie = extractSessionCookie(user1Response)!;
484
+
const hashedPassword2 = await clientHashPassword(TEST_USER_2.email, TEST_USER_2.password);
485
+
const user2Response = await fetch(`${BASE_URL}/api/auth/register`, {
487
+
headers: { "Content-Type": "application/json" },
488
+
body: JSON.stringify({
489
+
email: TEST_USER_2.email,
490
+
password: hashedPassword2,
493
+
const user2Cookie = extractSessionCookie(user2Response)!;
495
+
// Try to delete user2's session using user1's credentials
496
+
const response = await authRequest(`${BASE_URL}/api/sessions`, user1Cookie, {
498
+
headers: { "Content-Type": "application/json" },
499
+
body: JSON.stringify({ sessionId: user2Cookie }),
502
+
expect(response.status).toBe(404);
507
+
describe("API Endpoints - User Management", () => {
508
+
describe("DELETE /api/user", () => {
509
+
serverTest("should delete user account", async () => {
511
+
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
512
+
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
514
+
headers: { "Content-Type": "application/json" },
515
+
body: JSON.stringify({
516
+
email: TEST_USER.email,
517
+
password: hashedPassword,
520
+
const sessionCookie = extractSessionCookie(registerResponse)!;
523
+
const response = await authRequest(`${BASE_URL}/api/user`, sessionCookie, {
527
+
expect(response.status).toBe(200);
528
+
const data = await response.json();
529
+
expect(data.success).toBe(true);
531
+
// Verify user is deleted
532
+
const verifyResponse = await authRequest(`${BASE_URL}/api/auth/me`, sessionCookie);
533
+
expect(verifyResponse.status).toBe(401);
536
+
serverTest("should require authentication", async () => {
537
+
const response = await fetch(`${BASE_URL}/api/user`, {
541
+
expect(response.status).toBe(401);
545
+
describe("PUT /api/user/email", () => {
546
+
serverTest("should update user email", async () => {
548
+
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
549
+
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
551
+
headers: { "Content-Type": "application/json" },
552
+
body: JSON.stringify({
553
+
email: TEST_USER.email,
554
+
password: hashedPassword,
557
+
const sessionCookie = extractSessionCookie(registerResponse)!;
560
+
const newEmail = "newemail@example.com";
561
+
const response = await authRequest(`${BASE_URL}/api/user/email`, sessionCookie, {
563
+
headers: { "Content-Type": "application/json" },
564
+
body: JSON.stringify({ email: newEmail }),
567
+
expect(response.status).toBe(200);
568
+
const data = await response.json();
569
+
expect(data.success).toBe(true);
571
+
// Verify email updated
572
+
const meResponse = await authRequest(`${BASE_URL}/api/auth/me`, sessionCookie);
573
+
const meData = await meResponse.json();
574
+
expect(meData.email).toBe(newEmail);
577
+
serverTest("should reject duplicate email", async () => {
578
+
// Register two users
579
+
const hashedPassword1 = await clientHashPassword(TEST_USER.email, TEST_USER.password);
580
+
await fetch(`${BASE_URL}/api/auth/register`, {
582
+
headers: { "Content-Type": "application/json" },
583
+
body: JSON.stringify({
584
+
email: TEST_USER.email,
585
+
password: hashedPassword1,
589
+
const hashedPassword2 = await clientHashPassword(TEST_USER_2.email, TEST_USER_2.password);
590
+
const user2Response = await fetch(`${BASE_URL}/api/auth/register`, {
592
+
headers: { "Content-Type": "application/json" },
593
+
body: JSON.stringify({
594
+
email: TEST_USER_2.email,
595
+
password: hashedPassword2,
598
+
const user2Cookie = extractSessionCookie(user2Response)!;
600
+
// Try to update user2's email to user1's email
601
+
const response = await authRequest(`${BASE_URL}/api/user/email`, user2Cookie, {
603
+
headers: { "Content-Type": "application/json" },
604
+
body: JSON.stringify({ email: TEST_USER.email }),
607
+
expect(response.status).toBe(400);
608
+
const data = await response.json();
609
+
expect(data.error).toBe("Email already in use");
613
+
describe("PUT /api/user/password", () => {
614
+
serverTest("should update user password", async () => {
616
+
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
617
+
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
619
+
headers: { "Content-Type": "application/json" },
620
+
body: JSON.stringify({
621
+
email: TEST_USER.email,
622
+
password: hashedPassword,
625
+
const sessionCookie = extractSessionCookie(registerResponse)!;
628
+
const newPassword = await clientHashPassword(TEST_USER.email, "NewPassword123!");
629
+
const response = await authRequest(`${BASE_URL}/api/user/password`, sessionCookie, {
631
+
headers: { "Content-Type": "application/json" },
632
+
body: JSON.stringify({ password: newPassword }),
635
+
expect(response.status).toBe(200);
636
+
const data = await response.json();
637
+
expect(data.success).toBe(true);
639
+
// Verify can login with new password
640
+
const loginResponse = await fetch(`${BASE_URL}/api/auth/login`, {
642
+
headers: { "Content-Type": "application/json" },
643
+
body: JSON.stringify({
644
+
email: TEST_USER.email,
645
+
password: newPassword,
648
+
expect(loginResponse.status).toBe(200);
651
+
serverTest("should reject invalid password format", async () => {
653
+
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
654
+
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
656
+
headers: { "Content-Type": "application/json" },
657
+
body: JSON.stringify({
658
+
email: TEST_USER.email,
659
+
password: hashedPassword,
662
+
const sessionCookie = extractSessionCookie(registerResponse)!;
664
+
// Try to update with invalid format
665
+
const response = await authRequest(`${BASE_URL}/api/user/password`, sessionCookie, {
667
+
headers: { "Content-Type": "application/json" },
668
+
body: JSON.stringify({ password: "short" }),
671
+
expect(response.status).toBe(400);
672
+
const data = await response.json();
673
+
expect(data.error).toBe("Invalid password format");
677
+
describe("PUT /api/user/name", () => {
678
+
serverTest("should update user name", async () => {
680
+
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
681
+
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
683
+
headers: { "Content-Type": "application/json" },
684
+
body: JSON.stringify({
685
+
email: TEST_USER.email,
686
+
password: hashedPassword,
687
+
name: TEST_USER.name,
690
+
const sessionCookie = extractSessionCookie(registerResponse)!;
693
+
const newName = "Updated Name";
694
+
const response = await authRequest(`${BASE_URL}/api/user/name`, sessionCookie, {
696
+
headers: { "Content-Type": "application/json" },
697
+
body: JSON.stringify({ name: newName }),
700
+
expect(response.status).toBe(200);
701
+
const data = await response.json();
702
+
expect(data.success).toBe(true);
704
+
// Verify name updated
705
+
const meResponse = await authRequest(`${BASE_URL}/api/auth/me`, sessionCookie);
706
+
const meData = await meResponse.json();
707
+
expect(meData.name).toBe(newName);
710
+
serverTest("should reject missing name", async () => {
712
+
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
713
+
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
715
+
headers: { "Content-Type": "application/json" },
716
+
body: JSON.stringify({
717
+
email: TEST_USER.email,
718
+
password: hashedPassword,
721
+
const sessionCookie = extractSessionCookie(registerResponse)!;
723
+
const response = await authRequest(`${BASE_URL}/api/user/name`, sessionCookie, {
725
+
headers: { "Content-Type": "application/json" },
726
+
body: JSON.stringify({}),
729
+
expect(response.status).toBe(400);
733
+
describe("PUT /api/user/avatar", () => {
734
+
serverTest("should update user avatar", async () => {
736
+
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
737
+
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
739
+
headers: { "Content-Type": "application/json" },
740
+
body: JSON.stringify({
741
+
email: TEST_USER.email,
742
+
password: hashedPassword,
745
+
const sessionCookie = extractSessionCookie(registerResponse)!;
748
+
const newAvatar = "👨💻";
749
+
const response = await authRequest(`${BASE_URL}/api/user/avatar`, sessionCookie, {
751
+
headers: { "Content-Type": "application/json" },
752
+
body: JSON.stringify({ avatar: newAvatar }),
755
+
expect(response.status).toBe(200);
756
+
const data = await response.json();
757
+
expect(data.success).toBe(true);
759
+
// Verify avatar updated
760
+
const meResponse = await authRequest(`${BASE_URL}/api/auth/me`, sessionCookie);
761
+
const meData = await meResponse.json();
762
+
expect(meData.avatar).toBe(newAvatar);
767
+
describe("API Endpoints - Transcriptions", () => {
768
+
describe("GET /api/transcriptions/health", () => {
769
+
serverTest("should return transcription service health status", async () => {
770
+
const response = await fetch(`${BASE_URL}/api/transcriptions/health`);
772
+
expect(response.status).toBe(200);
773
+
const data = await response.json();
774
+
expect(data).toHaveProperty("available");
775
+
expect(typeof data.available).toBe("boolean");
779
+
describe("GET /api/transcriptions", () => {
780
+
serverTest("should return user transcriptions", async () => {
782
+
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
783
+
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
785
+
headers: { "Content-Type": "application/json" },
786
+
body: JSON.stringify({
787
+
email: TEST_USER.email,
788
+
password: hashedPassword,
791
+
const sessionCookie = extractSessionCookie(registerResponse)!;
793
+
// Get transcriptions
794
+
const response = await authRequest(`${BASE_URL}/api/transcriptions`, sessionCookie);
796
+
expect(response.status).toBe(200);
797
+
const data = await response.json();
798
+
expect(data.jobs).toBeDefined();
799
+
expect(Array.isArray(data.jobs)).toBe(true);
802
+
serverTest("should require authentication", async () => {
803
+
const response = await fetch(`${BASE_URL}/api/transcriptions`);
805
+
expect(response.status).toBe(401);
809
+
describe("POST /api/transcriptions", () => {
810
+
serverTest("should upload audio file and start transcription", async () => {
812
+
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
813
+
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
815
+
headers: { "Content-Type": "application/json" },
816
+
body: JSON.stringify({
817
+
email: TEST_USER.email,
818
+
password: hashedPassword,
821
+
const sessionCookie = extractSessionCookie(registerResponse)!;
823
+
// Create a test audio file
824
+
const audioBlob = new Blob(["fake audio data"], { type: "audio/mp3" });
825
+
const formData = new FormData();
826
+
formData.append("audio", audioBlob, "test.mp3");
827
+
formData.append("class_name", "Test Class");
830
+
const response = await authRequest(`${BASE_URL}/api/transcriptions`, sessionCookie, {
835
+
expect(response.status).toBe(200);
836
+
const data = await response.json();
837
+
expect(data.id).toBeDefined();
838
+
expect(data.message).toContain("Upload successful");
841
+
serverTest("should reject non-audio files", async () => {
843
+
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
844
+
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
846
+
headers: { "Content-Type": "application/json" },
847
+
body: JSON.stringify({
848
+
email: TEST_USER.email,
849
+
password: hashedPassword,
852
+
const sessionCookie = extractSessionCookie(registerResponse)!;
854
+
// Try to upload non-audio file
855
+
const textBlob = new Blob(["text file"], { type: "text/plain" });
856
+
const formData = new FormData();
857
+
formData.append("audio", textBlob, "test.txt");
859
+
const response = await authRequest(`${BASE_URL}/api/transcriptions`, sessionCookie, {
864
+
expect(response.status).toBe(400);
867
+
serverTest("should reject files exceeding size limit", async () => {
869
+
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
870
+
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
872
+
headers: { "Content-Type": "application/json" },
873
+
body: JSON.stringify({
874
+
email: TEST_USER.email,
875
+
password: hashedPassword,
878
+
const sessionCookie = extractSessionCookie(registerResponse)!;
880
+
// Create a file larger than 25MB (the limit)
881
+
const largeBlob = new Blob([new ArrayBuffer(26 * 1024 * 1024)], { type: "audio/mp3" });
882
+
const formData = new FormData();
883
+
formData.append("audio", largeBlob, "large.mp3");
885
+
const response = await authRequest(`${BASE_URL}/api/transcriptions`, sessionCookie, {
890
+
expect(response.status).toBe(400);
893
+
serverTest("should require authentication", async () => {
894
+
const audioBlob = new Blob(["fake audio data"], { type: "audio/mp3" });
895
+
const formData = new FormData();
896
+
formData.append("audio", audioBlob, "test.mp3");
898
+
const response = await fetch(`${BASE_URL}/api/transcriptions`, {
903
+
expect(response.status).toBe(401);
908
+
describe("API Endpoints - Admin", () => {
909
+
let adminCookie: string;
910
+
let userCookie: string;
911
+
let userId: number;
913
+
beforeEach(async () => {
914
+
if (!serverAvailable) return;
916
+
// Create admin user
917
+
const adminHash = await clientHashPassword(TEST_ADMIN.email, TEST_ADMIN.password);
918
+
const adminResponse = await fetch(`${BASE_URL}/api/auth/register`, {
920
+
headers: { "Content-Type": "application/json" },
921
+
body: JSON.stringify({
922
+
email: TEST_ADMIN.email,
923
+
password: adminHash,
924
+
name: TEST_ADMIN.name,
927
+
adminCookie = extractSessionCookie(adminResponse)!;
929
+
// Manually set admin role in database
930
+
db.run("UPDATE users SET role = 'admin' WHERE email = ?", [TEST_ADMIN.email]);
932
+
// Create regular user
933
+
const userHash = await clientHashPassword(TEST_USER.email, TEST_USER.password);
934
+
const userResponse = await fetch(`${BASE_URL}/api/auth/register`, {
936
+
headers: { "Content-Type": "application/json" },
937
+
body: JSON.stringify({
938
+
email: TEST_USER.email,
939
+
password: userHash,
940
+
name: TEST_USER.name,
943
+
userCookie = extractSessionCookie(userResponse)!;
946
+
const userIdResult = db.query<{ id: number }, [string]>(
947
+
"SELECT id FROM users WHERE email = ?"
948
+
).get(TEST_USER.email);
949
+
userId = userIdResult!.id;
952
+
describe("GET /api/admin/users", () => {
953
+
serverTest("should return all users for admin", async () => {
954
+
const response = await authRequest(`${BASE_URL}/api/admin/users`, adminCookie);
956
+
expect(response.status).toBe(200);
957
+
const data = await response.json();
958
+
expect(Array.isArray(data)).toBe(true);
959
+
expect(data.length).toBeGreaterThan(0);
962
+
serverTest("should reject non-admin users", async () => {
963
+
const response = await authRequest(`${BASE_URL}/api/admin/users`, userCookie);
965
+
expect(response.status).toBe(403);
968
+
serverTest("should require authentication", async () => {
969
+
const response = await fetch(`${BASE_URL}/api/admin/users`);
971
+
expect(response.status).toBe(401);
975
+
describe("GET /api/admin/transcriptions", () => {
976
+
serverTest("should return all transcriptions for admin", async () => {
977
+
const response = await authRequest(`${BASE_URL}/api/admin/transcriptions`, adminCookie);
979
+
expect(response.status).toBe(200);
980
+
const data = await response.json();
981
+
expect(Array.isArray(data)).toBe(true);
984
+
serverTest("should reject non-admin users", async () => {
985
+
const response = await authRequest(`${BASE_URL}/api/admin/transcriptions`, userCookie);
987
+
expect(response.status).toBe(403);
991
+
describe("DELETE /api/admin/users/:id", () => {
992
+
serverTest("should delete user as admin", async () => {
993
+
const response = await authRequest(
994
+
`${BASE_URL}/api/admin/users/${userId}`,
1001
+
expect(response.status).toBe(200);
1002
+
const data = await response.json();
1003
+
expect(data.success).toBe(true);
1005
+
// Verify user is deleted
1006
+
const verifyResponse = await authRequest(`${BASE_URL}/api/auth/me`, userCookie);
1007
+
expect(verifyResponse.status).toBe(401);
1010
+
serverTest("should reject non-admin users", async () => {
1011
+
const response = await authRequest(
1012
+
`${BASE_URL}/api/admin/users/${userId}`,
1019
+
expect(response.status).toBe(403);
1023
+
describe("PUT /api/admin/users/:id/role", () => {
1024
+
serverTest("should update user role as admin", async () => {
1025
+
const response = await authRequest(
1026
+
`${BASE_URL}/api/admin/users/${userId}/role`,
1030
+
headers: { "Content-Type": "application/json" },
1031
+
body: JSON.stringify({ role: "admin" }),
1035
+
expect(response.status).toBe(200);
1036
+
const data = await response.json();
1037
+
expect(data.success).toBe(true);
1039
+
// Verify role updated
1040
+
const meResponse = await authRequest(`${BASE_URL}/api/auth/me`, userCookie);
1041
+
const meData = await meResponse.json();
1042
+
expect(meData.role).toBe("admin");
1045
+
serverTest("should reject invalid roles", async () => {
1046
+
const response = await authRequest(
1047
+
`${BASE_URL}/api/admin/users/${userId}/role`,
1051
+
headers: { "Content-Type": "application/json" },
1052
+
body: JSON.stringify({ role: "superadmin" }),
1056
+
expect(response.status).toBe(400);
1060
+
describe("GET /api/admin/users/:id/details", () => {
1061
+
serverTest("should return user details for admin", async () => {
1062
+
const response = await authRequest(
1063
+
`${BASE_URL}/api/admin/users/${userId}/details`,
1067
+
expect(response.status).toBe(200);
1068
+
const data = await response.json();
1069
+
expect(data.id).toBe(userId);
1070
+
expect(data.email).toBe(TEST_USER.email);
1071
+
expect(data).toHaveProperty("passkeys");
1072
+
expect(data).toHaveProperty("sessions");
1075
+
serverTest("should reject non-admin users", async () => {
1076
+
const response = await authRequest(
1077
+
`${BASE_URL}/api/admin/users/${userId}/details`,
1081
+
expect(response.status).toBe(403);
1085
+
describe("PUT /api/admin/users/:id/name", () => {
1086
+
serverTest("should update user name as admin", async () => {
1087
+
const newName = "Admin Updated Name";
1088
+
const response = await authRequest(
1089
+
`${BASE_URL}/api/admin/users/${userId}/name`,
1093
+
headers: { "Content-Type": "application/json" },
1094
+
body: JSON.stringify({ name: newName }),
1098
+
expect(response.status).toBe(200);
1099
+
const data = await response.json();
1100
+
expect(data.success).toBe(true);
1103
+
serverTest("should reject empty names", async () => {
1104
+
const response = await authRequest(
1105
+
`${BASE_URL}/api/admin/users/${userId}/name`,
1109
+
headers: { "Content-Type": "application/json" },
1110
+
body: JSON.stringify({ name: "" }),
1114
+
expect(response.status).toBe(400);
1118
+
describe("PUT /api/admin/users/:id/email", () => {
1119
+
serverTest("should update user email as admin", async () => {
1120
+
const newEmail = "newemail@admin.com";
1121
+
const response = await authRequest(
1122
+
`${BASE_URL}/api/admin/users/${userId}/email`,
1126
+
headers: { "Content-Type": "application/json" },
1127
+
body: JSON.stringify({ email: newEmail }),
1131
+
expect(response.status).toBe(200);
1132
+
const data = await response.json();
1133
+
expect(data.success).toBe(true);
1136
+
serverTest("should reject duplicate emails", async () => {
1137
+
const response = await authRequest(
1138
+
`${BASE_URL}/api/admin/users/${userId}/email`,
1142
+
headers: { "Content-Type": "application/json" },
1143
+
body: JSON.stringify({ email: TEST_ADMIN.email }),
1147
+
expect(response.status).toBe(400);
1148
+
const data = await response.json();
1149
+
expect(data.error).toBe("Email already in use");
1153
+
describe("GET /api/admin/users/:id/sessions", () => {
1154
+
serverTest("should return user sessions as admin", async () => {
1155
+
const response = await authRequest(
1156
+
`${BASE_URL}/api/admin/users/${userId}/sessions`,
1160
+
expect(response.status).toBe(200);
1161
+
const data = await response.json();
1162
+
expect(Array.isArray(data)).toBe(true);
1166
+
describe("DELETE /api/admin/users/:id/sessions", () => {
1167
+
serverTest("should delete all user sessions as admin", async () => {
1168
+
const response = await authRequest(
1169
+
`${BASE_URL}/api/admin/users/${userId}/sessions`,
1176
+
expect(response.status).toBe(200);
1177
+
const data = await response.json();
1178
+
expect(data.success).toBe(true);
1180
+
// Verify sessions are deleted
1181
+
const verifyResponse = await authRequest(`${BASE_URL}/api/auth/me`, userCookie);
1182
+
expect(verifyResponse.status).toBe(401);
1187
+
describe("API Endpoints - Passkeys", () => {
1188
+
let sessionCookie: string;
1190
+
beforeEach(async () => {
1191
+
if (!serverAvailable) return;
1194
+
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
1195
+
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
1197
+
headers: { "Content-Type": "application/json" },
1198
+
body: JSON.stringify({
1199
+
email: TEST_USER.email,
1200
+
password: hashedPassword,
1203
+
sessionCookie = extractSessionCookie(registerResponse)!;
1206
+
describe("GET /api/passkeys", () => {
1207
+
serverTest("should return user passkeys", async () => {
1208
+
const response = await authRequest(`${BASE_URL}/api/passkeys`, sessionCookie);
1210
+
expect(response.status).toBe(200);
1211
+
const data = await response.json();
1212
+
expect(data.passkeys).toBeDefined();
1213
+
expect(Array.isArray(data.passkeys)).toBe(true);
1216
+
serverTest("should require authentication", async () => {
1217
+
const response = await fetch(`${BASE_URL}/api/passkeys`);
1219
+
expect(response.status).toBe(401);
1223
+
describe("POST /api/passkeys/register/options", () => {
1224
+
serverTest("should return registration options for authenticated user", async () => {
1225
+
const response = await authRequest(
1226
+
`${BASE_URL}/api/passkeys/register/options`,
1233
+
expect(response.status).toBe(200);
1234
+
const data = await response.json();
1235
+
expect(data).toHaveProperty("challenge");
1236
+
expect(data).toHaveProperty("rp");
1237
+
expect(data).toHaveProperty("user");
1240
+
serverTest("should require authentication", async () => {
1241
+
const response = await fetch(`${BASE_URL}/api/passkeys/register/options`, {
1245
+
expect(response.status).toBe(401);
1249
+
describe("POST /api/passkeys/authenticate/options", () => {
1250
+
serverTest("should return authentication options for email", async () => {
1251
+
const response = await fetch(`${BASE_URL}/api/passkeys/authenticate/options`, {
1253
+
headers: { "Content-Type": "application/json" },
1254
+
body: JSON.stringify({ email: TEST_USER.email }),
1257
+
expect(response.status).toBe(200);
1258
+
const data = await response.json();
1259
+
expect(data).toHaveProperty("challenge");
1262
+
serverTest("should handle non-existent email", async () => {
1263
+
const response = await fetch(`${BASE_URL}/api/passkeys/authenticate/options`, {
1265
+
headers: { "Content-Type": "application/json" },
1266
+
body: JSON.stringify({ email: "nonexistent@example.com" }),
1269
+
// Should still return options for privacy (don't leak user existence)
1270
+
expect([200, 404]).toContain(response.status);