diff options
Diffstat (limited to 'toolkit/components/utils/test')
5 files changed, 2652 insertions, 0 deletions
diff --git a/toolkit/components/utils/test/unit/test_ClientEnvironment.js b/toolkit/components/utils/test/unit/test_ClientEnvironment.js new file mode 100644 index 0000000000..7d0c8f9d51 --- /dev/null +++ b/toolkit/components/utils/test/unit/test_ClientEnvironment.js @@ -0,0 +1,147 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + AppConstants: "resource://gre/modules/AppConstants.sys.mjs", + ClientEnvironmentBase: + "resource://gre/modules/components-utils/ClientEnvironment.sys.mjs", + NormandyTestUtils: "resource://testing-common/NormandyTestUtils.sys.mjs", + TelemetryController: "resource://gre/modules/TelemetryController.sys.mjs", + updateAppInfo: "resource://testing-common/AppInfo.sys.mjs", +}); + +add_setup(() => { + updateAppInfo(); +}); + +add_task(async function test_OS_data() { + const os = ClientEnvironmentBase.os; + ok(os !== undefined, "OS data should be available in the context"); + + let osCount = 0; + if (os.isWindows) { + osCount += 1; + } + if (os.isMac) { + osCount += 1; + } + if (os.isLinux) { + osCount += 1; + } + ok(osCount <= 1, "At most one OS should match"); + + // if on Windows, Windows versions should be set, and Mac versions should not be + if (os.isWindows) { + equal( + typeof os.windowsVersion, + "number", + "Windows version should be a number" + ); + equal( + typeof os.windowsBuildNumber, + "number", + "Windows build number should be a number" + ); + equal(os.macVersion, null, "Mac version should not be set"); + equal(os.darwinVersion, null, "Darwin version should not be set"); + } + + // if on Mac, Mac versions should be set, and Windows versions should not be + if (os.isMac) { + equal(typeof os.macVersion, "number", "Mac version should be a number"); + equal( + typeof os.darwinVersion, + "number", + "Darwin version should be a number" + ); + equal(os.windowsVersion, null, "Windows version should not be set"); + equal( + os.windowsBuildNumber, + null, + "Windows build number version should not be set" + ); + } + + // if on Linux, no versions should be set + if (os.isLinux) { + equal(os.macVersion, null, "Mac version should not be set"); + equal(os.darwinVersion, null, "Darwin version should not be set"); + equal(os.windowsVersion, null, "Windows version should not be set"); + equal( + os.windowsBuildNumber, + null, + "Windows build number version should not be set" + ); + } +}); + +add_task(async function test_attributionData() { + try { + await ClientEnvironmentBase.attribution; + } catch (ex) { + equal( + ex.result, + Cr.NS_ERROR_FILE_NOT_FOUND, + "Test environment does not have attribution data" + ); + } +}); + +add_task(async function testLiveTelemetry() { + // Setup telemetry so we can read from it + do_get_profile(true); + await TelemetryController.testSetup(); + + equal( + ClientEnvironmentBase.liveTelemetry.main.environment.build.displayVersion, + AppConstants.MOZ_APP_VERSION_DISPLAY, + "Telemetry data is available" + ); + + Assert.throws( + () => ClientEnvironmentBase.liveTelemetry.anotherPingType, + /Live telemetry.*anotherPingType/, + "Non-main pings should raise an error if accessed" + ); + + // Put things back the way we found them + await TelemetryController.testShutdown(); +}); + +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( + { + skip_if: () => AppConstants.MOZ_BUILD_APP != "browser", + }, + async function testRandomizationId() { + // Should generate an id if none is set + await Services.prefs.clearUserPref("app.normandy.user_id"); + Assert.ok( + NormandyTestUtils.isUuid(ClientEnvironmentBase.randomizationId), + "randomizationId should be available" + ); + + // Should read the right preference + await Services.prefs.setStringPref("app.normandy.user_id", "fake id"); + Assert.equal( + ClientEnvironmentBase.randomizationId, + "fake id", + "randomizationId should read from preferences" + ); + } +); diff --git a/toolkit/components/utils/test/unit/test_FilterExpressions.js b/toolkit/components/utils/test/unit/test_FilterExpressions.js new file mode 100644 index 0000000000..c0f99463dc --- /dev/null +++ b/toolkit/components/utils/test/unit/test_FilterExpressions.js @@ -0,0 +1,410 @@ +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + FilterExpressions: + "resource://gre/modules/components-utils/FilterExpressions.sys.mjs", +}); + +// Basic JEXL tests +add_task(async function () { + let val; + // Test that basic expressions work + val = await FilterExpressions.eval("2+2"); + equal(val, 4, "basic expression works"); + + // Test that multiline expressions work + val = await FilterExpressions.eval(` + 2 + + + 2 + `); + equal(val, 4, "multiline expression works"); + + // Test that it reads from the context correctly. + val = await FilterExpressions.eval("first + second + 3", { + first: 1, + second: 2, + }); + equal(val, 6, "context is available to filter expressions"); +}); + +// Date tests +add_task(async function () { + let val; + // Test has a date transform + val = await FilterExpressions.eval('"2016-04-22"|date'); + const d = new Date(Date.UTC(2016, 3, 22)); // months are 0 based + equal(val.toString(), d.toString(), "Date transform works"); + + // Test dates are comparable + const context = { someTime: Date.UTC(2016, 0, 1) }; + val = await FilterExpressions.eval('"2015-01-01"|date < someTime', context); + ok(val, "dates are comparable with less-than"); + val = await FilterExpressions.eval('"2017-01-01"|date > someTime', context); + ok(val, "dates are comparable with greater-than"); +}); + +// Sampling tests +add_task(async function () { + let val; + // Test stable sample returns true for matching samples + val = await FilterExpressions.eval('["test"]|stableSample(1)'); + ok(val, "Stable sample returns true for 100% sample"); + + // Test stable sample returns true for matching samples + val = await FilterExpressions.eval('["test"]|stableSample(0)'); + ok(!val, "Stable sample returns false for 0% sample"); + + // Test stable sample for known samples + val = await FilterExpressions.eval('["test-1"]|stableSample(0.5)'); + ok(val, "Stable sample returns true for a known sample"); + val = await FilterExpressions.eval('["test-4"]|stableSample(0.5)'); + ok(!val, "Stable sample returns false for a known sample"); + + // Test bucket sample for known samples + val = await FilterExpressions.eval('["test-1"]|bucketSample(0, 5, 10)'); + ok(val, "Bucket sample returns true for a known sample"); + val = await FilterExpressions.eval('["test-4"]|bucketSample(0, 5, 10)'); + ok(!val, "Bucket sample returns false for a known sample"); +}); + +// Preference tests +add_task(async function () { + let val; + // Compare the value of the preference + Services.prefs.setIntPref("normandy.test.value", 3); + registerCleanupFunction(() => + Services.prefs.clearUserPref("normandy.test.value") + ); + + val = await FilterExpressions.eval( + '"normandy.test.value"|preferenceValue == 3' + ); + ok(val, "preferenceValue expression compares against preference values"); + val = await FilterExpressions.eval( + '"normandy.test.value"|preferenceValue == "test"' + ); + ok(!val, "preferenceValue expression fails value checks appropriately"); + + // preferenceValue can take a default value as an optional argument, which + // defaults to `undefined`. + val = await FilterExpressions.eval( + '"normandy.test.default"|preferenceValue(false) == false' + ); + ok( + val, + "preferenceValue takes optional 'default value' param for prefs without set values" + ); + val = await FilterExpressions.eval( + '"normandy.test.value"|preferenceValue(5) == 5' + ); + ok( + !val, + "preferenceValue default param is not returned for prefs with set values" + ); + + // Compare if the preference is user set + val = await FilterExpressions.eval( + '"normandy.test.isSet"|preferenceIsUserSet == true' + ); + ok( + !val, + "preferenceIsUserSet expression determines if preference is set at all" + ); + val = await FilterExpressions.eval( + '"normandy.test.value"|preferenceIsUserSet == true' + ); + ok( + val, + "preferenceIsUserSet expression determines if user's preference has been set" + ); + + // Compare if the preference has _any_ value, whether it's user-set or default, + val = await FilterExpressions.eval( + '"normandy.test.nonexistant"|preferenceExists == true' + ); + ok( + !val, + "preferenceExists expression determines if preference exists at all" + ); + val = await FilterExpressions.eval( + '"normandy.test.value"|preferenceExists == true' + ); + ok(val, "preferenceExists expression fails existence check appropriately"); +}); + +// keys tests +add_task(async function testKeys() { + let val; + + // Test an object defined in JEXL + val = await FilterExpressions.eval("{foo: 1, bar: 2}|keys"); + Assert.deepEqual( + new Set(val), + new Set(["foo", "bar"]), + "keys returns the keys from an object in JEXL" + ); + + // Test an object in the context + let context = { ctxObject: { baz: "string", biff: NaN } }; + val = await FilterExpressions.eval("ctxObject|keys", context); + + Assert.deepEqual( + new Set(val), + new Set(["baz", "biff"]), + "keys returns the keys from an object in the context" + ); + + // Test that values from the prototype are not included + context = { ctxObject: Object.create({ fooProto: 7 }) }; + context.ctxObject.baz = 8; + context.ctxObject.biff = 5; + equal( + await FilterExpressions.eval("ctxObject.fooProto", context), + 7, + "Prototype properties are accessible via property access" + ); + val = await FilterExpressions.eval("ctxObject|keys", context); + Assert.deepEqual( + new Set(val), + new Set(["baz", "biff"]), + "keys does not return properties from the object's prototype chain" + ); + + // Return undefined for non-objects + equal( + await FilterExpressions.eval("ctxObject|keys", { ctxObject: 45 }), + undefined, + "keys returns undefined for numbers" + ); + equal( + await FilterExpressions.eval("ctxObject|keys", { ctxObject: null }), + undefined, + "keys returns undefined for null" + ); + + // Object properties are not cached + let pong = 0; + context = { + ctxObject: { + get ping() { + return ++pong; + }, + }, + }; + await FilterExpressions.eval( + "ctxObject.ping == 0 || ctxObject.ping == 1", + context + ); + equal(pong, 2, "Properties are not reifed"); +}); + +add_task(async function testLength() { + equal( + await FilterExpressions.eval("[1, null, {a: 2, b: 3}, Infinity]|length"), + 4, + "length returns the length of the array it's applied to" + ); + + equal( + await FilterExpressions.eval("[]|length"), + 0, + "length is zero for an empty array" + ); + + // Should be undefined for non-Arrays + equal( + await FilterExpressions.eval("5|length"), + undefined, + "length is undefined when applied to numbers" + ); + equal( + await FilterExpressions.eval("null|length"), + undefined, + "length is undefined when applied to null" + ); + equal( + await FilterExpressions.eval("undefined|length"), + undefined, + "length is undefined when applied to undefined" + ); + equal( + await FilterExpressions.eval("{a: 1, b: 2, c: 3}|length"), + undefined, + "length is undefined when applied to non-Array objects" + ); +}); + +add_task(async function testMapToProperty() { + Assert.deepEqual( + await FilterExpressions.eval( + '[{a: 1}, {a: {b: 10}}, {a: [5,6,7,8]}]|mapToProperty("a")' + ), + [1, { b: 10 }, [5, 6, 7, 8]], + "mapToProperty returns an array of values when applied to an array of objects all with the property defined" + ); + + Assert.deepEqual( + await FilterExpressions.eval('[]|mapToProperty("a")'), + [], + "mapToProperty returns an empty array when applied to an empty array" + ); + + Assert.deepEqual( + await FilterExpressions.eval('[{a: 1}, {b: 2}, {a: 3}]|mapToProperty("a")'), + [1, undefined, 3], + "mapToProperty returns an array with undefined entries where the property is undefined" + ); + + // Should be undefined for non-Arrays + equal( + await FilterExpressions.eval('5|mapToProperty("a")'), + undefined, + "mapToProperty returns undefined when applied numbers" + ); + equal( + await FilterExpressions.eval('null|mapToProperty("a")'), + undefined, + "mapToProperty returns undefined when applied null" + ); + equal( + await FilterExpressions.eval('undefined|mapToProperty("a")'), + undefined, + "mapToProperty returns undefined when applied undefined" + ); + equal( + await FilterExpressions.eval('{a: 1, b: 2, c: 3}|mapToProperty("a")'), + undefined, + "mapToProperty returns undefined when applied non-Array objects" + ); +}); + +// intersect tests +add_task(async function testIntersect() { + let val; + + val = await FilterExpressions.eval("[1, 2, 3] intersect [4, 2, 6, 7, 3]"); + Assert.deepEqual( + new Set(val), + new Set([2, 3]), + "intersect finds the common elements between two lists in JEXL" + ); + + const context = { left: [5, 7], right: [4, 5, 3] }; + val = await FilterExpressions.eval("left intersect right", context); + Assert.deepEqual( + new Set(val), + new Set([5]), + "intersect finds the common elements between two lists in the context" + ); + + val = await FilterExpressions.eval( + "['string', 2] intersect [4, 'string', 'other', 3]" + ); + Assert.deepEqual( + new Set(val), + new Set(["string"]), + "intersect can compare strings" + ); + + // Return undefined when intersecting things that aren't lists. + equal( + await FilterExpressions.eval("5 intersect 7"), + undefined, + "intersect returns undefined for numbers" + ); + equal( + await FilterExpressions.eval("val intersect other", { + val: null, + other: null, + }), + undefined, + "intersect returns undefined for null" + ); + equal( + await FilterExpressions.eval("5 intersect [1, 2, 5]"), + undefined, + "intersect returns undefined if only one operand is a list" + ); +}); + +add_task(async function test_regExpMatch() { + let val; + + val = await FilterExpressions.eval('"foobar"|regExpMatch("^foo(.+?)$")'); + Assert.deepEqual( + new Set(val), + new Set(["foobar", "bar"]), + "regExpMatch returns the matches in an array" + ); + + val = await FilterExpressions.eval('"FOObar"|regExpMatch("^foo(.+?)$", "i")'); + Assert.deepEqual( + new Set(val), + new Set(["FOObar", "bar"]), + "regExpMatch accepts flags for matching" + ); + + val = await FilterExpressions.eval('"F00bar"|regExpMatch("^foo(.+?)$", "i")'); + Assert.equal(val, null, "regExpMatch returns null if there are no matches"); +}); + +add_task(async function test_versionCompare() { + let val; + + // 1.0.0 === 1 + val = await FilterExpressions.eval('"1.0.0"|versionCompare("1")'); + ok(val === 0); + + // 1.0.0 < 1.1 + val = await FilterExpressions.eval('"1.0.0"|versionCompare("1.1")'); + ok(val < 0); + + // 1.0.0 > 0.1 + val = await FilterExpressions.eval('"1.0.0"|versionCompare("0.1")'); + ok(val > 0); + + // 111.0.1 < 110 + val = await FilterExpressions.eval(`'111.0.1'|versionCompare('110') < 0`); + ok(val === false); + + // 111.0.1 < 111 + val = await FilterExpressions.eval(`'111.0.1'|versionCompare('111') < 0`); + ok(val === false); + + // 111.0.1 < 111.0.1 + val = await FilterExpressions.eval(`'111.0.1'|versionCompare('111.0.1') < 0`); + ok(val === false); + + // 111.0.1 < 111.0.2 + val = await FilterExpressions.eval(`'111.0.1'|versionCompare('111.0.2') < 0`); + ok(val === true); + + // 111.0.1 is < 112 + val = await FilterExpressions.eval(`'111.0.1'|versionCompare('112') < 0`); + ok(val === true); + + // 113.0a1 < 113 + val = await FilterExpressions.eval(`'113.0a1'|versionCompare('113') < 0`); + ok(val === true); + + // 113.0a1 < 113.0a1 + val = await FilterExpressions.eval(`'113.0a1'|versionCompare('113.0a1') < 0`); + ok(val === false); + + // 113.0a1 > 113.0a0 + val = await FilterExpressions.eval(`'113.0a1'|versionCompare('113.0a0') > 0`); + ok(val === true); + + // 113 > 113.0a0 + val = await FilterExpressions.eval(`'113'|versionCompare('113.0a0') > 0`); + ok(val === true); + + // 114 > 113.0a0 + val = await FilterExpressions.eval(`'114'|versionCompare('113.0a0') > 0`); + ok(val === true); + + // 112 > 113.0a0 + val = await FilterExpressions.eval(`'112'|versionCompare('113.0a0') > 0`); + ok(val === false); +}); diff --git a/toolkit/components/utils/test/unit/test_JsonSchemaValidator.js b/toolkit/components/utils/test/unit/test_JsonSchemaValidator.js new file mode 100644 index 0000000000..46e1f6de8b --- /dev/null +++ b/toolkit/components/utils/test/unit/test_JsonSchemaValidator.js @@ -0,0 +1,1963 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { JsonSchemaValidator } = ChromeUtils.importESModule( + "resource://gre/modules/components-utils/JsonSchemaValidator.sys.mjs" +); + +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(URL.isInstance(result.parsedValue), "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(URL.isInstance(result.parsedValue), "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(URL.isInstance(result.parsedValue), "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(URL.isInstance(result.parsedValue), "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(URL.isInstance(result.parsedValue), "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( + URL.isInstance(result.parsedValue.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); + } +} diff --git a/toolkit/components/utils/test/unit/test_Sampling.js b/toolkit/components/utils/test/unit/test_Sampling.js new file mode 100644 index 0000000000..c698ddf8b1 --- /dev/null +++ b/toolkit/components/utils/test/unit/test_Sampling.js @@ -0,0 +1,127 @@ +"use strict"; + +const { Sampling } = ChromeUtils.importESModule( + "resource://gre/modules/components-utils/Sampling.sys.mjs" +); + +add_task(async function testStableSample() { + // Absolute samples + equal( + await Sampling.stableSample("test", 1), + true, + "stableSample returns true for 100% sample" + ); + equal( + await Sampling.stableSample("test", 0), + false, + "stableSample returns false for 0% sample" + ); + + // Known samples. The numbers are nonces to make the tests pass + equal( + await Sampling.stableSample("test-0", 0.5), + true, + "stableSample returns true for known matching sample" + ); + equal( + await Sampling.stableSample("test-1", 0.5), + false, + "stableSample returns false for known non-matching sample" + ); +}); + +add_task(async function testBucketSample() { + // Absolute samples + equal( + await Sampling.bucketSample("test", 0, 10, 10), + true, + "bucketSample returns true for 100% sample" + ); + equal( + await Sampling.bucketSample("test", 0, 0, 10), + false, + "bucketSample returns false for 0% sample" + ); + + // Known samples. The numbers are nonces to make the tests pass + equal( + await Sampling.bucketSample("test-0", 0, 5, 10), + true, + "bucketSample returns true for known matching sample" + ); + equal( + await Sampling.bucketSample("test-1", 0, 5, 10), + false, + "bucketSample returns false for known non-matching sample" + ); +}); + +add_task(async function testRatioSample() { + // Invalid input + await Assert.rejects( + Sampling.ratioSample("test", []), + /ratios must be at least 1 element long/, + "ratioSample rejects for a list with no ratios" + ); + + // Absolute samples + equal( + await Sampling.ratioSample("test", [1]), + 0, + "ratioSample returns 0 for a list with only 1 ratio" + ); + equal( + await Sampling.ratioSample("test", [0, 0, 1, 0]), + 2, + "ratioSample returns the only non-zero bucket if all other buckets are zero" + ); + + // Known samples. The numbers are nonces to make the tests pass + equal( + await Sampling.ratioSample("test-0", [1, 1]), + 0, + "ratioSample returns the correct index for known matching sample" + ); + equal( + await Sampling.ratioSample("test-1", [1, 1]), + 1, + "ratioSample returns the correct index for known non-matching sample" + ); +}); + +add_task(async function testFractionToKey() { + // Test that results are always 12 character hexadecimal strings. + const expected_regex = /[0-9a-f]{12}/; + const count = 100; + let successes = 0; + for (let i = 0; i < count; i++) { + const p = Sampling.fractionToKey(Math.random()); + if (expected_regex.test(p)) { + successes++; + } + } + equal(successes, count, "fractionToKey makes keys the right length"); +}); + +add_task(async function testTruncatedHash() { + const expected_regex = /[0-9a-f]{12}/; + const count = 100; + let successes = 0; + for (let i = 0; i < count; i++) { + const h = await Sampling.truncatedHash(Math.random()); + if (expected_regex.test(h)) { + successes++; + } + } + equal(successes, count, "truncatedHash makes hashes the right length"); +}); + +add_task(async function testBufferToHex() { + const data = new ArrayBuffer(4); + const view = new DataView(data); + view.setUint8(0, 0xff); + view.setUint8(1, 0x7f); + view.setUint8(2, 0x3f); + view.setUint8(3, 0x1f); + equal(Sampling.bufferToHex(data), "ff7f3f1f"); +}); diff --git a/toolkit/components/utils/test/unit/xpcshell.ini b/toolkit/components/utils/test/unit/xpcshell.ini new file mode 100644 index 0000000000..314fcdd79c --- /dev/null +++ b/toolkit/components/utils/test/unit/xpcshell.ini @@ -0,0 +1,5 @@ +[test_ClientEnvironment.js] +skip-if = os == "android" && release_or_beta # Bug 1707041 +[test_FilterExpressions.js] +[test_JsonSchemaValidator.js] +[test_Sampling.js] |