summaryrefslogtreecommitdiffstats
path: root/toolkit/components/utils/test
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/utils/test')
-rw-r--r--toolkit/components/utils/test/unit/test_ClientEnvironment.js147
-rw-r--r--toolkit/components/utils/test/unit/test_FilterExpressions.js410
-rw-r--r--toolkit/components/utils/test/unit/test_JsonSchemaValidator.js1963
-rw-r--r--toolkit/components/utils/test/unit/test_Sampling.js127
-rw-r--r--toolkit/components/utils/test/unit/xpcshell.ini5
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]