From 9393f7dfe7a02408443c989357a594d8d311a6f0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 12 Mar 2021 11:50:13 +0100 Subject: [PATCH] todo-backend: add tests for TodoScmReader and make constructor public Signed-off-by: Patrik Oldsberg --- .../src/lib/TodoReader/TodoScmReader.test.ts | 186 ++++++++++++++++++ .../src/lib/TodoReader/TodoScmReader.ts | 12 +- 2 files changed, 194 insertions(+), 4 deletions(-) create mode 100644 plugins/todo-backend/src/lib/TodoReader/TodoScmReader.test.ts diff --git a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.test.ts b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.test.ts new file mode 100644 index 0000000000..83cc8b5ecf --- /dev/null +++ b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.test.ts @@ -0,0 +1,186 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { + getVoidLogger, + ReadTreeResponse, + UrlReader, +} from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { NotFoundError, NotModifiedError } from '@backstage/errors'; +import { ScmIntegrations } from '@backstage/integration'; +import { createTodoParser } from './createTodoParser'; +import { TodoScmReader } from './TodoScmReader'; + +function mockReader(): jest.Mocked { + return { + read: jest.fn(), + readTree: jest.fn(), + search: jest.fn(), + } as jest.Mocked; +} + +describe('TodoScmReader', () => { + it('should be created from config', () => { + const todoReader = TodoScmReader.fromConfig(new ConfigReader({}), { + logger: getVoidLogger(), + reader: mockReader(), + }); + expect(todoReader).toEqual(expect.any(TodoScmReader)); + }); + + it('should read TODOs', async () => { + const reader = mockReader(); + const todoReader = new TodoScmReader({ + logger: getVoidLogger(), + reader, + integrations: ScmIntegrations.fromConfig(new ConfigReader({})), + }); + + reader.readTree.mockResolvedValueOnce({ + etag: 'my-etag', + files: async () => [ + { + content: async () => Buffer.from('// TODO: my-todo', 'utf8'), + path: 'my-folder/my-file.js', + }, + ], + } as ReadTreeResponse); + + const url = 'https://github.com/backstage/backstage/catalog-info.yaml'; + const expected = { + items: [ + { + text: 'my-todo', + tag: 'TODO', + lineNumber: 1, + repoFilePath: 'my-folder/my-file.js', + viewUrl: + 'https://github.com/backstage/backstage/my-folder/my-file.js#L1', + }, + ], + }; + + await expect(todoReader.readTodos({ url })).resolves.toEqual(expected); + + expect(reader.readTree).toHaveBeenCalledTimes(1); + expect(reader.readTree).toHaveBeenCalledWith( + 'https://github.com/backstage/backstage/catalog-info.yaml', + { + etag: undefined, + filter: expect.any(Function), + }, + ); + + // Filter function should filter out dotfiles + const filterFunc = reader.readTree.mock.calls[0][1]!.filter!; + expect(filterFunc('my-file.js')).toBe(true); + expect(filterFunc('my-folder/my-file.js')).toBe(true); + expect(filterFunc('.my-file.js')).toBe(false); + expect(filterFunc('.my-folder/my-file.js')).toBe(false); + expect(filterFunc('my-folder/.my-file.js')).toBe(false); + + // A NotModifiedError should return a cached result + reader.readTree.mockRejectedValueOnce(new NotModifiedError('nope')); + await expect(todoReader.readTodos({ url })).resolves.toEqual(expected); + + expect(reader.readTree).toHaveBeenCalledTimes(2); + expect(reader.readTree).toHaveBeenLastCalledWith( + 'https://github.com/backstage/backstage/catalog-info.yaml', + { + etag: 'my-etag', + filter: expect.any(Function), + }, + ); + + // Other errors should re-throw + reader.readTree.mockRejectedValueOnce(new NotFoundError('not found')); + await expect(todoReader.readTodos({ url })).rejects.toThrow('not found'); + }); + + it('should use custom parser', async () => { + const parser = jest.fn(createTodoParser({ additionalTags: ['XXX'] })); + const reader = mockReader(); + const todoReader = new TodoScmReader({ + logger: getVoidLogger(), + reader, + parser, + integrations: ScmIntegrations.fromConfig(new ConfigReader({})), + }); + + reader.readTree.mockResolvedValueOnce({ + files: async () => [ + { + content: async () => Buffer.from('-- XXX: my-todo', 'utf8'), + path: 'my-file.lua', + }, + ], + } as ReadTreeResponse); + + await expect( + todoReader.readTodos({ + url: 'https://github.com/backstage/backstage/catalog-info.yaml', + }), + ).resolves.toEqual({ + items: [ + { + text: 'my-todo', + tag: 'XXX', + lineNumber: 1, + repoFilePath: 'my-file.lua', + viewUrl: 'https://github.com/backstage/backstage/my-file.lua#L1', + }, + ], + }); + expect(parser).toHaveBeenCalledTimes(1); + expect(parser).toHaveBeenCalledWith({ + content: '-- XXX: my-todo', + path: 'my-file.lua', + }); + }); + + it('should log and ignore parser errors', async () => { + const logger = getVoidLogger(); + const errorSpy = jest.spyOn(logger, 'error'); + const reader = mockReader(); + const todoReader = new TodoScmReader({ + logger, + reader, + parser() { + throw new Error('failed to parse'); + }, + integrations: ScmIntegrations.fromConfig(new ConfigReader({})), + }); + + reader.readTree.mockResolvedValueOnce({ + files: async () => [ + { + content: async () => Buffer.from('# XXX: my-todo', 'utf8'), + path: 'my-file.sh', + }, + ], + } as ReadTreeResponse); + + await expect( + todoReader.readTodos({ + url: 'https://github.com/o/r/catalog-info.yaml', + }), + ).resolves.toEqual({ items: [] }); + expect(errorSpy).toHaveBeenCalledWith( + 'Failed to parse TODO in https://github.com/o/r/catalog-info.yaml at my-file.sh, Error: failed to parse', + ); + }); +}); diff --git a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts index 07ebdf97eb..f13b0c05df 100644 --- a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts +++ b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts @@ -31,6 +31,7 @@ import { createTodoParser } from './createTodoParser'; type Options = { logger: Logger; reader: UrlReader; + integrations: ScmIntegrations; parser?: TodoParser; }; @@ -47,15 +48,18 @@ export class TodoScmReader implements TodoReader { private readonly cache = new Map(); - static fromConfig(config: Config, options: Options) { - return new TodoScmReader(options, ScmIntegrations.fromConfig(config)); + static fromConfig(config: Config, options: Omit) { + return new TodoScmReader({ + ...options, + integrations: ScmIntegrations.fromConfig(config), + }); } - private constructor(options: Options, integrations: ScmIntegrations) { + constructor(options: Options) { this.logger = options.logger; this.reader = options.reader; this.parser = options.parser ?? createTodoParser(); - this.integrations = integrations; + this.integrations = options.integrations; } async readTodos({ url }: ReadTodosOptions): Promise {