import {linkShared} from './helpers/shared-modules.mjs'; import {test} from 'node:test'; import assert from 'node:assert/strict'; import {readFile} from 'node:fs/promises'; import ts from 'typescript'; async function moduleURL(path,replace=value=>value){ const code=ts.transpileModule(await readFile(new URL(path,import.meta.url),'utf8'),{compilerOptions:{target:ts.ScriptTarget.ES2022,module:ts.ModuleKind.ESNext}}).outputText; return 'data:text/javascript;base64,'+Buffer.from(linkShared(replace(code))).toString('base64'); } const {PublicDataCache,publicCachePolicy}=await import(await moduleURL('../src/lib/api/public-cache.ts')); const protocol=await moduleURL('../src/lib/api/protocol.ts'); const {publicSiteData}=await import(await moduleURL('../src/lib/site-shared-data.ts',code=>code.replace("'./api/protocol'",JSON.stringify(protocol)))); test('public cache coalesces concurrent requests, isolates returned objects and refreshes after expiry',async()=>{ let now=0,reads=0,release;const cache=new PublicDataCache(()=>now); const load=()=>{reads++;return new Promise(resolve=>release=resolve);}; const a=cache.read('api-a|config',100,load),b=cache.read('api-a|config',100,load); assert.equal(reads,1);release({list:[{name:'original'}]}); const [first,second]=await Promise.all([a,b]);first.list[0].name='mutated';assert.equal(second.list[0].name,'original'); assert.equal((await cache.read('api-a|config',100,load)).list[0].name,'original');assert.equal(reads,1); now=101;const refreshed=cache.read('api-a|config',100,load);assert.equal(reads,2);release({list:[{name:'updated'}]});assert.equal((await refreshed).list[0].name,'updated'); assert.equal((await cache.read('api-b|config',100,async()=>({list:[]}))).list.length,0); }); test('only successful payloads are cached; configured stale values survive outages without caching failures',async()=>{ let now=0,reads=0;const cache=new PublicDataCache(()=>now),options={stale:true,accept:value=>value.code===200}; await cache.read('config',10,async()=>({code:200,name:'good'}),options);now=11; const failed=async()=>{reads++;throw new Error('upstream offline');}; assert.equal((await cache.read('config',10,failed,options)).name,'good');assert.equal((await cache.read('config',10,failed,options)).name,'good');assert.equal(reads,2); assert.equal((await cache.read('config',10,async()=>({code:500}),options)).name,'good'); await assert.rejects(cache.read('navigation',10,failed),/offline/); assert.equal((await cache.read('navigation',10,async()=>({code:200,name:'recovered'}))).name,'recovered'); await cache.read('invalid',10,async()=>({code:500}),{accept:value=>value.code===200}); assert.equal((await cache.read('invalid',10,async()=>({code:200,name:'valid'}))).name,'valid'); }); test('cache bounds and endpoint policies keep private data and indefinite growth out of the shared cache',async()=>{ const cache=new PublicDataCache(Date.now,2);let reads=0; const load=async()=>({count:++reads}); for(const key of ['a','b','c'])await cache.read(key,10000,load); assert.equal((await cache.read('a',10000,load)).count,4); await cache.read('a',0,load);assert.equal(reads,5); assert.equal(publicCachePolicy('playlet/home').ttl,30000);assert.equal(publicCachePolicy('config/info').ttl,60000); for(const endpoint of ['auth/getInfo','auth/guest','coin/trans/list','playlet/detail','playlet/chapter/detail','playlet/history/list','recharge/package/list'])assert.equal(publicCachePolicy(endpoint),undefined); assert.equal(publicCachePolicy('config/info','0').ttl,0);assert.equal(publicCachePolicy('playlet/home','999999').ttl,30000); }); test('shared browser snapshot contains visible configuration and footer fields without SDK or unknown secrets',()=>{ const data=publicSiteData({reportSdkConfig:'private-sdk-cipher',token:'private-token',config:{officialAppAddr:'https://app.test/download',telegram:'https://t.me/business',apiKey:'private-api-key'},advitise:{dialogList:[{id:'a',title:'广告',images:'https://ad.test/image',link:'https://ad.test',token:'private-ad-token'}],unknown:[{secret:'private-slot'}]}},{footerInfo:{brand:{title:'黄果短剧',content:'真实页脚',token:'private-brand-token'},contactList:[{name:'服务时间',value:'每天',token:'private-contact-token'}],navList:[{id:1,name:'规则',urlValue:'/ignored/',token:'private-nav-token',translationList:[{id:2,name:'关于我们',urlValue:'/richetext/关于我们',secret:'private-nav-child'}]}]}}); const text=JSON.stringify(data);for(const secret of ['private-sdk-cipher','private-token','private-api-key','private-ad-token','private-slot','private-contact-token','private-nav-token','private-nav-child','private-brand-token'])assert.ok(!text.includes(secret)); assert.equal('trackerConfigured' in data,false);assert.ok(!text.includes('t.me/business'));assert.deepEqual(data.footer.footerInfo.contactList,[]);assert.equal(data.config.config.officialAppAddr,'https://app.test/download');assert.equal(data.footer.footerInfo.brand.title,'黄果短剧');assert.equal(data.footer.footerInfo.brand.content,'真实页脚'); assert.equal(data.footer.footerInfo.navList[0].translationList[0].name,'关于我们');assert.equal(data.footer.footerInfo.navList[0].translationList[0].urlValue,'/richetext/关于我们'); assert.doesNotThrow(()=>publicSiteData({config:null,advitise:null},{footerInfo:null})); const filtered=publicSiteData({},{footerInfo:{navList:[{name:'帮助',translationList:[{name:'关于我们',urlValue:'/richtext/关于我们'},{name:'意见反馈',urlValue:'/info/feedback'},{name:'在线客服',urlValue:'https://help.test'},{name:'邮箱',urlValue:'mailto:a@b.test'},{name:'充值',urlValue:'/info/deposit/'}]}]}}); assert.deepEqual(filtered.footer.footerInfo.navList[0].translationList.map(item=>item.name),['关于我们']); });