/** * Subdomain Routing Suite — pure logic, no Next.js Edge runtime required. * * Verifies the middleware contract in src/lib/routing/subdomain.ts and the * return-origin helper in src/lib/seo/site.ts: * - admin.* → /admin route group, with /api/* and /dashboard* passing through * unchanged (fixes the 404 the admin UI would otherwise hit on its own * subdomain); * - app.* → dashboard surface only; auth/admin/legal redirect to the main * host, everything else is a dashboard sub-route; * - boundary-aware admin path detection (no accidental admin-gating of * /administrator or /api/adminx); * - strict first-party host validation (app.evil.example is never routed); * - originForFirstPartyHost returns the origin the user started on. */ import { describe, test, expect } from "./runner"; import { siteUrl, hostIsSiteFirstParty, originForFirstPartyHost } from "../../src/lib/seo/site"; import { decideSubdomain, isAdminPath, isDashboardPath, MAIN_HOST_ONLY_PREFIXES } from "../../src/lib/routing/subdomain"; const siteHost = new URL(siteUrl).host; const appHost = `app.${siteHost}`; const adminHost = `admin.${siteHost}`; describe("Subdomain routing — admin path detection (boundary-aware)", () => { test("/admin and /admin/* are admin", () => { expect(isAdminPath("/admin")).toBe(true); expect(isAdminPath("/admin/")).toBe(true); expect(isAdminPath("/admin/users")).toBe(true); }); test("/api/admin and /api/admin/* are admin", () => { expect(isAdminPath("/api/admin")).toBe(true); expect(isAdminPath("/api/admin/stats")).toBe(true); expect(isAdminPath("/api/admin/system/settings")).toBe(true); }); test("prefix lookalikes are NOT admin — no boundary false-positives", () => { expect(isAdminPath("/administrator")).toBe(false); expect(isAdminPath("/api/adminx")).toBe(false); expect(isAdminPath("/api/administrator")).toBe(false); expect(isAdminPath("/admin_old")).toBe(false); }); test("non-admin app routes are not admin", () => { expect(isAdminPath("/")).toBe(false); expect(isAdminPath("/dashboard")).toBe(false); expect(isAdminPath("/api/scan")).toBe(false); expect(isAdminPath("/api/auth/login")).toBe(false); }); }); describe("Subdomain routing — dashboard path detection (boundary-aware)", () => { test("/dashboard and /dashboard/* are dashboard", () => { expect(isDashboardPath("/dashboard")).toBe(true); expect(isDashboardPath("/dashboard/")).toBe(true); expect(isDashboardPath("/dashboard/export")).toBe(true); expect(isDashboardPath("/dashboard/onboarding")).toBe(true); }); test("prefix lookalikes are NOT dashboard", () => { expect(isDashboardPath("/dashboarding")).toBe(false); expect(isDashboardPath("/api/dashboard")).toBe(false); expect(isDashboardPath("/")).toBe(false); expect(isDashboardPath("/auth/login")).toBe(false); }); }); describe("Subdomain routing — admin.* branch", () => { test("the admin host is first-party", () => { expect(hostIsSiteFirstParty(adminHost)).toBe(true); }); test("/admin pages pass through unchanged", () => { const d1 = decideSubdomain(adminHost, "/admin"); const d2 = decideSubdomain(adminHost, "/admin/users"); expect(d1.kind).toBe("next"); expect(d2.kind).toBe("next"); }); test("/api/* passes through unchanged — the admin UI data calls keep working", () => { const d1 = decideSubdomain(adminHost, "/api/admin/stats"); const d2 = decideSubdomain(adminHost, "/api/admin/users"); const d3 = decideSubdomain(adminHost, "/api/auth/login"); expect(d1.kind).toBe("next"); expect(d2.kind).toBe("next"); expect(d3.kind).toBe("next"); }); test("/dashboard* passes through unchanged — admin shell links to the dashboard", () => { const d = decideSubdomain(adminHost, "/dashboard"); expect(d.kind).toBe("next"); }); test("root and unknown paths rewrite under /admin", () => { const root = decideSubdomain(adminHost, "/"); expect(root.kind).toBe("rewrite"); if (root.kind === "rewrite") expect(root.pathname).toBe("/admin"); const login = decideSubdomain(adminHost, "/login"); expect(login.kind).toBe("rewrite"); if (login.kind === "rewrite") expect(login.pathname).toBe("/admin/login"); }); test("a spoofed admin host is never routed", () => { expect(decideSubdomain("admin.evil.example", "/").kind).toBe("none"); expect(decideSubdomain(`admin.${siteHost}.evil.example`, "/").kind).toBe("none"); }); }); describe("Subdomain routing — app.* branch", () => { test("root serves the dashboard", () => { const d = decideSubdomain(appHost, "/"); expect(d.kind).toBe("rewrite"); if (d.kind === "rewrite") expect(d.pathname).toBe("/dashboard"); }); test("dashboard and API pass through unchanged", () => { expect(decideSubdomain(appHost, "/dashboard").kind).toBe("next"); expect(decideSubdomain(appHost, "/dashboard/receipts").kind).toBe("next"); expect(decideSubdomain(appHost, "/api/scan").kind).toBe("next"); expect(decideSubdomain(appHost, "/api/admin/stats").kind).toBe("next"); }); test("unknown paths become dashboard sub-routes", () => { const d = decideSubdomain(appHost, "/projects"); expect(d.kind).toBe("rewrite"); if (d.kind === "rewrite") expect(d.pathname).toBe("/dashboard/projects"); }); test("main-host-only content redirects to the bare domain", () => { for (const prefix of MAIN_HOST_ONLY_PREFIXES) { const d = decideSubdomain(appHost, prefix); expect(d.kind).toBe("redirect"); if (d.kind === "redirect") expect(d.pathname).toBe(prefix); } const login = decideSubdomain(appHost, "/auth/login"); expect(login.kind).toBe("redirect"); const termsDeep = decideSubdomain(appHost, "/terms/extra"); expect(termsDeep.kind).toBe("redirect"); }); test("boundary: /authx and /administrator stay dashboard routes, not redirects", () => { const authx = decideSubdomain(appHost, "/authx"); expect(authx.kind).toBe("rewrite"); if (authx.kind === "rewrite") expect(authx.pathname).toBe("/dashboard/authx"); const adminish = decideSubdomain(appHost, "/administrator"); expect(adminish.kind).toBe("rewrite"); if (adminish.kind === "rewrite") expect(adminish.pathname).toBe("/dashboard/administrator"); }); test("a spoofed app host is never routed", () => { expect(decideSubdomain("app.evil.example", "/").kind).toBe("none"); expect(decideSubdomain(`app.${siteHost}.evil.example`, "/").kind).toBe("none"); }); }); describe("Subdomain routing — non-subdomain hosts", () => { test("the bare site host and unrelated hosts get no subdomain decision", () => { expect(decideSubdomain(siteHost, "/dashboard").kind).toBe("none"); expect(decideSubdomain(siteHost, "/").kind).toBe("none"); expect(decideSubdomain("evil.example", "/").kind).toBe("none"); }); }); describe("Origin helper — return to the host the user started on", () => { test("first-party app./admin. hosts resolve to their own origin", () => { const appUrl = originForFirstPartyHost(appHost); expect(new URL(appUrl).host).toBe(appHost); expect(hostIsSiteFirstParty(new URL(appUrl).host)).toBe(true); const adminUrl = originForFirstPartyHost(adminHost); expect(new URL(adminUrl).host).toBe(adminHost); }); test("the bare site host keeps the site origin", () => { expect(originForFirstPartyHost(siteHost)).toBe(siteUrl); }); test("non-first-party hosts fall back to siteUrl — no open redirect", () => { expect(originForFirstPartyHost("evil.example")).toBe(siteUrl); expect(originForFirstPartyHost(`app.${siteHost}.evil.example`)).toBe(siteUrl); expect(originForFirstPartyHost("")).toBe(siteUrl); }); test("the origin keeps the site scheme but swaps the host", () => { const protocol = new URL(siteUrl).protocol; expect(originForFirstPartyHost(appHost)).toMatch(new RegExp(`^${protocol}\\/\\/`)); }); });