From dc4154a15fa43ac67216af590497fbb2e101fbe1 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 3 Apr 2023 13:58:42 +0200 Subject: [PATCH] config-loader: add ConfigSources tests + fixes Signed-off-by: Patrik Oldsberg --- .../src/sources/ConfigSources.test.ts | 234 ++++++++++++++++++ .../src/sources/ConfigSources.ts | 6 +- 2 files changed, 238 insertions(+), 2 deletions(-) create mode 100644 packages/config-loader/src/sources/ConfigSources.test.ts diff --git a/packages/config-loader/src/sources/ConfigSources.test.ts b/packages/config-loader/src/sources/ConfigSources.test.ts new file mode 100644 index 0000000000..b2fe125e55 --- /dev/null +++ b/packages/config-loader/src/sources/ConfigSources.test.ts @@ -0,0 +1,234 @@ +/* + * Copyright 2023 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 fs from 'fs-extra'; +import { ConfigSources } from './ConfigSources'; +import { ConfigSource } from './types'; +import { MutableConfigSource } from './MutableConfigSource'; + +jest.mock('./FileConfigSource', () => ({ + FileConfigSource: { + create: (opts: {}) => ({ ...opts, name: 'FileConfigSource' }), + }, +})); +jest.mock('./RemoteConfigSource', () => ({ + RemoteConfigSource: { + create: (opts: {}) => ({ ...opts, name: 'RemoteConfigSource' }), + }, +})); +jest.mock('./EnvConfigSource', () => ({ + EnvConfigSource: { + create: (opts: {}) => ({ ...opts, name: 'EnvConfigSource' }), + }, +})); + +function mergeSources(source: ConfigSource): ConfigSource[] { + return (source as any)[ + Symbol.for('@backstage/config-loader#MergedConfigSource.sources') + ] as ConfigSource[]; +} + +describe('ConfigSources', () => { + it('should parse args', () => { + expect(ConfigSources.parseArgs([])).toEqual([]); + + expect( + ConfigSources.parseArgs(['--config', 'a.yaml', '--config=b.yaml']), + ).toEqual([ + { type: 'path', target: 'a.yaml' }, + { type: 'path', target: 'b.yaml' }, + ]); + + expect( + ConfigSources.parseArgs([ + '--config', + 'a.yaml', + '--config', + 'http://example.com/config.yaml', + ]), + ).toEqual([ + { type: 'path', target: 'a.yaml' }, + { type: 'url', target: 'http://example.com/config.yaml' }, + ]); + }); + + it('should create default sources for targets', () => { + expect( + mergeSources( + ConfigSources.defaultForTargets({ rootDir: '/', targets: [] }), + ), + ).toEqual([{ name: 'FileConfigSource', path: '/app-config.yaml' }]); + + const fsSpy = jest.spyOn(fs, 'pathExistsSync').mockReturnValue(true); + expect( + mergeSources( + ConfigSources.defaultForTargets({ rootDir: '/', targets: [] }), + ), + ).toEqual([ + { name: 'FileConfigSource', path: '/app-config.yaml' }, + { name: 'FileConfigSource', path: '/app-config.local.yaml' }, + ]); + fsSpy.mockRestore(); + + expect( + mergeSources( + ConfigSources.defaultForTargets({ + rootDir: '/', + targets: [{ type: 'path', target: '/config.yaml' }], + }), + ), + ).toEqual([{ name: 'FileConfigSource', path: '/config.yaml' }]); + + const subFunc = async () => undefined; + expect( + mergeSources( + ConfigSources.defaultForTargets({ + rootDir: '/', + targets: [{ type: 'path', target: '/config.yaml' }], + substitutionFunc: subFunc, + }), + ), + ).toEqual([ + { + name: 'FileConfigSource', + path: '/config.yaml', + substitutionFunc: subFunc, + }, + ]); + + expect(() => + ConfigSources.defaultForTargets({ + rootDir: '/', + targets: [{ type: 'url', target: 'http://example.com/config.yaml' }], + }), + ).toThrow( + 'Config argument "http://example.com/config.yaml" looks like a URL but remote configuration is not enabled. Enable it by passing the `remote` option', + ); + + expect( + mergeSources( + ConfigSources.defaultForTargets({ + rootDir: '/', + targets: [{ type: 'url', target: 'http://example.com/config.yaml' }], + remote: {}, + }), + ), + ).toEqual([ + { name: 'RemoteConfigSource', url: 'http://example.com/config.yaml' }, + ]); + + expect( + mergeSources( + ConfigSources.defaultForTargets({ + rootDir: '/', + targets: [{ type: 'url', target: 'http://example.com/config.yaml' }], + remote: { reloadIntervalSeconds: 5 }, + }), + ), + ).toEqual([ + { + name: 'RemoteConfigSource', + url: 'http://example.com/config.yaml', + reloadIntervalSeconds: 5, + }, + ]); + }); + + it('should create a default source', () => { + expect( + mergeSources( + ConfigSources.default({ + rootDir: '/', + env: { HOME: '/' }, + }), + ), + ).toEqual([ + { name: 'FileConfigSource', path: '/app-config.yaml' }, + { name: 'EnvConfigSource', env: { HOME: '/' } }, + ]); + + expect( + mergeSources( + ConfigSources.default({ + rootDir: '/', + argv: ['--config', 'a.yaml', '--config=b.yaml'], + env: { HOME: '/' }, + }), + ), + ).toEqual([ + { name: 'FileConfigSource', path: 'a.yaml' }, + { name: 'FileConfigSource', path: 'b.yaml' }, + { name: 'EnvConfigSource', env: { HOME: '/' } }, + ]); + }); + + it('should merge sources', () => { + expect( + mergeSources( + ConfigSources.merge([ + ConfigSources.defaultForTargets({ + rootDir: '/', + targets: [ + { type: 'path', target: '/a.yaml' }, + { type: 'path', target: '/b.yaml' }, + { type: 'path', target: '/c.yaml' }, + ], + }), + ]), + ), + ).toEqual([ + { name: 'FileConfigSource', path: '/a.yaml' }, + { name: 'FileConfigSource', path: '/b.yaml' }, + { name: 'FileConfigSource', path: '/c.yaml' }, + ]); + }); + + it('should create an observable config', async () => { + const source = MutableConfigSource.create({ data: { a: 1 } }); + const config = await ConfigSources.toConfig(source); + const listener = jest.fn(); + const sub = config.subscribe?.(listener); + + expect(config.getNumber('a')).toBe(1); + expect(listener).not.toHaveBeenCalled(); + + source.setData({ a: 2 }); + await new Promise(resolve => setTimeout(resolve)); + expect(config.getNumber('a')).toBe(2); + expect(listener).toHaveBeenCalledTimes(1); + + sub?.unsubscribe(); + source.setData({ a: 3 }); + await new Promise(resolve => setTimeout(resolve)); + expect(config.getNumber('a')).toBe(3); + expect(listener).toHaveBeenCalledTimes(1); + + config.close(); + source.setData({ a: 4 }); + await new Promise(resolve => setTimeout(resolve)); + expect(config.getNumber('a')).toBe(3); + }); + + it('should fail to create config', async () => { + await expect( + ConfigSources.toConfig({ + async *readConfigData() { + throw new Error('NOPE'); + }, + }), + ).rejects.toThrow('NOPE'); + }); +}); diff --git a/packages/config-loader/src/sources/ConfigSources.ts b/packages/config-loader/src/sources/ConfigSources.ts index 13ab41cedd..e5501e5c2e 100644 --- a/packages/config-loader/src/sources/ConfigSources.ts +++ b/packages/config-loader/src/sources/ConfigSources.ts @@ -142,7 +142,7 @@ export class ConfigSources { if (arg.type === 'url') { if (!options.remote) { throw new Error( - `Config argument '${arg.target}' looks like a URL but remote configuration is not enabled. Enable it by passing the \`remote\` option`, + `Config argument "${arg.target}" looks like a URL but remote configuration is not enabled. Enable it by passing the \`remote\` option`, ); } return RemoteConfigSource.create({ @@ -200,7 +200,9 @@ export class ConfigSources { targets: this.parseArgs(options.argv), }); - const envSource = EnvConfigSource.create(options); + const envSource = EnvConfigSource.create({ + env: options.env, + }); return this.merge([argSource, envSource]); }