59 lines
2.7 KiB
TypeScript
59 lines
2.7 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
import { describe, expect, test } from "vitest";
|
|
import { featureKeys, type FeatureKey } from "@hivegrid/entitlements";
|
|
|
|
// Registry gate for a recurring footgun: AppSidebar gates entries via
|
|
// hasFeatureAccess("<key>"), but productAccess is only resolved for the keys in
|
|
// appData.ts tenantNavigationFeatureKeys. A key used in the sidebar but missing
|
|
// from that list silently hides the entry for ALL tenants/plans (happened to
|
|
// files+tasks → fixed in 1017cf58b, then again to dashboard/automation/leads/
|
|
// projects/hr/integrations/email-inbox/booking/forms → fixed 2026-07-07 after
|
|
// the R-P5 verification). This static contract makes the drift a test failure.
|
|
|
|
const sidebarSource = readFileSync(resolve(__dirname, "AppSidebar.tsx"), "utf8");
|
|
const appDataSource = readFileSync(resolve(__dirname, "../../utils/data/.server/appData.ts"), "utf8");
|
|
|
|
function usedSidebarFeatureKeys(): string[] {
|
|
const keys = new Set<string>();
|
|
for (const match of sidebarSource.matchAll(/hasFeatureAccess\("([^"]+)"\)/g)) {
|
|
keys.add(match[1]);
|
|
}
|
|
return [...keys].sort();
|
|
}
|
|
|
|
function resolvedNavigationFeatureKeys(): string[] {
|
|
const listMatch = appDataSource.match(/const tenantNavigationFeatureKeys = \[([\s\S]*?)\]/);
|
|
if (!listMatch) {
|
|
throw new Error("tenantNavigationFeatureKeys list not found in appData.ts");
|
|
}
|
|
const keys = new Set<string>();
|
|
for (const match of listMatch[1].matchAll(/featureKeys\.(\w+)/g)) {
|
|
const value = featureKeys[match[1] as keyof typeof featureKeys];
|
|
if (!value) {
|
|
throw new Error(`Unknown featureKeys entry in tenantNavigationFeatureKeys: ${match[1]}`);
|
|
}
|
|
keys.add(value);
|
|
}
|
|
return [...keys].sort();
|
|
}
|
|
|
|
describe("sidebar feature-access registry contract", () => {
|
|
test("every hasFeatureAccess key used by AppSidebar is resolved in appData", () => {
|
|
const used = usedSidebarFeatureKeys();
|
|
const resolved = resolvedNavigationFeatureKeys();
|
|
expect(used.length).toBeGreaterThan(0);
|
|
const missing = used.filter((key) => !resolved.includes(key));
|
|
expect(
|
|
missing,
|
|
`AppSidebar gates on feature keys that appData.ts tenantNavigationFeatureKeys does not resolve — those sidebar entries are invisible for every tenant. Add them to the list: ${missing.join(", ")}`
|
|
).toEqual([]);
|
|
});
|
|
|
|
test("all sidebar gate keys are valid entitlement feature keys", () => {
|
|
const validValues = new Set<string>(Object.values(featureKeys));
|
|
const invalid = usedSidebarFeatureKeys().filter((key) => !validValues.has(key as FeatureKey));
|
|
expect(invalid, `AppSidebar uses gate keys that are no entitlement feature keys: ${invalid.join(", ")}`).toEqual([]);
|
|
});
|
|
});
|