-
Notifications
You must be signed in to change notification settings - Fork 7
feat(authz): expand permissions view #125
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
jacobo-dominguez-wgu
merged 26 commits into
openedx:master
from
WGU-Open-edX:feature/expanded-permissions-view
Apr 21, 2026
Merged
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
92b57e8
refactor: moving files from libraries to authz module and minor impro…
jacobo-dominguez-wgu 6bd20a9
feat: roles table for audit user page
jacobo-dominguez-wgu d31bf6e
feat: expanded row view for user roles added
jesusbalderramawgu 80685fa
chore: border color updated
jesusbalderramawgu 97b916d
chore: styles and texts adjusted
jesusbalderramawgu 151174d
feat: refactor to handle the table accordion correctly after rebase
jesusbalderramawgu eacd9ee
fix: column width fixed
jesusbalderramawgu d2ce422
fix: imports fix
jesusbalderramawgu 8a5cf69
feat: functionality to keep one accordion open
jesusbalderramawgu 068ebfb
fix: lint and tests fixed
jesusbalderramawgu d867030
feat: missing tests added to get coverage
jesusbalderramawgu 6486da0
chore: unnecessary container removed
jesusbalderramawgu 0cd3d65
chore: removed space
jesusbalderramawgu a67a7b8
fix: fix in container
jesusbalderramawgu 8647ea4
feat: missing test added
jesusbalderramawgu 6bc59dc
fix: labels and styles adjusted to match with design
jesusbalderramawgu f4e1d8e
chore: refactor to avoid duplicated code
jesusbalderramawgu 4262dd0
fix: refactor of TableCells component
jesusbalderramawgu 0a6ce84
chore: refactor of files path for better organization
jesusbalderramawgu 08af990
chore: unnecessary code removed and fix and imports
jesusbalderramawgu d586e7e
fix: refetchOnWindowFocus added
jesusbalderramawgu a024561
fix: constants file merged, fix in imports and texts
jesusbalderramawgu 071f872
fix: height added to avoid a paragon bug with dropdown
jesusbalderramawgu 6dfacb9
fix: tests fixed
jesusbalderramawgu 0f4610a
fix: missing test added
jesusbalderramawgu f2839a1
chore: remove height class
jesusbalderramawgu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,6 @@ | ||
| import { render, screen, waitFor } from '@testing-library/react'; | ||
| import { | ||
| render, screen, waitFor, act, | ||
| } from '@testing-library/react'; | ||
| import { AppContext } from '@edx/frontend-platform/react'; | ||
| import userEvent from '@testing-library/user-event'; | ||
| import { MemoryRouter, Route, Routes } from 'react-router-dom'; | ||
|
|
@@ -17,6 +19,21 @@ jest.mock('@edx/frontend-platform/logging', () => ({ | |
| logError: jest.fn(), | ||
| })); | ||
|
|
||
| // Mock StudioHeader to avoid prop validation errors in tests | ||
| jest.mock('@edx/frontend-component-header', () => ({ | ||
| StudioHeader: ({ children, ...props }: any) => <div data-testid="mocked-studio-header" {...props}>{children}</div>, | ||
| })); | ||
|
|
||
| // Mock the useRevokeUserRoles hook | ||
| const mockRevokeUserRoles = jest.fn(); | ||
| jest.mock('@src/authz-module/data/hooks', () => ({ | ||
| ...jest.requireActual('@src/authz-module/data/hooks'), | ||
| useRevokeUserRoles: () => ({ | ||
| mutate: mockRevokeUserRoles, | ||
| isPending: false, | ||
| }), | ||
| })); | ||
|
|
||
| const mockUser = { | ||
| username: 'johndoe', | ||
| email: '[email protected]', | ||
|
|
@@ -50,9 +67,16 @@ const renderWithRouter = (route = '/audit/johndoe') => { | |
| authenticatedUser: { | ||
| username: 'testuser', | ||
| email: '[email protected]', | ||
| userId: 1, | ||
| }, | ||
| config: { | ||
| // @ts-ignore | ||
| 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, | ||
| }, | ||
| }; | ||
|
|
@@ -78,6 +102,11 @@ const renderWithRouter = (route = '/audit/johndoe') => { | |
| describe('AuditUserPage', () => { | ||
| beforeEach(() => { | ||
| jest.clearAllMocks(); | ||
| // Set up default mock behavior for useRevokeUserRoles | ||
| mockRevokeUserRoles.mockImplementation((variables, { onSuccess }) => { | ||
| // Simulate successful deletion by default | ||
| onSuccess({ errors: [], completed: ['role1'] }); | ||
| }); | ||
| }); | ||
|
|
||
| beforeAll(() => { | ||
|
|
@@ -181,6 +210,31 @@ describe('AuditUserPage', () => { | |
| }); | ||
| }); | ||
|
|
||
| it('expands row to show UserPermissions component when view all permissions is clicked', async () => { | ||
| (getAuthenticatedHttpClient as jest.Mock).mockReturnValue({ | ||
| get: jest | ||
| .fn() | ||
| .mockResolvedValueOnce({ data: mockUser }) | ||
| .mockResolvedValueOnce({ data: mockAssignments }), | ||
| }); | ||
|
|
||
| renderWithRouter(); | ||
| const user = userEvent.setup(); | ||
|
|
||
| await waitFor(() => { | ||
| expect(screen.getByText('Library Admin')).toBeInTheDocument(); | ||
| }); | ||
| // Find and click the "View All Permissions" link | ||
| const viewAllPermissionsLink = screen.getByText(/view all permissions/i); | ||
| expect(viewAllPermissionsLink).toBeInTheDocument(); | ||
| await user.click(viewAllPermissionsLink); | ||
| // Verify that the UserPermissions component is rendered (it should show detailed permissions) | ||
| await waitFor(() => { | ||
| // The UserPermissions component should be rendered in the expanded row | ||
| expect(viewAllPermissionsLink).toBeInTheDocument(); | ||
| }); | ||
| }); | ||
|
|
||
| it('renders the pagination controls when assignments are present', async () => { | ||
| (getAuthenticatedHttpClient as jest.Mock).mockReturnValue({ | ||
| get: jest | ||
|
|
@@ -250,7 +304,6 @@ describe('AuditUserPage', () => { | |
| .fn() | ||
| .mockResolvedValueOnce({ data: mockUser }) | ||
| .mockResolvedValueOnce({ data: mockAssignments }), | ||
| delete: jest.fn().mockResolvedValue({ data: { errors: [] } }), | ||
| }); | ||
|
|
||
| renderWithRouter(); | ||
|
|
@@ -269,26 +322,35 @@ describe('AuditUserPage', () => { | |
| }); | ||
|
|
||
| const removeButton = screen.getByRole('button', { name: /remove/i }); | ||
| await user.click(removeButton); | ||
|
|
||
| await act(async () => { | ||
| await user.click(removeButton); | ||
| }); | ||
|
|
||
| await waitFor(() => { | ||
| expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); | ||
| }); | ||
|
|
||
| await waitFor(() => { | ||
| expect(screen.getByText(/role has been successfully removed/i)).toBeInTheDocument(); | ||
| }); | ||
| }); | ||
|
|
||
| it('shows error toast when role revocation succeeds but returns errors', async () => { | ||
| // Override mock for this specific test case | ||
| mockRevokeUserRoles.mockImplementation((_, { onSuccess }) => { | ||
| // Call onSuccess immediately with errors | ||
| onSuccess({ | ||
| errors: ['Failed to revoke user role'], | ||
| completed: [], | ||
| }); | ||
| }); | ||
|
|
||
| (getAuthenticatedHttpClient as jest.Mock).mockReturnValue({ | ||
| get: jest | ||
| .fn() | ||
| .mockResolvedValueOnce({ data: mockUser }) | ||
| .mockResolvedValueOnce({ data: mockAssignments }), | ||
| delete: jest.fn().mockResolvedValue({ | ||
| data: { | ||
| errors: ['Failed to revoke user role'], | ||
| completed: [], | ||
| }, | ||
| }), | ||
| }); | ||
|
|
||
| renderWithRouter(); | ||
|
|
@@ -307,20 +369,28 @@ describe('AuditUserPage', () => { | |
| }); | ||
|
|
||
| const removeButton = screen.getByRole('button', { name: /remove/i }); | ||
| await user.click(removeButton); | ||
|
|
||
| await act(async () => { | ||
| await user.click(removeButton); | ||
| }); | ||
|
|
||
| await waitFor(() => { | ||
| expect(screen.getByText(/something went wrong/i)).toBeInTheDocument(); | ||
| }); | ||
| }); | ||
|
|
||
| it('shows error toast with retry when role revocation fails', async () => { | ||
| // Override mock for this specific test case | ||
| mockRevokeUserRoles.mockImplementation((variables, { onError }) => { | ||
| // Call onError immediately to simulate failure | ||
| onError(new Error('Network error')); | ||
| }); | ||
|
|
||
| (getAuthenticatedHttpClient as jest.Mock).mockReturnValue({ | ||
| get: jest | ||
| .fn() | ||
| .mockResolvedValueOnce({ data: mockUser }) | ||
| .mockResolvedValueOnce({ data: mockAssignments }), | ||
| delete: jest.fn().mockRejectedValue(new Error('Network error')), | ||
| }); | ||
|
|
||
| renderWithRouter(); | ||
|
|
@@ -339,7 +409,10 @@ describe('AuditUserPage', () => { | |
| }); | ||
|
|
||
| const removeButton = screen.getByRole('button', { name: /remove/i }); | ||
| await user.click(removeButton); | ||
|
|
||
| await act(async () => { | ||
| await user.click(removeButton); | ||
| }); | ||
|
|
||
| await waitFor(() => { | ||
| expect(screen.getByText(/something went wrong on our end/i)).toBeInTheDocument(); | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| import { screen } from '@testing-library/react'; | ||
| import { initializeMockApp } from '@edx/frontend-platform/testing'; | ||
| import { renderWrapper } from '@src/setupTest'; | ||
| import RenderAdminRole from './RenderAdminRole'; | ||
|
|
||
| describe('RenderAdminRole', () => { | ||
| const adminRole = 'course_admin'; | ||
| const superuserRole = 'django.superuser'; | ||
| const staffRole = 'django.globalstaff'; | ||
| const instructorRole = 'instructor'; | ||
| const emptyRole = ''; | ||
| const mixedCaseAdminRole = 'Library_Admin'; | ||
| const regularRole = 'course_staff'; | ||
|
|
||
| beforeAll(() => { | ||
| initializeMockApp({ | ||
| authenticatedUser: { | ||
| userId: 1, | ||
| username: 'testuser', | ||
| email: '[email protected]', | ||
| }, | ||
| }); | ||
| }); | ||
|
|
||
| it('renders without crashing', () => { | ||
| const { container } = renderWrapper(<RenderAdminRole role={adminRole} />); | ||
| expect(container.querySelector('.mb-0')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('displays admin message for roles containing admin', () => { | ||
| renderWrapper(<RenderAdminRole role={adminRole} />); | ||
| expect(screen.getByText(/super admins have full access/i)).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('displays staff message for superuser role', () => { | ||
| renderWrapper(<RenderAdminRole role={superuserRole} />); | ||
| expect(screen.getByText(/global staff have access/i)).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('displays staff message for globalstaff role', () => { | ||
| renderWrapper(<RenderAdminRole role={staffRole} />); | ||
| expect(screen.getByText(/global staff have access/i)).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('displays staff message for instructor role', () => { | ||
| renderWrapper(<RenderAdminRole role={instructorRole} />); | ||
| expect(screen.getByText(/global staff have access/i)).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('handles undefined role gracefully', () => { | ||
| renderWrapper(<RenderAdminRole role={undefined as any} />); | ||
| expect(screen.getByText(/global staff have access/i)).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('handles empty role string', () => { | ||
| renderWrapper(<RenderAdminRole role={emptyRole} />); | ||
| expect(screen.getByText(/global staff have access/i)).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('displays admin message for mixed case admin role', () => { | ||
| renderWrapper(<RenderAdminRole role={mixedCaseAdminRole} />); | ||
| expect(screen.getByText(/super admins have full access/i)).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('displays staff message for regular role without admin', () => { | ||
| renderWrapper(<RenderAdminRole role={regularRole} />); | ||
| expect(screen.getByText(/global staff have access/i)).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('has correct CSS classes', () => { | ||
| const { container } = renderWrapper(<RenderAdminRole role={adminRole} />); | ||
| const paragraph = container.querySelector('p'); | ||
| expect(paragraph).toHaveClass('mb-0', 'text-primary-300', 'font-weight-light'); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import { useIntl } from '@edx/frontend-platform/i18n'; | ||
| import messages from '@src/authz-module/audit-user/messages'; | ||
|
|
||
| interface RenderAdminRoleProps { | ||
| role: string; | ||
| } | ||
|
|
||
| const RenderAdminRole = ({ role }: RenderAdminRoleProps) => { | ||
| const intl = useIntl(); | ||
| // Determine which message to show based on role | ||
| const messageKey = role?.toLowerCase().includes('admin') | ||
| ? 'authz.user.table.permissions.role.admin' | ||
| : 'authz.user.table.permissions.role.staff'; | ||
|
|
||
| return ( | ||
| <p className="mb-0 text-primary-300 font-weight-light"> | ||
| {intl.formatMessage(messages[messageKey])} | ||
| </p> | ||
| ); | ||
| }; | ||
|
|
||
| export default RenderAdminRole; | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you move all the subcomponents created inside
src/authz-module/audit-userfolder intosrc/authz-module/audit-user/components/so it looks more organized?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you, I have updated this also I moved the components from the CustomCells component to TableCells component