import {routingModuleUrl} from './helpers/routing.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(file,replace=code=>code){ const filename=new URL(file,import.meta.url); const {outputText}=ts.transpileModule(await readFile(filename,'utf8'),{compilerOptions:{target:ts.ScriptTarget.ES2022,module:ts.ModuleKind.ESNext}}); return 'data:text/javascript;base64,'+Buffer.from(replace(outputText).replace(/(['"])\.\.\/routing\1/g,JSON.stringify(routingModuleUrl))+'\n//# sourceURL='+filename.pathname).toString('base64'); } const protocolUrl=await moduleUrl('../src/lib/api/protocol.ts'); const authUrl=await moduleUrl('../src/lib/client/auth.ts'); let sequence=0; async function client(){ const url=await moduleUrl('../src/lib/client/api.ts',code=>code.replace('"../api/protocol"',JSON.stringify(protocolUrl)).replace('"./auth"',JSON.stringify(authUrl+'#'+sequence++))); return new (await import(url)).ApiClient(); } const guest={userId:7,type:0,isRegister:false}; const other={userId:8,type:0,isRegister:false}; const response=(data,code=200,headers={})=>new Response(JSON.stringify({code,data,msg:code===200?'ok':'session expired'}),{status:code===1000?401:200,headers:{'Content-Type':'application/json',...headers}}); const deferred=()=>{let resolve;const promise=new Promise(done=>resolve=done);return {promise,resolve};}; async function withFetch(fetcher,work){const old=globalThis.fetch;globalThis.fetch=fetcher;try{await work();}finally{globalThis.fetch=old;}} test('device identity, first channel and invite survive page navigation without persisting credentials',async()=>{ const originals=Object.fromEntries(['document','localStorage','location'].map(key=>[key,Object.getOwnPropertyDescriptor(globalThis,key)])); const storage=new Map([['uuid','old-device']]); Object.defineProperty(globalThis,'document',{configurable:true,value:{cookie:'drama_device=cookie-device'}}); Object.defineProperty(globalThis,'localStorage',{configurable:true,value:{getItem:key=>storage.get(key),setItem:(key,value)=>storage.set(key,value)}}); Object.defineProperty(globalThis,'location',{configurable:true,value:{search:'?dc=first&inviteCode=INV123'}}); try{ const {authMetadata}=await import(authUrl+'#attribution'); assert.deepEqual(authMetadata(),{devID:'cookie-device',deviceId:'cookie-device',channel:'first',agentInviteCode:'INV123'}); globalThis.location.search='?dc=later'; assert.equal(authMetadata().channel,'first');assert.equal(authMetadata().agentInviteCode,'INV123'); assert.deepEqual([...storage.keys()].sort(),['channel','uuid']); globalThis.localStorage.getItem=()=>{throw new Error('blocked');}; globalThis.localStorage.setItem=()=>{throw new Error('blocked');}; globalThis.document.cookie=''; const fallback=(await import(authUrl+'#blocked')).authMetadata; assert.equal(fallback().devID,fallback().devID); }finally{for(const [key,descriptor] of Object.entries(originals)){if(descriptor)Object.defineProperty(globalThis,key,descriptor);else delete globalThis[key];}} }); test('concurrent private requests share one guest initialization and public data needs no session',async()=>{ const api=await client(),gate=deferred(),calls=[]; await withFetch(async(url,options)=>{ calls.push({url,body:options.body&&JSON.parse(options.body)}); if(url.endsWith('/auth/guest')){await gate.promise;return response(guest);} return response({}); },async()=>{ const first=api.get('comment/list'),second=api.get('playlet/detail'); await Promise.resolve();assert.equal(calls.length,1);gate.resolve();await Promise.all([first,second]); assert.equal(calls.filter(call=>call.url.endsWith('/auth/guest')).length,1); assert.ok(calls[0].body.devID);assert.equal(api.user.userId,7); const anonymous=await client();await anonymous.get('playlet/home');assert.equal(anonymous.user,null); }); }); test('history restoration revalidates current cookies and cannot overwrite a newer session',async()=>{ const api=await client();api.seedSession(guest); await withFetch(async()=>response(null),async()=>{await api.revalidateSession();assert.equal(api.user,null);}); const gate=deferred();api.seedSession(guest); await withFetch(async()=>{await gate.promise;return response(guest);},async()=>{ const refresh=api.revalidateSession();await Promise.resolve();api.setUser(other);gate.resolve();await refresh;assert.equal(api.user.userId,other.userId); }); }); test('an expired session is replaced by a fresh guest on the next private request and never prompts for login',async()=>{ const api=await client();api.seedSession(guest);const calls=[];let expired=true; await withFetch(async url=>{ calls.push(url); if(url.endsWith('/auth/guest'))return response(other); if(expired){expired=false;return response(null,1000);} return response({list:[]}); },async()=>{ await assert.rejects(api.get('comment/list',{objectId:1}));assert.equal(api.user,null); await api.get('comment/list',{objectId:1});assert.equal(api.user.userId,other.userId); assert.deepEqual(calls.map(url=>url.split('?')[0]),['/api/comment/list','/api/auth/guest','/api/comment/list']); }); }); test('search and suggestions wait for a shared guest session before requesting protected search endpoints',async()=>{ const api=await client(),gate=deferred(),calls=[]; await withFetch(async url=>{ calls.push(url); if(url.endsWith('/auth/guest')){await gate.promise;return response(guest);} return response({list:[],totalCount:0}); },async()=>{ const search=api.get('playlet/search',{keyword:'都市',pageNum:2,pageSize:25}); const suggestions=api.get('playlet/search/suggest',{keyword:'都市'}); await Promise.resolve();assert.deepEqual(calls,['/api/auth/guest']); gate.resolve();await Promise.all([search,suggestions]); assert.equal(calls.length,3);assert.ok(calls[1].includes('playlet/search?'));assert.ok(calls[1].includes('pageSize=25'));assert.ok(calls[2].includes('playlet/search/suggest?')); }); }); test('a server-prepared session is reused without another guest handshake',async()=>{ await withFetch(()=>assert.fail('prepared sessions must not refetch auth'),async()=>{ const ready=await client();ready.seedSession(other);assert.equal((await ready.initializeSession()).userId,other.userId); }); });