diff options
Diffstat (limited to 'toolkit/components/messaging-system/targeting/test/unit')
3 files changed, 339 insertions, 0 deletions
diff --git a/toolkit/components/messaging-system/targeting/test/unit/head.js b/toolkit/components/messaging-system/targeting/test/unit/head.js new file mode 100644 index 0000000000..25ed4b243c --- /dev/null +++ b/toolkit/components/messaging-system/targeting/test/unit/head.js @@ -0,0 +1,6 @@ +"use strict"; +// Globals + +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); diff --git a/toolkit/components/messaging-system/targeting/test/unit/test_targeting.js b/toolkit/components/messaging-system/targeting/test/unit/test_targeting.js new file mode 100644 index 0000000000..7d13e33751 --- /dev/null +++ b/toolkit/components/messaging-system/targeting/test/unit/test_targeting.js @@ -0,0 +1,327 @@ +const { ClientEnvironment } = ChromeUtils.importESModule( + "resource://normandy/lib/ClientEnvironment.sys.mjs" +); +const { TargetingContext } = ChromeUtils.importESModule( + "resource://messaging-system/targeting/Targeting.sys.mjs" +); +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); +const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +add_task(async function instance_with_default() { + let targeting = new TargetingContext(); + + let res = await targeting.eval( + `ctx.locale == '${Services.locale.appLocaleAsBCP47}'` + ); + + Assert.ok(res, "Has local context"); +}); + +add_task(async function instance_with_context() { + let targeting = new TargetingContext({ bar: 42 }); + + let res = await targeting.eval("ctx.bar == 42"); + + Assert.ok(res, "Merge provided context with default"); +}); + +add_task(async function eval_1_context() { + let targeting = new TargetingContext(); + + let res = await targeting.eval("custom1.bar == 42", { custom1: { bar: 42 } }); + + Assert.ok(res, "Eval uses provided context"); +}); + +add_task(async function eval_2_context() { + let targeting = new TargetingContext(); + + let res = await targeting.eval("custom1.bar == 42 && custom2.foo == 42", { + custom1: { bar: 42 }, + custom2: { foo: 42 }, + }); + + Assert.ok(res, "Eval uses provided context"); +}); + +add_task(async function eval_multiple_context() { + let targeting = new TargetingContext(); + + let res = await targeting.eval( + "custom1.bar == 42 && custom2.foo == 42 && custom3.baz == 42", + { custom1: { bar: 42 }, custom2: { foo: 42 } }, + { custom3: { baz: 42 } } + ); + + Assert.ok(res, "Eval uses provided context"); +}); + +add_task(async function eval_multiple_context_precedence() { + let targeting = new TargetingContext(); + + let res = await targeting.eval( + "custom1.bar == 42 && custom2.foo == 42", + { custom1: { bar: 24 }, custom2: { foo: 24 } }, + { custom1: { bar: 42 }, custom2: { foo: 42 } } + ); + + Assert.ok(res, "Last provided context overrides previously defined ones."); +}); + +add_task(async function eval_evalWithDefault() { + let targeting = new TargetingContext({ foo: 42 }); + + let res = await targeting.evalWithDefault("foo == 42"); + + Assert.ok(res, "Eval uses provided context"); +}); + +add_task(async function log_targeting_error_events() { + let ctx = { + get foo() { + throw new Error("unit test"); + }, + }; + let targeting = new TargetingContext(ctx); + let stub = sinon.stub(targeting, "_sendUndesiredEvent"); + + await Assert.rejects( + targeting.evalWithDefault("foo == 42", ctx), + /unit test/, + "Getter should throw" + ); + + Assert.equal(stub.callCount, 1, "Error event was logged"); + let { + args: [{ event, value }], + } = stub.firstCall; + Assert.equal(event, "attribute_error", "Correct error message"); + Assert.equal(value, "foo", "Correct attribute name"); +}); + +add_task(async function eval_evalWithDefault_precedence() { + let targeting = new TargetingContext({ region: "space" }); + let res = await targeting.evalWithDefault("region != 'space'"); + + Assert.ok(res, "Custom context does not override TargetingEnvironment"); +}); + +add_task(async function eval_evalWithDefault_combineContexts() { + let combinedCtxs = TargetingContext.combineContexts({ foo: 1 }, { foo: 2 }); + let targeting = new TargetingContext(combinedCtxs); + let res = await targeting.evalWithDefault("foo == 1"); + + Assert.ok(res, "First match is returned for combineContexts"); +}); + +add_task(async function log_targeting_error_events_in_namespace() { + let ctx = { + get foo() { + throw new Error("unit test"); + }, + }; + let targeting = new TargetingContext(ctx); + let stub = sinon.stub(targeting, "_sendUndesiredEvent"); + let catchStub = sinon.stub(); + + try { + await targeting.eval("ctx.foo == 42"); + } catch (e) { + catchStub(); + } + + Assert.equal(stub.callCount, 1, "Error event was logged"); + let { + args: [{ event, value }], + } = stub.firstCall; + Assert.equal(event, "attribute_error", "Correct error message"); + Assert.equal(value, "ctx.foo", "Correct attribute name"); + Assert.ok(catchStub.calledOnce, "eval throws errors"); +}); + +add_task(async function log_timeout_errors() { + let ctx = { + timeout: 1, + get foo() { + return new Promise(() => {}); + }, + }; + + let targeting = new TargetingContext(ctx); + let stub = sinon.stub(targeting, "_sendUndesiredEvent"); + let catchStub = sinon.stub(); + + try { + await targeting.eval("ctx.foo"); + } catch (e) { + catchStub(); + } + + Assert.equal(catchStub.callCount, 1, "Timeout error throws"); + Assert.equal(stub.callCount, 1, "Timeout event was logged"); + let { + args: [{ event, value }], + } = stub.firstCall; + Assert.equal(event, "attribute_timeout", "Correct error message"); + Assert.equal(value, "ctx.foo", "Correct attribute name"); +}); + +add_task(async function test_telemetry_event_timeout() { + Services.telemetry.clearEvents(); + let ctx = { + timeout: 1, + get foo() { + return new Promise(() => {}); + }, + }; + let expectedEvents = [ + ["messaging_experiments", "targeting", "attribute_timeout", "ctx.foo"], + ]; + let targeting = new TargetingContext(ctx); + + try { + await targeting.eval("ctx.foo"); + } catch (e) {} + + TelemetryTestUtils.assertEvents(expectedEvents); + Services.telemetry.clearEvents(); +}); + +add_task(async function test_telemetry_event_error() { + Services.telemetry.clearEvents(); + let ctx = { + get bar() { + throw new Error("unit test"); + }, + }; + let expectedEvents = [ + ["messaging_experiments", "targeting", "attribute_error", "ctx.bar"], + ]; + let targeting = new TargetingContext(ctx); + + try { + await targeting.eval("ctx.bar"); + } catch (e) {} + + TelemetryTestUtils.assertEvents(expectedEvents); + Services.telemetry.clearEvents(); +}); + +// Make sure that when using the Normandy-style ClientEnvironment context, +// `liveTelemetry` works. `liveTelemetry` is a particularly tricky object to +// proxy, so it's useful to check specifically. +add_task(async function test_live_telemetry() { + let ctx = { env: ClientEnvironment }; + let targeting = new TargetingContext(); + // This shouldn't throw. + await targeting.eval("env.liveTelemetry.main", ctx); +}); + +add_task(async function test_default_targeting() { + const targeting = new TargetingContext(); + const expected_attributes = [ + "locale", + "localeLanguageCode", + // "region", // Not available in test, requires network access to determine + "userId", + "version", + "channel", + "platform", + ]; + + for (let attribute of expected_attributes) { + let res = await targeting.eval(`ctx.${attribute}`); + Assert.ok(res, `[eval] result for ${attribute} should not be null`); + } + + for (let attribute of expected_attributes) { + let res = await targeting.evalWithDefault(attribute); + Assert.ok( + res, + `[evalWithDefault] result for ${attribute} should not be null` + ); + } +}); + +add_task(async function test_targeting_os() { + const targeting = new TargetingContext(); + await TestUtils.waitForCondition(() => + targeting.eval("ctx.os.isWindows || ctx.os.isMac || ctx.os.isLinux") + ); + let res = await targeting.eval( + `(ctx.os.isWindows && ctx.os.windowsVersion && ctx.os.windowsBuildNumber) || + (ctx.os.isMac && ctx.os.macVersion && ctx.os.darwinVersion) || + (ctx.os.isLinux && os.darwinVersion == null) + ` + ); + Assert.ok(res, `Should detect platform version got: ${res}`); +}); + +add_task(async function test_targeting_source_constructor() { + Services.telemetry.clearEvents(); + const targeting = new TargetingContext( + { + foo: true, + get bar() { + throw new Error("bar"); + }, + }, + { source: "unit_testing" } + ); + + let res = await targeting.eval("ctx.foo"); + Assert.ok(res, "Should eval to true"); + + let expectedEvents = [ + [ + "messaging_experiments", + "targeting", + "attribute_error", + "ctx.bar", + { source: "unit_testing" }, + ], + ]; + try { + await targeting.eval("ctx.bar"); + } catch (e) {} + + TelemetryTestUtils.assertEvents(expectedEvents); + Services.telemetry.clearEvents(); +}); + +add_task(async function test_targeting_source_override() { + Services.telemetry.clearEvents(); + const targeting = new TargetingContext( + { + foo: true, + get bar() { + throw new Error("bar"); + }, + }, + { source: "unit_testing" } + ); + + let res = await targeting.eval("ctx.foo"); + Assert.ok(res, "Should eval to true"); + + let expectedEvents = [ + [ + "messaging_experiments", + "targeting", + "attribute_error", + "bar", + { source: "override" }, + ], + ]; + try { + targeting.setTelemetrySource("override"); + await targeting.evalWithDefault("bar"); + } catch (e) {} + + TelemetryTestUtils.assertEvents(expectedEvents); + Services.telemetry.clearEvents(); +}); diff --git a/toolkit/components/messaging-system/targeting/test/unit/xpcshell.toml b/toolkit/components/messaging-system/targeting/test/unit/xpcshell.toml new file mode 100644 index 0000000000..023bab422b --- /dev/null +++ b/toolkit/components/messaging-system/targeting/test/unit/xpcshell.toml @@ -0,0 +1,6 @@ +[DEFAULT] +head = "head.js" +tags = "messaging-system" +firefox-appdir = "browser" + +["test_targeting.js"] |