1// Copyright 2014 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5var AssertTrue = requireNative('assert').AssertTrue;
6var JSONSchemaValidator = require('json_schema').JSONSchemaValidator;
7var LOG = requireNative('logging').LOG;
8
9function assertValid(type, instance, schema, types) {
10  var validator = new JSONSchemaValidator();
11  if (types)
12    validator.addTypes(types);
13  validator["validate" + type](instance, schema, "");
14  var success = true;
15  if (validator.errors.length != 0) {
16    LOG("Got unexpected errors");
17    for (var i = 0; i < validator.errors.length; i++) {
18      LOG(validator.errors[i].message + "  path: " + validator.errors[i].path);
19    }
20    success = false;
21  }
22  AssertTrue(success);
23}
24
25function assertNotValid(type, instance, schema, errors, types) {
26  var validator = new JSONSchemaValidator();
27  if (types)
28    validator.addTypes(types);
29  validator["validate" + type](instance, schema, "");
30  AssertTrue(validator.errors.length === errors.length);
31  var success = true;
32  for (var i = 0; i < errors.length; i++) {
33    if (validator.errors[i].message == errors[i]) {
34      LOG("Got expected error: " + validator.errors[i].message +
35          " for path: " + validator.errors[i].path);
36    } else {
37      LOG("Missed expected error: " + errors[i] + ". Got: " +
38          validator.errors[i].message + " instead.");
39      success = false;
40    }
41  }
42  AssertTrue(success);
43}
44
45function assertListConsistsOfElements(list, elements) {
46  var success = true;
47  for (var li = 0; li < list.length; li++) {
48    for (var ei = 0; ei < elements.length && list[li] != elements[ei]; ei++) { }
49    if (ei == elements.length) {
50      LOG("Expected type not found: " + list[li]);
51      success = false;
52    }
53  }
54  AssertTrue(success);
55}
56
57function assertEqualSets(set1, set2) {
58  assertListConsistsOfElements(set1, set2);
59  assertListConsistsOfElements(set2, set1);
60}
61
62function formatError(key, replacements) {
63  return JSONSchemaValidator.formatError(key, replacements);
64}
65
66function testFormatError() {
67  AssertTrue(formatError("propertyRequired") == "Property is required.");
68  AssertTrue(formatError("invalidEnum", ["foo, bar"]) ==
69         "Value must be one of: [foo, bar].");
70  AssertTrue(formatError("invalidType", ["foo", "bar"]) ==
71         "Expected 'foo' but got 'bar'.");
72}
73
74function testComplex() {
75  var schema = {
76    type: "array",
77    items: [
78      {
79        type: "object",
80        properties: {
81          id: {
82            type: "integer",
83            minimum: 1
84          },
85          url: {
86            type: "string",
87            pattern: /^https?\:/,
88            optional: true
89          },
90          index: {
91            type: "integer",
92            minimum: 0,
93            optional: true
94          },
95          selected: {
96            type: "boolean",
97            optional: true
98          }
99        }
100      },
101      { type: "function", optional: true },
102      { type: "function", optional: true }
103    ]
104  };
105
106  var instance = [
107    {
108      id: 42,
109      url: "http://www.google.com/",
110      index: 2,
111      selected: true
112    },
113    function(){},
114    function(){}
115  ];
116
117  assertValid("", instance, schema);
118  instance.length = 2;
119  assertValid("", instance, schema);
120  instance.length = 1;
121  instance.push({});
122  assertNotValid("", instance, schema,
123                 [formatError("invalidType", ["function", "object"])]);
124  instance[1] = function(){};
125
126  instance[0].url = "foo";
127  assertNotValid("", instance, schema,
128                 [formatError("stringPattern",
129                              [schema.items[0].properties.url.pattern])]);
130  delete instance[0].url;
131  assertValid("", instance, schema);
132
133  instance[0].id = 0;
134  assertNotValid("", instance, schema,
135                 [formatError("numberMinValue",
136                              [schema.items[0].properties.id.minimum])]);
137}
138
139function testEnum() {
140  var schema = {
141    enum: ["foo", 42, false]
142  };
143
144  assertValid("", "foo", schema);
145  assertValid("", 42, schema);
146  assertValid("", false, schema);
147  assertNotValid("", "42", schema, [formatError("invalidEnum",
148                                                [schema.enum.join(", ")])]);
149  assertNotValid("", null, schema, [formatError("invalidEnum",
150                                                [schema.enum.join(", ")])]);
151}
152
153function testChoices() {
154  var schema = {
155    choices: [
156      { type: "null" },
157      { type: "undefined" },
158      { type: "integer", minimum:42, maximum:42 },
159      { type: "object", properties: { foo: { type: "string" } } }
160    ]
161  }
162    assertValid("", null, schema);
163    assertValid("", undefined, schema);
164    assertValid("", 42, schema);
165    assertValid("", {foo: "bar"}, schema);
166
167    assertNotValid("", "foo", schema, [formatError("invalidChoice", [])]);
168    assertNotValid("", [], schema, [formatError("invalidChoice", [])]);
169    assertNotValid("", {foo: 42}, schema, [formatError("invalidChoice", [])]);
170}
171
172function testExtends() {
173  var parent = {
174    type: "number"
175  }
176  var schema = {
177    extends: parent
178  };
179
180  assertValid("", 42, schema);
181  assertNotValid("", "42", schema,
182                 [formatError("invalidType", ["number", "string"])]);
183
184  // Make the derived schema more restrictive
185  parent.minimum = 43;
186  assertNotValid("", 42, schema, [formatError("numberMinValue", [43])]);
187  assertValid("", 43, schema);
188}
189
190function testObject() {
191  var schema = {
192    properties: {
193      "foo": {
194        type: "string"
195      },
196      "bar": {
197        type: "integer"
198      }
199    }
200  };
201
202  assertValid("Object", {foo:"foo", bar:42}, schema);
203  assertNotValid("Object", {foo:"foo", bar:42,"extra":true}, schema,
204                 [formatError("unexpectedProperty")]);
205  assertNotValid("Object", {foo:"foo"}, schema,
206                 [formatError("propertyRequired")]);
207  assertNotValid("Object", {foo:"foo", bar:"42"}, schema,
208                 [formatError("invalidType", ["integer", "string"])]);
209
210  schema.additionalProperties = { type: "any" };
211  assertValid("Object", {foo:"foo", bar:42, "extra":true}, schema);
212  assertValid("Object", {foo:"foo", bar:42, "extra":"foo"}, schema);
213
214  schema.additionalProperties = { type: "boolean" };
215  assertValid("Object", {foo:"foo", bar:42, "extra":true}, schema);
216  assertNotValid("Object", {foo:"foo", bar:42, "extra":"foo"}, schema,
217                 [formatError("invalidType", ["boolean", "string"])]);
218
219  schema.properties.bar.optional = true;
220  assertValid("Object", {foo:"foo", bar:42}, schema);
221  assertValid("Object", {foo:"foo"}, schema);
222  assertValid("Object", {foo:"foo", bar:null}, schema);
223  assertValid("Object", {foo:"foo", bar:undefined}, schema);
224  assertNotValid("Object", {foo:"foo", bar:"42"}, schema,
225                 [formatError("invalidType", ["integer", "string"])]);
226}
227
228function testTypeReference() {
229  var referencedTypes = [
230    {
231      id: "MinLengthString",
232      type: "string",
233      minLength: 2
234    },
235    {
236      id: "Max10Int",
237      type: "integer",
238      maximum: 10
239    }
240  ];
241
242  var schema = {
243    type: "object",
244    properties: {
245      "foo": {
246        type: "string"
247      },
248      "bar": {
249        $ref: "Max10Int"
250      },
251      "baz": {
252        $ref: "MinLengthString"
253      }
254    }
255  };
256
257  var schemaInlineReference = {
258    type: "object",
259    properties: {
260      "foo": {
261        type: "string"
262      },
263      "bar": {
264        id: "NegativeInt",
265        type: "integer",
266        maximum: 0
267      },
268      "baz": {
269        $ref: "NegativeInt"
270      }
271    }
272  }
273
274  // Valid type references externally added.
275  assertValid("", {foo:"foo",bar:4,baz:"ab"}, schema, referencedTypes);
276
277  // Valida type references internally defined.
278  assertValid("", {foo:"foo",bar:-4,baz:-2}, schemaInlineReference);
279
280  // Failures in validation, but succesful schema reference.
281  assertNotValid("", {foo:"foo",bar:4,baz:"a"}, schema,
282                [formatError("stringMinLength", [2])], referencedTypes);
283  assertNotValid("", {foo:"foo",bar:20,baz:"abc"}, schema,
284                [formatError("numberMaxValue", [10])], referencedTypes);
285
286  // Remove MinLengthString type.
287  referencedTypes.shift();
288  assertNotValid("", {foo:"foo",bar:4,baz:"ab"}, schema,
289                 [formatError("unknownSchemaReference", ["MinLengthString"])],
290                 referencedTypes);
291
292  // Remove internal type "NegativeInt"
293  delete schemaInlineReference.properties.bar;
294  assertNotValid("", {foo:"foo",baz:-2}, schemaInlineReference,
295                 [formatError("unknownSchemaReference", ["NegativeInt"])]);
296}
297
298function testArrayTuple() {
299  var schema = {
300    items: [
301      {
302        type: "string"
303      },
304      {
305        type: "integer"
306      }
307    ]
308  };
309
310  assertValid("Array", ["42", 42], schema);
311  assertNotValid("Array", ["42", 42, "anything"], schema,
312                 [formatError("arrayMaxItems", [schema.items.length])]);
313  assertNotValid("Array", ["42"], schema, [formatError("itemRequired")]);
314  assertNotValid("Array", [42, 42], schema,
315                 [formatError("invalidType", ["string", "integer"])]);
316
317  schema.additionalProperties = { type: "any" };
318  assertValid("Array", ["42", 42, "anything"], schema);
319  assertValid("Array", ["42", 42, []], schema);
320
321  schema.additionalProperties = { type: "boolean" };
322  assertNotValid("Array", ["42", 42, "anything"], schema,
323                 [formatError("invalidType", ["boolean", "string"])]);
324  assertValid("Array", ["42", 42, false], schema);
325
326  schema.items[0].optional = true;
327  assertValid("Array", ["42", 42], schema);
328  assertValid("Array", [, 42], schema);
329  assertValid("Array", [null, 42], schema);
330  assertValid("Array", [undefined, 42], schema);
331  assertNotValid("Array", [42, 42], schema,
332                 [formatError("invalidType", ["string", "integer"])]);
333}
334
335function testArrayNonTuple() {
336  var schema = {
337    items: {
338      type: "string"
339    },
340    minItems: 2,
341    maxItems: 3
342  };
343
344  assertValid("Array", ["x", "x"], schema);
345  assertValid("Array", ["x", "x", "x"], schema);
346
347  assertNotValid("Array", ["x"], schema,
348                 [formatError("arrayMinItems", [schema.minItems])]);
349  assertNotValid("Array", ["x", "x", "x", "x"], schema,
350                 [formatError("arrayMaxItems", [schema.maxItems])]);
351  assertNotValid("Array", [42], schema,
352                 [formatError("arrayMinItems", [schema.minItems]),
353                  formatError("invalidType", ["string", "integer"])]);
354}
355
356function testString() {
357  var schema = {
358    minLength: 1,
359    maxLength: 10,
360    pattern: /^x/
361  };
362
363  assertValid("String", "x", schema);
364  assertValid("String", "xxxxxxxxxx", schema);
365
366  assertNotValid("String", "y", schema,
367                 [formatError("stringPattern", [schema.pattern])]);
368  assertNotValid("String", "xxxxxxxxxxx", schema,
369                 [formatError("stringMaxLength", [schema.maxLength])]);
370  assertNotValid("String", "", schema,
371                 [formatError("stringMinLength", [schema.minLength]),
372                  formatError("stringPattern", [schema.pattern])]);
373}
374
375function testNumber() {
376  var schema = {
377    minimum: 1,
378    maximum: 100,
379    maxDecimal: 2
380  };
381
382  assertValid("Number", 1, schema);
383  assertValid("Number", 50, schema);
384  assertValid("Number", 100, schema);
385  assertValid("Number", 88.88, schema);
386
387  assertNotValid("Number", 0.5, schema,
388                 [formatError("numberMinValue", [schema.minimum])]);
389  assertNotValid("Number", 100.1, schema,
390                 [formatError("numberMaxValue", [schema.maximum])]);
391  assertNotValid("Number", 100.111, schema,
392                 [formatError("numberMaxValue", [schema.maximum]),
393                  formatError("numberMaxDecimal", [schema.maxDecimal])]);
394
395  var nan = 0/0;
396  AssertTrue(isNaN(nan));
397  assertNotValid("Number", nan, schema,
398                 [formatError("numberFiniteNotNan", ["NaN"])]);
399
400  assertNotValid("Number", Number.POSITIVE_INFINITY, schema,
401                 [formatError("numberFiniteNotNan", ["Infinity"]),
402                  formatError("numberMaxValue", [schema.maximum])
403                 ]);
404
405  assertNotValid("Number", Number.NEGATIVE_INFINITY, schema,
406                 [formatError("numberFiniteNotNan", ["-Infinity"]),
407                  formatError("numberMinValue", [schema.minimum])
408                 ]);
409}
410
411function testIntegerBounds() {
412  assertValid("Number",           0, {type:"integer"});
413  assertValid("Number",          -1, {type:"integer"});
414  assertValid("Number",  2147483647, {type:"integer"});
415  assertValid("Number", -2147483648, {type:"integer"});
416  assertNotValid("Number",           0.5, {type:"integer"},
417                 [formatError("numberIntValue", [])]);
418  assertNotValid("Number", 10000000000,   {type:"integer"},
419                 [formatError("numberIntValue", [])]);
420  assertNotValid("Number",  2147483647.5, {type:"integer"},
421                 [formatError("numberIntValue", [])]);
422  assertNotValid("Number",  2147483648,   {type:"integer"},
423                 [formatError("numberIntValue", [])]);
424  assertNotValid("Number",  2147483649,   {type:"integer"},
425                 [formatError("numberIntValue", [])]);
426  assertNotValid("Number", -2147483649,   {type:"integer"},
427                 [formatError("numberIntValue", [])]);
428}
429
430function testType() {
431  // valid
432  assertValid("Type", {}, {type:"object"});
433  assertValid("Type", [], {type:"array"});
434  assertValid("Type", function(){}, {type:"function"});
435  assertValid("Type", "foobar", {type:"string"});
436  assertValid("Type", "", {type:"string"});
437  assertValid("Type", 88.8, {type:"number"});
438  assertValid("Type", 42, {type:"number"});
439  assertValid("Type", 0, {type:"number"});
440  assertValid("Type", 42, {type:"integer"});
441  assertValid("Type", 0, {type:"integer"});
442  assertValid("Type", true, {type:"boolean"});
443  assertValid("Type", false, {type:"boolean"});
444  assertValid("Type", null, {type:"null"});
445  assertValid("Type", undefined, {type:"undefined"});
446
447  // not valid
448  assertNotValid("Type", [], {type: "object"},
449                 [formatError("invalidType", ["object", "array"])]);
450  assertNotValid("Type", null, {type: "object"},
451                 [formatError("invalidType", ["object", "null"])]);
452  assertNotValid("Type", function(){}, {type: "object"},
453                 [formatError("invalidType", ["object", "function"])]);
454  assertNotValid("Type", 42, {type: "array"},
455                 [formatError("invalidType", ["array", "integer"])]);
456  assertNotValid("Type", 42, {type: "string"},
457                 [formatError("invalidType", ["string", "integer"])]);
458  assertNotValid("Type", "42", {type: "number"},
459                 [formatError("invalidType", ["number", "string"])]);
460  assertNotValid("Type", 88.8, {type: "integer"},
461                 [formatError("invalidTypeIntegerNumber")]);
462  assertNotValid("Type", 1, {type: "boolean"},
463                 [formatError("invalidType", ["boolean", "integer"])]);
464  assertNotValid("Type", false, {type: "null"},
465                 [formatError("invalidType", ["null", "boolean"])]);
466  assertNotValid("Type", undefined, {type: "null"},
467                 [formatError("invalidType", ["null", "undefined"])]);
468  assertNotValid("Type", {}, {type: "function"},
469                 [formatError("invalidType", ["function", "object"])]);
470}
471
472function testGetAllTypesForSchema() {
473  var referencedTypes = [
474    {
475      id: "ChoicesRef",
476      choices: [
477        { type: "integer" },
478        { type: "string" }
479      ]
480    },
481    {
482      id: "ObjectRef",
483      type: "object",
484    }
485  ];
486
487  var arraySchema = {
488    type: "array"
489  };
490
491  var choicesSchema = {
492    choices: [
493      { type: "object" },
494      { type: "function" }
495    ]
496  };
497
498  var objectRefSchema = {
499    $ref: "ObjectRef"
500  };
501
502  var complexSchema = {
503    choices: [
504      { $ref: "ChoicesRef" },
505      { type: "function" },
506      { $ref: "ObjectRef" }
507    ]
508  };
509
510  var validator = new JSONSchemaValidator();
511  validator.addTypes(referencedTypes);
512
513  var arraySchemaTypes = validator.getAllTypesForSchema(arraySchema);
514  assertEqualSets(arraySchemaTypes, ["array"]);
515
516  var choicesSchemaTypes = validator.getAllTypesForSchema(choicesSchema);
517  assertEqualSets(choicesSchemaTypes, ["object", "function"]);
518
519  var objectRefSchemaTypes = validator.getAllTypesForSchema(objectRefSchema);
520  assertEqualSets(objectRefSchemaTypes, ["object"]);
521
522  var complexSchemaTypes = validator.getAllTypesForSchema(complexSchema);
523  assertEqualSets(complexSchemaTypes,
524      ["integer", "string", "function", "object"]);
525}
526
527function testIsValidSchemaType() {
528  var referencedTypes = [
529    {
530      id: "ChoicesRef",
531      choices: [
532        { type: "integer" },
533        { type: "string" }
534      ]
535    }
536  ];
537
538  var objectSchema = {
539    type: "object",
540    optional: true
541  };
542
543  var complexSchema = {
544    choices: [
545      { $ref: "ChoicesRef" },
546      { type: "function" },
547    ]
548  };
549
550  var validator = new JSONSchemaValidator();
551  validator.addTypes(referencedTypes);
552
553  AssertTrue(validator.isValidSchemaType("object", objectSchema));
554  AssertTrue(!validator.isValidSchemaType("integer", objectSchema));
555  AssertTrue(!validator.isValidSchemaType("array", objectSchema));
556  AssertTrue(validator.isValidSchemaType("null", objectSchema));
557  AssertTrue(validator.isValidSchemaType("undefined", objectSchema));
558
559  AssertTrue(validator.isValidSchemaType("integer", complexSchema));
560  AssertTrue(validator.isValidSchemaType("function", complexSchema));
561  AssertTrue(validator.isValidSchemaType("string", complexSchema));
562  AssertTrue(!validator.isValidSchemaType("object", complexSchema));
563  AssertTrue(!validator.isValidSchemaType("null", complexSchema));
564  AssertTrue(!validator.isValidSchemaType("undefined", complexSchema));
565}
566
567function testCheckSchemaOverlap() {
568  var referencedTypes = [
569    {
570      id: "ChoicesRef",
571      choices: [
572        { type: "integer" },
573        { type: "string" }
574      ]
575    },
576    {
577      id: "ObjectRef",
578      type: "object",
579    }
580  ];
581
582  var arraySchema = {
583    type: "array"
584  };
585
586  var choicesSchema = {
587    choices: [
588      { type: "object" },
589      { type: "function" }
590    ]
591  };
592
593  var objectRefSchema = {
594    $ref: "ObjectRef"
595  };
596
597  var complexSchema = {
598    choices: [
599      { $ref: "ChoicesRef" },
600      { type: "function" },
601      { $ref: "ObjectRef" }
602    ]
603  };
604
605  var validator = new JSONSchemaValidator();
606  validator.addTypes(referencedTypes);
607
608  AssertTrue(!validator.checkSchemaOverlap(arraySchema, choicesSchema));
609  AssertTrue(!validator.checkSchemaOverlap(arraySchema, objectRefSchema));
610  AssertTrue(!validator.checkSchemaOverlap(arraySchema, complexSchema));
611  AssertTrue(validator.checkSchemaOverlap(choicesSchema, objectRefSchema));
612  AssertTrue(validator.checkSchemaOverlap(choicesSchema, complexSchema));
613  AssertTrue(validator.checkSchemaOverlap(objectRefSchema, complexSchema));
614}
615
616function testInstanceOf() {
617  function Animal() {};
618  function Cat() {};
619  function Dog() {};
620  Cat.prototype = new Animal;
621  Cat.prototype.constructor = Cat;
622  Dog.prototype = new Animal;
623  Dog.prototype.constructor = Dog;
624  var cat = new Cat();
625  var dog = new Dog();
626  var num = new Number(1);
627
628  // instanceOf should check type by walking up prototype chain.
629  assertValid("", cat, {type:"object", isInstanceOf:"Cat"});
630  assertValid("", cat, {type:"object", isInstanceOf:"Animal"});
631  assertValid("", cat, {type:"object", isInstanceOf:"Object"});
632  assertValid("", dog, {type:"object", isInstanceOf:"Dog"});
633  assertValid("", dog, {type:"object", isInstanceOf:"Animal"});
634  assertValid("", dog, {type:"object", isInstanceOf:"Object"});
635  assertValid("", num, {type:"object", isInstanceOf:"Number"});
636  assertValid("", num, {type:"object", isInstanceOf:"Object"});
637
638  assertNotValid("", cat, {type:"object", isInstanceOf:"Dog"},
639                 [formatError("notInstance", ["Dog"])]);
640  assertNotValid("", dog, {type:"object", isInstanceOf:"Cat"},
641                 [formatError("notInstance", ["Cat"])]);
642  assertNotValid("", cat, {type:"object", isInstanceOf:"String"},
643                 [formatError("notInstance", ["String"])]);
644  assertNotValid("", dog, {type:"object", isInstanceOf:"String"},
645                 [formatError("notInstance", ["String"])]);
646  assertNotValid("", num, {type:"object", isInstanceOf:"Array"},
647                [formatError("notInstance", ["Array"])]);
648  assertNotValid("", num, {type:"object", isInstanceOf:"String"},
649                [formatError("notInstance", ["String"])]);
650}
651
652// Tests exposed to schema_unittest.cc.
653exports.testFormatError = testFormatError;
654exports.testComplex = testComplex;
655exports.testEnum = testEnum;
656exports.testExtends = testExtends;
657exports.testObject = testObject;
658exports.testArrayTuple = testArrayTuple;
659exports.testArrayNonTuple = testArrayNonTuple;
660exports.testString = testString;
661exports.testNumber = testNumber;
662exports.testIntegerBounds = testIntegerBounds;
663exports.testType = testType;
664exports.testTypeReference = testTypeReference;
665exports.testGetAllTypesForSchema = testGetAllTypesForSchema;
666exports.testIsValidSchemaType = testIsValidSchemaType;
667exports.testCheckSchemaOverlap = testCheckSchemaOverlap;
668exports.testInstanceOf = testInstanceOf;
669