Files
web_hgdjc_h5/tests/analytics.test.mjs
T
谢宇宁andClaude Opus 5.5 f65d10e998 Initial commit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 23:07:59 +07:00

281 lines
25 KiB
JavaScript

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {readFile} from 'node:fs/promises';
import ts from 'typescript';
import {runInNewContext} from 'node:vm';
// Run the bundled SDK's real named-event validators without initializing it:
// accepted events stay in its pre-init queue and never contact an analytics server.
const sdkLogs=[];
const sdkSandbox={console:{warn:(...args)=>sdkLogs.push(args)}};
runInNewContext(await readFile(new URL('../public/scripts/web-sdk-v1.2.0.js',import.meta.url),'utf8'),sdkSandbox);
async function moduleUrl(file,replace=code=>code){
const code=ts.transpileModule(await readFile(new URL(file,import.meta.url),'utf8'),{compilerOptions:{target:ts.ScriptTarget.ES2022,module:ts.ModuleKind.ESNext}}).outputText;
return 'data:text/javascript;base64,'+Buffer.from(replace(code)).toString('base64');
}
const trackingUrl=await moduleUrl('../src/lib/tracking.ts');
const reportUrl=await moduleUrl('../src/lib/analytics/report-fields.ts');
const {TRACKER_APP_ID,COMMON_KEYS,SDK_COMMON_KEYS,base64JSON,trackingPage}=await import(trackingUrl);
const authUrl='data:text/javascript,export function authMetadata(){return {channel:"paid_1050",devID:"business-device",deviceId:"business-device"}}';
const userAgentUrl=await moduleUrl('../src/lib/api/user-agent.ts');
const analyticsUrl=await moduleUrl('../src/lib/client/analytics.ts',code=>code.replace("'../tracking'",JSON.stringify(trackingUrl)).replace("'./auth'",JSON.stringify(authUrl)).replace("'../api/user-agent'",JSON.stringify(userAgentUrl)).replace("'../analytics/report-fields'",JSON.stringify(reportUrl)));
const {SiteTracker}=await import(analyticsUrl);
const {trackingHeaders}=await import(await moduleUrl('../src/lib/api/tracking-headers.ts',code=>code.replace("'../tracking'",JSON.stringify(trackingUrl))));
const flush=()=>new Promise(resolve=>setImmediate(resolve));
const common=()=>({app_id:TRACKER_APP_ID,uid:'',channel:'paid_1050',sid:'sid-raw',client_ts:Math.floor(Date.now()/1000),device:'PC',device_id:'sdk-device',device_brand:'',device_model:'',device_fingerprint:'raw-fingerprint',fp_version:'3.0.1',user_agent:'test UA',sdk_version:'1.2.0',app_version:'1.0.0'});
class Element extends EventTarget {
dataset={};hidden=false;attributes=new Map();
getClientRects(){return this.hidden?[]:[{}];}
closest(selector){
if(selector.includes('[hidden]'))return this.hidden||this.closedDialog?this:null;
if(selector==='[data-track-item]'&&this.hasAttribute('data-track-item'))return this;
if(selector==='[data-track-ad="external"]'&&this.getAttribute('data-track-ad')==='external')return this;
if(selector.includes('a[data-app-download]')&&this.hasAttribute('data-app-download'))return this;
if(selector.includes('a[data-track-navigation]')&&this.dataset.trackNavigation)return this;
return this.parents?.[selector]||null;
}
getAttribute(name){return this.attributes.get(name)||null;}
hasAttribute(name){return this.attributes.has(name);}
}
class Video extends Element {currentTime=0;duration=100;playbackRate=1.25;paused=false;ended=false;seeking=false;}
function fixture(){
const saved=new Map(),events=[],calls=[],nodes=[],results=[],diagnostics=[],observed=new Set(),consoleCalls=[];
let clock=1000,video,intersection,mutation,modal=null;
const fields=common();
const sdk={init(options){calls.push(options);},track:new Proxy({}, {get:(_,method)=>async payload=>{events.push({method,payload});const result=JSON.parse(JSON.stringify(await sdkSandbox.WebSDK.track[method](payload)));results.push({method,...result});return result;}}),getCommonFields:async()=>({...fields}),setUid(uid){fields.uid=uid;},setChannel(channel){fields.channel=channel;},notifyPageLoad(){calls.push('route');},flush:async()=>{calls.push('flush');}};
const win=new EventTarget();win.WebSDK=sdk;win.console={warn:(...args)=>consoleCalls.push(args)};
const document=new EventTarget();Object.assign(document,{referrer:'https://search.example/',documentElement:new Element(),head:{append(){}},querySelector:selector=>selector==='dialog:modal'?modal:video||null,querySelectorAll:()=>nodes});
document.addEventListener('tracker:diagnostic',event=>diagnostics.push(event.detail));
const values={window:win,document,location:{pathname:'/',search:'',origin:'https://site.example',href:'https://site.example/'},performance:{now:()=>clock,getEntriesByType:()=>[{duration:42}]},Element,HTMLVideoElement:Video,IntersectionObserver:class{constructor(callback){intersection=callback;}observe(node){observed.add(node);}unobserve(node){observed.delete(node);}disconnect(){observed.clear();}},MutationObserver:class{constructor(callback){mutation=callback;}observe(){}},innerWidth:1000,innerHeight:800,screen:{width:1000,height:800}};
for(const [key,value] of Object.entries(values)){saved.set(key,Object.getOwnPropertyDescriptor(globalThis,key));Object.defineProperty(globalThis,key,{configurable:true,writable:true,value});}
const tracker=new SiteTracker();win.tracker=tracker;
return {tracker,events,calls,fields,nodes,win,document,results,diagnostics,observed,sdk,consoleCalls,location:values.location,mutate:()=>mutation([]),setModal:value=>{modal=value;},setClock:value=>{clock=value;},setVideo:value=>{video=value;},intersect:node=>intersection([{target:node,isIntersecting:true}]),close(){for(const [key,descriptor] of saved)if(descriptor)Object.defineProperty(globalThis,key,descriptor);else delete globalThis[key];}};
}
const drama={id:23456,title:'短剧标题',totalEpisode:12,themeIds:[8],themeNames:['都市'],tagIds:[9],tagNames:['爱情']};
const chapters=[{id:90001,currentEpisode:1,mediaTime:100},{id:90002,currentEpisode:2,mediaTime:100}];
function advertisement(placement='inline'){
const node=new Element();node.attributes.set('data-track-ad','external');
node.dataset={adId:'same-code',adSlotKey:'slot-3',adSlotName:'推荐应用',adType:'icon',adClickable:'1',adPlacement:placement};
return node;
}
test('real SDK accepts content exposures, clicks, search and navigation fields including the root parent id',async()=>{
const f=fixture();try{
f.tracker.configure({reportSdkConfig:'configured-cipher'});await flush();
const item=new Element();item.attributes.set('data-track-item','');item.dataset={itemId:'23456',itemType:'short_drama',itemName:'剧名'};
f.intersect(item);f.tracker.click({target:item,clientX:10,clientY:10});
f.tracker.search('短剧',28,1);
const nav=new Element();nav.dataset.trackNavigation='tg_group';nav.textContent='官方TG群';nav.href='https://t.me/example';nav.attributes.set('href',nav.href);
f.tracker.click({target:nav,clientX:10,clientY:10});
await flush();
for(const method of ['appPageView','appPageitemExposure','appPageitemClick','keywordSearch','navigation','pageClick'])assert.ok(f.results.some(result=>result.method===method&&result.ok),method);
assert.ok(f.events.filter(e=>e.method.startsWith('appPageitem')).every(e=>e.payload.parent_item_id==='0'));
assert.deepEqual(f.results.filter(result=>!result.ok),[]);assert.deepEqual(f.diagnostics,[]);
}finally{f.close();}
});
test('home, large-popup and grid-popup advertisements report independently without duplicating each placement',async()=>{
const f=fixture();try{
const nodes=['inline','startup-ad','startup-grid'].map(advertisement);
// Popup events can arrive before the asynchronous SDK configuration.
for(const node of nodes){f.tracker.externalAd(node,'show');f.tracker.externalAd(node,'show');f.tracker.externalAd(node,'click');}
assert.equal(f.events.length,0);
f.tracker.configure({reportSdkConfig:'configured-cipher'});await flush();
assert.equal(f.events.filter(e=>e.method==='adImpression').length,3);assert.equal(f.events.filter(e=>e.method==='adClick').length,3);
assert.ok(f.events.filter(e=>e.method==='adImpression').every(e=>e.payload.ad_slot_key==='slot-3'),'retain API advertisement slot codes');
assert.deepEqual(f.results.filter(result=>!result.ok),[]);
const count=f.events.length;f.tracker.externalAd(nodes[1],'close');assert.equal(f.events.length,count,'closing is not an ad click');
for(const node of nodes.slice(1)){f.tracker.advertising(node,'close');f.tracker.advertising(node,'close');}
const popup=f.events.filter(e=>e.method==='advertising');
assert.equal(popup.filter(e=>e.payload.event_type==='show').length,2);
assert.equal(popup.filter(e=>e.payload.event_type==='click').length,2);
assert.equal(popup.filter(e=>e.payload.event_type==='close').length,2);
await flush();assert.deepEqual(f.results.filter(result=>!result.ok),[]);
}finally{f.close();}
});
test('closed dialogs and obscured background content are observed again when the modal closes',async()=>{
const f=fixture();try{
f.tracker.configure({reportSdkConfig:'configured-cipher'});await flush();
const background=advertisement(),grid=advertisement('startup-grid'),offscreen=advertisement('offscreen');offscreen.dataset.adId='offscreen';
f.nodes.push(background,grid,offscreen);grid.closedDialog=true;f.mutate();
const modal={contains:node=>node===grid||node===offscreen};f.setModal(modal);grid.closedDialog=false;f.mutate();
assert.equal(f.observed.has(background),false);assert.ok(f.observed.has(grid));
f.intersect(background);assert.equal(f.events.filter(e=>e.method==='adImpression').length,0);
f.intersect(grid);assert.equal(f.events.filter(e=>e.method==='adImpression').length,1);
assert.equal(f.events.some(e=>e.payload.ad_id==='offscreen'),false,'a scrollable grid must only expose intersecting tiles');
f.intersect(offscreen);assert.equal(f.events.filter(e=>e.method==='adImpression').length,2);
grid.closedDialog=true;offscreen.closedDialog=true;f.setModal(null);f.mutate();
assert.ok(f.observed.has(background));f.intersect(background);assert.equal(f.events.filter(e=>e.method==='adImpression').length,3);
f.document.hidden=true;const hidden=advertisement('hidden');hidden.dataset.adId='hidden';const before=f.events.length;
f.tracker.externalAd(hidden,'show');f.intersect(hidden);assert.equal(f.events.length,before);
f.nodes.push(hidden);f.document.hidden=false;f.document.dispatchEvent(new Event('visibilitychange'));
assert.ok(f.observed.has(hidden));f.intersect(hidden);assert.equal(f.events.filter(e=>e.method==='adImpression').length,4);
}finally{f.close();}
});
test('SDK validation errors and denied event codes produce sanitized diagnostics instead of disappearing silently',async()=>{
const f=fixture();try{
f.tracker.configure({reportSdkConfig:'configured-cipher'});await flush();
const broken=new Element();broken.attributes.set('data-track-item','');broken.dataset={itemId:'7',itemType:'short_drama'};
f.intersect(broken);await flush();assert.equal(f.diagnostics[0].reason,'event_validation_failed');assert.ok(f.diagnostics[0].missingFields.includes('item_name'));
f.sdk.track={adImpression:async()=>({ok:false,reason:'event_code_not_allowed',encryptedConfig:'never-log-cipher',payload:{uid:'never-log-uid'}})};
f.tracker.externalAd(advertisement(),'show');await flush();
assert.equal(f.diagnostics.at(-1).reason,'event_code_not_allowed');assert.ok(!JSON.stringify(f.diagnostics).includes('never-log'));
f.tracker.externalAd(advertisement('next'),'show');await flush();assert.equal(f.diagnostics.filter(d=>d.reason==='event_code_not_allowed').length,1);
assert.deepEqual(f.consoleCalls,[]);assert.deepEqual(sdkLogs,[]);
}finally{f.close();}
});
test('bootstrap SDK configuration requests bypass SDK readiness, and delayed config preserves more than 200 queued events',async()=>{
const f=fixture();try{
f.sdk.getCommonFields=()=>{throw Error('configuration must not depend on SDK fields');};
assert.deepEqual(await f.tracker.requestHeaders('config/info','GET',{}),{});
for(let n=0;n<220;n++){const node=advertisement();node.dataset.adId='ad-'+n;f.tracker.externalAd(node,'show');}
f.tracker.externalAd(advertisement('startup-grid'),'click');
f.tracker.configure({reportSdkConfig:'configured-cipher'});await flush();
assert.equal(f.events.filter(e=>e.method==='adImpression').length,221);assert.equal(f.events.filter(e=>e.method==='adClick').length,1);
assert.deepEqual(f.results.filter(result=>!result.ok),[]);
}finally{f.close();}
});
test('all playback behavior and episode status payloads pass the real SDK validator',async()=>{
const f=fixture();try{
f.tracker.configure({reportSdkConfig:'configured-cipher'});await flush();
f.tracker.setDrama(drama,chapters[0],chapters);
const video=new Video(),mode=new Element();mode.dataset.playMode='main';video.parents={'[data-play-mode]':mode};f.setVideo(video);
for(const behavior of ['ep_play','ep_pause','ep_forward','ep_rewind','ep_complete','ep_share','ep_failed'])f.tracker.dramaEvent(behavior,video);
for(const behavior of ['ep_previous','ep_next','ep_switch','ep_unlock','ep_auto_play'])f.tracker.dramaStatusChange(chapters[1],behavior);
await flush();assert.deepEqual(f.results.filter(result=>!result.ok),[]);assert.deepEqual(f.diagnostics,[]);
assert.ok(f.results.some(result=>result.method==='dramaEvent'));assert.ok(f.results.some(result=>result.method==='dramaStatusChange'));
}finally{f.close();}
});
test('SDK loading can recover after the first script was unavailable and a failed init leaves queued events retryable',async()=>{
const f=fixture();try{
delete f.win.WebSDK;let loaded=0;
f.document.createElement=()=>({remove(){}});
f.document.head.append=script=>{loaded++;queueMicrotask(()=>{f.win.WebSDK=f.sdk;script.onload();});};
const initialize=f.sdk.init;let fail=true;
f.sdk.init=options=>{if(fail)throw Error('private-config-must-not-appear');return initialize(options);};
f.tracker.externalAd(advertisement('startup-ad'),'show');f.tracker.configure({reportSdkConfig:'configured-cipher'});await flush();
assert.equal(loaded,1);assert.equal(f.events.length,0);assert.equal(f.diagnostics[0].reason,'sdk_initialization_failed');
fail=false;f.tracker.configure({reportSdkConfig:'configured-cipher'});await flush();
assert.equal(f.events.filter(e=>e.method==='appPageView').length,1);assert.equal(f.events.filter(e=>e.method==='adImpression').length,1);
assert.ok(!JSON.stringify(f.diagnostics).includes('private-config'));
}finally{f.close();}
});
test('SDK initializes once from dynamic config, queues the first PV and tracks each native route once',async()=>{
const f=fixture();try{
f.tracker.configure({});await flush();assert.equal(f.calls.length,0);assert.equal(f.events.length,0);
f.tracker.configure({reportSdkConfig:'configured-cipher'});await flush();
assert.equal(f.calls[0].appId,TRACKER_APP_ID);assert.equal(f.calls[0].encryptedConfig,'configured-cipher');assert.deepEqual(f.calls[0].disableAutoEvents,['app_page_view','page_click']);
assert.equal(f.events.filter(e=>e.method==='appPageView').length,1);
f.tracker.configure({reportSdkConfig:'configured-cipher'});f.tracker.pageView();assert.equal(f.events.length,1);
f.location.pathname='/rank/new/page/2/';f.tracker.pageView();
const pv=f.events.at(-1);assert.equal(pv.payload.page_key,'rank_new');assert.equal(pv.payload.referrer_page_key,'home');assert.equal(f.calls.filter(c=>c==='route').length,1);
f.tracker.syncUser({userId:123456});assert.equal(f.fields.uid,'123456');f.tracker.syncUser(null);assert.equal(f.fields.uid,'');
for(const path of ['/navigation/','/info/about-us/','/member/settings/','/topics/3/','/video/8/','/richtext/关于我们/','/richetext/关于我们/'])assert.notEqual(trackingPage(path).key,'internal_other');
assert.equal(trackingPage('/richtext/关于我们/').key,'rich_text');
const count=f.events.filter(e=>e.method==='appPageView').length;
f.tracker.resumePage();assert.equal(f.events.filter(e=>e.method==='appPageView').length,count+1);
f.tracker.pageView();assert.equal(f.events.filter(e=>e.method==='appPageView').length,count+1,'ordinary bindings must not duplicate the restored page view');
}finally{f.close();}
});
test('ad visibility and clicks deduplicate across DOM replacements, resetting only on a new page',async()=>{
const f=fixture();try{
f.tracker.configure({reportSdkConfig:'configured-cipher'});await flush();
const ad=new Element();ad.attributes.set('data-track-ad','external');ad.dataset={adId:'advertise-code',adSlotKey:'slot',adSlotName:'广告位',adType:'banner',adClickable:'1'};
ad.hidden=true;f.intersect(ad);assert.equal(f.events.filter(e=>e.method==='adImpression').length,0);
ad.hidden=false;f.intersect(ad);f.tracker.externalAd(ad,'show');f.tracker.externalAd(ad,'click');
assert.equal(f.events.filter(e=>e.method==='adImpression').length,1);assert.equal(f.events.filter(e=>e.method==='adClick').length,1);
f.location.pathname='/shorts/';f.tracker.pageView();f.tracker.externalAd(ad,'show');assert.equal(f.events.filter(e=>e.method==='adImpression').length,2);
f.location.pathname='/';f.tracker.pageView();f.tracker.externalAd(ad,'show');assert.equal(f.events.filter(e=>e.method==='adImpression').length,2,'returning to a previously seen tab must not repeat its ads');
}finally{f.close();}
});
test('play durations exclude buffering and preview, speed is scaled, and seek/completion/status use named events',async()=>{
const f=fixture();try{
f.tracker.configure({reportSdkConfig:'configured-cipher'});await flush();
f.tracker.setDrama(drama,chapters[0],chapters);
const video=new Video(),mode=new Element();mode.dataset.playMode='main';video.parents={'[data-play-mode]':mode};f.setVideo(video);
const emit=type=>f.tracker.videoEvent({type,target:video});
emit('play');emit('playing');f.setClock(5000);emit('waiting');f.setClock(15000);emit('playing');f.setClock(18000);video.paused=true;video.currentTime=30;emit('pause');
const pause=f.events.filter(e=>e.payload.play_behavior_key==='ep_pause').at(-1);assert.equal(pause.payload.play_duration,7);assert.equal(pause.payload.play_speed,125);assert.equal(pause.payload.play_progress,30);
const count=f.events.length;mode.dataset.playMode='preview';emit('play');emit('playing');assert.equal(f.events.length,count);mode.dataset.playMode='main';
video.currentTime=20;emit('timeupdate');emit('seeking');video.currentTime=60;emit('seeked');assert.equal(f.events.at(-1).payload.play_behavior_key,'ep_forward');
video.currentTime=100;video.ended=true;emit('ended');assert.equal(f.events.at(-2).payload.play_behavior_key,'ep_complete');assert.equal(f.events.at(-1).payload.play_behavior_key,'ep_play');assert.equal(f.events.at(-1).payload.play_progress,100);
f.tracker.dramaStatusChange(chapters[1]);assert.equal(f.events.at(-1).payload.drama_behavior_key,'ep_next');f.tracker.dramaStatusChange(chapters[1],'ep_unlock');assert.equal(f.events.at(-1).payload.drama_behavior_key,'ep_unlock');
assert.ok(!f.events.some(e=>['dramaLike','dramaCollect','dramaPurchase','dramaComment'].includes(e.method)));
}finally{f.close();}
});
test('request headers preserve SDK values and snapshot the selected purchase chapter independently of route changes',async()=>{
const f=fixture();try{
f.tracker.configure({reportSdkConfig:'configured-cipher'});await flush();f.tracker.setDrama(drama,chapters[0],chapters);
const pending=f.tracker.requestHeaders('playlet/chapter/unlock','POST',{playletId:23456,chapterId:90002});
f.location.pathname='/shorts/';f.tracker.pageView();
const headers=await pending,context=JSON.parse(Buffer.from(headers['X-DC-Request-Context'],'base64url'));
assert.deepEqual(context.common,f.fields);assert.equal(context.drama.drama_id,'23456');assert.equal(context.drama.video_id,'90002');assert.equal(context.drama.episode,2);
assert.equal(context.event_name,'drama_purchase');assert.match(context.request_id,/^[a-f0-9]{32}$/);assert.equal(context.request_id,headers['X-Tracker-Request-Id']);
assert.deepEqual(JSON.parse(Buffer.from(headers['X-SDK-Common'],'base64')),f.fields);
const videoComment=await f.tracker.requestHeaders('comment/send','POST',{objectId:2,objectType:20});assert.equal(videoComment['X-DC-Request-Context'],undefined);
const order=await f.tracker.requestHeaders('coin/buy','POST',{});assert.ok(order['X-SDK-Common']);assert.equal(order['X-Tracker-Event-Name'],undefined);
}finally{f.close();}
});
test('terminal playback errors report failed once per attempt, exclude preview and use the video dictionary',async()=>{
const f=fixture();try{
f.tracker.configure({reportSdkConfig:'configured-cipher'});await flush();
f.tracker.setDrama(drama,chapters[0],chapters);
const video=new Video(),mode=new Element();mode.dataset.playMode='main';video.parents={'[data-play-mode]':mode};f.setVideo(video);
f.tracker.videoEvent({type:'playing',target:video});f.setClock(5000);video.currentTime=30;
f.tracker.playbackFailed(video);f.tracker.playbackFailed(video);
const failed=f.events.filter(e=>e.payload.play_behavior_key==='ep_failed');assert.equal(failed.length,1);assert.equal(failed[0].payload.play_behavior_name,'播放失败');assert.equal(failed[0].payload.play_duration,4);
mode.dataset.playMode='preview';f.tracker.prepareDramaModeChange();f.tracker.playbackFailed(video);assert.equal(f.events.filter(e=>e.payload.play_behavior_key==='ep_failed').length,1);
mode.dataset.playMode='main';f.tracker.prepareDramaModeChange();f.tracker.playbackFailed(video);assert.equal(f.events.filter(e=>e.payload.play_behavior_key==='ep_failed').length,2);
f.tracker.setDrama(drama,null,[],true);assert.equal(f.events.at(-1).payload.video_behavior_name,'视频展示');
f.tracker.prepareDramaModeChange();f.tracker.playbackFailed(video);assert.equal(f.events.at(-1).method,'videoEvent');assert.equal(f.events.at(-1).payload.video_behavior_key,'video_failed');assert.equal(f.events.at(-1).payload.video_behavior_name,'播放失败');
}finally{f.close();}
});
test('completion includes 100 percent play progress while preserving the final playback interval',async()=>{
const f=fixture();try{
f.tracker.configure({reportSdkConfig:'configured-cipher'});await flush();f.tracker.setDrama(drama,chapters[0],chapters);
const video=new Video(),mode=new Element();mode.dataset.playMode='main';video.parents={'[data-play-mode]':mode};f.setVideo(video);
f.tracker.videoEvent({type:'playing',target:video});f.setClock(9000);video.currentTime=100;video.ended=true;f.tracker.videoEvent({type:'ended',target:video});
const [complete,progress]=f.events.slice(-2);assert.equal(complete.payload.play_behavior_key,'ep_complete');assert.equal(progress.payload.play_behavior_key,'ep_play');
assert.equal(complete.payload.play_progress,100);assert.equal(progress.payload.play_progress,100);assert.equal(complete.payload.play_duration,8);assert.equal(progress.payload.play_duration,8);
const count=f.events.length;f.tracker.reportProgress();assert.equal(f.events.length,count,'the final playback interval must be consumed after progress reporting');
}finally{f.close();}
});
test('all SDK public fields reach login and order requests unchanged including numeric timestamps and optional system metadata',async()=>{
const f=fixture();try{
Object.assign(f.fields,{device_brand:'品牌',device_model:'model',system_name:'test-system',system_version:'1.2.3'});
for(const path of ['auth/login','auth/register','coin/buy','vip/pay']){
const headers=await f.tracker.requestHeaders(path,'POST',{}),forwarded=trackingHeaders(new Headers(headers),path,'POST');
assert.deepEqual(JSON.parse(Buffer.from(forwarded['X-SDK-Common'],'base64')),f.fields);assert.equal(forwarded['X-DC-Request-Context'],undefined);
assert.match(headers['X-User-Agent'],/^DevID=business-device;DevType=PC;SysType=h5_pc;Ver=1.0.0;BuildID=web;/);
}
}finally{f.close();}
});
test('the BFF accepts only the correct route, app, common fields and context schema without blocking business data',()=>{
const fields=common(),requestId='1234567890abcdef1234567890abcdef';
const context={v:1,request_id:requestId,event_name:'drama_purchase',common:fields,drama:{media_id:'',drama_id:'23456',episode:2,video_id:'90002',recommend_trace_id:''}};
const source=new Headers({'X-SDK-Common':base64JSON(Object.fromEntries(SDK_COMMON_KEYS.map(k=>[k,fields[k]]))),'X-Tracker-Request-Id':requestId,'X-Tracker-Event-Name':'drama_purchase','X-DC-Request-Context':base64JSON(context,true)});
const forwarded=trackingHeaders(source,'playlet/chapter/unlock','POST');assert.deepEqual(JSON.parse(Buffer.from(forwarded['X-DC-Request-Context'],'base64url')),context);
assert.equal(JSON.parse(Buffer.from(forwarded['X-SDK-Common'],'base64')).sid,'sid-raw');assert.equal(forwarded.Authorization,undefined);
for(const [path,method] of [['playlet/collect/set','POST'],['config/info','GET'],['playlet/chapter/unlock','GET']])assert.equal(trackingHeaders(source,path,method)['X-DC-Request-Context'],undefined);
for(const invalid of [{...context,request_id:'other'},{...context,common:{...fields,app_id:'JHA-1050'}},{...context,common:{...fields,token:'secret'}},{...context,drama:{...context.drama,episode:0}}]){
const headers=new Headers(source);headers.set('X-DC-Request-Context',base64JSON(invalid,true));const result=trackingHeaders(headers,'playlet/chapter/unlock','POST');assert.equal(result['X-DC-Request-Context'],undefined);assert.ok(result['X-SDK-Common']);
}
assert.deepEqual(trackingHeaders(new Headers({'X-SDK-Common':'invalid','X-DC-Request-Context':'x'.repeat(9000)}),'playlet/chapter/unlock','POST'),{});
});