Skip to content

Commit ce0d4ae

Browse files
committed
init throws for partial configs
1 parent b6781c4 commit ce0d4ae

File tree

2 files changed

+107
-7
lines changed

2 files changed

+107
-7
lines changed

src/feature-toggles.js

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -278,25 +278,51 @@ class FeatureToggles {
278278
...(sourceFilepath && { sourceFilepath }),
279279
},
280280
},
281-
"feature configuration is not an object"
281+
"configuration is not an object"
282282
);
283283
}
284284

285285
const { type, active, appUrl, fallbackValue, validations } = value;
286286

287+
if ([undefined, null].includes(fallbackValue)) {
288+
throw new VError(
289+
{
290+
name: VERROR_CLUSTER_NAME,
291+
info: {
292+
featureKey,
293+
source,
294+
...(sourceFilepath && { sourceFilepath }),
295+
},
296+
},
297+
"configuration has no or invalid fallback value"
298+
);
299+
}
300+
301+
if (!FEATURE_VALID_TYPES.includes(type)) {
302+
throw new VError(
303+
{
304+
name: VERROR_CLUSTER_NAME,
305+
info: {
306+
featureKey,
307+
source,
308+
...(sourceFilepath && { sourceFilepath }),
309+
},
310+
},
311+
"configuration has no or invalid type"
312+
);
313+
}
314+
287315
this.__fallbackValues[featureKey] = fallbackValue;
288316
this.__config[featureKey] = {};
289317

318+
this.__config[featureKey][CONFIG_KEY.TYPE] = type;
319+
290320
this.__config[featureKey][CONFIG_KEY.SOURCE] = source;
291321

292322
if (sourceFilepath) {
293323
this.__config[featureKey][CONFIG_KEY.SOURCE_FILEPATH] = sourceFilepath;
294324
}
295325

296-
if (type) {
297-
this.__config[featureKey][CONFIG_KEY.TYPE] = type;
298-
}
299-
300326
if (active === false) {
301327
this.__config[featureKey][CONFIG_KEY.ACTIVE] = false;
302328
}
@@ -334,7 +360,7 @@ class FeatureToggles {
334360
configRuntime
335361
);
336362

337-
// NOTE: this post-processing is easier to do after the config is merged
363+
// NOTE: this post-processing is easier to do after the configuration is merged
338364
this.__featureKeys = Object.keys(this.__fallbackValues);
339365
for (const featureKey of this.__featureKeys) {
340366
const validations = this.__config[featureKey][CONFIG_KEY.VALIDATIONS];

test/integration-local/feature-toggles.integration.test.js

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ jest.mock("fs", () => ({
1111
access: mockAccess,
1212
}));
1313

14+
const VError = require("verror");
15+
1416
const { stateFromInfo } = require("../__common__/from-info");
1517
const { FEATURE, mockConfig: config } = require("../__common__/mockdata");
1618

@@ -88,8 +90,80 @@ describe("local integration test", () => {
8890
test("init fails processing for bad formats", async () => {
8991
const badConfig = { ...configForRuntime, bla: undefined };
9092
await expect(toggles.initializeFeatures({ config: badConfig })).rejects.toMatchInlineSnapshot(
91-
`[FeatureTogglesError: initialization aborted, could not process configuration: feature configuration is not an object]`
93+
`[FeatureTogglesError: initialization aborted, could not process configuration: configuration is not an object]`
94+
);
95+
});
96+
97+
test("init throws for missing config type", async () => {
98+
const partialConfig = { [FEATURE.A]: { fallbackValue: 1 } };
99+
await expect(toggles.initializeFeatures({ config: partialConfig })).rejects.toMatchInlineSnapshot(
100+
`[FeatureTogglesError: initialization aborted, could not process configuration: configuration has no or invalid type]`
101+
);
102+
});
103+
104+
test("init throws for invalid config type", async () => {
105+
const partialConfig = { [FEATURE.A]: { type: "integer", fallbackValue: 1 } };
106+
await expect(toggles.initializeFeatures({ config: partialConfig })).rejects.toMatchInlineSnapshot(
107+
`[FeatureTogglesError: initialization aborted, could not process configuration: configuration has no or invalid type]`
108+
);
109+
});
110+
111+
test("init throws for missing config fallback value", async () => {
112+
const partialConfig = { [FEATURE.A]: { type: "number" } };
113+
await expect(toggles.initializeFeatures({ config: partialConfig })).rejects.toMatchInlineSnapshot(
114+
`[FeatureTogglesError: initialization aborted, could not process configuration: configuration has no or invalid fallback value]`
115+
);
116+
});
117+
118+
test("init throws for invalid config fallback value", async () => {
119+
const partialConfig = { [FEATURE.A]: { type: "number", fallbackValue: null } };
120+
await expect(toggles.initializeFeatures({ config: partialConfig })).rejects.toMatchInlineSnapshot(
121+
`[FeatureTogglesError: initialization aborted, could not process configuration: configuration has no or invalid fallback value]`
122+
);
123+
});
124+
125+
test("init config conflict with for first partial configs throws", async () => {
126+
const partialFileConfig = { [FEATURE.C]: { type: "number" } };
127+
128+
mockReadFile.mockImplementationOnce((path, cb) => cb(null, Buffer.from(JSON.stringify(partialFileConfig))));
129+
mockReadFile.mockImplementationOnce((path, cb) => cb(null, Buffer.from(JSON.stringify(configForFile))));
130+
131+
let caught;
132+
await toggles
133+
.initializeFeatures({ configFiles: ["toggles-1.json", "toggles-2.json"] })
134+
.catch((err) => (caught = err));
135+
expect(caught).toMatchInlineSnapshot(
136+
`[FeatureTogglesError: initialization aborted, could not process configuration: configuration has no or invalid fallback value]`
92137
);
138+
expect(VError.info(caught)).toMatchInlineSnapshot(`
139+
{
140+
"featureKey": "test/feature_c",
141+
"source": "FILE",
142+
"sourceFilepath": "toggles-1.json",
143+
}
144+
`);
145+
});
146+
147+
test("init config conflict with for second partial configs throws", async () => {
148+
const partialFileConfig = { [FEATURE.C]: { type: "number" } };
149+
150+
mockReadFile.mockImplementationOnce((path, cb) => cb(null, Buffer.from(JSON.stringify(configForFile))));
151+
mockReadFile.mockImplementationOnce((path, cb) => cb(null, Buffer.from(JSON.stringify(partialFileConfig))));
152+
153+
let caught;
154+
await toggles
155+
.initializeFeatures({ configFiles: ["toggles-1.json", "toggles-2.json"] })
156+
.catch((err) => (caught = err));
157+
expect(caught).toMatchInlineSnapshot(
158+
`[FeatureTogglesError: initialization aborted, could not process configuration: configuration has no or invalid fallback value]`
159+
);
160+
expect(VError.info(caught)).toMatchInlineSnapshot(`
161+
{
162+
"featureKey": "test/feature_c",
163+
"source": "FILE",
164+
"sourceFilepath": "toggles-2.json",
165+
}
166+
`);
93167
});
94168

95169
test("init config conflict between file and runtime overrides in favor of runtime", async () => {

0 commit comments

Comments
 (0)