Javascript  |  669行  |  20.61 KB

// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

var AssertTrue = requireNative('assert').AssertTrue;
var JSONSchemaValidator = require('json_schema').JSONSchemaValidator;
var LOG = requireNative('logging').LOG;

function assertValid(type, instance, schema, types) {
  var validator = new JSONSchemaValidator();
  if (types)
    validator.addTypes(types);
  validator["validate" + type](instance, schema, "");
  var success = true;
  if (validator.errors.length != 0) {
    LOG("Got unexpected errors");
    for (var i = 0; i < validator.errors.length; i++) {
      LOG(validator.errors[i].message + "  path: " + validator.errors[i].path);
    }
    success = false;
  }
  AssertTrue(success);
}

function assertNotValid(type, instance, schema, errors, types) {
  var validator = new JSONSchemaValidator();
  if (types)
    validator.addTypes(types);
  validator["validate" + type](instance, schema, "");
  AssertTrue(validator.errors.length === errors.length);
  var success = true;
  for (var i = 0; i < errors.length; i++) {
    if (validator.errors[i].message == errors[i]) {
      LOG("Got expected error: " + validator.errors[i].message +
          " for path: " + validator.errors[i].path);
    } else {
      LOG("Missed expected error: " + errors[i] + ". Got: " +
          validator.errors[i].message + " instead.");
      success = false;
    }
  }
  AssertTrue(success);
}

function assertListConsistsOfElements(list, elements) {
  var success = true;
  for (var li = 0; li < list.length; li++) {
    for (var ei = 0; ei < elements.length && list[li] != elements[ei]; ei++) { }
    if (ei == elements.length) {
      LOG("Expected type not found: " + list[li]);
      success = false;
    }
  }
  AssertTrue(success);
}

function assertEqualSets(set1, set2) {
  assertListConsistsOfElements(set1, set2);
  assertListConsistsOfElements(set2, set1);
}

function formatError(key, replacements) {
  return JSONSchemaValidator.formatError(key, replacements);
}

function testFormatError() {
  AssertTrue(formatError("propertyRequired") == "Property is required.");
  AssertTrue(formatError("invalidEnum", ["foo, bar"]) ==
         "Value must be one of: [foo, bar].");
  AssertTrue(formatError("invalidType", ["foo", "bar"]) ==
         "Expected 'foo' but got 'bar'.");
}

function testComplex() {
  var schema = {
    type: "array",
    items: [
      {
        type: "object",
        properties: {
          id: {
            type: "integer",
            minimum: 1
          },
          url: {
            type: "string",
            pattern: /^https?\:/,
            optional: true
          },
          index: {
            type: "integer",
            minimum: 0,
            optional: true
          },
          selected: {
            type: "boolean",
            optional: true
          }
        }
      },
      { type: "function", optional: true },
      { type: "function", optional: true }
    ]
  };

  var instance = [
    {
      id: 42,
      url: "http://www.google.com/",
      index: 2,
      selected: true
    },
    function(){},
    function(){}
  ];

  assertValid("", instance, schema);
  instance.length = 2;
  assertValid("", instance, schema);
  instance.length = 1;
  instance.push({});
  assertNotValid("", instance, schema,
                 [formatError("invalidType", ["function", "object"])]);
  instance[1] = function(){};

  instance[0].url = "foo";
  assertNotValid("", instance, schema,
                 [formatError("stringPattern",
                              [schema.items[0].properties.url.pattern])]);
  delete instance[0].url;
  assertValid("", instance, schema);

  instance[0].id = 0;
  assertNotValid("", instance, schema,
                 [formatError("numberMinValue",
                              [schema.items[0].properties.id.minimum])]);
}

function testEnum() {
  var schema = {
    enum: ["foo", 42, false]
  };

  assertValid("", "foo", schema);
  assertValid("", 42, schema);
  assertValid("", false, schema);
  assertNotValid("", "42", schema, [formatError("invalidEnum",
                                                [schema.enum.join(", ")])]);
  assertNotValid("", null, schema, [formatError("invalidEnum",
                                                [schema.enum.join(", ")])]);
}

function testChoices() {
  var schema = {
    choices: [
      { type: "null" },
      { type: "undefined" },
      { type: "integer", minimum:42, maximum:42 },
      { type: "object", properties: { foo: { type: "string" } } }
    ]
  }
    assertValid("", null, schema);
    assertValid("", undefined, schema);
    assertValid("", 42, schema);
    assertValid("", {foo: "bar"}, schema);

    assertNotValid("", "foo", schema, [formatError("invalidChoice", [])]);
    assertNotValid("", [], schema, [formatError("invalidChoice", [])]);
    assertNotValid("", {foo: 42}, schema, [formatError("invalidChoice", [])]);
}

function testExtends() {
  var parent = {
    type: "number"
  }
  var schema = {
    extends: parent
  };

  assertValid("", 42, schema);
  assertNotValid("", "42", schema,
                 [formatError("invalidType", ["number", "string"])]);

  // Make the derived schema more restrictive
  parent.minimum = 43;
  assertNotValid("", 42, schema, [formatError("numberMinValue", [43])]);
  assertValid("", 43, schema);
}

function testObject() {
  var schema = {
    properties: {
      "foo": {
        type: "string"
      },
      "bar": {
        type: "integer"
      }
    }
  };

  assertValid("Object", {foo:"foo", bar:42}, schema);
  assertNotValid("Object", {foo:"foo", bar:42,"extra":true}, schema,
                 [formatError("unexpectedProperty")]);
  assertNotValid("Object", {foo:"foo"}, schema,
                 [formatError("propertyRequired")]);
  assertNotValid("Object", {foo:"foo", bar:"42"}, schema,
                 [formatError("invalidType", ["integer", "string"])]);

  schema.additionalProperties = { type: "any" };
  assertValid("Object", {foo:"foo", bar:42, "extra":true}, schema);
  assertValid("Object", {foo:"foo", bar:42, "extra":"foo"}, schema);

  schema.additionalProperties = { type: "boolean" };
  assertValid("Object", {foo:"foo", bar:42, "extra":true}, schema);
  assertNotValid("Object", {foo:"foo", bar:42, "extra":"foo"}, schema,
                 [formatError("invalidType", ["boolean", "string"])]);

  schema.properties.bar.optional = true;
  assertValid("Object", {foo:"foo", bar:42}, schema);
  assertValid("Object", {foo:"foo"}, schema);
  assertValid("Object", {foo:"foo", bar:null}, schema);
  assertValid("Object", {foo:"foo", bar:undefined}, schema);
  assertNotValid("Object", {foo:"foo", bar:"42"}, schema,
                 [formatError("invalidType", ["integer", "string"])]);
}

function testTypeReference() {
  var referencedTypes = [
    {
      id: "MinLengthString",
      type: "string",
      minLength: 2
    },
    {
      id: "Max10Int",
      type: "integer",
      maximum: 10
    }
  ];

  var schema = {
    type: "object",
    properties: {
      "foo": {
        type: "string"
      },
      "bar": {
        $ref: "Max10Int"
      },
      "baz": {
        $ref: "MinLengthString"
      }
    }
  };

  var schemaInlineReference = {
    type: "object",
    properties: {
      "foo": {
        type: "string"
      },
      "bar": {
        id: "NegativeInt",
        type: "integer",
        maximum: 0
      },
      "baz": {
        $ref: "NegativeInt"
      }
    }
  }

  // Valid type references externally added.
  assertValid("", {foo:"foo",bar:4,baz:"ab"}, schema, referencedTypes);

  // Valida type references internally defined.
  assertValid("", {foo:"foo",bar:-4,baz:-2}, schemaInlineReference);

  // Failures in validation, but succesful schema reference.
  assertNotValid("", {foo:"foo",bar:4,baz:"a"}, schema,
                [formatError("stringMinLength", [2])], referencedTypes);
  assertNotValid("", {foo:"foo",bar:20,baz:"abc"}, schema,
                [formatError("numberMaxValue", [10])], referencedTypes);

  // Remove MinLengthString type.
  referencedTypes.shift();
  assertNotValid("", {foo:"foo",bar:4,baz:"ab"}, schema,
                 [formatError("unknownSchemaReference", ["MinLengthString"])],
                 referencedTypes);

  // Remove internal type "NegativeInt"
  delete schemaInlineReference.properties.bar;
  assertNotValid("", {foo:"foo",baz:-2}, schemaInlineReference,
                 [formatError("unknownSchemaReference", ["NegativeInt"])]);
}

function testArrayTuple() {
  var schema = {
    items: [
      {
        type: "string"
      },
      {
        type: "integer"
      }
    ]
  };

  assertValid("Array", ["42", 42], schema);
  assertNotValid("Array", ["42", 42, "anything"], schema,
                 [formatError("arrayMaxItems", [schema.items.length])]);
  assertNotValid("Array", ["42"], schema, [formatError("itemRequired")]);
  assertNotValid("Array", [42, 42], schema,
                 [formatError("invalidType", ["string", "integer"])]);

  schema.additionalProperties = { type: "any" };
  assertValid("Array", ["42", 42, "anything"], schema);
  assertValid("Array", ["42", 42, []], schema);

  schema.additionalProperties = { type: "boolean" };
  assertNotValid("Array", ["42", 42, "anything"], schema,
                 [formatError("invalidType", ["boolean", "string"])]);
  assertValid("Array", ["42", 42, false], schema);

  schema.items[0].optional = true;
  assertValid("Array", ["42", 42], schema);
  assertValid("Array", [, 42], schema);
  assertValid("Array", [null, 42], schema);
  assertValid("Array", [undefined, 42], schema);
  assertNotValid("Array", [42, 42], schema,
                 [formatError("invalidType", ["string", "integer"])]);
}

function testArrayNonTuple() {
  var schema = {
    items: {
      type: "string"
    },
    minItems: 2,
    maxItems: 3
  };

  assertValid("Array", ["x", "x"], schema);
  assertValid("Array", ["x", "x", "x"], schema);

  assertNotValid("Array", ["x"], schema,
                 [formatError("arrayMinItems", [schema.minItems])]);
  assertNotValid("Array", ["x", "x", "x", "x"], schema,
                 [formatError("arrayMaxItems", [schema.maxItems])]);
  assertNotValid("Array", [42], schema,
                 [formatError("arrayMinItems", [schema.minItems]),
                  formatError("invalidType", ["string", "integer"])]);
}

function testString() {
  var schema = {
    minLength: 1,
    maxLength: 10,
    pattern: /^x/
  };

  assertValid("String", "x", schema);
  assertValid("String", "xxxxxxxxxx", schema);

  assertNotValid("String", "y", schema,
                 [formatError("stringPattern", [schema.pattern])]);
  assertNotValid("String", "xxxxxxxxxxx", schema,
                 [formatError("stringMaxLength", [schema.maxLength])]);
  assertNotValid("String", "", schema,
                 [formatError("stringMinLength", [schema.minLength]),
                  formatError("stringPattern", [schema.pattern])]);
}

function testNumber() {
  var schema = {
    minimum: 1,
    maximum: 100,
    maxDecimal: 2
  };

  assertValid("Number", 1, schema);
  assertValid("Number", 50, schema);
  assertValid("Number", 100, schema);
  assertValid("Number", 88.88, schema);

  assertNotValid("Number", 0.5, schema,
                 [formatError("numberMinValue", [schema.minimum])]);
  assertNotValid("Number", 100.1, schema,
                 [formatError("numberMaxValue", [schema.maximum])]);
  assertNotValid("Number", 100.111, schema,
                 [formatError("numberMaxValue", [schema.maximum]),
                  formatError("numberMaxDecimal", [schema.maxDecimal])]);

  var nan = 0/0;
  AssertTrue(isNaN(nan));
  assertNotValid("Number", nan, schema,
                 [formatError("numberFiniteNotNan", ["NaN"])]);

  assertNotValid("Number", Number.POSITIVE_INFINITY, schema,
                 [formatError("numberFiniteNotNan", ["Infinity"]),
                  formatError("numberMaxValue", [schema.maximum])
                 ]);

  assertNotValid("Number", Number.NEGATIVE_INFINITY, schema,
                 [formatError("numberFiniteNotNan", ["-Infinity"]),
                  formatError("numberMinValue", [schema.minimum])
                 ]);
}

function testIntegerBounds() {
  assertValid("Number",           0, {type:"integer"});
  assertValid("Number",          -1, {type:"integer"});
  assertValid("Number",  2147483647, {type:"integer"});
  assertValid("Number", -2147483648, {type:"integer"});
  assertNotValid("Number",           0.5, {type:"integer"},
                 [formatError("numberIntValue", [])]);
  assertNotValid("Number", 10000000000,   {type:"integer"},
                 [formatError("numberIntValue", [])]);
  assertNotValid("Number",  2147483647.5, {type:"integer"},
                 [formatError("numberIntValue", [])]);
  assertNotValid("Number",  2147483648,   {type:"integer"},
                 [formatError("numberIntValue", [])]);
  assertNotValid("Number",  2147483649,   {type:"integer"},
                 [formatError("numberIntValue", [])]);
  assertNotValid("Number", -2147483649,   {type:"integer"},
                 [formatError("numberIntValue", [])]);
}

function testType() {
  // valid
  assertValid("Type", {}, {type:"object"});
  assertValid("Type", [], {type:"array"});
  assertValid("Type", function(){}, {type:"function"});
  assertValid("Type", "foobar", {type:"string"});
  assertValid("Type", "", {type:"string"});
  assertValid("Type", 88.8, {type:"number"});
  assertValid("Type", 42, {type:"number"});
  assertValid("Type", 0, {type:"number"});
  assertValid("Type", 42, {type:"integer"});
  assertValid("Type", 0, {type:"integer"});
  assertValid("Type", true, {type:"boolean"});
  assertValid("Type", false, {type:"boolean"});
  assertValid("Type", null, {type:"null"});
  assertValid("Type", undefined, {type:"undefined"});

  // not valid
  assertNotValid("Type", [], {type: "object"},
                 [formatError("invalidType", ["object", "array"])]);
  assertNotValid("Type", null, {type: "object"},
                 [formatError("invalidType", ["object", "null"])]);
  assertNotValid("Type", function(){}, {type: "object"},
                 [formatError("invalidType", ["object", "function"])]);
  assertNotValid("Type", 42, {type: "array"},
                 [formatError("invalidType", ["array", "integer"])]);
  assertNotValid("Type", 42, {type: "string"},
                 [formatError("invalidType", ["string", "integer"])]);
  assertNotValid("Type", "42", {type: "number"},
                 [formatError("invalidType", ["number", "string"])]);
  assertNotValid("Type", 88.8, {type: "integer"},
                 [formatError("invalidTypeIntegerNumber")]);
  assertNotValid("Type", 1, {type: "boolean"},
                 [formatError("invalidType", ["boolean", "integer"])]);
  assertNotValid("Type", false, {type: "null"},
                 [formatError("invalidType", ["null", "boolean"])]);
  assertNotValid("Type", undefined, {type: "null"},
                 [formatError("invalidType", ["null", "undefined"])]);
  assertNotValid("Type", {}, {type: "function"},
                 [formatError("invalidType", ["function", "object"])]);
}

function testGetAllTypesForSchema() {
  var referencedTypes = [
    {
      id: "ChoicesRef",
      choices: [
        { type: "integer" },
        { type: "string" }
      ]
    },
    {
      id: "ObjectRef",
      type: "object",
    }
  ];

  var arraySchema = {
    type: "array"
  };

  var choicesSchema = {
    choices: [
      { type: "object" },
      { type: "function" }
    ]
  };

  var objectRefSchema = {
    $ref: "ObjectRef"
  };

  var complexSchema = {
    choices: [
      { $ref: "ChoicesRef" },
      { type: "function" },
      { $ref: "ObjectRef" }
    ]
  };

  var validator = new JSONSchemaValidator();
  validator.addTypes(referencedTypes);

  var arraySchemaTypes = validator.getAllTypesForSchema(arraySchema);
  assertEqualSets(arraySchemaTypes, ["array"]);

  var choicesSchemaTypes = validator.getAllTypesForSchema(choicesSchema);
  assertEqualSets(choicesSchemaTypes, ["object", "function"]);

  var objectRefSchemaTypes = validator.getAllTypesForSchema(objectRefSchema);
  assertEqualSets(objectRefSchemaTypes, ["object"]);

  var complexSchemaTypes = validator.getAllTypesForSchema(complexSchema);
  assertEqualSets(complexSchemaTypes,
      ["integer", "string", "function", "object"]);
}

function testIsValidSchemaType() {
  var referencedTypes = [
    {
      id: "ChoicesRef",
      choices: [
        { type: "integer" },
        { type: "string" }
      ]
    }
  ];

  var objectSchema = {
    type: "object",
    optional: true
  };

  var complexSchema = {
    choices: [
      { $ref: "ChoicesRef" },
      { type: "function" },
    ]
  };

  var validator = new JSONSchemaValidator();
  validator.addTypes(referencedTypes);

  AssertTrue(validator.isValidSchemaType("object", objectSchema));
  AssertTrue(!validator.isValidSchemaType("integer", objectSchema));
  AssertTrue(!validator.isValidSchemaType("array", objectSchema));
  AssertTrue(validator.isValidSchemaType("null", objectSchema));
  AssertTrue(validator.isValidSchemaType("undefined", objectSchema));

  AssertTrue(validator.isValidSchemaType("integer", complexSchema));
  AssertTrue(validator.isValidSchemaType("function", complexSchema));
  AssertTrue(validator.isValidSchemaType("string", complexSchema));
  AssertTrue(!validator.isValidSchemaType("object", complexSchema));
  AssertTrue(!validator.isValidSchemaType("null", complexSchema));
  AssertTrue(!validator.isValidSchemaType("undefined", complexSchema));
}

function testCheckSchemaOverlap() {
  var referencedTypes = [
    {
      id: "ChoicesRef",
      choices: [
        { type: "integer" },
        { type: "string" }
      ]
    },
    {
      id: "ObjectRef",
      type: "object",
    }
  ];

  var arraySchema = {
    type: "array"
  };

  var choicesSchema = {
    choices: [
      { type: "object" },
      { type: "function" }
    ]
  };

  var objectRefSchema = {
    $ref: "ObjectRef"
  };

  var complexSchema = {
    choices: [
      { $ref: "ChoicesRef" },
      { type: "function" },
      { $ref: "ObjectRef" }
    ]
  };

  var validator = new JSONSchemaValidator();
  validator.addTypes(referencedTypes);

  AssertTrue(!validator.checkSchemaOverlap(arraySchema, choicesSchema));
  AssertTrue(!validator.checkSchemaOverlap(arraySchema, objectRefSchema));
  AssertTrue(!validator.checkSchemaOverlap(arraySchema, complexSchema));
  AssertTrue(validator.checkSchemaOverlap(choicesSchema, objectRefSchema));
  AssertTrue(validator.checkSchemaOverlap(choicesSchema, complexSchema));
  AssertTrue(validator.checkSchemaOverlap(objectRefSchema, complexSchema));
}

function testInstanceOf() {
  function Animal() {};
  function Cat() {};
  function Dog() {};
  Cat.prototype = new Animal;
  Cat.prototype.constructor = Cat;
  Dog.prototype = new Animal;
  Dog.prototype.constructor = Dog;
  var cat = new Cat();
  var dog = new Dog();
  var num = new Number(1);

  // instanceOf should check type by walking up prototype chain.
  assertValid("", cat, {type:"object", isInstanceOf:"Cat"});
  assertValid("", cat, {type:"object", isInstanceOf:"Animal"});
  assertValid("", cat, {type:"object", isInstanceOf:"Object"});
  assertValid("", dog, {type:"object", isInstanceOf:"Dog"});
  assertValid("", dog, {type:"object", isInstanceOf:"Animal"});
  assertValid("", dog, {type:"object", isInstanceOf:"Object"});
  assertValid("", num, {type:"object", isInstanceOf:"Number"});
  assertValid("", num, {type:"object", isInstanceOf:"Object"});

  assertNotValid("", cat, {type:"object", isInstanceOf:"Dog"},
                 [formatError("notInstance", ["Dog"])]);
  assertNotValid("", dog, {type:"object", isInstanceOf:"Cat"},
                 [formatError("notInstance", ["Cat"])]);
  assertNotValid("", cat, {type:"object", isInstanceOf:"String"},
                 [formatError("notInstance", ["String"])]);
  assertNotValid("", dog, {type:"object", isInstanceOf:"String"},
                 [formatError("notInstance", ["String"])]);
  assertNotValid("", num, {type:"object", isInstanceOf:"Array"},
                [formatError("notInstance", ["Array"])]);
  assertNotValid("", num, {type:"object", isInstanceOf:"String"},
                [formatError("notInstance", ["String"])]);
}

// Tests exposed to schema_unittest.cc.
exports.testFormatError = testFormatError;
exports.testComplex = testComplex;
exports.testEnum = testEnum;
exports.testExtends = testExtends;
exports.testObject = testObject;
exports.testArrayTuple = testArrayTuple;
exports.testArrayNonTuple = testArrayNonTuple;
exports.testString = testString;
exports.testNumber = testNumber;
exports.testIntegerBounds = testIntegerBounds;
exports.testType = testType;
exports.testTypeReference = testTypeReference;
exports.testGetAllTypesForSchema = testGetAllTypesForSchema;
exports.testIsValidSchemaType = testIsValidSchemaType;
exports.testCheckSchemaOverlap = testCheckSchemaOverlap;
exports.testInstanceOf = testInstanceOf;