From 72116243287db45f35f3b94cd655632a0902fe35 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Nov 2020 19:20:16 +0100 Subject: [PATCH] config-loader: added tests for schema collection --- .../src/lib/schema/collect.test.ts | 229 ++++++++++++++++++ .../config-loader/src/lib/schema/collect.ts | 29 ++- 2 files changed, 247 insertions(+), 11 deletions(-) create mode 100644 packages/config-loader/src/lib/schema/collect.test.ts diff --git a/packages/config-loader/src/lib/schema/collect.test.ts b/packages/config-loader/src/lib/schema/collect.test.ts new file mode 100644 index 0000000000..7aca4f7c0c --- /dev/null +++ b/packages/config-loader/src/lib/schema/collect.test.ts @@ -0,0 +1,229 @@ +/* + * 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 mockFs from 'mock-fs'; +import { collectConfigSchemas } from './collect'; + +const mockSchema = { + type: 'object', + properties: { + key: { + type: 'string', + visibility: 'frontend', + }, + }, +}; + +describe('collectConfigSchemas', () => { + afterEach(() => { + mockFs.restore(); + }); + + it('should not find any schemas without packages', async () => { + mockFs({ + 'lerna.json': JSON.stringify({ + packages: ['packages/*'], + }), + }); + + await expect(collectConfigSchemas([])).resolves.toEqual([]); + }); + + it('should find schema in a local package', async () => { + mockFs({ + node_modules: { + a: { + 'package.json': JSON.stringify({ + name: 'a', + configSchema: mockSchema, + }), + }, + }, + }); + + await expect(collectConfigSchemas(['a'])).resolves.toEqual([ + { + path: 'node_modules/a/package.json', + value: mockSchema, + }, + ]); + }); + + it('should find schema in transitive dependencies', async () => { + mockFs({ + node_modules: { + a: { + 'package.json': JSON.stringify({ + name: 'a', + dependencies: { b: '0.0.0', '@backstage/mock': '0.0.0' }, + }), + }, + b: { + 'package.json': JSON.stringify({ + name: 'b', + dependencies: { + c1: '0.0.0', + c2: '0.0.0', + '@backstage/mock': '0.0.0', + }, + configSchema: { ...mockSchema, title: 'b' }, + }), + }, + c1: { + 'package.json': JSON.stringify({ + name: 'c1', + dependencies: { d1: '0.0.0' }, + configSchema: { ...mockSchema, title: 'c1' }, + }), + }, + c2: { + 'package.json': JSON.stringify({ + name: 'c2', + dependencies: { d2: '0.0.0' }, + }), + }, + d1: { + 'package.json': JSON.stringify({ + name: 'd1', + dependencies: {}, + configSchema: { ...mockSchema, title: 'd1' }, + }), + }, + d2: { + 'package.json': JSON.stringify({ + name: 'd2', + dependencies: {}, + configSchema: { ...mockSchema, title: 'd2' }, + }), + }, + }, + }); + + await expect(collectConfigSchemas(['a'])).resolves.toEqual([ + { + path: 'node_modules/b/package.json', + value: { ...mockSchema, title: 'b' }, + }, + { + path: 'node_modules/c1/package.json', + value: { ...mockSchema, title: 'c1' }, + }, + { + path: 'node_modules/d1/package.json', + value: { ...mockSchema, title: 'd1' }, + }, + ]); + }); + + it('should schema of different types', async () => { + mockFs({ + node_modules: { + a: { + 'package.json': JSON.stringify({ + name: 'a', + configSchema: { ...mockSchema, title: 'inline' }, + }), + }, + b: { + 'package.json': JSON.stringify({ + name: 'b', + configSchema: 'schema.json', + }), + 'schema.json': JSON.stringify({ ...mockSchema, title: 'external' }), + }, + c: { + 'package.json': JSON.stringify({ + name: 'c', + configSchema: 'schema.d.ts', + }), + 'schema.d.ts': `export interface Config { + /** @visibility secret */ + tsKey: string + }`, + }, + }, + // TypeScript compilation needs to load some real files inside the typescript dir + '../../node_modules/typescript': (mockFs as any).load( + '../../node_modules/typescript', + ), + }); + + await expect(collectConfigSchemas(['a', 'b', 'c'])).resolves.toEqual([ + { + path: 'node_modules/a/package.json', + value: { ...mockSchema, title: 'inline' }, + }, + { + path: 'node_modules/b/schema.json', + value: { ...mockSchema, title: 'external' }, + }, + { + path: 'node_modules/c/schema.d.ts', + value: { + $schema: 'http://json-schema.org/draft-07/schema#', + type: 'object', + properties: { + tsKey: { + type: 'string', + visibility: 'secret', + }, + }, + required: ['tsKey'], + }, + }, + ]); + }); + + it('should not allow unknown schema file types', async () => { + mockFs({ + node_modules: { + a: { + 'package.json': JSON.stringify({ + name: 'a', + configSchema: 'schema.yaml', + }), + 'schema.yaml': mockSchema, + }, + }, + }); + + await expect(collectConfigSchemas(['a'])).rejects.toThrow( + 'Config schema files must be .json or .d.ts, got schema.yaml', + ); + }); + + it('should reject typescript config declaration without a Config type', async () => { + mockFs({ + node_modules: { + a: { + 'package.json': JSON.stringify({ + name: 'a', + configSchema: 'schema.d.ts', + }), + 'schema.d.ts': `export interface NotConfig {}`, + }, + }, + // TypeScript compilation needs to load some real files inside the typescript dir + '../../node_modules/typescript': (mockFs as any).load( + '../../node_modules/typescript', + ), + }); + + await expect(collectConfigSchemas(['a'])).rejects.toThrow( + 'Invalid schema in node_modules/a/schema.d.ts, missing Config export', + ); + }); +}); diff --git a/packages/config-loader/src/lib/schema/collect.ts b/packages/config-loader/src/lib/schema/collect.ts index 9d8e6e73f9..0e53562875 100644 --- a/packages/config-loader/src/lib/schema/collect.ts +++ b/packages/config-loader/src/lib/schema/collect.ts @@ -151,17 +151,24 @@ function compileTsSchemas(paths: string[]) { }); const tsSchemas = paths.map(path => { - const value = generateSchema( - program, - // All schemas should export a `Config` symbol - 'Config', - // This enables usage of @visibility is doc comments - { - required: true, - validationKeywords: ['visibility'], - }, - [path.split(sep).join('/')], // Unix paths are expected for all OSes here - ) as JsonObject | null; + let value; + try { + value = generateSchema( + program, + // All schemas should export a `Config` symbol + 'Config', + // This enables usage of @visibility is doc comments + { + required: true, + validationKeywords: ['visibility'], + }, + [path.split(sep).join('/')], // Unix paths are expected for all OSes here + ) as JsonObject | null; + } catch (error) { + if (error.message !== 'type Config not found') { + throw error; + } + } if (!value) { throw new Error(`Invalid schema in ${path}, missing Config export`);