import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import assert from 'node:assert/strict';
import http from 'node:http';
import { spawn } from 'node:child_process';
const repoRoot = path.resolve(path.join(import.meta.dirname, '..', '..', '..', '..'));
const consoleScript = path.join(repoRoot, 'ee', 'appliance', 'host-service', 'console.mjs');
const serverScript = path.join(repoRoot, 'ee', 'appliance', 'host-service', 'server.mjs');
function httpRequest(url, options = {}) {
return new Promise((resolve, reject) => {
const req = http.request(url, {
method: options.method || 'GET',
headers: options.headers || {}
}, (res) => {
const chunks = [];
res.on('data', (chunk) => chunks.push(chunk));
res.on('end', () => resolve({
statusCode: res.statusCode || 0,
headers: res.headers,
body: Buffer.concat(chunks).toString('utf8')
}));
});
req.on('error', reject);
if (options.body) req.write(options.body);
req.end();
});
}
function httpGet(url, headers) {
return httpRequest(url, { headers });
}
function postJson(url, payload, headers = {}) {
return httpRequest(url, {
method: 'POST',
headers: { 'content-type': 'application/json', ...headers },
body: JSON.stringify(payload)
});
}
test('T003 first-boot smoke: console banner and the token -> password -> session flow', async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'alga-t003-'));
const tokenFile = path.join(tmp, 'setup-token');
const staticUiDir = path.join(tmp, 'status-ui');
const issueFile = path.join(tmp, 'issue');
const motdFile = path.join(tmp, 'motd');
const runBannerFile = path.join(tmp, 'run-banner');
const consoleTtyFile = path.join(tmp, 'tty1');
const buildInfoFile = path.join(tmp, 'build-info.json');
fs.writeFileSync(tokenFile, 'token-123\n');
fs.writeFileSync(consoleTtyFile, '');
fs.writeFileSync(buildInfoFile, JSON.stringify({ buildTimestamp: '2026-05-27T19:42:11Z' }));
fs.mkdirSync(path.join(staticUiDir, 'setup'), { recursive: true });
fs.mkdirSync(path.join(staticUiDir, 'assets'), { recursive: true });
fs.writeFileSync(path.join(staticUiDir, 'index.html'), '
Status UI
');
fs.writeFileSync(path.join(staticUiDir, 'setup', 'index.html'), 'Setup UI
');
fs.writeFileSync(path.join(staticUiDir, 'assets', 'app.js'), 'console.log("status-ui");');
const consoleResult = await new Promise((resolve) => {
const child = spawn(process.execPath, [consoleScript], {
cwd: repoRoot,
env: {
...process.env,
ALGA_APPLIANCE_TOKEN_FILE: tokenFile,
ALGA_APPLIANCE_PORT: '18080',
ALGA_APPLIANCE_ISSUE_FILE: issueFile,
ALGA_APPLIANCE_MOTD_FILE: motdFile,
ALGA_APPLIANCE_RUN_BANNER_FILE: runBannerFile,
ALGA_APPLIANCE_CONSOLE_TTYS: consoleTtyFile,
ALGA_APPLIANCE_BUILD_INFO_FILE: buildInfoFile
},
stdio: ['ignore', 'pipe', 'pipe']
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (chunk) => { stdout += chunk.toString('utf8'); });
child.stderr.on('data', (chunk) => { stderr += chunk.toString('utf8'); });
child.on('close', (code) => resolve({ code, stdout, stderr }));
});
assert.equal(consoleResult.code, 0, consoleResult.stderr);
assert.match(consoleResult.stdout, /Alga Appliance setup handoff/);
assert.match(consoleResult.stdout, /Build timestamp: 2026-05-27T19:42:11Z/);
assert.match(consoleResult.stdout, /setup UI served by the Kubernetes-hosted control plane/);
assert.match(consoleResult.stdout, /Setup URL: http:\/\/.+:18080\//);
assert.match(consoleResult.stdout, /One-time setup token: token-123/);
assert.match(consoleResult.stdout, /Sign in to this host with the account you created during installation/);
assert.match(consoleResult.stdout, /Forgot the management password\? sudo alga-appliance-reset-admin/);
// The OS credential is no longer generated or printed.
assert.doesNotMatch(consoleResult.stdout, /Temporary password/);
assert.doesNotMatch(consoleResult.stdout, /Password change required/);
assert.doesNotMatch(consoleResult.stdout, /-u alga-appliance\.service/);
assert.match(fs.readFileSync(issueFile, 'utf8'), /One-time setup token: token-123/);
assert.match(fs.readFileSync(motdFile, 'utf8'), /Setup URL: http:\/\/.+:18080\//);
assert.doesNotMatch(fs.readFileSync(motdFile, 'utf8'), /\?token=/);
assert.match(fs.readFileSync(consoleTtyFile, 'utf8'), /One-time setup token: token-123/);
const server = spawn(process.execPath, [serverScript], {
cwd: repoRoot,
env: {
...process.env,
ALGA_APPLIANCE_DISABLE_SETUP_QUEUE: '1',
ALGA_APPLIANCE_PORT: '18081',
ALGA_APPLIANCE_TOKEN_FILE: tokenFile,
ALGA_APPLIANCE_ADMIN_CREDENTIAL_FILE: path.join(tmp, 'admin-ui-credential.json'),
ALGA_APPLIANCE_SESSION_SECRET_FILE: path.join(tmp, 'session-secret'),
ALGA_APPLIANCE_STATE_FILE: path.join(tmp, 'install-state.json'),
ALGA_APPLIANCE_SETUP_INPUTS_FILE: path.join(tmp, 'setup-inputs.json'),
ALGA_APPLIANCE_STATUS_UI_DIR: staticUiDir
},
stdio: ['ignore', 'pipe', 'pipe']
});
const base = 'http://127.0.0.1:18081';
try {
await new Promise((resolve) => setTimeout(resolve, 350));
const health = await httpGet(`${base}/healthz`);
assert.equal(health.statusCode, 200);
// Fresh appliance: needs the one-time token first.
const state0 = await httpGet(`${base}/api/auth/state`);
assert.equal(state0.statusCode, 200);
assert.equal(JSON.parse(state0.body).phase, 'needs-token');
// The SPA shell is served without a session (it renders the login screen).
const setupPage = await httpGet(`${base}/setup`);
assert.equal(setupPage.statusCode, 200);
assert.match(setupPage.body, /Setup UI/);
const staticAsset = await httpGet(`${base}/assets/app.js`);
assert.equal(staticAsset.statusCode, 200);
// Data endpoints are gated.
const configNoAuth = await httpGet(`${base}/api/setup/config`);
assert.equal(configNoAuth.statusCode, 401);
// Wrong token is rejected; correct token advances to set-password.
const badToken = await postJson(`${base}/api/auth/redeem-token`, { token: 'nope' });
assert.equal(badToken.statusCode, 401);
const goodToken = await postJson(`${base}/api/auth/redeem-token`, { token: 'token-123' });
assert.equal(goodToken.statusCode, 200);
// Weak password is rejected.
const weak = await postJson(`${base}/api/auth/set-password`, { token: 'token-123', password: 'short' });
assert.equal(weak.statusCode, 400);
// Set the management password -> receive a session cookie.
const setPw = await postJson(`${base}/api/auth/set-password`, { token: 'token-123', password: 'Str0ng!Pass' });
assert.equal(setPw.statusCode, 200);
const setCookie = (setPw.headers['set-cookie'] || [])[0] || '';
assert.match(setCookie, /alga_appliance_session=/);
const cookie = setCookie.split(';')[0];
// Token is now consumed: redeeming again is a conflict.
const reRedeem = await postJson(`${base}/api/auth/redeem-token`, { token: 'token-123' });
assert.equal(reRedeem.statusCode, 409);
// Authenticated now.
const stateAuthed = await httpGet(`${base}/api/auth/state`, { Cookie: cookie });
assert.equal(JSON.parse(stateAuthed.body).phase, 'authenticated');
const config = await httpGet(`${base}/api/setup/config`, { Cookie: cookie, Host: '192.0.2.10:18081' });
assert.equal(config.statusCode, 200);
const configBody = JSON.parse(config.body);
assert.equal(configBody.mode, 'setup');
assert.equal(configBody.defaults.channel, 'stable');
assert.equal(configBody.defaults.appHostname, 'http://192.0.2.10:3000');
const submit = await postJson(`${base}/api/setup`, {
channel: 'stable',
appHostname: 'alga.example.com',
dnsMode: 'system',
tenantName: 'Acme MSP',
adminFirstName: 'Ava',
adminLastName: 'Admin',
adminEmail: 'ava@example.com',
adminPassword: 'Str0ng!Pass',
adminPasswordConfirm: 'Str0ng!Pass'
}, { Cookie: cookie });
assert.equal(submit.statusCode, 202);
const submitBody = JSON.parse(submit.body);
assert.equal(submitBody.ok, true);
assert.equal(submitBody.acceptedInputs.appHostname, 'alga.example.com');
const persisted = JSON.parse(fs.readFileSync(path.join(tmp, 'setup-inputs.json'), 'utf8'));
assert.equal(persisted.initialTenant.adminEmail, 'ava@example.com');
assert.equal(JSON.parse(fs.readFileSync(path.join(tmp, 'install-state.json'), 'utf8')).status, 'setup-queued');
const statusPage = await httpGet(`${base}/`, { Cookie: cookie });
assert.equal(statusPage.statusCode, 200);
assert.match(statusPage.body, /Status UI/);
// Logging in again with the chosen password works; wrong password does not.
const loginOk = await postJson(`${base}/api/auth/login`, { password: 'Str0ng!Pass' });
assert.equal(loginOk.statusCode, 200);
const loginBad = await postJson(`${base}/api/auth/login`, { password: 'nope' });
assert.equal(loginBad.statusCode, 401);
} finally {
server.kill('SIGTERM');
}
});