From a86f111b97a98f8e23dd5d680923f17f16f4259e Mon Sep 17 00:00:00 2001 From: blam Date: Sun, 26 Apr 2020 01:57:34 +0200 Subject: [PATCH] feat(scaffolder): make the scaffolder repository a little more flexible so we can load from multiple sources --- .../src/lib/repo/disk.test.ts | 83 +++++++++++++++++++ .../scaffolder-backend/src/lib/repo/disk.ts | 80 ++++++++++++++++++ .../src/lib/repo/index.test.ts | 56 +++++++++++++ .../scaffolder-backend/src/lib/repo/index.ts | 48 +++++++++++ plugins/scaffolder-backend/src/plugin.test.ts | 47 ++++++----- plugins/scaffolder-backend/src/plugin.ts | 10 +-- 6 files changed, 295 insertions(+), 29 deletions(-) create mode 100644 plugins/scaffolder-backend/src/lib/repo/disk.test.ts create mode 100644 plugins/scaffolder-backend/src/lib/repo/disk.ts create mode 100644 plugins/scaffolder-backend/src/lib/repo/index.test.ts create mode 100644 plugins/scaffolder-backend/src/lib/repo/index.ts diff --git a/plugins/scaffolder-backend/src/lib/repo/disk.test.ts b/plugins/scaffolder-backend/src/lib/repo/disk.test.ts new file mode 100644 index 0000000000..e54a3d3050 --- /dev/null +++ b/plugins/scaffolder-backend/src/lib/repo/disk.test.ts @@ -0,0 +1,83 @@ +/* + * Copyright 2020 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 { DiskRepository } from './disk'; +import * as path from 'path'; +describe('Template Repository', () => { + it('should load a simple template from a simple directory', async () => { + const testTemplateDir = path.resolve( + __dirname, + '../../../test/mock-simple-template-dir', + ); + const templateInfo = require(`${testTemplateDir}/mock-template/template-info.json`); + + const repository = new DiskRepository(testTemplateDir); + + await repository.reindex(); + + const templates = await repository.list(); + + expect(templates).toHaveLength(1); + expect(templates[0].id).toBe(templateInfo.id); + expect(templates[0].name).toBe(templateInfo.name); + expect(templates[0].description).toBe(templateInfo.description); + expect(templates[0].ownerId).toBe(templateInfo.ownerId); + }); + + it('should successfully load multiple templates from the same folder', async () => { + const testTemplateDir = path.resolve( + __dirname, + '../../../test/mock-multiple-templates-dir', + ); + + const repository = new DiskRepository(testTemplateDir); + + await repository.reindex(); + + const templates = await repository.list(); + + expect(templates).toHaveLength(2); + }); + + it('should return empty array when there are no templates', async () => { + const testTemplateDir = path.resolve( + __dirname, + '/some-folder-that-deffo-does-not-exist', + ); + + const repository = new DiskRepository(testTemplateDir); + + await repository.reindex(); + + const templates = await repository.list(); + + expect(templates).toHaveLength(0); + }); + + it('should be able to handle templates with invalid json and ignore them from the returned array', async () => { + const testTemplateDir = path.resolve( + __dirname, + '../../../test/mock-failing-template-dir', + ); + + const repository = new DiskRepository(testTemplateDir); + + await repository.reindex(); + + const templates = await repository.list(); + + expect(templates).toHaveLength(1); + }); +}); diff --git a/plugins/scaffolder-backend/src/lib/repo/disk.ts b/plugins/scaffolder-backend/src/lib/repo/disk.ts new file mode 100644 index 0000000000..c53277a69a --- /dev/null +++ b/plugins/scaffolder-backend/src/lib/repo/disk.ts @@ -0,0 +1,80 @@ +/* + * Copyright 2020 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 glob from 'glob'; +import { promises as fs } from 'fs'; +import { logger } from 'lib/logger'; +import { Template, RepositoryBase as Base } from '.'; + +export class DiskRepository implements Base { + private repository: Template[] = []; + + constructor(private repoDir = `${__dirname}/templates`) { + this.reindex(); + } + + public async list(): Promise { + return this.repository; + } + + public async reindex(): Promise { + this.repository = await this.index(); + } + + public async prepare(template: string): Promise { + return template; + } + + private async index(): Promise { + return new Promise((resolve, reject) => { + glob(`${this.repoDir}/**/template-info.json`, async (err, matches) => { + if (err) { + reject(err); + } + + const fileContents: Array<{ + path: string; + contents: string; + }> = await Promise.all( + matches.map(async (path: string) => ({ + path, + contents: await fs.readFile(path, 'utf-8'), + })), + ); + + const validFiles = fileContents.reduce( + (templates: Template[], currentFile) => { + try { + const parsed: Template = JSON.parse(currentFile.contents); + return [...templates, parsed]; + } catch (ex) { + logger.error('Failure parsing JSON for template', { + path: currentFile.path, + }); + } + + return templates; + }, + [], + ); + + resolve(validFiles as Template[]); + }); + }); + } +} + +export const Repository = new DiskRepository(); diff --git a/plugins/scaffolder-backend/src/lib/repo/index.test.ts b/plugins/scaffolder-backend/src/lib/repo/index.test.ts new file mode 100644 index 0000000000..d20b0342fd --- /dev/null +++ b/plugins/scaffolder-backend/src/lib/repo/index.test.ts @@ -0,0 +1,56 @@ +/* + * Copyright 2020 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 { RepositoryBase, Repository } from '.'; + +describe('Repository Interface Test', () => { + const mockRepo = new (class MockRepository implements RepositoryBase { + list = jest.fn(); + prepare = jest.fn(); + reindex = jest.fn(); + + public reset = () => { + this.list.mockReset(); + this.prepare.mockReset(); + this.reindex.mockReset(); + }; + })(); + + afterEach(() => mockRepo.reset()); + + it('should call list of the set repo when calling list', async () => { + Repository.setRepository(mockRepo); + + await Repository.list(); + + expect(mockRepo.list).toHaveBeenCalled(); + }); + + it('should reindex on the repo when calling reindex', async () => { + Repository.setRepository(mockRepo); + + await Repository.reindex(); + + expect(mockRepo.reindex).toHaveBeenCalled(); + }); + + it('should call prepare with the correct id when calling prepare', async () => { + Repository.setRepository(mockRepo); + + await Repository.prepare('testid'); + + expect(mockRepo.prepare).toHaveBeenCalledWith('testid'); + }); +}); diff --git a/plugins/scaffolder-backend/src/lib/repo/index.ts b/plugins/scaffolder-backend/src/lib/repo/index.ts new file mode 100644 index 0000000000..ade9f0397e --- /dev/null +++ b/plugins/scaffolder-backend/src/lib/repo/index.ts @@ -0,0 +1,48 @@ +import { DiskRepository } from './disk'; + +/* + * Copyright 2020 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. + */ +export interface Template { + id: string; + name: string; + description: string; + ownerId: string; +} + +export abstract class RepositoryBase { + abstract async list(): Promise; + abstract async reindex(): Promise; + // returns a directory to run cookiecutter in + abstract async prepare(id: string): Promise; +} + +class Interface implements RepositoryBase { + repo?: RepositoryBase; + + constructor() { + this.repo = new DiskRepository(); + } + + public setRepository(repo: RepositoryBase) { + this.repo = repo; + } + + list = () => this.repo!.list(); + prepare = (id: string) => this.repo!.prepare(id); + reindex = () => this.repo!.reindex(); +} + +export const Repository = new Interface(); diff --git a/plugins/scaffolder-backend/src/plugin.test.ts b/plugins/scaffolder-backend/src/plugin.test.ts index 85c04231c0..78879453bc 100644 --- a/plugins/scaffolder-backend/src/plugin.test.ts +++ b/plugins/scaffolder-backend/src/plugin.test.ts @@ -13,26 +13,31 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -// /* -// * Copyright 2020 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 { plugiroutn } from './plugin'; +import { router } from './plugin'; -// describe('scaffolder', () => { -// it('should export plugin', () => { -// expect(plugin).toBeDefined(); -// }); -// }); +import { Repository, Template } from 'lib/repo'; +import supertest from 'supertest'; +import express from 'express'; + + +describe('scaffolder backend', () => { + const app = express().use(router); + const mockTemplate: Template = { + id: 'test-mock-template', + name: 'mock', + description: 'test for tests', + ownerId: 'lol', + }; + + it('should return a list of templates that are in the directory which the plugin is initialised in', async () => { + jest.spyOn(Repository, 'list').mockResolvedValue([mockTemplate]); + + const { body } = await supertest(app) + .get('/v1/templates') + .send(); + + expect(body.length).toBe(1); + expect(body[0]).toStrictEqual(mockTemplate); + }); +}); diff --git a/plugins/scaffolder-backend/src/plugin.ts b/plugins/scaffolder-backend/src/plugin.ts index cccce7a229..f3bfa7033b 100644 --- a/plugins/scaffolder-backend/src/plugin.ts +++ b/plugins/scaffolder-backend/src/plugin.ts @@ -33,14 +33,8 @@ import express from 'express'; export const router = express.Router(); router.get('/v1/templates', async (_, res) => { - res - .status(200) - .send([ - { id: 'component1' }, - { id: 'component2' }, - { id: 'component3' }, - { id: 'component4' }, - ]); + const templates = await Repository.list(); + res.status(200).json(templates); }); router.get('/v1/template/:templateId', async (_, res) => {