Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/utils/app/HostRouting.server.test.ts
T
2026-07-08 16:15:37 +02:00

322 lines
20 KiB
TypeScript

import { describe, expect, test } from "vitest";
import {
getCanonicalHostRedirectUrl,
getHostAwareInternalPath,
getHostAwareInternalUrlPath,
getHostRoutingContext,
getHostRoutingContextFromRequest,
} from "./HostRouting.server";
import type { DomainRoutingConfig } from "@hivegrid/config";
const config: DomainRoutingConfig = {
appBaseDomain: "localhost:3000",
webAppHost: "app.localhost:3000",
adminAppHost: "admin.localhost:3000",
apiHost: "api.localhost:3000",
protocol: "http",
};
const env = {
APP_BASE_DOMAIN: config.appBaseDomain,
WEB_APP_HOST: config.webAppHost,
ADMIN_APP_HOST: config.adminAppHost,
API_HOST: config.apiHost,
APP_PROTOCOL: config.protocol,
};
describe("HostRouting", () => {
test("classifies canonical local hosts", () => {
expect(getHostRoutingContext("app.localhost:3000", config)).toBe("app");
expect(getHostRoutingContext("admin.localhost:3000", config)).toBe("admin");
expect(getHostRoutingContext("api.localhost:3000", config)).toBe("api");
expect(getHostRoutingContext("localhost:3000", config)).toBe("root");
});
test("maps host-aware app paths to canonical internal route paths", () => {
expect(getHostAwareInternalPath("/acme/dashboard", "app")).toBe("/acme/dashboard");
expect(getHostAwareInternalPath("/", "app")).toBe("/app");
});
test("maps app host request URLs without adding removed prefixes", () => {
expect(getHostAwareInternalUrlPath("http://app.localhost:3000/acme/dashboard?tab=activity#events", config)).toBe("/acme/dashboard?tab=activity#events");
expect(getHostAwareInternalUrlPath("http://app.localhost:3000/app/acme/dashboard?tab=activity#events", config)).toBe(
"/acme/dashboard?tab=activity#events"
);
expect(getHostAwareInternalUrlPath("http://app.localhost:3000/app/acme/dashboard.data?_routes=routes%2Fapp.%24tenant%2Fdashboard", config)).toBe(
"/acme/dashboard.data?_routes=routes%2Fapp.%24tenant%2Fdashboard"
);
});
test("keeps public auth routes on app and admin hosts from becoming tenant routes", () => {
expect(getHostAwareInternalUrlPath("http://app.localhost:3000/login?redirectTo=%2Facme%2Fdashboard", config)).toBe(
"/login?redirectTo=%2Facme%2Fdashboard"
);
expect(getHostAwareInternalUrlPath("http://app.localhost:3000/auth/magic-link", config)).toBe("/auth/magic-link");
expect(getHostAwareInternalUrlPath("http://app.localhost:3000/plan-required/acme", config)).toBe("/plan-required/acme");
expect(getHostAwareInternalUrlPath("http://app.localhost:3000/subscribe/acme?plan=pro", config)).toBe("/subscribe/acme?plan=pro");
expect(getHostAwareInternalUrlPath("http://app.localhost:3000/new-account", config)).toBe("/new-account");
expect(getHostAwareInternalUrlPath("http://app.localhost:3000/preferences", config)).toBe("/preferences");
expect(getHostAwareInternalUrlPath("http://app.localhost:3000/onboarding/session_123", config)).toBe("/onboarding/session_123");
expect(getHostAwareInternalUrlPath("http://app.localhost:3000/feature-disabled?feature=summary", config)).toBe("/feature-disabled?feature=summary");
expect(getHostAwareInternalUrlPath("http://app.localhost:3000/postman_collection.json", config)).toBe("/postman_collection.json");
expect(getHostAwareInternalUrlPath("http://app.localhost:3000/sitemap.xml", config)).toBe("/sitemap.xml");
expect(getHostAwareInternalUrlPath("http://app.localhost:3000/docs/en", config)).toBe("/docs/en");
expect(getHostAwareInternalUrlPath("http://app.localhost:3000/healthcheck", config)).toBe("/healthcheck");
expect(getHostAwareInternalUrlPath("http://app.localhost:3000/healthz", config)).toBe("/healthz");
expect(getHostAwareInternalUrlPath("http://app.localhost:3000/readyz", config)).toBe("/readyz");
expect(getHostAwareInternalUrlPath("http://app.localhost:3000/swagger", config)).toBe("/swagger");
expect(getHostAwareInternalUrlPath("http://app.localhost:3000/unauthorized/app.settings.members", config)).toBe("/unauthorized/app.settings.members");
expect(getHostAwareInternalUrlPath("http://app.localhost:3000/404", config)).toBe("/404");
expect(getHostAwareInternalUrlPath("http://admin.localhost:3000/login", config)).toBe("/login");
expect(getHostAwareInternalUrlPath("http://admin.localhost:3000/preferences", config)).toBe("/preferences");
expect(getHostAwareInternalUrlPath("http://admin.localhost:3000/onboarding/session_123", config)).toBe("/onboarding/session_123");
expect(getHostAwareInternalUrlPath("http://admin.localhost:3000/feature-disabled?feature=summary", config)).toBe("/feature-disabled?feature=summary");
expect(getHostAwareInternalUrlPath("http://admin.localhost:3000/postman_collection.json", config)).toBe("/postman_collection.json");
expect(getHostAwareInternalUrlPath("http://admin.localhost:3000/sitemap.xml", config)).toBe("/sitemap.xml");
});
test("keeps React Router manifest discovery on canonical hosts", () => {
expect(getHostAwareInternalUrlPath("http://admin.localhost:3000/__manifest?paths=%2Fmfa-challenge&version=abc", config)).toBe(
"/__manifest?paths=%2Fmfa-challenge&version=abc"
);
expect(getHostAwareInternalUrlPath("http://app.localhost:3000/__manifest?paths=%2Facme%2Fdashboard&version=abc", config)).toBe(
"/__manifest?paths=%2Facme%2Fdashboard&version=abc"
);
});
test("keeps canonical admin host routes on the admin host", () => {
expect(getHostAwareInternalUrlPath("http://admin.localhost:3000/dashboard?tab=users#active", config)).toBe("/dashboard?tab=users#active");
expect(getHostAwareInternalUrlPath("http://admin.localhost:3000/dashboard.data?_routes=routes%2Fdashboard", config)).toBe(
"/dashboard.data?_routes=routes%2Fdashboard"
);
expect(getHostAwareInternalUrlPath("http://admin.localhost:3000/mfa-challenge?redirectTo=%2Fdashboard", config)).toBe(
"/mfa-challenge?redirectTo=%2Fdashboard"
);
expect(getHostAwareInternalUrlPath("http://admin.localhost:3000/mfa-challenge.data?_routes=routes%2Fmfa-challenge", config)).toBe(
"/mfa-challenge.data?_routes=routes%2Fmfa-challenge"
);
expect(getHostAwareInternalUrlPath("http://admin.localhost:3000/internal/ai/chat", config)).toBe("/internal/ai/chat");
});
test("maps host-aware admin and API paths", () => {
expect(getHostAwareInternalPath("/accounts", "admin")).toBe("/accounts");
expect(getHostAwareInternalPath("/users", "admin")).toBe("/users");
expect(getHostAwareInternalPath("/health", "api")).toBe("/v1/health");
expect(getHostAwareInternalPath("/healthz", "api")).toBe("/healthz");
expect(getHostAwareInternalPath("/readyz", "api")).toBe("/readyz");
expect(getHostAwareInternalPath("/v1/health", "api")).toBe("/v1/health");
expect(getHostAwareInternalPath("/api/v1/health", "api")).toBe("/v1/health");
});
test("maps API host request URLs without path double-prefixing and preserves suffixes", () => {
expect(getHostAwareInternalUrlPath("http://api.localhost:3000/health?check=deep#status", config)).toBe(
"/v1/health?check=deep#status"
);
expect(getHostAwareInternalUrlPath("http://api.localhost:3000/v1/health?check=deep#status", config)).toBe(
"/v1/health?check=deep#status"
);
expect(getHostAwareInternalUrlPath("http://api.localhost:3000/api/v1/health?check=deep#status", config)).toBe(
"/v1/health?check=deep#status"
);
});
test("maps admin host request URLs without double-prefixing", () => {
expect(getHostAwareInternalUrlPath("http://admin.localhost:3000/accounts?tab=users#active", config)).toBe("/accounts?tab=users#active");
expect(getHostAwareInternalUrlPath("http://admin.localhost:3000/users?tab=users#active", config)).toBe("/users?tab=users#active");
});
test("uses forwarded host when deriving request context behind a proxy", () => {
const request = new Request("http://localhost:3000/acme/dashboard", {
headers: {
host: "localhost:3000",
"x-forwarded-host": " app.localhost:3000 , api.localhost:3000",
},
});
expect(getHostRoutingContextFromRequest(request, env)).toBe("app");
});
test("uses forwarded host for internal URL mapping behind a proxy", () => {
const request = new Request("http://localhost:3000/acme/dashboard?tab=activity#events", {
headers: {
host: "localhost:3000",
"x-forwarded-host": " app.localhost:3000 , api.localhost:3000",
},
});
expect(getHostAwareInternalUrlPath(request, config)).toBe("/acme/dashboard?tab=activity#events");
});
test("uses forwarded host for canonical redirects behind a proxy", () => {
const request = new Request("http://localhost:3000/settings?tab=users#active", {
headers: {
host: "localhost:3000",
"x-forwarded-host": " admin.localhost:3000 , app.localhost:3000",
},
});
expect(getCanonicalHostRedirectUrl(request, config)).toBeNull();
});
test("uses the first forwarded host value for routing", () => {
const request = new Request("http://localhost:3000/acme/dashboard?tab=activity#events", {
headers: {
host: "localhost:3000",
"x-forwarded-host": " admin.localhost:3000 , api.localhost:3000",
},
});
expect(getHostRoutingContextFromRequest(request, env)).toBe("admin");
expect(getCanonicalHostRedirectUrl(request, config)).toBe("http://app.localhost:3000/acme/dashboard?tab=activity#events");
});
test("does not let forwarded host spoof an already canonical direct host", () => {
const request = new Request("http://app.localhost:3000/dashboard?tab=activity#events", {
headers: {
host: "app.localhost:3000",
"x-forwarded-host": "admin.localhost:3000",
},
});
expect(getHostRoutingContextFromRequest(request, env)).toBe("app");
expect(getCanonicalHostRedirectUrl(request, config)).toBe("http://admin.localhost:3000/dashboard?tab=activity#events");
});
test("redirects root-host canonical admin and tenant paths to their canonical hosts", () => {
expect(getCanonicalHostRedirectUrl("http://localhost:3000/dashboard?tab=activity#events", config)).toBe(
"http://admin.localhost:3000/dashboard?tab=activity#events"
);
expect(getCanonicalHostRedirectUrl("http://localhost:3000/accounts", config)).toBe("http://admin.localhost:3000/accounts");
expect(getCanonicalHostRedirectUrl("http://localhost:3000/acme/dashboard?tab=activity#events", config)).toBe(
"http://app.localhost:3000/acme/dashboard?tab=activity#events"
);
expect(getCanonicalHostRedirectUrl("http://localhost:3000/app/acme/dashboard?tab=activity#events", config)).toBe(
"http://app.localhost:3000/acme/dashboard?tab=activity#events"
);
expect(getCanonicalHostRedirectUrl("http://localhost:3000/api/v1/health?check=deep#status", config)).toBe(
"http://api.localhost:3000/v1/health?check=deep#status"
);
expect(getCanonicalHostRedirectUrl("http://localhost:3000/admin/settings?tab=users#active", config)).toBeNull();
expect(getCanonicalHostRedirectUrl("http://localhost:3000/admin", config)).toBeNull();
});
test("does not redirect a canonical route to itself when adminAppHost equals webAppHost (no 308 loop)", () => {
// Single-Host-Staging / Fehlkonfig: adminAppHost == webAppHost. /dashboard (Admin-Surface) wuerde sonst
// kanonisch auf denselben Host+Pfad umgeleitet -> 308-Endlosschleife. Der Self-Redirect-Guard liefert null.
const sameHostConfig: DomainRoutingConfig = { ...config, adminAppHost: config.webAppHost };
expect(getCanonicalHostRedirectUrl("http://app.localhost:3000/dashboard?tab=activity#events", sameHostConfig)).toBeNull();
expect(getCanonicalHostRedirectUrl("http://app.localhost:3000/accounts/users", sameHostConfig)).toBeNull();
// Getrennte Hosts (Prod) bleiben unveraendert -> Redirect feuert weiterhin.
expect(getCanonicalHostRedirectUrl("http://app.localhost:3000/dashboard?tab=activity#events", config)).toBe(
"http://admin.localhost:3000/dashboard?tab=activity#events"
);
});
test("redirects canonical route paths off the wrong canonical host", () => {
expect(getCanonicalHostRedirectUrl("http://app.localhost:3000/dashboard?tab=activity#events", config)).toBe(
"http://admin.localhost:3000/dashboard?tab=activity#events"
);
expect(getCanonicalHostRedirectUrl("http://app.localhost:3000/accounts/users?filter=active", config)).toBe(
"http://admin.localhost:3000/accounts/users?filter=active"
);
expect(getCanonicalHostRedirectUrl("http://app.localhost:3000/internal/imports/dry-run", config)).toBeNull();
expect(getCanonicalHostRedirectUrl("http://api.localhost:3000/dashboard?tab=activity#events", config)).toBe(
"http://admin.localhost:3000/dashboard?tab=activity#events"
);
expect(getCanonicalHostRedirectUrl("http://admin.localhost:3000/acme/dashboard?tab=activity#events", config)).toBe(
"http://app.localhost:3000/acme/dashboard?tab=activity#events"
);
expect(getCanonicalHostRedirectUrl("http://admin.localhost:3000/acme", config)).toBe("http://app.localhost:3000/acme");
expect(getCanonicalHostRedirectUrl("http://api.localhost:3000/acme", config)).toBe("http://app.localhost:3000/acme");
expect(getCanonicalHostRedirectUrl("http://api.localhost:3000/acme/dashboard?tab=activity#events", config)).toBe(
"http://app.localhost:3000/acme/dashboard?tab=activity#events"
);
expect(getCanonicalHostRedirectUrl("http://admin.localhost:3000/acme/portals/customer", config)).toBeNull();
expect(getCanonicalHostRedirectUrl("http://admin.localhost:3000/acme/widgets", config)).toBeNull();
expect(getCanonicalHostRedirectUrl("http://admin.localhost:3000/acme/workflow-engine", config)).toBeNull();
expect(getCanonicalHostRedirectUrl("http://admin.localhost:3000/acme/contact", config)).toBe("http://app.localhost:3000/acme/contact");
expect(getCanonicalHostRedirectUrl("http://admin.localhost:3000/v1/users?cursor=next", config)).toBe("http://api.localhost:3000/v1/users?cursor=next");
expect(getCanonicalHostRedirectUrl("http://app.localhost:3000/v1/users?cursor=next", config)).toBe("http://api.localhost:3000/v1/users?cursor=next");
expect(getCanonicalHostRedirectUrl("http://app.localhost:3000/api/v1/health?check=deep#status", config)).toBe(
"http://api.localhost:3000/v1/health?check=deep#status"
);
expect(getCanonicalHostRedirectUrl("http://app.localhost:3000/app/acme/dashboard?tab=activity#events", config)).toBe(
"http://app.localhost:3000/acme/dashboard?tab=activity#events"
);
expect(getCanonicalHostRedirectUrl("http://admin.localhost:3000/api/v1/health?check=deep#status", config)).toBe(
"http://api.localhost:3000/v1/health?check=deep#status"
);
expect(getCanonicalHostRedirectUrl("http://app.localhost:3000/admin/settings?tab=users#active", config)).toBeNull();
});
test("redirects API host unversioned aliases to canonical v1 paths", () => {
expect(getCanonicalHostRedirectUrl("http://api.localhost:3000/health?check=deep#status", config)).toBe(
"http://api.localhost:3000/v1/health?check=deep#status"
);
expect(getCanonicalHostRedirectUrl("http://api.localhost:3000/accounts?cursor=next", config)).toBe(
"http://api.localhost:3000/v1/accounts?cursor=next"
);
expect(getCanonicalHostRedirectUrl("http://api.localhost:3000/auth/login", config)).toBe("http://api.localhost:3000/v1/auth/login");
expect(getCanonicalHostRedirectUrl("http://api.localhost:3000/analytics/events", config)).toBe("http://api.localhost:3000/v1/analytics/events");
expect(getCanonicalHostRedirectUrl("http://api.localhost:3000/entities/contact/rows", config)).toBe(
"http://api.localhost:3000/v1/entities/contact/rows"
);
expect(getCanonicalHostRedirectUrl("http://api.localhost:3000/api-keys/validate", config)).toBe(
"http://api.localhost:3000/v1/api-keys/validate"
);
});
test("does not canonicalize unsupported API host aliases", () => {
expect(getCanonicalHostRedirectUrl("http://api.localhost:3000/webhooks/inbound", config)).toBe("http://app.localhost:3000/webhooks/inbound");
expect(getCanonicalHostRedirectUrl("http://api.localhost:3000/imports/dry-run", config)).toBe("http://app.localhost:3000/imports/dry-run");
expect(getCanonicalHostRedirectUrl("http://api.localhost:3000/workflows/run/workflow_1", config)).toBeNull();
});
test("keeps public root-host routes on the root host", () => {
expect(getCanonicalHostRedirectUrl("http://localhost:3000/login?redirectTo=%2Facme%2Fdashboard", config)).toBeNull();
});
test("redirects public web routes away from the API host", () => {
expect(getCanonicalHostRedirectUrl("http://api.localhost:3000/login?redirectTo=%2Facme%2Fdashboard", config)).toBe(
"http://app.localhost:3000/login?redirectTo=%2Facme%2Fdashboard"
);
expect(getCanonicalHostRedirectUrl("http://api.localhost:3000/healthcheck", config)).toBe("http://app.localhost:3000/healthcheck");
});
test("redirects app-host reserved admin slugs instead of treating them as tenants", () => {
expect(getCanonicalHostRedirectUrl("http://app.localhost:3000/crm/dashboard", config)).toBeNull();
expect(getCanonicalHostRedirectUrl("http://app.localhost:3000/accounts/users", config)).toBe("http://admin.localhost:3000/accounts/users");
});
test("does not canonicalize disabled plugin route trees to active hosts", () => {
expect(getCanonicalHostRedirectUrl("http://localhost:3000/crm/dashboard", config)).toBeNull();
expect(getCanonicalHostRedirectUrl("http://localhost:3000/internal/ai/chat", config)).toBeNull();
expect(getCanonicalHostRedirectUrl("http://localhost:3000/internal/api/__workflows/jobs", config)).toBeNull();
expect(getCanonicalHostRedirectUrl("http://localhost:3000/internal/api/workflows/run/workflow_1", config)).toBeNull();
expect(getCanonicalHostRedirectUrl("http://localhost:3000/workflows/run/workflow_1", config)).toBeNull();
expect(getCanonicalHostRedirectUrl("http://localhost:3000/admin/internal/ai/chat", config)).toBeNull();
expect(getCanonicalHostRedirectUrl("http://localhost:3000/admin/__workflows/jobs", config)).toBeNull();
expect(getCanonicalHostRedirectUrl("http://localhost:3000/knowledge-base/articles", config)).toBeNull();
expect(getCanonicalHostRedirectUrl("http://localhost:3000/help/getting-started", config)).toBeNull();
expect(getCanonicalHostRedirectUrl("http://localhost:3000/mcp", config)).toBeNull();
expect(getCanonicalHostRedirectUrl("http://api.localhost:3000/help/getting-started", config)).toBeNull();
expect(getCanonicalHostRedirectUrl("http://api.localhost:3000/mcp", config)).toBeNull();
expect(getCanonicalHostRedirectUrl("http://api.localhost:3000/internal/ai/chat", config)).toBeNull();
expect(getCanonicalHostRedirectUrl("http://api.localhost:3000/__workflows/jobs", config)).toBeNull();
expect(getCanonicalHostRedirectUrl("http://app.localhost:3000/knowledge-base/articles", config)).toBeNull();
expect(getCanonicalHostRedirectUrl("http://app.localhost:3000/acme/internal/ai/chat", config)).toBeNull();
expect(getCanonicalHostRedirectUrl("http://admin.localhost:3000/acme/crm", config)).toBeNull();
expect(getCanonicalHostRedirectUrl("http://admin.localhost:3000/acme/__widgets/widget_1", config)).toBeNull();
expect(getCanonicalHostRedirectUrl("http://admin.localhost:3000/acme/internal/ai/chat", config)).toBeNull();
expect(getCanonicalHostRedirectUrl("http://admin.localhost:3000/__workflows/jobs", config)).toBeNull();
expect(getCanonicalHostRedirectUrl("http://localhost:3000/acme/internal/ai/chat", config)).toBeNull();
});
test("builds canonical redirect URLs for versioned API paths", () => {
expect(getCanonicalHostRedirectUrl("http://api.localhost:3000/v1/health?check=deep#status", config)).toBeNull();
expect(getCanonicalHostRedirectUrl("http://localhost:3000/v1/health?check=deep#status", config)).toBe(
"http://api.localhost:3000/v1/health?check=deep#status"
);
});
});