diff options
Diffstat (limited to 'browser/components/attribution/test/browser')
4 files changed, 381 insertions, 0 deletions
diff --git a/browser/components/attribution/test/browser/browser.ini b/browser/components/attribution/test/browser/browser.ini new file mode 100644 index 0000000000..55050ac799 --- /dev/null +++ b/browser/components/attribution/test/browser/browser.ini @@ -0,0 +1,8 @@ +[DEFAULT] +support-files = + head.js + +[browser_AttributionCode_telemetry.js] +skip-if = (os != "win" && toolkit != "cocoa") # Windows and macOS only telemetry. +[browser_AttributionCode_Mac_telemetry.js] +skip-if = toolkit != "cocoa" # macOS only telemetry. diff --git a/browser/components/attribution/test/browser/browser_AttributionCode_Mac_telemetry.js b/browser/components/attribution/test/browser/browser_AttributionCode_Mac_telemetry.js new file mode 100644 index 0000000000..126d0d880d --- /dev/null +++ b/browser/components/attribution/test/browser/browser_AttributionCode_Mac_telemetry.js @@ -0,0 +1,249 @@ +ChromeUtils.defineESModuleGetters(this, { + TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs", +}); +const { MacAttribution } = ChromeUtils.import( + "resource:///modules/MacAttribution.jsm" +); +const { AttributionIOUtils } = ChromeUtils.import( + "resource:///modules/AttributionCode.jsm" +); +const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm"); + +async function assertCacheExistsAndIsEmpty() { + // We should have written to the cache, and be able to read back + // with no errors. + const histogram = Services.telemetry.getHistogramById( + "BROWSER_ATTRIBUTION_ERRORS" + ); + histogram.clear(); + + ok(await AttributionIOUtils.exists(AttributionCode.attributionFile.path)); + Assert.deepEqual( + "", + new TextDecoder().decode( + await AttributionIOUtils.read(AttributionCode.attributionFile.path) + ) + ); + + AttributionCode._clearCache(); + let result = await AttributionCode.getAttrDataAsync(); + Assert.deepEqual(result, {}, "Should be able to get cached result"); + + Assert.deepEqual({}, histogram.snapshot().values || {}); +} + +add_task(async function test_write_error() { + const sandbox = sinon.createSandbox(); + let attributionSvc = Cc["@mozilla.org/mac-attribution;1"].getService( + Ci.nsIMacAttributionService + ); + attributionSvc.setReferrerUrl( + MacAttribution.applicationPath, + "https://example.com?content=content", + true + ); + + await AttributionCode.deleteFileAsync(); + AttributionCode._clearCache(); + + const histogram = Services.telemetry.getHistogramById( + "BROWSER_ATTRIBUTION_ERRORS" + ); + + let oldExists = AttributionIOUtils.exists; + let oldWrite = AttributionIOUtils.write; + try { + // Clear any existing telemetry + histogram.clear(); + + // Force the file to not exist and then cause a write error. This is delicate + // because various background tasks may invoke `IOUtils.writeAtomic` while + // this test is running. Be careful to only stub the one call. + AttributionIOUtils.exists = () => false; + AttributionIOUtils.write = () => { + throw new Error("write_error"); + }; + + // Try to read the attribution code. + let result = await AttributionCode.getAttrDataAsync(); + Assert.deepEqual( + result, + { content: "content" }, + "Should be able to get a result even if the file doesn't write" + ); + + TelemetryTestUtils.assertHistogram(histogram, INDEX_WRITE_ERROR, 1); + } finally { + AttributionIOUtils.exists = oldExists; + AttributionIOUtils.write = oldWrite; + await AttributionCode.deleteFileAsync(); + AttributionCode._clearCache(); + histogram.clear(); + sandbox.restore(); + } +}); + +add_task(async function test_unusual_referrer() { + // This referrer URL looks malformed, but the malformed bits are dropped, so + // it's actually ok. This is what allows extraneous bits like `fbclid` tags + // to be ignored. + let attributionSvc = Cc["@mozilla.org/mac-attribution;1"].getService( + Ci.nsIMacAttributionService + ); + attributionSvc.setReferrerUrl( + MacAttribution.applicationPath, + "https://example.com?content=&=campaign", + true + ); + + await AttributionCode.deleteFileAsync(); + AttributionCode._clearCache(); + + const histogram = Services.telemetry.getHistogramById( + "BROWSER_ATTRIBUTION_ERRORS" + ); + try { + // Clear any existing telemetry + histogram.clear(); + + // Try to read the attribution code + await AttributionCode.getAttrDataAsync(); + + let result = await AttributionCode.getAttrDataAsync(); + Assert.deepEqual(result, {}, "Should be able to get empty result"); + + Assert.deepEqual({}, histogram.snapshot().values || {}); + + await assertCacheExistsAndIsEmpty(); + } finally { + await AttributionCode.deleteFileAsync(); + AttributionCode._clearCache(); + histogram.clear(); + } +}); + +add_task(async function test_blank_referrer() { + let attributionSvc = Cc["@mozilla.org/mac-attribution;1"].getService( + Ci.nsIMacAttributionService + ); + attributionSvc.setReferrerUrl(MacAttribution.applicationPath, "", true); + + await AttributionCode.deleteFileAsync(); + AttributionCode._clearCache(); + + const histogram = Services.telemetry.getHistogramById( + "BROWSER_ATTRIBUTION_ERRORS" + ); + + try { + // Clear any existing telemetry + histogram.clear(); + + // Try to read the attribution code + let result = await AttributionCode.getAttrDataAsync(); + Assert.deepEqual(result, {}, "Should be able to get empty result"); + + Assert.deepEqual({}, histogram.snapshot().values || {}); + + await assertCacheExistsAndIsEmpty(); + } finally { + await AttributionCode.deleteFileAsync(); + AttributionCode._clearCache(); + histogram.clear(); + } +}); + +add_task(async function test_no_referrer() { + const sandbox = sinon.createSandbox(); + let newApplicationPath = MacAttribution.applicationPath + ".test"; + sandbox.stub(MacAttribution, "applicationPath").get(() => newApplicationPath); + + await AttributionCode.deleteFileAsync(); + AttributionCode._clearCache(); + + const histogram = Services.telemetry.getHistogramById( + "BROWSER_ATTRIBUTION_ERRORS" + ); + try { + // Clear any existing telemetry + histogram.clear(); + + // Try to read the attribution code + await AttributionCode.getAttrDataAsync(); + + let result = await AttributionCode.getAttrDataAsync(); + Assert.deepEqual(result, {}, "Should be able to get empty result"); + + Assert.deepEqual({}, histogram.snapshot().values || {}); + + await assertCacheExistsAndIsEmpty(); + } finally { + await AttributionCode.deleteFileAsync(); + AttributionCode._clearCache(); + histogram.clear(); + sandbox.restore(); + } +}); + +add_task(async function test_broken_referrer() { + let attributionSvc = Cc["@mozilla.org/mac-attribution;1"].getService( + Ci.nsIMacAttributionService + ); + attributionSvc.setReferrerUrl( + MacAttribution.applicationPath, + "https://example.com?content=content", + true + ); + + // This uses macOS internals to change the GUID so that it will look like the + // application has quarantine data but nothing will be pressent in the + // quarantine database. This shouldn't happen in the wild. + function generateQuarantineGUID() { + let str = Services.uuid + .generateUUID() + .toString() + .toUpperCase(); + // Strip {}. + return str.substring(1, str.length - 1); + } + + // These magic constants are macOS GateKeeper flags. + let string = [ + "01c1", + "5991b778", + "Safari.app", + generateQuarantineGUID(), + ].join(";"); + let bytes = new TextEncoder().encode(string); + await IOUtils.setMacXAttr( + MacAttribution.applicationPath, + "com.apple.quarantine", + bytes + ); + + await AttributionCode.deleteFileAsync(); + AttributionCode._clearCache(); + + const histogram = Services.telemetry.getHistogramById( + "BROWSER_ATTRIBUTION_ERRORS" + ); + try { + // Clear any existing telemetry + histogram.clear(); + + // Try to read the attribution code + await AttributionCode.getAttrDataAsync(); + + let result = await AttributionCode.getAttrDataAsync(); + Assert.deepEqual(result, {}, "Should be able to get empty result"); + + TelemetryTestUtils.assertHistogram(histogram, INDEX_QUARANTINE_ERROR, 1); + histogram.clear(); + + await assertCacheExistsAndIsEmpty(); + } finally { + await AttributionCode.deleteFileAsync(); + AttributionCode._clearCache(); + histogram.clear(); + } +}); diff --git a/browser/components/attribution/test/browser/browser_AttributionCode_telemetry.js b/browser/components/attribution/test/browser/browser_AttributionCode_telemetry.js new file mode 100644 index 0000000000..f51c7fbd15 --- /dev/null +++ b/browser/components/attribution/test/browser/browser_AttributionCode_telemetry.js @@ -0,0 +1,101 @@ +ChromeUtils.defineESModuleGetters(this, { + TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs", +}); +const { AttributionIOUtils } = ChromeUtils.import( + "resource:///modules/AttributionCode.jsm" +); + +add_task(async function test_parse_error() { + if (AppConstants.platform == "macosx") { + // On macOS, the underlying data is the OS-level quarantine + // database. We need to start from nothing to isolate the cache. + const { MacAttribution } = ChromeUtils.import( + "resource:///modules/MacAttribution.jsm" + ); + let attributionSvc = Cc["@mozilla.org/mac-attribution;1"].getService( + Ci.nsIMacAttributionService + ); + attributionSvc.setReferrerUrl(MacAttribution.applicationPath, "", true); + } + + registerCleanupFunction(async () => { + await AttributionCode.deleteFileAsync(); + AttributionCode._clearCache(); + }); + const histogram = Services.telemetry.getHistogramById( + "BROWSER_ATTRIBUTION_ERRORS" + ); + // Delete the file to trigger a read error + await AttributionCode.deleteFileAsync(); + AttributionCode._clearCache(); + // Clear any existing telemetry + histogram.clear(); + let result = await AttributionCode.getAttrDataAsync(); + Assert.deepEqual( + result, + {}, + "Shouldn't be able to get a result if the file doesn't exist" + ); + + // Write an invalid file to trigger a decode error + // Skip this for MSIX packages though - we can't write or delete + // the attribution file there, everything happens in memory instead. + if ( + AppConstants.platform === "win" && + !Services.sysinfo.getProperty("hasWinPackageId") + ) { + await AttributionCode.deleteFileAsync(); + AttributionCode._clearCache(); + // Empty string is valid on macOS. + await AttributionCode.writeAttributionFile( + AppConstants.platform == "macosx" ? "invalid" : "" + ); + result = await AttributionCode.getAttrDataAsync(); + Assert.deepEqual(result, {}, "Should have failed to parse"); + + // `assertHistogram` also ensures that `read_error` index 0 is 0 + // as we should not have recorded telemetry from the previous `getAttrDataAsync` call + TelemetryTestUtils.assertHistogram(histogram, INDEX_DECODE_ERROR, 1); + // Reset + histogram.clear(); + } +}); + +add_task(async function test_read_error() { + registerCleanupFunction(async () => { + await AttributionCode.deleteFileAsync(); + AttributionCode._clearCache(); + }); + const histogram = Services.telemetry.getHistogramById( + "BROWSER_ATTRIBUTION_ERRORS" + ); + + // Delete the file to trigger a read error + await AttributionCode.deleteFileAsync(); + AttributionCode._clearCache(); + // Clear any existing telemetry + histogram.clear(); + + // Force the file to exist but then cause a read error + let oldExists = AttributionIOUtils.exists; + AttributionIOUtils.exists = () => true; + + let oldRead = AttributionIOUtils.read; + AttributionIOUtils.read = () => { + throw new Error("read_error"); + }; + + registerCleanupFunction(() => { + AttributionIOUtils.exists = oldExists; + AttributionIOUtils.read = oldRead; + }); + + // Try to read the file + await AttributionCode.getAttrDataAsync(); + + // It should record the read error + TelemetryTestUtils.assertHistogram(histogram, INDEX_READ_ERROR, 1); + + // Clear any existing telemetry + histogram.clear(); +}); diff --git a/browser/components/attribution/test/browser/head.js b/browser/components/attribution/test/browser/head.js new file mode 100644 index 0000000000..78e233e9f8 --- /dev/null +++ b/browser/components/attribution/test/browser/head.js @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; + +const { AttributionCode } = ChromeUtils.import( + "resource:///modules/AttributionCode.jsm" +); + +// Keep in sync with `BROWSER_ATTRIBUTION_ERRORS` in Histograms.json. +const INDEX_READ_ERROR = 0; +const INDEX_DECODE_ERROR = 1; +const INDEX_WRITE_ERROR = 2; +const INDEX_QUARANTINE_ERROR = 3; + +add_setup(function() { + // AttributionCode._clearCache is only possible in a testing environment + Services.env.set("XPCSHELL_TEST_PROFILE_DIR", "testing"); + + registerCleanupFunction(() => { + Services.env.set("XPCSHELL_TEST_PROFILE_DIR", null); + }); +}); |