All checks were successful
Quality gates / quality (pull_request) Successful in 1m28s
106 lines
6.2 KiB
JavaScript
106 lines
6.2 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFile } from 'node:fs/promises';
|
|
import { spawnSync } from 'node:child_process';
|
|
|
|
const servicePath = new URL('../deploy/timmy-staging.service', import.meta.url);
|
|
const envPath = new URL('../deploy/timmy-staging.env.example', import.meta.url);
|
|
const caddyPath = new URL('../deploy/Caddyfile.staging.example', import.meta.url);
|
|
const runbookPath = new URL('../docs/STAGING-RUNBOOK.md', import.meta.url);
|
|
|
|
const literalSecret = /(password|token|secret|api[_-]?key)\s*[=:]\s*(?!\$\{|<|CHANGE_ME|false|$)["']?[A-Za-z0-9/+_.-]{8,}/i;
|
|
|
|
test('systemd template runs a dedicated loopback-only agent-disabled service', async () => {
|
|
const service = await readFile(servicePath, 'utf8');
|
|
for (const directive of [
|
|
'User=timmy-staging', 'Group=timmy-staging',
|
|
'WorkingDirectory=/opt/timmy-staging/current',
|
|
'EnvironmentFile=/etc/timmy-staging.env',
|
|
'Environment=HOST=127.0.0.1', 'Environment=PORT=4174',
|
|
'Environment=TIMMY_BASE_PATH=/timmy-staging',
|
|
'Environment=TIMMY_AGENT_ENABLED=false',
|
|
]) assert.match(service, new RegExp(`^${directive.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'm'), directive);
|
|
assert.match(service, /^ExecStart=\/usr\/bin\/env TIMMY_AGENT_ENABLED=false TIMMY_VISION_ENABLED=false \/usr\/local\/lib\/timmy-staging\/node server\.mjs$/m);
|
|
assert.doesNotMatch(service, /ExecStart=\/usr\/(?:local\/)?bin\/node|0\.0\.0\.0|TIMMY_AGENT_ENABLED=true/);
|
|
assert.doesNotMatch(service, literalSecret);
|
|
});
|
|
|
|
test('systemd command-level environment keeps agent and vision disabled after EnvironmentFile overrides', async () => {
|
|
const service = await readFile(servicePath, 'utf8');
|
|
const environmentFileIndex = service.indexOf('EnvironmentFile=');
|
|
const execLine = service.match(/^ExecStart=(.+)$/m)?.[1];
|
|
assert.ok(execLine, 'ExecStart must exist');
|
|
assert.ok(environmentFileIndex < service.indexOf(`ExecStart=${execLine}`), 'EnvironmentFile must be loaded before command overrides');
|
|
const argv = execLine.trim().split(/\s+/);
|
|
assert.equal(argv.shift(), '/usr/bin/env');
|
|
const assignments = argv.filter(value => /^TIMMY_(?:AGENT|VISION)_ENABLED=/.test(value));
|
|
assert.deepEqual(assignments, ['TIMMY_AGENT_ENABLED=false', 'TIMMY_VISION_ENABLED=false']);
|
|
const probe = spawnSync('/usr/bin/env', [
|
|
...assignments, process.execPath, '-e',
|
|
'process.stdout.write(`${process.env.TIMMY_AGENT_ENABLED},${process.env.TIMMY_VISION_ENABLED}`)',
|
|
], { env: { ...process.env, TIMMY_AGENT_ENABLED: 'true', TIMMY_VISION_ENABLED: 'true' }, encoding: 'utf8' });
|
|
assert.equal(probe.status, 0, probe.stderr);
|
|
assert.equal(probe.stdout, 'false,false');
|
|
});
|
|
|
|
test('systemd template applies filesystem privilege process and resource confinement', async () => {
|
|
const service = await readFile(servicePath, 'utf8');
|
|
for (const directive of [
|
|
'UMask=0077', 'NoNewPrivileges=true', 'PrivateTmp=true', 'PrivateDevices=true',
|
|
'ProtectSystem=strict', 'ProtectHome=true', 'ReadWritePaths=/var/lib/timmy-staging',
|
|
'RestrictSUIDSGID=true', 'LockPersonality=true', 'RestrictNamespaces=true',
|
|
'ProtectKernelTunables=true', 'ProtectKernelModules=true', 'ProtectKernelLogs=true',
|
|
'ProtectControlGroups=true', 'ProtectClock=true', 'ProtectHostname=true',
|
|
'CapabilityBoundingSet=', 'AmbientCapabilities=', 'RestrictRealtime=true',
|
|
'TasksMax=64', 'MemoryMax=512M', 'LimitNOFILE=1024',
|
|
]) assert.match(service, new RegExp(`^${directive.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'm'), directive);
|
|
assert.equal((service.match(/^ReadWritePaths=/gm) || []).length, 1);
|
|
});
|
|
|
|
test('environment example is least privilege base-path staging configuration without credentials', async () => {
|
|
const env = await readFile(envPath, 'utf8');
|
|
for (const setting of [
|
|
'HOST=127.0.0.1', 'PORT=4174', 'TIMMY_BASE_PATH=/timmy-staging',
|
|
'TIMMY_STAGING_LABEL=true', 'TIMMY_AGENT_ENABLED=false', 'TIMMY_VISION_ENABLED=false',
|
|
]) assert.match(env, new RegExp(`^${setting.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'm'));
|
|
assert.match(env, /install -m 600/);
|
|
assert.doesNotMatch(env, /TIMMY_AGENT_ACCESS_TOKEN|GITEA_TOKEN|OPENAI_API_KEY|COOKIE/);
|
|
assert.doesNotMatch(env, literalSecret);
|
|
});
|
|
|
|
test('Caddy snippet isolates git and authenticates staging before a catchall', async () => {
|
|
const caddy = await readFile(caddyPath, 'utf8');
|
|
const git = caddy.indexOf('handle @git');
|
|
const staging = caddy.indexOf('handle_path /timmy-staging/*');
|
|
const catchall = caddy.indexOf('handle {');
|
|
assert.ok(git >= 0 && staging > git && catchall > staging, 'route order must be git, staging, catchall');
|
|
assert.match(caddy, /basic_auth/);
|
|
assert.match(caddy, /\{\$TIMMY_STAGING_PASSWORD_HASH\}/);
|
|
assert.match(caddy, /reverse_proxy 127\.0\.0\.1:4174/);
|
|
assert.match(caddy, /max_size 8MB/);
|
|
assert.match(caddy, /X-Content-Type-Options "nosniff"/);
|
|
assert.match(caddy, /X-Frame-Options "DENY"/);
|
|
assert.match(caddy, /Referrer-Policy "no-referrer"/);
|
|
assert.match(caddy, /Content-Security-Policy/);
|
|
assert.match(caddy, /rewrite \* \/timmy-staging\{uri\}/, 'handle_path strips once, then upstream base path is explicitly reconstructed');
|
|
assert.doesNotMatch(caddy, /\$2[aby]\$[A-Za-z0-9./]{20,}|password\s+[^<{\s]/i);
|
|
});
|
|
|
|
test('runbook covers safe installation operation verification rollback backup and removal', async () => {
|
|
const runbook = await readFile(runbookPath, 'utf8');
|
|
for (const heading of [
|
|
'Prerequisites', 'DNS and URL', 'Install', 'Promote', 'Smoke test', 'Status and logs',
|
|
'Rollback', 'Backup', 'Remove staging', 'Template validation',
|
|
]) assert.match(runbook, new RegExp(`^## .*${heading}`, 'mi'), heading);
|
|
assert.match(runbook, /sha256/i);
|
|
assert.match(runbook, /install -D -o root -g root -m 755[^\n]+\/usr\/local\/lib\/timmy-staging\/node/);
|
|
assert.match(runbook, /\/usr\/local\/lib\/timmy-staging\/node --version/);
|
|
assert.match(runbook, /chmod 600|install -m 600/);
|
|
assert.match(runbook, /systemctl restart timmy-staging\.service/);
|
|
assert.match(runbook, /no prior release[^.]*stops[^.]*service/i);
|
|
assert.match(runbook, /failed release[^.]*inert[^.]*evidence/i);
|
|
assert.match(runbook, /journalctl -u timmy-staging\.service/);
|
|
assert.match(runbook, /do not.*live|approval/i);
|
|
assert.doesNotMatch(runbook, literalSecret);
|
|
});
|