@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.
- package/.turbo/turbo-build.log +2 -2
- package/CHANGELOG.md +20 -0
- package/dist/__tests__/integration/auth-flow.test.js +181 -0
- package/dist/__tests__/integration/auth-flow.test.js.map +1 -0
- package/dist/__tests__/integration/multi-tenant.test.js +323 -0
- package/dist/__tests__/integration/multi-tenant.test.js.map +1 -0
- package/dist/__tests__/integration/proxy-auth.test.js +276 -0
- package/dist/__tests__/integration/proxy-auth.test.js.map +1 -0
- package/dist/__tests__/integration/sharing-credentials.test.js +280 -0
- package/dist/__tests__/integration/sharing-credentials.test.js.map +1 -0
- package/dist/__tests__/unit/auth.guard.test.js +74 -0
- package/dist/__tests__/unit/auth.guard.test.js.map +1 -0
- package/dist/__tests__/unit/auth.service.test.js +145 -0
- package/dist/__tests__/unit/auth.service.test.js.map +1 -0
- package/dist/__tests__/unit/mcp.service.test.js +213 -0
- package/dist/__tests__/unit/mcp.service.test.js.map +1 -0
- package/dist/__tests__/unit/sharing.service.test.js +153 -0
- package/dist/__tests__/unit/sharing.service.test.js.map +1 -0
- package/dist/app.module.js +16 -3
- package/dist/app.module.js.map +1 -1
- package/dist/main.js +17 -1
- package/dist/main.js.map +1 -1
- package/dist/modules/auth/auth.config.js +97 -0
- package/dist/modules/auth/auth.config.js.map +1 -0
- package/dist/modules/auth/auth.guard.js +70 -0
- package/dist/modules/auth/auth.guard.js.map +1 -0
- package/dist/modules/auth/auth.module.js +30 -0
- package/dist/modules/auth/auth.module.js.map +1 -0
- package/dist/modules/auth/auth.service.js +118 -0
- package/dist/modules/auth/auth.service.js.map +1 -0
- package/dist/modules/auth/decorators/active-org-id.decorator.js +11 -0
- package/dist/modules/auth/decorators/active-org-id.decorator.js.map +1 -0
- package/dist/modules/auth/decorators/current-user.decorator.js +11 -0
- package/dist/modules/auth/decorators/current-user.decorator.js.map +1 -0
- package/dist/modules/auth/decorators/public.decorator.js +9 -0
- package/dist/modules/auth/decorators/public.decorator.js.map +1 -0
- package/dist/modules/auth/decorators/skip-org-check.decorator.js +10 -0
- package/dist/modules/auth/decorators/skip-org-check.decorator.js.map +1 -0
- package/dist/modules/database/prisma.service.js +11 -9
- package/dist/modules/database/prisma.service.js.map +1 -1
- package/dist/modules/debug/debug.controller.js +2 -0
- package/dist/modules/debug/debug.controller.js.map +1 -1
- package/dist/modules/health/health.controller.js +17 -0
- package/dist/modules/health/health.controller.js.map +1 -1
- package/dist/modules/mcp/mcp-presets.js +96 -0
- package/dist/modules/mcp/mcp-presets.js.map +1 -0
- package/dist/modules/mcp/mcp-seed.service.js +8 -167
- package/dist/modules/mcp/mcp-seed.service.js.map +1 -1
- package/dist/modules/mcp/mcp.controller.js +89 -27
- package/dist/modules/mcp/mcp.controller.js.map +1 -1
- package/dist/modules/mcp/mcp.service.js +124 -46
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/oauth/oauth.controller.js +2 -0
- package/dist/modules/oauth/oauth.controller.js.map +1 -1
- package/dist/modules/profiles/profiles.controller.js +107 -51
- package/dist/modules/profiles/profiles.controller.js.map +1 -1
- package/dist/modules/profiles/profiles.service.js +172 -49
- package/dist/modules/profiles/profiles.service.js.map +1 -1
- package/dist/modules/proxy/proxy.controller.js +109 -92
- package/dist/modules/proxy/proxy.controller.js.map +1 -1
- package/dist/modules/proxy/proxy.module.js +3 -1
- package/dist/modules/proxy/proxy.module.js.map +1 -1
- package/dist/modules/proxy/proxy.service.js +201 -37
- package/dist/modules/proxy/proxy.service.js.map +1 -1
- package/dist/modules/settings/settings.controller.js +2 -0
- package/dist/modules/settings/settings.controller.js.map +1 -1
- package/dist/modules/settings/settings.service.js +1 -1
- package/dist/modules/settings/settings.service.js.map +1 -1
- package/dist/modules/sharing/sharing.controller.js +83 -0
- package/dist/modules/sharing/sharing.controller.js.map +1 -0
- package/dist/modules/sharing/sharing.module.js +30 -0
- package/dist/modules/sharing/sharing.module.js.map +1 -0
- package/dist/modules/sharing/sharing.service.js +167 -0
- package/dist/modules/sharing/sharing.service.js.map +1 -0
- package/docker-entrypoint.sh +2 -2
- package/package.json +11 -7
- package/src/__tests__/integration/auth-flow.test.ts +216 -0
- package/src/__tests__/integration/multi-tenant.test.ts +415 -0
- package/src/__tests__/integration/proxy-auth.test.ts +349 -0
- package/src/__tests__/integration/sharing-credentials.test.ts +293 -0
- package/src/__tests__/unit/auth.guard.test.ts +66 -0
- package/src/__tests__/unit/auth.service.test.ts +162 -0
- package/src/__tests__/unit/mcp.service.test.ts +219 -0
- package/src/__tests__/unit/sharing.service.test.ts +147 -0
- package/src/app.module.ts +17 -4
- package/src/main.ts +21 -1
- package/src/modules/auth/auth.config.ts +114 -0
- package/src/modules/auth/auth.guard.ts +88 -0
- package/src/modules/auth/auth.module.ts +16 -0
- package/src/modules/auth/auth.service.ts +131 -0
- package/src/modules/auth/decorators/active-org-id.decorator.ts +13 -0
- package/src/modules/auth/decorators/current-user.decorator.ts +13 -0
- package/src/modules/auth/decorators/public.decorator.ts +10 -0
- package/src/modules/auth/decorators/skip-org-check.decorator.ts +11 -0
- package/src/modules/database/prisma.service.ts +9 -9
- package/src/modules/debug/debug.controller.ts +2 -0
- package/src/modules/health/health.controller.ts +14 -0
- package/src/modules/mcp/mcp-presets.ts +87 -0
- package/src/modules/mcp/mcp-seed.service.ts +8 -174
- package/src/modules/mcp/mcp.controller.ts +70 -19
- package/src/modules/mcp/mcp.service.ts +126 -52
- package/src/modules/oauth/oauth.controller.ts +2 -0
- package/src/modules/profiles/profiles.controller.ts +76 -23
- package/src/modules/profiles/profiles.service.ts +187 -75
- package/src/modules/proxy/proxy.controller.ts +86 -81
- package/src/modules/proxy/proxy.module.ts +2 -1
- package/src/modules/proxy/proxy.service.ts +236 -27
- package/src/modules/settings/settings.controller.ts +2 -0
- package/src/modules/settings/settings.service.ts +1 -1
- package/src/modules/sharing/sharing.controller.ts +46 -0
- package/src/modules/sharing/sharing.module.ts +16 -0
- package/src/modules/sharing/sharing.service.ts +173 -0
- package/vitest.config.ts +20 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
|
|
2
|
-
> @dxheroes/local-mcp-backend@0.
|
|
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
|
- [46m[1m TSC [22m[49m[36m Initializing type checker...[39m
|
|
6
6
|
✔ [46m[1m TSC [22m[49m[36m Initializing type checker...[39m
|
|
7
7
|
[32m> [39m[42m[1m TSC [22m[49m[32m Found 0 issues.[39m
|
|
8
8
|
[36m> [39m[46m[1m SWC [22m[49m [36mRunning...[39m
|
|
9
|
-
Successfully compiled:
|
|
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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"}
|