diff options
Diffstat (limited to 'toolkit/components/utils/test/browser')
3 files changed, 2017 insertions, 0 deletions
diff --git a/toolkit/components/utils/test/browser/browser.ini b/toolkit/components/utils/test/browser/browser.ini new file mode 100644 index 0000000000..09d139f297 --- /dev/null +++ b/toolkit/components/utils/test/browser/browser.ini @@ -0,0 +1,2 @@ +[browser_ClientEnvironment.js] +[browser_JsonSchemaValidator.js] diff --git a/toolkit/components/utils/test/browser/browser_ClientEnvironment.js b/toolkit/components/utils/test/browser/browser_ClientEnvironment.js new file mode 100644 index 0000000000..8ce0a6a228 --- /dev/null +++ b/toolkit/components/utils/test/browser/browser_ClientEnvironment.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* + * This can't be an xpcshell-test because, according to + * `toolkit/modules/Services.jsm`, "Not all applications implement + * nsIXULAppInfo (e.g. xpcshell doesn't)." + */ +const { ClientEnvironmentBase } = ChromeUtils.import( + "resource://gre/modules/components-utils/ClientEnvironment.jsm" +); + +const { NormandyTestUtils } = ChromeUtils.import( + "resource://testing-common/NormandyTestUtils.jsm" +); + +add_task(function testBuildId() { + ok( + ClientEnvironmentBase.appinfo !== undefined, + "appinfo should be available in the context" + ); + ok( + typeof ClientEnvironmentBase.appinfo === "object", + "appinfo should be an object" + ); + ok( + typeof ClientEnvironmentBase.appinfo.appBuildID === "string", + "buildId should be a string" + ); +}); + +add_task(async function testRandomizationId() { + // Should generate an id if none is set + await SpecialPowers.clearUserPref("app.normandy.user_id"); + ok( + NormandyTestUtils.isUuid(ClientEnvironmentBase.randomizationId), + "randomizationId should be available" + ); + + // Should read the right preference + await SpecialPowers.pushPrefEnv({ + set: [["app.normandy.user_id", "fake id"]], + }); + is( + ClientEnvironmentBase.randomizationId, + "fake id", + "randomizationId should read from preferences" + ); +}); diff --git a/toolkit/components/utils/test/browser/browser_JsonSchemaValidator.js b/toolkit/components/utils/test/browser/browser_JsonSchemaValidator.js new file mode 100644 index 0000000000..47fb935fc6 --- /dev/null +++ b/toolkit/components/utils/test/browser/browser_JsonSchemaValidator.js @@ -0,0 +1,1964 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.import( + "resource://gre/modules/components-utils/JsonSchemaValidator.jsm", + this +); + +add_task(async function test_boolean_values() { + let schema = { + type: "boolean", + }; + + // valid values + validate({ + value: true, + schema, + expectedResult: { + valid: true, + parsedValue: true, + }, + }); + validate({ + value: false, + schema, + expectedResult: { + valid: true, + parsedValue: false, + }, + }); + validate({ + value: 0, + schema, + expectedResult: { + valid: true, + parsedValue: false, + }, + }); + validate({ + value: 1, + schema, + expectedResult: { + valid: true, + parsedValue: true, + }, + }); + + // Invalid values: + validate({ + value: "0", + schema, + expectedResult: { + valid: false, + parsedValue: "0", + error: { + invalidValue: "0", + invalidPropertyNameComponents: [], + message: `The value '"0"' does not match the expected type 'boolean'`, + }, + }, + }); + validate({ + value: "true", + schema, + expectedResult: { + valid: false, + parsedValue: "true", + error: { + invalidValue: "true", + invalidPropertyNameComponents: [], + message: `The value '"true"' does not match the expected type 'boolean'`, + }, + }, + }); + validate({ + value: 2, + schema, + expectedResult: { + valid: false, + parsedValue: 2, + error: { + invalidValue: 2, + invalidPropertyNameComponents: [], + message: `The value '2' does not match the expected type 'boolean'`, + }, + }, + }); + validate({ + value: undefined, + schema, + expectedResult: { + valid: false, + parsedValue: undefined, + error: { + invalidValue: undefined, + invalidPropertyNameComponents: [], + message: `The value 'undefined' does not match the expected type 'boolean'`, + }, + }, + }); + validate({ + value: {}, + schema, + expectedResult: { + valid: false, + parsedValue: {}, + error: { + invalidValue: {}, + invalidPropertyNameComponents: [], + message: `The value '{}' does not match the expected type 'boolean'`, + }, + }, + }); + validate({ + value: null, + schema, + expectedResult: { + valid: false, + parsedValue: null, + error: { + invalidValue: null, + invalidPropertyNameComponents: [], + message: `The value 'null' does not match the expected type 'boolean'`, + }, + }, + }); +}); + +add_task(async function test_number_values() { + let schema = { + type: "number", + }; + + validate({ + value: 1, + schema, + expectedResult: { + valid: true, + parsedValue: 1, + }, + }); + + // Invalid values: + validate({ + value: "1", + schema, + expectedResult: { + valid: false, + parsedValue: "1", + error: { + invalidValue: "1", + invalidPropertyNameComponents: [], + message: `The value '"1"' does not match the expected type 'number'`, + }, + }, + }); + validate({ + value: true, + schema, + expectedResult: { + valid: false, + parsedValue: true, + error: { + invalidValue: true, + invalidPropertyNameComponents: [], + message: `The value 'true' does not match the expected type 'number'`, + }, + }, + }); + validate({ + value: {}, + schema, + expectedResult: { + valid: false, + parsedValue: {}, + error: { + invalidValue: {}, + invalidPropertyNameComponents: [], + message: `The value '{}' does not match the expected type 'number'`, + }, + }, + }); + validate({ + value: null, + schema, + expectedResult: { + valid: false, + parsedValue: null, + error: { + invalidValue: null, + invalidPropertyNameComponents: [], + message: `The value 'null' does not match the expected type 'number'`, + }, + }, + }); +}); + +add_task(async function test_integer_values() { + // Integer is an alias for number + let schema = { + type: "integer", + }; + + validate({ + value: 1, + schema, + expectedResult: { + valid: true, + parsedValue: 1, + }, + }); + + // Invalid values: + validate({ + value: "1", + schema, + expectedResult: { + valid: false, + parsedValue: "1", + error: { + invalidValue: "1", + invalidPropertyNameComponents: [], + message: `The value '"1"' does not match the expected type 'integer'`, + }, + }, + }); + validate({ + value: true, + schema, + expectedResult: { + valid: false, + parsedValue: true, + error: { + invalidValue: true, + invalidPropertyNameComponents: [], + message: `The value 'true' does not match the expected type 'integer'`, + }, + }, + }); + validate({ + value: {}, + schema, + expectedResult: { + valid: false, + parsedValue: {}, + error: { + invalidValue: {}, + invalidPropertyNameComponents: [], + message: `The value '{}' does not match the expected type 'integer'`, + }, + }, + }); + validate({ + value: null, + schema, + expectedResult: { + valid: false, + parsedValue: null, + error: { + invalidValue: null, + invalidPropertyNameComponents: [], + message: `The value 'null' does not match the expected type 'integer'`, + }, + }, + }); +}); + +add_task(async function test_null_values() { + let schema = { + type: "null", + }; + + validate({ + value: null, + schema, + expectedResult: { + valid: true, + parsedValue: null, + }, + }); + + // Invalid values: + validate({ + value: 1, + schema, + expectedResult: { + valid: false, + parsedValue: 1, + error: { + invalidValue: 1, + invalidPropertyNameComponents: [], + message: `The value '1' does not match the expected type 'null'`, + }, + }, + }); + validate({ + value: "1", + schema, + expectedResult: { + valid: false, + parsedValue: "1", + error: { + invalidValue: "1", + invalidPropertyNameComponents: [], + message: `The value '"1"' does not match the expected type 'null'`, + }, + }, + }); + validate({ + value: true, + schema, + expectedResult: { + valid: false, + parsedValue: true, + error: { + invalidValue: true, + invalidPropertyNameComponents: [], + message: `The value 'true' does not match the expected type 'null'`, + }, + }, + }); + validate({ + value: {}, + schema, + expectedResult: { + valid: false, + parsedValue: {}, + error: { + invalidValue: {}, + invalidPropertyNameComponents: [], + message: `The value '{}' does not match the expected type 'null'`, + }, + }, + }); + validate({ + value: [], + schema, + expectedResult: { + valid: false, + parsedValue: [], + error: { + invalidValue: [], + invalidPropertyNameComponents: [], + message: `The value '[]' does not match the expected type 'null'`, + }, + }, + }); +}); + +add_task(async function test_string_values() { + let schema = { + type: "string", + }; + + validate({ + value: "foobar", + schema, + expectedResult: { + valid: true, + parsedValue: "foobar", + }, + }); + + // Invalid values: + validate({ + value: 1, + schema, + expectedResult: { + valid: false, + parsedValue: 1, + error: { + invalidValue: 1, + invalidPropertyNameComponents: [], + message: `The value '1' does not match the expected type 'string'`, + }, + }, + }); + validate({ + value: true, + schema, + expectedResult: { + valid: false, + parsedValue: true, + error: { + invalidValue: true, + invalidPropertyNameComponents: [], + message: `The value 'true' does not match the expected type 'string'`, + }, + }, + }); + validate({ + value: undefined, + schema, + expectedResult: { + valid: false, + parsedValue: undefined, + error: { + invalidValue: undefined, + invalidPropertyNameComponents: [], + message: `The value 'undefined' does not match the expected type 'string'`, + }, + }, + }); + validate({ + value: {}, + schema, + expectedResult: { + valid: false, + parsedValue: {}, + error: { + invalidValue: {}, + invalidPropertyNameComponents: [], + message: `The value '{}' does not match the expected type 'string'`, + }, + }, + }); + validate({ + value: null, + schema, + expectedResult: { + valid: false, + parsedValue: null, + error: { + invalidValue: null, + invalidPropertyNameComponents: [], + message: `The value 'null' does not match the expected type 'string'`, + }, + }, + }); +}); + +add_task(async function test_URL_values() { + let schema = { + type: "URL", + }; + + let result = validate({ + value: "https://www.example.com/foo#bar", + schema, + expectedResult: { + valid: true, + parsedValue: new URL("https://www.example.com/foo#bar"), + }, + }); + Assert.ok(result.parsedValue instanceof URL, "parsedValue is a URL"); + Assert.equal( + result.parsedValue.origin, + "https://www.example.com", + "origin is correct" + ); + Assert.equal( + result.parsedValue.pathname + result.parsedValue.hash, + "/foo#bar", + "pathname is correct" + ); + + // Invalid values: + validate({ + value: "", + schema, + expectedResult: { + valid: false, + parsedValue: "", + error: { + invalidValue: "", + invalidPropertyNameComponents: [], + message: `The value '""' does not match the expected type 'URL'`, + }, + }, + }); + validate({ + value: "www.example.com", + schema, + expectedResult: { + valid: false, + parsedValue: "www.example.com", + error: { + invalidValue: "www.example.com", + invalidPropertyNameComponents: [], + message: + `The value '"www.example.com"' does not match the expected ` + + `type 'URL'`, + }, + }, + }); + validate({ + value: "https://:!$%", + schema, + expectedResult: { + valid: false, + parsedValue: "https://:!$%", + error: { + invalidValue: "https://:!$%", + invalidPropertyNameComponents: [], + message: `The value '"https://:!$%"' does not match the expected type 'URL'`, + }, + }, + }); + validate({ + value: {}, + schema, + expectedResult: { + valid: false, + parsedValue: {}, + error: { + invalidValue: {}, + invalidPropertyNameComponents: [], + message: `The value '{}' does not match the expected type 'URL'`, + }, + }, + }); +}); + +add_task(async function test_URLorEmpty_values() { + let schema = { + type: "URLorEmpty", + }; + + let result = validate({ + value: "https://www.example.com/foo#bar", + schema, + expectedResult: { + valid: true, + parsedValue: new URL("https://www.example.com/foo#bar"), + }, + }); + Assert.ok(result.parsedValue instanceof URL, "parsedValue is a URL"); + Assert.equal( + result.parsedValue.origin, + "https://www.example.com", + "origin is correct" + ); + Assert.equal( + result.parsedValue.pathname + result.parsedValue.hash, + "/foo#bar", + "pathname is correct" + ); + + // Test that this type also accept empty strings + result = validate({ + value: "", + schema, + expectedResult: { + valid: true, + parsedValue: "", + }, + }); + Assert.equal(typeof result.parsedValue, "string", "parsedValue is a string"); + + // Invalid values: + validate({ + value: " ", + schema, + expectedResult: { + valid: false, + parsedValue: " ", + error: { + invalidValue: " ", + invalidPropertyNameComponents: [], + message: `The value '" "' does not match the expected type 'URLorEmpty'`, + }, + }, + }); + validate({ + value: "www.example.com", + schema, + expectedResult: { + valid: false, + parsedValue: "www.example.com", + error: { + invalidValue: "www.example.com", + invalidPropertyNameComponents: [], + message: + `The value '"www.example.com"' does not match the expected ` + + `type 'URLorEmpty'`, + }, + }, + }); + validate({ + value: "https://:!$%", + schema, + expectedResult: { + valid: false, + parsedValue: "https://:!$%", + error: { + invalidValue: "https://:!$%", + invalidPropertyNameComponents: [], + message: + `The value '"https://:!$%"' does not match the expected ` + + `type 'URLorEmpty'`, + }, + }, + }); + validate({ + value: {}, + schema, + expectedResult: { + valid: false, + parsedValue: {}, + error: { + invalidValue: {}, + invalidPropertyNameComponents: [], + message: `The value '{}' does not match the expected type 'URLorEmpty'`, + }, + }, + }); +}); + +add_task(async function test_origin_values() { + // Origin is a URL that doesn't contain a path/query string (i.e., it's only scheme + host + port) + let schema = { + type: "origin", + }; + + let result = validate({ + value: "https://www.example.com", + schema, + expectedResult: { + valid: true, + parsedValue: new URL("https://www.example.com/"), + }, + }); + Assert.ok(result.parsedValue instanceof URL, "parsedValue is a URL"); + Assert.equal( + result.parsedValue.origin, + "https://www.example.com", + "origin is correct" + ); + Assert.equal( + result.parsedValue.pathname + result.parsedValue.hash, + "/", + "pathname is correct" + ); + + // Invalid values: + validate({ + value: "https://www.example.com/foobar", + schema, + expectedResult: { + valid: false, + parsedValue: new URL("https://www.example.com/foobar"), + error: { + invalidValue: "https://www.example.com/foobar", + invalidPropertyNameComponents: [], + message: + `The value '"https://www.example.com/foobar"' does not match the ` + + `expected type 'origin'`, + }, + }, + }); + validate({ + value: "https://:!$%", + schema, + expectedResult: { + valid: false, + parsedValue: "https://:!$%", + error: { + invalidValue: "https://:!$%", + invalidPropertyNameComponents: [], + message: + `The value '"https://:!$%"' does not match the expected ` + + `type 'origin'`, + }, + }, + }); + validate({ + value: {}, + schema, + expectedResult: { + valid: false, + parsedValue: {}, + error: { + invalidValue: {}, + invalidPropertyNameComponents: [], + message: `The value '{}' does not match the expected type 'origin'`, + }, + }, + }); +}); + +add_task(async function test_origin_file_values() { + // File URLs can also be origins + let schema = { + type: "origin", + }; + + let result = validate({ + value: "file:///foo/bar", + schema, + expectedResult: { + valid: true, + parsedValue: new URL("file:///foo/bar"), + }, + }); + Assert.ok(result.parsedValue instanceof URL, "parsedValue is a URL"); + Assert.equal( + result.parsedValue.href, + "file:///foo/bar", + "Should get what we passed in" + ); +}); + +add_task(async function test_origin_file_values() { + // File URLs can also be origins + let schema = { + type: "origin", + }; + + let result = validate({ + value: "file:///foo/bar/foobar.html", + schema, + expectedResult: { + valid: true, + parsedValue: new URL("file:///foo/bar/foobar.html"), + }, + }); + Assert.ok(result.parsedValue instanceof URL, "parsedValue is a URL"); + Assert.equal( + result.parsedValue.href, + "file:///foo/bar/foobar.html", + "Should get what we passed in" + ); +}); + +add_task(async function test_array_values() { + // The types inside an array object must all be the same + let schema = { + type: "array", + items: { + type: "number", + }, + }; + + validate({ + value: [1, 2, 3], + schema, + expectedResult: { + valid: true, + parsedValue: [1, 2, 3], + }, + }); + + // An empty array is also valid + validate({ + value: [], + schema, + expectedResult: { + valid: true, + parsedValue: [], + }, + }); + + // Invalid values: + validate({ + value: [1, true, 3], + schema, + expectedResult: { + valid: false, + parsedValue: true, + error: { + invalidValue: true, + invalidPropertyNameComponents: [1], + message: + `The value 'true' does not match the expected type 'number'. The ` + + `invalid value is property '1' in [1,true,3]`, + }, + }, + }); + validate({ + value: 2, + schema, + expectedResult: { + valid: false, + parsedValue: undefined, + error: { + invalidValue: 2, + invalidPropertyNameComponents: [], + message: `The value '2' does not match the expected type 'array'`, + }, + }, + }); + validate({ + value: {}, + schema, + expectedResult: { + valid: false, + parsedValue: undefined, + error: { + invalidValue: {}, + invalidPropertyNameComponents: [], + message: `The value '{}' does not match the expected type 'array'`, + }, + }, + }); +}); + +add_task(async function test_non_strict_arrays() { + // Non-strict arrays ignores invalid values (don't include + // them in the parsed output), instead of failing the validation. + // Note: invalid values might still report errors to the console. + let schema = { + type: "array", + strict: false, + items: { + type: "string", + }, + }; + + validate({ + value: ["valid1", "valid2", false, 3, "valid3"], + schema, + expectedResult: { + valid: true, + parsedValue: ["valid1", "valid2", "valid3"], + }, + }); + + // Checks that strict defaults to true; + delete schema.strict; + validate({ + value: ["valid1", "valid2", false, 3, "valid3"], + schema, + expectedResult: { + valid: false, + parsedValue: false, + error: { + invalidValue: false, + invalidPropertyNameComponents: [2], + message: + `The value 'false' does not match the expected type 'string'. The ` + + `invalid value is property '2' in ` + + `["valid1","valid2",false,3,"valid3"]`, + }, + }, + }); + + // Pass allowArrayNonMatchingItems, should be valid + validate({ + value: ["valid1", "valid2", false, 3, "valid3"], + schema, + options: { + allowArrayNonMatchingItems: true, + }, + expectedResult: { + valid: true, + parsedValue: ["valid1", "valid2", "valid3"], + }, + }); +}); + +add_task(async function test_object_values() { + // valid values below + + validate({ + value: { + foo: "hello", + bar: 123, + }, + schema: { + type: "object", + properties: { + foo: { + type: "string", + }, + bar: { + type: "number", + }, + }, + }, + expectedResult: { + valid: true, + parsedValue: { + foo: "hello", + bar: 123, + }, + }, + }); + + validate({ + value: { + foo: "hello", + bar: { + baz: 123, + }, + }, + schema: { + type: "object", + properties: { + foo: { + type: "string", + }, + bar: { + type: "object", + properties: { + baz: { + type: "number", + }, + }, + }, + }, + }, + expectedResult: { + valid: true, + parsedValue: { + foo: "hello", + bar: { + baz: 123, + }, + }, + }, + }); + + // allowExtraProperties + let result = validate({ + value: { + url: "https://www.example.com/foo#bar", + title: "Foo", + alias: "Bar", + }, + schema: { + type: "object", + properties: { + url: { + type: "URL", + }, + title: { + type: "string", + }, + }, + }, + options: { + allowExtraProperties: true, + }, + expectedResult: { + valid: true, + parsedValue: { + url: new URL("https://www.example.com/foo#bar"), + title: "Foo", + }, + }, + }); + Assert.ok( + result.parsedValue.url instanceof URL, + "types inside the object are also parsed" + ); + Assert.equal( + result.parsedValue.url.href, + "https://www.example.com/foo#bar", + "URL was correctly parsed" + ); + + // allowExplicitUndefinedProperties + validate({ + value: { + foo: undefined, + }, + schema: { + type: "object", + properties: { + foo: { + type: "string", + }, + }, + }, + options: { + allowExplicitUndefinedProperties: true, + }, + expectedResult: { + valid: true, + parsedValue: {}, + }, + }); + + // allowNullAsUndefinedProperties + validate({ + value: { + foo: null, + }, + schema: { + type: "object", + properties: { + foo: { + type: "string", + }, + }, + }, + options: { + allowNullAsUndefinedProperties: true, + }, + expectedResult: { + valid: true, + parsedValue: {}, + }, + }); + + // invalid values below + + validate({ + value: null, + schema: { + type: "object", + }, + expectedResult: { + valid: false, + parsedValue: null, + error: { + invalidValue: null, + invalidPropertyNameComponents: [], + message: `The value 'null' does not match the expected type 'object'`, + }, + }, + }); + + validate({ + value: { + url: "not a URL", + }, + schema: { + type: "object", + properties: { + url: { + type: "URL", + }, + }, + }, + expectedResult: { + valid: false, + parsedValue: "not a URL", + error: { + invalidValue: "not a URL", + invalidPropertyNameComponents: ["url"], + message: + `The value '"not a URL"' does not match the expected type 'URL'. ` + + `The invalid value is property 'url' in {"url":"not a URL"}`, + }, + }, + }); + + validate({ + value: "test", + schema: { + type: "object", + properties: { + foo: { + type: "string", + }, + }, + }, + expectedResult: { + valid: false, + error: { + invalidValue: "test", + invalidPropertyNameComponents: [], + message: `The value '"test"' does not match the expected type 'object'`, + }, + }, + }); + + validate({ + value: { + foo: 123, + }, + schema: { + type: "object", + properties: { + foo: { + type: "string", + }, + }, + }, + expectedResult: { + valid: false, + parsedValue: 123, + error: { + invalidValue: 123, + invalidPropertyNameComponents: ["foo"], + message: + `The value '123' does not match the expected type 'string'. ` + + `The invalid value is property 'foo' in {"foo":123}`, + }, + }, + }); + + validate({ + value: { + foo: { + bar: 456, + }, + }, + schema: { + type: "object", + properties: { + foo: { + type: "object", + properties: { + bar: { + type: "string", + }, + }, + }, + }, + }, + expectedResult: { + valid: false, + parsedValue: 456, + error: { + invalidValue: 456, + invalidPropertyNameComponents: ["foo", "bar"], + message: + `The value '456' does not match the expected type 'string'. ` + + `The invalid value is property 'foo.bar' in {"foo":{"bar":456}}`, + }, + }, + }); + + // null non-required property with strict=true: invalid + validate({ + value: { + foo: null, + }, + schema: { + type: "object", + properties: { + foo: { + type: "string", + }, + }, + }, + expectedResult: { + valid: false, + parsedValue: null, + error: { + invalidValue: null, + invalidPropertyNameComponents: ["foo"], + message: + `The value 'null' does not match the expected type 'string'. ` + + `The invalid value is property 'foo' in {"foo":null}`, + }, + }, + }); + validate({ + value: { + foo: null, + }, + schema: { + type: "object", + strict: true, + properties: { + foo: { + type: "string", + }, + }, + }, + options: { + allowNullAsUndefinedProperties: true, + }, + expectedResult: { + valid: false, + parsedValue: null, + error: { + invalidValue: null, + invalidPropertyNameComponents: ["foo"], + message: + `The value 'null' does not match the expected type 'string'. ` + + `The invalid value is property 'foo' in {"foo":null}`, + }, + }, + }); + + // non-null falsey non-required property with strict=false: invalid + validate({ + value: { + foo: false, + }, + schema: { + type: "object", + properties: { + foo: { + type: "string", + }, + }, + }, + options: { + allowExplicitUndefinedProperties: true, + allowNullAsUndefinedProperties: true, + }, + expectedResult: { + valid: false, + parsedValue: false, + error: { + invalidValue: false, + invalidPropertyNameComponents: ["foo"], + message: + `The value 'false' does not match the expected type 'string'. ` + + `The invalid value is property 'foo' in {"foo":false}`, + }, + }, + }); + validate({ + value: { + foo: false, + }, + schema: { + type: "object", + strict: false, + properties: { + foo: { + type: "string", + }, + }, + }, + expectedResult: { + valid: false, + parsedValue: false, + error: { + invalidValue: false, + invalidPropertyNameComponents: ["foo"], + message: + `The value 'false' does not match the expected type 'string'. ` + + `The invalid value is property 'foo' in {"foo":false}`, + }, + }, + }); + + validate({ + value: { + bogus: "test", + }, + schema: { + type: "object", + properties: { + foo: { + type: "string", + }, + }, + }, + expectedResult: { + valid: false, + parsedValue: undefined, + error: { + invalidValue: { bogus: "test" }, + invalidPropertyNameComponents: [], + message: `Object has unexpected property 'bogus'`, + }, + }, + }); + + validate({ + value: { + foo: { + bogus: "test", + }, + }, + schema: { + type: "object", + properties: { + foo: { + type: "object", + properties: { + bar: { + type: "string", + }, + }, + }, + }, + }, + expectedResult: { + valid: false, + parsedValue: undefined, + error: { + invalidValue: { bogus: "test" }, + invalidPropertyNameComponents: ["foo"], + message: + `Object has unexpected property 'bogus'. The invalid value is ` + + `property 'foo' in {"foo":{"bogus":"test"}}`, + }, + }, + }); +}); + +add_task(async function test_array_of_objects() { + // This schema is used, for example, for bookmarks + let schema = { + type: "array", + items: { + type: "object", + properties: { + url: { + type: "URL", + }, + title: { + type: "string", + }, + }, + }, + }; + + validate({ + value: [ + { + url: "https://www.example.com/bookmark1", + title: "Foo", + }, + { + url: "https://www.example.com/bookmark2", + title: "Bar", + }, + ], + schema, + expectedResult: { + valid: true, + parsedValue: [ + { + url: new URL("https://www.example.com/bookmark1"), + title: "Foo", + }, + { + url: new URL("https://www.example.com/bookmark2"), + title: "Bar", + }, + ], + }, + }); +}); + +add_task(async function test_missing_arrays_inside_objects() { + let schema = { + type: "object", + properties: { + allow: { + type: "array", + items: { + type: "boolean", + }, + }, + block: { + type: "array", + items: { + type: "boolean", + }, + }, + }, + }; + + validate({ + value: { + allow: [true, true, true], + }, + schema, + expectedResult: { + valid: true, + parsedValue: { + allow: [true, true, true], + }, + }, + }); +}); + +add_task(async function test_required_vs_nonrequired_properties() { + let schema = { + type: "object", + properties: { + "non-required-property": { + type: "number", + }, + + "required-property": { + type: "number", + }, + }, + required: ["required-property"], + }; + + validate({ + value: { + "required-property": 5, + "non-required-property": undefined, + }, + schema, + options: { + allowExplicitUndefinedProperties: true, + }, + expectedResult: { + valid: true, + parsedValue: { + "required-property": 5, + }, + }, + }); + + validate({ + value: { + "non-required-property": 5, + }, + schema, + expectedResult: { + valid: false, + parsedValue: undefined, + error: { + invalidValue: { + "non-required-property": 5, + }, + invalidPropertyNameComponents: [], + message: `Object is missing required property 'required-property'`, + }, + }, + }); +}); + +add_task(async function test_number_or_string_values() { + let schema = { + type: ["number", "string"], + }; + + validate({ + value: 1, + schema, + expectedResult: { + valid: true, + parsedValue: 1, + }, + }); + validate({ + value: "foobar", + schema, + expectedResult: { + valid: true, + parsedValue: "foobar", + }, + }); + validate({ + value: "1", + schema, + expectedResult: { + valid: true, + parsedValue: "1", + }, + }); + + // Invalid values: + validate({ + value: true, + schema, + expectedResult: { + valid: false, + parsedValue: undefined, + error: { + invalidValue: true, + invalidPropertyNameComponents: [], + message: `The value 'true' does not match any type in ["number","string"]`, + }, + }, + }); + validate({ + value: {}, + schema, + expectedResult: { + valid: false, + parsedValue: undefined, + error: { + invalidValue: {}, + invalidPropertyNameComponents: [], + message: `The value '{}' does not match any type in ["number","string"]`, + }, + }, + }); + validate({ + value: null, + schema, + expectedResult: { + valid: false, + parsedValue: undefined, + error: { + invalidValue: null, + invalidPropertyNameComponents: [], + message: `The value 'null' does not match any type in ["number","string"]`, + }, + }, + }); +}); + +add_task(async function test_number_or_array_values() { + let schema = { + type: ["number", "array"], + items: { + type: "number", + }, + }; + + validate({ + value: 1, + schema, + expectedResult: { + valid: true, + parsedValue: 1, + }, + }); + validate({ + value: [1, 2, 3], + schema, + expectedResult: { + valid: true, + parsedValue: [1, 2, 3], + }, + }); + + // Invalid values: + validate({ + value: true, + schema, + expectedResult: { + valid: false, + parsedValue: undefined, + error: { + invalidValue: true, + invalidPropertyNameComponents: [], + message: `The value 'true' does not match any type in ["number","array"]`, + }, + }, + }); + validate({ + value: {}, + schema, + expectedResult: { + valid: false, + parsedValue: undefined, + error: { + invalidValue: {}, + invalidPropertyNameComponents: [], + message: `The value '{}' does not match any type in ["number","array"]`, + }, + }, + }); + validate({ + value: null, + schema, + expectedResult: { + valid: false, + parsedValue: undefined, + error: { + invalidValue: null, + invalidPropertyNameComponents: [], + message: `The value 'null' does not match any type in ["number","array"]`, + }, + }, + }); + validate({ + value: ["a", "b"], + schema, + expectedResult: { + valid: false, + parsedValue: undefined, + error: { + invalidValue: ["a", "b"], + invalidPropertyNameComponents: [], + message: `The value '["a","b"]' does not match any type in ["number","array"]`, + }, + }, + }); + validate({ + value: [[]], + schema, + expectedResult: { + valid: false, + parsedValue: undefined, + error: { + invalidValue: [[]], + invalidPropertyNameComponents: [], + message: `The value '[[]]' does not match any type in ["number","array"]`, + }, + }, + }); + validate({ + value: [0, 1, [2, 3]], + schema, + expectedResult: { + valid: false, + parsedValue: undefined, + error: { + invalidValue: [0, 1, [2, 3]], + invalidPropertyNameComponents: [], + message: + `The value '[0,1,[2,3]]' does not match any type in ` + + `["number","array"]`, + }, + }, + }); +}); + +add_task(function test_number_or_null_Values() { + let schema = { + type: ["number", "null"], + }; + + validate({ + value: 1, + schema, + expectedResult: { + valid: true, + parsedValue: 1, + }, + }); + validate({ + value: null, + schema, + expectedResult: { + valid: true, + parsedValue: null, + }, + }); + + // Invalid values: + validate({ + value: true, + schema, + expectedResult: { + valid: false, + parsedValue: undefined, + error: { + invalidValue: true, + invalidPropertyNameComponents: [], + message: `The value 'true' does not match any type in ["number","null"]`, + }, + }, + }); + validate({ + value: "string", + schema, + expectedResult: { + valid: false, + parsedValue: undefined, + error: { + invalidValue: "string", + invalidPropertyNameComponents: [], + message: `The value '"string"' does not match any type in ["number","null"]`, + }, + }, + }); + validate({ + value: {}, + schema, + expectedResult: { + valid: false, + parsedValue: undefined, + error: { + invalidValue: {}, + invalidPropertyNameComponents: [], + message: `The value '{}' does not match any type in ["number","null"]`, + }, + }, + }); + validate({ + value: ["a", "b"], + schema, + expectedResult: { + valid: false, + parsedValue: undefined, + error: { + invalidValue: ["a", "b"], + invalidPropertyNameComponents: [], + message: `The value '["a","b"]' does not match any type in ["number","null"]`, + }, + }, + }); +}); + +add_task(async function test_patternProperties() { + let schema = { + type: "object", + properties: { + "S-bool-property": { type: "boolean" }, + }, + patternProperties: { + "^S-": { type: "string" }, + "^N-": { type: "number" }, + "^B-": { type: "boolean" }, + }, + }; + + validate({ + value: { + "S-string": "test", + "N-number": 5, + "B-boolean": true, + "S-bool-property": false, + }, + schema, + expectedResult: { + valid: true, + parsedValue: { + "S-string": "test", + "N-number": 5, + "B-boolean": true, + "S-bool-property": false, + }, + }, + }); + + validate({ + value: { + "N-string": "test", + }, + schema, + expectedResult: { + valid: false, + parsedValue: "test", + error: { + invalidValue: "test", + invalidPropertyNameComponents: ["N-string"], + message: + `The value '"test"' does not match the expected type 'number'. ` + + `The invalid value is property 'N-string' in {"N-string":"test"}`, + }, + }, + }); + + validate({ + value: { + "S-number": 5, + }, + schema, + expectedResult: { + valid: false, + parsedValue: 5, + error: { + invalidValue: 5, + invalidPropertyNameComponents: ["S-number"], + message: + `The value '5' does not match the expected type 'string'. ` + + `The invalid value is property 'S-number' in {"S-number":5}`, + }, + }, + }); + + schema = { + type: "object", + patternProperties: { + "[": { " type": "string" }, + }, + }; + + Assert.throws( + () => JsonSchemaValidator.validate({}, schema), + /Invalid property pattern/, + "Checking that invalid property patterns throw" + ); +}); + +add_task(async function test_JSON_type() { + let schema = { + type: "JSON", + }; + + validate({ + value: { + a: "b", + }, + schema, + expectedResult: { + valid: true, + parsedValue: { + a: "b", + }, + }, + }); + validate({ + value: '{"a": "b"}', + schema, + expectedResult: { + valid: true, + parsedValue: { + a: "b", + }, + }, + }); + + validate({ + value: "{This{is{not{JSON}}}}", + schema, + expectedResult: { + valid: false, + parsedValue: undefined, + error: { + invalidValue: "{This{is{not{JSON}}}}", + invalidPropertyNameComponents: [], + message: `JSON string could not be parsed: "{This{is{not{JSON}}}}"`, + }, + }, + }); + validate({ + value: "0", + schema, + expectedResult: { + valid: false, + parsedValue: undefined, + error: { + invalidValue: "0", + invalidPropertyNameComponents: [], + message: `JSON was not an object: "0"`, + }, + }, + }); + validate({ + value: "true", + schema, + expectedResult: { + valid: false, + parsedValue: undefined, + error: { + invalidValue: "true", + invalidPropertyNameComponents: [], + message: `JSON was not an object: "true"`, + }, + }, + }); +}); + +add_task(async function test_enum() { + let schema = { + type: "string", + enum: ["one", "two"], + }; + + validate({ + value: "one", + schema, + expectedResult: { + valid: true, + parsedValue: "one", + }, + }); + + validate({ + value: "three", + schema, + expectedResult: { + valid: false, + parsedValue: undefined, + error: { + invalidValue: "three", + invalidPropertyNameComponents: [], + message: + `The value '"three"' is not one of the enumerated values ` + + `["one","two"]`, + }, + }, + }); +}); + +add_task(async function test_bool_enum() { + let schema = { + type: "boolean", + enum: ["one", "two"], + }; + + // `enum` is ignored because `type` is boolean. + validate({ + value: true, + schema, + expectedResult: { + valid: true, + parsedValue: true, + }, + }); +}); + +add_task(async function test_boolint_enum() { + let schema = { + type: "boolean", + enum: ["one", "two"], + }; + + // `enum` is ignored because `type` is boolean and the integer value was + // coerced to boolean. + validate({ + value: 1, + schema, + expectedResult: { + valid: true, + parsedValue: true, + }, + }); +}); + +/** + * Validates a value against a schema and asserts that the result is as + * expected. + * + * @param {*} value + * The value to validate. + * @param {object} schema + * The schema to validate against. + * @param {object} expectedResult + * The expected result. See JsonSchemaValidator.validate for what this object + * should look like. If the expected result is invalid, then this object + * should have an `error` property with all the properties of validation + * errors, including `message`, except that `rootValue` and `rootSchema` are + * unnecessary because this function will add them for you. + * @param {object} options + * Options to pass to JsonSchemaValidator.validate. + * @return {object} The return value of JsonSchemaValidator.validate, which is + * a result. + */ +function validate({ value, schema, expectedResult, options = undefined }) { + let result = JsonSchemaValidator.validate(value, schema, options); + + checkObject( + result, + expectedResult, + { + valid: false, + parsedValue: true, + }, + "Checking result property: " + ); + + Assert.equal("error" in result, "error" in expectedResult, "result.error"); + if (result.error && expectedResult.error) { + expectedResult.error = Object.assign(expectedResult.error, { + rootValue: value, + rootSchema: schema, + }); + checkObject( + result.error, + expectedResult.error, + { + rootValue: true, + rootSchema: false, + invalidPropertyNameComponents: false, + invalidValue: true, + message: false, + }, + "Checking result.error property: " + ); + } + + return result; +} + +/** + * Asserts that an object is the same as an expected object. + * + * @param {*} actual + * The actual object. + * @param {*} expected + * The expected object. + * @param {object} properties + * The properties to compare in the two objects. This value should be an + * object. The keys are the names of properties in the two objects. The + * values are booleans: true means that the property should be compared using + * strict equality and false means deep equality. Deep equality is used if + * the property is an object. + */ +function checkObject(actual, expected, properties, message) { + for (let [name, strict] of Object.entries(properties)) { + let assertFunc = + !strict || typeof expected[name] == "object" + ? "deepEqual" + : "strictEqual"; + Assert[assertFunc](actual[name], expected[name], message + name); + } +} |