Files
2026-10-16 14:10:10 +07:00

87 lines
8.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { createContext } from '../src/server/context';
import { buildPage, matchPageRoute } from '../src/server/pages';
import { list } from '../src/server/pages/catalog';
import { pagination } from '../src/server/pages/pagination';
import { pageRoutes } from '../src/server/pages/routes';
import { play } from '../src/server/pages/playback';
import { applyLegacyContract } from '../src/server/services/contracts';
import { choose, normalizePayload, playURL, positive, profileImage, videoURL } from '../src/server/services/model';
const ctx = (path: string) => {
const context=createContext(new Request('http://localhost'+path,{headers:{cookie:'token=test-session; isRegister=1'}}));
context.user={userId:7,accountNo:'member',recharge:12345,isRegister:true,type:1};context.state.set('auth:fresh',true);return context;
};
test('legacy route families retain exact dynamic parameters and reject repeated slashes',()=>{
const routes:[string,string,Record<string,string>][]=[
['/','Index',{}],['/list/drama/2/3/page/4/','List',{theme:'drama',updateStatus:'2',sortType:'3',page:'4'}],
['/series/anime/12/2/page/3/','Series',{media:'anime',topic:'12',sortType:'2',page:'3'}],
['/more-list/recommend/1/drama/page/2/','MoreList',{configId:'recommend',timeRange:'1',theme:'drama',page:'2'}],
['/more-series-list/recommend/page/2/','MoreSeriesList',{configId:'recommend',page:'2'}],
['/topic/sort/2/page/3/','Topic',{sortType:'2',page:'3'}],
['/topic-detail/title/sort/3/page/2/','TopicDetail',{slug:'title',sortType:'3',page:'2'}],
['/tags/tag/sort/2/page/4/','Tags',{slug:'tag',sortType:'2',page:'4'}],
['/search/%E7%9F%AD%E5%89%A7/page/2/','Search',{q:'短剧',page:'2'}],
['/play/drama/100/recommendations/page/2/','Play',{slug:'drama',ep:'100',guessPage:'2'}],
['/series-play/video/recommendations/page/2/','SeriesPlay',{slug:'video',guessPage:'2'}],
['/popular-recommend/3/page/2/','PopularRecommend',{rankType:'3',page:'2'}],
['/rank/2/page/3/','Rank',{sortOrder:'2',page:'3'}],['/recently-added/page/3/','RecentlyAdded',{page:'3'}],
];
for(const [path,handler,params]of routes)assert.deepEqual(matchPageRoute(path),{handler,params},path);
for(const path of ['/favorites/chapter/page/3/','/history/page/2/','/purchased/page/2/','/profile/','/setting/','/recharge/','/login/','/register/'])assert.equal(matchPageRoute(path),null,path);
for(const path of ['/list//','//list/','/settings//','/list/theme/extra/','/list/drama/2/3/page/2/extra/','/series/anime/'])assert.equal(matchPageRoute(path),null,path);
});
test('the 58 surviving original public HTML route registrations resolve with unchanged parameter names',()=>{
const removedAccountHandlers=new Set(['Profile','History','Favorites','PurchasedSeries','Setting','Recharge','LoginPage','RegisterPage']);
const legacy=JSON.parse(readFileSync(new URL('./fixtures/legacy-page-routes.json',import.meta.url),'utf8')) as [string,string][];
assert.equal(legacy.length,71);const original=legacy.filter(([,handler])=>!removedAccountHandlers.has(handler));assert.equal(original.length,58);
assert.deepEqual(pageRoutes.filter(([,handler])=>handler!=='HomeRoad'),original);assert.equal(pageRoutes.some(([,handler])=>removedAccountHandlers.has(handler)),false);
assert.deepEqual(pageRoutes.filter(([,handler])=>handler==='HomeRoad'),[['/home-road/','HomeRoad']]);
for(const path of ['/home-road/','/return-home/'])assert.deepEqual(matchPageRoute(path),{handler:'HomeRoad',params:{}},path);
const values:Record<string,string>={q:'短剧',theme:'drama',slug:'drama',configId:'recommend',media:'anime',topic:'12',updateStatus:'2',sortType:'3',sortOrder:'2',rankType:'3',timeRange:'1',page:'2',guessPage:'2',ep:'100',filter:'chapter'};
for(const [pattern,handler]of original){const params:Record<string,string>={};const path=pattern.replace(/:(\w+)/g,(_,name)=>{params[name]=values[name];return encodeURIComponent(values[name]);});assert.deepEqual(matchPageRoute(path),{handler,params},pattern);}
});
test('pagination keeps source zero fields and route numbers use decimal integer parsing',()=>{
const small=pagination(n=>'/page/'+n+'/',2,25,0);assert.equal(small.Page,0);assert.equal(small.TotalPages,0);assert.equal(small.Visible,false);assert.equal(small.Items,null);
const many=pagination(n=>'/page/'+n+'/',50,25,250);assert.equal(many.Page,10);assert.equal(many.HasNext,false);assert.equal(many.PrevURL,'/page/9/');assert.equal(many.Items.at(-1).Active,true);
assert.equal(choose('+2',[2,3],1),2);for(const value of ['2.0','2e0','0x2'])assert.equal(choose(value,[2,3],1),1);assert.equal(positive('+2'),2);
});
test('API model aliases preserve JSON fields, latest mutable values and chapter gates',()=>{
const raw=normalizePayload({id:10,videoUrl:'signed-media.m3u8',seoInfo:'{"webTitle":"页面","webKeyworld":"a,b;a"}',payType:1,needUnlock:true,costCoins:25});
assert.equal(raw.ID,10);assert.equal(raw.VideoURL,'signed-media.m3u8');assert.deepEqual(raw.seoInfo.keywords,['a','b']);assert.equal(raw.NeedCoinGate,true);assert.equal(raw.AllowWatch,false);assert.equal(raw.UnlockCost,25);
raw.seoInfo.title='新标题';assert.equal(raw.seoInfo.Title,'新标题');assert.equal(JSON.stringify(raw).includes('"VideoURL"'),false);
});
test('missing upstream fields retain legacy Go zero values rather than blank attributes',()=>{
const detail=normalizePayload(applyLegacyContract('/api/app/playlet/detail',{id:10,playletChapterInfo:[{id:100}]}));
assert.equal(detail.CountLike,0);assert.equal(detail.IsLike,0);assert.equal(detail.Preview,'');assert.equal(detail.ThemeNames,null);assert.equal(detail.PlayletChapterInfo[0].MediaTime,0);assert.equal(detail.PlayletChapterInfo[0].NeedUnlock,false);assert.equal(detail.PlayletChapterInfo[0].AllowWatch,true);
});
test('list and playback page loaders preserve filters, canonical paths and locked media',async()=>{
const originalFetch=globalThis.fetch,originalUpstream=process.env.UPSTREAM_API_URL;process.env.UPSTREAM_API_URL='http://page-contract-test';const calls:{url:URL;headers:Headers}[]=[];
globalThis.fetch=async(input,init)=>{
const url=new URL(String(input));calls.push({url,headers:new Headers(init?.headers)});let data:any={list:[],totalCount:0};
if(url.pathname==='/api/app/theme/list')data={list:[{id:12,slug:'romance',name:'爱情',status:true,rank:1}]};
if(url.pathname==='/api/app/playlet/all/list')data={list:[{id:10,title:'剧'}],totalCount:100,pageNum:Number(url.searchParams.get('pageNum')),pageSize:Number(url.searchParams.get('pageSize')),seoInfo:{title:'全部短剧'}};
if(url.pathname==='/api/app/playlet/detail')data={id:10,slug:'drama',playName:'剧名',desc:'剧情介绍',themeIds:[12],themeNames:['爱情'],tagIds:[1],tagNames:['甜宠'],totalEpisode:2,preview:'preview.m3u8',playletChapterInfo:[{id:101,currentEpisode:1,payType:0},{id:102,currentEpisode:2,payType:1,needUnlock:true,costCoins:20}],seoInfo:{keywords:['剧名']}};
if(url.pathname==='/api/app/playlet/chapter/detail')data={id:102,currentEpisode:2,payType:1,needUnlock:true,canWatch:false,videoUrl:'protected.m3u8',costCoins:20,balance:12345,mediaTime:60};
if(url.pathname==='/api/app/recharge/package/list')data={vipList:[],coinList:[]};
return Response.json({code:200,data});
};
try{
const listed=await list(ctx('/list/12/2/3/page/2/'),{theme:'12',updateStatus:'2',sortType:'3',page:'2'});
assert.equal(listed.themeId,12);assert.equal(listed.themeSlug,'romance');assert.equal(listed.canonicalPath,'/list/romance/2/3/page/2/');assert.equal(listed.pagination.NextURL,'/list/romance/2/3/page/3/');assert.equal(listed.pageHeading,'爱情短剧');
const catalogue=calls.find(call=>call.url.pathname==='/api/app/playlet/all/list')!;assert.equal(catalogue.url.searchParams.get('themeSlug'),'romance');assert.equal(catalogue.headers.has('authorization'),false);
const played=await play(ctx('/play/drama/102/'),{slug:'drama',ep:'102'});assert.ok(played);assert.equal(played.chapterID,102);assert.equal(played.episode,2);assert.equal(played.canWatch,false);assert.equal(played.mainURL,'');assert.equal(played.previewURL,'preview.m3u8');assert.equal(played.balanceText,'12,345');assert.equal(played.seoInfo.title,'剧名 - 第二集');assert.equal(played.canonicalPath,'/drama/drama/');assert.equal(played.comments,undefined);assert.ok(!calls.some(call=>call.url.pathname==='/api/app/comment/list'));
const chapter=calls.find(call=>call.url.pathname==='/api/app/playlet/chapter/detail')!;assert.equal(chapter.url.searchParams.get('chapterId'),'102');assert.equal(chapter.headers.get('authorization'),'test-session');
assert.equal(await play(ctx('/play/drama/999/'),{slug:'drama',ep:'999'}),null);
for(const path of ['/search/?q=legacy','/search/?page=2','/play/drama/?guessPage=2','/favorites/?filter=all','/favorites/invalid/','/history/page/10001/','/settings//'])assert.equal((await buildPage(ctx(path))).status,404,path);
}finally{globalThis.fetch=originalFetch;if(originalUpstream===undefined)delete process.env.UPSTREAM_API_URL;else process.env.UPSTREAM_API_URL=originalUpstream;}
});