helpers.ts
| 1 | import { createServer } from 'net'; |
| 2 | import { rmSync, mkdirSync, writeFileSync } from 'fs'; |
| 3 | import { spawnSync as nodeSpawnSync } from 'child_process'; |
| 4 | import path from 'path'; |
| 5 | import type { Page } from 'playwright'; |
| 6 | import config from '../src/config.ts'; |
| 7 | import { createApp } from '../src/app.ts'; |
| 8 | import { resetDb } from '../src/db/index.ts'; |
| 9 | |
| 10 | export const DATA_DIR = './data-test'; |
| 11 | export const ADMIN_PASS = 'testpass123'; |
| 12 | |
| 13 | // Override config for tests — this runs at import time, before any app code |
| 14 | // reads these values, so no preload script is needed. |
| 15 | config.DATA_DIR = path.resolve(DATA_DIR); |
| 16 | config.RATE_LIMIT_DISABLED = true; |
| 17 | config.SSH_DISABLED = true; |
| 18 | |
| 19 | /** Bind to port 0 and return the OS-assigned free port number. */ |
| 20 | function getFreePort(): Promise<number> { |
| 21 | return new Promise((resolve, reject) => { |
| 22 | const srv = createServer(); |
| 23 | srv.listen(0, '127.0.0.1', () => { |
| 24 | const port = (srv.address() as { port: number }).port; |
| 25 | srv.close((err) => (err ? reject(err) : resolve(port))); |
| 26 | }); |
| 27 | srv.on('error', reject); |
| 28 | }); |
| 29 | } |
| 30 | |
| 31 | // Resolved once in setupTestEnv() and re-exported for tests. |
| 32 | export let PORT = 0; |
| 33 | export let BASE = ''; |
| 34 | |
| 35 | /** Server handle — an Elysia app instance running in-process. */ |
| 36 | export type ServerHandle = Awaited<ReturnType<typeof createApp>>; |
| 37 | |
| 38 | export async function setupTestEnv() { |
| 39 | PORT = await getFreePort(); |
| 40 | BASE = `http://localhost:${PORT}`; |
| 41 | rmSync(DATA_DIR, { recursive: true, force: true }); |
| 42 | mkdirSync(`${DATA_DIR}/repos`, { recursive: true }); |
| 43 | |
| 44 | nodeSpawnSync('bun', ['run', 'src/db/init.ts'], { |
| 45 | env: { ...process.env, DATA_DIR, ADMIN_PASSWORD: ADMIN_PASS }, |
| 46 | stdio: 'ignore', |
| 47 | }); |
| 48 | |
| 49 | // Reopen DB connection to the freshly-created database file. |
| 50 | resetDb(); |
| 51 | } |
| 52 | |
| 53 | export async function spawnServer(): Promise<ServerHandle> { |
| 54 | return createApp(PORT); |
| 55 | } |
| 56 | |
| 57 | export async function killServer(app: ServerHandle): Promise<void> { |
| 58 | await app.stop(true); |
| 59 | } |
| 60 | |
| 61 | export async function login( |
| 62 | page: Page, |
| 63 | username = 'admin', |
| 64 | password = ADMIN_PASS, |
| 65 | ) { |
| 66 | await page.goto(`${BASE}/login`); |
| 67 | await page.fill('[name=username]', username); |
| 68 | await page.fill('[name=password]', password); |
| 69 | await page.click('button[type=submit]'); |
| 70 | await page.waitForURL(BASE + '/'); |
| 71 | } |
| 72 | |
| 73 | export async function logout(page: Page) { |
| 74 | await page.click('form[action="/logout"] button'); |
| 75 | await page.waitForURL(BASE + '/'); |
| 76 | } |
| 77 | |
| 78 | const git = (args: string[], cwd?: string) => |
| 79 | nodeSpawnSync('git', [...(cwd ? ['-C', cwd] : []), ...args], { stdio: 'ignore' }); |
| 80 | |
| 81 | export function gitOutput(args: string[], cwd?: string): string { |
| 82 | const r = nodeSpawnSync('git', [...(cwd ? ['-C', cwd] : []), ...args], { stdio: ['ignore', 'pipe', 'ignore'] }); |
| 83 | return r.stdout?.toString().trim() ?? ''; |
| 84 | } |
| 85 | |
| 86 | /** Push an initial commit into a bare repo that already exists on disk. */ |
| 87 | export function seedRepo(name: string) { |
| 88 | const repoPath = `${process.cwd()}/${DATA_DIR}/repos/${name}.git`; |
| 89 | const tmp = `/tmp/hf-seed-${Date.now()}`; |
| 90 | try { |
| 91 | git(['clone', repoPath, tmp]); |
| 92 | git(['config', 'user.email', 'test@test.com'], tmp); |
| 93 | git(['config', 'user.name', 'Test'], tmp); |
| 94 | writeFileSync(`${tmp}/README.md`, `# ${name}\n`); |
| 95 | writeFileSync(`${tmp}/index.js`, `console.log("hello");\n`); |
| 96 | git(['add', '-A'], tmp); |
| 97 | git(['commit', '-m', 'Initial commit'], tmp); |
| 98 | git(['push', 'origin', 'HEAD:main'], tmp); |
| 99 | } finally { |
| 100 | rmSync(tmp, { recursive: true, force: true }); |
| 101 | } |
| 102 | } |
| 103 | |
| 104 | export function writeTempFile(path: string, content: string) { |
| 105 | writeFileSync(path, content); |
| 106 | } |
| 107 | |
| 108 | /** Commit a subdirectory with the given files into an existing repo on main. */ |
| 109 | export function seedSubdir(repoName: string, dirPath: string, files: Record<string, string>) { |
| 110 | const repoDir = `${process.cwd()}/${DATA_DIR}/repos/${repoName}.git`; |
| 111 | const tmp = `/tmp/hf-subdir-${Date.now()}`; |
| 112 | try { |
| 113 | git(['clone', repoDir, tmp]); |
| 114 | git(['config', 'user.email', 'test@test.com'], tmp); |
| 115 | git(['config', 'user.name', 'Test'], tmp); |
| 116 | mkdirSync(path.join(tmp, dirPath), { recursive: true }); |
| 117 | for (const [fileName, content] of Object.entries(files)) { |
| 118 | writeFileSync(path.join(tmp, dirPath, fileName), content); |
| 119 | } |
| 120 | git(['add', '-A'], tmp); |
| 121 | git(['commit', '-m', `Add ${dirPath}`], tmp); |
| 122 | git(['push', 'origin', 'HEAD:main'], tmp); |
| 123 | } finally { |
| 124 | rmSync(tmp, { recursive: true, force: true }); |
| 125 | } |
| 126 | } |
| 127 | |
| 128 | /** Return the HEAD commit hash of a repo. */ |
| 129 | export function getHeadCommit(repoName: string): string { |
| 130 | const repoPath = `${process.cwd()}/${DATA_DIR}/repos/${repoName}.git`; |
| 131 | const result = nodeSpawnSync('git', ['-C', repoPath, 'rev-parse', 'HEAD'], { |
| 132 | stdio: ['ignore', 'pipe', 'ignore'], |
| 133 | }); |
| 134 | return result.stdout?.toString().trim() ?? ''; |
| 135 | } |
| 136 | |
| 137 | /** Create a new branch in an existing repo (from current HEAD). */ |
| 138 | export function seedBranch(name: string, branchName: string) { |
| 139 | const repoPath = `${process.cwd()}/${DATA_DIR}/repos/${name}.git`; |
| 140 | const tmp = `/tmp/hf-branch-${Date.now()}`; |
| 141 | try { |
| 142 | git(['clone', repoPath, tmp]); |
| 143 | git(['config', 'user.email', 'test@test.com'], tmp); |
| 144 | git(['config', 'user.name', 'Test'], tmp); |
| 145 | git(['checkout', '-b', branchName], tmp); |
| 146 | git(['push', 'origin', branchName], tmp); |
| 147 | } finally { |
| 148 | rmSync(tmp, { recursive: true, force: true }); |
| 149 | } |
| 150 | } |
| 151 |