import assert from "node:assert/strict"; import { spawn } from "node:child_process"; import { createHash, randomBytes } from "node:crypto"; import { createServer, type IncomingHttpHeaders } from "node:http"; import { mkdir, readFile, readdir, stat, unlink, writeFile, } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { gunzipSync, gzipSync } from "node:zlib"; import { chromium, type Page } from "@playwright/test"; // Test the optimized App Router response, not a renderer or a development server. // All authentication, content and mutations terminate at this loopback mock. type RecordData = Record; interface UpstreamCall { case: string; path: string; headers: IncomingHttpHeaders; body: RecordData; cookies: string[]; } const output = process.env.NATIVE_SSR_OUTPUT || "/tmp/hkdj-native-ssr-smoke"; const calls: UpstreamCall[] = []; const results: RecordData[] = []; async function expectPageStyles(page: Page, required: string[], absent: string[] = []) { const tokens = new Set((await page.locator("html").getAttribute("data-hk-page-styles") || "").split(/\s+/)); for (const token of required) assert.ok(tokens.has(token), `Missing page style scope: ${token}`); for (const token of absent) assert.ok(!tokens.has(token), `Inactive page style scope: ${token}`); assert.equal(await page.locator('link[rel="stylesheet"][href^="/static/css/"]').count(), 0); assert.ok(await page.locator('link[rel="stylesheet"][href*="/_next/"][href$=".css"]').count() > 0, "Next serves compiled CSS chunks"); } const canonical = "https://canonical.drama.test"; const userAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) NativeSSRSmoke/1.0"; const indexNow = "native_ssr_verification_20261010"; const verifyFilename = `native-ssr-${randomBytes(6).toString("hex")}.html`; const verifyPath = resolve("public", verifyFilename); const sitemapDir = join(output, "sitemaps"); const handoffDir = join( tmpdir(), "huakui-next-ssr", createHash("sha256") .update(process.cwd() + "|production") .digest("hex") .slice(0, 16), ); let previewLogs = ""; let browser: Awaited> | undefined; let navigationPreview: ReturnType | undefined; let fixtureSequence = 0; let bigList = false; let listUnavailable = false; // The account system was removed; SSR and the BFF must never reach these upstream APIs. const removedUpstream = [ "/api/app/auth/login", "/api/app/auth/register", "/api/app/auth/logout", "/api/app/user/center/data", "/api/app/playlet/history/list", "/api/app/playlet/collect/list", "/api/app/recharge/package/list", "/api/app/comment/send", "/api/app/comment/list", "/api/app/coin/buy", "/api/app/playlet/chapter/unlock", ]; const removedCalls = (caseName?: string) => calls.filter( (call) => (!caseName || call.case === caseName) && removedUpstream.some((path) => call.path.split("?")[0] === path), ); function user(token: string) { const guest = token.startsWith("guest-"); const suffix = token.replace(/[^a-zA-Z0-9]/g, ""); return { userId: guest ? 900 : token === "registered-B" ? 802 : 801, token, accountNo: "account_" + suffix, nickName: "用户_" + suffix, isRegister: !guest, type: guest ? 0 : 1, hasPassword: !guest, recharge: guest ? 18 : 88, avatar: "", vipType: 0, currentVipExpire: "", }; } function work(id = 41, title = "隔离短剧") { return { id, slug: "isolated-" + id, title, playName: title, cover: "/static/favicon.png", totalEpisode: 2, updateStatus: 2, countBrowse: 48, countLike: 4, countCollect: 2, status: 1, themeNames: ["都市"], themeIds: [1], themeSlugs: ["city"], tagNames: ["剧情"], tagIds: [1], tagSlugs: ["plot"], }; } function readToken(headers: IncomingHttpHeaders): string { return String(headers.authorization || ""); } function nameFor(token: string): string { return user(token).nickName; } const mock = createServer(async (req, res) => { try { const url = new URL(req.url || "/", "http://upstream"); const chunks: Buffer[] = []; for await (const chunk of req) chunks.push(Buffer.from(chunk)); const body = JSON.parse( Buffer.concat(chunks).toString() || "{}", ) as RecordData; const caseName = String(req.headers["x-smoke-case"] || "unmarked"); const sequence = ++fixtureSequence; const cookies = [ `wire_${sequence}=${caseName}; Path=/wire/${sequence}; HttpOnly; SameSite=Lax`, `wire_root_${sequence}=ok; Path=/; Expires=Wed, 21 Oct 2037 07:28:00 GMT; SameSite=Lax`, ]; calls.push({ case: caseName, path: url.pathname + url.search, headers: { ...req.headers }, body, cookies, }); res.setHeader("Set-Cookie", cookies); const send = (data: unknown = {}, code = 200, status = 200) => { res.statusCode = status; res.setHeader("Content-Type", "application/json"); res.end( JSON.stringify({ code, data, tip: code === 200 ? "" : "隔离测试上游失败", }), ); }; const token = readToken(req.headers); if (url.pathname === "/api/app/auth/guest") { send(user("guest-" + caseName)); return; } if (url.pathname === "/api/app/auth/getInfo") { send(user(token)); return; } if (url.pathname === "/api/app/config/info") { send({ config: { appAddr: "https://download.example.test/real-entry", gmailAddr: "support@example.test", foreverUrlAddr: canonical + "/ignored/path?tracking=1", }, newAddressInfo: {}, seoInfo: { title: "花魁短剧", description: "隔离站点描述", keywords: ["花魁短剧", "短剧"], webSiteName: "花魁短剧", canonicalUrl: canonical, }, seoNavInfo: {}, navPageList: [], advitise: {}, }); return; } if (url.pathname === "/api/app/translation/category/list") { send({ categoryList: [ { name: "首页", urlValue: "/", categoryType: 1 }, { name: "短剧", urlValue: "/list/", categoryType: 1 }, ], }); return; } if (url.pathname === "/api/app/theme/list") { send({ list: [{ id: 1, name: "都市", slug: "city", status: true, rank: 1 }], }); return; } if (url.pathname === "/api/app/playlet/home") { send({ seoInfo: { title: "SSR首页标题", description: "SSR首页描述", keywords: ["首屏", "短剧"], }, announcements: [], homeBannerData: [], homeConfigData: [], advitise: {}, }); return; } if (url.pathname === "/api/app/playlet/hot-like") { send({ hotSearch: [], guessLike: [] }); return; } if (url.pathname === "/api/app/playlet/tag/options") { send({ list: [] }); return; } if (url.pathname === "/api/app/playlet/all/list") { if (listUnavailable && Number(url.searchParams.get("pageSize")) === 24) { send({}, 503, 503); return; } const list = bigList && Number(url.searchParams.get("pageSize")) === 24 ? Array.from({ length: 24 }, (_, i) => work( 100 + i, `大快照${i}_` + randomBytes(1100).toString("base64url"), ), ) : [work(), work(42, "隔离短剧二")]; send({ list, pageNum: Number(url.searchParams.get("pageNum") || 1), pageSize: Number(url.searchParams.get("pageSize") || 24), totalCount: 72, seoInfo: { title: "短剧列表专属标题", description: "短剧列表专属描述", keywords: ["列表测试"], }, }); return; } if (url.pathname === "/api/app/playlet/detail") { if (url.searchParams.get("slug") === "missing") { send({}); return; } if (caseName === "recover" && token === "expired-content") { send({}, 1002); return; } send({ ...work(), slug: "isolated", desc: "播放页真实剧情简介", onlineAt: "2026-10-01 10:00:00", preview: "/preview.mp4", playletChapterInfo: [ { id: 401, currentEpisode: 1, payType: 0, canWatch: true }, { id: 402, currentEpisode: 2, payType: 0, canWatch: true }, ], relatedRecommendData: [], seoInfo: { title: "作品标题", description: "作品描述", keywords: ["作品测试"], }, advitise: {}, }); return; } if (url.pathname === "/api/app/playlet/chapter/detail") { send({ id: Number(url.searchParams.get("chapterId") || 401), playletId: 41, currentEpisode: Number(url.searchParams.get("chapterId")) === 402 ? 2 : 1, canWatch: true, payType: 0, videoUrl: "/signed/isolated.m3u8?token=preserved", mediaTime: 12, balance: 18, countLike: 4, countCollect: 2, }); return; } if (url.pathname === "/api/app/playlet/search") { send({ keyword: url.searchParams.get("keyword"), list: [work(), work(42)], totalCount: 2, pageNum: 1, pageSize: 25, }); return; } send({ list: [], totalCount: 0, pageNum: 1, pageSize: 4 }); } catch (error) { res.statusCode = 500; res.end(JSON.stringify({ code: 500, tip: String(error) })); } }); await mkdir(sitemapDir, { recursive: true }); await writeFile( join(sitemapDir, "sitemap.xml"), `${canonical}/sitemaps/sitemap-content.xml\n`, ); await writeFile( join(sitemapDir, "sitemap-content.xml"), `${canonical}/drama/isolated/`, ); await writeFile(verifyPath, "native-ssr-verification-file"); await new Promise((done) => mock.listen(0, "127.0.0.1", done)); const bound = mock.address(); if (!bound || typeof bound === "string") throw new Error("Mock address unavailable"); const reserve = createServer(); await new Promise((done) => reserve.listen(0, "127.0.0.1", done)); const reserved = reserve.address(); if (!reserved || typeof reserved === "string") throw new Error("Preview address unavailable"); const port = reserved.port; await new Promise((done) => reserve.close(() => done())); const origin = `http://127.0.0.1:${port}`; const preview = spawn( process.execPath, [ resolve("node_modules/next/dist/bin/next"), "start", "--port", String(port), "--hostname", "127.0.0.1", ], { env: { ...process.env, NODE_ENV: "production", APP_ENV: "production", UPSTREAM_API_URL: `http://127.0.0.1:${bound.port}`, UPSTREAM_TIMEOUT_MS: "3000", SEO_BAIDU_SITE_VERIFICATION: "native-ssr-baidu-code", SEO_BAIDU_HTML_FILE: verifyFilename, SEO_INDEXNOW_KEY: indexNow, SITEMAP_DIR: sitemapDir, NAVIGATION_ONLY: "false", NEXT_TELEMETRY_DISABLED: "1", }, stdio: ["ignore", "pipe", "pipe"], }, ); preview.stdout?.on("data", (chunk) => { previewLogs += String(chunk); }); preview.stderr?.on("data", (chunk) => { previewLogs += String(chunk); }); function headers(caseName: string, token = ""): Headers { return new Headers({ "x-smoke-case": caseName, "user-agent": userAgent, "x-forwarded-for": "203.0.113.42, 198.51.100.7", "x-user-agent": "DevType=SSR smoke;SysType=h5_ios;sid=original", ...(token ? { cookie: `token=${token}; nickName=STALE_COOKIE; userId=1; isRegister=${token.startsWith("guest-") ? "0" : "1"}; channel=smoke-channel; client_cookie=must_forward`, } : {}), }); } function caseCalls(caseName: string): UpstreamCall[] { return calls.filter((call) => call.case === caseName); } function cookiesForwarded(response: Response, caseName: string) { const actual = response.headers.getSetCookie(); for (const call of caseCalls(caseName)) for (const cookie of call.cookies) { const pair = cookie.split(";")[0]; assert.ok( actual.some((item) => item.split(";")[0] === pair), `${caseName}: missing upstream Set-Cookie ${pair}`, ); } assert.equal( actual.some( (cookie) => cookie.startsWith("wire_root_") && !cookie.includes("21 Oct 2037"), ), false, "Expires comma must not split cookies", ); } function noInternalLeak(response: Response, body: string) { for (const name of response.headers.keys()) assert.equal( /^x-hk-|^x-middleware-request-x-hk-/i.test(name), false, "Internal response header " + name, ); assert.equal( /x-hk-page(?:-id)?["':=]/i.test(body), false, "Internal snapshot serialized to HTML", ); } function noDuplicateBuild(caseName: string, allowedRecovery = false) { const counts = new Map(); for (const call of caseCalls(caseName)) counts.set(call.path, (counts.get(call.path) || 0) + 1); for (const [path, count] of counts) assert.equal( count, allowedRecovery && path.startsWith("/api/app/playlet/detail?") ? 2 : 1, `${caseName}: duplicate upstream ${path}`, ); } function meta(html: string, name: string): string { const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); return ( html.match( new RegExp( `]+(?:name|property)="${escaped}"[^>]+content="([^"]*)"`, "i", ), )?.[1] || "" ); } function link(html: string, rel: string): string { return ( html.match( new RegExp(`]+rel="${rel}"[^>]+href="([^"]*)"`, "i"), )?.[1] || "" ); } function graphs(html: string): RecordData[] { return [ ...html.matchAll( /]+type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/gi, ), ].flatMap((match) => { const parsed = JSON.parse(match[1]); return parsed["@graph"] || [parsed]; }); } async function request( caseName: string, path: string, token = "", init: RequestInit = {}, ) { const requestHeaders = headers(caseName, token); new Headers(init.headers).forEach((value, key) => requestHeaders.set(key, value), ); const response = await fetch(origin + path, { ...init, headers: requestHeaders, redirect: "manual", }); const html = await response.text(); if (!["/robots.txt", "/llms.txt"].includes(path)) cookiesForwarded(response, caseName); noInternalLeak(response, html); return { response, html }; } async function files(): Promise { try { return (await readdir(handoffDir)).filter((name) => /^[a-f0-9]{32}\.json\.gz$/.test(name), ); } catch { return []; } } try { const deadline = Date.now() + 30000; while (true) { if (preview.exitCode !== null) throw new Error("Next exited: " + previewLogs); try { const health = await fetch(origin + "/healthz"); if (health.ok) break; } catch {} if (Date.now() > deadline) throw new Error("Next did not start: " + previewLogs); await new Promise((done) => setTimeout(done, 150)); } console.log("native SSR: initial guest and fresh registered context"); const guest = await request("guest", "/drama/isolated/?dc=smoke-first"); assert.equal(guest.response.status, 200); assert.ok(guest.html.includes(nameFor("guest-guest"))); assert.equal(guest.html.includes("STALE_COOKIE"), false); assert.equal( caseCalls("guest").filter((call) => call.path === "/api/app/auth/guest") .length, 1, ); assert.ok( guest.response.headers .getSetCookie() .some((cookie) => cookie.startsWith("token=guest-guest;")), ); assert.ok( guest.response.headers .getSetCookie() .some((cookie) => cookie.startsWith("channel=smoke-first;")), ); noDuplicateBuild("guest"); assert.deepEqual(removedCalls("guest"), [], "Playback SSR no longer loads recharge packages"); const registered = await request("registered", "/dramas/", "registered-A"); assert.equal(registered.response.status, 200); assert.ok(registered.html.includes(nameFor("registered-A"))); assert.equal(registered.html.includes("STALE_COOKIE"), false); noDuplicateBuild("registered"); for (const call of caseCalls("registered")) { assert.equal(call.headers["x-real-ip"], "203.0.113.42"); assert.equal(call.headers["user-agent"], userAgent); assert.equal( call.headers["x-user-agent"], "DevType=SSR smoke;SysType=h5_ios;sid=original", ); assert.equal( Object.keys(call.headers).some((key) => key.startsWith("x-hk-")), false, ); const privateRequest = /\/auth\//.test( call.path, ); if (privateRequest) { assert.equal(call.headers.authorization, "registered-A"); assert.ok( String(call.headers.cookie).includes("client_cookie=must_forward"), ); } else { assert.equal(call.headers.authorization, undefined); assert.equal(call.headers.cookie, undefined); } } results.push({ initialGuest: true, freshUser: true, completeUpstreamCookies: true, singleBuild: true, headerForwarding: true, publicAuthIsolation: true, }); console.log("native SSR: content session recovery before first byte"); const recover = await request( "recover", "/drama/isolated/", "expired-content", ); assert.equal(recover.response.status, 200); assert.ok(recover.html.includes(nameFor("guest-recover"))); assert.equal(recover.html.includes(nameFor("expired-content")), false); assert.ok( recover.response.headers .getSetCookie() .some((cookie) => cookie.startsWith("token=guest-recover;")), ); assert.equal( caseCalls("recover").filter((call) => call.path === "/api/app/auth/guest") .length, 1, ); noDuplicateBuild("recover", true); const detailCalls = caseCalls("recover").filter((call) => call.path.startsWith("/api/app/playlet/detail?"), ); assert.deepEqual( detailCalls.map((call) => call.headers.authorization), ["expired-content", "guest-recover"], ); const chapterCall = caseCalls("recover").find((call) => call.path.startsWith("/api/app/playlet/chapter/detail?"), ); assert.equal(chapterCall?.headers.authorization, "guest-recover"); assert.ok( String(chapterCall?.headers.cookie).includes("token=guest-recover"), ); results.push({ contentGuestRecovery: true, recoveryCookiesBeforeSSR: true }); console.log( "native SSR: large file handoff and concurrent account isolation", ); const before = new Set(await files()); bigList = true; const large = await request("large", "/dramas/", "registered-A"); bigList = false; assert.equal(large.response.status, 200); assert.ok(large.html.includes("大快照23_")); noDuplicateBuild("large"); const created = (await files()).filter((name) => !before.has(name)); assert.ok(created.length > 0, "Large DTO must use file handoff"); const largeFile = created[0]; const payload = JSON.parse( gunzipSync(await readFile(join(handoffDir, largeFile))).toString(), ); assert.equal(payload.page.view, "list"); assert.equal(payload.page.data.userName, nameFor("registered-A")); assert.equal((await stat(join(handoffDir, largeFile))).mode & 0o777, 0o600); assert.equal((await stat(handoffDir)).mode & 0o777, 0o700); const forged = gzipSync( JSON.stringify({ v: 1, expires: Date.now() + 60000, binding: "fake", page: { view: "list", data: { userName: "FORGED_PRIVATE_USER" } }, }), ).toString("base64url"); const concurrent = await Promise.all( Array.from({ length: 12 }, (_, i) => request( "parallel-" + i, "/dramas/", i % 2 ? "registered-B" : "registered-A", { headers: { "x-hk-page-id": largeFile.slice(0, -8), "x-hk-page": forged, "x-hk-url": "https://attacker.test/private", }, }, ), ), ); for (let i = 0; i < concurrent.length; i++) { const expected = nameFor(i % 2 ? "registered-B" : "registered-A"), other = nameFor(i % 2 ? "registered-A" : "registered-B"); assert.equal(concurrent[i].response.status, 200); assert.ok(concurrent[i].html.includes(expected)); assert.equal(concurrent[i].html.includes(other), false); assert.equal(concurrent[i].html.includes("FORGED_PRIVATE_USER"), false); noDuplicateBuild("parallel-" + i); } results.push({ largeFileHandoff: true, fileMode: "0600", directoryMode: "0700", parallelAccounts: 12, forgedSnapshotRejected: true, internalHeadersHidden: true, }); const rsc = await request("rsc", "/dramas/?_rsc=", "registered-B", { headers: { RSC: "1" }, }); assert.equal( rsc.response.status, 200, JSON.stringify(Object.fromEntries(rsc.response.headers)), ); assert.ok( rsc.response.headers.get("content-type")?.includes("text/x-component"), ); assert.ok(rsc.html.includes(nameFor("registered-B"))); assert.equal(rsc.html.includes(nameFor("registered-A")), false); noDuplicateBuild("rsc"); for (const call of caseCalls("rsc")) { assert.equal(call.headers.rsc, undefined); assert.equal(call.headers["next-router-prefetch"], undefined); assert.equal(call.headers["next-url"], undefined); } const head = await request("head", "/dramas/", "registered-A", { method: "HEAD", }); assert.equal(head.response.status, 200); assert.equal(head.html, ""); noDuplicateBuild("head"); results.push({ rscNavigationContext: true, headCookies: true }); console.log("native SSR: exact redirect, metadata and discovery contract"); for (const [caseName, path, method, status, location] of [ [ "redirect301", "/dramas?filter=preserved", "GET", 301, origin + "/dramas/?filter=preserved", ], [ "legacy-alias301", "/play/story%2Fa?sign=a+b&sign=%2b", "GET", 301, origin + "/drama/story%2Fa/?sign=a+b&sign=%2b", ], // Removed account pages (legacy and public) permanently return home without a query. ["legacy-account301", "/setting/", "HEAD", 301, origin + "/"], ["legacy-login301", "/login/?next=%2Fprofile", "GET", 301, origin + "/"], ["account301", "/account/", "GET", 301, origin + "/"], ["account-settings301", "/account/settings/", "GET", 301, origin + "/"], ["account-recharge301", "/account/recharge/?pkg=1", "HEAD", 301, origin + "/"], ["account-login301", "/account/login", "GET", 301, origin + "/"], [ "redirect307", "/api/feedback/add/?dc=preserved", "POST", 307, origin + "/api/feedback/add?dc=preserved", ], ["redirect302", "/?dc=attribution", "GET", 302, origin + "/"], [ "download302", "/app/download/", "GET", 302, "https://download.example.test/real-entry", ], ] as const) { const { response } = await request(caseName, path, "", { method }); assert.equal(response.status, status); assert.equal( new URL(response.headers.get("location") || "", origin).href, location, ); if (caseName !== "download302") { assert.equal(caseCalls(caseName).length, 0); assert.equal(response.headers.getSetCookie().length, 0); } } for (const [index, pathname] of [ "/api/auth/login", "/api/auth/register", "/api/auth/logout", "/api/playlet/collect/set", "/api/playlet/like/set", "/api/comment/send", "/api/comment/list", "/api/playlet/chapter/unlock", "/api/coin/buy", ].entries()) { const removedAPI = await request("removed-api-" + index, pathname, "registered-A", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}", }); assert.equal(removedAPI.response.status, 404, "Removed BFF route " + pathname); const upstreamPath = "/api/app" + pathname.slice("/api".length); assert.equal( caseCalls("removed-api-" + index).some((call) => call.path.split("?")[0] === upstreamPath), false, "Removed BFF route " + pathname + " reached upstream", ); assert.deepEqual(removedCalls("removed-api-" + index), []); } const home = await request("home", "/", "registered-A"); assert.equal(home.response.status, 200); assert.equal(home.html.includes("继续观看"), false, "Home no longer renders viewing history"); assert.deepEqual(removedCalls("home"), [], "Home no longer fetches viewing history"); for (const marker of ["data-login-open", "hk-user-menu", 'href="/account/']) assert.equal(home.html.includes(marker), false, "Home still renders account entry " + marker); assert.ok(home.html.includes("SSR首页标题 - 花魁短剧")); assert.equal(link(home.html, "canonical"), canonical + "/"); assert.equal(meta(home.html, "description"), "SSR首页描述"); assert.equal(meta(home.html, "og:locale"), "zh_CN"); assert.equal(meta(home.html, "twitter:card"), "summary_large_image"); assert.equal( meta(home.html, "google-site-verification"), "Y_ldHPiw-yBVgVw_jZ4UG5WJw-xjO31mBMfhflqMGw8", ); assert.equal( meta(home.html, "baidu-site-verification"), "native-ssr-baidu-code", ); assert.ok(graphs(home.html).some((node) => node["@type"] === "WebSite")); assert.ok(graphs(home.html).some((node) => node["@type"] === "Organization")); const list = await request( "page2", "/dramas/all/0/1/page/2/?dc=discard-canonical", "registered-A", ); assert.equal(list.response.status, 200); assert.equal( link(list.html, "canonical"), canonical + "/dramas/all/0/1/page/2/", ); assert.equal(link(list.html, "prev"), canonical + "/dramas/"); assert.equal(link(list.html, "next"), canonical + "/dramas/all/0/1/page/3/"); assert.ok(list.html.includes("第2页")); assert.ok(graphs(list.html).some((node) => node["@type"] === "ItemList")); noDuplicateBuild("page2"); const episode = await request( "episode", "/drama/isolated/402/", "registered-A", ); assert.equal(episode.response.status, 200); assert.ok(meta(episode.html, "robots").includes("noindex")); assert.equal(link(episode.html, "canonical"), ""); assert.equal(graphs(episode.html).length, 0); for (const [caseName, path] of [ ["missing", "/unrecognized-native-route/"], ["missing-work", "/drama/missing/"], ["bad-page", "/dramas/all/0/1/page/100/"], ["unknown-api", "/api/native-smoke-unknown"], ["unknown-media", "/h5/native-smoke-unknown/"], ] as const) { const missing = await request(caseName, path, "registered-A"); assert.equal(missing.response.status, 404); assert.ok(meta(missing.html, "robots").includes("noindex")); assert.equal(link(missing.html, "canonical"), ""); assert.equal(graphs(missing.html).length, 0); noDuplicateBuild(caseName); } const missingSitemap = await request("unknown-sitemap", "/sitemaps/sitemap-missing.xml", "registered-A"); assert.equal(missingSitemap.response.status, 404); assert.match(missingSitemap.response.headers.get("x-robots-tag") || "", /noindex/); assert.match(missingSitemap.response.headers.get("content-type") || "", /text\/html/); assert.ok(meta(missingSitemap.html, "robots").includes("noindex")); assert.equal(link(missingSitemap.html, "canonical"), ""); assert.equal(graphs(missingSitemap.html).length, 0); noDuplicateBuild("unknown-sitemap"); listUnavailable = true; const unavailable = await request("unavailable", "/dramas/", "registered-A"); listUnavailable = false; assert.equal( unavailable.response.status, 503, "Temporary upstream failure must retain a 503 HTML status", ); assert.ok(meta(unavailable.html, "robots").includes("noindex")); assert.equal(link(unavailable.html, "canonical"), ""); assert.equal(graphs(unavailable.html).length, 0); assert.ok(unavailable.response.headers.get("retry-after")); noDuplicateBuild("unavailable"); const robots = await request("robots", "/robots.txt"); assert.equal(robots.response.status, 200); assert.ok(robots.html.includes("Sitemap: " + canonical + "/sitemap.xml")); assert.ok(robots.html.includes("Host: canonical.drama.test")); assert.equal(/Disallow: \/(?:profile|search|login)/.test(robots.html), false); assert.equal(robots.response.headers.getSetCookie().length, 0); assert.equal(robots.response.headers.get("cache-control"), null); assert.equal(robots.response.headers.get("content-length"), String(Buffer.byteLength(robots.html))); const llms = await request("llms", "/llms.txt"); assert.equal(llms.response.status, 200); assert.ok(llms.html.includes("# 花魁短剧")); assert.ok(llms.html.includes(canonical + "/dramas/")); assert.equal(llms.response.headers.getSetCookie().length, 0); for (const path of ["/sitemap.xml", "/sitemaps/sitemap-content.xml"]) { const xml = await request("sitemap" + path, path); assert.equal(xml.response.status, 200); assert.ok( xml.response.headers.get("content-type")?.includes("text/xml"), ); assert.ok(xml.response.headers.get("last-modified")); assert.equal(xml.response.headers.get("accept-ranges"), "bytes"); assert.equal(xml.response.headers.get("content-length"), String(Buffer.byteLength(xml.html))); assert.equal(xml.response.headers.get("cache-control"), null); assert.equal(xml.response.headers.getSetCookie().length, 0); assert.ok(xml.html.includes(canonical)); assert.equal(caseCalls("sitemap" + path).length, 0); } const originalXML = await readFile(join(sitemapDir, "sitemap.xml"), "utf8"); const fullXML = await request("sitemap-bytes", "/sitemap.xml?sig=a%2Bb%3D"); assert.equal(fullXML.html, originalXML, "Go serves generated XML unchanged"); const modified = fullXML.response.headers.get("last-modified")!; const unchangedXML = await request("sitemap304", "/sitemap.xml", "", { headers: { "If-Modified-Since": modified } }); assert.equal(unchangedXML.response.status, 304); assert.equal(unchangedXML.html, ""); assert.equal(unchangedXML.response.headers.get("content-type"), null); const partXML = await request("sitemap206", "/sitemap.xml", "", { headers: { Range: "bytes=0-31" } }); assert.equal(partXML.response.status, 206); assert.equal(partXML.html, originalXML.slice(0, 32)); assert.equal(partXML.response.headers.get("content-range"), `bytes 0-31/${Buffer.byteLength(originalXML)}`); const multipartXML = await request("sitemap-multipart", "/sitemap.xml", "", { headers: { Range: "bytes=0-2,8-10" } }); assert.equal(multipartXML.response.status, 206); assert.match(multipartXML.response.headers.get("content-type") || "", /multipart\/byteranges; boundary=/); assert.ok(multipartXML.html.includes(originalXML.slice(0, 3))); const invalidRange = await request("sitemap416", "/sitemap.xml", "", { headers: { Range: "bytes=999999-" } }); assert.equal(invalidRange.response.status, 416); assert.equal(invalidRange.response.headers.get("content-range"), `bytes */${Buffer.byteLength(originalXML)}`); const fileHEAD = await request("sitemap-head", "/sitemap.xml", "", { method: "HEAD" }); assert.equal(fileHEAD.response.status, 200); assert.equal(fileHEAD.html, ""); assert.equal(fileHEAD.response.headers.get("content-length"), String(Buffer.byteLength(originalXML))); const conditionalHEAD = await request("sitemap-head304", "/sitemap.xml", "", { method: "HEAD", headers: { "If-Modified-Since": modified } }); assert.equal(conditionalHEAD.response.status, 304); assert.equal(conditionalHEAD.response.headers.get("content-length"), null); for (const [index, pathname] of ["/site%6Daps/sitemap-content.xml", "/sitemaps%2Fsitemap-content.xml", "/sitemap%2Exml", "/robot%73.txt"].entries()) { const encoded = await request("discovery-encoded-" + index, pathname); assert.equal(encoded.response.status, 200, pathname); assert.match(encoded.response.headers.get("content-type") || "", pathname.includes("robot") ? /text\/plain/ : /text\/xml/); assert.equal(encoded.response.headers.getSetCookie().length, 0); assert.equal(encoded.html, pathname.includes("robot") ? robots.html : await readFile(join(sitemapDir, pathname.includes("content") ? "sitemap-content.xml" : "sitemap.xml"), "utf8")); } for (const [index, pathname] of ["/sitemap.xml%2F", "/sitemaps/sitemap-content.xml%2F", "/site%6Daps/sitemap-content.xml/"].entries()) { const redirected = await request("discovery-encoded-tail-" + index, pathname + "?sig=a%2Bb%3D"); assert.equal(redirected.response.status, 301, pathname); assert.equal(new URL(redirected.response.headers.get("location")!, origin).href, origin + decodeURIComponent(pathname).slice(0, -1) + "?sig=a%2Bb%3D"); assert.equal(redirected.response.headers.getSetCookie().length, 0); } for (const [index, pathname] of ["/robots.txt", "/llms.txt", "/sitemap.xml", "/sitemaps/invalid.xml"].entries()) { for (const [methodIndex, method] of ["POST", "PUT", "DELETE", "OPTIONS"].entries()) { const failed = await request(`discovery-method-${index}-${methodIndex}`, pathname, "registered-A", { method, headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: "test=1" }); assert.equal(failed.response.status, 404, method + " " + pathname); assert.equal(failed.response.headers.get("allow"), "GET"); assert.match(failed.response.headers.get("content-type") || "", /text\/html/); assert.ok(meta(failed.html, "robots").includes("noindex")); assert.equal(link(failed.html, "canonical"), ""); assert.equal(graphs(failed.html).length, 0); } } for (const [index, pathname] of ["/robots.txt/", "/sitemap.xml/", "/sitemaps/invalid.xml/", "/sitemaps/sitemap-a%2F.xml", "/sitemaps/sitemap-content%252Exml"].entries()) { const missing = await request("discovery-invalid-" + index, pathname, "registered-A", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: "test=1" }); assert.equal(missing.response.status, 404, pathname); assert.match(missing.response.headers.get("content-type") || "", /text\/html/); assert.equal(missing.response.headers.get("allow"), pathname.includes("%252E") ? "GET" : null); assert.ok(meta(missing.html, "robots").includes("noindex")); } const verify = await request("verify", "/" + verifyFilename); assert.equal(verify.response.status, 200); assert.equal(verify.html, "native-ssr-verification-file"); const now = await request("indexnow", "/" + indexNow + ".txt"); assert.equal(now.response.status, 200); assert.equal(now.html, indexNow); // Go DHInit registers only navigation pages/static assets, including when files exist. const navigationReserve = createServer(); await new Promise(done => navigationReserve.listen(0, "127.0.0.1", done)); const navigationBound = navigationReserve.address(); if (!navigationBound || typeof navigationBound === "string") throw new Error("Navigation preview port unavailable"); const navigationOrigin = `http://127.0.0.1:${navigationBound.port}`; await new Promise(done => navigationReserve.close(() => done())); navigationPreview = spawn(process.execPath, [resolve("node_modules/next/dist/bin/next"), "start", "--port", String(navigationBound.port), "--hostname", "127.0.0.1"], { env: { ...process.env, APP_ENV: "production", NODE_ENV: "production", UPSTREAM_API_URL: `http://127.0.0.1:${bound.port}`, UPSTREAM_TIMEOUT_MS: "3000", SITEMAP_DIR: sitemapDir, NAVIGATION_ONLY: "true", NEXT_TELEMETRY_DISABLED: "1" }, stdio: ["ignore", "pipe", "pipe"], }); navigationPreview.stderr?.on("data", chunk => { previewLogs += "[navigation] " + String(chunk); }); const navigationDeadline = Date.now() + 30000; while (true) { try { await fetch(navigationOrigin + "/robots.txt"); break; } catch (error) { if (Date.now() >= navigationDeadline) throw error; await new Promise(done => setTimeout(done, 200)); } } for (const pathname of ["/robots.txt", "/robots.txt/", "/llms.txt", "/sitemap.xml", "/sitemaps/sitemap-content.xml"]) { for (const method of ["GET", "HEAD", "POST"]) { const response = await fetch(navigationOrigin + pathname, { method, headers: headers("navigation-discovery", "registered-A") }); assert.equal(response.status, 404, "DHInit " + method + " " + pathname); assert.match(response.headers.get("content-type") || "", /text\/html/); assert.equal(response.headers.get("allow"), null); assert.match(response.headers.get("x-robots-tag") || "", /noindex/); const html = await response.text(); if (method === "HEAD") assert.equal(html, ""); else assert.ok(meta(html, "robots").includes("noindex")); } } navigationPreview.kill("SIGTERM"); navigationPreview = undefined; results.push({ redirects: [301, 307, 302], removedAccountRedirectHome: true, removedAccountAPIs404: true, canonical: true, pagination: true, jsonLD: true, verification: true, notFound: true, unavailable503: true, robots: true, llms: true, sitemaps: true, indexNow: true, fileConditionalRequests: [304, 206, 416], discoveryMethods: "Go HTML 404 with Allow GET", navigationDiscovery404: true, }); console.log( "native SSR: real document navigation updates page CSS and metadata", ); browser = await chromium.launch({ headless: true, executablePath: process.env.CHROME_PATH || "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", }); const navigationErrors: string[] = []; for (const mobile of [false, true]) { const context = await browser.newContext({ viewport: mobile ? { width: 375, height: 812 } : { width: 1440, height: 900 }, extraHTTPHeaders: { "x-smoke-case": mobile ? "browser-mobile" : "browser-desktop", }, }); await context.addCookies([ { name: "token", value: "registered-B", url: origin }, ]); await context.addInitScript(() => localStorage.setItem("hg_age_ok", "1")); await context.route("**/*", (route) => route.request().url().startsWith(origin) ? route.continue() : route.abort(), ); const page = await context.newPage(); page.on("pageerror", (error) => navigationErrors.push(error.message)); let documents = 0; page.on("request", (request) => { if (request.isNavigationRequest()) documents++; }); if (!mobile) { for (const path of ["/account/register/", "/account/login/", "/login/"]) { await page.goto(origin + path, { waitUntil: "domcontentloaded" }); assert.equal(new URL(page.url()).pathname, "/"); assert.equal(await page.locator("body").getAttribute("class"), ""); await expectPageStyles(page, ["home"]); assert.ok((await page.title()).includes("SSR首页标题")); const settled = documents; await page.waitForTimeout(800); assert.equal(documents, settled, "Desktop removed-account redirect must settle"); assert.equal( await page.locator("[data-login-pop], [data-register-pop], [data-login-open], [data-register-open], .hk-user-menu, .hk-user-chip, a[href^='/account']").count(), 0, "Home renders no login dialog or account entry", ); } await page.screenshot({ path: join(output, "desktop-navigation.png") }); } else { await page.goto(origin + "/discover/search/", { waitUntil: "networkidle" }); assert.equal( await page.locator("body").getAttribute("class"), "hk-mobile-search", ); const input = page.locator(".hk-mobile-search__input"); await input.fill("a/b"); await Promise.all([ page.waitForURL(origin + "/search/a%2Fb/"), input.press("Enter"), ]); await page.locator("[data-search-results]").waitFor(); assert.equal( await page.locator("body").getAttribute("class"), "hk-search-page", ); await expectPageStyles(page, ["search-results"], ["mobile-search"]); assert.ok( ( await page.locator('head meta[name="robots"]').getAttribute("content") )?.includes("noindex"), ); await page.screenshot({ path: join(output, "mobile-search-navigation.png"), fullPage: true, }); await page.goto(origin + "/account/", { waitUntil: "networkidle" }); assert.equal(new URL(page.url()).pathname, "/"); assert.equal(await page.locator("body").getAttribute("class"), ""); await expectPageStyles(page, ["home"], ["mobile-search", "search-results"]); assert.deepEqual( await page.locator(".hk-mobile-tabbar__item").allTextContents(), ["首页", "短剧", "剧集"], "Mobile tabbar has no account tab", ); await page.locator("[data-drawer-open]").click(); await page.locator("[data-drawer]").waitFor(); assert.equal( await page.locator("[data-drawer] :is(.hk-drawer__user, a[href^='/account'], [data-login-open])").count(), 0, "Drawer has no user block or account links", ); } await context.close(); } assert.deepEqual(navigationErrors, []); assert.deepEqual(removedCalls(), [], "No request reached a removed account upstream API"); results.push({ desktopRemovedAccountRedirect: true, mobileSearchDocumentNavigation: true, mobileRemovedAccountRedirectAndTabbar: true, pageClassAndStylesRefresh: true, browserErrors: 0, }); await writeFile( join(output, "results.json"), JSON.stringify({ optimizedNext: true, results }, null, 2), ); await writeFile(join(output, "home.html"), home.html); console.log( JSON.stringify({ optimizedNext: true, results, output }, null, 2), ); } catch (error) { await writeFile(join(output, "next.log"), previewLogs); await writeFile( join(output, "upstream.json"), JSON.stringify(calls, null, 2), ); throw error; } finally { navigationPreview?.kill("SIGTERM"); await browser?.close(); preview.kill("SIGTERM"); await new Promise((done) => mock.close(() => done())); await unlink(verifyPath).catch(() => {}); }