// The /fpagent installer surface: a public script, and a proxy that reads the // private Gitea release with GITEA_RELEASE_INSTALL_KEY so the operator's // laptop never needs credentials. import test from 'node:test'; import assert from 'node:assert/strict'; import { execFileSync } from 'node:child_process'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { createApp, FPAGENT_REPO } from '../server.js'; import { startServer, stopServer, urlOf } from './helpers.js'; const TOKEN = 'release-install-secret'; const TAG = 'v9.9.9'; const ARCHIVE = 'fpagent-deadbee-linux-x86_64.tar.gz'; const UBUNTU_ARCHIVE = 'fpagent-deadbee-ubuntu-jammy-x86_64.tar.gz'; const RELEASE = { tag_name: TAG, assets: [ { name: ARCHIVE }, { name: `${ARCHIVE}.sha256` }, { name: UBUNTU_ARCHIVE }, { name: 'not a safe name' }, { name: '../escape' }, ], }; // Fake Gitea: refuses without the token, serves the release and its assets. async function startFakeGitea({ releaseStatus = 200, downloadStatus = 200 } = {}) { const calls = []; const server = await startServer((req, res) => { calls.push({ url: req.url, auth: req.headers.authorization }); if (req.headers.authorization !== `token ${TOKEN}`) { res.writeHead(404); res.end(); return; } if (req.url === `/api/v1/repos/${FPAGENT_REPO}/releases/latest`) { res.writeHead(releaseStatus, { 'content-type': 'application/json' }); res.end(JSON.stringify(RELEASE)); return; } if (req.url.startsWith(`/${FPAGENT_REPO}/releases/download/`)) { res.writeHead(downloadStatus, { 'content-type': 'application/gzip' }); res.end(Buffer.from('bundle-bytes')); return; } res.writeHead(404); res.end(); }); return { server, calls }; } async function bootFpagentApp(giteaUrl, overrides = {}) { const app = createApp({ giteaUrl, fpagentRepo: FPAGENT_REPO, giteaToken: TOKEN, cropperClients: null, ...overrides, }); const server = await new Promise((resolve) => { const s = app.listen(0, '127.0.0.1', () => resolve(s)); }); return { server, base: urlOf(server) }; } test('latest release is proxied as tools-relative URLs, without the token', async () => { const gitea = await startFakeGitea(); const { server, base } = await bootFpagentApp(urlOf(gitea.server)); try { const r = await fetch(`${base}/fpagent/releases/latest`); assert.equal(r.status, 200); const body = await r.json(); assert.equal(body.tag, TAG); // Unsafe asset names are dropped; the rest point back at this app. assert.deepEqual( body.assets.map((a) => a.name), [ARCHIVE, `${ARCHIVE}.sha256`, UBUNTU_ARCHIVE] ); for (const a of body.assets) { assert.equal(a.url, `/fpagent/releases/download/${TAG}/${a.name}`); } assert.ok(!JSON.stringify(body).includes(TOKEN)); assert.equal(gitea.calls[0].auth, `token ${TOKEN}`); // server authenticated upstream } finally { await stopServer(server); await stopServer(gitea.server); } }); test('bundles stream through this app; the client sends no credentials', async () => { const gitea = await startFakeGitea(); const { server, base } = await bootFpagentApp(urlOf(gitea.server)); try { const r = await fetch(`${base}/fpagent/releases/download/${TAG}/${ARCHIVE}`); assert.equal(r.status, 200); assert.equal(Buffer.from(await r.arrayBuffer()).toString(), 'bundle-bytes'); const assetCall = gitea.calls.find((c) => c.url.includes('/releases/download/')); assert.equal(assetCall.auth, `token ${TOKEN}`); } finally { await stopServer(server); await stopServer(gitea.server); } }); test('unsafe asset names are rejected before reaching the forge', async () => { const gitea = await startFakeGitea(); const { server, base } = await bootFpagentApp(urlOf(gitea.server)); try { const r = await fetch(`${base}/fpagent/releases/download/${TAG}/-bad`); assert.equal(r.status, 400); assert.equal(gitea.calls.length, 0); } finally { await stopServer(server); await stopServer(gitea.server); } }); test('a missing bundle surfaces as 404', async () => { const gitea = await startFakeGitea({ downloadStatus: 404 }); const { server, base } = await bootFpagentApp(urlOf(gitea.server)); try { const r = await fetch(`${base}/fpagent/releases/download/${TAG}/${ARCHIVE}`); assert.equal(r.status, 404); } finally { await stopServer(server); await stopServer(gitea.server); } }); test('without GITEA_RELEASE_INSTALL_KEY the installer reports 503', async () => { const { server, base } = await bootFpagentApp('https://sources.yrka.app', { giteaToken: '' }); try { const r = await fetch(`${base}/fpagent/releases/latest`); assert.equal(r.status, 503); } finally { await stopServer(server); } }); test('install.sh is served publicly at /fpagent/install.sh', async () => { const { server, base } = await bootFpagentApp('https://sources.yrka.app', { giteaToken: '' }); try { const r = await fetch(`${base}/fpagent/install.sh`); assert.equal(r.status, 200); assert.match(await r.text(), /FPAGENT_BASE_URL/); } finally { await stopServer(server); } }); test('install.sh is valid bash', () => { const script = path.join( path.dirname(fileURLToPath(import.meta.url)), '..', 'public', 'fpagent', 'install.sh' ); execFileSync('bash', ['-n', script], { stdio: 'pipe' }); });