import { describe, expect, it } from 'vitest'; import { AUTH_SESSION_COOKIE_NAME, AuthError, getCurrentUser, getConfiguredAdminEmails, hashSessionToken, isConfiguredAdminEmail, parseCookieHeader, requireAdmin, requireUser, } from '../worker/auth'; import type { Env } from '../worker/env'; type SessionRow = { id: string; email: string | null; name: string | null; image_url: string | null; role: 'user' | 'admin'; expires_at: string; session_token_hash: string; }; class FakeSessionD1 { constructor(private readonly rows: SessionRow[]) {} prepare(sql: string): FakeSessionStatement { return new FakeSessionStatement(this.rows, sql); } } class FakeSessionStatement { private params: unknown[] = []; constructor( private readonly rows: SessionRow[], private readonly sql: string, ) {} bind(...params: unknown[]): this { this.params = params; return this; } async first(): Promise { expect(this.sql).toContain('session_token_hash'); expect(this.sql).toContain('JOIN users'); expect(this.sql).toContain('expires_at'); const hash = String(this.params[0]); const row = this.rows.find((candidate) => candidate.session_token_hash === hash); if (!row) { return null; } return { id: row.id, email: row.email, name: row.name, image_url: row.image_url, role: row.role, expires_at: row.expires_at, } as T; } } function makeEnv(rows: SessionRow[] = [], overrides: Partial = {}): { env: Env; db: FakeSessionD1 } { const db = new FakeSessionD1(rows); return { env: { DB: db as unknown as D1Database, PUBLIC_HOST: 'heygo.cc', PRIVATE_HOST: 'my.heygo.cc', APP_BASE_URL: 'https://heygo.cc', COOKIE_DOMAIN: '.heygo.cc', ...overrides, }, db, }; } function makeRequest(cookieHeader?: string | null): Request { const headers = new Headers(); if (cookieHeader != null) { headers.set('cookie', cookieHeader); } return new Request('https://my.heygo.cc/api/me', { headers }); } function futureIso(): string { return new Date(Date.now() + 60 * 60 * 1000).toISOString(); } function pastIso(): string { return new Date(Date.now() - 60 * 60 * 1000).toISOString(); } describe('parseCookieHeader', () => { it('returns empty object for null/undefined/empty', () => { expect(parseCookieHeader(null)).toEqual({}); expect(parseCookieHeader(undefined)).toEqual({}); expect(parseCookieHeader('')).toEqual({}); }); it('parses multiple cookies', () => { expect(parseCookieHeader('a=1; b=2; c=hello')).toEqual({ a: '1', b: '2', c: 'hello', }); }); it('trims whitespace around keys and values', () => { expect(parseCookieHeader(' a = 1 ; b = 2 ')).toEqual({ a: '1', b: '2', }); }); }); describe('hashSessionToken', () => { it('is deterministic and matches known SHA-256 for "hello"', async () => { const hash = await hashSessionToken('hello'); expect(hash).toBe('2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824'); }); it('returns lowercase hex', async () => { const hash = await hashSessionToken('SomeMixedCaseToken!'); expect(hash).toMatch(/^[0-9a-f]{64}$/); }); }); describe('configured admin emails', () => { it('parses comma, semicolon, and whitespace separated admin emails case-insensitively', () => { const emails = getConfiguredAdminEmails({ ADMIN_EMAILS: ' Boss@Heygo.cc, owner@example.com; ops@example.com\n', }); expect([...emails]).toEqual(['boss@heygo.cc', 'owner@example.com', 'ops@example.com']); expect(isConfiguredAdminEmail('boss@heygo.cc', { ADMIN_EMAILS: 'Boss@Heygo.cc' })).toBe(true); expect(isConfiguredAdminEmail('BOSS@HEYGO.CC', { ADMIN_EMAILS: 'boss@heygo.cc' })).toBe(true); expect(isConfiguredAdminEmail('person@heygo.cc', { ADMIN_EMAILS: 'boss@heygo.cc' })).toBe(false); }); }); describe('getCurrentUser', () => { it('returns null when there is no Cookie header', async () => { const { env } = makeEnv(); const user = await getCurrentUser(makeRequest(), env); expect(user).toBeNull(); }); it('returns null when the session cookie is missing', async () => { const { env } = makeEnv(); const user = await getCurrentUser(makeRequest('other=1'), env); expect(user).toBeNull(); }); it('returns null when the session token has no matching session row', async () => { const { env } = makeEnv(); const cookie = `${AUTH_SESSION_COOKIE_NAME}=unknown-token`; const user = await getCurrentUser(makeRequest(cookie), env); expect(user).toBeNull(); }); it('returns the user for a valid, non-expired session', async () => { const token = 'valid-session-token'; const hash = await hashSessionToken(token); const { env } = makeEnv([ { id: 'user_1', email: 'admin@heygo.cc', name: 'Admin', image_url: 'https://example.com/avatar.png', role: 'admin', expires_at: futureIso(), session_token_hash: hash, }, ]); const cookie = `${AUTH_SESSION_COOKIE_NAME}=${token}`; const user = await getCurrentUser(makeRequest(cookie), env); expect(user).toEqual({ id: 'user_1', email: 'admin@heygo.cc', name: 'Admin', imageUrl: 'https://example.com/avatar.png', role: 'admin', }); }); it('treats a configured admin email as admin even when the database role is user', async () => { const token = 'configured-admin-token'; const hash = await hashSessionToken(token); const { env } = makeEnv( [ { id: 'user_configured_admin', email: 'Boss@Heygo.cc', name: 'Boss', image_url: null, role: 'user', expires_at: futureIso(), session_token_hash: hash, }, ], { ADMIN_EMAILS: 'boss@heygo.cc' }, ); const cookie = `${AUTH_SESSION_COOKIE_NAME}=${token}`; const user = await getCurrentUser(makeRequest(cookie), env); expect(user?.role).toBe('admin'); }); it('returns null for an expired session', async () => { const token = 'expired-session-token'; const hash = await hashSessionToken(token); const { env } = makeEnv([ { id: 'user_2', email: 'user@heygo.cc', name: 'User', image_url: null, role: 'user', expires_at: pastIso(), session_token_hash: hash, }, ]); const cookie = `${AUTH_SESSION_COOKIE_NAME}=${token}`; const user = await getCurrentUser(makeRequest(cookie), env); expect(user).toBeNull(); }); }); describe('requireUser', () => { it('throws AuthError when unauthenticated', async () => { const { env } = makeEnv(); await expect(requireUser(makeRequest(), env)).rejects.toThrow(AuthError); await expect(requireUser(makeRequest(), env)).rejects.toMatchObject({ status: 401 }); }); it('returns the user when authenticated', async () => { const token = 'valid-user-token'; const hash = await hashSessionToken(token); const { env } = makeEnv([ { id: 'user_3', email: 'person@heygo.cc', name: 'Person', image_url: null, role: 'user', expires_at: futureIso(), session_token_hash: hash, }, ]); const cookie = `${AUTH_SESSION_COOKIE_NAME}=${token}`; const user = await requireUser(makeRequest(cookie), env); expect(user.id).toBe('user_3'); }); }); describe('requireAdmin', () => { it('rejects an unauthenticated request', async () => { const { env } = makeEnv(); await expect(requireAdmin(makeRequest(), env)).rejects.toThrow(AuthError); await expect(requireAdmin(makeRequest(), env)).rejects.toMatchObject({ status: 401 }); }); it('rejects a normal (non-admin) user', async () => { const token = 'normal-user-token'; const hash = await hashSessionToken(token); const { env } = makeEnv([ { id: 'user_4', email: 'normal@heygo.cc', name: 'Normal', image_url: null, role: 'user', expires_at: futureIso(), session_token_hash: hash, }, ]); const cookie = `${AUTH_SESSION_COOKIE_NAME}=${token}`; await expect(requireAdmin(makeRequest(cookie), env)).rejects.toThrow(AuthError); await expect(requireAdmin(makeRequest(cookie), env)).rejects.toMatchObject({ status: 403 }); }); it('returns the admin user for an admin session', async () => { const token = 'admin-session-token'; const hash = await hashSessionToken(token); const { env } = makeEnv([ { id: 'user_5', email: 'boss@heygo.cc', name: 'Boss', image_url: null, role: 'admin', expires_at: futureIso(), session_token_hash: hash, }, ]); const cookie = `${AUTH_SESSION_COOKIE_NAME}=${token}`; const user = await requireAdmin(makeRequest(cookie), env); expect(user.id).toBe('user_5'); expect(user.role).toBe('admin'); }); it('allows a configured admin email through requireAdmin', async () => { const token = 'configured-admin-require-token'; const hash = await hashSessionToken(token); const { env } = makeEnv( [ { id: 'user_6', email: 'owner@heygo.cc', name: 'Owner', image_url: null, role: 'user', expires_at: futureIso(), session_token_hash: hash, }, ], { ADMIN_EMAILS: 'owner@heygo.cc' }, ); const cookie = `${AUTH_SESSION_COOKIE_NAME}=${token}`; const user = await requireAdmin(makeRequest(cookie), env); expect(user.id).toBe('user_6'); expect(user.role).toBe('admin'); }); });