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