163 lines
7.5 KiB
TypeScript
163 lines
7.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { remixConfigRoutes } from "@react-router/remix-config-routes-adapter";
|
|
import { defaultDisabledPluginRouteFilePatterns, defaultIgnoredRouteFilePatterns } from "./routes";
|
|
import { createRoutesFromFolders } from "./utils/compat/v1-route-convention";
|
|
import { resolveRepoPath } from "./utils/testing/repoPaths";
|
|
|
|
const routes = remixConfigRoutes(async (defineRoutes) => {
|
|
return createRoutesFromFolders(defineRoutes, {
|
|
appDirectory: resolveRepoPath("apps/web/app"),
|
|
ignoredFilePatterns: [...defaultIgnoredRouteFilePatterns, ...defaultDisabledPluginRouteFilePatterns],
|
|
});
|
|
});
|
|
|
|
type RouteManifest = Awaited<typeof routes>;
|
|
type RouteManifestEntry = RouteManifest[number];
|
|
|
|
function flattenRoutes(manifest: RouteManifest) {
|
|
const flattened: RouteManifestEntry[] = [];
|
|
const visit = (entry: RouteManifestEntry) => {
|
|
flattened.push(entry);
|
|
Object.values(entry.children ?? {}).forEach(visit);
|
|
};
|
|
Object.values(manifest).forEach(visit);
|
|
return flattened;
|
|
}
|
|
|
|
describe("app route discovery", () => {
|
|
it("excludes colocated test and spec files from the production route manifest", async () => {
|
|
const routeFiles = flattenRoutes(await routes).map((entry) => entry.file);
|
|
|
|
expect(routeFiles).not.toEqual(expect.arrayContaining([expect.stringMatching(/\.test\.[tj]sx?$/), expect.stringMatching(/\.spec\.[tj]sx?$/)]));
|
|
expect(routeFiles).not.toEqual(expect.arrayContaining([expect.stringMatching(/\.server\.[tj]sx?$/)]));
|
|
expect(routeFiles).toEqual(expect.arrayContaining(["routes/forgot-password.tsx", "routes/invitation.$id.tsx"]));
|
|
expect(routeFiles).toEqual(expect.arrayContaining(["routes/401.tsx", "routes/403.tsx", "routes/404.tsx", "routes/500.tsx"]));
|
|
expect(routeFiles).toEqual(expect.arrayContaining(["routes/plan-required.$tenant.tsx", "routes/feature-disabled.tsx"]));
|
|
});
|
|
|
|
it("exposes promoted tenant app routes without legacy /app prefixes", async () => {
|
|
const routePaths = flattenRoutes(await routes)
|
|
.map((entry) => entry.path)
|
|
.filter((path): path is string => typeof path === "string");
|
|
expect(routePaths).not.toEqual(expect.arrayContaining(["app/:tenant"]));
|
|
expect(routePaths).not.toEqual(expect.arrayContaining([expect.stringMatching(/^app\/:tenant\//)]));
|
|
expect(routePaths).toEqual(expect.arrayContaining([":tenant"]));
|
|
expect(routePaths).toEqual(expect.arrayContaining(["plan-required/:tenant"]));
|
|
});
|
|
|
|
it("exposes admin routes only through admin-host aliases", async () => {
|
|
const flattenedRoutes = flattenRoutes(await routes);
|
|
const legacyAdminRoutes = flattenedRoutes.filter((entry) => (entry.id === "routes/admin" || entry.id.startsWith("routes/admin/")) && !entry.id.endsWith(".__host"));
|
|
const adminHostRoutes = flattenedRoutes.filter((entry) => entry.id === "routes/admin.__host" || (entry.id.startsWith("routes/admin/") && entry.id.endsWith(".__host")));
|
|
const routePaths = flattenedRoutes
|
|
.map((entry) => entry.path)
|
|
.filter((path): path is string => typeof path === "string");
|
|
|
|
expect(legacyAdminRoutes).toEqual([]);
|
|
expect(adminHostRoutes.length).toBeGreaterThan(0);
|
|
expect(routePaths).not.toEqual(expect.arrayContaining(["admin", expect.stringMatching(/^admin\//)]));
|
|
});
|
|
|
|
it("does not expose the removed public pricing marketing routes", async () => {
|
|
const flattenedRoutes = flattenRoutes(await routes);
|
|
const routeFiles = flattenedRoutes.map((entry) => entry.file);
|
|
const routePaths = flattenedRoutes
|
|
.map((entry) => entry.path)
|
|
.filter((path): path is string => typeof path === "string");
|
|
|
|
expect(routeFiles).not.toEqual(expect.arrayContaining(["routes/pricing.tsx", "routes/pricing.$session.success.tsx"]));
|
|
expect(routePaths).not.toEqual(expect.arrayContaining(["pricing", "pricing/:session/success"]));
|
|
expect(routePaths).toEqual(expect.arrayContaining(["subscribe/:tenant"]));
|
|
});
|
|
|
|
it("registers product-core tenant routes and keeps disabled plugin route trees out of the core app", async () => {
|
|
const flattenedRoutes = flattenRoutes(await routes);
|
|
const routeFiles = flattenedRoutes.map((entry) => entry.file);
|
|
const routePaths = flattenedRoutes
|
|
.map((entry) => entry.path)
|
|
.filter((path): path is string => typeof path === "string");
|
|
|
|
expect(routeFiles).not.toEqual(
|
|
expect.arrayContaining([
|
|
expect.stringMatching(/^routes\/admin\/(?:__workflows|ai|chat|crm|email-marketing|help-desk|internal\/ai|knowledge-base|prompts)(?:\.tsx|\/)/),
|
|
expect.stringMatching(/^routes\/admin\/portals\.tsx$/),
|
|
expect.stringMatching(/^routes\/app\.\$tenant\/(?:__portals|__widgets|chat|internal\/ai|sites)(?:\.tsx|\/)/),
|
|
expect.stringMatching(/^routes\/help\//),
|
|
expect.stringMatching(/^routes\/internal\/api\/__workflows\//),
|
|
expect.stringMatching(/^routes\/mcp\//),
|
|
])
|
|
);
|
|
expect(routeFiles).toEqual(
|
|
expect.arrayContaining([
|
|
"routes/app.$tenant/crm.tsx",
|
|
"routes/app.$tenant/email-marketing.tsx",
|
|
"routes/app.$tenant/__workflows/workflow-engine.tsx",
|
|
])
|
|
);
|
|
expect(routePaths).not.toEqual(
|
|
expect.arrayContaining([
|
|
"ai",
|
|
"chat",
|
|
"help-desk",
|
|
"knowledge-base",
|
|
"mcp",
|
|
"prompts",
|
|
"portals",
|
|
"help/:slug/:lang?/articles/:article",
|
|
"help/:slug/:lang?/categories/:category",
|
|
"help/:slug/:lang?",
|
|
"internal/api/workflows",
|
|
"internal/api/workflows/run/:id",
|
|
"internal/api/workflows/executions/:id",
|
|
"internal/api/workflows/executions/:id/continue",
|
|
"internal/api/workflows/stream/:executionId",
|
|
":tenant/chat",
|
|
":tenant/sites",
|
|
":tenant/portals",
|
|
":tenant/portals/:portal",
|
|
":tenant/widgets",
|
|
])
|
|
);
|
|
expect(routePaths).toEqual(expect.arrayContaining(["crm", "email-marketing", "workflow-engine"]));
|
|
});
|
|
|
|
it("does not keep internal workflow API proxy targets registered", async () => {
|
|
const flattenedRoutes = flattenRoutes(await routes);
|
|
const routeFiles = flattenedRoutes.map((entry) => entry.file);
|
|
const routePaths = flattenedRoutes
|
|
.map((entry) => entry.path)
|
|
.filter((path): path is string => typeof path === "string");
|
|
|
|
expect(routeFiles).not.toEqual(
|
|
expect.arrayContaining([
|
|
"routes/internal/api/__workflows/workflows.run.$id.tsx",
|
|
"routes/internal/api/__workflows/workflows.executions.$id.tsx",
|
|
"routes/internal/api/__workflows/workflows.executions.$id.continue.tsx",
|
|
"routes/internal/api/__workflows/workflows.executions.$id.continue.start.tsx",
|
|
"routes/internal/api/__workflows/workflows.stream.$executionId.tsx",
|
|
])
|
|
);
|
|
expect(routePaths).not.toEqual(
|
|
expect.arrayContaining([
|
|
"internal/api/workflows/run/:id",
|
|
"internal/api/workflows/executions/:id",
|
|
"internal/api/workflows/executions/:id/continue",
|
|
"internal/api/workflows/executions/:id/continue/start",
|
|
"internal/api/workflows/stream/:executionId",
|
|
])
|
|
);
|
|
});
|
|
|
|
it("does not expose removed fake/demo admin entity routes", async () => {
|
|
const flattenedRoutes = flattenRoutes(await routes);
|
|
const routeFiles = flattenedRoutes.map((entry) => entry.file);
|
|
const routePaths = flattenedRoutes
|
|
.map((entry) => entry.path)
|
|
.filter((path): path is string => typeof path === "string");
|
|
|
|
expect(routeFiles).not.toEqual(expect.arrayContaining(["routes/admin/entities/fake-rows.tsx"]));
|
|
expect(routePaths).not.toEqual(expect.arrayContaining(["entities/fake-rows"]));
|
|
});
|
|
});
|