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/); });