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:
Neemys
2022-06-16 17:50:24 +02:00
parent 4d64f22c9d
commit 619b515172
6 changed files with 57 additions and 178 deletions
+22 -116
View File
@@ -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),
);
}),
);
+9 -46
View File
@@ -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,
+1 -9
View File
@@ -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;