1111
1212import { render , screen , within } from '@testing-library/react' ;
1313import userEvent from '@testing-library/user-event' ;
14+ import { NuqsTestingAdapter } from 'nuqs/adapters/testing' ;
1415import type { ReactNode } from 'react' ;
1516import { beforeEach , describe , expect , test , vi } from 'vitest' ;
1617
@@ -23,104 +24,7 @@ import { beforeEach, describe, expect, test, vi } from 'vitest';
2324 * (they're ACL-only principals, not SASL-SCRAM accounts)
2425 */
2526
26- const { listACLsData } = vi . hoisted ( ( ) => ( {
27- listACLsData : {
28- // Return a SCRAM user, a non-SCRAM user (ACL-only), and a Group principal
29- data : [
30- { host : '*' , principal : 'User:scram-admin' , principalType : 'User' , principalName : 'scram-admin' , hasAcl : true } ,
31- {
32- host : '*' ,
33- principal : 'User:acl-only-user' ,
34- principalType : 'User' ,
35- principalName : 'acl-only-user' ,
36- hasAcl : true ,
37- } ,
38- { host : '*' , principal : 'Group:engineering' , principalType : 'Group' , principalName : 'engineering' , hasAcl : true } ,
39- ] ,
40- error : null ,
41- isError : false ,
42- isLoading : false ,
43- } ,
44- } ) ) ;
45-
46- vi . mock ( '@redpanda-data/ui' , ( ) => {
47- const Div = ( { children, ...props } : { children ?: ReactNode ; [ key : string ] : unknown } ) => (
48- < div { ...props } > { children } </ div >
49- ) ;
50-
51- return {
52- Alert : Div ,
53- AlertDescription : Div ,
54- AlertIcon : ( ) => < span /> ,
55- AlertTitle : Div ,
56- Badge : Div ,
57- Box : Div ,
58- Button : ( {
59- children,
60- isDisabled,
61- onClick,
62- ...props
63- } : {
64- children ?: ReactNode ;
65- isDisabled ?: boolean ;
66- onClick ?: ( ) => void ;
67- [ key : string ] : unknown ;
68- } ) => (
69- < button disabled = { isDisabled } onClick = { onClick } { ...props } >
70- { children }
71- </ button >
72- ) ,
73- createStandaloneToast : ( ) => ( {
74- ToastContainer : ( ) => null ,
75- toast : vi . fn ( ) ,
76- } ) ,
77- DataTable : ( {
78- columns,
79- data,
80- emptyText,
81- } : {
82- columns : Array < {
83- cell ?: ( ctx : { row : { original : Record < string , unknown > } } ) => ReactNode ;
84- header ?: ReactNode ;
85- id ?: string ;
86- } > ;
87- data : Record < string , unknown > [ ] ;
88- emptyText ?: ReactNode ;
89- } ) =>
90- data . length > 0 ? (
91- < table >
92- < tbody >
93- { data . map ( ( row , rowIndex ) => (
94- < tr data-testid = { `row-${ row . name ?? rowIndex } ` } key = { String ( row . name ?? rowIndex ) } >
95- { columns . map ( ( column , colIndex ) => (
96- < td key = { column . id ?? colIndex } > { column . cell ?.( { row : { original : row } } ) ?? null } </ td >
97- ) ) }
98- </ tr >
99- ) ) }
100- </ tbody >
101- </ table >
102- ) : (
103- < div > { emptyText } </ div >
104- ) ,
105- Flex : Div ,
106- SearchField : ( {
107- placeholderText,
108- searchText,
109- setSearchText,
110- } : {
111- placeholderText ?: string ;
112- searchText ?: string ;
113- setSearchText ?: ( value : string ) => void ;
114- } ) => (
115- < input onChange = { ( e ) => setSearchText ?.( e . target . value ) } placeholder = { placeholderText } value = { searchText ?? '' } />
116- ) ,
117- Skeleton : Div ,
118- Text : Div ,
119- Tooltip : ( { children } : { children ?: ReactNode } ) => < > { children } </ > ,
120- redpandaTheme : { } ,
121- redpandaToastOptions : { defaultOptions : { } } ,
122- } ;
123- } ) ;
27+ const NuqsWrapper = ( { children } : { children : ReactNode } ) => < NuqsTestingAdapter > { children } </ NuqsTestingAdapter > ;
12428
12529vi . mock ( '@tanstack/react-router' , async ( importOriginal ) => {
12630 const actual = await importOriginal < typeof import ( '@tanstack/react-router' ) > ( ) ;
@@ -135,6 +39,10 @@ vi.mock('@tanstack/react-router', async (importOriginal) => {
13539 } ;
13640} ) ;
13741
42+ vi . mock ( '../shared/security-tabs-nav' , ( ) => ( {
43+ SecurityTabsNav : ( ) => null ,
44+ } ) ) ;
45+
13846vi . mock ( '../shared/delete-user-confirm-modal' , ( ) => ( {
13947 DeleteUserConfirmModal : ( {
14048 open,
@@ -155,10 +63,6 @@ vi.mock('../shared/delete-user-confirm-modal', () => ({
15563 ) : null ,
15664} ) ) ;
15765
158- vi . mock ( '../shared/user-role-tags' , ( ) => ( {
159- UserRoleTags : ( ) => null ,
160- } ) ) ;
161-
16266vi . mock ( '../../../../components/misc/error-result' , ( ) => ( {
16367 default : ( ) => null ,
16468} ) ) ;
@@ -209,27 +113,60 @@ vi.mock('../../../../state/rest-interfaces', () => ({
209113 AclRequestDefault : { } ,
210114} ) ) ;
211115
212- vi . mock ( '../../../misc/section' , ( ) => ( {
213- default : ( { children } : { children ?: ReactNode } ) => < section > { children } </ section > ,
214- } ) ) ;
215-
216- vi . mock ( 'react-query/api/cluster-status' , ( ) => ( {
217- useGetRedpandaInfoQuery : ( ) => ( { data : { } , isSuccess : true } ) ,
116+ vi . mock ( '../../../../react-query/api/acl' , ( ) => ( {
117+ useCreateACLMutation : ( ) => ( { mutateAsync : vi . fn ( ) } ) ,
118+ useDeleteAclMutation : ( ) => ( { mutateAsync : vi . fn ( ) } ) ,
218119} ) ) ;
219120
220- vi . mock ( 'react-query/api/user' , ( ) => ( {
121+ vi . mock ( '../../../../ react-query/api/user' , ( ) => ( {
221122 useInvalidateUsersCache : ( ) => vi . fn ( ) ,
222123 useDeleteUserMutation : ( ) => ( { mutateAsync : vi . fn ( ) . mockResolvedValue ( undefined ) } ) ,
223- // "scram-admin" is a SCRAM user; "acl-only-user" is NOT (only has ACLs)
224- useListUsersQuery : ( ) => ( {
225- data : { users : [ { name : 'scram-admin' } ] } ,
226- isLoading : false ,
227- } ) ,
124+ useListUsersQuery : ( ) => ( { data : { users : [ ] } , isLoading : false } ) ,
228125} ) ) ;
229126
230- vi . mock ( 'react-query/api/acl' , ( ) => ( {
231- useDeleteAclMutation : ( ) => ( { mutateAsync : vi . fn ( ) } ) ,
232- useListACLAsPrincipalGroups : ( ) => listACLsData ,
127+ vi . mock ( '../hooks/use-principal-permissions' , ( ) => ( {
128+ usePrincipalPermissions : ( ) => ( {
129+ principalGroups : [
130+ {
131+ principal : 'User:scram-admin' ,
132+ principalType : 'User' ,
133+ principalName : 'scram-admin' ,
134+ isScramUser : true ,
135+ directAcls : [ ] ,
136+ roleAclGroups : [ ] ,
137+ directAclCount : 0 ,
138+ inheritedAclCount : 0 ,
139+ denyCount : 0 ,
140+ } ,
141+ {
142+ principal : 'User:acl-only-user' ,
143+ principalType : 'User' ,
144+ principalName : 'acl-only-user' ,
145+ isScramUser : false ,
146+ directAcls : [ ] ,
147+ roleAclGroups : [ ] ,
148+ directAclCount : 0 ,
149+ inheritedAclCount : 0 ,
150+ denyCount : 0 ,
151+ } ,
152+ {
153+ principal : 'Group:engineering' ,
154+ principalType : 'Group' ,
155+ principalName : 'engineering' ,
156+ isScramUser : false ,
157+ directAcls : [ ] ,
158+ roleAclGroups : [ ] ,
159+ directAclCount : 0 ,
160+ inheritedAclCount : 0 ,
161+ denyCount : 0 ,
162+ } ,
163+ ] ,
164+ isAclsLoading : false ,
165+ isAclsError : false ,
166+ aclsError : null ,
167+ isUsersError : false ,
168+ usersError : null ,
169+ } ) ,
233170} ) ) ;
234171
235172import { PermissionsListTab } from './permissions-list-tab' ;
@@ -242,11 +179,11 @@ describe('Permissions List - delete dropdown for different principal types', ()
242179 test ( 'Group principal does not show "Delete User" options in dropdown' , async ( ) => {
243180 const user = userEvent . setup ( ) ;
244181
245- render ( < PermissionsListTab /> ) ;
182+ render ( < PermissionsListTab /> , { wrapper : NuqsWrapper } ) ;
246183
247184 const groupRow = await screen . findByTestId ( 'row-engineering' ) ;
248- const deleteButton = within ( groupRow ) . getByRole ( 'button ') ;
249- await user . click ( deleteButton ) ;
185+ const actionsDiv = within ( groupRow ) . getByTestId ( 'actions-engineering ') ;
186+ await user . click ( within ( actionsDiv ) . getByRole ( 'button' ) ) ;
250187
251188 // Group should only have "Delete (ACLs only)", not user-delete options
252189 expect ( screen . queryByText ( 'Delete (User and ACLs)' ) ) . not . toBeInTheDocument ( ) ;
@@ -257,12 +194,11 @@ describe('Permissions List - delete dropdown for different principal types', ()
257194 test ( 'SCRAM user principal has "Delete User" options enabled' , async ( ) => {
258195 const user = userEvent . setup ( ) ;
259196
260- // "scram-admin" exists in usersData.users — it's a real SCRAM user
261- render ( < PermissionsListTab /> ) ;
197+ render ( < PermissionsListTab /> , { wrapper : NuqsWrapper } ) ;
262198
263199 const scramRow = await screen . findByTestId ( 'row-scram-admin' ) ;
264- const deleteButton = within ( scramRow ) . getByRole ( 'button ') ;
265- await user . click ( deleteButton ) ;
200+ const actionsDiv = within ( scramRow ) . getByTestId ( 'actions-scram-admin ') ;
201+ await user . click ( within ( actionsDiv ) . getByRole ( 'button' ) ) ;
266202
267203 // SCRAM user should have all delete options available and enabled
268204 const deleteUserAndAcls = screen . getByText ( 'Delete (User and ACLs)' ) ;
@@ -277,13 +213,12 @@ describe('Permissions List - delete dropdown for different principal types', ()
277213 test ( 'Group principal has "Delete (ACLs only)" available' , async ( ) => {
278214 const user = userEvent . setup ( ) ;
279215
280- render ( < PermissionsListTab /> ) ;
216+ render ( < PermissionsListTab /> , { wrapper : NuqsWrapper } ) ;
281217
282218 const groupRow = await screen . findByTestId ( 'row-engineering' ) ;
283- const deleteButton = within ( groupRow ) . getByRole ( 'button ') ;
284- await user . click ( deleteButton ) ;
219+ const actionsDiv = within ( groupRow ) . getByTestId ( 'actions-engineering ') ;
220+ await user . click ( within ( actionsDiv ) . getByRole ( 'button' ) ) ;
285221
286- // Even though user-delete options are hidden, "Delete (ACLs only)" is always available
287222 expect ( screen . getByText ( 'Delete (ACLs only)' ) ) . toBeInTheDocument ( ) ;
288223 } ) ;
289224} ) ;
0 commit comments