This repository has been archived on 2026-10-14. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files

101 lines
5.6 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
test('popup never reads cookies, saves explicit tokens, lists and revokes Juice in My vault', async () => {
class Element {
constructor(text = '', value = '') { this.textContent = text; this.value = value; this.children = []; this.disabled = false; }
addEventListener(name, listener) { this[name] = listener; }
append(...items) { this.children.push(...items); }
add(item) { this.children.push(item); }
replaceChildren(...items) { this.children = items; this.value = ''; }
querySelectorAll() { return this.children.flatMap(c => c.children || []); }
}
const ids = ['juice-panel', 'origin', 'auth', 'connect', 'save', 'squeeze', 'status', 'vault-status',
'juice-status', 'sources', 'juices', 'name', 'minutes', 'token', 'token-label', 'save-form', 'subject'];
const nodes = Object.fromEntries(ids.map(id => [id, new Element()]));
nodes.minutes.value = '60'; nodes.name.value = 'Site session';
nodes.subject.value = 'Draft assistant';
const grants = new Set();
const permissionRequests = []; const calls = [];
let grantNext = false; let cookieReads = 0; let juiceAvailable = false;
let sourceExists = false;
const publicItem = {id: 'ABC123', name: 'SITE_SESSION', vault_kind: 'user'};
globalThis.document = {getElementById: id => nodes[id],
querySelectorAll: () => [{value: 'content:read'}, {value: 'content:draft-write'}],
createElement: () => new Element(), createTextNode: text => new Element(text)};
globalThis.Option = Element;
globalThis.window = {addEventListener() {}};
globalThis.chrome = {
tabs: {query: async () => [{url: 'https://wp.phly.cloud/wp-admin/'}]},
permissions: {
contains: async ({origins}) => origins.every(origin => grants.has(origin)),
request: async ({origins}) => {
permissionRequests.push(origins);
if (grantNext) origins.forEach(origin => grants.add(origin));
return grantNext;
}
},
cookies: {getAll: async () => { cookieReads++; throw new Error('Cookie API must never be used'); }}
};
globalThis.fetch = async (url, options) => {
const path = url.split('/persimmons/')[1];
const body = options.body ? JSON.parse(options.body) : undefined;
calls.push({path, body});
let data; let status = 200;
if (path === 'list') data = {connections: sourceExists ? [publicItem] : []};
else if (path === 'create') { sourceExists = true; data = publicItem; }
else if (!juiceAvailable) { data = {error: 'SECRET_SERVER_FIXTURE'}; status = 404; }
else if (path === 'juice/list') data = {juice: [{id: 'JUICE123', vault_kind: 'user', status: 'active'}]};
else data = {ok: true};
return new Response(JSON.stringify(data), {status, headers: {'content-type': 'application/json'}});
};
const settle = async () => {
for (let i = 0; i < 100 && nodes.connect.disabled; i++) await new Promise(resolve => setImmediate(resolve));
assert.equal(nodes.connect.disabled, false, 'operation completed');
};
await import('../popup.mjs');
assert.equal(nodes.origin.textContent, 'https://wp.phly.cloud');
assert.equal(cookieReads, 0); assert.equal(calls.length, 0);
nodes.connect.onclick(); await settle();
assert.equal(calls.length, 0); assert.match(nodes.status.textContent, /not granted/);
grantNext = true;
nodes.connect.onclick(); await settle();
assert.deepEqual(calls.map(c => c.path), ['list', 'juice/list']);
assert.equal(cookieReads, 0); assert.equal(nodes.save.disabled, false);
nodes.token.value = 'SYNTHETIC_TOKEN';
nodes['save-form'].onsubmit({preventDefault() {}}); await settle();
const saved = calls.find(c => c.path === 'create').body;
assert.equal(saved.vault_kind, 'user');
assert.equal(saved.account, 'https://wp.phly.cloud');
assert.equal(saved.credential, 'SYNTHETIC_TOKEN');
assert.equal(saved.credential_type, 'api-token');
assert.deepEqual(saved.scopes, ['content:read', 'content:draft-write']);
assert.equal(saved.expires, undefined);
assert.equal(nodes.sources.value, 'ABC123');
assert.equal(nodes.token.value, ''); assert.equal(cookieReads, 0);
juiceAvailable = true;
nodes.squeeze.onclick(); await settle();
assert.equal(calls.find(c => c.path === 'juice/create').body.source_id, 'ABC123');
assert.equal(calls.find(c => c.path === 'juice/create').body.subject, 'Draft assistant');
assert.ok(Math.abs(Date.parse(calls.find(c => c.path === 'juice/create').body.expires_at) - Date.now() - 3600000) < 5000);
const revoke = nodes.juices.children[0].children[1];
revoke.onclick(); await settle();
assert.deepEqual(calls.find(c => c.path === 'juice/revoke').body, {id: 'JUICE123', vault_kind: 'user'});
const flatten = node => [node.textContent, ...node.children.map(flatten)].join(' ');
assert.doesNotMatch(Object.values(nodes).map(flatten).join(' '), /SECRET_|SYNTHETIC_TOKEN/);
calls.length = 0;
await import('../popup.mjs?reopened');
assert.equal(calls.length, 0, 'reopening does not fetch secondary Juice');
nodes['juice-panel'].open = true; nodes['juice-panel'].hidden = false;
nodes['juice-panel'].toggle(); await settle();
assert.deepEqual(calls.map(c => c.path), ['list', 'juice/list']);
assert.equal(cookieReads, 0);
assert.deepEqual(permissionRequests[0], ['https://teahub.xyz/*']);
// A failed save clears the manually entered token too.
globalThis.fetch = async () => { throw new Error('SYNTHETIC_TOKEN'); };
nodes.token.value = 'SYNTHETIC_TOKEN';
nodes['save-form'].onsubmit({preventDefault() {}}); await settle();
assert.equal(nodes.token.value, '');
assert.doesNotMatch(nodes.status.textContent, /SYNTHETIC_TOKEN/);
});