diff --git a/dom/webidl/AddonManager.webidl b/dom/webidl/AddonManager.webidl index 0ccf6c92817f..908192784cbd 100644 --- a/dom/webidl/AddonManager.webidl +++ b/dom/webidl/AddonManager.webidl @@ -56,6 +56,11 @@ dictionary addonInstallOptions { DOMString? hash = null; }; +dictionary sendAbuseReportOptions { + // This should be an Authorization HTTP header value. + DOMString? authorization = null; +}; + [HeaderFile="mozilla/AddonManagerWebAPI.h", Func="mozilla::AddonManagerWebAPI::IsAPIEnabled", 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. */ Promise 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 sendAbuseReport( + DOMString addonId, + record data, + optional sendAbuseReportOptions options = {} + ); }; [ChromeOnly,Exposed=Window,HeaderFile="mozilla/AddonManagerWebAPI.h"] diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js index 29d14cb0f359..ebd76f395d03 100644 --- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -1859,6 +1859,7 @@ pref("services.common.uptake.sampleRate", 1); // 1% pref("extensions.abuseReport.enabled", false); // 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.addonAbuseReport.url", "https://services.addons.mozilla.org/api/v5/abuse/report/addon/"); // Blocklist preferences pref("extensions.blocklist.enabled", true); diff --git a/toolkit/mozapps/extensions/AbuseReporter.sys.mjs b/toolkit/mozapps/extensions/AbuseReporter.sys.mjs index 966e2a6dd5d5..21130120e78e 100644 --- a/toolkit/mozapps/extensions/AbuseReporter.sys.mjs +++ b/toolkit/mozapps/extensions/AbuseReporter.sys.mjs @@ -3,6 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 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. const MAX_STRING_LENGTH = 255; @@ -16,6 +17,8 @@ const AMO_SUPPORTED_ADDON_TYPES = [ "dictionary", ]; +const PREF_ADDON_ABUSE_REPORT_URL = "extensions.addonAbuseReport.url"; + const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { @@ -23,6 +26,50 @@ ChromeUtils.defineESModuleGetters(lazy, { 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} + * 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. */ @@ -37,6 +84,76 @@ export const AbuseReporter = { 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} 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 * as part of the submission request, besides the `reason`, `message` which are diff --git a/toolkit/mozapps/extensions/AddonManager.sys.mjs b/toolkit/mozapps/extensions/AddonManager.sys.mjs index 9890e15bf4ae..b84b3093018f 100644 --- a/toolkit/mozapps/extensions/AddonManager.sys.mjs +++ b/toolkit/mozapps/extensions/AddonManager.sys.mjs @@ -80,6 +80,7 @@ var AsyncShutdown = realAsyncShutdown; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { + AbuseReporter: "resource://gre/modules/AbuseReporter.sys.mjs", AddonRepository: "resource://gre/modules/addons/AddonRepository.sys.mjs", Extension: "resource://gre/modules/Extension.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) { let addon = await AddonManager.getAddonByID(id); if (!addon) { diff --git a/toolkit/mozapps/extensions/amWebAPI.sys.mjs b/toolkit/mozapps/extensions/amWebAPI.sys.mjs index 64d4bf78d20f..59d093603aed 100644 --- a/toolkit/mozapps/extensions/amWebAPI.sys.mjs +++ b/toolkit/mozapps/extensions/amWebAPI.sys.mjs @@ -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() { if (this.listenerCount == 0) { this.broker.setAddonListener(data => { diff --git a/toolkit/mozapps/extensions/test/browser/browser.toml b/toolkit/mozapps/extensions/test/browser/browser.toml index b43064da16a4..c8d46cbd4598 100644 --- a/toolkit/mozapps/extensions/test/browser/browser.toml +++ b/toolkit/mozapps/extensions/test/browser/browser.toml @@ -175,6 +175,8 @@ https_first_disabled = true ["browser_webapi_install_disabled.js"] +["browser_webapi_sendAbuseReport.js"] + ["browser_webapi_theme.js"] ["browser_webapi_uninstall.js"] diff --git a/toolkit/mozapps/extensions/test/browser/browser_amo_abuse_report.js b/toolkit/mozapps/extensions/test/browser/browser_amo_abuse_report.js index 7cd2d5d57d8b..057201fdfe08 100644 --- a/toolkit/mozapps/extensions/test/browser/browser_amo_abuse_report.js +++ b/toolkit/mozapps/extensions/test/browser/browser_amo_abuse_report.js @@ -64,3 +64,22 @@ add_task(async function test_report_action_hidden_on_langpack_addons() { ); 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(); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_webapi_sendAbuseReport.js b/toolkit/mozapps/extensions/test/browser/browser_webapi_sendAbuseReport.js new file mode 100644 index 000000000000..d6b70bad9179 --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_sendAbuseReport.js @@ -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(); + }); +}); diff --git a/toolkit/mozapps/extensions/test/browser/head_abuse_report.js b/toolkit/mozapps/extensions/test/browser/head_abuse_report.js index 173f6ab7ea8e..9f6f1b4ed314 100644 --- a/toolkit/mozapps/extensions/test/browser/head_abuse_report.js +++ b/toolkit/mozapps/extensions/test/browser/head_abuse_report.js @@ -15,10 +15,7 @@ const { AddonTestUtils } = ChromeUtils.importESModule( const EXT_DICTIONARY_ADDON_ID = "fake-dictionary@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_UNSUPPORTED_TYPE_ADDON_ID = "report-unsupported-type@mochi.test"; -const THEME_NO_UNINSTALL_ID = "theme-without-perm-can-uninstall@mochi.test"; let gManagerWindow; @@ -37,11 +34,6 @@ async function closeAboutAddons() { const AbuseReportTestUtils = { _mockProvider: null, - _mockServer: null, - _abuseRequestHandlers: [], - - // Mock addon details API endpoint. - amoAddonDetailsMap: new Map(), // Setup the test environment by setting the expected prefs and initializing // MockProvider. @@ -87,21 +79,6 @@ const AbuseReportTestUtils = { _setupMockProvider() { this._mockProvider = new MockProvider(); 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, name: "This is a system addon", @@ -110,12 +87,6 @@ const AbuseReportTestUtils = { type: "extension", 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, name: "This is a fake langpack", diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_AbuseReporter.js b/toolkit/mozapps/extensions/test/xpcshell/test_AbuseReporter.js index 4fe0821c7078..a9f551502704 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_AbuseReporter.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_AbuseReporter.js @@ -2,7 +2,7 @@ * http://creativecommons.org/publicdomain/zero/1.0/ */ -const { AbuseReporter } = ChromeUtils.importESModule( +const { AbuseReporter, AbuseReportError } = ChromeUtils.importESModule( "resource://gre/modules/AbuseReporter.sys.mjs" ); @@ -17,6 +17,10 @@ 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 = { @@ -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"); +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(); }); @@ -221,3 +273,240 @@ add_task(async function test_normalized_addon_install_source_and_method() { 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(); +});