const {test} = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const vm = require('node:vm'); const source = fs.readFileSync('src/main/frontend/push-settings.js', 'utf8').replace('export function', 'function'); const settle = () => new Promise(resolve => setImmediate(resolve)); function setup({permission='granted', enabled=true}={}) { const elements=[]; const create = tag => { const node={tag, children:[], listeners:{}, append(...items){this.children.push(...items)}, replaceChildren(){this.children=[]}, setAttribute(){}, addEventListener(name, callback){this.listeners[name]=callback}}; elements.push(node); return node; }; const requests=[]; let permissionCalls=0, subscriptions=0, unsubscribed=0; const subscription={toJSON:()=>({endpoint:'https://fcm.googleapis.com/wp/test',keys:{p256dh:'public',auth:'auth'}}), async unsubscribe(){unsubscribed++;}}; const env={isSecureContext:true, PushManager: function(){}, Notification:{async requestPermission(){permissionCalls++;return permission}}, navigator:{serviceWorker:{ready:Promise.resolve({pushManager:{async getSubscription(){return subscriptions?subscription:null},async subscribe(){subscriptions++;return subscription}}})}}, atob: value=>Buffer.from(value,'base64').toString('binary'),setTimeout,clearTimeout, async fetch(url, options){requests.push({url,...options});return {ok:true,async json(){return {enabled:options.method==='POST',version:options.method==='GET'?null:1}}}}}; const context={window:env,document:{createElement:create,createTextNode:text=>({text})},Intl,URL,Uint8Array,Promise,Error}; vm.runInNewContext(source,context); const root=create('div'); env.werkjournalPushSettings(root,{german:false,enabled,publicKey:'BA',url:'/account/push',csrfHeader:'X-CSRF-TOKEN',csrf:'csrf'}); return {elements,requests,toggle:()=>elements.find(e=>e.tag==='input'),permissionCalls:()=>permissionCalls,subscriptions:()=>subscriptions,unsubscribed:()=>unsubscribed}; } test('viewing settings never requests permission; opt-in persists only this device', async()=>{ const f=setup();await settle();assert.equal(f.permissionCalls(),0); const toggle=f.toggle();assert.equal(toggle.disabled,false);toggle.checked=true; await toggle.listeners.change();assert.equal(f.permissionCalls(),1);assert.equal(f.subscriptions(),1); const post=f.requests.find(r=>r.method==='POST');assert.equal(post.headers['X-CSRF-TOKEN'],'csrf'); assert.equal(JSON.parse(post.body).expectedVersion,null);assert.equal(toggle.checked,true); toggle.checked=false;await toggle.listeners.change();assert.equal(f.unsubscribed(),1); assert.ok(f.requests.some(r=>r.method==='DELETE'&&r.url.endsWith('expectedVersion=1'))); }); test('denied permission never creates a subscription or server mutation',async()=>{ const f=setup({permission:'denied'});await settle();const toggle=f.toggle();toggle.checked=true; await toggle.listeners.change();assert.equal(f.subscriptions(),0);assert.equal(f.requests.length,1); assert.equal(toggle.checked,false);assert.ok(f.elements.some(e=>e.textContent?.includes('not allowed'))); }); test('disabled server does not expose an active opt-in control',async()=>{ const f=setup({enabled:false});await settle();assert.equal(f.toggle(),undefined);assert.equal(f.requests.length,0); });