Files
safetynet/tests/company-news.test.mjs
T

36 lines
3.4 KiB
JavaScript
Raw Normal View History

import test from 'node:test';
import assert from 'node:assert/strict';
import {createCompanyNewsReader,parseCompanyNews,publicNewsAddress} from '../apps/server/company-news.mjs';
const policy={version:1,startPage:{newsEnabled:true,feedUrl:'https://news.example/feed'}};
const rss=Buffer.from('<rss><channel><item><title><![CDATA[<b>Company news</b> & update]]></title><link>https://intranet.example/article</link><pubDate>2026-10-13</pubDate></item><item><title>Bad URL</title><link>javascript:alert(1)</link></item></channel></rss>');
test('RSS and Atom produce bounded plain text and safe links',()=>{
assert.deepEqual(parseCompanyNews(rss),[{title:'Company news & update',url:'https://intranet.example/article',publishedAt:'2026-10-13T00:00:00.000Z'}]);
assert.equal(parseCompanyNews(Buffer.from('<feed><entry><title>Announcement</title><link href="https://company.example/story" rel="alternate"/></entry></feed>'))[0].title,'Announcement');
assert.throws(()=>parseCompanyNews(Buffer.from('<!DOCTYPE rss [<!ENTITY x SYSTEM "file:///secret">]><rss/>')));
assert.throws(()=>parseCompanyNews(Buffer.from('<rss><item></rss>')));
assert.throws(()=>parseCompanyNews(Buffer.alloc(524289)));
});
test('news disabled does not resolve or request anything',async()=>{
const read=createCompanyNewsReader({lookup:()=>{throw Error('Must not resolve');}});
assert.deepEqual(await read({version:1,startPage:{newsEnabled:false}}),[]);
});
test('public DNS is pinned and feed is cached without article requests',async()=>{
let calls=0;const read=createCompanyNewsReader({lookup:async()=>[{address:'8.8.8.8',family:4}],request:async(url,options)=>{calls++;assert.equal(options.address,'8.8.8.8');assert.equal(url.href,policy.startPage.feedUrl);return rss;}});
assert.equal((await read(policy)).length,1);await read(policy);assert.equal(calls,1);
});
test('SSRF rejects private, loopback, mapped IPv6, mixed DNS and numeric URLs',async()=>{
for(const address of ['10.0.0.8','127.0.0.1','::1','::ffff:127.0.0.1','169.254.169.254']){
assert.equal(publicNewsAddress(address),false);
const read=createCompanyNewsReader({lookup:async()=>[{address,family:address.includes(':')?6:4}],request:()=>{throw Error('Must not fetch');}});await assert.rejects(read(policy));
}
const mixed=createCompanyNewsReader({lookup:async()=>[{address:'8.8.8.8',family:4},{address:'10.0.0.8',family:4}]});await assert.rejects(mixed(policy));
const read=createCompanyNewsReader();for(const feedUrl of ['http://news.example/feed','https://user:[email protected]/feed','https://127.0.0.1/feed'])await assert.rejects(read({...policy,startPage:{...policy.startPage,feedUrl}}));
});
test('exact corporate allowlist permits private DNS but never loopback',async()=>{
const read=createCompanyNewsReader({allowedHosts:'news.example',lookup:async()=>[{address:'10.0.0.8',family:4}],request:async()=>rss});assert.equal((await read(policy)).length,1);
const loop=createCompanyNewsReader({allowedHosts:'news.example',lookup:async()=>[{address:'127.0.0.1',family:4}]});await assert.rejects(loop(policy));
const suffix=createCompanyNewsReader({allowedHosts:'example',lookup:async()=>[{address:'10.0.0.8',family:4}]});await assert.rejects(suffix(policy));
});
test('failed feeds are not cached',async()=>{
let calls=0;const read=createCompanyNewsReader({lookup:async()=>[{address:'8.8.8.8',family:4}],request:async()=>{calls++;throw Error('Offline');}});await assert.rejects(read(policy));await assert.rejects(read(policy));assert.equal(calls,2);
});