整体UI修改
This commit is contained in:
+110
-311
@@ -112,7 +112,7 @@ before(async()=>{
|
||||
else if(request.headers.authorization==='http-expired'){response.writeHead(401,{'Content-Type':'application/json'});response.end(JSON.stringify({code:401,data:null}));return;}
|
||||
else if(request.headers.authorization==='registered-user-a-secret'||request.headers.authorization==='registered-user-b-secret'){const first=request.headers.authorization==='registered-user-a-secret';data={userId:first?101:202,nickName:first?'用户甲':'用户乙',recharge:first?111:222,type:1,isRegister:true,token:request.headers.authorization,password:'must-not-leak'};}
|
||||
else data={userId:77,nickName:'接口昵称',token:'must-not-leak',recharge:90};
|
||||
}else if(path==='config/info')data={seoInfo:{webSiteName:'91短剧'},...seoConfig,config:{foreverUrlAddr:'https://default.drama.test',telegram:'https://example.com/contact',tgGroup:'https://example.com/group',twitterAddr:'https://example.com/twitter',officialAppAddr:downloadAddress,...seoConfig.config},navPageList:Object.hasOwn(seoConfig,'navPageList')?seoConfig.navPageList:[{line:2,url:'https://example.com/entry-two'},{line:1,url:'https://example.com/entry-one'},{line:3,url:'javascript:alert(1)'}]};
|
||||
}else if(path==='config/info')data={seoInfo:{webSiteName:'黄果吃瓜'},...seoConfig,config:{foreverUrlAddr:'https://huangguocg.com',telegram:'https://example.com/contact',tgGroup:'https://example.com/group',twitterAddr:'https://example.com/twitter',officialAppAddr:downloadAddress,...seoConfig.config},navPageList:Object.hasOwn(seoConfig,'navPageList')?seoConfig.navPageList:[{line:2,url:'https://example.com/entry-two'},{line:1,url:'https://example.com/entry-one'},{line:3,url:'javascript:alert(1)'}]};
|
||||
else if(path==='navigation/header'){data=headerPayload;if(headerFailure)code=500;}
|
||||
else if(path==='config/footer/info'&&footerPayload)data=footerPayload;
|
||||
else if(path==='translation/category/list')data={categoryList:categories};
|
||||
@@ -156,15 +156,15 @@ before(async()=>{
|
||||
|
||||
after(async()=>{await stopNext();if(mock)await new Promise(resolve=>mock.close(resolve));if(seoSnapshotPath)await rm(seoSnapshotPath,{force:true});});
|
||||
|
||||
test('a cold configuration failure never uses request/forwarded hosts as canonical or publisher URLs',async()=>{
|
||||
test('a cold configuration failure never uses request/forwarded hosts as canonical or publisher URLs, only the site domain',async()=>{
|
||||
try{
|
||||
configFailure='http';
|
||||
const response=await fetch(base+'/info/about-us/',{headers:{'X-Forwarded-Host':'untrusted.entry.test','X-Forwarded-Proto':'https'}});
|
||||
assert.equal(response.status,200);
|
||||
const html=await response.text(),head=html.match(/<head>([\s\S]*?)<\/head>/)[1];
|
||||
assert.ok(!head.includes('rel="canonical"'));assert.ok(!head.includes('property="og:url"'));
|
||||
assert.ok(!head.includes('untrusted.entry.test'));assert.ok(!/<script\b[^>]*data-site-json-ld=/.test(html));
|
||||
assert.match(head,/<title>关于我们 - 91短剧<\/title>/);
|
||||
assert.match(head,/rel="canonical" href="https:\/\/[^"]+\/info\/about-us\/"/);
|
||||
assert.ok(!head.includes('untrusted.entry.test'));
|
||||
assert.match(head,/<title>关于我们 - 黄果吃瓜<\/title>/);
|
||||
}finally{configFailure='';}
|
||||
});
|
||||
|
||||
@@ -179,7 +179,7 @@ test('robots.txt serves crawler text without sessions and advertises the configu
|
||||
assert.match(body,/User-Agent: \*/i);assert.ok(!body.includes('<html'));assert.ok(!body.includes('untrusted.example'));
|
||||
if(testEnvironment==='production'){
|
||||
assert.match(body,/^Allow: \/$/m);assert.match(body,/^Disallow: \/api\/$/m);assert.match(body,/^Disallow: \/healthz$/m);
|
||||
assert.match(body,/^Sitemap: https:\/\/default\.drama\.test\/sitemap\.xml$/m);
|
||||
assert.match(body,/^Sitemap: https:\/\/huangguocg\.com\/sitemap\.xml$/m);
|
||||
assert.ok(!/^Disallow: \/$/m.test(body));
|
||||
}else {assert.match(body,/^Disallow: \/$/m);assert.ok(!body.includes('Sitemap:'));}
|
||||
}
|
||||
@@ -187,17 +187,17 @@ test('robots.txt serves crawler text without sessions and advertises the configu
|
||||
assert.ok(reads.every(call=>call.path==='/api/app/config/info'&&!call.auth),'only public site configuration is needed');
|
||||
});
|
||||
|
||||
test('robots sitemap uses permanent-domain precedence, survives configuration failure and never guesses a host',async()=>{
|
||||
test('robots sitemap always uses the site domain, survives configuration failure and never guesses a host',async()=>{
|
||||
if(testEnvironment!=='production')return;
|
||||
try{
|
||||
seoConfig={config:{foreverUrlAddr:'https://robots.drama.test/nested/?source=1'},newAddressInfo:{foreverUrlAddr:'https://backup.drama.test'}};
|
||||
seoConfig={config:{foreverUrlAddr:'https://huangguocg.com/nested/?source=1'},newAddressInfo:{foreverUrlAddr:'https://backup.drama.test'}};
|
||||
let response=await fetch(base+'/robots.txt');assert.equal(response.status,200);
|
||||
assert.match(await response.text(),/^Sitemap: https:\/\/robots\.drama\.test\/sitemap\.xml$/m);
|
||||
assert.match(await response.text(),/^Sitemap: https:\/\/huangguocg\.com\/sitemap\.xml$/m);
|
||||
configFailure='http';
|
||||
response=await fetch(base+'/robots.txt',{headers:{'X-Forwarded-Host':'fake.test'}});assert.equal(response.status,200);
|
||||
assert.match(await response.text(),/^Sitemap: https:\/\/robots\.drama\.test\/sitemap\.xml$/m);
|
||||
assert.match(await response.text(),/^Sitemap: https:\/\/huangguocg\.com\/sitemap\.xml$/m);
|
||||
configFailure='';seoConfig={config:{foreverUrlAddr:''},newAddressInfo:{foreverUrlAddr:'https://backup.drama.test/base/'}};
|
||||
response=await fetch(base+'/robots.txt');assert.match(await response.text(),/^Sitemap: https:\/\/backup\.drama\.test\/sitemap\.xml$/m);
|
||||
response=await fetch(base+'/robots.txt');assert.match(await response.text(),/^Sitemap: https:\/\/huangguocg\.com\/sitemap\.xml$/m);
|
||||
}finally{configFailure='';seoConfig={};}
|
||||
});
|
||||
|
||||
@@ -218,8 +218,9 @@ test('SSR uses upstream content, escapes HTML, and does not restore homepage fil
|
||||
const response=await fetch(base);const html=await response.text();
|
||||
assert.equal(response.status,200);
|
||||
assert.equal((html.match(/<h1\b/g)||[]).length,1,'only the homepage title may use h1, including hidden templates');
|
||||
assert.match(html,/<h1 id="home-title" class="home-page-title">91短剧 · 百万大神原创短剧<\/h1>/);
|
||||
assert.match(html,/<h2>接口剧名 <script>alert\(1\)<\/script><\/h2>/);
|
||||
assert.match(html,/<h1 id="home-title" class="home-page-title">黄果吃瓜 · 百万大神原创短剧<\/h1>/);
|
||||
assert.match(html,/<h2 class="hg-hero-title"><a href="\/detail\/23456\/">接口剧名 <script>alert\(1\)<\/script><\/a><\/h2>/);
|
||||
assert.match(html,/<ol class="hg-hero-list" aria-label="今日推荐剧集"><li><a href="\/detail\/23456\/" data-feature="0" aria-current="true">/);
|
||||
assert.ok(html.includes('后台配置标题'));
|
||||
assert.ok(html.includes('接口剧名 <script>'));
|
||||
assert.ok(!html.includes('<script>alert(1)</script>'));
|
||||
@@ -268,13 +269,13 @@ test('/navigation renders direct download links without a hover QR panel',async(
|
||||
const before=calls.length;
|
||||
const response=await fetch(base+'/navigation/',{redirect:'manual'});const html=await response.text();
|
||||
assert.equal(response.status,200);assert.equal(response.headers.get('location'),null);
|
||||
assert.ok(html.includes('<title>官方入口 - 91短剧</title>'));
|
||||
assert.ok(html.includes('<title>官方入口 - 黄果吃瓜</title>'));
|
||||
assert.ok(html.includes('<div id="entry-navigation" class="navigation-page">'));
|
||||
assert.ok(!html.includes('<dialog id="entry-navigation"'));
|
||||
assert.ok(html.includes('91短剧 · 线路一'));assert.ok(html.includes('91短剧 · 线路二'));assert.ok(!html.includes('91短剧 · 线路三'));
|
||||
assert.ok(html.includes('href="https://example.com/entry-one"'));assert.ok(html.includes('href="https://example.com/entry-two"'));
|
||||
assert.ok(html.includes('黄果吃瓜 · 线路一'));assert.ok(!html.includes('黄果吃瓜 · 线路二'));
|
||||
assert.ok(html.includes('class="entry-route" href="https://huangguocg.com/"'));assert.ok(!html.includes('href="https://example.com/entry-one"'));assert.ok(!html.includes('href="https://example.com/entry-two"'));
|
||||
assert.ok(!html.includes('href="javascript:'));
|
||||
assert.ok(html.includes('href="https://example.com/contact"'));assert.ok(html.includes('href="https://example.com/group"'));
|
||||
assert.ok(html.includes('官方TG:@jbd125'));assert.ok(html.includes('href="https://t.me/jbd125"'));assert.ok(!html.includes('href="https://example.com/contact"'));assert.ok(!html.includes('href="https://example.com/group"'));
|
||||
assert.ok(html.includes('href="/home-road/"'));assert.ok(html.includes('href="/apps/"'));
|
||||
assert.match(html,/<a class="entry-download" href="https:\/\/example.com\/download" target="_blank" rel="sponsored nofollow noopener noreferrer" data-app-download=""/);
|
||||
assert.match(html,/<a class="entry-channel" href="https:\/\/example.com\/download" target="_blank" rel="sponsored nofollow noopener noreferrer" data-app-download=""/);
|
||||
@@ -286,31 +287,32 @@ test('/navigation renders direct download links without a hover QR panel',async(
|
||||
assert.equal((await fetch(base+'/navigation/',{redirect:'manual',headers:{'Cache-Control':'no-cache'}})).status,200);
|
||||
});
|
||||
|
||||
test('/navigation renders configured identity, routes, permanent entry and channels in its initial HTML',async()=>{
|
||||
test('/navigation uses the fixed name and site domain with configured description and channels in its initial HTML',async()=>{
|
||||
try{
|
||||
seoConfig={seoNavInfo:{dhSiteName:'动态导航站',dhDescription:'后端配置的导航介绍'},config:{telegram:'@configured_contact',tgGroup:'@configured_group',twitterAddr:'https://x.com/configured_91',foreverUrlAddr:'https://permanent.drama.test/entry'},navPageList:[{line:4,url:'https://four.drama.test'},{line:2,url:'https://two.drama.test'}]};
|
||||
const start=calls.length,response=await fetch(base+'/navigation/'),html=await response.text();
|
||||
assert.equal(response.status,200);assert.ok(html.includes('动态导航站官方入口'));assert.ok(html.includes('后端配置的导航介绍'));
|
||||
assert.ok(html.includes('动态导航站 · 线路二'));assert.ok(html.includes('动态导航站 · 线路四'));assert.ok(!html.includes('动态导航站 · 线路一'));
|
||||
assert.ok(html.indexOf('动态导航站 · 线路二')<html.indexOf('动态导航站 · 线路四'));
|
||||
assert.match(html,/<a class="entry-permanent" href="https:\/\/permanent.drama.test\/entry" rel="sponsored nofollow noopener noreferrer"/);
|
||||
for(const url of ['https://t.me/configured_contact','https://t.me/configured_group','https://x.com/configured_91'])assert.ok(html.includes(`href="${url}"`));
|
||||
assert.equal(response.status,200);assert.ok(html.includes('黄果吃瓜官方入口'));assert.ok(!html.includes('动态导航站官方入口'));assert.ok(!html.includes('动态导航站 · '));assert.ok(!html.match(/<title>[^<]*<\/title>/)[0].includes('动态导航站'));assert.ok(html.includes('后端配置的导航介绍'));
|
||||
assert.ok(html.includes('黄果吃瓜 · 线路一'));assert.ok(!html.includes('黄果吃瓜 · 线路二'));for(const host of ['permanent.drama.test','two.drama.test','four.drama.test'])assert.ok(!html.includes('href="https://'+host),host);
|
||||
assert.match(html,/<a class="entry-permanent" href="https:\/\/huangguocg.com\/" rel="sponsored nofollow noopener noreferrer"/);
|
||||
for(const url of ['https://t.me/configured_contact','https://t.me/configured_group','https://x.com/configured_91'])assert.ok(!html.includes(`href="${url}"`),url);
|
||||
assert.ok(html.includes('class="entry-contact" href="https://t.me/jbd125"'));assert.ok(!html.includes('官方推特'));
|
||||
assert.ok(!html.includes('huangguoshortdrama'));assert.ok(!html.includes('huangguodrama'));
|
||||
assert.deepEqual(calls.slice(start).map(call=>call.path),['/api/app/config/info']);
|
||||
}finally{seoConfig={};}
|
||||
});
|
||||
|
||||
test('/navigation omits unconfigured routes and contacts instead of showing homepage links or demo accounts',async()=>{
|
||||
test('/navigation keeps the site entry and official TG but omits unconfigured downloads',async()=>{
|
||||
const saved=downloadAddress;
|
||||
try{
|
||||
downloadAddress='';
|
||||
seoConfig={navPageList:null,config:{foreverUrlAddr:'',telegram:'',telegramAdd:'',tgGroup:'',twitterAddr:''}};
|
||||
const response=await fetch(base+'/navigation/'),html=await response.text();
|
||||
assert.equal(response.status,200);assert.ok(html.includes('暂未配置访问线路'));
|
||||
for(const className of ['entry-route','entry-permanent','entry-contact','entry-download'])assert.ok(!html.includes(`class="${className}"`));
|
||||
assert.equal(response.status,200);assert.ok(!html.includes('暂未配置访问线路'));
|
||||
for(const className of ['entry-route','entry-permanent'])assert.ok(html.includes(`class="${className}" href="https://huangguocg.com/"`),className);
|
||||
assert.ok(html.includes('class="entry-contact" href="https://t.me/jbd125"'));assert.ok(!html.includes('class="entry-download"'));
|
||||
for(const label of ['官方TG群','官方推特'])assert.ok(!html.includes(label));
|
||||
assert.ok(html.includes('href="/home-road/"'));assert.ok(html.includes('href="/apps/"'));
|
||||
assert.ok(html.includes('grid-template-columns:repeat(2, minmax(0, 1fr))'));
|
||||
assert.ok(html.includes('grid-template-columns:repeat(3, minmax(0, 1fr))'));
|
||||
}finally{seoConfig={};downloadAddress=saved;}
|
||||
});
|
||||
|
||||
@@ -328,8 +330,9 @@ test('/navigation omits every download entry when no valid App URL is configured
|
||||
}finally{downloadAddress=saved;}
|
||||
});
|
||||
|
||||
test('all clean page URLs support direct access and refresh',async()=>{
|
||||
const paths=['/shorts/','/topics/','/rank/','/apps/','/home-road/','/topics/3/','/theme/8/','/tag/9/','/config/31/','/detail/23456/','/play/23456/90001/','/search/'+encodeURIComponent('都市 / 爱情?#%')+'/','/member/center/','/member/favorites/','/member/history/','/member/likes/','/member/purchased/','/member/settings/','/info/deposit/','/info/deposit/vip/','/info/user-center/','/info/settings/','/info/about-us/','/info/user-agreement/','/info/privacy-policy/','/info/help-center/','/info/2257/','/info/dmca/','/info/feedback/'];
|
||||
test('all clean page URLs support direct access and refresh, while removed account and recharge pages are 404',async()=>{
|
||||
for(const path of ['/member/center/','/member/history/','/info/deposit/','/info/deposit/vip/','/info/user-center/','/info/settings/'])assert.equal((await fetch(base+path,{redirect:'manual'})).status,404,path);
|
||||
const paths=['/shorts/','/topics/','/rank/','/apps/','/home-road/','/topics/3/','/theme/8/','/tag/9/','/config/31/','/detail/23456/','/play/23456/90001/','/search/'+encodeURIComponent('都市 / 爱情?#%')+'/','/info/about-us/','/info/user-agreement/','/info/privacy-policy/','/info/help-center/','/info/2257/','/info/dmca/'];
|
||||
for(const path of paths){
|
||||
const response=await fetch(new URL(path,base+'/'),{redirect:'manual'});
|
||||
assert.equal(response.status,200,path);
|
||||
@@ -349,7 +352,7 @@ test('all clean page URLs support direct access and refresh',async()=>{
|
||||
|
||||
test('page slash redirects preserve encoded paths and queries without requesting upstream content',async()=>{
|
||||
const keyword=encodeURIComponent('都市 / 爱情?#% example.com');
|
||||
const paths=['/navigation','/search','/search/'+keyword,'/search/'+keyword+'/page/2','/rank/hot','/rank/new','/rank/follow','/info/deposit/vip','/shorts/page/2'];
|
||||
const paths=['/navigation','/search','/search/'+keyword,'/search/'+keyword+'/page/2','/rank/hot','/rank/new','/rank/follow','/shorts/page/2'];
|
||||
const query='?channel=a%2Fb+1&sort=2&source=x%252Fy';
|
||||
const before=calls.length;
|
||||
for(const path of paths){
|
||||
@@ -383,11 +386,11 @@ test('empty and paginated search entries retain native paths and keyword-specifi
|
||||
test('fixed information pages retain local article markup and only load shared head configuration in SSR',async()=>{
|
||||
for(const [key,title,marker] of [
|
||||
['about-us','关于我们','品牌介绍'],['user-agreement','用户协议','关于《使用条款》'],['privacy-policy','隐私政策','八、问题和建议'],
|
||||
['help-center','常见问题','<details open>'],['2257','2257 合规声明','资料留存'],['dmca','DMCA 版权声明','mailto:huangguoaiai@gmail.com'],
|
||||
['help-center','常见问题','<details open>'],['2257','2257 合规声明','资料留存'],['dmca','DMCA 版权声明','href="https://t.me/jbd125"'],
|
||||
]){
|
||||
const count=calls.length,response=await fetch(base+`/info/${key}/`,{redirect:'manual'});
|
||||
assert.equal(response.status,200);assert.equal(response.headers.get('location'),null);
|
||||
const html=await response.text();assert.ok(html.includes(`<title>${title} - 91短剧</title>`));assert.ok(html.includes(marker),key);
|
||||
const html=await response.text();assert.ok(html.includes(`<title>${title} - 黄果吃瓜</title>`));assert.ok(html.includes(marker),key);
|
||||
const main=html.match(/<main\b[^>]*\bid="info-page"[^>]*>/)?.[0];assert.ok(main);assert.ok(main.includes(`data-info="${key}"`));assert.ok(!/\shidden(?:[=\s>])/.test(main));
|
||||
assert.deepEqual(calls.slice(count).map(call=>call.path).sort(),['/api/app/auth/guest','/api/app/config/info','/api/app/config/footer/info','/api/app/navigation/header','/api/app/translation/category/list'].sort(),'fixed page SSR must not fetch homepage or rich text');
|
||||
assert.ok(html.includes('class="home-content" hidden=""'));
|
||||
@@ -412,7 +415,7 @@ test('ranking slash URLs render their titles and bare URLs redirect to the same
|
||||
continue;
|
||||
}
|
||||
assert.equal(response.status,200,path);assert.equal(response.headers.get('location'),null,path);
|
||||
const html=await response.text();assert.ok(html.includes(`<title>${title} - 91短剧</title>`),path);
|
||||
const html=await response.text();assert.ok(html.includes(`<title>${title} - 黄果吃瓜</title>`),path);
|
||||
assert.ok(html.includes('href="/rank/hot/"'));assertPageLinksHaveTrailingSlash(html);
|
||||
}
|
||||
}
|
||||
@@ -420,7 +423,7 @@ test('ranking slash URLs render their titles and bare URLs redirect to the same
|
||||
|
||||
test('SSR emits configured TDK, site_name, canonical and bounded pagination links inside head for browser and crawler user agents',async()=>{
|
||||
try{
|
||||
seoConfig={config:{foreverUrlAddr:'https://canonical.drama.test/entry/?channel=abc'},seoInfo:JSON.stringify({title:'后台首页标题',description:'后台首页描述',keyworld:'首页词,短剧',webSiteName:'91剧场'})};
|
||||
seoConfig={config:{foreverUrlAddr:'https://huangguocg.com/entry/?channel=abc'},seoInfo:JSON.stringify({title:'后台首页标题',description:'后台首页描述',keyworld:'首页词,短剧',webSiteName:'91剧场'})};
|
||||
categories=[{urlValue:'/playlet/',seoInfo:{title:'短剧导航标题',description:'短剧导航描述',keyworld:['导航关键词']}}];
|
||||
listPayload={list:[fixture],totalCount:72,seoInfo:{title:'列表接口标题',description:'列表接口描述',keywords:['接口关键词']}};
|
||||
const headOf=html=>{const match=html.match(/<head>([\s\S]*?)<\/head>/);assert.ok(match);return match[1];};
|
||||
@@ -430,8 +433,8 @@ test('SSR emits configured TDK, site_name, canonical and bounded pagination link
|
||||
assert.match(head,/<meta name="description" content="后台首页描述"/);
|
||||
assert.match(head,/<meta name="keywords" content="首页词,短剧"/);
|
||||
assert.match(head,/<meta property="og:site_name" content="91剧场"/);
|
||||
assert.match(head,/<link rel="canonical" href="https:\/\/canonical.drama.test\/"/);
|
||||
assert.match(head,/<meta property="og:image" content="https:\/\/canonical.drama.test\/og-default.png"/);
|
||||
assert.match(head,/<link rel="canonical" href="https:\/\/huangguocg.com\/"/);
|
||||
assert.match(head,/<meta property="og:image" content="https:\/\/huangguocg.com\/og-default.png"/);
|
||||
assert.match(head,/<meta name="twitter:card" content="summary_large_image"/);
|
||||
assert.equal(calls.slice(start).filter(call=>call.path==='/api/app/playlet/home').length,1,'SSR and metadata share one homepage request');
|
||||
assert.equal(calls.slice(start).filter(call=>call.path==='/api/app/config/info').length,1,'SSR and metadata share one configuration request');
|
||||
@@ -441,13 +444,13 @@ test('SSR emits configured TDK, site_name, canonical and bounded pagination link
|
||||
const response=await fetch(base+path+'?channel=campaign',{redirect:'manual',headers:{'User-Agent':agent}});
|
||||
assert.equal(response.status,200);assert.equal(response.headers.get('location'),null);
|
||||
const head=headOf(await response.text());
|
||||
const canonical=`https://canonical.drama.test/shorts/${page>1?`page/${page}/`:''}`;
|
||||
const canonical=`https://huangguocg.com/shorts/${page>1?`page/${page}/`:''}`;
|
||||
assert.ok(head.includes(`<link rel="canonical" href="${canonical}"`));
|
||||
assert.ok(head.includes(`<meta property="og:url" content="${canonical}"`));
|
||||
assert.ok(head.includes(`<title>短剧导航标题${page>1?' - 第'+page+'页':''}</title>`));
|
||||
assert.ok(head.includes(`<meta name="keywords" content="导航关键词${page>1?',第'+page+'页':''}"`));
|
||||
assert.equal(head.includes('<link rel="prev"'),page>1);assert.equal(head.includes('<link rel="next"'),page<3);
|
||||
if(page===2)assert.ok(head.includes('rel="prev" href="https://canonical.drama.test/shorts/?channel=campaign"'));
|
||||
if(page===2)assert.ok(head.includes('rel="prev" href="https://huangguocg.com/shorts/?channel=campaign"'));
|
||||
assert.equal((head.match(/rel="canonical"/g)||[]).length,1);
|
||||
}
|
||||
}
|
||||
@@ -461,29 +464,29 @@ test('SSR emits configured TDK, site_name, canonical and bounded pagination link
|
||||
}finally{seoConfig={};categories=[];listPayload=undefined;}
|
||||
});
|
||||
|
||||
test('detail SEO reads protected upstream content without exposing credentials and keeps registered sessions intact',async()=>{
|
||||
test('detail SEO reads protected upstream content without exposing credentials and replaces stale accounts with a guest',async()=>{
|
||||
try{
|
||||
requireSeoAuth=true;
|
||||
seoConfig={seoInfo:{webSiteName:'91剧场'},config:{foreverUrlAddr:'https://canonical.drama.test/'}};
|
||||
seoConfig={seoInfo:{webSiteName:'91剧场'},config:{foreverUrlAddr:'https://huangguocg.com/'}};
|
||||
detailSeo={seoInfo:{title:'剧集独立标题',description:'真实剧集描述',keywords:'剧集词'}};
|
||||
const start=calls.length;
|
||||
const response=await fetch(base+'/detail/23456/',{headers:{'X-Real-IP':'203.0.113.8'}});assert.equal(response.status,200);
|
||||
const html=await response.text(),head=html.match(/<head>([\s\S]*?)<\/head>/)[1];
|
||||
assert.ok(head.includes('<title>剧集独立标题 - 91剧场</title>'));assert.ok(head.includes('content="真实剧集描述"'));
|
||||
assert.ok(head.includes('rel="canonical" href="https://canonical.drama.test/detail/23456/"'));
|
||||
assert.ok(head.includes('rel="canonical" href="https://huangguocg.com/detail/23456/"'));
|
||||
assert.equal(calls.slice(start).filter(call=>call.path==='/api/app/auth/guest').length,1);
|
||||
assert.equal(calls.slice(start).find(call=>call.path==='/api/app/auth/guest').ip,'203.0.113.8');
|
||||
assert.ok(!html.includes('test-session-secret'));assert.ok(!html.includes('drama_session'));
|
||||
const registeredStart=calls.length;
|
||||
const registered=await fetch(base+'/detail/23456/',{headers:{Cookie:'drama_session=expired; drama_registered=1'}});
|
||||
assert.equal(registered.status,200);const signedOut=await registered.text();
|
||||
assert.ok(signedOut.includes('请先登录账号'));assert.ok(signedOut.includes('data-auth-mode="login"'));assert.ok(signedOut.includes('noindex'));assert.ok(!signedOut.includes('rel="canonical"'));
|
||||
assert.equal(calls.slice(registeredStart).filter(call=>call.path==='/api/app/auth/guest').length,0,'registered identity is never replaced with guest');
|
||||
assert.equal(registered.status,200);const restored=await registered.text();
|
||||
assert.ok(!restored.includes('请先登录账号'));assert.ok(!restored.includes('data-auth-mode'));assert.ok(restored.includes('rel="canonical" href="https://huangguocg.com/detail/23456/"'));
|
||||
assert.equal(calls.slice(registeredStart).filter(call=>call.path==='/api/app/auth/guest').length,1,'an expired session always becomes a guest');
|
||||
}finally{requireSeoAuth=false;seoConfig={};detailSeo=undefined;}
|
||||
});
|
||||
|
||||
test('permanent canonical and site identity survive config errors, timeouts, missing fields and a server restart',async()=>{
|
||||
const domain='https://stable.drama.test',brand='稳定站点';
|
||||
test('site-domain canonical and site identity survive config errors, timeouts and a server restart',async()=>{
|
||||
const domain='https://huangguocg.com',brand='稳定站点';
|
||||
const identity=async()=>{
|
||||
const response=await fetch(base+'/shorts/page/2/?channel=campaign',{headers:{'X-Forwarded-Host':'alternate.entry.test','X-Forwarded-Proto':'http'}});
|
||||
assert.equal(response.status,200);
|
||||
@@ -503,14 +506,11 @@ test('permanent canonical and site identity survive config errors, timeouts, mis
|
||||
listPayload={list:[fixture],totalCount:72};
|
||||
await identity();
|
||||
for(const failure of ['http','business','malformed','timeout']){configFailure=failure;await identity();}
|
||||
configFailure='';
|
||||
seoConfig={config:{foreverUrlAddr:''},newAddressInfo:{foreverUrlAddr:''},seoInfo:{webSiteName:'不可替换的错误品牌'}};
|
||||
await identity();
|
||||
configFailure='http';await stopNext();await startNext();await identity();
|
||||
configFailure='';
|
||||
seoConfig={config:{foreverUrlAddr:'https://updated.drama.test'},seoInfo:{webSiteName:'更新站点'}};
|
||||
seoConfig={config:{foreverUrlAddr:'https://other.drama.test'},seoInfo:{webSiteName:'更新站点'}};
|
||||
const html=await (await fetch(base+'/shorts/')).text();
|
||||
assert.ok(html.includes('rel="canonical" href="https://updated.drama.test/shorts/"'));
|
||||
assert.ok(html.includes('rel="canonical" href="https://huangguocg.com/shorts/"'));
|
||||
assert.ok(html.includes('property="og:site_name" content="更新站点"'));
|
||||
}finally{configFailure='';seoConfig={};categories=[];listPayload=undefined;}
|
||||
});
|
||||
@@ -523,7 +523,7 @@ test('SSR JSON-LD matches each canonical route, list page, content entity and vi
|
||||
const graph=JSON.parse(scripts[0][1]);assert.equal(graph['@context'],'https://schema.org');return graph['@graph'];
|
||||
};
|
||||
try{
|
||||
const origin='https://schema.drama.test';
|
||||
const origin='https://huangguocg.com';
|
||||
seoConfig={seoInfo:{webSiteName:'91剧场'},config:{foreverUrlAddr:origin}};
|
||||
listPayload={list:[{...fixture,id:7},{...fixture,id:8}],totalCount:72,name:'当前接口栏目'};
|
||||
detailSeo={onlineAt:'2025-10-18 10:30:00',mediaUpdatedAt:'2025-10-19 09:00:00',title:'剧名 </script><script>alert(1)</script>',desc:'真实剧集简介'};
|
||||
@@ -541,7 +541,7 @@ test('SSR JSON-LD matches each canonical route, list page, content entity and vi
|
||||
const items=graph.find(node=>node['@type']==='ItemList').itemListElement;
|
||||
assert.deepEqual(items.map(item=>[item.position,item.url]),[[25,origin+'/detail/7/'],[26,origin+'/detail/8/']]);
|
||||
}
|
||||
if(path==='/info/help-center/')assert.equal(graph.find(node=>node['@type']==='FAQPage').mainEntity.length,6);
|
||||
if(path==='/info/help-center/')assert.equal(graph.find(node=>node['@type']==='FAQPage').mainEntity.length,5);
|
||||
}
|
||||
for(const path of ['/detail/23456/','/play/23456/90001/']){
|
||||
html=await (await fetch(base+path)).text();graph=graphOf(html);
|
||||
@@ -563,10 +563,9 @@ test('paginated routes serve slash URLs directly and redirect bare paths with th
|
||||
try{
|
||||
listPayload=accountListPayload={list:[fixture],totalCount:48};
|
||||
searchPayload={list:[fixture],totalCount:48,pageSize:24};
|
||||
commentPayload=transactionsPayload={list:[],totalCount:18};
|
||||
homePayload={...savedHome,homeConfigData:[{id:31,status:true,name:'后台配置标题',type:1,list:Array.from({length:48},(_,i)=>({...fixture,id:fixture.id+i}))}]};
|
||||
const keyword=encodeURIComponent('都市 / page/7/?#%');
|
||||
const bases=['/shorts/','/topics/','/topics/7/','/theme/8/','/tag/9/','/config/31/','/rank/','/rank/hot/','/rank/new/','/rank/follow/','/search/'+keyword+'/', '/member/favorites/','/member/history/','/member/likes/','/member/purchased/','/info/deposit/','/info/deposit/vip/','/detail/23456/','/play/23456/90001/'];
|
||||
const bases=['/shorts/','/topics/','/topics/7/','/theme/8/','/tag/9/','/config/31/','/rank/','/rank/hot/','/rank/new/','/rank/follow/','/search/'+keyword+'/'];
|
||||
for(const basePath of bases){
|
||||
for(const suffix of ['page/2/','page/2']){
|
||||
const path=basePath+suffix+'?sort=2';
|
||||
@@ -584,7 +583,7 @@ test('paginated routes serve slash URLs directly and redirect bare paths with th
|
||||
for(const path of ['/shorts/page/0/','/shorts/page/01/','/shorts/page/2/page/3/','/apps/page/2/','/home-road/page/2/','/info/about-us/page/2/']){
|
||||
const response=await fetch(base+path,{redirect:'manual'});assert.equal(response.status,404,path);assert.equal(response.headers.get('location'),null,path);
|
||||
}
|
||||
}finally{homePayload=savedHome;listPayload=accountListPayload=searchPayload=commentPayload=transactionsPayload=undefined;}
|
||||
}finally{homePayload=savedHome;listPayload=searchPayload=undefined;}
|
||||
});
|
||||
|
||||
test('guest session token stays in HttpOnly cookie and device header is forwarded',async()=>{
|
||||
@@ -596,7 +595,7 @@ test('guest session token stays in HttpOnly cookie and device header is forwarde
|
||||
assert.ok(calls.findLast(call=>call.path.endsWith('/auth/guest')).device.includes('DevID='));
|
||||
});
|
||||
|
||||
test('browser BFF stays separate from Go app routes and preserves sessions, queries and logout',async()=>{
|
||||
test('browser BFF stays separate from Go app routes, preserves sessions and queries, and rejects removed account routes',async()=>{
|
||||
let start=calls.length;
|
||||
const guest=await fetch(base+'/api/auth/guest',{method:'POST',headers:{Origin:base,'Content-Type':'application/json'},body:JSON.stringify({devID:'bff-device'})});
|
||||
assert.equal(guest.status,200);assert.equal((await guest.json()).data.token,undefined);
|
||||
@@ -610,8 +609,7 @@ test('browser BFF stays separate from Go app routes and preserves sessions, quer
|
||||
assert.equal(calls.at(-1).path,'/api/app/playlet/search');assert.equal(new URLSearchParams(calls.at(-1).search).get('pageNum'),'2');
|
||||
start=calls.length;
|
||||
assert.equal((await fetch(base+'/api/not-allowed')).status,404);
|
||||
const logout=await fetch(base+'/api/auth/logout',{method:'POST',headers:{Origin:base,'Content-Type':'application/json'},body:'{}'});
|
||||
assert.equal(logout.status,200);assert.ok(logout.headers.getSetCookie().some(value=>value.startsWith('drama_session=;')&&value.includes('Max-Age=0')));
|
||||
for(const path of ['auth/logout','auth/login','auth/register','coin/buy','playlet/chapter/unlock'])assert.equal((await fetch(base+'/api/'+path,{method:'POST',headers:{Origin:base,'Content-Type':'application/json',Cookie:cookie},body:'{}'})).status,404,path);
|
||||
assert.equal(calls.length,start);
|
||||
});
|
||||
|
||||
@@ -660,50 +658,6 @@ test('public content ignores expired credentials; double-encoded data is normali
|
||||
assert.equal(payload.data.list[0].id,23456);
|
||||
});
|
||||
|
||||
test('registered session expiry never retries a write as a guest',async()=>{
|
||||
const guestCount=calls.filter(call=>call.path.endsWith('/auth/guest')).length;
|
||||
const response=await fetch(base+'/api/app/playlet/collect/set',{method:'POST',headers:{Cookie:'drama_session=expired; drama_registered=1','Content-Type':'application/json'},body:JSON.stringify({id:23456,isCollect:true})});
|
||||
assert.equal(response.status,401);assert.equal((await response.json()).code,1000);
|
||||
assert.equal(calls.filter(call=>call.path.endsWith('/auth/guest')).length,guestCount);
|
||||
assert.equal(response.headers.get('X-Auth-State'),'login-required');
|
||||
assert.ok(response.headers.getSetCookie().some(value=>value.startsWith('drama_session=;')&&value.includes('Max-Age=0')));
|
||||
});
|
||||
|
||||
test('login sends device/channel and trimmed credentials without attaching the previous identity',async()=>{
|
||||
const response=await fetch(base+'/api/app/auth/login',{method:'POST',headers:{'Content-Type':'application/json','X-Real-IP':'203.0.113.9',Cookie:'drama_session=old-guest; drama_device=device-stable; drama_channel=first'},body:JSON.stringify({account:' player01 ',password:' Secure123 ',devID:'different',channel:'later'})});
|
||||
assert.equal(response.status,200);const data=await response.json();assert.equal(data.data.userId,88);assert.equal(data.data.token,undefined);
|
||||
const call=calls.at(-1);assert.equal(call.auth,undefined);assert.deepEqual(JSON.parse(call.body),{account:'player01',password:' Secure123 ',devID:'device-stable',channel:'first'});
|
||||
assert.ok(call.device.includes('DevID=device-stable;'));
|
||||
assert.equal(call.ip,'203.0.113.9');
|
||||
assert.ok(response.headers.getSetCookie().some(value=>value.startsWith('drama_registered=1;')));
|
||||
});
|
||||
|
||||
test('binding retains the guest Authorization, device, channel and invite and fills the optional email',async()=>{
|
||||
const response=await fetch(base+'/api/app/auth/register',{method:'POST',headers:{'Content-Type':'application/json',Cookie:'drama_session=old-guest; drama_registered=0; drama_device=device-stable; drama_channel=first'},body:JSON.stringify({accountNo:' player01 ',password:'Secure123',agentInviteCode:' INV123 ',channel:'later'})});
|
||||
assert.equal(response.status,200);const data=await response.json();assert.equal(data.data.userId,77);assert.equal(data.data.token,undefined);
|
||||
const call=calls.at(-1);assert.equal(call.auth,'old-guest');
|
||||
assert.deepEqual(JSON.parse(call.body),{accountNo:'player01',password:'Secure123',email:'player01@91dj.local',devID:'device-stable',channel:'first',agentInviteCode:'INV123'});
|
||||
const memberResponse=await fetch(base+'/api/app/auth/register',{method:'POST',headers:{'Content-Type':'application/json',Cookie:'drama_session=registered; drama_registered=1'},body:JSON.stringify({accountNo:'player02',password:'Secure123',email:'me@example.com'})});
|
||||
assert.equal(memberResponse.status,200);assert.equal(calls.at(-1).auth,undefined);assert.equal(JSON.parse(calls.at(-1).body).email,'me@example.com');
|
||||
});
|
||||
|
||||
test('refresh restores an existing account and keeps its token when getInfo omits it',async()=>{
|
||||
const count=calls.length;
|
||||
const response=await fetch(base+'/api/app/auth/guest',{method:'POST',headers:{'Content-Type':'application/json',Cookie:'drama_session=test-registered-secret; drama_registered=1'},body:'{"automatic":true}'});
|
||||
assert.equal(response.status,200);const data=await response.json();assert.equal(data.data.userId,88);assert.equal(data.data.isRegister,true);assert.equal(data.data.token,undefined);
|
||||
assert.deepEqual(calls.slice(count).map(call=>call.path),['/api/app/auth/getInfo']);
|
||||
assert.ok(response.headers.getSetCookie().some(value=>value.startsWith('drama_session=test-registered-secret;')));
|
||||
assert.ok(response.headers.getSetCookie().some(value=>value.startsWith('drama_registered=1;')));
|
||||
});
|
||||
|
||||
test('an expired registered account is cleared during initialization without becoming a guest',async()=>{
|
||||
const count=calls.length;
|
||||
const response=await fetch(base+'/api/app/auth/guest',{method:'POST',headers:{'Content-Type':'application/json',Cookie:'drama_session=expired; drama_registered=1'},body:'{"automatic":true}'});
|
||||
assert.equal(response.status,401);assert.equal(response.headers.get('X-Auth-State'),'login-required');
|
||||
assert.deepEqual(calls.slice(count).map(call=>call.path),['/api/app/auth/getInfo']);
|
||||
assert.ok(response.headers.getSetCookie().some(value=>value.startsWith('drama_session=;')&&value.includes('Max-Age=0')));
|
||||
});
|
||||
|
||||
test('expired guest requests recover once with the same device, including an explicitly rejected write',async()=>{
|
||||
for(const [path,method,body] of [['auth/getInfo','GET',undefined],['playlet/collect/set','POST',JSON.stringify({id:23456,isCollect:true})]]){
|
||||
const count=calls.length;
|
||||
@@ -724,40 +678,14 @@ test('an HTTP 401 follows the same guest recovery path',async()=>{
|
||||
assert.deepEqual(calls.slice(count).map(call=>call.path),['/api/app/auth/getInfo','/api/app/auth/guest','/api/app/auth/getInfo']);
|
||||
});
|
||||
|
||||
test('SDK snapshots reach order APIs and the same drama context survives the guest retry',async()=>{
|
||||
const common={app_id:'1047',uid:'',channel:'organic',sid:'sdk-session',client_ts:Math.floor(Date.now()/1000),device:'PC',device_id:'sdk-device',device_brand:'',device_model:'',device_fingerprint:'raw-sdk-fingerprint',fp_version:'3.0.1',user_agent:'test UA',sdk_version:'1.2.0',app_version:'1.0.0'};
|
||||
const sdk=Buffer.from(JSON.stringify(common)).toString('base64');
|
||||
const requestId='1234567890abcdef1234567890abcdef',context={v:1,request_id:requestId,event_name:'drama_collect',common,drama:{media_id:'',drama_id:'23456',episode:1,video_id:'90001',recommend_trace_id:''}};
|
||||
test('client tracking headers are never forwarded, including across the guest retry',async()=>{
|
||||
const sdk=Buffer.from(JSON.stringify({app_id:'1047',device_id:'sdk-device'})).toString('base64'),requestId='1234567890abcdef1234567890abcdef';
|
||||
const count=calls.length;
|
||||
const response=await fetch(base+'/api/app/playlet/collect/set',{method:'POST',headers:{'Content-Type':'application/json',Cookie:'drama_session=expired; drama_registered=0; drama_device=business-device','X-SDK-Common':sdk,'X-Tracker-Request-Id':requestId,'X-Tracker-Event-Name':'drama_collect','X-DC-Request-Context':Buffer.from(JSON.stringify(context)).toString('base64url')},body:'{"id":23456,"isCollect":true}'});
|
||||
const response=await fetch(base+'/api/app/playlet/collect/set',{method:'POST',headers:{'Content-Type':'application/json',Cookie:'drama_session=expired; drama_registered=0; drama_device=business-device','X-SDK-Common':sdk,'X-Tracker-Request-Id':requestId,'X-Tracker-Event-Name':'drama_collect','X-DC-Request-Context':'e30'},body:'{"playletId":23456,"isCollect":1}'});
|
||||
assert.equal(response.status,200);
|
||||
const requests=calls.slice(count);assert.equal(requests.length,3);
|
||||
for(const call of [requests[0],requests[2]]){
|
||||
assert.equal(call.trackerId,requestId);assert.equal(call.trackerEvent,'drama_collect');assert.deepEqual(JSON.parse(Buffer.from(call.trackerContext,'base64url')),context);assert.equal(call.sdk,sdk);
|
||||
}
|
||||
assert.equal(requests[1].trackerContext,undefined);assert.equal(requests[1].sdk,sdk);assert.equal(JSON.parse(requests[1].body).devID,'business-device');
|
||||
await fetch(base+'/api/app/coin/buy',{method:'POST',headers:{'Content-Type':'application/json',Cookie:'drama_session=test-session-secret','X-SDK-Common':sdk},body:'{"packageId":1,"payType":"alipay"}'});
|
||||
assert.equal(calls.at(-1).sdk,sdk);assert.equal(calls.at(-1).trackerContext,undefined);
|
||||
});
|
||||
|
||||
test('bad credentials and malformed successful login responses never overwrite an existing account cookie',async()=>{
|
||||
for(const [account,status] of [['wrong',200],['missing-token',502]]){
|
||||
const response=await fetch(base+'/api/app/auth/login',{method:'POST',headers:{'Content-Type':'application/json',Cookie:'drama_session=valid-account; drama_registered=1'},body:JSON.stringify({account,password:'Secure123'})});
|
||||
assert.equal(response.status,status);assert.notEqual((await response.json()).code,200);
|
||||
assert.ok(!response.headers.getSetCookie().some(value=>value.startsWith('drama_session=')||value.startsWith('drama_registered=')));
|
||||
}
|
||||
});
|
||||
|
||||
test('manual logout skips the following automatic guest initialization and explicit sign-in can resume',async()=>{
|
||||
const logout=await fetch(base+'/api/app/auth/logout',{method:'POST',body:'{}'});
|
||||
assert.ok(logout.headers.getSetCookie().some(value=>value.startsWith('drama_skip_auto=1;')));
|
||||
const count=calls.length;
|
||||
const skipped=await fetch(base+'/api/app/auth/guest',{method:'POST',headers:{'Content-Type':'application/json',Cookie:'drama_skip_auto=1'},body:'{"automatic":true}'});
|
||||
assert.equal(skipped.status,200);assert.equal((await skipped.json()).data,null);assert.equal(calls.length,count);
|
||||
assert.ok(skipped.headers.getSetCookie().some(value=>value.startsWith('drama_skip_auto=;')&&value.includes('Max-Age=0')));
|
||||
const explicit=await fetch(base+'/api/app/auth/guest',{method:'POST',headers:{'Content-Type':'application/json',Cookie:'drama_skip_auto=1; drama_device=device-stable'},body:'{}'});
|
||||
assert.equal(explicit.status,200);assert.equal((await explicit.json()).data.userId,77);
|
||||
assert.ok(explicit.headers.getSetCookie().some(value=>value.startsWith('drama_skip_auto=;')&&value.includes('Max-Age=0')));
|
||||
for(const call of requests){assert.equal(call.sdk,undefined);assert.equal(call.trackerId,undefined);assert.equal(call.trackerEvent,undefined);assert.equal(call.trackerContext,undefined);}
|
||||
assert.equal(JSON.parse(requests[1].body).devID,'business-device');
|
||||
});
|
||||
|
||||
test('proxy rejects unlisted endpoints and wrong methods before calling upstream',async()=>{
|
||||
@@ -772,15 +700,6 @@ test('chapter permissions and identifiers are preserved from the API',async()=>{
|
||||
const payload=await response.json();assert.equal(payload.data.id,90001);assert.equal(payload.data.canWatch,false);assert.equal(payload.data.singleCoins,13);
|
||||
});
|
||||
|
||||
test('confirmed chapter unlock forwards the exact episode and identity and returns the upstream balance',async()=>{
|
||||
const count=calls.length;
|
||||
const response=await fetch(base+'/api/app/playlet/chapter/unlock',{method:'POST',headers:{'Content-Type':'application/json',Cookie:'drama_session=test-session-secret; drama_device=unlock-test-device'},body:JSON.stringify({playletId:23456,chapterId:90001})});
|
||||
assert.equal(response.status,200);const payload=await response.json();assert.equal(payload.code,200);assert.equal(payload.data.balance,77);
|
||||
assert.equal(calls.length,count+1);const call=calls.at(-1);
|
||||
assert.equal(call.path,'/api/app/playlet/chapter/unlock');assert.equal(call.auth,'test-session-secret');
|
||||
assert.deepEqual(JSON.parse(call.body),{playletId:23456,chapterId:90001});assert.ok(call.device.includes('DevID=unlock-test-device;'));
|
||||
});
|
||||
|
||||
test('media proxy preserves signed URLs and HLS content while isolating application credentials',async()=>{
|
||||
const query='?token=a%2Fb+1&sig=x%252Fy';
|
||||
const response=await fetch(base+'/h5/m3u8/folder/movie.m3u8'+query,{redirect:'manual',headers:{Authorization:'private',Cookie:'drama_session=private','X-Forwarded-For':'203.0.113.10, 10.0.0.1'}});
|
||||
@@ -826,14 +745,6 @@ test('media byte ranges and upstream failure states remain usable by the player'
|
||||
assert.equal((await fetch(base+'/h5/m3u8/missing.m3u8')).status,404);
|
||||
});
|
||||
|
||||
test('logout clears cookies without mutating upstream content',async()=>{
|
||||
const count=calls.length;
|
||||
const response=await fetch(base+'/api/app/auth/logout',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'});
|
||||
assert.equal(response.status,200);assert.ok(response.headers.getSetCookie().some(value=>value.startsWith('drama_session=;')&&value.includes('Max-Age=0')));
|
||||
assert.equal(calls.length,count);
|
||||
});
|
||||
|
||||
|
||||
function currentMain(html,id){
|
||||
const match=html.match(new RegExp('<main\\b[^>]*\\bid="'+id+'"[^>]*>[\\s\\S]*?<\\/main>'));
|
||||
assert.ok(match,'main '+id+' must exist');assert.ok(!/^<main[^>]*\shidden(?:[=\s>])/.test(match[0]),id+' must be visible');
|
||||
@@ -890,39 +801,36 @@ test('standalone lists use H2 card titles while cards within named sections rema
|
||||
assert.ok(section);assert.equal((section.match(/<h2\b/g)||[]).length,1);
|
||||
assert.equal((section.match(/<h3\b/g)||[]).length,1,'homepage cards are below their section heading');
|
||||
}
|
||||
accountListPayload={list:[fixture],totalCount:1};
|
||||
const member=await (await fetch(base+'/member/center/')).text();
|
||||
const shelf=member.match(/<section class="member-library"[^>]*>([\s\S]*?)<\/section>/)?.[1];
|
||||
assert.ok(shelf);assert.match(shelf,/<h2 id="member-library-title"/);assert.match(shelf,/<h3>/);
|
||||
const detail=currentMain(await (await fetch(base+'/detail/23456/')).text(),'drama-play-page');
|
||||
assert.match(detail,/<aside class="play-related"><h2>热门短剧<\/h2>[\s\S]*?<h3>同类推荐短剧<\/h3>/);
|
||||
assert.equal((detail.match(/<h1\b/g)||[]).length,1);
|
||||
assert.match(detail,/<h2 id="hg-recommend-title">推荐视频<\/h2>[\s\S]*?<h3 class="hg-vitem-title"><a href="\/detail\/23458\/" title="《同类推荐短剧》在线观看">同类推荐短剧<\/a><\/h3>/);
|
||||
const search=currentMain(await (await fetch(base+'/search/关键词/')).text(),'search-results-page');
|
||||
assert.match(search,/<h1\b/);assert.match(search,/<h2>/);assert.ok(!/<h3\b/.test(search));
|
||||
}finally{homePayload=savedHome;listPayload=accountListPayload=undefined;}
|
||||
assert.match(search,/<h1\b/);assert.match(search,/<h2 class="hg-card-title">/);assert.ok(!/<h3\b/.test(search));
|
||||
}finally{homePayload=savedHome;listPayload=undefined;}
|
||||
});
|
||||
|
||||
test('detail, playback, video and comment pages contain real initial content without exposing private payloads or denied video sources',async()=>{
|
||||
test('detail and playback pages contain real initial content and recommendations without exposing private payloads, and play every supplied source',async()=>{
|
||||
try{
|
||||
detailSeo={desc:'服务端剧集完整简介',token:'private-detail-token',reportSdkConfig:{secret:'private-sdk-secret'}};
|
||||
chapterListPayload={list:[{id:90001,currentEpisode:1,canWatch:true,videoUrl:'https://cdn.example/free.mp4'},{id:90002,currentEpisode:2,canWatch:false,needUnlock:true,payType:1,singleCoins:13,videoUrl:'https://cdn.example/denied-video-secret.mp4'}]};
|
||||
chapterPayload={...chapterListPayload.list[1],playletId:23456};
|
||||
commentPayload={list:[{id:45,text:'服务端第2页评论',userName:'评论用户',token:'private-comment-token',children:[{id:46,text:'嵌套回复'}]}],totalCount:25};
|
||||
videoPayload={...fixture,id:23457,title:'服务端独立视频',video:{url:'https://cdn.example/public-video.mp4',cover:fixture.cover},desc:'视频真实简介'};
|
||||
for(const path of ['/detail/23456/page/2/?commentSort=1','/play/23456/90002/page/2/']){
|
||||
for(const path of ['/detail/23456/','/play/23456/90002/']){
|
||||
const start=calls.length,response=await fetch(base+path);assert.equal(response.status,200);
|
||||
const html=await response.text(),main=currentMain(html,'drama-play-page');
|
||||
assert.ok(main.includes('服务端第2页评论'));assert.ok(main.includes('嵌套回复'));assert.ok(main.includes('评论 (25)'));
|
||||
assert.ok(main.includes('/page/3/'));assert.ok(!html.includes('后台配置标题'));
|
||||
for(const secret of ['private-detail-token','private-comment-token','private-sdk-secret','denied-video-secret.mp4'])assert.ok(!html.includes(secret),secret);
|
||||
assert.equal(calls.slice(start).filter(call=>call.path==='/api/app/comment/list').length,1,'metadata/body must share comments');
|
||||
assert.ok(main.includes('<h2 id="hg-recommend-title">推荐视频</h2>'));assert.ok(!main.includes('play-comment'));assert.ok(main.includes('剧情简介</h2>'));assert.ok(main.includes('全集列表</h2>'));
|
||||
assert.ok(!html.includes('后台配置标题'));
|
||||
for(const secret of ['private-detail-token','private-sdk-secret'])assert.ok(!html.includes(secret),secret);
|
||||
if(path.startsWith('/play/'))assert.ok(html.includes('denied-video-secret.mp4'),'a supplied source is free to watch');
|
||||
assert.equal(calls.slice(start).filter(call=>call.path==='/api/app/comment/list').length,0,'comments are no longer requested');
|
||||
if(path.startsWith('/video/')){assert.ok(main.includes('服务端独立视频'));assert.ok(main.includes('<video '));}
|
||||
else{
|
||||
assert.ok(main.includes('服务端剧集完整简介'));assert.ok(main.includes('href="/play/23456/90001/"'));assert.ok(main.includes('data-locked-chapter="90002"'));
|
||||
assert.ok(main.includes('服务端剧集完整简介'));assert.ok(main.includes('href="/play/23456/90001/"'));assert.ok(main.includes('href="/play/23456/90002/"'));assert.ok(!main.includes('play-episode-locked'));
|
||||
if(path.startsWith('/detail/'))assert.ok(main.includes('is-overview'));else assert.ok(main.includes('<video '));
|
||||
}
|
||||
assertPageLinksHaveTrailingSlash(main);
|
||||
}
|
||||
}finally{detailSeo=undefined;chapterPayload=undefined;chapterListPayload=undefined;commentPayload=undefined;videoPayload=undefined;}
|
||||
}finally{detailSeo=undefined;chapterPayload=undefined;chapterListPayload=undefined;videoPayload=undefined;}
|
||||
});
|
||||
|
||||
test('search paths decode keyword segments once and include keyword/result cards before JavaScript runs',async()=>{
|
||||
@@ -967,7 +875,7 @@ test('Chinese content slugs generate ID-based links in rendered pages and struct
|
||||
chapterPayload={id:90001,playletId:23456,currentEpisode:1,canWatch:true,videoUrl:'https://media.drama.test/1.mp4'};
|
||||
const detail=await (await fetch(base+'/play/23456/90001/')).text();
|
||||
assert.ok(detail.includes('href="/tag/9/"'));assert.ok(detail.includes('<span>无标识标签</span>'));assert.ok(!detail.includes('href="/tag//"'));
|
||||
assert.ok(detail.includes('rel="canonical" href="https://default.drama.test/play/23456/90001/"'));
|
||||
assert.ok(detail.includes('rel="canonical" href="https://huangguocg.com/play/23456/90001/"'));
|
||||
for(const html of [home,detail]){
|
||||
for(const [,href] of html.matchAll(/<a\b[^>]*href="(\/[^"#?]*)/g))if(!href.startsWith('/search/'))assert.ok(!/[^\x00-\x7f]/.test(decodeURIComponent(href)),href);
|
||||
}
|
||||
@@ -1030,11 +938,9 @@ test('known pagination limits return 404 while existing empty first pages remain
|
||||
const overflow=await fetch(base+path+'page/2/',{redirect:'manual'});assert.equal(overflow.status,404,path);await overflow.text();
|
||||
}
|
||||
listPayload={list:[fixture],totalCount:48};
|
||||
commentPayload=transactionsPayload={list:[],totalCount:20};
|
||||
accountListPayload={list:[],totalCount:48};
|
||||
searchPayload={list:[fixture],totalCount:48,pageSize:24};
|
||||
for(const path of ['/shorts/','/rank/hot/','/rank/new/','/rank/follow/','/topics/','/topics/7/','/theme/8/','/tag/9/',
|
||||
'/detail/23456/','/play/23456/90001/','/search/关键词/','/member/history/','/member/favorites/','/member/purchased/','/info/deposit/','/info/deposit/vip/']){
|
||||
'/search/关键词/']){
|
||||
for(const [page,status] of [[2,200],[3,404]]){
|
||||
const response=await fetch(base+path+'page/'+page+'/',{redirect:'manual'}),html=await response.text();
|
||||
assert.equal(response.status,status,path+' page '+page);assert.equal(response.headers.get('location'),null);
|
||||
@@ -1044,7 +950,7 @@ test('known pagination limits return 404 while existing empty first pages remain
|
||||
const config=await fetch(base+'/config/31/page/2/');assert.equal(config.status,404);await config.text();
|
||||
searchPayload={list:[],totalCount:0};
|
||||
const emptySearch=await fetch(base+'/search/没有匹配项/');assert.equal(emptySearch.status,200);assert.ok((await emptySearch.text()).includes('搜索无结果'));
|
||||
}finally{listPayload=commentPayload=transactionsPayload=accountListPayload=searchPayload=undefined;}
|
||||
}finally{listPayload=searchPayload=undefined;}
|
||||
});
|
||||
|
||||
test('topic aliases and leading-zero IDs serve content directly with a single primary canonical',async()=>{
|
||||
@@ -1053,7 +959,7 @@ test('topic aliases and leading-zero IDs serve content directly with a single pr
|
||||
for(const [path,target] of [['/theme/007/','/topics/7/'],['/theme/007/page/2/','/topics/7/page/2/'],['/topics/007/','/topics/7/'],['/detail/023456/','/detail/23456/']]){
|
||||
const start=calls.length,response=await fetch(base+path+'?utm_source=campaign',{redirect:'manual'}),html=await response.text();
|
||||
assert.equal(response.status,200,path);assert.equal(response.headers.get('location'),null);
|
||||
assert.ok(html.includes(`rel="canonical" href="https://default.drama.test${target}"`),path);
|
||||
assert.ok(html.includes(`rel="canonical" href="https://huangguocg.com${target}"`),path);
|
||||
assert.match(html,/<meta name="robots" content="index,\s*follow/);
|
||||
assert.ok(html.includes(path.startsWith('/detail/')?'play-detail-title':'同一专题'));
|
||||
if(path.startsWith('/theme/'))assert.equal(calls.slice(start).filter(call=>call.path==='/api/app/playlet/topic/detail').length,1);
|
||||
@@ -1061,25 +967,20 @@ test('topic aliases and leading-zero IDs serve content directly with a single pr
|
||||
}finally{listPayload=undefined;}
|
||||
});
|
||||
|
||||
test('comment pages stay usable but noindex, while ordinary catalog pagination remains indexable',async()=>{
|
||||
test('detail and play pages have no comment pagination, while ordinary catalog pagination remains indexable',async()=>{
|
||||
try{
|
||||
commentPayload={list:[{id:321,text:'第二页的真实评论'}],totalCount:25};
|
||||
chapterPayload={id:90001,playletId:23456,currentEpisode:2,canWatch:true,videoUrl:'https://media.drama.test/2.mp4'};
|
||||
for(const path of ['/detail/23456/page/2/','/play/23456/90001/page/2/']){
|
||||
const response=await fetch(base+path,{redirect:'manual',headers:{'User-Agent':'YandexBot'}}),html=await response.text();
|
||||
assert.equal(response.status,200);assert.equal(response.headers.get('location'),null);
|
||||
assert.match(html,/<meta name="robots" content="noindex,\s*follow"/);assert.ok(!html.includes('rel="canonical"'));
|
||||
assert.ok(!/<script\b[^>]*data-site-json-ld=/.test(html));
|
||||
assert.ok(html.includes('第二页的真实评论'));assert.ok(html.includes('href="'+path.replace('/page/2/','/page/3/')+'"'));
|
||||
const response=await fetch(base+path,{redirect:'manual'});assert.equal(response.status,404,path);await response.text();
|
||||
}
|
||||
listPayload={list:[fixture],totalCount:72};
|
||||
const html=await (await fetch(base+'/shorts/page/2/')).text();
|
||||
assert.match(html,/<meta name="robots" content="index,\s*follow/);
|
||||
assert.ok(html.includes('rel="canonical" href="https://default.drama.test/shorts/page/2/"'));
|
||||
}finally{commentPayload=chapterPayload=listPayload=undefined;}
|
||||
assert.ok(html.includes('rel="canonical" href="https://huangguocg.com/shorts/page/2/"'));
|
||||
}finally{chapterPayload=listPayload=undefined;}
|
||||
});
|
||||
|
||||
test('SSR applies more-list content rules and real episode access rules without exposing locked media',async()=>{
|
||||
test('SSR applies more-list content rules and treats every supplied episode source as free',async()=>{
|
||||
const savedHome=homePayload;
|
||||
try{
|
||||
const block={id:31,status:true,name:'都市精选',type:1,list:[fixture]};
|
||||
@@ -1093,17 +994,15 @@ test('SSR applies more-list content rules and real episode access rules without
|
||||
detailSeo={desc:'剧集共用简介',onlineAt:'2025-10-18 10:30:00'};
|
||||
const chapter={id:90001,playletId:23456,currentEpisode:3,payType:1,videoUrl:'https://media.drama.test/paid-episode.m3u8'};
|
||||
for(const allowed of [false,true]){
|
||||
// Payment flags are ignored: a supplied source is always free to watch and indexable.
|
||||
chapterPayload={...chapter,canWatch:allowed,needUnlock:!allowed};
|
||||
const response=await fetch(base+'/play/23456/90001/'),html=await response.text();assert.equal(response.status,200);
|
||||
assert.equal(/<meta name="robots" content="index,/.test(html),allowed);
|
||||
assert.equal(html.includes('rel="canonical"'),allowed);assert.equal(html.includes(chapter.videoUrl),allowed);
|
||||
assert.ok(html.includes('class="play-screen play-paywalled-content"'));
|
||||
if(allowed){
|
||||
const graph=JSON.parse(html.match(/<script\b[^>]*data-site-json-ld=""[^>]*>([\s\S]*?)<\/script>/)[1])['@graph'];
|
||||
const page=graph.find(node=>node['@type']==='WebPage');
|
||||
assert.equal(page.isAccessibleForFree,false);assert.equal(page.hasPart.cssSelector,'.play-paywalled-content');
|
||||
assert.equal(graph.find(node=>node['@type']==='VideoObject').isAccessibleForFree,false);
|
||||
}else assert.ok(!/<script\b[^>]*data-site-json-ld=/.test(html));
|
||||
assert.match(html,/<meta name="robots" content="index,/);
|
||||
assert.ok(html.includes('rel="canonical"'));assert.ok(html.includes(chapter.videoUrl));
|
||||
assert.ok(html.includes('class="play-screen"'));assert.ok(!html.includes('play-paywalled-content'));
|
||||
const graph=JSON.parse(html.match(/<script\b[^>]*data-site-json-ld=""[^>]*>([\s\S]*?)<\/script>/)[1])['@graph'];
|
||||
assert.equal(graph.find(node=>node['@type']==='WebPage').isAccessibleForFree,undefined);
|
||||
assert.equal(graph.find(node=>node['@type']==='VideoObject').isAccessibleForFree,true);
|
||||
}
|
||||
chapterPayload={...chapter,canWatch:true,needUnlock:false,payType:0,videoUrl:''};
|
||||
const html=await (await fetch(base+'/play/23456/90001/')).text();
|
||||
@@ -1156,19 +1055,15 @@ test('HEAD requests report the same missing-resource and temporary-error statuse
|
||||
}finally{responseOverrides={};}
|
||||
});
|
||||
|
||||
test('optional data failures preserve real detail content and do not manufacture pagination 404s',async()=>{
|
||||
test('optional data failures preserve real detail content',async()=>{
|
||||
try{
|
||||
responseOverrides={
|
||||
'comment/list':{status:503},'playlet/chapter/list':{status:503},'playlet/guess-like/list':{status:503},
|
||||
'playlet/chapter/list':{status:503},'playlet/guess-like/list':{status:503},
|
||||
};
|
||||
for(const path of ['/detail/23456/','/play/23456/90001/']){
|
||||
const response=await fetch(base+path);assert.equal(response.status,200,path);
|
||||
const html=await response.text();assert.ok(html.includes('接口剧名'));assert.ok(html.includes('评论暂不可用'));
|
||||
const commentsPage=await fetch(base+path+'page/2/');assert.equal(commentsPage.status,500,'unknown comments count must not become 404');await commentsPage.text();
|
||||
const html=await response.text();assert.ok(html.includes('接口剧名'));assert.ok(html.includes('暂无推荐视频'));
|
||||
}
|
||||
responseOverrides={'coin/trans/list':{status:503}};
|
||||
const wallet=await fetch(base+'/info/deposit/');assert.equal(wallet.status,200);await wallet.text();
|
||||
const page=await fetch(base+'/info/deposit/page/2/');assert.equal(page.status,500);await page.text();
|
||||
}finally{responseOverrides={};}
|
||||
});
|
||||
|
||||
@@ -1240,10 +1135,10 @@ test('navigation failure leaves page content available and never restores a hard
|
||||
});
|
||||
|
||||
|
||||
test('applications, return-home, feedback and contacts arrive as complete HTML without a loading shell',async()=>{
|
||||
test('applications, return-home and contacts arrive as complete HTML without a loading shell',async()=>{
|
||||
try{
|
||||
appsPayload={categoryList:[{id:80,title:'接口应用分类',appList:[{id:81,name:'接口应用',url:'https://apps.example/download',logo:'https://apps.example/logo.webp'}]}]};
|
||||
for(const [path,panel,marker] of [['/apps/','channel-page','接口应用分类'],['/home-road/','channel-page','return-home-directory'],['/info/feedback/','info-page','id="feedback-form"'],['/info/about-us/','info-page','https://example.com/contact']]){
|
||||
for(const [path,panel,marker] of [['/apps/','channel-page','接口应用分类'],['/home-road/','channel-page','return-home-directory'],['/info/about-us/','info-page','官方TG:<a href="https://t.me/jbd125"']]){
|
||||
const start=calls.length,response=await fetch(base+path);assert.equal(response.status,200,path);
|
||||
const html=await response.text(),main=currentMain(html,panel);assert.ok(main.includes(marker),path);assert.ok(!/api-loading|正在加载|加载中/.test(main),path);
|
||||
if(path==='/apps/')assert.equal(calls.slice(start).filter(call=>call.path==='/api/app/applist/list').length,1);
|
||||
@@ -1251,94 +1146,6 @@ test('applications, return-home, feedback and contacts arrive as complete HTML w
|
||||
}finally{appsPayload=undefined;}
|
||||
});
|
||||
|
||||
test('fresh private pages render real guest records with the same persisted identity and no credentials in HTML',async()=>{
|
||||
try{
|
||||
accountListPayload={list:[{...fixture,title:'游客自己的历史',password:'private-list-password'}],totalCount:72};
|
||||
const start=calls.length,response=await fetch(base+'/member/history/page/2/'),html=await response.text(),main=currentMain(html,'info-page');
|
||||
assert.equal(response.status,200);assert.match(response.headers.get('cache-control'),/no-store/);
|
||||
assert.ok(main.includes('游客自己的历史'));assert.ok(main.includes('真实接口用户'));assert.ok(main.includes('href="/member/history/page/3/"'));
|
||||
assert.ok(!/api-loading|正在加载|加载中/.test(main));
|
||||
for(const secret of ['test-session-secret','private-list-password'])assert.ok(!html.includes(secret));
|
||||
assert.ok(response.headers.getSetCookie().some(value=>value.startsWith('drama_session=')&&/HttpOnly/.test(value)));
|
||||
const reads=calls.slice(start);assert.equal(reads.filter(call=>call.path==='/api/app/auth/guest').length,1);assert.equal(reads.filter(call=>call.path==='/api/app/auth/getInfo').length,0);
|
||||
assert.equal(reads.find(call=>call.path==='/api/app/playlet/history/list').auth,'test-session-secret');
|
||||
}finally{accountListPayload=undefined;}
|
||||
});
|
||||
|
||||
test('likes pages render the current account list, native links and pagination before JavaScript without shared caching',async()=>{
|
||||
try{
|
||||
strictContract=true;
|
||||
accountListPayload=(path,request)=>({list:[{...fixture,slug:'liked-story',title:request.headers.authorization==='registered-user-a-secret'?'甲的点赞短剧':'乙的点赞短剧',token:'private-liked-token'}],totalCount:72});
|
||||
for(const [token,title,other] of [['registered-user-a-secret','甲的点赞短剧','乙的点赞短剧'],['registered-user-b-secret','乙的点赞短剧','甲的点赞短剧']]){
|
||||
const start=calls.length;
|
||||
const response=await fetch(base+'/member/likes/page/2/',{headers:{Cookie:'drama_session='+token+'; drama_registered=1'}}),html=await response.text(),main=currentMain(html,'info-page');
|
||||
assert.equal(response.status,200);assert.match(response.headers.get('cache-control'),/private.*no-store/);
|
||||
assert.match(html,/<meta name="robots" content="noindex,\s*nofollow"/);
|
||||
assert.match(main,/<h1\b[^>]*>我的点赞<\/h1>/);assert.ok(main.includes(title));assert.ok(!main.includes(other));
|
||||
assert.ok(main.includes('href="/member/likes/" aria-current="page"'));assert.ok(main.includes('href="/member/likes/page/3/"'));
|
||||
assert.ok(main.includes('href="/detail/liked-story/"'));assert.ok(main.includes('共 72 部短剧'));
|
||||
assert.ok(!/api-loading|正在加载|加载中/.test(main));
|
||||
for(const secret of [token,'private-liked-token'])assert.ok(!html.includes(secret));
|
||||
const reads=calls.slice(start).filter(call=>call.path==='/api/app/playlet/like/list');
|
||||
assert.equal(reads.length,1);assert.equal(reads[0].auth,token);assert.equal(reads[0].cookie,undefined);
|
||||
assert.equal(new URLSearchParams(reads[0].search).get('pageNum'),'2');assert.equal(new URLSearchParams(reads[0].search).get('pageSize'),'24');
|
||||
}
|
||||
const response=await fetch(base+'/api/playlet/like/list?pageNum=3&pageSize=24',{headers:{Cookie:'drama_session=registered-user-a-secret; drama_registered=1'}});
|
||||
assert.equal(response.status,200);assert.match(response.headers.get('cache-control'),/no-store/);
|
||||
assert.equal((await response.json()).code,200);assert.equal(calls.at(-1).auth,'registered-user-a-secret');
|
||||
}finally{accountListPayload=undefined;strictContract=false;}
|
||||
});
|
||||
|
||||
test('likes distinguish empty records, out-of-range pages, backend failures and expired accounts',async()=>{
|
||||
const headers={Cookie:'drama_session=registered-user-a-secret; drama_registered=1'};
|
||||
try{
|
||||
accountListPayload={list:[],totalCount:0};
|
||||
const emptyResponse=await fetch(base+'/member/likes/',{headers}),emptyHtml=await emptyResponse.text();
|
||||
assert.equal(emptyResponse.status,200);assert.ok(emptyHtml.includes('暂无点赞的短剧'));assert.ok(emptyHtml.includes('href="/shorts/">去发现好剧</a>'));
|
||||
assert.equal((await fetch(base+'/member/likes/page/2/',{headers})).status,404);
|
||||
responseOverrides={'playlet/like/list':{status:503,code:503}};
|
||||
const failed=await fetch(base+'/member/likes/',{headers}),failedHtml=await failed.text();
|
||||
assert.equal(failed.status,500);assert.match(failedHtml,/<meta name="robots" content="noindex/);assert.ok(!failedHtml.includes('暂无点赞的短剧'));
|
||||
responseOverrides={};
|
||||
const start=calls.length;
|
||||
const expiredResponse=await fetch(base+'/member/likes/',{headers:{Cookie:'drama_session=expired; drama_registered=1'}}),expiredHtml=await expiredResponse.text();
|
||||
assert.ok(expiredHtml.includes('请先登录账号'));assert.ok(expiredHtml.includes('data-auth-mode="login"'));
|
||||
assert.ok(!calls.slice(start).some(call=>['/api/app/auth/guest','/api/app/playlet/like/list'].includes(call.path)));
|
||||
}finally{accountListPayload=undefined;responseOverrides={};}
|
||||
});
|
||||
|
||||
test('private settings isolate two users and ignore forged SSR identity headers',async()=>{
|
||||
const start=calls.length;
|
||||
for(const [token,name,other] of [['registered-user-a-secret','用户甲','用户乙'],['registered-user-b-secret','用户乙','用户甲']]){
|
||||
const forged=Buffer.from(JSON.stringify({userId:999,nickName:'伪造管理员'})).toString('base64url');
|
||||
const response=await fetch(base+'/member/settings/',{headers:{Cookie:'drama_session='+token+'; drama_registered=1','x-drama-ssr-session':'ready','x-drama-ssr-user':forged}});
|
||||
const html=await response.text(),main=currentMain(html,'info-page');assert.ok(main.includes(name));assert.ok(!html.includes(other));assert.ok(!html.includes('伪造管理员'));
|
||||
for(const secret of [token,'must-not-leak'])assert.ok(!html.includes(secret));assert.ok(!main.includes('api-loading'));
|
||||
}
|
||||
assert.equal(calls.slice(start).filter(call=>call.path==='/api/app/auth/getInfo').length,2);assert.ok(!calls.slice(start).some(call=>call.path==='/api/app/auth/guest'));
|
||||
});
|
||||
|
||||
test('registered expiry and explicit logout render sign-in immediately without guest downgrade',async()=>{
|
||||
for(const cookie of ['drama_session=expired; drama_registered=1','drama_skip_auto=1']){
|
||||
const start=calls.length,response=await fetch(base+'/member/settings/',{headers:{Cookie:cookie}}),html=await response.text(),main=currentMain(html,'info-page');
|
||||
assert.ok(main.includes('请先登录账号'));assert.ok(main.includes('data-auth-mode="login"'));assert.ok(!main.includes('id="profile-form"'));assert.ok(!main.includes('api-loading'));
|
||||
assert.ok(!calls.slice(start).some(call=>call.path==='/api/app/auth/guest'));
|
||||
}
|
||||
});
|
||||
|
||||
test('wallet SSR contains configured packages and transactions without creating an order or leaking extra fields',async()=>{
|
||||
try{
|
||||
packagesPayload={coinList:[{id:91,name:'真实金币套餐',price:1000,baseCoin:100,giftCoin:5,totalCoin:105,packageType:2,payMethods:[{type:'alipay',typeName:'支付宝'}],token:'private-package-token'}],vipList:[]};
|
||||
transactionsPayload={list:[{title:'真实交易明细',changeAmount:25,createdAt:'2026-10-21 10:30:00',token:'private-transaction-token'}],totalCount:18};
|
||||
const start=calls.length,response=await fetch(base+'/info/deposit/'),html=await response.text(),main=currentMain(html,'info-page');
|
||||
assert.ok(main.includes('100金币'));assert.ok(main.includes('真实交易明细'));assert.ok(main.includes('href="/info/deposit/page/2/"'));assert.ok(!/api-loading|正在加载|加载中/.test(main));
|
||||
for(const secret of ['private-package-token','private-transaction-token','test-session-secret'])assert.ok(!html.includes(secret));
|
||||
const reads=calls.slice(start);assert.equal(reads.filter(call=>call.path==='/api/app/recharge/package/list').length,1);assert.equal(reads.filter(call=>call.path==='/api/app/coin/trans/list').length,1);
|
||||
assert.ok(!reads.some(call=>['/api/app/coin/buy','/api/app/vip/pay','/api/app/playlet/chapter/unlock'].includes(call.path)));
|
||||
}finally{packagesPayload=undefined;transactionsPayload=undefined;}
|
||||
});
|
||||
|
||||
|
||||
test('header and complete page content share the server-prepared identity before scripts run',async()=>{
|
||||
const start=calls.length,response=await fetch(base+'/shorts/'),html=await response.text();
|
||||
assert.ok(html.indexOf('id="global-site-header"')<html.indexOf('<main class="shell"'));
|
||||
@@ -1352,34 +1159,32 @@ test('slug details and episodes render directly with numeric dependent reads, sl
|
||||
strictContract=true;
|
||||
detailSeo={slug:'city-story',title:'都市故事',desc:'作品独立介绍',tagNames:['都市'],tagIds:[9],tagSlugs:['urban'],onlineAt:'2026-10-20'};
|
||||
chapterPayload={id:90001,playletId:23456,currentEpisode:3,canWatch:true,videoUrl:'https://media.drama.test/3.mp4'};
|
||||
commentPayload={list:[],totalCount:25};
|
||||
for(const [path,chapter] of [['/detail/city-story/',false],['/play/city-story/90001/',true]]){
|
||||
const start=calls.length,response=await fetch(base+path,{redirect:'manual'}),html=await response.text();
|
||||
assert.equal(response.status,200,path);assert.equal(response.headers.get('location'),null);
|
||||
const main=currentMain(html,'drama-play-page');assert.ok(main.includes('都市故事'));
|
||||
assert.ok(main.includes('href="/play/city-story/90001/"'));
|
||||
assert.ok(main.includes(`href="${path}page/2/"`));
|
||||
assert.ok(html.includes(`rel="canonical" href="https://default.drama.test${path}"`));
|
||||
assert.ok(html.includes(`rel="canonical" href="https://huangguocg.com${path}"`));
|
||||
const reads=calls.slice(start),detail=reads.filter(call=>call.path==='/api/app/playlet/detail');assert.equal(detail.length,1);
|
||||
assert.equal(new URLSearchParams(detail[0].search).get('slug'),'city-story');assert.equal(new URLSearchParams(detail[0].search).has('id'),false);
|
||||
assert.equal(new URLSearchParams(reads.find(call=>call.path==='/api/app/playlet/chapter/list').search).get('playletSlug'),'city-story');
|
||||
assert.equal(new URLSearchParams(reads.find(call=>call.path==='/api/app/playlet/guess-like/list').search).get('playletId'),'23456');
|
||||
assert.equal(new URLSearchParams(reads.find(call=>call.path==='/api/app/comment/list').search).get('objectId'),chapter?'90001':'23456');
|
||||
assert.ok(!reads.some(call=>call.path==='/api/app/comment/list'));
|
||||
if(chapter){assert.ok(main.includes('href="/tag/urban/"'));assert.ok(main.includes('href="/detail/city-story/"'));}
|
||||
const graph=JSON.parse(html.match(/<script\b[^>]*data-site-json-ld=""[^>]*>([\s\S]*?)<\/script>/)[1])['@graph'];
|
||||
assert.equal(graph.find(node=>node['@type']==='TVSeries').url,'https://default.drama.test/detail/city-story/');
|
||||
if(chapter)assert.equal(graph.find(node=>node['@type']==='TVEpisode').url,'https://default.drama.test/play/city-story/90001/');
|
||||
assert.equal(graph.find(node=>node['@type']==='TVSeries').url,'https://huangguocg.com/detail/city-story/');
|
||||
if(chapter)assert.equal(graph.find(node=>node['@type']==='TVEpisode').url,'https://huangguocg.com/play/city-story/90001/');
|
||||
}
|
||||
const legacy=await fetch(base+'/detail/23456/',{redirect:'manual'}),html=await legacy.text();
|
||||
assert.equal(legacy.status,200);assert.equal(legacy.headers.get('location'),null);
|
||||
assert.ok(html.includes('rel="canonical" href="https://default.drama.test/detail/city-story/"'));assert.ok(html.includes('href="/detail/city-story/page/2/"'));
|
||||
assert.ok(html.includes('rel="canonical" href="https://huangguocg.com/detail/city-story/"'));
|
||||
detailSeo={...detailSeo,slug:'都市 / 剧?#%2F'};
|
||||
const path='/play/'+encodeURIComponent(detailSeo.slug)+'/90001/';
|
||||
const special=await fetch(base+path);assert.equal(special.status,200);
|
||||
const specialHtml=await special.text();
|
||||
assert.ok(specialHtml.includes('rel="canonical" href="https://default.drama.test/play/23456/90001/"'));
|
||||
assert.ok(specialHtml.includes('rel="canonical" href="https://huangguocg.com/play/23456/90001/"'));
|
||||
assert.ok(specialHtml.includes('href="/play/23456/90001/"'));
|
||||
}finally{strictContract=false;detailSeo=chapterPayload=commentPayload=undefined;}
|
||||
}finally{strictContract=false;detailSeo=chapterPayload=undefined;}
|
||||
});
|
||||
|
||||
test('home, topics, aliases, tags, more and search generate slug-first destinations and serve refreshed slug URLs',async()=>{
|
||||
@@ -1395,7 +1200,7 @@ test('home, topics, aliases, tags, more and search generate slug-first destinati
|
||||
assert.equal(response.status,200,path);assert.equal(response.headers.get('location'),null);
|
||||
if(path==='/')for(const href of ['/detail/city-story/','/topics/urban-topic/','/config/editors-choice/'])assert.ok(html.includes(`href="${href}"`));
|
||||
if(path.startsWith('/topics/urban')||path.startsWith('/theme/')){
|
||||
assert.ok(html.includes(`rel="canonical" href="https://default.drama.test${path.replace('/theme/','/topics/')}"`));
|
||||
assert.ok(html.includes(`rel="canonical" href="https://huangguocg.com${path.replace('/theme/','/topics/')}"`));
|
||||
const query=new URLSearchParams(calls.slice(start).find(call=>call.path==='/api/app/playlet/topic/detail').search);
|
||||
assert.equal(query.get('id'),'7');assert.equal(query.has('slug'),false);
|
||||
}
|
||||
@@ -1419,12 +1224,12 @@ test('missing slugs stay real 404 responses and topic directory outages stay ser
|
||||
}finally{detailSeo=listPayload=undefined;responseOverrides={};}
|
||||
});
|
||||
|
||||
test('detail, episode, chapter list, comments and recommendations are requested before detail finishes',async()=>{
|
||||
test('detail, episode, chapter list and recommendations are requested before detail finishes',async()=>{
|
||||
let release;blockedDetail={promise:new Promise(resolve=>release=resolve)};
|
||||
const start=calls.length;
|
||||
const pending=fetch(base+'/play/23456/90001/');
|
||||
try{
|
||||
const paths=['playlet/detail','playlet/chapter/detail','playlet/chapter/list','comment/list','playlet/guess-like/list'].map(path=>'/api/app/'+path);
|
||||
const paths=['playlet/detail','playlet/chapter/detail','playlet/chapter/list','playlet/guess-like/list'].map(path=>'/api/app/'+path);
|
||||
for(let attempt=0;attempt<100;attempt++){
|
||||
if(paths.every(path=>calls.slice(start).some(call=>call.path===path)))break;
|
||||
await delay(10);
|
||||
@@ -1434,7 +1239,7 @@ test('detail, episode, chapter list, comments and recommendations are requested
|
||||
const html=await (await pending).text();assert.ok(currentMain(html,'drama-play-page').includes('play-detail-title'));
|
||||
});
|
||||
|
||||
test('public payloads are reused across page requests while two accounts and permissions stay uncached',async()=>{
|
||||
test('public payloads are reused across page requests while permissions stay uncached',async()=>{
|
||||
await stopNext();await startNext({PUBLIC_DATA_CACHE_TTL_MS:'60000'});
|
||||
const previousHeader=headerPayload;
|
||||
try{
|
||||
@@ -1448,12 +1253,6 @@ test('public payloads are reused across page requests while two accounts and per
|
||||
assert.equal(calls.slice(start).filter(call=>call.path==='/api/app/playlet/home').length,1);
|
||||
headerPayload={categoryList:[]};
|
||||
assert.ok(navigationHtml(await (await fetch(base+'/shorts/')).text(),'top').includes('短剧'),'public navigation remains valid within TTL');
|
||||
const privateStart=calls.length;
|
||||
for(const [token,name,other] of [['registered-user-a-secret','用户甲','用户乙'],['registered-user-b-secret','用户乙','用户甲']]){
|
||||
const html=await (await fetch(base+'/member/settings/',{headers:{Cookie:'drama_session='+token+'; drama_registered=1'}})).text();
|
||||
assert.ok(currentMain(html,'info-page').includes(name));assert.ok(!html.includes(other));
|
||||
}
|
||||
assert.equal(calls.slice(privateStart).filter(call=>call.path==='/api/app/auth/getInfo').length,2);
|
||||
const permissionStart=calls.length;
|
||||
await (await fetch(base+'/play/23456/90001/')).text();await (await fetch(base+'/play/23456/90001/')).text();
|
||||
for(const path of ['playlet/detail','playlet/chapter/detail'])assert.equal(calls.slice(permissionStart).filter(call=>call.path==='/api/app/'+path).length,2,path+' must stay realtime');
|
||||
@@ -1463,7 +1262,7 @@ test('public payloads are reused across page requests while two accounts and per
|
||||
}
|
||||
});
|
||||
|
||||
test('current-origin deployments render SSR and proxy account, API and signed media requests through the current site',async()=>{
|
||||
test('current-origin deployments render SSR and proxy guest, API and signed media requests through the current site',async()=>{
|
||||
await stopNext();await startNext({UPSTREAM_API_URL:'current'});
|
||||
// Emulate Nginx: preserve the visitor Host and route only backend paths to Go.
|
||||
const ingress=createServer((request,response)=>{
|
||||
@@ -1485,8 +1284,8 @@ test('current-origin deployments render SSR and proxy account, API and signed me
|
||||
const config=await fetch(origin+'/api/config/info');assert.equal(config.status,200);assert.equal((await config.json()).code,200);
|
||||
const session=await fetch(origin+'/api/auth/guest',{method:'POST',headers:{'Content-Type':'application/json',Origin:origin},body:'{}'});
|
||||
assert.equal(session.status,200);assert.match(session.headers.get('set-cookie'),/drama_session=/);assert.equal((await session.json()).code,200);
|
||||
const privatePage=await fetch(origin+'/member/settings/',{headers:{Cookie:'drama_session=registered-user-a-secret; drama_registered=1'}});
|
||||
assert.equal(privatePage.status,200);assert.ok(currentMain(await privatePage.text(),'info-page').includes('用户甲'));
|
||||
const detailPage=await fetch(origin+'/detail/23456/');
|
||||
assert.equal(detailPage.status,200);assert.ok((await detailPage.text()).includes('接口剧名'));
|
||||
assert.equal(calls.at(-1).cookie,undefined);
|
||||
const key=await fetch(origin+'/enkey?id=12700&ticket=a%2Fb+1');
|
||||
assert.equal(key.status,200);assert.equal((await key.arrayBuffer()).byteLength,16);
|
||||
|
||||
Reference in New Issue
Block a user