Skip to content

Commit 2eac66f

Browse files
test: adding unit test for audit user page components
1 parent 215d0bc commit 2eac66f

8 files changed

Lines changed: 744 additions & 8 deletions

File tree

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import { render, screen, waitFor } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import { MemoryRouter, Route, Routes } from 'react-router-dom';
4+
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
5+
import { IntlProvider } from '@edx/frontend-platform/i18n';
6+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
7+
import AuditUserPage from './index';
8+
9+
jest.mock('@edx/frontend-platform/auth', () => ({
10+
getAuthenticatedHttpClient: jest.fn(),
11+
configure: jest.fn(), // Add this line
12+
}));
13+
14+
const mockUser = {
15+
username: 'johndoe',
16+
17+
profile_image: { has_image: false },
18+
};
19+
const mockAssignments = {
20+
count: 1,
21+
results: [
22+
{
23+
id: '1',
24+
role: 'library_admin',
25+
org: 'Test Org',
26+
scope: 'lib:test',
27+
permissionCount: 5,
28+
},
29+
],
30+
next: null,
31+
previous: null,
32+
};
33+
34+
const renderWithRouter = (route = '/audit/johndoe') => {
35+
const queryClient = new QueryClient({
36+
defaultOptions: {
37+
queries: {
38+
retry: false,
39+
},
40+
},
41+
});
42+
43+
return render(
44+
<QueryClientProvider client={queryClient}>
45+
<IntlProvider locale="en">
46+
<MemoryRouter initialEntries={[route]}>
47+
<Routes>
48+
<Route path="/audit/:username" element={<AuditUserPage />} />
49+
<Route path="/authz" element={<div>Home Page</div>} />
50+
</Routes>
51+
</MemoryRouter>
52+
</IntlProvider>
53+
</QueryClientProvider>,
54+
);
55+
};
56+
57+
describe('AuditUserPage', () => {
58+
beforeEach(() => {
59+
jest.clearAllMocks();
60+
});
61+
62+
it('renders user info and table when data is loaded', async () => {
63+
(getAuthenticatedHttpClient as jest.Mock).mockReturnValue({
64+
get: jest
65+
.fn()
66+
.mockResolvedValueOnce({ data: mockUser })
67+
.mockResolvedValueOnce({ data: mockAssignments }),
68+
});
69+
70+
renderWithRouter();
71+
72+
await waitFor(() => {
73+
expect(screen.getByRole('heading', { name: 'johndoe' })).toBeInTheDocument();
74+
expect(screen.getByText('[email protected]')).toBeInTheDocument();
75+
expect(screen.getByRole('button', { name: /assign role/i })).toBeInTheDocument();
76+
expect(screen.getByText('Library Admin')).toBeInTheDocument();
77+
expect(screen.getByText('Test Org')).toBeInTheDocument();
78+
expect(screen.getByText('lib:test')).toBeInTheDocument();
79+
expect(screen.getByText('5 permissions available')).toBeInTheDocument();
80+
});
81+
});
82+
83+
it('navigates to home if user is not found', async () => {
84+
(getAuthenticatedHttpClient as jest.Mock).mockReturnValue({
85+
get: jest
86+
.fn()
87+
.mockResolvedValueOnce({ data: null })
88+
.mockResolvedValueOnce({ data: mockAssignments }),
89+
});
90+
91+
renderWithRouter();
92+
93+
await waitFor(() => {
94+
expect(screen.getByText('Home Page')).toBeInTheDocument();
95+
});
96+
});
97+
98+
it('allows user to interact with Assign Role button', async () => {
99+
(getAuthenticatedHttpClient as jest.Mock).mockReturnValue({
100+
get: jest
101+
.fn()
102+
.mockResolvedValueOnce({ data: mockUser })
103+
.mockResolvedValueOnce({ data: mockAssignments }),
104+
});
105+
106+
renderWithRouter();
107+
108+
await waitFor(() => {
109+
expect(screen.getByRole('button', { name: /assign role/i })).toBeInTheDocument();
110+
});
111+
112+
const user = userEvent.setup();
113+
const button = screen.getByRole('button', { name: /assign role/i });
114+
await user.click(button);
115+
expect(button).not.toBeInTheDocument();
116+
});
117+
118+
it('renders empty state when user has no assignments', async () => {
119+
(getAuthenticatedHttpClient as jest.Mock).mockReturnValue({
120+
get: jest
121+
.fn()
122+
.mockResolvedValueOnce({ data: mockUser })
123+
.mockResolvedValueOnce({
124+
data: {
125+
count: 0, results: [], next: null, previous: null,
126+
},
127+
}),
128+
});
129+
130+
renderWithRouter();
131+
132+
await waitFor(() => {
133+
expect(screen.getByRole('heading', { name: 'johndoe' })).toBeInTheDocument();
134+
expect(screen.queryByText('5 permissions available')).not.toBeInTheDocument();
135+
expect(screen.getByRole('table')).toBeInTheDocument();
136+
});
137+
});
138+
139+
it('renders correct table headers', async () => {
140+
(getAuthenticatedHttpClient as jest.Mock).mockReturnValue({
141+
get: jest
142+
.fn()
143+
.mockResolvedValueOnce({ data: mockUser })
144+
.mockResolvedValueOnce({ data: mockAssignments }),
145+
});
146+
147+
renderWithRouter();
148+
149+
await waitFor(() => {
150+
expect(screen.getByText('Role')).toBeInTheDocument();
151+
expect(screen.getByText('Organization')).toBeInTheDocument();
152+
expect(screen.getByText('Scope')).toBeInTheDocument();
153+
expect(screen.getByText('Permissions')).toBeInTheDocument();
154+
expect(screen.getByText('Actions')).toBeInTheDocument();
155+
});
156+
});
157+
158+
it('renders the pagination controls when assignments are present', async () => {
159+
(getAuthenticatedHttpClient as jest.Mock).mockReturnValue({
160+
get: jest
161+
.fn()
162+
.mockResolvedValueOnce({ data: mockUser })
163+
.mockResolvedValueOnce({ data: mockAssignments }),
164+
});
165+
166+
renderWithRouter();
167+
168+
await waitFor(() => {
169+
expect(screen.getByText('Showing 1 of 1.')).toBeInTheDocument();
170+
});
171+
});
172+
173+
it('renders the breadcrumb navigation with home link', async () => {
174+
(getAuthenticatedHttpClient as jest.Mock).mockReturnValue({
175+
get: jest
176+
.fn()
177+
.mockResolvedValueOnce({ data: mockUser })
178+
.mockResolvedValueOnce({ data: mockAssignments }),
179+
});
180+
181+
renderWithRouter();
182+
183+
await waitFor(() => {
184+
expect(screen.getByRole('link', { name: /roles and permissions management/i })).toBeInTheDocument();
185+
expect(screen.getByText(mockUser.username, { selector: 'li[aria-current="page"]' })).toBeInTheDocument();
186+
});
187+
});
188+
});

src/authz-module/audit-user/index.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
Container, DataTable,
66
} from '@openedx/paragon';
77
import TableFooter from '@src/authz-module/components/TableFooter/TableFooter';
8-
import { TABLE_DEFAULT_PAGE_SIZE } from '@src/authz-module/constants';
8+
import { AUTHZ_HOME_PATH, TABLE_DEFAULT_PAGE_SIZE } from '@src/authz-module/constants';
99
import AuthZLayout from '@src/authz-module/components/AuthZLayout';
1010
import { useNavigate, useParams } from 'react-router-dom';
1111
import { useUserAccount } from '@src/data/hooks';
@@ -24,16 +24,15 @@ const AuditUserPage = () => {
2424
const navigate = useNavigate();
2525
const { isLoading: isLoadingUser, data: user } = useUserAccount(username ?? '');
2626
const { querySettings, handleTableFetch } = useQuerySettings();
27-
// TODO: use actual assigned roles data when API is ready, currently using dummy data for development purpose
2827
const { data: { results: userAssignments } = { results: [] } } = useUserAssignedRoles(username ?? '', querySettings);
29-
const authzHomePath = '/authz';
28+
3029
if (!user && !isLoadingUser) {
31-
navigate(authzHomePath);
30+
navigate(AUTHZ_HOME_PATH);
3231
}
3332
const navLinks = [
3433
{
3534
label: formatMessage(baseMessages['authz.management.home.nav.link']),
36-
to: authzHomePath,
35+
to: AUTHZ_HOME_PATH,
3736
},
3837
];
3938
const additionalColumns = [

src/authz-module/components/TableCells.test.tsx

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,15 @@ import {
88
RoleCell,
99
OrgCell,
1010
ScopeCell,
11+
PermissionsCell,
12+
ActionsCell,
13+
ViewAllPermissionsCell,
1114
} from './TableCells';
1215

16+
// TODO: remove console.log mocks and implement actual logic for these cells, then update tests accordingly
17+
// Mock console.log for TODO functions
18+
jest.spyOn(console, 'log').mockImplementation(() => {});
19+
1320
const mockNavigate = jest.fn();
1421

1522
jest.mock('react-router-dom', () => ({
@@ -417,4 +424,149 @@ describe('TableCells Components', () => {
417424
expect(screen.queryByText('Global')).not.toBeInTheDocument();
418425
});
419426
});
427+
428+
describe('PermissionsCell', () => {
429+
it('displays "Total Access" for Django superuser role', () => {
430+
const props = {
431+
row: {
432+
original: {
433+
role: 'django.superuser',
434+
org: 'Test Org',
435+
scope: 'Test Scope',
436+
permissionCount: 10,
437+
},
438+
},
439+
column: { id: 'permissions' },
440+
};
441+
442+
renderWrapper(<PermissionsCell {...props} />);
443+
444+
expect(screen.getByText('Total Access')).toBeInTheDocument();
445+
});
446+
447+
it('displays "Partial Access" for Django global staff role', () => {
448+
const props = {
449+
row: {
450+
original: {
451+
role: 'django.globalstaff',
452+
permissionCount: 5,
453+
org: 'Test Org',
454+
scope: 'Test Scope',
455+
},
456+
},
457+
column: { id: 'permissions' },
458+
};
459+
460+
renderWrapper(<PermissionsCell {...props} />);
461+
462+
expect(screen.getByText('Partial Access')).toBeInTheDocument();
463+
});
464+
465+
it('displays permission count for non-Django roles', () => {
466+
const props = {
467+
row: {
468+
original: {
469+
role: 'library_admin',
470+
permissionCount: 3,
471+
org: 'Test Org',
472+
scope: 'Test Scope',
473+
},
474+
},
475+
column: { id: 'permissions' },
476+
};
477+
478+
renderWrapper(<PermissionsCell {...props} />);
479+
480+
expect(screen.getByText('3 permissions available')).toBeInTheDocument();
481+
});
482+
});
483+
484+
describe('ActionsCell', () => {
485+
const mockRow = {
486+
original: {
487+
role: 'library_admin', id: '123', org: 'Test Org', scope: 'Test Scope', permissionCount: 1,
488+
},
489+
};
490+
491+
it('renders a delete button', () => {
492+
const props = {
493+
row: mockRow,
494+
column: { id: 'actions' },
495+
};
496+
497+
renderWrapper(<ActionsCell {...props} />);
498+
499+
const deleteButton = screen.getByRole('button', { name: /delete role action/i });
500+
expect(deleteButton).toBeInTheDocument();
501+
});
502+
503+
it('calls handleDelete when delete button is clicked', async () => {
504+
const user = userEvent.setup();
505+
const props = {
506+
row: mockRow,
507+
column: { id: 'actions' },
508+
};
509+
510+
renderWrapper(<ActionsCell {...props} />);
511+
512+
const deleteButton = screen.getByRole('button', { name: /delete role action/i });
513+
await user.click(deleteButton);
514+
// TODO: replace console.log with actual delete logic and update this test accordingly
515+
// eslint-disable-next-line no-console
516+
expect(console.log).toHaveBeenCalledWith('Delete clicked for row:', mockRow);
517+
});
518+
519+
it('handles keyboard interaction for delete button', async () => {
520+
const user = userEvent.setup();
521+
const props = {
522+
row: mockRow,
523+
column: { id: 'actions' },
524+
};
525+
526+
renderWrapper(<ActionsCell {...props} />);
527+
528+
const deleteButton = screen.getByRole('button', { name: /delete role action/i });
529+
deleteButton.focus();
530+
await user.keyboard('{Enter}');
531+
// TODO: replace console.log with actual delete logic and update this test accordingly
532+
// eslint-disable-next-line no-console
533+
expect(console.log).toHaveBeenCalledWith('Delete clicked for row:', mockRow);
534+
});
535+
});
536+
537+
describe('ViewAllPermissionsCell', () => {
538+
const mockRow = {
539+
original: {
540+
role: 'library_admin', id: '123', org: 'Test Org', scope: 'Test Scope', permissionCount: 1,
541+
},
542+
};
543+
544+
it('renders a view more link', () => {
545+
const props = {
546+
row: mockRow,
547+
column: { id: 'viewMore' },
548+
};
549+
550+
renderWrapper(<ViewAllPermissionsCell {...props} />);
551+
552+
expect(screen.getByText('View all permissions')).toBeInTheDocument();
553+
});
554+
555+
it('calls onClick handler when view more link is clicked', async () => {
556+
const user = userEvent.setup();
557+
const props = {
558+
row: mockRow,
559+
column: { id: 'viewMore' },
560+
};
561+
562+
renderWrapper(<ViewAllPermissionsCell {...props} />);
563+
564+
const viewMoreButton = screen.getByText('View all permissions');
565+
await user.click(viewMoreButton);
566+
567+
// TODO: replace console.log with actual view more logic and update this test accordingly
568+
// eslint-disable-next-line no-console
569+
expect(console.log).toHaveBeenCalledWith('View more clicked for row:', mockRow);
570+
});
571+
});
420572
});

0 commit comments

Comments
 (0)