2
* Copyright 2020 The Backstage Authors
4
* Licensed under the Apache License, Version 2.0 (the "License");
5
* you may not use this file except in compliance with the License.
6
* You may obtain a copy of the License at
8
* http://www.apache.org/licenses/LICENSE-2.0
10
* Unless required by applicable law or agreed to in writing, software
11
* distributed under the License is distributed on an "AS IS" BASIS,
12
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
* See the License for the specific language governing permissions and
14
* limitations under the License.
17
import { withLogCollector } from '@backstage/test-utils';
18
import { ConfigReader } from './reader';
37
strings: ['string1', 'string2'],
38
badStrings: ['string1', ''],
39
worseStrings: ['string1', 3] as string[],
40
worstStrings: ['string1', 'string2', {}] as string[],
45
strings: ['string1', 'string2'],
47
nestlings: [{ boolean: true }, { string: 'string' }, { number: 42 }] as {}[],
50
function expectValidValues(config: ConfigReader) {
51
expect(config.keys()).toEqual(Object.keys(DATA));
52
expect(config.get('zero')).toBe(0);
53
expect(config.has('zero')).toBe(true);
54
expect(config.has('false')).toBe(true);
55
expect(config.has('null')).toBe(true);
56
expect(config.has('missing')).toBe(false);
57
expect(config.has('nested.one')).toBe(true);
58
expect(config.has('nested.missing')).toBe(false);
59
expect(config.has('nested.null')).toBe(true);
60
expect(config.getNumber('zero')).toBe(0);
61
expect(config.getNumber('one')).toBe(1);
62
expect(config.getNumber('zeroString')).toBe(0);
63
expect(config.getNumber('oneString')).toBe(1);
64
expect(config.getOptional('true')).toBe(true);
65
expect(config.getBoolean('true')).toBe(true);
66
expect(config.getBoolean('false')).toBe(false);
67
expect(config.getBoolean('stringFalse')).toBe(false);
68
expect(config.getBoolean('zero')).toBe(false);
69
expect(config.getBoolean('one')).toBe(true);
70
expect(config.getBoolean('zeroString')).toBe(false);
71
expect(config.getBoolean('oneString')).toBe(true);
72
expect(config.getBoolean('yes')).toBe(true);
73
expect(config.getBoolean('no')).toBe(false);
74
expect(config.getBoolean('y')).toBe(true);
75
expect(config.getBoolean('n')).toBe(false);
76
expect(config.getBoolean('on')).toBe(true);
77
expect(config.getBoolean('off')).toBe(false);
78
expect(config.getString('string')).toBe('string');
79
expect(config.get('strings')).toEqual(['string1', 'string2']);
80
expect(config.getStringArray('strings')).toEqual(['string1', 'string2']);
81
expect(config.getConfig('nested').getNumber('one')).toBe(1);
82
expect(config.get('nested')).toEqual({
86
strings: ['string1', 'string2'],
88
expect(config.getConfig('nested').getString('string')).toBe('string');
89
expect(config.getOptionalConfig('nested')!.getStringArray('strings')).toEqual(
90
['string1', 'string2'],
92
expect(config.getOptional('missing')).toBe(undefined);
93
expect(config.getOptionalConfig('missing')).toBe(undefined);
94
expect(config.getOptionalConfigArray('missing')).toBe(undefined);
95
expect(config.getNumber('zero')).toBe(0);
96
expect(config.getBoolean('true')).toBe(true);
97
expect(config.getString('string')).toBe('string');
98
expect(config.getStringArray('strings')).toEqual(['string1', 'string2']);
100
const [config1, config2, config3] = config.getConfigArray('nestlings');
101
expect(config1.getBoolean('boolean')).toBe(true);
102
expect(config2.getString('string')).toBe('string');
103
expect(config3.getNumber('number')).toBe(42);
105
config.getOptionalConfigArray('nestlings')![0].getBoolean('boolean'),
109
function expectInvalidValues(config: ConfigReader) {
110
expect(() => config.getBoolean('string')).toThrow(
111
"Unable to convert config value for key 'string' in 'ctx' to a boolean",
113
expect(() => config.getNumber('string')).toThrow(
114
"Unable to convert config value for key 'string' in 'ctx' to a number",
116
expect(() => config.getString('one')).toThrow(
117
"Invalid type in config for key 'one' in 'ctx', got number, wanted string",
119
expect(() => config.getNumber('true')).toThrow(
120
"Invalid type in config for key 'true' in 'ctx', got boolean, wanted number",
122
expect(() => config.getStringArray('null')).toThrow(
123
"Invalid type in config for key 'null' in 'ctx', got null, wanted string-array",
125
expect(() => config.getString('emptyString')).toThrow(
126
"Invalid type in config for key 'emptyString' in 'ctx', got empty-string, wanted string",
128
expect(() => config.getStringArray('badStrings')).toThrow(
129
"Invalid type in config for key 'badStrings[1]' in 'ctx', got empty-string, wanted string",
131
expect(() => config.getStringArray('worseStrings')).toThrow(
132
"Invalid type in config for key 'worseStrings[1]' in 'ctx', got number, wanted string",
134
expect(() => config.getStringArray('worstStrings')).toThrow(
135
"Invalid type in config for key 'worstStrings[2]' in 'ctx', got object, wanted string",
137
expect(() => config.getConfig('one')).toThrow(
138
"Invalid type in config for key 'one' in 'ctx', got number, wanted object",
140
expect(() => config.getConfigArray('one')).toThrow(
141
"Invalid type in config for key 'one' in 'ctx', got number, wanted object-array",
143
expect(() => config.getBoolean('missing')).toThrow(
144
"Missing required config value at 'missing'",
146
expect(() => config.getNumber('missing')).toThrow(
147
"Missing required config value at 'missing'",
149
expect(() => config.getString('missing')).toThrow(
150
"Missing required config value at 'missing'",
152
expect(() => config.getStringArray('missing')).toThrow(
153
"Missing required config value at 'missing'",
159
describe('ConfigReader', () => {
160
it('should read empty config with valid keys', () => {
161
const config = new ConfigReader({}, CTX);
162
expect(config.keys()).toEqual([]);
163
expect(config.getOptional()).toEqual({});
164
expect(config.getOptional('x')).toBeUndefined();
165
expect(config.getOptionalString('x')).toBeUndefined();
166
expect(config.getOptionalString('x_x')).toBeUndefined();
167
expect(config.getOptionalString('x-X')).toBeUndefined();
168
expect(config.getOptionalString('x0')).toBeUndefined();
169
expect(config.getOptionalString('X-x2')).toBeUndefined();
170
expect(config.getOptionalString('x0_x0')).toBeUndefined();
171
expect(config.getOptionalString('x_x-x_x')).toBeUndefined();
174
new ConfigReader(undefined, CTX).getOptionalString('x'),
178
it('should throw on invalid keys', () => {
179
const config = new ConfigReader({}, CTX);
181
expect(() => config.has('.')).toThrow(/^Invalid config key/);
182
expect(() => config.get('0')).toThrow(/^Invalid config key/);
183
expect(() => config.getOptional('(')).toThrow(/^Invalid config key/);
184
expect(() => config.getString('z-_')).toThrow(/^Invalid config key/);
185
expect(() => config.getOptionalString('-')).toThrow(/^Invalid config key/);
186
expect(() => config.getNumber('.a')).toThrow(/^Invalid config key/);
187
expect(() => config.getConfig('0.a')).toThrow(/^Invalid config key/);
188
expect(() => config.getOptionalConfig('0a')).toThrow(/^Invalid config key/);
189
expect(() => config.getString('a.0a')).toThrow(/^Invalid config key/);
190
expect(() => config.getString('a..a')).toThrow(/^Invalid config key/);
191
expect(() => config.getString('a.')).toThrow(/^Invalid config key/);
192
expect(() => config.getString('a...')).toThrow(/^Invalid config key/);
193
expect(() => config.getString('a.a.a.a.')).toThrow(/^Invalid config key/);
194
expect(() => config.getString('a._')).toThrow(/^Invalid config key/);
195
expect(() => config.getString('a.-.a')).toThrow(/^Invalid config key/);
197
expect(() => new ConfigReader(undefined, CTX).getString('.')).toThrow(
198
/^Invalid config key/,
202
it('should read valid values', () => {
203
const config = new ConfigReader(DATA, CTX);
204
expectValidValues(config);
207
it('should fail to read invalid values', () => {
208
const config = new ConfigReader(DATA, CTX);
209
expectInvalidValues(config);
212
it('should warn when accessing filtered keys in development mode', () => {
213
const oldEnv = process.env.NODE_ENV;
214
(process.env as any).NODE_ENV = 'development';
216
const config = ConfigReader.fromConfigs([
220
filteredKeys: ['a', 'a2', 'b[0]'],
224
expect(withLogCollector(() => config.getOptional('a'))).toMatchObject({
226
"Failed to read configuration value at 'a' as it is not visible. See https://backstage.io/docs/conf/defining#visibility for instructions on how to make it visible.",
230
withLogCollector(() => config.getOptionalString('a2')),
233
"Failed to read configuration value at 'a2' as it is not visible. See https://backstage.io/docs/conf/defining#visibility for instructions on how to make it visible.",
237
withLogCollector(() => config.getOptionalConfigArray('b')),
240
"Failed to read configuration array at 'b' as it does not have any visible elements. See https://backstage.io/docs/conf/defining#visibility for instructions on how to make it visible.",
244
(process.env as any).NODE_ENV = oldEnv;
247
it('only warns once when accessing filtered keys in development mode', () => {
248
const oldEnv = process.env.NODE_ENV;
249
(process.env as any).NODE_ENV = 'development';
251
const config = ConfigReader.fromConfigs([
259
expect(withLogCollector(() => config.getOptional('a'))).toMatchObject({
261
"Failed to read configuration value at 'a' as it is not visible. See https://backstage.io/docs/conf/defining#visibility for instructions on how to make it visible.",
264
expect(withLogCollector(() => config.getOptional('a'))).toMatchObject({
268
(process.env as any).NODE_ENV = oldEnv;
271
it('should not warn when accessing filtered keys outside of development mode', () => {
272
const config = ConfigReader.fromConfigs([
276
filteredKeys: ['a', 'b[0]'],
280
expect(withLogCollector(() => config.getOptional('a'))).toMatchObject({
283
expect(withLogCollector(() => config.getOptionalString('a'))).toMatchObject(
287
withLogCollector(() => config.getOptionalConfigArray('b')),
288
).toMatchObject({ warn: [] });
291
it('should coerce number strings to numbers', () => {
292
const config = ConfigReader.fromConfigs([
301
expect(config.getNumber('port')).toEqual(123);
305
describe('ConfigReader with fallback', () => {
306
it('should behave as if without fallback', () => {
307
const config = new ConfigReader({}, CTX, new ConfigReader(DATA, CTX));
308
expect(config.getOptionalString('x')).toBeUndefined();
309
expect(() => config.getString('.')).toThrow(/^Invalid config key/);
310
expect(() => config.getString('a.')).toThrow(/^Invalid config key/);
313
it('should read values from itself', () => {
314
const config = new ConfigReader(DATA, CTX, new ConfigReader({}, CTX));
315
expectValidValues(config);
316
expectInvalidValues(config);
319
it('should read values from a fallback', () => {
320
const config = new ConfigReader({}, CTX, new ConfigReader(DATA, CTX));
321
expectValidValues(config);
322
expectInvalidValues(config);
325
it('should read values from multiple levels of fallbacks', () => {
326
const config = new ConfigReader(
332
new ConfigReader({}, CTX, new ConfigReader(DATA, CTX)),
335
expectValidValues(config);
336
expectInvalidValues(config);
339
it('should show error with correct context', () => {
340
const config = ConfigReader.fromConfigs([
379
expect(() => config.getNumber('a')).toThrow(
380
"Invalid type in config for key 'a' in 'z', got boolean, wanted number",
382
expect(() => config.getNumber('b')).toThrow(
383
"Invalid type in config for key 'b' in 'y', got boolean, wanted number",
385
expect(() => config.getNumber('c')).toThrow(
386
"Invalid type in config for key 'c' in 'x', got boolean, wanted number",
388
expect(() => config.getNumber('nested1.a')).toThrow(
389
"Invalid type in config for key 'nested1.a' in 'y', got boolean, wanted number",
391
expect(() => config.getNumber('nested1.b')).toThrow(
392
"Invalid type in config for key 'nested1.b' in 'z', got boolean, wanted number",
394
expect(() => config.getConfig('nested1').getNumber('a')).toThrow(
395
"Invalid type in config for key 'nested1.a' in 'y', got boolean, wanted number",
397
expect(() => config.getConfig('nested1').getNumber('b')).toThrow(
398
"Invalid type in config for key 'nested1.b' in 'z', got boolean, wanted number",
400
expect(() => config.getNumber('badBefore.a')).toThrow(
401
"Invalid type in config for key 'badBefore' in 'y', got boolean, wanted object",
403
expect(() => config.getNumber('badBefore.b')).toThrow(
404
"Invalid type in config for key 'badBefore' in 'y', got boolean, wanted object",
406
expect(() => config.getNumber('badAfter.a')).toThrow(
407
"Invalid type in config for key 'badAfter.a' in 'y', got boolean, wanted number",
409
expect(() => config.getNumber('badAfter.b')).toThrow(
410
"Invalid type in config for key 'badAfter' in 'z', got boolean, wanted object",
414
it('should read merged objects', () => {
421
configs: [{ a: 'a' }],
430
configs: [{ b: 'b' }],
434
const config = new ConfigReader(a, CTX, new ConfigReader(b, CTX));
436
expect(config.keys()).toEqual(['merged']);
437
expect(config.has('merged.x')).toBe(true);
438
expect(config.has('merged.y')).toBe(true);
439
expect(config.has('merged.w')).toBe(false);
440
expect(config.getConfig('merged').has('x')).toBe(true);
441
expect(config.getConfig('merged').has('y')).toBe(true);
442
expect(config.getConfig('merged').has('w')).toBe(false);
443
expect(config.getConfig('merged').keys()).toEqual([
451
expect(config.getConfig('merged.config').keys()).toEqual(['d', 'e']);
453
expect(config.getString('merged.x')).toBe('x');
454
expect(config.getString('merged.y')).toBe('y');
455
expect(config.getString('merged.z')).toBe('z1');
456
expect(config.getConfig('merged').getString('x')).toBe('x');
457
expect(config.getConfig('merged').getString('y')).toBe('y');
458
expect(config.getConfig('merged').getString('z')).toBe('z1');
459
expect(config.getString('merged.config.d')).toBe('d');
460
expect(config.getString('merged.config.e')).toBe('e');
461
expect(config.getConfig('merged').getString('config.d')).toBe('d');
462
expect(config.getConfig('merged').getString('config.e')).toBe('e');
463
expect(config.getConfig('merged').getConfig('config').getString('d')).toBe(
466
expect(config.getConfig('merged').getConfig('config').getString('e')).toBe(
470
// Arrays are not merged
471
expect(config.getStringArray('merged.arr')).toEqual(['a', 'b']);
472
expect(config.getConfig('merged').getStringArray('arr')).toEqual([
476
expect(() => config.getConfig('merged').getStringArray('x')).toThrow(
477
"Invalid type in config for key 'merged.x' in 'ctx', got string, wanted string-array",
480
// Config arrays aren't merged either
481
expect(config.getConfigArray('merged.configs').length).toBe(1);
482
expect(config.getConfigArray('merged.configs')[0].getString('a')).toBe('a');
483
expect(config.getConfigArray('merged.configs')[0].getString('a')).toBe('a');
485
config.getConfigArray('merged.configs')[0].getString('missing'),
486
).toThrow("Missing required config value at 'merged.configs[0].missing'");
488
config.getConfigArray('merged.configs')[0].getOptionalString('b'),
491
// Config arrays aren't merged either
492
expect(config.getConfig('merged').getConfigArray('configs').length).toBe(1);
494
config.getConfig('merged').getConfigArray('configs')[0].getString('a'),
499
.getConfigArray('configs')[0]
500
.getOptionalString('b'),
505
describe('ConfigReader.get()', () => {
509
y: ['y11', 'y12', 'y13'],
549
it('should be able to select sub-configs', () => {
550
expect(new ConfigReader(config1).get('a')).toEqual(config1.a);
551
expect(new ConfigReader(config1).get('b')).toEqual(config1.b);
552
expect(new ConfigReader(config2).get('b')).toEqual(config2.b);
553
expect(new ConfigReader(config2).get('c')).toEqual(config2.c);
554
expect(new ConfigReader(config3).get('c')).toEqual(config3.c);
555
expect(new ConfigReader(config2).get('c.c1')).toEqual(config2.c.c1);
556
expect(new ConfigReader(config2).getConfig('c').get('c1')).toEqual(
561
it('should merge in fallback configs', () => {
563
ConfigReader.fromConfigs([configs[0], configs[1], configs[2]]).get(),
567
y: ['y11', 'y12', 'y13'],
577
expect(ConfigReader.fromConfigs([configs[1], configs[0]]).get('a')).toEqual(
580
y: ['y11', 'y12', 'y13'],
584
expect(ConfigReader.fromConfigs([configs[1], configs[0]]).get('b')).toEqual(
591
expect(ConfigReader.fromConfigs([configs[1], configs[0]]).get('c')).toEqual(
598
expect(ConfigReader.fromConfigs([configs[1], configs[0]]).get('a')).toEqual(
601
y: ['y11', 'y12', 'y13'],
605
expect(ConfigReader.fromConfigs([configs[1], configs[0]]).get('b')).toEqual(
612
expect(ConfigReader.fromConfigs([configs[1], configs[0]]).get('c')).toEqual(
621
ConfigReader.fromConfigs([configs[1], configs[2]]).getOptional('b'),
628
ConfigReader.fromConfigs([configs[1], configs[2]]).getOptional('c'),
634
it('should not merge non-objects', () => {
635
const config = ConfigReader.fromConfigs([
672
expect(config.get('a')).toEqual(['1', '2']);
673
expect(config.get('b')).toEqual(['1']);
674
expect(config.get('c')).toEqual([]);
675
expect(config.get('d')).toEqual({ x: 'x' });
676
expect(config.get('e')).toEqual(['3']);
677
expect(config.get('f')).toEqual('foo');
678
expect(config.get('g')).toEqual({ z: 'z' });
679
expect(config.get('h')).toEqual({ a: 'a1', b: 'b2', c: 'c1' });
680
expect(config.getConfig('h').get()).toEqual({ a: 'a1', b: 'b2', c: 'c1' });
681
expect(config.getOptional()).toEqual({
699
it('should return deep clones of the backing data', () => {
714
const reader = ConfigReader.fromConfigs([
715
{ data: data1, context: '1' },
716
{ data: data2, context: '2' },
719
reader.get<any>().foo.bar.push(1);
720
reader.get<any>('foo').bar.push(1);
721
reader.get<any>('foo.bar').push(1);
722
reader.get<any>().foo.baz.x = 1;
723
reader.get<any>('foo').baz.x = 1;
724
reader.get<any>('foo.baz').x = 1;
725
reader.get<any>().x.y.z.w = 1;
726
reader.get<any>('x').y.z.w = 1;
727
reader.get<any>('x.y').z.w = 1;
728
reader.get<any>('x.y.z').w = 1;
730
const readerSingle = ConfigReader.fromConfigs([
731
{ data: data1, context: '1' },
734
readerSingle.get<any>().foo.bar.push(1);
735
readerSingle.get<any>('foo').bar.push(1);
736
readerSingle.get<any>('foo.bar').push(1);
737
readerSingle.get<any>().foo.baz.x = 1;
738
readerSingle.get<any>('foo').baz.x = 1;
739
readerSingle.get<any>('foo.baz').x = 1;
741
expect(data1.foo.bar).toEqual([]);
742
expect(data1.foo.baz).toEqual({});
743
expect(data2.x.y.z).toEqual({});