Files
AriWeb-Suite/modules/webmaker/test/hal-managed.test.js
T

47 lines
7.2 KiB
JavaScript

'use strict';
// v1.9.0 (Ariweb Suite HAL 0.4.0 대응) 자동 검증 — HAL 통합 설정(lib/hal-managed.js): 적용·결과 파일·잠금(라이브러리 + 서버 API). 외부 서비스 없음.
// 비밀값 시험 값은 실행할 때마다 crypto 로 만든다(고정 문자열을 저장소에 두지 않는다).
const test = require('node:test'); const assert = require('node:assert/strict');
const fs = require('fs'); const path = require('path'); const os = require('os'); const crypto = require('crypto'); const { spawn } = require('child_process');
const ROOT = path.join(__dirname, '..');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'wm-halm-')); process.env.DATA_ROOT = tmp; process.env.DATA_DIR = path.join(tmp, 'sites'); process.env.SITES_ROOT = path.join(tmp, 'www'); fs.mkdirSync(process.env.SITES_ROOT, { recursive: true });
const rnd = (p) => `${p}-${crypto.randomBytes(12).toString('hex')}`;
const writeManaged = (dir, values) => { const hash = 'sha256:' + crypto.createHash('sha256').update(JSON.stringify(values)).digest('hex'); fs.writeFileSync(path.join(dir, 'hal-managed.json'), JSON.stringify({ format: 1, module: 'webmaker', hash, values }), { mode: 0o600 }); return hash; };
test('hal-managed: 파일이 없으면 아무것도 하지 않는다', () => {
const hm = require(ROOT + '/lib/hal-managed'); assert.deepEqual(hm.apply(), { skipped: 'no_file' }); assert.deepEqual(hm.managedKeys(), []); assert.deepEqual(hm.lockedKeys({ marginPct: 1 }), []); assert.equal(hm.publicInfo().managed, false);
});
test('hal-managed: settings·설정 파일·보관소에 적용하고 결과 파일(비밀값 없음)을 남긴다 · 같은 hash 는 건너뜀 · 모르는 항목은 오류로 기록', () => {
const hm = require(ROOT + '/lib/hal-managed'); const settings = require(ROOT + '/lib/settings'); const config = require(ROOT + '/lib/config'); const vault = require(ROOT + '/lib/vault');
const key = rnd('sk-ant'); const svc = rnd('svc');
const hash = writeManaged(tmp, { marginPct: 35, 'fx.mode': 'manual', 'fx.manualRate': 1400, signupCredit: 500, maxEdits: 7, 'auth.mode': 'api-key', 'auth.apiKey': key, serviceApiKey: svc, 'service.allowSignup': false, 'ai.maxConcurrentJobs': 3, 'nope.key': 1 });
const r = hm.apply(); assert.equal(r.hash, hash); assert.deepEqual(r.errors.map((x) => x.key), ['nope.key']);
const s = settings.load(); assert.equal(s.marginPct, 35); assert.equal(s.fx.mode, 'manual'); assert.equal(s.fx.manualRate, 1400); assert.equal(s.signupCredit, 500); assert.equal(s.maxEdits, 7); assert.equal(s.auth.mode, 'api-key'); assert.equal(s.serviceApiKey, svc);
assert.equal(vault.secretOf('anthropic-api-key'), key, 'Claude 키는 보관소에'); assert.equal(config.get('service.allowSignup'), false); assert.equal(Number(config.get('ai.maxConcurrentJobs')), 3);
const applied = fs.readFileSync(hm.APPLIED, 'utf8'); assert.ok(!applied.includes(key) && !applied.includes(svc), '결과 파일에 비밀값 없음'); const aj = JSON.parse(applied); assert.equal(aj.hash, hash); assert.equal(aj.halCompat, '0.4.0'); assert.ok(aj.keys.includes('auth.apiKey'));
assert.equal(hm.apply().skipped, undefined, '오류가 남은 적용은 다음에 다시 시도'); writeManaged(tmp, { marginPct: 36 }); const r2 = hm.apply(); assert.deepEqual(r2.errors, []); assert.equal(settings.load().marginPct, 36); assert.equal(hm.apply().skipped, 'same_hash');
const info = hm.publicInfo(); assert.equal(info.managed, true); assert.deepEqual(info.keys, ['marginPct']); assert.ok(!JSON.stringify(info).includes(key));
});
test('hal-managed: 잠금 — 관리 항목 변경은 409 managed_by_hal(마스크·빈 비밀값은 변경 아님), 설정 페이지 저장도 잠김', () => {
const hm = require(ROOT + '/lib/hal-managed'); writeManaged(tmp, { marginPct: 30, 'auth.mode': 'api-key', 'auth.apiKey': rnd('k'), 'service.allowSignup': true });
assert.deepEqual(hm.lockedKeys({ marginPct: 40, signupCredit: 1, auth: { mode: 'server-login', apiKey: '••••' } }).sort(), ['auth.mode', 'marginPct']); assert.deepEqual(hm.lockedKeys({ auth: { apiKey: '' } }), []); assert.deepEqual(hm.lockedKeys({ signupCredit: 1 }), []);
assert.throws(() => hm.assertNotManaged(['marginPct']), (e) => e.status === 409 && e.code === 'managed_by_hal'); assert.deepEqual(hm.lockedVaultKinds(), ['anthropic-api-key']);
assert.throws(() => require(ROOT + '/lib/setupapi').save({ values: { 'service.allowSignup': false } }, 'test'), (e) => e.status === 409 && e.code === 'managed_by_hal');
});
test('서버: 관리 항목 저장 409·설정 응답의 halManaged·기동 때 적용', async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'wm-halm-srv-')); fs.writeFileSync(path.join(dir, 'setup.token'), crypto.randomBytes(16).toString('hex') + '\n', { mode: 0o600 }); writeManaged(dir, { marginPct: 33 });
const PORT = 4000 + Math.floor(Math.random() * 90); const B = `http://127.0.0.1:${PORT}`; const jar = {};
const api = async (p, init = {}, who = 'a') => { const headers = { 'Content-Type': 'application/json', referer: `${B}/` }; if (jar[who]) headers.cookie = jar[who]; const r = await fetch(B + p, { ...init, headers, redirect: 'manual' }); const sc = r.headers.get('set-cookie'); if (sc) jar[who] = sc.split(';')[0]; let b = {}; try { b = await r.json(); } catch (_) {} return { status: r.status, b }; };
const child = spawn('node', [path.join(ROOT, 'server.js')], { env: { ...process.env, PORT: String(PORT), HOST: '127.0.0.1', DATA_ROOT: dir, DATA_DIR: path.join(dir, 'sites'), SITES_ROOT: path.join(dir, 'www'), PUBLIC_BASE_URL: B, WEBMAKER_SETUP_PORT: '0' }, stdio: ['ignore', 'pipe', 'pipe'] });
let out = ''; child.stdout.on('data', (c) => { out += c; }); child.stderr.on('data', (c) => { out += c; });
try {
for (let i = 0; i < 100 && !/listening on/.test(out); i++) await new Promise((r) => setTimeout(r, 100)); assert.match(out, /listening on/);
for (let i = 0; i < 50 && !fs.existsSync(path.join(dir, 'hal-managed.applied.json')); i++) await new Promise((r) => setTimeout(r, 100)); assert.ok(fs.existsSync(path.join(dir, 'hal-managed.applied.json')), '기동 때 적용');
const tok = fs.readFileSync(path.join(dir, 'setup.token'), 'utf8').trim(); const pw = rnd('Pw1!'); assert.equal((await api('/api/setup/login', { method: 'POST', body: JSON.stringify({ token: tok }) }, 's')).status, 200);
assert.ok((await api('/api/setup/admin/create', { method: 'POST', body: JSON.stringify({ id: 'root1', name: 'Root', password: pw }) }, 's')).status < 300); assert.equal((await api('/api/auth/login', { method: 'POST', body: JSON.stringify({ id: 'root1', password: pw }) }, 'r')).status, 200);
const g = await api('/api/admin/settings', {}, 'r'); assert.equal(g.status, 200); assert.equal(g.b.marginPct, 33); assert.deepEqual(g.b.halManaged.keys, ['marginPct']); assert.equal(g.b.halManaged.halCompat, '0.4.0');
const p = await api('/api/admin/settings', { method: 'PUT', body: JSON.stringify({ marginPct: 50 }) }, 'r'); assert.equal(p.status, 409); assert.equal(p.b.code, 'managed_by_hal'); assert.deepEqual(p.b.keys, ['marginPct']);
assert.equal((await api('/api/admin/settings', { method: 'PUT', body: JSON.stringify({ signupCredit: 10 }) }, 'r')).status, 200, '관리하지 않는 항목은 그대로 저장');
} finally { try { child.kill('SIGTERM'); } catch (_) {} }
});