helpers.ts
Raw
1import { createServer } from 'net';
2import { rmSync, mkdirSync, writeFileSync } from 'fs';
3import { spawnSync as nodeSpawnSync } from 'child_process';
4import path from 'path';
5import type { Page } from 'playwright';
6import config from '../src/config.ts';
7import { createApp } from '../src/app.ts';
8import { resetDb } from '../src/db/index.ts';
9
10export const DATA_DIR = './data-test';
11export 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.
15config.DATA_DIR = path.resolve(DATA_DIR);
16config.RATE_LIMIT_DISABLED = true;
17config.SSH_DISABLED = true;
18
19/** Bind to port 0 and return the OS-assigned free port number. */
20function 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.
32export let PORT = 0;
33export let BASE = '';
34
35/** Server handle — an Elysia app instance running in-process. */
36export type ServerHandle = Awaited<ReturnType<typeof createApp>>;
37
38export 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
53export async function spawnServer(): Promise<ServerHandle> {
54 return createApp(PORT);
55}
56
57export async function killServer(app: ServerHandle): Promise<void> {
58 await app.stop(true);
59}
60
61export 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
73export async function logout(page: Page) {
74 await page.click('form[action="/logout"] button');
75 await page.waitForURL(BASE + '/');
76}
77
78const git = (args: string[], cwd?: string) =>
79 nodeSpawnSync('git', [...(cwd ? ['-C', cwd] : []), ...args], { stdio: 'ignore' });
80
81export 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. */
87export 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
104export 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. */
109export 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. */
129export 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). */
138export 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