@@ -21,6 +21,9 @@ const mocks = vi.hoisted(() => ({
2121 approvalError : null as Error | null ,
2222 availabilityError : null as Error | null ,
2323 retryAvailability : vi . fn ( ) ,
24+ removeAccounts : vi . fn ( ) ,
25+ accountRemovalError : null as Error | null ,
26+ accountRemovalPending : false ,
2427} ) )
2528
2629vi . mock ( 'next/navigation' , ( ) => ( {
@@ -60,6 +63,11 @@ vi.mock('@/hooks/queries/kb/connectors', () => ({
6063} ) )
6164vi . mock ( '@/hooks/queries/organization-accounts' , ( ) => ( {
6265 useOrganizationAccounts : mocks . accounts ,
66+ useUpdateOrganizationAccounts : ( ) => ( {
67+ mutate : mocks . removeAccounts ,
68+ error : mocks . accountRemovalError ,
69+ isPending : mocks . accountRemovalPending ,
70+ } ) ,
6371} ) )
6472vi . mock ( '@/hooks/queries/search-integrations' , ( ) => ( {
6573 useUpdateSearchIntegration : ( ) => ( {
@@ -126,6 +134,8 @@ describe('organization provider management', () => {
126134 mocks . access = { admin : true , members : true }
127135 mocks . approvalError = null
128136 mocks . availabilityError = null
137+ mocks . accountRemovalError = null
138+ mocks . accountRemovalPending = false
129139 mocks . overview . mockReturnValue ( { data : { providers : [ provider ] } , isPending : false } )
130140 mocks . sources . mockReturnValue ( {
131141 data : [ source ] ,
@@ -168,6 +178,96 @@ describe('organization provider management', () => {
168178 await act ( async ( ) => button ! . click ( ) )
169179 }
170180
181+ function withSlackAccounts ( approved = true , status = 'active' ) {
182+ mocks . overview . mockReturnValue ( {
183+ data : { providers : [ { ...provider , connectorType : 'slack' , approved } ] } ,
184+ } )
185+ mocks . accounts . mockReturnValue ( {
186+ data : {
187+ credentialGroup : {
188+ ...credentialGroup ,
189+ options : [
190+ { ...credentialGroup . options [ 0 ] , label : 'Google' , required : true } ,
191+ {
192+ id : 'slack-option' ,
193+ provider : 'slack' ,
194+ label : 'Slack' ,
195+ required : false ,
196+ status,
197+ configurationStatus : 'ready' ,
198+ } ,
199+ ] ,
200+ } ,
201+ } ,
202+ } )
203+ }
204+
205+ it . each ( [ 'active' , 'disabled' ] ) (
206+ 'removes only Slack account setup after confirmation, including a %s option' ,
207+ async ( status ) => {
208+ withSlackAccounts ( true , status )
209+ await render ( 'slack' )
210+ await click ( 'Remove account setup' )
211+ expect ( mocks . removeAccounts ) . not . toHaveBeenCalled ( )
212+ expect ( document . querySelector ( '[role="dialog"]' ) ) . toHaveTextContent ( 'saved app configuration' )
213+ await click ( 'Remove' )
214+ expect ( mocks . removeAccounts ) . toHaveBeenCalledExactlyOnceWith (
215+ {
216+ organizationId : 'org-one' ,
217+ groupId : 'accounts-one' ,
218+ update : {
219+ options : [ { id : 'google-option' , provider : 'google' , label : 'Google' , required : true } ] ,
220+ } ,
221+ } ,
222+ { onSuccess : expect . any ( Function ) }
223+ )
224+ await act ( async ( ) => mocks . removeAccounts . mock . calls [ 0 ] [ 1 ] . onSuccess ( ) )
225+ expect ( document . querySelector ( '[role="dialog"]' ) ) . toBeNull ( )
226+ }
227+ )
228+
229+ it ( 'offers removal when Slack is deactivated and allows cancelling without a mutation' , async ( ) => {
230+ withSlackAccounts ( false )
231+ await render ( 'slack' )
232+ await click ( 'Remove account setup' )
233+ await click ( 'Cancel' )
234+ expect ( document . querySelector ( '[role="dialog"]' ) ) . toBeNull ( )
235+ expect ( mocks . removeAccounts ) . not . toHaveBeenCalled ( )
236+ } )
237+
238+ it ( 'keeps connector-dependency errors visible in the removal dialog' , async ( ) => {
239+ withSlackAccounts ( )
240+ mocks . accountRemovalError = new Error ( 'Remove the source using these accounts first.' )
241+ await render ( 'slack' )
242+ await click ( 'Remove account setup' )
243+ await click ( 'Remove' )
244+ expect ( document . querySelector ( '[role="dialog"] [role="alert"]' ) ) . toHaveTextContent (
245+ 'Remove the source using these accounts first.'
246+ )
247+ } )
248+
249+ it ( 'passes the removal action to the Slack Accounts tab header' , async ( ) => {
250+ withSlackAccounts ( )
251+ await render ( 'slack' , '?view=accounts' )
252+ const actions = mocks . people . mock . calls . at ( - 1 ) ! [ 0 ] . panel . actions
253+ expect ( actions ) . toEqual ( [
254+ expect . objectContaining ( { text : 'Remove account setup' , onSelect : expect . any ( Function ) } ) ,
255+ ] )
256+ await act ( async ( ) => actions [ 0 ] . onSelect ( ) )
257+ expect ( document . querySelector ( '[role="dialog"]' ) ) . toHaveTextContent (
258+ 'Remove Slack account setup?'
259+ )
260+ } )
261+
262+ it ( 'keeps Slack cleanup available even when personal source creation is unavailable' , async ( ) => {
263+ withSlackAccounts ( )
264+ mocks . personal = false
265+ await render ( 'slack' )
266+ expect ( mocks . accounts ) . toHaveBeenCalledWith ( 'org-one' )
267+ await click ( 'Remove account setup' )
268+ expect ( document . querySelector ( '[role="dialog"]' ) ) . not . toBeNull ( )
269+ } )
270+
171271 it ( 'uses named source links even when the admin has not reconnected their own account' , async ( ) => {
172272 await render ( )
173273 expect ( mocks . sources ) . toHaveBeenCalledWith (
@@ -450,6 +550,9 @@ describe('organization provider management', () => {
450550 expect ( container . textContent ) . toContain ( 'Set up the Slack app to connect accounts.' )
451551 expect ( container . querySelector ( 'input[placeholder="Search people..."]' ) ) . toHaveValue ( 'alex' )
452552 await click ( 'Set up Slack app' )
553+ await act ( async ( ) => {
554+ await vi . waitFor ( ( ) => expect ( mocks . updateUrl ) . toHaveBeenCalled ( ) )
555+ } )
453556 const query = new URLSearchParams ( mocks . updateUrl . mock . calls . at ( - 1 ) ! [ 0 ] . queryString )
454557 expect ( query . get ( 'connectedAccounts' ) ) . toBe ( 'slack' )
455558 expect ( query . get ( 'view' ) ) . toBe ( 'accounts' )
0 commit comments