58 lines
5.7 KiB
JavaScript
58 lines
5.7 KiB
JavaScript
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),['关于我们']);
|
|
});
|