diff --git a/site.config.ci.tsx b/site.config.ci.tsx index 577e871b..1a065205 100644 --- a/site.config.ci.tsx +++ b/site.config.ci.tsx @@ -13,7 +13,6 @@ const siteConfig: SiteConfig = { logoutUrl: 'http://localhost:8000/logout', environment: EnvironmentTypes.PRODUCTION, - basename: '/admin-console', apps: [ shellApp, headerApp, diff --git a/site.config.dev.tsx b/site.config.dev.tsx index aae20a78..e90dcf30 100644 --- a/site.config.dev.tsx +++ b/site.config.dev.tsx @@ -15,7 +15,6 @@ const siteConfig: SiteConfig = { logoutUrl: 'http://local.openedx.io:8000/logout', environment: EnvironmentTypes.DEVELOPMENT, - basename: '/admin-console', apps: [ shellApp, headerApp, diff --git a/site.config.test.tsx b/site.config.test.tsx index db7698ed..6fec5a0f 100644 --- a/site.config.test.tsx +++ b/site.config.test.tsx @@ -12,7 +12,6 @@ const siteConfig: SiteConfig = { // Use 'test' instead of EnvironmentTypes.TEST to break a circular dependency // when mocking `@openedx/frontend-base` itself. environment: 'test' as SiteConfig['environment'], - basename: '/admin-console', apps: [{ appId, config: {}, diff --git a/src/authz-module/audit-user/index.test.tsx b/src/authz-module/audit-user/index.test.tsx index e83bcb1b..4643f2fc 100644 --- a/src/authz-module/audit-user/index.test.tsx +++ b/src/authz-module/audit-user/index.test.tsx @@ -93,7 +93,7 @@ const renderWithRouter = (route = '/audit/johndoe') => { } /> - Home Page} /> + Home Page} /> diff --git a/src/authz-module/components/AddRoleButton.test.tsx b/src/authz-module/components/AddRoleButton.test.tsx index 048a1735..79a976e8 100644 --- a/src/authz-module/components/AddRoleButton.test.tsx +++ b/src/authz-module/components/AddRoleButton.test.tsx @@ -74,7 +74,7 @@ describe('AddRoleButton', () => { await user.click(button); expect(mockNavigate).toHaveBeenCalledTimes(1); - expect(mockNavigate).toHaveBeenCalledWith('/authz/assign-role'); + expect(mockNavigate).toHaveBeenCalledWith('/admin-console/authz/assign-role'); }); it('navigates to assign role page with username query parameter when presetUsername is provided', async () => { @@ -86,7 +86,7 @@ describe('AddRoleButton', () => { await user.click(button); expect(mockNavigate).toHaveBeenCalledTimes(1); - expect(mockNavigate).toHaveBeenCalledWith(`/authz/assign-role?users=${presetUsername}`); + expect(mockNavigate).toHaveBeenCalledWith(`/admin-console/authz/assign-role?users=${presetUsername}`); }); it('handles special characters in presetUsername correctly', async () => { @@ -98,7 +98,7 @@ describe('AddRoleButton', () => { await user.click(button); expect(mockNavigate).toHaveBeenCalledTimes(1); - expect(mockNavigate).toHaveBeenCalledWith(`/authz/assign-role?${new URLSearchParams({ users: presetUsername }).toString()}`); + expect(mockNavigate).toHaveBeenCalledWith(`/admin-console/authz/assign-role?${new URLSearchParams({ users: presetUsername }).toString()}`); }); }); @@ -113,7 +113,7 @@ describe('AddRoleButton', () => { expect(button).toHaveFocus(); await user.keyboard('{Enter}'); - expect(mockNavigate).toHaveBeenCalledWith('/authz/assign-role'); + expect(mockNavigate).toHaveBeenCalledWith('/admin-console/authz/assign-role'); }); it('responds to spacebar activation', async () => { @@ -124,7 +124,7 @@ describe('AddRoleButton', () => { button.focus(); await user.keyboard(' '); - expect(mockNavigate).toHaveBeenCalledWith('/authz/assign-role'); + expect(mockNavigate).toHaveBeenCalledWith('/admin-console/authz/assign-role'); }); it('handles multiple clicks gracefully', async () => { @@ -138,7 +138,7 @@ describe('AddRoleButton', () => { await user.click(button); expect(mockNavigate).toHaveBeenCalledTimes(3); - expect(mockNavigate).toHaveBeenCalledWith('/authz/assign-role?users=testuser'); + expect(mockNavigate).toHaveBeenCalledWith('/admin-console/authz/assign-role?users=testuser'); }); }); diff --git a/src/authz-module/components/TableCells.test.tsx b/src/authz-module/components/TableCells.test.tsx index bd79c35c..fbf116b9 100644 --- a/src/authz-module/components/TableCells.test.tsx +++ b/src/authz-module/components/TableCells.test.tsx @@ -181,7 +181,7 @@ describe('TableCells Components', () => { const viewButton = screen.getByRole('button', { name: /view/i }); await user.click(viewButton); - expect(mockNavigate).toHaveBeenCalledWith('/authz/user/johndoe'); + expect(mockNavigate).toHaveBeenCalledWith('/admin-console/authz/user/johndoe'); }); it('navigates with correct username for different user', async () => { @@ -201,7 +201,7 @@ describe('TableCells Components', () => { const viewButton = screen.getByRole('button', { name: /view/i }); await user.click(viewButton); - expect(mockNavigate).toHaveBeenCalledWith('/authz/user/janedoe'); + expect(mockNavigate).toHaveBeenCalledWith('/admin-console/authz/user/janedoe'); }); it('handles empty username gracefully', async () => { @@ -221,7 +221,7 @@ describe('TableCells Components', () => { const viewButton = screen.getByRole('button', { name: /view/i }); await user.click(viewButton); - expect(mockNavigate).toHaveBeenCalledWith('/authz/user/'); + expect(mockNavigate).toHaveBeenCalledWith('/admin-console/authz/user/'); }); it('handles special characters in username', async () => { @@ -241,7 +241,7 @@ describe('TableCells Components', () => { const viewButton = screen.getByRole('button', { name: /view/i }); await user.click(viewButton); - expect(mockNavigate).toHaveBeenCalledWith('/authz/user/user+with@special.chars'); + expect(mockNavigate).toHaveBeenCalledWith('/admin-console/authz/user/user%2Bwith%40special.chars'); }); it('disables the view action and shows a tooltip when course authoring is disabled for the course', async () => { diff --git a/src/authz-module/components/TableCells.tsx b/src/authz-module/components/TableCells.tsx index 36ec474d..f1df74d1 100644 --- a/src/authz-module/components/TableCells.tsx +++ b/src/authz-module/components/TableCells.tsx @@ -8,7 +8,7 @@ import { UserRoleWithPermissions, RoleToDelete } from '@src/types'; import { useNavigate } from 'react-router-dom'; import { useContext, useMemo, type ComponentProps } from 'react'; import { - ADMIN_ROLES, DJANGO_MANAGED_ROLES, MAP_ROLE_KEY_TO_LABEL, + ADMIN_ROLES, buildAuditUserPath, DJANGO_MANAGED_ROLES, MAP_ROLE_KEY_TO_LABEL, } from '@src/authz-module/constants'; import { Icon, IconButton, OverlayTrigger, Tooltip, DataTableContext, @@ -96,7 +96,7 @@ const NameCell = ({ row }: CellProps) => { const ViewActionCell = ({ row, isCourseEnabled }: CellProps & Partial) => { const { formatMessage } = useIntl(); const navigate = useNavigate(); - const viewPath = `/authz/user/${row.original.username}`; + const viewPath = buildAuditUserPath(row.original.username ?? ''); const isCourseScope = !row.original.role?.startsWith('lib') && !DJANGO_MANAGED_ROLES.includes(row.original.role); const isDisabled = isCourseEnabled !== undefined && isCourseScope && !isCourseEnabled(row.original.scope); diff --git a/src/authz-module/constants.test.ts b/src/authz-module/constants.test.ts index 60b4f9d0..6423e965 100644 --- a/src/authz-module/constants.test.ts +++ b/src/authz-module/constants.test.ts @@ -1,9 +1,9 @@ import { - buildWizardPath, getOrgAggregateScopeKey, getPlatformAggregateScopeKey, ROUTES, + AUTHZ_HOME_PATH, buildAuditUserPath, buildWizardPath, getOrgAggregateScopeKey, getPlatformAggregateScopeKey, ROUTES, } from './constants'; import type { ContextType } from './constants'; -const BASE = `${ROUTES.HOME_PATH}${ROUTES.ASSIGN_ROLE_WIZARD_PATH}`; +const BASE = `${AUTHZ_HOME_PATH}/${ROUTES.ASSIGN_ROLE_WIZARD_PATH}`; describe('buildWizardPath', () => { it('returns the base path when called with no arguments', () => { @@ -36,6 +36,17 @@ describe('buildWizardPath', () => { }); }); +describe('buildAuditUserPath', () => { + it('builds the absolute audit path for a username', () => { + expect(buildAuditUserPath('alice')).toBe(`${AUTHZ_HOME_PATH}/user/alice`); + }); + + it('URL-encodes the username', () => { + expect(buildAuditUserPath('user+with@special.chars')) + .toBe(`${AUTHZ_HOME_PATH}/user/user%2Bwith%40special.chars`); + }); +}); + describe('getOrgAggregateScopeKey', () => { it('returns course wildcard scope for course context', () => { expect(getOrgAggregateScopeKey('course', 'MIT')).toBe('course-v1:MIT+*'); diff --git a/src/authz-module/constants.ts b/src/authz-module/constants.ts index 040e97ad..9dc9fcc0 100644 --- a/src/authz-module/constants.ts +++ b/src/authz-module/constants.ts @@ -37,14 +37,19 @@ export const SKELETON_ROWS = Array.from({ length: 10 }).map(() => ({ roles: [], })); +/** Absolute base path of the module; src/routes.tsx derives its route prefix from this. */ +export const AUTHZ_HOME_PATH = '/admin-console/authz'; + +/** Segments nested under the authz splat route, relative to AUTHZ_HOME_PATH. */ export const ROUTES = { - HOME_PATH: '/authz', - AUDIT_USER_PATH: '/user/:username', - ASSIGN_ROLE_WIZARD_PATH: '/assign-role', + AUDIT_USER_PATH: 'user/:username', + ASSIGN_ROLE_WIZARD_PATH: 'assign-role', }; +export const buildAuditUserPath = (username: string) => `${AUTHZ_HOME_PATH}/user/${encodeURIComponent(username)}`; + export const buildWizardPath = (options?: { users?: string; from?: string }) => { - const base = `${ROUTES.HOME_PATH}${ROUTES.ASSIGN_ROLE_WIZARD_PATH}`; + const base = `${AUTHZ_HOME_PATH}/${ROUTES.ASSIGN_ROLE_WIZARD_PATH}`; if (!options) return base; const params = new URLSearchParams(); if (options.users) params.set('users', options.users); @@ -63,8 +68,6 @@ export enum RoleOperationErrorStatus { export const MAX_TABLE_FILTERS_APPLIED = 10; -export const AUTHZ_HOME_PATH = '/authz'; - export const MAP_ROLE_KEY_TO_LABEL: Record = { library_admin: 'Library Admin', library_author: 'Library Author', diff --git a/src/authz-module/role-assignation-wizard/AssignRoleWizardPage.test.tsx b/src/authz-module/role-assignation-wizard/AssignRoleWizardPage.test.tsx index e19bbab2..8c50badb 100644 --- a/src/authz-module/role-assignation-wizard/AssignRoleWizardPage.test.tsx +++ b/src/authz-module/role-assignation-wizard/AssignRoleWizardPage.test.tsx @@ -104,15 +104,15 @@ describe('AssignRoleWizardPage', () => { const user = userEvent.setup(); renderPage(); await user.click(screen.getByRole('button', { name: /Cancel/i })); - expect(navigate).toHaveBeenCalledWith('/authz'); + expect(navigate).toHaveBeenCalledWith('/admin-console/authz'); }); it('navigates to the from= path when Cancel is clicked and a from param is present', async () => { - const { navigate } = setupMocks({ from: '/authz/libraries/lib:123/alice' }); + const { navigate } = setupMocks({ from: '/admin-console/authz/libraries/lib:123/alice' }); const user = userEvent.setup(); renderPage(); await user.click(screen.getByRole('button', { name: /Cancel/i })); - expect(navigate).toHaveBeenCalledWith('/authz/libraries/lib:123/alice'); + expect(navigate).toHaveBeenCalledWith('/admin-console/authz/libraries/lib:123/alice'); }); it('navigates to home path when from is an external URL', async () => { @@ -120,7 +120,7 @@ describe('AssignRoleWizardPage', () => { const user = userEvent.setup(); renderPage(); await user.click(screen.getByRole('button', { name: /Cancel/i })); - expect(navigate).toHaveBeenCalledWith('/authz'); + expect(navigate).toHaveBeenCalledWith('/admin-console/authz'); }); it('navigates to the user-specific view when a single preset user is set', async () => { @@ -128,15 +128,15 @@ describe('AssignRoleWizardPage', () => { const user = userEvent.setup(); renderPage(); await user.click(screen.getByRole('button', { name: /Cancel/i })); - expect(navigate).toHaveBeenCalledWith('/authz/user/alice'); + expect(navigate).toHaveBeenCalledWith('/admin-console/authz/user/alice'); }); it('navigates to returnTo when multiple preset users are set', async () => { - const { navigate } = setupMocks({ users: 'alice,bob', from: '/authz/team' }); + const { navigate } = setupMocks({ users: 'alice,bob', from: '/admin-console/authz/team' }); const user = userEvent.setup(); renderPage(); await user.click(screen.getByRole('button', { name: /Cancel/i })); - expect(navigate).toHaveBeenCalledWith('/authz/team'); + expect(navigate).toHaveBeenCalledWith('/admin-console/authz/team'); }); describe('assignable roles', () => { diff --git a/src/authz-module/role-assignation-wizard/AssignRoleWizardPage.tsx b/src/authz-module/role-assignation-wizard/AssignRoleWizardPage.tsx index 24ab3984..f618f103 100644 --- a/src/authz-module/role-assignation-wizard/AssignRoleWizardPage.tsx +++ b/src/authz-module/role-assignation-wizard/AssignRoleWizardPage.tsx @@ -3,7 +3,7 @@ import { useIntl } from '@openedx/frontend-base'; import { useValidateUserPermissionsNonSuspense } from '@src/data/hooks'; import AssignRoleWizard from './AssignRoleWizard'; import AuthZLayout from '@src/authz-module/components/AuthZLayout'; -import { ROUTES } from '@src/authz-module/constants'; +import { AUTHZ_HOME_PATH, buildAuditUserPath } from '@src/authz-module/constants'; import messages from './messages'; import { CONTENT_COURSE_PERMISSIONS, CONTENT_LIBRARY_PERMISSIONS, courseRolesMetadata, libraryRolesMetadata, @@ -17,11 +17,11 @@ const AssignRoleWizardPage = () => { const [searchParams] = useSearchParams(); const initialUsers = searchParams.get('users') || ''; const raw = searchParams.get('from') ?? ''; - const returnTo = (raw.startsWith('/') && !raw.startsWith('//')) ? raw : ROUTES.HOME_PATH; + const returnTo = (raw.startsWith('/') && !raw.startsWith('//')) ? raw : AUTHZ_HOME_PATH; const presetUser = initialUsers.trim(); const destination = (presetUser && !presetUser.includes(',')) - ? `${ROUTES.HOME_PATH}/user/${presetUser}` + ? buildAuditUserPath(presetUser) : returnTo; const { data: managePermissions } = useValidateUserPermissionsNonSuspense(MANAGE_TEAM_PERMISSIONS); diff --git a/src/authz-module/team-members/TeamMembersTable.test.tsx b/src/authz-module/team-members/TeamMembersTable.test.tsx index 946a9c0a..c9ba6ca4 100644 --- a/src/authz-module/team-members/TeamMembersTable.test.tsx +++ b/src/authz-module/team-members/TeamMembersTable.test.tsx @@ -218,7 +218,7 @@ describe('TeamMembersTable', () => { }); const viewButtons = screen.getAllByRole('button', { name: /view/i }); await user.click(viewButtons[0]); - expect(mockNavigate).toHaveBeenCalledWith('/authz/user/johndoe'); + expect(mockNavigate).toHaveBeenCalledWith('/admin-console/authz/user/johndoe'); }); it('renders safely when role assignments data is undefined', () => { diff --git a/src/routes.test.tsx b/src/routes.test.tsx index 3346fe33..0ed942c8 100644 --- a/src/routes.test.tsx +++ b/src/routes.test.tsx @@ -17,7 +17,7 @@ describe('routes', () => { it('has the expected id and path', () => { expect(routes[0].id).toBe('org.openedx.frontend.route.adminConsole.main'); - expect(routes[0].path).toBe('/authz/*'); + expect(routes[0].path).toBe('/admin-console/authz/*'); }); it('is labelled with the admin console role', () => { diff --git a/src/routes.tsx b/src/routes.tsx index 71831b67..de5d8da8 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -1,12 +1,14 @@ import { lazy } from 'react'; import { authenticatedLoader } from '@openedx/frontend-base'; +import { AUTHZ_HOME_PATH } from './authz-module/constants'; + const Main = lazy(() => import('./Main')); const routes = [ { id: 'org.openedx.frontend.route.adminConsole.main', - path: '/authz/*', + path: `${AUTHZ_HOME_PATH}/*`, loader: authenticatedLoader, Component: Main, handle: {