const {test} = require('node:test'); const assert = require('node:assert/strict'); const vm = require('node:vm'); const fs = require('node:fs'); const path = require('node:path'); const source = fs.readFileSync(path.join(__dirname, '../static/js/native-push.js'), 'utf8'); const id = '12345678-1234-1234-1234-123456789abc'; const tag = 'a'.repeat(64); async function setup(options={}) { const listeners = {}, navigations = [], elements = []; const session = {authenticated:true, session_tag:tag, ...options.session}; const config = {textContent:JSON.stringify({openNotification:'Open new notification'})}; const main = {prepend(node) { elements.push(node); }}; const document = { getElementById(name) { return name === 'native-push-config' ? config : null; }, querySelector(name) { return name === 'main' ? main : null; }, createElement(type) { return {type,children:[],append(node){this.children.push(node);},setAttribute(){}}; }, addEventListener() {}, body:main, }; const push = { addListener(name, callback) { listeners[name]=callback; return Promise.resolve(); }, checkPermissions:async()=>({receive:'granted'}), register:async()=>{}, }; const device = {getInfo:async()=>({deviceId:id,appVersion:'1.1.0',binding:options.binding ?? tag}), prepareSession:async()=>{}}; vm.runInNewContext(source, {document, window:{Capacitor:{getPlatform:()=> 'android',Plugins:{PushNotifications:push,MetalCircleDevice:device}},addEventListener(){}}, location:{pathname:'/',assign(value){navigations.push(value);}}, fetch:async()=>({ok:true,json:async()=>session}), localStorage:{getItem(){return 'seen';}}}); await new Promise(resolve=>setImmediate(resolve)); return {listeners,navigations,elements}; } test('tap opens only backend-resolved destination for the matching session',async()=>{ const app=await setup(); await app.listeners.pushNotificationActionPerformed({notification:{data:{notification_id:id,session_tag:tag,url:'https://evil.invalid'}}}); assert.deepEqual(app.navigations,['/notifications/'+id]); }); test('old-account push cannot navigate after user switch',async()=>{ const app=await setup({session:{session_tag:'b'.repeat(64)}}); await app.listeners.pushNotificationActionPerformed({notification:{data:{notification_id:id,session_tag:tag}}}); assert.deepEqual(app.navigations,[]); }); test('logout and native binding mismatch cannot open a notification',async()=>{ for(const options of [{session:{authenticated:false}}, {binding:''}]) { const app=await setup(options); await app.listeners.pushNotificationActionPerformed({notification:{data:{notification_id:id,session_tag:tag}}}); assert.deepEqual(app.navigations,[]); } }); test('arbitrary URL and malformed identifier are ignored',async()=>{ const app=await setup(); for(const data of [{url:'https://evil.invalid'}, {notification_id:'../../profile',session_tag:tag}]) await app.listeners.pushNotificationActionPerformed({notification:{data}}); assert.deepEqual(app.navigations,[]); }); test('foreground hint uses local text and never remote HTML',async()=>{ const app=await setup(); await app.listeners.pushNotificationReceived({body:'',data:{notification_id:id,session_tag:tag}}); assert.equal(app.elements.length,1); const link=app.elements[0].children[0]; assert.equal(link.textContent,'Open new notification'); assert.equal(link.href,'/notifications/'+id); assert.equal(link.innerHTML,undefined); });