11/** @vitest -environment node */
22import type { SessionPrincipal } from '@sim/auth/principal'
33import { dbChainMockFns , queueTableRows , resetDbChainMock , schemaMock } from '@sim/testing'
4- import { eq } from 'drizzle-orm'
4+ import { eq , inArray } from 'drizzle-orm'
55import { beforeEach , describe , expect , it , vi } from 'vitest'
66
77const mocks = vi . hoisted ( ( ) => ( {
@@ -10,7 +10,8 @@ const mocks = vi.hoisted(() => ({
1010 group : vi . fn ( ) ,
1111 workspace : vi . fn ( ) ,
1212 permission : vi . fn ( ) ,
13- requireAccess : vi . fn ( ) ,
13+ scopedAvailable : vi . fn ( ) ,
14+ policy : vi . fn ( ) ,
1415} ) )
1516vi . mock ( '@/lib/billing/core/workspace-access' , ( ) => ( {
1617 getWorkspaceOwnerSubscriptionAccess : mocks . billing ,
@@ -21,16 +22,22 @@ vi.mock('@/lib/credential-groups/availability', () => ({
2122vi . mock ( '@/lib/credential-groups/credentials' , ( ) => ( {
2223 loadScopedAccountsCredentialListContext : mocks . group ,
2324} ) )
24- vi . mock ( '@/lib/credential-groups/application/organization-workspace-access' , ( ) => ( {
25- requireOrganizationAccountsWorkspaceAccess : mocks . requireAccess ,
25+ vi . mock ( '@/lib/credential-groups/scoped-availability' , ( ) => ( {
26+ isScopedCredentialGroupsAvailable : mocks . scopedAvailable ,
27+ } ) )
28+ vi . mock ( '@/lib/resource-policies/repository' , ( ) => ( {
29+ requireResourcePolicy : mocks . policy ,
2630} ) )
2731vi . mock ( '@/lib/mcp/application/context' , ( ) => ( { resolveMcpWorkspaceContext : mocks . workspace } ) )
2832vi . mock ( '@sim/platform-authz/workspace' , ( ) => ( {
2933 permissionSatisfies : ( permission : string | null ) => permission !== null ,
3034 resolveEffectiveWorkspacePermission : mocks . permission ,
3135} ) )
3236
33- import { buildOrganizationAccountAccessPolicy } from '@/lib/credential-groups/application/workspace-access-policy'
37+ import {
38+ buildOrganizationAccountAccessPolicy ,
39+ organizationAccountAccessPolicyCodec ,
40+ } from '@/lib/credential-groups/application/workspace-access-policy'
3441import { listManagedMcpConnectionsUseCase } from '@/lib/mcp/application/managed-connections'
3542
3643const principal : SessionPrincipal = { kind : 'session' , userId : 'user-1' , sessionId : 'session-1' }
@@ -53,6 +60,7 @@ describe('managed MCP connection catalog', () => {
5360 resetDbChainMock ( )
5461 mocks . billing . mockResolvedValue ( { organizationId : 'org-1' } )
5562 mocks . available . mockResolvedValue ( true )
63+ mocks . scopedAvailable . mockResolvedValue ( true )
5664 mocks . group . mockResolvedValue ( { credentialGroupId : 'group-1' } )
5765 mocks . workspace . mockResolvedValue ( {
5866 workspaceId : 'workspace-1' ,
@@ -61,11 +69,11 @@ describe('managed MCP connection catalog', () => {
6169 billedAccountUserId : 'owner-1' ,
6270 } )
6371 mocks . permission . mockResolvedValue ( 'read' )
64- mocks . requireAccess . mockResolvedValue (
65- buildOrganizationAccountAccessPolicy ( 'group-1' , [
72+ mocks . policy . mockResolvedValue ( {
73+ document : buildOrganizationAccountAccessPolicy ( 'group-1' , [
6674 { workspaceId : 'workspace-1' , access : { mode : 'all' } } ,
67- ] )
68- )
75+ ] ) ,
76+ } )
6977 } )
7078
7179 it ( 'uses organization ownership and workspace access before exposing credential operations' , async ( ) => {
@@ -78,11 +86,12 @@ describe('managed MCP connection catalog', () => {
7886 ] )
7987 const result = await listManagedMcpConnectionsUseCase . execute ( { principal, input } )
8088 expect ( mocks . group ) . toHaveBeenCalledWith ( { kind : 'organization' , organizationId : 'org-1' } )
81- expect ( mocks . requireAccess ) . toHaveBeenCalledWith (
89+ expect ( mocks . policy ) . toHaveBeenCalledWith (
8290 expect . objectContaining ( {
8391 organizationId : 'org-1' ,
84- credentialGroupId : 'group-1' ,
85- workspaceId : 'workspace-1' ,
92+ resourceType : 'credential_group' ,
93+ resourceId : 'group-1' ,
94+ codec : organizationAccountAccessPolicyCodec ,
8695 } )
8796 )
8897 expect ( eq ) . toHaveBeenCalledWith ( schemaMock . credential . organizationId , 'org-1' )
@@ -95,14 +104,93 @@ describe('managed MCP connection catalog', () => {
95104 } )
96105 } )
97106
98- it ( 'denies revoked workspace access before reading credentials' , async ( ) => {
99- mocks . requireAccess . mockRejectedValue ( new Error ( 'Workspace access revoked' ) )
107+ it ( 'returns an empty catalog when organization connected accounts are not configured' , async ( ) => {
108+ mocks . group . mockResolvedValue ( null )
109+ await expect ( listManagedMcpConnectionsUseCase . execute ( { principal, input } ) ) . resolves . toEqual ( {
110+ servers : [ ] ,
111+ tools : [ ] ,
112+ } )
113+ expect ( mocks . policy ) . not . toHaveBeenCalled ( )
114+ expect ( dbChainMockFns . from ) . not . toHaveBeenCalled ( )
115+ } )
116+
117+ it . each ( [ 'workspace' , 'organization' ] ) (
118+ 'returns an empty catalog when %s availability is disabled' ,
119+ async ( scope ) => {
120+ const available = scope === 'workspace' ? mocks . available : mocks . scopedAvailable
121+ available . mockResolvedValue ( false )
122+ await expect ( listManagedMcpConnectionsUseCase . execute ( { principal, input } ) ) . resolves . toEqual (
123+ {
124+ servers : [ ] ,
125+ tools : [ ] ,
126+ }
127+ )
128+ expect ( mocks . policy ) . not . toHaveBeenCalled ( )
129+ expect ( dbChainMockFns . from ) . not . toHaveBeenCalled ( )
130+ }
131+ )
132+
133+ it . each ( [
134+ { name : 'no grants' , grants : [ ] } ,
135+ {
136+ name : 'another workspace only' ,
137+ grants : [ { workspaceId : 'other-workspace' , access : { mode : 'all' as const } } ] ,
138+ } ,
139+ {
140+ name : 'OAuth only' ,
141+ grants : [
142+ {
143+ workspaceId : input . workspaceId ,
144+ access : { mode : 'selected' as const , credentialTypes : [ 'oauth:gmail' as const ] } ,
145+ } ,
146+ ] ,
147+ } ,
148+ ] ) ( 'returns an empty catalog without an MCP workspace grant: $name' , async ( { grants } ) => {
149+ mocks . policy . mockResolvedValue ( {
150+ document : buildOrganizationAccountAccessPolicy ( 'group-1' , grants ) ,
151+ } )
152+ await expect ( listManagedMcpConnectionsUseCase . execute ( { principal, input } ) ) . resolves . toEqual ( {
153+ servers : [ ] ,
154+ tools : [ ] ,
155+ } )
156+ expect ( dbChainMockFns . from ) . not . toHaveBeenCalled ( )
157+ } )
158+
159+ it ( 'only queries connectors granted to this workspace' , async ( ) => {
160+ mocks . policy . mockResolvedValue ( {
161+ document : buildOrganizationAccountAccessPolicy ( 'group-1' , [
162+ {
163+ workspaceId : input . workspaceId ,
164+ access : { mode : 'selected' , credentialTypes : [ 'mcp:fireflies' ] } ,
165+ } ,
166+ ] ) ,
167+ } )
168+ await listManagedMcpConnectionsUseCase . execute ( { principal, input } )
169+ expect ( inArray ) . toHaveBeenCalledWith ( schemaMock . mcpServers . managedConnectorId , [ 'fireflies' ] )
170+ } )
171+
172+ it ( 'rejects callers without workspace access before checking catalog availability' , async ( ) => {
173+ mocks . permission . mockResolvedValue ( null )
100174 await expect ( listManagedMcpConnectionsUseCase . execute ( { principal, input } ) ) . rejects . toThrow (
101- 'revoked '
175+ 'Insufficient workspace permissions '
102176 )
177+ expect ( mocks . billing ) . not . toHaveBeenCalled ( )
178+ expect ( mocks . policy ) . not . toHaveBeenCalled ( )
103179 expect ( dbChainMockFns . from ) . not . toHaveBeenCalled ( )
104180 } )
105181
182+ it . each ( [ 'scopedAvailable' , 'policy' ] as const ) (
183+ 'propagates %s failures before reading credentials' ,
184+ async ( dependency ) => {
185+ const error = new Error ( 'Database unavailable' )
186+ mocks [ dependency ] . mockRejectedValue ( error )
187+ await expect ( listManagedMcpConnectionsUseCase . execute ( { principal, input } ) ) . rejects . toBe (
188+ error
189+ )
190+ expect ( dbChainMockFns . from ) . not . toHaveBeenCalled ( )
191+ }
192+ )
193+
106194 it . each ( [
107195 [ Array . from ( { length : 501 } , ( ) => metadata ) , 'connection limit' ] ,
108196 [ [ { ...metadata , toolSnapshotBytes : 6 * 1024 * 1024 } ] , 'metadata limit' ] ,
0 commit comments