]>
Commit | Line | Data |
---|---|---|
eb39fafa DC |
1 | /** |
2 | * @fileoverview Tests for config validator. | |
3 | * @author Brandon Mills | |
4 | */ | |
5 | ||
6 | "use strict"; | |
7 | ||
8 | //------------------------------------------------------------------------------ | |
9 | // Requirements | |
10 | //------------------------------------------------------------------------------ | |
11 | ||
12 | const assert = require("chai").assert, | |
13 | { Linter } = require("../../../lib/linter"), | |
14 | validator = require("../../../lib/shared/config-validator"), | |
15 | Rules = require("../../../lib/linter/rules"); | |
16 | const linter = new Linter(); | |
17 | ||
18 | //------------------------------------------------------------------------------ | |
19 | // Tests | |
20 | //------------------------------------------------------------------------------ | |
21 | ||
22 | /** | |
23 | * Fake a rule object | |
24 | * @param {Object} context context passed to the rules by eslint | |
25 | * @returns {Object} mocked rule listeners | |
26 | * @private | |
27 | */ | |
28 | function mockRule(context) { | |
29 | return { | |
30 | Program(node) { | |
31 | context.report(node, "Expected a validation error."); | |
32 | } | |
33 | }; | |
34 | } | |
35 | ||
36 | mockRule.schema = [ | |
37 | { | |
38 | enum: ["first", "second"] | |
39 | } | |
40 | ]; | |
41 | ||
42 | /** | |
43 | * Fake a rule object | |
44 | * @param {Object} context context passed to the rules by eslint | |
45 | * @returns {Object} mocked rule listeners | |
46 | * @private | |
47 | */ | |
48 | function mockObjectRule(context) { | |
49 | return { | |
50 | Program(node) { | |
51 | context.report(node, "Expected a validation error."); | |
52 | } | |
53 | }; | |
54 | } | |
55 | ||
56 | mockObjectRule.schema = { | |
57 | enum: ["first", "second"] | |
58 | }; | |
59 | ||
60 | /** | |
61 | * Fake a rule with no options | |
62 | * @param {Object} context context passed to the rules by eslint | |
63 | * @returns {Object} mocked rule listeners | |
64 | * @private | |
65 | */ | |
66 | function mockNoOptionsRule(context) { | |
67 | return { | |
68 | Program(node) { | |
69 | context.report(node, "Expected a validation error."); | |
70 | } | |
71 | }; | |
72 | } | |
73 | ||
74 | mockNoOptionsRule.schema = []; | |
75 | ||
76 | const mockRequiredOptionsRule = { | |
77 | meta: { | |
78 | schema: { | |
79 | type: "array", | |
80 | minItems: 1 | |
81 | } | |
82 | }, | |
83 | create(context) { | |
84 | return { | |
85 | Program(node) { | |
86 | context.report(node, "Expected a validation error."); | |
87 | } | |
88 | }; | |
89 | } | |
90 | }; | |
91 | ||
92 | describe("Validator", () => { | |
93 | ||
94 | /** | |
95 | * Gets a loaded rule given a rule ID | |
96 | * @param {string} ruleId The ID of the rule | |
97 | * @returns {{create: Function}} The loaded rule | |
98 | */ | |
99 | function ruleMapper(ruleId) { | |
100 | return linter.getRules().get(ruleId) || new Rules().get(ruleId); | |
101 | } | |
102 | ||
103 | beforeEach(() => { | |
104 | linter.defineRule("mock-rule", mockRule); | |
105 | linter.defineRule("mock-required-options-rule", mockRequiredOptionsRule); | |
106 | }); | |
107 | ||
108 | describe("validate", () => { | |
109 | ||
110 | it("should do nothing with an empty config", () => { | |
111 | validator.validate({}, "tests", ruleMapper); | |
112 | }); | |
113 | ||
114 | it("should do nothing with a valid eslint config", () => { | |
115 | validator.validate( | |
116 | { | |
117 | $schema: "http://json.schemastore.org/eslintrc", | |
118 | root: true, | |
119 | globals: { globalFoo: "readonly" }, | |
120 | parser: "parserFoo", | |
121 | env: { browser: true }, | |
122 | plugins: ["pluginFoo", "pluginBar"], | |
123 | settings: { foo: "bar" }, | |
124 | extends: ["configFoo", "configBar"], | |
125 | parserOptions: { foo: "bar" }, | |
126 | rules: {} | |
127 | }, | |
128 | "tests", | |
129 | ruleMapper | |
130 | ); | |
131 | }); | |
132 | ||
133 | it("should throw with an unknown property", () => { | |
134 | const fn = validator.validate.bind( | |
135 | null, | |
136 | { | |
137 | foo: true | |
138 | }, | |
139 | "tests", | |
140 | ruleMapper | |
141 | ); | |
142 | ||
143 | assert.throws(fn, "Unexpected top-level property \"foo\"."); | |
144 | }); | |
145 | ||
146 | describe("root", () => { | |
147 | it("should throw with a string value", () => { | |
148 | const fn = validator.validate.bind(null, { root: "true" }, null, ruleMapper); | |
149 | ||
150 | assert.throws(fn, "Property \"root\" is the wrong type (expected boolean but got `\"true\"`)."); | |
151 | }); | |
152 | ||
153 | it("should throw with a numeric value", () => { | |
154 | const fn = validator.validate.bind(null, { root: 0 }, null, ruleMapper); | |
155 | ||
156 | assert.throws(fn, "Property \"root\" is the wrong type (expected boolean but got `0`)."); | |
157 | }); | |
158 | }); | |
159 | ||
160 | describe("globals", () => { | |
161 | it("should throw with a string value", () => { | |
162 | const fn = validator.validate.bind(null, { globals: "jQuery" }, null, ruleMapper); | |
163 | ||
164 | assert.throws(fn, "Property \"globals\" is the wrong type (expected object but got `\"jQuery\"`)."); | |
165 | }); | |
166 | ||
167 | it("should throw with an array value", () => { | |
168 | const fn = validator.validate.bind(null, { globals: ["jQuery"] }, null, ruleMapper); | |
169 | ||
170 | assert.throws(fn, "Property \"globals\" is the wrong type (expected object but got `[\"jQuery\"]`)."); | |
171 | }); | |
172 | }); | |
173 | ||
174 | describe("parser", () => { | |
175 | it("should not throw with a null value", () => { | |
176 | validator.validate({ parser: null }, null, ruleMapper); | |
177 | }); | |
178 | }); | |
179 | ||
180 | describe("env", () => { | |
181 | ||
182 | it("should throw with an array environment", () => { | |
183 | const fn = validator.validate.bind(null, { env: [] }, null, ruleMapper); | |
184 | ||
185 | assert.throws(fn, "Property \"env\" is the wrong type (expected object but got `[]`)."); | |
186 | }); | |
187 | ||
188 | it("should throw with a primitive environment", () => { | |
189 | const fn = validator.validate.bind(null, { env: 1 }, null, ruleMapper); | |
190 | ||
191 | assert.throws(fn, "Property \"env\" is the wrong type (expected object but got `1`)."); | |
192 | }); | |
193 | ||
194 | it("should catch invalid environments", () => { | |
195 | const fn = validator.validate.bind(null, { env: { browser: true, invalid: true } }, null, ruleMapper); | |
196 | ||
197 | assert.throws(fn, "Environment key \"invalid\" is unknown\n"); | |
198 | }); | |
199 | ||
200 | it("should catch disabled invalid environments", () => { | |
201 | const fn = validator.validate.bind(null, { env: { browser: true, invalid: false } }, null, ruleMapper); | |
202 | ||
203 | assert.throws(fn, "Environment key \"invalid\" is unknown\n"); | |
204 | }); | |
205 | ||
206 | it("should do nothing with an undefined environment", () => { | |
207 | validator.validate({}, null, ruleMapper); | |
208 | }); | |
209 | ||
210 | }); | |
211 | ||
212 | describe("plugins", () => { | |
213 | it("should not throw with an empty array", () => { | |
214 | validator.validate({ plugins: [] }, null, ruleMapper); | |
215 | }); | |
216 | ||
217 | it("should throw with a string", () => { | |
218 | const fn = validator.validate.bind(null, { plugins: "react" }, null, ruleMapper); | |
219 | ||
220 | assert.throws(fn, "Property \"plugins\" is the wrong type (expected array but got `\"react\"`)."); | |
221 | }); | |
222 | }); | |
223 | ||
224 | describe("settings", () => { | |
225 | it("should not throw with an empty object", () => { | |
226 | validator.validate({ settings: {} }, null, ruleMapper); | |
227 | }); | |
228 | ||
229 | it("should throw with an array", () => { | |
230 | const fn = validator.validate.bind(null, { settings: ["foo"] }, null, ruleMapper); | |
231 | ||
232 | assert.throws(fn, "Property \"settings\" is the wrong type (expected object but got `[\"foo\"]`)."); | |
233 | }); | |
234 | }); | |
235 | ||
236 | describe("extends", () => { | |
237 | it("should not throw with an empty array", () => { | |
238 | validator.validate({ extends: [] }, null, ruleMapper); | |
239 | }); | |
240 | ||
241 | it("should not throw with a string", () => { | |
242 | validator.validate({ extends: "react" }, null, ruleMapper); | |
243 | }); | |
244 | ||
245 | it("should throw with an object", () => { | |
246 | const fn = validator.validate.bind(null, { extends: {} }, null, ruleMapper); | |
247 | ||
248 | assert.throws(fn, "ESLint configuration in null is invalid:\n\t- Property \"extends\" is the wrong type (expected string but got `{}`).\n\t- Property \"extends\" is the wrong type (expected array but got `{}`).\n\t- \"extends\" should match exactly one schema in oneOf. Value: {}."); | |
249 | }); | |
250 | }); | |
251 | ||
252 | describe("parserOptions", () => { | |
253 | it("should not throw with an empty object", () => { | |
254 | validator.validate({ parserOptions: {} }, null, ruleMapper); | |
255 | }); | |
256 | ||
257 | it("should throw with an array", () => { | |
258 | const fn = validator.validate.bind(null, { parserOptions: ["foo"] }, null, ruleMapper); | |
259 | ||
260 | assert.throws(fn, "Property \"parserOptions\" is the wrong type (expected object but got `[\"foo\"]`)."); | |
261 | }); | |
262 | }); | |
263 | ||
264 | describe("rules", () => { | |
265 | ||
266 | it("should do nothing with an empty rules object", () => { | |
267 | validator.validate({ rules: {} }, "tests", ruleMapper); | |
268 | }); | |
269 | ||
270 | it("should do nothing with a valid config with rules", () => { | |
271 | validator.validate({ rules: { "mock-rule": [2, "second"] } }, "tests", ruleMapper); | |
272 | }); | |
273 | ||
274 | it("should do nothing with a valid config when severity is off", () => { | |
275 | validator.validate({ rules: { "mock-rule": ["off", "second"] } }, "tests", ruleMapper); | |
276 | }); | |
277 | ||
278 | it("should do nothing with an invalid config when severity is off", () => { | |
279 | validator.validate({ rules: { "mock-required-options-rule": "off" } }, "tests", ruleMapper); | |
280 | }); | |
281 | ||
282 | it("should do nothing with an invalid config when severity is an array with 'off'", () => { | |
283 | validator.validate({ rules: { "mock-required-options-rule": ["off"] } }, "tests", ruleMapper); | |
284 | }); | |
285 | ||
286 | it("should do nothing with a valid config when severity is warn", () => { | |
287 | validator.validate({ rules: { "mock-rule": ["warn", "second"] } }, "tests", ruleMapper); | |
288 | }); | |
289 | ||
290 | it("should do nothing with a valid config when severity is error", () => { | |
291 | validator.validate({ rules: { "mock-rule": ["error", "second"] } }, "tests", ruleMapper); | |
292 | }); | |
293 | ||
294 | it("should do nothing with a valid config when severity is Off", () => { | |
295 | validator.validate({ rules: { "mock-rule": ["Off", "second"] } }, "tests", ruleMapper); | |
296 | }); | |
297 | ||
298 | it("should do nothing with a valid config when severity is Warn", () => { | |
299 | validator.validate({ rules: { "mock-rule": ["Warn", "second"] } }, "tests", ruleMapper); | |
300 | }); | |
301 | ||
302 | it("should do nothing with a valid config when severity is Error", () => { | |
303 | validator.validate({ rules: { "mock-rule": ["Error", "second"] } }, "tests", ruleMapper); | |
304 | }); | |
305 | ||
306 | it("should catch invalid rule options", () => { | |
307 | const fn = validator.validate.bind(null, { rules: { "mock-rule": [3, "third"] } }, "tests", ruleMapper); | |
308 | ||
309 | assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '3').\n"); | |
310 | }); | |
311 | ||
312 | it("should allow for rules with no options", () => { | |
313 | linter.defineRule("mock-no-options-rule", mockNoOptionsRule); | |
314 | ||
315 | validator.validate({ rules: { "mock-no-options-rule": 2 } }, "tests", ruleMapper); | |
316 | }); | |
317 | ||
318 | it("should not allow options for rules with no options", () => { | |
319 | linter.defineRule("mock-no-options-rule", mockNoOptionsRule); | |
320 | ||
321 | const fn = validator.validate.bind(null, { rules: { "mock-no-options-rule": [2, "extra"] } }, "tests", ruleMapper); | |
322 | ||
323 | assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-no-options-rule\" is invalid:\n\tValue [\"extra\"] should NOT have more than 0 items.\n"); | |
324 | }); | |
325 | }); | |
326 | ||
327 | describe("globals", () => { | |
328 | it("should disallow globals set to invalid values", () => { | |
329 | assert.throws( | |
330 | () => validator.validate({ globals: { foo: "AAAAA" } }, "tests", ruleMapper), | |
331 | "ESLint configuration of global 'foo' in tests is invalid:\n'AAAAA' is not a valid configuration for a global (use 'readonly', 'writable', or 'off')" | |
332 | ); | |
333 | }); | |
334 | }); | |
335 | ||
336 | describe("overrides", () => { | |
337 | it("should not throw with an empty overrides array", () => { | |
338 | validator.validate({ overrides: [] }, "tests", ruleMapper); | |
339 | }); | |
340 | ||
341 | it("should not throw with a valid overrides array", () => { | |
342 | validator.validate({ overrides: [{ files: "*", rules: {} }] }, "tests", ruleMapper); | |
343 | }); | |
344 | ||
345 | it("should throw if override does not specify files", () => { | |
346 | const fn = validator.validate.bind(null, { overrides: [{ rules: {} }] }, "tests", ruleMapper); | |
347 | ||
348 | assert.throws(fn, "ESLint configuration in tests is invalid:\n\t- \"overrides[0]\" should have required property 'files'. Value: {\"rules\":{}}.\n"); | |
349 | }); | |
350 | ||
351 | it("should throw if override has an empty files array", () => { | |
352 | const fn = validator.validate.bind(null, { overrides: [{ files: [] }] }, "tests", ruleMapper); | |
353 | ||
354 | assert.throws(fn, "ESLint configuration in tests is invalid:\n\t- Property \"overrides[0].files\" is the wrong type (expected string but got `[]`).\n\t- \"overrides[0].files\" should NOT have fewer than 1 items. Value: [].\n\t- \"overrides[0].files\" should match exactly one schema in oneOf. Value: [].\n"); | |
355 | }); | |
356 | ||
357 | it("should not throw if override has nested overrides", () => { | |
358 | validator.validate({ overrides: [{ files: "*", overrides: [{ files: "*", rules: {} }] }] }, "tests", ruleMapper); | |
359 | }); | |
360 | ||
361 | it("should not throw if override extends", () => { | |
362 | validator.validate({ overrides: [{ files: "*", extends: "eslint-recommended" }] }, "tests", ruleMapper); | |
363 | }); | |
364 | ||
365 | it("should throw if override tries to set root", () => { | |
366 | const fn = validator.validate.bind(null, { overrides: [{ files: "*", root: "true" }] }, "tests", ruleMapper); | |
367 | ||
368 | assert.throws(fn, "ESLint configuration in tests is invalid:\n\t- Unexpected top-level property \"overrides[0].root\".\n"); | |
369 | }); | |
370 | ||
371 | describe("env", () => { | |
372 | ||
373 | it("should catch invalid environments", () => { | |
374 | const fn = validator.validate.bind(null, { overrides: [{ files: "*", env: { browser: true, invalid: true } }] }, null, ruleMapper); | |
375 | ||
376 | assert.throws(fn, "Environment key \"invalid\" is unknown\n"); | |
377 | }); | |
378 | ||
379 | it("should catch disabled invalid environments", () => { | |
380 | const fn = validator.validate.bind(null, { overrides: [{ files: "*", env: { browser: true, invalid: false } }] }, null, ruleMapper); | |
381 | ||
382 | assert.throws(fn, "Environment key \"invalid\" is unknown\n"); | |
383 | }); | |
384 | ||
385 | }); | |
386 | ||
387 | describe("rules", () => { | |
388 | ||
389 | it("should catch invalid rule options", () => { | |
390 | const fn = validator.validate.bind(null, { overrides: [{ files: "*", rules: { "mock-rule": [3, "third"] } }] }, "tests", ruleMapper); | |
391 | ||
392 | assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '3').\n"); | |
393 | }); | |
394 | ||
395 | it("should not allow options for rules with no options", () => { | |
396 | linter.defineRule("mock-no-options-rule", mockNoOptionsRule); | |
397 | ||
398 | const fn = validator.validate.bind(null, { overrides: [{ files: "*", rules: { "mock-no-options-rule": [2, "extra"] } }] }, "tests", ruleMapper); | |
399 | ||
400 | assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-no-options-rule\" is invalid:\n\tValue [\"extra\"] should NOT have more than 0 items.\n"); | |
401 | }); | |
402 | }); | |
403 | ||
404 | }); | |
405 | ||
406 | }); | |
407 | ||
408 | describe("getRuleOptionsSchema", () => { | |
409 | ||
410 | it("should return null for a missing rule", () => { | |
411 | assert.strictEqual(validator.getRuleOptionsSchema(ruleMapper("non-existent-rule")), null); | |
412 | }); | |
413 | ||
414 | it("should not modify object schema", () => { | |
415 | linter.defineRule("mock-object-rule", mockObjectRule); | |
416 | assert.deepStrictEqual(validator.getRuleOptionsSchema(ruleMapper("mock-object-rule")), { | |
417 | enum: ["first", "second"] | |
418 | }); | |
419 | }); | |
420 | ||
421 | }); | |
422 | ||
423 | describe("validateRuleOptions", () => { | |
424 | ||
425 | it("should throw for incorrect warning level number", () => { | |
426 | const fn = validator.validateRuleOptions.bind(null, ruleMapper("mock-rule"), "mock-rule", 3, "tests"); | |
427 | ||
428 | assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '3').\n"); | |
429 | }); | |
430 | ||
431 | it("should throw for incorrect warning level string", () => { | |
432 | const fn = validator.validateRuleOptions.bind(null, ruleMapper("mock-rule"), "mock-rule", "booya", "tests"); | |
433 | ||
434 | assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '\"booya\"').\n"); | |
435 | }); | |
436 | ||
437 | it("should throw for invalid-type warning level", () => { | |
438 | const fn = validator.validateRuleOptions.bind(null, ruleMapper("mock-rule"), "mock-rule", [["error"]], "tests"); | |
439 | ||
440 | assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '[ \"error\" ]').\n"); | |
441 | }); | |
442 | ||
443 | it("should only check warning level for nonexistent rules", () => { | |
444 | const fn = validator.validateRuleOptions.bind(null, ruleMapper("non-existent-rule"), "non-existent-rule", [3, "foobar"], "tests"); | |
445 | ||
446 | assert.throws(fn, "tests:\n\tConfiguration for rule \"non-existent-rule\" is invalid:\n\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '3').\n"); | |
447 | }); | |
448 | ||
449 | it("should only check warning level for plugin rules", () => { | |
450 | const fn = validator.validateRuleOptions.bind(null, ruleMapper("plugin/rule"), "plugin/rule", 3, "tests"); | |
451 | ||
452 | assert.throws(fn, "tests:\n\tConfiguration for rule \"plugin/rule\" is invalid:\n\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '3').\n"); | |
453 | }); | |
454 | ||
455 | it("should throw for incorrect configuration values", () => { | |
456 | const fn = validator.validateRuleOptions.bind(null, ruleMapper("mock-rule"), "mock-rule", [2, "frist"], "tests"); | |
457 | ||
458 | assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tValue \"frist\" should be equal to one of the allowed values.\n"); | |
459 | }); | |
460 | ||
461 | it("should throw for too many configuration values", () => { | |
462 | const fn = validator.validateRuleOptions.bind(null, ruleMapper("mock-rule"), "mock-rule", [2, "first", "second"], "tests"); | |
463 | ||
464 | assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tValue [\"first\",\"second\"] should NOT have more than 1 items.\n"); | |
465 | }); | |
466 | ||
467 | }); | |
468 | ||
469 | }); |