Modify sonarqube frontend plugin to call the new sonarqube backend
backend only have mock API for now Signed-off-by: Neemys <36508659+Neemys@users.noreply.github.com>
This commit is contained in:
@@ -18,7 +18,7 @@ import { setupRequestMockHandlers } from '@backstage/test-utils';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { FindingSummary, SonarQubeClient } from './index';
|
||||
import { ComponentWrapper, MeasuresWrapper } from './types';
|
||||
import { FindingsWrapper } from './types';
|
||||
import { UrlPatternDiscovery } from '@backstage/core-app-api';
|
||||
import { IdentityApi } from '@backstage/core-plugin-api';
|
||||
|
||||
@@ -40,133 +40,66 @@ const identityApiGuest: IdentityApi = {
|
||||
describe('SonarQubeClient', () => {
|
||||
setupRequestMockHandlers(server);
|
||||
|
||||
const mockBaseUrl = 'http://backstage:9191/api/proxy';
|
||||
const mockBaseUrl = 'http://backstage:9191/api/sonarqube';
|
||||
const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl);
|
||||
|
||||
const setupHandlers = (
|
||||
metricKeys = [
|
||||
'alert_status',
|
||||
'bugs',
|
||||
'reliability_rating',
|
||||
'vulnerabilities',
|
||||
'security_rating',
|
||||
'security_hotspots_reviewed',
|
||||
'security_review_rating',
|
||||
'code_smells',
|
||||
'sqale_rating',
|
||||
'coverage',
|
||||
'duplicated_lines_density',
|
||||
],
|
||||
) => {
|
||||
const setupHandlers = () => {
|
||||
server.use(
|
||||
rest.get(`${mockBaseUrl}/sonarqube/metrics/search`, (req, res, ctx) => {
|
||||
expect(req.url.searchParams.get('ps')).toBe('500');
|
||||
|
||||
// emulate paging to check if everything is requested
|
||||
if (req.url.searchParams.get('p') === '1') {
|
||||
return res(
|
||||
ctx.json({
|
||||
metrics: metricKeys.slice(0, 5).map(k => ({ key: k })),
|
||||
total: metricKeys.length,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// make sure this is only called twice
|
||||
expect(req.url.searchParams.get('p')).toBe('2');
|
||||
return res(
|
||||
ctx.json({
|
||||
metrics: metricKeys.slice(5).map(k => ({ key: k })),
|
||||
total: metricKeys.length,
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
server.use(
|
||||
rest.get(`${mockBaseUrl}/sonarqube/components/show`, (req, res, ctx) => {
|
||||
expect(req.url.searchParams.toString()).toBe('component=our%3Aservice');
|
||||
return res(
|
||||
ctx.json({
|
||||
component: {
|
||||
analysisDate: '2020-01-01T00:00:00Z',
|
||||
},
|
||||
} as ComponentWrapper),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
server.use(
|
||||
rest.get(`${mockBaseUrl}/sonarqube/measures/search`, (req, res, ctx) => {
|
||||
rest.get(`${mockBaseUrl}/findings`, (req, res, ctx) => {
|
||||
expect(req.url.searchParams.toString()).toBe(
|
||||
`projectKeys=our%3Aservice&metricKeys=${metricKeys.join('%2C')}`,
|
||||
'componentKey=our%3Aservice',
|
||||
);
|
||||
|
||||
return res(
|
||||
ctx.json({
|
||||
analysisDate: '2020-01-01T00:00:00Z',
|
||||
measures: [
|
||||
{
|
||||
metric: 'alert_status',
|
||||
value: 'OK',
|
||||
component: 'our:service',
|
||||
},
|
||||
{
|
||||
metric: 'alert_status',
|
||||
value: 'ERROR',
|
||||
component: 'other-service',
|
||||
},
|
||||
{
|
||||
metric: 'bugs',
|
||||
value: '2',
|
||||
component: 'our:service',
|
||||
},
|
||||
{
|
||||
metric: 'reliability_rating',
|
||||
value: '3.0',
|
||||
component: 'our:service',
|
||||
},
|
||||
{
|
||||
metric: 'vulnerabilities',
|
||||
value: '4',
|
||||
component: 'our:service',
|
||||
},
|
||||
{
|
||||
metric: 'security_rating',
|
||||
value: '1.0',
|
||||
component: 'our:service',
|
||||
},
|
||||
{
|
||||
metric: 'security_hotspots_reviewed',
|
||||
value: '100',
|
||||
component: 'our:service',
|
||||
},
|
||||
{
|
||||
metric: 'security_review_rating',
|
||||
value: '1.0',
|
||||
component: 'our:service',
|
||||
},
|
||||
{
|
||||
metric: 'code_smells',
|
||||
value: '100',
|
||||
component: 'our:service',
|
||||
},
|
||||
{
|
||||
metric: 'sqale_rating',
|
||||
value: '2.0',
|
||||
component: 'our:service',
|
||||
},
|
||||
{
|
||||
metric: 'coverage',
|
||||
value: '55.5',
|
||||
component: 'our:service',
|
||||
},
|
||||
{
|
||||
metric: 'duplicated_lines_density',
|
||||
value: '1.0',
|
||||
component: 'our:service',
|
||||
},
|
||||
].filter(m => metricKeys.includes(m.metric)),
|
||||
} as MeasuresWrapper),
|
||||
],
|
||||
} as FindingsWrapper),
|
||||
);
|
||||
}),
|
||||
);
|
||||
@@ -246,47 +179,19 @@ describe('SonarQubeClient', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should only request selected metrics', async () => {
|
||||
setupHandlers(['alert_status', 'bugs']);
|
||||
|
||||
const client = new SonarQubeClient({
|
||||
discoveryApi,
|
||||
baseUrl: 'http://a.instance.local',
|
||||
identityApi: identityApiAuthenticated,
|
||||
});
|
||||
|
||||
const summary = await client.getFindingSummary('our:service');
|
||||
|
||||
expect(summary).toEqual(
|
||||
expect.objectContaining({
|
||||
lastAnalysis: '2020-01-01T00:00:00Z',
|
||||
metrics: {
|
||||
alert_status: 'OK',
|
||||
bugs: '2',
|
||||
},
|
||||
projectUrl: 'http://a.instance.local/dashboard?id=our%3Aservice',
|
||||
}) as FindingSummary,
|
||||
);
|
||||
expect(summary?.getIssuesUrl('CODE_SMELL')).toEqual(
|
||||
'http://a.instance.local/project/issues?id=our%3Aservice&types=CODE_SMELL&resolved=false',
|
||||
);
|
||||
expect(summary?.getComponentMeasuresUrl('COVERAGE')).toEqual(
|
||||
'http://a.instance.local/component_measures?id=our%3Aservice&metric=coverage&resolved=false&view=list',
|
||||
);
|
||||
});
|
||||
|
||||
it('should add identity token for logged in users', async () => {
|
||||
setupHandlers();
|
||||
server.use(
|
||||
rest.get(`${mockBaseUrl}/sonarqube/components/show`, (req, res, ctx) => {
|
||||
expect(req.url.searchParams.toString()).toBe('component=our%3Aservice');
|
||||
rest.get(`${mockBaseUrl}/findings`, (req, res, ctx) => {
|
||||
expect(req.url.searchParams.toString()).toBe(
|
||||
'componentKey=our%3Aservice',
|
||||
);
|
||||
expect(req.headers.get('Authorization')).toBe('Bearer fake-id-token');
|
||||
return res(
|
||||
ctx.json({
|
||||
component: {
|
||||
analysisDate: '2020-01-01T00:00:00Z',
|
||||
},
|
||||
} as ComponentWrapper),
|
||||
analysisDate: '2020-01-01T00:00:00Z',
|
||||
measures: [],
|
||||
} as FindingsWrapper),
|
||||
);
|
||||
}),
|
||||
);
|
||||
@@ -304,15 +209,16 @@ describe('SonarQubeClient', () => {
|
||||
it('should omit identity token for guest users', async () => {
|
||||
setupHandlers();
|
||||
server.use(
|
||||
rest.get(`${mockBaseUrl}/sonarqube/components/show`, (req, res, ctx) => {
|
||||
expect(req.url.searchParams.toString()).toBe('component=our%3Aservice');
|
||||
rest.get(`${mockBaseUrl}/findings`, (req, res, ctx) => {
|
||||
expect(req.url.searchParams.toString()).toBe(
|
||||
'componentKey=our%3Aservice',
|
||||
);
|
||||
expect(req.headers.has('Authorization')).toBeFalsy();
|
||||
return res(
|
||||
ctx.json({
|
||||
component: {
|
||||
analysisDate: '2020-01-01T00:00:00Z',
|
||||
},
|
||||
} as ComponentWrapper),
|
||||
analysisDate: '2020-01-01T00:00:00Z',
|
||||
measures: [],
|
||||
} as FindingsWrapper),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import fetch from 'cross-fetch';
|
||||
import { FindingSummary, Metrics, SonarQubeApi } from './SonarQubeApi';
|
||||
import { ComponentWrapper, MeasuresWrapper } from './types';
|
||||
import { FindingsWrapper } from './types';
|
||||
import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
|
||||
|
||||
export class SonarQubeClient implements SonarQubeApi {
|
||||
@@ -44,7 +44,7 @@ export class SonarQubeClient implements SonarQubeApi {
|
||||
): Promise<T | undefined> {
|
||||
const { token: idToken } = await this.identityApi.getCredentials();
|
||||
|
||||
const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/sonarqube`;
|
||||
const apiUrl = `${await this.discoveryApi.getBaseUrl('sonarqube')}`;
|
||||
const response = await fetch(
|
||||
`${apiUrl}/${path}?${new URLSearchParams(query).toString()}`,
|
||||
{
|
||||
@@ -60,27 +60,6 @@ export class SonarQubeClient implements SonarQubeApi {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async getSupportedMetrics(): Promise<string[]> {
|
||||
const metrics: string[] = [];
|
||||
let nextPage: number = 1;
|
||||
|
||||
for (;;) {
|
||||
const result = await this.callApi<{
|
||||
metrics: Array<{ key: string }>;
|
||||
total: number;
|
||||
}>('metrics/search', { ps: 500, p: nextPage });
|
||||
|
||||
metrics.push(...(result?.metrics?.map(m => m.key) ?? []));
|
||||
|
||||
if (result && metrics.length < result.total) {
|
||||
nextPage++;
|
||||
continue;
|
||||
}
|
||||
|
||||
return metrics;
|
||||
}
|
||||
}
|
||||
|
||||
async getFindingSummary(
|
||||
componentKey?: string,
|
||||
): Promise<FindingSummary | undefined> {
|
||||
@@ -88,13 +67,6 @@ export class SonarQubeClient implements SonarQubeApi {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const component = await this.callApi<ComponentWrapper>('components/show', {
|
||||
component: componentKey,
|
||||
});
|
||||
if (!component) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const metrics: Metrics = {
|
||||
alert_status: undefined,
|
||||
bugs: undefined,
|
||||
@@ -109,28 +81,19 @@ export class SonarQubeClient implements SonarQubeApi {
|
||||
duplicated_lines_density: undefined,
|
||||
};
|
||||
|
||||
// select the metrics that are supported by the SonarQube instance
|
||||
const supportedMetrics = await this.getSupportedMetrics();
|
||||
const metricKeys = Object.keys(metrics).filter(m =>
|
||||
supportedMetrics.includes(m),
|
||||
);
|
||||
|
||||
const measures = await this.callApi<MeasuresWrapper>('measures/search', {
|
||||
projectKeys: componentKey,
|
||||
metricKeys: metricKeys.join(','),
|
||||
const findings = await this.callApi<FindingsWrapper>('findings', {
|
||||
componentKey: componentKey,
|
||||
});
|
||||
if (!measures) {
|
||||
if (!findings) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
measures.measures
|
||||
.filter(m => m.component === componentKey)
|
||||
.forEach(m => {
|
||||
metrics[m.metric] = m.value;
|
||||
});
|
||||
findings.measures.forEach(m => {
|
||||
metrics[m.metric] = m.value;
|
||||
});
|
||||
|
||||
return {
|
||||
lastAnalysis: component.component.analysisDate,
|
||||
lastAnalysis: findings.analysisDate,
|
||||
metrics,
|
||||
projectUrl: `${this.baseUrl}dashboard?id=${encodeURIComponent(
|
||||
componentKey,
|
||||
|
||||
@@ -14,15 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export interface ComponentWrapper {
|
||||
component: Component;
|
||||
}
|
||||
|
||||
export interface Component {
|
||||
export interface FindingsWrapper {
|
||||
analysisDate: string;
|
||||
}
|
||||
|
||||
export interface MeasuresWrapper {
|
||||
measures: Measure[];
|
||||
}
|
||||
|
||||
@@ -55,7 +48,6 @@ export type MetricKey =
|
||||
export interface Measure {
|
||||
metric: MetricKey;
|
||||
value: string;
|
||||
component: string;
|
||||
}
|
||||
|
||||
export type SonarUrlProcessorFunc = (identifier: string) => string;
|
||||
|
||||
Reference in New Issue
Block a user