todo-backend: add tests for TodoScmReader and make constructor public
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -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<UrlReader> {
|
||||
return {
|
||||
read: jest.fn(),
|
||||
readTree: jest.fn(),
|
||||
search: jest.fn(),
|
||||
} as jest.Mocked<UrlReader>;
|
||||
}
|
||||
|
||||
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',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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<string, CacheItem>();
|
||||
|
||||
static fromConfig(config: Config, options: Options) {
|
||||
return new TodoScmReader(options, ScmIntegrations.fromConfig(config));
|
||||
static fromConfig(config: Config, options: Omit<Options, 'integrations'>) {
|
||||
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<ReadTodosResult> {
|
||||
|
||||
Reference in New Issue
Block a user