import type { AuthInfo, AuthActionUiCapability, DashboardWidgetsUiCapability, OrganizationFunctionActionContext, OrganizationFunctionActionContribution, PlatformNavItem, 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 { inheritedNavigationLayout, materializeNavigationLayout, moveNavigationEntry, navigationEditorOrder } from "../src/components/navigationPreferenceLayout"; import { composeProductSurfaces, projectProductNavigation } from "../src/platform/productSurfaces"; 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 authActionCapability: AuthActionUiCapability = { actions: ["change_password"], RequiredAction: () => null, LoginHelp: () => null }; const publicAuthModule: PlatformWebModule = { id: "access", label: "Access", version: "test", publicRoutes: [{ path: "/password-recovery", render: () => null }], uiCapabilities: { "auth.actions": authActionCapability } }; assert(uiCapability("auth.actions", [publicAuthModule]) === authActionCapability, "required authentication actions can resolve from the public catalogue without normal scopes"); assert(uiCapability("auth.actions", []) === null, "core-only compositions have no authentication action implementation"); 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( "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 railItems = [ { to: "/files", label: "Files", surfaceId: "files", navigationId: "files", navigationCustomLayout: true, navigationLayoutSource: "tenant", navigationSection: { id: "separator:a", label: "A" } }, { to: "/mail", label: "Mail", surfaceId: "mail", navigationId: "messages", navigationLocked: true, navigationCustomLayout: true, navigationLayoutSource: "tenant", navigationSection: { id: "separator:b", label: "B" } } ]; assert(groupNavigationItems(railItems, []).map((group) => group.label).join() === "A,B", "explicit separators survive module-default grouping and filtering"); const viewNavigation = { contract_version: "1" as const, order: ["messages", "separator:view", "files"], hidden: ["mail"], separators: [{ id: "separator:view", label: "View" }] }; const viewGroups = groupNavigationItems(railItems, [], { navigation: viewNavigation }); assert(viewGroups[0]?.items[0]?.to === "/mail", "View layout supports navigation aliases and cannot hide locked module"); assert(viewGroups[1]?.label === "View" && viewGroups.flatMap((group) => group.items).length === 2, "View separators retain every authorized item once"); assert(groupNavigationItems(railItems.map((item) => ({ ...item, navigationLayoutSource: "user" })), [], { navigation: viewNavigation })[0]?.items[0]?.to === "/files", "personal layout takes precedence over View presentation order"); assert(groupNavigationItems(railItems.map((item) => ({ ...item, navigationVisibilitySource: "user" })), [], { navigation: { ...viewNavigation, hidden: ["files"] } }).flatMap((group) => group.items).some((item) => item.to === "/files"), "legacy personal visibility-only preferences also take precedence over View navigation hiding"); const inheritedRail = inheritedNavigationLayout(railItems, "system", []); assert(inheritedRail.order.join() === "files,messages", "system baseline ignores current tenant grouping"); const materialized = materializeNavigationLayout({ contract_version: "1", order: ["mail", "files"], hidden: [] }, { contract_version: "1", order: ["separator:work", "files", "mail"], hidden: [], separators: [{ id: "separator:work", label: "Work" }] }); assert(materialized.order.join() === "separator:work,mail,files", "legacy drafts gain inherited separator without losing their item order"); assert(moveNavigationEntry(["a", "separator:b", "c"], "c", "a").join() === "c,a,separator:b", "drag reorder treats separators as ordinary ordered entries"); assert(navigationEditorOrder(railItems, { contract_version: "1", order: ["files", "messages"], hidden: ["files"], separators: [] }).join() === "messages", "removed modules leave editor list while remaining available for re-add"); const productSurfaceExplanation = { reason: "authorization" as const, title: "Messages are unavailable", description: "No message source is available for the current responsibility.", resolution: "Ask the responsible administrator to review the assignment." }; const messageSurfaceModules: PlatformWebModule[] = [ { id: "mail", label: "Mail", version: "test", productSurfaces: [{ contractVersion: "1", id: "communication.messages", moduleId: "mail", label: "Messages", description: "Read messages without merging channel custody.", iconName: "mail", entryPath: "/messages", routePath: "/mail", surfaceIds: ["mail.route.mail"], presentations: ["task", "reader"], capabilityIds: [], searchSourceIds: ["mail.mailbox_messages"], helpContextIds: ["mail.quick_access.messages"], documentationTopicIds: ["mail.quick-access-and-product-area"], allOf: [], anyOf: ["mail:mailbox:read"], aliases: ["/inbox"], order: 10, unavailable: productSurfaceExplanation }] }, { id: "postbox", label: "Postbox", version: "test", productSurfaceMetadata: [{ contract_version: "1", id: "communication.messages", module_id: "postbox", label: "Messages", description: "Read messages without merging channel custody.", icon: "mail", entry_path: "/messages", route_path: "/postbox", surface_ids: ["postbox.route.postbox"], presentations: ["task", "reader"], capability_ids: [], search_source_ids: ["postbox.messages"], help_context_ids: ["postbox.quick_access.messages"], documentation_topic_ids: ["postbox.quick-access-and-product-area"], required_all: [], required_any: ["postbox:message:read"], aliases: ["/inbox"], order: 20, unavailable: productSurfaceExplanation }] } ]; const composedMessages = composeProductSurfaces(messageSurfaceModules); assert(composedMessages.length === 1, "related owner routes should compose into one product identity"); assert(composedMessages[0]?.entryPath === "/messages", "the composed identity should keep its stable entry path"); assert(composedMessages[0]?.contributors.map((item) => item.moduleId).join(",") === "mail,postbox", "composition should retain ordered technical provenance"); assert(composedMessages[0]?.aliases.join(",") === "/inbox", "migration aliases should be de-duplicated across owners"); const messageNavigation = projectProductNavigation( [ { to: "/dashboard", label: "Dashboard", order: 1 }, { to: "/mail", label: "Mail", order: 50, anyOf: ["mail:mailbox:read"] }, { to: "/postbox", label: "Postbox", order: 51, anyOf: ["postbox:message:read"] }, { to: "/admin", label: "Administration", order: 90, anyOf: ["core:admin:read"] } ], messageSurfaceModules, { scopes: ["mail:mailbox:read", "postbox:message:read"] } as AuthInfo ); assert( messageNavigation.primaryItems.map((item) => item.to).join(",") === "/dashboard,/messages", "ordinary navigation should replace authorized owner routes with one stable product entry" ); assert( messageNavigation.primaryItems.find((item) => item.to === "/messages")?.activePaths?.includes("/postbox"), "the product entry should remain active on an owner route" ); assert( messageNavigation.allToolItems.map((item) => item.to).join(",") === "/dashboard,/mail,/postbox", "the explicit tool catalogue should retain authorized technical routes" ); const mailOnlyNavigation = projectProductNavigation( [ { to: "/mail", label: "Mail", order: 50, anyOf: ["mail:mailbox:read"] }, { to: "/postbox", label: "Postbox", order: 51, anyOf: ["postbox:message:read"] } ], messageSurfaceModules, { scopes: ["mail:mailbox:read"] } as AuthInfo ); const reorderedOwners: PlatformNavItem[] = [ { to: "/postbox", label: "Postbox", order: 0, navigationId: "postbox.navigation.postbox", surfaceId: "postbox.navigation.postbox", anyOf: ["postbox:message:read"], navigationLocked: true, navigationLockSource: "system", navigationCustomLayout: true, navigationSection: { id: "separator:locked", label: "Locked group" } }, { to: "/files", label: "Files", order: 1 }, { to: "/mail", label: "Mail", order: 2, navigationId: "mail.navigation.mail", surfaceId: "mail.navigation.mail", anyOf: ["mail:mailbox:read"] } ]; const reorderedProductNavigation = projectProductNavigation(reorderedOwners, messageSurfaceModules, { scopes: ["mail:mailbox:read", "postbox:message:read"] } as AuthInfo); const reorderedMessages = reorderedProductNavigation.primaryItems[0]; assert(reorderedProductNavigation.primaryItems.map((item) => item.to).join() === "/messages,/files", "composed placement follows earliest effective owner order rather than fixed contributor priority"); assert(reorderedMessages.navigationSection?.id === "separator:locked", "composed entry keeps the earliest owner separator"); assert(reorderedMessages.navigationAliases?.includes("mail.navigation.mail") && reorderedMessages.navigationAliases?.includes("postbox.navigation.postbox"), "composition retains every consumed owner navigation ID for layout aliases"); assert(reorderedMessages.navigationLocked && reorderedMessages.navigationLockSource === "system", "a secondary owner's system lock survives composition with an unlocked primary owner"); assert(composeProductSurfaces(messageSurfaceModules)[0].contributors[0].moduleId === "mail", "custom rail placement never changes operational contributor priority"); const lockedAfterView = groupNavigationItems(reorderedProductNavigation.primaryItems, [], { navigation: { contract_version: "1", order: [], hidden: ["communication.messages", "mail.navigation.mail", "postbox.navigation.postbox"], separators: [] } }); assert(lockedAfterView.flatMap((group) => group.items).some((item) => item.to === "/messages"), "View aliases cannot hide a composed entry locked by any consumed authorized owner"); const aliasOrdered = groupNavigationItems(reorderedProductNavigation.primaryItems, [], { navigation: { contract_version: "1", order: ["/files", "separator:view", "mail.navigation.mail"], hidden: [], separators: [{ id: "separator:view", label: "View group" }] } }); assert(aliasOrdered[0]?.items[0]?.to === "/files" && aliasOrdered[1]?.items[0]?.to === "/messages" && aliasOrdered[1]?.label === "View group", "View ordering recognizes non-placement owner aliases and assigns the requested separator exactly once"); const unauthorizedLockedOwner = projectProductNavigation(reorderedOwners, messageSurfaceModules, { scopes: ["mail:mailbox:read"] } as AuthInfo).primaryItems.find((item) => item.to === "/messages"); assert(unauthorizedLockedOwner?.navigationLocked === false && !unauthorizedLockedOwner.navigationAliases?.includes("postbox.navigation.postbox"), "unavailable optional owners contribute neither aliases nor lock metadata"); assert( mailOnlyNavigation.primaryItems.map((item) => item.to).join(",") === "/messages", "composition should remain stable when only one optional contributor is authorized" ); assert( mailOnlyNavigation.allToolItems.map((item) => item.to).join(",") === "/mail", "the tool catalogue must not disclose unauthorized owner routes" ); const focusedMessageNavigation = projectProductNavigation( [{ to: "/mail", label: "Mail", order: 50, anyOf: ["mail:mailbox:read"] }], messageSurfaceModules, { scopes: ["mail:mailbox:read", "postbox:message:read"] } as AuthInfo, null, [ { to: "/mail", label: "Mail", order: 50, anyOf: ["mail:mailbox:read"] }, { to: "/postbox", label: "Postbox", order: 51, anyOf: ["postbox:message:read"] } ] ); assert( focusedMessageNavigation.primaryItems.map((item) => item.to).join(",") === "/messages", "a focused View should keep the selected stable product destination" ); assert( focusedMessageNavigation.allToolItems.map((item) => item.to).join(",") === "/mail,/postbox", "All available tools should provide an explicit permission-derived escape from View focus" ); 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" );