Source code
Revision control
Copy as Markdown
Other Tools
Test Info:
/* Any copyright is dedicated to the Public Domain.
*/
const { AbuseReporter, AbuseReportError } = ChromeUtils.importESModule(
"resource://gre/modules/AbuseReporter.sys.mjs"
);
const { ClientID } = ChromeUtils.importESModule(
"resource://gre/modules/ClientID.sys.mjs"
);
const APPVERSION = "1";
const ADDON_ID = "test-addon@tests.mozilla.org";
const FAKE_INSTALL_INFO = {
source: "fake-Install:Source",
method: "fake:install method",
};
const EXPECTED_API_RESPONSE = {
id: ADDON_ID,
some: "other-props",
};
async function installTestExtension(overrideOptions = {}) {
const extOptions = {
manifest: {
browser_specific_settings: { gecko: { id: ADDON_ID } },
name: "Test Extension",
},
useAddonManager: "permanent",
amInstallTelemetryInfo: FAKE_INSTALL_INFO,
...overrideOptions,
};
const extension = ExtensionTestUtils.loadExtension(extOptions);
await extension.startup();
const addon = await AddonManager.getAddonByID(ADDON_ID);
return { extension, addon };
}
async function assertBaseReportData({ reportData, addon }) {
// Report properties related to addon metadata.
equal(reportData.addon, ADDON_ID, "Got expected 'addon'");
equal(
reportData.addon_version,
addon.version,
"Got expected 'addon_version'"
);
equal(
reportData.install_date,
addon.installDate.toISOString(),
"Got expected 'install_date' in ISO format"
);
equal(
reportData.addon_install_origin,
addon.sourceURI.spec,
"Got expected 'addon_install_origin'"
);
equal(
reportData.addon_install_source,
"fake_install_source",
"Got expected 'addon_install_source'"
);
equal(
reportData.addon_install_method,
"fake_install_method",
"Got expected 'addon_install_method'"
);
equal(
reportData.addon_signature,
"privileged",
"Got expected 'addon_signature'"
);
// Report properties related to the environment.
equal(
reportData.client_id,
await ClientID.getClientIdHash(),
"Got the expected 'client_id'"
);
equal(
reportData.app,
AppConstants.platform === "android" ? "android" : "firefox",
"Got expected 'app'"
);
equal(reportData.appversion, APPVERSION, "Got expected 'appversion'");
equal(
reportData.lang,
Services.locale.appLocaleAsBCP47,
"Got expected 'lang'"
);
equal(
reportData.operating_system,
AppConstants.platform,
"Got expected 'operating_system'"
);
equal(
reportData.operating_system_version,
Services.sysinfo.getProperty("version"),
"Got expected 'operating_system_version'"
);
}
async function assertRejectsAbuseReportError(promise, errorType, errorInfo) {
let error;
await Assert.rejects(
promise,
err => {
error = err;
return err instanceof AbuseReportError;
},
`Got an AbuseReportError`
);
equal(error.errorType, errorType, "Got the expected errorType");
equal(error.errorInfo, errorInfo, "Got the expected errorInfo");
ok(
error.message.includes(errorType),
"errorType should be included in the error message"
);
if (errorInfo) {
ok(
error.message.includes(errorInfo),
"errorInfo should be included in the error message"
);
}
}
function handleSubmitRequest({ request, response }) {
response.setStatusLine(request.httpVersion, 200, "OK");
response.setHeader("Content-Type", "application/json", false);
response.write(JSON.stringify(EXPECTED_API_RESPONSE));
}
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "49");
const server = createHttpServer({ hosts: ["test.addons.org"] });
// Mock abuse report API endpoint.
let apiRequestHandler;
server.registerPathHandler("/api/abuse/report/addon/", (request, response) => {
const stream = request.bodyInputStream;
const buffer = NetUtil.readInputStream(stream, stream.available());
const data = new TextDecoder().decode(buffer);
apiRequestHandler({ data, request, response });
});
add_setup(async () => {
Services.prefs.setCharPref(
"extensions.addonAbuseReport.url",
);
await promiseStartupManager();
});
add_task(async function test_addon_report_data() {
info("Verify report property for a privileged extension");
const { addon, extension } = await installTestExtension();
const data = await AbuseReporter.getReportData(addon);
await assertBaseReportData({ reportData: data, addon });
await extension.unload();
info("Verify 'addon_signature' report property for non privileged extension");
AddonTestUtils.usePrivilegedSignatures = false;
const { addon: addon2, extension: extension2 } = await installTestExtension();
const data2 = await AbuseReporter.getReportData(addon2);
equal(
data2.addon_signature,
"signed",
"Got expected 'addon_signature' for non privileged extension"
);
await extension2.unload();
info("Verify 'addon_install_method' report property on temporary install");
const { addon: addon3, extension: extension3 } = await installTestExtension({
useAddonManager: "temporary",
});
const data3 = await AbuseReporter.getReportData(addon3);
equal(
data3.addon_install_source,
"temporary_addon",
"Got expected 'addon_install_method' on temporary install"
);
await extension3.unload();
});
add_task(
{
skip_if: () => AppConstants.platform === "android",
},
async function test_dictionary_report_data() {
info("Verify 'addon_signature' report property for a dictionary");
const dict = await promiseInstallWebExtension({
manifest: {
name: "Dictionary",
browser_specific_settings: { gecko: { id: "dictionary@mozilla.com" } },
dictionaries: {
und: "und.dic",
},
},
files: {
"und.dic": "",
"und.aff": "",
},
});
const dataDict = await AbuseReporter.getReportData(dict);
equal(
dataDict.addon_signature,
"not_required",
"Got expected 'addon_signature' for dictionary"
);
await dict.uninstall();
}
);
// This tests verifies how the addon installTelemetryInfo values are being
// normalized into the addon_install_source and addon_install_method
// expected by the API endpoint.
add_task(async function test_normalized_addon_install_source_and_method() {
async function assertAddonInstallMethod(amInstallTelemetryInfo, expected) {
const { addon, extension } = await installTestExtension({
amInstallTelemetryInfo,
});
const {
addon_install_method,
addon_install_source,
addon_install_source_url,
} = await AbuseReporter.getReportData(addon);
Assert.deepEqual(
{
addon_install_method,
addon_install_source,
addon_install_source_url,
},
{
addon_install_method: expected.method,
addon_install_source: expected.source,
addon_install_source_url: expected.sourceURL,
},
`Got the expected report data for ${JSON.stringify(
amInstallTelemetryInfo
)}`
);
await extension.unload();
}
// Array of testcases: the `test` property contains the installTelemetryInfo value
// and the `expect` contains the expected normalized values.
const TEST_CASES = [
// Explicitly verify normalized values on missing telemetry info.
{
test: null,
expect: { source: null, method: null },
},
// Verify expected normalized values for some common install telemetry info.
{
test: { source: "about:addons", method: "drag-and-drop" },
expect: { source: "about_addons", method: "drag_and_drop" },
},
{
test: { source: "amo", method: "amWebAPI" },
expect: { source: "amo", method: "amwebapi" },
},
{
test: { source: "app-profile", method: "sideload" },
expect: { source: "app_profile", method: "sideload" },
},
{
test: { source: "distribution" },
expect: { source: "distribution", method: null },
},
{
test: {
method: "installTrigger",
source: "test-host",
},
expect: {
method: "installtrigger",
source: "test_host",
},
},
{
test: {
method: "link",
source: "unknown",
},
expect: {
method: "link",
source: "unknown",
},
},
];
for (const { expect, test } of TEST_CASES) {
await assertAddonInstallMethod(test, expect);
}
});
add_task(async function test_sendAbuseReport() {
const { addon, extension } = await installTestExtension();
// Data passed by the caller.
const formData = { "some-data-from-the-caller": true };
// Metadata stored by Gecko, only passed when the add-on is installed, which
// is what this test case verifies.
//
// NOTE: We JSON stringify + parse to get rid of the undefined values, which
// we do not send to the server.
const metadata = JSON.parse(
JSON.stringify(await AbuseReporter.getReportData(addon))
);
// Register a request handler to (1) access the data submitted and (2) return
// a 200 response.
let dataSubmitted;
apiRequestHandler = ({ data, request, response }) => {
Assert.equal(
request.getHeader("content-type"),
"application/json",
"expected content-type header"
);
Assert.ok(
!request.hasHeader("authorization"),
"expected no authorization header"
);
dataSubmitted = JSON.parse(data);
handleSubmitRequest({ request, response });
};
const response = await AbuseReporter.sendAbuseReport(ADDON_ID, formData);
Assert.deepEqual(
response,
EXPECTED_API_RESPONSE,
"expected successful response"
);
Assert.deepEqual(
dataSubmitted,
{
...formData,
...metadata,
// The add-on ID is unconditionally passed as `addon` on purpose. See:
addon: ADDON_ID,
},
"expected the right data to be sent to the server"
);
await extension.unload();
});
add_task(async function test_sendAbuseReport_addon_not_installed() {
const formData = { "some-data-from-the-caller": true };
// Register a request handler to (1) access the data submitted and (2) return
// a 200 response.
let dataSubmitted;
apiRequestHandler = ({ data, request, response }) => {
Assert.equal(
request.getHeader("content-type"),
"application/json",
"expected content-type header"
);
Assert.ok(
!request.hasHeader("authorization"),
"expected no authorization header"
);
dataSubmitted = JSON.parse(data);
handleSubmitRequest({ request, response });
};
const response = await AbuseReporter.sendAbuseReport(ADDON_ID, formData);
Assert.deepEqual(
response,
EXPECTED_API_RESPONSE,
"expected successful response"
);
Assert.deepEqual(
dataSubmitted,
{
...formData,
// The add-on ID is unconditionally passed as `addon` on purpose. See:
addon: ADDON_ID,
},
"expected the right data to be sent to the server"
);
});
add_task(async function test_sendAbuseReport_with_authorization() {
const { addon, extension } = await installTestExtension();
// Data passed by the caller.
const formData = { "some-data-from-the-caller": true };
// Metadata stored by Gecko, only passed when the add-on is installed, which
// is what this test case verifies.
//
// NOTE: We JSON stringify + parse to get rid of the undefined values, which
// we do not send to the server.
const metadata = JSON.parse(
JSON.stringify(await AbuseReporter.getReportData(addon))
);
const authorization = "some authorization header";
// Register a request handler to (1) access the data submitted and (2) return
// a 200 response.
let dataSubmitted;
apiRequestHandler = ({ data, request, response }) => {
Assert.equal(
request.getHeader("content-type"),
"application/json",
"expected content-type header"
);
Assert.equal(
request.getHeader("authorization"),
authorization,
"expected authorization header"
);
dataSubmitted = JSON.parse(data);
handleSubmitRequest({ request, response });
};
const response = await AbuseReporter.sendAbuseReport(ADDON_ID, formData, {
authorization,
});
Assert.deepEqual(
response,
EXPECTED_API_RESPONSE,
"expected successful response"
);
Assert.deepEqual(
dataSubmitted,
{
...formData,
...metadata,
// The add-on ID is unconditionally passed as `addon` on purpose. See:
addon: ADDON_ID,
},
"expected the right data to be sent to the server"
);
await extension.unload();
});
add_task(async function test_sendAbuseReport_errors() {
const { extension } = await installTestExtension();
async function testErrorCode({
responseStatus,
responseText = "",
expectedErrorType,
expectedErrorInfo,
expectRequest = true,
}) {
info(
`Test expected AbuseReportError on response status "${responseStatus}"`
);
let requestReceived = false;
apiRequestHandler = ({ request, response }) => {
requestReceived = true;
response.setStatusLine(request.httpVersion, responseStatus, "Error");
response.write(responseText);
};
const promise = AbuseReporter.sendAbuseReport(ADDON_ID, {});
if (typeof expectedErrorType === "string") {
// Assert a specific AbuseReportError errorType.
await assertRejectsAbuseReportError(
promise,
expectedErrorType,
expectedErrorInfo
);
} else {
// Assert on a given Error class.
await Assert.rejects(
promise,
expectedErrorType,
"expected correct Error class"
);
}
equal(
requestReceived,
expectRequest,
`${expectRequest ? "" : "Not "}received a request as expected`
);
}
await testErrorCode({
responseStatus: 500,
responseText: "A server error",
expectedErrorType: "ERROR_SERVER",
expectedErrorInfo: JSON.stringify({
status: 500,
responseText: "A server error",
}),
});
await testErrorCode({
responseStatus: 404,
responseText: "Not found error",
expectedErrorType: "ERROR_CLIENT",
expectedErrorInfo: JSON.stringify({
status: 404,
responseText: "Not found error",
}),
});
// Test response with unexpected status code.
await testErrorCode({
responseStatus: 604,
responseText: "An unexpected status code",
expectedErrorType: "ERROR_UNKNOWN",
expectedErrorInfo: JSON.stringify({
status: 604,
responseText: "An unexpected status code",
}),
});
// Test response status 200 with invalid json data.
await testErrorCode({
responseStatus: 200,
expectedErrorType: /SyntaxError: JSON.parse/,
});
// Test on invalid url.
Services.prefs.setCharPref(
"extensions.addonAbuseReport.url",
);
await testErrorCode({
expectedErrorType: "ERROR_NETWORK",
expectRequest: false,
});
await extension.unload();
});