Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion site.config.ci.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ const siteConfig: SiteConfig = {
logoutUrl: 'http://localhost:8000/logout',

environment: EnvironmentTypes.PRODUCTION,
basename: '/admin-console',
apps: [
shellApp,
headerApp,
Expand Down
1 change: 0 additions & 1 deletion site.config.dev.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ const siteConfig: SiteConfig = {
logoutUrl: 'http://local.openedx.io:8000/logout',

environment: EnvironmentTypes.DEVELOPMENT,
basename: '/admin-console',
apps: [
shellApp,
headerApp,
Expand Down
1 change: 0 additions & 1 deletion site.config.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {},
Expand Down
2 changes: 1 addition & 1 deletion src/authz-module/audit-user/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ const renderWithRouter = (route = '/audit/johndoe') => {
<MemoryRouter initialEntries={[route]}>
<Routes>
<Route path="/audit/:username" element={<AuditUserPage />} />
<Route path="/authz" element={<div>Home Page</div>} />
<Route path="/admin-console/authz" element={<div>Home Page</div>} />
</Routes>
</MemoryRouter>
</ToastManagerProvider>
Expand Down
12 changes: 6 additions & 6 deletions src/authz-module/components/AddRoleButton.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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 () => {
Expand All @@ -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()}`);
});
});

Expand All @@ -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 () => {
Expand All @@ -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 () => {
Expand All @@ -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');
});
});

Expand Down
8 changes: 4 additions & 4 deletions src/authz-module/components/TableCells.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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 () => {
Expand All @@ -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 () => {
Expand All @@ -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 () => {
Expand Down
4 changes: 2 additions & 2 deletions src/authz-module/components/TableCells.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -96,7 +96,7 @@ const NameCell = ({ row }: CellProps) => {
const ViewActionCell = ({ row, isCourseEnabled }: CellProps & Partial<ViewActionCellExtraProps>) => {
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);

Expand Down
15 changes: 13 additions & 2 deletions src/authz-module/constants.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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+*');
Expand Down
15 changes: 9 additions & 6 deletions src/authz-module/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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<string, string> = {
library_admin: 'Library Admin',
library_author: 'Library Author',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,39 +104,39 @@ 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 () => {
const { navigate } = setupMocks({ from: 'https://evil.com' });
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 () => {
const { navigate } = setupMocks({ users: 'alice' });
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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/authz-module/team-members/TeamMembersTable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
2 changes: 1 addition & 1 deletion src/routes.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
4 changes: 3 additions & 1 deletion src/routes.tsx
Original file line number Diff line number Diff line change
@@ -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: {
Expand Down
Loading