/** * Shared test helpers: a valid PanelConfig factory, an authed session cookie, an Ed25519 PKCS#8 PEM * generator (for the manage-token test), and simple fakes for the CP client + token minter. */ import { SESSION_COOKIE_NAME } from '../src/security/cookies.js' import { createSessionToken } from '../src/security/session.js' import type { PanelConfig } from '../src/config.js' import type { CpClient, HostView, IssuedPairing } from '../src/cp-client.js' import type { ManageTokenMinter } from '../src/manage-token.js' export const TEST_SESSION_SECRET = 'test-session-secret-0123456789' export function makeConfig(overrides: Partial = {}): PanelConfig { return { panelPassword: 'hunter2-correct-horse', sessionSecret: TEST_SESSION_SECRET, cpUrl: 'http://127.0.0.1:8080', baseDomain: 'terminal.yaojia.wang', operatorAccountId: 'acct-operator-001', capabilitySignKeyPath: '/nonexistent/key.pem', tunnelZone: 'terminal.yaojia.wang', panelBindPort: 8090, ...overrides, } } /** A valid session cookie header value for injection (`cookie` header). */ export function authCookieHeader(secret: string = TEST_SESSION_SECRET, nowMs: number = Date.now()): string { return `${SESSION_COOKIE_NAME}=${createSessionToken(secret, nowMs)}` } /** A minter that returns a fixed opaque token and records how many times it was called. */ export function fakeMinter(token = 'fake.manage.token'): ManageTokenMinter & { readonly calls: () => number } { let n = 0 return { async mint() { n += 1 return token }, calls: () => n, } } export interface FakeCpCall { readonly method: string readonly accountIdOrHostId: string readonly token: string } /** A CP client fake that records calls and returns canned data (or throws an injected error). */ export function fakeCpClient(opts: { hosts?: readonly HostView[] pairing?: IssuedPairing throwErr?: Error } = {}): CpClient & { readonly calls: readonly FakeCpCall[] } { const calls: FakeCpCall[] = [] const guard = (): void => { if (opts.throwErr) throw opts.throwErr } return { calls, async listHosts(accountId, token) { calls.push({ method: 'listHosts', accountIdOrHostId: accountId, token }) guard() return opts.hosts ?? [] }, async createPairingCode(accountId, token) { calls.push({ method: 'createPairingCode', accountIdOrHostId: accountId, token }) guard() return opts.pairing ?? { code: 'ABCD-EFGH', expiresAt: '2026-01-01T00:00:00.000Z' } }, async deleteHost(hostId, token) { calls.push({ method: 'deleteHost', accountIdOrHostId: hostId, token }) guard() }, } } /** Generate an Ed25519 PKCS#8 PEM (private key) for signing-key tests. */ export async function generatePkcs8Pem(): Promise<{ pem: string; publicRaw: Uint8Array }> { const pair = (await globalThis.crypto.subtle.generateKey({ name: 'Ed25519' }, true, ['sign', 'verify'])) as unknown as { publicKey: CryptoKey privateKey: CryptoKey } const pkcs8 = new Uint8Array(await globalThis.crypto.subtle.exportKey('pkcs8', pair.privateKey)) const raw = new Uint8Array(await globalThis.crypto.subtle.exportKey('raw', pair.publicKey)) const b64 = Buffer.from(pkcs8).toString('base64') const lines = b64.match(/.{1,64}/g) ?? [b64] const pem = `-----BEGIN PRIVATE KEY-----\n${lines.join('\n')}\n-----END PRIVATE KEY-----\n` return { pem, publicRaw: raw } }