···
-
import { describe, expect, test, beforeAll, afterAll, beforeEach } from "bun:test";
import db from "./db/schema";
import { hashPasswordClient } from "./lib/client-auth";
···
serverAvailable = response.ok || response.status === 404;
-
`\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`
···
// Helper to hash passwords like the client would
-
async function clientHashPassword(email: string, password: string): Promise<string> {
return await hashPasswordClient(password, email);
// Helper to extract session cookie
-
function extractSessionCookie(response: Response): string | null {
const setCookie = response.headers.get("set-cookie");
-
if (!setCookie) return null;
const match = setCookie.match(/session=([^;]+)/);
-
return match ? match[1] : null;
// Helper to make authenticated requests
···
function cleanupTestData() {
// Delete test users and their related data (cascade will handle most of it)
// Include 'newemail%' to catch users whose emails were updated during tests
-
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%')");
-
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%')");
-
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%')");
-
db.run("DELETE FROM users WHERE email LIKE 'test%' OR email LIKE 'admin@%' OR email LIKE 'newemail%'");
// Clear ALL rate limit data to prevent accumulation across tests
// (IP-based rate limits don't contain test/admin in the key)
···
describe("API Endpoints - Authentication", () => {
describe("POST /api/auth/register", () => {
serverTest("should register a new user successfully", async () => {
-
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
const response = await fetch(`${BASE_URL}/api/auth/register`, {
···
expect(data.error).toBe("Email and password required");
-
serverTest("should reject registration with invalid password format", async () => {
-
const response = await fetch(`${BASE_URL}/api/auth/register`, {
-
headers: { "Content-Type": "application/json" },
-
email: TEST_USER.email,
-
expect(response.status).toBe(400);
-
const data = await response.json();
-
expect(data.error).toBe("Invalid password format");
serverTest("should reject duplicate email registration", async () => {
-
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
await fetch(`${BASE_URL}/api/auth/register`, {
···
serverTest("should enforce rate limiting on registration", async () => {
-
const hashedPassword = await clientHashPassword("test@example.com", "password");
// Make registration attempts until rate limit is hit (limit is 5 per hour)
let rateLimitHit = false;
···
describe("POST /api/auth/login", () => {
serverTest("should login successfully with valid credentials", async () => {
-
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
serverTest("should reject login with invalid credentials", async () => {
-
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
// Login with wrong password
-
const wrongPassword = await clientHashPassword(TEST_USER.email, "WrongPassword123!");
const response = await fetch(`${BASE_URL}/api/auth/login`, {
headers: { "Content-Type": "application/json" },
···
serverTest("should enforce rate limiting on login attempts", async () => {
-
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
// Make 11 login attempts (limit is 10 per 15 minutes per IP)
let rateLimitHit = false;
···
describe("POST /api/auth/logout", () => {
serverTest("should logout successfully", async () => {
-
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
const loginResponse = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
password: hashedPassword,
-
const sessionCookie = extractSessionCookie(loginResponse)!;
-
const response = await authRequest(`${BASE_URL}/api/auth/logout`, sessionCookie, {
expect(response.status).toBe(200);
const data = await response.json();
···
describe("GET /api/auth/me", () => {
-
serverTest("should return current user info when authenticated", async () => {
-
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
-
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
-
headers: { "Content-Type": "application/json" },
-
email: TEST_USER.email,
-
password: hashedPassword,
-
const sessionCookie = extractSessionCookie(registerResponse)!;
-
const response = await authRequest(`${BASE_URL}/api/auth/me`, sessionCookie);
-
expect(response.status).toBe(200);
-
const data = await response.json();
-
expect(data.email).toBe(TEST_USER.email);
-
expect(data.name).toBe(TEST_USER.name);
-
expect(data.role).toBeDefined();
serverTest("should return 401 when not authenticated", async () => {
const response = await fetch(`${BASE_URL}/api/auth/me`);
···
serverTest("should return 401 with invalid session", async () => {
-
const response = await authRequest(`${BASE_URL}/api/auth/me`, "invalid-session");
expect(response.status).toBe(401);
const data = await response.json();
···
describe("GET /api/sessions", () => {
serverTest("should return user sessions", async () => {
-
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
password: hashedPassword,
-
const sessionCookie = extractSessionCookie(registerResponse)!;
-
const response = await authRequest(`${BASE_URL}/api/sessions`, sessionCookie);
expect(response.status).toBe(200);
const data = await response.json();
···
describe("DELETE /api/sessions", () => {
serverTest("should delete specific session", async () => {
// Register user and create multiple sessions
-
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
const session1Response = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
password: hashedPassword,
-
const session1Cookie = extractSessionCookie(session1Response)!;
const session2Response = await fetch(`${BASE_URL}/api/auth/login`, {
···
password: hashedPassword,
-
const session2Cookie = extractSessionCookie(session2Response)!;
-
const sessionsResponse = await authRequest(`${BASE_URL}/api/sessions`, session1Cookie);
const sessionsData = await sessionsResponse.json();
const targetSessionId = sessionsData.sessions.find(
-
(s: any) => s.id === session2Cookie
-
const response = await authRequest(`${BASE_URL}/api/sessions`, session1Cookie, {
-
headers: { "Content-Type": "application/json" },
-
body: JSON.stringify({ sessionId: targetSessionId }),
expect(response.status).toBe(200);
const data = await response.json();
expect(data.success).toBe(true);
// Verify session 2 is deleted
-
const verifyResponse = await authRequest(`${BASE_URL}/api/auth/me`, session2Cookie);
expect(verifyResponse.status).toBe(401);
serverTest("should not delete another user's session", async () => {
-
const hashedPassword1 = await clientHashPassword(TEST_USER.email, TEST_USER.password);
const user1Response = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
password: hashedPassword1,
-
const user1Cookie = extractSessionCookie(user1Response)!;
-
const hashedPassword2 = await clientHashPassword(TEST_USER_2.email, TEST_USER_2.password);
const user2Response = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
password: hashedPassword2,
-
const user2Cookie = extractSessionCookie(user2Response)!;
// Try to delete user2's session using user1's credentials
-
const response = await authRequest(`${BASE_URL}/api/sessions`, user1Cookie, {
-
headers: { "Content-Type": "application/json" },
-
body: JSON.stringify({ sessionId: user2Cookie }),
expect(response.status).toBe(404);
···
describe("DELETE /api/user", () => {
serverTest("should delete user account", async () => {
-
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
password: hashedPassword,
-
const sessionCookie = extractSessionCookie(registerResponse)!;
-
const response = await authRequest(`${BASE_URL}/api/user`, sessionCookie, {
expect(response.status).toBe(200);
const data = await response.json();
expect(data.success).toBe(true);
// Verify user is deleted
-
const verifyResponse = await authRequest(`${BASE_URL}/api/auth/me`, sessionCookie);
expect(verifyResponse.status).toBe(401);
···
describe("PUT /api/user/email", () => {
serverTest("should update user email", async () => {
-
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
password: hashedPassword,
-
const sessionCookie = extractSessionCookie(registerResponse)!;
const newEmail = "newemail@example.com";
-
const response = await authRequest(`${BASE_URL}/api/user/email`, sessionCookie, {
-
headers: { "Content-Type": "application/json" },
-
body: JSON.stringify({ email: newEmail }),
expect(response.status).toBe(200);
const data = await response.json();
expect(data.success).toBe(true);
-
const meResponse = await authRequest(`${BASE_URL}/api/auth/me`, sessionCookie);
const meData = await meResponse.json();
expect(meData.email).toBe(newEmail);
serverTest("should reject duplicate email", async () => {
-
const hashedPassword1 = await clientHashPassword(TEST_USER.email, TEST_USER.password);
await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
-
const hashedPassword2 = await clientHashPassword(TEST_USER_2.email, TEST_USER_2.password);
const user2Response = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
password: hashedPassword2,
-
const user2Cookie = extractSessionCookie(user2Response)!;
// Try to update user2's email to user1's email
-
const response = await authRequest(`${BASE_URL}/api/user/email`, user2Cookie, {
-
headers: { "Content-Type": "application/json" },
-
body: JSON.stringify({ email: TEST_USER.email }),
expect(response.status).toBe(400);
const data = await response.json();
···
describe("PUT /api/user/password", () => {
serverTest("should update user password", async () => {
-
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
password: hashedPassword,
-
const sessionCookie = extractSessionCookie(registerResponse)!;
-
const newPassword = await clientHashPassword(TEST_USER.email, "NewPassword123!");
-
const response = await authRequest(`${BASE_URL}/api/user/password`, sessionCookie, {
-
headers: { "Content-Type": "application/json" },
-
body: JSON.stringify({ password: newPassword }),
expect(response.status).toBe(200);
const data = await response.json();
···
serverTest("should reject invalid password format", async () => {
-
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
password: hashedPassword,
-
const sessionCookie = extractSessionCookie(registerResponse)!;
// Try to update with invalid format
-
const response = await authRequest(`${BASE_URL}/api/user/password`, sessionCookie, {
-
headers: { "Content-Type": "application/json" },
-
body: JSON.stringify({ password: "short" }),
expect(response.status).toBe(400);
const data = await response.json();
···
describe("PUT /api/user/name", () => {
serverTest("should update user name", async () => {
-
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
-
const sessionCookie = extractSessionCookie(registerResponse)!;
const newName = "Updated Name";
-
const response = await authRequest(`${BASE_URL}/api/user/name`, sessionCookie, {
-
headers: { "Content-Type": "application/json" },
-
body: JSON.stringify({ name: newName }),
expect(response.status).toBe(200);
const data = await response.json();
expect(data.success).toBe(true);
-
const meResponse = await authRequest(`${BASE_URL}/api/auth/me`, sessionCookie);
const meData = await meResponse.json();
expect(meData.name).toBe(newName);
serverTest("should reject missing name", async () => {
-
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
password: hashedPassword,
-
const sessionCookie = extractSessionCookie(registerResponse)!;
-
const response = await authRequest(`${BASE_URL}/api/user/name`, sessionCookie, {
-
headers: { "Content-Type": "application/json" },
-
body: JSON.stringify({}),
expect(response.status).toBe(400);
···
describe("PUT /api/user/avatar", () => {
serverTest("should update user avatar", async () => {
-
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
password: hashedPassword,
-
const sessionCookie = extractSessionCookie(registerResponse)!;
-
const response = await authRequest(`${BASE_URL}/api/user/avatar`, sessionCookie, {
-
headers: { "Content-Type": "application/json" },
-
body: JSON.stringify({ avatar: newAvatar }),
expect(response.status).toBe(200);
const data = await response.json();
expect(data.success).toBe(true);
-
const meResponse = await authRequest(`${BASE_URL}/api/auth/me`, sessionCookie);
const meData = await meResponse.json();
expect(meData.avatar).toBe(newAvatar);
···
describe("API Endpoints - Transcriptions", () => {
describe("GET /api/transcriptions/health", () => {
-
serverTest("should return transcription service health status", async () => {
-
const response = await fetch(`${BASE_URL}/api/transcriptions/health`);
-
expect(response.status).toBe(200);
-
const data = await response.json();
-
expect(data).toHaveProperty("available");
-
expect(typeof data.available).toBe("boolean");
describe("GET /api/transcriptions", () => {
serverTest("should return user transcriptions", async () => {
-
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
password: hashedPassword,
-
const sessionCookie = extractSessionCookie(registerResponse)!;
-
const response = await authRequest(`${BASE_URL}/api/transcriptions`, sessionCookie);
expect(response.status).toBe(200);
const data = await response.json();
···
describe("POST /api/transcriptions", () => {
serverTest("should upload audio file and start transcription", async () => {
-
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
password: hashedPassword,
-
const sessionCookie = extractSessionCookie(registerResponse)!;
// Create a test audio file
const audioBlob = new Blob(["fake audio data"], { type: "audio/mp3" });
···
formData.append("class_name", "Test Class");
-
const response = await authRequest(`${BASE_URL}/api/transcriptions`, sessionCookie, {
expect(response.status).toBe(200);
const data = await response.json();
···
serverTest("should reject non-audio files", async () => {
-
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
password: hashedPassword,
-
const sessionCookie = extractSessionCookie(registerResponse)!;
// Try to upload non-audio file
const textBlob = new Blob(["text file"], { type: "text/plain" });
const formData = new FormData();
formData.append("audio", textBlob, "test.txt");
-
const response = await authRequest(`${BASE_URL}/api/transcriptions`, sessionCookie, {
expect(response.status).toBe(400);
serverTest("should reject files exceeding size limit", async () => {
-
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
password: hashedPassword,
-
const sessionCookie = extractSessionCookie(registerResponse)!;
// Create a file larger than 100MB (the actual limit)
-
const largeBlob = new Blob([new ArrayBuffer(101 * 1024 * 1024)], { type: "audio/mp3" });
const formData = new FormData();
formData.append("audio", largeBlob, "large.mp3");
-
const response = await authRequest(`${BASE_URL}/api/transcriptions`, sessionCookie, {
expect(response.status).toBe(400);
const data = await response.json();
···
if (!serverAvailable) return;
-
const adminHash = await clientHashPassword(TEST_ADMIN.email, TEST_ADMIN.password);
const adminResponse = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
-
adminCookie = extractSessionCookie(adminResponse)!;
// Manually set admin role in database
-
db.run("UPDATE users SET role = 'admin' WHERE email = ?", [TEST_ADMIN.email]);
-
const userHash = await clientHashPassword(TEST_USER.email, TEST_USER.password);
const userResponse = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
-
userCookie = extractSessionCookie(userResponse)!;
-
const userIdResult = db.query<{ id: number }, [string]>(
-
"SELECT id FROM users WHERE email = ?"
-
).get(TEST_USER.email);
-
userId = userIdResult!.id;
describe("GET /api/admin/users", () => {
serverTest("should return all users for admin", async () => {
-
const response = await authRequest(`${BASE_URL}/api/admin/users`, adminCookie);
expect(response.status).toBe(200);
const data = await response.json();
···
serverTest("should reject non-admin users", async () => {
-
const response = await authRequest(`${BASE_URL}/api/admin/users`, userCookie);
expect(response.status).toBe(403);
···
describe("GET /api/admin/transcriptions", () => {
serverTest("should return all transcriptions for admin", async () => {
-
const response = await authRequest(`${BASE_URL}/api/admin/transcriptions`, adminCookie);
expect(response.status).toBe(200);
const data = await response.json();
···
serverTest("should reject non-admin users", async () => {
-
const response = await authRequest(`${BASE_URL}/api/admin/transcriptions`, userCookie);
expect(response.status).toBe(403);
···
expect(response.status).toBe(200);
···
expect(data.success).toBe(true);
// Verify user is deleted
-
const verifyResponse = await authRequest(`${BASE_URL}/api/auth/me`, userCookie);
expect(verifyResponse.status).toBe(401);
···
expect(response.status).toBe(403);
···
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ role: "admin" }),
expect(response.status).toBe(200);
···
expect(data.success).toBe(true);
-
const meResponse = await authRequest(`${BASE_URL}/api/auth/me`, userCookie);
const meData = await meResponse.json();
expect(meData.role).toBe("admin");
···
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ role: "superadmin" }),
expect(response.status).toBe(400);
···
serverTest("should return user details for admin", async () => {
const response = await authRequest(
`${BASE_URL}/api/admin/users/${userId}/details`,
expect(response.status).toBe(200);
···
serverTest("should reject non-admin users", async () => {
const response = await authRequest(
`${BASE_URL}/api/admin/users/${userId}/details`,
expect(response.status).toBe(403);
···
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: newName }),
expect(response.status).toBe(200);
···
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "" }),
expect(response.status).toBe(400);
···
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: newEmail }),
expect(response.status).toBe(200);
···
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: TEST_ADMIN.email }),
expect(response.status).toBe(400);
···
serverTest("should return user sessions as admin", async () => {
const response = await authRequest(
`${BASE_URL}/api/admin/users/${userId}/sessions`,
expect(response.status).toBe(200);
···
expect(response.status).toBe(200);
···
expect(data.success).toBe(true);
// Verify sessions are deleted
-
const verifyResponse = await authRequest(`${BASE_URL}/api/auth/me`, userCookie);
expect(verifyResponse.status).toBe(401);
···
if (!serverAvailable) return;
-
const hashedPassword = await clientHashPassword(TEST_USER.email, TEST_USER.password);
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
password: hashedPassword,
-
sessionCookie = extractSessionCookie(registerResponse)!;
describe("GET /api/passkeys", () => {
serverTest("should return user passkeys", async () => {
-
const response = await authRequest(`${BASE_URL}/api/passkeys`, sessionCookie);
expect(response.status).toBe(200);
const data = await response.json();
···
describe("POST /api/passkeys/register/options", () => {
-
serverTest("should return registration options for authenticated user", async () => {
-
const response = await authRequest(
`${BASE_URL}/api/passkeys/register/options`,
-
expect(response.status).toBe(200);
-
const data = await response.json();
-
expect(data).toHaveProperty("challenge");
-
expect(data).toHaveProperty("rp");
-
expect(data).toHaveProperty("user");
-
serverTest("should require authentication", async () => {
-
const response = await fetch(`${BASE_URL}/api/passkeys/register/options`, {
expect(response.status).toBe(401);
describe("POST /api/passkeys/authenticate/options", () => {
serverTest("should return authentication options for email", async () => {
-
const response = await fetch(`${BASE_URL}/api/passkeys/authenticate/options`, {
-
headers: { "Content-Type": "application/json" },
-
body: JSON.stringify({ email: TEST_USER.email }),
expect(response.status).toBe(200);
const data = await response.json();
···
serverTest("should handle non-existent email", async () => {
-
const response = await fetch(`${BASE_URL}/api/passkeys/authenticate/options`, {
-
headers: { "Content-Type": "application/json" },
-
body: JSON.stringify({ email: "nonexistent@example.com" }),
// Should still return options for privacy (don't leak user existence)
expect([200, 404]).toContain(response.status);
···
import db from "./db/schema";
import { hashPasswordClient } from "./lib/client-auth";
···
serverAvailable = response.ok || response.status === 404;
+
`\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`,
···
// Helper to hash passwords like the client would
+
async function clientHashPassword(
return await hashPasswordClient(password, email);
// Helper to extract session cookie
+
function extractSessionCookie(response: Response): string {
const setCookie = response.headers.get("set-cookie");
+
if (!setCookie) throw new Error("No set-cookie header found");
const match = setCookie.match(/session=([^;]+)/);
+
if (!match) throw new Error("No session cookie found in set-cookie header");
// Helper to make authenticated requests
···
function cleanupTestData() {
// Delete test users and their related data (cascade will handle most of it)
// Include 'newemail%' to catch users whose emails were updated during tests
+
"DELETE FROM sessions WHERE user_id IN (SELECT id FROM users WHERE email LIKE 'test%' OR email LIKE 'admin@%' OR email LIKE 'newemail%')",
+
"DELETE FROM passkeys WHERE user_id IN (SELECT id FROM users WHERE email LIKE 'test%' OR email LIKE 'admin@%' OR email LIKE 'newemail%')",
+
"DELETE FROM transcriptions WHERE user_id IN (SELECT id FROM users WHERE email LIKE 'test%' OR email LIKE 'admin@%' OR email LIKE 'newemail%')",
+
"DELETE FROM users WHERE email LIKE 'test%' OR email LIKE 'admin@%' OR email LIKE 'newemail%'",
// Clear ALL rate limit data to prevent accumulation across tests
// (IP-based rate limits don't contain test/admin in the key)
···
describe("API Endpoints - Authentication", () => {
describe("POST /api/auth/register", () => {
serverTest("should register a new user successfully", async () => {
+
const hashedPassword = await clientHashPassword(
const response = await fetch(`${BASE_URL}/api/auth/register`, {
···
expect(data.error).toBe("Email and password required");
+
"should reject registration with invalid password format",
+
const response = await fetch(`${BASE_URL}/api/auth/register`, {
+
headers: { "Content-Type": "application/json" },
+
email: TEST_USER.email,
+
expect(response.status).toBe(400);
+
const data = await response.json();
+
expect(data.error).toBe("Invalid password format");
serverTest("should reject duplicate email registration", async () => {
+
const hashedPassword = await clientHashPassword(
await fetch(`${BASE_URL}/api/auth/register`, {
···
serverTest("should enforce rate limiting on registration", async () => {
+
const hashedPassword = await clientHashPassword(
// Make registration attempts until rate limit is hit (limit is 5 per hour)
let rateLimitHit = false;
···
describe("POST /api/auth/login", () => {
serverTest("should login successfully with valid credentials", async () => {
+
const hashedPassword = await clientHashPassword(
await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
serverTest("should reject login with invalid credentials", async () => {
+
const hashedPassword = await clientHashPassword(
await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
// Login with wrong password
+
const wrongPassword = await clientHashPassword(
const response = await fetch(`${BASE_URL}/api/auth/login`, {
headers: { "Content-Type": "application/json" },
···
serverTest("should enforce rate limiting on login attempts", async () => {
+
const hashedPassword = await clientHashPassword(
// Make 11 login attempts (limit is 10 per 15 minutes per IP)
let rateLimitHit = false;
···
describe("POST /api/auth/logout", () => {
serverTest("should logout successfully", async () => {
+
const hashedPassword = await clientHashPassword(
const loginResponse = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
password: hashedPassword,
+
const sessionCookie = extractSessionCookie(loginResponse);
+
const response = await authRequest(
+
`${BASE_URL}/api/auth/logout`,
expect(response.status).toBe(200);
const data = await response.json();
···
describe("GET /api/auth/me", () => {
+
"should return current user info when authenticated",
+
const hashedPassword = await clientHashPassword(
+
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
+
headers: { "Content-Type": "application/json" },
+
email: TEST_USER.email,
+
password: hashedPassword,
+
const sessionCookie = extractSessionCookie(registerResponse);
+
const response = await authRequest(
+
`${BASE_URL}/api/auth/me`,
+
expect(response.status).toBe(200);
+
const data = await response.json();
+
expect(data.email).toBe(TEST_USER.email);
+
expect(data.name).toBe(TEST_USER.name);
+
expect(data.role).toBeDefined();
serverTest("should return 401 when not authenticated", async () => {
const response = await fetch(`${BASE_URL}/api/auth/me`);
···
serverTest("should return 401 with invalid session", async () => {
+
const response = await authRequest(
+
`${BASE_URL}/api/auth/me`,
expect(response.status).toBe(401);
const data = await response.json();
···
describe("GET /api/sessions", () => {
serverTest("should return user sessions", async () => {
+
const hashedPassword = await clientHashPassword(
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
password: hashedPassword,
+
const sessionCookie = extractSessionCookie(registerResponse);
+
const response = await authRequest(
+
`${BASE_URL}/api/sessions`,
expect(response.status).toBe(200);
const data = await response.json();
···
describe("DELETE /api/sessions", () => {
serverTest("should delete specific session", async () => {
// Register user and create multiple sessions
+
const hashedPassword = await clientHashPassword(
const session1Response = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
password: hashedPassword,
+
const session1Cookie = extractSessionCookie(session1Response);
const session2Response = await fetch(`${BASE_URL}/api/auth/login`, {
···
password: hashedPassword,
+
const session2Cookie = extractSessionCookie(session2Response);
+
const sessionsResponse = await authRequest(
+
`${BASE_URL}/api/sessions`,
const sessionsData = await sessionsResponse.json();
const targetSessionId = sessionsData.sessions.find(
+
(s: { id: string }) => s.id === session2Cookie,
+
const response = await authRequest(
+
`${BASE_URL}/api/sessions`,
+
headers: { "Content-Type": "application/json" },
+
body: JSON.stringify({ sessionId: targetSessionId }),
expect(response.status).toBe(200);
const data = await response.json();
expect(data.success).toBe(true);
// Verify session 2 is deleted
+
const verifyResponse = await authRequest(
+
`${BASE_URL}/api/auth/me`,
expect(verifyResponse.status).toBe(401);
serverTest("should not delete another user's session", async () => {
+
const hashedPassword1 = await clientHashPassword(
const user1Response = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
password: hashedPassword1,
+
const user1Cookie = extractSessionCookie(user1Response);
+
const hashedPassword2 = await clientHashPassword(
const user2Response = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
password: hashedPassword2,
+
const user2Cookie = extractSessionCookie(user2Response);
// Try to delete user2's session using user1's credentials
+
const response = await authRequest(
+
`${BASE_URL}/api/sessions`,
+
headers: { "Content-Type": "application/json" },
+
body: JSON.stringify({ sessionId: user2Cookie }),
expect(response.status).toBe(404);
···
describe("DELETE /api/user", () => {
serverTest("should delete user account", async () => {
+
const hashedPassword = await clientHashPassword(
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
password: hashedPassword,
+
const sessionCookie = extractSessionCookie(registerResponse);
+
const response = await authRequest(
+
`${BASE_URL}/api/user`,
expect(response.status).toBe(200);
const data = await response.json();
expect(data.success).toBe(true);
// Verify user is deleted
+
const verifyResponse = await authRequest(
+
`${BASE_URL}/api/auth/me`,
expect(verifyResponse.status).toBe(401);
···
describe("PUT /api/user/email", () => {
serverTest("should update user email", async () => {
+
const hashedPassword = await clientHashPassword(
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
password: hashedPassword,
+
const sessionCookie = extractSessionCookie(registerResponse);
const newEmail = "newemail@example.com";
+
const response = await authRequest(
+
`${BASE_URL}/api/user/email`,
+
headers: { "Content-Type": "application/json" },
+
body: JSON.stringify({ email: newEmail }),
expect(response.status).toBe(200);
const data = await response.json();
expect(data.success).toBe(true);
+
const meResponse = await authRequest(
+
`${BASE_URL}/api/auth/me`,
const meData = await meResponse.json();
expect(meData.email).toBe(newEmail);
serverTest("should reject duplicate email", async () => {
+
const hashedPassword1 = await clientHashPassword(
await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
+
const hashedPassword2 = await clientHashPassword(
const user2Response = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
password: hashedPassword2,
+
const user2Cookie = extractSessionCookie(user2Response);
// Try to update user2's email to user1's email
+
const response = await authRequest(
+
`${BASE_URL}/api/user/email`,
+
headers: { "Content-Type": "application/json" },
+
body: JSON.stringify({ email: TEST_USER.email }),
expect(response.status).toBe(400);
const data = await response.json();
···
describe("PUT /api/user/password", () => {
serverTest("should update user password", async () => {
+
const hashedPassword = await clientHashPassword(
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
password: hashedPassword,
+
const sessionCookie = extractSessionCookie(registerResponse);
+
const newPassword = await clientHashPassword(
+
const response = await authRequest(
+
`${BASE_URL}/api/user/password`,
+
headers: { "Content-Type": "application/json" },
+
body: JSON.stringify({ password: newPassword }),
expect(response.status).toBe(200);
const data = await response.json();
···
serverTest("should reject invalid password format", async () => {
+
const hashedPassword = await clientHashPassword(
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
password: hashedPassword,
+
const sessionCookie = extractSessionCookie(registerResponse);
// Try to update with invalid format
+
const response = await authRequest(
+
`${BASE_URL}/api/user/password`,
+
headers: { "Content-Type": "application/json" },
+
body: JSON.stringify({ password: "short" }),
expect(response.status).toBe(400);
const data = await response.json();
···
describe("PUT /api/user/name", () => {
serverTest("should update user name", async () => {
+
const hashedPassword = await clientHashPassword(
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
+
const sessionCookie = extractSessionCookie(registerResponse);
const newName = "Updated Name";
+
const response = await authRequest(
+
`${BASE_URL}/api/user/name`,
+
headers: { "Content-Type": "application/json" },
+
body: JSON.stringify({ name: newName }),
expect(response.status).toBe(200);
const data = await response.json();
expect(data.success).toBe(true);
+
const meResponse = await authRequest(
+
`${BASE_URL}/api/auth/me`,
const meData = await meResponse.json();
expect(meData.name).toBe(newName);
serverTest("should reject missing name", async () => {
+
const hashedPassword = await clientHashPassword(
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
password: hashedPassword,
+
const sessionCookie = extractSessionCookie(registerResponse);
+
const response = await authRequest(
+
`${BASE_URL}/api/user/name`,
+
headers: { "Content-Type": "application/json" },
+
body: JSON.stringify({}),
expect(response.status).toBe(400);
···
describe("PUT /api/user/avatar", () => {
serverTest("should update user avatar", async () => {
+
const hashedPassword = await clientHashPassword(
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
password: hashedPassword,
+
const sessionCookie = extractSessionCookie(registerResponse);
+
const response = await authRequest(
+
`${BASE_URL}/api/user/avatar`,
+
headers: { "Content-Type": "application/json" },
+
body: JSON.stringify({ avatar: newAvatar }),
expect(response.status).toBe(200);
const data = await response.json();
expect(data.success).toBe(true);
+
const meResponse = await authRequest(
+
`${BASE_URL}/api/auth/me`,
const meData = await meResponse.json();
expect(meData.avatar).toBe(newAvatar);
···
describe("API Endpoints - Transcriptions", () => {
describe("GET /api/transcriptions/health", () => {
+
"should return transcription service health status",
+
const response = await fetch(`${BASE_URL}/api/transcriptions/health`);
+
expect(response.status).toBe(200);
+
const data = await response.json();
+
expect(data).toHaveProperty("available");
+
expect(typeof data.available).toBe("boolean");
describe("GET /api/transcriptions", () => {
serverTest("should return user transcriptions", async () => {
+
const hashedPassword = await clientHashPassword(
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
password: hashedPassword,
+
const sessionCookie = extractSessionCookie(registerResponse);
+
const response = await authRequest(
+
`${BASE_URL}/api/transcriptions`,
expect(response.status).toBe(200);
const data = await response.json();
···
describe("POST /api/transcriptions", () => {
serverTest("should upload audio file and start transcription", async () => {
+
const hashedPassword = await clientHashPassword(
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
password: hashedPassword,
+
const sessionCookie = extractSessionCookie(registerResponse);
// Create a test audio file
const audioBlob = new Blob(["fake audio data"], { type: "audio/mp3" });
···
formData.append("class_name", "Test Class");
+
const response = await authRequest(
+
`${BASE_URL}/api/transcriptions`,
expect(response.status).toBe(200);
const data = await response.json();
···
serverTest("should reject non-audio files", async () => {
+
const hashedPassword = await clientHashPassword(
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
password: hashedPassword,
+
const sessionCookie = extractSessionCookie(registerResponse);
// Try to upload non-audio file
const textBlob = new Blob(["text file"], { type: "text/plain" });
const formData = new FormData();
formData.append("audio", textBlob, "test.txt");
+
const response = await authRequest(
+
`${BASE_URL}/api/transcriptions`,
expect(response.status).toBe(400);
serverTest("should reject files exceeding size limit", async () => {
+
const hashedPassword = await clientHashPassword(
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
password: hashedPassword,
+
const sessionCookie = extractSessionCookie(registerResponse);
// Create a file larger than 100MB (the actual limit)
+
const largeBlob = new Blob([new ArrayBuffer(101 * 1024 * 1024)], {
const formData = new FormData();
formData.append("audio", largeBlob, "large.mp3");
+
const response = await authRequest(
+
`${BASE_URL}/api/transcriptions`,
expect(response.status).toBe(400);
const data = await response.json();
···
if (!serverAvailable) return;
+
const adminHash = await clientHashPassword(
const adminResponse = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
+
adminCookie = extractSessionCookie(adminResponse);
// Manually set admin role in database
+
db.run("UPDATE users SET role = 'admin' WHERE email = ?", [
+
const userHash = await clientHashPassword(
const userResponse = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
+
userCookie = extractSessionCookie(userResponse);
+
const userIdResult = db
+
.query<{ id: number }, [string]>("SELECT id FROM users WHERE email = ?")
+
userId = userIdResult?.id;
describe("GET /api/admin/users", () => {
serverTest("should return all users for admin", async () => {
+
const response = await authRequest(
+
`${BASE_URL}/api/admin/users`,
expect(response.status).toBe(200);
const data = await response.json();
···
serverTest("should reject non-admin users", async () => {
+
const response = await authRequest(
+
`${BASE_URL}/api/admin/users`,
expect(response.status).toBe(403);
···
describe("GET /api/admin/transcriptions", () => {
serverTest("should return all transcriptions for admin", async () => {
+
const response = await authRequest(
+
`${BASE_URL}/api/admin/transcriptions`,
expect(response.status).toBe(200);
const data = await response.json();
···
serverTest("should reject non-admin users", async () => {
+
const response = await authRequest(
+
`${BASE_URL}/api/admin/transcriptions`,
expect(response.status).toBe(403);
···
expect(response.status).toBe(200);
···
expect(data.success).toBe(true);
// Verify user is deleted
+
const verifyResponse = await authRequest(
+
`${BASE_URL}/api/auth/me`,
expect(verifyResponse.status).toBe(401);
···
expect(response.status).toBe(403);
···
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ role: "admin" }),
expect(response.status).toBe(200);
···
expect(data.success).toBe(true);
+
const meResponse = await authRequest(
+
`${BASE_URL}/api/auth/me`,
const meData = await meResponse.json();
expect(meData.role).toBe("admin");
···
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ role: "superadmin" }),
expect(response.status).toBe(400);
···
serverTest("should return user details for admin", async () => {
const response = await authRequest(
`${BASE_URL}/api/admin/users/${userId}/details`,
expect(response.status).toBe(200);
···
serverTest("should reject non-admin users", async () => {
const response = await authRequest(
`${BASE_URL}/api/admin/users/${userId}/details`,
expect(response.status).toBe(403);
···
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: newName }),
expect(response.status).toBe(200);
···
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "" }),
expect(response.status).toBe(400);
···
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: newEmail }),
expect(response.status).toBe(200);
···
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: TEST_ADMIN.email }),
expect(response.status).toBe(400);
···
serverTest("should return user sessions as admin", async () => {
const response = await authRequest(
`${BASE_URL}/api/admin/users/${userId}/sessions`,
expect(response.status).toBe(200);
···
expect(response.status).toBe(200);
···
expect(data.success).toBe(true);
// Verify sessions are deleted
+
const verifyResponse = await authRequest(
+
`${BASE_URL}/api/auth/me`,
expect(verifyResponse.status).toBe(401);
···
if (!serverAvailable) return;
+
const hashedPassword = await clientHashPassword(
const registerResponse = await fetch(`${BASE_URL}/api/auth/register`, {
headers: { "Content-Type": "application/json" },
···
password: hashedPassword,
+
sessionCookie = extractSessionCookie(registerResponse);
describe("GET /api/passkeys", () => {
serverTest("should return user passkeys", async () => {
+
const response = await authRequest(
+
`${BASE_URL}/api/passkeys`,
expect(response.status).toBe(200);
const data = await response.json();
···
describe("POST /api/passkeys/register/options", () => {
+
"should return registration options for authenticated user",
+
const response = await authRequest(
+
`${BASE_URL}/api/passkeys/register/options`,
+
expect(response.status).toBe(200);
+
const data = await response.json();
+
expect(data).toHaveProperty("challenge");
+
expect(data).toHaveProperty("rp");
+
expect(data).toHaveProperty("user");
+
serverTest("should require authentication", async () => {
+
const response = await fetch(
`${BASE_URL}/api/passkeys/register/options`,
expect(response.status).toBe(401);
describe("POST /api/passkeys/authenticate/options", () => {
serverTest("should return authentication options for email", async () => {
+
const response = await fetch(
+
`${BASE_URL}/api/passkeys/authenticate/options`,
+
headers: { "Content-Type": "application/json" },
+
body: JSON.stringify({ email: TEST_USER.email }),
expect(response.status).toBe(200);
const data = await response.json();
···
serverTest("should handle non-existent email", async () => {
+
const response = await fetch(
+
`${BASE_URL}/api/passkeys/authenticate/options`,
+
headers: { "Content-Type": "application/json" },
+
body: JSON.stringify({ email: "nonexistent@example.com" }),
// Should still return options for privacy (don't leak user existence)
expect([200, 404]).toContain(response.status);