diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index 7e42a26d9f..b510e9cbbe 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -49,6 +49,7 @@ "react-router-dom": "6.0.0-beta.0 || ^6.3.0" }, "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/core-app-api": "workspace:^", "@backstage/dev-utils": "workspace:^", @@ -60,7 +61,7 @@ "@types/node": "^16.11.26", "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5", - "msw": "^1.0.0" + "msw": "^1.2.3" }, "files": [ "dist" diff --git a/plugins/newrelic/src/api/index.test.ts b/plugins/newrelic/src/api/index.test.ts index 94bdc8ff94..f45482be87 100644 --- a/plugins/newrelic/src/api/index.test.ts +++ b/plugins/newrelic/src/api/index.test.ts @@ -14,15 +14,24 @@ * limitations under the License. */ -import { NewRelicClient } from '.'; -import { FetchApi } from '@backstage/core-plugin-api'; +import { NewRelicApplication, NewRelicClient } from '.'; import { DiscoveryApi } from '@backstage/core-plugin-api'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { MockFetchApi, setupRequestMockHandlers } from '@backstage/test-utils'; beforeEach(() => { jest.resetAllMocks(); }); describe('NewRelicClient', () => { + const server = setupServer(); + setupRequestMockHandlers(server); + + beforeEach(() => { + server.resetHandlers(); + }); + test.each([ ['https://test.test/BASEPATH/apm/api/applications.json', '/BASEPATH'], ['https://test.test/BASEPATH2/apm/api/applications.json', '/BASEPATH2'], @@ -31,17 +40,18 @@ describe('NewRelicClient', () => { ])( 'It correctly forms the request url (%p) when proxyPathBase is %p', async (expectedUrl, basePathOverride) => { + server.use( + rest.get(expectedUrl, (_, res, ctx) => + res(ctx.status(200), ctx.json({ applications: [] })), + ), + ); + const mockedDiscoveryApi: DiscoveryApi = { getBaseUrl: jest.fn().mockResolvedValueOnce('https://test.test'), }; - const mockedFetchApi: FetchApi = { - fetch: jest.fn().mockResolvedValueOnce({ - ok: true, - json: async () => [], - headers: new Map(), - }), - }; + const mockedFetchApi = new MockFetchApi(); + const fetchSpy = jest.spyOn(mockedFetchApi, 'fetch'); const client = new NewRelicClient({ discoveryApi: mockedDiscoveryApi, @@ -50,7 +60,7 @@ describe('NewRelicClient', () => { }); await client.getApplications(); - expect(mockedFetchApi.fetch).toHaveBeenCalledWith(expectedUrl); + expect(fetchSpy).toHaveBeenCalledWith(expectedUrl); }, ); @@ -59,7 +69,7 @@ describe('NewRelicClient', () => { getBaseUrl: jest.fn().mockResolvedValueOnce('https://test.test'), }; - const mockedApplicationOne = { + const mockedApplicationOne: NewRelicApplication = { id: 1, application_summary: { apdex_score: 0, @@ -81,8 +91,16 @@ describe('NewRelicClient', () => { }, }; - const mockedApplicationTwo = { + const mockedApplicationTwo: NewRelicApplication = { id: 2, + application_summary: { + apdex_score: -900, + error_rate: 0, + host_count: 0, + instance_count: 0, + response_time: 0, + throughput: 0, + }, name: 'Testing Application #2', language: 'en-us', health_status: 'Working', @@ -95,7 +113,7 @@ describe('NewRelicClient', () => { }, }; - const mockedApplicationThree = { + const mockedApplicationThree: NewRelicApplication = { id: 3, application_summary: { apdex_score: -900, @@ -117,45 +135,65 @@ describe('NewRelicClient', () => { }, }; - const mockedFetchApi: FetchApi = { - fetch: jest - .fn() - .mockResolvedValueOnce({ - ok: true, - json: async () => ({ applications: [mockedApplicationOne] }), - headers: new Map([ - [ - 'link', - '; rel="next", ; rel="first"', - ], - ['otherheader', 'otherValue'], - ]), - }) - .mockResolvedValueOnce({ - ok: true, - json: async () => ({ applications: [] }), - headers: new Map([ - ['link', '; rel="next",'], - ]), - }) - .mockResolvedValueOnce({ - ok: true, - json: async () => ({ - applications: [mockedApplicationTwo, mockedApplicationThree], - }), - headers: new Map([ - [ - 'link', - '; rel="first", ; rel="next"', - ], - ]), - }), - }; + const queryToRequestData = new Map< + string | null, + { link?: string | string[]; apps: NewRelicApplication[] } + >([ + [ + null, + { + link: [ + '; rel="next"', + '; rel="bad"', + ], + apps: [mockedApplicationOne], + }, + ], + [ + '2', + { + link: '; rel="next",', + apps: [], + }, + ], + [ + '3', + { + apps: [mockedApplicationTwo, mockedApplicationThree], + }, + ], + ]); + + const mockedFetchApi = new MockFetchApi(); + const fetchSpy = jest.spyOn(mockedFetchApi, 'fetch'); + + server.use( + rest.get( + 'https://test.test/newrelic/apm/api/applications.json', + (req, res, ctx) => { + const nextPageNumber = req.url.searchParams.get('page'); + const requestData = queryToRequestData.get(nextPageNumber) ?? { + apps: [], + }; + + const { link, apps: applications } = requestData; + const statusTransform = ctx.status(200); + const responseBody = ctx.json({ applications }); + + if (!!link) { + return res(statusTransform, ctx.set({ link }), responseBody); + } + + return res(statusTransform, responseBody); + }, + ), + ); const client = new NewRelicClient({ discoveryApi: mockedDiscoveryApi, fetchApi: mockedFetchApi, }); + const actual = await client.getApplications(); const expected = { applications: [ @@ -165,14 +203,14 @@ describe('NewRelicClient', () => { ], }; - expect(mockedFetchApi.fetch).toHaveBeenCalledTimes(3); - expect(mockedFetchApi.fetch).toHaveBeenCalledWith( + expect(fetchSpy).toHaveBeenCalledTimes(3); + expect(fetchSpy).toHaveBeenCalledWith( 'https://test.test/newrelic/apm/api/applications.json', ); - expect(mockedFetchApi.fetch).toHaveBeenCalledWith( + expect(fetchSpy).toHaveBeenCalledWith( 'https://test.test/newrelic/apm/api/applications.json?page=2', ); - expect(mockedFetchApi.fetch).toHaveBeenCalledWith( + expect(fetchSpy).toHaveBeenCalledWith( 'https://test.test/newrelic/apm/api/applications.json?page=3', ); expect(actual).toStrictEqual(expected); @@ -185,19 +223,28 @@ describe('NewRelicClient', () => { getBaseUrl: jest.fn().mockResolvedValueOnce('https://test.test'), }; - const mockedFetchApi: FetchApi = { - fetch: jest.fn().mockResolvedValueOnce({ - ok: true, - json: async () => ({ applications: [] }), - headers: new Map([ - [ - linkHeaderName, - '; rel="next", ; rel="next"', - ], - ['otherheader', 'otherValue'], - ]), + const mockedFetchApi = new MockFetchApi(); + const fetchSpy = jest.spyOn(mockedFetchApi, 'fetch'); + + server.use( + rest.get( + 'https://test.test/newrelic/apm/api/applications.json', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set( + linkHeaderName, + '; rel="next"', + ), + ctx.json({ applications: [] }), + ), + ), + rest.get('https://test.test/badroute', () => { + throw new Error( + 'NewRelicClient attempted to paginate when it should not have', + ); }), - }; + ); const client = new NewRelicClient({ discoveryApi: mockedDiscoveryApi, @@ -205,8 +252,8 @@ describe('NewRelicClient', () => { }); await client.getApplications(); - expect(mockedFetchApi.fetch).toHaveBeenCalledTimes(1); - expect(mockedFetchApi.fetch).toHaveBeenCalledWith( + expect(fetchSpy).toHaveBeenCalledTimes(1); + expect(fetchSpy).toHaveBeenCalledWith( 'https://test.test/newrelic/apm/api/applications.json', ); }, @@ -228,16 +275,25 @@ describe('NewRelicClient', () => { getBaseUrl: jest.fn().mockResolvedValueOnce('https://test.test'), }; - const mockedFetchApi: FetchApi = { - fetch: jest.fn().mockResolvedValueOnce({ - ok: true, - json: async () => ({ applications: [] }), - headers: new Map([ - ['Link', linkHeaderValue], - ['otherheader', 'otherValue'], - ]), + const mockedFetchApi = new MockFetchApi(); + const fetchSpy = jest.spyOn(mockedFetchApi, 'fetch'); + + server.use( + rest.get( + 'https://test.test/newrelic/apm/api/applications.json', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('link', linkHeaderValue), + ctx.json({ applications: [] }), + ), + ), + rest.get('https://test.test/badroute', () => { + throw new Error( + 'NewRelicClient attempted to paginate when it should not have', + ); }), - }; + ); const client = new NewRelicClient({ discoveryApi: mockedDiscoveryApi, @@ -245,45 +301,33 @@ describe('NewRelicClient', () => { }); await client.getApplications(); - expect(mockedFetchApi.fetch).toHaveBeenCalledTimes(1); - expect(mockedFetchApi.fetch).toHaveBeenCalledWith( + expect(fetchSpy).toHaveBeenCalledTimes(1); + expect(fetchSpy).toHaveBeenCalledWith( 'https://test.test/newrelic/apm/api/applications.json', ); }, ); test.each([ - [ - { - ok: false, - statusText: 'statusText', - json: async () => ({ error: { title: 'TESTING' } }), - }, - ], - [ - { - ok: false, - statusText: 'statusText', - json: async () => ({}), - }, - ], + [404, { error: { title: 'TESTING' } }], + [500, {}], ])( 'It returns an empty array of applications when the fetch is not okay', - async fetchResult => { + async (statusCode, jsonBody) => { const mockedDiscoveryApi: DiscoveryApi = { getBaseUrl: jest.fn().mockResolvedValueOnce('https://test.test'), }; - const mockedFetchApi: FetchApi = { - fetch: jest.fn().mockResolvedValueOnce({ - ok: true, - json: async () => fetchResult, - }), - }; + server.use( + rest.get( + 'https://test.test/newrelic/apm/api/applications.json', + (_, res, ctx) => res(ctx.status(statusCode), ctx.json(jsonBody)), + ), + ); const client = new NewRelicClient({ discoveryApi: mockedDiscoveryApi, - fetchApi: mockedFetchApi, + fetchApi: new MockFetchApi(), }); const actual = await client.getApplications(); @@ -296,15 +340,15 @@ describe('NewRelicClient', () => { getBaseUrl: jest.fn().mockResolvedValueOnce('https://test.test'), }; - const mockedFetchApi: FetchApi = { - fetch: () => { - throw new Error('TESTING'); - }, - }; + server.use( + rest.get('https://test.test/newrelic/apm/api/applications.json', () => { + throw new Error('Network Error'); + }), + ); const client = new NewRelicClient({ discoveryApi: mockedDiscoveryApi, - fetchApi: mockedFetchApi, + fetchApi: new MockFetchApi(), }); const actual = await client.getApplications(); @@ -316,16 +360,16 @@ describe('NewRelicClient', () => { getBaseUrl: jest.fn().mockResolvedValueOnce('https://test.test'), }; - const mockedFetchApi: FetchApi = { - fetch: jest.fn().mockResolvedValue({ - ok: true, - json: async () => ({ applications: [] }), - }), - }; + server.use( + rest.get( + 'https://test.test/newrelic/apm/api/applications.json', + (_, res, ctx) => res(ctx.status(200), ctx.json({ applications: [] })), + ), + ); const client = new NewRelicClient({ discoveryApi: mockedDiscoveryApi, - fetchApi: mockedFetchApi, + fetchApi: new MockFetchApi(), }); await client.getApplications(); diff --git a/yarn.lock b/yarn.lock index 1399f1d622..b7d49886f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7590,6 +7590,7 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-newrelic@workspace:plugins/newrelic" dependencies: + "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/core-app-api": "workspace:^" "@backstage/core-components": "workspace:^" @@ -7608,7 +7609,7 @@ __metadata: "@types/parse-link-header": ^2.0.1 "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 - msw: ^1.0.0 + msw: ^1.2.3 parse-link-header: ^2.0.0 react-use: ^17.2.4 peerDependencies: @@ -32846,9 +32847,9 @@ __metadata: languageName: node linkType: hard -"msw@npm:^1.0.0, msw@npm:^1.0.1, msw@npm:^1.2.1": - version: 1.2.2 - resolution: "msw@npm:1.2.2" +"msw@npm:^1.0.0, msw@npm:^1.0.1, msw@npm:^1.2.1, msw@npm:^1.2.3": + version: 1.2.3 + resolution: "msw@npm:1.2.3" dependencies: "@mswjs/cookies": ^0.2.2 "@mswjs/interceptors": ^0.17.5 @@ -32876,7 +32877,7 @@ __metadata: optional: true bin: msw: cli/index.js - checksum: e42cec8f5523663020bdecf6a7977a10aa86a4718d1920def3fbde0ff3734391873668cc6e3996d6790add3c74dac95a952f8560ce2543697280125eb55138e8 + checksum: 832a6fc3973726a97e03ce2690c3b6e1774b5156d064a478657a1b33f9933e4834af833cc8a859e1b717fa9b71f1271b3e66a1ec6eed7f76bfe79406e9ec3986 languageName: node linkType: hard