#
tokens: 25334/50000 1/236 files (page 10/11)
lines: on (toggle) GitHub
raw markdown copy reset
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 | 
```
Page 10/11FirstPrevNextLast