This is page 10 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__/sonarqube.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import nock from 'nock';
2 | import { SonarQubeClient } from '../sonarqube.js';
3 |
4 | describe('SonarQubeClient', () => {
5 | const baseUrl = 'https://sonarqube.example.com';
6 | const token = 'test-token';
7 | let client: SonarQubeClient;
8 |
9 | beforeEach(() => {
10 | client = new SonarQubeClient(token, baseUrl);
11 | nock.cleanAll();
12 | });
13 |
14 | afterEach(() => {
15 | nock.cleanAll();
16 | });
17 |
18 | describe('listProjects', () => {
19 | it('should fetch projects successfully', async () => {
20 | const mockResponse = {
21 | components: [
22 | {
23 | key: 'project1',
24 | name: 'Project 1',
25 | qualifier: 'TRK',
26 | visibility: 'public',
27 | lastAnalysisDate: '2023-01-01',
28 | revision: 'cfb82f55c6ef32e61828c4cb3db2da12795fd767',
29 | managed: false,
30 | },
31 | {
32 | key: 'project2',
33 | name: 'Project 2',
34 | qualifier: 'TRK',
35 | visibility: 'private',
36 | revision: '7be96a94ac0c95a61ee6ee0ef9c6f808d386a355',
37 | managed: false,
38 | },
39 | ],
40 | paging: {
41 | pageIndex: 1,
42 | pageSize: 10,
43 | total: 2,
44 | },
45 | };
46 |
47 | nock(baseUrl)
48 | .get('/api/projects/search')
49 | .query(true)
50 | .matchHeader('authorization', 'Bearer test-token')
51 | .reply(200, mockResponse);
52 |
53 | const result = await client.listProjects();
54 |
55 | // Should return transformed data with 'projects' instead of 'components'
56 | expect(result.projects).toHaveLength(2);
57 | expect(result.projects?.[0]?.key).toBe('project1');
58 | expect(result.projects?.[1]?.key).toBe('project2');
59 | expect(result.paging).toEqual(mockResponse.paging);
60 | });
61 |
62 | it('should handle pagination parameters', async () => {
63 | const mockResponse = {
64 | components: [
65 | {
66 | key: 'project3',
67 | name: 'Project 3',
68 | qualifier: 'TRK',
69 | visibility: 'public',
70 | revision: 'abc12345def67890abc12345def67890abc12345',
71 | managed: false,
72 | },
73 | ],
74 | paging: {
75 | pageIndex: 2,
76 | pageSize: 1,
77 | total: 3,
78 | },
79 | };
80 |
81 | const scope = nock(baseUrl)
82 | .get('/api/projects/search')
83 | .query(true)
84 | .matchHeader('authorization', 'Bearer test-token')
85 | .reply(200, mockResponse);
86 |
87 | const result = await client.listProjects({
88 | page: 2,
89 | pageSize: 1,
90 | });
91 |
92 | // Should return transformed data with 'projects' instead of 'components'
93 | expect(result.projects).toHaveLength(1);
94 | expect(result.projects?.[0]?.key).toBe('project3');
95 | expect(result.paging).toEqual(mockResponse.paging);
96 | expect(scope.isDone()).toBe(true);
97 | });
98 | });
99 |
100 | describe('getIssues', () => {
101 | it('should fetch issues successfully', async () => {
102 | const mockResponse = {
103 | issues: [
104 | {
105 | key: 'issue1',
106 | rule: 'rule1',
107 | severity: 'MAJOR',
108 | component: 'component1',
109 | project: 'project1',
110 | line: 42,
111 | status: 'OPEN',
112 | issueStatus: 'ACCEPTED',
113 | message: 'Fix this issue',
114 | messageFormattings: [
115 | {
116 | start: 0,
117 | end: 4,
118 | type: 'CODE',
119 | },
120 | ],
121 | tags: ['bug', 'security'],
122 | creationDate: '2023-01-01',
123 | updateDate: '2023-01-02',
124 | type: 'BUG',
125 | cleanCodeAttribute: 'CLEAR',
126 | cleanCodeAttributeCategory: 'INTENTIONAL',
127 | prioritizedRule: false,
128 | impacts: [
129 | {
130 | softwareQuality: 'SECURITY',
131 | severity: 'HIGH',
132 | },
133 | ],
134 | textRange: {
135 | startLine: 42,
136 | endLine: 42,
137 | startOffset: 0,
138 | endOffset: 100,
139 | },
140 | },
141 | ],
142 | components: [
143 | {
144 | key: 'component1',
145 | enabled: true,
146 | qualifier: 'FIL',
147 | name: 'Component 1',
148 | longName: 'src/main/component1.java',
149 | path: 'src/main/component1.java',
150 | },
151 | ],
152 | rules: [
153 | {
154 | key: 'rule1',
155 | name: 'Rule 1',
156 | status: 'READY',
157 | lang: 'java',
158 | langName: 'Java',
159 | },
160 | ],
161 | paging: {
162 | pageIndex: 1,
163 | pageSize: 10,
164 | total: 1,
165 | },
166 | };
167 |
168 | nock(baseUrl)
169 | .get('/api/issues/search')
170 | .query(true)
171 | .matchHeader('authorization', 'Bearer test-token')
172 | .reply(200, mockResponse);
173 |
174 | const result = await client.getIssues({
175 | projectKey: 'project1',
176 | page: undefined,
177 | pageSize: undefined,
178 | });
179 | expect(result).toEqual(mockResponse);
180 | expect(result.issues?.[0]?.cleanCodeAttribute).toBe('CLEAR');
181 | expect(result.issues?.[0]?.impacts?.[0]?.softwareQuality).toBe('SECURITY');
182 | expect(result.components?.[0]?.qualifier).toBe('FIL');
183 | expect(result.rules?.[0]?.lang).toBe('java');
184 | });
185 |
186 | it('should handle filtering by severity', async () => {
187 | const mockResponse = {
188 | issues: [
189 | {
190 | key: 'issue2',
191 | rule: 'rule2',
192 | severity: 'CRITICAL',
193 | component: 'component2',
194 | project: 'project1',
195 | line: 100,
196 | status: 'OPEN',
197 | issueStatus: 'CONFIRMED',
198 | message: 'Critical issue',
199 | tags: ['security'],
200 | creationDate: '2023-01-03',
201 | updateDate: '2023-01-03',
202 | type: 'VULNERABILITY',
203 | cleanCodeAttribute: 'CLEAR',
204 | cleanCodeAttributeCategory: 'RESPONSIBLE',
205 | prioritizedRule: true,
206 | impacts: [
207 | {
208 | softwareQuality: 'SECURITY',
209 | severity: 'HIGH',
210 | },
211 | ],
212 | },
213 | ],
214 | components: [
215 | {
216 | key: 'component2',
217 | qualifier: 'FIL',
218 | name: 'Component 2',
219 | },
220 | ],
221 | rules: [
222 | {
223 | key: 'rule2',
224 | name: 'Rule 2',
225 | status: 'READY',
226 | lang: 'java',
227 | langName: 'Java',
228 | },
229 | ],
230 | paging: {
231 | pageIndex: 1,
232 | pageSize: 5,
233 | total: 1,
234 | },
235 | };
236 |
237 | const scope = nock(baseUrl)
238 | .get('/api/issues/search')
239 | .query({
240 | projects: 'project1',
241 | severities: 'CRITICAL',
242 | p: 1,
243 | ps: 5,
244 | })
245 | .matchHeader('authorization', 'Bearer test-token')
246 | .reply(200, mockResponse);
247 |
248 | const result = await client.getIssues({
249 | projectKey: 'project1',
250 | severity: 'CRITICAL',
251 | page: 1,
252 | pageSize: 5,
253 | });
254 |
255 | expect(result).toEqual(mockResponse);
256 | expect(scope.isDone()).toBe(true);
257 | });
258 |
259 | it('should handle multiple filter parameters', async () => {
260 | const mockResponse = {
261 | issues: [
262 | {
263 | key: 'issue3',
264 | rule: 'rule3',
265 | severity: 'MAJOR',
266 | component: 'component3',
267 | project: 'project1',
268 | line: 200,
269 | status: 'RESOLVED',
270 | message: 'Fixed issue',
271 | tags: ['code-smell'],
272 | creationDate: '2023-01-04',
273 | updateDate: '2023-01-05',
274 | type: 'CODE_SMELL',
275 | },
276 | ],
277 | components: [],
278 | rules: [],
279 | paging: {
280 | pageIndex: 1,
281 | pageSize: 10,
282 | total: 1,
283 | },
284 | };
285 |
286 | const scope = nock(baseUrl)
287 | .get('/api/issues/search')
288 | .query((queryObj) => {
289 | return (
290 | queryObj.projects === 'project1' &&
291 | queryObj.statuses === 'RESOLVED,CLOSED' &&
292 | queryObj.types === 'CODE_SMELL' &&
293 | queryObj.tags === 'code-smell,performance' &&
294 | queryObj.createdAfter === '2023-01-01' &&
295 | queryObj.languages === 'java,typescript'
296 | );
297 | })
298 | .matchHeader('authorization', 'Bearer test-token')
299 | .reply(200, mockResponse);
300 |
301 | const result = await client.getIssues({
302 | projectKey: 'project1',
303 | statuses: ['RESOLVED', 'CLOSED'],
304 | types: ['CODE_SMELL'],
305 | tags: ['code-smell', 'performance'],
306 | createdAfter: '2023-01-01',
307 | languages: ['java', 'typescript'],
308 | page: undefined,
309 | pageSize: undefined,
310 | });
311 |
312 | expect(result).toEqual(mockResponse);
313 | expect(scope.isDone()).toBe(true);
314 | });
315 |
316 | it('should handle boolean filter parameters', async () => {
317 | const mockResponse = {
318 | issues: [
319 | {
320 | key: 'issue4',
321 | rule: 'rule4',
322 | severity: 'BLOCKER',
323 | component: 'component4',
324 | project: 'project1',
325 | status: 'OPEN',
326 | message: 'New issue',
327 | tags: ['security'],
328 | creationDate: '2023-01-06',
329 | updateDate: '2023-01-06',
330 | type: 'VULNERABILITY',
331 | },
332 | ],
333 | components: [],
334 | rules: [],
335 | paging: {
336 | pageIndex: 1,
337 | pageSize: 10,
338 | total: 1,
339 | },
340 | };
341 |
342 | const scope = nock(baseUrl)
343 | .get('/api/issues/search')
344 | .query((queryObj) => {
345 | return (
346 | queryObj.projects === 'project1' &&
347 | queryObj.resolved === 'false' &&
348 | queryObj.sinceLeakPeriod === 'true' &&
349 | queryObj.inNewCodePeriod === 'true'
350 | );
351 | })
352 | .matchHeader('authorization', 'Bearer test-token')
353 | .reply(200, mockResponse);
354 |
355 | const result = await client.getIssues({
356 | projectKey: 'project1',
357 | resolved: false,
358 | sinceLeakPeriod: true,
359 | inNewCodePeriod: true,
360 | page: undefined,
361 | pageSize: undefined,
362 | });
363 |
364 | expect(result).toEqual(mockResponse);
365 | expect(scope.isDone()).toBe(true);
366 | });
367 | });
368 |
369 | describe('getMetrics', () => {
370 | it('should fetch metrics successfully', async () => {
371 | const mockResponse = {
372 | metrics: [
373 | {
374 | id: 'metric1',
375 | key: 'team_size',
376 | name: 'Team size',
377 | description: 'Number of people in the team',
378 | domain: 'Management',
379 | type: 'INT',
380 | direction: 0,
381 | qualitative: false,
382 | hidden: false,
383 | custom: true,
384 | },
385 | {
386 | id: 'metric2',
387 | key: 'uncovered_lines',
388 | name: 'Uncovered lines',
389 | description: 'Uncovered lines',
390 | domain: 'Tests',
391 | type: 'INT',
392 | direction: 1,
393 | qualitative: true,
394 | hidden: false,
395 | custom: false,
396 | },
397 | ],
398 | paging: {
399 | pageIndex: 1,
400 | pageSize: 100,
401 | total: 2,
402 | },
403 | };
404 |
405 | nock(baseUrl)
406 | .get('/api/metrics/search')
407 | .query(true)
408 | .matchHeader('authorization', 'Bearer test-token')
409 | .reply(200, mockResponse);
410 |
411 | const result = await client.getMetrics();
412 | expect(result).toEqual(mockResponse);
413 | expect(result.metrics).toHaveLength(2);
414 | expect(result.metrics?.[0]?.key).toBe('team_size');
415 | expect(result.metrics?.[1]?.key).toBe('uncovered_lines');
416 | expect(result.paging).toEqual(mockResponse.paging);
417 | });
418 |
419 | it('should handle pagination parameters', async () => {
420 | const mockResponse = {
421 | metrics: [
422 | {
423 | id: 'metric3',
424 | key: 'code_coverage',
425 | name: 'Code Coverage',
426 | description: 'Code coverage percentage',
427 | domain: 'Tests',
428 | type: 'PERCENT',
429 | direction: 1,
430 | qualitative: true,
431 | hidden: false,
432 | custom: false,
433 | },
434 | ],
435 | paging: {
436 | pageIndex: 2,
437 | pageSize: 1,
438 | total: 3,
439 | },
440 | };
441 |
442 | const scope = nock(baseUrl)
443 | .get('/api/metrics/search')
444 | .query((actualQuery) => {
445 | return actualQuery.p === '2' && actualQuery.ps === '1';
446 | })
447 | .matchHeader('authorization', 'Bearer test-token')
448 | .reply(200, mockResponse);
449 |
450 | const result = await client.getMetrics({
451 | page: 2,
452 | pageSize: 1,
453 | });
454 |
455 | expect(result.metrics).toHaveLength(1);
456 | expect(result.metrics?.[0]?.key).toBe('code_coverage');
457 | expect(result.paging).toEqual(mockResponse.paging);
458 | expect(scope.isDone()).toBe(true);
459 | });
460 | });
461 |
462 | describe('getHealth', () => {
463 | it('should fetch health status successfully', async () => {
464 | const mockV2Response = {
465 | status: 'GREEN',
466 | checkedAt: '2023-12-01T10:00:00Z',
467 | };
468 |
469 | const expectedResponse = {
470 | health: 'GREEN',
471 | causes: [],
472 | };
473 |
474 | nock(baseUrl)
475 | .get('/api/v2/system/health')
476 | .matchHeader('authorization', 'Bearer test-token')
477 | .reply(200, mockV2Response);
478 |
479 | const result = await client.getHealth();
480 | expect(result).toEqual(expectedResponse);
481 | expect(result.health).toBe('GREEN');
482 | expect(result.causes).toEqual([]);
483 | });
484 |
485 | it('should handle warning health status', async () => {
486 | const mockV2Response = {
487 | status: 'YELLOW',
488 | checkedAt: '2023-12-01T10:00:00Z',
489 | nodes: [
490 | {
491 | name: 'node1',
492 | status: 'YELLOW',
493 | causes: ['Disk space low'],
494 | },
495 | ],
496 | };
497 |
498 | const expectedResponse = {
499 | health: 'YELLOW',
500 | causes: ['Disk space low'],
501 | };
502 |
503 | nock(baseUrl)
504 | .get('/api/v2/system/health')
505 | .matchHeader('authorization', 'Bearer test-token')
506 | .reply(200, mockV2Response);
507 |
508 | const result = await client.getHealth();
509 | expect(result).toEqual(expectedResponse);
510 | expect(result.health).toBe('YELLOW');
511 | expect(result.causes).toContain('Disk space low');
512 | });
513 | });
514 |
515 | describe('getStatus', () => {
516 | it('should fetch system status successfully', async () => {
517 | const mockResponse = {
518 | id: '20230101-1234',
519 | version: '10.3.0.82913',
520 | status: 'UP',
521 | };
522 |
523 | nock(baseUrl)
524 | .get('/api/system/status')
525 | .matchHeader('authorization', 'Bearer test-token')
526 | .reply(200, mockResponse);
527 |
528 | const result = await client.getStatus();
529 | expect(result).toEqual(mockResponse);
530 | expect(result.id).toBe('20230101-1234');
531 | expect(result.version).toBe('10.3.0.82913');
532 | expect(result.status).toBe('UP');
533 | });
534 | });
535 |
536 | describe('ping', () => {
537 | it('should ping SonarQube successfully', async () => {
538 | nock(baseUrl)
539 | .get('/api/system/ping')
540 | .matchHeader('authorization', 'Bearer test-token')
541 | .reply(200, 'pong');
542 |
543 | const result = await client.ping();
544 | expect(result).toBe('pong');
545 | });
546 |
547 | it('should handle projects filter parameter', async () => {
548 | const mockResponse = {
549 | issues: [],
550 | components: [],
551 | rules: [],
552 | paging: { pageIndex: 1, pageSize: 10, total: 0 },
553 | };
554 |
555 | const scope = nock(baseUrl)
556 | .get('/api/issues/search')
557 | .query((actualQuery) => {
558 | return actualQuery.projects === 'proj1,proj2';
559 | })
560 | .matchHeader('authorization', 'Bearer test-token')
561 | .reply(200, mockResponse);
562 |
563 | await client.getIssues({
564 | projects: ['proj1', 'proj2'],
565 | page: undefined,
566 | pageSize: undefined,
567 | });
568 |
569 | expect(scope.isDone()).toBe(true);
570 | });
571 |
572 | it('should handle component filter parameters', async () => {
573 | const mockResponse = {
574 | issues: [],
575 | components: [],
576 | rules: [],
577 | paging: { pageIndex: 1, pageSize: 10, total: 0 },
578 | };
579 |
580 | // When both componentKeys and components are provided, only the last one (components) is used
581 | const scope = nock(baseUrl)
582 | .get('/api/issues/search')
583 | .query((actualQuery) => {
584 | return (
585 | actualQuery.projects === 'project1' &&
586 | actualQuery.components === 'comp2' && // components overrides componentKeys
587 | actualQuery.onComponentOnly === 'true'
588 | );
589 | })
590 | .matchHeader('authorization', 'Bearer test-token')
591 | .reply(200, mockResponse);
592 |
593 | await client.getIssues({
594 | projectKey: 'project1',
595 | componentKeys: ['comp1'],
596 | components: ['comp2'],
597 | onComponentOnly: true,
598 | page: undefined,
599 | pageSize: undefined,
600 | });
601 |
602 | expect(scope.isDone()).toBe(true);
603 | });
604 |
605 | it('should handle branch and pull request parameters', async () => {
606 | const mockResponse = {
607 | issues: [],
608 | components: [],
609 | rules: [],
610 | paging: { pageIndex: 1, pageSize: 10, total: 0 },
611 | };
612 |
613 | const scope = nock(baseUrl)
614 | .get('/api/issues/search')
615 | .query((actualQuery) => {
616 | return actualQuery.branch === 'feature/test' && actualQuery.pullRequest === '123';
617 | })
618 | .matchHeader('authorization', 'Bearer test-token')
619 | .reply(200, mockResponse);
620 |
621 | await client.getIssues({
622 | branch: 'feature/test',
623 | pullRequest: '123',
624 | page: undefined,
625 | pageSize: undefined,
626 | });
627 |
628 | expect(scope.isDone()).toBe(true);
629 | });
630 |
631 | it('should handle issue and type filters', async () => {
632 | const mockResponse = {
633 | issues: [],
634 | components: [],
635 | rules: [],
636 | paging: { pageIndex: 1, pageSize: 10, total: 0 },
637 | };
638 |
639 | const scope = nock(baseUrl)
640 | .get('/api/issues/search')
641 | .query((actualQuery) => {
642 | return (
643 | actualQuery.issues === 'ISSUE-1,ISSUE-2' &&
644 | actualQuery.severities === 'BLOCKER,CRITICAL' &&
645 | actualQuery.statuses === 'OPEN,CONFIRMED' &&
646 | actualQuery.resolutions === 'FALSE-POSITIVE,WONTFIX' &&
647 | actualQuery.resolved === 'true' &&
648 | actualQuery.types === 'BUG,VULNERABILITY'
649 | );
650 | })
651 | .matchHeader('authorization', 'Bearer test-token')
652 | .reply(200, mockResponse);
653 |
654 | await client.getIssues({
655 | issues: ['ISSUE-1', 'ISSUE-2'],
656 | severities: ['BLOCKER', 'CRITICAL'],
657 | statuses: ['OPEN', 'CONFIRMED'],
658 | resolutions: ['FALSE-POSITIVE', 'WONTFIX'],
659 | resolved: true,
660 | types: ['BUG', 'VULNERABILITY'],
661 | page: undefined,
662 | pageSize: undefined,
663 | });
664 |
665 | expect(scope.isDone()).toBe(true);
666 | });
667 |
668 | it('should handle rules and tags filters', async () => {
669 | const mockResponse = {
670 | issues: [],
671 | components: [],
672 | rules: [],
673 | paging: { pageIndex: 1, pageSize: 10, total: 0 },
674 | };
675 |
676 | const scope = nock(baseUrl)
677 | .get('/api/issues/search')
678 | .query((actualQuery) => {
679 | return (
680 | actualQuery.rules === 'java:S1234,java:S5678' &&
681 | actualQuery.tags === 'security,performance' &&
682 | actualQuery.languages === 'java,javascript'
683 | );
684 | })
685 | .matchHeader('authorization', 'Bearer test-token')
686 | .reply(200, mockResponse);
687 |
688 | await client.getIssues({
689 | rules: ['java:S1234', 'java:S5678'],
690 | tags: ['security', 'performance'],
691 | languages: ['java', 'javascript'],
692 | page: undefined,
693 | pageSize: undefined,
694 | });
695 |
696 | expect(scope.isDone()).toBe(true);
697 | });
698 |
699 | it('should handle date and assignment filter parameters', async () => {
700 | const mockResponse = {
701 | issues: [],
702 | components: [],
703 | rules: [],
704 | paging: { pageIndex: 1, pageSize: 10, total: 0 },
705 | };
706 |
707 | const scope = nock(baseUrl)
708 | .get('/api/issues/search')
709 | .query((actualQuery) => {
710 | return (
711 | actualQuery.createdAfter === '2023-01-01' &&
712 | actualQuery.createdBefore === '2023-12-31' &&
713 | actualQuery.createdAt === '2023-06-15' &&
714 | actualQuery.createdInLast === '7d' &&
715 | actualQuery.assigned === 'true' &&
716 | actualQuery.assignees === 'user1,user2' &&
717 | // API now uses repeated 'author' parameter instead of 'authors'
718 | Array.isArray(actualQuery.author) &&
719 | actualQuery.author[0] === 'author1' &&
720 | actualQuery.author[1] === 'author2'
721 | );
722 | })
723 | .matchHeader('authorization', 'Bearer test-token')
724 | .reply(200, mockResponse);
725 |
726 | await client.getIssues({
727 | createdAfter: '2023-01-01',
728 | createdBefore: '2023-12-31',
729 | createdAt: '2023-06-15',
730 | createdInLast: '7d',
731 | assigned: true,
732 | assignees: ['user1', 'user2'],
733 | author: 'author1',
734 | authors: ['author1', 'author2'],
735 | page: undefined,
736 | pageSize: undefined,
737 | });
738 |
739 | expect(scope.isDone()).toBe(true);
740 | });
741 |
742 | it('should handle security standards filter parameters', async () => {
743 | const mockResponse = {
744 | issues: [],
745 | components: [],
746 | rules: [],
747 | paging: { pageIndex: 1, pageSize: 10, total: 0 },
748 | };
749 |
750 | const scope = nock(baseUrl)
751 | .get('/api/issues/search')
752 | .query((actualQuery) => {
753 | return (
754 | actualQuery.cwe === '79,89' &&
755 | actualQuery.owaspTop10 === 'a1,a3' &&
756 | actualQuery['owaspTop10-2021'] === 'a01,a03' &&
757 | actualQuery.sansTop25 === 'insecure-interaction,risky-resource' &&
758 | actualQuery.sonarsourceSecurityCategory === 'sql-injection,xss' &&
759 | actualQuery.sonarsourceSecurity === 'injection'
760 | );
761 | })
762 | .matchHeader('authorization', 'Bearer test-token')
763 | .reply(200, mockResponse);
764 |
765 | await client.getIssues({
766 | cwe: ['79', '89'],
767 | owaspTop10: ['a1', 'a3'],
768 | owaspTop10v2021: ['a01', 'a03'],
769 | sansTop25: ['insecure-interaction', 'risky-resource'],
770 | sonarsourceSecurity: ['sql-injection', 'xss'],
771 | sonarsourceSecurityCategory: ['injection'],
772 | page: undefined,
773 | pageSize: undefined,
774 | });
775 |
776 | expect(scope.isDone()).toBe(true);
777 | });
778 |
779 | it('should handle Clean Code and impact parameters', async () => {
780 | const mockResponse = {
781 | issues: [],
782 | components: [],
783 | rules: [],
784 | paging: { pageIndex: 1, pageSize: 10, total: 0 },
785 | };
786 |
787 | const scope = nock(baseUrl)
788 | .get('/api/issues/search')
789 | .query((actualQuery) => {
790 | return (
791 | actualQuery.cleanCodeAttributeCategories === 'INTENTIONAL,RESPONSIBLE' &&
792 | actualQuery.impactSeverities === 'HIGH,MEDIUM' &&
793 | actualQuery.impactSoftwareQualities === 'SECURITY,RELIABILITY' &&
794 | actualQuery.issueStatuses === 'OPEN,CONFIRMED'
795 | );
796 | })
797 | .matchHeader('authorization', 'Bearer test-token')
798 | .reply(200, mockResponse);
799 |
800 | await client.getIssues({
801 | cleanCodeAttributeCategories: ['INTENTIONAL', 'RESPONSIBLE'],
802 | impactSeverities: ['HIGH', 'MEDIUM'],
803 | impactSoftwareQualities: ['SECURITY', 'RELIABILITY'],
804 | issueStatuses: ['OPEN', 'CONFIRMED'],
805 | page: undefined,
806 | pageSize: undefined,
807 | });
808 |
809 | expect(scope.isDone()).toBe(true);
810 | });
811 |
812 | it('should handle facets and additional fields', async () => {
813 | const mockResponse = {
814 | issues: [],
815 | components: [],
816 | rules: [],
817 | facets: [
818 | {
819 | property: 'severities',
820 | values: [{ val: 'BLOCKER', count: 10 }],
821 | },
822 | ],
823 | paging: { pageIndex: 1, pageSize: 10, total: 0 },
824 | };
825 |
826 | const scope = nock(baseUrl)
827 | .get('/api/issues/search')
828 | .query((actualQuery) => {
829 | return (
830 | actualQuery.facets === 'severities,types' &&
831 | actualQuery.facetMode === 'effort' &&
832 | actualQuery.additionalFields === '_all'
833 | );
834 | })
835 | .matchHeader('authorization', 'Bearer test-token')
836 | .reply(200, mockResponse);
837 |
838 | await client.getIssues({
839 | facets: ['severities', 'types'],
840 | facetMode: 'effort',
841 | additionalFields: ['_all'],
842 | page: undefined,
843 | pageSize: undefined,
844 | });
845 |
846 | expect(scope.isDone()).toBe(true);
847 | });
848 |
849 | it('should handle period and sorting parameters', async () => {
850 | const mockResponse = {
851 | issues: [],
852 | components: [],
853 | rules: [],
854 | paging: { pageIndex: 1, pageSize: 10, total: 0 },
855 | };
856 |
857 | const scope = nock(baseUrl)
858 | .get('/api/issues/search')
859 | .query((actualQuery) => {
860 | return actualQuery.inNewCodePeriod === 'true' && actualQuery.sinceLeakPeriod === 'true';
861 | })
862 | .matchHeader('authorization', 'Bearer test-token')
863 | .reply(200, mockResponse);
864 |
865 | await client.getIssues({
866 | inNewCodePeriod: true,
867 | sinceLeakPeriod: true,
868 | s: 'FILE_LINE',
869 | asc: false,
870 | page: undefined,
871 | pageSize: undefined,
872 | });
873 |
874 | expect(scope.isDone()).toBe(true);
875 | });
876 |
877 | it('should handle deprecated severity parameter', async () => {
878 | const mockResponse = {
879 | issues: [],
880 | components: [],
881 | rules: [],
882 | paging: { pageIndex: 1, pageSize: 10, total: 0 },
883 | };
884 |
885 | const scope = nock(baseUrl)
886 | .get('/api/issues/search')
887 | .query((actualQuery) => {
888 | return actualQuery.severities === 'MAJOR';
889 | })
890 | .matchHeader('authorization', 'Bearer test-token')
891 | .reply(200, mockResponse);
892 |
893 | await client.getIssues({ severity: 'MAJOR', page: undefined, pageSize: undefined });
894 | expect(scope.isDone()).toBe(true);
895 | });
896 |
897 | it('should handle resolved parameter with false value', async () => {
898 | const mockResponse = {
899 | issues: [],
900 | components: [],
901 | rules: [],
902 | paging: { pageIndex: 1, pageSize: 10, total: 0 },
903 | };
904 |
905 | const scope = nock(baseUrl)
906 | .get('/api/issues/search')
907 | .query((actualQuery) => {
908 | return actualQuery.resolved === 'false';
909 | })
910 | .matchHeader('authorization', 'Bearer test-token')
911 | .reply(200, mockResponse);
912 |
913 | await client.getIssues({ resolved: false, page: undefined, pageSize: undefined });
914 | expect(scope.isDone()).toBe(true);
915 | });
916 |
917 | it('should handle assigned parameter with false value', async () => {
918 | const mockResponse = {
919 | issues: [],
920 | components: [],
921 | rules: [],
922 | paging: { pageIndex: 1, pageSize: 10, total: 0 },
923 | };
924 |
925 | const scope = nock(baseUrl)
926 | .get('/api/issues/search')
927 | .query((actualQuery) => {
928 | return actualQuery.assigned === 'false';
929 | })
930 | .matchHeader('authorization', 'Bearer test-token')
931 | .reply(200, mockResponse);
932 |
933 | await client.getIssues({ assigned: false, page: undefined, pageSize: undefined });
934 | expect(scope.isDone()).toBe(true);
935 | });
936 | });
937 |
938 | describe('getComponentMeasures', () => {
939 | it('should fetch component measures successfully', async () => {
940 | const mockResponse = {
941 | component: {
942 | key: 'my-project',
943 | name: 'My Project',
944 | qualifier: 'TRK',
945 | measures: [
946 | {
947 | metric: 'complexity',
948 | value: '42',
949 | bestValue: false,
950 | },
951 | {
952 | metric: 'bugs',
953 | value: '5',
954 | bestValue: false,
955 | },
956 | ],
957 | },
958 | metrics: [
959 | {
960 | id: 'metric1',
961 | key: 'complexity',
962 | name: 'Complexity',
963 | description: 'Cyclomatic complexity',
964 | domain: 'Complexity',
965 | type: 'INT',
966 | direction: -1,
967 | qualitative: true,
968 | hidden: false,
969 | custom: false,
970 | },
971 | {
972 | id: 'metric2',
973 | key: 'bugs',
974 | name: 'Bugs',
975 | description: 'Number of bugs',
976 | domain: 'Reliability',
977 | type: 'INT',
978 | direction: -1,
979 | qualitative: true,
980 | hidden: false,
981 | custom: false,
982 | },
983 | ],
984 | };
985 |
986 | nock(baseUrl)
987 | .get('/api/measures/component')
988 | .query((queryObj) => {
989 | return queryObj.component === 'my-project' && queryObj.metricKeys === 'complexity,bugs';
990 | })
991 | .matchHeader('authorization', 'Bearer test-token')
992 | .reply(200, mockResponse);
993 |
994 | const result = await client.getComponentMeasures({
995 | component: 'my-project',
996 | metricKeys: ['complexity', 'bugs'],
997 | });
998 |
999 | expect(result).toEqual(mockResponse);
1000 | expect(result.component.key).toBe('my-project');
1001 | expect(result.component.measures).toHaveLength(2);
1002 | expect(result.metrics).toHaveLength(2);
1003 | expect(result.component.measures?.[0]?.metric).toBe('complexity');
1004 | expect(result.component.measures?.[0]?.value).toBe('42');
1005 | });
1006 |
1007 | it('should handle additional parameters', async () => {
1008 | const mockResponse = {
1009 | component: {
1010 | key: 'my-project',
1011 | name: 'My Project',
1012 | qualifier: 'TRK',
1013 | measures: [
1014 | {
1015 | metric: 'coverage',
1016 | value: '87.5',
1017 | bestValue: false,
1018 | },
1019 | ],
1020 | },
1021 | metrics: [
1022 | {
1023 | id: 'metric3',
1024 | key: 'coverage',
1025 | name: 'Coverage',
1026 | description: 'Test coverage',
1027 | domain: 'Coverage',
1028 | type: 'PERCENT',
1029 | direction: 1,
1030 | qualitative: true,
1031 | hidden: false,
1032 | custom: false,
1033 | },
1034 | ],
1035 | period: {
1036 | index: 1,
1037 | mode: 'previous_version',
1038 | date: '2023-01-01T00:00:00+0000',
1039 | },
1040 | };
1041 |
1042 | const scope = nock(baseUrl)
1043 | .get('/api/measures/component')
1044 | .query((queryObj) => {
1045 | return (
1046 | queryObj.component === 'my-project' &&
1047 | queryObj.metricKeys === 'coverage' &&
1048 | queryObj.additionalFields === 'periods' &&
1049 | queryObj.branch === 'main'
1050 | );
1051 | })
1052 | .matchHeader('authorization', 'Bearer test-token')
1053 | .reply(200, mockResponse);
1054 |
1055 | const result = await client.getComponentMeasures({
1056 | component: 'my-project',
1057 | metricKeys: ['coverage'],
1058 | additionalFields: ['periods'],
1059 | branch: 'main',
1060 | });
1061 |
1062 | expect(result).toEqual(mockResponse);
1063 | expect(scope.isDone()).toBe(true);
1064 | expect(result.period?.mode).toBe('previous_version');
1065 | });
1066 | });
1067 |
1068 | describe('getComponentsMeasures', () => {
1069 | it('should fetch multiple components measures successfully', async () => {
1070 | const mockResponse = {
1071 | components: [
1072 | {
1073 | key: 'project1',
1074 | name: 'Project 1',
1075 | qualifier: 'TRK',
1076 | measures: [
1077 | {
1078 | metric: 'bugs',
1079 | value: '12',
1080 | },
1081 | {
1082 | metric: 'vulnerabilities',
1083 | value: '5',
1084 | },
1085 | ],
1086 | },
1087 | {
1088 | key: 'project2',
1089 | name: 'Project 2',
1090 | qualifier: 'TRK',
1091 | measures: [
1092 | {
1093 | metric: 'bugs',
1094 | value: '7',
1095 | },
1096 | {
1097 | metric: 'vulnerabilities',
1098 | value: '0',
1099 | bestValue: true,
1100 | },
1101 | ],
1102 | },
1103 | ],
1104 | metrics: [
1105 | {
1106 | id: 'metric2',
1107 | key: 'bugs',
1108 | name: 'Bugs',
1109 | description: 'Number of bugs',
1110 | domain: 'Reliability',
1111 | type: 'INT',
1112 | direction: -1,
1113 | qualitative: true,
1114 | hidden: false,
1115 | custom: false,
1116 | },
1117 | {
1118 | id: 'metric3',
1119 | key: 'vulnerabilities',
1120 | name: 'Vulnerabilities',
1121 | description: 'Number of vulnerabilities',
1122 | domain: 'Security',
1123 | type: 'INT',
1124 | direction: -1,
1125 | qualitative: true,
1126 | hidden: false,
1127 | custom: false,
1128 | },
1129 | ],
1130 | paging: {
1131 | pageIndex: 1,
1132 | pageSize: 100,
1133 | total: 2,
1134 | },
1135 | };
1136 |
1137 | // Mock individual component calls
1138 | nock(baseUrl)
1139 | .get('/api/measures/component')
1140 | .query({
1141 | component: 'project1',
1142 | metricKeys: 'bugs,vulnerabilities',
1143 | })
1144 | .matchHeader('authorization', 'Bearer test-token')
1145 | .reply(200, {
1146 | component: mockResponse.components[0],
1147 | metrics: mockResponse.metrics,
1148 | });
1149 |
1150 | nock(baseUrl)
1151 | .get('/api/measures/component')
1152 | .query({
1153 | component: 'project2',
1154 | metricKeys: 'bugs,vulnerabilities',
1155 | })
1156 | .matchHeader('authorization', 'Bearer test-token')
1157 | .reply(200, {
1158 | component: mockResponse.components[1],
1159 | metrics: mockResponse.metrics,
1160 | });
1161 |
1162 | // Mock the additional call for metrics from first component
1163 | nock(baseUrl)
1164 | .get('/api/measures/component')
1165 | .query({
1166 | component: 'project1',
1167 | metricKeys: 'bugs,vulnerabilities',
1168 | })
1169 | .matchHeader('authorization', 'Bearer test-token')
1170 | .reply(200, {
1171 | component: mockResponse.components[0],
1172 | metrics: mockResponse.metrics,
1173 | });
1174 |
1175 | const result = await client.getComponentsMeasures({
1176 | componentKeys: ['project1', 'project2'],
1177 | metricKeys: ['bugs', 'vulnerabilities'],
1178 | page: undefined,
1179 | pageSize: undefined,
1180 | });
1181 |
1182 | expect(result).toEqual(mockResponse);
1183 | expect(result.components).toHaveLength(2);
1184 | expect(result.components?.[0]?.key).toBe('project1');
1185 | expect(result.components?.[1]?.key).toBe('project2');
1186 | expect(result.components?.[0]?.measures).toHaveLength(2);
1187 | expect(result.components?.[1]?.measures).toHaveLength(2);
1188 | expect(result.metrics).toHaveLength(2);
1189 | expect(result.paging.total).toBe(2);
1190 | });
1191 |
1192 | it('should handle pagination and additional parameters', async () => {
1193 | const mockResponse = {
1194 | components: [
1195 | {
1196 | key: 'project3',
1197 | name: 'Project 3',
1198 | qualifier: 'TRK',
1199 | measures: [
1200 | {
1201 | metric: 'code_smells',
1202 | value: '45',
1203 | },
1204 | ],
1205 | },
1206 | ],
1207 | metrics: [
1208 | {
1209 | id: 'metric4',
1210 | key: 'code_smells',
1211 | name: 'Code Smells',
1212 | description: 'Number of code smells',
1213 | domain: 'Maintainability',
1214 | type: 'INT',
1215 | direction: -1,
1216 | qualitative: true,
1217 | hidden: false,
1218 | custom: false,
1219 | },
1220 | ],
1221 | paging: {
1222 | pageIndex: 2,
1223 | pageSize: 1,
1224 | total: 3,
1225 | },
1226 | period: {
1227 | index: 1,
1228 | mode: 'previous_version',
1229 | date: '2023-01-01T00:00:00+0000',
1230 | },
1231 | };
1232 |
1233 | // Mock individual component calls - all three components
1234 | nock(baseUrl)
1235 | .get('/api/measures/component')
1236 | .query({
1237 | component: 'project1',
1238 | metricKeys: 'code_smells',
1239 | additionalFields: 'periods',
1240 | branch: 'main',
1241 | })
1242 | .matchHeader('authorization', 'Bearer test-token')
1243 | .reply(200, {
1244 | component: {
1245 | key: 'project1',
1246 | name: 'Project 1',
1247 | qualifier: 'TRK',
1248 | measures: [{ metric: 'code_smells', value: '10' }],
1249 | },
1250 | metrics: mockResponse.metrics,
1251 | period: mockResponse.period,
1252 | });
1253 |
1254 | nock(baseUrl)
1255 | .get('/api/measures/component')
1256 | .query({
1257 | component: 'project2',
1258 | metricKeys: 'code_smells',
1259 | additionalFields: 'periods',
1260 | branch: 'main',
1261 | })
1262 | .matchHeader('authorization', 'Bearer test-token')
1263 | .reply(200, {
1264 | component: {
1265 | key: 'project2',
1266 | name: 'Project 2',
1267 | qualifier: 'TRK',
1268 | measures: [{ metric: 'code_smells', value: '20' }],
1269 | },
1270 | metrics: mockResponse.metrics,
1271 | period: mockResponse.period,
1272 | });
1273 |
1274 | const scope = nock(baseUrl)
1275 | .get('/api/measures/component')
1276 | .query({
1277 | component: 'project3',
1278 | metricKeys: 'code_smells',
1279 | additionalFields: 'periods',
1280 | branch: 'main',
1281 | })
1282 | .matchHeader('authorization', 'Bearer test-token')
1283 | .reply(200, {
1284 | component: mockResponse.components[0],
1285 | metrics: mockResponse.metrics,
1286 | period: mockResponse.period,
1287 | });
1288 |
1289 | // Mock the additional call for metrics from first component
1290 | nock(baseUrl)
1291 | .get('/api/measures/component')
1292 | .query({
1293 | component: 'project1',
1294 | metricKeys: 'code_smells',
1295 | additionalFields: 'periods',
1296 | branch: 'main',
1297 | })
1298 | .matchHeader('authorization', 'Bearer test-token')
1299 | .reply(200, {
1300 | component: {
1301 | key: 'project1',
1302 | name: 'Project 1',
1303 | qualifier: 'TRK',
1304 | measures: [{ metric: 'code_smells', value: '10' }],
1305 | },
1306 | metrics: mockResponse.metrics,
1307 | period: mockResponse.period,
1308 | });
1309 |
1310 | const result = await client.getComponentsMeasures({
1311 | componentKeys: 'project1,project2,project3',
1312 | metricKeys: 'code_smells',
1313 | page: 2,
1314 | pageSize: 1,
1315 | additionalFields: ['periods'],
1316 | branch: 'main',
1317 | });
1318 |
1319 | // Since we paginate after fetching all components, we should have only 1 result
1320 | expect(result.components).toHaveLength(1);
1321 | expect(result.components?.[0]?.key).toBe('project2'); // Page 2, size 1 would show the 2nd component
1322 | expect(result.paging.pageIndex).toBe(2);
1323 | expect(result.paging.pageSize).toBe(1);
1324 | expect(result.paging.total).toBe(3); // Total of 3 components
1325 | expect(result.period?.mode).toBe('previous_version');
1326 | expect(scope.isDone()).toBe(true);
1327 | });
1328 |
1329 | it('should handle comma-separated componentKeys string', async () => {
1330 | const mockResponse = {
1331 | components: [
1332 | {
1333 | key: 'comp1',
1334 | name: 'Component 1',
1335 | qualifier: 'FIL',
1336 | measures: [{ metric: 'coverage', value: '80' }],
1337 | },
1338 | {
1339 | key: 'comp2',
1340 | name: 'Component 2',
1341 | qualifier: 'FIL',
1342 | measures: [{ metric: 'coverage', value: '90' }],
1343 | },
1344 | ],
1345 | metrics: [
1346 | {
1347 | key: 'coverage',
1348 | name: 'Coverage',
1349 | type: 'PERCENT',
1350 | },
1351 | ],
1352 | };
1353 |
1354 | // Mock individual component calls
1355 | nock(baseUrl)
1356 | .get('/api/measures/component')
1357 | .query({
1358 | component: 'comp1',
1359 | metricKeys: 'coverage',
1360 | })
1361 | .matchHeader('authorization', 'Bearer test-token')
1362 | .reply(200, {
1363 | component: mockResponse.components[0],
1364 | metrics: mockResponse.metrics,
1365 | });
1366 |
1367 | nock(baseUrl)
1368 | .get('/api/measures/component')
1369 | .query({
1370 | component: 'comp2',
1371 | metricKeys: 'coverage',
1372 | })
1373 | .matchHeader('authorization', 'Bearer test-token')
1374 | .reply(200, {
1375 | component: mockResponse.components[1],
1376 | metrics: mockResponse.metrics,
1377 | });
1378 |
1379 | // Mock for extracting metrics
1380 | nock(baseUrl)
1381 | .get('/api/measures/component')
1382 | .query({
1383 | component: 'comp1',
1384 | metricKeys: 'coverage',
1385 | })
1386 | .matchHeader('authorization', 'Bearer test-token')
1387 | .reply(200, {
1388 | component: mockResponse.components[0],
1389 | metrics: mockResponse.metrics,
1390 | });
1391 |
1392 | const result = await client.getComponentsMeasures({
1393 | componentKeys: 'comp1,comp2',
1394 | metricKeys: ['coverage'],
1395 | page: undefined,
1396 | pageSize: undefined,
1397 | });
1398 |
1399 | expect(result.components).toHaveLength(2);
1400 | expect(result.components?.[0]?.key).toBe('comp1');
1401 | expect(result.components?.[1]?.key).toBe('comp2');
1402 | });
1403 |
1404 | it('should handle comma-separated metricKeys string', async () => {
1405 | const mockResponse = {
1406 | components: [
1407 | {
1408 | key: 'project1',
1409 | name: 'Project 1',
1410 | qualifier: 'TRK',
1411 | measures: [
1412 | { metric: 'coverage', value: '75' },
1413 | { metric: 'duplicated_lines_density', value: '5' },
1414 | ],
1415 | },
1416 | ],
1417 | metrics: [
1418 | {
1419 | key: 'coverage',
1420 | name: 'Coverage',
1421 | type: 'PERCENT',
1422 | },
1423 | {
1424 | key: 'duplicated_lines_density',
1425 | name: 'Duplicated Lines',
1426 | type: 'PERCENT',
1427 | },
1428 | ],
1429 | };
1430 |
1431 | // Mock individual component call
1432 | nock(baseUrl)
1433 | .get('/api/measures/component')
1434 | .query({
1435 | component: 'project1',
1436 | metricKeys: 'coverage,duplicated_lines_density',
1437 | })
1438 | .matchHeader('authorization', 'Bearer test-token')
1439 | .reply(200, {
1440 | component: mockResponse.components[0],
1441 | metrics: mockResponse.metrics,
1442 | });
1443 |
1444 | // Mock for extracting metrics
1445 | nock(baseUrl)
1446 | .get('/api/measures/component')
1447 | .query({
1448 | component: 'project1',
1449 | metricKeys: 'coverage,duplicated_lines_density',
1450 | })
1451 | .matchHeader('authorization', 'Bearer test-token')
1452 | .reply(200, {
1453 | component: mockResponse.components[0],
1454 | metrics: mockResponse.metrics,
1455 | });
1456 |
1457 | const result = await client.getComponentsMeasures({
1458 | componentKeys: ['project1'],
1459 | metricKeys: 'coverage,duplicated_lines_density',
1460 | page: undefined,
1461 | pageSize: undefined,
1462 | });
1463 |
1464 | expect(result.components).toHaveLength(1);
1465 | expect(result.components?.[0]?.measures).toHaveLength(2);
1466 | expect(result.metrics).toHaveLength(2);
1467 | });
1468 | });
1469 |
1470 | describe('getMeasuresHistory', () => {
1471 | it('should fetch measures history successfully', async () => {
1472 | const mockResponse = {
1473 | paging: {
1474 | pageIndex: 1,
1475 | pageSize: 100,
1476 | total: 2,
1477 | },
1478 | measures: [
1479 | {
1480 | metric: 'coverage',
1481 | history: [
1482 | {
1483 | date: '2023-01-01T00:00:00+0000',
1484 | value: '85.5',
1485 | },
1486 | {
1487 | date: '2023-01-02T00:00:00+0000',
1488 | value: '87.2',
1489 | },
1490 | ],
1491 | },
1492 | {
1493 | metric: 'bugs',
1494 | history: [
1495 | {
1496 | date: '2023-01-01T00:00:00+0000',
1497 | value: '12',
1498 | },
1499 | {
1500 | date: '2023-01-02T00:00:00+0000',
1501 | value: '5',
1502 | },
1503 | ],
1504 | },
1505 | ],
1506 | };
1507 |
1508 | nock(baseUrl)
1509 | .get('/api/measures/search_history')
1510 | .query((queryObj) => {
1511 | return queryObj.component === 'my-project' && queryObj.metrics === 'coverage,bugs';
1512 | })
1513 | .matchHeader('authorization', 'Bearer test-token')
1514 | .reply(200, mockResponse);
1515 |
1516 | const result = await client.getMeasuresHistory({
1517 | component: 'my-project',
1518 | metrics: ['coverage', 'bugs'],
1519 | page: undefined,
1520 | pageSize: undefined,
1521 | });
1522 |
1523 | expect(result).toEqual(mockResponse);
1524 | expect(result.measures).toHaveLength(2);
1525 | expect(result.measures?.[0]?.metric).toBe('coverage');
1526 | expect(result.measures?.[1]?.metric).toBe('bugs');
1527 | expect(result.measures?.[0]?.history).toHaveLength(2);
1528 | expect(result.measures?.[1]?.history).toHaveLength(2);
1529 | expect(result.paging.total).toBe(2);
1530 | });
1531 |
1532 | it('should handle date range and pagination parameters', async () => {
1533 | const mockResponse = {
1534 | paging: {
1535 | pageIndex: 1,
1536 | pageSize: 100,
1537 | total: 1,
1538 | },
1539 | measures: [
1540 | {
1541 | metric: 'code_smells',
1542 | history: [
1543 | {
1544 | date: '2023-01-15T00:00:00+0000',
1545 | value: '45',
1546 | },
1547 | {
1548 | date: '2023-01-20T00:00:00+0000',
1549 | value: '32',
1550 | },
1551 | ],
1552 | },
1553 | ],
1554 | };
1555 |
1556 | const scope = nock(baseUrl)
1557 | .get('/api/measures/search_history')
1558 | .query((queryObj) => {
1559 | return (
1560 | queryObj.component === 'my-project' &&
1561 | queryObj.metrics === 'code_smells' &&
1562 | queryObj.from === '2023-01-15' &&
1563 | queryObj.to === '2023-01-31' &&
1564 | queryObj.branch === 'main'
1565 | );
1566 | })
1567 | .matchHeader('authorization', 'Bearer test-token')
1568 | .reply(200, mockResponse);
1569 |
1570 | const result = await client.getMeasuresHistory({
1571 | component: 'my-project',
1572 | metrics: ['code_smells'],
1573 | from: '2023-01-15',
1574 | to: '2023-01-31',
1575 | branch: 'main',
1576 | page: undefined,
1577 | pageSize: undefined,
1578 | });
1579 |
1580 | expect(result).toEqual(mockResponse);
1581 | expect(scope.isDone()).toBe(true);
1582 | expect(result.measures).toHaveLength(1);
1583 | expect(result.measures?.[0]?.metric).toBe('code_smells');
1584 | expect(result.measures?.[0]?.history).toHaveLength(2);
1585 | expect(result.measures?.[0]?.history?.[0]?.date).toBe('2023-01-15T00:00:00+0000');
1586 | expect(result.measures?.[0]?.history?.[1]?.date).toBe('2023-01-20T00:00:00+0000');
1587 | });
1588 | });
1589 |
1590 | describe('Hotspots', () => {
1591 | it('should search hotspots', async () => {
1592 | const mockResponse = {
1593 | hotspots: [
1594 | {
1595 | key: 'AYg1234567890',
1596 | component: 'com.example:my-project:src/main/java/Example.java',
1597 | project: 'com.example:my-project',
1598 | securityCategory: 'sql-injection',
1599 | vulnerabilityProbability: 'HIGH',
1600 | status: 'TO_REVIEW',
1601 | line: 42,
1602 | message: 'Make sure using this database query is safe.',
1603 | author: '[email protected]',
1604 | creationDate: '2023-01-15T10:30:00+0000',
1605 | },
1606 | ],
1607 | components: [
1608 | {
1609 | key: 'com.example:my-project:src/main/java/Example.java',
1610 | name: 'Example.java',
1611 | path: 'src/main/java/Example.java',
1612 | },
1613 | ],
1614 | paging: {
1615 | pageIndex: 1,
1616 | pageSize: 100,
1617 | total: 1,
1618 | },
1619 | };
1620 |
1621 | const scope = nock(baseUrl)
1622 | .get('/api/hotspots/search')
1623 | .query((actualQuery) => {
1624 | return (
1625 | actualQuery.projectKey === 'my-project' &&
1626 | actualQuery.status === 'TO_REVIEW' &&
1627 | actualQuery.p === '1' &&
1628 | actualQuery.ps === '50'
1629 | );
1630 | })
1631 | .matchHeader('authorization', 'Bearer test-token')
1632 | .reply(200, mockResponse);
1633 |
1634 | const result = await client.hotspots({
1635 | projectKey: 'my-project',
1636 | status: 'TO_REVIEW',
1637 | page: 1,
1638 | pageSize: 50,
1639 | });
1640 |
1641 | expect(result).toEqual(mockResponse);
1642 | expect(scope.isDone()).toBe(true);
1643 | expect(result.hotspots).toHaveLength(1);
1644 | expect(result.hotspots?.[0]?.key).toBe('AYg1234567890');
1645 | });
1646 |
1647 | it('should search hotspots with all filters', async () => {
1648 | const mockResponse = {
1649 | hotspots: [],
1650 | components: [],
1651 | paging: { pageIndex: 1, pageSize: 100, total: 0 },
1652 | };
1653 |
1654 | const scope = nock(baseUrl)
1655 | .get('/api/hotspots/search')
1656 | .query((actualQuery) => {
1657 | // Note: The API's support for branch, pullRequest, and inNewCodePeriod has not been confirmed. Ensure these filters are supported before relying on them.
1658 | return (
1659 | actualQuery.projectKey === 'my-project' &&
1660 | actualQuery.status === 'REVIEWED' &&
1661 | actualQuery.resolution === 'FIXED' &&
1662 | actualQuery.files === 'file1.java,file2.java' &&
1663 | actualQuery.onlyMine === 'true' &&
1664 | actualQuery.sinceLeakPeriod === 'true'
1665 | );
1666 | })
1667 | .matchHeader('authorization', 'Bearer test-token')
1668 | .reply(200, mockResponse);
1669 |
1670 | await client.hotspots({
1671 | projectKey: 'my-project',
1672 | branch: 'feature-branch',
1673 | pullRequest: 'PR-123',
1674 | status: 'REVIEWED',
1675 | resolution: 'FIXED',
1676 | files: ['file1.java', 'file2.java'],
1677 | assignedToMe: true,
1678 | sinceLeakPeriod: true,
1679 | inNewCodePeriod: true,
1680 | page: undefined,
1681 | pageSize: undefined,
1682 | });
1683 |
1684 | expect(scope.isDone()).toBe(true);
1685 | });
1686 |
1687 | it('should get hotspot details', async () => {
1688 | const mockResponse = {
1689 | key: 'AYg1234567890',
1690 | component: {
1691 | key: 'com.example:my-project:src/main/java/Example.java',
1692 | name: 'Example.java',
1693 | path: 'src/main/java/Example.java',
1694 | qualifier: 'FIL',
1695 | },
1696 | project: {
1697 | key: 'com.example:my-project',
1698 | name: 'My Project',
1699 | qualifier: 'TRK',
1700 | },
1701 | rule: {
1702 | key: 'java:S2077',
1703 | name: 'SQL injection',
1704 | securityCategory: 'sql-injection',
1705 | vulnerabilityProbability: 'HIGH',
1706 | },
1707 | status: 'TO_REVIEW',
1708 | line: 42,
1709 | message: 'Make sure using this database query is safe.',
1710 | author: '[email protected]',
1711 | creationDate: '2023-01-15T10:30:00+0000',
1712 | updateDate: '2023-01-15T10:30:00+0000',
1713 | textRange: {
1714 | startLine: 42,
1715 | endLine: 42,
1716 | startOffset: 10,
1717 | endOffset: 50,
1718 | },
1719 | flows: [],
1720 | canChangeStatus: true,
1721 | };
1722 |
1723 | const scope = nock(baseUrl)
1724 | .get('/api/hotspots/show')
1725 | .query({ hotspot: 'AYg1234567890' })
1726 | .matchHeader('authorization', 'Bearer test-token')
1727 | .reply(200, mockResponse);
1728 |
1729 | const result = await client.hotspot('AYg1234567890');
1730 |
1731 | expect(result).toEqual(mockResponse);
1732 | expect(scope.isDone()).toBe(true);
1733 | expect(result.key).toBe('AYg1234567890');
1734 | expect(result.rule.securityCategory).toBe('sql-injection');
1735 | });
1736 |
1737 | it('should update hotspot status', async () => {
1738 | const scope = nock(baseUrl)
1739 | .post('/api/hotspots/change_status', {
1740 | hotspot: 'AYg1234567890',
1741 | status: 'REVIEWED',
1742 | resolution: 'FIXED',
1743 | comment: 'Fixed by using prepared statements',
1744 | })
1745 | .matchHeader('authorization', 'Bearer test-token')
1746 | .reply(204);
1747 |
1748 | await client.updateHotspotStatus({
1749 | hotspot: 'AYg1234567890',
1750 | status: 'REVIEWED',
1751 | resolution: 'FIXED',
1752 | comment: 'Fixed by using prepared statements',
1753 | });
1754 |
1755 | expect(scope.isDone()).toBe(true);
1756 | });
1757 |
1758 | it('should update hotspot status without optional fields', async () => {
1759 | const scope = nock(baseUrl)
1760 | .post('/api/hotspots/change_status', {
1761 | hotspot: 'AYg1234567890',
1762 | status: 'TO_REVIEW',
1763 | })
1764 | .matchHeader('authorization', 'Bearer test-token')
1765 | .reply(204);
1766 |
1767 | await client.updateHotspotStatus({
1768 | hotspot: 'AYg1234567890',
1769 | status: 'TO_REVIEW',
1770 | });
1771 |
1772 | expect(scope.isDone()).toBe(true);
1773 | });
1774 | });
1775 |
1776 | describe('Issue Resolution Methods', () => {
1777 | describe('markIssueFalsePositive', () => {
1778 | it('should mark issue as false positive successfully', async () => {
1779 | const mockResponse = {
1780 | issue: {
1781 | key: 'ISSUE-123',
1782 | status: 'RESOLVED',
1783 | resolution: 'FALSE-POSITIVE',
1784 | },
1785 | components: [],
1786 | rules: [],
1787 | users: [],
1788 | };
1789 |
1790 | const scope = nock(baseUrl)
1791 | .post('/api/issues/do_transition', {
1792 | issue: 'ISSUE-123',
1793 | transition: 'falsepositive',
1794 | })
1795 | .matchHeader('authorization', 'Bearer test-token')
1796 | .reply(200, mockResponse);
1797 |
1798 | const result = await client.markIssueFalsePositive({
1799 | issueKey: 'ISSUE-123',
1800 | });
1801 |
1802 | expect(result).toEqual(mockResponse);
1803 | expect(scope.isDone()).toBe(true);
1804 | });
1805 |
1806 | it('should mark issue as false positive with comment', async () => {
1807 | const mockResponse = {
1808 | issue: {
1809 | key: 'ISSUE-123',
1810 | status: 'RESOLVED',
1811 | resolution: 'FALSE-POSITIVE',
1812 | },
1813 | components: [],
1814 | rules: [],
1815 | users: [],
1816 | };
1817 |
1818 | const commentScope = nock(baseUrl)
1819 | .post('/api/issues/add_comment', {
1820 | issue: 'ISSUE-123',
1821 | text: 'This is a false positive',
1822 | })
1823 | .matchHeader('authorization', 'Bearer test-token')
1824 | .reply(200, {});
1825 |
1826 | const transitionScope = nock(baseUrl)
1827 | .post('/api/issues/do_transition', {
1828 | issue: 'ISSUE-123',
1829 | transition: 'falsepositive',
1830 | })
1831 | .matchHeader('authorization', 'Bearer test-token')
1832 | .reply(200, mockResponse);
1833 |
1834 | const result = await client.markIssueFalsePositive({
1835 | issueKey: 'ISSUE-123',
1836 | comment: 'This is a false positive',
1837 | });
1838 |
1839 | expect(result).toEqual(mockResponse);
1840 | expect(commentScope.isDone()).toBe(true);
1841 | expect(transitionScope.isDone()).toBe(true);
1842 | });
1843 | });
1844 |
1845 | describe('markIssueWontFix', () => {
1846 | it("should mark issue as won't fix successfully", async () => {
1847 | const mockResponse = {
1848 | issue: {
1849 | key: 'ISSUE-456',
1850 | status: 'RESOLVED',
1851 | resolution: 'WONTFIX',
1852 | },
1853 | components: [],
1854 | rules: [],
1855 | users: [],
1856 | };
1857 |
1858 | const scope = nock(baseUrl)
1859 | .post('/api/issues/do_transition', {
1860 | issue: 'ISSUE-456',
1861 | transition: 'wontfix',
1862 | })
1863 | .matchHeader('authorization', 'Bearer test-token')
1864 | .reply(200, mockResponse);
1865 |
1866 | const result = await client.markIssueWontFix({
1867 | issueKey: 'ISSUE-456',
1868 | });
1869 |
1870 | expect(result).toEqual(mockResponse);
1871 | expect(scope.isDone()).toBe(true);
1872 | });
1873 |
1874 | it("should mark issue as won't fix with comment", async () => {
1875 | const mockResponse = {
1876 | issue: {
1877 | key: 'ISSUE-456',
1878 | status: 'RESOLVED',
1879 | resolution: 'WONTFIX',
1880 | },
1881 | components: [],
1882 | rules: [],
1883 | users: [],
1884 | };
1885 |
1886 | const commentScope = nock(baseUrl)
1887 | .post('/api/issues/add_comment', {
1888 | issue: 'ISSUE-456',
1889 | text: "Won't fix due to constraints",
1890 | })
1891 | .matchHeader('authorization', 'Bearer test-token')
1892 | .reply(200, {});
1893 |
1894 | const transitionScope = nock(baseUrl)
1895 | .post('/api/issues/do_transition', {
1896 | issue: 'ISSUE-456',
1897 | transition: 'wontfix',
1898 | })
1899 | .matchHeader('authorization', 'Bearer test-token')
1900 | .reply(200, mockResponse);
1901 |
1902 | const result = await client.markIssueWontFix({
1903 | issueKey: 'ISSUE-456',
1904 | comment: "Won't fix due to constraints",
1905 | });
1906 |
1907 | expect(result).toEqual(mockResponse);
1908 | expect(commentScope.isDone()).toBe(true);
1909 | expect(transitionScope.isDone()).toBe(true);
1910 | });
1911 | });
1912 |
1913 | describe('markIssuesFalsePositive', () => {
1914 | it('should mark multiple issues as false positive successfully', async () => {
1915 | const mockResponse1 = {
1916 | issue: {
1917 | key: 'ISSUE-123',
1918 | status: 'RESOLVED',
1919 | resolution: 'FALSE-POSITIVE',
1920 | },
1921 | components: [],
1922 | rules: [],
1923 | users: [],
1924 | };
1925 |
1926 | const mockResponse2 = {
1927 | issue: {
1928 | key: 'ISSUE-124',
1929 | status: 'RESOLVED',
1930 | resolution: 'FALSE-POSITIVE',
1931 | },
1932 | components: [],
1933 | rules: [],
1934 | users: [],
1935 | };
1936 |
1937 | const scope1 = nock(baseUrl)
1938 | .post('/api/issues/do_transition', {
1939 | issue: 'ISSUE-123',
1940 | transition: 'falsepositive',
1941 | })
1942 | .matchHeader('authorization', 'Bearer test-token')
1943 | .reply(200, mockResponse1);
1944 |
1945 | const scope2 = nock(baseUrl)
1946 | .post('/api/issues/do_transition', {
1947 | issue: 'ISSUE-124',
1948 | transition: 'falsepositive',
1949 | })
1950 | .matchHeader('authorization', 'Bearer test-token')
1951 | .reply(200, mockResponse2);
1952 |
1953 | const result = await client.markIssuesFalsePositive({
1954 | issueKeys: ['ISSUE-123', 'ISSUE-124'],
1955 | });
1956 |
1957 | expect(result).toEqual([mockResponse1, mockResponse2]);
1958 | expect(scope1.isDone()).toBe(true);
1959 | expect(scope2.isDone()).toBe(true);
1960 | });
1961 | });
1962 |
1963 | describe('markIssuesWontFix', () => {
1964 | it("should mark multiple issues as won't fix successfully", async () => {
1965 | const mockResponse1 = {
1966 | issue: {
1967 | key: 'ISSUE-456',
1968 | status: 'RESOLVED',
1969 | resolution: 'WONTFIX',
1970 | },
1971 | components: [],
1972 | rules: [],
1973 | users: [],
1974 | };
1975 |
1976 | const mockResponse2 = {
1977 | issue: {
1978 | key: 'ISSUE-457',
1979 | status: 'RESOLVED',
1980 | resolution: 'WONTFIX',
1981 | },
1982 | components: [],
1983 | rules: [],
1984 | users: [],
1985 | };
1986 |
1987 | const scope1 = nock(baseUrl)
1988 | .post('/api/issues/do_transition', {
1989 | issue: 'ISSUE-456',
1990 | transition: 'wontfix',
1991 | })
1992 | .matchHeader('authorization', 'Bearer test-token')
1993 | .reply(200, mockResponse1);
1994 |
1995 | const scope2 = nock(baseUrl)
1996 | .post('/api/issues/do_transition', {
1997 | issue: 'ISSUE-457',
1998 | transition: 'wontfix',
1999 | })
2000 | .matchHeader('authorization', 'Bearer test-token')
2001 | .reply(200, mockResponse2);
2002 |
2003 | const result = await client.markIssuesWontFix({
2004 | issueKeys: ['ISSUE-456', 'ISSUE-457'],
2005 | });
2006 |
2007 | expect(result).toEqual([mockResponse1, mockResponse2]);
2008 | expect(scope1.isDone()).toBe(true);
2009 | expect(scope2.isDone()).toBe(true);
2010 | });
2011 | });
2012 |
2013 | describe('addCommentToIssue', () => {
2014 | it('should add a comment to an issue', async () => {
2015 | const mockResponse = {
2016 | issue: {
2017 | key: 'ISSUE-789',
2018 | comments: [
2019 | {
2020 | key: 'comment-123',
2021 | login: 'test-user',
2022 | htmlText: '<p>Test comment</p>',
2023 | markdown: 'Test comment',
2024 | updatable: true,
2025 | createdAt: '2024-01-01T10:00:00+0000',
2026 | },
2027 | ],
2028 | },
2029 | };
2030 |
2031 | const scope = nock(baseUrl)
2032 | .post('/api/issues/add_comment', {
2033 | issue: 'ISSUE-789',
2034 | text: 'Test comment',
2035 | })
2036 | .matchHeader('authorization', 'Bearer test-token')
2037 | .reply(200, mockResponse);
2038 |
2039 | const result = await client.addCommentToIssue({
2040 | issueKey: 'ISSUE-789',
2041 | text: 'Test comment',
2042 | });
2043 |
2044 | expect(scope.isDone()).toBe(true);
2045 | expect(result.key).toBe('comment-123');
2046 | expect(result.markdown).toBe('Test comment');
2047 | });
2048 |
2049 | it('should add a comment with markdown formatting', async () => {
2050 | const mockResponse = {
2051 | issue: {
2052 | key: 'ISSUE-789',
2053 | comments: [
2054 | {
2055 | key: 'comment-456',
2056 | login: 'test-user',
2057 | htmlText: '<p>Test with <strong>markdown</strong></p>',
2058 | markdown: 'Test with **markdown**',
2059 | updatable: true,
2060 | createdAt: '2024-01-01T11:00:00+0000',
2061 | },
2062 | ],
2063 | },
2064 | };
2065 |
2066 | const scope = nock(baseUrl)
2067 | .post('/api/issues/add_comment', {
2068 | issue: 'ISSUE-789',
2069 | text: 'Test with **markdown**',
2070 | })
2071 | .matchHeader('authorization', 'Bearer test-token')
2072 | .reply(200, mockResponse);
2073 |
2074 | const result = await client.addCommentToIssue({
2075 | issueKey: 'ISSUE-789',
2076 | text: 'Test with **markdown**',
2077 | });
2078 |
2079 | expect(scope.isDone()).toBe(true);
2080 | expect(result.markdown).toBe('Test with **markdown**');
2081 | expect(result.htmlText).toBe('<p>Test with <strong>markdown</strong></p>');
2082 | });
2083 | });
2084 |
2085 | describe('assignIssue', () => {
2086 | it('should assign an issue to a user', async () => {
2087 | const issueKey = 'ISSUE-999';
2088 | const assignee = 'john.doe';
2089 |
2090 | // Mock the assign API call
2091 | const assignScope = nock(baseUrl)
2092 | .post('/api/issues/assign', {
2093 | issue: issueKey,
2094 | assignee: assignee,
2095 | })
2096 | .matchHeader('authorization', 'Bearer test-token')
2097 | .reply(200);
2098 |
2099 | // Mock the search API call
2100 | const searchScope = nock(baseUrl)
2101 | .get('/api/issues/search')
2102 | .query({
2103 | issues: issueKey,
2104 | additionalFields: '_all',
2105 | })
2106 | .matchHeader('authorization', 'Bearer test-token')
2107 | .reply(200, {
2108 | issues: [
2109 | {
2110 | key: issueKey,
2111 | message: 'Test issue',
2112 | component: 'src/test.js',
2113 | assignee: assignee,
2114 | assigneeName: 'John Doe',
2115 | severity: 'MAJOR',
2116 | type: 'BUG',
2117 | status: 'OPEN',
2118 | },
2119 | ],
2120 | total: 1,
2121 | });
2122 |
2123 | const result = await client.assignIssue({
2124 | issueKey,
2125 | assignee,
2126 | });
2127 |
2128 | expect(assignScope.isDone()).toBe(true);
2129 | expect(searchScope.isDone()).toBe(true);
2130 | expect(result.key).toBe(issueKey);
2131 | expect((result as any).assignee).toBe(assignee);
2132 | });
2133 |
2134 | it('should unassign an issue when assignee is not provided', async () => {
2135 | const issueKey = 'ISSUE-888';
2136 |
2137 | // Mock the assign API call
2138 | const assignScope = nock(baseUrl)
2139 | .post('/api/issues/assign', {
2140 | issue: issueKey,
2141 | })
2142 | .matchHeader('authorization', 'Bearer test-token')
2143 | .reply(200);
2144 |
2145 | // Mock the search API call
2146 | const searchScope = nock(baseUrl)
2147 | .get('/api/issues/search')
2148 | .query({
2149 | issues: issueKey,
2150 | additionalFields: '_all',
2151 | })
2152 | .matchHeader('authorization', 'Bearer test-token')
2153 | .reply(200, {
2154 | issues: [
2155 | {
2156 | key: issueKey,
2157 | message: 'Test issue',
2158 | component: 'src/test.js',
2159 | assignee: null,
2160 | assigneeName: null,
2161 | severity: 'MINOR',
2162 | type: 'CODE_SMELL',
2163 | status: 'OPEN',
2164 | },
2165 | ],
2166 | total: 1,
2167 | });
2168 |
2169 | const result = await client.assignIssue({
2170 | issueKey,
2171 | });
2172 |
2173 | expect(assignScope.isDone()).toBe(true);
2174 | expect(searchScope.isDone()).toBe(true);
2175 | expect(result.key).toBe(issueKey);
2176 | expect((result as any).assignee).toBeNull();
2177 | });
2178 | });
2179 |
2180 | describe('confirmIssue', () => {
2181 | it('should confirm an issue', async () => {
2182 | const mockResponse = {
2183 | issue: { key: 'ISSUE-123', status: 'CONFIRMED' },
2184 | components: [],
2185 | rules: [],
2186 | users: [],
2187 | };
2188 |
2189 | const scope = nock(baseUrl)
2190 | .post('/api/issues/do_transition', {
2191 | issue: 'ISSUE-123',
2192 | transition: 'confirm',
2193 | })
2194 | .matchHeader('authorization', 'Bearer test-token')
2195 | .reply(200, mockResponse);
2196 |
2197 | const result = await client.confirmIssue({
2198 | issueKey: 'ISSUE-123',
2199 | });
2200 |
2201 | expect(scope.isDone()).toBe(true);
2202 | expect(result.issue.status).toBe('CONFIRMED');
2203 | });
2204 |
2205 | it('should confirm an issue with comment', async () => {
2206 | const mockResponse = {
2207 | issue: { key: 'ISSUE-123', status: 'CONFIRMED' },
2208 | components: [],
2209 | rules: [],
2210 | users: [],
2211 | };
2212 |
2213 | const commentScope = nock(baseUrl)
2214 | .post('/api/issues/add_comment', {
2215 | issue: 'ISSUE-123',
2216 | text: 'Confirmed after review',
2217 | })
2218 | .matchHeader('authorization', 'Bearer test-token')
2219 | .reply(200);
2220 |
2221 | const transitionScope = nock(baseUrl)
2222 | .post('/api/issues/do_transition', {
2223 | issue: 'ISSUE-123',
2224 | transition: 'confirm',
2225 | })
2226 | .matchHeader('authorization', 'Bearer test-token')
2227 | .reply(200, mockResponse);
2228 |
2229 | const result = await client.confirmIssue({
2230 | issueKey: 'ISSUE-123',
2231 | comment: 'Confirmed after review',
2232 | });
2233 |
2234 | expect(commentScope.isDone()).toBe(true);
2235 | expect(transitionScope.isDone()).toBe(true);
2236 | expect(result.issue.status).toBe('CONFIRMED');
2237 | });
2238 | });
2239 |
2240 | describe('unconfirmIssue', () => {
2241 | it('should unconfirm an issue', async () => {
2242 | const mockResponse = {
2243 | issue: { key: 'ISSUE-123', status: 'REOPENED' },
2244 | components: [],
2245 | rules: [],
2246 | users: [],
2247 | };
2248 |
2249 | const scope = nock(baseUrl)
2250 | .post('/api/issues/do_transition', {
2251 | issue: 'ISSUE-123',
2252 | transition: 'unconfirm',
2253 | })
2254 | .matchHeader('authorization', 'Bearer test-token')
2255 | .reply(200, mockResponse);
2256 |
2257 | const result = await client.unconfirmIssue({
2258 | issueKey: 'ISSUE-123',
2259 | });
2260 |
2261 | expect(scope.isDone()).toBe(true);
2262 | expect(result.issue.status).toBe('REOPENED');
2263 | });
2264 | });
2265 |
2266 | describe('resolveIssue', () => {
2267 | it('should resolve an issue', async () => {
2268 | const mockResponse = {
2269 | issue: { key: 'ISSUE-123', status: 'RESOLVED', resolution: 'FIXED' },
2270 | components: [],
2271 | rules: [],
2272 | users: [],
2273 | };
2274 |
2275 | const scope = nock(baseUrl)
2276 | .post('/api/issues/do_transition', {
2277 | issue: 'ISSUE-123',
2278 | transition: 'resolve',
2279 | })
2280 | .matchHeader('authorization', 'Bearer test-token')
2281 | .reply(200, mockResponse);
2282 |
2283 | const result = await client.resolveIssue({
2284 | issueKey: 'ISSUE-123',
2285 | });
2286 |
2287 | expect(scope.isDone()).toBe(true);
2288 | expect(result.issue.status).toBe('RESOLVED');
2289 | expect(result.issue.resolution).toBe('FIXED');
2290 | });
2291 | });
2292 |
2293 | describe('reopenIssue', () => {
2294 | it('should reopen an issue', async () => {
2295 | const mockResponse = {
2296 | issue: { key: 'ISSUE-123', status: 'REOPENED' },
2297 | components: [],
2298 | rules: [],
2299 | users: [],
2300 | };
2301 |
2302 | const scope = nock(baseUrl)
2303 | .post('/api/issues/do_transition', {
2304 | issue: 'ISSUE-123',
2305 | transition: 'reopen',
2306 | })
2307 | .matchHeader('authorization', 'Bearer test-token')
2308 | .reply(200, mockResponse);
2309 |
2310 | const result = await client.reopenIssue({
2311 | issueKey: 'ISSUE-123',
2312 | });
2313 |
2314 | expect(scope.isDone()).toBe(true);
2315 | expect(result.issue.status).toBe('REOPENED');
2316 | });
2317 | });
2318 | });
2319 | });
2320 |
```