···
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
+
// 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%'");
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");
88
+
if (serverAvailable) {
94
+
if (serverAvailable) {
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)`);
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);
115
+
const response = await fetch(`${BASE_URL}/api/auth/register`, {
117
+
headers: { "Content-Type": "application/json" },
118
+
body: JSON.stringify({
119
+
email: TEST_USER.email,
120
+
password: hashedPassword,
121
+
name: TEST_USER.name,
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();
132
+
serverTest("should reject registration with missing email", async () => {
133
+
const response = await fetch(`${BASE_URL}/api/auth/register`, {
135
+
headers: { "Content-Type": "application/json" },
136
+
body: JSON.stringify({
137
+
password: "hashedpassword123456",
141
+
expect(response.status).toBe(400);
142
+
const data = await response.json();
143
+
expect(data.error).toBe("Email and password required");
146
+
serverTest("should reject registration with invalid password format", async () => {
147
+
const response = await fetch(`${BASE_URL}/api/auth/register`, {
149
+
headers: { "Content-Type": "application/json" },
150
+
body: JSON.stringify({
151
+
email: TEST_USER.email,
156
+
expect(response.status).toBe(400);
157
+
const data = await response.json();
158
+
expect(data.error).toBe("Invalid password format");
161
+
serverTest("should reject duplicate email registration", async () => {
162
+
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
164
+
// First registration
165
+
await fetch(`${BASE_URL}/api/auth/register`, {
167
+
headers: { "Content-Type": "application/json" },
168
+
body: JSON.stringify({
169
+
email: TEST_USER.email,
170
+
password: hashedPassword,
171
+
name: TEST_USER.name,
175
+
// Duplicate registration
176
+
const response = await fetch(`${BASE_URL}/api/auth/register`, {
178
+
headers: { "Content-Type": "application/json" },
179
+
body: JSON.stringify({
180
+
email: TEST_USER.email,
181
+
password: hashedPassword,
182
+
name: TEST_USER.name,
186
+
expect(response.status).toBe(400);
187
+
const data = await response.json();
188
+
expect(data.error).toBe("Email already registered");
191
+
serverTest("should enforce rate limiting on registration", async () => {
192
+
const hashedPassword = await clientHashPassword("test@example.com", "password");
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`, {
199
+
headers: { "Content-Type": "application/json" },
200
+
body: JSON.stringify({
201
+
email: `test${i}@example.com`,
202
+
password: hashedPassword,
206
+
if (response.status === 429) {
207
+
rateLimitHit = true;
212
+
// Verify that rate limiting was triggered
213
+
expect(rateLimitHit).toBe(true);
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`, {
223
+
headers: { "Content-Type": "application/json" },
224
+
body: JSON.stringify({
225
+
email: TEST_USER.email,
226
+
password: hashedPassword,
227
+
name: TEST_USER.name,
232
+
const response = await fetch(`${BASE_URL}/api/auth/login`, {
234
+
headers: { "Content-Type": "application/json" },
235
+
body: JSON.stringify({
236
+
email: TEST_USER.email,
237
+
password: hashedPassword,
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();
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`, {
253
+
headers: { "Content-Type": "application/json" },
254
+
body: JSON.stringify({
255
+
email: TEST_USER.email,
256
+
password: hashedPassword,
260
+
// Login with wrong password
261
+
const wrongPassword = await clientHashPassword(TEST_USER.email, "WrongPassword123!");
262
+
const response = await fetch(`${BASE_URL}/api/auth/login`, {
264
+
headers: { "Content-Type": "application/json" },
265
+
body: JSON.stringify({
266
+
email: TEST_USER.email,
267
+
password: wrongPassword,
271
+
expect(response.status).toBe(401);
272
+
const data = await response.json();
273
+
expect(data.error).toBe("Invalid email or password");
276
+
serverTest("should reject login with missing fields", async () => {
277
+
const response = await fetch(`${BASE_URL}/api/auth/login`, {
279
+
headers: { "Content-Type": "application/json" },
280
+
body: JSON.stringify({
281
+
email: TEST_USER.email,
285
+
expect(response.status).toBe(400);
286
+
const data = await response.json();
287
+
expect(data.error).toBe("Email and password required");
290
+
serverTest("should enforce rate limiting on login attempts", async () => {
291
+
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
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`, {
298
+
headers: { "Content-Type": "application/json" },
299
+
body: JSON.stringify({
300
+
email: TEST_USER.email,
301
+
password: hashedPassword,
305
+
if (response.status === 429) {
306
+
rateLimitHit = true;
311
+
// Verify that rate limiting was triggered
312
+
expect(rateLimitHit).toBe(true);
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`, {
322
+
headers: { "Content-Type": "application/json" },
323
+
body: JSON.stringify({
324
+
email: TEST_USER.email,
325
+
password: hashedPassword,
328
+
const sessionCookie = extractSessionCookie(loginResponse)!;
331
+
const response = await authRequest(`${BASE_URL}/api/auth/logout`, sessionCookie, {
335
+
expect(response.status).toBe(200);
336
+
const data = await response.json();
337
+
expect(data.success).toBe(true);
339
+
// Verify cookie is cleared
340
+
const setCookie = response.headers.get("set-cookie");
341
+
expect(setCookie).toContain("Max-Age=0");
344
+
serverTest("should logout even without valid session", async () => {
345
+
const response = await fetch(`${BASE_URL}/api/auth/logout`, {
349
+
expect(response.status).toBe(200);
350
+
const data = await response.json();
351
+
expect(data.success).toBe(true);
355
+
describe("GET /api/auth/me", () => {
356
+
serverTest("should return current user info when authenticated", async () => {
358
+
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
359
+
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
361
+
headers: { "Content-Type": "application/json" },
362
+
body: JSON.stringify({
363
+
email: TEST_USER.email,
364
+
password: hashedPassword,
365
+
name: TEST_USER.name,
368
+
const sessionCookie = extractSessionCookie(registerResponse)!;
370
+
// Get current user
371
+
const response = await authRequest(`${BASE_URL}/api/auth/me`, sessionCookie);
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();
380
+
serverTest("should return 401 when not authenticated", async () => {
381
+
const response = await fetch(`${BASE_URL}/api/auth/me`);
383
+
expect(response.status).toBe(401);
384
+
const data = await response.json();
385
+
expect(data.error).toBe("Not authenticated");
388
+
serverTest("should return 401 with invalid session", async () => {
389
+
const response = await authRequest(`${BASE_URL}/api/auth/me`, "invalid-session");
391
+
expect(response.status).toBe(401);
392
+
const data = await response.json();
393
+
expect(data.error).toBe("Invalid session");
398
+
describe("API Endpoints - Session Management", () => {
399
+
describe("GET /api/sessions", () => {
400
+
serverTest("should return user sessions", async () => {
402
+
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
403
+
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
405
+
headers: { "Content-Type": "application/json" },
406
+
body: JSON.stringify({
407
+
email: TEST_USER.email,
408
+
password: hashedPassword,
411
+
const sessionCookie = extractSessionCookie(registerResponse)!;
414
+
const response = await authRequest(`${BASE_URL}/api/sessions`, sessionCookie);
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");
425
+
serverTest("should require authentication", async () => {
426
+
const response = await fetch(`${BASE_URL}/api/sessions`);
428
+
expect(response.status).toBe(401);
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`, {
438
+
headers: { "Content-Type": "application/json" },
439
+
body: JSON.stringify({
440
+
email: TEST_USER.email,
441
+
password: hashedPassword,
444
+
const session1Cookie = extractSessionCookie(session1Response)!;
446
+
const session2Response = await fetch(`${BASE_URL}/api/auth/login`, {
448
+
headers: { "Content-Type": "application/json" },
449
+
body: JSON.stringify({
450
+
email: TEST_USER.email,
451
+
password: hashedPassword,
454
+
const session2Cookie = extractSessionCookie(session2Response)!;
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
463
+
// Delete session 2
464
+
const response = await authRequest(`${BASE_URL}/api/sessions`, session1Cookie, {
466
+
headers: { "Content-Type": "application/json" },
467
+
body: JSON.stringify({ sessionId: targetSessionId }),
470
+
expect(response.status).toBe(200);
471
+
const data = await response.json();
472
+
expect(data.success).toBe(true);
474
+
// Verify session 2 is deleted
475
+
const verifyResponse = await authRequest(`${BASE_URL}/api/auth/me`, session2Cookie);
476
+
expect(verifyResponse.status).toBe(401);
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`, {
484
+
headers: { "Content-Type": "application/json" },
485
+
body: JSON.stringify({
486
+
email: TEST_USER.email,
487
+
password: hashedPassword1,
490
+
const user1Cookie = extractSessionCookie(user1Response)!;
492
+
const hashedPassword2 = await clientHashPassword(TEST_USER_2.email, TEST_USER_2.password);
493
+
const user2Response = await fetch(`${BASE_URL}/api/auth/register`, {
495
+
headers: { "Content-Type": "application/json" },
496
+
body: JSON.stringify({
497
+
email: TEST_USER_2.email,
498
+
password: hashedPassword2,
501
+
const user2Cookie = extractSessionCookie(user2Response)!;
503
+
// Try to delete user2's session using user1's credentials
504
+
const response = await authRequest(`${BASE_URL}/api/sessions`, user1Cookie, {
506
+
headers: { "Content-Type": "application/json" },
507
+
body: JSON.stringify({ sessionId: user2Cookie }),
510
+
expect(response.status).toBe(404);
515
+
describe("API Endpoints - User Management", () => {
516
+
describe("DELETE /api/user", () => {
517
+
serverTest("should delete user account", async () => {
519
+
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
520
+
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
522
+
headers: { "Content-Type": "application/json" },
523
+
body: JSON.stringify({
524
+
email: TEST_USER.email,
525
+
password: hashedPassword,
528
+
const sessionCookie = extractSessionCookie(registerResponse)!;
531
+
const response = await authRequest(`${BASE_URL}/api/user`, sessionCookie, {
535
+
expect(response.status).toBe(200);
536
+
const data = await response.json();
537
+
expect(data.success).toBe(true);
539
+
// Verify user is deleted
540
+
const verifyResponse = await authRequest(`${BASE_URL}/api/auth/me`, sessionCookie);
541
+
expect(verifyResponse.status).toBe(401);
544
+
serverTest("should require authentication", async () => {
545
+
const response = await fetch(`${BASE_URL}/api/user`, {
549
+
expect(response.status).toBe(401);
553
+
describe("PUT /api/user/email", () => {
554
+
serverTest("should update user email", async () => {
556
+
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
557
+
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
559
+
headers: { "Content-Type": "application/json" },
560
+
body: JSON.stringify({
561
+
email: TEST_USER.email,
562
+
password: hashedPassword,
565
+
const sessionCookie = extractSessionCookie(registerResponse)!;
568
+
const newEmail = "newemail@example.com";
569
+
const response = await authRequest(`${BASE_URL}/api/user/email`, sessionCookie, {
571
+
headers: { "Content-Type": "application/json" },
572
+
body: JSON.stringify({ email: newEmail }),
575
+
expect(response.status).toBe(200);
576
+
const data = await response.json();
577
+
expect(data.success).toBe(true);
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);
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`, {
590
+
headers: { "Content-Type": "application/json" },
591
+
body: JSON.stringify({
592
+
email: TEST_USER.email,
593
+
password: hashedPassword1,
597
+
const hashedPassword2 = await clientHashPassword(TEST_USER_2.email, TEST_USER_2.password);
598
+
const user2Response = await fetch(`${BASE_URL}/api/auth/register`, {
600
+
headers: { "Content-Type": "application/json" },
601
+
body: JSON.stringify({
602
+
email: TEST_USER_2.email,
603
+
password: hashedPassword2,
606
+
const user2Cookie = extractSessionCookie(user2Response)!;
608
+
// Try to update user2's email to user1's email
609
+
const response = await authRequest(`${BASE_URL}/api/user/email`, user2Cookie, {
611
+
headers: { "Content-Type": "application/json" },
612
+
body: JSON.stringify({ email: TEST_USER.email }),
615
+
expect(response.status).toBe(400);
616
+
const data = await response.json();
617
+
expect(data.error).toBe("Email already in use");
621
+
describe("PUT /api/user/password", () => {
622
+
serverTest("should update user password", async () => {
624
+
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
625
+
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
627
+
headers: { "Content-Type": "application/json" },
628
+
body: JSON.stringify({
629
+
email: TEST_USER.email,
630
+
password: hashedPassword,
633
+
const sessionCookie = extractSessionCookie(registerResponse)!;
636
+
const newPassword = await clientHashPassword(TEST_USER.email, "NewPassword123!");
637
+
const response = await authRequest(`${BASE_URL}/api/user/password`, sessionCookie, {
639
+
headers: { "Content-Type": "application/json" },
640
+
body: JSON.stringify({ password: newPassword }),
643
+
expect(response.status).toBe(200);
644
+
const data = await response.json();
645
+
expect(data.success).toBe(true);
647
+
// Verify can login with new password
648
+
const loginResponse = await fetch(`${BASE_URL}/api/auth/login`, {
650
+
headers: { "Content-Type": "application/json" },
651
+
body: JSON.stringify({
652
+
email: TEST_USER.email,
653
+
password: newPassword,
656
+
expect(loginResponse.status).toBe(200);
659
+
serverTest("should reject invalid password format", async () => {
661
+
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
662
+
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
664
+
headers: { "Content-Type": "application/json" },
665
+
body: JSON.stringify({
666
+
email: TEST_USER.email,
667
+
password: hashedPassword,
670
+
const sessionCookie = extractSessionCookie(registerResponse)!;
672
+
// Try to update with invalid format
673
+
const response = await authRequest(`${BASE_URL}/api/user/password`, sessionCookie, {
675
+
headers: { "Content-Type": "application/json" },
676
+
body: JSON.stringify({ password: "short" }),
679
+
expect(response.status).toBe(400);
680
+
const data = await response.json();
681
+
expect(data.error).toBe("Invalid password format");
685
+
describe("PUT /api/user/name", () => {
686
+
serverTest("should update user name", async () => {
688
+
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
689
+
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
691
+
headers: { "Content-Type": "application/json" },
692
+
body: JSON.stringify({
693
+
email: TEST_USER.email,
694
+
password: hashedPassword,
695
+
name: TEST_USER.name,
698
+
const sessionCookie = extractSessionCookie(registerResponse)!;
701
+
const newName = "Updated Name";
702
+
const response = await authRequest(`${BASE_URL}/api/user/name`, sessionCookie, {
704
+
headers: { "Content-Type": "application/json" },
705
+
body: JSON.stringify({ name: newName }),
708
+
expect(response.status).toBe(200);
709
+
const data = await response.json();
710
+
expect(data.success).toBe(true);
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);
718
+
serverTest("should reject missing name", async () => {
720
+
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
721
+
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
723
+
headers: { "Content-Type": "application/json" },
724
+
body: JSON.stringify({
725
+
email: TEST_USER.email,
726
+
password: hashedPassword,
729
+
const sessionCookie = extractSessionCookie(registerResponse)!;
731
+
const response = await authRequest(`${BASE_URL}/api/user/name`, sessionCookie, {
733
+
headers: { "Content-Type": "application/json" },
734
+
body: JSON.stringify({}),
737
+
expect(response.status).toBe(400);
741
+
describe("PUT /api/user/avatar", () => {
742
+
serverTest("should update user avatar", async () => {
744
+
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
745
+
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
747
+
headers: { "Content-Type": "application/json" },
748
+
body: JSON.stringify({
749
+
email: TEST_USER.email,
750
+
password: hashedPassword,
753
+
const sessionCookie = extractSessionCookie(registerResponse)!;
756
+
const newAvatar = "👨💻";
757
+
const response = await authRequest(`${BASE_URL}/api/user/avatar`, sessionCookie, {
759
+
headers: { "Content-Type": "application/json" },
760
+
body: JSON.stringify({ avatar: newAvatar }),
763
+
expect(response.status).toBe(200);
764
+
const data = await response.json();
765
+
expect(data.success).toBe(true);
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);
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`);
780
+
expect(response.status).toBe(200);
781
+
const data = await response.json();
782
+
expect(data).toHaveProperty("available");
783
+
expect(typeof data.available).toBe("boolean");
787
+
describe("GET /api/transcriptions", () => {
788
+
serverTest("should return user transcriptions", async () => {
790
+
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
791
+
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
793
+
headers: { "Content-Type": "application/json" },
794
+
body: JSON.stringify({
795
+
email: TEST_USER.email,
796
+
password: hashedPassword,
799
+
const sessionCookie = extractSessionCookie(registerResponse)!;
801
+
// Get transcriptions
802
+
const response = await authRequest(`${BASE_URL}/api/transcriptions`, sessionCookie);
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);
810
+
serverTest("should require authentication", async () => {
811
+
const response = await fetch(`${BASE_URL}/api/transcriptions`);
813
+
expect(response.status).toBe(401);
817
+
describe("POST /api/transcriptions", () => {
818
+
serverTest("should upload audio file and start transcription", async () => {
820
+
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
821
+
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
823
+
headers: { "Content-Type": "application/json" },
824
+
body: JSON.stringify({
825
+
email: TEST_USER.email,
826
+
password: hashedPassword,
829
+
const sessionCookie = extractSessionCookie(registerResponse)!;
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");
838
+
const response = await authRequest(`${BASE_URL}/api/transcriptions`, sessionCookie, {
843
+
expect(response.status).toBe(200);
844
+
const data = await response.json();
845
+
expect(data.id).toBeDefined();
846
+
expect(data.message).toContain("Upload successful");
849
+
serverTest("should reject non-audio files", async () => {
851
+
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
852
+
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
854
+
headers: { "Content-Type": "application/json" },
855
+
body: JSON.stringify({
856
+
email: TEST_USER.email,
857
+
password: hashedPassword,
860
+
const sessionCookie = extractSessionCookie(registerResponse)!;
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");
867
+
const response = await authRequest(`${BASE_URL}/api/transcriptions`, sessionCookie, {
872
+
expect(response.status).toBe(400);
875
+
serverTest("should reject files exceeding size limit", async () => {
877
+
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
878
+
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
880
+
headers: { "Content-Type": "application/json" },
881
+
body: JSON.stringify({
882
+
email: TEST_USER.email,
883
+
password: hashedPassword,
886
+
const sessionCookie = extractSessionCookie(registerResponse)!;
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");
893
+
const response = await authRequest(`${BASE_URL}/api/transcriptions`, sessionCookie, {
898
+
expect(response.status).toBe(400);
899
+
const data = await response.json();
900
+
expect(data.error).toContain("File size must be less than");
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");
908
+
const response = await fetch(`${BASE_URL}/api/transcriptions`, {
913
+
expect(response.status).toBe(401);
918
+
describe("API Endpoints - Admin", () => {
919
+
let adminCookie: string;
920
+
let userCookie: string;
921
+
let userId: number;
923
+
beforeEach(async () => {
924
+
if (!serverAvailable) return;
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`, {
930
+
headers: { "Content-Type": "application/json" },
931
+
body: JSON.stringify({
932
+
email: TEST_ADMIN.email,
933
+
password: adminHash,
934
+
name: TEST_ADMIN.name,
937
+
adminCookie = extractSessionCookie(adminResponse)!;
939
+
// Manually set admin role in database
940
+
db.run("UPDATE users SET role = 'admin' WHERE email = ?", [TEST_ADMIN.email]);
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`, {
946
+
headers: { "Content-Type": "application/json" },
947
+
body: JSON.stringify({
948
+
email: TEST_USER.email,
949
+
password: userHash,
950
+
name: TEST_USER.name,
953
+
userCookie = extractSessionCookie(userResponse)!;
956
+
const userIdResult = db.query<{ id: number }, [string]>(
957
+
"SELECT id FROM users WHERE email = ?"
958
+
).get(TEST_USER.email);
959
+
userId = userIdResult!.id;
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);
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);
972
+
serverTest("should reject non-admin users", async () => {
973
+
const response = await authRequest(`${BASE_URL}/api/admin/users`, userCookie);
975
+
expect(response.status).toBe(403);
978
+
serverTest("should require authentication", async () => {
979
+
const response = await fetch(`${BASE_URL}/api/admin/users`);
981
+
expect(response.status).toBe(401);
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);
989
+
expect(response.status).toBe(200);
990
+
const data = await response.json();
991
+
expect(Array.isArray(data)).toBe(true);
994
+
serverTest("should reject non-admin users", async () => {
995
+
const response = await authRequest(`${BASE_URL}/api/admin/transcriptions`, userCookie);
997
+
expect(response.status).toBe(403);
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}`,
1011
+
expect(response.status).toBe(200);
1012
+
const data = await response.json();
1013
+
expect(data.success).toBe(true);
1015
+
// Verify user is deleted
1016
+
const verifyResponse = await authRequest(`${BASE_URL}/api/auth/me`, userCookie);
1017
+
expect(verifyResponse.status).toBe(401);
1020
+
serverTest("should reject non-admin users", async () => {
1021
+
const response = await authRequest(
1022
+
`${BASE_URL}/api/admin/users/${userId}`,
1029
+
expect(response.status).toBe(403);
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`,
1040
+
headers: { "Content-Type": "application/json" },
1041
+
body: JSON.stringify({ role: "admin" }),
1045
+
expect(response.status).toBe(200);
1046
+
const data = await response.json();
1047
+
expect(data.success).toBe(true);
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");
1055
+
serverTest("should reject invalid roles", async () => {
1056
+
const response = await authRequest(
1057
+
`${BASE_URL}/api/admin/users/${userId}/role`,
1061
+
headers: { "Content-Type": "application/json" },
1062
+
body: JSON.stringify({ role: "superadmin" }),
1066
+
expect(response.status).toBe(400);
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`,
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");
1085
+
serverTest("should reject non-admin users", async () => {
1086
+
const response = await authRequest(
1087
+
`${BASE_URL}/api/admin/users/${userId}/details`,
1091
+
expect(response.status).toBe(403);
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`,
1103
+
headers: { "Content-Type": "application/json" },
1104
+
body: JSON.stringify({ name: newName }),
1108
+
expect(response.status).toBe(200);
1109
+
const data = await response.json();
1110
+
expect(data.success).toBe(true);
1113
+
serverTest("should reject empty names", async () => {
1114
+
const response = await authRequest(
1115
+
`${BASE_URL}/api/admin/users/${userId}/name`,
1119
+
headers: { "Content-Type": "application/json" },
1120
+
body: JSON.stringify({ name: "" }),
1124
+
expect(response.status).toBe(400);
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`,
1136
+
headers: { "Content-Type": "application/json" },
1137
+
body: JSON.stringify({ email: newEmail }),
1141
+
expect(response.status).toBe(200);
1142
+
const data = await response.json();
1143
+
expect(data.success).toBe(true);
1146
+
serverTest("should reject duplicate emails", async () => {
1147
+
const response = await authRequest(
1148
+
`${BASE_URL}/api/admin/users/${userId}/email`,
1152
+
headers: { "Content-Type": "application/json" },
1153
+
body: JSON.stringify({ email: TEST_ADMIN.email }),
1157
+
expect(response.status).toBe(400);
1158
+
const data = await response.json();
1159
+
expect(data.error).toBe("Email already in use");
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`,
1170
+
expect(response.status).toBe(200);
1171
+
const data = await response.json();
1172
+
expect(Array.isArray(data)).toBe(true);
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`,
1186
+
expect(response.status).toBe(200);
1187
+
const data = await response.json();
1188
+
expect(data.success).toBe(true);
1190
+
// Verify sessions are deleted
1191
+
const verifyResponse = await authRequest(`${BASE_URL}/api/auth/me`, userCookie);
1192
+
expect(verifyResponse.status).toBe(401);
1197
+
describe("API Endpoints - Passkeys", () => {
1198
+
let sessionCookie: string;
1200
+
beforeEach(async () => {
1201
+
if (!serverAvailable) return;
1204
+
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
1205
+
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
1207
+
headers: { "Content-Type": "application/json" },
1208
+
body: JSON.stringify({
1209
+
email: TEST_USER.email,
1210
+
password: hashedPassword,
1213
+
sessionCookie = extractSessionCookie(registerResponse)!;
1216
+
describe("GET /api/passkeys", () => {
1217
+
serverTest("should return user passkeys", async () => {
1218
+
const response = await authRequest(`${BASE_URL}/api/passkeys`, sessionCookie);
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);
1226
+
serverTest("should require authentication", async () => {
1227
+
const response = await fetch(`${BASE_URL}/api/passkeys`);
1229
+
expect(response.status).toBe(401);
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`,
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");
1250
+
serverTest("should require authentication", async () => {
1251
+
const response = await fetch(`${BASE_URL}/api/passkeys/register/options`, {
1255
+
expect(response.status).toBe(401);
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`, {
1263
+
headers: { "Content-Type": "application/json" },
1264
+
body: JSON.stringify({ email: TEST_USER.email }),
1267
+
expect(response.status).toBe(200);
1268
+
const data = await response.json();
1269
+
expect(data).toHaveProperty("challenge");
1272
+
serverTest("should handle non-existent email", async () => {
1273
+
const response = await fetch(`${BASE_URL}/api/passkeys/authenticate/options`, {
1275
+
headers: { "Content-Type": "application/json" },
1276
+
body: JSON.stringify({ email: "nonexistent@example.com" }),
1279
+
// Should still return options for privacy (don't leak user existence)
1280
+
expect([200, 404]).toContain(response.status);