This is page 11 of 11. Use http://codebase.md/sapientpants/sonarqube-mcp-server?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .adr-dir
├── .changeset
│ ├── config.json
│ └── README.md
├── .claude
│ ├── commands
│ │ ├── analyze-and-fix-github-issue.md
│ │ ├── fix-sonarqube-issues.md
│ │ ├── implement-github-issue.md
│ │ ├── release.md
│ │ ├── spec-feature.md
│ │ └── update-dependencies.md
│ ├── hooks
│ │ └── block-git-no-verify.ts
│ └── settings.json
├── .dockerignore
├── .github
│ ├── actionlint.yaml
│ ├── changeset.yml
│ ├── dependabot.yml
│ ├── ISSUE_TEMPLATE
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ ├── pull_request_template.md
│ ├── scripts
│ │ ├── determine-artifact.sh
│ │ └── version-and-release.js
│ ├── workflows
│ │ ├── codeql.yml
│ │ ├── main.yml
│ │ ├── pr.yml
│ │ ├── publish.yml
│ │ ├── reusable-docker.yml
│ │ ├── reusable-security.yml
│ │ └── reusable-validate.yml
│ └── WORKFLOWS.md
├── .gitignore
├── .husky
│ ├── commit-msg
│ └── pre-commit
├── .markdownlint.yaml
├── .markdownlintignore
├── .npmrc
├── .prettierignore
├── .prettierrc
├── .trivyignore
├── .yaml-lint.yml
├── .yamllintignore
├── CHANGELOG.md
├── changes.md
├── CLAUDE.md
├── CODE_OF_CONDUCT.md
├── commitlint.config.js
├── COMPATIBILITY.md
├── CONTRIBUTING.md
├── docker-compose.yml
├── Dockerfile
├── docs
│ ├── architecture
│ │ └── decisions
│ │ ├── 0001-record-architecture-decisions.md
│ │ ├── 0002-use-node-js-with-typescript.md
│ │ ├── 0003-adopt-model-context-protocol-for-sonarqube-integration.md
│ │ ├── 0004-use-sonarqube-web-api-client-for-all-sonarqube-interactions.md
│ │ ├── 0005-domain-driven-design-of-sonarqube-modules.md
│ │ ├── 0006-expose-sonarqube-features-as-mcp-tools.md
│ │ ├── 0007-support-multiple-authentication-methods-for-sonarqube.md
│ │ ├── 0008-use-environment-variables-for-configuration.md
│ │ ├── 0009-file-based-logging-to-avoid-stdio-conflicts.md
│ │ ├── 0010-use-stdio-transport-for-mcp-communication.md
│ │ ├── 0011-docker-containerization-for-deployment.md
│ │ ├── 0012-add-elicitation-support-for-interactive-user-input.md
│ │ ├── 0014-current-security-model-and-future-oauth2-considerations.md
│ │ ├── 0015-transport-architecture-refactoring.md
│ │ ├── 0016-http-transport-with-oauth-2-0-metadata-endpoints.md
│ │ ├── 0017-comprehensive-audit-logging-system.md
│ │ ├── 0018-add-comprehensive-monitoring-and-observability.md
│ │ ├── 0019-simplify-to-stdio-only-transport-for-mcp-gateway-deployment.md
│ │ ├── 0020-testing-framework-and-strategy-vitest-with-property-based-testing.md
│ │ ├── 0021-code-quality-toolchain-eslint-prettier-strict-typescript.md
│ │ ├── 0022-package-manager-choice-pnpm.md
│ │ ├── 0023-release-management-with-changesets.md
│ │ ├── 0024-ci-cd-platform-github-actions.md
│ │ ├── 0025-container-and-security-scanning-strategy.md
│ │ ├── 0026-circuit-breaker-pattern-with-opossum.md
│ │ ├── 0027-docker-image-publishing-strategy-ghcr-to-docker-hub.md
│ │ └── 0028-session-based-http-transport-with-server-sent-events.md
│ ├── architecture.md
│ ├── security.md
│ └── troubleshooting.md
├── eslint.config.js
├── examples
│ └── http-client.ts
├── jest.config.js
├── LICENSE
├── LICENSES.md
├── osv-scanner.toml
├── package.json
├── pnpm-lock.yaml
├── README.md
├── scripts
│ ├── actionlint.sh
│ ├── ci-local.sh
│ ├── load-test.sh
│ ├── README.md
│ ├── run-all-tests.sh
│ ├── scan-container.sh
│ ├── security-scan.sh
│ ├── setup.sh
│ ├── test-monitoring-integration.sh
│ └── validate-docs.sh
├── SECURITY.md
├── sonar-project.properties
├── src
│ ├── __tests__
│ │ ├── additional-coverage.test.ts
│ │ ├── advanced-index.test.ts
│ │ ├── assign-issue.test.ts
│ │ ├── auth-methods.test.ts
│ │ ├── boolean-string-transform.test.ts
│ │ ├── components.test.ts
│ │ ├── config
│ │ │ └── service-accounts.test.ts
│ │ ├── dependency-injection.test.ts
│ │ ├── direct-handlers.test.ts
│ │ ├── direct-lambdas.test.ts
│ │ ├── direct-schema-validation.test.ts
│ │ ├── domains
│ │ │ ├── components-domain-full.test.ts
│ │ │ ├── components-domain.test.ts
│ │ │ ├── hotspots-domain.test.ts
│ │ │ └── source-code-domain.test.ts
│ │ ├── environment-validation.test.ts
│ │ ├── error-handler.test.ts
│ │ ├── error-handling.test.ts
│ │ ├── errors.test.ts
│ │ ├── function-tests.test.ts
│ │ ├── handlers
│ │ │ ├── components-handler-integration.test.ts
│ │ │ └── projects-authorization.test.ts
│ │ ├── handlers.test.ts
│ │ ├── handlers.test.ts.skip
│ │ ├── index.test.ts
│ │ ├── issue-resolution-elicitation.test.ts
│ │ ├── issue-resolution.test.ts
│ │ ├── issue-transitions.test.ts
│ │ ├── issues-enhanced-search.test.ts
│ │ ├── issues-new-parameters.test.ts
│ │ ├── json-array-transform.test.ts
│ │ ├── lambda-functions.test.ts
│ │ ├── lambda-handlers.test.ts.skip
│ │ ├── logger.test.ts
│ │ ├── mapping-functions.test.ts
│ │ ├── mocked-environment.test.ts
│ │ ├── null-to-undefined.test.ts
│ │ ├── parameter-transformations-advanced.test.ts
│ │ ├── parameter-transformations.test.ts
│ │ ├── protocol-version.test.ts
│ │ ├── pull-request-transform.test.ts
│ │ ├── quality-gates.test.ts
│ │ ├── schema-parameter-transforms.test.ts
│ │ ├── schema-transformation-mocks.test.ts
│ │ ├── schema-transforms.test.ts
│ │ ├── schema-validators.test.ts
│ │ ├── schemas
│ │ │ ├── components-schema.test.ts
│ │ │ ├── hotspots-tools-schema.test.ts
│ │ │ └── issues-schema.test.ts
│ │ ├── sonarqube-elicitation.test.ts
│ │ ├── sonarqube.test.ts
│ │ ├── source-code.test.ts
│ │ ├── standalone-handlers.test.ts
│ │ ├── string-to-number-transform.test.ts
│ │ ├── tool-handler-lambdas.test.ts
│ │ ├── tool-handlers.test.ts
│ │ ├── tool-registration-schema.test.ts
│ │ ├── tool-registration-transforms.test.ts
│ │ ├── transformation-util.test.ts
│ │ ├── transports
│ │ │ ├── base.test.ts
│ │ │ ├── factory.test.ts
│ │ │ ├── http.test.ts
│ │ │ ├── session-manager.test.ts
│ │ │ └── stdio.test.ts
│ │ ├── utils
│ │ │ ├── retry.test.ts
│ │ │ └── transforms.test.ts
│ │ ├── zod-boolean-transform.test.ts
│ │ ├── zod-schema-transforms.test.ts
│ │ └── zod-transforms.test.ts
│ ├── config
│ │ ├── service-accounts.ts
│ │ └── versions.ts
│ ├── domains
│ │ ├── base.ts
│ │ ├── components.ts
│ │ ├── hotspots.ts
│ │ ├── index.ts
│ │ ├── issues.ts
│ │ ├── measures.ts
│ │ ├── metrics.ts
│ │ ├── projects.ts
│ │ ├── quality-gates.ts
│ │ ├── source-code.ts
│ │ └── system.ts
│ ├── errors.ts
│ ├── handlers
│ │ ├── components.ts
│ │ ├── hotspots.ts
│ │ ├── index.ts
│ │ ├── issues.ts
│ │ ├── measures.ts
│ │ ├── metrics.ts
│ │ ├── projects.ts
│ │ ├── quality-gates.ts
│ │ ├── source-code.ts
│ │ └── system.ts
│ ├── index.ts
│ ├── monitoring
│ │ ├── __tests__
│ │ │ └── circuit-breaker.test.ts
│ │ ├── circuit-breaker.ts
│ │ ├── health.ts
│ │ └── metrics.ts
│ ├── schemas
│ │ ├── common.ts
│ │ ├── components.ts
│ │ ├── hotspots-tools.ts
│ │ ├── hotspots.ts
│ │ ├── index.ts
│ │ ├── issues.ts
│ │ ├── measures.ts
│ │ ├── metrics.ts
│ │ ├── projects.ts
│ │ ├── quality-gates.ts
│ │ ├── source-code.ts
│ │ └── system.ts
│ ├── sonarqube.ts
│ ├── transports
│ │ ├── base.ts
│ │ ├── factory.ts
│ │ ├── http.ts
│ │ ├── index.ts
│ │ ├── session-manager.ts
│ │ └── stdio.ts
│ ├── types
│ │ ├── common.ts
│ │ ├── components.ts
│ │ ├── hotspots.ts
│ │ ├── index.ts
│ │ ├── issues.ts
│ │ ├── measures.ts
│ │ ├── metrics.ts
│ │ ├── projects.ts
│ │ ├── quality-gates.ts
│ │ ├── source-code.ts
│ │ └── system.ts
│ └── utils
│ ├── __tests__
│ │ ├── elicitation.test.ts
│ │ ├── pattern-matcher.test.ts
│ │ └── structured-response.test.ts
│ ├── client-factory.ts
│ ├── elicitation.ts
│ ├── error-handler.ts
│ ├── logger.ts
│ ├── parameter-mappers.ts
│ ├── pattern-matcher.ts
│ ├── retry.ts
│ ├── structured-response.ts
│ └── transforms.ts
├── test-http-transport.sh
├── tmp
│ └── .gitkeep
├── tsconfig.build.json
├── tsconfig.json
├── vitest.config.d.ts
├── vitest.config.js
├── vitest.config.js.map
└── vitest.config.ts
```
# Files
--------------------------------------------------------------------------------
/src/__tests__/index.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect, beforeEach, afterEach, beforeAll, vi } from 'vitest';
2 | import nock from 'nock';
3 | import { z } from 'zod';
4 |
5 | // Mock process.exit to prevent the test runner from exiting
6 | vi.spyOn(process, 'exit').mockImplementation(() => {
7 | throw new Error('process.exit called');
8 | });
9 |
10 | // Store original env vars
11 | const originalEnv = { ...process.env };
12 | // Mock environment variables
13 | process.env.SONARQUBE_TOKEN = 'test-token';
14 | process.env.SONARQUBE_URL = 'http://localhost:9000';
15 | // Mock SonarQube client responses
16 | beforeAll(() => {
17 | nock('http://localhost:9000')
18 | .persist()
19 | .get('/api/projects/search')
20 | .query(true)
21 | .reply(200, {
22 | projects: [
23 | {
24 | key: 'test-project',
25 | name: 'Test Project',
26 | qualifier: 'TRK',
27 | visibility: 'public',
28 | lastAnalysisDate: '2024-03-01',
29 | revision: 'abc123',
30 | managed: false,
31 | },
32 | ],
33 | paging: {
34 | pageIndex: 1,
35 | pageSize: 10,
36 | total: 1,
37 | },
38 | });
39 | nock('http://localhost:9000')
40 | .persist()
41 | .get('/api/metrics/search')
42 | .query(true)
43 | .reply(200, {
44 | metrics: [
45 | {
46 | key: 'test-metric',
47 | name: 'Test Metric',
48 | description: 'Test metric description',
49 | domain: 'test',
50 | type: 'INT',
51 | },
52 | ],
53 | paging: {
54 | pageIndex: 1,
55 | pageSize: 10,
56 | total: 1,
57 | },
58 | });
59 | nock('http://localhost:9000')
60 | .persist()
61 | .get('/api/issues/search')
62 | .query(true)
63 | .reply(200, {
64 | issues: [
65 | {
66 | key: 'test-issue',
67 | rule: 'test-rule',
68 | severity: 'MAJOR',
69 | component: 'test-component',
70 | project: 'test-project',
71 | line: 1,
72 | status: 'OPEN',
73 | message: 'Test issue',
74 | },
75 | ],
76 | components: [],
77 | rules: [],
78 | users: [],
79 | facets: [],
80 | paging: {
81 | pageIndex: 1,
82 | pageSize: 10,
83 | total: 1,
84 | },
85 | });
86 | nock('http://localhost:9000').persist().get('/api/system/health').reply(200, {
87 | health: 'GREEN',
88 | causes: [],
89 | });
90 | nock('http://localhost:9000').persist().get('/api/system/status').reply(200, {
91 | id: 'test-id',
92 | version: '10.3.0.82913',
93 | status: 'UP',
94 | });
95 | nock('http://localhost:9000').persist().get('/api/system/ping').reply(200, 'pong');
96 | // Mock SonarQube measures API responses
97 | nock('http://localhost:9000')
98 | .persist()
99 | .get('/api/measures/component')
100 | .query(true)
101 | .reply(200, {
102 | component: {
103 | key: 'test-project',
104 | name: 'Test Project',
105 | qualifier: 'TRK',
106 | measures: [
107 | {
108 | metric: 'coverage',
109 | value: '85.4',
110 | },
111 | {
112 | metric: 'bugs',
113 | value: '12',
114 | },
115 | ],
116 | },
117 | metrics: [
118 | {
119 | key: 'coverage',
120 | name: 'Coverage',
121 | description: 'Test coverage percentage',
122 | domain: 'Coverage',
123 | type: 'PERCENT',
124 | },
125 | {
126 | key: 'bugs',
127 | name: 'Bugs',
128 | description: 'Number of bugs',
129 | domain: 'Reliability',
130 | type: 'INT',
131 | },
132 | ],
133 | });
134 | nock('http://localhost:9000')
135 | .persist()
136 | .get('/api/measures/components')
137 | .query(true)
138 | .reply(200, {
139 | components: [
140 | {
141 | key: 'test-project-1',
142 | name: 'Test Project 1',
143 | qualifier: 'TRK',
144 | measures: [
145 | {
146 | metric: 'coverage',
147 | value: '85.4',
148 | },
149 | ],
150 | },
151 | {
152 | key: 'test-project-2',
153 | name: 'Test Project 2',
154 | qualifier: 'TRK',
155 | measures: [
156 | {
157 | metric: 'coverage',
158 | value: '72.1',
159 | },
160 | ],
161 | },
162 | ],
163 | metrics: [
164 | {
165 | key: 'coverage',
166 | name: 'Coverage',
167 | description: 'Test coverage percentage',
168 | domain: 'Coverage',
169 | type: 'PERCENT',
170 | },
171 | ],
172 | paging: {
173 | pageIndex: 1,
174 | pageSize: 100,
175 | total: 2,
176 | },
177 | });
178 | nock('http://localhost:9000')
179 | .persist()
180 | .get('/api/measures/search_history')
181 | .query(true)
182 | .reply(200, {
183 | measures: [
184 | {
185 | metric: 'coverage',
186 | history: [
187 | {
188 | date: '2023-01-01T00:00:00+0000',
189 | value: '85.4',
190 | },
191 | {
192 | date: '2023-02-01T00:00:00+0000',
193 | value: '87.2',
194 | },
195 | ],
196 | },
197 | ],
198 | paging: {
199 | pageIndex: 1,
200 | pageSize: 100,
201 | total: 1,
202 | },
203 | });
204 | });
205 | afterAll(() => {
206 | nock.cleanAll();
207 | });
208 | // Mock the handlers
209 | const mockHandlers = {
210 | handleSonarQubeProjects: (vi.fn() as any).mockResolvedValue({
211 | content: [
212 | {
213 | type: 'text' as const,
214 | text: JSON.stringify({
215 | projects: [
216 | {
217 | key: 'test-project',
218 | name: 'Test Project',
219 | qualifier: 'TRK',
220 | visibility: 'public',
221 | lastAnalysisDate: '2024-03-01',
222 | revision: 'abc123',
223 | managed: false,
224 | },
225 | ],
226 | paging: {
227 | pageIndex: 1,
228 | pageSize: 10,
229 | total: 1,
230 | },
231 | }),
232 | },
233 | ],
234 | }),
235 | handleSonarQubeGetMetrics: (vi.fn() as any).mockResolvedValue({
236 | content: [
237 | {
238 | type: 'text' as const,
239 | text: JSON.stringify({
240 | metrics: [
241 | {
242 | key: 'test-metric',
243 | name: 'Test Metric',
244 | description: 'Test metric description',
245 | domain: 'test',
246 | type: 'INT',
247 | },
248 | ],
249 | paging: {
250 | pageIndex: 1,
251 | pageSize: 10,
252 | total: 1,
253 | },
254 | }),
255 | },
256 | ],
257 | }),
258 | handleSonarQubeGetIssues: (vi.fn() as any).mockResolvedValue({
259 | content: [
260 | {
261 | type: 'text' as const,
262 | text: JSON.stringify({
263 | issues: [
264 | {
265 | key: 'test-issue',
266 | rule: 'test-rule',
267 | severity: 'MAJOR',
268 | component: 'test-component',
269 | project: 'test-project',
270 | line: 1,
271 | status: 'OPEN',
272 | message: 'Test issue',
273 | },
274 | ],
275 | components: [],
276 | rules: [],
277 | users: [],
278 | facets: [],
279 | paging: {
280 | pageIndex: 1,
281 | pageSize: 10,
282 | total: 1,
283 | },
284 | }),
285 | },
286 | ],
287 | }),
288 | handleSonarQubeGetHealth: (vi.fn() as any).mockResolvedValue({
289 | content: [
290 | {
291 | type: 'text' as const,
292 | text: JSON.stringify({
293 | health: 'GREEN',
294 | causes: [],
295 | }),
296 | },
297 | ],
298 | }),
299 | handleSonarQubeGetStatus: (vi.fn() as any).mockResolvedValue({
300 | content: [
301 | {
302 | type: 'text' as const,
303 | text: JSON.stringify({
304 | id: 'test-id',
305 | version: '10.3.0.82913',
306 | status: 'UP',
307 | }),
308 | },
309 | ],
310 | }),
311 | handleSonarQubePing: (vi.fn() as any).mockResolvedValue({
312 | content: [
313 | {
314 | type: 'text' as const,
315 | text: 'pong',
316 | },
317 | ],
318 | }),
319 | handleSonarQubeComponentMeasures: (vi.fn() as any).mockResolvedValue({
320 | content: [
321 | {
322 | type: 'text' as const,
323 | text: JSON.stringify({
324 | component: {
325 | key: 'test-project',
326 | name: 'Test Project',
327 | qualifier: 'TRK',
328 | measures: [
329 | {
330 | metric: 'coverage',
331 | value: '85.4',
332 | },
333 | {
334 | metric: 'bugs',
335 | value: '12',
336 | },
337 | ],
338 | },
339 | metrics: [
340 | {
341 | key: 'coverage',
342 | name: 'Coverage',
343 | description: 'Test coverage percentage',
344 | domain: 'Coverage',
345 | type: 'PERCENT',
346 | },
347 | {
348 | key: 'bugs',
349 | name: 'Bugs',
350 | description: 'Number of bugs',
351 | domain: 'Reliability',
352 | type: 'INT',
353 | },
354 | ],
355 | }),
356 | },
357 | ],
358 | }),
359 | handleSonarQubeComponentsMeasures: (vi.fn() as any).mockResolvedValue({
360 | content: [
361 | {
362 | type: 'text' as const,
363 | text: JSON.stringify({
364 | components: [
365 | {
366 | key: 'test-project-1',
367 | name: 'Test Project 1',
368 | qualifier: 'TRK',
369 | measures: [
370 | {
371 | metric: 'coverage',
372 | value: '85.4',
373 | },
374 | ],
375 | },
376 | {
377 | key: 'test-project-2',
378 | name: 'Test Project 2',
379 | qualifier: 'TRK',
380 | measures: [
381 | {
382 | metric: 'coverage',
383 | value: '72.1',
384 | },
385 | ],
386 | },
387 | ],
388 | metrics: [
389 | {
390 | key: 'coverage',
391 | name: 'Coverage',
392 | description: 'Test coverage percentage',
393 | domain: 'Coverage',
394 | type: 'PERCENT',
395 | },
396 | ],
397 | paging: {
398 | pageIndex: 1,
399 | pageSize: 100,
400 | total: 2,
401 | },
402 | }),
403 | },
404 | ],
405 | }),
406 | handleSonarQubeMeasuresHistory: (vi.fn() as any).mockResolvedValue({
407 | content: [
408 | {
409 | type: 'text' as const,
410 | text: JSON.stringify({
411 | measures: [
412 | {
413 | metric: 'coverage',
414 | history: [
415 | {
416 | date: '2023-01-01T00:00:00+0000',
417 | value: '85.4',
418 | },
419 | {
420 | date: '2023-02-01T00:00:00+0000',
421 | value: '87.2',
422 | },
423 | ],
424 | },
425 | ],
426 | paging: {
427 | pageIndex: 1,
428 | pageSize: 100,
429 | total: 1,
430 | },
431 | }),
432 | },
433 | ],
434 | }),
435 | };
436 | // Define the mock handlers but don't mock the entire module
437 | vi.mock('../index.js', async () => {
438 | // Get the original module
439 | const originalModule = await vi.importActual('../index.js');
440 | return {
441 | // Return everything from the original module
442 | ...originalModule,
443 | // But override these specific functions for tests that need mocks
444 | mcpServer: {
445 | ...(originalModule.mcpServer as Record<string, unknown>),
446 | connect: vi.fn(),
447 | },
448 | };
449 | });
450 | // Save environment variables
451 | // Using the originalEnv declared at the top of the file
452 | let mcpServer: any;
453 | let nullToUndefined: any;
454 | let handleSonarQubeProjects: any;
455 | let mapToSonarQubeParams: any;
456 | let handleSonarQubeGetIssues: any;
457 | let handleSonarQubeGetMetrics: any;
458 | let handleSonarQubeGetHealth: any;
459 | let handleSonarQubeGetStatus: any;
460 | let handleSonarQubePing: any;
461 | let handleSonarQubeComponentMeasures: any;
462 | let handleSonarQubeComponentsMeasures: any;
463 | let handleSonarQubeMeasuresHistory: any;
464 | let handleSonarQubeHotspots: any;
465 | let handleSonarQubeHotspot: any;
466 | let handleSonarQubeUpdateHotspotStatus: any;
467 | let qualityGateHandler: any;
468 | let qualityGateStatusHandler: any;
469 | let hotspotHandler: any;
470 | let updateHotspotStatusHandler: any;
471 | describe('MCP Server', () => {
472 | beforeAll(async () => {
473 | const module = await import('../index.js');
474 | mcpServer = module.mcpServer;
475 | nullToUndefined = module.nullToUndefined;
476 | handleSonarQubeProjects = module.handleSonarQubeProjects;
477 | mapToSonarQubeParams = module.mapToSonarQubeParams;
478 | handleSonarQubeGetIssues = module.handleSonarQubeGetIssues;
479 | handleSonarQubeGetMetrics = module.handleSonarQubeGetMetrics;
480 | handleSonarQubeGetHealth = module.handleSonarQubeGetHealth;
481 | handleSonarQubeGetStatus = module.handleSonarQubeGetStatus;
482 | handleSonarQubePing = module.handleSonarQubePing;
483 | handleSonarQubeComponentMeasures = module.handleSonarQubeComponentMeasures;
484 | handleSonarQubeComponentsMeasures = module.handleSonarQubeComponentsMeasures;
485 | handleSonarQubeMeasuresHistory = module.handleSonarQubeMeasuresHistory;
486 | handleSonarQubeHotspots = module.handleSonarQubeHotspots;
487 | handleSonarQubeHotspot = module.handleSonarQubeHotspot;
488 | handleSonarQubeUpdateHotspotStatus = module.handleSonarQubeUpdateHotspotStatus;
489 | qualityGateHandler = module.qualityGateHandler;
490 | qualityGateStatusHandler = module.qualityGateStatusHandler;
491 | hotspotHandler = module.hotspotHandler;
492 | updateHotspotStatusHandler = module.updateHotspotStatusHandler;
493 | });
494 | beforeEach(() => {
495 | vi.resetModules();
496 | process.env = { ...originalEnv };
497 | // Ensure test environment variables are set
498 | process.env.SONARQUBE_TOKEN = 'test-token';
499 | process.env.SONARQUBE_URL = 'http://localhost:9000';
500 | nock.cleanAll();
501 | });
502 | afterEach(() => {
503 | process.env = originalEnv;
504 | vi.restoreAllMocks();
505 | nock.cleanAll();
506 | });
507 | it('should have initialized the MCP server', () => {
508 | expect(mcpServer).toBeDefined();
509 | expect(mcpServer.server).toBeDefined();
510 | });
511 | describe('Tool registration', () => {
512 | let testServer: any;
513 | let registeredTools: Map<string, any>;
514 | beforeEach(() => {
515 | registeredTools = new Map();
516 | testServer = {
517 | tool: vi.fn((name: string, description: string, schema: any, handler: any) => {
518 | registeredTools.set(name, { description, schema, handler });
519 | }),
520 | };
521 | // Register tools
522 | testServer.tool(
523 | 'projects',
524 | 'List all SonarQube projects',
525 | { page: {}, page_size: {} },
526 | mockHandlers.handleSonarQubeProjects
527 | );
528 | testServer.tool(
529 | 'metrics',
530 | 'Get available metrics from SonarQube',
531 | { page: {}, page_size: {} },
532 | mockHandlers.handleSonarQubeGetMetrics
533 | );
534 | testServer.tool(
535 | 'issues',
536 | 'Get issues for a SonarQube project',
537 | {
538 | project_key: {},
539 | severity: {},
540 | page: {},
541 | page_size: {},
542 | statuses: {},
543 | resolutions: {},
544 | resolved: {},
545 | types: {},
546 | rules: {},
547 | tags: {},
548 | },
549 | mockHandlers.handleSonarQubeGetIssues
550 | );
551 | testServer.tool(
552 | 'system_health',
553 | 'Get the health status of the SonarQube instance',
554 | {},
555 | mockHandlers.handleSonarQubeGetHealth
556 | );
557 | testServer.tool(
558 | 'system_status',
559 | 'Get the status of the SonarQube instance',
560 | {},
561 | mockHandlers.handleSonarQubeGetStatus
562 | );
563 | testServer.tool(
564 | 'system_ping',
565 | 'Ping the SonarQube instance to check if it is up',
566 | {},
567 | mockHandlers.handleSonarQubePing
568 | );
569 | testServer.tool(
570 | 'measures_component',
571 | 'Get measures for a specific component',
572 | {
573 | component: {},
574 | metric_keys: {},
575 | additional_fields: {},
576 | branch: {},
577 | pull_request: {},
578 | period: {},
579 | },
580 | mockHandlers.handleSonarQubeComponentMeasures
581 | );
582 | testServer.tool(
583 | 'measures_components',
584 | 'Get measures for multiple components',
585 | {
586 | component_keys: {},
587 | metric_keys: {},
588 | additional_fields: {},
589 | branch: {},
590 | pull_request: {},
591 | period: {},
592 | page: {},
593 | page_size: {},
594 | },
595 | mockHandlers.handleSonarQubeComponentsMeasures
596 | );
597 | testServer.tool(
598 | 'measures_history',
599 | 'Get measures history for a component',
600 | {
601 | component: {},
602 | metrics: {},
603 | from: {},
604 | to: {},
605 | branch: {},
606 | pull_request: {},
607 | page: {},
608 | page_size: {},
609 | },
610 | mockHandlers.handleSonarQubeMeasuresHistory
611 | );
612 | });
613 | it('should register all required tools', () => {
614 | expect(registeredTools.size).toBe(9);
615 | expect(registeredTools.has('projects')).toBe(true);
616 | expect(registeredTools.has('metrics')).toBe(true);
617 | expect(registeredTools.has('issues')).toBe(true);
618 | expect(registeredTools.has('system_health')).toBe(true);
619 | expect(registeredTools.has('system_status')).toBe(true);
620 | expect(registeredTools.has('system_ping')).toBe(true);
621 | expect(registeredTools.has('measures_component')).toBe(true);
622 | expect(registeredTools.has('measures_components')).toBe(true);
623 | expect(registeredTools.has('measures_history')).toBe(true);
624 | });
625 | it('should register tools with correct descriptions', () => {
626 | expect(registeredTools.get('projects').description).toBe('List all SonarQube projects');
627 | expect(registeredTools.get('metrics').description).toBe(
628 | 'Get available metrics from SonarQube'
629 | );
630 | expect(registeredTools.get('issues').description).toBe('Get issues for a SonarQube project');
631 | expect(registeredTools.get('system_health').description).toBe(
632 | 'Get the health status of the SonarQube instance'
633 | );
634 | expect(registeredTools.get('system_status').description).toBe(
635 | 'Get the status of the SonarQube instance'
636 | );
637 | expect(registeredTools.get('system_ping').description).toBe(
638 | 'Ping the SonarQube instance to check if it is up'
639 | );
640 | expect(registeredTools.get('measures_component').description).toBe(
641 | 'Get measures for a specific component'
642 | );
643 | expect(registeredTools.get('measures_components').description).toBe(
644 | 'Get measures for multiple components'
645 | );
646 | expect(registeredTools.get('measures_history').description).toBe(
647 | 'Get measures history for a component'
648 | );
649 | });
650 | it('should register tools with correct handlers', () => {
651 | expect(registeredTools.get('projects').handler).toBe(mockHandlers.handleSonarQubeProjects);
652 | expect(registeredTools.get('metrics').handler).toBe(mockHandlers.handleSonarQubeGetMetrics);
653 | expect(registeredTools.get('issues').handler).toBe(mockHandlers.handleSonarQubeGetIssues);
654 | expect(registeredTools.get('system_health').handler).toBe(
655 | mockHandlers.handleSonarQubeGetHealth
656 | );
657 | expect(registeredTools.get('system_status').handler).toBe(
658 | mockHandlers.handleSonarQubeGetStatus
659 | );
660 | expect(registeredTools.get('system_ping').handler).toBe(mockHandlers.handleSonarQubePing);
661 | expect(registeredTools.get('measures_component').handler).toBe(
662 | mockHandlers.handleSonarQubeComponentMeasures
663 | );
664 | expect(registeredTools.get('measures_components').handler).toBe(
665 | mockHandlers.handleSonarQubeComponentsMeasures
666 | );
667 | expect(registeredTools.get('measures_history').handler).toBe(
668 | mockHandlers.handleSonarQubeMeasuresHistory
669 | );
670 | });
671 | });
672 | describe('nullToUndefined', () => {
673 | it('should return undefined for null', () => {
674 | expect(nullToUndefined(null)).toBeUndefined();
675 | });
676 | it('should return the value for non-null', () => {
677 | expect(nullToUndefined('value')).toBe('value');
678 | });
679 | });
680 | describe('handleSonarQubeProjects', () => {
681 | it('should fetch and return a list of projects', async () => {
682 | nock('http://localhost:9000')
683 | .get('/api/projects/search')
684 | .query(true)
685 | .reply(200, {
686 | components: [
687 | {
688 | key: 'project1',
689 | name: 'Project 1',
690 | qualifier: 'TRK',
691 | visibility: 'public',
692 | lastAnalysisDate: '2024-03-01',
693 | revision: 'abc123',
694 | managed: false,
695 | },
696 | ],
697 | paging: { pageIndex: 1, pageSize: 1, total: 1 },
698 | });
699 | const response = await handleSonarQubeProjects({ page: 1, page_size: 1 });
700 | expect(response.content[0].text).toContain('project1');
701 | });
702 | });
703 | describe('mapToSonarQubeParams', () => {
704 | it('should map MCP tool parameters to SonarQube client parameters', () => {
705 | const params = mapToSonarQubeParams({ project_key: 'key', severity: 'MAJOR' });
706 | expect(params.projectKey).toBe('key');
707 | expect(params.severity).toBe('MAJOR');
708 | });
709 | });
710 | describe('handleSonarQubeGetIssues', () => {
711 | it('should fetch and return a list of issues', async () => {
712 | nock('http://localhost:9000')
713 | .get('/api/issues/search')
714 | .query(true)
715 | .reply(200, {
716 | issues: [
717 | {
718 | key: 'issue1',
719 | rule: 'rule1',
720 | severity: 'MAJOR',
721 | component: 'comp1',
722 | project: 'proj1',
723 | line: 1,
724 | status: 'OPEN',
725 | message: 'Test issue',
726 | },
727 | ],
728 | components: [],
729 | rules: [],
730 | paging: { pageIndex: 1, pageSize: 1, total: 1 },
731 | });
732 | const response = await handleSonarQubeGetIssues({ projectKey: 'key' });
733 | expect(response.content[0].text).toContain('issue');
734 | });
735 | });
736 | describe('handleSonarQubeGetMetrics', () => {
737 | it('should fetch and return a list of metrics', async () => {
738 | nock('http://localhost:9000')
739 | .get('/api/metrics/search')
740 | .query(true)
741 | .reply(200, {
742 | metrics: [
743 | {
744 | key: 'metric1',
745 | name: 'Metric 1',
746 | description: 'Test metric',
747 | domain: 'domain1',
748 | type: 'INT',
749 | },
750 | ],
751 | paging: { pageIndex: 1, pageSize: 1, total: 1 },
752 | });
753 | const response = await handleSonarQubeGetMetrics({ page: 1, pageSize: 1 });
754 | expect(response.content[0].text).toContain('metric');
755 | });
756 | });
757 | describe('handleSonarQubeGetHealth', () => {
758 | it('should fetch and return health status', async () => {
759 | nock('http://localhost:9000').get('/api/v2/system/health').reply(200, {
760 | status: 'GREEN',
761 | checkedAt: '2023-12-01T10:00:00Z',
762 | });
763 | const response = await handleSonarQubeGetHealth();
764 | expect(response.content[0].text).toContain('GREEN');
765 | });
766 | });
767 | describe('handleSonarQubeGetStatus', () => {
768 | it('should fetch and return system status', async () => {
769 | nock('http://localhost:9000').get('/api/system/status').reply(200, {
770 | id: 'test-id',
771 | version: '10.3.0.82913',
772 | status: 'UP',
773 | });
774 | const response = await handleSonarQubeGetStatus();
775 | expect(response.content[0].text).toContain('UP');
776 | });
777 | });
778 | describe('handleSonarQubePing', () => {
779 | it('should ping the system and return the result', async () => {
780 | nock('http://localhost:9000').get('/api/system/ping').reply(200, 'pong');
781 | const response = await handleSonarQubePing();
782 | expect(response.content[0].text).toBe('pong');
783 | });
784 | });
785 | describe('Conditional server start', () => {
786 | it('should not start the server if NODE_ENV is test', () => {
787 | process.env.NODE_ENV = 'test';
788 | const mcpConnectSpy = vi.spyOn(mcpServer, 'connect');
789 | // Since the server doesn't start in test mode, we verify that connect is not called
790 | expect(mcpConnectSpy).not.toHaveBeenCalled();
791 | mcpConnectSpy.mockRestore();
792 | });
793 | it('should use transport factory in production mode', () => {
794 | // Test that our transport factory is used (covered by integration)
795 | // The actual server startup is tested manually or in integration tests
796 | // since we can't easily test the module-level code execution
797 | expect(true).toBe(true);
798 | });
799 | });
800 | describe('Schema transformations', () => {
801 | it('should handle page and page_size transformations correctly', () => {
802 | // Use the actual schema from the tool registration
803 | const pageSchema = z
804 | .string()
805 | .optional()
806 | .transform((val: any) => (val ? parseInt(val, 10) || null : null));
807 | // Test valid number strings
808 | expect(pageSchema.parse('10')).toBe(10);
809 | expect(pageSchema.parse('20')).toBe(20);
810 | // Test invalid number strings
811 | expect(pageSchema.parse('invalid')).toBe(null);
812 | expect(pageSchema.parse('not-a-number')).toBe(null);
813 | // Test empty/undefined values
814 | expect(pageSchema.parse(undefined)).toBe(null);
815 | expect(pageSchema.parse('')).toBe(null);
816 | });
817 | it('should handle boolean transformations correctly', () => {
818 | const booleanSchema = z
819 | .union([z.boolean(), z.string().transform((val: any) => val === 'true')])
820 | .nullable()
821 | .optional();
822 | // Test string values
823 | expect(booleanSchema.parse('true')).toBe(true);
824 | expect(booleanSchema.parse('false')).toBe(false);
825 | // Test boolean values
826 | expect(booleanSchema.parse(true)).toBe(true);
827 | expect(booleanSchema.parse(false)).toBe(false);
828 | // Test null/undefined values
829 | expect(booleanSchema.parse(null)).toBe(null);
830 | expect(booleanSchema.parse(undefined)).toBe(undefined);
831 | });
832 | it('should handle array transformations correctly', () => {
833 | const stringArraySchema = z.array(z.string()).nullable().optional();
834 | const statusSchema = z
835 | .array(
836 | z.enum([
837 | 'OPEN',
838 | 'CONFIRMED',
839 | 'REOPENED',
840 | 'RESOLVED',
841 | 'CLOSED',
842 | 'TO_REVIEW',
843 | 'IN_REVIEW',
844 | 'REVIEWED',
845 | ])
846 | )
847 | .nullable()
848 | .optional();
849 | const resolutionSchema = z
850 | .array(z.enum(['FALSE-POSITIVE', 'WONTFIX', 'FIXED', 'REMOVED']))
851 | .nullable()
852 | .optional();
853 | const typeSchema = z
854 | .array(z.enum(['CODE_SMELL', 'BUG', 'VULNERABILITY', 'SECURITY_HOTSPOT']))
855 | .nullable()
856 | .optional();
857 | // Test valid arrays
858 | expect(statusSchema.parse(['OPEN', 'CONFIRMED'])).toEqual(['OPEN', 'CONFIRMED']);
859 | expect(resolutionSchema.parse(['FALSE-POSITIVE', 'WONTFIX'])).toEqual([
860 | 'FALSE-POSITIVE',
861 | 'WONTFIX',
862 | ]);
863 | expect(typeSchema.parse(['CODE_SMELL', 'BUG'])).toEqual(['CODE_SMELL', 'BUG']);
864 | expect(stringArraySchema.parse(['value1', 'value2'])).toEqual(['value1', 'value2']);
865 | // Test null/undefined values
866 | expect(statusSchema.parse(null)).toBe(null);
867 | expect(resolutionSchema.parse(null)).toBe(null);
868 | expect(typeSchema.parse(null)).toBe(null);
869 | expect(stringArraySchema.parse(null)).toBe(null);
870 | expect(statusSchema.parse(undefined)).toBe(undefined);
871 | expect(resolutionSchema.parse(undefined)).toBe(undefined);
872 | expect(typeSchema.parse(undefined)).toBe(undefined);
873 | expect(stringArraySchema.parse(undefined)).toBe(undefined);
874 | // Test invalid values
875 | expect(() => statusSchema.parse(['INVALID'])).toThrow();
876 | expect(() => resolutionSchema.parse(['INVALID'])).toThrow();
877 | expect(() => typeSchema.parse(['INVALID'])).toThrow();
878 | });
879 | it('should handle severity schema correctly', () => {
880 | const severitySchema = z
881 | .enum(['INFO', 'MINOR', 'MAJOR', 'CRITICAL', 'BLOCKER'])
882 | .nullable()
883 | .optional();
884 | // Test valid values
885 | expect(severitySchema.parse('INFO')).toBe('INFO');
886 | expect(severitySchema.parse('MINOR')).toBe('MINOR');
887 | expect(severitySchema.parse('MAJOR')).toBe('MAJOR');
888 | expect(severitySchema.parse('CRITICAL')).toBe('CRITICAL');
889 | expect(severitySchema.parse('BLOCKER')).toBe('BLOCKER');
890 | // Test null/undefined values
891 | expect(severitySchema.parse(null)).toBe(null);
892 | expect(severitySchema.parse(undefined)).toBe(undefined);
893 | // Test invalid values
894 | expect(() => severitySchema.parse('INVALID')).toThrow();
895 | });
896 | it('should handle date parameters correctly', () => {
897 | const dateSchema = z.string().nullable().optional();
898 | // Test valid dates
899 | expect(dateSchema.parse('2024-01-01')).toBe('2024-01-01');
900 | expect(dateSchema.parse('2024-12-31')).toBe('2024-12-31');
901 | // Test null/undefined values
902 | expect(dateSchema.parse(null)).toBe(null);
903 | expect(dateSchema.parse(undefined)).toBe(undefined);
904 | });
905 | it('should handle hotspot search boolean transformations correctly', () => {
906 | // Test string to boolean transformation schemas used in hotspot search
907 | const hotspotBooleanSchema = z
908 | .union([z.boolean(), z.string().transform((val: any) => val === 'true')])
909 | .nullable()
910 | .optional();
911 | // Test boolean values
912 | expect(hotspotBooleanSchema.parse(true)).toBe(true);
913 | expect(hotspotBooleanSchema.parse(false)).toBe(false);
914 | // Test string values
915 | expect(hotspotBooleanSchema.parse('true')).toBe(true);
916 | expect(hotspotBooleanSchema.parse('false')).toBe(false);
917 | expect(hotspotBooleanSchema.parse('any')).toBe(false);
918 | // Test null/undefined values
919 | expect(hotspotBooleanSchema.parse(null)).toBe(null);
920 | expect(hotspotBooleanSchema.parse(undefined)).toBe(undefined);
921 | });
922 | it('should handle complex parameter combinations', () => {
923 | // Mock SonarQube API response
924 | nock('http://localhost:9000')
925 | .get('/api/issues/search')
926 | .query(true)
927 | .reply(200, {
928 | issues: [],
929 | components: [],
930 | rules: [],
931 | paging: { pageIndex: 1, pageSize: 100, total: 0 },
932 | });
933 | const params = {
934 | project_key: 'test-project',
935 | severity: 'MAJOR',
936 | statuses: ['OPEN', 'CONFIRMED'],
937 | resolutions: ['FALSE-POSITIVE', 'WONTFIX'],
938 | types: ['CODE_SMELL', 'BUG'],
939 | rules: ['rule1', 'rule2'],
940 | tags: ['tag1', 'tag2'],
941 | created_after: '2024-01-01',
942 | created_before: '2024-12-31',
943 | created_at: '2024-06-15',
944 | created_in_last: '7d',
945 | assignees: ['user1', 'user2'],
946 | authors: ['author1', 'author2'],
947 | cwe: ['cwe1', 'cwe2'],
948 | languages: ['java', 'typescript'],
949 | owasp_top10: ['a1', 'a2'],
950 | sans_top25: ['sans1', 'sans2'],
951 | sonarsource_security: ['sec1', 'sec2'],
952 | on_component_only: true,
953 | facets: ['facet1', 'facet2'],
954 | since_leak_period: true,
955 | in_new_code_period: true,
956 | };
957 | // Verify parameters are properly mapped
958 | const mappedParams = mapToSonarQubeParams(params);
959 | expect(mappedParams).toBeDefined();
960 | expect(mappedParams.projectKey).toBe('test-project');
961 | });
962 | });
963 | describe('Tool handlers', () => {
964 | beforeEach(() => {
965 | vi.resetAllMocks();
966 | });
967 | describe('handleSonarQubeComponentMeasures', () => {
968 | it('should fetch and return component measures', async () => {
969 | nock('http://localhost:9000')
970 | .get('/api/measures/component')
971 | .query(true)
972 | .reply(200, {
973 | component: {
974 | key: 'test-component',
975 | name: 'Test Component',
976 | qualifier: 'TRK',
977 | measures: [
978 | {
979 | metric: 'coverage',
980 | value: '85.4',
981 | },
982 | ],
983 | },
984 | metrics: [
985 | {
986 | key: 'coverage',
987 | name: 'Coverage',
988 | description: 'Test coverage',
989 | domain: 'Coverage',
990 | type: 'PERCENT',
991 | },
992 | ],
993 | });
994 | const response = await handleSonarQubeComponentMeasures({
995 | component: 'test-component',
996 | metricKeys: ['coverage'],
997 | });
998 | expect(response.content[0].text).toContain('test-component');
999 | expect(response.content[0].text).toContain('coverage');
1000 | expect(response.content[0].text).toContain('85.4');
1001 | });
1002 | it('should fetch component measures with all optional parameters', async () => {
1003 | nock('http://localhost:9000')
1004 | .get('/api/measures/component')
1005 | .query((queryObject) => {
1006 | return (
1007 | queryObject.component === 'test-component' &&
1008 | queryObject.metricKeys === 'coverage,bugs' &&
1009 | queryObject.additionalFields === 'periods,metrics' &&
1010 | queryObject.branch === 'main' &&
1011 | queryObject.pullRequest === 'pr-123'
1012 | );
1013 | })
1014 | .matchHeader('authorization', 'Bearer test-token')
1015 | .reply(200, {
1016 | component: {
1017 | key: 'test-component',
1018 | name: 'Test Component',
1019 | qualifier: 'TRK',
1020 | measures: [
1021 | {
1022 | metric: 'coverage',
1023 | value: '85.4',
1024 | period: { index: 1, value: '+5.4' },
1025 | },
1026 | {
1027 | metric: 'bugs',
1028 | value: '10',
1029 | period: { index: 1, value: '-2' },
1030 | },
1031 | ],
1032 | periods: [{ index: 1, mode: 'previous_version', date: '2023-01-01T00:00:00+0000' }],
1033 | },
1034 | metrics: [
1035 | {
1036 | key: 'coverage',
1037 | name: 'Coverage',
1038 | description: 'Test coverage',
1039 | domain: 'Coverage',
1040 | type: 'PERCENT',
1041 | },
1042 | {
1043 | key: 'bugs',
1044 | name: 'Bugs',
1045 | description: 'Number of bugs',
1046 | domain: 'Reliability',
1047 | type: 'INT',
1048 | },
1049 | ],
1050 | });
1051 | const response = await handleSonarQubeComponentMeasures({
1052 | component: 'test-component',
1053 | metricKeys: ['coverage', 'bugs'],
1054 | additionalFields: ['periods', 'metrics'],
1055 | branch: 'main',
1056 | pullRequest: 'pr-123',
1057 | period: '1',
1058 | });
1059 | const result = JSON.parse(response.content[0].text);
1060 | expect(result.component.key).toBe('test-component');
1061 | expect(result.component.measures).toHaveLength(2);
1062 | expect(result.component.periods).toBeDefined();
1063 | expect(result.metrics).toHaveLength(2);
1064 | expect(result.component.measures[0].period).toBeDefined();
1065 | expect(result.component.measures[0].period.index).toBe(1);
1066 | });
1067 | });
1068 | describe('handleSonarQubeComponentsMeasures', () => {
1069 | it('should fetch and return measures for multiple components', async () => {
1070 | // Mock individual component measure calls
1071 | nock('http://localhost:9000')
1072 | .get('/api/measures/component')
1073 | .query({
1074 | component: 'test-component-1',
1075 | metricKeys: 'bugs',
1076 | })
1077 | .matchHeader('authorization', 'Bearer test-token')
1078 | .reply(200, {
1079 | component: {
1080 | key: 'test-component-1',
1081 | name: 'Test Component 1',
1082 | qualifier: 'TRK',
1083 | measures: [
1084 | {
1085 | metric: 'bugs',
1086 | value: '10',
1087 | },
1088 | ],
1089 | },
1090 | metrics: [
1091 | {
1092 | key: 'bugs',
1093 | name: 'Bugs',
1094 | description: 'Number of bugs',
1095 | domain: 'Reliability',
1096 | type: 'INT',
1097 | },
1098 | ],
1099 | });
1100 | nock('http://localhost:9000')
1101 | .get('/api/measures/component')
1102 | .query({
1103 | component: 'test-component-2',
1104 | metricKeys: 'bugs',
1105 | })
1106 | .matchHeader('authorization', 'Bearer test-token')
1107 | .reply(200, {
1108 | component: {
1109 | key: 'test-component-2',
1110 | name: 'Test Component 2',
1111 | qualifier: 'TRK',
1112 | measures: [
1113 | {
1114 | metric: 'bugs',
1115 | value: '5',
1116 | },
1117 | ],
1118 | },
1119 | metrics: [
1120 | {
1121 | key: 'bugs',
1122 | name: 'Bugs',
1123 | description: 'Number of bugs',
1124 | domain: 'Reliability',
1125 | type: 'INT',
1126 | },
1127 | ],
1128 | });
1129 | // Mock the additional call to get metrics from first component
1130 | nock('http://localhost:9000')
1131 | .get('/api/measures/component')
1132 | .query({
1133 | component: 'test-component-1',
1134 | metricKeys: 'bugs',
1135 | })
1136 | .matchHeader('authorization', 'Bearer test-token')
1137 | .reply(200, {
1138 | component: {
1139 | key: 'test-component-1',
1140 | name: 'Test Component 1',
1141 | qualifier: 'TRK',
1142 | measures: [
1143 | {
1144 | metric: 'bugs',
1145 | value: '10',
1146 | },
1147 | ],
1148 | },
1149 | metrics: [
1150 | {
1151 | key: 'bugs',
1152 | name: 'Bugs',
1153 | description: 'Number of bugs',
1154 | domain: 'Reliability',
1155 | type: 'INT',
1156 | },
1157 | ],
1158 | });
1159 | const response = await handleSonarQubeComponentsMeasures({
1160 | componentKeys: ['test-component-1', 'test-component-2'],
1161 | metricKeys: ['bugs'],
1162 | page: 1,
1163 | pageSize: 100,
1164 | });
1165 | expect(response.content[0].text).toContain('test-component-1');
1166 | expect(response.content[0].text).toContain('test-component-2');
1167 | expect(response.content[0].text).toContain('bugs');
1168 | });
1169 | it('should fetch components measures with all optional parameters', async () => {
1170 | // Mock individual component measure calls with optional parameters
1171 | nock('http://localhost:9000')
1172 | .get('/api/measures/component')
1173 | .query({
1174 | component: 'test-component-1',
1175 | metricKeys: 'coverage,bugs',
1176 | additionalFields: 'periods,metrics',
1177 | branch: 'develop',
1178 | pullRequest: 'pr-456',
1179 | })
1180 | .matchHeader('authorization', 'Bearer test-token')
1181 | .reply(200, {
1182 | component: {
1183 | key: 'test-component-1',
1184 | name: 'Test Component 1',
1185 | qualifier: 'TRK',
1186 | measures: [
1187 | {
1188 | metric: 'coverage',
1189 | value: '85.4',
1190 | period: { index: 2, value: '+5.4' },
1191 | },
1192 | {
1193 | metric: 'bugs',
1194 | value: '10',
1195 | period: { index: 2, value: '-2' },
1196 | },
1197 | ],
1198 | periods: [{ index: 2, mode: 'previous_version', date: '2023-01-01T00:00:00+0000' }],
1199 | },
1200 | metrics: [
1201 | {
1202 | key: 'coverage',
1203 | name: 'Coverage',
1204 | description: 'Test coverage',
1205 | domain: 'Coverage',
1206 | type: 'PERCENT',
1207 | },
1208 | {
1209 | key: 'bugs',
1210 | name: 'Bugs',
1211 | description: 'Number of bugs',
1212 | domain: 'Reliability',
1213 | type: 'INT',
1214 | },
1215 | ],
1216 | paging: {
1217 | pageIndex: 3,
1218 | pageSize: 25,
1219 | total: 50,
1220 | },
1221 | });
1222 | // Mock the additional call to get metrics from first component
1223 | nock('http://localhost:9000')
1224 | .get('/api/measures/component')
1225 | .query({
1226 | component: 'test-component-1',
1227 | metricKeys: 'coverage,bugs',
1228 | additionalFields: 'periods,metrics',
1229 | branch: 'develop',
1230 | pullRequest: 'pr-456',
1231 | })
1232 | .matchHeader('authorization', 'Bearer test-token')
1233 | .reply(200, {
1234 | component: {
1235 | key: 'test-component-1',
1236 | name: 'Test Component 1',
1237 | qualifier: 'TRK',
1238 | measures: [
1239 | {
1240 | metric: 'coverage',
1241 | value: '85.4',
1242 | period: { index: 2, value: '+5.4' },
1243 | },
1244 | {
1245 | metric: 'bugs',
1246 | value: '10',
1247 | period: { index: 2, value: '-2' },
1248 | },
1249 | ],
1250 | periods: [{ index: 2, mode: 'previous_version', date: '2023-01-01T00:00:00+0000' }],
1251 | },
1252 | metrics: [
1253 | {
1254 | key: 'coverage',
1255 | name: 'Coverage',
1256 | description: 'Test coverage',
1257 | domain: 'Coverage',
1258 | type: 'PERCENT',
1259 | },
1260 | {
1261 | key: 'bugs',
1262 | name: 'Bugs',
1263 | description: 'Number of bugs',
1264 | domain: 'Reliability',
1265 | type: 'INT',
1266 | },
1267 | ],
1268 | period: {
1269 | index: 2,
1270 | mode: 'previous_version',
1271 | date: '2023-01-01T00:00:00+0000',
1272 | },
1273 | });
1274 | // Mock second component
1275 | nock('http://localhost:9000')
1276 | .get('/api/measures/component')
1277 | .query({
1278 | component: 'test-component-2',
1279 | metricKeys: 'coverage,bugs',
1280 | additionalFields: 'periods,metrics',
1281 | branch: 'develop',
1282 | pullRequest: 'pr-456',
1283 | })
1284 | .matchHeader('authorization', 'Bearer test-token')
1285 | .reply(200, {
1286 | component: {
1287 | key: 'test-component-2',
1288 | name: 'Test Component 2',
1289 | qualifier: 'TRK',
1290 | measures: [
1291 | {
1292 | metric: 'coverage',
1293 | value: '78.2',
1294 | period: { index: 2, value: '+3.1' },
1295 | },
1296 | {
1297 | metric: 'bugs',
1298 | value: '5',
1299 | period: { index: 2, value: '-1' },
1300 | },
1301 | ],
1302 | periods: [{ index: 2, mode: 'previous_version', date: '2023-01-01T00:00:00+0000' }],
1303 | },
1304 | metrics: [
1305 | {
1306 | key: 'coverage',
1307 | name: 'Coverage',
1308 | description: 'Test coverage',
1309 | domain: 'Coverage',
1310 | type: 'PERCENT',
1311 | },
1312 | {
1313 | key: 'bugs',
1314 | name: 'Bugs',
1315 | description: 'Number of bugs',
1316 | domain: 'Reliability',
1317 | type: 'INT',
1318 | },
1319 | ],
1320 | period: {
1321 | index: 2,
1322 | mode: 'previous_version',
1323 | date: '2023-01-01T00:00:00+0000',
1324 | },
1325 | });
1326 | const response = await handleSonarQubeComponentsMeasures({
1327 | componentKeys: ['test-component-1', 'test-component-2'],
1328 | metricKeys: ['coverage', 'bugs'],
1329 | additionalFields: ['periods', 'metrics'],
1330 | branch: 'develop',
1331 | pullRequest: 'pr-456',
1332 | period: '2',
1333 | page: 1,
1334 | pageSize: 25,
1335 | });
1336 | const result = JSON.parse(response.content[0].text);
1337 | expect(result.components).toHaveLength(2);
1338 | expect(result.metrics).toHaveLength(2);
1339 | expect(result.paging.pageIndex).toBe(1);
1340 | expect(result.paging.pageSize).toBe(25);
1341 | expect(result.paging.total).toBe(2);
1342 | expect(result.components[0].key).toBe('test-component-1');
1343 | expect(result.components[0].measures).toHaveLength(2);
1344 | expect(result.components[0].periods).toBeDefined();
1345 | expect(result.components[0].measures[0].period).toBeDefined();
1346 | expect(result.components[0].measures[0].period.index).toBe(2);
1347 | });
1348 | });
1349 | describe('handleSonarQubeMeasuresHistory', () => {
1350 | it('should fetch and return measures history', async () => {
1351 | nock('http://localhost:9000')
1352 | .get('/api/measures/search_history')
1353 | .query(true)
1354 | .reply(200, {
1355 | measures: [
1356 | {
1357 | metric: 'coverage',
1358 | history: [
1359 | {
1360 | date: '2023-01-01T00:00:00+0000',
1361 | value: '80.0',
1362 | },
1363 | {
1364 | date: '2023-02-01T00:00:00+0000',
1365 | value: '85.0',
1366 | },
1367 | ],
1368 | },
1369 | ],
1370 | paging: {
1371 | pageIndex: 1,
1372 | pageSize: 100,
1373 | total: 1,
1374 | },
1375 | });
1376 | const response = await handleSonarQubeMeasuresHistory({
1377 | component: 'test-component',
1378 | metrics: ['coverage'],
1379 | from: '2023-01-01',
1380 | to: '2023-02-01',
1381 | });
1382 | expect(response.content[0].text).toContain('coverage');
1383 | expect(response.content[0].text).toContain('history');
1384 | expect(response.content[0].text).toContain('2023-01-01');
1385 | expect(response.content[0].text).toContain('2023-02-01');
1386 | });
1387 | it('should fetch measures history with all optional parameters', async () => {
1388 | nock('http://localhost:9000')
1389 | .get('/api/measures/search_history')
1390 | .query((queryObject) => {
1391 | return (
1392 | queryObject.component === 'test-component' &&
1393 | queryObject.metrics === 'coverage,bugs,code_smells' &&
1394 | queryObject.from === '2023-01-01' &&
1395 | queryObject.to === '2023-12-31' &&
1396 | queryObject.branch === 'release' &&
1397 | queryObject.pullRequest === 'pr-789' &&
1398 | queryObject.ps === '30' &&
1399 | queryObject.p === '2'
1400 | );
1401 | })
1402 | .reply(200, {
1403 | measures: [
1404 | {
1405 | metric: 'coverage',
1406 | history: [
1407 | {
1408 | date: '2023-01-01T00:00:00+0000',
1409 | value: '80.0',
1410 | },
1411 | {
1412 | date: '2023-03-01T00:00:00+0000',
1413 | value: '83.5',
1414 | },
1415 | {
1416 | date: '2023-06-01T00:00:00+0000',
1417 | value: '85.0',
1418 | },
1419 | {
1420 | date: '2023-09-01T00:00:00+0000',
1421 | value: '87.2',
1422 | },
1423 | {
1424 | date: '2023-12-01T00:00:00+0000',
1425 | value: '90.1',
1426 | },
1427 | ],
1428 | },
1429 | {
1430 | metric: 'bugs',
1431 | history: [
1432 | {
1433 | date: '2023-01-01T00:00:00+0000',
1434 | value: '15',
1435 | },
1436 | {
1437 | date: '2023-03-01T00:00:00+0000',
1438 | value: '12',
1439 | },
1440 | {
1441 | date: '2023-06-01T00:00:00+0000',
1442 | value: '10',
1443 | },
1444 | {
1445 | date: '2023-09-01T00:00:00+0000',
1446 | value: '7',
1447 | },
1448 | {
1449 | date: '2023-12-01T00:00:00+0000',
1450 | value: '5',
1451 | },
1452 | ],
1453 | },
1454 | {
1455 | metric: 'code_smells',
1456 | history: [
1457 | {
1458 | date: '2023-01-01T00:00:00+0000',
1459 | value: '50',
1460 | },
1461 | {
1462 | date: '2023-03-01T00:00:00+0000',
1463 | value: '45',
1464 | },
1465 | {
1466 | date: '2023-06-01T00:00:00+0000',
1467 | value: '40',
1468 | },
1469 | {
1470 | date: '2023-09-01T00:00:00+0000',
1471 | value: '35',
1472 | },
1473 | {
1474 | date: '2023-12-01T00:00:00+0000',
1475 | value: '30',
1476 | },
1477 | ],
1478 | },
1479 | ],
1480 | paging: {
1481 | pageIndex: 2,
1482 | pageSize: 30,
1483 | total: 60,
1484 | },
1485 | });
1486 | const response = await handleSonarQubeMeasuresHistory({
1487 | component: 'test-component',
1488 | metrics: ['coverage', 'bugs', 'code_smells'],
1489 | from: '2023-01-01',
1490 | to: '2023-12-31',
1491 | branch: 'release',
1492 | pullRequest: 'pr-789',
1493 | page: 2,
1494 | pageSize: 30,
1495 | });
1496 | const result = JSON.parse(response.content[0].text);
1497 | expect(result.measures).toHaveLength(3);
1498 | expect(result.paging.pageIndex).toBe(2);
1499 | expect(result.paging.pageSize).toBe(30);
1500 | expect(result.paging.total).toBe(60);
1501 | // Check coverage metric
1502 | expect(result.measures[0].metric).toBe('coverage');
1503 | expect(result.measures[0].history).toHaveLength(5);
1504 | expect(result.measures[0].history[0].date).toBe('2023-01-01T00:00:00+0000');
1505 | expect(result.measures[0].history[0].value).toBe('80.0');
1506 | expect(result.measures[0].history[4].date).toBe('2023-12-01T00:00:00+0000');
1507 | expect(result.measures[0].history[4].value).toBe('90.1');
1508 | // Check bugs metric
1509 | expect(result.measures[1].metric).toBe('bugs');
1510 | expect(result.measures[1].history).toHaveLength(5);
1511 | expect(result.measures[1].history[0].value).toBe('15');
1512 | expect(result.measures[1].history[4].value).toBe('5');
1513 | // Check code_smells metric
1514 | expect(result.measures[2].metric).toBe('code_smells');
1515 | expect(result.measures[2].history).toHaveLength(5);
1516 | expect(result.measures[2].history[0].value).toBe('50');
1517 | expect(result.measures[2].history[4].value).toBe('30');
1518 | });
1519 | });
1520 | describe('measures_component tool lambda', () => {
1521 | it('should call handleSonarQubeComponentMeasures with correct parameters', async () => {
1522 | // Create a simulated lambda function that mimics the tool handler
1523 | const componentMeasuresLambda = async (params: Record<string, unknown>) => {
1524 | return await handleSonarQubeComponentMeasures({
1525 | component: params.component as string,
1526 | metricKeys: Array.isArray(params.metric_keys)
1527 | ? (params.metric_keys as string[])
1528 | : [params.metric_keys as string],
1529 | additionalFields: params.additional_fields as string[] | undefined,
1530 | branch: params.branch as string | undefined,
1531 | pullRequest: params.pull_request as string | undefined,
1532 | period: params.period as string | undefined,
1533 | });
1534 | };
1535 | // Mock the handleSonarQubeComponentMeasures function
1536 | const mockHandler = (vi.fn() as any).mockResolvedValue({
1537 | content: [{ type: 'text', text: '{"component":{}}' }],
1538 | });
1539 | const originalHandler = handleSonarQubeComponentMeasures;
1540 | handleSonarQubeComponentMeasures = mockHandler;
1541 | // Test with string metrics parameter
1542 | await componentMeasuresLambda({
1543 | component: 'my-project',
1544 | metric_keys: 'coverage',
1545 | branch: 'main',
1546 | });
1547 | // Test with array metrics parameter
1548 | await componentMeasuresLambda({
1549 | component: 'my-project',
1550 | metric_keys: ['coverage', 'bugs'],
1551 | additional_fields: ['periods'],
1552 | pull_request: 'pr-123',
1553 | period: '1',
1554 | });
1555 | // Check that the handler was called with the correct parameters
1556 | expect(mockHandler).toHaveBeenCalledTimes(2);
1557 | // Check first call with string parameter
1558 | expect(mockHandler.mock.calls[0][0]).toEqual({
1559 | component: 'my-project',
1560 | metricKeys: ['coverage'],
1561 | branch: 'main',
1562 | additionalFields: undefined,
1563 | pullRequest: undefined,
1564 | period: undefined,
1565 | });
1566 | // Check second call with array parameter
1567 | expect(mockHandler.mock.calls[1][0]).toEqual({
1568 | component: 'my-project',
1569 | metricKeys: ['coverage', 'bugs'],
1570 | additionalFields: ['periods'],
1571 | branch: undefined,
1572 | pullRequest: 'pr-123',
1573 | period: '1',
1574 | });
1575 | // Restore the original handler
1576 | handleSonarQubeComponentMeasures = originalHandler;
1577 | });
1578 | });
1579 | describe('measures_components tool lambda', () => {
1580 | it('should call handleSonarQubeComponentsMeasures with correct parameters', async () => {
1581 | // Create a simulated lambda function that mimics the tool handler
1582 | const componentsMeasuresLambda = async (params: Record<string, unknown>) => {
1583 | return await handleSonarQubeComponentsMeasures({
1584 | componentKeys: Array.isArray(params.component_keys)
1585 | ? (params.component_keys as string[])
1586 | : [params.component_keys as string],
1587 | metricKeys: Array.isArray(params.metric_keys)
1588 | ? (params.metric_keys as string[])
1589 | : [params.metric_keys as string],
1590 | additionalFields: params.additional_fields as string[] | undefined,
1591 | branch: params.branch as string | undefined,
1592 | pullRequest: params.pull_request as string | undefined,
1593 | period: params.period as string | undefined,
1594 | page: nullToUndefined(params.page) as number | undefined,
1595 | pageSize: nullToUndefined(params.page_size) as number | undefined,
1596 | });
1597 | };
1598 | // Mock the handler function
1599 | const mockHandler = (vi.fn() as any).mockResolvedValue({
1600 | content: [{ type: 'text', text: '{"components":[]}' }],
1601 | });
1602 | const originalHandler = handleSonarQubeComponentsMeasures;
1603 | handleSonarQubeComponentsMeasures = mockHandler;
1604 | // Test with string parameters
1605 | await componentsMeasuresLambda({
1606 | component_keys: 'project1',
1607 | metric_keys: 'coverage',
1608 | page: '1',
1609 | page_size: '10',
1610 | });
1611 | // Test with array parameters
1612 | await componentsMeasuresLambda({
1613 | component_keys: ['project1', 'project2'],
1614 | metric_keys: ['coverage', 'bugs'],
1615 | additional_fields: ['periods'],
1616 | branch: 'main',
1617 | period: '1',
1618 | });
1619 | // Test with pull request parameter
1620 | await componentsMeasuresLambda({
1621 | component_keys: 'project1',
1622 | metric_keys: ['coverage', 'bugs'],
1623 | pull_request: 'pr-123',
1624 | });
1625 | // Check that the handler was called with the correct parameters
1626 | expect(mockHandler).toHaveBeenCalledTimes(3);
1627 | // Check first call with string parameters
1628 | expect(mockHandler.mock.calls[0][0]).toEqual({
1629 | componentKeys: ['project1'],
1630 | metricKeys: ['coverage'],
1631 | additionalFields: undefined,
1632 | branch: undefined,
1633 | pullRequest: undefined,
1634 | period: undefined,
1635 | page: '1',
1636 | pageSize: '10',
1637 | });
1638 | // Check second call with array parameters
1639 | expect(mockHandler.mock.calls[1][0]).toEqual({
1640 | componentKeys: ['project1', 'project2'],
1641 | metricKeys: ['coverage', 'bugs'],
1642 | additionalFields: ['periods'],
1643 | branch: 'main',
1644 | pullRequest: undefined,
1645 | period: '1',
1646 | page: undefined,
1647 | pageSize: undefined,
1648 | });
1649 | // Check third call with pull request parameter
1650 | expect(mockHandler.mock.calls[2][0]).toEqual({
1651 | componentKeys: ['project1'],
1652 | metricKeys: ['coverage', 'bugs'],
1653 | additionalFields: undefined,
1654 | branch: undefined,
1655 | pullRequest: 'pr-123',
1656 | period: undefined,
1657 | page: undefined,
1658 | pageSize: undefined,
1659 | });
1660 | // Restore the original handler
1661 | handleSonarQubeComponentsMeasures = originalHandler;
1662 | });
1663 | });
1664 | describe('measures_history tool lambda', () => {
1665 | it('should call handleSonarQubeMeasuresHistory with correct parameters', async () => {
1666 | // Create a simulated lambda function that mimics the tool handler
1667 | const measuresHistoryLambda = async (params: Record<string, unknown>) => {
1668 | return await handleSonarQubeMeasuresHistory({
1669 | component: params.component as string,
1670 | metrics: Array.isArray(params.metrics)
1671 | ? (params.metrics as string[])
1672 | : [params.metrics as string],
1673 | from: params.from as string | undefined,
1674 | to: params.to as string | undefined,
1675 | branch: params.branch as string | undefined,
1676 | pullRequest: params.pull_request as string | undefined,
1677 | page: nullToUndefined(params.page) as number | undefined,
1678 | pageSize: nullToUndefined(params.page_size) as number | undefined,
1679 | });
1680 | };
1681 | // Mock the handler function
1682 | const mockHandler = (vi.fn() as any).mockResolvedValue({
1683 | content: [{ type: 'text', text: '{"measures":[]}' }],
1684 | });
1685 | const originalHandler = handleSonarQubeMeasuresHistory;
1686 | handleSonarQubeMeasuresHistory = mockHandler;
1687 | // Test with string parameter
1688 | await measuresHistoryLambda({
1689 | component: 'my-project',
1690 | metrics: 'coverage',
1691 | from: '2023-01-01',
1692 | to: '2023-02-01',
1693 | });
1694 | // Test with array parameter
1695 | await measuresHistoryLambda({
1696 | component: 'my-project',
1697 | metrics: ['coverage', 'bugs'],
1698 | branch: 'main',
1699 | page: '2',
1700 | page_size: '20',
1701 | });
1702 | // Test with pull request parameter
1703 | await measuresHistoryLambda({
1704 | component: 'my-project',
1705 | metrics: ['coverage'],
1706 | pull_request: 'pr-123',
1707 | });
1708 | // Test full parameter set
1709 | await measuresHistoryLambda({
1710 | component: 'my-project',
1711 | metrics: ['coverage', 'bugs', 'code_smells'],
1712 | from: '2023-01-01',
1713 | to: '2023-12-31',
1714 | branch: 'develop',
1715 | pull_request: 'pr-456',
1716 | page: '3',
1717 | page_size: '50',
1718 | });
1719 | // Check that the handler was called with the correct parameters
1720 | expect(mockHandler).toHaveBeenCalledTimes(4);
1721 | // Check first call with string parameter
1722 | expect(mockHandler.mock.calls[0][0]).toEqual({
1723 | component: 'my-project',
1724 | metrics: ['coverage'],
1725 | from: '2023-01-01',
1726 | to: '2023-02-01',
1727 | branch: undefined,
1728 | pullRequest: undefined,
1729 | page: undefined,
1730 | pageSize: undefined,
1731 | });
1732 | // Check second call with array parameter
1733 | expect(mockHandler.mock.calls[1][0]).toEqual({
1734 | component: 'my-project',
1735 | metrics: ['coverage', 'bugs'],
1736 | from: undefined,
1737 | to: undefined,
1738 | branch: 'main',
1739 | pullRequest: undefined,
1740 | page: '2',
1741 | pageSize: '20',
1742 | });
1743 | // Check third call with pull request parameter
1744 | expect(mockHandler.mock.calls[2][0]).toEqual({
1745 | component: 'my-project',
1746 | metrics: ['coverage'],
1747 | from: undefined,
1748 | to: undefined,
1749 | branch: undefined,
1750 | pullRequest: 'pr-123',
1751 | page: undefined,
1752 | pageSize: undefined,
1753 | });
1754 | // Check fourth call with full parameter set
1755 | expect(mockHandler.mock.calls[3][0]).toEqual({
1756 | component: 'my-project',
1757 | metrics: ['coverage', 'bugs', 'code_smells'],
1758 | from: '2023-01-01',
1759 | to: '2023-12-31',
1760 | branch: 'develop',
1761 | pullRequest: 'pr-456',
1762 | page: '3',
1763 | pageSize: '50',
1764 | });
1765 | // Restore the original handler
1766 | handleSonarQubeMeasuresHistory = originalHandler;
1767 | });
1768 | });
1769 | it('should fully process SonarQube projects response', async () => {
1770 | const fullProjectsResponse = {
1771 | projects: [
1772 | {
1773 | key: 'test-project',
1774 | name: 'Test Project',
1775 | qualifier: 'TRK',
1776 | visibility: 'public',
1777 | lastAnalysisDate: '2024-03-01',
1778 | revision: 'abc123',
1779 | managed: false,
1780 | extra: 'should be excluded',
1781 | },
1782 | ],
1783 | paging: {
1784 | pageIndex: 1,
1785 | pageSize: 10,
1786 | total: 1,
1787 | },
1788 | };
1789 | mockHandlers.handleSonarQubeProjects.mockResolvedValueOnce({
1790 | content: [
1791 | {
1792 | type: 'text',
1793 | text: JSON.stringify(fullProjectsResponse),
1794 | },
1795 | ],
1796 | });
1797 | const result = await mockHandlers.handleSonarQubeProjects({ page: 1, page_size: 10 });
1798 | const data = JSON.parse(result.content[0].text);
1799 | expect(data.projects[0].key).toBe('test-project');
1800 | expect(data.projects[0].name).toBe('Test Project');
1801 | expect(data.projects[0].qualifier).toBe('TRK');
1802 | expect(data.projects[0].visibility).toBe('public');
1803 | expect(data.projects[0].lastAnalysisDate).toBe('2024-03-01');
1804 | expect(data.projects[0].revision).toBe('abc123');
1805 | expect(data.projects[0].managed).toBe(false);
1806 | expect(data.paging.pageIndex).toBe(1);
1807 | expect(data.paging.pageSize).toBe(10);
1808 | expect(data.paging.total).toBe(1);
1809 | });
1810 | it('should fully process SonarQube issues response', async () => {
1811 | const fullIssuesResponse = {
1812 | issues: [
1813 | {
1814 | key: 'test-issue',
1815 | rule: 'test-rule',
1816 | severity: 'MAJOR',
1817 | component: 'test-component',
1818 | project: 'test-project',
1819 | line: 1,
1820 | status: 'OPEN',
1821 | issueStatus: 'OPEN',
1822 | message: 'Test issue',
1823 | messageFormattings: [],
1824 | effort: '1h',
1825 | debt: '1h',
1826 | author: 'test-author',
1827 | tags: ['tag1', 'tag2'],
1828 | creationDate: '2024-03-01',
1829 | updateDate: '2024-03-02',
1830 | type: 'BUG',
1831 | cleanCodeAttribute: 'CONSISTENT',
1832 | cleanCodeAttributeCategory: 'ADAPTABLE',
1833 | prioritizedRule: true,
1834 | impacts: [{ severity: 'HIGH', softwareQuality: 'SECURITY' }],
1835 | textRange: { startLine: 1, endLine: 1, startOffset: 0, endOffset: 10 },
1836 | comments: [],
1837 | transitions: [],
1838 | actions: [],
1839 | flows: [],
1840 | quickFixAvailable: false,
1841 | ruleDescriptionContextKey: 'context',
1842 | codeVariants: [],
1843 | hash: 'hash',
1844 | },
1845 | ],
1846 | components: [{ key: 'comp1', name: 'Component 1' }],
1847 | rules: [{ key: 'rule1', name: 'Rule 1' }],
1848 | users: [{ login: 'user1', name: 'User 1' }],
1849 | facets: [{ property: 'facet1', values: [] }],
1850 | paging: {
1851 | pageIndex: 1,
1852 | pageSize: 10,
1853 | total: 1,
1854 | },
1855 | };
1856 | mockHandlers.handleSonarQubeGetIssues.mockResolvedValueOnce({
1857 | content: [
1858 | {
1859 | type: 'text',
1860 | text: JSON.stringify(fullIssuesResponse),
1861 | },
1862 | ],
1863 | });
1864 | const result = await mockHandlers.handleSonarQubeGetIssues({
1865 | projectKey: 'test-project',
1866 | severity: 'MAJOR',
1867 | page: 1,
1868 | pageSize: 10,
1869 | statuses: ['OPEN'],
1870 | resolutions: ['FIXED'],
1871 | resolved: true,
1872 | types: ['BUG'],
1873 | rules: ['rule1'],
1874 | tags: ['tag1'],
1875 | createdAfter: '2024-01-01',
1876 | createdBefore: '2024-03-01',
1877 | createdAt: '2024-02-01',
1878 | createdInLast: '30d',
1879 | assignees: ['user1'],
1880 | authors: ['author1'],
1881 | cwe: ['cwe1'],
1882 | languages: ['java'],
1883 | owaspTop10: ['a1'],
1884 | sansTop25: ['sans1'],
1885 | sonarsourceSecurity: ['ss1'],
1886 | onComponentOnly: true,
1887 | facets: ['facet1'],
1888 | sinceLeakPeriod: true,
1889 | inNewCodePeriod: true,
1890 | });
1891 | const data = JSON.parse(result.content[0].text);
1892 | // Check all fields are properly mapped
1893 | expect(data.issues[0].key).toBe('test-issue');
1894 | expect(data.issues[0].rule).toBe('test-rule');
1895 | expect(data.issues[0].severity).toBe('MAJOR');
1896 | expect(data.issues[0].component).toBe('test-component');
1897 | expect(data.issues[0].project).toBe('test-project');
1898 | expect(data.issues[0].line).toBe(1);
1899 | expect(data.issues[0].status).toBe('OPEN');
1900 | expect(data.issues[0].issueStatus).toBe('OPEN');
1901 | expect(data.issues[0].message).toBe('Test issue');
1902 | expect(data.issues[0].effort).toBe('1h');
1903 | expect(data.issues[0].debt).toBe('1h');
1904 | expect(data.issues[0].author).toBe('test-author');
1905 | expect(data.issues[0].tags).toEqual(['tag1', 'tag2']);
1906 | expect(data.issues[0].creationDate).toBe('2024-03-01');
1907 | expect(data.issues[0].updateDate).toBe('2024-03-02');
1908 | expect(data.issues[0].type).toBe('BUG');
1909 | expect(data.issues[0].cleanCodeAttribute).toBe('CONSISTENT');
1910 | expect(data.issues[0].cleanCodeAttributeCategory).toBe('ADAPTABLE');
1911 | expect(data.issues[0].prioritizedRule).toBe(true);
1912 | expect(data.issues[0].impacts).toHaveLength(1);
1913 | expect(data.issues[0].impacts[0].severity).toBe('HIGH');
1914 | // Check other response data
1915 | expect(data.components).toHaveLength(1);
1916 | expect(data.rules).toHaveLength(1);
1917 | expect(data.users).toHaveLength(1);
1918 | expect(data.facets).toHaveLength(1);
1919 | expect(data.paging.pageIndex).toBe(1);
1920 | expect(data.paging.pageSize).toBe(10);
1921 | expect(data.paging.total).toBe(1);
1922 | });
1923 | it('should handle metrics response', async () => {
1924 | const metricsResponse = {
1925 | metrics: [
1926 | {
1927 | key: 'test-metric',
1928 | name: 'Test Metric',
1929 | description: 'Test metric description',
1930 | domain: 'test',
1931 | type: 'INT',
1932 | },
1933 | ],
1934 | paging: {
1935 | pageIndex: 1,
1936 | pageSize: 10,
1937 | total: 1,
1938 | },
1939 | };
1940 | mockHandlers.handleSonarQubeGetMetrics.mockResolvedValueOnce({
1941 | content: [
1942 | {
1943 | type: 'text',
1944 | text: JSON.stringify(metricsResponse),
1945 | },
1946 | ],
1947 | });
1948 | const result = await mockHandlers.handleSonarQubeGetMetrics({
1949 | page: 1,
1950 | pageSize: 10,
1951 | });
1952 | const data = JSON.parse(result.content[0].text);
1953 | expect(data.metrics).toHaveLength(1);
1954 | expect(data.metrics[0].key).toBe('test-metric');
1955 | expect(data.metrics[0].name).toBe('Test Metric');
1956 | expect(data.paging.pageIndex).toBe(1);
1957 | });
1958 | });
1959 | describe('Tool registration schemas', () => {
1960 | it('should correctly transform page parameters', () => {
1961 | const pageSchema = z
1962 | .string()
1963 | .optional()
1964 | .transform((val: any) => (val ? parseInt(val, 10) || null : null));
1965 | expect(pageSchema.parse('10')).toBe(10);
1966 | expect(pageSchema.parse('not-a-number')).toBe(null);
1967 | expect(pageSchema.parse('')).toBe(null);
1968 | expect(pageSchema.parse(undefined)).toBe(null);
1969 | });
1970 | it('should validate severity enum schema', () => {
1971 | const severitySchema = z
1972 | .enum(['INFO', 'MINOR', 'MAJOR', 'CRITICAL', 'BLOCKER'])
1973 | .nullable()
1974 | .optional();
1975 | expect(severitySchema.parse('MAJOR')).toBe('MAJOR');
1976 | expect(severitySchema.parse('BLOCKER')).toBe('BLOCKER');
1977 | expect(severitySchema.parse(null)).toBe(null);
1978 | expect(severitySchema.parse(undefined)).toBe(undefined);
1979 | expect(() => severitySchema.parse('INVALID')).toThrow();
1980 | });
1981 | it('should validate status schema', () => {
1982 | const statusSchema = z
1983 | .array(
1984 | z.enum([
1985 | 'OPEN',
1986 | 'CONFIRMED',
1987 | 'REOPENED',
1988 | 'RESOLVED',
1989 | 'CLOSED',
1990 | 'TO_REVIEW',
1991 | 'IN_REVIEW',
1992 | 'REVIEWED',
1993 | ])
1994 | )
1995 | .nullable()
1996 | .optional();
1997 | expect(statusSchema.parse(['OPEN', 'CONFIRMED'])).toEqual(['OPEN', 'CONFIRMED']);
1998 | expect(statusSchema.parse(null)).toBe(null);
1999 | expect(statusSchema.parse(undefined)).toBe(undefined);
2000 | expect(() => statusSchema.parse(['INVALID'])).toThrow();
2001 | });
2002 | it('should validate resolution schema', () => {
2003 | const resolutionSchema = z
2004 | .array(z.enum(['FALSE-POSITIVE', 'WONTFIX', 'FIXED', 'REMOVED']))
2005 | .nullable()
2006 | .optional();
2007 | expect(resolutionSchema.parse(['FALSE-POSITIVE', 'WONTFIX'])).toEqual([
2008 | 'FALSE-POSITIVE',
2009 | 'WONTFIX',
2010 | ]);
2011 | expect(resolutionSchema.parse(null)).toBe(null);
2012 | expect(resolutionSchema.parse(undefined)).toBe(undefined);
2013 | expect(() => resolutionSchema.parse(['INVALID'])).toThrow();
2014 | });
2015 | it('should validate type schema', () => {
2016 | const typeSchema = z
2017 | .array(z.enum(['CODE_SMELL', 'BUG', 'VULNERABILITY', 'SECURITY_HOTSPOT']))
2018 | .nullable()
2019 | .optional();
2020 | expect(typeSchema.parse(['CODE_SMELL', 'BUG'])).toEqual(['CODE_SMELL', 'BUG']);
2021 | expect(typeSchema.parse(null)).toBe(null);
2022 | expect(typeSchema.parse(undefined)).toBe(undefined);
2023 | expect(() => typeSchema.parse(['INVALID'])).toThrow();
2024 | });
2025 | it('should transform boolean parameters', () => {
2026 | const booleanSchema = z
2027 | .union([z.boolean(), z.string().transform((val: any) => val === 'true')])
2028 | .nullable()
2029 | .optional();
2030 | expect(booleanSchema.parse('true')).toBe(true);
2031 | expect(booleanSchema.parse('false')).toBe(false);
2032 | expect(booleanSchema.parse(true)).toBe(true);
2033 | expect(booleanSchema.parse(false)).toBe(false);
2034 | expect(booleanSchema.parse(null)).toBe(null);
2035 | expect(booleanSchema.parse(undefined)).toBe(undefined);
2036 | });
2037 | });
2038 | describe('Tool registration lambdas', () => {
2039 | beforeEach(() => {
2040 | // Reset all mocks
2041 | vi.resetAllMocks();
2042 | });
2043 | it('should test the metrics tool lambda', async () => {
2044 | // Mock the handleSonarQubeGetMetrics function to track calls
2045 | const mockGetMetrics = (vi.fn() as any).mockResolvedValue({
2046 | content: [{ type: 'text', text: '{"metrics":[]}' }],
2047 | });
2048 | const originalHandler = handleSonarQubeGetMetrics;
2049 | handleSonarQubeGetMetrics = mockGetMetrics;
2050 | // Create the lambda handler that's in the tool registration
2051 | const metricsLambda = async (params: Record<string, unknown>) => {
2052 | const result = await handleSonarQubeGetMetrics({
2053 | page: nullToUndefined(params.page) as number | undefined,
2054 | pageSize: nullToUndefined(params.page_size) as number | undefined,
2055 | });
2056 | return {
2057 | content: [
2058 | {
2059 | type: 'text',
2060 | text: JSON.stringify(result, null, 2),
2061 | },
2062 | ],
2063 | };
2064 | };
2065 | // Call the lambda with params
2066 | await metricsLambda({ page: '5', page_size: '20' });
2067 | // Check that handleSonarQubeGetMetrics was called with the right params
2068 | expect(mockGetMetrics).toHaveBeenCalledWith({
2069 | page: '5',
2070 | pageSize: '20',
2071 | });
2072 | // Restore the original function
2073 | handleSonarQubeGetMetrics = originalHandler;
2074 | });
2075 | it('should test the issues tool lambda', async () => {
2076 | // Mock the handleSonarQubeGetIssues function to track calls
2077 | const mockGetIssues = (vi.fn() as any).mockResolvedValue({
2078 | content: [{ type: 'text', text: '{"issues":[]}' }],
2079 | });
2080 | const originalHandler = handleSonarQubeGetIssues;
2081 | handleSonarQubeGetIssues = mockGetIssues;
2082 | // Mock mapToSonarQubeParams to return expected output
2083 | const originalMapFunction = mapToSonarQubeParams;
2084 | const mockMapFunction = (vi.fn() as any).mockReturnValue({
2085 | projectKey: 'test-project',
2086 | severity: 'MAJOR',
2087 | });
2088 | mapToSonarQubeParams = mockMapFunction;
2089 | // Create the lambda handler that's in the tool registration
2090 | const issuesLambda = async (params: Record<string, unknown>) => {
2091 | return await handleSonarQubeGetIssues(mapToSonarQubeParams(params));
2092 | };
2093 | // Call the lambda with params
2094 | await issuesLambda({
2095 | project_key: 'test-project',
2096 | severity: 'MAJOR',
2097 | });
2098 | // Check that mapToSonarQubeParams was called with the right params
2099 | expect(mockMapFunction).toHaveBeenCalledWith({
2100 | project_key: 'test-project',
2101 | severity: 'MAJOR',
2102 | });
2103 | // Check that handleSonarQubeGetIssues was called with the mapped params
2104 | expect(mockGetIssues).toHaveBeenCalledWith({
2105 | projectKey: 'test-project',
2106 | severity: 'MAJOR',
2107 | });
2108 | // Restore the original functions
2109 | handleSonarQubeGetIssues = originalHandler;
2110 | mapToSonarQubeParams = originalMapFunction;
2111 | });
2112 | it('should test the hotspot search tool lambda', async () => {
2113 | // Mock the handleSonarQubeSearchHotspots function to track calls
2114 | const mockSearchHotspots = (vi.fn() as any).mockResolvedValue({
2115 | content: [{ type: 'text', text: '{"hotspots":[]}' }],
2116 | });
2117 | const originalHandler = handleSonarQubeHotspots;
2118 | handleSonarQubeHotspots = mockSearchHotspots;
2119 | // Mock mapToSonarQubeParams to return expected output
2120 | const originalMapFunction = mapToSonarQubeParams;
2121 | const mockMapFunction = (vi.fn() as any).mockReturnValue({
2122 | projectKey: 'test-project',
2123 | status: 'TO_REVIEW',
2124 | assignedToMe: true,
2125 | sinceLeakPeriod: false,
2126 | inNewCodePeriod: true,
2127 | page: 1,
2128 | pageSize: 50,
2129 | });
2130 | mapToSonarQubeParams = mockMapFunction;
2131 | // Create the lambda handler that's in the tool registration
2132 | const searchHotspotsLambda = async (params: Record<string, unknown>) => {
2133 | return await handleSonarQubeHotspots(mapToSonarQubeParams(params));
2134 | };
2135 | // Call the lambda with params that include string booleans
2136 | await searchHotspotsLambda({
2137 | project_key: 'test-project',
2138 | status: 'TO_REVIEW',
2139 | assigned_to_me: 'true',
2140 | since_leak_period: 'false',
2141 | in_new_code_period: 'true',
2142 | page: '1',
2143 | page_size: '50',
2144 | });
2145 | // Check that mapToSonarQubeParams was called with the right params
2146 | expect(mockMapFunction).toHaveBeenCalledWith({
2147 | project_key: 'test-project',
2148 | status: 'TO_REVIEW',
2149 | assigned_to_me: 'true',
2150 | since_leak_period: 'false',
2151 | in_new_code_period: 'true',
2152 | page: '1',
2153 | page_size: '50',
2154 | });
2155 | // Check that handleSonarQubeSearchHotspots was called with the mapped params
2156 | expect(mockSearchHotspots).toHaveBeenCalledWith({
2157 | projectKey: 'test-project',
2158 | status: 'TO_REVIEW',
2159 | assignedToMe: true,
2160 | sinceLeakPeriod: false,
2161 | inNewCodePeriod: true,
2162 | page: 1,
2163 | pageSize: 50,
2164 | });
2165 | // Restore the original functions
2166 | handleSonarQubeHotspots = originalHandler;
2167 | mapToSonarQubeParams = originalMapFunction;
2168 | });
2169 | it('should test the quality gate handler lambda', async () => {
2170 | // Set up mock for the API call
2171 | nock('http://localhost:9000')
2172 | .get('/api/qualitygates/show')
2173 | .query({ id: 'gate-123' })
2174 | .reply(200, {
2175 | id: 'gate-123',
2176 | name: 'Test Quality Gate',
2177 | conditions: [],
2178 | isBuiltIn: false,
2179 | });
2180 | // Test the lambda
2181 | const result = await qualityGateHandler({ id: 'gate-123' });
2182 | expect(result).toBeDefined();
2183 | expect(result.content[0].text).toContain('gate-123');
2184 | });
2185 | it('should test the project quality gate status handler lambda', async () => {
2186 | // Set up mock for the API call
2187 | nock('http://localhost:9000')
2188 | .get('/api/qualitygates/project_status')
2189 | .query({
2190 | projectKey: 'test-project',
2191 | branch: 'main',
2192 | pullRequest: 'pr-123',
2193 | })
2194 | .reply(200, {
2195 | projectStatus: {
2196 | status: 'OK',
2197 | conditions: [],
2198 | },
2199 | });
2200 | // Test the lambda with all parameters
2201 | const result = await qualityGateStatusHandler({
2202 | project_key: 'test-project',
2203 | branch: 'main',
2204 | pull_request: 'pr-123',
2205 | });
2206 | expect(result).toBeDefined();
2207 | expect(result.content[0].text).toContain('OK');
2208 | });
2209 | it('should test the get hotspot details handler lambda', async () => {
2210 | // Set up mock for the API call
2211 | nock('http://localhost:9000')
2212 | .get('/api/hotspots/show')
2213 | .query({
2214 | hotspot: 'hotspot-123',
2215 | })
2216 | .reply(200, {
2217 | key: 'hotspot-123',
2218 | component: 'test',
2219 | project: 'test-project',
2220 | rule: {
2221 | key: 'java:S2068',
2222 | name: 'Hard-coded credentials',
2223 | securityCategory: 'weak-cryptography',
2224 | },
2225 | status: 'TO_REVIEW',
2226 | line: 42,
2227 | message: 'Make sure this password is not hard-coded.',
2228 | author: '[email protected]',
2229 | creationDate: '2023-01-15T10:30:00+0000',
2230 | });
2231 | // Test the lambda
2232 | const result = await hotspotHandler({ hotspot_key: 'hotspot-123' });
2233 | expect(result).toBeDefined();
2234 | expect(result.content[0].text).toContain('hotspot-123');
2235 | });
2236 | it('should test the update hotspot status handler lambda', async () => {
2237 | // Set up mock for the API call
2238 | nock('http://localhost:9000')
2239 | .post('/api/hotspots/change_status', {
2240 | hotspot: 'hotspot-123',
2241 | status: 'REVIEWED',
2242 | resolution: 'SAFE',
2243 | comment: 'Reviewed and safe',
2244 | })
2245 | .reply(200, {});
2246 | // Test the lambda with all parameters
2247 | const result = await updateHotspotStatusHandler({
2248 | hotspot_key: 'hotspot-123',
2249 | status: 'REVIEWED',
2250 | resolution: 'SAFE',
2251 | comment: 'Reviewed and safe',
2252 | });
2253 | expect(result).toBeDefined();
2254 | expect(result.content[0].text).toContain('successfully');
2255 | });
2256 | });
2257 | describe('Tool schema validations', () => {
2258 | it('should validate and transform all issue tool schemas', () => {
2259 | // Create schemas that match what's in the tool registration
2260 | const pageSchema = z
2261 | .string()
2262 | .optional()
2263 | .transform((val: any) => (val ? parseInt(val, 10) || null : null));
2264 | const booleanSchema = z
2265 | .union([z.boolean(), z.string().transform((val: any) => val === 'true')])
2266 | .nullable()
2267 | .optional();
2268 | const severitySchema = z
2269 | .enum(['INFO', 'MINOR', 'MAJOR', 'CRITICAL', 'BLOCKER'])
2270 | .nullable()
2271 | .optional();
2272 | const statusSchema = z
2273 | .array(
2274 | z.enum([
2275 | 'OPEN',
2276 | 'CONFIRMED',
2277 | 'REOPENED',
2278 | 'RESOLVED',
2279 | 'CLOSED',
2280 | 'TO_REVIEW',
2281 | 'IN_REVIEW',
2282 | 'REVIEWED',
2283 | ])
2284 | )
2285 | .nullable()
2286 | .optional();
2287 | const resolutionSchema = z
2288 | .array(z.enum(['FALSE-POSITIVE', 'WONTFIX', 'FIXED', 'REMOVED']))
2289 | .nullable()
2290 | .optional();
2291 | const typeSchema = z
2292 | .array(z.enum(['CODE_SMELL', 'BUG', 'VULNERABILITY', 'SECURITY_HOTSPOT']))
2293 | .nullable()
2294 | .optional();
2295 | const stringArraySchema = z.array(z.string()).nullable().optional();
2296 | // Create the complete schema
2297 | const schema = z.object({
2298 | project_key: z.string(),
2299 | severity: severitySchema,
2300 | page: pageSchema,
2301 | page_size: pageSchema,
2302 | statuses: statusSchema,
2303 | resolutions: resolutionSchema,
2304 | resolved: booleanSchema,
2305 | types: typeSchema,
2306 | rules: stringArraySchema,
2307 | tags: stringArraySchema,
2308 | created_after: z.string().nullable().optional(),
2309 | created_before: z.string().nullable().optional(),
2310 | created_at: z.string().nullable().optional(),
2311 | created_in_last: z.string().nullable().optional(),
2312 | assignees: stringArraySchema,
2313 | authors: stringArraySchema,
2314 | cwe: stringArraySchema,
2315 | languages: stringArraySchema,
2316 | owasp_top10: stringArraySchema,
2317 | sans_top25: stringArraySchema,
2318 | sonarsource_security: stringArraySchema,
2319 | on_component_only: booleanSchema,
2320 | facets: stringArraySchema,
2321 | since_leak_period: booleanSchema,
2322 | in_new_code_period: booleanSchema,
2323 | });
2324 | // Test the complete schema
2325 | const testData = {
2326 | project_key: 'test-project',
2327 | severity: 'MAJOR',
2328 | page: '10',
2329 | page_size: '20',
2330 | statuses: ['OPEN', 'CONFIRMED'],
2331 | resolutions: ['FALSE-POSITIVE', 'WONTFIX'],
2332 | resolved: 'true',
2333 | types: ['CODE_SMELL', 'BUG'],
2334 | rules: ['rule1', 'rule2'],
2335 | tags: ['tag1', 'tag2'],
2336 | created_after: '2024-01-01',
2337 | created_before: '2024-12-31',
2338 | created_at: '2024-06-15',
2339 | created_in_last: '7d',
2340 | assignees: ['user1', 'user2'],
2341 | authors: ['author1', 'author2'],
2342 | cwe: ['cwe1', 'cwe2'],
2343 | languages: ['java', 'typescript'],
2344 | owasp_top10: ['a1', 'a2'],
2345 | sans_top25: ['sans1', 'sans2'],
2346 | sonarsource_security: ['sec1', 'sec2'],
2347 | on_component_only: 'true',
2348 | facets: ['facet1', 'facet2'],
2349 | since_leak_period: 'true',
2350 | in_new_code_period: 'true',
2351 | };
2352 | // Validate through the Zod schema
2353 | const validated = schema.parse(testData);
2354 | // Check transformations happened correctly
2355 | expect(validated.page).toBe(10);
2356 | expect(validated.page_size).toBe(20);
2357 | expect(validated.resolved).toBe(true);
2358 | expect(validated.on_component_only).toBe(true);
2359 | expect(validated.since_leak_period).toBe(true);
2360 | expect(validated.in_new_code_period).toBe(true);
2361 | // Check arrays were kept intact
2362 | expect(validated.statuses).toEqual(['OPEN', 'CONFIRMED']);
2363 | expect(validated.resolutions).toEqual(['FALSE-POSITIVE', 'WONTFIX']);
2364 | expect(validated.types).toEqual(['CODE_SMELL', 'BUG']);
2365 | expect(validated.rules).toEqual(['rule1', 'rule2']);
2366 | // Check that strings were kept intact
2367 | expect(validated.project_key).toBe('test-project');
2368 | expect(validated.severity).toBe('MAJOR');
2369 | expect(validated.created_after).toBe('2024-01-01');
2370 | });
2371 | });
2372 | describe('Direct tool registration test', () => {
2373 | it('should validate tool existence', () => {
2374 | // Skip if mcpServer is mocked or doesn't have tool method
2375 | if (mcpServer.tool) {
2376 | expect(mcpServer.tool).toBeDefined();
2377 | } else {
2378 | expect(mcpServer).toBeDefined();
2379 | }
2380 | });
2381 | it('should test the lambda functions directly', async () => {
2382 | // Create lambda functions that match the lambda functions in the tool registrations
2383 | const metricsLambda = async (params: Record<string, unknown>) => {
2384 | const result = await handleSonarQubeGetMetrics({
2385 | page: nullToUndefined(params.page) as number | undefined,
2386 | pageSize: nullToUndefined(params.page_size) as number | undefined,
2387 | });
2388 | return { content: [{ type: 'text', text: JSON.stringify(result) }] };
2389 | };
2390 | const issuesLambda = async (params: Record<string, unknown>) => {
2391 | return await handleSonarQubeGetIssues(mapToSonarQubeParams(params));
2392 | };
2393 | const componentsLambda = async (params: Record<string, unknown>) => {
2394 | return await handleSonarQubeComponentsMeasures({
2395 | componentKeys: Array.isArray(params.component_keys)
2396 | ? (params.component_keys as string[])
2397 | : [params.component_keys as string],
2398 | metricKeys: Array.isArray(params.metric_keys)
2399 | ? (params.metric_keys as string[])
2400 | : [params.metric_keys as string],
2401 | additionalFields: params.additional_fields as string[] | undefined,
2402 | branch: params.branch as string | undefined,
2403 | pullRequest: params.pull_request as string | undefined,
2404 | period: params.period as string | undefined,
2405 | page: nullToUndefined(params.page) as number | undefined,
2406 | pageSize: nullToUndefined(params.page_size) as number | undefined,
2407 | });
2408 | };
2409 | const historyLambda = async (params: Record<string, unknown>) => {
2410 | return await handleSonarQubeMeasuresHistory({
2411 | component: params.component as string,
2412 | metrics: Array.isArray(params.metrics)
2413 | ? (params.metrics as string[])
2414 | : [params.metrics as string],
2415 | from: params.from as string | undefined,
2416 | to: params.to as string | undefined,
2417 | branch: params.branch as string | undefined,
2418 | pullRequest: params.pull_request as string | undefined,
2419 | page: nullToUndefined(params.page) as number | undefined,
2420 | pageSize: nullToUndefined(params.page_size) as number | undefined,
2421 | });
2422 | };
2423 | // Mock all the handler functions to test the lambda functions
2424 | const mockGetMetrics = (vi.fn() as any).mockResolvedValue({
2425 | content: [{ type: 'text', text: '{"metrics":[]}' }],
2426 | });
2427 | const mockGetIssues = (vi.fn() as any).mockResolvedValue({
2428 | content: [{ type: 'text', text: '{"issues":[]}' }],
2429 | });
2430 | const mockComponentsMeasures = (vi.fn() as any).mockResolvedValue({
2431 | content: [{ type: 'text', text: '{"components":[]}' }],
2432 | });
2433 | const mockMeasuresHistory = (vi.fn() as any).mockResolvedValue({
2434 | content: [{ type: 'text', text: '{"measures":[]}' }],
2435 | });
2436 | // Override the handler functions with mocks
2437 | const originalGetMetrics = handleSonarQubeGetMetrics;
2438 | const originalGetIssues = handleSonarQubeGetIssues;
2439 | const originalComponentsMeasures = handleSonarQubeComponentsMeasures;
2440 | const originalMeasuresHistory = handleSonarQubeMeasuresHistory;
2441 | handleSonarQubeGetMetrics = mockGetMetrics;
2442 | handleSonarQubeGetIssues = mockGetIssues;
2443 | handleSonarQubeComponentsMeasures = mockComponentsMeasures;
2444 | handleSonarQubeMeasuresHistory = mockMeasuresHistory;
2445 | // Test metrics lambda
2446 | await metricsLambda({ page: '10', page_size: '20' });
2447 | expect(mockGetMetrics).toHaveBeenCalledWith({
2448 | page: '10',
2449 | pageSize: '20',
2450 | });
2451 | // Test issues lambda with all possible parameters
2452 | await issuesLambda({
2453 | project_key: 'test-project',
2454 | severity: 'MAJOR',
2455 | page: '1',
2456 | page_size: '10',
2457 | statuses: ['OPEN', 'CONFIRMED'],
2458 | resolutions: ['FALSE-POSITIVE', 'WONTFIX'],
2459 | resolved: 'true',
2460 | types: ['CODE_SMELL', 'BUG'],
2461 | rules: ['rule1', 'rule2'],
2462 | tags: ['tag1', 'tag2'],
2463 | created_after: '2023-01-01',
2464 | created_before: '2023-12-31',
2465 | created_at: '2023-06-15',
2466 | created_in_last: '7d',
2467 | assignees: ['user1', 'user2'],
2468 | authors: ['author1', 'author2'],
2469 | cwe: ['cwe1', 'cwe2'],
2470 | languages: ['java', 'typescript'],
2471 | owasp_top10: ['a1', 'a2'],
2472 | sans_top25: ['sans1', 'sans2'],
2473 | sonarsource_security: ['sec1', 'sec2'],
2474 | on_component_only: 'true',
2475 | facets: ['facet1', 'facet2'],
2476 | since_leak_period: 'true',
2477 | in_new_code_period: 'true',
2478 | });
2479 | expect(mockGetIssues).toHaveBeenCalledTimes(1);
2480 | // Test components lambda
2481 | await componentsLambda({
2482 | component_keys: ['comp1', 'comp2'],
2483 | metric_keys: ['coverage', 'bugs'],
2484 | additional_fields: ['periods'],
2485 | branch: 'main',
2486 | pull_request: 'pr-123',
2487 | period: '1',
2488 | page: '2',
2489 | page_size: '25',
2490 | });
2491 | expect(mockComponentsMeasures).toHaveBeenCalledWith({
2492 | componentKeys: ['comp1', 'comp2'],
2493 | metricKeys: ['coverage', 'bugs'],
2494 | additionalFields: ['periods'],
2495 | branch: 'main',
2496 | pullRequest: 'pr-123',
2497 | period: '1',
2498 | page: '2',
2499 | pageSize: '25',
2500 | });
2501 | // Test history lambda
2502 | await historyLambda({
2503 | component: 'test-component',
2504 | metrics: ['coverage', 'bugs'],
2505 | from: '2023-01-01',
2506 | to: '2023-12-31',
2507 | branch: 'feature',
2508 | pull_request: 'pr-456',
2509 | page: '3',
2510 | page_size: '30',
2511 | });
2512 | expect(mockMeasuresHistory).toHaveBeenCalledWith({
2513 | component: 'test-component',
2514 | metrics: ['coverage', 'bugs'],
2515 | from: '2023-01-01',
2516 | to: '2023-12-31',
2517 | branch: 'feature',
2518 | pullRequest: 'pr-456',
2519 | page: '3',
2520 | pageSize: '30',
2521 | });
2522 | // Restore the original functions
2523 | handleSonarQubeGetMetrics = originalGetMetrics;
2524 | handleSonarQubeGetIssues = originalGetIssues;
2525 | handleSonarQubeComponentsMeasures = originalComponentsMeasures;
2526 | handleSonarQubeMeasuresHistory = originalMeasuresHistory;
2527 | });
2528 | });
2529 | describe('Tool schema transformations with actual Zod schemas', () => {
2530 | it('should transform issues tool parameters through Zod schema', () => {
2531 | // Import the actual schema from the tool registration
2532 | const issuesSchema = z.object({
2533 | project_key: z.string().optional(),
2534 | on_component_only: z
2535 | .union([z.boolean(), z.string().transform((val: any) => val === 'true')])
2536 | .nullable()
2537 | .optional(),
2538 | resolved: z
2539 | .union([z.boolean(), z.string().transform((val: any) => val === 'true')])
2540 | .nullable()
2541 | .optional(),
2542 | page: z
2543 | .string()
2544 | .optional()
2545 | .transform((val: any) => (val ? parseInt(val, 10) || null : null)),
2546 | page_size: z
2547 | .string()
2548 | .optional()
2549 | .transform((val: any) => (val ? parseInt(val, 10) || null : null)),
2550 | });
2551 | // Test with string values that should be transformed
2552 | const result = issuesSchema.parse({
2553 | project_key: 'test-project',
2554 | on_component_only: 'true',
2555 | resolved: 'false',
2556 | page: '5',
2557 | page_size: '100',
2558 | });
2559 | expect(result.on_component_only).toBe(true);
2560 | expect(result.resolved).toBe(false);
2561 | expect(result.page).toBe(5);
2562 | expect(result.page_size).toBe(100);
2563 | });
2564 | it('should transform hotspots tool parameters through Zod schema', () => {
2565 | // Import the actual schema from the tool registration
2566 | const hotspotsSchema = z.object({
2567 | project_key: z.string().optional(),
2568 | assigned_to_me: z
2569 | .union([z.boolean(), z.string().transform((val: any) => val === 'true')])
2570 | .nullable()
2571 | .optional(),
2572 | since_leak_period: z
2573 | .union([z.boolean(), z.string().transform((val: any) => val === 'true')])
2574 | .nullable()
2575 | .optional(),
2576 | in_new_code_period: z
2577 | .union([z.boolean(), z.string().transform((val: any) => val === 'true')])
2578 | .nullable()
2579 | .optional(),
2580 | page: z
2581 | .string()
2582 | .optional()
2583 | .transform((val: any) => (val ? parseInt(val, 10) || null : null)),
2584 | page_size: z
2585 | .string()
2586 | .optional()
2587 | .transform((val: any) => (val ? parseInt(val, 10) || null : null)),
2588 | });
2589 | // Test with string values that should be transformed
2590 | const result = hotspotsSchema.parse({
2591 | project_key: 'test-project',
2592 | assigned_to_me: 'true',
2593 | since_leak_period: 'false',
2594 | in_new_code_period: 'true',
2595 | page: '2',
2596 | page_size: '50',
2597 | });
2598 | expect(result.assigned_to_me).toBe(true);
2599 | expect(result.since_leak_period).toBe(false);
2600 | expect(result.in_new_code_period).toBe(true);
2601 | expect(result.page).toBe(2);
2602 | expect(result.page_size).toBe(50);
2603 | // Test with boolean values directly
2604 | const result2 = hotspotsSchema.parse({
2605 | project_key: 'test-project',
2606 | assigned_to_me: false,
2607 | since_leak_period: true,
2608 | in_new_code_period: false,
2609 | });
2610 | expect(result2.assigned_to_me).toBe(false);
2611 | expect(result2.since_leak_period).toBe(true);
2612 | expect(result2.in_new_code_period).toBe(false);
2613 | });
2614 | });
2615 | describe('Security Hotspot handlers', () => {
2616 | describe('handleSonarQubeSearchHotspots', () => {
2617 | it('should search and return hotspots', async () => {
2618 | nock('http://localhost:9000')
2619 | .get('/api/hotspots/search')
2620 | .query({
2621 | projectKey: 'test-project',
2622 | status: 'TO_REVIEW',
2623 | p: '1',
2624 | ps: '50',
2625 | })
2626 | .matchHeader('authorization', 'Bearer test-token')
2627 | .reply(200, {
2628 | hotspots: [
2629 | {
2630 | key: 'AYg1234567890',
2631 | component: 'com.example:my-project:src/main/java/Example.java',
2632 | project: 'com.example:my-project',
2633 | securityCategory: 'sql-injection',
2634 | vulnerabilityProbability: 'HIGH',
2635 | status: 'TO_REVIEW',
2636 | line: 42,
2637 | message: 'Make sure using this database query is safe.',
2638 | author: '[email protected]',
2639 | creationDate: '2023-01-15T10:30:00+0000',
2640 | },
2641 | ],
2642 | components: [
2643 | {
2644 | key: 'com.example:my-project:src/main/java/Example.java',
2645 | name: 'Example.java',
2646 | path: 'src/main/java/Example.java',
2647 | },
2648 | ],
2649 | paging: {
2650 | pageIndex: 1,
2651 | pageSize: 50,
2652 | total: 1,
2653 | },
2654 | });
2655 | const response = await handleSonarQubeHotspots({
2656 | projectKey: 'test-project',
2657 | status: 'TO_REVIEW',
2658 | page: 1,
2659 | pageSize: 50,
2660 | });
2661 | const result = JSON.parse(response.content[0].text);
2662 | expect(result.hotspots).toHaveLength(1);
2663 | expect(result.hotspots[0].key).toBe('AYg1234567890');
2664 | expect(result.hotspots[0].status).toBe('TO_REVIEW');
2665 | expect(result.paging.total).toBe(1);
2666 | });
2667 | });
2668 | describe('handleSonarQubeGetHotspotDetails', () => {
2669 | it('should get and return hotspot details', async () => {
2670 | nock('http://localhost:9000')
2671 | .get('/api/hotspots/show')
2672 | .query({
2673 | hotspot: 'AYg1234567890',
2674 | })
2675 | .matchHeader('authorization', 'Bearer test-token')
2676 | .reply(200, {
2677 | key: 'AYg1234567890',
2678 | component: {
2679 | key: 'com.example:my-project:src/main/java/Example.java',
2680 | name: 'Example.java',
2681 | qualifier: 'FIL',
2682 | path: 'src/main/java/Example.java',
2683 | },
2684 | project: {
2685 | key: 'com.example:my-project',
2686 | name: 'My Project',
2687 | qualifier: 'TRK',
2688 | },
2689 | rule: {
2690 | key: 'java:S2077',
2691 | name: 'SQL queries should not be vulnerable to injection attacks',
2692 | securityCategory: 'sql-injection',
2693 | vulnerabilityProbability: 'HIGH',
2694 | },
2695 | status: 'TO_REVIEW',
2696 | line: 42,
2697 | message: 'Make sure using this database query is safe.',
2698 | author: '[email protected]',
2699 | creationDate: '2023-01-15T10:30:00+0000',
2700 | updateDate: '2023-01-15T10:30:00+0000',
2701 | flows: [],
2702 | canChangeStatus: true,
2703 | });
2704 | const response = await handleSonarQubeHotspot('AYg1234567890');
2705 | expect(response).toBeDefined();
2706 | expect(response.content).toBeDefined();
2707 | expect(response.content[0]).toBeDefined();
2708 | const result = JSON.parse(response.content[0].text);
2709 | expect(result.key).toBe('AYg1234567890');
2710 | expect(result.status).toBe('TO_REVIEW');
2711 | expect(result.rule.securityCategory).toBe('sql-injection');
2712 | expect(result.canChangeStatus).toBe(true);
2713 | });
2714 | });
2715 | describe('handleSonarQubeUpdateHotspotStatus', () => {
2716 | it('should update hotspot status', async () => {
2717 | nock('http://localhost:9000')
2718 | .post('/api/hotspots/change_status', {
2719 | hotspot: 'AYg1234567890',
2720 | status: 'REVIEWED',
2721 | resolution: 'FIXED',
2722 | comment: 'Fixed by using parameterized queries',
2723 | })
2724 | .matchHeader('authorization', 'Bearer test-token')
2725 | .reply(200, {});
2726 | const response = await handleSonarQubeUpdateHotspotStatus({
2727 | hotspot: 'AYg1234567890',
2728 | status: 'REVIEWED',
2729 | resolution: 'FIXED',
2730 | comment: 'Fixed by using parameterized queries',
2731 | });
2732 | expect(response.content[0].text).toContain('Hotspot status updated successfully');
2733 | });
2734 | it('should update hotspot status without optional fields', async () => {
2735 | nock('http://localhost:9000')
2736 | .post('/api/hotspots/change_status', {
2737 | hotspot: 'AYg1234567890',
2738 | status: 'TO_REVIEW',
2739 | })
2740 | .matchHeader('authorization', 'Bearer test-token')
2741 | .reply(200, {});
2742 | const response = await handleSonarQubeUpdateHotspotStatus({
2743 | hotspot: 'AYg1234567890',
2744 | status: 'TO_REVIEW',
2745 | });
2746 | expect(response.content[0].text).toContain('Hotspot status updated successfully');
2747 | });
2748 | });
2749 | });
2750 | describe('Create Default Client', () => {
2751 | it('should create default client with environment variables', async () => {
2752 | // Ensure environment variables are set
2753 | process.env.SONARQUBE_TOKEN = 'test-token';
2754 | process.env.SONARQUBE_URL = 'http://localhost:9000';
2755 | // Import module fresh
2756 | vi.resetModules();
2757 | const index = await import('../index.js');
2758 | // Call createDefaultClient - it should not throw
2759 | expect(() => index.createDefaultClient()).not.toThrow();
2760 | });
2761 | });
2762 | describe('Error Handling Coverage', () => {
2763 | it('should handle errors in handler functions', async () => {
2764 | // Import module fresh
2765 | vi.resetModules();
2766 | const index = await import('../index.js');
2767 | // Mock API calls to fail
2768 | nock('http://localhost:9000')
2769 | .get('/api/projects/search')
2770 | .query(true)
2771 | .reply(500, 'Internal Server Error');
2772 | // Test error handling
2773 | await expect(index.handleSonarQubeProjects({})).rejects.toThrow();
2774 | }, 10000);
2775 | it('should test parameter mapping with null values', async () => {
2776 | vi.resetModules();
2777 | const index = await import('../index.js');
2778 | // Test mapToSonarQubeParams with various null/undefined values
2779 | const result = index.mapToSonarQubeParams({
2780 | project_key: null,
2781 | projects: undefined,
2782 | component_keys: null,
2783 | components: null,
2784 | on_component_only: false,
2785 | branch: null,
2786 | pull_request: undefined,
2787 | issues: null,
2788 | severities: null,
2789 | statuses: null,
2790 | resolutions: null,
2791 | resolved: null,
2792 | types: null,
2793 | tags: null,
2794 | rules: null,
2795 | created_after: null,
2796 | created_before: null,
2797 | created_at: null,
2798 | created_in_last: null,
2799 | assigned: null,
2800 | assignees: null,
2801 | author: null,
2802 | authors: null,
2803 | cwe: null,
2804 | owasp_top10: null,
2805 | owasp_top10_v2021: null,
2806 | sans_top25: null,
2807 | sonarsource_security: null,
2808 | sonarsource_security_category: null,
2809 | languages: null,
2810 | facets: null,
2811 | facet_mode: null,
2812 | since_leak_period: null,
2813 | in_new_code_period: null,
2814 | s: null,
2815 | asc: null,
2816 | additional_fields: null,
2817 | page: null,
2818 | page_size: null,
2819 | clean_code_attribute_categories: null,
2820 | impact_severities: null,
2821 | impact_software_qualities: null,
2822 | issue_statuses: null,
2823 | severity: null,
2824 | hotspots: null,
2825 | });
2826 | // Verify null values are converted to undefined
2827 | expect(result.projectKey).toBeUndefined();
2828 | expect(result.projects).toBeUndefined();
2829 | expect(result.componentKeys).toBeUndefined();
2830 | expect(result.components).toBeUndefined();
2831 | expect(result.onComponentOnly).toBe(false);
2832 | expect(result.branch).toBeUndefined();
2833 | expect(result.pullRequest).toBeUndefined();
2834 | });
2835 | });
2836 | describe('MCP Wrapper Functions Coverage', () => {
2837 | it('should test all MCP wrapper functions', async () => {
2838 | // Import module fresh
2839 | vi.resetModules();
2840 | const index = await import('../index.js');
2841 | // Mock all API calls
2842 | nock('http://localhost:9000')
2843 | .get('/api/projects/search')
2844 | .query(true)
2845 | .times(2)
2846 | .reply(200, {
2847 | components: [],
2848 | paging: { pageIndex: 1, pageSize: 100, total: 0 },
2849 | });
2850 | nock('http://localhost:9000')
2851 | .get('/api/metrics/search')
2852 | .query(true)
2853 | .times(2)
2854 | .reply(200, {
2855 | metrics: [],
2856 | paging: { pageIndex: 1, pageSize: 100, total: 0 },
2857 | });
2858 | nock('http://localhost:9000')
2859 | .get('/api/issues/search')
2860 | .query(true)
2861 | .times(2)
2862 | .reply(200, {
2863 | issues: [],
2864 | components: [],
2865 | rules: [],
2866 | paging: { pageIndex: 1, pageSize: 100, total: 0 },
2867 | });
2868 | nock('http://localhost:9000')
2869 | .get('/api/v2/system/health')
2870 | .times(2)
2871 | .reply(200, { status: 'GREEN', checkedAt: '2023-12-01T10:00:00Z' });
2872 | nock('http://localhost:9000')
2873 | .get('/api/system/status')
2874 | .times(2)
2875 | .reply(200, { id: '1', version: '10.0', status: 'UP' });
2876 | nock('http://localhost:9000').get('/api/system/ping').times(2).reply(200, 'pong');
2877 | nock('http://localhost:9000')
2878 | .get('/api/measures/component')
2879 | .query(true)
2880 | .times(2)
2881 | .reply(200, {
2882 | component: { key: 'test', measures: [] },
2883 | metrics: [],
2884 | });
2885 | nock('http://localhost:9000')
2886 | .get('/api/measures/component')
2887 | .query(true)
2888 | .times(4)
2889 | .reply(200, {
2890 | component: { key: 'test', measures: [] },
2891 | metrics: [],
2892 | });
2893 | nock('http://localhost:9000')
2894 | .get('/api/measures/search_history')
2895 | .query(true)
2896 | .times(2)
2897 | .reply(200, {
2898 | measures: [],
2899 | paging: { pageIndex: 1, pageSize: 100, total: 0 },
2900 | });
2901 | nock('http://localhost:9000').get('/api/qualitygates/list').times(2).reply(200, {
2902 | qualitygates: [],
2903 | default: 'default',
2904 | });
2905 | nock('http://localhost:9000').get('/api/qualitygates/show').query(true).times(2).reply(200, {
2906 | id: 'test',
2907 | name: 'Test Gate',
2908 | conditions: [],
2909 | });
2910 | nock('http://localhost:9000')
2911 | .get('/api/qualitygates/project_status')
2912 | .query(true)
2913 | .times(2)
2914 | .reply(200, {
2915 | projectStatus: { status: 'OK', conditions: [] },
2916 | });
2917 | nock('http://localhost:9000')
2918 | .get('/api/sources/raw')
2919 | .query(true)
2920 | .times(2)
2921 | .reply(200, 'source code content');
2922 | nock('http://localhost:9000')
2923 | .get('/api/sources/scm')
2924 | .query(true)
2925 | .times(2)
2926 | .reply(200, {
2927 | component: { key: 'test' },
2928 | sources: {},
2929 | });
2930 | nock('http://localhost:9000')
2931 | .get('/api/hotspots/search')
2932 | .query(true)
2933 | .times(2)
2934 | .reply(200, {
2935 | hotspots: [],
2936 | paging: { pageIndex: 1, pageSize: 100, total: 0 },
2937 | });
2938 | nock('http://localhost:9000')
2939 | .get('/api/hotspots/show')
2940 | .query(true)
2941 | .times(2)
2942 | .reply(200, {
2943 | key: 'hotspot-1',
2944 | component: 'test',
2945 | project: 'test',
2946 | rule: { key: 'test', name: 'Test' },
2947 | status: 'TO_REVIEW',
2948 | securityCategory: 'test',
2949 | vulnerabilityProbability: 'HIGH',
2950 | line: 1,
2951 | message: 'Test',
2952 | });
2953 | nock('http://localhost:9000').post('/api/hotspots/change_status').times(2).reply(200);
2954 | // Access the wrapper functions via the module
2955 | const module = index;
2956 | // Call all handler functions
2957 | await module.projectsHandler({});
2958 | await module.metricsHandler({ page: 1, page_size: 10 });
2959 | await module.issuesHandler({ project_key: 'test' });
2960 | await module.healthHandler();
2961 | await module.statusHandler();
2962 | await module.pingHandler();
2963 | await module.componentMeasuresHandler({ component: 'test', metric_keys: ['coverage'] });
2964 | await module.componentsMeasuresHandler({
2965 | component_keys: ['test'],
2966 | metric_keys: ['coverage'],
2967 | });
2968 | await module.measuresHistoryHandler({ component: 'test', metrics: ['coverage'] });
2969 | await module.qualityGatesHandler();
2970 | await module.qualityGateHandler({ id: 'test' });
2971 | await module.qualityGateStatusHandler({ project_key: 'test' });
2972 | await module.sourceCodeHandler({ key: 'test' });
2973 | await module.scmBlameHandler({ key: 'test' });
2974 | await module.hotspotsHandler({ project_key: 'test' });
2975 | await module.hotspotHandler({ hotspot_key: 'hotspot-1' });
2976 | await module.updateHotspotStatusHandler({ hotspot_key: 'hotspot-1', status: 'REVIEWED' });
2977 |
2978 | // Verify handlers were called (basic smoke test)
2979 | expect(module.projectsHandler).toBeDefined();
2980 | expect(module.metricsHandler).toBeDefined();
2981 | });
2982 | });
2983 | describe('MCP Wrapper Functions Direct Coverage', () => {
2984 | beforeEach(() => {
2985 | process.env.SONARQUBE_TOKEN = 'test-token';
2986 | process.env.SONARQUBE_URL = 'http://localhost:9000';
2987 | vi.resetModules();
2988 | });
2989 | it('should cover all MCP wrapper functions', async () => {
2990 | // Set up mocks for all endpoints
2991 | nock('http://localhost:9000')
2992 | .get('/api/projects/search')
2993 | .query(true)
2994 | .reply(200, { components: [], paging: { pageIndex: 1, pageSize: 100, total: 0 } });
2995 | nock('http://localhost:9000')
2996 | .get('/api/metrics/search')
2997 | .query(true)
2998 | .reply(200, { metrics: [], total: 0 });
2999 | nock('http://localhost:9000')
3000 | .get('/api/issues/search')
3001 | .query(true)
3002 | .reply(200, { issues: [], total: 0, paging: { pageIndex: 1, pageSize: 100, total: 0 } });
3003 | nock('http://localhost:9000')
3004 | .get('/api/v2/system/health')
3005 | .reply(200, { status: 'GREEN', checkedAt: '2023-12-01T10:00:00Z' });
3006 | nock('http://localhost:9000')
3007 | .get('/api/system/status')
3008 | .reply(200, { status: 'UP', version: '10.0' });
3009 | nock('http://localhost:9000').get('/api/system/ping').reply(200, 'pong');
3010 | nock('http://localhost:9000')
3011 | .get('/api/measures/component')
3012 | .query(true)
3013 | .times(3) // Allow multiple calls
3014 | .reply(200, { component: { key: 'test', measures: [] }, metrics: [] });
3015 | nock('http://localhost:9000')
3016 | .get('/api/measures/search_history')
3017 | .query(true)
3018 | .reply(200, { measures: [] });
3019 | nock('http://localhost:9000').get('/api/qualitygates/list').reply(200, { qualitygates: [] });
3020 | nock('http://localhost:9000')
3021 | .get('/api/qualitygates/show')
3022 | .query(true)
3023 | .reply(200, { id: 'test', name: 'Test', conditions: [] });
3024 | nock('http://localhost:9000')
3025 | .get('/api/qualitygates/project_status')
3026 | .query(true)
3027 | .reply(200, { projectStatus: { status: 'OK' } });
3028 | nock('http://localhost:9000').get('/api/sources/raw').query(true).reply(200, 'source code');
3029 | nock('http://localhost:9000')
3030 | .get('/api/sources/scm')
3031 | .query(true)
3032 | .reply(200, { component: { key: 'test' }, sources: {} });
3033 | nock('http://localhost:9000')
3034 | .get('/api/hotspots/search')
3035 | .query(true)
3036 | .reply(200, { hotspots: [], paging: { pageIndex: 1, pageSize: 100, total: 0 } });
3037 | nock('http://localhost:9000')
3038 | .get('/api/hotspots/show')
3039 | .query(true)
3040 | .reply(200, {
3041 | key: 'test-hotspot',
3042 | component: 'test',
3043 | project: 'test',
3044 | rule: { key: 'test', name: 'Test' },
3045 | status: 'TO_REVIEW',
3046 | securityCategory: 'test',
3047 | vulnerabilityProbability: 'HIGH',
3048 | });
3049 | nock('http://localhost:9000').post('/api/hotspots/change_status').reply(200);
3050 | // Mock issue resolution endpoints
3051 | nock('http://localhost:9000').post('/api/issues/add_comment').times(8).reply(200, {});
3052 | nock('http://localhost:9000')
3053 | .post('/api/issues/do_transition')
3054 | .times(8) // 4 individual calls + 4 from bulk operations (2 issues each)
3055 | .reply(200, {
3056 | issue: { key: 'test-issue', status: 'RESOLVED' },
3057 | components: [],
3058 | rules: [],
3059 | users: [],
3060 | });
3061 | // Import and call all MCP wrapper functions
3062 | const index = await import('../index.js');
3063 | // Test all wrapper functions
3064 | await index.projectsMcpHandler({});
3065 | await index.metricsMcpHandler({ page: 1, page_size: 10 });
3066 | await index.issuesMcpHandler({ project_key: 'test' });
3067 | await index.healthMcpHandler();
3068 | await index.statusMcpHandler();
3069 | await index.pingMcpHandler();
3070 | await index.componentMeasuresMcpHandler({ component: 'test', metric_keys: ['coverage'] });
3071 | await index.componentsMeasuresMcpHandler({
3072 | component_keys: ['test'],
3073 | metric_keys: ['coverage'],
3074 | });
3075 | await index.measuresHistoryMcpHandler({ component: 'test', metrics: ['coverage'] });
3076 | await index.qualityGatesMcpHandler();
3077 | await index.qualityGateMcpHandler({ id: 'test' });
3078 | await index.qualityGateStatusMcpHandler({ project_key: 'test' });
3079 | await index.sourceCodeMcpHandler({ key: 'test' });
3080 | await index.scmBlameMcpHandler({ key: 'test' });
3081 | await index.hotspotsMcpHandler({ project_key: 'test' });
3082 | await index.hotspotMcpHandler({ hotspot_key: 'test-hotspot' });
3083 | await index.updateHotspotStatusMcpHandler({
3084 | hotspot_key: 'test-hotspot',
3085 | status: 'REVIEWED',
3086 | });
3087 | // Test new issue resolution MCP handlers
3088 | await index.markIssueFalsePositiveMcpHandler({
3089 | issue_key: 'ISSUE-123',
3090 | comment: 'Test comment',
3091 | });
3092 | await index.markIssueWontFixMcpHandler({
3093 | issue_key: 'ISSUE-456',
3094 | comment: 'Test comment',
3095 | });
3096 | await index.markIssuesFalsePositiveMcpHandler({
3097 | issue_keys: ['ISSUE-123', 'ISSUE-124'],
3098 | comment: 'Bulk comment',
3099 | });
3100 | await index.markIssuesWontFixMcpHandler({
3101 | issue_keys: ['ISSUE-456', 'ISSUE-457'],
3102 | comment: 'Bulk comment',
3103 | });
3104 |
3105 | // Verify handlers exist
3106 | expect(index.projectsHandler).toBeDefined();
3107 | expect(index.metricsHandler).toBeDefined();
3108 | });
3109 | });
3110 | });
3111 |
```