chore: add tests for nodes

This commit is contained in:
Aarnav Tale
2025-12-03 19:05:54 -05:00
parent 45de5f6833
commit d363ed5486
4 changed files with 176 additions and 4 deletions
+21 -2
View File
@@ -4,12 +4,14 @@ import {
type HeadscaleApiInterface,
} from '~/server/headscale/api';
import { type HeadscaleEnv, startHeadscale } from './start-headscale';
import { startTailscaleNode, TailscaleNodeEnv } from './start-tailscale';
export type Version = keyof typeof hashes;
export const HS_VERSIONS = Object.keys(hashes) as Version[];
interface VersionStateEntry {
env: HeadscaleEnv;
tailscaleNode: TailscaleNodeEnv;
bootstrap: HeadscaleApiInterface;
}
@@ -20,9 +22,13 @@ async function ensureVersion(version: Version) {
}
const env = await startHeadscale(version);
const tailscaleNode = await startTailscaleNode(
version,
env.container.getMappedPort(8080),
);
const bootstrap = await createHeadscaleInterface(env.apiUrl);
const entry = { env, bootstrap };
const entry = { env, tailscaleNode, bootstrap };
versionState.set(version, entry);
return entry;
}
@@ -37,12 +43,25 @@ export async function getRuntimeClient(version: Version) {
return bootstrap.getRuntimeClient(env.apiKey);
}
export async function getNode(version: Version) {
const { tailscaleNode } = await ensureVersion(version);
return {
authCode: tailscaleNode.authCode,
nodeName: tailscaleNode.nodeName,
};
}
export async function stopAllVersions() {
for (const { env } of versionState.values()) {
for (const { env, tailscaleNode } of versionState.values()) {
await env.container.stop({
remove: true,
removeVolumes: true,
});
await tailscaleNode.container.stop({
remove: true,
removeVolumes: true,
});
}
versionState.clear();
@@ -49,5 +49,3 @@ export async function startHeadscale(version: string): Promise<HeadscaleEnv> {
const apiKey = JSON.parse(exec.stdout.toString());
return { container, apiUrl, apiKey };
}
// await startHeadscale('0.26.0');
@@ -0,0 +1,85 @@
import { createInterface } from 'node:readline';
import tc from 'testcontainers';
import hashes from '~/openapi-operation-hashes.json';
export type Version = keyof typeof hashes;
export interface TailscaleNodeEnv {
container: tc.StartedTestContainer;
authCode: string;
nodeName: string;
}
export async function startTailscaleNode(
version: Version,
headscalePort: number,
): Promise<TailscaleNodeEnv> {
let resolveAuthCode!: (code: string) => void;
let rejectAuthCode!: (err: Error) => void;
let authCodeResolved = false;
const authCodePromise = new Promise<string>((resolve, reject) => {
resolveAuthCode = resolve;
rejectAuthCode = reject;
});
const nodeName = `test-node-${version.replace(/\./g, '-')}`;
const prefix = `http://localhost:8080/register/`;
const container = await new tc.GenericContainer('tailscale/tailscale:latest')
.withNetworkMode('host')
.withEnvironment({
TS_STATE_DIR: '/tailscale-state',
TS_EXTRA_ARGS: [
`--login-server=http://localhost:${headscalePort}`,
`--hostname=${nodeName}`,
'--accept-dns=false',
'--accept-routes=false',
].join(' '),
})
.withLogConsumer((stream) => {
const rl = createInterface({ input: stream });
rl.on('line', (line: string) => {
if (authCodeResolved) return;
const idx = line.indexOf(prefix);
if (idx === -1) return;
const after = line.slice(idx + prefix.length).trim();
if (!after) return;
const token = after.split(/\s+/)[0];
if (!token) return;
authCodeResolved = true;
resolveAuthCode(token);
rl.close();
});
rl.on('close', () => {
if (!authCodeResolved) {
rejectAuthCode(
new Error(
'Tailscale container log stream closed before auth code was found',
),
);
}
});
})
.withWaitStrategy(tc.Wait.forLogMessage(prefix).withStartupTimeout(30_000))
.start();
const authCode = await Promise.race<string>([
authCodePromise,
new Promise((_, reject) =>
setTimeout(
() =>
reject(
new Error(`Timed out waiting for Tailscale auth URL on ${prefix}`),
),
25_000,
),
),
]);
return { container, authCode, nodeName };
}