import { createServer } from 'net'; import { rmSync, mkdirSync, writeFileSync } from 'fs'; import { spawnSync as nodeSpawnSync } from 'child_process'; import path from 'path'; import type { Page } from 'playwright'; import config from '../src/config.ts'; import { createApp } from '../src/app.ts'; import { resetDb } from '../src/db/index.ts'; // Set by tests/preload.ts before any module reads it — see the comment // there for why it cannot be assigned from this file. export const DATA_DIR = process.env.DATA_DIR ?? './data-test'; export const ADMIN_PASS = 'testpass123'; // If the preload did not run, DATA_DIR is still ./data and src/db/index.ts has // already migrated the real database in its module body. Stop before any test // also authenticates against the live admin row. Checked here rather than in // the preload, which by definition cannot catch its own absence. if (path.resolve(DATA_DIR) === path.resolve('./data')) { throw new Error( 'Tests are pointed at the production DATA_DIR. tests/preload.ts did not run — ' + 'check that bunfig.toml is present and that bun was started from the repo root.', ); } // DATA_DIR is already in place via the preload; these two only affect code // that reads them per request, so assigning here is soon enough. config.DATA_DIR = path.resolve(DATA_DIR); config.RATE_LIMIT_DISABLED = true; config.SSH_DISABLED = true; /** Bind to port 0 and return the OS-assigned free port number. */ function getFreePort(): Promise { return new Promise((resolve, reject) => { const srv = createServer(); srv.listen(0, '127.0.0.1', () => { const port = (srv.address() as { port: number }).port; srv.close((err) => (err ? reject(err) : resolve(port))); }); srv.on('error', reject); }); } // Resolved once in setupTestEnv() and re-exported for tests. export let PORT = 0; export let BASE = ''; /** Server handle — an Elysia app instance running in-process. */ export type ServerHandle = Awaited>; export async function setupTestEnv() { PORT = await getFreePort(); BASE = `http://localhost:${PORT}`; rmSync(DATA_DIR, { recursive: true, force: true }); mkdirSync(`${DATA_DIR}/repos`, { recursive: true }); nodeSpawnSync('bun', ['run', 'src/db/init.ts'], { env: { ...process.env, DATA_DIR, ADMIN_PASSWORD: ADMIN_PASS }, stdio: 'ignore', }); // Reopen DB connection to the freshly-created database file. resetDb(); } export async function spawnServer(): Promise { return createApp(PORT); } export async function killServer(app: ServerHandle): Promise { await app.stop(true); } export async function login( page: Page, username = 'admin', password = ADMIN_PASS, ) { await page.goto(`${BASE}/login`); await page.fill('[name=username]', username); await page.fill('[name=password]', password); await page.click('button[type=submit]'); await page.waitForURL(BASE + '/'); } export async function logout(page: Page) { await page.click('form[action="/logout"] button'); await page.waitForURL(BASE + '/'); } const git = (args: string[], cwd?: string) => nodeSpawnSync('git', [...(cwd ? ['-C', cwd] : []), ...args], { stdio: 'ignore' }); export function gitOutput(args: string[], cwd?: string): string { const r = nodeSpawnSync('git', [...(cwd ? ['-C', cwd] : []), ...args], { stdio: ['ignore', 'pipe', 'ignore'] }); return r.stdout?.toString().trim() ?? ''; } /** Push an initial commit into a bare repo that already exists on disk. */ export function seedRepo(name: string) { const repoPath = `${process.cwd()}/${DATA_DIR}/repos/${name}.git`; const tmp = `/tmp/hf-seed-${Date.now()}`; try { git(['clone', repoPath, tmp]); git(['config', 'user.email', 'test@test.com'], tmp); git(['config', 'user.name', 'Test'], tmp); writeFileSync(`${tmp}/README.md`, `# ${name}\n`); writeFileSync(`${tmp}/index.js`, `console.log("hello");\n`); git(['add', '-A'], tmp); git(['commit', '-m', 'Initial commit'], tmp); git(['push', 'origin', 'HEAD:main'], tmp); } finally { rmSync(tmp, { recursive: true, force: true }); } } export function writeTempFile(path: string, content: string) { writeFileSync(path, content); } /** Commit a subdirectory with the given files into an existing repo on main. */ export function seedSubdir(repoName: string, dirPath: string, files: Record) { const repoDir = `${process.cwd()}/${DATA_DIR}/repos/${repoName}.git`; const tmp = `/tmp/hf-subdir-${Date.now()}`; try { git(['clone', repoDir, tmp]); git(['config', 'user.email', 'test@test.com'], tmp); git(['config', 'user.name', 'Test'], tmp); mkdirSync(path.join(tmp, dirPath), { recursive: true }); for (const [fileName, content] of Object.entries(files)) { writeFileSync(path.join(tmp, dirPath, fileName), content); } git(['add', '-A'], tmp); git(['commit', '-m', `Add ${dirPath}`], tmp); git(['push', 'origin', 'HEAD:main'], tmp); } finally { rmSync(tmp, { recursive: true, force: true }); } } /** Return the HEAD commit hash of a repo. */ export function getHeadCommit(repoName: string): string { const repoPath = `${process.cwd()}/${DATA_DIR}/repos/${repoName}.git`; const result = nodeSpawnSync('git', ['-C', repoPath, 'rev-parse', 'HEAD'], { stdio: ['ignore', 'pipe', 'ignore'], }); return result.stdout?.toString().trim() ?? ''; } /** Create a new branch in an existing repo (from current HEAD). */ export function seedBranch(name: string, branchName: string) { const repoPath = `${process.cwd()}/${DATA_DIR}/repos/${name}.git`; const tmp = `/tmp/hf-branch-${Date.now()}`; try { git(['clone', repoPath, tmp]); git(['config', 'user.email', 'test@test.com'], tmp); git(['config', 'user.name', 'Test'], tmp); git(['checkout', '-b', branchName], tmp); git(['push', 'origin', branchName], tmp); } finally { rmSync(tmp, { recursive: true, force: true }); } }