@@ -7,8 +7,19 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
77import { RequestAccessAction } from '@/components/access-requests/request-access-action'
88import type { AccessRequestTarget } from '@/lib/api/contracts/access-requests'
99
10+ const mocks = vi . hoisted ( ( ) => ( {
11+ discovery : vi . fn ( ) ,
12+ create : vi . fn ( ) ,
13+ push : vi . fn ( ) ,
14+ refetch : vi . fn ( ) ,
15+ } ) )
16+ vi . mock ( 'next/navigation' , ( ) => ( {
17+ useRouter : ( ) => ( { push : mocks . push } ) ,
18+ usePathname : ( ) => '/workspace/workspace' ,
19+ } ) )
1020vi . mock ( '@/hooks/queries/access-requests' , ( ) => ( {
11- useCreateAccessRequest : ( ) => ( { mutate : vi . fn ( ) , isPending : false , error : null } ) ,
21+ useCreateAccessRequest : ( ) => ( { mutate : mocks . create , isPending : false , error : null } ) ,
22+ useDiscoverAccessRequests : mocks . discovery ,
1223} ) )
1324
1425const scope = { kind : 'workspace' , workspaceId : 'workspace' } as const
@@ -18,6 +29,16 @@ describe('request form lifecycle', () => {
1829 let container : HTMLDivElement
1930 let root : Root
2031 beforeEach ( ( ) => {
32+ vi . clearAllMocks ( )
33+ mocks . discovery . mockReturnValue ( {
34+ isSuccess : true ,
35+ isPending : false ,
36+ isError : false ,
37+ data : {
38+ enabled : true ,
39+ entries : [ { target : tables , state : 'requestable' , pendingRequestId : null } ] ,
40+ } ,
41+ } )
2142 ; ( globalThis as { IS_REACT_ACT_ENVIRONMENT ?: boolean } ) . IS_REACT_ACT_ENVIRONMENT = true
2243 container = document . createElement ( 'div' )
2344 document . body . appendChild ( container )
@@ -57,4 +78,165 @@ describe('request form lifecycle', () => {
5778 render ( undefined , { kind : 'feature' , configKey : 'hideKnowledgeBaseTab' } )
5879 expect ( document . querySelector ( '[role="dialog"]' ) ) . toBeNull ( )
5980 } )
81+ const clickDialogButton = ( label : string ) => {
82+ const button = Array . from (
83+ document . querySelectorAll < HTMLButtonElement > ( '[role="dialog"] button' )
84+ ) . find ( ( button ) => button . textContent === label )
85+ expect ( button ) . toBeDefined ( )
86+ act ( ( ) => button ! . click ( ) )
87+ }
88+
89+ it ( 'does no target discovery until opened and looks up only the chosen target' , ( ) => {
90+ render ( )
91+ expect ( mocks . discovery ) . not . toHaveBeenCalled ( )
92+ open ( )
93+ expect ( mocks . discovery ) . toHaveBeenCalledWith ( {
94+ ...scope ,
95+ targetKind : 'feature' ,
96+ targetKey : 'feature:hideTablesTab' ,
97+ limit : 1 ,
98+ offset : 0 ,
99+ } )
100+ expect ( document . querySelector ( 'textarea' ) ) . not . toBeNull ( )
101+ clickDialogButton ( 'Send request' )
102+ expect ( mocks . create ) . toHaveBeenCalledWith (
103+ { scope, target : tables , reason : '' } ,
104+ expect . any ( Object )
105+ )
106+ } )
107+
108+ it ( 'opens the existing pending request without allowing another reason or submission' , ( ) => {
109+ mocks . discovery . mockReturnValue ( {
110+ isSuccess : true ,
111+ data : {
112+ enabled : true ,
113+ entries : [ { state : 'requestable' , pendingRequestId : 'pending/request' } ] ,
114+ } ,
115+ } )
116+ render ( )
117+ open ( )
118+ expect ( document . querySelector ( 'textarea' ) ) . toBeNull ( )
119+ clickDialogButton ( 'View request' )
120+ expect ( mocks . push ) . toHaveBeenCalledWith (
121+ '/workspace/workspace/access-requests?requestId=pending%2Frequest'
122+ )
123+ expect ( mocks . create ) . not . toHaveBeenCalled ( )
124+ expect ( document . querySelector ( '[role="dialog"]' ) ) . toBeNull ( )
125+ } )
126+
127+ it ( 'routes a pending member limit request in its organization scope' , ( ) => {
128+ const organizationScope = { kind : 'organization' , organizationId : 'organization' } as const
129+ const limit = { kind : 'usage_limit' , id : 'member' } as const
130+ mocks . discovery . mockReturnValue ( {
131+ isSuccess : true ,
132+ data : {
133+ enabled : true ,
134+ entries : [ { state : 'requestable' , pendingRequestId : 'limit-request' } ] ,
135+ } ,
136+ } )
137+ act ( ( ) =>
138+ root . render (
139+ < RequestAccessAction scope = { organizationScope } target = { limit } label = 'Member usage limit' />
140+ )
141+ )
142+ open ( )
143+ expect ( mocks . discovery ) . toHaveBeenCalledWith ( {
144+ ...organizationScope ,
145+ targetKind : 'usage_limit' ,
146+ targetKey : 'usage_limit:member' ,
147+ limit : 1 ,
148+ offset : 0 ,
149+ } )
150+ clickDialogButton ( 'View request' )
151+ expect ( mocks . push ) . toHaveBeenCalledWith (
152+ '/access-requests?requestId=limit-request&organizationId=organization'
153+ )
154+ expect ( mocks . create ) . not . toHaveBeenCalled ( )
155+ } )
156+
157+ it ( 'blocks submission until discovery completes' , ( ) => {
158+ mocks . discovery . mockReturnValue ( { isPending : true } )
159+ render ( )
160+ open ( )
161+ clickDialogButton ( 'Loading...' )
162+ expect ( document . querySelector ( 'textarea' ) ) . toBeNull ( )
163+ expect ( mocks . create ) . not . toHaveBeenCalled ( )
164+ } )
165+
166+ it ( 'offers retry instead of using cached eligibility after a failed refresh' , ( ) => {
167+ mocks . discovery . mockReturnValue ( {
168+ isSuccess : false ,
169+ isError : true ,
170+ error : new Error ( 'Access check failed' ) ,
171+ refetch : mocks . refetch ,
172+ data : { enabled : true , entries : [ { state : 'requestable' , pendingRequestId : null } ] } ,
173+ } )
174+ render ( )
175+ open ( )
176+ expect ( document . querySelector ( '[role="dialog"]' ) ?. textContent ) . toContain ( 'Access check failed' )
177+ expect ( document . querySelector ( 'textarea' ) ) . toBeNull ( )
178+ clickDialogButton ( 'Retry' )
179+ expect ( mocks . refetch ) . toHaveBeenCalledOnce ( )
180+ expect ( mocks . create ) . not . toHaveBeenCalled ( )
181+ } )
182+
183+ it . each ( [
184+ { enabled : false , entries : [ ] } ,
185+ { enabled : true , entries : [ ] } ,
186+ {
187+ enabled : true ,
188+ entries : [
189+ { state : 'allowed' , reason : 'Access is already available.' , pendingRequestId : null } ,
190+ ] ,
191+ } ,
192+ {
193+ enabled : true ,
194+ entries : [ { state : 'unavailable' , reason : 'Access is unavailable.' , pendingRequestId : null } ] ,
195+ } ,
196+ ] ) ( 'does not submit when the target is not requestable: %j' , ( data ) => {
197+ mocks . discovery . mockReturnValue ( { isSuccess : true , data } )
198+ render ( )
199+ open ( )
200+ clickDialogButton ( 'Send request' )
201+ expect ( document . querySelector ( 'textarea' ) ) . toBeNull ( )
202+ expect ( mocks . create ) . not . toHaveBeenCalled ( )
203+ } )
204+ it ( 'preserves the caller navigation handler for a newly discovered pending request' , ( ) => {
205+ const onViewRequest = vi . fn ( )
206+ mocks . discovery . mockReturnValue ( {
207+ isSuccess : true ,
208+ data : { enabled : true , entries : [ { state : 'requestable' , pendingRequestId : 'existing' } ] } ,
209+ } )
210+ act ( ( ) =>
211+ root . render (
212+ < RequestAccessAction
213+ scope = { scope }
214+ target = { tables }
215+ label = 'Tables'
216+ onViewRequest = { onViewRequest }
217+ />
218+ )
219+ )
220+ open ( )
221+ clickDialogButton ( 'View request' )
222+ expect ( onViewRequest ) . toHaveBeenCalledWith ( 'existing' )
223+ expect ( mocks . push ) . not . toHaveBeenCalled ( )
224+ } )
225+
226+ it ( 'presents newly granted access as a neutral result' , ( ) => {
227+ mocks . discovery . mockReturnValue ( {
228+ isSuccess : true ,
229+ data : {
230+ enabled : true ,
231+ entries : [ { state : 'allowed' , reason : null , pendingRequestId : null } ] ,
232+ } ,
233+ } )
234+ render ( )
235+ open ( )
236+ expect ( document . querySelector ( '[role="dialog"]' ) ?. textContent ) . toContain (
237+ 'Access is already available.'
238+ )
239+ expect ( document . querySelector ( '[role="alert"]' ) ) . toBeNull ( )
240+ expect ( document . querySelector ( 'textarea' ) ) . toBeNull ( )
241+ } )
60242} )
0 commit comments