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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ jobs:
run: npm ci
- name: Lint
run: npm run lint
- name: Types
run: npm run types
- name: Test
run: npm run test
- name: Build
Expand Down
42 changes: 12 additions & 30 deletions src/authz-module/audit-user/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
import { AppContext } from '@edx/frontend-platform/react';
import userEvent from '@testing-library/user-event';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { mockHttpClient, mockAppContext } from '@src/setupTest';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ToastManagerProvider } from '@src/components/ToastManager/ToastManagerContext';
Expand Down Expand Up @@ -76,24 +76,6 @@ const renderWithRouter = (route = '/audit/johndoe') => {
},
});

const mockAppContext = {
authenticatedUser: {
username: 'testuser',
email: 'testuser@example.com',
userId: 1,
},
config: {
LMS_BASE_URL: 'http://localhost:18000',
STUDIO_BASE_URL: 'http://localhost:18010',
AUTHZ_MICROFRONTEND_URL: 'http://localhost:18012',
ACCESS_TOKEN_COOKIE_NAME: 'edx-jwt-cookie-header-payload',
BASE_URL: 'http://localhost:18012',
ENVIRONMENT: 'test',
LANGUAGE_PREFERENCE_COOKIE_NAME: 'openedx-language-preference',
...process.env,
},
};

return render(
<AppContext.Provider value={mockAppContext}>
<QueryClientProvider client={queryClient}>
Expand All @@ -116,7 +98,7 @@ describe('AuditUserPage', () => {
beforeEach(() => {
jest.clearAllMocks();
// Set up default mock behavior for useRevokeUserRoles
mockRevokeUserRoles.mockImplementation((variables, { onSuccess }) => {
mockRevokeUserRoles.mockImplementation((_variables, { onSuccess }) => {
// Simulate successful deletion by default
onSuccess({ errors: [], completed: ['role1'] });
});
Expand Down Expand Up @@ -154,7 +136,7 @@ describe('AuditUserPage', () => {
});

it('navigates to home if user is not found', async () => {
(getAuthenticatedHttpClient as jest.Mock).mockReturnValue({
mockHttpClient().mockReturnValue({
get: jest
.fn()
.mockResolvedValueOnce({ data: null })
Expand All @@ -169,7 +151,7 @@ describe('AuditUserPage', () => {
});

it('allows user to interact with Assign Role button', async () => {
(getAuthenticatedHttpClient as jest.Mock).mockReturnValue({
mockHttpClient().mockReturnValue({
get: jest
.fn()
.mockResolvedValueOnce({ data: mockUser })
Expand Down Expand Up @@ -215,7 +197,7 @@ describe('AuditUserPage', () => {
});

it('renders correct table headers', async () => {
(getAuthenticatedHttpClient as jest.Mock).mockReturnValue({
mockHttpClient().mockReturnValue({
get: jest
.fn()
.mockResolvedValueOnce({ data: mockUser })
Expand Down Expand Up @@ -294,7 +276,7 @@ describe('AuditUserPage', () => {
});

it('renders the breadcrumb navigation with home link', async () => {
(getAuthenticatedHttpClient as jest.Mock).mockReturnValue({
mockHttpClient().mockReturnValue({
get: jest
.fn()
.mockResolvedValueOnce({ data: mockUser })
Expand All @@ -310,7 +292,7 @@ describe('AuditUserPage', () => {
});

it('opens and closes the ConfirmDeletionModal when delete is clicked and cancel is pressed', async () => {
(getAuthenticatedHttpClient as jest.Mock).mockReturnValue({
mockHttpClient().mockReturnValue({
get: jest
.fn()
.mockResolvedValueOnce({ data: mockUser })
Expand Down Expand Up @@ -342,7 +324,7 @@ describe('AuditUserPage', () => {
});

it('calls onSave when confirming deletion in ConfirmDeletionModal', async () => {
(getAuthenticatedHttpClient as jest.Mock).mockReturnValue({
mockHttpClient().mockReturnValue({
get: jest
.fn()
.mockResolvedValueOnce({ data: mockUser })
Expand Down Expand Up @@ -389,7 +371,7 @@ describe('AuditUserPage', () => {
});
});

(getAuthenticatedHttpClient as jest.Mock).mockReturnValue({
mockHttpClient().mockReturnValue({
get: jest
.fn()
.mockResolvedValueOnce({ data: mockUser })
Expand Down Expand Up @@ -424,12 +406,12 @@ describe('AuditUserPage', () => {

it('shows error toast with retry when role revocation fails', async () => {
// Override mock for this specific test case
mockRevokeUserRoles.mockImplementation((variables, { onError }) => {
mockRevokeUserRoles.mockImplementation((_variables, { onError }) => {
// Call onError immediately to simulate failure
onError(new Error('Network error'));
});

(getAuthenticatedHttpClient as jest.Mock).mockReturnValue({
mockHttpClient().mockReturnValue({
get: jest
.fn()
.mockResolvedValueOnce({ data: mockUser })
Expand Down Expand Up @@ -464,7 +446,7 @@ describe('AuditUserPage', () => {
});

it('shows the extra warning when rolesCount is 1', async () => {
(getAuthenticatedHttpClient as jest.Mock).mockReturnValue({
mockHttpClient().mockReturnValue({
get: jest
.fn()
.mockResolvedValueOnce({ data: mockUser })
Expand Down
9 changes: 4 additions & 5 deletions src/authz-module/audit-user/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
} from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import type { AppContextType } from '@edx/frontend-platform/react';
import {
Container, DataTable,
} from '@openedx/paragon';
Expand All @@ -23,7 +22,7 @@ import {
} from '@src/authz-module/components/TableCells';
import { useQuerySettings } from '@src/authz-module/hooks/useQuerySettings';
import { useRevokeUserRoles, useUserAssignedRoles } from '@src/authz-module/data/hooks';
import { RoleToDelete } from 'types';
import { RoleToDelete } from '@src/types';
import { useToastManager } from '@src/components/ToastManager/ToastManagerContext';
import UserPermissions from '@src/authz-module/components/UserPermissions';
import OrgFilter from '@src/authz-module/components/TableControlBar/OrgFilter';
Expand All @@ -37,7 +36,7 @@ const AuditUserPage = () => {
const { formatMessage } = useIntl();
const [columnsWithFiltersApplied, setColumnsWithFiltersApplied] = useState<string[]>([]);
const { username } = useParams();
const { authenticatedUser } = useContext(AppContext as React.Context<AppContextType>);
const { authenticatedUser } = useContext(AppContext);
const navigate = useNavigate();
const {
isLoading: isLoadingUser, data: user, isError: isErrorUser, error: errorUser,
Expand Down Expand Up @@ -112,10 +111,10 @@ const AuditUserPage = () => {
Header: formatMessage(messages['authz.user.table.action.column.header']),
Cell: createActionsCell({
onClickDeleteButton: handleShowConfirmDeletionModal,
isUserAuthenticatedPage: username === authenticatedUser.username,
isUserAuthenticatedPage: username === authenticatedUser?.username,
}),
},
], [authenticatedUser.username, formatMessage, handleShowConfirmDeletionModal, username]);
], [authenticatedUser?.username, formatMessage, handleShowConfirmDeletionModal, username]);

const columns = useMemo(() => [
{
Expand Down
1 change: 0 additions & 1 deletion src/authz-module/authz-home/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import React from 'react';
import { screen } from '@testing-library/react';
import { useAllRoleAssignments, useOrgs, useScopes } from '@src/authz-module/data/hooks';
import { renderWithAllProviders } from '@src/setupTest';
Expand Down
1 change: 0 additions & 1 deletion src/authz-module/components/AddRoleButton.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon';
import { Plus } from '@openedx/paragon/icons';
Expand Down
4 changes: 2 additions & 2 deletions src/authz-module/components/AuthZTitle.test.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { ReactNode } from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import AuthZTitle, { AuthZTitleProps } from './AuthZTitle';
import AuthZTitle from './AuthZTitle';

jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
Link: ({ children, to }:{ children:ReactNode, to:string }) => <a href={to}>{children}</a>,
}));

describe('AuthZTitle', () => {
const defaultProps: AuthZTitleProps = {
const defaultProps = {
activeLabel: 'Current Page',
pageTitle: 'Page Title',
pageSubtitle: 'Page Subtitle',
Expand Down
3 changes: 3 additions & 0 deletions src/authz-module/components/PermissionTable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const mockRoles: Role[] = [
permissions: [],
role: '',
contextType: '',
scope: '',
},
{
name: 'Editor',
Expand All @@ -19,6 +20,7 @@ const mockRoles: Role[] = [
permissions: [],
role: '',
contextType: '',
scope: '',
},
{
name: 'Viewer',
Expand All @@ -27,6 +29,7 @@ const mockRoles: Role[] = [
permissions: [],
role: '',
contextType: '',
scope: '',
},
];

Expand Down
76 changes: 34 additions & 42 deletions src/authz-module/components/RenderPermissionColumn.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,43 @@
import { Icon } from '@openedx/paragon';
import { RolePermission } from 'types';
import { PermissionItem } from '@src/types';
import ResourceTooltip from './ResourceTooltip';

interface ExtendedRolePermission extends RolePermission {
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
}

interface PermissionItem {
key: string;
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
label: string;
description: string;
perms: ExtendedRolePermission[];
}

interface RenderPermissionColumnProps {
items: PermissionItem[];
}

const RenderPermissionColumn = ({ items }: RenderPermissionColumnProps) => items.map(({
key, icon, label, description, perms,
}) => (
<div key={key} className="mb-4 col-12">
<div className="d-flex align-items-center mb-2">
<Icon src={icon} className="mr-2 text-primary" size="xs" />
<h5 className="text-primary m-0">{label}</h5>
<ResourceTooltip
resourceGroup={{
key, label, description, permissions: perms,
}}
/>
</div>
<ul className="mb-0 list-unstyled d-flex align-items-center">
{perms.map((perm, index) => (
<li
key={perm.key}
className={`d-flex align-items-center text-primary-400 ${index !== perms.length - 1 ? 'border-right pr-2' : ''
} ${index !== 0 ? 'pl-2' : ''}`}
>
<Icon src={perm.icon} className="mr-2" size="xs" />
<span className="text-primary small font-weight-light">
{perm.label}
</span>
</li>
))}
</ul>
</div>
));
const RenderPermissionColumn = ({ items }: RenderPermissionColumnProps) => (
<>
{items.map(({
key, icon, label, description, perms,
}) => (
<div key={key} className="mb-4 col-12">
<div className="d-flex align-items-center mb-2">
<Icon src={icon} className="mr-2 text-primary" size="xs" />
<h5 className="text-primary m-0">{label}</h5>
<ResourceTooltip
resourceGroup={{
key, label, description, permissions: perms,
}}
/>
</div>
<ul className="mb-0 list-unstyled d-flex align-items-center">
{perms.map((perm, index) => (
<li
key={perm.key}
className={`d-flex align-items-center text-primary-400 ${index !== perms.length - 1 ? 'border-right pr-2' : ''
} ${index !== 0 ? 'pl-2' : ''}`}
>
<Icon src={perm.icon} className="mr-2" size="xs" />
<span className="text-primary small font-weight-light">
{perm.label}
</span>
</li>
))}
</ul>
</div>
))}
</>
);

export default RenderPermissionColumn;
14 changes: 1 addition & 13 deletions src/authz-module/components/RenderPermissionInLine.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,7 @@
import { Icon } from '@openedx/paragon';
import { RolePermission } from 'types';
import { PermissionItem } from '@src/types';
import ResourceTooltip from './ResourceTooltip';

interface ExtendedRolePermission extends RolePermission {
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
}

interface PermissionItem {
key: string;
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
label: string;
description: string;
perms: ExtendedRolePermission[];
}

interface RenderPermissionInLineProps {
items: PermissionItem[];
}
Expand Down
11 changes: 8 additions & 3 deletions src/authz-module/components/ResourceTooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { Icon, OverlayTrigger, Popover } from '@openedx/paragon';
import { Info } from '@openedx/paragon/icons';
import { PermissionsResourceGrouped, RoleResourceGroup } from '@src/types';
import { PermissionMetadata } from '@src/types';

type ResourceTooltipProps = {
resourceGroup: PermissionsResourceGrouped | RoleResourceGroup;
resourceGroup: {
key: string;
label: string;
description: string;
permissions: PermissionMetadata[];
};
};

const ResourceTooltip = ({ resourceGroup }:ResourceTooltipProps) => (
Expand All @@ -17,7 +22,7 @@ const ResourceTooltip = ({ resourceGroup }:ResourceTooltipProps) => (
<p className="small">{resourceGroup.description}</p>
<ul className="small">
{resourceGroup.permissions.map(permission => (
<li key={permission.key}><b>{permission.label.trim()}:</b> {permission.description}</li>
<li key={permission.key}><b>{permission.label?.trim() ?? ''}:</b> {permission.description}</li>
))}
</ul>
</Popover.Content>
Expand Down
Loading
Loading