UNPKG

18.7 kBPlain TextView Raw
1// *****************************************************************************
2// Copyright (C) 2022 Ericsson and others.
3//
4// This program and the accompanying materials are made available under the
5// terms of the Eclipse Public License v. 2.0 which is available at
6// http://www.eclipse.org/legal/epl-2.0.
7//
8// This Source Code may also be made available under the following Secondary
9// Licenses when the conditions for such availability set forth in the Eclipse
10// Public License v. 2.0 are satisfied: GNU General Public License, version 2
11// with the GNU Classpath Exception which is available at
12// https://www.gnu.org/software/classpath/license.html.
13//
14// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15// *****************************************************************************
16
17import { Container } from 'inversify';
18import { PreferenceValidationService } from './preference-validation-service';
19import { PreferenceItem, PreferenceSchemaProvider } from './preference-contribution';
20import { PreferenceLanguageOverrideService } from './preference-language-override-service';
21import * as assert from 'assert';
22import { JSONValue } from '@phosphor/coreutils';
23import { IJSONSchema, JsonType } from '../../common/json-schema';
24
25/* eslint-disable no-null/no-null */
26
27describe('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 // Between 4 and 6 should be rejected
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});