1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 | import { Container } from 'inversify';
|
18 | import { PreferenceValidationService } from './preference-validation-service';
|
19 | import { PreferenceItem, PreferenceSchemaProvider } from './preference-contribution';
|
20 | import { PreferenceLanguageOverrideService } from './preference-language-override-service';
|
21 | import * as assert from 'assert';
|
22 | import { JSONValue } from '@phosphor/coreutils';
|
23 | import { IJSONSchema, JsonType } from '../../common/json-schema';
|
24 |
|
25 |
|
26 |
|
27 | describe('Preference Validation Service', () => {
|
28 | const container = new Container();
|
29 | container.bind(PreferenceSchemaProvider).toConstantValue({ getDefaultValue: PreferenceSchemaProvider.prototype.getDefaultValue } as PreferenceSchemaProvider);
|
30 | container.bind(PreferenceLanguageOverrideService).toSelf().inSingletonScope();
|
31 | const validator = container.resolve(PreferenceValidationService);
|
32 | const validateBySchema: (value: JSONValue, schema: PreferenceItem) => JSONValue = validator.validateBySchema.bind(validator, 'dummy');
|
33 |
|
34 | describe('should validate strings', () => {
|
35 | const expected = 'expected';
|
36 | it('good input -> should return the same string', () => {
|
37 | const actual = validateBySchema(expected, { type: 'string' });
|
38 | assert.strictEqual(actual, expected);
|
39 | });
|
40 | it('bad input -> should return the default', () => {
|
41 | const actual = validateBySchema(3, { type: 'string', default: expected });
|
42 | assert.strictEqual(actual, expected);
|
43 | });
|
44 | it('bad input -> should return string even if default is not a string', () => {
|
45 | const actual = validateBySchema(3, { type: 'string', default: 3 });
|
46 | assert.strictEqual(typeof actual, 'string');
|
47 | assert.strictEqual(actual, '3');
|
48 | });
|
49 | it('bad input -> should return an empty string if no default', () => {
|
50 | const actual = validateBySchema(3, { type: 'string' });
|
51 | assert.strictEqual(actual, '');
|
52 | });
|
53 | });
|
54 | describe('should validate numbers', () => {
|
55 | const expected = 1.23;
|
56 | it('good input -> should return the same number', () => {
|
57 | const actual = validateBySchema(expected, { type: 'number' });
|
58 | assert.strictEqual(actual, expected);
|
59 | });
|
60 | it('bad input -> should return the default', () => {
|
61 | const actual = validateBySchema('zxy', { type: 'number', default: expected });
|
62 | assert.strictEqual(actual, expected);
|
63 | });
|
64 | it('bad input -> should return a number even if the default is not a number', () => {
|
65 | const actual = validateBySchema('zxy', { type: 'number', default: ['fun array'] });
|
66 | assert.strictEqual(actual, 0);
|
67 | });
|
68 | it('bad input -> should return 0 if no default', () => {
|
69 | const actual = validateBySchema('zxy', { type: 'number' });
|
70 | assert.strictEqual(actual, 0);
|
71 | });
|
72 | it('should do its best to make a number of a string', () => {
|
73 | const actual = validateBySchema(expected.toString(), { type: 'number' });
|
74 | assert.strictEqual(actual, expected);
|
75 | });
|
76 | it('should return the max if input is greater than max', () => {
|
77 | const maximum = 50;
|
78 | const actual = validateBySchema(100, { type: 'number', maximum });
|
79 | assert.strictEqual(actual, maximum);
|
80 | });
|
81 | it('should return the minimum if input is less than minimum', () => {
|
82 | const minimum = 30;
|
83 | const actual = validateBySchema(15, { type: 'number', minimum });
|
84 | assert.strictEqual(actual, minimum);
|
85 | });
|
86 | });
|
87 | describe('should validate integers', () => {
|
88 | const expected = 2;
|
89 | it('good input -> should return the same number', () => {
|
90 | const actual = validateBySchema(expected, { type: 'integer' });
|
91 | assert.strictEqual(actual, expected);
|
92 | });
|
93 | it('bad input -> should return the default', () => {
|
94 | const actual = validateBySchema('zxy', { type: 'integer', default: expected });
|
95 | assert.strictEqual(actual, expected);
|
96 | });
|
97 | it('bad input -> should return 0 if no default', () => {
|
98 | const actual = validateBySchema('zxy', { type: 'integer' });
|
99 | assert.strictEqual(actual, 0);
|
100 | });
|
101 | it('should round a non-integer', () => {
|
102 | const actual = validateBySchema(1.75, { type: 'integer' });
|
103 | assert.strictEqual(actual, expected);
|
104 | });
|
105 | });
|
106 | describe('should validate booleans', () => {
|
107 | it('good input -> should return the same value', () => {
|
108 | assert.strictEqual(validateBySchema(true, { type: 'boolean' }), true);
|
109 | assert.strictEqual(validateBySchema(false, { type: 'boolean' }), false);
|
110 | });
|
111 | it('bad input -> should return the default', () => {
|
112 | const actual = validateBySchema(['not a boolean!'], { type: 'boolean', default: true });
|
113 | assert.strictEqual(actual, true);
|
114 | });
|
115 | it('bad input -> should return false if no default', () => {
|
116 | const actual = validateBySchema({ isBoolean: 'no' }, { type: 'boolean' });
|
117 | assert.strictEqual(actual, false);
|
118 | });
|
119 | it('should treat string "true" and "false" as equivalent to booleans', () => {
|
120 | assert.strictEqual(validateBySchema('true', { type: 'boolean' }), true);
|
121 | assert.strictEqual(validateBySchema('false', { type: 'boolean' }), false);
|
122 | });
|
123 | });
|
124 | describe('should validate null', () => {
|
125 | it('should always just return null', () => {
|
126 | assert.strictEqual(validateBySchema({ whatever: ['anything'] }, { type: 'null' }), null);
|
127 | assert.strictEqual(validateBySchema('not null', { type: 'null' }), null);
|
128 | assert.strictEqual(validateBySchema(123, { type: 'null', default: 123 }), null);
|
129 | });
|
130 | });
|
131 | describe('should validate enums', () => {
|
132 | const expected = 'expected';
|
133 | const defaultValue = 'default';
|
134 | const options = [expected, defaultValue, 'other-value'];
|
135 | it('good value -> should return same value', () => {
|
136 | const actual = validateBySchema(expected, { enum: options });
|
137 | assert.strictEqual(actual, expected);
|
138 | });
|
139 | it('bad value -> should return default value', () => {
|
140 | const actual = validateBySchema('not-in-enum', { enum: options, defaultValue });
|
141 | assert.strictEqual(actual, defaultValue);
|
142 | });
|
143 | it('bad value -> should return first value if no default or bad default', () => {
|
144 | const noDefault = validateBySchema(['not-in-enum'], { enum: options });
|
145 | assert.strictEqual(noDefault, expected);
|
146 | const badDefault = validateBySchema({ inEnum: false }, { enum: options, default: 'not-in-enum' });
|
147 | assert.strictEqual(badDefault, expected);
|
148 | });
|
149 | });
|
150 | describe('should validate objects', () => {
|
151 | it('should reject non object types', () => {
|
152 | const schema = { type: 'object' } as const;
|
153 | assert.deepStrictEqual(validateBySchema(null, schema), {});
|
154 | assert.deepStrictEqual(validateBySchema('null', schema), {});
|
155 | assert.deepStrictEqual(validateBySchema(3, schema), {});
|
156 | });
|
157 | it('should reject objects that are missing required fields', () => {
|
158 | const schema: PreferenceItem = { type: 'object', properties: { 'required': { type: 'string' }, 'not-required': { type: 'number' } }, required: ['required'] };
|
159 | assert.deepStrictEqual(validateBySchema({ 'not-required': 3 }, schema), {});
|
160 | const defaultValue = { required: 'present' };
|
161 | assert.deepStrictEqual(validateBySchema({ 'not-required': 3 }, { ...schema, defaultValue }), defaultValue);
|
162 | });
|
163 | it('should reject objects that have impermissible extra properties', () => {
|
164 | const schema: PreferenceItem = { type: 'object', properties: { 'required': { type: 'string' } }, additionalProperties: false };
|
165 | assert.deepStrictEqual(validateBySchema({ 'required': 'hello', 'not-required': 3 }, schema), {});
|
166 | });
|
167 | it('should accept objects with extra properties if extra properties are not forbidden', () => {
|
168 | const input = { 'required': 'hello', 'not-forbidden': 3 };
|
169 | const schema: PreferenceItem = { type: 'object', properties: { 'required': { type: 'string' } }, additionalProperties: true };
|
170 | assert.deepStrictEqual(validateBySchema(input, schema), input);
|
171 | assert.deepStrictEqual(validateBySchema(input, { ...schema, additionalProperties: undefined }), input);
|
172 | });
|
173 | it("should reject objects with properties that violate the property's rules", () => {
|
174 | const input = { required: 'not-a-number!' };
|
175 | const schema: PreferenceItem = { type: 'object', properties: { required: { type: 'number' } } };
|
176 | assert.deepStrictEqual(validateBySchema(input, schema), {});
|
177 | });
|
178 | it('should reject objects with extra properties that violate the extra property rules', () => {
|
179 | const input = { required: 3, 'not-required': 'not-a-number!' };
|
180 | const schema: PreferenceItem = { type: 'object', properties: { required: { type: 'number' } }, additionalProperties: { type: 'number' } };
|
181 | assert.deepStrictEqual(validateBySchema(input, schema), {});
|
182 | });
|
183 | });
|
184 | describe('should validate arrays', () => {
|
185 | const expected = ['one-string', 'two-string'];
|
186 | it('good input -> should return same value', () => {
|
187 | const actual = validateBySchema(expected, { type: 'array', items: { type: 'string' } });
|
188 | assert.deepStrictEqual(actual, expected);
|
189 | const augmentedExpected = [3, ...expected, 4];
|
190 | const augmentedActual = validateBySchema(augmentedExpected, { type: 'array', items: { type: ['number', 'string'] } });
|
191 | assert.deepStrictEqual(augmentedActual, augmentedExpected);
|
192 | });
|
193 | it('bad input -> should filter out impermissible items', () => {
|
194 | const actual = validateBySchema([3, ...expected, []], { type: 'array', items: { type: 'string' } });
|
195 | assert.deepStrictEqual(actual, expected);
|
196 | });
|
197 | });
|
198 | describe('should validate tuples', () => {
|
199 | const schema: PreferenceItem & { items: IJSONSchema[] } = {
|
200 | 'type': 'array',
|
201 | 'items': [{
|
202 | 'type': 'number',
|
203 | },
|
204 | {
|
205 | 'type': 'string',
|
206 | }],
|
207 | };
|
208 | it('good input -> returns same object', () => {
|
209 | const expected = [1, 'two'];
|
210 | assert.strictEqual(validateBySchema(expected, schema), expected);
|
211 | });
|
212 | it('bad input -> should use the default if supplied present and valid', () => {
|
213 | const defaultValue = [8, 'three'];
|
214 | const withDefault = { ...schema, default: defaultValue };
|
215 | assert.strictEqual(validateBySchema('not even an array!', withDefault), defaultValue);
|
216 | assert.strictEqual(validateBySchema(['first fails', 'second ok'], withDefault), defaultValue);
|
217 | assert.strictEqual(validateBySchema([], withDefault), defaultValue);
|
218 | assert.strictEqual(validateBySchema([2, ['second fails']], withDefault), defaultValue);
|
219 | });
|
220 | it('bad input -> in the absence of a default, it should return any good values or the default for each subschema', () => {
|
221 | const withSubDefault: PreferenceItem = { ...schema, items: [{ type: 'string', default: 'cool' }, ...schema.items] };
|
222 | assert.deepStrictEqual(validateBySchema('not an array', withSubDefault), ['cool', 0, '']);
|
223 | assert.deepStrictEqual(validateBySchema([2, 8, null], withSubDefault), ['cool', 8, '']);
|
224 | });
|
225 | it("bad input -> uses the default, but fixes fields that don't match schema", () => {
|
226 | const defaultValue = [8, 8];
|
227 | const withDefault = { ...schema, default: defaultValue };
|
228 | assert.deepStrictEqual(validateBySchema('something invalid', withDefault), [8, '']);
|
229 | });
|
230 | });
|
231 | describe('should validate type arrays', () => {
|
232 | const type: JsonType[] = ['boolean', 'string', 'number'];
|
233 | it('good input -> returns same value', () => {
|
234 | const goodBoolean = validateBySchema(true, { type });
|
235 | assert.strictEqual(goodBoolean, true);
|
236 | const goodString = validateBySchema('string', { type });
|
237 | assert.strictEqual(goodString, 'string');
|
238 | const goodNumber = validateBySchema(1.23, { type });
|
239 | assert.strictEqual(goodNumber, 1.23);
|
240 | });
|
241 | it('bad input -> returns default if default valid', () => {
|
242 | const stringDefault = 'default';
|
243 | const booleanDefault = true;
|
244 | const numberDefault = 100;
|
245 | assert.strictEqual(validateBySchema([], { type, default: stringDefault }), stringDefault);
|
246 | assert.strictEqual(validateBySchema([], { type, default: booleanDefault }), booleanDefault);
|
247 | assert.strictEqual(validateBySchema([], { type, default: numberDefault }), numberDefault);
|
248 | });
|
249 | it("bad input -> returns first validator's result if no default or bad default", () => {
|
250 | assert.strictEqual(validateBySchema([], { type }), false);
|
251 | assert.strictEqual(validateBySchema([], { type, default: {} }), false);
|
252 | });
|
253 | });
|
254 | describe('should validate anyOfs', () => {
|
255 | const schema: PreferenceItem = { anyOf: [{ type: 'number', minimum: 1 }, { type: 'array', items: { type: 'string' } }], default: 5 };
|
256 | it('good input -> returns same value', () => {
|
257 | assert.strictEqual(validateBySchema(3, schema), 3);
|
258 | const goodArray = ['a string', 'here too'];
|
259 | assert.strictEqual(validateBySchema(goodArray, schema), goodArray);
|
260 | });
|
261 | it('bad input -> returns default if present and valid', () => {
|
262 | assert.strictEqual(validateBySchema({}, schema), 5);
|
263 | });
|
264 | it('bad input -> first validator, if default absent or default ill-formed', () => {
|
265 | assert.strictEqual(validateBySchema({}, { ...schema, default: 0 }), 1);
|
266 | assert.strictEqual(validateBySchema({}, { ...schema, default: undefined }), 1);
|
267 | });
|
268 | });
|
269 | describe('should validate oneOfs', () => {
|
270 |
|
271 | const schema: PreferenceItem = { oneOf: [{ type: 'number', minimum: 1, maximum: 6 }, { type: 'number', minimum: 4, maximum: 10 }], default: 8 };
|
272 | it('good input -> returns same value', () => {
|
273 | assert.strictEqual(validateBySchema(2, schema), 2);
|
274 | assert.strictEqual(validateBySchema(7, schema), 7);
|
275 | });
|
276 | it('bad input -> returns default if present and valid', () => {
|
277 | assert.strictEqual(validateBySchema(5, schema), 8);
|
278 | });
|
279 | it('bad input -> returns value if default absent or invalid.', () => {
|
280 | assert.strictEqual(validateBySchema(5, { ...schema, default: undefined }), 5);
|
281 | });
|
282 | });
|
283 | describe('should validate consts', () => {
|
284 | const schema: PreferenceItem = { const: { 'the only': 'possible value' }, default: 'ignore-the-default' };
|
285 | const goodValue = { 'the only': 'possible value' };
|
286 | it('good input -> returns same value', () => {
|
287 | assert.strictEqual(validateBySchema(goodValue, schema), goodValue);
|
288 | });
|
289 | it('bad input -> returns the const value for any other value', () => {
|
290 | assert.deepStrictEqual(validateBySchema('literally anything else', schema), goodValue);
|
291 | assert.deepStrictEqual(validateBySchema('ignore-the-default', schema), goodValue);
|
292 | });
|
293 | });
|
294 | describe('should maintain triple equality for valid object types', () => {
|
295 | const arraySchema: PreferenceItem = { type: 'array', items: { type: 'string' } };
|
296 | it('maintains triple equality for arrays', () => {
|
297 | const input = ['one-string', 'two-string'];
|
298 | assert(validateBySchema(input, arraySchema) === input);
|
299 | });
|
300 | it('does not maintain triple equality if the array is only partially correct', () => {
|
301 | const input = ['one-string', 'two-string', 3];
|
302 | assert.notStrictEqual(validateBySchema(input, arraySchema), input);
|
303 | });
|
304 | it('maintains triple equality for objects', () => {
|
305 | const schema: PreferenceItem = {
|
306 | 'type': 'object',
|
307 | properties: {
|
308 | primitive: { type: 'string' },
|
309 | complex: { type: 'object', properties: { nested: { type: 'number' } } }
|
310 | }
|
311 | };
|
312 | const input = { primitive: 'is a string', complex: { nested: 3 } };
|
313 | assert(validateBySchema(input, schema) === input);
|
314 | });
|
315 | });
|
316 | it('should return the value if any error occurs', () => {
|
317 | let wasCalled = false;
|
318 | const originalValidator = validator['validateString'];
|
319 | validator['validateString'] = () => {
|
320 | wasCalled = true;
|
321 | throw new Error('Only a test!');
|
322 | };
|
323 | const input = { shouldBeValid: false };
|
324 | const output = validateBySchema(input, { type: 'string' });
|
325 | assert(wasCalled);
|
326 | assert(input === output);
|
327 | validator['validateString'] = originalValidator;
|
328 | });
|
329 | it('should return the same object if no validation possible', () => {
|
330 | for (const input of ['whatever', { valid: 'hard to say' }, 234, ["no one knows if I'm not", 'so I am']]) {
|
331 | assert(validateBySchema(input, {}) === input);
|
332 | }
|
333 | });
|
334 | });
|