From 60f1093db9cc26b372f8819974bef7a52fbe869d Mon Sep 17 00:00:00 2001 From: matcha Date: Sun, 4 Oct 2026 02:46:38 +0000 Subject: [PATCH] Consolidate Persimmons source --- extension/tests/popup.test.mjs | 100 +++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 extension/tests/popup.test.mjs diff --git a/extension/tests/popup.test.mjs b/extension/tests/popup.test.mjs new file mode 100644 index 0000000..f06b72a --- /dev/null +++ b/extension/tests/popup.test.mjs @@ -0,0 +1,100 @@ +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/); +});