163 lines
8.7 KiB
JavaScript
163 lines
8.7 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { describe, it } from "vitest";
|
|
|
|
import { verifyWebBuildSurfaces } from "./verify-web-build-surfaces.mjs";
|
|
|
|
const portalRoutes = {
|
|
root: { id: "root", path: "" },
|
|
"routes/healthz": { id: "routes/healthz", path: "healthz" },
|
|
"routes/readyz": { id: "routes/readyz", path: "readyz" },
|
|
"routes/healthcheck": { id: "routes/healthcheck", path: "healthcheck" },
|
|
"routes/api.metrics.portal": { id: "routes/api.metrics.portal", path: "api/metrics" },
|
|
"routes/customer-portal/layout": { id: "routes/customer-portal/layout", path: ":tenant" },
|
|
"routes/customer-portal/dashboard": { id: "routes/customer-portal/dashboard", index: true },
|
|
"routes/customer-portal/projects": { id: "routes/customer-portal/projects", path: "projects" },
|
|
"routes/customer-portal/project-detail": { id: "routes/customer-portal/project-detail", path: "projects/:projectId" },
|
|
"routes/customer-portal/document-download": { id: "routes/customer-portal/document-download", path: "documents/:releaseId" },
|
|
"routes/customer-portal/upload": { id: "routes/customer-portal/upload", path: "upload" },
|
|
"routes/customer-portal/settings": { id: "routes/customer-portal/settings", path: "settings" },
|
|
"routes/customer-portal/branding-logo": { id: "routes/customer-portal/branding-logo", path: "branding/logo" },
|
|
"routes/customer-portal/export-download": { id: "routes/customer-portal/export-download", path: "exports/:exportId" },
|
|
"routes/customer-portal/project-upload": { id: "routes/customer-portal/project-upload", path: "projects/:projectId/upload" },
|
|
"routes/customer-portal/project-complaint": {
|
|
id: "routes/customer-portal/project-complaint",
|
|
path: "projects/:projectId/complaints/:formId",
|
|
},
|
|
"routes/customer-portal/logout": { id: "routes/customer-portal/logout", path: "logout" },
|
|
"routes/customer-portal/auth": { id: "routes/customer-portal/auth", path: "*" },
|
|
"routes/customer-portal/layout.__custom-host": {
|
|
id: "routes/customer-portal/layout.__custom-host",
|
|
parentId: "root",
|
|
},
|
|
...Object.fromEntries(
|
|
[
|
|
["dashboard", undefined],
|
|
["projects", "projects"],
|
|
["upload", "upload"],
|
|
["settings", "settings"],
|
|
["branding-logo", "branding/logo"],
|
|
["export-download", "exports/:exportId"],
|
|
["project-upload", "projects/:projectId/upload"],
|
|
["project-complaint", "projects/:projectId/complaints/:formId"],
|
|
["project-detail", "projects/:projectId"],
|
|
["document-download", "documents/:releaseId"],
|
|
["logout", "logout"],
|
|
["auth.login", "login/*"],
|
|
["auth.forgot-password", "forgot-password"],
|
|
["auth.setup", "setup/*"],
|
|
["auth.verify-email", "verify-email/*"],
|
|
["auth.reset-password", "reset-password/*"],
|
|
].map(([name, path]) => {
|
|
const id = `routes/customer-portal/${name}.__custom-host`;
|
|
return [id, { id, parentId: "routes/customer-portal/layout.__custom-host", ...(path ? { path } : { index: true }) }];
|
|
})
|
|
),
|
|
};
|
|
|
|
describe("verify-web-build-surfaces", () => {
|
|
it("accepts isolated Core and Customer Portal manifests", () => {
|
|
const root = fixture({ root: { id: "root" }, "routes/app": { id: "routes/app", path: ":tenant" } }, portalRoutes);
|
|
assert.deepEqual(verifyWebBuildSurfaces(root), { coreRouteCount: 2, portalRouteCount: 35 });
|
|
});
|
|
|
|
it("rejects Customer Portal routes in Core and Core routes in Portal", () => {
|
|
const crossedCore = fixture({ root: { id: "root" }, "routes/customer-portal/layout": { id: "routes/customer-portal/layout" } }, portalRoutes);
|
|
assert.throws(() => verifyWebBuildSurfaces(crossedCore), /Core manifest contains Customer Portal routes/);
|
|
|
|
const crossedPortal = fixture({ root: { id: "root" } }, { ...portalRoutes, "routes/app": { id: "routes/app" } });
|
|
assert.throws(() => verifyWebBuildSurfaces(crossedPortal), /Portal manifest route isolation failed/);
|
|
});
|
|
|
|
it("rejects opposite-surface server chunks even when route manifests are isolated", () => {
|
|
const portalLeak = fixture({ root: { id: "root" } }, portalRoutes);
|
|
writeServerAsset(portalLeak, "build-portal", "root-core.server-fixture.js", "export function loadCoreRootData() {}\n");
|
|
assert.throws(() => verifyWebBuildSurfaces(portalLeak), /Portal server build contains opposite-surface code/);
|
|
|
|
const coreLeak = fixture({ root: { id: "root" } }, portalRoutes);
|
|
writeServerAsset(coreLeak, "build", "portal-root.server-fixture.js", "export function loadPortalRootData() {}\n");
|
|
assert.throws(() => verifyWebBuildSurfaces(coreLeak), /Core server build contains opposite-surface code/);
|
|
});
|
|
|
|
it("rejects the Core cache bootstrap in the Portal server artifact", () => {
|
|
const portalLeak = fixture({ root: { id: "root" } }, portalRoutes);
|
|
writeServerAsset(portalLeak, "build-portal", "cache-bootstrap-fixture.js", "export function startCacheSubscribers() {}\n");
|
|
|
|
assert.throws(() => verifyWebBuildSurfaces(portalLeak), /Portal server build contains opposite-surface code \(cache-bootstrap\)/);
|
|
});
|
|
|
|
it("rejects client assets compiled for the opposite surface", () => {
|
|
const portalLeak = fixture({ root: { id: "root" } }, portalRoutes);
|
|
writeClientAsset(portalLeak, "build-portal", "core-root-fixture.js", 'throw new Error("Core build received Portal root data");\n');
|
|
assert.throws(() => verifyWebBuildSurfaces(portalLeak), /Portal client build contains opposite-surface code/);
|
|
|
|
const coreLeak = fixture({ root: { id: "root" } }, portalRoutes);
|
|
writeClientAsset(coreLeak, "build", "portal-root-fixture.js", 'throw new Error("Portal build received Core root data");\n');
|
|
assert.throws(() => verifyWebBuildSurfaces(coreLeak), /Core client build contains opposite-surface code/);
|
|
});
|
|
|
|
it("rejects Core browser code and public files in the Portal artifact", () => {
|
|
const clientLeak = fixture({ root: { id: "root" } }, portalRoutes);
|
|
writeClientAsset(clientLeak, "build-portal", "core-cookie-fixture.js", 'const cookie = "__Host-hivegrid_app_session";\n');
|
|
assert.throws(() => verifyWebBuildSurfaces(clientLeak), /Portal client build contains opposite-surface code/);
|
|
|
|
const publicLeak = fixture({ root: { id: "root" } }, portalRoutes);
|
|
writePublicAsset(publicLeak, "build-portal", "sw.js", "core service worker");
|
|
assert.throws(() => verifyWebBuildSurfaces(publicLeak), /Portal public artifact contains Core\/unapproved files/);
|
|
});
|
|
|
|
it("rejects a full Core translation catalog staged under the Portal locale path", () => {
|
|
const localeLeak = fixture({ root: { id: "root" } }, portalRoutes);
|
|
writePublicAsset(
|
|
localeLeak,
|
|
"build-portal",
|
|
"locales/de/translations.json",
|
|
JSON.stringify({ customerPortal: { login: "Anmelden" }, cookies: { title: "Cookies" } })
|
|
);
|
|
assert.throws(() => verifyWebBuildSurfaces(localeLeak), /must contain only the customerPortal catalog/);
|
|
});
|
|
});
|
|
|
|
function fixture(coreRoutes, customerPortalRoutes) {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "hivegrid-web-surfaces-"));
|
|
writeBuild(root, "build", coreRoutes);
|
|
writeBuild(root, "build-portal", customerPortalRoutes);
|
|
return root;
|
|
}
|
|
|
|
function writeBuild(root, directory, routes) {
|
|
const clientAssets = path.join(root, "apps", "web", directory, "client", "assets");
|
|
const server = path.join(root, "apps", "web", directory, "server");
|
|
fs.mkdirSync(clientAssets, { recursive: true });
|
|
fs.mkdirSync(server, { recursive: true });
|
|
fs.writeFileSync(path.join(server, "index.js"), "export {};\n");
|
|
fs.writeFileSync(path.join(clientAssets, "manifest-fixture.js"), `window.__reactRouterManifest=${JSON.stringify({ routes })};`);
|
|
if (directory === "build-portal") {
|
|
writePublicAsset(root, directory, "favicon-32x32.png", "favicon");
|
|
writePublicAsset(root, directory, "fonts/instrument-sans-latin.woff2", "font");
|
|
writePublicAsset(root, directory, "fonts/instrument-sans-latin-ext.woff2", "font-ext");
|
|
for (const locale of ["de", "en", "pl", "ru", "uk", "hu"]) {
|
|
writePublicAsset(root, directory, `locales/${locale}/translations.json`, JSON.stringify({ customerPortal: { login: locale } }));
|
|
}
|
|
}
|
|
}
|
|
|
|
function writeServerAsset(root, directory, fileName, source) {
|
|
const assets = path.join(root, "apps", "web", directory, "server", "assets");
|
|
fs.mkdirSync(assets, { recursive: true });
|
|
fs.writeFileSync(path.join(assets, fileName), source);
|
|
}
|
|
|
|
function writeClientAsset(root, directory, fileName, source) {
|
|
const assets = path.join(root, "apps", "web", directory, "client", "assets");
|
|
fs.writeFileSync(path.join(assets, fileName), source);
|
|
}
|
|
|
|
function writePublicAsset(root, directory, relativePath, source) {
|
|
const filePath = path.join(root, "apps", "web", directory, "client", relativePath);
|
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
fs.writeFileSync(filePath, source);
|
|
}
|