diff --git a/backstage-plugin/plugins/open-dora/package.json b/backstage-plugin/plugins/open-dora/package.json index b1d1f87..803736b 100644 --- a/backstage-plugin/plugins/open-dora/package.json +++ b/backstage-plugin/plugins/open-dora/package.json @@ -49,7 +49,8 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "*", "cross-fetch": "^3.1.5", - "msw": "^1.0.0" + "msw": "^1.3.2", + "undici": "^5.27.2" }, "files": [ "dist", @@ -64,6 +65,14 @@ "lines": 100, "statements": 100 } - } + }, + "testEnvironmentOptions": { + "customExportConditions": [ + "" + ] + }, + "setupFiles": [ + "../testing/jest.polyfills.js" + ] } } 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 96f5776..249df57 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 @@ -1,38 +1,33 @@ +import type { EntityRelation } from '@backstage/catalog-model'; +import { ApiProvider } from '@backstage/core-app-api'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; +import { + MockConfigApi, + renderInTestApp, + TestApiRegistry, +} from '@backstage/test-utils'; +import { act, fireEvent, screen } from '@testing-library/react'; +import { rest } from 'msw'; import React from 'react'; +import { baseUrl, metricUrl } from '../../../testing/mswHandlers'; +import { + GroupDataService, + groupDataServiceApiRef, +} from '../../services/GroupDataService'; +import { server } from '../../setupTests'; import { DashboardComponent, EntityDashboardComponent, } from './DashboardComponent'; -import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; -import { groupDataServiceApiRef } from '../../services/GroupDataService'; -import { ApiProvider } from '@backstage/core-app-api'; -import { fireEvent, screen, act } from '@testing-library/react'; -import { MetricData } from '../../models/MetricData'; -import { EntityProvider } from '@backstage/plugin-catalog-react'; -import type { EntityRelation } from '@backstage/catalog-model'; -async function renderComponentWithApis( - component: JSX.Element, - mockData?: jest.Mock, -) { - const groupDataApiMock = { - retrieveMetricDataPoints: - mockData ?? - jest - .fn() - .mockResolvedValueOnce({ - aggregation: 'weekly', - dataPoints: [{ key: '10/23', value: 1.0 }], - }) - .mockResolvedValueOnce({ - aggregation: 'weekly', - dataPoints: [{ key: '11/23', value: 2.0 }], - }), - }; +async function renderComponentWithApis(component: JSX.Element) { + const mockConfig = new MockConfigApi({ + 'open-dora': { apiBaseUrl: baseUrl }, + }); const apiRegistry = TestApiRegistry.from([ groupDataServiceApiRef, - groupDataApiMock, + new GroupDataService({ configApi: mockConfig }), ]); return await renderInTestApp( @@ -41,8 +36,8 @@ async function renderComponentWithApis( } describe('DashboardComponent', () => { - function renderDashboardComponent(mockData?: jest.Mock) { - return renderComponentWithApis(, mockData); + function renderDashboardComponent() { + return renderComponentWithApis(); } it('should show a dropdown with the aggregation choices', async () => { @@ -62,172 +57,152 @@ describe('DashboardComponent', () => { expect(queryByText('OpenDORA (by Devoteam)')).not.toBeNull(); }); - it('should show a graph for deployment frequency data', async () => { - const { queryByText } = await renderDashboardComponent( - jest - .fn() - .mockResolvedValueOnce({ - aggregation: 'weekly', - dataPoints: [ - { key: 'count_first_key', value: 1.0 }, - { key: 'count_second_key', value: 1.0 }, - { key: 'count_third_key', value: 1.0 }, - ], - }) - .mockResolvedValueOnce({ - aggregation: 'weekly', - dataPoints: [ - { key: 'average_first_key', value: 2.0 }, - { key: 'average_second_key', value: 2.0 }, - { key: 'average_third_key', value: 2.0 }, - ], - }), + it('should show graphs for deployment frequency data', async () => { + server.use( + rest.get(metricUrl, (req, res, ctx) => { + const type = req.url.searchParams.get('type'); + return res( + ctx.json({ + aggregation: 'weekly', + dataPoints: [ + { key: `${type}_first_key`, value: 1.0 }, + { key: `${type}_second_key`, value: 1.0 }, + { key: `${type}_third_key`, value: 1.0 }, + ], + }), + ); + }), ); + const { queryByText } = await renderDashboardComponent(); expect(queryByText('Deployment Frequency')).not.toBeNull(); - expect(queryByText('count_first_key')).not.toBeNull(); - expect(queryByText('count_second_key')).not.toBeNull(); - expect(queryByText('count_third_key')).not.toBeNull(); + expect(queryByText('df_count_first_key')).not.toBeNull(); + expect(queryByText('df_count_second_key')).not.toBeNull(); + expect(queryByText('df_count_third_key')).not.toBeNull(); expect(queryByText('Deployment Frequency Average')).not.toBeNull(); - expect(queryByText('average_first_key')).not.toBeNull(); - expect(queryByText('average_second_key')).not.toBeNull(); - expect(queryByText('average_third_key')).not.toBeNull(); + expect(queryByText('df_average_first_key')).not.toBeNull(); + expect(queryByText('df_average_second_key')).not.toBeNull(); + expect(queryByText('df_average_third_key')).not.toBeNull(); }); it('should retrieve new data when the aggregation is changed', async () => { - const apiMock = jest - .fn() - .mockResolvedValueOnce({ - aggregation: 'weekly', - dataPoints: [ - { key: 'count_first_key', value: 1.0 }, - { key: 'count_second_key', value: 1.0 }, - { key: 'count_third_key', value: 1.0 }, - ], - }) - .mockResolvedValueOnce({ - aggregation: 'weekly', - dataPoints: [ - { key: 'average_first_key', value: 2.0 }, - { key: 'average_second_key', value: 2.0 }, - { key: 'average_third_key', value: 2.0 }, - ], - }) - .mockResolvedValueOnce({ - aggregation: 'monthly', - dataPoints: [ - { key: 'count_new_first_key', value: 1.0 }, - { key: 'count_new_second_key', value: 1.0 }, - { key: 'count_new_third_key', value: 1.0 }, - ], - }) - .mockResolvedValueOnce({ - aggregation: 'monthly', - dataPoints: [ - { key: 'average_new_first_key', value: 2.0 }, - { key: 'average_new_second_key', value: 2.0 }, - { key: 'average_new_third_key', value: 2.0 }, - ], - }); - const { queryByText, getByText } = await renderDashboardComponent(apiMock); - - expect(apiMock).toHaveBeenCalledTimes(2); - expect(apiMock).toHaveBeenCalledWith({ - type: 'df_count', - aggregation: 'weekly', - }); - expect(apiMock).toHaveBeenLastCalledWith({ - type: 'df_average', - aggregation: 'weekly', - }); + server.use( + rest.get(metricUrl, (req, res, ctx) => { + const params = req.url.searchParams; + const type = params.get('type'); + const aggregation = params.get('aggregation'); + + return res( + ctx.json({ + aggregation: aggregation, + dataPoints: [ + { key: `${aggregation}_${type}_first_key`, value: 1.0 }, + ], + }), + ); + }), + ); + const { queryByText, getByText } = await renderDashboardComponent(); - expect(queryByText('count_first_key')).not.toBeNull(); - expect(queryByText('count_second_key')).not.toBeNull(); - expect(queryByText('count_third_key')).not.toBeNull(); + expect(queryByText('weekly_df_count_first_key')).not.toBeNull(); + expect(queryByText('weekly_df_average_first_key')).not.toBeNull(); + + expect(queryByText('monthly_df_count_first_key')).toBeNull(); + expect(queryByText('monthly_df_average_first_key')).toBeNull(); fireEvent.mouseDown(getByText('Weekly')); await act(async () => { fireEvent.click(screen.getByText('Monthly')); }); - expect(apiMock).toHaveBeenCalledTimes(4); - expect(apiMock).toHaveBeenCalledWith({ - type: 'df_count', - aggregation: 'monthly', - }); - expect(apiMock).toHaveBeenLastCalledWith({ - type: 'df_average', - aggregation: 'monthly', - }); - expect(queryByText('count_first_key')).toBeNull(); - expect(queryByText('count_second_key')).toBeNull(); - expect(queryByText('count_third_key')).toBeNull(); + expect(queryByText('weekly_df_count_first_key')).toBeNull(); + expect(queryByText('weekly_df_average_first_key')).toBeNull(); - expect(queryByText('count_new_first_key')).not.toBeNull(); - expect(queryByText('count_new_second_key')).not.toBeNull(); - expect(queryByText('count_new_third_key')).not.toBeNull(); - - expect(queryByText('average_first_key')).toBeNull(); - expect(queryByText('average_second_key')).toBeNull(); - expect(queryByText('average_third_key')).toBeNull(); - - expect(queryByText('average_new_first_key')).not.toBeNull(); - expect(queryByText('average_new_second_key')).not.toBeNull(); - expect(queryByText('average_new_third_key')).not.toBeNull(); + expect(queryByText('monthly_df_count_first_key')).not.toBeNull(); + expect(queryByText('monthly_df_average_first_key')).not.toBeNull(); }); it('should show loading indicator when waiting on data to return', async () => { - jest.useFakeTimers(); - - const apiMock = jest - .fn() - .mockImplementationOnce(() => { - return new Promise(resolve => { - resolve({ - aggregation: 'monthly', - dataPoints: [{ key: 'count_data_key', value: 1.0 }], - }); - }); - }) - .mockImplementationOnce(() => { - return new Promise(resolve => { - setTimeout(() => { - resolve({ - aggregation: 'monthly', - dataPoints: [{ key: 'average_data_key', value: 1.0 }], - }); - }, 1000); - }); - }); - const { queryByText, queryByRole, findByRole } = - await renderDashboardComponent(apiMock); - - expect(await findByRole('progressbar')).not.toBeNull(); - expect(queryByText('average_data_key')).toBeNull(); + jest.useFakeTimers({ + legacyFakeTimers: true, + }); + + server.use( + rest.get(metricUrl, async (_, res, ctx) => { + await new Promise(resolve => setTimeout(resolve, 10000)); + return res( + ctx.json({ + aggregation: 'weekly', + dataPoints: [{ key: `first_key`, value: 1.0 }], + }), + ); + }), + ); + + const { queryByText, queryByRole, findAllByRole, queryAllByText } = + await renderDashboardComponent(); + + expect(await findAllByRole('progressbar')).toHaveLength(2); + expect(queryByText('first_key')).toBeNull(); await act(async () => { jest.runAllTimers(); }); expect(queryByRole('progressbar')).toBeNull(); - expect(queryByText('average_data_key')).not.toBeNull(); + expect(queryAllByText('first_key')).toHaveLength(2); }); it('should show the error returned from the service', async () => { - const { queryAllByText } = await renderDashboardComponent( - jest.fn().mockRejectedValue({ status: 500, message: 'server error' }), + server.use( + rest.get(metricUrl, (_, res, ctx) => { + return res(ctx.status(401)); + }), + ); + const { queryAllByText, getByText } = await renderDashboardComponent(); + expect(queryAllByText('Error: Unauthorized')).toHaveLength(2); + + server.use( + rest.get(metricUrl, (_, res) => { + return res.networkError('Host unreachable'); + }), ); - expect(queryAllByText('server error')).not.toBeNull(); - expect(queryAllByText('server error')).toHaveLength(2); + + // Trigger another request + fireEvent.mouseDown(getByText('Weekly')); + await act(async () => { + fireEvent.click(screen.getByText('Monthly')); + }); + + expect(queryAllByText('Error: Failed to fetch')).toHaveLength(2); }); }); describe('EntityDashboardComponent', () => { - function renderEntityDashboardComponent( - mockData?: jest.Mock, - relations?: EntityRelation[], - ) { + function renderEntityDashboardComponent(relations?: EntityRelation[]) { + server.use( + 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, + dataPoints: [ + { + key: `${project}_${team}_${aggregation}_${type}_first_key`, + value: 1.0, + }, + ], + }), + ); + }), + ); + return renderComponentWithApis( { > , - mockData, ); } it('should send component info to the service from the context', async () => { - const apiMock = jest.fn().mockResolvedValue({ - aggregation: 'weekly', - dataPoints: [{ key: 'first_key', value: 1.0 }], - }); - - await renderEntityDashboardComponent(apiMock, [ + const { queryByText } = await renderEntityDashboardComponent([ { targetRef: 'kind:namespace/owner-name', type: 'ownedBy' }, ]); - expect(apiMock).toHaveBeenCalledTimes(2); - - expect(apiMock).toHaveBeenLastCalledWith({ - type: 'df_average', - aggregation: 'weekly', - project: 'entity-name', - team: 'owner-name', - }); + expect( + queryByText('entity-name_owner-name_weekly_df_average_first_key'), + ).not.toBeNull(); }); it('should send component info without owner info', async () => { - const apiMock = jest.fn().mockResolvedValue({ - aggregation: 'weekly', - dataPoints: [{ key: 'first_key', value: 1.0 }], - }); - - await renderEntityDashboardComponent(apiMock); + const { queryByText } = await renderEntityDashboardComponent(); - expect(apiMock).toHaveBeenCalledTimes(2); - expect(apiMock).toHaveBeenCalledWith({ - type: 'df_count', - aggregation: 'weekly', - project: 'entity-name', - }); - - expect(apiMock).toHaveBeenLastCalledWith({ - type: 'df_average', - aggregation: 'weekly', - project: 'entity-name', - }); + expect( + queryByText('entity-name_null_weekly_df_average_first_key'), + ).not.toBeNull(); }); }); diff --git a/backstage-plugin/plugins/open-dora/src/services/GroupDataService.test.ts b/backstage-plugin/plugins/open-dora/src/services/GroupDataService.test.ts new file mode 100644 index 0000000..58efcf3 --- /dev/null +++ b/backstage-plugin/plugins/open-dora/src/services/GroupDataService.test.ts @@ -0,0 +1,121 @@ +import { MockConfigApi } from '@backstage/test-utils'; +import { rest } from 'msw'; +import { baseUrl, metricUrl } from '../../testing/mswHandlers'; +import { server } from '../setupTests'; +import { GroupDataService } from './GroupDataService'; + +function createService() { + server.use( + 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')); + }); +}); diff --git a/backstage-plugin/plugins/open-dora/src/services/GroupDataService.ts b/backstage-plugin/plugins/open-dora/src/services/GroupDataService.ts index cdef898..b5a7271 100644 --- a/backstage-plugin/plugins/open-dora/src/services/GroupDataService.ts +++ b/backstage-plugin/plugins/open-dora/src/services/GroupDataService.ts @@ -1,5 +1,3 @@ -// TODO: Add tests and remove ignore. -/* istanbul ignore file */ import { ConfigApi, createApiRef } from '@backstage/core-plugin-api'; import { MetricData } from '../models/MetricData'; @@ -17,6 +15,7 @@ export class GroupDataService { aggregation?: string; }) { const baseUrl = this.options.configApi.getString('open-dora.apiBaseUrl'); + const url = new URL(baseUrl); url.pathname = 'dora/api/metric'; for (const [key, value] of Object.entries(params)) { @@ -27,6 +26,19 @@ export class GroupDataService { const data = await fetch(url.toString(), { method: 'GET', }); - return (await data.json()) as MetricData; + + if (!data.ok) { + throw new Error(data.statusText); + } + + const response = await data.json(); + if ( + response.aggregation === undefined || + response.dataPoints === undefined + ) { + throw new Error('Unexpected response'); + } + + return response as MetricData; } } diff --git a/backstage-plugin/plugins/open-dora/src/setupTests.ts b/backstage-plugin/plugins/open-dora/src/setupTests.ts index 48c09b5..a7211d8 100644 --- a/backstage-plugin/plugins/open-dora/src/setupTests.ts +++ b/backstage-plugin/plugins/open-dora/src/setupTests.ts @@ -1,2 +1,10 @@ import '@testing-library/jest-dom'; import 'cross-fetch/polyfill'; +import { setupServer } from 'msw/node'; +import { handlers } from '../testing/mswHandlers'; + +export const server = setupServer(...handlers); + +beforeAll(() => server.listen()); +afterEach(() => server.resetHandlers()); +afterAll(() => server.close()); diff --git a/backstage-plugin/plugins/open-dora/testing/jest.polyfills.js b/backstage-plugin/plugins/open-dora/testing/jest.polyfills.js new file mode 100644 index 0000000..9598a44 --- /dev/null +++ b/backstage-plugin/plugins/open-dora/testing/jest.polyfills.js @@ -0,0 +1,30 @@ +// jest.polyfills.js +/** + * @note The block below contains polyfills for Node.js globals + * required for Jest to function when running JSDOM tests. + * These HAVE to be require's and HAVE to be in this exact + * order, since "undici" depends on the "TextEncoder" global API. + * + * Consider migrating to a more modern test runner if + * you don't want to deal with this. + */ + +const { TextDecoder, TextEncoder } = require('node:util'); + +Object.defineProperties(globalThis, { + TextDecoder: { value: TextDecoder }, + TextEncoder: { value: TextEncoder }, +}); + +const { Blob, File } = require('node:buffer'); +const { fetch, Headers, FormData, Request, Response } = require('undici'); + +Object.defineProperties(globalThis, { + fetch: { value: fetch, writable: true }, + Blob: { value: Blob }, + File: { value: File }, + Headers: { value: Headers }, + FormData: { value: FormData }, + Request: { value: Request }, + Response: { value: Response }, +}); diff --git a/backstage-plugin/plugins/open-dora/testing/mswHandlers.ts b/backstage-plugin/plugins/open-dora/testing/mswHandlers.ts new file mode 100644 index 0000000..f4df50b --- /dev/null +++ b/backstage-plugin/plugins/open-dora/testing/mswHandlers.ts @@ -0,0 +1,15 @@ +import { rest } from 'msw'; + +export const baseUrl = 'http://localhost:10666'; +export const metricUrl = `${baseUrl}/dora/api/metric`; + +export const handlers = [ + rest.get(metricUrl, (_, res, ctx) => { + return res( + ctx.json({ + aggregation: 'weekly', + dataPoints: [{ key: '10/23', value: 1.0 }], + }), + ); + }), +]; diff --git a/backstage-plugin/yarn.lock b/backstage-plugin/yarn.lock index 4416a39..a0f7882 100644 --- a/backstage-plugin/yarn.lock +++ b/backstage-plugin/yarn.lock @@ -2286,6 +2286,11 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.37.0.tgz#cf1b5fa24217fe007f6487a26d765274925efa7d" integrity sha512-x5vzdtOOGgFVDCUs81QRB2+liax8rFg3+7hqM+QhBG0/G3F1ZsoYl97UrqgHgQ9KKT7G6c4V+aTUCgu/n22v1A== +"@fastify/busboy@^2.0.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.0.tgz#0709e9f4cb252351c609c6e6d8d6779a8d25edff" + integrity sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA== + "@floating-ui/core@^1.4.1": version "1.4.1" resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.4.1.tgz#0d633f4b76052668afb932492ac452f7ebe97f17" @@ -3458,16 +3463,16 @@ "@types/set-cookie-parser" "^2.4.0" set-cookie-parser "^2.4.6" -"@mswjs/interceptors@^0.17.5": - version "0.17.9" - resolved "https://registry.yarnpkg.com/@mswjs/interceptors/-/interceptors-0.17.9.tgz#0096fc88fea63ee42e36836acae8f4ae33651c04" - integrity sha512-4LVGt03RobMH/7ZrbHqRxQrS9cc2uh+iNKSj8UWr8M26A2i793ju+csaB5zaqYltqJmA2jUq4VeYfKmVqvsXQg== +"@mswjs/interceptors@^0.17.10": + version "0.17.10" + resolved "https://registry.yarnpkg.com/@mswjs/interceptors/-/interceptors-0.17.10.tgz#857b41f30e2b92345ed9a4e2b1d0a08b8b6fcad4" + integrity sha512-N8x7eSLGcmUFNWZRxT1vsHvypzIRgQYdG0rJey/rZCy6zT/30qDt8Joj7FxzGNLSwXbeZqJOMqDurp7ra4hgbw== dependencies: "@open-draft/until" "^1.0.3" "@types/debug" "^4.1.7" "@xmldom/xmldom" "^0.8.3" debug "^4.3.3" - headers-polyfill "^3.1.0" + headers-polyfill "3.2.5" outvariant "^1.2.1" strict-event-emitter "^0.2.4" web-encoding "^1.1.5" @@ -4528,13 +4533,20 @@ resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== -"@types/debug@^4.0.0", "@types/debug@^4.1.7": +"@types/debug@^4.0.0": version "4.1.7" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82" integrity sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg== dependencies: "@types/ms" "*" +"@types/debug@^4.1.7": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" + integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== + dependencies: + "@types/ms" "*" + "@types/eslint-scope@^3.7.3": version "3.7.4" resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16" @@ -4652,9 +4664,9 @@ integrity sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA== "@types/js-levenshtein@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@types/js-levenshtein/-/js-levenshtein-1.1.1.tgz#ba05426a43f9e4e30b631941e0aa17bf0c890ed5" - integrity sha512-qC4bCqYGy1y/NP7dDVr7KJarn+PbX1nSpwA7JXdu0HxT3QYjO8MJ+cntENtHFVy2dRAyBV23OZ6MxsW1AM1L8g== + version "1.1.3" + resolved "https://registry.yarnpkg.com/@types/js-levenshtein/-/js-levenshtein-1.1.3.tgz#a6fd0bdc8255b274e5438e0bfb25f154492d1106" + integrity sha512-jd+Q+sD20Qfu9e2aEXogiO3vpOC1PYJOUdyN9gvs4Qrvkg4wF43L5OhqrPeokdv8TL0/mXoYfpkcoGZMNN2pkQ== "@types/jsdom@^20.0.0": version "20.0.1" @@ -4764,10 +4776,10 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== -"@types/react-dom@<18.0.0", "@types/react-dom@^17": - version "17.0.20" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.20.tgz#e0c8901469d732b36d8473b40b679ad899da1b53" - integrity sha512-4pzIjSxDueZZ90F52mU3aPoogkHIoSIDG+oQ+wQK7Cy2B9S+MvOqY0uEA/qawKz381qrEDkvpwyt8Bm31I8sbA== +"@types/react-dom@<18.0.0": + version "17.0.23" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.23.tgz#55b98df6b27595c8ca84e04e5b5df0f86bca7d24" + integrity sha512-lnJAZfMEDxfvELeeT24w4rnUYwpzUzQAOTfJQbWYnLcx8AEfz+fXJDCbowIBqNK/Bi4D6j8ovT8Qsda2OtDApA== dependencies: "@types/react" "^17" @@ -4868,9 +4880,9 @@ "@types/node" "*" "@types/set-cookie-parser@^2.4.0": - version "2.4.2" - resolved "https://registry.yarnpkg.com/@types/set-cookie-parser/-/set-cookie-parser-2.4.2.tgz#b6a955219b54151bfebd4521170723df5e13caad" - integrity sha512-fBZgytwhYAUkj/jC/FAV4RQ5EerRup1YQsXQCh8rZfiHkc4UahC192oH0smGwsXol3cL3A5oETuAHeQHmhXM4w== + version "2.4.6" + resolved "https://registry.yarnpkg.com/@types/set-cookie-parser/-/set-cookie-parser-2.4.6.tgz#13f6b345e8fc2ba6c9cbc079139eef4caf52e183" + integrity sha512-tjIRMxGztGfIbW2/d20MdJmAPZbabtdW051cKfU+nvZXUnKKifHbY2CyL/C0EGabUB8ahIRjanYzTqJUQR8TAQ== dependencies: "@types/node" "*" @@ -5237,9 +5249,9 @@ "@xtuc/long" "4.2.2" "@xmldom/xmldom@^0.8.3": - version "0.8.6" - resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.6.tgz#8a1524eb5bd5e965c1e3735476f0262469f71440" - integrity sha512-uRjjusqpoqfmRkTaNuLJ2VohVr67Q5YwDATW3VU7PfzTj6IRaihGrYI7zckGZjxQPBIp63nfvJbM+Yu5ICh0Bg== + version "0.8.10" + resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.10.tgz#a1337ca426aa61cef9fe15b5b28e340a72f6fa99" + integrity sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw== "@xobotyi/scrollbar-width@^1.9.5": version "1.9.5" @@ -6202,14 +6214,6 @@ chalk@2.4.2, chalk@^2.0.0: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad" - integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - chalk@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" @@ -8935,11 +8939,16 @@ graphql-tag@^2.10.3: dependencies: tslib "^2.1.0" -"graphql@^15.0.0 || ^16.0.0", graphql@^16.0.0: +graphql@^16.0.0: version "16.6.0" resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.6.0.tgz#c2dcffa4649db149f6282af726c8c83f1c7c5fdb" integrity sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw== +graphql@^16.8.1: + version "16.8.1" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07" + integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw== + gzip-size@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462" @@ -9081,10 +9090,10 @@ he@^1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== -headers-polyfill@^3.1.0, headers-polyfill@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/headers-polyfill/-/headers-polyfill-3.1.2.tgz#9a4dcb545c5b95d9569592ef7ec0708aab763fbe" - integrity sha512-tWCK4biJ6hcLqTviLXVR9DTRfYGQMXEIUj3gwJ2rZ5wO/at3XtkI4g8mCvFdUF9l1KMBNCfmNAdnahm1cgavQA== +headers-polyfill@3.2.5: + version "3.2.5" + resolved "https://registry.yarnpkg.com/headers-polyfill/-/headers-polyfill-3.2.5.tgz#6e67d392c9d113d37448fe45014e0afdd168faed" + integrity sha512-tUCGvt191vNSQgttSyJoibR+VO+I6+iCHIUdhzEMJKE+EAL8BwCN7fUOZlY4ofOelNHsK+gEjxB/B+9N3EWtdA== highlight.js@^10.4.1, highlight.js@~10.7.0: version "10.7.3" @@ -11832,21 +11841,21 @@ ms@2.1.3, ms@^2.0.0, ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -msw@^1.0.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/msw/-/msw-1.2.1.tgz#9dd347583eeba5e5c7f33b54be5600a899dc61bd" - integrity sha512-bF7qWJQSmKn6bwGYVPXOxhexTCGD5oJSZg8yt8IBClxvo3Dx/1W0zqE1nX9BSWmzRsCKWfeGWcB/vpqV6aclpw== +msw@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/msw/-/msw-1.3.2.tgz#35e0271293e893fc3c55116e90aad5d955c66899" + integrity sha512-wKLhFPR+NitYTkQl5047pia0reNGgf0P6a1eTnA5aNlripmiz0sabMvvHcicE8kQ3/gZcI0YiPFWmYfowfm3lA== dependencies: "@mswjs/cookies" "^0.2.2" - "@mswjs/interceptors" "^0.17.5" + "@mswjs/interceptors" "^0.17.10" "@open-draft/until" "^1.0.3" "@types/cookie" "^0.4.1" "@types/js-levenshtein" "^1.1.1" - chalk "4.1.1" + chalk "^4.1.1" chokidar "^3.4.2" cookie "^0.4.2" - graphql "^15.0.0 || ^16.0.0" - headers-polyfill "^3.1.2" + graphql "^16.8.1" + headers-polyfill "3.2.5" inquirer "^8.2.0" is-node-process "^1.2.0" js-levenshtein "^1.1.6" @@ -15504,6 +15513,13 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +undici@^5.27.2: + version "5.27.2" + resolved "https://registry.yarnpkg.com/undici/-/undici-5.27.2.tgz#a270c563aea5b46cc0df2550523638c95c5d4411" + integrity sha512-iS857PdOEy/y3wlM3yRp+6SNQQ6xU0mmZcwRSriqk+et/cwWAtwmIGf6WkoDN2EK/AMdCO/dfXzIwi+rFMrjjQ== + dependencies: + "@fastify/busboy" "^2.0.0" + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc"