From a5e1fb1304df4f9db66979f0bb626c4513703843 Mon Sep 17 00:00:00 2001 From: Kyle Watson Date: Mon, 20 Nov 2023 10:56:32 +0100 Subject: [PATCH] Chore/#71 refactor tests using msw (#107) * test(backstage-plugin): Add msw and refactor tests Add msw to mock backend and avoid mocking the service. Simplify some test cases by checking for only the needed elements. Add error handling to the data service. Addresses #71 * test(backstage-plugin): Add data service tests Use msw to add group data service tests and remove the ignore comment.a Addresses #71 * test(backstage-plugin): Fix linting test files Move test setup files out of source folder to avoid needing dependencies instead of dev dependencies when compiling typescript. Addresses #71 * test(backstage-plugin): Downgrade msw Downgrade msw to be inline with backstage setup and make it work with the tsconfig. Addresses #71 * test(backstage-plugin): Add consts for urls Add reusable consts for testing urls to mock with msw and supply in mock config Addresses #71 * test(backstage-plugin): Change value used in tests So that we can check for the exact decimal used from the response. Should help make the test a bit clearer. Addresses #71 --- .../plugins/open-dora/package.json | 13 +- .../DashboardComponent.test.tsx | 338 ++++++++---------- .../src/services/GroupDataService.test.ts | 121 +++++++ .../src/services/GroupDataService.ts | 18 +- .../plugins/open-dora/src/setupTests.ts | 8 + .../open-dora/testing/jest.polyfills.js | 30 ++ .../plugins/open-dora/testing/mswHandlers.ts | 15 + backstage-plugin/yarn.lock | 96 ++--- 8 files changed, 400 insertions(+), 239 deletions(-) create mode 100644 backstage-plugin/plugins/open-dora/src/services/GroupDataService.test.ts create mode 100644 backstage-plugin/plugins/open-dora/testing/jest.polyfills.js create mode 100644 backstage-plugin/plugins/open-dora/testing/mswHandlers.ts 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"