11import { describe , it , expect , vi , beforeEach } from 'vitest'
2- import { screen , waitFor } from '@testing-library/react'
2+ import { screen , waitFor , fireEvent } from '@testing-library/react'
33import { renderWithProviders } from '../test/test-utils'
4- import { makePolicy , makePolicyAssignment } from '../test/factories'
4+ import { makePolicyAssignment , makeDataSource } from '../test/factories'
55import { makeUser } from '../test/factories'
66
77vi . mock ( '../api/policies' , ( ) => ( {
88 listDatasourcePolicies : vi . fn ( ) ,
9- listPolicies : vi . fn ( ) ,
109 assignPolicy : vi . fn ( ) ,
1110 removeAssignment : vi . fn ( ) ,
1211} ) )
1312
13+ vi . mock ( '../api/datasources' , ( ) => ( {
14+ listDataSources : vi . fn ( ) ,
15+ } ) )
16+
1417vi . mock ( '../api/users' , ( ) => ( {
1518 listUsers : vi . fn ( ) ,
1619} ) )
1720
18- import { listDatasourcePolicies , listPolicies } from '../api/policies'
21+ import { listDatasourcePolicies , assignPolicy } from '../api/policies'
22+ import { listDataSources } from '../api/datasources'
1923import { listUsers } from '../api/users'
20- import { PolicyAssignmentsReadonly , PolicyAssignmentPanel } from './PolicyAssignmentPanel'
24+ import {
25+ PolicyAssignmentsReadonly ,
26+ PolicyAssignmentEditPanel ,
27+ DatasourceAssignmentsReadonly ,
28+ } from './PolicyAssignmentPanel'
2129
2230const mockListDsPolicies = listDatasourcePolicies as ReturnType < typeof vi . fn >
23- const mockListPolicies = listPolicies as ReturnType < typeof vi . fn >
31+ const mockAssignPolicy = assignPolicy as ReturnType < typeof vi . fn >
32+ const mockListDataSources = listDataSources as ReturnType < typeof vi . fn >
2433const mockListUsers = listUsers as ReturnType < typeof vi . fn >
2534
2635beforeEach ( ( ) => {
2736 vi . clearAllMocks ( )
2837 mockListDsPolicies . mockResolvedValue ( [ ] )
29- mockListPolicies . mockResolvedValue ( { data : [ ] , total : 0 , page : 1 , page_size : 100 } )
38+ mockAssignPolicy . mockResolvedValue ( { } )
39+ mockListDataSources . mockResolvedValue ( { data : [ ] , total : 0 , page : 1 , page_size : 200 } )
3040 mockListUsers . mockResolvedValue ( { data : [ ] , total : 0 , page : 1 , page_size : 100 } )
3141} )
3242
@@ -68,12 +78,141 @@ describe('PolicyAssignmentsReadonly', () => {
6878 } )
6979} )
7080
71- // ===== PolicyAssignmentPanel =====
81+ // ===== PolicyAssignmentEditPanel =====
82+
83+ describe ( 'PolicyAssignmentEditPanel' , ( ) => {
84+ it ( 'shows "No assignments yet" when assignments prop is empty' , ( ) => {
85+ renderWithProviders (
86+ < PolicyAssignmentEditPanel
87+ policyId = "p-1"
88+ assignments = { [ ] }
89+ onAssignmentChange = { vi . fn ( ) }
90+ /> ,
91+ { authenticated : true } ,
92+ )
93+ expect ( screen . getByText ( 'No assignments yet.' ) ) . toBeInTheDocument ( )
94+ } )
95+
96+ it ( 'renders assignments table with datasource link and Remove button' , ( ) => {
97+ const a = makePolicyAssignment ( {
98+ id : 'a-1' ,
99+ data_source_id : 'ds-99' ,
100+ datasource_name : 'staging-db' ,
101+ username : 'alice' ,
102+ priority : 50 ,
103+ } )
104+ renderWithProviders (
105+ < PolicyAssignmentEditPanel
106+ policyId = "p-1"
107+ assignments = { [ a ] }
108+ onAssignmentChange = { vi . fn ( ) }
109+ /> ,
110+ { authenticated : true } ,
111+ )
112+ const link = screen . getByRole ( 'link' , { name : 'staging-db' } )
113+ expect ( link ) . toHaveAttribute ( 'href' , '/datasources/ds-99/edit' )
114+ expect ( screen . getByRole ( 'button' , { name : / r e m o v e / i } ) ) . toBeInTheDocument ( )
115+ } )
116+
117+ it ( 'shows the Add Assignment form with datasource, user, and priority fields' , ( ) => {
118+ renderWithProviders (
119+ < PolicyAssignmentEditPanel
120+ policyId = "p-1"
121+ assignments = { [ ] }
122+ onAssignmentChange = { vi . fn ( ) }
123+ /> ,
124+ { authenticated : true } ,
125+ )
126+ expect ( screen . getByRole ( 'button' , { name : / a s s i g n p o l i c y / i } ) ) . toBeInTheDocument ( )
127+ expect ( screen . getByRole ( 'spinbutton' ) ) . toBeInTheDocument ( ) // priority number input
128+ } )
129+
130+ it ( 'populates datasource dropdown from listDataSources' , async ( ) => {
131+ const ds = makeDataSource ( { id : 'ds-1' , name : 'my-db' , is_active : true } )
132+ mockListDataSources . mockResolvedValue ( { data : [ ds ] , total : 1 , page : 1 , page_size : 200 } )
133+ renderWithProviders (
134+ < PolicyAssignmentEditPanel
135+ policyId = "p-1"
136+ assignments = { [ ] }
137+ onAssignmentChange = { vi . fn ( ) }
138+ /> ,
139+ { authenticated : true } ,
140+ )
141+ await waitFor ( ( ) => expect ( screen . getByText ( 'my-db' ) ) . toBeInTheDocument ( ) )
142+ } )
143+
144+ it ( 'populates user dropdown from listUsers' , async ( ) => {
145+ const user = makeUser ( { id : 'u-1' , username : 'bob' } )
146+ mockListUsers . mockResolvedValue ( { data : [ user ] , total : 1 , page : 1 , page_size : 100 } )
147+ renderWithProviders (
148+ < PolicyAssignmentEditPanel
149+ policyId = "p-1"
150+ assignments = { [ ] }
151+ onAssignmentChange = { vi . fn ( ) }
152+ /> ,
153+ { authenticated : true } ,
154+ )
155+ await waitFor ( ( ) => expect ( screen . getByText ( 'bob' ) ) . toBeInTheDocument ( ) )
156+ } )
157+
158+ it ( 'renders a Remove button for each assignment' , ( ) => {
159+ const assignments = [
160+ makePolicyAssignment ( { id : 'a-1' , datasource_name : 'db1' } ) ,
161+ makePolicyAssignment ( { id : 'a-2' , datasource_name : 'db2' } ) ,
162+ ]
163+ renderWithProviders (
164+ < PolicyAssignmentEditPanel
165+ policyId = "p-1"
166+ assignments = { assignments }
167+ onAssignmentChange = { vi . fn ( ) }
168+ /> ,
169+ { authenticated : true } ,
170+ )
171+ expect ( screen . getAllByRole ( 'button' , { name : / r e m o v e / i } ) ) . toHaveLength ( 2 )
172+ } )
72173
73- describe ( 'PolicyAssignmentPanel' , ( ) => {
174+ it ( 'shows error message when duplicate assignment returns 409' , async ( ) => {
175+ const ds = makeDataSource ( { id : 'ds-1' , name : 'prod-db' , is_active : true } )
176+ mockListDataSources . mockResolvedValue ( { data : [ ds ] , total : 1 , page : 1 , page_size : 200 } )
177+ mockAssignPolicy . mockRejectedValue ( {
178+ response : { data : { error : 'This policy is already assigned to this datasource for all users' } } ,
179+ } )
180+
181+ const { container } = renderWithProviders (
182+ < PolicyAssignmentEditPanel
183+ policyId = "p-1"
184+ assignments = { [ ] }
185+ onAssignmentChange = { vi . fn ( ) }
186+ /> ,
187+ { authenticated : true } ,
188+ )
189+
190+ await waitFor ( ( ) => expect ( screen . getByText ( 'prod-db' ) ) . toBeInTheDocument ( ) )
191+
192+ const dsSelect = container . querySelectorAll ( 'select' ) [ 0 ]
193+ fireEvent . change ( dsSelect , { target : { value : 'ds-1' } } )
194+
195+ await waitFor ( ( ) =>
196+ expect ( screen . getByRole ( 'button' , { name : / a s s i g n p o l i c y / i } ) ) . not . toBeDisabled ( ) ,
197+ )
198+ screen . getByRole ( 'button' , { name : / a s s i g n p o l i c y / i } ) . click ( )
199+
200+ await waitFor ( ( ) =>
201+ expect (
202+ screen . getByText ( / a l r e a d y a s s i g n e d t o t h i s d a t a s o u r c e f o r a l l u s e r s / i) ,
203+ ) . toBeInTheDocument ( ) ,
204+ )
205+ } )
206+ } )
207+
208+ // ===== DatasourceAssignmentsReadonly =====
209+
210+ describe ( 'DatasourceAssignmentsReadonly' , ( ) => {
74211 it ( 'shows "No policies assigned yet" when empty' , async ( ) => {
75212 mockListDsPolicies . mockResolvedValue ( [ ] )
76- renderWithProviders ( < PolicyAssignmentPanel datasourceId = "ds-1" /> , { authenticated : true } )
213+ renderWithProviders ( < DatasourceAssignmentsReadonly datasourceId = "ds-1" /> , {
214+ authenticated : true ,
215+ } )
77216 await waitFor ( ( ) =>
78217 expect ( screen . getByText ( 'No policies assigned yet.' ) ) . toBeInTheDocument ( ) ,
79218 )
@@ -86,41 +225,32 @@ describe('PolicyAssignmentPanel', () => {
86225 priority : 100 ,
87226 } )
88227 mockListDsPolicies . mockResolvedValue ( [ a ] )
89- renderWithProviders ( < PolicyAssignmentPanel datasourceId = "ds-1" /> , { authenticated : true } )
228+ renderWithProviders ( < DatasourceAssignmentsReadonly datasourceId = "ds-1" /> , {
229+ authenticated : true ,
230+ } )
90231 await waitFor ( ( ) => expect ( screen . getByText ( 'row-filter' ) ) . toBeInTheDocument ( ) )
91232 const link = screen . getByRole ( 'link' , { name : 'row-filter' } )
92233 expect ( link ) . toHaveAttribute ( 'href' , '/policies/p-99/edit' )
93234 } )
94235
95- it ( 'shows the add assignment form with policy, user, and priority fields' , async ( ) => {
96- renderWithProviders ( < PolicyAssignmentPanel datasourceId = "ds-1" /> , { authenticated : true } )
236+ it ( 'shows the "Manage assignments from the policy edit page" note' , async ( ) => {
237+ renderWithProviders ( < DatasourceAssignmentsReadonly datasourceId = "ds-1" /> , {
238+ authenticated : true ,
239+ } )
97240 await waitFor ( ( ) =>
98- expect ( screen . getByRole ( 'button' , { name : / a s s i g n p o l i c y / i } ) ) . toBeInTheDocument ( ) ,
241+ expect (
242+ screen . getByText ( / m a n a g e a s s i g n m e n t s f r o m t h e p o l i c y e d i t p a g e / i) ,
243+ ) . toBeInTheDocument ( ) ,
99244 )
100- expect ( screen . getByRole ( 'spinbutton' ) ) . toBeInTheDocument ( ) // priority number input
101- } )
102-
103- it ( 'populates policy dropdown from listPolicies' , async ( ) => {
104- const policy = makePolicy ( { id : 'p-1' , name : 'deny-cols' , policy_type : 'column_deny' } )
105- mockListPolicies . mockResolvedValue ( { data : [ policy ] , total : 1 , page : 1 , page_size : 100 } )
106- renderWithProviders ( < PolicyAssignmentPanel datasourceId = "ds-1" /> , { authenticated : true } )
107- await waitFor ( ( ) => expect ( screen . getByText ( / d e n y - c o l s .* c o l u m n _ d e n y / ) ) . toBeInTheDocument ( ) )
108245 } )
109246
110- it ( 'populates user dropdown from listUsers' , async ( ) => {
111- const user = makeUser ( { id : 'u-1' , username : 'bob' } )
112- mockListUsers . mockResolvedValue ( { data : [ user ] , total : 1 , page : 1 , page_size : 100 } )
113- renderWithProviders ( < PolicyAssignmentPanel datasourceId = "ds-1" /> , { authenticated : true } )
114- await waitFor ( ( ) => expect ( screen . getByText ( 'bob' ) ) . toBeInTheDocument ( ) )
115- } )
116-
117- it ( 'renders a Remove button for each assignment' , async ( ) => {
118- const assignments = [
119- makePolicyAssignment ( { id : 'a-1' , policy_name : 'p1' } ) ,
120- makePolicyAssignment ( { id : 'a-2' , policy_name : 'p2' } ) ,
121- ]
122- mockListDsPolicies . mockResolvedValue ( assignments )
123- renderWithProviders ( < PolicyAssignmentPanel datasourceId = "ds-1" /> , { authenticated : true } )
124- await waitFor ( ( ) => expect ( screen . getAllByRole ( 'button' , { name : / r e m o v e / i } ) ) . toHaveLength ( 2 ) )
247+ it ( 'does not render a Remove button' , async ( ) => {
248+ const a = makePolicyAssignment ( { policy_name : 'deny-cols' } )
249+ mockListDsPolicies . mockResolvedValue ( [ a ] )
250+ renderWithProviders ( < DatasourceAssignmentsReadonly datasourceId = "ds-1" /> , {
251+ authenticated : true ,
252+ } )
253+ await waitFor ( ( ) => expect ( screen . getByText ( 'deny-cols' ) ) . toBeInTheDocument ( ) )
254+ expect ( screen . queryByRole ( 'button' , { name : / r e m o v e / i } ) ) . toBeNull ( )
125255 } )
126256} )
0 commit comments