Skip to content

Commit cbebcfb

Browse files
sapientpantsclaude
andauthored
Add Source Code API capabilities (#67)
* Add Source Code API capabilities Implemented two new tools to provide access to source code with issues highlighted: - source_code: View source code with annotations showing issues - scm_blame: Access raw source and SCM blame information 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * Fix SonarQube code smell in api.ts Replace repeated union type with type alias 'AuthCredentials' to improve code maintainability. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * Refactor duplicated transform code in index.ts - Created a common stringToNumberTransform function - Replaced repeated transform code in tool registrations - Added tests for the new utility function 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * Add tests for boolean transform functions - Added tests for Zod boolean transform schema - Added tests for string-to-boolean conversions - Improved coverage of boolean transforms in index.ts 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Claude <[email protected]>
1 parent 13b85c3 commit cbebcfb

File tree

8 files changed

+1027
-46
lines changed

8 files changed

+1027
-46
lines changed
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/// <reference types="jest" />
2+
3+
/**
4+
* @jest-environment node
5+
*/
6+
7+
import { describe, it, expect } from '@jest/globals';
8+
import { z } from 'zod';
9+
10+
describe('Boolean string transform', () => {
11+
// Test the boolean transform that's used in the tool registrations
12+
const booleanStringTransform = (val: string) => val === 'true';
13+
14+
// Create a schema that matches the one in index.ts
15+
const booleanSchema = z
16+
.union([z.boolean(), z.string().transform(booleanStringTransform)])
17+
.nullable()
18+
.optional();
19+
20+
describe('direct transform function', () => {
21+
it('should transform "true" to true', () => {
22+
expect(booleanStringTransform('true')).toBe(true);
23+
});
24+
25+
it('should transform anything else to false', () => {
26+
expect(booleanStringTransform('false')).toBe(false);
27+
expect(booleanStringTransform('True')).toBe(false);
28+
expect(booleanStringTransform('1')).toBe(false);
29+
expect(booleanStringTransform('')).toBe(false);
30+
});
31+
});
32+
33+
describe('zod schema with boolean transform', () => {
34+
it('should accept and pass through boolean values', () => {
35+
expect(booleanSchema.parse(true)).toBe(true);
36+
expect(booleanSchema.parse(false)).toBe(false);
37+
});
38+
39+
it('should transform string "true" to boolean true', () => {
40+
expect(booleanSchema.parse('true')).toBe(true);
41+
});
42+
43+
it('should transform other string values to boolean false', () => {
44+
expect(booleanSchema.parse('false')).toBe(false);
45+
expect(booleanSchema.parse('1')).toBe(false);
46+
expect(booleanSchema.parse('')).toBe(false);
47+
});
48+
49+
it('should pass through null and undefined', () => {
50+
expect(booleanSchema.parse(null)).toBeNull();
51+
expect(booleanSchema.parse(undefined)).toBeUndefined();
52+
});
53+
});
54+
55+
// Test multiple boolean schema transformations in the same schema
56+
describe('multiple boolean transforms in schema', () => {
57+
// Create a schema with multiple boolean transforms
58+
const complexSchema = z.object({
59+
resolved: z
60+
.union([z.boolean(), z.string().transform(booleanStringTransform)])
61+
.nullable()
62+
.optional(),
63+
on_component_only: z
64+
.union([z.boolean(), z.string().transform(booleanStringTransform)])
65+
.nullable()
66+
.optional(),
67+
since_leak_period: z
68+
.union([z.boolean(), z.string().transform(booleanStringTransform)])
69+
.nullable()
70+
.optional(),
71+
in_new_code_period: z
72+
.union([z.boolean(), z.string().transform(booleanStringTransform)])
73+
.nullable()
74+
.optional(),
75+
});
76+
77+
it('should transform multiple boolean string values', () => {
78+
const result = complexSchema.parse({
79+
resolved: 'true',
80+
on_component_only: 'false',
81+
since_leak_period: true,
82+
in_new_code_period: 'true',
83+
});
84+
85+
expect(result).toEqual({
86+
resolved: true,
87+
on_component_only: false,
88+
since_leak_period: true,
89+
in_new_code_period: true,
90+
});
91+
});
92+
93+
it('should handle mix of boolean, string, null and undefined values', () => {
94+
const result = complexSchema.parse({
95+
resolved: true,
96+
on_component_only: 'true',
97+
since_leak_period: null,
98+
});
99+
100+
expect(result).toEqual({
101+
resolved: true,
102+
on_component_only: true,
103+
since_leak_period: null,
104+
});
105+
});
106+
});
107+
});

src/__tests__/mocks/sonarqube-client.mock.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
MeasuresHistoryParams,
88
PaginationParams,
99
ProjectQualityGateParams,
10+
ScmBlameParams,
11+
SourceCodeParams,
1012
SonarQubeComponentMeasuresResult,
1113
SonarQubeComponentsMeasuresResult,
1214
SonarQubeHealthStatus,
@@ -16,6 +18,8 @@ import {
1618
SonarQubeQualityGate,
1719
SonarQubeQualityGateStatus,
1820
SonarQubeQualityGatesResult,
21+
SonarQubeScmBlameResult,
22+
SonarQubeSourceResult,
1923
SonarQubeSystemStatus,
2024
SonarQubeMeasuresHistoryResult,
2125
} from '../../sonarqube.js';
@@ -37,6 +41,8 @@ export class MockSonarQubeClient implements ISonarQubeClient {
3741
listQualityGatesMock: jest.Mock = jest.fn();
3842
getQualityGateMock: jest.Mock = jest.fn();
3943
getProjectQualityGateStatusMock: jest.Mock = jest.fn();
44+
getSourceCodeMock: jest.Mock = jest.fn();
45+
getScmBlameMock: jest.Mock = jest.fn();
4046

4147
constructor() {
4248
this.setupDefaultMocks();
@@ -97,6 +103,14 @@ export class MockSonarQubeClient implements ISonarQubeClient {
97103
return this.getProjectQualityGateStatusMock(params) as Promise<SonarQubeQualityGateStatus>;
98104
}
99105

106+
async getSourceCode(params: SourceCodeParams): Promise<SonarQubeSourceResult> {
107+
return this.getSourceCodeMock(params) as Promise<SonarQubeSourceResult>;
108+
}
109+
110+
async getScmBlame(params: ScmBlameParams): Promise<SonarQubeScmBlameResult> {
111+
return this.getScmBlameMock(params) as Promise<SonarQubeScmBlameResult>;
112+
}
113+
100114
// Reset all mocks
101115
reset() {
102116
this.listProjectsMock.mockReset();
@@ -111,6 +125,8 @@ export class MockSonarQubeClient implements ISonarQubeClient {
111125
this.listQualityGatesMock.mockReset();
112126
this.getQualityGateMock.mockReset();
113127
this.getProjectQualityGateStatusMock.mockReset();
128+
this.getSourceCodeMock.mockReset();
129+
this.getScmBlameMock.mockReset();
114130

115131
// Re-setup default mock implementations
116132
this.setupDefaultMocks();
@@ -338,5 +354,73 @@ export class MockSonarQubeClient implements ISonarQubeClient {
338354
},
339355
} as SonarQubeQualityGateStatus)
340356
);
357+
358+
// Get source code
359+
this.getSourceCodeMock.mockImplementation(() =>
360+
Promise.resolve({
361+
component: {
362+
key: 'test-component',
363+
path: 'src/test.js',
364+
qualifier: 'FIL',
365+
name: 'test.js',
366+
longName: 'src/test.js',
367+
language: 'js',
368+
},
369+
sources: [
370+
{
371+
line: 1,
372+
code: 'function test() {',
373+
scmAuthor: 'developer',
374+
scmDate: '2023-01-01',
375+
scmRevision: 'abc123',
376+
},
377+
{
378+
line: 2,
379+
code: ' return "test";',
380+
scmAuthor: 'developer',
381+
scmDate: '2023-01-01',
382+
scmRevision: 'abc123',
383+
},
384+
{
385+
line: 3,
386+
code: '}',
387+
scmAuthor: 'developer',
388+
scmDate: '2023-01-01',
389+
scmRevision: 'abc123',
390+
},
391+
],
392+
} as SonarQubeSourceResult)
393+
);
394+
395+
// Get SCM blame
396+
this.getScmBlameMock.mockImplementation(() =>
397+
Promise.resolve({
398+
component: {
399+
key: 'test-component',
400+
path: 'src/test.js',
401+
qualifier: 'FIL',
402+
name: 'test.js',
403+
longName: 'src/test.js',
404+
language: 'js',
405+
},
406+
sources: {
407+
'1': {
408+
author: 'developer',
409+
date: '2023-01-01',
410+
revision: 'abc123',
411+
},
412+
'2': {
413+
author: 'developer',
414+
date: '2023-01-01',
415+
revision: 'abc123',
416+
},
417+
'3': {
418+
author: 'developer',
419+
date: '2023-01-01',
420+
revision: 'abc123',
421+
},
422+
},
423+
} as SonarQubeScmBlameResult)
424+
);
341425
}
342426
}

0 commit comments

Comments
 (0)