import { test, before, after } from "node:test";
import assert from "node:assert/strict";
import { createServer, request as httpRequest } from "node:http";
import { spawn } from "node:child_process";
import { setTimeout as delay } from "node:timers/promises";
import { createHash } from "node:crypto";
import { rm, readFile } from "node:fs/promises";
const appContract = JSON.parse(
await readFile(
new URL("./fixtures/sgdj-app-contract.json", import.meta.url),
"utf8",
),
).operations;
let strictContract = false;
import { join } from "node:path";
let mock,
next,
base,
output = "";
let mockOrigin,
seoSnapshotPath,
configFailure = "";
let downloadAddress = "https://example.com/download";
let seoConfig = {},
categories = [],
listPayload,
detailSeo;
let chapterPayload, videoPayload, chapterListPayload, commentPayload;
let tagOptionsPayload;
let requireSeoAuth = false,
searchFailure = false,
searchPayload;
let responseOverrides = {};
let upstreamKeySlash = false;
const defaultHeader = {
categoryList: [
{ id: 1, name: "首页", urlValue: "", categoryType: 1 },
{ id: 2, name: "短剧", urlValue: "/shorts/", categoryType: 1 },
{ id: 3, name: "专题", urlValue: "/topics/", categoryType: 1 },
{ id: 4, name: "热榜", urlValue: "/hot", categoryType: 1 },
{ id: 5, name: "回家的路", urlValue: "", categoryType: 1 },
{ id: 6, name: "福利应用", urlValue: "", categoryType: 1 },
],
};
let headerPayload = defaultHeader,
headerFailure = false;
let appsPayload, packagesPayload, accountListPayload, transactionsPayload;
let blockedDetail, footerPayload, themePayload, richTextPayload;
const calls = [];
const testEnvironment =
process.env.TEST_APP_ENV === "test" ? "test" : "production";
const testBuildDirectory = testEnvironment === "test" ? ".next-test" : ".next";
const fixture = {
id: 23456,
title: "接口剧名 ",
cover: "https://example.com/poster.webp",
totalEpisode: 7,
countLike: 1234,
tagNames: ["都市"],
showOnBanner: true,
};
let homePayload = {
homeBannerData: [fixture],
homeConfigData: [
{ id: 31, status: true, name: "后台配置标题", type: 1, list: [fixture] },
],
};
const playlist =
'#EXTM3U\n#EXT-X-KEY:METHOD=AES-128,URI="/enkey?token=a%2Fb"\n#EXTINF:2,\nhttps://cdn.example/segment.ts?md=signed+value\n#EXT-X-ENDLIST\n';
async function listen(server) {
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
return server.address().port;
}
function assertPageLinksHaveTrailingSlash(html) {
for (const match of html.matchAll(
/]*\bhref="(\/[^"#?]*)(?:[?#][^"]*)?"/g,
))
assert.ok(match[1].endsWith("/"), match[1]);
}
async function startNext(environment = {}) {
const reservation = createServer();
const port = await listen(reservation);
await new Promise((resolve) => reservation.close(resolve));
base = `http://127.0.0.1:${port}`;
output = "";
next = spawn(
process.execPath,
[
"node_modules/next/dist/bin/next",
"start",
"--hostname",
"0.0.0.0",
"--port",
String(port),
],
{
cwd: process.cwd(),
env: {
...process.env,
APP_ENV: testEnvironment,
APP_DIST_DIR: testBuildDirectory,
UPSTREAM_API_URL: mockOrigin,
API_BASE_URL: "http://127.0.0.1:1",
PUBLIC_DATA_CACHE_TTL_MS: "0",
...environment,
},
stdio: ["ignore", "pipe", "pipe"],
},
);
next.stdout.on("data", (chunk) => {
output += chunk.toString();
});
next.stderr.on("data", (chunk) => {
output += chunk.toString();
});
for (let attempt = 0; attempt < 60; attempt++) {
try {
if ((await fetch(base + "/favicon.png")).ok) return;
} catch {}
if (next.exitCode !== null) throw new Error(output);
await delay(100);
}
throw new Error("Test server did not start: " + output);
}
async function stopNext() {
if (!next || next.exitCode !== null) return;
const stopped = new Promise((resolve) => next.once("exit", resolve));
next.kill("SIGTERM");
await stopped;
}
test("deployment health endpoint bypasses guest creation, avoids redirects and never caches", async () => {
for (const path of ["/healthz", "/healthz/"]) {
const previous = calls.length;
const response = await fetch(base + path, { redirect: "manual" });
assert.equal(response.status, 200);
assert.equal(response.headers.get("set-cookie"), null);
assert.match(response.headers.get("cache-control"), /no-store/);
assert.match(response.headers.get("x-robots-tag"), /noindex/);
assert.deepEqual(await response.json(), {
status: "ok",
environment: testEnvironment,
application: "jha-1050-jgdj-web",
});
assert.equal(calls.length, previous);
}
const head = await fetch(base + "/healthz", {
method: "HEAD",
redirect: "manual",
});
assert.equal(head.status, 200);
assert.equal(await head.text(), "");
});
before(
async () => {
mock = createServer(async (request, response) => {
let raw = "";
for await (const chunk of request) raw += chunk;
const url = new URL(request.url, "http://local");
calls.push({
path: url.pathname,
search: url.search,
method: request.method,
auth: request.headers.authorization,
cookie: request.headers.cookie,
range: request.headers.range,
device: request.headers["x-user-agent"],
ua: request.headers["user-agent"],
accept: request.headers.accept,
contentType: request.headers["content-type"],
ip: request.headers["x-real-ip"],
forwarded: request.headers["x-forwarded-for"],
sdk: request.headers["x-sdk-common"],
trackerId: request.headers["x-tracker-request-id"],
trackerEvent: request.headers["x-tracker-event-name"],
trackerContext: request.headers["x-dc-request-context"],
body: raw,
});
if (url.pathname.startsWith("/h5/m3u8/")) {
if (url.pathname.endsWith("redirect.m3u8")) {
response.writeHead(302, { Location: "/unexpected" });
response.end();
return;
}
if (url.pathname.endsWith("missing.m3u8")) {
response.writeHead(404);
response.end();
return;
}
if (url.pathname.endsWith("denied.m3u8")) {
response.writeHead(200, { "Content-Type": "application/json" });
response.end(
JSON.stringify({ code: 1000, msg: "expired signed URL" }),
);
return;
}
if (url.pathname.endsWith("invalid.m3u8")) {
response.writeHead(200, { "Content-Type": "text/html" });
response.end("Error");
return;
}
if (url.pathname.endsWith(".ts")) {
response.writeHead(206, {
"Content-Type": "application/octet-stream",
"Content-Range": "bytes 0-2/10",
"Accept-Ranges": "bytes",
});
response.end(Buffer.from([9, 8, 7]));
return;
}
response.writeHead(200, { "Content-Type": "application/octet-stream" });
response.end(playlist);
return;
}
if (
["/enkey", "/enkey/", "/newenkey", "/newenkey/"].includes(url.pathname)
) {
const canonical =
url.pathname.replace(/\/$/, "") + (upstreamKeySlash ? "/" : "");
if (url.pathname !== canonical) {
response.writeHead(301, { Location: canonical + url.search });
response.end();
return;
}
response.writeHead(200, { "Content-Type": "application/octet-stream" });
response.end(
url.searchParams.has("invalid")
? Buffer.from(JSON.stringify({ code: 1000, msg: "bad key" }))
: Buffer.from(Array.from({ length: 16 }, (_, index) => index)),
);
return;
}
const path = url.pathname.replace("/api/app/", "");
if (Object.hasOwn(responseOverrides, path)) {
const {
status = 200,
code = 200,
data = null,
raw: overrideRaw,
} = responseOverrides[path];
response.writeHead(status, { "Content-Type": "application/json" });
response.end(overrideRaw ?? JSON.stringify({ code, data }));
return;
}
if (path === "playlet/detail" && blockedDetail)
await blockedDetail.promise;
if (strictContract) {
const op = appContract[request.method + " " + path];
const invalid =
!op ||
[...url.searchParams.keys()].some((key) => !op.query.includes(key)) ||
op.requiredQuery.some((key) => !url.searchParams.has(key));
if (invalid) {
response.writeHead(400, { "Content-Type": "application/json" });
response.end(
JSON.stringify({
code: 400,
msg: "Undocumented endpoint or query",
data: null,
}),
);
return;
}
}
let data = { list: [], totalCount: 0 };
let code = 200;
if (path === "auth/guest")
data = {
token: "test-session-secret",
userId: 77,
nickName: "真实接口用户",
type: 0,
};
else if (path === "auth/login") {
const body = JSON.parse(raw);
if (body.account === "wrong") code = 1000;
data = {
...(body.account === "missing-token"
? {}
: { token: "test-registered-secret" }),
userId: 88,
nickName: "正式用户",
type: 1,
isRegister: true,
};
} else if (path === "auth/register")
data = {
token: "test-bound-secret",
userId: 77,
nickName: "绑定游客",
type: 1,
isRegister: true,
};
else if (path === "auth/getInfo") {
if (request.headers.authorization === "expired") code = 1000;
else if (request.headers.authorization === "test-session-secret")
data = { userId: 77, nickName: "真实接口用户", type: 0, recharge: 90 };
else if (request.headers.authorization === "test-registered-secret")
data = { userId: 88, nickName: "正式用户", type: 1 };
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: "禁果短剧" },
...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 === "navigation/header") {
data = headerPayload;
if (headerFailure) code = 500;
} else if (path === "config/footer/info" && footerPayload)
data = footerPayload;
else if (path === "config/richtext/list") {
const titles = new Set(
(url.searchParams.get("titles") || "")
.split(",")
.map((item) => item.trim())
.filter(Boolean),
);
const list = Array.isArray(richTextPayload) ? richTextPayload : [];
data = {
list: titles.size
? list.filter((item) => titles.has(item.title))
: list,
};
}
else if (path === "translation/category/list")
data = { categoryList: categories };
else if (path === "theme/list")
data = themePayload || {
list: [7, 8, 10404].map((id) => ({
id,
name: "题材" + id,
slug: id === 7 ? "urban-topic" : undefined,
})),
};
else if (path === "applist/list" && appsPayload) data = appsPayload;
else if (path === "recharge/package/list" && packagesPayload)
data = packagesPayload;
else if (
[
"playlet/history/list",
"playlet/collect/list",
"playlet/like/list",
"playlet/bought/list",
].includes(path) &&
accountListPayload
)
data =
typeof accountListPayload === "function"
? accountListPayload(path, request)
: accountListPayload;
else if (path === "coin/trans/list" && transactionsPayload)
data = transactionsPayload;
else if (path === "playlet/home") data = homePayload;
else if (path === "playlet/home-config/list") {
const modules =
homePayload.modules ||
(homePayload.homeConfigData || [])
.filter((block) => block && block.status)
.map((block) => ({
id: block.id,
slug: block.slug,
title: block.name,
desc: block.desc,
moduleType: block.type === 3 ? 2 : 1,
list: block.list,
topicList: block.themeList,
}));
const module = modules.find((item) =>
url.searchParams.has("slug")
? item.slug === url.searchParams.get("slug")
: item.id === Number(url.searchParams.get("id")),
);
if (!module) {
code = 5009;
data = null;
} else {
const page = Number(url.searchParams.get("pageNum") || 1),
size = Number(url.searchParams.get("pageSize") || 24),
items =
(module.moduleType === 2 ? module.topicList : module.list) || [];
data = {
config: module,
list:
module.moduleType === 1
? items.slice((page - 1) * size, page * size)
: [],
topicList:
module.moduleType === 2
? items.slice((page - 1) * size, page * size)
: [],
totalCount: items.length,
pageNum: page,
pageSize: size,
};
}
} else if (path === "playlet/tag/options")
data = tagOptionsPayload || { list: [{ id: 9, name: "测试标签" }] };
else if (path === "playlet/search")
data = JSON.stringify(
searchPayload || {
list: [fixture],
totalCount: 1,
pageNum: 1,
pageSize: 24,
},
);
else if (path === "playlet/guess-like/list")
data = {
list: [{ ...fixture, id: 23458, title: "同类推荐短剧" }],
totalCount: 1,
};
else if (path === "playlet/detail")
data = {
...fixture,
...detailSeo,
isCollect: 2,
isLike: 2,
playletChapterInfo: [
{ id: 90001, currentEpisode: 1, canWatch: true },
],
};
else if (path === "video/detail" && videoPayload) data = videoPayload;
else if (
listPayload &&
[
"playlet/hot/rank/list",
"playlet/topic/list",
"playlet/topic/detail",
"playlet/all/list",
"playlet/operate/list",
"playlet/tag/list",
].includes(path)
)
data =
typeof listPayload === "function"
? listPayload(path, url)
: listPayload;
else if (path === "playlet/chapter/list")
data = chapterListPayload || {
list: [{ id: 90001, currentEpisode: 1, canWatch: true }],
};
else if (path === "playlet/chapter/detail")
data = chapterPayload || {
id: 90001,
playletId: 23456,
currentEpisode: 1,
canWatch: false,
needUnlock: true,
singleCoins: 13,
balance: 90,
};
else if (path === "playlet/chapter/unlock") {
const body = JSON.parse(raw);
data = { ...body, balance: 77 };
} else if (path === "playlet/collect/set") {
if (
request.headers.authorization === "expired" ||
JSON.parse(raw).id === 999
)
code = 1000;
else data = { id: 23456, isCollect: 1, countCollect: 1 };
} else if (path === "comment/list")
data = commentPayload || { list: [], totalCount: 0 };
if (path === "playlet/topic/detail" && !data.id) {
const slug = url.searchParams.get("topicSlug");
if (slug) {
const topic = listPayload?.list?.find((item) => item.slug === slug);
if (topic) data = { ...data, ...topic };
else {
code = 5009;
data = null;
}
} else data = { ...data, id: Number(url.searchParams.get("id")) };
}
if (path === "playlet/search" && searchFailure) {
code = 5100;
data = null;
}
if (path === "config/info" && configFailure) {
if (configFailure === "http" || configFailure === "timeout") {
if (configFailure === "timeout") await delay(5200);
response.writeHead(503, { "Content-Type": "application/json" });
response.end(JSON.stringify({ code: 503, data: null }));
return;
}
if (configFailure === "business") code = 500;
if (configFailure === "malformed") data = null;
}
if (
requireSeoAuth &&
["playlet/detail", "comment/list", "playlet/search"].includes(path) &&
!request.headers.authorization
)
code = 1000;
response.writeHead(200, { "Content-Type": "application/json" });
response.end(
JSON.stringify({
code,
data,
msg: code === 200 ? "success" : "session expired",
}),
);
});
const mockPort = await listen(mock);
mockOrigin = `http://127.0.0.1:${mockPort}`;
seoSnapshotPath = join(
process.cwd(),
testBuildDirectory,
"cache",
"seo",
createHash("sha256").update(mockOrigin).digest("hex") + ".json",
);
await rm(seoSnapshotPath, { force: true });
await startNext();
},
{ timeout: 15000 },
);
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 () => {
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(/([\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(!/"));
assert.ok(!html.includes("data-must-watch-sort"));
assert.ok(!html.includes("人人嫌猎户粗野"));
assert.ok(html.includes('href="/shorts/"'));
assert.ok(html.includes('href="/detail/23456/"'));
assert.ok(html.includes('href="/config/31/"'));
assert.ok(html.includes('data-src="https://example.com/poster.webp"'));
assert.ok(html.includes('src="/placeholder.png"'));
assert.ok(
!/
]*\ssrc="https:\/\/example.com\/poster.webp"/.test(html),
);
assert.ok(
!/href="#(?:page|info|member|detail|play|video|search)\//.test(html),
);
assert.ok(!html.includes('id="entry-navigation"'));
assert.ok(!html.includes("entry-navigation-pending"));
assert.match(
html,
/