Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
30c711e
refactor: moving files from libraries to authz module and minor impro…
jacobo-dominguez-wgu Mar 19, 2026
99e02f9
feat: roles table for audit user page
jacobo-dominguez-wgu Mar 21, 2026
b2ad0d5
feat: integrating api for permissions assignments on audit user page …
jacobo-dominguez-wgu Apr 14, 2026
2e0051b
test: adding unit test for audit user page components
jacobo-dominguez-wgu Apr 14, 2026
dc3dea5
fix: addressing pr comments
jacobo-dominguez-wgu Apr 15, 2026
c890104
feat: roles table for audit user page
jacobo-dominguez-wgu Mar 21, 2026
42dd537
feat: roles table for audit user page
jacobo-dominguez-wgu Mar 21, 2026
43cf50f
feat: adding filtering and sorting to audit user table
jacobo-dominguez-wgu Mar 26, 2026
6099cb7
fix: orgs and roles filter fixed to send correct values to API
jesusbalderramawgu Apr 15, 2026
d323999
chore: spaces and imports changed
jesusbalderramawgu Apr 15, 2026
c548b36
fix: tests fixed and missing props added
jesusbalderramawgu Apr 15, 2026
c93447c
feat: missing tests added to get the correct coverage
jesusbalderramawgu Apr 16, 2026
643133a
feat: tests added in api to get correct coverage
jesusbalderramawgu Apr 16, 2026
c83b8e1
feat: tests to get more coverage
jesusbalderramawgu Apr 16, 2026
99c4979
fix: tableControlBar tests fixed
jesusbalderramawgu Apr 16, 2026
78a60d5
feat: missing tests for ProtectedRoute and TableControlBar
jesusbalderramawgu Apr 16, 2026
79b3b44
chore: unnecessary test removed
jesusbalderramawgu Apr 16, 2026
8c3b463
chore: unnecessary file removed
jesusbalderramawgu Apr 16, 2026
01869f0
fix: missing code in filters
jesusbalderramawgu Apr 20, 2026
db7a09f
fix: lint fixes
jesusbalderramawgu Apr 20, 2026
ccfc5dc
feat: tests added to utils.tsx
jesusbalderramawgu Apr 20, 2026
0df0065
fix: unnecessary code removed after rebase
jesusbalderramawgu Apr 20, 2026
8c9be4e
fix: Role filter name fixed
jesusbalderramawgu Apr 20, 2026
c2dd278
fix: Role filter name fixed
jesusbalderramawgu Apr 20, 2026
2126d9a
fix: addressing pr comments
jacobo-dominguez-wgu Apr 20, 2026
82211dc
fix: name filter fix sending the correct value
jesusbalderramawgu Apr 21, 2026
bb9cd1b
fix: tests and code fixed in audit userafter rebase
jesusbalderramawgu Apr 21, 2026
eabb149
fix: css workaround to open filter dropdown hidden
jacobo-dominguez-wgu Apr 21, 2026
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
115 changes: 77 additions & 38 deletions src/authz-module/audit-user/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ToastManagerProvider } from '@src/components/ToastManager/ToastManagerContext';
import { useUserAccount } from '@src/data/hooks';
import { useUserAssignedRoles } from '@src/authz-module/data/hooks';
import AuditUserPage from './index';

jest.mock('@edx/frontend-platform/auth', () => ({
Expand All @@ -24,6 +26,12 @@ jest.mock('@edx/frontend-component-header', () => ({
StudioHeader: ({ children, ...props }: any) => <div data-testid="mocked-studio-header" {...props}>{children}</div>,
}));

// Mock data hooks
jest.mock('@src/data/hooks', () => ({
...jest.requireActual('@src/data/hooks'),
useUserAccount: jest.fn(),
}));

// Mock the useRevokeUserRoles hook
const mockRevokeUserRoles = jest.fn();
jest.mock('@src/authz-module/data/hooks', () => ({
Expand All @@ -32,6 +40,7 @@ jest.mock('@src/authz-module/data/hooks', () => ({
mutate: mockRevokeUserRoles,
isPending: false,
}),
useUserAssignedRoles: jest.fn(),
}));

const mockUser = {
Expand Down Expand Up @@ -115,24 +124,29 @@ describe('AuditUserPage', () => {
});

it('renders user info and table when data is loaded', async () => {
(getAuthenticatedHttpClient as jest.Mock).mockReturnValue({
get: jest
.fn()
.mockResolvedValueOnce({ data: mockUser })
.mockResolvedValueOnce({ data: mockAssignments }),
(useUserAccount as jest.Mock).mockReturnValue({
data: mockUser,
isLoading: false,
isError: false,
error: null,
});

(useUserAssignedRoles as jest.Mock).mockReturnValue({
data: mockAssignments,
isLoading: false,
isError: false,
error: null,
});

renderWithRouter();

await waitFor(() => {
expect(screen.getByRole('heading', { name: 'johndoe' })).toBeInTheDocument();
expect(screen.getByText('[email protected]')).toBeInTheDocument();
expect(screen.getByText('johndoe', { selector: 'li[aria-current="page"]' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /assign role/i })).toBeInTheDocument();
expect(screen.getByText('Library Admin')).toBeInTheDocument();
expect(screen.getByText('Test Org')).toBeInTheDocument();
expect(screen.getByText('lib:test')).toBeInTheDocument();
expect(screen.getByText('5 permissions available')).toBeInTheDocument();
});

// Check that the table is rendered (even if empty initially)
expect(screen.getByRole('table')).toBeInTheDocument();
});

it('navigates to home if user is not found', async () => {
Expand All @@ -146,7 +160,7 @@ describe('AuditUserPage', () => {
renderWithRouter();

await waitFor(() => {
expect(screen.getByText('Home Page')).toBeInTheDocument();
expect(screen.getByText('Roles and Permissions Management')).toBeInTheDocument();
});
});

Expand All @@ -171,21 +185,26 @@ describe('AuditUserPage', () => {
});

it('renders empty state when user has no assignments', async () => {
(getAuthenticatedHttpClient as jest.Mock).mockReturnValue({
get: jest
.fn()
.mockResolvedValueOnce({ data: mockUser })
.mockResolvedValueOnce({
data: {
count: 0, results: [], next: null, previous: null,
},
}),
(useUserAccount as jest.Mock).mockReturnValue({
data: mockUser,
isLoading: false,
isError: false,
error: null,
});

(useUserAssignedRoles as jest.Mock).mockReturnValue({
data: {
count: 0, results: [], next: null, previous: null,
},
isLoading: false,
isError: false,
error: null,
});

renderWithRouter();

await waitFor(() => {
expect(screen.getByRole('heading', { name: 'johndoe' })).toBeInTheDocument();
expect(screen.getByText('johndoe', { selector: 'li[aria-current="page"]' })).toBeInTheDocument();
expect(screen.queryByText('5 permissions available')).not.toBeInTheDocument();
expect(screen.getByRole('table')).toBeInTheDocument();
});
Expand All @@ -202,20 +221,26 @@ describe('AuditUserPage', () => {
renderWithRouter();

await waitFor(() => {
expect(screen.getByText('Role')).toBeInTheDocument();
expect(screen.getByText('Organization')).toBeInTheDocument();
expect(screen.getByText('Scope')).toBeInTheDocument();
expect(screen.getByText('Permissions')).toBeInTheDocument();
expect(screen.getByText('Actions')).toBeInTheDocument();
// Using columnheader role to be more specific about table headers
expect(screen.getByRole('columnheader', { name: /role/i })).toBeInTheDocument();
expect(screen.getByRole('columnheader', { name: /organization/i })).toBeInTheDocument();
expect(screen.getByRole('columnheader', { name: /scope/i })).toBeInTheDocument();
expect(screen.getByRole('columnheader', { name: /permissions/i })).toBeInTheDocument();
expect(screen.getByRole('columnheader', { name: /actions/i })).toBeInTheDocument();
});
});

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 }),
(useUserAccount as jest.Mock).mockReturnValue({
data: mockUser,
isLoading: false,
error: null,
});

(useUserAssignedRoles as jest.Mock).mockReturnValue({
data: mockAssignments,
isLoading: false,
error: null,
});

renderWithRouter();
Expand All @@ -236,17 +261,31 @@ describe('AuditUserPage', () => {
});

it('renders the pagination controls when assignments are present', async () => {
(getAuthenticatedHttpClient as jest.Mock).mockReturnValue({
get: jest
.fn()
.mockResolvedValueOnce({ data: mockUser })
.mockResolvedValueOnce({ data: mockAssignments }),
(useUserAccount as jest.Mock).mockReturnValue({
data: mockUser,
isLoading: false,
error: null,
});

(useUserAssignedRoles as jest.Mock).mockReturnValue({
data: mockAssignments,
isLoading: false,
error: null,
});

renderWithRouter();

// Wait for user data first
await waitFor(() => {
expect(screen.getByText('johndoe', { selector: 'li[aria-current="page"]' })).toBeInTheDocument();
});

// Then check for assignment data and pagination
await waitFor(() => {
expect(screen.getByText('Showing 1 of 1.')).toBeInTheDocument();
// Look for pagination controls
expect(screen.getByRole('navigation', { name: /table pagination/i })).toBeInTheDocument();
// Check that some users count is shown (format might vary)
expect(screen.getByText(/showing/i)).toBeInTheDocument();
});
});

Expand Down
27 changes: 23 additions & 4 deletions src/authz-module/audit-user/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,16 @@ import { useRevokeUserRoles, useUserAssignedRoles } from '@src/authz-module/data
import { RoleToDelete } from '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';
import RolesFilter from '@src/authz-module/components/TableControlBar/RolesFilter';
import TableControlBar from '@src/authz-module/components/TableControlBar/TableControlBar';
import messages from './messages';
import ConfirmDeletionModal from '../components/ConfirmDeletionModal';
import { getCellHeader } from '../components/utils';

const AuditUserPage = () => {
const { formatMessage } = useIntl();
const [columnsWithFiltersApplied, setColumnsWithFiltersApplied] = useState<string[]>([]);
const { username } = useParams();
const { authenticatedUser } = useContext(AppContext as React.Context<AppContextType>);
const navigate = useNavigate();
Expand Down Expand Up @@ -90,28 +95,37 @@ const AuditUserPage = () => {

const columns = useMemo(() => [
{
Header: formatMessage(messages['authz.user.table.role.column.header']),
Header: getCellHeader('role', formatMessage(messages['authz.user.table.role.column.header']), columnsWithFiltersApplied),
accessor: 'role',
Cell: RoleCell,
filter: 'includesValue',
Filter: RolesFilter,
filterButtonText: formatMessage(messages['authz.user.table.role.column.header']),
filterOrder: 2,
},
{
Header: formatMessage(messages['authz.user.table.organization.column.header']),
Header: getCellHeader('org', formatMessage(messages['authz.user.table.organization.column.header']), columnsWithFiltersApplied),
accessor: 'org',
Cell: OrgCell,
filter: 'includesValue',
Filter: OrgFilter,
filterButtonText: formatMessage(messages['authz.user.table.organization.column.header']),
filterOrder: 1,
},
{
Header: formatMessage(messages['authz.user.table.scope.column.header']),
Header: getCellHeader('scope', formatMessage(messages['authz.user.table.scope.column.header']), columnsWithFiltersApplied),
accessor: 'scope',
Cell: ScopeCell,
disableFilters: true,

},
{
Header: formatMessage(messages['authz.user.table.permissions.column.header']),
Cell: PermissionsCell,
disableFilters: true,
disableSortBy: true,
},
], [formatMessage]);
], [formatMessage, columnsWithFiltersApplied]);

const pageCount = Math.ceil(count / TABLE_DEFAULT_PAGE_SIZE);

Expand Down Expand Up @@ -208,8 +222,12 @@ const AuditUserPage = () => {
<Container className="bg-light-200 p-5">
<DataTable
isPaginated
isFilterable
isSortable
manualPagination
data={userAssignments}
manualFilters
manualSortBy
fetchData={fetchData}
itemCount={count}
pageCount={pageCount}
Expand All @@ -222,6 +240,7 @@ const AuditUserPage = () => {
<UserPermissions row={row} />
)}
>
<TableControlBar onFilterChange={setColumnsWithFiltersApplied} />
<DataTable.Table />
<TableFooter />
</DataTable>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { DataTableContext, TextFilter } from '@openedx/paragon';
import userEvent from '@testing-library/user-event';
import TableControlBar from './TableControlBar';
import RolesFilter from './RolesFilter';
import ScopesFilter from './ScopesFilter';
import MultipleChoiceFilter from './MultipleChoiceFilter';

const mockSetAllFilters = jest.fn();
const mockOnFilterChange = jest.fn();
Expand Down Expand Up @@ -38,7 +40,7 @@ jest.mock('@src/authz-module/data/hooks', () => ({
count: 0, next: null, previous: null, results: [],
},
}),
useScopes: () => ({ data: { scopes: [] } }),
useScopes: () => ({ data: { results: [] } }),
}));

describe('TableControlBar', () => {
Expand Down Expand Up @@ -112,6 +114,47 @@ describe('TableControlBar', () => {
expect(screen.getByRole('textbox')).toBeInTheDocument();
});

it('renders scopes filter when configured', () => {
const contextWithScopesFilter = {
columns: [
{
id: 'scopes',
Header: 'Scopes',
Filter: ScopesFilter,
canFilter: true,
filterButtonText: 'Select Scopes',
setFilter: jest.fn(),
},
],
};

renderWithContext(<TableControlBar />, contextWithScopesFilter);
expect(screen.getByText('Select Scopes')).toBeInTheDocument();
});

it('renders multiple choice filter when configured', () => {
const contextWithMultipleChoiceFilter = {
columns: [
{
id: 'status',
Header: 'Status',
Filter: MultipleChoiceFilter,
canFilter: true,
filterButtonText: 'Select Status',
setFilter: jest.fn(),
filterChoices: [
{ displayName: 'Active', value: 'active' },
{ displayName: 'Inactive', value: 'inactive' },
],
filterValue: [],
},
],
};

renderWithContext(<TableControlBar />, contextWithMultipleChoiceFilter);
expect(screen.getByText('Select Status')).toBeInTheDocument();
});

it('displays filter chips when filters are applied', () => {
const contextWithAppliedFilters = {
state: {
Expand Down Expand Up @@ -167,12 +210,6 @@ describe('TableControlBar', () => {
expect(mockOnFilterChange).toHaveBeenCalledWith(['test']);
});

it('handles empty columns gracefully', () => {
renderWithContext(<TableControlBar />);
const container = document.querySelector('.authz-table-control-bar');
expect(container).toBeInTheDocument();
expect(screen.queryByText('Filter by')).not.toBeInTheDocument();
});
it('handles empty columns gracefully', () => {
renderWithContext(<TableControlBar />);
const container = document.querySelector('.authz-table-control-bar');
Expand Down Expand Up @@ -233,7 +270,6 @@ describe('TableControlBar', () => {
};

renderWithContext(<TableControlBar />, contextWithFilter);

const handleSetFilters = mockSetFilter.mock.calls[0]?.[0];
if (handleSetFilters) {
const newFilter = { groupName: 'roles', value: 'admin', displayName: 'Admin' };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ const TableControlBar = ({ onFilterChange }: TableControlBarProps) => {
if (Filter === RolesFilter) {
return (
<RolesFilter
key={column.id || column.accessor}
{...column}
setFilter={handleSetFilters(column.setFilter)}
disabled={filtersLimitReached}
Expand All @@ -122,6 +123,7 @@ const TableControlBar = ({ onFilterChange }: TableControlBarProps) => {
if (Filter === OrgFilter) {
return (
<OrgFilter
key={column.id || column.accessor}
{...column}
setFilter={handleSetFilters(column.setFilter)}
disabled={filtersLimitReached}
Expand All @@ -131,6 +133,7 @@ const TableControlBar = ({ onFilterChange }: TableControlBarProps) => {
if (Filter === MultipleChoiceFilter) {
return (
<MultipleChoiceFilter
key={column.id || column.accessor}
{...column}
setFilter={handleSetFilters(column.setFilter)}
disabled={filtersLimitReached}
Expand All @@ -140,6 +143,7 @@ const TableControlBar = ({ onFilterChange }: TableControlBarProps) => {
if (Filter === ScopesFilter) {
return (
<ScopesFilter
key={column.id || column.accessor}
{...column}
setFilter={handleSetFilters(column.setFilter)}
disabled={filtersLimitReached}
Expand Down
1 change: 1 addition & 0 deletions src/authz-module/components/TableFooter/TableFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const Footer = () => {
variant="reduced"
currentPage={pageIndex + 1}
pageCount={pageCount}
paginationLabel={formatMessage(messages['authz.table.footer.pagination.label'])}
onPageSelect={(pageNum) => gotoPage(pageNum - 1)}
/>
</TableFooter>
Expand Down
Loading