diff --git a/plugins/adr-backend/src/service/router.test.ts b/plugins/adr-backend/src/service/router.test.ts new file mode 100644 index 0000000000..7c42e6eba7 --- /dev/null +++ b/plugins/adr-backend/src/service/router.test.ts @@ -0,0 +1,206 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + ReadTreeResponse, + ReadTreeResponseFile, + ReadUrlResponse, + SearchResponse, + UrlReader, +} from '@backstage/backend-common'; +import express from 'express'; +import request from 'supertest'; +import { createRouter } from './router'; + +const makeBufferFromString = (string: string) => async () => + Buffer.from(string); + +const testingUrlFakeFileTree: ReadTreeResponseFile[] = [ + { + path: 'folder/testFile001.txt', + content: makeBufferFromString('folder/testFile001.txt content'), + }, + { + path: 'testFile001.txt', + content: makeBufferFromString('testFile002.txt content'), + }, + { + path: 'testFile002.txt', + content: makeBufferFromString('testFile001.txt content'), + }, +]; + +const makeFileContent = async (fileContent: string) => { + const result: ReadUrlResponse = { + buffer: makeBufferFromString(fileContent), + }; + return result; +}; + +const testFileOneContent = 'testFileOne content'; +const testFileTwoContent = 'testFileTwo content'; +const genericFileContent = 'file content'; + +const mockUrlReader: UrlReader = { + read: function (): Promise { + throw new Error('read not implemented.'); + }, + readUrl: function (url: string): Promise { + switch (url) { + case 'testFileOne': + return makeFileContent(testFileOneContent); + case 'testFileTwo': + return makeFileContent(testFileTwoContent); + default: + return makeFileContent(genericFileContent); + } + }, + readTree: function (): Promise { + const result: ReadTreeResponse = { + files: async () => testingUrlFakeFileTree, + archive: function (): Promise { + throw new Error('Function not implemented.'); + }, + dir: function (): Promise { + throw new Error('Function not implemented.'); + }, + etag: '', + }; + + const resultPromise = async () => result; + return resultPromise(); + }, + search: function (): Promise { + throw new Error('search not implemented.'); + }, +}; + +describe('createRouter', () => { + let app: express.Express; + + beforeEach(async () => { + jest.resetAllMocks(); + + const router = await createRouter(mockUrlReader); + app = express().use(router); + }); + + describe('GET /getAdrFilesAtUrl', () => { + it('returns bad request (400) when no url is provided', async () => { + const urlNotSpecifiedRequest = await request(app).get( + '/getAdrFilesAtUrl', + ); + const urlNotSpecifiedStatus = urlNotSpecifiedRequest.status; + const urlNotSpecifiedMessage = urlNotSpecifiedRequest.body.message; + + const urlNotFilledRequest = await request(app).get( + '/getAdrFilesAtUrl?url=', + ); + const urlNotFilledStatus = urlNotFilledRequest.status; + const urlNotFilledMessage = urlNotFilledRequest.body.message; + + const expectedStatusCode = 400; + const expectedErrorMessage = 'No URL provided'; + + expect(urlNotSpecifiedStatus).toBe(expectedStatusCode); + expect(urlNotSpecifiedMessage).toBe(expectedErrorMessage); + + expect(urlNotFilledStatus).toBe(expectedStatusCode); + expect(urlNotFilledMessage).toBe(expectedErrorMessage); + }); + + it('returns the correct listing when reading a url', async () => { + const result = await request(app).get('/getAdrFilesAtUrl?url=testing'); + const { status, body, error } = result; + + const expectedStatusCode = 200; + const expectedBody = { + data: [ + { + type: 'file', + name: 'testFile001.txt', + path: 'folder/testFile001.txt', + }, + { + type: 'file', + name: 'testFile001.txt', + path: 'testFile001.txt', + }, + { + type: 'file', + name: 'testFile002.txt', + path: 'testFile002.txt', + }, + ], + }; + + expect(error).toBeFalsy(); + expect(status).toBe(expectedStatusCode); + expect(body).toEqual(expectedBody); + }); + }); + + describe('GET /readAdrFileAtUrl', () => { + it('returns bad request (400) when no url is provided', async () => { + const urlNotSpecifiedRequest = await request(app).get( + '/readAdrFileAtUrl', + ); + const urlNotSpecifiedStatus = urlNotSpecifiedRequest.status; + const urlNotSpecifiedMessage = urlNotSpecifiedRequest.body.message; + + const urlNotFilledRequest = await request(app).get( + '/readAdrFileAtUrl?url=', + ); + const urlNotFilledStatus = urlNotFilledRequest.status; + const urlNotFilledMessage = urlNotFilledRequest.body.message; + + const expectedStatusCode = 400; + const expectedErrorMessage = 'No URL provided'; + + expect(urlNotSpecifiedStatus).toBe(expectedStatusCode); + expect(urlNotSpecifiedMessage).toBe(expectedErrorMessage); + + expect(urlNotFilledStatus).toBe(expectedStatusCode); + expect(urlNotFilledMessage).toBe(expectedErrorMessage); + }); + + it('returns the correct file contents when reading a url', async () => { + const fileOneResponse = await request(app).get( + '/readAdrFileAtUrl?url=testFileOne', + ); + const fileOneStatus = fileOneResponse.status; + const fileOneBody = fileOneResponse.body; + const fileOneError = fileOneResponse.error; + + const fileTwoResponse = await request(app).get( + '/readAdrFileAtUrl?url=testFileTwo', + ); + const fileTwoStatus = fileTwoResponse.status; + const fileTwoBody = fileTwoResponse.body; + const fileTwoError = fileTwoResponse.error; + + const expectedStatusCode = 200; + + expect(fileOneError).toBeFalsy(); + expect(fileOneStatus).toBe(expectedStatusCode); + expect(fileOneBody.data).toBe(testFileOneContent); + + expect(fileTwoError).toBeFalsy(); + expect(fileTwoStatus).toBe(expectedStatusCode); + expect(fileTwoBody.data).toBe(testFileTwoContent); + }); + }); +}); diff --git a/plugins/adr/package.json b/plugins/adr/package.json index b28fdeb99c..dda581c5cd 100644 --- a/plugins/adr/package.json +++ b/plugins/adr/package.json @@ -46,9 +46,13 @@ "react-router-dom": "6.0.0-beta.0 || ^6.3.0" }, "devDependencies": { + "@backstage/catalog-client": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/core-app-api": "workspace:^", "@backstage/dev-utils": "workspace:^", + "@backstage/integration": "workspace:^", + "@backstage/plugin-catalog": "workspace:^", + "@backstage/plugin-permission-react": "workspace:^", "@backstage/test-utils": "workspace:^", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", diff --git a/plugins/adr/src/components/AdrReader/AdrReader.test.tsx b/plugins/adr/src/components/AdrReader/AdrReader.test.tsx new file mode 100644 index 0000000000..1b1cdcf851 --- /dev/null +++ b/plugins/adr/src/components/AdrReader/AdrReader.test.tsx @@ -0,0 +1,129 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { EntityLayout } from '@backstage/plugin-catalog'; +import { Entity, ANNOTATION_SOURCE_LOCATION } from '@backstage/catalog-model'; +import { ApiProvider } from '@backstage/core-app-api'; +import { CatalogApi } from '@backstage/catalog-client'; +import { + EntityProvider, + catalogApiRef, + starredEntitiesApiRef, + MockStarredEntitiesApi, +} from '@backstage/plugin-catalog-react'; +import { scmIntegrationsApiRef } from '@backstage/integration-react'; +import { permissionApiRef } from '@backstage/plugin-permission-react'; +import { + renderInTestApp, + TestApiRegistry, + MockPermissionApi, +} from '@backstage/test-utils'; +import { AdrReader } from './AdrReader'; +import { ScmIntegrationRegistry } from '@backstage/integration'; +import { ANNOTATION_ADR_LOCATION } from '@backstage/plugin-adr-common'; +import { + octokitAdrFileFetcher, + urlReaderAdrFileFetcher, +} from '../../hooks/adrFileFetcher'; + +const mockApis = TestApiRegistry.from( + [catalogApiRef, {} as CatalogApi], + [starredEntitiesApiRef, new MockStarredEntitiesApi()], + [permissionApiRef, new MockPermissionApi()], + [ + scmIntegrationsApiRef, + { + resolveUrl: options => `${options.url}`, + } as ScmIntegrationRegistry, + ], +); + +const mockEntity: Entity = { + kind: 'TestEntity', + metadata: { + name: 'Testing Entity 1', + annotations: { + [ANNOTATION_ADR_LOCATION]: 'testAdrFolder', + [ANNOTATION_SOURCE_LOCATION]: 'source:location', + }, + }, + apiVersion: '', +}; + +afterEach(() => { + jest.resetAllMocks(); +}); + +describe('AdrReader', () => { + it('Falls back to octokitAdrFileFetcher when adrFileFetcher is not specified', async () => { + const spyInstance = jest + .spyOn(octokitAdrFileFetcher, 'useReadAdrFileAtUrl') + .mockImplementation(() => { + return { data: '' }; + }); + + await renderInTestApp( + + + + + + + + + , + ); + + expect(spyInstance).toHaveBeenCalled(); + }); + + it('Uses an alternative AdrFileFetcher when provided', async () => { + const octokitSpyInstance = jest + .spyOn(octokitAdrFileFetcher, 'useReadAdrFileAtUrl') + .mockImplementation(() => { + return { + data: '', + }; + }); + + const urlReadersSpyInstance = jest + .spyOn(urlReaderAdrFileFetcher, 'useReadAdrFileAtUrl') + .mockImplementation(() => { + return { + data: '', + }; + }); + + await renderInTestApp( + + + + + + + + + , + ); + + expect(octokitSpyInstance).not.toHaveBeenCalled(); + expect(urlReadersSpyInstance).toHaveBeenCalled(); + }); +}); diff --git a/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.test.tsx b/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.test.tsx new file mode 100644 index 0000000000..ca6397e736 --- /dev/null +++ b/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.test.tsx @@ -0,0 +1,139 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { EntityLayout } from '@backstage/plugin-catalog'; +import { Entity, ANNOTATION_SOURCE_LOCATION } from '@backstage/catalog-model'; +import { ApiProvider } from '@backstage/core-app-api'; +import { CatalogApi } from '@backstage/catalog-client'; +import { + EntityProvider, + catalogApiRef, + starredEntitiesApiRef, + MockStarredEntitiesApi, +} from '@backstage/plugin-catalog-react'; +import { scmIntegrationsApiRef } from '@backstage/integration-react'; +import { permissionApiRef } from '@backstage/plugin-permission-react'; +import { + renderInTestApp, + TestApiRegistry, + MockPermissionApi, +} from '@backstage/test-utils'; +import { ScmIntegrationRegistry } from '@backstage/integration'; +import { ANNOTATION_ADR_LOCATION } from '@backstage/plugin-adr-common'; +import { + octokitAdrFileFetcher, + urlReaderAdrFileFetcher, +} from '../../hooks/adrFileFetcher'; +import { EntityAdrContent } from './EntityAdrContent'; +import { rootRouteRef } from '../../routes'; + +const mockApis = TestApiRegistry.from( + [catalogApiRef, {} as CatalogApi], + [starredEntitiesApiRef, new MockStarredEntitiesApi()], + [permissionApiRef, new MockPermissionApi()], + [ + scmIntegrationsApiRef, + { + resolveUrl: options => `${options.url}`, + } as ScmIntegrationRegistry, + ], +); + +const mockEntity: Entity = { + kind: 'TestEntity', + metadata: { + name: 'Testing Entity 1', + annotations: { + [ANNOTATION_ADR_LOCATION]: 'testAdrFolder', + [ANNOTATION_SOURCE_LOCATION]: 'source:location', + }, + }, + apiVersion: '', +}; + +afterEach(() => { + jest.resetAllMocks(); +}); + +describe('EntityAdrContent', () => { + it('Falls back to octokitAdrFileFetcher when adrFileFetcher is not specified', async () => { + const getAdrFilesSpyInstance = jest + .spyOn(octokitAdrFileFetcher, 'useGetAdrFilesAtUrl') + .mockImplementation(() => { + return { + data: [], + }; + }); + + await renderInTestApp( + + + + + + + + + , + { + mountedRoutes: { + '/adr': rootRouteRef, + }, + }, + ); + + expect(getAdrFilesSpyInstance).toHaveBeenCalled(); + }); + + it('Uses an alternative AdrFileFetcher when provided', async () => { + const octokitGetAdrFilesSpyInstance = jest + .spyOn(octokitAdrFileFetcher, 'useGetAdrFilesAtUrl') + .mockImplementation(() => { + return { + data: [], + }; + }); + + const urlReadersGetAdrFilesSpyInstance = jest + .spyOn(urlReaderAdrFileFetcher, 'useGetAdrFilesAtUrl') + .mockImplementation(() => { + return { + data: [], + }; + }); + + await renderInTestApp( + + + + + + + + + , + { + mountedRoutes: { + '/adr': rootRouteRef, + }, + }, + ); + + expect(octokitGetAdrFilesSpyInstance).not.toHaveBeenCalled(); + expect(urlReadersGetAdrFilesSpyInstance).toHaveBeenCalled(); + }); +}); diff --git a/yarn.lock b/yarn.lock index 3f38b22fc1..eb0e5f3476 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4289,16 +4289,20 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-adr@workspace:plugins/adr" dependencies: + "@backstage/catalog-client": "workspace:^" "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/core-app-api": "workspace:^" "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" + "@backstage/integration": "workspace:^" "@backstage/integration-react": "workspace:^" "@backstage/plugin-adr-common": "workspace:^" + "@backstage/plugin-catalog": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" "@backstage/plugin-permission-common": "workspace:^" + "@backstage/plugin-permission-react": "workspace:^" "@backstage/plugin-search-common": "workspace:^" "@backstage/plugin-search-react": "workspace:^" "@backstage/test-utils": "workspace:^"