170 lines
9.3 KiB
JavaScript
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).');
|