@dxheroes/local-mcp-backend 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/CHANGELOG.md +20 -0
  3. package/dist/__tests__/integration/auth-flow.test.js +181 -0
  4. package/dist/__tests__/integration/auth-flow.test.js.map +1 -0
  5. package/dist/__tests__/integration/multi-tenant.test.js +323 -0
  6. package/dist/__tests__/integration/multi-tenant.test.js.map +1 -0
  7. package/dist/__tests__/integration/proxy-auth.test.js +276 -0
  8. package/dist/__tests__/integration/proxy-auth.test.js.map +1 -0
  9. package/dist/__tests__/integration/sharing-credentials.test.js +280 -0
  10. package/dist/__tests__/integration/sharing-credentials.test.js.map +1 -0
  11. package/dist/__tests__/unit/auth.guard.test.js +74 -0
  12. package/dist/__tests__/unit/auth.guard.test.js.map +1 -0
  13. package/dist/__tests__/unit/auth.service.test.js +145 -0
  14. package/dist/__tests__/unit/auth.service.test.js.map +1 -0
  15. package/dist/__tests__/unit/mcp.service.test.js +213 -0
  16. package/dist/__tests__/unit/mcp.service.test.js.map +1 -0
  17. package/dist/__tests__/unit/sharing.service.test.js +153 -0
  18. package/dist/__tests__/unit/sharing.service.test.js.map +1 -0
  19. package/dist/app.module.js +16 -3
  20. package/dist/app.module.js.map +1 -1
  21. package/dist/main.js +17 -1
  22. package/dist/main.js.map +1 -1
  23. package/dist/modules/auth/auth.config.js +97 -0
  24. package/dist/modules/auth/auth.config.js.map +1 -0
  25. package/dist/modules/auth/auth.guard.js +70 -0
  26. package/dist/modules/auth/auth.guard.js.map +1 -0
  27. package/dist/modules/auth/auth.module.js +30 -0
  28. package/dist/modules/auth/auth.module.js.map +1 -0
  29. package/dist/modules/auth/auth.service.js +118 -0
  30. package/dist/modules/auth/auth.service.js.map +1 -0
  31. package/dist/modules/auth/decorators/active-org-id.decorator.js +11 -0
  32. package/dist/modules/auth/decorators/active-org-id.decorator.js.map +1 -0
  33. package/dist/modules/auth/decorators/current-user.decorator.js +11 -0
  34. package/dist/modules/auth/decorators/current-user.decorator.js.map +1 -0
  35. package/dist/modules/auth/decorators/public.decorator.js +9 -0
  36. package/dist/modules/auth/decorators/public.decorator.js.map +1 -0
  37. package/dist/modules/auth/decorators/skip-org-check.decorator.js +10 -0
  38. package/dist/modules/auth/decorators/skip-org-check.decorator.js.map +1 -0
  39. package/dist/modules/database/prisma.service.js +11 -9
  40. package/dist/modules/database/prisma.service.js.map +1 -1
  41. package/dist/modules/debug/debug.controller.js +2 -0
  42. package/dist/modules/debug/debug.controller.js.map +1 -1
  43. package/dist/modules/health/health.controller.js +17 -0
  44. package/dist/modules/health/health.controller.js.map +1 -1
  45. package/dist/modules/mcp/mcp-presets.js +96 -0
  46. package/dist/modules/mcp/mcp-presets.js.map +1 -0
  47. package/dist/modules/mcp/mcp-seed.service.js +8 -167
  48. package/dist/modules/mcp/mcp-seed.service.js.map +1 -1
  49. package/dist/modules/mcp/mcp.controller.js +89 -27
  50. package/dist/modules/mcp/mcp.controller.js.map +1 -1
  51. package/dist/modules/mcp/mcp.service.js +124 -46
  52. package/dist/modules/mcp/mcp.service.js.map +1 -1
  53. package/dist/modules/oauth/oauth.controller.js +2 -0
  54. package/dist/modules/oauth/oauth.controller.js.map +1 -1
  55. package/dist/modules/profiles/profiles.controller.js +107 -51
  56. package/dist/modules/profiles/profiles.controller.js.map +1 -1
  57. package/dist/modules/profiles/profiles.service.js +172 -49
  58. package/dist/modules/profiles/profiles.service.js.map +1 -1
  59. package/dist/modules/proxy/proxy.controller.js +109 -92
  60. package/dist/modules/proxy/proxy.controller.js.map +1 -1
  61. package/dist/modules/proxy/proxy.module.js +3 -1
  62. package/dist/modules/proxy/proxy.module.js.map +1 -1
  63. package/dist/modules/proxy/proxy.service.js +201 -37
  64. package/dist/modules/proxy/proxy.service.js.map +1 -1
  65. package/dist/modules/settings/settings.controller.js +2 -0
  66. package/dist/modules/settings/settings.controller.js.map +1 -1
  67. package/dist/modules/settings/settings.service.js +1 -1
  68. package/dist/modules/settings/settings.service.js.map +1 -1
  69. package/dist/modules/sharing/sharing.controller.js +83 -0
  70. package/dist/modules/sharing/sharing.controller.js.map +1 -0
  71. package/dist/modules/sharing/sharing.module.js +30 -0
  72. package/dist/modules/sharing/sharing.module.js.map +1 -0
  73. package/dist/modules/sharing/sharing.service.js +167 -0
  74. package/dist/modules/sharing/sharing.service.js.map +1 -0
  75. package/docker-entrypoint.sh +2 -2
  76. package/package.json +11 -7
  77. package/src/__tests__/integration/auth-flow.test.ts +216 -0
  78. package/src/__tests__/integration/multi-tenant.test.ts +415 -0
  79. package/src/__tests__/integration/proxy-auth.test.ts +349 -0
  80. package/src/__tests__/integration/sharing-credentials.test.ts +293 -0
  81. package/src/__tests__/unit/auth.guard.test.ts +66 -0
  82. package/src/__tests__/unit/auth.service.test.ts +162 -0
  83. package/src/__tests__/unit/mcp.service.test.ts +219 -0
  84. package/src/__tests__/unit/sharing.service.test.ts +147 -0
  85. package/src/app.module.ts +17 -4
  86. package/src/main.ts +21 -1
  87. package/src/modules/auth/auth.config.ts +114 -0
  88. package/src/modules/auth/auth.guard.ts +88 -0
  89. package/src/modules/auth/auth.module.ts +16 -0
  90. package/src/modules/auth/auth.service.ts +131 -0
  91. package/src/modules/auth/decorators/active-org-id.decorator.ts +13 -0
  92. package/src/modules/auth/decorators/current-user.decorator.ts +13 -0
  93. package/src/modules/auth/decorators/public.decorator.ts +10 -0
  94. package/src/modules/auth/decorators/skip-org-check.decorator.ts +11 -0
  95. package/src/modules/database/prisma.service.ts +9 -9
  96. package/src/modules/debug/debug.controller.ts +2 -0
  97. package/src/modules/health/health.controller.ts +14 -0
  98. package/src/modules/mcp/mcp-presets.ts +87 -0
  99. package/src/modules/mcp/mcp-seed.service.ts +8 -174
  100. package/src/modules/mcp/mcp.controller.ts +70 -19
  101. package/src/modules/mcp/mcp.service.ts +126 -52
  102. package/src/modules/oauth/oauth.controller.ts +2 -0
  103. package/src/modules/profiles/profiles.controller.ts +76 -23
  104. package/src/modules/profiles/profiles.service.ts +187 -75
  105. package/src/modules/proxy/proxy.controller.ts +86 -81
  106. package/src/modules/proxy/proxy.module.ts +2 -1
  107. package/src/modules/proxy/proxy.service.ts +236 -27
  108. package/src/modules/settings/settings.controller.ts +2 -0
  109. package/src/modules/settings/settings.service.ts +1 -1
  110. package/src/modules/sharing/sharing.controller.ts +46 -0
  111. package/src/modules/sharing/sharing.module.ts +16 -0
  112. package/src/modules/sharing/sharing.service.ts +173 -0
  113. package/vitest.config.ts +20 -0
@@ -1,9 +1,9 @@
1
1
 
2
- > @dxheroes/local-mcp-backend@0.8.0 build /home/runner/work/local-mcp-gateway/local-mcp-gateway/apps/backend
2
+ > @dxheroes/local-mcp-backend@0.9.0 build /home/runner/work/local-mcp-gateway/local-mcp-gateway/apps/backend
3
3
  > nest build
4
4
 
5
5
  -  TSC  Initializing type checker...
6
6
  ✔  TSC  Initializing type checker...
7
7
  >  TSC  Found 0 issues.
8
8
  >  SWC  Running...
9
- Successfully compiled: 36 files with swc (76.61ms)
9
+ Successfully compiled: 56 files with swc (92.5ms)
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.0](https://github.com/DXHeroes/local-mcp-gateway/compare/backend-v0.8.0...backend-v0.9.0) (2026-03-10)
4
+
5
+
6
+ ### Features
7
+
8
+ * add mandatory org context, Better Auth, and MCP preset gallery ([18ddf94](https://github.com/DXHeroes/local-mcp-gateway/commit/18ddf94d5269a9b77e9e251b96b6fc56bd496ad1))
9
+
10
+
11
+ ### Dependencies
12
+
13
+ * The following workspace dependencies were updated
14
+ * dependencies
15
+ * @dxheroes/local-mcp-core bumped to 0.7.0
16
+ * @dxheroes/local-mcp-database bumped to 0.5.0
17
+ * @dxheroes/mcp-gemini-deep-research bumped to 0.5.3
18
+ * @dxheroes/mcp-merk bumped to 0.3.3
19
+ * @dxheroes/mcp-toggl bumped to 0.3.3
20
+ * devDependencies
21
+ * @dxheroes/local-mcp-config bumped to 0.4.8
22
+
3
23
  ## [0.8.0](https://github.com/DXHeroes/local-mcp-gateway/compare/backend-v0.7.1...backend-v0.8.0) (2026-03-10)
4
24
 
5
25
 
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Integration Tests: Auth guard flow
3
+ *
4
+ * Verifies the AuthGuard + AuthService interaction for:
5
+ * - Unauthenticated requests are rejected
6
+ * - Authenticated requests attach user and session
7
+ * - @Public() routes bypass auth
8
+ * - Session data attached to request
9
+ */ import { UnauthorizedException } from "@nestjs/common";
10
+ import { beforeEach, describe, expect, it, vi } from "vitest";
11
+ import { AuthGuard } from "../../modules/auth/auth.guard.js";
12
+ // ────────────────────────────────────────────────
13
+ // Helper: create mock ExecutionContext from Express-like request
14
+ // ────────────────────────────────────────────────
15
+ function createMockContext(headers = {}, overrides = {}) {
16
+ const request = {
17
+ headers,
18
+ user: undefined,
19
+ sessionData: undefined,
20
+ ...overrides
21
+ };
22
+ const ctx = {
23
+ switchToHttp: ()=>({
24
+ getRequest: ()=>request
25
+ }),
26
+ getHandler: ()=>({}),
27
+ getClass: ()=>({})
28
+ };
29
+ return {
30
+ ctx,
31
+ getRequest: ()=>request
32
+ };
33
+ }
34
+ // ────────────────────────────────────────────────
35
+ // Helper: create an AuthService mock
36
+ // ────────────────────────────────────────────────
37
+ function createAuthServiceMock() {
38
+ return {
39
+ getSession: vi.fn().mockResolvedValue(null),
40
+ validateMcpToken: vi.fn().mockResolvedValue(null),
41
+ getUserOrganizations: vi.fn().mockResolvedValue([])
42
+ };
43
+ }
44
+ describe('Auth guard flow', ()=>{
45
+ // ────────────────────────────────────────────────
46
+ // Auth always enabled
47
+ // ────────────────────────────────────────────────
48
+ describe('auth validation', ()=>{
49
+ let guard;
50
+ let authService;
51
+ let reflector;
52
+ beforeEach(()=>{
53
+ authService = createAuthServiceMock();
54
+ reflector = {
55
+ getAllAndOverride: vi.fn().mockReturnValue(false)
56
+ };
57
+ guard = new AuthGuard(authService, reflector);
58
+ });
59
+ it('rejects unauthenticated requests with UnauthorizedException', async ()=>{
60
+ authService.getSession.mockResolvedValue(null);
61
+ const { ctx } = createMockContext();
62
+ await expect(guard.canActivate(ctx)).rejects.toThrow(UnauthorizedException);
63
+ });
64
+ it('authenticated request attaches user and session to req', async ()=>{
65
+ const mockUser = {
66
+ id: 'user-1',
67
+ name: 'Test User',
68
+ email: 'test@example.com',
69
+ image: null
70
+ };
71
+ const mockSession = {
72
+ id: 'sess-1',
73
+ userId: 'user-1',
74
+ activeOrganizationId: 'org-1'
75
+ };
76
+ authService.getSession.mockResolvedValue({
77
+ user: mockUser,
78
+ session: mockSession
79
+ });
80
+ const { ctx, getRequest } = createMockContext({
81
+ cookie: 'better-auth.session_token=valid-token'
82
+ });
83
+ const result = await guard.canActivate(ctx);
84
+ expect(result).toBe(true);
85
+ expect(getRequest().user).toEqual(mockUser);
86
+ expect(getRequest().sessionData).toEqual(mockSession);
87
+ });
88
+ it('converts Express headers to Web API Headers for Better Auth', async ()=>{
89
+ authService.getSession.mockResolvedValue(null);
90
+ const { ctx } = createMockContext({
91
+ cookie: 'better-auth.session_token=abc123',
92
+ 'user-agent': 'test-agent'
93
+ });
94
+ try {
95
+ await guard.canActivate(ctx);
96
+ } catch {
97
+ // Expected UnauthorizedException
98
+ }
99
+ // Verify getSession was called with a Headers object
100
+ expect(authService.getSession).toHaveBeenCalledTimes(1);
101
+ const passedHeaders = authService.getSession.mock.calls[0][0];
102
+ expect(passedHeaders).toBeInstanceOf(Headers);
103
+ expect(passedHeaders.get('cookie')).toBe('better-auth.session_token=abc123');
104
+ });
105
+ it('session with activeOrganizationId is preserved on request', async ()=>{
106
+ const mockUser = {
107
+ id: 'user-1',
108
+ name: 'Org User',
109
+ email: 'org@example.com'
110
+ };
111
+ const mockSession = {
112
+ id: 'sess-2',
113
+ userId: 'user-1',
114
+ activeOrganizationId: 'org-abc'
115
+ };
116
+ authService.getSession.mockResolvedValue({
117
+ user: mockUser,
118
+ session: mockSession
119
+ });
120
+ const { ctx, getRequest } = createMockContext({
121
+ cookie: 'session=valid'
122
+ });
123
+ await guard.canActivate(ctx);
124
+ const sessionData = getRequest().sessionData;
125
+ expect(sessionData.activeOrganizationId).toBe('org-abc');
126
+ });
127
+ });
128
+ // ────────────────────────────────────────────────
129
+ // @Public() decorator
130
+ // ────────────────────────────────────────────────
131
+ describe('@Public() routes', ()=>{
132
+ let guard;
133
+ let authService;
134
+ let reflector;
135
+ beforeEach(()=>{
136
+ authService = createAuthServiceMock();
137
+ reflector = {
138
+ getAllAndOverride: vi.fn().mockReturnValue(true)
139
+ }; // @Public() returns true
140
+ guard = new AuthGuard(authService, reflector);
141
+ });
142
+ it('@Public() route bypasses auth entirely', async ()=>{
143
+ const { ctx } = createMockContext();
144
+ const result = await guard.canActivate(ctx);
145
+ expect(result).toBe(true);
146
+ expect(authService.getSession).not.toHaveBeenCalled();
147
+ });
148
+ it('@Public() route does not attach user to request', async ()=>{
149
+ const { ctx, getRequest } = createMockContext();
150
+ await guard.canActivate(ctx);
151
+ // User is not set by guard on public routes
152
+ expect(getRequest().user).toBeUndefined();
153
+ });
154
+ });
155
+ // ────────────────────────────────────────────────
156
+ // AuthService.validateMcpToken
157
+ // ────────────────────────────────────────────────
158
+ describe('AuthService.validateMcpToken (mock)', ()=>{
159
+ let authService;
160
+ beforeEach(()=>{
161
+ authService = createAuthServiceMock();
162
+ });
163
+ it('returns user when token is valid', async ()=>{
164
+ const mockUser = {
165
+ id: 'user-1',
166
+ name: 'Token User',
167
+ email: 'token@example.com'
168
+ };
169
+ authService.validateMcpToken.mockResolvedValue(mockUser);
170
+ const result = await authService.validateMcpToken('valid-bearer-token');
171
+ expect(result).toEqual(mockUser);
172
+ });
173
+ it('returns null for expired or invalid token', async ()=>{
174
+ authService.validateMcpToken.mockResolvedValue(null);
175
+ const result = await authService.validateMcpToken('expired-token');
176
+ expect(result).toBeNull();
177
+ });
178
+ });
179
+ });
180
+
181
+ //# sourceMappingURL=auth-flow.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/__tests__/integration/auth-flow.test.ts"],"sourcesContent":["/**\n * Integration Tests: Auth guard flow\n *\n * Verifies the AuthGuard + AuthService interaction for:\n * - Unauthenticated requests are rejected\n * - Authenticated requests attach user and session\n * - @Public() routes bypass auth\n * - Session data attached to request\n */\n\nimport { ExecutionContext, UnauthorizedException } from '@nestjs/common';\nimport { Reflector } from '@nestjs/core';\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\nimport { AuthGuard } from '../../modules/auth/auth.guard.js';\nimport { AuthService, type AuthUser } from '../../modules/auth/auth.service.js';\n\n// ────────────────────────────────────────────────\n// Helper: create mock ExecutionContext from Express-like request\n// ────────────────────────────────────────────────\n\nfunction createMockContext(\n headers: Record<string, string> = {},\n overrides: Record<string, unknown> = {}\n): { ctx: ExecutionContext; getRequest: () => Record<string, unknown> } {\n const request: Record<string, unknown> = {\n headers,\n user: undefined,\n sessionData: undefined,\n ...overrides,\n };\n\n const ctx = {\n switchToHttp: () => ({\n getRequest: () => request,\n }),\n getHandler: () => ({}),\n getClass: () => ({}),\n } as unknown as ExecutionContext;\n\n return { ctx, getRequest: () => request };\n}\n\n// ────────────────────────────────────────────────\n// Helper: create an AuthService mock\n// ────────────────────────────────────────────────\n\nfunction createAuthServiceMock() {\n return {\n getSession: vi.fn().mockResolvedValue(null),\n validateMcpToken: vi.fn().mockResolvedValue(null),\n getUserOrganizations: vi.fn().mockResolvedValue([]),\n };\n}\n\ndescribe('Auth guard flow', () => {\n // ────────────────────────────────────────────────\n // Auth always enabled\n // ────────────────────────────────────────────────\n\n describe('auth validation', () => {\n let guard: AuthGuard;\n let authService: ReturnType<typeof createAuthServiceMock>;\n let reflector: { getAllAndOverride: ReturnType<typeof vi.fn> };\n\n beforeEach(() => {\n authService = createAuthServiceMock();\n reflector = { getAllAndOverride: vi.fn().mockReturnValue(false) };\n guard = new AuthGuard(\n authService as unknown as AuthService,\n reflector as unknown as Reflector\n );\n });\n\n it('rejects unauthenticated requests with UnauthorizedException', async () => {\n authService.getSession.mockResolvedValue(null);\n const { ctx } = createMockContext();\n\n await expect(guard.canActivate(ctx)).rejects.toThrow(UnauthorizedException);\n });\n\n it('authenticated request attaches user and session to req', async () => {\n const mockUser: AuthUser = {\n id: 'user-1',\n name: 'Test User',\n email: 'test@example.com',\n image: null,\n };\n const mockSession = {\n id: 'sess-1',\n userId: 'user-1',\n activeOrganizationId: 'org-1',\n };\n authService.getSession.mockResolvedValue({ user: mockUser, session: mockSession });\n\n const { ctx, getRequest } = createMockContext({\n cookie: 'better-auth.session_token=valid-token',\n });\n\n const result = await guard.canActivate(ctx);\n\n expect(result).toBe(true);\n expect(getRequest().user).toEqual(mockUser);\n expect(getRequest().sessionData).toEqual(mockSession);\n });\n\n it('converts Express headers to Web API Headers for Better Auth', async () => {\n authService.getSession.mockResolvedValue(null);\n\n const { ctx } = createMockContext({\n cookie: 'better-auth.session_token=abc123',\n 'user-agent': 'test-agent',\n });\n\n try {\n await guard.canActivate(ctx);\n } catch {\n // Expected UnauthorizedException\n }\n\n // Verify getSession was called with a Headers object\n expect(authService.getSession).toHaveBeenCalledTimes(1);\n const passedHeaders = authService.getSession.mock.calls[0][0];\n expect(passedHeaders).toBeInstanceOf(Headers);\n expect(passedHeaders.get('cookie')).toBe('better-auth.session_token=abc123');\n });\n\n it('session with activeOrganizationId is preserved on request', async () => {\n const mockUser: AuthUser = {\n id: 'user-1',\n name: 'Org User',\n email: 'org@example.com',\n };\n const mockSession = {\n id: 'sess-2',\n userId: 'user-1',\n activeOrganizationId: 'org-abc',\n };\n authService.getSession.mockResolvedValue({ user: mockUser, session: mockSession });\n\n const { ctx, getRequest } = createMockContext({ cookie: 'session=valid' });\n\n await guard.canActivate(ctx);\n\n const sessionData = getRequest().sessionData as typeof mockSession;\n expect(sessionData.activeOrganizationId).toBe('org-abc');\n });\n });\n\n // ────────────────────────────────────────────────\n // @Public() decorator\n // ────────────────────────────────────────────────\n\n describe('@Public() routes', () => {\n let guard: AuthGuard;\n let authService: ReturnType<typeof createAuthServiceMock>;\n let reflector: { getAllAndOverride: ReturnType<typeof vi.fn> };\n\n beforeEach(() => {\n authService = createAuthServiceMock();\n reflector = { getAllAndOverride: vi.fn().mockReturnValue(true) }; // @Public() returns true\n guard = new AuthGuard(\n authService as unknown as AuthService,\n reflector as unknown as Reflector\n );\n });\n\n it('@Public() route bypasses auth entirely', async () => {\n const { ctx } = createMockContext();\n\n const result = await guard.canActivate(ctx);\n\n expect(result).toBe(true);\n expect(authService.getSession).not.toHaveBeenCalled();\n });\n\n it('@Public() route does not attach user to request', async () => {\n const { ctx, getRequest } = createMockContext();\n\n await guard.canActivate(ctx);\n\n // User is not set by guard on public routes\n expect(getRequest().user).toBeUndefined();\n });\n });\n\n // ────────────────────────────────────────────────\n // AuthService.validateMcpToken\n // ────────────────────────────────────────────────\n\n describe('AuthService.validateMcpToken (mock)', () => {\n let authService: ReturnType<typeof createAuthServiceMock>;\n\n beforeEach(() => {\n authService = createAuthServiceMock();\n });\n\n it('returns user when token is valid', async () => {\n const mockUser: AuthUser = {\n id: 'user-1',\n name: 'Token User',\n email: 'token@example.com',\n };\n authService.validateMcpToken.mockResolvedValue(mockUser);\n\n const result = await authService.validateMcpToken('valid-bearer-token');\n expect(result).toEqual(mockUser);\n });\n\n it('returns null for expired or invalid token', async () => {\n authService.validateMcpToken.mockResolvedValue(null);\n\n const result = await authService.validateMcpToken('expired-token');\n expect(result).toBeNull();\n });\n });\n});\n"],"names":["UnauthorizedException","beforeEach","describe","expect","it","vi","AuthGuard","createMockContext","headers","overrides","request","user","undefined","sessionData","ctx","switchToHttp","getRequest","getHandler","getClass","createAuthServiceMock","getSession","fn","mockResolvedValue","validateMcpToken","getUserOrganizations","guard","authService","reflector","getAllAndOverride","mockReturnValue","canActivate","rejects","toThrow","mockUser","id","name","email","image","mockSession","userId","activeOrganizationId","session","cookie","result","toBe","toEqual","toHaveBeenCalledTimes","passedHeaders","mock","calls","toBeInstanceOf","Headers","get","not","toHaveBeenCalled","toBeUndefined","toBeNull"],"mappings":"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"}
@@ -0,0 +1,323 @@
1
+ /**
2
+ * Integration Tests: Multi-tenant data isolation
3
+ *
4
+ * Verifies that ProfilesService and McpService correctly isolate data
5
+ * between organizations while allowing access to system records.
6
+ */ import { ForbiddenException } from "@nestjs/common";
7
+ import { beforeEach, describe, expect, it, vi } from "vitest";
8
+ /** Sentinel value for unauthenticated MCP access */ const UNAUTHENTICATED_ID = '__unauthenticated__';
9
+ import { McpService } from "../../modules/mcp/mcp.service.js";
10
+ import { McpRegistry } from "../../modules/mcp/mcp-registry.js";
11
+ import { ProfilesService } from "../../modules/profiles/profiles.service.js";
12
+ const PROFILES = [];
13
+ const SERVERS = [];
14
+ // ────────────────────────────────────────────────
15
+ // Helper: build a mock PrismaService over in-memory data
16
+ // ────────────────────────────────────────────────
17
+ function buildMockPrisma() {
18
+ return {
19
+ profile: {
20
+ findMany: vi.fn().mockImplementation(({ where } = {})=>{
21
+ let result = [
22
+ ...PROFILES
23
+ ];
24
+ const whereObj = where;
25
+ if (whereObj?.OR) {
26
+ const orConds = whereObj.OR;
27
+ result = result.filter((p)=>orConds.some((cond)=>{
28
+ if ('organizationId' in cond) {
29
+ return cond.organizationId === null ? p.organizationId === null : p.organizationId === cond.organizationId;
30
+ }
31
+ if ('id' in cond && cond.id?.in) {
32
+ return cond.id.in.includes(p.id);
33
+ }
34
+ return false;
35
+ }));
36
+ }
37
+ return Promise.resolve(result);
38
+ }),
39
+ findUnique: vi.fn().mockImplementation(({ where })=>{
40
+ const found = PROFILES.find((p)=>where.id && p.id === where.id || where.name && p.name === where.name);
41
+ return Promise.resolve(found ?? null);
42
+ }),
43
+ create: vi.fn().mockImplementation(({ data })=>{
44
+ const profile = {
45
+ id: `profile-${Date.now()}`,
46
+ name: data.name,
47
+ description: data.description ?? null,
48
+ userId: data.userId ?? null,
49
+ organizationId: data.organizationId ?? null,
50
+ mcpServers: []
51
+ };
52
+ PROFILES.push(profile);
53
+ return Promise.resolve(profile);
54
+ }),
55
+ delete: vi.fn().mockResolvedValue(undefined),
56
+ update: vi.fn().mockResolvedValue({})
57
+ },
58
+ mcpServer: {
59
+ findMany: vi.fn().mockImplementation(({ where } = {})=>{
60
+ let result = [
61
+ ...SERVERS
62
+ ];
63
+ const whereObj = where;
64
+ if (whereObj?.OR) {
65
+ const orConds = whereObj.OR;
66
+ result = result.filter((s)=>orConds.some((cond)=>{
67
+ if ('organizationId' in cond) {
68
+ return cond.organizationId === null ? s.organizationId === null : s.organizationId === cond.organizationId;
69
+ }
70
+ if ('id' in cond && cond.id?.in) {
71
+ return cond.id.in.includes(s.id);
72
+ }
73
+ return false;
74
+ }));
75
+ }
76
+ return Promise.resolve(result);
77
+ }),
78
+ findUnique: vi.fn().mockImplementation(({ where })=>{
79
+ const found = SERVERS.find((s)=>s.id === where.id);
80
+ return Promise.resolve(found ?? null);
81
+ }),
82
+ create: vi.fn().mockImplementation(({ data })=>{
83
+ const server = {
84
+ id: `server-${Date.now()}`,
85
+ name: data.name,
86
+ type: data.type,
87
+ config: data.config,
88
+ apiKeyConfig: data.apiKeyConfig ?? null,
89
+ oauthConfig: data.oauthConfig ?? null,
90
+ userId: data.userId ?? null,
91
+ organizationId: data.organizationId ?? null,
92
+ profiles: [],
93
+ oauthToken: null,
94
+ toolsCache: []
95
+ };
96
+ SERVERS.push(server);
97
+ return Promise.resolve(server);
98
+ }),
99
+ delete: vi.fn().mockResolvedValue(undefined),
100
+ update: vi.fn().mockResolvedValue({})
101
+ },
102
+ member: {
103
+ findMany: vi.fn().mockResolvedValue([])
104
+ },
105
+ mcpServerToolsCache: {
106
+ findMany: vi.fn().mockResolvedValue([])
107
+ },
108
+ gatewaySetting: {
109
+ upsert: vi.fn().mockResolvedValue({})
110
+ }
111
+ };
112
+ }
113
+ // ────────────────────────────────────────────────
114
+ // Tests: ProfilesService data isolation
115
+ // ────────────────────────────────────────────────
116
+ describe('Multi-tenant data isolation', ()=>{
117
+ describe('ProfilesService', ()=>{
118
+ let profilesService;
119
+ let prisma;
120
+ let proxyService;
121
+ beforeEach(()=>{
122
+ // Reset in-memory data
123
+ PROFILES.length = 0;
124
+ PROFILES.push({
125
+ id: 'sys-profile',
126
+ name: 'default',
127
+ description: 'System default profile',
128
+ userId: null,
129
+ organizationId: null,
130
+ mcpServers: []
131
+ }, {
132
+ id: 'org-a-profile',
133
+ name: 'org-a-dev',
134
+ description: 'Org A dev profile',
135
+ userId: 'user-a',
136
+ organizationId: 'org-a',
137
+ mcpServers: []
138
+ }, {
139
+ id: 'org-b-profile',
140
+ name: 'org-b-prod',
141
+ description: 'Org B prod profile',
142
+ userId: 'user-b',
143
+ organizationId: 'org-b',
144
+ mcpServers: []
145
+ });
146
+ prisma = buildMockPrisma();
147
+ proxyService = {
148
+ getToolsForServer: vi.fn().mockResolvedValue([])
149
+ };
150
+ profilesService = new ProfilesService(prisma, proxyService);
151
+ });
152
+ it('Org A sees org A profiles + system profiles, not Org B profiles', async ()=>{
153
+ const result = await profilesService.findAll('user-a', 'org-a');
154
+ const ids = result.map((p)=>p.id);
155
+ expect(ids).toContain('sys-profile');
156
+ expect(ids).toContain('org-a-profile');
157
+ expect(ids).not.toContain('org-b-profile');
158
+ });
159
+ it('Org B sees org B profiles + system profiles, not Org A profiles', async ()=>{
160
+ const result = await profilesService.findAll('user-b', 'org-b');
161
+ const ids = result.map((p)=>p.id);
162
+ expect(ids).toContain('sys-profile');
163
+ expect(ids).toContain('org-b-profile');
164
+ expect(ids).not.toContain('org-a-profile');
165
+ });
166
+ it('system profiles (organizationId=null) are visible to all orgs', async ()=>{
167
+ const resultA = await profilesService.findAll('user-a', 'org-a');
168
+ const resultB = await profilesService.findAll('user-b', 'org-b');
169
+ expect(resultA.some((p)=>p.id === 'sys-profile')).toBe(true);
170
+ expect(resultB.some((p)=>p.id === 'sys-profile')).toBe(true);
171
+ });
172
+ it('creating a profile sets userId and organizationId', async ()=>{
173
+ const created = await profilesService.create({
174
+ name: 'a-new'
175
+ }, 'user-a', 'org-a');
176
+ expect(created.userId).toBe('user-a');
177
+ expect(created.organizationId).toBe('org-a');
178
+ });
179
+ it('Org A cannot access Org B profile by ID', async ()=>{
180
+ await expect(profilesService.findById('org-b-profile', 'user-a', 'org-a')).rejects.toThrow(ForbiddenException);
181
+ });
182
+ it('Org A can access system profile by ID', async ()=>{
183
+ const profile = await profilesService.findById('sys-profile', 'user-a', 'org-a');
184
+ expect(profile).toBeDefined();
185
+ expect(profile.id).toBe('sys-profile');
186
+ });
187
+ it('unauthenticated user sees all profiles', async ()=>{
188
+ const result = await profilesService.findAll(UNAUTHENTICATED_ID);
189
+ // Unauthenticated findAll queries without WHERE filter
190
+ expect(prisma.profile.findMany).toHaveBeenCalledWith(expect.objectContaining({
191
+ orderBy: {
192
+ name: 'asc'
193
+ }
194
+ }));
195
+ // The mock returns all profiles for filterless queries
196
+ expect(result.length).toBeGreaterThanOrEqual(1);
197
+ });
198
+ });
199
+ // ────────────────────────────────────────────────
200
+ // Tests: McpService data isolation
201
+ // ────────────────────────────────────────────────
202
+ describe('McpService', ()=>{
203
+ let mcpService;
204
+ let prisma;
205
+ let registry;
206
+ let debugService;
207
+ beforeEach(()=>{
208
+ // Reset in-memory data
209
+ SERVERS.length = 0;
210
+ SERVERS.push({
211
+ id: 'sys-server',
212
+ name: 'System Builtin',
213
+ type: 'builtin',
214
+ config: '{"builtinId":"fetch"}',
215
+ apiKeyConfig: null,
216
+ oauthConfig: null,
217
+ userId: null,
218
+ organizationId: null,
219
+ profiles: [],
220
+ oauthToken: null,
221
+ toolsCache: []
222
+ }, {
223
+ id: 'org-a-server',
224
+ name: 'A Custom',
225
+ type: 'external',
226
+ config: '{"command":"node"}',
227
+ apiKeyConfig: '{"apiKey":"a-key"}',
228
+ oauthConfig: null,
229
+ userId: 'user-a',
230
+ organizationId: 'org-a',
231
+ profiles: [],
232
+ oauthToken: null,
233
+ toolsCache: []
234
+ }, {
235
+ id: 'org-b-server',
236
+ name: 'B Custom',
237
+ type: 'external',
238
+ config: '{"command":"node"}',
239
+ apiKeyConfig: '{"apiKey":"b-key"}',
240
+ oauthConfig: null,
241
+ userId: 'user-b',
242
+ organizationId: 'org-b',
243
+ profiles: [],
244
+ oauthToken: null,
245
+ toolsCache: []
246
+ });
247
+ prisma = buildMockPrisma();
248
+ registry = new McpRegistry();
249
+ debugService = {
250
+ createLog: vi.fn().mockResolvedValue({
251
+ id: 'log-1'
252
+ }),
253
+ updateLog: vi.fn().mockResolvedValue({})
254
+ };
255
+ mcpService = new McpService(prisma, registry, debugService);
256
+ });
257
+ it('Org A sees org A servers + system servers, not Org B servers', async ()=>{
258
+ const result = await mcpService.findAll('user-a', 'org-a');
259
+ const ids = result.map((s)=>s.id);
260
+ expect(ids).toContain('sys-server');
261
+ expect(ids).toContain('org-a-server');
262
+ expect(ids).not.toContain('org-b-server');
263
+ });
264
+ it('Org B sees org B servers + system servers, not Org A servers', async ()=>{
265
+ const result = await mcpService.findAll('user-b', 'org-b');
266
+ const ids = result.map((s)=>s.id);
267
+ expect(ids).toContain('sys-server');
268
+ expect(ids).toContain('org-b-server');
269
+ expect(ids).not.toContain('org-a-server');
270
+ });
271
+ it('builtin/system servers (organizationId=null) are visible to all orgs', async ()=>{
272
+ const resultA = await mcpService.findAll('user-a', 'org-a');
273
+ const resultB = await mcpService.findAll('user-b', 'org-b');
274
+ expect(resultA.some((s)=>s.id === 'sys-server')).toBe(true);
275
+ expect(resultB.some((s)=>s.id === 'sys-server')).toBe(true);
276
+ });
277
+ it('Org A cannot access Org B server by ID', async ()=>{
278
+ await expect(mcpService.findById('org-b-server', 'user-a', 'org-a')).rejects.toThrow(ForbiddenException);
279
+ });
280
+ it('Org A can access system server by ID', async ()=>{
281
+ const server = await mcpService.findById('sys-server', 'user-a', 'org-a');
282
+ expect(server).toBeDefined();
283
+ expect(server.id).toBe('sys-server');
284
+ });
285
+ it('Creating a server in Org A sets organizationId to org-a', async ()=>{
286
+ await mcpService.create({
287
+ name: 'New',
288
+ type: 'external',
289
+ config: {
290
+ command: 'node'
291
+ }
292
+ }, 'user-a', 'org-a');
293
+ expect(prisma.mcpServer.create).toHaveBeenCalledWith(expect.objectContaining({
294
+ data: expect.objectContaining({
295
+ userId: 'user-a',
296
+ organizationId: 'org-a'
297
+ })
298
+ }));
299
+ });
300
+ it('unauthenticated user sees all servers without filtering', async ()=>{
301
+ await mcpService.findAll(UNAUTHENTICATED_ID);
302
+ // Anonymous uses the unfiltered findMany path
303
+ expect(prisma.mcpServer.findMany).toHaveBeenCalledWith(expect.objectContaining({
304
+ orderBy: {
305
+ name: 'asc'
306
+ }
307
+ }));
308
+ });
309
+ it('Org A cannot delete Org B server', async ()=>{
310
+ await expect(mcpService.delete('org-b-server', 'user-a', 'org-a')).rejects.toThrow(ForbiddenException);
311
+ });
312
+ it('Org A can delete own org server', async ()=>{
313
+ await mcpService.delete('org-a-server', 'user-a', 'org-a');
314
+ expect(prisma.mcpServer.delete).toHaveBeenCalledWith({
315
+ where: {
316
+ id: 'org-a-server'
317
+ }
318
+ });
319
+ });
320
+ });
321
+ });
322
+
323
+ //# sourceMappingURL=multi-tenant.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/__tests__/integration/multi-tenant.test.ts"],"sourcesContent":["/**\n * Integration Tests: Multi-tenant data isolation\n *\n * Verifies that ProfilesService and McpService correctly isolate data\n * between organizations while allowing access to system records.\n */\n\nimport { ForbiddenException } from '@nestjs/common';\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\n\n/** Sentinel value for unauthenticated MCP access */\nconst UNAUTHENTICATED_ID = '__unauthenticated__';\n\nimport type { PrismaService } from '../../modules/database/prisma.service.js';\nimport type { DebugService } from '../../modules/debug/debug.service.js';\nimport { McpService } from '../../modules/mcp/mcp.service.js';\nimport { McpRegistry } from '../../modules/mcp/mcp-registry.js';\nimport { ProfilesService } from '../../modules/profiles/profiles.service.js';\nimport type { ProxyService } from '../../modules/proxy/proxy.service.js';\n\n// ────────────────────────────────────────────────\n// In-memory data store simulating Prisma\n// ────────────────────────────────────────────────\n\ninterface InMemoryProfile {\n id: string;\n name: string;\n description: string | null;\n userId: string | null;\n organizationId: string | null;\n mcpServers: unknown[];\n}\n\ninterface InMemoryMcpServer {\n id: string;\n name: string;\n type: string;\n config: string;\n apiKeyConfig: string | null;\n oauthConfig: string | null;\n userId: string | null;\n organizationId: string | null;\n profiles: unknown[];\n oauthToken: null;\n toolsCache: unknown[];\n}\n\nconst PROFILES: InMemoryProfile[] = [];\n\nconst SERVERS: InMemoryMcpServer[] = [];\n\n// ────────────────────────────────────────────────\n// Helper: build a mock PrismaService over in-memory data\n// ────────────────────────────────────────────────\n\nfunction buildMockPrisma() {\n return {\n profile: {\n findMany: vi.fn().mockImplementation(({ where }: Record<string, unknown> = {}) => {\n let result = [...PROFILES];\n const whereObj = where as Record<string, unknown> | undefined;\n if (whereObj?.OR) {\n const orConds = whereObj.OR as Record<string, unknown>[];\n result = result.filter((p) =>\n orConds.some((cond: Record<string, unknown>) => {\n if ('organizationId' in cond) {\n return cond.organizationId === null\n ? p.organizationId === null\n : p.organizationId === cond.organizationId;\n }\n if ('id' in cond && (cond.id as Record<string, unknown>)?.in) {\n return ((cond.id as Record<string, unknown>).in as string[]).includes(p.id);\n }\n return false;\n })\n );\n }\n return Promise.resolve(result);\n }),\n findUnique: vi\n .fn()\n .mockImplementation(({ where }: Record<string, Record<string, unknown>>) => {\n const found = PROFILES.find(\n (p) => (where.id && p.id === where.id) || (where.name && p.name === where.name)\n );\n return Promise.resolve(found ?? null);\n }),\n create: vi.fn().mockImplementation(({ data }: Record<string, Record<string, unknown>>) => {\n const profile: InMemoryProfile = {\n id: `profile-${Date.now()}`,\n name: data.name as string,\n description: (data.description as string) ?? null,\n userId: (data.userId as string) ?? null,\n organizationId: (data.organizationId as string) ?? null,\n mcpServers: [],\n };\n PROFILES.push(profile);\n return Promise.resolve(profile);\n }),\n delete: vi.fn().mockResolvedValue(undefined),\n update: vi.fn().mockResolvedValue({}),\n },\n mcpServer: {\n findMany: vi.fn().mockImplementation(({ where }: Record<string, unknown> = {}) => {\n let result = [...SERVERS];\n const whereObj = where as Record<string, unknown> | undefined;\n if (whereObj?.OR) {\n const orConds = whereObj.OR as Record<string, unknown>[];\n result = result.filter((s) =>\n orConds.some((cond: Record<string, unknown>) => {\n if ('organizationId' in cond) {\n return cond.organizationId === null\n ? s.organizationId === null\n : s.organizationId === cond.organizationId;\n }\n if ('id' in cond && (cond.id as Record<string, unknown>)?.in) {\n return ((cond.id as Record<string, unknown>).in as string[]).includes(s.id);\n }\n return false;\n })\n );\n }\n return Promise.resolve(result);\n }),\n findUnique: vi\n .fn()\n .mockImplementation(({ where }: Record<string, Record<string, unknown>>) => {\n const found = SERVERS.find((s) => s.id === where.id);\n return Promise.resolve(found ?? null);\n }),\n create: vi.fn().mockImplementation(({ data }: Record<string, Record<string, unknown>>) => {\n const server: InMemoryMcpServer = {\n id: `server-${Date.now()}`,\n name: data.name as string,\n type: data.type as string,\n config: data.config as string,\n apiKeyConfig: (data.apiKeyConfig as string) ?? null,\n oauthConfig: (data.oauthConfig as string) ?? null,\n userId: (data.userId as string) ?? null,\n organizationId: (data.organizationId as string) ?? null,\n profiles: [],\n oauthToken: null,\n toolsCache: [],\n };\n SERVERS.push(server);\n return Promise.resolve(server);\n }),\n delete: vi.fn().mockResolvedValue(undefined),\n update: vi.fn().mockResolvedValue({}),\n },\n member: {\n findMany: vi.fn().mockResolvedValue([]),\n },\n mcpServerToolsCache: {\n findMany: vi.fn().mockResolvedValue([]),\n },\n gatewaySetting: {\n upsert: vi.fn().mockResolvedValue({}),\n },\n };\n}\n\n// ────────────────────────────────────────────────\n// Tests: ProfilesService data isolation\n// ────────────────────────────────────────────────\n\ndescribe('Multi-tenant data isolation', () => {\n describe('ProfilesService', () => {\n let profilesService: ProfilesService;\n let prisma: ReturnType<typeof buildMockPrisma>;\n let proxyService: Record<string, ReturnType<typeof vi.fn>>;\n\n beforeEach(() => {\n // Reset in-memory data\n PROFILES.length = 0;\n PROFILES.push(\n {\n id: 'sys-profile',\n name: 'default',\n description: 'System default profile',\n userId: null,\n organizationId: null,\n mcpServers: [],\n },\n {\n id: 'org-a-profile',\n name: 'org-a-dev',\n description: 'Org A dev profile',\n userId: 'user-a',\n organizationId: 'org-a',\n mcpServers: [],\n },\n {\n id: 'org-b-profile',\n name: 'org-b-prod',\n description: 'Org B prod profile',\n userId: 'user-b',\n organizationId: 'org-b',\n mcpServers: [],\n }\n );\n\n prisma = buildMockPrisma();\n proxyService = {\n getToolsForServer: vi.fn().mockResolvedValue([]),\n };\n\n profilesService = new ProfilesService(\n prisma as unknown as PrismaService,\n proxyService as unknown as ProxyService\n );\n });\n\n it('Org A sees org A profiles + system profiles, not Org B profiles', async () => {\n const result = await profilesService.findAll('user-a', 'org-a');\n\n const ids = result.map((p: { id: string }) => p.id);\n expect(ids).toContain('sys-profile');\n expect(ids).toContain('org-a-profile');\n expect(ids).not.toContain('org-b-profile');\n });\n\n it('Org B sees org B profiles + system profiles, not Org A profiles', async () => {\n const result = await profilesService.findAll('user-b', 'org-b');\n\n const ids = result.map((p: { id: string }) => p.id);\n expect(ids).toContain('sys-profile');\n expect(ids).toContain('org-b-profile');\n expect(ids).not.toContain('org-a-profile');\n });\n\n it('system profiles (organizationId=null) are visible to all orgs', async () => {\n const resultA = await profilesService.findAll('user-a', 'org-a');\n const resultB = await profilesService.findAll('user-b', 'org-b');\n\n expect(resultA.some((p: { id: string }) => p.id === 'sys-profile')).toBe(true);\n expect(resultB.some((p: { id: string }) => p.id === 'sys-profile')).toBe(true);\n });\n\n it('creating a profile sets userId and organizationId', async () => {\n const created = await profilesService.create({ name: 'a-new' }, 'user-a', 'org-a');\n expect(created.userId).toBe('user-a');\n expect(created.organizationId).toBe('org-a');\n });\n\n it('Org A cannot access Org B profile by ID', async () => {\n await expect(profilesService.findById('org-b-profile', 'user-a', 'org-a')).rejects.toThrow(\n ForbiddenException\n );\n });\n\n it('Org A can access system profile by ID', async () => {\n const profile = await profilesService.findById('sys-profile', 'user-a', 'org-a');\n expect(profile).toBeDefined();\n expect(profile.id).toBe('sys-profile');\n });\n\n it('unauthenticated user sees all profiles', async () => {\n const result = await profilesService.findAll(UNAUTHENTICATED_ID);\n\n // Unauthenticated findAll queries without WHERE filter\n expect(prisma.profile.findMany).toHaveBeenCalledWith(\n expect.objectContaining({\n orderBy: { name: 'asc' },\n })\n );\n // The mock returns all profiles for filterless queries\n expect(result.length).toBeGreaterThanOrEqual(1);\n });\n });\n\n // ────────────────────────────────────────────────\n // Tests: McpService data isolation\n // ────────────────────────────────────────────────\n\n describe('McpService', () => {\n let mcpService: McpService;\n let prisma: ReturnType<typeof buildMockPrisma>;\n let registry: McpRegistry;\n let debugService: Record<string, ReturnType<typeof vi.fn>>;\n\n beforeEach(() => {\n // Reset in-memory data\n SERVERS.length = 0;\n SERVERS.push(\n {\n id: 'sys-server',\n name: 'System Builtin',\n type: 'builtin',\n config: '{\"builtinId\":\"fetch\"}',\n apiKeyConfig: null,\n oauthConfig: null,\n userId: null,\n organizationId: null,\n profiles: [],\n oauthToken: null,\n toolsCache: [],\n },\n {\n id: 'org-a-server',\n name: 'A Custom',\n type: 'external',\n config: '{\"command\":\"node\"}',\n apiKeyConfig: '{\"apiKey\":\"a-key\"}',\n oauthConfig: null,\n userId: 'user-a',\n organizationId: 'org-a',\n profiles: [],\n oauthToken: null,\n toolsCache: [],\n },\n {\n id: 'org-b-server',\n name: 'B Custom',\n type: 'external',\n config: '{\"command\":\"node\"}',\n apiKeyConfig: '{\"apiKey\":\"b-key\"}',\n oauthConfig: null,\n userId: 'user-b',\n organizationId: 'org-b',\n profiles: [],\n oauthToken: null,\n toolsCache: [],\n }\n );\n\n prisma = buildMockPrisma();\n registry = new McpRegistry();\n debugService = {\n createLog: vi.fn().mockResolvedValue({ id: 'log-1' }),\n updateLog: vi.fn().mockResolvedValue({}),\n };\n\n mcpService = new McpService(\n prisma as unknown as PrismaService,\n registry,\n debugService as unknown as DebugService\n );\n });\n\n it('Org A sees org A servers + system servers, not Org B servers', async () => {\n const result = await mcpService.findAll('user-a', 'org-a');\n\n const ids = result.map((s: { id: string }) => s.id);\n expect(ids).toContain('sys-server');\n expect(ids).toContain('org-a-server');\n expect(ids).not.toContain('org-b-server');\n });\n\n it('Org B sees org B servers + system servers, not Org A servers', async () => {\n const result = await mcpService.findAll('user-b', 'org-b');\n\n const ids = result.map((s: { id: string }) => s.id);\n expect(ids).toContain('sys-server');\n expect(ids).toContain('org-b-server');\n expect(ids).not.toContain('org-a-server');\n });\n\n it('builtin/system servers (organizationId=null) are visible to all orgs', async () => {\n const resultA = await mcpService.findAll('user-a', 'org-a');\n const resultB = await mcpService.findAll('user-b', 'org-b');\n\n expect(resultA.some((s: { id: string }) => s.id === 'sys-server')).toBe(true);\n expect(resultB.some((s: { id: string }) => s.id === 'sys-server')).toBe(true);\n });\n\n it('Org A cannot access Org B server by ID', async () => {\n await expect(mcpService.findById('org-b-server', 'user-a', 'org-a')).rejects.toThrow(\n ForbiddenException\n );\n });\n\n it('Org A can access system server by ID', async () => {\n const server = await mcpService.findById('sys-server', 'user-a', 'org-a');\n expect(server).toBeDefined();\n expect(server.id).toBe('sys-server');\n });\n\n it('Creating a server in Org A sets organizationId to org-a', async () => {\n await mcpService.create(\n { name: 'New', type: 'external', config: { command: 'node' } },\n 'user-a',\n 'org-a'\n );\n\n expect(prisma.mcpServer.create).toHaveBeenCalledWith(\n expect.objectContaining({\n data: expect.objectContaining({ userId: 'user-a', organizationId: 'org-a' }),\n })\n );\n });\n\n it('unauthenticated user sees all servers without filtering', async () => {\n await mcpService.findAll(UNAUTHENTICATED_ID);\n\n // Anonymous uses the unfiltered findMany path\n expect(prisma.mcpServer.findMany).toHaveBeenCalledWith(\n expect.objectContaining({\n orderBy: { name: 'asc' },\n })\n );\n });\n\n it('Org A cannot delete Org B server', async () => {\n await expect(mcpService.delete('org-b-server', 'user-a', 'org-a')).rejects.toThrow(\n ForbiddenException\n );\n });\n\n it('Org A can delete own org server', async () => {\n await mcpService.delete('org-a-server', 'user-a', 'org-a');\n expect(prisma.mcpServer.delete).toHaveBeenCalledWith({ where: { id: 'org-a-server' } });\n });\n });\n});\n"],"names":["ForbiddenException","beforeEach","describe","expect","it","vi","UNAUTHENTICATED_ID","McpService","McpRegistry","ProfilesService","PROFILES","SERVERS","buildMockPrisma","profile","findMany","fn","mockImplementation","where","result","whereObj","OR","orConds","filter","p","some","cond","organizationId","id","in","includes","Promise","resolve","findUnique","found","find","name","create","data","Date","now","description","userId","mcpServers","push","delete","mockResolvedValue","undefined","update","mcpServer","s","server","type","config","apiKeyConfig","oauthConfig","profiles","oauthToken","toolsCache","member","mcpServerToolsCache","gatewaySetting","upsert","profilesService","prisma","proxyService","length","getToolsForServer","findAll","ids","map","toContain","not","resultA","resultB","toBe","created","findById","rejects","toThrow","toBeDefined","toHaveBeenCalledWith","objectContaining","orderBy","toBeGreaterThanOrEqual","mcpService","registry","debugService","createLog","updateLog","command"],"mappings":"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"}