import test from "node:test"; import assert from "node:assert/strict"; import { canonicalizePageURLs, ROUTE_MIGRATIONS, toLegacyPath, toPublicPath, toPublicURL } from "../src/lib/routes"; import { createContext } from "../src/server/context"; import { canonicalRedirect } from "../src/server/redirects"; import { matchPageRoute } from "../src/server/pages"; import { pageRoutes, publicPageRoutes } from "../src/server/pages/routes"; import { preparePage } from "../src/server/react/page-data"; import { trackerPage } from "../src/server/services/shared"; test("Huakui route prefixes preserve encoded slugs, raw queries and fragments", () => { const suffix = "/%E7%9F%AD%E5%89%A7%2F%E9%9B%86%252F1/?x=a+b&x=%2b&token=a%2FB#episode%202"; assert.equal(toPublicPath("/play" + suffix), "/drama" + suffix); assert.equal(toLegacyPath("/drama" + suffix), "/play" + suffix); assert.equal(toLegacyPath("/series/watch/video/recommendations/page/2/"), "/series-play/video/recommendations/page/2/"); for (const path of ["/account/settings/", "/settings/", "/profile/", "/account/login/"]) { assert.equal(toPublicPath(path), path, "removed account path is not migrated " + path); assert.equal(toLegacyPath(path), path, "removed account path is not migrated " + path); } assert.equal(toPublicPath("/dh/"), "/navigation/"); assert.equal(toPublicPath("/home-road/"), "/return-home/"); assert.equal(toLegacyPath("/return-home/"), "/home-road/"); for (const path of ["/", "/series/anime/all/1/", "/search/word/", "/listings/", "/api/playlet/list", "/h5/m3u8/signed", "/static/images/play/foo.svg", "//external.example/play/a/"]) { assert.equal(toPublicPath(path), path, path); } for (const { legacy, public: current } of ROUTE_MIGRATIONS) { assert.equal(toPublicPath(legacy + "/"), current + "/"); assert.equal(toPublicPath(current + "/"), current + "/", "idempotent " + current); } }); test("all registered HTML routes resolve under new public URLs with identical parameters", () => { const values: Record = { q: "短剧/a", theme: "romance", slug: "剧名/a", configId: "recommend", media: "anime", topic: "12", updateStatus: "2", sortType: "3", sortOrder: "2", rankType: "3", timeRange: "1", page: "2", guessPage: "2", ep: "100", filter: "chapter" }; assert.equal(pageRoutes.length, 59); assert.equal(publicPageRoutes.length, pageRoutes.length); for (const [pattern, handler] of pageRoutes) { const params: Record = {}; const legacy = pattern.replace(/:(\w+)/g, (_, key: string) => { params[key] = values[key]; return encodeURIComponent(values[key]); }); const current = toPublicPath(legacy); assert.deepEqual(matchPageRoute(current), { handler, params }, pattern); assert.deepEqual(matchPageRoute(legacy), { handler, params }, "legacy " + pattern); for (const method of ["GET", "HEAD"]) { assert.equal(canonicalRedirect(createContext(new Request("https://huakui.example" + current, { method }))), null, "canonical " + current); const response = canonicalRedirect(createContext(new Request("https://huakui.example" + legacy, { method }))); if (current === legacy) assert.equal(response, null, "unchanged " + legacy); else { assert.equal(response?.status, 301, "legacy " + legacy); assert.equal(response?.headers.get("Location"), current, "single-hop " + legacy); } } } for (const path of ["/dramas//", "/dramas/theme/extra/", "/series/watch/", "/account/unknown/", "/account/", "/account/settings/", "/account/favorites/chapter/", "/profile/", "/login/", "/collection/a/page/2/extra/", "/curated/dramas/", "/discover/search/extra/", "/return-home//", "/return-home/extra/", "/home-road/extra/"]) { assert.equal(matchPageRoute(path), null, path); } }); test("legacy pages redirect once with 301 to new canonical path and keep request suffix bytes", () => { const saved = process.env.NAVIGATION_ONLY; process.env.NAVIGATION_ONLY = "false"; try { const suffix = "?x=a+b&x=%2b&return=%2Fplay%2Fa%2F#part%202"; for (const [legacy, current] of [["/play/a%2Fb", "/drama/a%2Fb/"], ["/list/romance/2/1/page/2/", "/dramas/romance/2/1/page/2/"], ["/dh", "/navigation/"], ["/home-road", "/return-home/"], ["/home-road/", "/return-home/"]]) { for (const method of ["GET", "HEAD"]) { const response = canonicalRedirect(createContext(new Request("https://huakui.example" + legacy + suffix, { method }))); assert.equal(response?.status, 301, legacy); assert.equal(response?.headers.get("Location"), current + suffix, legacy); assert.equal(canonicalRedirect(createContext(new Request("https://huakui.example" + current + suffix, { method }))), null, "single hop " + current); } } assert.equal(canonicalRedirect(createContext(new Request("https://huakui.example/dramas")))?.headers.get("Location"), "/dramas/"); // Removed account surfaces permanently return home without carrying the query or fragment. for (const path of ["/account", "/account/", "/account/login/", "/account/register/", "/account/history/page/2/", "/account/favorites/chapter/page/3/", "/account/purchases/", "/account/recharge/", "/account/settings/", "/login", "/login/", "/register/", "/profile", "/profile/", "/history/page/2/", "/favorites/chapter/", "/purchased/", "/setting/", "/settings", "/settings/", "/recharge/"]) { for (const method of ["GET", "HEAD"]) { const response = canonicalRedirect(createContext(new Request("https://huakui.example" + path + suffix, { method }))); assert.equal(response?.status, 301, method + " " + path); assert.equal(response?.headers.get("Location"), "/", method + " " + path); } assert.equal(canonicalRedirect(createContext(new Request("https://huakui.example" + path, { method: "POST" }))), null, "POST " + path); } for (const path of ["/accounts/", "/account-settings/", "/loginx/", "/history-of-drama/", "/settingsx"]) { assert.notEqual(canonicalRedirect(createContext(new Request("https://huakui.example" + path)))?.headers.get("Location"), "/", "segment boundary " + path); } for (const path of ["/list//", "/list/theme/extra/", "/static/css/list.css", "/h5/m3u8/signed?sign=%2B", "/api/feedback/add", "/api/playlet/collect/set/"]) { assert.equal(canonicalRedirect(createContext(new Request("https://huakui.example" + path))), null, path); } for (const path of ["/play/a", "/drama/a", "/profile", "/account", "/home-road/", "/return-home/"]) { assert.equal(canonicalRedirect(createContext(new Request("https://huakui.example" + path, { method: "POST" }))), null, path); } const post = canonicalRedirect(createContext(new Request("https://huakui.example/api/feedback/add/?sign=%2B", { method: "POST" }))); assert.equal(post?.status, 307); assert.equal(post?.headers.get("Location"), "/api/feedback/add?sign=%2B"); assert.equal(canonicalRedirect(createContext(new Request("https://huakui.example/api/playlet/collect/set/?sign=%2B", { method: "POST" }))), null); } finally { if (saved === undefined) delete process.env.NAVIGATION_ONLY; else process.env.NAVIGATION_ONLY = saved; } }); test("DTO link canonicalization leaves external, media, SDK, payment and SEO inputs intact", () => { const external = "https://cdn.example/play/signed.m3u8?sign=%2B"; const data = { requestPath: "/play/a/1/", canonicalPath: "/play/a/", works: [{ URL: "/play/a%2Fb/", url: "https://huakui.example/topic-detail/a/?q=%2b" }], pagination: { PrevURL: "/list/", nextURL: "/list/romance/1/1/page/2/" }, categoryList: [{ urlValue: "/rank/" }, { urlValue: "https://external.example/list/" }], chapter: { url: "/play/signed-media.m3u8?sign=%2B" }, video: { video: { url: "/play/signed-video.m3u8?sign=%2B" } }, advitise: { url: "https://huakui.example/play/ad/" }, config: { url: "/play/config/" }, mainURL: external, previewURL: "/play/signed-preview.m3u8?sign=%2B", payment: { url: "/play/payment/" }, canonicalURL: "https://huakui.example/play/a/", seoInfo: { canonicalUrl: "https://huakui.example/play/a/" }, jsonLD: '{"url":"https://huakui.example/play/a/"}', }; assert.equal(canonicalizePageURLs(data, "https://huakui.example"), data); assert.equal(data.requestPath, "/drama/a/1/"); assert.equal(data.canonicalPath, "/drama/a/"); assert.equal(data.works[0].URL, "/drama/a%2Fb/"); assert.equal(data.works[0].url, "https://huakui.example/collection/a/?q=%2b"); assert.equal(data.pagination.PrevURL, "/dramas/"); assert.equal(data.pagination.nextURL, "/dramas/romance/1/1/page/2/"); assert.equal(data.categoryList[0].urlValue, "/rankings/"); assert.equal(data.categoryList[1].urlValue, "https://external.example/list/"); assert.equal(data.chapter.url, "/play/signed-media.m3u8?sign=%2B"); assert.equal(data.video.video.url, "/play/signed-video.m3u8?sign=%2B"); assert.equal(data.advitise.url, "https://huakui.example/play/ad/"); assert.equal(data.config.url, "/play/config/"); assert.equal(data.mainURL, external); assert.equal(data.previewURL, "/play/signed-preview.m3u8?sign=%2B"); assert.equal(data.payment.url, "/play/payment/"); assert.equal(data.canonicalURL, "https://huakui.example/play/a/"); assert.equal(data.seoInfo.canonicalUrl, "https://huakui.example/play/a/"); assert.equal(data.jsonLD, '{"url":"https://huakui.example/play/a/"}'); assert.equal(toPublicURL("https://huakui.example/play/a%2Fb/?sign=a+b#e", "https://huakui.example/"), "https://huakui.example/drama/a%2Fb/?sign=a+b#e"); assert.equal(toPublicURL(external, "https://huakui.example"), external); }); test("navigation-only mode recognizes the new navigation path without enabling other page redirects", () => { const saved = process.env.NAVIGATION_ONLY; process.env.NAVIGATION_ONLY = "true"; try { assert.equal(canonicalRedirect(createContext(new Request("https://huakui.example/nav?channel=%2B")))?.headers.get("Location"), "/navigation/?channel=%2B"); assert.equal(canonicalRedirect(createContext(new Request("https://huakui.example/navigation/"))), null); assert.equal(canonicalRedirect(createContext(new Request("https://huakui.example/list/"))), null); assert.equal(canonicalRedirect(createContext(new Request("https://huakui.example/home-road/"))), null); assert.equal(canonicalRedirect(createContext(new Request("https://huakui.example/account/settings/"))), null); } finally { if (saved === undefined) delete process.env.NAVIGATION_ONLY; else process.env.NAVIGATION_ONLY = saved; } }); test("new content paths reach unchanged upstream loaders and preserve navigation-only isolation", async () => { const savedFetch = globalThis.fetch; const savedUpstream = process.env.UPSTREAM_API_URL; const savedNavigation = process.env.NAVIGATION_ONLY; process.env.UPSTREAM_API_URL = "http://huakui-route-loader-test"; process.env.NAVIGATION_ONLY = "false"; const calls: URL[] = []; globalThis.fetch = async (input) => { const url = new URL(String(input)); calls.push(url); let data: Record = {}; if (url.pathname === "/api/app/config/info") data = { config: { foreverUrlAddr: "https://huakui.example" } }; if (url.pathname === "/api/app/theme/list") data = { list: [{ id: 12, name: "爱情", slug: "romance", status: true }] }; if (url.pathname === "/api/app/playlet/all/list") data = { list: [{ id: 10, playName: "作品", slug: "story" }], totalCount: 100, pageNum: Number(url.searchParams.get("pageNum")), pageSize: 24 }; return Response.json({ code: 200, data }); }; const context = (path: string) => { const ctx = createContext(new Request("https://huakui.example" + path, { headers: { cookie: "token=route-test; isRegister=1" } })); ctx.user = { userId: 7, nickName: "读者", type: 1, isRegister: true }; ctx.state.set("auth:fresh", true); return ctx; }; try { const list = await preparePage(context("/dramas/romance/1/1/page/2/")); assert.equal(list.status || 200, 200); assert.equal(list.view, "list"); assert.equal(list.data.themeId, 12); assert.equal(list.data.updateStatus, 1); assert.equal(list.data.requestPath, "/dramas/romance/1/1/page/2/"); assert.equal(list.data.canonicalPath, "/dramas/romance/1/1/page/2/"); assert.equal(list.data.pagination.NextURL || list.data.pagination.nextURL, "/dramas/romance/1/1/page/3/"); assert.equal(list.data.canonicalURL, "https://huakui.example/dramas/romance/1/1/page/2/"); assert.match(list.data.robots, /^index,follow/); assert.equal(list.data.paginationNextURL, "https://huakui.example/dramas/romance/1/1/page/3/"); const sorted = await preparePage(context("/dramas/romance/1/2/")); assert.equal(sorted.data.robots, "noindex,follow"); assert.equal(sorted.data.canonicalURL, undefined); const upstream = calls.find((url) => url.pathname === "/api/app/playlet/all/list"); assert.equal(upstream?.searchParams.get("themeSlug"), "romance"); assert.equal(upstream?.searchParams.get("pageNum"), "2"); for (const path of ["/account/", "/account/settings/", "/account/favorites/chapter/", "/setting/"]) assert.equal((await preparePage(context(path))).status, 404, path); const homeRoad = await preparePage(context("/return-home/")); assert.equal(homeRoad.status || 200, 200); assert.equal(homeRoad.view, "homeRoad"); assert.equal(homeRoad.data.requestPath, "/return-home/"); assert.equal(homeRoad.data.pageKey, "home_road"); assert.match(homeRoad.data.metaTitle, /回家的路.*花魁短剧/); assert.equal(homeRoad.data.robots, "noindex,follow"); assert.equal(homeRoad.data.canonicalURL, undefined); assert.equal(homeRoad.data.jsonLD, undefined); assert.ok(homeRoad.data.faqItems.length > 0); assert.equal((await preparePage(context("/navigation/"))).data.homeRoadURL, "/return-home/"); process.env.NAVIGATION_ONLY = "true"; const navigation = await preparePage(context("/navigation/")); assert.equal(navigation.view, "nav"); assert.equal(navigation.data.isNavPage, true); assert.equal(navigation.data.homeRoadURL, "https://huakui.example/return-home/"); assert.equal((await preparePage(context("/dramas/"))).status, 404); assert.equal((await preparePage(context("/return-home/"))).status, 404); } finally { globalThis.fetch = savedFetch; if (savedUpstream === undefined) delete process.env.UPSTREAM_API_URL; else process.env.UPSTREAM_API_URL = savedUpstream; if (savedNavigation === undefined) delete process.env.NAVIGATION_ONLY; else process.env.NAVIGATION_ONLY = savedNavigation; } }); test("navigation-only home road links use a configured main-site origin, then the saved PDF fallback", async () => { const savedFetch = globalThis.fetch; const savedUpstream = process.env.UPSTREAM_API_URL; const savedNavigation = process.env.NAVIGATION_ONLY; process.env.NAVIGATION_ONLY = "true"; let config: Record = {}; globalThis.fetch = async (input) => { const url = new URL(String(input)); return Response.json({ code: 200, data: url.pathname === "/api/app/config/info" ? { config } : {} }); }; const cases: [Record, string][] = [ [{ foreverUrlAddr: "https://main.huakui.test/old/path?ch=main", newUrlAddr: "https://latest.huakui.test/" }, "https://main.huakui.test/return-home/"], [{ foreverUrlAddr: "https://example.com/", newUrlAddr: "https://latest.huakui.test/old/path" }, "https://latest.huakui.test/return-home/"], [{ foreverUrlAddr: "javascript:invalid", newUrlAddr: "/", pdfAddr: "https://files.huakui.test/access.pdf" }, "https://files.huakui.test/access.pdf"], [{}, ""], ]; try { for (const [index, [configured, expected]] of cases.entries()) { // Isolate public-config caches while testing each configured deployment. process.env.UPSTREAM_API_URL = `http://home-road-navigation-test-${index}`; config = configured; const context = createContext(new Request("https://navigation.huakui.test/navigation/", { headers: { cookie: "token=nav-test; isRegister=1" } })); context.user = { userId: 7, type: 1, isRegister: true }; context.state.set("auth:fresh", true); const page = await preparePage(context); assert.equal(page.status || 200, 200); assert.equal(page.view, "nav"); assert.equal(page.data.homeRoadURL, expected); } } finally { globalThis.fetch = savedFetch; if (savedUpstream === undefined) delete process.env.UPSTREAM_API_URL; else process.env.UPSTREAM_API_URL = savedUpstream; if (savedNavigation === undefined) delete process.env.NAVIGATION_ONLY; else process.env.NAVIGATION_ONLY = savedNavigation; } }); test("semantic page paths retain their original analytics classification", () => { const paths: [string, string, string][] = [ ["/list/romance/", "/dramas/romance/", "list"], ["/play/a%2Fb/123/", "/drama/a%2Fb/123/", "play"], ["/series-play/film/", "/series/watch/film/", "series_play"], ["/series/all/all/1/", "/series/all/all/1/", "series"], ["/topic/", "/collections/", "topic"], ["/topic-detail/story/", "/collection/story/", "topic_detail"], ["/rank/2/", "/rankings/2/", "rank"], ["/popular-recommend/", "/featured/", "popular_recommend"], ["/recently-added/", "/latest/", "recently_added"], ["/more-list/recommend/", "/curated/dramas/recommend/", "more_list"], ["/more-series-list/recommend/", "/curated/series/recommend/", "more_series_list"], ["/search-mobile/", "/discover/search/", "search"], ["/home-road/", "/return-home/", "home_road"], ]; for (const [legacy, current, key] of paths) { assert.deepEqual(trackerPage(current), trackerPage(legacy), current); assert.equal(trackerPage(current)[0], key, current); } assert.equal(trackerPage("/unknown/")[0], "internal_other"); });