@dxheroes/local-mcp-backend 0.13.0 → 0.15.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 (51) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/CHANGELOG.md +53 -0
  3. package/dist/__tests__/integration/proxy-auth.test.js +51 -5
  4. package/dist/__tests__/integration/proxy-auth.test.js.map +1 -1
  5. package/dist/__tests__/unit/debug.service.test.js +86 -0
  6. package/dist/__tests__/unit/debug.service.test.js.map +1 -0
  7. package/dist/__tests__/unit/profiles.service.test.js +67 -1
  8. package/dist/__tests__/unit/profiles.service.test.js.map +1 -1
  9. package/dist/__tests__/unit/proxy-logging.test.js +197 -0
  10. package/dist/__tests__/unit/proxy-logging.test.js.map +1 -0
  11. package/dist/__tests__/unit/proxy.service.test.js +148 -28
  12. package/dist/__tests__/unit/proxy.service.test.js.map +1 -1
  13. package/dist/common/filters/all-exceptions.filter.js +11 -9
  14. package/dist/common/filters/all-exceptions.filter.js.map +1 -1
  15. package/dist/common/interceptors/logging.interceptor.js +20 -6
  16. package/dist/common/interceptors/logging.interceptor.js.map +1 -1
  17. package/dist/common/logging/app-logger.js +51 -0
  18. package/dist/common/logging/app-logger.js.map +1 -0
  19. package/dist/common/logging/redact-sensitive-fields.js +33 -0
  20. package/dist/common/logging/redact-sensitive-fields.js.map +1 -0
  21. package/dist/common/logging/request-context.js +17 -0
  22. package/dist/common/logging/request-context.js.map +1 -0
  23. package/dist/modules/debug/debug.controller.js +18 -11
  24. package/dist/modules/debug/debug.controller.js.map +1 -1
  25. package/dist/modules/debug/debug.service.js +101 -22
  26. package/dist/modules/debug/debug.service.js.map +1 -1
  27. package/dist/modules/profiles/profiles.controller.js +18 -0
  28. package/dist/modules/profiles/profiles.controller.js.map +1 -1
  29. package/dist/modules/profiles/profiles.service.js +8 -13
  30. package/dist/modules/profiles/profiles.service.js.map +1 -1
  31. package/dist/modules/proxy/proxy.controller.js +25 -12
  32. package/dist/modules/proxy/proxy.controller.js.map +1 -1
  33. package/dist/modules/proxy/proxy.service.js +388 -230
  34. package/dist/modules/proxy/proxy.service.js.map +1 -1
  35. package/package.json +10 -9
  36. package/src/__tests__/integration/proxy-auth.test.ts +70 -19
  37. package/src/__tests__/unit/debug.service.test.ts +112 -0
  38. package/src/__tests__/unit/profiles.service.test.ts +75 -2
  39. package/src/__tests__/unit/proxy-logging.test.ts +193 -0
  40. package/src/__tests__/unit/proxy.service.test.ts +245 -168
  41. package/src/common/filters/all-exceptions.filter.ts +16 -16
  42. package/src/common/interceptors/logging.interceptor.ts +24 -5
  43. package/src/common/logging/app-logger.ts +62 -0
  44. package/src/common/logging/redact-sensitive-fields.ts +40 -0
  45. package/src/common/logging/request-context.ts +25 -0
  46. package/src/modules/debug/debug.controller.ts +10 -10
  47. package/src/modules/debug/debug.service.ts +130 -28
  48. package/src/modules/profiles/profiles.controller.ts +12 -0
  49. package/src/modules/profiles/profiles.service.ts +11 -11
  50. package/src/modules/proxy/proxy.controller.ts +22 -10
  51. package/src/modules/proxy/proxy.service.ts +516 -283
@@ -1,9 +1,9 @@
1
1
 
2
- > @dxheroes/local-mcp-backend@0.13.0 build /home/runner/work/local-mcp-gateway/local-mcp-gateway/apps/backend
2
+ > @dxheroes/local-mcp-backend@0.15.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: 70 files with swc (113.12ms)
9
+ Successfully compiled: 75 files with swc (113.25ms)
package/CHANGELOG.md CHANGED
@@ -1,5 +1,58 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.15.0](https://github.com/DXHeroes/local-mcp-gateway/compare/backend-v0.14.0...backend-v0.15.0) (2026-03-18)
4
+
5
+
6
+ ### Features
7
+
8
+ * **debug:** enhance logging retrieval with pagination and filtering support ([815f268](https://github.com/DXHeroes/local-mcp-gateway/commit/815f2686f1ec44ee81bd6455509254fea21cf8c1))
9
+ * **logging:** implement structured logging and error handling across services ([5de103a](https://github.com/DXHeroes/local-mcp-gateway/commit/5de103a22bf25a7780437e89e67b6526d2b5b894))
10
+ * **profiles:** implement getInfo method for profile aggregation ([1a8efb4](https://github.com/DXHeroes/local-mcp-gateway/commit/1a8efb4bd3e199ee9355646850ca86179f39f6a7))
11
+ * **proxy:** enhance profile retrieval with user context ([de5ba86](https://github.com/DXHeroes/local-mcp-gateway/commit/de5ba86dfdd370e84cfe5b5b2a261dda6b63e2ee))
12
+
13
+
14
+ ### Code Refactoring
15
+
16
+ * **tests:** improve type handling and error management in proxy-auth tests ([74c7a61](https://github.com/DXHeroes/local-mcp-gateway/commit/74c7a61f11ae66581aa9f8340940e30b479935f1))
17
+
18
+
19
+ ### Dependencies
20
+
21
+ * The following workspace dependencies were updated
22
+ * dependencies
23
+ * @dxheroes/local-mcp-core bumped to 0.10.2
24
+ * @dxheroes/local-mcp-database bumped to 0.7.2
25
+ * @dxheroes/mcp-abra-flexi bumped to 0.4.3
26
+ * @dxheroes/mcp-fakturoid bumped to 0.5.1
27
+ * @dxheroes/mcp-gemini-deep-research bumped to 0.5.11
28
+ * @dxheroes/mcp-merk bumped to 0.4.1
29
+ * @dxheroes/mcp-toggl bumped to 0.3.11
30
+ * devDependencies
31
+ * @dxheroes/local-mcp-config bumped to 0.4.16
32
+
33
+ ## [0.14.0](https://github.com/DXHeroes/local-mcp-gateway/compare/backend-v0.13.0...backend-v0.14.0) (2026-03-18)
34
+
35
+
36
+ ### Features
37
+
38
+ * **proxy:** add argument coercion for Merk search tool calls ([b9c9a0c](https://github.com/DXHeroes/local-mcp-gateway/commit/b9c9a0c3f0bc91fc1aca493fdb828271190c805c))
39
+ * **proxy:** implement argument coercion for tool calls based on input schema ([e9ac930](https://github.com/DXHeroes/local-mcp-gateway/commit/e9ac930a7b739590577075fce146a80c157f0708))
40
+
41
+
42
+ ### Dependencies
43
+
44
+ * The following workspace dependencies were updated
45
+ * dependencies
46
+ * @dxheroes/local-mcp-core bumped to 0.10.1
47
+ * @dxheroes/local-mcp-database bumped to 0.7.1
48
+ * @dxheroes/mcp-abra-flexi bumped to 0.4.2
49
+ * @dxheroes/mcp-fakturoid bumped to 0.5.0
50
+ * @dxheroes/mcp-gemini-deep-research bumped to 0.5.10
51
+ * @dxheroes/mcp-merk bumped to 0.4.0
52
+ * @dxheroes/mcp-toggl bumped to 0.3.10
53
+ * devDependencies
54
+ * @dxheroes/local-mcp-config bumped to 0.4.15
55
+
3
56
  ## [0.13.0](https://github.com/DXHeroes/local-mcp-gateway/compare/backend-v0.12.0...backend-v0.13.0) (2026-03-16)
4
57
 
5
58
 
@@ -94,6 +94,13 @@ function createMockRequest(headers = {}, query = {}) {
94
94
  user: undefined
95
95
  };
96
96
  }
97
+ function createMockResponse() {
98
+ return {
99
+ json: vi.fn((payload)=>payload),
100
+ setHeader: vi.fn(),
101
+ write: vi.fn()
102
+ };
103
+ }
97
104
  function createMockExecutionContext(req) {
98
105
  return {
99
106
  switchToHttp: ()=>({
@@ -126,9 +133,10 @@ describe('McpOAuthGuard', ()=>{
126
133
  try {
127
134
  await guard.canActivate(ctx);
128
135
  } catch (error) {
129
- expect(error.wwwAuthenticate).toContain('resource_metadata=');
130
- expect(error.wwwAuthenticate).toContain('resource_metadata_uri=');
131
- expect(error.wwwAuthenticate).toContain('/.well-known/oauth-protected-resource');
136
+ const authError = error;
137
+ expect(authError.wwwAuthenticate).toContain('resource_metadata=');
138
+ expect(authError.wwwAuthenticate).toContain('resource_metadata_uri=');
139
+ expect(authError.wwwAuthenticate).toContain('/.well-known/oauth-protected-resource');
132
140
  }
133
141
  });
134
142
  it('rejects invalid Bearer token', async ()=>{
@@ -328,8 +336,46 @@ describe('Proxy controller auth', ()=>{
328
336
  await expect(controller.handleGatewayRequest(req, mcpRequest)).rejects.toThrow(NotFoundException);
329
337
  });
330
338
  it('org-scoped profile info endpoint uses orgSlug', async ()=>{
331
- await controller.getOrgProfileInfo('my-org', 'my-profile');
332
- expect(proxyService.getProfileInfoByOrgSlug).toHaveBeenCalledWith('my-profile', 'my-org');
339
+ const req = createMockRequest({
340
+ authorization: 'Bearer token-a'
341
+ });
342
+ req.user = userA;
343
+ await controller.getOrgProfileInfo('my-org', 'my-profile', req);
344
+ expect(proxyService.getProfileInfoByOrgSlug).toHaveBeenCalledWith('my-profile', 'my-org', 'user-a');
345
+ });
346
+ it('org-scoped profile endpoint reports tool count from user-scoped info lookup', async ()=>{
347
+ proxyService.getProfileInfoByOrgSlug.mockResolvedValue({
348
+ tools: [
349
+ {
350
+ name: 'tool-a',
351
+ description: 'A'
352
+ },
353
+ {
354
+ name: 'tool-b',
355
+ description: 'B'
356
+ }
357
+ ],
358
+ serverStatus: {
359
+ total: 1,
360
+ connected: 1,
361
+ servers: {}
362
+ }
363
+ });
364
+ const req = createMockRequest({
365
+ authorization: 'Bearer token-a'
366
+ });
367
+ req.user = userA;
368
+ const res = createMockResponse();
369
+ await controller.getOrgMcpEndpoint('my-org', 'my-profile', req, res);
370
+ expect(proxyService.getProfileInfoByOrgSlug).toHaveBeenCalledWith('my-profile', 'my-org', 'user-a');
371
+ expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
372
+ profile: expect.objectContaining({
373
+ name: 'my-profile',
374
+ toolCount: 2,
375
+ serverCount: 1,
376
+ connectedServers: 1
377
+ })
378
+ }));
333
379
  });
334
380
  });
335
381
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/__tests__/integration/proxy-auth.test.ts"],"sourcesContent":["/**\n * Integration Tests: Proxy controller auth\n *\n * Verifies:\n * - McpOAuthGuard always enforces Bearer token\n * - WWW-Authenticate header with resource_metadata_uri on 401\n * - Valid tokens resolve user and pass through\n * - Org-scoped profile lookup through the proxy service\n * - Gateway endpoint uses default profile with user scoping\n */\n\nimport { NotFoundException, UnauthorizedException } from '@nestjs/common';\nimport type { EventEmitter2 } from '@nestjs/event-emitter';\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\nimport { type AuthService, type AuthUser } from '../../modules/auth/auth.service.js';\nimport type { McpRequest, McpResponse, ProxyService } from '../../modules/proxy/proxy.service.js';\nimport type { SettingsService } from '../../modules/settings/settings.service.js';\n\n// Dynamic import to handle ESM\nconst { McpOAuthGuard } = await import('../../modules/auth/mcp-oauth.guard.js');\nconst { ProxyController } = await import('../../modules/proxy/proxy.controller.js');\n\n// ────────────────────────────────────────────────\n// Mock factories\n// ────────────────────────────────────────────────\n\nfunction createMockAuthService() {\n return {\n getAuth: vi.fn(),\n getSession: vi.fn().mockResolvedValue(null),\n validateMcpToken: vi.fn().mockResolvedValue(null),\n getUserOrganizations: vi.fn().mockResolvedValue([]),\n onModuleInit: vi.fn(),\n };\n}\n\nfunction createMockConfigService(overrides: Record<string, unknown> = {}) {\n const defaults: Record<string, unknown> = {\n 'app.port': 3001,\n BETTER_AUTH_URL: 'http://localhost:3001',\n };\n const config = { ...defaults, ...overrides };\n return {\n get: vi.fn((key: string) => config[key]),\n };\n}\n\nfunction createMockProxyService() {\n return {\n handleRequest: vi.fn().mockResolvedValue({\n jsonrpc: '2.0',\n id: 1,\n result: { tools: [] },\n } as McpResponse),\n handleRequestByOrgSlug: vi.fn().mockResolvedValue({\n jsonrpc: '2.0',\n id: 1,\n result: { tools: [] },\n } as McpResponse),\n getProfileInfo: vi.fn().mockResolvedValue({\n tools: [],\n serverStatus: { total: 0, connected: 0, servers: {} },\n }),\n getProfileInfoByOrgSlug: vi.fn().mockResolvedValue({\n tools: [],\n serverStatus: { total: 0, connected: 0, servers: {} },\n }),\n getToolsForServer: vi.fn().mockResolvedValue([]),\n };\n}\n\nfunction createMockSettingsService() {\n return {\n getDefaultGatewayProfile: vi.fn().mockResolvedValue('default'),\n getSetting: vi.fn(),\n setSetting: vi.fn(),\n };\n}\n\nfunction createMockEventEmitter() {\n return {\n emit: vi.fn(),\n on: vi.fn(),\n off: vi.fn(),\n };\n}\n\nfunction createMockRequest(\n headers: Record<string, string> = {},\n query: Record<string, string> = {}\n) {\n return {\n headers,\n query,\n on: vi.fn(),\n user: undefined as any,\n } as unknown as import('express').Request;\n}\n\nfunction createMockExecutionContext(req: import('express').Request) {\n return {\n switchToHttp: () => ({\n getRequest: () => req,\n getResponse: () => ({}),\n }),\n getHandler: () => ({}),\n getClass: () => ({}),\n } as any;\n}\n\n// ────────────────────────────────────────────────\n// McpOAuthGuard tests\n// ────────────────────────────────────────────────\n\ndescribe('McpOAuthGuard', () => {\n let guard: InstanceType<typeof McpOAuthGuard>;\n let authService: ReturnType<typeof createMockAuthService>;\n\n beforeEach(() => {\n authService = createMockAuthService();\n const configService = createMockConfigService();\n guard = new McpOAuthGuard(authService as unknown as AuthService, configService as any);\n });\n\n it('rejects request without Bearer token with 401', async () => {\n const req = createMockRequest();\n const ctx = createMockExecutionContext(req);\n\n await expect(guard.canActivate(ctx)).rejects.toThrow(UnauthorizedException);\n });\n\n it('returns WWW-Authenticate header on missing token', async () => {\n const req = createMockRequest();\n const ctx = createMockExecutionContext(req);\n\n try {\n await guard.canActivate(ctx);\n } catch (error: any) {\n expect(error.wwwAuthenticate).toContain('resource_metadata=');\n expect(error.wwwAuthenticate).toContain('resource_metadata_uri=');\n expect(error.wwwAuthenticate).toContain('/.well-known/oauth-protected-resource');\n }\n });\n\n it('rejects invalid Bearer token', async () => {\n authService.validateMcpToken.mockResolvedValue(null);\n const req = createMockRequest({ authorization: 'Bearer bad-token' });\n const ctx = createMockExecutionContext(req);\n\n await expect(guard.canActivate(ctx)).rejects.toThrow(UnauthorizedException);\n expect(authService.validateMcpToken).toHaveBeenCalledWith('bad-token');\n });\n\n it('allows valid Bearer token and attaches user', async () => {\n const user: AuthUser = { id: 'user-1', name: 'Test', email: 'test@example.com' };\n authService.validateMcpToken.mockResolvedValue(user);\n const req = createMockRequest({ authorization: 'Bearer valid-token' });\n const ctx = createMockExecutionContext(req);\n\n const result = await guard.canActivate(ctx);\n\n expect(result).toBe(true);\n expect(req.user).toEqual(user);\n });\n\n it('accepts access_token query param (SSE fallback)', async () => {\n const user: AuthUser = { id: 'user-1', name: 'Test', email: 'test@example.com' };\n authService.validateMcpToken.mockResolvedValue(user);\n const req = createMockRequest({}, { access_token: 'sse-token' });\n const ctx = createMockExecutionContext(req);\n\n const result = await guard.canActivate(ctx);\n\n expect(result).toBe(true);\n expect(authService.validateMcpToken).toHaveBeenCalledWith('sse-token');\n });\n\n it('accepts session cookie when no Bearer token is present', async () => {\n const user: AuthUser = { id: 'session-user', name: 'Session', email: 'session@example.com' };\n authService.getSession.mockResolvedValue({ user, session: { id: 's1', userId: user.id } });\n const req = createMockRequest({ cookie: 'better-auth.session_token=abc123' });\n const ctx = createMockExecutionContext(req);\n\n const result = await guard.canActivate(ctx);\n\n expect(result).toBe(true);\n expect(req.user).toEqual(user);\n expect(authService.validateMcpToken).not.toHaveBeenCalled();\n });\n\n it('rejects when neither Bearer token nor session cookie is valid', async () => {\n authService.getSession.mockResolvedValue(null);\n const req = createMockRequest({ cookie: 'better-auth.session_token=expired' });\n const ctx = createMockExecutionContext(req);\n\n await expect(guard.canActivate(ctx)).rejects.toThrow(UnauthorizedException);\n });\n\n it('does not fall through to session cookie when Bearer token is invalid', async () => {\n const user: AuthUser = { id: 'session-user', name: 'Session', email: 'session@example.com' };\n authService.validateMcpToken.mockResolvedValue(null);\n authService.getSession.mockResolvedValue({ user, session: { id: 's1', userId: user.id } });\n const req = createMockRequest({ authorization: 'Bearer bad-token' });\n const ctx = createMockExecutionContext(req);\n\n await expect(guard.canActivate(ctx)).rejects.toThrow(UnauthorizedException);\n expect(authService.getSession).not.toHaveBeenCalled();\n });\n});\n\n// ────────────────────────────────────────────────\n// Proxy controller auth tests\n// ────────────────────────────────────────────────\n\ndescribe('Proxy controller auth', () => {\n let controller: InstanceType<typeof ProxyController>;\n let proxyService: ReturnType<typeof createMockProxyService>;\n let settingsService: ReturnType<typeof createMockSettingsService>;\n let eventEmitter: ReturnType<typeof createMockEventEmitter>;\n\n beforeEach(() => {\n proxyService = createMockProxyService();\n settingsService = createMockSettingsService();\n eventEmitter = createMockEventEmitter();\n\n controller = new ProxyController(\n proxyService as unknown as ProxyService,\n settingsService as unknown as SettingsService,\n eventEmitter as unknown as EventEmitter2\n );\n });\n\n describe('guard-authenticated user passthrough', () => {\n it('uses req.user set by McpOAuthGuard', async () => {\n const guardUser: AuthUser = { id: 'guard-user', name: 'Guard', email: 'g@test.com' };\n const req = createMockRequest({ authorization: 'Bearer some-token' });\n (req as any).user = guardUser;\n\n const mcpRequest: McpRequest = { jsonrpc: '2.0', id: 1, method: 'tools/list' };\n\n await controller.handleOrgMcpRequest(req, 'my-org', 'my-profile', mcpRequest);\n\n expect(proxyService.handleRequestByOrgSlug).toHaveBeenCalledWith(\n 'my-profile',\n 'my-org',\n mcpRequest,\n 'guard-user'\n );\n });\n });\n\n describe('org-scoped profile lookup', () => {\n const userA: AuthUser = { id: 'user-a', name: 'User A', email: 'a@test.com' };\n const userB: AuthUser = { id: 'user-b', name: 'User B', email: 'b@test.com' };\n\n it('org slug and user are passed through to proxy service', async () => {\n const req = createMockRequest({ authorization: 'Bearer token-a' });\n (req as any).user = userA;\n const mcpRequest: McpRequest = { jsonrpc: '2.0', id: 1, method: 'tools/list' };\n\n await controller.handleOrgMcpRequest(req, 'org-a', 'user-a-profile', mcpRequest);\n\n expect(proxyService.handleRequestByOrgSlug).toHaveBeenCalledWith(\n 'user-a-profile',\n 'org-a',\n mcpRequest,\n 'user-a'\n );\n });\n\n it('different users get different userId passed to proxy', async () => {\n const reqA = createMockRequest({ authorization: 'Bearer token-a' });\n (reqA as any).user = userA;\n const mcpReq: McpRequest = { jsonrpc: '2.0', id: 1, method: 'tools/list' };\n\n await controller.handleOrgMcpRequest(reqA, 'shared-org', 'shared-profile', mcpReq);\n\n const reqB = createMockRequest({ authorization: 'Bearer token-b' });\n (reqB as any).user = userB;\n\n await controller.handleOrgMcpRequest(reqB, 'shared-org', 'shared-profile', mcpReq);\n\n expect(proxyService.handleRequestByOrgSlug).toHaveBeenNthCalledWith(\n 1,\n 'shared-profile',\n 'shared-org',\n mcpReq,\n 'user-a'\n );\n expect(proxyService.handleRequestByOrgSlug).toHaveBeenNthCalledWith(\n 2,\n 'shared-profile',\n 'shared-org',\n mcpReq,\n 'user-b'\n );\n });\n\n it('gateway endpoint uses default profile with user scoping', async () => {\n settingsService.getDefaultGatewayProfile.mockResolvedValue('my-default');\n\n const req = createMockRequest({ authorization: 'Bearer token-a' });\n (req as any).user = userA;\n const mcpRequest: McpRequest = { jsonrpc: '2.0', id: 1, method: 'tools/list' };\n\n await controller.handleGatewayRequest(req, mcpRequest);\n\n expect(proxyService.handleRequest).toHaveBeenCalledWith('my-default', mcpRequest, 'user-a');\n });\n\n it('gateway wraps NotFoundException with descriptive message', async () => {\n settingsService.getDefaultGatewayProfile.mockResolvedValue('missing-profile');\n proxyService.handleRequest.mockRejectedValue(\n new NotFoundException('Profile \"missing-profile\" not found')\n );\n\n const req = createMockRequest({ authorization: 'Bearer token-a' });\n (req as any).user = userA;\n const mcpRequest: McpRequest = { jsonrpc: '2.0', id: 1, method: 'tools/list' };\n\n await expect(controller.handleGatewayRequest(req, mcpRequest)).rejects.toThrow(\n NotFoundException\n );\n });\n\n it('org-scoped profile info endpoint uses orgSlug', async () => {\n await controller.getOrgProfileInfo('my-org', 'my-profile');\n\n expect(proxyService.getProfileInfoByOrgSlug).toHaveBeenCalledWith('my-profile', 'my-org');\n });\n });\n});\n"],"names":["NotFoundException","UnauthorizedException","beforeEach","describe","expect","it","vi","McpOAuthGuard","ProxyController","createMockAuthService","getAuth","fn","getSession","mockResolvedValue","validateMcpToken","getUserOrganizations","onModuleInit","createMockConfigService","overrides","defaults","BETTER_AUTH_URL","config","get","key","createMockProxyService","handleRequest","jsonrpc","id","result","tools","handleRequestByOrgSlug","getProfileInfo","serverStatus","total","connected","servers","getProfileInfoByOrgSlug","getToolsForServer","createMockSettingsService","getDefaultGatewayProfile","getSetting","setSetting","createMockEventEmitter","emit","on","off","createMockRequest","headers","query","user","undefined","createMockExecutionContext","req","switchToHttp","getRequest","getResponse","getHandler","getClass","guard","authService","configService","ctx","canActivate","rejects","toThrow","error","wwwAuthenticate","toContain","authorization","toHaveBeenCalledWith","name","email","toBe","toEqual","access_token","session","userId","cookie","not","toHaveBeenCalled","controller","proxyService","settingsService","eventEmitter","guardUser","mcpRequest","method","handleOrgMcpRequest","userA","userB","reqA","mcpReq","reqB","toHaveBeenNthCalledWith","handleGatewayRequest","mockRejectedValue","getOrgProfileInfo"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/__tests__/integration/proxy-auth.test.ts"],"sourcesContent":["/**\n * Integration Tests: Proxy controller auth\n *\n * Verifies:\n * - McpOAuthGuard always enforces Bearer token\n * - WWW-Authenticate header with resource_metadata_uri on 401\n * - Valid tokens resolve user and pass through\n * - Org-scoped profile lookup through the proxy service\n * - Gateway endpoint uses default profile with user scoping\n */\n\nimport { NotFoundException, UnauthorizedException, type ExecutionContext } from '@nestjs/common';\nimport type { ConfigService } from '@nestjs/config';\nimport type { EventEmitter2 } from '@nestjs/event-emitter';\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\nimport { type AuthService, type AuthUser } from '../../modules/auth/auth.service.js';\nimport type { McpRequest, McpResponse, ProxyService } from '../../modules/proxy/proxy.service.js';\nimport type { SettingsService } from '../../modules/settings/settings.service.js';\n\n// Dynamic import to handle ESM\nconst { McpOAuthGuard } = await import('../../modules/auth/mcp-oauth.guard.js');\nconst { ProxyController } = await import('../../modules/proxy/proxy.controller.js');\n\n// ────────────────────────────────────────────────\n// Mock factories\n// ────────────────────────────────────────────────\n\nfunction createMockAuthService() {\n return {\n getAuth: vi.fn(),\n getSession: vi.fn().mockResolvedValue(null),\n validateMcpToken: vi.fn().mockResolvedValue(null),\n getUserOrganizations: vi.fn().mockResolvedValue([]),\n onModuleInit: vi.fn(),\n };\n}\n\nfunction createMockConfigService(overrides: Record<string, unknown> = {}) {\n const defaults: Record<string, unknown> = {\n 'app.port': 3001,\n BETTER_AUTH_URL: 'http://localhost:3001',\n };\n const config = { ...defaults, ...overrides };\n return {\n get: vi.fn((key: string) => config[key]),\n };\n}\n\nfunction createMockProxyService() {\n return {\n handleRequest: vi.fn().mockResolvedValue({\n jsonrpc: '2.0',\n id: 1,\n result: { tools: [] },\n } as McpResponse),\n handleRequestByOrgSlug: vi.fn().mockResolvedValue({\n jsonrpc: '2.0',\n id: 1,\n result: { tools: [] },\n } as McpResponse),\n getProfileInfo: vi.fn().mockResolvedValue({\n tools: [],\n serverStatus: { total: 0, connected: 0, servers: {} },\n }),\n getProfileInfoByOrgSlug: vi.fn().mockResolvedValue({\n tools: [],\n serverStatus: { total: 0, connected: 0, servers: {} },\n }),\n getToolsForServer: vi.fn().mockResolvedValue([]),\n };\n}\n\nfunction createMockSettingsService() {\n return {\n getDefaultGatewayProfile: vi.fn().mockResolvedValue('default'),\n getSetting: vi.fn(),\n setSetting: vi.fn(),\n };\n}\n\nfunction createMockEventEmitter() {\n return {\n emit: vi.fn(),\n on: vi.fn(),\n off: vi.fn(),\n };\n}\n\ntype RequestWithUser = import('express').Request & { user?: AuthUser };\n\nfunction createMockRequest(\n headers: Record<string, string> = {},\n query: Record<string, string> = {}\n): RequestWithUser {\n return {\n headers,\n query,\n on: vi.fn(),\n user: undefined,\n } as unknown as RequestWithUser;\n}\n\nfunction createMockResponse() {\n return {\n json: vi.fn((payload: unknown) => payload),\n setHeader: vi.fn(),\n write: vi.fn(),\n } as unknown as import('express').Response;\n}\n\nfunction createMockExecutionContext(req: import('express').Request): ExecutionContext {\n return {\n switchToHttp: () => ({\n getRequest: () => req,\n getResponse: () => ({}),\n }),\n getHandler: () => ({}),\n getClass: () => ({}),\n } as unknown as ExecutionContext;\n}\n\n// ────────────────────────────────────────────────\n// McpOAuthGuard tests\n// ────────────────────────────────────────────────\n\ndescribe('McpOAuthGuard', () => {\n let guard: InstanceType<typeof McpOAuthGuard>;\n let authService: ReturnType<typeof createMockAuthService>;\n\n beforeEach(() => {\n authService = createMockAuthService();\n const configService = createMockConfigService();\n guard = new McpOAuthGuard(\n authService as unknown as AuthService,\n configService as unknown as ConfigService\n );\n });\n\n it('rejects request without Bearer token with 401', async () => {\n const req = createMockRequest();\n const ctx = createMockExecutionContext(req);\n\n await expect(guard.canActivate(ctx)).rejects.toThrow(UnauthorizedException);\n });\n\n it('returns WWW-Authenticate header on missing token', async () => {\n const req = createMockRequest();\n const ctx = createMockExecutionContext(req);\n\n try {\n await guard.canActivate(ctx);\n } catch (error: unknown) {\n const authError = error as { wwwAuthenticate?: string };\n expect(authError.wwwAuthenticate).toContain('resource_metadata=');\n expect(authError.wwwAuthenticate).toContain('resource_metadata_uri=');\n expect(authError.wwwAuthenticate).toContain('/.well-known/oauth-protected-resource');\n }\n });\n\n it('rejects invalid Bearer token', async () => {\n authService.validateMcpToken.mockResolvedValue(null);\n const req = createMockRequest({ authorization: 'Bearer bad-token' });\n const ctx = createMockExecutionContext(req);\n\n await expect(guard.canActivate(ctx)).rejects.toThrow(UnauthorizedException);\n expect(authService.validateMcpToken).toHaveBeenCalledWith('bad-token');\n });\n\n it('allows valid Bearer token and attaches user', async () => {\n const user: AuthUser = { id: 'user-1', name: 'Test', email: 'test@example.com' };\n authService.validateMcpToken.mockResolvedValue(user);\n const req = createMockRequest({ authorization: 'Bearer valid-token' });\n const ctx = createMockExecutionContext(req);\n\n const result = await guard.canActivate(ctx);\n\n expect(result).toBe(true);\n expect(req.user).toEqual(user);\n });\n\n it('accepts access_token query param (SSE fallback)', async () => {\n const user: AuthUser = { id: 'user-1', name: 'Test', email: 'test@example.com' };\n authService.validateMcpToken.mockResolvedValue(user);\n const req = createMockRequest({}, { access_token: 'sse-token' });\n const ctx = createMockExecutionContext(req);\n\n const result = await guard.canActivate(ctx);\n\n expect(result).toBe(true);\n expect(authService.validateMcpToken).toHaveBeenCalledWith('sse-token');\n });\n\n it('accepts session cookie when no Bearer token is present', async () => {\n const user: AuthUser = { id: 'session-user', name: 'Session', email: 'session@example.com' };\n authService.getSession.mockResolvedValue({ user, session: { id: 's1', userId: user.id } });\n const req = createMockRequest({ cookie: 'better-auth.session_token=abc123' });\n const ctx = createMockExecutionContext(req);\n\n const result = await guard.canActivate(ctx);\n\n expect(result).toBe(true);\n expect(req.user).toEqual(user);\n expect(authService.validateMcpToken).not.toHaveBeenCalled();\n });\n\n it('rejects when neither Bearer token nor session cookie is valid', async () => {\n authService.getSession.mockResolvedValue(null);\n const req = createMockRequest({ cookie: 'better-auth.session_token=expired' });\n const ctx = createMockExecutionContext(req);\n\n await expect(guard.canActivate(ctx)).rejects.toThrow(UnauthorizedException);\n });\n\n it('does not fall through to session cookie when Bearer token is invalid', async () => {\n const user: AuthUser = { id: 'session-user', name: 'Session', email: 'session@example.com' };\n authService.validateMcpToken.mockResolvedValue(null);\n authService.getSession.mockResolvedValue({ user, session: { id: 's1', userId: user.id } });\n const req = createMockRequest({ authorization: 'Bearer bad-token' });\n const ctx = createMockExecutionContext(req);\n\n await expect(guard.canActivate(ctx)).rejects.toThrow(UnauthorizedException);\n expect(authService.getSession).not.toHaveBeenCalled();\n });\n});\n\n// ────────────────────────────────────────────────\n// Proxy controller auth tests\n// ────────────────────────────────────────────────\n\ndescribe('Proxy controller auth', () => {\n let controller: InstanceType<typeof ProxyController>;\n let proxyService: ReturnType<typeof createMockProxyService>;\n let settingsService: ReturnType<typeof createMockSettingsService>;\n let eventEmitter: ReturnType<typeof createMockEventEmitter>;\n\n beforeEach(() => {\n proxyService = createMockProxyService();\n settingsService = createMockSettingsService();\n eventEmitter = createMockEventEmitter();\n\n controller = new ProxyController(\n proxyService as unknown as ProxyService,\n settingsService as unknown as SettingsService,\n eventEmitter as unknown as EventEmitter2\n );\n });\n\n describe('guard-authenticated user passthrough', () => {\n it('uses req.user set by McpOAuthGuard', async () => {\n const guardUser: AuthUser = { id: 'guard-user', name: 'Guard', email: 'g@test.com' };\n const req = createMockRequest({ authorization: 'Bearer some-token' });\n req.user = guardUser;\n\n const mcpRequest: McpRequest = { jsonrpc: '2.0', id: 1, method: 'tools/list' };\n\n await controller.handleOrgMcpRequest(req, 'my-org', 'my-profile', mcpRequest);\n\n expect(proxyService.handleRequestByOrgSlug).toHaveBeenCalledWith(\n 'my-profile',\n 'my-org',\n mcpRequest,\n 'guard-user'\n );\n });\n });\n\n describe('org-scoped profile lookup', () => {\n const userA: AuthUser = { id: 'user-a', name: 'User A', email: 'a@test.com' };\n const userB: AuthUser = { id: 'user-b', name: 'User B', email: 'b@test.com' };\n\n it('org slug and user are passed through to proxy service', async () => {\n const req = createMockRequest({ authorization: 'Bearer token-a' });\n req.user = userA;\n const mcpRequest: McpRequest = { jsonrpc: '2.0', id: 1, method: 'tools/list' };\n\n await controller.handleOrgMcpRequest(req, 'org-a', 'user-a-profile', mcpRequest);\n\n expect(proxyService.handleRequestByOrgSlug).toHaveBeenCalledWith(\n 'user-a-profile',\n 'org-a',\n mcpRequest,\n 'user-a'\n );\n });\n\n it('different users get different userId passed to proxy', async () => {\n const reqA = createMockRequest({ authorization: 'Bearer token-a' });\n reqA.user = userA;\n const mcpReq: McpRequest = { jsonrpc: '2.0', id: 1, method: 'tools/list' };\n\n await controller.handleOrgMcpRequest(reqA, 'shared-org', 'shared-profile', mcpReq);\n\n const reqB = createMockRequest({ authorization: 'Bearer token-b' });\n reqB.user = userB;\n\n await controller.handleOrgMcpRequest(reqB, 'shared-org', 'shared-profile', mcpReq);\n\n expect(proxyService.handleRequestByOrgSlug).toHaveBeenNthCalledWith(\n 1,\n 'shared-profile',\n 'shared-org',\n mcpReq,\n 'user-a'\n );\n expect(proxyService.handleRequestByOrgSlug).toHaveBeenNthCalledWith(\n 2,\n 'shared-profile',\n 'shared-org',\n mcpReq,\n 'user-b'\n );\n });\n\n it('gateway endpoint uses default profile with user scoping', async () => {\n settingsService.getDefaultGatewayProfile.mockResolvedValue('my-default');\n\n const req = createMockRequest({ authorization: 'Bearer token-a' });\n req.user = userA;\n const mcpRequest: McpRequest = { jsonrpc: '2.0', id: 1, method: 'tools/list' };\n\n await controller.handleGatewayRequest(req, mcpRequest);\n\n expect(proxyService.handleRequest).toHaveBeenCalledWith('my-default', mcpRequest, 'user-a');\n });\n\n it('gateway wraps NotFoundException with descriptive message', async () => {\n settingsService.getDefaultGatewayProfile.mockResolvedValue('missing-profile');\n proxyService.handleRequest.mockRejectedValue(\n new NotFoundException('Profile \"missing-profile\" not found')\n );\n\n const req = createMockRequest({ authorization: 'Bearer token-a' });\n req.user = userA;\n const mcpRequest: McpRequest = { jsonrpc: '2.0', id: 1, method: 'tools/list' };\n\n await expect(controller.handleGatewayRequest(req, mcpRequest)).rejects.toThrow(\n NotFoundException\n );\n });\n\n it('org-scoped profile info endpoint uses orgSlug', async () => {\n const req = createMockRequest({ authorization: 'Bearer token-a' });\n req.user = userA;\n\n await controller.getOrgProfileInfo('my-org', 'my-profile', req);\n\n expect(proxyService.getProfileInfoByOrgSlug).toHaveBeenCalledWith(\n 'my-profile',\n 'my-org',\n 'user-a'\n );\n });\n\n it('org-scoped profile endpoint reports tool count from user-scoped info lookup', async () => {\n proxyService.getProfileInfoByOrgSlug.mockResolvedValue({\n tools: [{ name: 'tool-a', description: 'A' }, { name: 'tool-b', description: 'B' }],\n serverStatus: { total: 1, connected: 1, servers: {} },\n });\n\n const req = createMockRequest({ authorization: 'Bearer token-a' });\n req.user = userA;\n const res = createMockResponse();\n\n await controller.getOrgMcpEndpoint('my-org', 'my-profile', req, res);\n\n expect(proxyService.getProfileInfoByOrgSlug).toHaveBeenCalledWith(\n 'my-profile',\n 'my-org',\n 'user-a'\n );\n expect(res.json).toHaveBeenCalledWith(\n expect.objectContaining({\n profile: expect.objectContaining({\n name: 'my-profile',\n toolCount: 2,\n serverCount: 1,\n connectedServers: 1,\n }),\n })\n );\n });\n });\n});\n"],"names":["NotFoundException","UnauthorizedException","beforeEach","describe","expect","it","vi","McpOAuthGuard","ProxyController","createMockAuthService","getAuth","fn","getSession","mockResolvedValue","validateMcpToken","getUserOrganizations","onModuleInit","createMockConfigService","overrides","defaults","BETTER_AUTH_URL","config","get","key","createMockProxyService","handleRequest","jsonrpc","id","result","tools","handleRequestByOrgSlug","getProfileInfo","serverStatus","total","connected","servers","getProfileInfoByOrgSlug","getToolsForServer","createMockSettingsService","getDefaultGatewayProfile","getSetting","setSetting","createMockEventEmitter","emit","on","off","createMockRequest","headers","query","user","undefined","createMockResponse","json","payload","setHeader","write","createMockExecutionContext","req","switchToHttp","getRequest","getResponse","getHandler","getClass","guard","authService","configService","ctx","canActivate","rejects","toThrow","error","authError","wwwAuthenticate","toContain","authorization","toHaveBeenCalledWith","name","email","toBe","toEqual","access_token","session","userId","cookie","not","toHaveBeenCalled","controller","proxyService","settingsService","eventEmitter","guardUser","mcpRequest","method","handleOrgMcpRequest","userA","userB","reqA","mcpReq","reqB","toHaveBeenNthCalledWith","handleGatewayRequest","mockRejectedValue","getOrgProfileInfo","description","res","getOrgMcpEndpoint","objectContaining","profile","toolCount","serverCount","connectedServers"],"mappings":"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"}
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Tests for DebugService filtering behavior
3
+ */ import { beforeEach, describe, expect, it, vi } from "vitest";
4
+ import { DebugService } from "../../modules/debug/debug.service.js";
5
+ describe('DebugService', ()=>{
6
+ let service;
7
+ let prisma;
8
+ beforeEach(()=>{
9
+ prisma = {
10
+ debugLog: {
11
+ findMany: vi.fn().mockResolvedValue([]),
12
+ count: vi.fn().mockResolvedValue(0),
13
+ create: vi.fn(),
14
+ update: vi.fn(),
15
+ deleteMany: vi.fn()
16
+ }
17
+ };
18
+ service = new DebugService(prisma);
19
+ });
20
+ it('applies combined filters to the Prisma where clause', async ()=>{
21
+ const since = '2026-03-18T08:00:00.000Z';
22
+ const until = '2026-03-18T12:00:00.000Z';
23
+ await service.getLogs({
24
+ profileId: 'profile-1',
25
+ mcpServerId: 'server-1',
26
+ requestType: 'tools/call',
27
+ status: 'success',
28
+ since,
29
+ until,
30
+ page: '2',
31
+ limit: '25'
32
+ });
33
+ expect(prisma.debugLog.findMany).toHaveBeenCalledWith(expect.objectContaining({
34
+ where: {
35
+ profileId: 'profile-1',
36
+ mcpServerId: 'server-1',
37
+ requestType: 'tools/call',
38
+ status: 'success',
39
+ createdAt: {
40
+ gte: new Date(since),
41
+ lte: new Date(until)
42
+ }
43
+ },
44
+ take: 25,
45
+ skip: 25
46
+ }));
47
+ expect(prisma.debugLog.count).toHaveBeenCalledWith({
48
+ where: {
49
+ profileId: 'profile-1',
50
+ mcpServerId: 'server-1',
51
+ requestType: 'tools/call',
52
+ status: 'success',
53
+ createdAt: {
54
+ gte: new Date(since),
55
+ lte: new Date(until)
56
+ }
57
+ }
58
+ });
59
+ });
60
+ it('returns stable pagination metadata for page-based queries', async ()=>{
61
+ prisma.debugLog.count.mockResolvedValueOnce(55);
62
+ const result = await service.getLogs({
63
+ page: '3',
64
+ limit: '20'
65
+ });
66
+ expect(result).toEqual(expect.objectContaining({
67
+ logs: [],
68
+ total: 55,
69
+ page: 3,
70
+ limit: 20,
71
+ totalPages: 3
72
+ }));
73
+ });
74
+ it('supports offset as a compatibility shim when page is omitted', async ()=>{
75
+ await service.getLogs({
76
+ offset: '40',
77
+ limit: '20'
78
+ });
79
+ expect(prisma.debugLog.findMany).toHaveBeenCalledWith(expect.objectContaining({
80
+ take: 20,
81
+ skip: 40
82
+ }));
83
+ });
84
+ });
85
+
86
+ //# sourceMappingURL=debug.service.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/__tests__/unit/debug.service.test.ts"],"sourcesContent":["/**\n * Tests for DebugService filtering behavior\n */\n\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\nimport type { PrismaService } from '../../modules/database/prisma.service.js';\nimport { DebugService } from '../../modules/debug/debug.service.js';\n\ndescribe('DebugService', () => {\n let service: DebugService;\n let prisma: {\n debugLog: {\n findMany: ReturnType<typeof vi.fn>;\n count: ReturnType<typeof vi.fn>;\n create: ReturnType<typeof vi.fn>;\n update: ReturnType<typeof vi.fn>;\n deleteMany: ReturnType<typeof vi.fn>;\n };\n };\n\n beforeEach(() => {\n prisma = {\n debugLog: {\n findMany: vi.fn().mockResolvedValue([]),\n count: vi.fn().mockResolvedValue(0),\n create: vi.fn(),\n update: vi.fn(),\n deleteMany: vi.fn(),\n },\n };\n\n service = new DebugService(prisma as unknown as PrismaService);\n });\n\n it('applies combined filters to the Prisma where clause', async () => {\n const since = '2026-03-18T08:00:00.000Z';\n const until = '2026-03-18T12:00:00.000Z';\n\n await service.getLogs({\n profileId: 'profile-1',\n mcpServerId: 'server-1',\n requestType: 'tools/call',\n status: 'success',\n since,\n until,\n page: '2',\n limit: '25',\n });\n\n expect(prisma.debugLog.findMany).toHaveBeenCalledWith(\n expect.objectContaining({\n where: {\n profileId: 'profile-1',\n mcpServerId: 'server-1',\n requestType: 'tools/call',\n status: 'success',\n createdAt: {\n gte: new Date(since),\n lte: new Date(until),\n },\n },\n take: 25,\n skip: 25,\n })\n );\n expect(prisma.debugLog.count).toHaveBeenCalledWith({\n where: {\n profileId: 'profile-1',\n mcpServerId: 'server-1',\n requestType: 'tools/call',\n status: 'success',\n createdAt: {\n gte: new Date(since),\n lte: new Date(until),\n },\n },\n });\n });\n\n it('returns stable pagination metadata for page-based queries', async () => {\n prisma.debugLog.count.mockResolvedValueOnce(55);\n\n const result = await service.getLogs({\n page: '3',\n limit: '20',\n });\n\n expect(result).toEqual(\n expect.objectContaining({\n logs: [],\n total: 55,\n page: 3,\n limit: 20,\n totalPages: 3,\n })\n );\n });\n\n it('supports offset as a compatibility shim when page is omitted', async () => {\n await service.getLogs({\n offset: '40',\n limit: '20',\n });\n\n expect(prisma.debugLog.findMany).toHaveBeenCalledWith(\n expect.objectContaining({\n take: 20,\n skip: 40,\n })\n );\n });\n});\n"],"names":["beforeEach","describe","expect","it","vi","DebugService","service","prisma","debugLog","findMany","fn","mockResolvedValue","count","create","update","deleteMany","since","until","getLogs","profileId","mcpServerId","requestType","status","page","limit","toHaveBeenCalledWith","objectContaining","where","createdAt","gte","Date","lte","take","skip","mockResolvedValueOnce","result","toEqual","logs","total","totalPages","offset"],"mappings":"AAAA;;CAEC,GAED,SAASA,UAAU,EAAEC,QAAQ,EAAEC,MAAM,EAAEC,EAAE,EAAEC,EAAE,QAAQ,SAAS;AAE9D,SAASC,YAAY,QAAQ,uCAAuC;AAEpEJ,SAAS,gBAAgB;IACvB,IAAIK;IACJ,IAAIC;IAUJP,WAAW;QACTO,SAAS;YACPC,UAAU;gBACRC,UAAUL,GAAGM,EAAE,GAAGC,iBAAiB,CAAC,EAAE;gBACtCC,OAAOR,GAAGM,EAAE,GAAGC,iBAAiB,CAAC;gBACjCE,QAAQT,GAAGM,EAAE;gBACbI,QAAQV,GAAGM,EAAE;gBACbK,YAAYX,GAAGM,EAAE;YACnB;QACF;QAEAJ,UAAU,IAAID,aAAaE;IAC7B;IAEAJ,GAAG,uDAAuD;QACxD,MAAMa,QAAQ;QACd,MAAMC,QAAQ;QAEd,MAAMX,QAAQY,OAAO,CAAC;YACpBC,WAAW;YACXC,aAAa;YACbC,aAAa;YACbC,QAAQ;YACRN;YACAC;YACAM,MAAM;YACNC,OAAO;QACT;QAEAtB,OAAOK,OAAOC,QAAQ,CAACC,QAAQ,EAAEgB,oBAAoB,CACnDvB,OAAOwB,gBAAgB,CAAC;YACtBC,OAAO;gBACLR,WAAW;gBACXC,aAAa;gBACbC,aAAa;gBACbC,QAAQ;gBACRM,WAAW;oBACTC,KAAK,IAAIC,KAAKd;oBACde,KAAK,IAAID,KAAKb;gBAChB;YACF;YACAe,MAAM;YACNC,MAAM;QACR;QAEF/B,OAAOK,OAAOC,QAAQ,CAACI,KAAK,EAAEa,oBAAoB,CAAC;YACjDE,OAAO;gBACLR,WAAW;gBACXC,aAAa;gBACbC,aAAa;gBACbC,QAAQ;gBACRM,WAAW;oBACTC,KAAK,IAAIC,KAAKd;oBACde,KAAK,IAAID,KAAKb;gBAChB;YACF;QACF;IACF;IAEAd,GAAG,6DAA6D;QAC9DI,OAAOC,QAAQ,CAACI,KAAK,CAACsB,qBAAqB,CAAC;QAE5C,MAAMC,SAAS,MAAM7B,QAAQY,OAAO,CAAC;YACnCK,MAAM;YACNC,OAAO;QACT;QAEAtB,OAAOiC,QAAQC,OAAO,CACpBlC,OAAOwB,gBAAgB,CAAC;YACtBW,MAAM,EAAE;YACRC,OAAO;YACPf,MAAM;YACNC,OAAO;YACPe,YAAY;QACd;IAEJ;IAEApC,GAAG,gEAAgE;QACjE,MAAMG,QAAQY,OAAO,CAAC;YACpBsB,QAAQ;YACRhB,OAAO;QACT;QAEAtB,OAAOK,OAAOC,QAAQ,CAACC,QAAQ,EAAEgB,oBAAoB,CACnDvB,OAAOwB,gBAAgB,CAAC;YACtBM,MAAM;YACNC,MAAM;QACR;IAEJ;AACF"}
@@ -66,7 +66,15 @@ describe('ProfilesService', ()=>{
66
66
  $transaction: vi.fn().mockImplementation((cb)=>cb(prisma))
67
67
  };
68
68
  proxyService = {
69
- getToolsForServer: vi.fn().mockResolvedValue([])
69
+ getToolsForServer: vi.fn().mockResolvedValue([]),
70
+ getProfileInfoById: vi.fn().mockResolvedValue({
71
+ tools: [],
72
+ serverStatus: {
73
+ total: 0,
74
+ connected: 0,
75
+ servers: {}
76
+ }
77
+ })
70
78
  };
71
79
  sharingService = {
72
80
  getSharedResourceIds: vi.fn().mockResolvedValue([]),
@@ -517,6 +525,64 @@ describe('ProfilesService', ()=>{
517
525
  });
518
526
  });
519
527
  // ---------------------------------------------------------------------------
528
+ // getInfo
529
+ // ---------------------------------------------------------------------------
530
+ describe('getInfo', ()=>{
531
+ it('should return aggregated info for an accessible profile', async ()=>{
532
+ prisma.profile.findUnique.mockResolvedValueOnce({
533
+ userId,
534
+ organizationId: orgId
535
+ });
536
+ proxyService.getProfileInfoById.mockResolvedValueOnce({
537
+ tools: [
538
+ {
539
+ name: 'tool-a',
540
+ description: 'Tool A'
541
+ }
542
+ ],
543
+ serverStatus: {
544
+ total: 1,
545
+ connected: 1,
546
+ servers: {
547
+ [serverId]: {
548
+ connected: true,
549
+ toolCount: 1
550
+ }
551
+ }
552
+ }
553
+ });
554
+ const result = await service.getInfo(profileId, userId, orgId);
555
+ expect(proxyService.getProfileInfoById).toHaveBeenCalledWith(profileId);
556
+ expect(result).toEqual({
557
+ tools: [
558
+ {
559
+ name: 'tool-a',
560
+ description: 'Tool A'
561
+ }
562
+ ],
563
+ serverStatus: {
564
+ total: 1,
565
+ connected: 1,
566
+ servers: {
567
+ [serverId]: {
568
+ connected: true,
569
+ toolCount: 1
570
+ }
571
+ }
572
+ }
573
+ });
574
+ });
575
+ it('should reject access to an unshared profile', async ()=>{
576
+ prisma.profile.findUnique.mockResolvedValueOnce({
577
+ userId: 'other-user',
578
+ organizationId: orgId
579
+ });
580
+ sharingService.isSharedWith.mockResolvedValueOnce(false);
581
+ await expect(service.getInfo(profileId, userId, orgId)).rejects.toThrow(ForbiddenException);
582
+ expect(proxyService.getProfileInfoById).not.toHaveBeenCalled();
583
+ });
584
+ });
585
+ // ---------------------------------------------------------------------------
520
586
  // addServer
521
587
  // ---------------------------------------------------------------------------
522
588
  describe('addServer', ()=>{