544 lines
15 KiB
JavaScript
544 lines
15 KiB
JavaScript
/* Any copyright is dedicated to the Public Domain.
|
|
* http://creativecommons.org/publicdomain/zero/1.0/
|
|
*/
|
|
|
|
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",
|
|
"http://test.addons.org/api/abuse/report/addon/"
|
|
);
|
|
|
|
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",
|
|
sourceURL: "http://host.triggered.install/example?test=1",
|
|
},
|
|
expect: {
|
|
method: "installtrigger",
|
|
source: "test_host",
|
|
sourceURL: "http://host.triggered.install/example?test=1",
|
|
},
|
|
},
|
|
{
|
|
test: {
|
|
method: "link",
|
|
source: "unknown",
|
|
sourceURL: "https://another.host/installExtension?name=ext1",
|
|
},
|
|
expect: {
|
|
method: "link",
|
|
source: "unknown",
|
|
sourceURL: "https://another.host/installExtension?name=ext1",
|
|
},
|
|
},
|
|
];
|
|
|
|
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:
|
|
// https://mozilla.github.io/addons-server/topics/api/abuse.html#submitting-an-add-on-abuse-report
|
|
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:
|
|
// https://mozilla.github.io/addons-server/topics/api/abuse.html#submitting-an-add-on-abuse-report
|
|
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:
|
|
// https://mozilla.github.io/addons-server/topics/api/abuse.html#submitting-an-add-on-abuse-report
|
|
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",
|
|
"invalid-protocol://abuse-report"
|
|
);
|
|
await testErrorCode({
|
|
expectedErrorType: "ERROR_NETWORK",
|
|
expectRequest: false,
|
|
});
|
|
|
|
await extension.unload();
|
|
});
|