diff --git a/backstage-plugin/plugins/open-dora/dev/index.tsx b/backstage-plugin/plugins/open-dora/dev/index.tsx index 6294fdb..3c1c562 100644 --- a/backstage-plugin/plugins/open-dora/dev/index.tsx +++ b/backstage-plugin/plugins/open-dora/dev/index.tsx @@ -1,11 +1,11 @@ import { createDevApp } from '@backstage/dev-utils'; +import { MockConfigApi } from '@backstage/test-utils'; import React from 'react'; import { openDoraPlugin, OpenDoraPluginPage } from '../src'; -import { MockConfigApi } from '@backstage/test-utils'; import { - GroupDataService, - groupDataServiceApiRef, -} from '../src/services/GroupDataService'; + DoraDataService, + doraDataServiceApiRef, +} from '../src/services/DoraDataService'; const mockConfig = new MockConfigApi({ 'open-dora': { @@ -16,9 +16,9 @@ const mockConfig = new MockConfigApi({ createDevApp() .registerPlugin(openDoraPlugin) .registerApi({ - api: groupDataServiceApiRef, + api: doraDataServiceApiRef, deps: {}, - factory: () => new GroupDataService({ configApi: mockConfig }), + factory: () => new DoraDataService({ configApi: mockConfig }), }) .addPage({ element: , diff --git a/backstage-plugin/plugins/open-dora/src/components/DashboardComponent/DashboardComponent.test.tsx b/backstage-plugin/plugins/open-dora/src/components/DashboardComponent/DashboardComponent.test.tsx index 1bf5dbb..0415afa 100644 --- a/backstage-plugin/plugins/open-dora/src/components/DashboardComponent/DashboardComponent.test.tsx +++ b/backstage-plugin/plugins/open-dora/src/components/DashboardComponent/DashboardComponent.test.tsx @@ -11,9 +11,9 @@ import { rest } from 'msw'; import React from 'react'; import { baseUrl, metricUrl } from '../../../testing/mswHandlers'; import { - GroupDataService, - groupDataServiceApiRef, -} from '../../services/GroupDataService'; + DoraDataService, + doraDataServiceApiRef, +} from '../../services/DoraDataService'; import { server } from '../../setupTests'; import { DashboardComponent, @@ -26,8 +26,8 @@ async function renderComponentWithApis(component: JSX.Element) { }); const apiRegistry = TestApiRegistry.from([ - groupDataServiceApiRef, - new GroupDataService({ configApi: mockConfig }), + doraDataServiceApiRef, + new DoraDataService({ configApi: mockConfig }), ]); return await renderInTestApp( diff --git a/backstage-plugin/plugins/open-dora/src/hooks/MetricBenchmarkHook.ts b/backstage-plugin/plugins/open-dora/src/hooks/MetricBenchmarkHook.ts index b3679d5..23a382a 100644 --- a/backstage-plugin/plugins/open-dora/src/hooks/MetricBenchmarkHook.ts +++ b/backstage-plugin/plugins/open-dora/src/hooks/MetricBenchmarkHook.ts @@ -1,18 +1,18 @@ import { useApi } from '@backstage/core-plugin-api'; import { useEffect, useState } from 'react'; import { dfBenchmarkKey } from '../models/DfBenchmarkData'; -import { groupDataServiceApiRef } from '../services/GroupDataService'; +import { doraDataServiceApiRef } from '../services/DoraDataService'; export const useMetricBenchmark = (type: string) => { - const groupDataService = useApi(groupDataServiceApiRef); + const doraDataService = useApi(doraDataServiceApiRef); const [benchmark, setDfBenchmark] = useState(); const [error, setError] = useState(); useEffect(() => { - groupDataService.retrieveBenchmarkData({ type: type }).then(response => { + doraDataService.retrieveBenchmarkData({ type: type }).then(response => { setDfBenchmark(response.key); }, setError); - }, [groupDataService, type]); + }, [doraDataService, type]); return { error: error, benchmark: benchmark }; }; diff --git a/backstage-plugin/plugins/open-dora/src/hooks/MetricDataHook.ts b/backstage-plugin/plugins/open-dora/src/hooks/MetricDataHook.ts index 99c2a5d..ba4de67 100644 --- a/backstage-plugin/plugins/open-dora/src/hooks/MetricDataHook.ts +++ b/backstage-plugin/plugins/open-dora/src/hooks/MetricDataHook.ts @@ -1,17 +1,17 @@ import { useApi } from '@backstage/core-plugin-api'; import { useContext, useEffect, useState } from 'react'; import { MetricData } from '../models/MetricData'; -import { groupDataServiceApiRef } from '../services/GroupDataService'; +import { doraDataServiceApiRef } from '../services/DoraDataService'; import { MetricContext } from '../services/MetricContext'; export const useMetricData = (type: string) => { - const groupDataService = useApi(groupDataServiceApiRef); + const doraDataService = useApi(doraDataServiceApiRef); const [chartData, setChartData] = useState(); const [error, setError] = useState(); const { aggregation, team, project } = useContext(MetricContext); useEffect(() => { - groupDataService + doraDataService .retrieveMetricDataPoints({ type: type, team: team, @@ -25,7 +25,7 @@ export const useMetricData = (type: string) => { setError(new Error('No data found')); } }, setError); - }, [aggregation, team, project, groupDataService, type]); + }, [aggregation, team, project, doraDataService, type]); return { error: error, chartData: chartData }; }; diff --git a/backstage-plugin/plugins/open-dora/src/plugin.ts b/backstage-plugin/plugins/open-dora/src/plugin.ts index 014ab1a..21acf70 100644 --- a/backstage-plugin/plugins/open-dora/src/plugin.ts +++ b/backstage-plugin/plugins/open-dora/src/plugin.ts @@ -6,9 +6,9 @@ import { } from '@backstage/core-plugin-api'; import { rootRouteRef } from './routes'; import { - GroupDataService, - groupDataServiceApiRef, -} from './services/GroupDataService'; + DoraDataService, + doraDataServiceApiRef, +} from './services/DoraDataService'; export const openDoraPlugin = createPlugin({ id: 'opendora', @@ -17,9 +17,9 @@ export const openDoraPlugin = createPlugin({ }, apis: [ createApiFactory({ - api: groupDataServiceApiRef, + api: doraDataServiceApiRef, deps: { configApi: configApiRef }, - factory: ({ configApi }) => new GroupDataService({ configApi }), + factory: ({ configApi }) => new DoraDataService({ configApi }), }), ], }); diff --git a/backstage-plugin/plugins/open-dora/src/services/DoraDataService.test.ts b/backstage-plugin/plugins/open-dora/src/services/DoraDataService.test.ts new file mode 100644 index 0000000..02236fc --- /dev/null +++ b/backstage-plugin/plugins/open-dora/src/services/DoraDataService.test.ts @@ -0,0 +1,201 @@ +import { MockConfigApi } from '@backstage/test-utils'; +import { rest } from 'msw'; +import { baseUrl, benchmarkUrl, metricUrl } from '../../testing/mswHandlers'; +import { server } from '../setupTests'; +import { DoraDataService } from './DoraDataService'; + +function createService() { + server.use( + rest.get(benchmarkUrl, (req, res, ctx) => { + const params = req.url.searchParams; + const type = params.get('type'); + + switch (type) { + case 'df': { + return res( + ctx.json({ + key: 'lt-6month', + }), + ); + } + default: { + return res(ctx.status(400)); + } + } + }), + rest.get(metricUrl, (req, res, ctx) => { + const params = req.url.searchParams; + const type = params.get('type'); + const aggregation = params.get('aggregation'); + const project = params.get('project'); + const team = params.get('team'); + + return res( + ctx.json({ + aggregation: aggregation || 'weekly', + dataPoints: [ + { + key: `${project}_${team}_${aggregation}_${type}_first_key`, + value: 2.3, + }, + ], + }), + ); + }), + ); + const mockConfig = new MockConfigApi({ + 'open-dora': { apiBaseUrl: baseUrl }, + }); + + return new DoraDataService({ configApi: mockConfig }); +} + +describe('DoraDataService', () => { + describe('retriveMetricDataPoints', () => { + it('should return data from the server', async () => { + const service = createService(); + + expect( + await service.retrieveMetricDataPoints({ type: 'df_count' }), + ).toEqual({ + aggregation: 'weekly', + dataPoints: [{ key: 'null_null_null_df_count_first_key', value: 2.3 }], + }); + }); + + it('should use provided details in the query parameters', async () => { + const service = createService(); + + expect( + await service.retrieveMetricDataPoints({ + type: 'df_count', + aggregation: 'monthly', + project: 'project1', + team: 'team1', + }), + ).toEqual({ + aggregation: 'monthly', + dataPoints: [ + { key: 'project1_team1_monthly_df_count_first_key', value: 2.3 }, + ], + }); + }); + + it('should throw an error if the response does not contain metric data', async () => { + const service = createService(); + + server.use( + rest.get(metricUrl, (_, res, ctx) => { + return res(ctx.json({ other: 'data' })); + }), + ); + await expect( + service.retrieveMetricDataPoints({ + type: 'df_count', + }), + ).rejects.toEqual(new Error('Unexpected response')); + }); + + it('should throw an error when the server is unreachable', async () => { + const service = createService(); + + server.use( + rest.get(metricUrl, (_, res) => { + return res.networkError('Host unreachable'); + }), + ); + await expect( + service.retrieveMetricDataPoints({ + type: 'df_count', + }), + ).rejects.toEqual(new Error('Failed to fetch')); + }); + + it('should throw an error when the server returns a non-ok status', async () => { + const service = createService(); + + server.use( + rest.get(metricUrl, (_, res, ctx) => { + return res(ctx.status(401)); + }), + ); + await expect( + service.retrieveMetricDataPoints({ + type: 'df_count', + }), + ).rejects.toEqual(new Error('Unauthorized')); + + server.use( + rest.get(metricUrl, (_, res, ctx) => { + return res(ctx.status(500)); + }), + ); + await expect( + service.retrieveMetricDataPoints({ + type: 'df_count', + }), + ).rejects.toEqual(new Error('Internal Server Error')); + }); + }); + + describe('retrieveBenchmarkData', () => { + it('should return deployment frequency overall data from the server', async () => { + const service = createService(); + + expect(await service.retrieveBenchmarkData({ type: 'df' })).toEqual({ + key: 'lt-6month', + }); + }); + + it('should throw an error if the response does not contain metric data', async () => { + const service = createService(); + + server.use( + rest.get(benchmarkUrl, (_, res, ctx) => { + return res(ctx.json({ other: 'data' })); + }), + ); + await expect( + service.retrieveBenchmarkData({ + type: 'df', + }), + ).rejects.toEqual(new Error('Unexpected response')); + }); + + it('should send the params in the request', async () => { + const service = createService(); + + await expect( + service.retrieveBenchmarkData({ + type: 'invalid_type', + }), + ).rejects.toEqual(new Error('Bad Request')); + }); + + it('should throw an error when the server returns a non-ok status', async () => { + const service = createService(); + + server.use( + rest.get(benchmarkUrl, (_, res, ctx) => { + return res(ctx.status(401)); + }), + ); + await expect( + service.retrieveBenchmarkData({ + type: 'df', + }), + ).rejects.toEqual(new Error('Unauthorized')); + + server.use( + rest.get(benchmarkUrl, (_, res, ctx) => { + return res(ctx.status(500)); + }), + ); + await expect( + service.retrieveBenchmarkData({ + type: 'df', + }), + ).rejects.toEqual(new Error('Internal Server Error')); + }); + }); +}); diff --git a/backstage-plugin/plugins/open-dora/src/services/GroupDataService.ts b/backstage-plugin/plugins/open-dora/src/services/DoraDataService.ts similarity index 62% rename from backstage-plugin/plugins/open-dora/src/services/GroupDataService.ts rename to backstage-plugin/plugins/open-dora/src/services/DoraDataService.ts index 15b6496..d4fd5c1 100644 --- a/backstage-plugin/plugins/open-dora/src/services/GroupDataService.ts +++ b/backstage-plugin/plugins/open-dora/src/services/DoraDataService.ts @@ -1,24 +1,19 @@ import { ConfigApi, createApiRef } from '@backstage/core-plugin-api'; -import { MetricData } from '../models/MetricData'; import { dfBenchmarkData } from '../models/DfBenchmarkData'; +import { MetricData } from '../models/MetricData'; -export const groupDataServiceApiRef = createApiRef({ - id: 'plugin.open-dora.group-data', +export const doraDataServiceApiRef = createApiRef({ + id: 'plugin.open-dora.data-service', }); -export class GroupDataService { +export class DoraDataService { constructor(private options: { configApi: ConfigApi }) {} - async retrieveMetricDataPoints(params: { - type: string; - team?: string; - project?: string; - aggregation?: string; - }) { + private async retrieveData(params: Record, path: string) { const baseUrl = this.options.configApi.getString('open-dora.apiBaseUrl'); const url = new URL(baseUrl); - url.pathname = 'dora/api/metric'; + url.pathname = path; for (const [key, value] of Object.entries(params)) { if (value) { url.searchParams.append(key, value); @@ -32,7 +27,16 @@ export class GroupDataService { throw new Error(data.statusText); } - const response = await data.json(); + return await data.json(); + } + + async retrieveMetricDataPoints(params: { + type: string; + team?: string; + project?: string; + aggregation?: string; + }) { + const response = await this.retrieveData(params, 'dora/api/metric'); if ( response.aggregation === undefined || response.dataPoints === undefined @@ -44,24 +48,8 @@ export class GroupDataService { } async retrieveBenchmarkData(params: { type: string }) { - const baseUrl = this.options.configApi.getString('open-dora.apiBaseUrl'); - - const url = new URL(baseUrl); - url.pathname = 'dora/api/benchmark'; - for (const [key, value] of Object.entries(params)) { - if (value) { - url.searchParams.append(key, value); - } - } - const data = await fetch(url.toString(), { - method: 'GET', - }); - - if (!data.ok) { - throw new Error(data.statusText); - } + const response = await this.retrieveData(params, 'dora/api/benchmark'); - const response = await data.json(); if (response.key === undefined) { throw new Error('Unexpected response'); } diff --git a/backstage-plugin/plugins/open-dora/src/services/GroupDataService.test.ts b/backstage-plugin/plugins/open-dora/src/services/GroupDataService.test.ts deleted file mode 100644 index 2e3a67a..0000000 --- a/backstage-plugin/plugins/open-dora/src/services/GroupDataService.test.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { MockConfigApi } from '@backstage/test-utils'; -import { rest } from 'msw'; -import { baseUrl, benchmarkUrl, metricUrl } from '../../testing/mswHandlers'; -import { server } from '../setupTests'; -import { GroupDataService } from './GroupDataService'; - -function createService() { - server.use( - rest.get(benchmarkUrl, (req, res, ctx) => { - const params = req.url.searchParams; - const type = params.get('type'); - - switch (type) { - case 'df': { - return res( - ctx.json({ - key: 'lt-6month', - }), - ); - } - default: { - return res(ctx.status(400)); - } - } - }), - rest.get(metricUrl, (req, res, ctx) => { - const params = req.url.searchParams; - const type = params.get('type'); - const aggregation = params.get('aggregation'); - const project = params.get('project'); - const team = params.get('team'); - - return res( - ctx.json({ - aggregation: aggregation || 'weekly', - dataPoints: [ - { - key: `${project}_${team}_${aggregation}_${type}_first_key`, - value: 2.3, - }, - ], - }), - ); - }), - ); - const mockConfig = new MockConfigApi({ - 'open-dora': { apiBaseUrl: baseUrl }, - }); - - return new GroupDataService({ configApi: mockConfig }); -} - -describe('GroupDataService', () => { - it('should return data from the server', async () => { - const service = createService(); - - expect( - await service.retrieveMetricDataPoints({ type: 'df_count' }), - ).toEqual({ - aggregation: 'weekly', - dataPoints: [{ key: 'null_null_null_df_count_first_key', value: 2.3 }], - }); - }); - - it('should use provided details in the query parameters', async () => { - const service = createService(); - - expect( - await service.retrieveMetricDataPoints({ - type: 'df_count', - aggregation: 'monthly', - project: 'project1', - team: 'team1', - }), - ).toEqual({ - aggregation: 'monthly', - dataPoints: [ - { key: 'project1_team1_monthly_df_count_first_key', value: 2.3 }, - ], - }); - }); - - it('should throw an error if the response does not contain metric data', async () => { - const service = createService(); - - server.use( - rest.get(metricUrl, (_, res, ctx) => { - return res(ctx.json({ other: 'data' })); - }), - ); - await expect( - service.retrieveMetricDataPoints({ - type: 'df_count', - }), - ).rejects.toEqual(new Error('Unexpected response')); - }); - - it('should throw an error when the server is unreachable', async () => { - const service = createService(); - - server.use( - rest.get(metricUrl, (_, res) => { - return res.networkError('Host unreachable'); - }), - ); - await expect( - service.retrieveMetricDataPoints({ - type: 'df_count', - }), - ).rejects.toEqual(new Error('Failed to fetch')); - }); - - it('should throw an error when the server returns a non-ok status', async () => { - const service = createService(); - - server.use( - rest.get(metricUrl, (_, res, ctx) => { - return res(ctx.status(401)); - }), - ); - await expect( - service.retrieveMetricDataPoints({ - type: 'df_count', - }), - ).rejects.toEqual(new Error('Unauthorized')); - - server.use( - rest.get(metricUrl, (_, res, ctx) => { - return res(ctx.status(500)); - }), - ); - await expect( - service.retrieveMetricDataPoints({ - type: 'df_count', - }), - ).rejects.toEqual(new Error('Internal Server Error')); - }); -}); - -describe('BenchmarkService', () => { - it('should return deployment frequency overall data from the server', async () => { - const service = createService(); - - expect(await service.retrieveBenchmarkData({ type: 'df' })).toEqual({ - key: 'lt-6month', - }); - }); - - it('should throw an error if the response does not contain metric data', async () => { - const service = createService(); - - server.use( - rest.get(benchmarkUrl, (_, res, ctx) => { - return res(ctx.json({ other: 'data' })); - }), - ); - await expect( - service.retrieveBenchmarkData({ - type: 'df', - }), - ).rejects.toEqual(new Error('Unexpected response')); - }); - - it('should return 404 for invalid types', async () => { - const service = createService(); - - await expect( - service.retrieveBenchmarkData({ - type: 'invalid_type', - }), - ).rejects.toEqual(new Error('Bad Request')); - }); - - it('should throw an error when the server returns a non-ok status', async () => { - const service = createService(); - - server.use( - rest.get(benchmarkUrl, (_, res, ctx) => { - return res(ctx.status(401)); - }), - ); - await expect( - service.retrieveBenchmarkData({ - type: 'df', - }), - ).rejects.toEqual(new Error('Unauthorized')); - - server.use( - rest.get(benchmarkUrl, (_, res, ctx) => { - return res(ctx.status(500)); - }), - ); - await expect( - service.retrieveBenchmarkData({ - type: 'df', - }), - ).rejects.toEqual(new Error('Internal Server Error')); - }); -});