103 lines
6.3 KiB
JavaScript
103 lines
6.3 KiB
JavaScript
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);
|
|
});
|
|
});
|