Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/utils/services/.server/urlService.test.ts
T

189 lines
7.8 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import type { Params } from "react-router";
vi.mock("../../db.server", () => ({
db: {
tenant: {
findFirst: vi.fn(),
},
},
}));
vi.mock("../../cache.server", () => ({
cachified: vi.fn(),
clearCacheKey: vi.fn(),
}));
import { cachified } from "../../cache.server";
import { db } from "../../db.server";
import { getTenantIdFromUrl, getTenantIdOrNull, resolveTenantLookupSource } from "./urlService";
const env = {
APP_BASE_DOMAIN: "localhost:3000",
WEB_APP_HOST: "app.localhost:3000",
ADMIN_APP_HOST: "admin.localhost:3000",
API_HOST: "api.localhost:3000",
APP_PROTOCOL: "http",
};
function makeRequest(host: string, pathname = "/app/acme") {
return new Request(`http://localhost:3000${pathname}`, {
headers: {
host,
},
});
}
async function expectRedirectToApp(promise: Promise<unknown>) {
try {
await promise;
throw new Error("Expected redirect to /app");
} catch (error) {
expect(error).toBeInstanceOf(Response);
expect((error as Response).status).toBe(302);
expect((error as Response).headers.get("Location")).toBe("http://app.localhost:3000/");
}
}
describe("resolveTenantLookupSource", () => {
beforeEach(() => {
vi.stubEnv("APP_BASE_DOMAIN", env.APP_BASE_DOMAIN);
vi.stubEnv("WEB_APP_HOST", env.WEB_APP_HOST);
vi.stubEnv("ADMIN_APP_HOST", env.ADMIN_APP_HOST);
vi.stubEnv("API_HOST", env.API_HOST);
vi.stubEnv("APP_PROTOCOL", env.APP_PROTOCOL);
});
it("uses explicit params when no request exists to keep route-loader behavior stable", () => {
expect(resolveTenantLookupSource({ params: { tenant: "acme" } })).toEqual({ source: "app-route-param", tenant: "acme" });
expect(resolveTenantLookupSource({ params: {} })).toEqual({ source: "none", tenant: null });
});
it("allows tenant lookup only for app-host requests with params or legacy app paths", () => {
expect(resolveTenantLookupSource({ request: makeRequest(env.WEB_APP_HOST, "/acme/dashboard"), params: { tenant: "acme" } })).toEqual({
source: "app-route-param",
tenant: "acme",
});
expect(resolveTenantLookupSource({ request: makeRequest(env.WEB_APP_HOST, "/app/acme/dashboard"), params: {} })).toEqual({
source: "app-route-param",
tenant: "acme",
});
});
it.each([
["admin", env.ADMIN_APP_HOST],
["api", env.API_HOST],
["root", env.APP_BASE_DOMAIN],
["unknown", "unknown.localhost:3000"],
])("does not infer tenant context for %s-host /app paths", (_label, host) => {
expect(resolveTenantLookupSource({ request: makeRequest(host, "/app/acme/dashboard"), params: { tenant: "acme" } })).toEqual({
source: "none",
tenant: null,
});
});
it("keeps public app-host routes out of tenant resolution", () => {
expect(resolveTenantLookupSource({ request: makeRequest(env.WEB_APP_HOST, "/"), params: {} })).toEqual({ source: "none", tenant: null });
expect(resolveTenantLookupSource({ request: makeRequest(env.WEB_APP_HOST, "/app"), params: {} })).toEqual({ source: "none", tenant: null });
expect(resolveTenantLookupSource({ request: makeRequest(env.WEB_APP_HOST, "/login"), params: {} })).toEqual({ source: "none", tenant: null });
expect(resolveTenantLookupSource({ request: makeRequest(env.WEB_APP_HOST, "/reset?token=abc"), params: {} })).toEqual({ source: "none", tenant: null });
});
it("does not self-redirect when the app host is also the base domain", () => {
vi.stubEnv("APP_BASE_DOMAIN", "boiler.saas-ctrl.com");
vi.stubEnv("WEB_APP_HOST", "boiler.saas-ctrl.com");
vi.stubEnv("ADMIN_APP_HOST", "admin.boiler.saas-ctrl.com");
vi.stubEnv("API_HOST", "api.boiler.saas-ctrl.com");
vi.stubEnv("APP_PROTOCOL", "https");
expect(resolveTenantLookupSource({ request: makeRequest("boiler.saas-ctrl.com", "/"), params: {} })).toEqual({ source: "none", tenant: null });
expect(resolveTenantLookupSource({ request: makeRequest("boiler.saas-ctrl.com", "/app"), params: {} })).toEqual({ source: "none", tenant: null });
expect(resolveTenantLookupSource({ request: makeRequest("boiler.saas-ctrl.com", "/login"), params: {} })).toEqual({ source: "none", tenant: null });
});
});
describe("getTenantIdOrNull", () => {
beforeEach(() => {
vi.stubEnv("APP_BASE_DOMAIN", env.APP_BASE_DOMAIN);
vi.stubEnv("WEB_APP_HOST", env.WEB_APP_HOST);
vi.stubEnv("ADMIN_APP_HOST", env.ADMIN_APP_HOST);
vi.stubEnv("API_HOST", env.API_HOST);
vi.stubEnv("APP_PROTOCOL", env.APP_PROTOCOL);
vi.clearAllMocks();
});
it.each([
["admin", env.ADMIN_APP_HOST],
["api", env.API_HOST],
["root", env.APP_BASE_DOMAIN],
["unknown", "unknown.localhost:3000"],
])("returns null without DB lookup for %s-host /app paths", async (_label, host) => {
await expect(getTenantIdOrNull({ request: makeRequest(host, "/app/acme/dashboard"), params: { tenant: "acme" } })).resolves.toBeNull();
expect(db.tenant.findFirst).not.toHaveBeenCalled();
});
it("allows admin-host routes to opt into tenant scope through tenantId query parameters", async () => {
vi.mocked(db.tenant.findFirst).mockResolvedValueOnce({ id: "tenant_1" });
await expect(getTenantIdOrNull({ request: makeRequest(env.ADMIN_APP_HOST, "/forms?tenantId=acme"), params: {} })).resolves.toBe("tenant_1");
expect(db.tenant.findFirst).toHaveBeenCalledWith({
where: { OR: [{ slug: "acme" }, { id: "acme" }] },
select: { id: true },
});
});
it("keeps admin-host routes global when tenantId query parameters are absent", async () => {
await expect(getTenantIdOrNull({ request: makeRequest(env.ADMIN_APP_HOST, "/forms"), params: {} })).resolves.toBeNull();
expect(db.tenant.findFirst).not.toHaveBeenCalled();
});
it("looks up exactly one tenant-bearing app-host request and returns its id", async () => {
vi.mocked(db.tenant.findFirst).mockResolvedValueOnce({ id: "tenant_1" });
await expect(getTenantIdOrNull({ request: makeRequest(env.WEB_APP_HOST, "/acme/dashboard"), params: { tenant: "acme" } })).resolves.toBe("tenant_1");
expect(db.tenant.findFirst).toHaveBeenCalledTimes(1);
expect(db.tenant.findFirst).toHaveBeenCalledWith({
where: { OR: [{ slug: "acme" }, { id: "acme" }] },
select: { id: true },
});
});
it("returns null for public app-host routes without DB lookup", async () => {
await expect(getTenantIdOrNull({ request: makeRequest(env.WEB_APP_HOST, "/"), params: {} })).resolves.toBeNull();
await expect(getTenantIdOrNull({ request: makeRequest(env.WEB_APP_HOST, "/app"), params: {} })).resolves.toBeNull();
await expect(getTenantIdOrNull({ request: makeRequest(env.WEB_APP_HOST, "/login"), params: {} })).resolves.toBeNull();
expect(db.tenant.findFirst).not.toHaveBeenCalled();
});
it("redirects app-host requests with unknown tenants to /app", async () => {
vi.mocked(db.tenant.findFirst).mockResolvedValueOnce(null);
const consoleLog = vi.spyOn(console, "log").mockImplementation(() => undefined);
await expectRedirectToApp(getTenantIdOrNull({ request: makeRequest(env.WEB_APP_HOST, "/acme/dashboard"), params: { tenant: "acme" } }));
expect(db.tenant.findFirst).toHaveBeenCalledTimes(1);
consoleLog.mockRestore();
});
});
describe("getTenantIdFromUrl", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(cachified).mockImplementation((async ({ getFreshValue }: { getFreshValue: (context?: unknown) => unknown }) => getFreshValue()) as typeof cachified);
});
it("keeps explicit tenant param lookup behavior stable", async () => {
vi.mocked(db.tenant.findFirst).mockResolvedValueOnce({ id: "tenant_1" });
await expect(getTenantIdFromUrl({ tenant: "acme" } as Params)).resolves.toBe("tenant_1");
expect(db.tenant.findFirst).toHaveBeenCalledWith({
where: { OR: [{ slug: "acme" }, { id: "acme" }] },
select: { id: true },
});
});
});