import { describe, expect, it } from 'vitest'; import worker from '../worker/index'; import { AUTH_SESSION_COOKIE_NAME, hashSessionToken } from '../worker/auth'; import type { Env } from '../worker/env'; type UserRole = 'user' | 'admin'; type SessionUserRow = { id: string; email: string | null; name: string | null; image_url: string | null; role: UserRole; expires_at: string; session_token_hash: string; }; type FirstCall = { sql: string; params: unknown[]; }; class FakeAuthD1 { readonly rows: SessionUserRow[] = []; readonly firstCalls: FirstCall[] = []; prepare(sql: string): FakeAuthStatement { return new FakeAuthStatement(this, sql); } findSessionByHash(hash: string): SessionUserRow | null { return this.rows.find((r) => r.session_token_hash === hash) ?? null; } } class FakeAuthStatement { private params: unknown[] = []; constructor( private readonly db: FakeAuthD1, private readonly sql: string, ) {} bind(...params: unknown[]): this { this.params = params; return this; } async first(): Promise { this.db.firstCalls.push({ sql: this.sql, params: this.params }); // The getCurrentUser session lookup: SELECT users ... JOIN sessions if (this.sql.includes('FROM sessions') && this.sql.includes('JOIN users')) { const hash = String(this.params[0]); const row = this.db.findSessionByHash(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; } return null; } } class FakeExecutionContext { waitUntil(): void {} passThroughOnException(): void {} } type EnvOverrides = Partial; function makeEnv(db: FakeAuthD1, overrides: EnvOverrides = {}): Env { return { DB: db as unknown as D1Database, PUBLIC_HOST: 'localhost', PRIVATE_HOST: 'localhost', APP_BASE_URL: 'http://localhost:8787', COOKIE_DOMAIN: '', ...overrides, } as Env; } async function fetchWorker( path: string, env: Env, init: RequestInit = {}, ): Promise { const url = `http://localhost:8787${path}`; const request = new Request(url, init); const ctx = new FakeExecutionContext(); return worker.fetch( request as unknown as Parameters[0], env as unknown as Parameters[1], ctx as unknown as Parameters[2], ); } function futureIso(): string { return new Date(Date.now() + 60 * 60 * 1000).toISOString(); } function cookieHeader(token: string): string { return `${AUTH_SESSION_COOKIE_NAME}=${token}`; } describe('GET /api/auth/me', () => { it('returns { user: null } with 200 when no Cookie header is present', async () => { const db = new FakeAuthD1(); const env = makeEnv(db); const response = await fetchWorker('/api/auth/me', env, { method: 'GET' }); expect(response.status).toBe(200); const data = (await response.json()) as { user: unknown; publicBaseUrl: string; privateBaseUrl: string }; expect(data).toEqual({ user: null, publicBaseUrl: 'http://localhost:8787', privateBaseUrl: 'http://localhost:8787', }); // No session lookup should have run when there's no cookie expect(db.firstCalls).toHaveLength(0); }); it('returns { user: null } with 200 when the session cookie is missing', async () => { const db = new FakeAuthD1(); const env = makeEnv(db); const response = await fetchWorker('/api/auth/me', env, { method: 'GET', headers: { cookie: 'other_cookie=abc' }, }); expect(response.status).toBe(200); const data = (await response.json()) as { user: unknown; publicBaseUrl: string; privateBaseUrl: string }; expect(data).toEqual({ user: null, publicBaseUrl: 'http://localhost:8787', privateBaseUrl: 'http://localhost:8787', }); expect(db.firstCalls).toHaveLength(0); }); it('returns the user payload when a valid session cookie exists', async () => { const token = 'valid-me-token'; const hash = await hashSessionToken(token); const db = new FakeAuthD1(); db.rows.push({ 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 env = makeEnv(db); const response = await fetchWorker('/api/auth/me', env, { method: 'GET', headers: { cookie: cookieHeader(token) }, }); expect(response.status).toBe(200); const data = (await response.json()) as { user: { id: string; email: string | null; name: string | null; imageUrl: string | null; role: 'user' | 'admin'; } | null; publicBaseUrl: string; privateBaseUrl: string; }; expect(data.user).toEqual({ id: 'user_1', email: 'admin@heygo.cc', name: 'Admin', imageUrl: 'https://example.com/avatar.png', role: 'admin', }); expect(data.publicBaseUrl).toBe('http://localhost:8787'); expect(data.privateBaseUrl).toBe('http://localhost:8787'); // One session lookup was performed expect(db.firstCalls).toHaveLength(1); expect(db.firstCalls[0].sql).toContain('FROM sessions'); expect(db.firstCalls[0].sql).toContain('JOIN users'); }); it('returns the user payload for a non-admin user', async () => { const token = 'normal-me-token'; const hash = await hashSessionToken(token); const db = new FakeAuthD1(); db.rows.push({ id: 'user_2', email: 'person@heygo.cc', name: 'Person', image_url: null, role: 'user', expires_at: futureIso(), session_token_hash: hash, }); const env = makeEnv(db); const response = await fetchWorker('/api/auth/me', env, { method: 'GET', headers: { cookie: cookieHeader(token) }, }); expect(response.status).toBe(200); const data = (await response.json()) as { user: { role: string } | null; publicBaseUrl: string; privateBaseUrl: string }; expect(data.user?.role).toBe('user'); expect(data.publicBaseUrl).toBe('http://localhost:8787'); expect(data.privateBaseUrl).toBe('http://localhost:8787'); }); it('returns { user: null } when the session token has no matching row', async () => { const db = new FakeAuthD1(); const env = makeEnv(db); const response = await fetchWorker('/api/auth/me', env, { method: 'GET', headers: { cookie: cookieHeader('unknown-token') }, }); expect(response.status).toBe(200); const data = (await response.json()) as { user: unknown; publicBaseUrl: string; privateBaseUrl: string }; expect(data).toEqual({ user: null, publicBaseUrl: 'http://localhost:8787', privateBaseUrl: 'http://localhost:8787', }); expect(db.firstCalls).toHaveLength(1); }); it('returns { user: null } for an expired session', async () => { const token = 'expired-me-token'; const hash = await hashSessionToken(token); const db = new FakeAuthD1(); db.rows.push({ id: 'user_3', email: 'expired@heygo.cc', name: 'Expired', image_url: null, role: 'user', expires_at: new Date(Date.now() - 60 * 60 * 1000).toISOString(), session_token_hash: hash, }); const env = makeEnv(db); const response = await fetchWorker('/api/auth/me', env, { method: 'GET', headers: { cookie: cookieHeader(token) }, }); expect(response.status).toBe(200); const data = (await response.json()) as { user: unknown; publicBaseUrl: string; privateBaseUrl: string }; expect(data).toEqual({ user: null, publicBaseUrl: 'http://localhost:8787', privateBaseUrl: 'http://localhost:8787', }); }); it('rejects non-GET methods with 405', async () => { const db = new FakeAuthD1(); const env = makeEnv(db); const response = await fetchWorker('/api/auth/me', env, { method: 'POST' }); expect(response.status).toBe(405); const data = (await response.json()) as { error: string }; expect(data.error).toMatch(/method/i); // No session lookup should have run expect(db.firstCalls).toHaveLength(0); }); it('POST /api/auth/logout clears the shared session cookie outside local dev', async () => { const db = new FakeAuthD1(); const env = makeEnv(db, { PUBLIC_HOST: 'dev.heygo.cc', PRIVATE_HOST: 'my.dev.heygo.cc', APP_BASE_URL: 'https://dev.heygo.cc', COOKIE_DOMAIN: '.heygo.cc', }); const response = await fetchWorker('/api/auth/logout', env, { method: 'POST' }); expect(response.status).toBe(200); expect(await response.json()).toEqual({ ok: true }); const setCookie = response.headers.get('set-cookie'); expect(setCookie).toContain(`${AUTH_SESSION_COOKIE_NAME}=;`); expect(setCookie).toContain('Expires=Thu, 01 Jan 1970 00:00:00 GMT'); expect(setCookie).toContain('Domain=.heygo.cc'); expect(setCookie).toContain('HttpOnly'); expect(response.headers.get('cache-control')).toBe('no-store'); }); it('rejects GET /api/auth/logout with 405', async () => { const db = new FakeAuthD1(); const env = makeEnv(db); const response = await fetchWorker('/api/auth/logout', env, { method: 'GET' }); expect(response.status).toBe(405); }); it('falls through (returns null) for non-matching paths so the worker 404s', async () => { const db = new FakeAuthD1(); const env = makeEnv(db); // A path that starts with /api/ but is not /api/auth/me should fall through // to the other handlers and ultimately the worker's 404 fallback. const response = await fetchWorker('/api/some-other-endpoint', env, { method: 'GET' }); expect(response.status).toBe(404); expect(db.firstCalls).toHaveLength(0); }); it('applies private-host no-store headers on private host', async () => { const db = new FakeAuthD1(); // Use a private host so withPrivateHostNoStoreHeaders adds no-store const env = makeEnv(db, { PUBLIC_HOST: 'heygo.cc', PRIVATE_HOST: 'my.heygo.cc', }); const url = 'https://my.heygo.cc/api/auth/me'; const request = new Request(url, { method: 'GET' }); const ctx = new FakeExecutionContext(); const response = await worker.fetch( request as unknown as Parameters[0], env as unknown as Parameters[1], ctx as unknown as Parameters[2], ); expect(response.status).toBe(200); expect(response.headers.get('cache-control')).toContain('no-store'); }); });