diff --git a/plugins/sonarqube-backend/src/service/router.test.ts b/plugins/sonarqube-backend/src/service/router.test.ts index 24e482896b..756fba6164 100644 --- a/plugins/sonarqube-backend/src/service/router.test.ts +++ b/plugins/sonarqube-backend/src/service/router.test.ts @@ -19,13 +19,23 @@ import express from 'express'; import request from 'supertest'; import { createRouter } from './router'; +import { SonarqubeFindings } from './sonarqubeInfoProvider'; describe('createRouter', () => { let app: express.Express; + const getBaseUrlMock: jest.Mock = jest.fn(); + const getFindingsMock: jest.Mock< + Promise, + [string, string] + > = jest.fn(); beforeAll(async () => { const router = await createRouter({ logger: getVoidLogger(), + sonarqubeInfoProvider: { + getBaseUrl: getBaseUrlMock, + getFindings: getFindingsMock, + }, }); app = express().use(router); }); @@ -35,17 +45,76 @@ describe('createRouter', () => { }); describe('GET /findings', () => { + const DUMMY_COMPONENT_KEY = 'my:component'; + const DUMMY_INSTANCE_KEY = 'myInstance'; it('returns ok', async () => { + const measures = { + analysisDate: '2022-01-01T00:00:00Z', + measures: [{ metric: 'vulnerabilities', value: '54' }], + }; + + getFindingsMock.mockReturnValue(Promise.resolve(measures)); const response = await request(app) .get('/findings') - .set('componentKey', 'my:app') + .query({ + componentKey: DUMMY_COMPONENT_KEY, + instanceKey: DUMMY_INSTANCE_KEY, + }) + .send(); + expect(getFindingsMock).toBeCalledTimes(1); + expect(getFindingsMock).toBeCalledWith( + DUMMY_COMPONENT_KEY, + DUMMY_INSTANCE_KEY, + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual(measures); + }); + it('returns an error when component key is not defined', async () => { + const response = await request(app) + .get('/findings') + .query({ + instanceKey: DUMMY_INSTANCE_KEY, + }) .send(); + expect(response.status).toEqual(400); + }); + + it('use an empty string as instance name when instance key not provided', async () => { + const measures = { + analysisDate: '2021-04-08', + measures: [{ metric: 'vulnerabilities', value: '54' }], + }; + + getFindingsMock.mockReturnValue(Promise.resolve(measures)); + const response = await request(app) + .get('/findings') + .query({ + componentKey: DUMMY_COMPONENT_KEY, + }) + .send(); + + expect(getFindingsMock).toBeCalledTimes(1); + expect(getFindingsMock).toBeCalledWith(DUMMY_COMPONENT_KEY, ''); expect(response.status).toEqual(200); - expect(response.body).toEqual({ - analysisDate: '2022-10-22T04:55:23Z', - measures: [{ metric: 'coverage', value: '50' }], - }); + expect(response.body).toEqual(measures); + }); + }); + describe('GET /instanceUrl', () => { + const DUMMY_INSTANCE_KEY = 'myInstance'; + const DUMMY_INSTANCE_URL = 'http://sonarqube.example.com'; + it('returns ok', async () => { + getBaseUrlMock.mockReturnValue(DUMMY_INSTANCE_URL); + const response = await request(app) + .get('/instanceUrl') + .query({ + instanceKey: DUMMY_INSTANCE_KEY, + }) + .send(); + expect(getBaseUrlMock).toBeCalledTimes(1); + expect(getBaseUrlMock).toBeCalledWith(DUMMY_INSTANCE_KEY); + expect(response.status).toEqual(200); + expect(response.body).toEqual({ instanceUrl: DUMMY_INSTANCE_URL }); }); }); }); diff --git a/plugins/sonarqube-backend/src/service/router.ts b/plugins/sonarqube-backend/src/service/router.ts index 54f0038ca9..c1f672a2c4 100644 --- a/plugins/sonarqube-backend/src/service/router.ts +++ b/plugins/sonarqube-backend/src/service/router.ts @@ -15,37 +15,87 @@ */ import { errorHandler } from '@backstage/backend-common'; -import express from 'express'; +import express, { RequestHandler } from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; +import { + SonarqubeFindings, + SonarqubeInfoProvider, +} from './sonarqubeInfoProvider'; +import { InputError } from '../../../../packages/errors'; +/** + * Dependencies needed by the router + * @public + */ export interface RouterOptions { + /** + * Logger for logging purposes + */ logger: Logger; + /** + * Info provider to be able to get all necessary information for the APIs + */ + sonarqubeInfoProvider: SonarqubeInfoProvider; } +/** + * @public + * + * Constructs a sonarqube router. + * + * Expose endpoint to get information on or for a sonarqube instance. + * + * @param options - Dependencies of the router + */ export async function createRouter( options: RouterOptions, ): Promise { - const { logger } = options; + const { logger, sonarqubeInfoProvider } = options; const router = Router(); router.use(express.json()); // mock api for now - router.get('/findings', (request, response) => { - logger.info(request.params); + router.get('/findings', (async (request, response) => { + const componentKey = request.query.componentKey; + let instanceKey = request.query.instanceKey; + + if (!componentKey) + throw new InputError('ComponentKey must be provided as a single string.'); + + if (!instanceKey) { + instanceKey = ''; + logger.info( + `Retrieving findings for component ${componentKey} in default sonarqube instance`, + ); + } else { + logger.info( + `Retrieving findings for component ${componentKey} in sonarqube instance name ${instanceKey}`, + ); + } + + response.send( + await sonarqubeInfoProvider.getFindings(componentKey, instanceKey), + ); + }) as RequestHandler); + + router.get('/instanceUrl', ((request, response) => { + let requestedInstanceKey = request.query.instanceKey; + if (requestedInstanceKey) { + logger.info( + `Retrieving sonarqube instance URL for key ${requestedInstanceKey}`, + ); + } else { + requestedInstanceKey = ''; + logger.info( + `Retrieving default sonarqube instance URL as parameter is inexistant, empty or malformed`, + ); + } response.send({ - analysisDate: '2022-10-22T04:55:23Z', - measures: [{ metric: 'coverage', value: '50' }], + instanceUrl: sonarqubeInfoProvider.getBaseUrl(requestedInstanceKey), }); - }); - router.get('/instanceUrl', (request, response) => { - logger.info(request.params); - response.send({ - instanceUrl: `https://instance.local?${encodeURI( - request.query.instanceKey as string, - )}`, - }); - }); + }) as RequestHandler); + router.use(errorHandler()); return router; } diff --git a/plugins/sonarqube-backend/src/service/standaloneServer.ts b/plugins/sonarqube-backend/src/service/standaloneServer.ts index 78f39b2680..06a8d4ffb2 100644 --- a/plugins/sonarqube-backend/src/service/standaloneServer.ts +++ b/plugins/sonarqube-backend/src/service/standaloneServer.ts @@ -14,10 +14,14 @@ * limitations under the License. */ -import { createServiceBuilder } from '@backstage/backend-common'; +import { + createServiceBuilder, + loadBackendConfig, +} from '@backstage/backend-common'; import { Server } from 'http'; import { Logger } from 'winston'; import { createRouter } from './router'; +import { DefaultSonarqubeInfoProvider } from './sonarqubeInfoProvider'; export interface ServerOptions { port: number; @@ -30,13 +34,15 @@ export async function startStandaloneServer( ): Promise { const logger = options.logger.child({ service: 'sonarqube-backend-backend' }); logger.debug('Starting application server...'); + const config = await loadBackendConfig({ logger, argv: process.argv }); const router = await createRouter({ logger, + sonarqubeInfoProvider: DefaultSonarqubeInfoProvider.fromConfig(config), }); let service = createServiceBuilder(module) .setPort(options.port) - .addRouter('/sonarqube-backend', router); + .addRouter('/sonarqube', router); if (options.enableCors) { service = service.enableCors({ origin: 'http://localhost:3000' }); }