Files
heygo/tests/api.links.test.ts
T
Hermes Agent e4fbbaec14 feat: add terraform IaC, dev/prod environments, and multi-host worker support
- Terraform configs for D1 databases, KV namespaces, Worker custom domains
- wrangler.dev.jsonc and wrangler.prod.jsonc for environment-specific deployments
- Worker code refactored to use env vars for host checking (PUBLIC_HOST, PRIVATE_HOST)
- Configurable app URLs and cookie domain via env vars
- Deploy and migrate npm scripts for dev/prod
- Updated all tests with new env fixtures
- Deployment guide in README
2026-06-20 14:26:56 +10:00

728 lines
25 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import worker from '../worker/index';
import { AUTH_SESSION_COOKIE_NAME, hashSessionToken } from '../worker/auth';
type LinkScope = 'public' | 'private';
type LinkStatus = 'active' | 'archived' | 'deleted';
type LinkType = 'redirect' | 'custom';
type LinkRow = {
id: string;
scope: LinkScope;
owner_user_id: string | null;
alias: string;
link_type: LinkType;
target_url: string | null;
content_markdown: string | null;
description: string | null;
status: LinkStatus;
click_count: number;
created_at: string;
updated_at: string;
};
type SessionRow = {
id: string;
email: string | null;
name: string | null;
image_url: string | null;
role: 'user' | 'admin';
expires_at: string;
session_token_hash: string;
};
type RunCall = {
sql: string;
params: unknown[];
};
type AllResult<T> = {
results: T[];
success: true;
meta: Record<string, never>;
};
type FakeD1Options = {
throwOnInsert?: Error;
throwOnUpdate?: Error;
};
class FakeD1Database {
readonly preparedSql: string[] = [];
readonly runCalls: RunCall[] = [];
constructor(
readonly links: LinkRow[] = [],
private readonly sessions: SessionRow[] = [],
private readonly options: FakeD1Options = {},
) {}
prepare(sql: string): FakeD1PreparedStatement {
this.preparedSql.push(sql);
return new FakeD1PreparedStatement(this, sql);
}
findSession(hash: string): SessionRow | null {
return this.sessions.find((candidate) => candidate.session_token_hash === hash) ?? null;
}
listLinks(scope: LinkScope, ownerUserId?: string): LinkRow[] {
return this.links
.filter((link) => {
if (link.scope !== scope || link.status !== 'active') {
return false;
}
return scope === 'public' ? true : link.owner_user_id === ownerUserId;
})
.sort((a, b) => b.updated_at.localeCompare(a.updated_at));
}
findDuplicate(
scope: LinkScope,
alias: string,
ownerUserId: string | null,
statusFilter: 'active' | 'not-deleted',
excludeId?: string,
): LinkRow | null {
return (
this.links.find((link) => {
const statusMatches = statusFilter === 'active' ? link.status === 'active' : link.status !== 'deleted';
if (link.scope !== scope || !statusMatches || link.alias !== alias || link.id === excludeId) {
return false;
}
return scope === 'public' ? true : link.owner_user_id === ownerUserId;
}) ?? null
);
}
maybeThrowOnInsert(): void {
if (this.options.throwOnInsert) {
throw this.options.throwOnInsert;
}
}
maybeThrowOnUpdate(): void {
if (this.options.throwOnUpdate) {
throw this.options.throwOnUpdate;
}
}
findEditableLink(scope: LinkScope, id: string, ownerUserId?: string): LinkRow | null {
return (
this.links.find((link) => {
if (link.id !== id || link.scope !== scope || link.status !== 'active') {
return false;
}
return scope === 'public' ? true : link.owner_user_id === ownerUserId;
}) ?? null
);
}
insertLink(params: unknown[]): void {
const [id, scope, ownerUserId, alias, linkType, targetUrl, contentMarkdown, description] = params;
this.links.push({
id: String(id),
scope: scope as LinkScope,
owner_user_id: ownerUserId == null ? null : String(ownerUserId),
alias: String(alias),
link_type: linkType as LinkType,
target_url: targetUrl == null ? null : String(targetUrl),
content_markdown: contentMarkdown == null ? null : String(contentMarkdown),
description: description == null ? null : String(description),
status: 'active',
click_count: 0,
created_at: '2026-06-20T00:00:00.000Z',
updated_at: '2026-06-20T00:00:00.000Z',
});
}
}
class FakeD1PreparedStatement {
constructor(
private readonly db: FakeD1Database,
private readonly sql: string,
private readonly params: unknown[] = [],
) {}
bind(...params: unknown[]): FakeD1PreparedStatement {
return new FakeD1PreparedStatement(this.db, this.sql, params);
}
async first<T>(): Promise<T | null> {
if (this.sql.includes('session_token_hash')) {
const row = this.db.findSession(String(this.params[0]));
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;
}
if (
this.sql.includes('alias=?') &&
(this.sql.includes('status=\'active\'') || this.sql.includes("status!='deleted'") || this.sql.includes("status != 'deleted'"))
) {
const scope = this.sql.includes("scope='public'") ? 'public' : 'private';
const hasOwner = this.sql.includes('owner_user_id=?');
const ownerUserId = hasOwner ? String(this.params[0]) : null;
const alias = String(this.params[hasOwner ? 1 : 0]);
const excludeId = this.sql.includes('id!=?') ? String(this.params[hasOwner ? 2 : 1]) : undefined;
const statusFilter = this.sql.includes("status!='deleted'") || this.sql.includes("status != 'deleted'") ? 'not-deleted' : 'active';
const row = this.db.findDuplicate(scope, alias, ownerUserId, statusFilter, excludeId);
return (row ? rowToDbResult(row) : null) as T | null;
}
if (this.sql.includes('id=?') && this.sql.includes('status=\'active\'')) {
const scope = this.sql.includes("scope='public'") ? 'public' : 'private';
const hasOwner = this.sql.includes('owner_user_id=?');
const id = String(this.params[0]);
const ownerUserId = hasOwner ? String(this.params[1]) : undefined;
const row = this.db.findEditableLink(scope, id, ownerUserId);
return (row ? rowToDbResult(row) : null) as T | null;
}
return null;
}
async all<T>(): Promise<AllResult<T>> {
if (this.sql.includes("scope='public'")) {
return { results: this.db.listLinks('public').map(rowToDbResult) as T[], success: true, meta: {} };
}
if (this.sql.includes("scope='private'")) {
return {
results: this.db.listLinks('private', String(this.params[0])).map(rowToDbResult) as T[],
success: true,
meta: {},
};
}
return { results: [], success: true, meta: {} };
}
async run(): Promise<D1Result> {
this.db.runCalls.push({ sql: this.sql, params: this.params });
if (this.sql.startsWith('INSERT INTO links')) {
this.db.maybeThrowOnInsert();
this.db.insertLink(this.params);
}
if (this.sql.startsWith('UPDATE links SET') && this.sql.includes('status=\'deleted\'')) {
const id = String(this.params[0]);
const hasOwner = this.sql.includes('owner_user_id=?');
const ownerUserId = hasOwner ? String(this.params[1]) : undefined;
const scope = this.sql.includes("scope='public'") ? 'public' : 'private';
const row = this.db.findEditableLink(scope, id, ownerUserId);
if (row) {
row.status = 'deleted';
row.updated_at = '2026-06-20T00:00:01.000Z';
}
}
if (this.sql.startsWith('UPDATE links SET') && !this.sql.includes('status=\'deleted\'')) {
this.db.maybeThrowOnUpdate();
const [alias, linkType, targetUrl, contentMarkdown, description, id] = this.params;
const row = this.db.links.find((candidate) => candidate.id === id);
if (row) {
row.alias = String(alias);
row.link_type = linkType as LinkType;
row.target_url = targetUrl == null ? null : String(targetUrl);
row.content_markdown = contentMarkdown == null ? null : String(contentMarkdown);
row.description = description == null ? null : String(description);
row.updated_at = '2026-06-20T00:00:01.000Z';
}
}
return { success: true, meta: { changes: 1 } } as unknown as D1Result;
}
}
class FakeExecutionContext {
waitUntil(): void {}
passThroughOnException(): void {}
}
function rowToDbResult(row: LinkRow) {
return {
id: row.id,
alias: row.alias,
scope: row.scope,
link_type: row.link_type,
target_url: row.target_url,
content_markdown: row.content_markdown,
description: row.description,
owner_user_id: row.owner_user_id,
click_count: row.click_count,
status: row.status,
created_at: row.created_at,
updated_at: row.updated_at,
};
}
function link(overrides: Partial<LinkRow> = {}): LinkRow {
return {
id: 'link_1',
scope: 'private',
owner_user_id: 'user_1',
alias: 'docs',
link_type: 'redirect',
target_url: 'https://example.com/docs',
content_markdown: null,
description: null,
status: 'active',
click_count: 0,
created_at: '2026-06-20T00:00:00.000Z',
updated_at: '2026-06-20T00:00:00.000Z',
...overrides,
};
}
function futureIso(): string {
return new Date(Date.now() + 60 * 60 * 1000).toISOString();
}
async function userSession(token: string, userId: string, role: 'user' | 'admin' = 'user'): Promise<SessionRow> {
return {
id: userId,
email: `${userId}@heygo.cc`,
name: userId,
image_url: null,
role,
expires_at: futureIso(),
session_token_hash: await hashSessionToken(token),
};
}
function cookie(token: string): string {
return `${AUTH_SESSION_COOKIE_NAME}=${token}`;
}
function makeEnv(links: LinkRow[] = [], sessions: SessionRow[] = [], dbOptions: FakeD1Options = {}) {
const db = new FakeD1Database(links, sessions, dbOptions);
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',
},
db,
ctx: new FakeExecutionContext(),
};
}
async function fetchWorker(
path: string,
opts: {
method?: string;
body?: unknown;
links?: LinkRow[];
sessions?: SessionRow[];
cookie?: string;
dbOptions?: FakeD1Options;
} = {},
) {
const { env, db, ctx } = makeEnv(opts.links ?? [], opts.sessions ?? [], opts.dbOptions);
const headers = new Headers();
if (opts.cookie) {
headers.set('cookie', opts.cookie);
}
if (opts.body !== undefined) {
headers.set('content-type', 'application/json');
}
const response = await worker.fetch(
new Request(`https://heygo.cc${path}`, {
method: opts.method ?? 'GET',
headers,
body: opts.body === undefined ? undefined : JSON.stringify(opts.body),
}) as unknown as Parameters<typeof worker.fetch>[0],
env as unknown as Parameters<typeof worker.fetch>[1],
ctx as unknown as Parameters<typeof worker.fetch>[2],
);
return { response, db };
}
async function expectJson<T = any>(response: Response): Promise<T> {
expect(response.headers.get('content-type')).toContain('application/json');
return response.json() as Promise<T>;
}
function uniqueConstraintError(): Error {
return new Error('D1_ERROR: UNIQUE constraint failed: links.alias');
}
describe('link CRUD API', () => {
it('creates a private link and lowercases the alias', async () => {
const session = await userSession('token-a', 'user_1');
const { response } = await fetchWorker('/api/links/private', {
method: 'POST',
cookie: cookie('token-a'),
sessions: [session],
body: { alias: 'My_Link', linkType: 'redirect', targetUrl: 'https://example.com/a', description: 'A' },
});
expect(response.status).toBe(201);
const body = await expectJson(response);
expect(body.link).toMatchObject({
alias: 'my_link',
scope: 'private',
ownerUserId: 'user_1',
linkType: 'redirect',
targetUrl: 'https://example.com/a',
status: 'active',
});
});
it('rejects custom links without contentMarkdown', async () => {
const session = await userSession('token-custom-missing-content', 'user_1');
const { response } = await fetchWorker('/api/links/private', {
method: 'POST',
cookie: cookie('token-custom-missing-content'),
sessions: [session],
body: { alias: 'custom-page', linkType: 'custom' },
});
expect(response.status).toBe(400);
await expect(expectJson(response)).resolves.toHaveProperty('error');
});
it('creates custom links with explicit null targetUrl', async () => {
const session = await userSession('token-custom-null-target', 'user_1');
const { response } = await fetchWorker('/api/links/private', {
method: 'POST',
cookie: cookie('token-custom-null-target'),
sessions: [session],
body: { alias: 'custom-page', linkType: 'custom', targetUrl: null, contentMarkdown: '# Hello' },
});
expect(response.status).toBe(201);
const body = await expectJson(response);
expect(body.link).toMatchObject({
alias: 'custom-page',
linkType: 'custom',
targetUrl: null,
contentMarkdown: '# Hello',
});
});
it('rejects redirect links with non-http targetUrl schemes', async () => {
const session = await userSession('token-js-url', 'user_1');
const { response } = await fetchWorker('/api/links/private', {
method: 'POST',
cookie: cookie('token-js-url'),
sessions: [session],
body: { alias: 'bad-url', linkType: 'redirect', targetUrl: 'javascript:alert(1)' },
});
expect(response.status).toBe(400);
await expect(expectJson(response)).resolves.toHaveProperty('error');
});
it('rejects duplicate private aliases for the same user with 409', async () => {
const session = await userSession('token-a', 'user_1');
const { response } = await fetchWorker('/api/links/private', {
method: 'POST',
cookie: cookie('token-a'),
sessions: [session],
links: [link({ alias: 'docs', owner_user_id: 'user_1' })],
body: { alias: 'Docs', linkType: 'redirect', targetUrl: 'https://example.com/new' },
});
expect(response.status).toBe(409);
await expect(expectJson(response)).resolves.toEqual({ error: 'Alias already exists' });
});
it('rejects archived duplicate private aliases for the same user with 409', async () => {
const session = await userSession('token-archived-private', 'user_1');
const { response } = await fetchWorker('/api/links/private', {
method: 'POST',
cookie: cookie('token-archived-private'),
sessions: [session],
links: [link({ alias: 'docs', owner_user_id: 'user_1', status: 'archived' })],
body: { alias: 'Docs', linkType: 'redirect', targetUrl: 'https://example.com/new' },
});
expect(response.status).toBe(409);
await expect(expectJson(response)).resolves.toEqual({ error: 'Alias already exists' });
});
it('allows deleted private aliases to be reused by the same user', async () => {
const session = await userSession('token-deleted-private', 'user_1');
const { response } = await fetchWorker('/api/links/private', {
method: 'POST',
cookie: cookie('token-deleted-private'),
sessions: [session],
links: [link({ alias: 'docs', owner_user_id: 'user_1', status: 'deleted' })],
body: { alias: 'Docs', linkType: 'redirect', targetUrl: 'https://example.com/new' },
});
expect(response.status).toBe(201);
const body = await expectJson(response);
expect(body.link).toMatchObject({ alias: 'docs', ownerUserId: 'user_1' });
});
it('returns 409 JSON when a private link insert hits a unique constraint race', async () => {
const session = await userSession('token-insert-race', 'user_1');
const { response } = await fetchWorker('/api/links/private', {
method: 'POST',
cookie: cookie('token-insert-race'),
sessions: [session],
body: { alias: 'race', linkType: 'redirect', targetUrl: 'https://example.com/race' },
dbOptions: { throwOnInsert: uniqueConstraintError() },
});
expect(response.status).toBe(409);
await expect(expectJson(response)).resolves.toEqual({ error: 'Alias already exists' });
});
it('allows the same private alias for different users', async () => {
const session = await userSession('token-b', 'user_2');
const { response } = await fetchWorker('/api/links/private', {
method: 'POST',
cookie: cookie('token-b'),
sessions: [session],
links: [link({ id: 'link_a', alias: 'shared', owner_user_id: 'user_1' })],
body: { alias: 'shared', linkType: 'redirect', targetUrl: 'https://example.com/user-b' },
});
expect(response.status).toBe(201);
const body = await expectJson(response);
expect(body.link).toMatchObject({ alias: 'shared', ownerUserId: 'user_2' });
});
it('returns 401 JSON for unauthenticated private API requests', async () => {
const { response } = await fetchWorker('/api/links/private');
expect(response.status).toBe(401);
await expect(expectJson(response)).resolves.toEqual({ error: 'Authentication required' });
});
it('only lets a private link owner PATCH their link', async () => {
const owner = await userSession('owner-token', 'owner');
const other = await userSession('other-token', 'other');
const links = [link({ id: 'private_link', alias: 'old', owner_user_id: 'owner' })];
const denied = await fetchWorker('/api/links/private/private_link', {
method: 'PATCH',
cookie: cookie('other-token'),
sessions: [owner, other],
links,
body: { alias: 'new', linkType: 'redirect', targetUrl: 'https://example.com/new' },
});
expect([403, 404]).toContain(denied.response.status);
expect(links[0].alias).toBe('old');
const allowed = await fetchWorker('/api/links/private/private_link', {
method: 'PATCH',
cookie: cookie('owner-token'),
sessions: [owner, other],
links,
body: { alias: 'New', linkType: 'redirect', targetUrl: 'https://example.com/new', description: 'updated' },
});
expect(allowed.response.status).toBe(200);
const body = await expectJson(allowed.response);
expect(body.link).toMatchObject({
id: 'private_link',
alias: 'new',
targetUrl: 'https://example.com/new',
description: 'updated',
ownerUserId: 'owner',
});
});
it('soft-deletes an owner private link', async () => {
const session = await userSession('token-a', 'user_1');
const links = [link({ id: 'delete_me', owner_user_id: 'user_1' })];
const { response } = await fetchWorker('/api/links/private/delete_me', {
method: 'DELETE',
cookie: cookie('token-a'),
sessions: [session],
links,
});
expect(response.status).toBe(200);
await expect(expectJson(response)).resolves.toEqual({ ok: true });
expect(links[0].status).toBe('deleted');
});
it('lists only the current user active private links', async () => {
const session = await userSession('token-a', 'user_1');
const { response } = await fetchWorker('/api/links/private', {
cookie: cookie('token-a'),
sessions: [session],
links: [
link({ id: 'own_active', alias: 'own', owner_user_id: 'user_1', status: 'active' }),
link({ id: 'own_deleted', alias: 'deleted', owner_user_id: 'user_1', status: 'deleted' }),
link({ id: 'other_active', alias: 'other', owner_user_id: 'user_2', status: 'active' }),
],
});
expect(response.status).toBe(200);
const body = await expectJson(response);
expect(body.links.map((item: { id: string }) => item.id)).toEqual(['own_active']);
});
it('lists active public links without login', async () => {
const { response } = await fetchWorker('/api/links/public', {
links: [
link({ id: 'public_active', scope: 'public', owner_user_id: null, alias: 'pub', status: 'active' }),
link({ id: 'public_deleted', scope: 'public', owner_user_id: null, alias: 'gone', status: 'deleted' }),
link({ id: 'private_active', scope: 'private', owner_user_id: 'user_1', alias: 'priv', status: 'active' }),
],
});
expect(response.status).toBe(200);
const body = await expectJson(response);
expect(body.links.map((item: { id: string }) => item.id)).toEqual(['public_active']);
});
it('rejects non-admin public link creation with 403', async () => {
const session = await userSession('token-a', 'user_1', 'user');
const { response } = await fetchWorker('/api/admin/public-links', {
method: 'POST',
cookie: cookie('token-a'),
sessions: [session],
body: { alias: 'pub', linkType: 'redirect', targetUrl: 'https://example.com/pub' },
});
expect(response.status).toBe(403);
await expect(expectJson(response)).resolves.toEqual({ error: 'Admin access required' });
});
it('lets an admin create a public link', async () => {
const session = await userSession('admin-token', 'admin_1', 'admin');
const { response } = await fetchWorker('/api/admin/public-links', {
method: 'POST',
cookie: cookie('admin-token'),
sessions: [session],
body: { alias: 'Public', linkType: 'redirect', targetUrl: 'https://example.com/pub' },
});
expect(response.status).toBe(201);
const body = await expectJson(response);
expect(body.link).toMatchObject({
alias: 'public',
scope: 'public',
ownerUserId: null,
linkType: 'redirect',
targetUrl: 'https://example.com/pub',
});
});
it('rejects duplicate public aliases with 409', async () => {
const session = await userSession('admin-token', 'admin_1', 'admin');
const { response } = await fetchWorker('/api/admin/public-links', {
method: 'POST',
cookie: cookie('admin-token'),
sessions: [session],
links: [link({ id: 'public_existing', scope: 'public', owner_user_id: null, alias: 'pub' })],
body: { alias: 'Pub', linkType: 'redirect', targetUrl: 'https://example.com/new' },
});
expect(response.status).toBe(409);
await expect(expectJson(response)).resolves.toEqual({ error: 'Alias already exists' });
});
it('rejects archived public aliases with 409', async () => {
const session = await userSession('admin-token-archived', 'admin_1', 'admin');
const { response } = await fetchWorker('/api/admin/public-links', {
method: 'POST',
cookie: cookie('admin-token-archived'),
sessions: [session],
links: [link({ id: 'public_archived', scope: 'public', owner_user_id: null, alias: 'pub', status: 'archived' })],
body: { alias: 'Pub', linkType: 'redirect', targetUrl: 'https://example.com/new' },
});
expect(response.status).toBe(409);
await expect(expectJson(response)).resolves.toEqual({ error: 'Alias already exists' });
});
it('lets an admin PATCH a public link alias, target, and content', async () => {
const session = await userSession('admin-token', 'admin_1', 'admin');
const links = [link({ id: 'public_link', scope: 'public', owner_user_id: null, alias: 'old' })];
const { response } = await fetchWorker('/api/admin/public-links/public_link', {
method: 'PATCH',
cookie: cookie('admin-token'),
sessions: [session],
links,
body: { alias: 'Updated', linkType: 'custom', contentMarkdown: '# Hello', description: 'Custom' },
});
expect(response.status).toBe(200);
const body = await expectJson(response);
expect(body.link).toMatchObject({
id: 'public_link',
alias: 'updated',
scope: 'public',
linkType: 'custom',
targetUrl: null,
contentMarkdown: '# Hello',
description: 'Custom',
});
});
it('returns 409 JSON when a public link update hits a unique constraint race', async () => {
const session = await userSession('admin-token-update-race', 'admin_1', 'admin');
const links = [link({ id: 'public_link', scope: 'public', owner_user_id: null, alias: 'old' })];
const { response } = await fetchWorker('/api/admin/public-links/public_link', {
method: 'PATCH',
cookie: cookie('admin-token-update-race'),
sessions: [session],
links,
body: { alias: 'new', linkType: 'redirect', targetUrl: 'https://example.com/new' },
dbOptions: { throwOnUpdate: uniqueConstraintError() },
});
expect(response.status).toBe(409);
await expect(expectJson(response)).resolves.toEqual({ error: 'Alias already exists' });
});
it('lets an admin soft-delete a public link', async () => {
const session = await userSession('admin-token', 'admin_1', 'admin');
const links = [link({ id: 'public_link', scope: 'public', owner_user_id: null, alias: 'public' })];
const { response } = await fetchWorker('/api/admin/public-links/public_link', {
method: 'DELETE',
cookie: cookie('admin-token'),
sessions: [session],
links,
});
expect(response.status).toBe(200);
await expect(expectJson(response)).resolves.toEqual({ ok: true });
expect(links[0].status).toBe('deleted');
});
it('returns 400 for invalid aliases and invalid bodies', async () => {
const session = await userSession('token-a', 'user_1');
const badAlias = await fetchWorker('/api/links/private', {
method: 'POST',
cookie: cookie('token-a'),
sessions: [session],
body: { alias: '../bad', linkType: 'redirect', targetUrl: 'https://example.com' },
});
expect(badAlias.response.status).toBe(400);
await expect(expectJson(badAlias.response)).resolves.toHaveProperty('error');
const missingTarget = await fetchWorker('/api/links/private', {
method: 'POST',
cookie: cookie('token-a'),
sessions: [session],
body: { alias: 'badbody', linkType: 'redirect' },
});
expect(missingTarget.response.status).toBe(400);
await expect(expectJson(missingTarget.response)).resolves.toHaveProperty('error');
});
});