Files
RustProxy/tools/checks/certificate_api_0319_ui_test.mjs
T
2026-10-12 14:57:37 +02:00

170 lines
9.3 KiB
JavaScript

#!/usr/bin/env node
// Execute the real 0.31.9 Certificate API panel code against a simulated DOM/API.
// This is not a compiled Rust proxy test.
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import vm from 'node:vm';
const source = readFileSync('src/admin/web/certificate-api.js', 'utf8');
const html = readFileSync('src/admin/web/index.html', 'utf8');
const english = JSON.parse(readFileSync('langs/en_US.json', 'utf8')).translations;
const polish = JSON.parse(readFileSync('langs/pl_pl.json', 'utf8')).translations;
for (const pack of [english, polish]) {
for (const key of ['certificateApiView', 'certificateApiHide', 'certificateApiEdit',
'certificateApiSave', 'certificateApiCancelEdit', 'certificateApiRegenerate',
'certificateApiRegenerateConfirm', 'certificateApiRegenerated', 'certificateApiTokenHidden']) {
assert.ok(pack[key], key);
}
}
for (const id of ['certificate-api-save', 'certificate-api-cancel-edit', 'certificate-api-token-field',
'certificate-api-secret', 'certificate-api-credentials', 'certificate-api-edit-note']) {
assert.ok(html.includes(`id="${id}"`), id);
}
class FakeNode {
constructor(tag = 'div', content = '', cls = '') {
this.tagName = tag; this.value = ''; this.content = String(content ?? '');
this.className = cls; this.children = []; this.events = {};
this.dataset = {}; this.hidden = false; this.disabled = false;
this.checked = false; this.readOnly = false;
this.classList = { add: cls => { this.className += ` ${cls}`; } };
}
get textContent() { return this.content + this.children.map(child => child.textContent).join(''); }
set textContent(value) { this.content = String(value ?? ''); this.children = []; }
addEventListener(type, callback) { this.events[type] = callback; }
append(...children) { this.children.push(...children); }
replaceChildren(...children) { this.children = [...children]; this.content = ''; }
scrollIntoView() { }
focus() { }
async fire(type, event = {}) { assert.ok(this.events[type], `${this.tagName} missing ${type}`); await this.events[type](event); }
findByText(value) { return [this, ...this.children.flatMap(child => child.findAll())].find(child => child.content === value); }
findAll() { return [this, ...this.children.flatMap(child => child.findAll())]; }
}
const fields = new Map();
const dollar = id => {
if (!fields.has(id)) fields.set(id, new FakeNode(id));
return fields.get(id);
};
const node = (tag, text, cls) => new FakeNode(tag, text, cls);
let persisted = [{ key_id: 'renew-prod', comment: 'Old owner', certificate_ids: ['first-cert'],
allow_deploy: false, created_at: 1690000000, last_used_at: 1700000000 }];
let activeToken = 'original-secret-not-readable';
const requests = [];
const makeStatus = () => ({ configured: persisted.length > 0, credentials: structuredClone(persisted),
max_credentials: 32, scopes: ['certificates:reload', 'certificates:deploy'] });
async function api(path, options = {}) {
const method = options.method || 'GET';
const data = options.body ? JSON.parse(options.body) : {};
requests.push({ path, method, data });
if (path === '/api/certificate-api' && method === 'GET') return makeStatus();
if (path === '/api/certificate-api' && method === 'DELETE') { persisted = []; return { revoked: 1 }; }
if (path === '/api/certificate-api' && method === 'PUT') {
activeToken = data.token || 'created-once-token';
const entry = { key_id: data.key_id || 'generated-id', comment: data.comment,
certificate_ids: data.certificate_ids, allow_deploy: data.allow_deploy,
created_at: 1800000000, last_used_at: null };
persisted.push(entry);
return { ...makeStatus(), credential: entry, token: activeToken, generated: !data.token };
}
const match = path.match(/^\/api\/certificate-api\/([^/]+)(\/regenerate)?$/);
assert.ok(match, `Unexpected ${method} ${path}`);
const credential = persisted.find(entry => entry.key_id === match[1]);
assert.ok(credential);
if (method === 'GET') return { credential: structuredClone(credential) };
if (method === 'PATCH') {
assert.deepEqual(Object.keys(data).sort(), ['allow_deploy', 'certificate_ids', 'comment']);
assert.ok(!('token' in data) && !('token_hash' in data));
Object.assign(credential, { comment: data.comment, certificate_ids: data.certificate_ids, allow_deploy: data.allow_deploy });
return { credential: structuredClone(credential) };
}
if (method === 'POST' && match[2] === '/regenerate') {
activeToken = 'replacement-secret-once';
credential.last_used_at = null;
credential.created_at = 1800000001;
return { credential: structuredClone(credential), token: activeToken };
}
if (method === 'DELETE') {
persisted = persisted.filter(entry => entry.key_id !== credential.key_id);
return { revoked: true };
}
throw new Error(`Unsupported ${method} ${path}`);
}
const toasts = [];
const ctx = {
document: {}, window: {}, $: dollar, el: node,
action: (text, cb) => { const result = node('button', text, 'quiet'); result.addEventListener('click', cb); result.type = 'button'; return result; },
t: key => english[key] || key,
location: { origin: 'https://admin.example.test' },
authenticated: true, api, appConfirm: async () => true,
formatDateTime: timestamp => new Date(timestamp).toISOString(),
toast: value => toasts.push(value),
fieldError: (_, message) => new Error(message), showFieldError: () => {},
};
vm.createContext(ctx);
vm.runInContext(source, ctx, { filename: 'src/admin/web/certificate-api.js' });
await new Promise(resolve => setImmediate(resolve));
const list = dollar('certificate-api-credentials');
const actionsFor = (rowIndex = 0) => list.children[rowIndex].children[0].children[1].children;
const click = async (label, rowIndex = 0) => {
const btn = actionsFor(rowIndex).find(item => item.content === label);
assert.ok(btn, `Missing button: ${label}`);
await btn.fire('click');
};
assert.equal(list.children.length, 1);
assert.ok(requests.some(request => request.path === '/api/certificate-api' && request.method === 'GET'));
await click(english.certificateApiView);
assert.equal(list.children[0].children.length, 2, 'details expanded');
const previewText = list.children[0].children[1].textContent;
assert.match(previewText, /renew-prod/);
assert.match(previewText, /Old owner/);
assert.match(previewText, /first-cert/);
assert.match(previewText, /Unavailable/);
assert.ok(!previewText.includes(activeToken), 'never show existing bearer token');
assert.ok(requests.some(request => request.path === '/api/certificate-api/renew-prod' && request.method === 'GET'));
await click(english.certificateApiEdit);
assert.equal(dollar('certificate-api-key-id').value, 'renew-prod');
assert.equal(dollar('certificate-api-key-id').readOnly, true);
assert.equal(dollar('certificate-api-comment').value, 'Old owner');
assert.equal(dollar('certificate-api-token-field').hidden, true);
assert.equal(dollar('certificate-api-save').content, english.certificateApiSave);
assert.equal(dollar('certificate-api-cancel-edit').hidden, false);
dollar('certificate-api-comment').value = 'New owner';
dollar('certificate-api-certificate-ids').value = 'second-cert,third-cert';
dollar('certificate-api-allow-deploy').checked = true;
await dollar('certificate-api-form').fire('submit', { preventDefault() {} });
assert.equal(persisted[0].comment, 'New owner');
assert.deepEqual(persisted[0].certificate_ids, ['second-cert', 'third-cert']);
assert.equal(persisted[0].allow_deploy, true);
assert.equal(persisted[0].created_at, 1690000000, 'editing must not rotate');
assert.equal(activeToken, 'original-secret-not-readable');
assert.equal(dollar('certificate-api-token-field').hidden, false);
assert.equal(dollar('certificate-api-save').content, english.certificateApiRotate);
assert.equal(dollar('certificate-api-secret-wrap').hidden, true);
await click(english.certificateApiRegenerate);
assert.equal(activeToken, 'replacement-secret-once');
assert.equal(dollar('certificate-api-secret').textContent, activeToken);
assert.equal(dollar('certificate-api-secret-wrap').hidden, false);
assert.equal(persisted[0].comment, 'New owner', 'metadata unchanged by regeneration');
assert.equal(persisted[0].allow_deploy, true);
assert.equal(persisted[0].last_used_at, null);
assert.equal(list.textContent.includes(activeToken), false, 'token not in persistent credential DOM');
await click(english.certificateApiHide);
assert.equal(dollar('certificate-api-secret').textContent, 'replacement-secret-once', 'collapsing is not a new API secret read');
await click(english.certificateApiView);
assert.equal(dollar('certificate-api-secret-wrap').hidden, true, 'reading settings does not re-expose the token');
assert.equal(dollar('certificate-api-secret').textContent, '');
assert.ok(!list.textContent.includes(activeToken));
ctx.window.rustProxyCertificateApi.reset();
assert.equal(dollar('certificate-api-secret').textContent, '');
assert.equal(list.children.length, 0);
await ctx.window.rustProxyCertificateApi.load();
assert.equal(list.children.length, 1);
assert.ok(!list.textContent.includes(activeToken));
assert.equal(requests.filter(item => item.path.endsWith('/regenerate')).length, 1);
assert.ok(toasts.includes(english.certificateApiSaved));
assert.ok(toasts.includes(english.certificateApiRegenerated));
console.log('PASS: 0.31.9 Certificate API view/edit/regenerate/one-time secret UI (real module, simulated DOM/API).');