forked from mirrors/gecko-dev
Bug 1898446 - Introduce a new sendAbuseReport() method on the AddonManager web API (mozAddonManager). r=rpl,smaug
Differential Revision: https://phabricator.services.mozilla.com/D209017
This commit is contained in:
parent
09a14c53a9
commit
a3fca22df4
10 changed files with 584 additions and 30 deletions
|
|
@ -56,6 +56,11 @@ dictionary addonInstallOptions {
|
||||||
DOMString? hash = null;
|
DOMString? hash = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
dictionary sendAbuseReportOptions {
|
||||||
|
// This should be an Authorization HTTP header value.
|
||||||
|
DOMString? authorization = null;
|
||||||
|
};
|
||||||
|
|
||||||
[HeaderFile="mozilla/AddonManagerWebAPI.h",
|
[HeaderFile="mozilla/AddonManagerWebAPI.h",
|
||||||
Func="mozilla::AddonManagerWebAPI::IsAPIEnabled",
|
Func="mozilla::AddonManagerWebAPI::IsAPIEnabled",
|
||||||
JSImplementation="@mozilla.org/addon-web-api/manager;1",
|
JSImplementation="@mozilla.org/addon-web-api/manager;1",
|
||||||
|
|
@ -79,6 +84,30 @@ interface AddonManager : EventTarget {
|
||||||
* @return A promise that resolves to an instance of AddonInstall.
|
* @return A promise that resolves to an instance of AddonInstall.
|
||||||
*/
|
*/
|
||||||
Promise<AddonInstall> createInstall(optional addonInstallOptions options = {});
|
Promise<AddonInstall> createInstall(optional addonInstallOptions options = {});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends an abuse report to the AMO API.
|
||||||
|
*
|
||||||
|
* NOTE: The type for `data` and for the return value are loose because both
|
||||||
|
* the AMO API might change its response and the caller (AMO frontend) might
|
||||||
|
* also want to pass slightly different data in the future.
|
||||||
|
*
|
||||||
|
* @param addonId
|
||||||
|
* The ID of the add-on to report.
|
||||||
|
* @param data
|
||||||
|
* The caller passes the data to be sent to the AMO API.
|
||||||
|
* @param options
|
||||||
|
* Optional - A set of options. It currently only supports
|
||||||
|
* `authorization`, which is expected to be the value of an
|
||||||
|
* Authorization HTTP header when provided.
|
||||||
|
* @return A promise that resolves to the AMO API response, or an error when
|
||||||
|
* something went wrong.
|
||||||
|
*/
|
||||||
|
[NewObject] Promise<any> sendAbuseReport(
|
||||||
|
DOMString addonId,
|
||||||
|
record<DOMString, DOMString?> data,
|
||||||
|
optional sendAbuseReportOptions options = {}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
[ChromeOnly,Exposed=Window,HeaderFile="mozilla/AddonManagerWebAPI.h"]
|
[ChromeOnly,Exposed=Window,HeaderFile="mozilla/AddonManagerWebAPI.h"]
|
||||||
|
|
|
||||||
|
|
@ -1859,6 +1859,7 @@ pref("services.common.uptake.sampleRate", 1); // 1%
|
||||||
pref("extensions.abuseReport.enabled", false);
|
pref("extensions.abuseReport.enabled", false);
|
||||||
// Whether Firefox integrated abuse reporting feature should be opening the new abuse report form hosted on AMO.
|
// Whether Firefox integrated abuse reporting feature should be opening the new abuse report form hosted on AMO.
|
||||||
pref("extensions.abuseReport.amoFormURL", "https://addons.mozilla.org/%LOCALE%/%APP%/feedback/addon/%addonID%/");
|
pref("extensions.abuseReport.amoFormURL", "https://addons.mozilla.org/%LOCALE%/%APP%/feedback/addon/%addonID%/");
|
||||||
|
pref("extensions.addonAbuseReport.url", "https://services.addons.mozilla.org/api/v5/abuse/report/addon/");
|
||||||
|
|
||||||
// Blocklist preferences
|
// Blocklist preferences
|
||||||
pref("extensions.blocklist.enabled", true);
|
pref("extensions.blocklist.enabled", true);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
|
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
|
||||||
|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
||||||
|
|
||||||
// Maximum length of the string properties sent to the API endpoint.
|
// Maximum length of the string properties sent to the API endpoint.
|
||||||
const MAX_STRING_LENGTH = 255;
|
const MAX_STRING_LENGTH = 255;
|
||||||
|
|
@ -16,6 +17,8 @@ const AMO_SUPPORTED_ADDON_TYPES = [
|
||||||
"dictionary",
|
"dictionary",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const PREF_ADDON_ABUSE_REPORT_URL = "extensions.addonAbuseReport.url";
|
||||||
|
|
||||||
const lazy = {};
|
const lazy = {};
|
||||||
|
|
||||||
ChromeUtils.defineESModuleGetters(lazy, {
|
ChromeUtils.defineESModuleGetters(lazy, {
|
||||||
|
|
@ -23,6 +26,50 @@ ChromeUtils.defineESModuleGetters(lazy, {
|
||||||
ClientID: "resource://gre/modules/ClientID.sys.mjs",
|
ClientID: "resource://gre/modules/ClientID.sys.mjs",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyPreferenceGetter(
|
||||||
|
lazy,
|
||||||
|
"ADDON_ABUSE_REPORT_URL",
|
||||||
|
PREF_ADDON_ABUSE_REPORT_URL
|
||||||
|
);
|
||||||
|
|
||||||
|
const ERROR_TYPES = Object.freeze([
|
||||||
|
"ERROR_CLIENT",
|
||||||
|
"ERROR_NETWORK",
|
||||||
|
"ERROR_SERVER",
|
||||||
|
"ERROR_UNKNOWN",
|
||||||
|
]);
|
||||||
|
|
||||||
|
export class AbuseReportError extends Error {
|
||||||
|
constructor(errorType, errorInfo = undefined) {
|
||||||
|
if (!ERROR_TYPES.includes(errorType)) {
|
||||||
|
throw new Error(`Unexpected AbuseReportError type "${errorType}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let message = errorInfo ? `${errorType} - ${errorInfo}` : errorType;
|
||||||
|
|
||||||
|
super(message);
|
||||||
|
this.name = "AbuseReportError";
|
||||||
|
this.errorType = errorType;
|
||||||
|
this.errorInfo = errorInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an error info string from a fetch response object.
|
||||||
|
*
|
||||||
|
* @param {Response} response
|
||||||
|
* A fetch response object to convert into an errorInfo string.
|
||||||
|
*
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
* The errorInfo string to be included in an AbuseReportError.
|
||||||
|
*/
|
||||||
|
async function responseToErrorInfo(response) {
|
||||||
|
return JSON.stringify({
|
||||||
|
status: response.status,
|
||||||
|
responseText: await response.text().catch(() => ""),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A singleton used to manage abuse reports for add-ons.
|
* A singleton used to manage abuse reports for add-ons.
|
||||||
*/
|
*/
|
||||||
|
|
@ -37,6 +84,76 @@ export const AbuseReporter = {
|
||||||
return AMO_SUPPORTED_ADDON_TYPES.includes(addonType);
|
return AMO_SUPPORTED_ADDON_TYPES.includes(addonType);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an add-on abuse report using the AMO API. The data passed to this
|
||||||
|
* method might be augmented with report data known by Firefox.
|
||||||
|
*
|
||||||
|
* @param {string} addonId
|
||||||
|
* @param {{[key: string]: string|null}} data
|
||||||
|
* Abuse report data to be submitting to the AMO API along with the
|
||||||
|
* additional abuse report data known by Firefox.
|
||||||
|
* @param {object} [options]
|
||||||
|
* @param {string} [options.authorization]
|
||||||
|
* An optional value of an Authorization HTTP header to be set on the
|
||||||
|
* submission request.
|
||||||
|
*
|
||||||
|
* @returns {Promise<object>} Return a promise that resolves to the JSON AMO
|
||||||
|
* API response (or an error when something went wrong).
|
||||||
|
*/
|
||||||
|
async sendAbuseReport(addonId, data, options = {}) {
|
||||||
|
const rejectReportError = async (errorType, { response } = {}) => {
|
||||||
|
// Leave errorInfo empty if there is no response or fails to be converted
|
||||||
|
// into an error info object.
|
||||||
|
const errorInfo = response
|
||||||
|
? await responseToErrorInfo(response).catch(() => undefined)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
throw new AbuseReportError(errorType, errorInfo);
|
||||||
|
};
|
||||||
|
|
||||||
|
let abuseReport = { addon: addonId, ...data };
|
||||||
|
|
||||||
|
// If the add-on is installed, augment the data with internal report data.
|
||||||
|
const addon = await lazy.AddonManager.getAddonByID(addonId);
|
||||||
|
if (addon) {
|
||||||
|
const metadata = await AbuseReporter.getReportData(addon);
|
||||||
|
abuseReport = { ...abuseReport, ...metadata };
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = { "Content-Type": "application/json" };
|
||||||
|
if (options?.authorization?.length) {
|
||||||
|
headers.authorization = options.authorization;
|
||||||
|
}
|
||||||
|
|
||||||
|
let response;
|
||||||
|
try {
|
||||||
|
response = await fetch(lazy.ADDON_ABUSE_REPORT_URL, {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "omit",
|
||||||
|
referrerPolicy: "no-referrer",
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(abuseReport),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Cu.reportError(err);
|
||||||
|
return rejectReportError("ERROR_NETWORK");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.ok && response.status >= 200 && response.status < 400) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status >= 400 && response.status < 500) {
|
||||||
|
return rejectReportError("ERROR_CLIENT", { response });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status >= 500 && response.status < 600) {
|
||||||
|
return rejectReportError("ERROR_SERVER", { response });
|
||||||
|
}
|
||||||
|
|
||||||
|
return rejectReportError("ERROR_UNKNOWN", { response });
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper function that retrieves from an addon object all the data to send
|
* Helper function that retrieves from an addon object all the data to send
|
||||||
* as part of the submission request, besides the `reason`, `message` which are
|
* as part of the submission request, besides the `reason`, `message` which are
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,7 @@ var AsyncShutdown = realAsyncShutdown;
|
||||||
const lazy = {};
|
const lazy = {};
|
||||||
|
|
||||||
ChromeUtils.defineESModuleGetters(lazy, {
|
ChromeUtils.defineESModuleGetters(lazy, {
|
||||||
|
AbuseReporter: "resource://gre/modules/AbuseReporter.sys.mjs",
|
||||||
AddonRepository: "resource://gre/modules/addons/AddonRepository.sys.mjs",
|
AddonRepository: "resource://gre/modules/addons/AddonRepository.sys.mjs",
|
||||||
Extension: "resource://gre/modules/Extension.sys.mjs",
|
Extension: "resource://gre/modules/Extension.sys.mjs",
|
||||||
RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
|
RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
|
||||||
|
|
@ -3553,6 +3554,10 @@ var AddonManagerInternal = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async sendAbuseReport(target, addonId, data, options) {
|
||||||
|
return lazy.AbuseReporter.sendAbuseReport(addonId, data, options);
|
||||||
|
},
|
||||||
|
|
||||||
async addonUninstall(target, id) {
|
async addonUninstall(target, id) {
|
||||||
let addon = await AddonManager.getAddonByID(id);
|
let addon = await AddonManager.getAddonByID(id);
|
||||||
if (!addon) {
|
if (!addon) {
|
||||||
|
|
|
||||||
|
|
@ -244,6 +244,18 @@ export class WebAPI extends APIObject {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendAbuseReport(addonId, data, options) {
|
||||||
|
return this._apiTask(
|
||||||
|
"sendAbuseReport",
|
||||||
|
[addonId, data, options],
|
||||||
|
result => {
|
||||||
|
// The result below is a JS object coming from the expected AMO API
|
||||||
|
// endpoint response in JSON format.
|
||||||
|
return Cu.cloneInto(result, this.window);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
eventListenerAdded() {
|
eventListenerAdded() {
|
||||||
if (this.listenerCount == 0) {
|
if (this.listenerCount == 0) {
|
||||||
this.broker.setAddonListener(data => {
|
this.broker.setAddonListener(data => {
|
||||||
|
|
|
||||||
|
|
@ -175,6 +175,8 @@ https_first_disabled = true
|
||||||
|
|
||||||
["browser_webapi_install_disabled.js"]
|
["browser_webapi_install_disabled.js"]
|
||||||
|
|
||||||
|
["browser_webapi_sendAbuseReport.js"]
|
||||||
|
|
||||||
["browser_webapi_theme.js"]
|
["browser_webapi_theme.js"]
|
||||||
|
|
||||||
["browser_webapi_uninstall.js"]
|
["browser_webapi_uninstall.js"]
|
||||||
|
|
|
||||||
|
|
@ -64,3 +64,22 @@ add_task(async function test_report_action_hidden_on_langpack_addons() {
|
||||||
);
|
);
|
||||||
await closeAboutAddons();
|
await closeAboutAddons();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
add_task(async function test_report_action_hidden_on_system_addons() {
|
||||||
|
await openAboutAddons("extension");
|
||||||
|
await AbuseReportTestUtils.assertReportActionHidden(
|
||||||
|
gManagerWindow,
|
||||||
|
EXT_SYSTEM_ADDON_ID
|
||||||
|
);
|
||||||
|
await closeAboutAddons();
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_report_action_hidden_on_builtin_addons() {
|
||||||
|
const DEFAULT_BUILTIN_THEME_ID = "default-theme@mozilla.org";
|
||||||
|
await openAboutAddons("theme");
|
||||||
|
await AbuseReportTestUtils.assertReportActionHidden(
|
||||||
|
gManagerWindow,
|
||||||
|
DEFAULT_BUILTIN_THEME_ID
|
||||||
|
);
|
||||||
|
await closeAboutAddons();
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
|
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
|
|
||||||
|
const { AddonTestUtils } = ChromeUtils.importESModule(
|
||||||
|
"resource://testing-common/AddonTestUtils.sys.mjs"
|
||||||
|
);
|
||||||
|
|
||||||
|
const TESTPAGE = `${SECURE_TESTROOT}webapi_checkavailable.html`;
|
||||||
|
const ADDON_ID = "@test-extension-to-report";
|
||||||
|
|
||||||
|
AddonTestUtils.initMochitest(this);
|
||||||
|
|
||||||
|
const server = AddonTestUtils.createHttpServer({ hosts: ["test.addons.org"] });
|
||||||
|
let apiRequestHandler;
|
||||||
|
server.registerPathHandler("/api/abuse/report/addon/", (request, response) => {
|
||||||
|
apiRequestHandler(request, response);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_setup(async function () {
|
||||||
|
await SpecialPowers.pushPrefEnv({
|
||||||
|
set: [
|
||||||
|
["extensions.webapi.testing", true],
|
||||||
|
[
|
||||||
|
"extensions.addonAbuseReport.url",
|
||||||
|
// eslint-disable-next-line @microsoft/sdl/no-insecure-url
|
||||||
|
"http://test.addons.org/api/abuse/report/addon/",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_mozAddonManager_sendAbuseReport() {
|
||||||
|
apiRequestHandler = (req, res) => {
|
||||||
|
res.setStatusLine(req.httpVersion, 200, "OK");
|
||||||
|
res.setHeader("Content-Type", "application/json", false);
|
||||||
|
res.write('{"ok":true}');
|
||||||
|
};
|
||||||
|
|
||||||
|
await BrowserTestUtils.withNewTab(TESTPAGE, async browser => {
|
||||||
|
const extension = ExtensionTestUtils.loadExtension({
|
||||||
|
manifest: {
|
||||||
|
name: "some extension reported",
|
||||||
|
browser_specific_settings: { gecko: { id: ADDON_ID } },
|
||||||
|
},
|
||||||
|
useAddonManager: "temporary",
|
||||||
|
});
|
||||||
|
await extension.startup();
|
||||||
|
|
||||||
|
const response = await SpecialPowers.spawn(browser, [ADDON_ID], addonId => {
|
||||||
|
const data = { some: "data" };
|
||||||
|
return content.navigator.mozAddonManager.sendAbuseReport(addonId, data);
|
||||||
|
});
|
||||||
|
Assert.deepEqual(
|
||||||
|
response,
|
||||||
|
{ ok: true },
|
||||||
|
"expected API response to be returned"
|
||||||
|
);
|
||||||
|
|
||||||
|
await extension.unload();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_mozAddonManager_sendAbuseReport_error() {
|
||||||
|
apiRequestHandler = (req, res) => {
|
||||||
|
res.setStatusLine(req.httpVersion, 400, "BAD REQUEST");
|
||||||
|
};
|
||||||
|
|
||||||
|
await BrowserTestUtils.withNewTab(TESTPAGE, async browser => {
|
||||||
|
const extension = ExtensionTestUtils.loadExtension({
|
||||||
|
manifest: {
|
||||||
|
name: "some extension reported",
|
||||||
|
browser_specific_settings: { gecko: { id: ADDON_ID } },
|
||||||
|
},
|
||||||
|
useAddonManager: "temporary",
|
||||||
|
});
|
||||||
|
await extension.startup();
|
||||||
|
|
||||||
|
const webApiResult = await SpecialPowers.spawn(
|
||||||
|
browser,
|
||||||
|
[ADDON_ID],
|
||||||
|
addonId => {
|
||||||
|
const data = { some: "data" };
|
||||||
|
return content.navigator.mozAddonManager
|
||||||
|
.sendAbuseReport(addonId, data)
|
||||||
|
.then(
|
||||||
|
res => ({ gotRejection: false, result: res }),
|
||||||
|
err => ({
|
||||||
|
gotRejection: true,
|
||||||
|
message: err.message,
|
||||||
|
errorName: err.name,
|
||||||
|
isErrorInstance: err instanceof content.Error,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Assert.deepEqual(
|
||||||
|
webApiResult,
|
||||||
|
{
|
||||||
|
gotRejection: true,
|
||||||
|
message: 'ERROR_CLIENT - {"status":400,"responseText":""}',
|
||||||
|
errorName: "Error",
|
||||||
|
isErrorInstance: true,
|
||||||
|
},
|
||||||
|
"expected rejection"
|
||||||
|
);
|
||||||
|
|
||||||
|
await extension.unload();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -15,10 +15,7 @@ const { AddonTestUtils } = ChromeUtils.importESModule(
|
||||||
|
|
||||||
const EXT_DICTIONARY_ADDON_ID = "fake-dictionary@mochi.test";
|
const EXT_DICTIONARY_ADDON_ID = "fake-dictionary@mochi.test";
|
||||||
const EXT_LANGPACK_ADDON_ID = "fake-langpack@mochi.test";
|
const EXT_LANGPACK_ADDON_ID = "fake-langpack@mochi.test";
|
||||||
const EXT_WITH_PRIVILEGED_URL_ID = "ext-with-privileged-url@mochi.test";
|
|
||||||
const EXT_SYSTEM_ADDON_ID = "test-system-addon@mochi.test";
|
const EXT_SYSTEM_ADDON_ID = "test-system-addon@mochi.test";
|
||||||
const EXT_UNSUPPORTED_TYPE_ADDON_ID = "report-unsupported-type@mochi.test";
|
|
||||||
const THEME_NO_UNINSTALL_ID = "theme-without-perm-can-uninstall@mochi.test";
|
|
||||||
|
|
||||||
let gManagerWindow;
|
let gManagerWindow;
|
||||||
|
|
||||||
|
|
@ -37,11 +34,6 @@ async function closeAboutAddons() {
|
||||||
|
|
||||||
const AbuseReportTestUtils = {
|
const AbuseReportTestUtils = {
|
||||||
_mockProvider: null,
|
_mockProvider: null,
|
||||||
_mockServer: null,
|
|
||||||
_abuseRequestHandlers: [],
|
|
||||||
|
|
||||||
// Mock addon details API endpoint.
|
|
||||||
amoAddonDetailsMap: new Map(),
|
|
||||||
|
|
||||||
// Setup the test environment by setting the expected prefs and initializing
|
// Setup the test environment by setting the expected prefs and initializing
|
||||||
// MockProvider.
|
// MockProvider.
|
||||||
|
|
@ -87,21 +79,6 @@ const AbuseReportTestUtils = {
|
||||||
_setupMockProvider() {
|
_setupMockProvider() {
|
||||||
this._mockProvider = new MockProvider();
|
this._mockProvider = new MockProvider();
|
||||||
this._mockProvider.createAddons([
|
this._mockProvider.createAddons([
|
||||||
{
|
|
||||||
id: THEME_NO_UNINSTALL_ID,
|
|
||||||
name: "This theme cannot be uninstalled",
|
|
||||||
version: "1.1",
|
|
||||||
creator: { name: "Theme creator", url: "http://example.com/creator" },
|
|
||||||
type: "theme",
|
|
||||||
permissions: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: EXT_WITH_PRIVILEGED_URL_ID,
|
|
||||||
name: "This extension has an unexpected privileged creator URL",
|
|
||||||
version: "1.1",
|
|
||||||
creator: { name: "creator", url: "about:config" },
|
|
||||||
type: "extension",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: EXT_SYSTEM_ADDON_ID,
|
id: EXT_SYSTEM_ADDON_ID,
|
||||||
name: "This is a system addon",
|
name: "This is a system addon",
|
||||||
|
|
@ -110,12 +87,6 @@ const AbuseReportTestUtils = {
|
||||||
type: "extension",
|
type: "extension",
|
||||||
isSystem: true,
|
isSystem: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: EXT_UNSUPPORTED_TYPE_ADDON_ID,
|
|
||||||
name: "This is a fake unsupported addon type",
|
|
||||||
version: "1.1",
|
|
||||||
type: "unsupported_addon_type",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: EXT_LANGPACK_ADDON_ID,
|
id: EXT_LANGPACK_ADDON_ID,
|
||||||
name: "This is a fake langpack",
|
name: "This is a fake langpack",
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
* http://creativecommons.org/publicdomain/zero/1.0/
|
* http://creativecommons.org/publicdomain/zero/1.0/
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { AbuseReporter } = ChromeUtils.importESModule(
|
const { AbuseReporter, AbuseReportError } = ChromeUtils.importESModule(
|
||||||
"resource://gre/modules/AbuseReporter.sys.mjs"
|
"resource://gre/modules/AbuseReporter.sys.mjs"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -17,6 +17,10 @@ const FAKE_INSTALL_INFO = {
|
||||||
source: "fake-Install:Source",
|
source: "fake-Install:Source",
|
||||||
method: "fake:install method",
|
method: "fake:install method",
|
||||||
};
|
};
|
||||||
|
const EXPECTED_API_RESPONSE = {
|
||||||
|
id: ADDON_ID,
|
||||||
|
some: "other-props",
|
||||||
|
};
|
||||||
|
|
||||||
async function installTestExtension(overrideOptions = {}) {
|
async function installTestExtension(overrideOptions = {}) {
|
||||||
const extOptions = {
|
const extOptions = {
|
||||||
|
|
@ -96,9 +100,57 @@ async function assertBaseReportData({ reportData, addon }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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");
|
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 () => {
|
add_setup(async () => {
|
||||||
|
Services.prefs.setCharPref(
|
||||||
|
"extensions.addonAbuseReport.url",
|
||||||
|
"http://test.addons.org/api/abuse/report/addon/"
|
||||||
|
);
|
||||||
|
|
||||||
await promiseStartupManager();
|
await promiseStartupManager();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -221,3 +273,240 @@ add_task(async function test_normalized_addon_install_source_and_method() {
|
||||||
await assertAddonInstallMethod(test, expect);
|
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();
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue