308 lines
10 KiB
TypeScript
308 lines
10 KiB
TypeScript
import type {
|
|
AuthInfo,
|
|
DashboardWidgetsUiCapability,
|
|
OrganizationFunctionActionContext,
|
|
OrganizationFunctionActionContribution,
|
|
PlatformWebModule
|
|
} from "../src/types";
|
|
import {
|
|
hasUiCapability,
|
|
moduleInstalled,
|
|
moduleIntegrationEnabled,
|
|
routeContributionsForModules,
|
|
uiCapability
|
|
} from "../src/platform/moduleLogic";
|
|
import {
|
|
isViewSurfaceVisible,
|
|
viewSurfaceCatalogueForModules,
|
|
visibleRoutesForProjection
|
|
} from "../src/platform/views";
|
|
import { groupNavigationItems } from "../src/platform/productAreas";
|
|
import { hasAnyScope, scopeGrants } from "../src/utils/permissions";
|
|
|
|
function assert(condition: unknown, message: string): void {
|
|
if (!condition) throw new Error(message);
|
|
}
|
|
|
|
const access: PlatformWebModule = { id: "access", label: "Access", version: "test" };
|
|
const filesCapability = { kind: "fileExplorer" };
|
|
const mailCapability = { kind: "mailProfiles" };
|
|
const files: PlatformWebModule = {
|
|
id: "files",
|
|
label: "Files",
|
|
version: "test",
|
|
routes: [{ path: "/files", order: 20, render: () => null }],
|
|
uiCapabilities: { "files.fileExplorer": filesCapability }
|
|
};
|
|
const mail: PlatformWebModule = {
|
|
id: "mail",
|
|
label: "Mail",
|
|
version: "test",
|
|
routes: [{ path: "/mail", order: 30, render: () => null }],
|
|
uiCapabilities: { "mail.profiles": mailCapability }
|
|
};
|
|
const campaigns: PlatformWebModule = {
|
|
id: "campaigns",
|
|
label: "Campaigns",
|
|
version: "test",
|
|
dependencies: ["access"],
|
|
optionalDependencies: ["files", "mail"],
|
|
routes: [{ path: "/campaigns", order: 10, render: () => null }]
|
|
};
|
|
|
|
const cases: Array<{ name: string; modules: PlatformWebModule[]; files: boolean; mail: boolean }> = [
|
|
{ name: "core-only", modules: [access], files: false, mail: false },
|
|
{ name: "files-only", modules: [access, files], files: true, mail: false },
|
|
{ name: "mail-only", modules: [access, mail], files: false, mail: true },
|
|
{ name: "campaign-only", modules: [access, campaigns], files: false, mail: false },
|
|
{ name: "campaign-files", modules: [access, campaigns, files], files: true, mail: false },
|
|
{ name: "campaign-mail", modules: [access, campaigns, mail], files: false, mail: true },
|
|
{ name: "full-product", modules: [access, campaigns, files, mail], files: true, mail: true }
|
|
];
|
|
|
|
for (const testCase of cases) {
|
|
const campaignInstalled = moduleInstalled("campaigns", testCase.modules);
|
|
assert(moduleInstalled("access", testCase.modules), `${testCase.name}: access should be installed`);
|
|
assert(moduleInstalled("files", testCase.modules) === testCase.files, `${testCase.name}: files installed state`);
|
|
assert(moduleInstalled("mail", testCase.modules) === testCase.mail, `${testCase.name}: mail installed state`);
|
|
assert(hasUiCapability("files.fileExplorer", testCase.modules) === testCase.files, `${testCase.name}: files UI capability`);
|
|
assert(hasUiCapability("mail.profiles", testCase.modules) === testCase.mail, `${testCase.name}: mail UI capability`);
|
|
assert(moduleIntegrationEnabled("campaigns", "files", testCase.modules) === (campaignInstalled && testCase.files), `${testCase.name}: campaign files integration`);
|
|
assert(moduleIntegrationEnabled("campaigns", "mail", testCase.modules) === (campaignInstalled && testCase.mail), `${testCase.name}: campaign mail integration`);
|
|
}
|
|
|
|
assert(uiCapability("files.fileExplorer", [access, files]) === filesCapability, "files capability should return the module-provided object");
|
|
assert(uiCapability("mail.profiles", [access, mail]) === mailCapability, "mail capability should return the module-provided object");
|
|
|
|
const configurableDashboardWidgets: DashboardWidgetsUiCapability = {
|
|
widgets: [
|
|
{
|
|
id: "files.recent",
|
|
surfaceId: "files.widget.recent",
|
|
title: "Recent files",
|
|
moduleId: "files",
|
|
supportedSizes: ["medium", "wide"],
|
|
defaultConfiguration: { limit: 10, includeFolders: false },
|
|
configurationFields: [
|
|
{
|
|
id: "limit",
|
|
label: "Items",
|
|
kind: "number",
|
|
min: 1,
|
|
max: 50
|
|
},
|
|
{
|
|
id: "includeFolders",
|
|
label: "Include folders",
|
|
kind: "boolean"
|
|
}
|
|
],
|
|
render: ({ configuration, effectiveView, instanceId }) =>
|
|
`${instanceId}:${effectiveView?.activeViewId ?? "full"}:${String(configuration.limit)}`
|
|
}
|
|
]
|
|
};
|
|
const dashboardProvider: PlatformWebModule = {
|
|
id: "dashboard-provider",
|
|
label: "Dashboard provider",
|
|
version: "test",
|
|
uiCapabilities: {
|
|
"dashboard.widgets": configurableDashboardWidgets
|
|
}
|
|
};
|
|
const resolvedDashboardWidgets = uiCapability<DashboardWidgetsUiCapability>(
|
|
"dashboard.widgets",
|
|
[dashboardProvider]
|
|
);
|
|
assert(
|
|
resolvedDashboardWidgets?.widgets[0].configurationFields?.length === 2,
|
|
"dashboard widget capabilities should retain provider-defined configuration"
|
|
);
|
|
|
|
const routes = routeContributionsForModules([access, files, mail, campaigns]).map((route) => route.path);
|
|
assert(routes.join(",") === "/campaigns,/files,/mail", "routes should aggregate and sort by contribution order");
|
|
|
|
assert(scopeGrants("tenant:*", "calendar:event:read"), "tenant wildcard should grant tenant-level module scopes");
|
|
assert(!scopeGrants("tenant:*", "system:settings:read"), "tenant wildcard should not grant system scopes");
|
|
|
|
let selectedFunctionId = "";
|
|
const functionAction: OrganizationFunctionActionContribution = {
|
|
id: "test.view-assignments",
|
|
label: "View assignments",
|
|
icon: "assignments-icon",
|
|
order: 40,
|
|
anyOf: ["idm:organization_assignment:read"],
|
|
onClick: (context) => {
|
|
selectedFunctionId = context.function.id;
|
|
}
|
|
};
|
|
functionAction.onClick({
|
|
settings: { apiBaseUrl: "", apiKey: "", accessToken: "" },
|
|
auth: {} as OrganizationFunctionActionContext["auth"],
|
|
function: {
|
|
id: "function-1",
|
|
tenant_id: "tenant-1",
|
|
organization_unit_id: "unit-1",
|
|
slug: "test-function",
|
|
name: "Test function"
|
|
}
|
|
});
|
|
assert(selectedFunctionId === "function-1", "organization function actions receive their row context");
|
|
assert(!("render" in functionAction), "organization function actions expose metadata instead of arbitrary rendered controls");
|
|
|
|
const productNavigation = [
|
|
{ to: "/dashboard", label: "Dashboard", surfaceId: "dashboard.nav.dashboard" },
|
|
{ to: "/tasks", label: "Tasks", surfaceId: "tasks.nav.tasks" },
|
|
{ to: "/files", label: "Files", surfaceId: "files.nav.files" },
|
|
{ to: "/admin", label: "Administration", surfaceId: "admin.nav.admin" }
|
|
];
|
|
const productAreas = [
|
|
{
|
|
id: "work",
|
|
moduleId: "tasks",
|
|
label: "Work",
|
|
iconName: "list-checks" as const,
|
|
surfaceIds: ["tasks.nav.tasks"],
|
|
order: 10
|
|
},
|
|
{
|
|
id: "records-documents",
|
|
moduleId: "files",
|
|
label: "Records and documents",
|
|
iconName: "folder" as const,
|
|
surfaceIds: ["files.nav.files"],
|
|
order: 20
|
|
}
|
|
];
|
|
const groupedNavigation = groupNavigationItems(productNavigation, productAreas, {
|
|
navigationMode: "grouped",
|
|
productAreaOrder: ["records-documents", "work"],
|
|
productAreaLabels: { work: "My work" }
|
|
});
|
|
assert(
|
|
groupedNavigation.map((group) => group.id).join(",") ===
|
|
"overview,product-area:records-documents,product-area:work,more-tools",
|
|
"product-area navigation should preserve dashboard, configured order, and unclassified tools"
|
|
);
|
|
assert(
|
|
groupedNavigation[2]?.label === "My work",
|
|
"View presentation should override a product-area label"
|
|
);
|
|
assert(
|
|
groupNavigationItems(productNavigation, productAreas, { navigationMode: "flat" })[0]?.items.length === 4,
|
|
"flat navigation should retain every authorized destination"
|
|
);
|
|
|
|
const viewAwareFiles: PlatformWebModule = {
|
|
...files,
|
|
navItems: [{ to: "/files", label: "Files", order: 20, anyOf: ["files:file:read"] }],
|
|
viewSurfaces: [
|
|
{
|
|
id: "files.settings.connectors",
|
|
moduleId: "files",
|
|
kind: "section",
|
|
label: "File connectors"
|
|
}
|
|
]
|
|
};
|
|
const viewCatalogue = viewSurfaceCatalogueForModules([viewAwareFiles]);
|
|
assert(
|
|
viewCatalogue.some((surface) => surface.id === "files.module"),
|
|
"view catalogue should derive a module root"
|
|
);
|
|
assert(
|
|
viewCatalogue.some((surface) => surface.id === "files.nav.files"),
|
|
"view catalogue should derive navigation surfaces"
|
|
);
|
|
assert(
|
|
viewCatalogue.some((surface) => surface.id === "files.route.files"),
|
|
"view catalogue should derive route surfaces"
|
|
);
|
|
|
|
const serverNormalizedCatalogue = viewSurfaceCatalogueForModules([
|
|
{
|
|
...viewAwareFiles,
|
|
viewSurfaces: [
|
|
{
|
|
id: "files.module",
|
|
moduleId: "files",
|
|
kind: "module",
|
|
label: "Files",
|
|
parentId: null
|
|
},
|
|
...(viewAwareFiles.viewSurfaces ?? [])
|
|
]
|
|
}
|
|
]);
|
|
assert(
|
|
serverNormalizedCatalogue.find((surface) => surface.id === "files.module")
|
|
?.parentId == null,
|
|
"server-normalized module roots must not become their own parent"
|
|
);
|
|
|
|
const filesView = {
|
|
activeViewId: "view-files",
|
|
activeRevisionId: "revision-files",
|
|
activeViewName: "Files only",
|
|
visibleSurfaceIds: [
|
|
"files.module",
|
|
"files.nav.files",
|
|
"files.route.files"
|
|
],
|
|
locked: false,
|
|
availableViews: [],
|
|
provenance: [],
|
|
diagnostics: []
|
|
};
|
|
assert(
|
|
isViewSurfaceVisible(filesView, "files.route.files", viewCatalogue),
|
|
"selected child surfaces should remain visible when their ancestor is selected"
|
|
);
|
|
assert(
|
|
!isViewSurfaceVisible(filesView, "files.settings.connectors", viewCatalogue),
|
|
"unselected sections should be hidden"
|
|
);
|
|
assert(
|
|
visibleRoutesForProjection([viewAwareFiles], filesView).length === 1,
|
|
"selected routes should remain in the effective route list"
|
|
);
|
|
const unauthorizedFilesUser = {
|
|
user: { id: "user-1", account_id: "account-1", email: "viewer@example.test" },
|
|
tenant: { id: "tenant-1", slug: "test", name: "Test" },
|
|
scopes: [],
|
|
roles: [],
|
|
groups: [],
|
|
principal: {
|
|
account_id: "account-1",
|
|
tenant_id: "tenant-1",
|
|
scopes: [],
|
|
group_ids: [],
|
|
auth_method: "session"
|
|
},
|
|
profile_loaded: true,
|
|
roles_loaded: true,
|
|
groups_loaded: true
|
|
} satisfies AuthInfo;
|
|
assert(
|
|
isViewSurfaceVisible(filesView, "files.nav.files", viewCatalogue) &&
|
|
!hasAnyScope(unauthorizedFilesUser, ["files:file:read"]),
|
|
"View visibility must not add the Access permission required by a module"
|
|
);
|
|
|
|
const missingParentView = {
|
|
...filesView,
|
|
visibleSurfaceIds: ["files.route.files"]
|
|
};
|
|
assert(
|
|
!isViewSurfaceVisible(
|
|
missingParentView,
|
|
"files.route.files",
|
|
viewCatalogue
|
|
),
|
|
"a selected child should stay hidden when its module ancestor is not selected"
|
|
);
|
|
assert(
|
|
isViewSurfaceVisible(filesView, "future.module.action", viewCatalogue),
|
|
"unknown surfaces should fail open for forward compatibility and recovery"
|
|
);
|