@@ -15,6 +15,8 @@ const mocks = vi.hoisted(() => ({
1515 deleteConnector : vi . fn ( ) ,
1616 syncConnector : vi . fn ( ) ,
1717 resolveBilling : vi . fn ( ) ,
18+ getCredentialActorContext : vi . fn ( ) ,
19+ canUseCredential : vi . fn ( ) ,
1820 resolveTokenIdentity : vi . fn ( ) ,
1921 refreshToken : vi . fn ( ) ,
2022 validateConnectorConfig : vi . fn ( ) ,
@@ -56,6 +58,8 @@ vi.mock('@/lib/knowledge/orchestration/connectors', () => ({
5658} ) )
5759
5860vi . mock ( '@/lib/credentials/access' , ( ) => ( {
61+ getCredentialActorContext : mocks . getCredentialActorContext ,
62+ canUseCredential : mocks . canUseCredential ,
5963 resolveCredentialTokenIdentity : mocks . resolveTokenIdentity ,
6064} ) )
6165
@@ -120,6 +124,17 @@ describe('knowledge connector application use cases', () => {
120124 mocks . resolvePermission . mockResolvedValue ( 'write' )
121125 mocks . resolveKnowledgeBase . mockResolvedValue ( crossWorkspaceContext )
122126 mocks . resolveConnector . mockResolvedValue ( connectorContext )
127+ mocks . getCredentialActorContext . mockResolvedValue ( {
128+ credential : { id : 'credential-1' , workspaceId : 'workspace-a' } ,
129+ member : { role : 'member' } ,
130+ hasWorkspaceAccess : true ,
131+ canWriteWorkspace : true ,
132+ isAdmin : false ,
133+ } )
134+ mocks . canUseCredential . mockImplementation (
135+ ( access : { hasWorkspaceAccess : boolean ; member : unknown ; isAdmin : boolean } ) =>
136+ access . hasWorkspaceAccess && ( Boolean ( access . member ) || access . isAdmin )
137+ )
123138 mocks . resolveTokenIdentity . mockResolvedValue ( { kind : 'oauth' , userId : 'credential-owner' } )
124139 mocks . refreshToken . mockResolvedValue ( 'access-token' )
125140 mocks . validateConnectorConfig . mockResolvedValue ( { valid : true } )
@@ -296,6 +311,7 @@ describe('knowledge connector application use cases', () => {
296311 expect ( mocks . resolvePermission . mock . invocationCallOrder [ 0 ] ) . toBeLessThan (
297312 mocks . updateConnector . mock . invocationCallOrder [ 0 ]
298313 )
314+ expect ( mocks . getCredentialActorContext ) . toHaveBeenCalledWith ( 'credential-1' , 'shared-user' )
299315 expect ( mocks . resolveTokenIdentity ) . toHaveBeenCalledWith ( 'credential-1' , 'workspace-a' )
300316 expect ( mocks . refreshToken ) . toHaveBeenCalledWith (
301317 'credential-1' ,
@@ -305,6 +321,117 @@ describe('knowledge connector application use cases', () => {
305321 expect ( mocks . validateConnectorConfig ) . toHaveBeenCalledWith ( 'access-token' , { space : 'ENG' } )
306322 } )
307323
324+ it ( 'rejects connector creation when the writer cannot use the workspace credential' , async ( ) => {
325+ const sameWorkspaceContext = {
326+ ...connectorContext ,
327+ workspaceId : 'workspace-a' ,
328+ knowledgeBaseId : 'knowledge-a' ,
329+ knowledgeBase : { id : 'knowledge-a' , name : 'Workspace A docs' } ,
330+ connector : { ...connectorContext . connector , knowledgeBaseId : 'knowledge-a' } ,
331+ }
332+ mocks . resolveKnowledgeBase . mockResolvedValueOnce ( sameWorkspaceContext )
333+ mocks . getCredentialActorContext . mockResolvedValueOnce ( {
334+ credential : { id : 'credential-1' , workspaceId : 'workspace-a' } ,
335+ member : null ,
336+ hasWorkspaceAccess : true ,
337+ canWriteWorkspace : true ,
338+ isAdmin : false ,
339+ } )
340+ mocks . createConnector . mockImplementationOnce (
341+ async ( input : { resolveAccessToken : ( credentialId : string ) => Promise < string | null > } ) => {
342+ const accessToken = await input . resolveAccessToken ( 'credential-1' )
343+ return accessToken
344+ ? { success : true , connector : sameWorkspaceContext . connector }
345+ : {
346+ success : false ,
347+ error : 'Credential has no access token. Please reconnect your account.' ,
348+ errorCode : 'validation' ,
349+ }
350+ }
351+ )
352+
353+ await expect (
354+ createKnowledgeConnector . execute ( {
355+ principal : delegatedPrincipal ,
356+ input : {
357+ knowledgeBaseId : 'knowledge-a' ,
358+ assertedWorkspaceId : 'workspace-a' ,
359+ connectorType : 'confluence' ,
360+ credentialId : 'credential-1' ,
361+ sourceConfig : { } ,
362+ syncIntervalMinutes : 1440 ,
363+ resolveBillingAttribution : mocks . resolveBilling ,
364+ } ,
365+ } )
366+ ) . rejects . toMatchObject ( { code : 'validation' } )
367+
368+ expect ( mocks . getCredentialActorContext ) . toHaveBeenCalledWith ( 'credential-1' , 'shared-user' )
369+ expect ( mocks . resolveTokenIdentity ) . not . toHaveBeenCalled ( )
370+ expect ( mocks . refreshToken ) . not . toHaveBeenCalled ( )
371+ } )
372+
373+ it ( 'rejects source-config revalidation after credential membership is removed' , async ( ) => {
374+ const sameWorkspaceContext = {
375+ ...connectorContext ,
376+ workspaceId : 'workspace-a' ,
377+ knowledgeBaseId : 'knowledge-a' ,
378+ knowledgeBase : { id : 'knowledge-a' , name : 'Workspace A docs' } ,
379+ connector : { ...connectorContext . connector , knowledgeBaseId : 'knowledge-a' } ,
380+ }
381+ mocks . resolveConnector . mockResolvedValueOnce ( sameWorkspaceContext )
382+ mocks . updateConnector . mockResolvedValueOnce ( {
383+ success : true ,
384+ connector : { ...sameWorkspaceContext . connector , sourceConfig : { space : 'ENG' } } ,
385+ } )
386+
387+ await updateKnowledgeConnector . execute ( {
388+ principal : delegatedPrincipal ,
389+ input : {
390+ connectorId : 'connector-b' ,
391+ assertedWorkspaceId : 'workspace-a' ,
392+ updates : { sourceConfig : { space : 'ENG' } } ,
393+ } ,
394+ } )
395+
396+ const orchestrationInput = mocks . updateConnector . mock . calls [ 0 ] ?. [ 0 ] as {
397+ validateSourceConfig ?: (
398+ connector : {
399+ connectorType : string
400+ credentialId : string
401+ encryptedApiKey : null
402+ } ,
403+ sourceConfig : Record < string , unknown >
404+ ) => Promise < unknown >
405+ }
406+ if ( ! orchestrationInput . validateSourceConfig ) {
407+ throw new Error ( 'Application command did not provide source-config validation' )
408+ }
409+ mocks . getCredentialActorContext . mockResolvedValueOnce ( {
410+ credential : { id : 'credential-1' , workspaceId : 'workspace-a' } ,
411+ member : null ,
412+ hasWorkspaceAccess : true ,
413+ canWriteWorkspace : true ,
414+ isAdmin : false ,
415+ } )
416+
417+ await expect (
418+ orchestrationInput . validateSourceConfig (
419+ {
420+ connectorType : 'confluence' ,
421+ credentialId : 'credential-1' ,
422+ encryptedApiKey : null ,
423+ } ,
424+ { space : 'ENG' }
425+ )
426+ ) . resolves . toEqual ( {
427+ message : 'Credential is no longer usable in this workspace. Please reconnect it.' ,
428+ errorCode : 'validation' ,
429+ } )
430+ expect ( mocks . resolveTokenIdentity ) . not . toHaveBeenCalled ( )
431+ expect ( mocks . refreshToken ) . not . toHaveBeenCalled ( )
432+ expect ( mocks . validateConnectorConfig ) . not . toHaveBeenCalled ( )
433+ } )
434+
308435 it . each ( [
309436 [
310437 'create' ,
0 commit comments