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
10// Set by tests/preload.ts before any module reads it — see the comment
11// there for why it cannot be assigned from this file.
12export const DATA_DIR = process.env.DATA_DIR ?? './data-test';
13export const ADMIN_PASS = 'testpass123';
14
15// If the preload did not run, DATA_DIR is still ./data and src/db/index.ts has
16// already migrated the real database in its module body. Stop before any test
17// also authenticates against the live admin row. Checked here rather than in
18// the preload, which by definition cannot catch its own absence.
19if (path.resolve(DATA_DIR) === path.resolve('./data')) {
20 throw new Error(
21 'Tests are pointed at the production DATA_DIR. tests/preload.ts did not run — ' +
22 'check that bunfig.toml is present and that bun was started from the repo root.',
23 );
24}
25
26// DATA_DIR is already in place via the preload; these two only affect code
27// that reads them per request, so assigning here is soon enough.
28config.DATA_DIR = path.resolve(DATA_DIR);
29config.RATE_LIMIT_DISABLED = true;
30config.SSH_DISABLED = true;
31
32/** Bind to port 0 and return the OS-assigned free port number. */
33function getFreePort(): Promise<number> {
34 return new Promise((resolve, reject) => {
35 const srv = createServer();
36 srv.listen(0, '127.0.0.1', () => {
37 const port = (srv.address() as { port: number }).port;
38 srv.close((err) => (err ? reject(err) : resolve(port)));
39 });
40 srv.on('error', reject);
41 });
42}
43
44// Resolved once in setupTestEnv() and re-exported for tests.
45export let PORT = 0;
46export let BASE = '';
47
48/** Server handle — an Elysia app instance running in-process. */
49export type ServerHandle = Awaited<ReturnType<typeof createApp>>;
50
51export async function setupTestEnv() {
52 PORT = await getFreePort();
53 BASE = `http://localhost:${PORT}`;
54 rmSync(DATA_DIR, { recursive: true, force: true });
55 mkdirSync(`${DATA_DIR}/repos`, { recursive: true });
56
57 nodeSpawnSync('bun', ['run', 'src/db/init.ts'], {
58 env: { ...process.env, DATA_DIR, ADMIN_PASSWORD: ADMIN_PASS },
59 stdio: 'ignore',
60 });
61
62 // Reopen DB connection to the freshly-created database file.
63 resetDb();
64}
65
66export async function spawnServer(): Promise<ServerHandle> {
67 return createApp(PORT);
68}
69
70export async function killServer(app: ServerHandle): Promise<void> {
71 await app.stop(true);
72}
73
74export async function login(
75 page: Page,
76 username = 'admin',
77 password = ADMIN_PASS,
78) {
79 await page.goto(`${BASE}/login`);
80 await page.fill('[name=username]', username);
81 await page.fill('[name=password]', password);
82 await page.click('button[type=submit]');
83 await page.waitForURL(BASE + '/');
84}
85
86export async function logout(page: Page) {
87 await page.click('form[action="/logout"] button');
88 await page.waitForURL(BASE + '/');
89}
90
91const git = (args: string[], cwd?: string) =>
92 nodeSpawnSync('git', [...(cwd ? ['-C', cwd] : []), ...args], { stdio: 'ignore' });
93
94export function gitOutput(args: string[], cwd?: string): string {
95 const r = nodeSpawnSync('git', [...(cwd ? ['-C', cwd] : []), ...args], { stdio: ['ignore', 'pipe', 'ignore'] });
96 return r.stdout?.toString().trim() ?? '';
97}
98
99/** Push an initial commit into a bare repo that already exists on disk. */
100export function seedRepo(name: string) {
101 const repoPath = `${process.cwd()}/${DATA_DIR}/repos/${name}.git`;
102 const tmp = `/tmp/hf-seed-${Date.now()}`;
103 try {
104 git(['clone', repoPath, tmp]);
105 git(['config', 'user.email', 'test@test.com'], tmp);
106 git(['config', 'user.name', 'Test'], tmp);
107 writeFileSync(`${tmp}/README.md`, `# ${name}\n`);
108 writeFileSync(`${tmp}/index.js`, `console.log("hello");\n`);
109 git(['add', '-A'], tmp);
110 git(['commit', '-m', 'Initial commit'], tmp);
111 git(['push', 'origin', 'HEAD:main'], tmp);
112 } finally {
113 rmSync(tmp, { recursive: true, force: true });
114 }
115}
116
117export function writeTempFile(path: string, content: string) {
118 writeFileSync(path, content);
119}
120
121/** Commit a subdirectory with the given files into an existing repo on main. */
122export function seedSubdir(repoName: string, dirPath: string, files: Record<string, string>) {
123 const repoDir = `${process.cwd()}/${DATA_DIR}/repos/${repoName}.git`;
124 const tmp = `/tmp/hf-subdir-${Date.now()}`;
125 try {
126 git(['clone', repoDir, tmp]);
127 git(['config', 'user.email', 'test@test.com'], tmp);
128 git(['config', 'user.name', 'Test'], tmp);
129 mkdirSync(path.join(tmp, dirPath), { recursive: true });
130 for (const [fileName, content] of Object.entries(files)) {
131 writeFileSync(path.join(tmp, dirPath, fileName), content);
132 }
133 git(['add', '-A'], tmp);
134 git(['commit', '-m', `Add ${dirPath}`], tmp);
135 git(['push', 'origin', 'HEAD:main'], tmp);
136 } finally {
137 rmSync(tmp, { recursive: true, force: true });
138 }
139}
140
141/** Return the HEAD commit hash of a repo. */
142export function getHeadCommit(repoName: string): string {
143 const repoPath = `${process.cwd()}/${DATA_DIR}/repos/${repoName}.git`;
144 const result = nodeSpawnSync('git', ['-C', repoPath, 'rev-parse', 'HEAD'], {
145 stdio: ['ignore', 'pipe', 'ignore'],
146 });
147 return result.stdout?.toString().trim() ?? '';
148}
149
150/** Create a new branch in an existing repo (from current HEAD). */
151export function seedBranch(name: string, branchName: string) {
152 const repoPath = `${process.cwd()}/${DATA_DIR}/repos/${name}.git`;
153 const tmp = `/tmp/hf-branch-${Date.now()}`;
154 try {
155 git(['clone', repoPath, tmp]);
156 git(['config', 'user.email', 'test@test.com'], tmp);
157 git(['config', 'user.name', 'Test'], tmp);
158 git(['checkout', '-b', branchName], tmp);
159 git(['push', 'origin', branchName], tmp);
160 } finally {
161 rmSync(tmp, { recursive: true, force: true });
162 }
163}
164