69 lines
5.8 KiB
JavaScript
69 lines
5.8 KiB
JavaScript
import {test} from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {readFile} from 'node:fs/promises';
|
|
import vm from 'node:vm';
|
|
import ts from 'typescript';
|
|
|
|
const contract=JSON.parse(await readFile(new URL('./fixtures/91av-app-contract.json',import.meta.url),'utf8')).operations;
|
|
const compile=async file=>ts.transpileModule(await readFile(new URL(file,import.meta.url),'utf8'),{compilerOptions:{target:ts.ScriptTarget.ES2022,module:ts.ModuleKind.CommonJS}}).outputText;
|
|
const protocol={},routing={},rendering={},channel={},server={};
|
|
vm.runInNewContext(await compile('../src/lib/api/protocol.ts'),{exports:protocol,URL});
|
|
vm.runInNewContext(await compile('../src/lib/routing.ts'),{exports:routing,URL,URLSearchParams});
|
|
vm.runInNewContext(await compile('../src/lib/client/render.ts'),{exports:rendering,URL,require:key=>key==='../routing'?routing:protocol});
|
|
vm.runInNewContext(await compile('../src/lib/client/channel.ts'),{exports:channel,URL,URLSearchParams,require:key=>key==='../routing'?routing:key==='../api/protocol'?protocol:key==='./render'?rendering:{}});
|
|
vm.runInNewContext(await compile('../src/lib/api/server.ts'),{exports:server,require:key=>key==='react'?{cache:fn=>fn}:protocol});
|
|
const plain=value=>JSON.parse(JSON.stringify(value));
|
|
|
|
test('proxy methods and public endpoints are backed by the reviewed API contract',async()=>{
|
|
for(const path of server.PUBLIC_PATHS)assert.ok(contract['GET '+path],path);
|
|
const code=ts.createSourceFile('server.ts',await readFile(new URL('../src/lib/api/server.ts',import.meta.url),'utf8'),ts.ScriptTarget.Latest,true);
|
|
const walk=node=>{
|
|
if(ts.isStringLiteral(node)&&node.text.includes('/')&&server.allowedRequest(node.text,'POST'))assert.ok(contract['POST '+node.text],node.text);
|
|
ts.forEachChild(node,walk);
|
|
};walk(code);
|
|
for(const method of ['GET','POST'])for(const path of ['video/detail','video/all/list','playlet/operate/list','playlet/home/config/detail/list','statcenter/sync'])assert.equal(server.allowedRequest(path,method),false,method+' '+path);
|
|
});
|
|
|
|
test('removed account, recharge, unlock and login endpoints are no longer proxied',()=>{
|
|
for(const path of ['playlet/like/list','playlet/collect/list','playlet/history/list','playlet/bought/list','user/center/data','coin/trans/list','coin/orders','vip/orders','recharge/package/list'])assert.equal(server.allowedRequest(path,'GET'),false,path);
|
|
for(const path of ['auth/login','auth/register','auth/resetPassword','auth/profile','auth/logout','coin/buy','vip/pay','playlet/chapter/unlock','upload/image','playlet/history/clear','playlet/history/delete','feedback/add'])assert.equal(server.allowedRequest(path,'POST'),false,path);
|
|
assert.equal(server.allowedRequest('auth/guest','POST'),true);
|
|
});
|
|
|
|
test('all public list routes send only supported query fields with their native page numbers',()=>{
|
|
for(const path of ['/shorts/page/2/','/rank/hot/page/2/','/rank/new/page/2/','/rank/follow/page/2/','/topics/page/2/','/topics/10404/page/2/','/theme/10404/page/2/','/tag/6/page/2/','/config/10081/']){
|
|
const route=routing.parseSitePath(path),request=channel.channelRequest(route,'?sort=2'),operation=contract['GET '+request.endpoint];
|
|
assert.ok(operation,path);
|
|
for(const key of Object.keys(request.query))assert.ok(operation.query.includes(key),path+' unsupported '+key);
|
|
for(const key of operation.requiredQuery)assert.notEqual(request.query[key],undefined,path+' missing '+key);
|
|
if(route.key!=='config')assert.equal(request.query.pageNum,2,path);
|
|
}
|
|
assert.deepEqual(plain(channel.channelRequest(routing.parseSitePath('/shorts/'),'?sort=2')),{endpoint:'playlet/all/list',query:{pageNum:1,pageSize:24,sortType:2}});
|
|
});
|
|
|
|
test('shorts random recommendations send sortType 4 and preserve selection and pagination filters',async()=>{
|
|
const route=routing.parseSitePath('/shorts/page/2/'),calls=[];
|
|
const data=await channel.loadChannel({get:async(endpoint,query)=>{calls.push({endpoint,query});return {list:[{id:8,title:'随机短剧'}],totalCount:72};}},route,'?sort=4');
|
|
assert.deepEqual(plain(calls),[{endpoint:'playlet/all/list',query:{pageNum:2,pageSize:24,sortType:4}}]);
|
|
const view=channel.channelView(route,data,'/shorts/page/2/?sort=4');
|
|
assert.ok(view.html.includes('data-sort="4" aria-pressed="true">随机推荐</button>'));
|
|
assert.equal((view.html.match(/aria-pressed="true"/g)||[]).length,1);
|
|
assert.ok(view.html.includes('href="/shorts/page/3/?sort=4"'));
|
|
assert.equal(routing.paginationPath('/shorts/page/2/?sort=2',1,{sort:4}),'/shorts/?sort=4');
|
|
for(const sort of [1,2,3,4])assert.equal(channel.channelRequest(route,'?sort='+sort).query.sortType,sort);
|
|
for(const sort of ['0','5','NaN','1.5'])assert.equal(channel.channelRequest(route,'?sort='+sort).query.sortType,1);
|
|
assert.equal(channel.channelRequest(routing.parseSitePath('/tag/6/'),'?sort=4').query.sortType,1);
|
|
});
|
|
|
|
test('home more fallback uses the exact enabled block without inventing a paginated catalog',async()=>{
|
|
const calls=[];
|
|
const api={get:async(endpoint,query)=>{calls.push({endpoint,query});return {homeConfigData:[{id:1,status:true,name:'正确栏目',type:1,list:[{id:8,title:'唯一短剧'}]},{id:2,status:false,list:[{id:9}]}]};}};
|
|
const first=await channel.loadChannel(api,routing.parseSitePath('/config/1/'));
|
|
assert.equal(first.name,'正确栏目');assert.deepEqual(plain(first.list),[{id:8,title:'唯一短剧'}]);assert.equal(first.totalCount,1);
|
|
const second=await channel.loadChannel(api,routing.parseSitePath('/config/1/page/2/'));
|
|
assert.deepEqual(plain(second.list),[]);assert.equal(second.totalCount,1);
|
|
assert.equal((await channel.loadChannel(api,routing.parseSitePath('/config/2/'))).missing,true);
|
|
assert.ok(calls.every(call=>call.endpoint==='playlet/home'&&Object.keys(call.query).length===0));
|
|
assert.equal(routing.parseSitePath('/series/'),null);assert.equal(routing.parseSitePath('/video/7/'),null);
|
|
});
|