import { describe, test, expect, beforeEach, mock } from 'bun:test' import { randomUUID } from 'node:crypto' import { tmpdir } from 'node:os' import { resolve } from 'node:path' import { mockConfigModule } from './helpers/mock-config' // Mock config before any imports mock.module('../config', () => mockConfigModule()) import { Hono } from 'hono' import type { MiddlewareHandler } from 'hono/types' import { initDatabase, resetDbSingleton } from '../db/sqlite' // Dummy serveStatic — returns a middleware. // When `path: 'index.html'` is set, serves SPA fallback HTML. // Otherwise, passes through (simulates "file not found" so downstream routes handle it). const serveStatic: (opts: Record) => MiddlewareHandler = ( opts: Record, ) => { const path = opts.path as string | undefined if (path === 'index.html') { return async c => c.html('SPA') } return async (_c, next) => { await next() } } // Dummy upgradeWebSocket const upgradeWebSocket = (() => { return () => async (_c: import('hono').Context, next: import('hono').Next) => { await next() } }) as unknown as import('hono/ws').UpgradeWebSocket import { registerRoutes } from '../registerRoutes' function createApp(): Hono { const app = new Hono() registerRoutes(app, serveStatic, upgradeWebSocket) return app } function setupTestDb(): void { // Initialize to a unique temp file per test file so the singleton never // resolves to the production default (tmpdir()/rcs-dev.db). We don't // reset between tests within this file because route handlers share the // singleton — closing it mid-test would break parallel requests. But // each test file gets its own module graph under --isolate, so a // file-scoped path is safe. try { resetDbSingleton() } catch { // ignore } const uniquePath = resolve( tmpdir(), `rcs-routes-${randomUUID().replace(/-/g, '').slice(0, 8)}.db`, ) initDatabase(uniquePath) } describe('Route mounting integration', () => { let app: Hono beforeEach(() => { setupTestDb() app = createApp() }) // ------------------------------------------------------------------------- // Task 1: auth-routes mounted at /web/auth // ------------------------------------------------------------------------- describe('auth-routes mounted at /web/auth', () => { test('GET /web/auth/me returns 401 (not 404) when unauthenticated', async () => { const res = await app.request('/web/auth/me', { method: 'GET' }) // Should NOT be 404 — route is mounted. 401 because no auth token. expect(res.status).not.toBe(404) expect(res.status).toBe(401) }) test('POST /web/auth/login returns 400 (not 404) for empty body', async () => { const res = await app.request('/web/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}), }) // Route exists → should not 404. Missing fields → 400. expect(res.status).not.toBe(404) expect(res.status).toBe(400) }) test('GET /web/auth/setup-status returns 200 (not 404)', async () => { const res = await app.request('/web/auth/setup-status', { method: 'GET', }) expect(res.status).not.toBe(404) expect(res.status).toBe(200) }) }) // ------------------------------------------------------------------------- // Task 1: shares mounted at /web // ------------------------------------------------------------------------- describe('shares routes mounted at /web', () => { test('POST /web/sessions/test/shares returns 401 (not 404)', async () => { const res = await app.request('/web/sessions/test/shares', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}), }) // Route mounted → not 404. No auth → 401. expect(res.status).not.toBe(404) expect(res.status).toBe(401) }) test('GET /web/sessions/s/test-token returns 401 (not 404)', async () => { const res = await app.request('/web/sessions/s/test-token', { method: 'GET', }) // Route mounted → not 404. No auth → 401. expect(res.status).not.toBe(404) expect(res.status).toBe(401) }) }) // ------------------------------------------------------------------------- // Task 1: teams mounted at /web // ------------------------------------------------------------------------- describe('teams routes mounted at /web', () => { test('GET /web/teams returns 401 (not 404)', async () => { const res = await app.request('/web/teams', { method: 'GET' }) // Route mounted → not 404. No auth → 401. expect(res.status).not.toBe(404) expect(res.status).toBe(401) }) test('POST /web/teams returns 401 (not 404)', async () => { const res = await app.request('/web/teams', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'test', slug: 'test-team' }), }) expect(res.status).not.toBe(404) expect(res.status).toBe(401) }) }) // ------------------------------------------------------------------------- // Existing /web/bind stub still works // ------------------------------------------------------------------------- describe('legacy /web/bind stub preserved', () => { test('POST /web/bind returns 410 Gone', async () => { const res = await app.request('/web/bind', { method: 'POST' }) expect(res.status).toBe(410) }) }) }) describe('SPA fallback', () => { let app: Hono beforeEach(() => { setupTestDb() app = createApp() }) test('GET /login serves index.html (SPA fallback)', async () => { const res = await app.request('/login', { method: 'GET' }) expect(res.status).toBe(200) const text = await res.text() expect(text).toContain('SPA') }) test('GET /setup serves index.html', async () => { const res = await app.request('/setup', { method: 'GET' }) expect(res.status).toBe(200) const text = await res.text() expect(text).toContain('SPA') }) test('GET /join serves index.html', async () => { const res = await app.request('/join', { method: 'GET' }) expect(res.status).toBe(200) const text = await res.text() expect(text).toContain('SPA') }) test('GET /settings serves index.html', async () => { const res = await app.request('/settings', { method: 'GET' }) expect(res.status).toBe(200) const text = await res.text() expect(text).toContain('SPA') }) test('GET /teams serves index.html', async () => { const res = await app.request('/teams', { method: 'GET' }) expect(res.status).toBe(200) const text = await res.text() expect(text).toContain('SPA') }) test('GET /teams/some-id serves index.html', async () => { const res = await app.request('/teams/some-id', { method: 'GET' }) expect(res.status).toBe(200) const text = await res.text() expect(text).toContain('SPA') }) test('SPA fallback does NOT intercept /web/auth/me', async () => { const res = await app.request('/web/auth/me', { method: 'GET' }) // Should be 401 from the auth route, not 200 from SPA fallback expect(res.status).toBe(401) }) test('SPA fallback does NOT intercept /v1/sessions', async () => { const res = await app.request('/v1/sessions', { method: 'GET' }) // Should not be SPA fallback — expect a real API response (likely 401) const text = await res.text() expect(text).not.toContain('SPA') }) test('Existing /code fallback still works', async () => { const res = await app.request('/code', { method: 'GET' }) expect(res.status).toBe(200) const text = await res.text() expect(text).toContain('SPA') }) })