mirror of
https://github.com/wahyd4/heygo.git
synced 2026-08-08 21:05:48 +10:00
341 lines
9.6 KiB
TypeScript
341 lines
9.6 KiB
TypeScript
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<T>(): Promise<T | null> {
|
|
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: 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');
|
|
});
|
|
});
|