summaryrefslogtreecommitdiffstats
path: root/toolkit/components/shopping/test/xpcshell
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /toolkit/components/shopping/test/xpcshell
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/shopping/test/xpcshell')
-rw-r--r--toolkit/components/shopping/test/xpcshell/data/analysis_request.json4
-rw-r--r--toolkit/components/shopping/test/xpcshell/data/analysis_response.json24
-rw-r--r--toolkit/components/shopping/test/xpcshell/data/analysis_status_completed_response.json4
-rw-r--r--toolkit/components/shopping/test/xpcshell/data/analysis_status_in_progress_response.json4
-rw-r--r--toolkit/components/shopping/test/xpcshell/data/analysis_status_pending_response.json4
-rw-r--r--toolkit/components/shopping/test/xpcshell/data/analyze_pending.json3
-rw-r--r--toolkit/components/shopping/test/xpcshell/data/attribution_response.json3
-rw-r--r--toolkit/components/shopping/test/xpcshell/data/bad_request.json4
-rw-r--r--toolkit/components/shopping/test/xpcshell/data/image.jpgbin0 -> 12152 bytes
-rw-r--r--toolkit/components/shopping/test/xpcshell/data/invalid_analysis_request.json4
-rw-r--r--toolkit/components/shopping/test/xpcshell/data/invalid_analysis_response.json7
-rw-r--r--toolkit/components/shopping/test/xpcshell/data/invalid_recommendations_request.json4
-rw-r--r--toolkit/components/shopping/test/xpcshell/data/invalid_recommendations_response.json14
-rw-r--r--toolkit/components/shopping/test/xpcshell/data/needs_analysis_response.json6
-rw-r--r--toolkit/components/shopping/test/xpcshell/data/recommendations_request.json4
-rw-r--r--toolkit/components/shopping/test/xpcshell/data/recommendations_response.json14
-rw-r--r--toolkit/components/shopping/test/xpcshell/data/report_response.json3
-rw-r--r--toolkit/components/shopping/test/xpcshell/data/service_unavailable.json4
-rw-r--r--toolkit/components/shopping/test/xpcshell/data/too_many_requests.json4
-rw-r--r--toolkit/components/shopping/test/xpcshell/data/unprocessable_entity.json4
-rw-r--r--toolkit/components/shopping/test/xpcshell/head.js57
-rw-r--r--toolkit/components/shopping/test/xpcshell/test_fetchImage.js106
-rw-r--r--toolkit/components/shopping/test/xpcshell/test_product.js940
-rw-r--r--toolkit/components/shopping/test/xpcshell/test_product_urls.js297
-rw-r--r--toolkit/components/shopping/test/xpcshell/test_product_validator.js91
-rw-r--r--toolkit/components/shopping/test/xpcshell/xpcshell.toml38
26 files changed, 1647 insertions, 0 deletions
diff --git a/toolkit/components/shopping/test/xpcshell/data/analysis_request.json b/toolkit/components/shopping/test/xpcshell/data/analysis_request.json
new file mode 100644
index 0000000000..28efa08fdf
--- /dev/null
+++ b/toolkit/components/shopping/test/xpcshell/data/analysis_request.json
@@ -0,0 +1,4 @@
+{
+ "product_id": "B07W59LRL9",
+ "website": "amazon.com"
+}
diff --git a/toolkit/components/shopping/test/xpcshell/data/analysis_response.json b/toolkit/components/shopping/test/xpcshell/data/analysis_response.json
new file mode 100644
index 0000000000..d3413b0643
--- /dev/null
+++ b/toolkit/components/shopping/test/xpcshell/data/analysis_response.json
@@ -0,0 +1,24 @@
+{
+ "product_id": "B07W59LRL9",
+ "grade": "B",
+ "adjusted_rating": 4.7,
+ "needs_analysis": false,
+ "analysis_url": "https://staging.fakespot.com/product/garmin-010-02157-10-fenix-6x-sapphire-premium-multisport-gps-watch-features-mapping-music-grade-adjusted-pace-guidance-and-pulse-ox-sensors-dark-gray-with-black-band",
+ "highlights": {
+ "price": ["This watch is great and the price was even better."],
+ "quality": [
+ "Other than that, I am very impressed with the watch and it’s capabilities.",
+ "This watch performs above expectations in every way with the exception of the heart rate monitor.",
+ "Battery life is no better than the 3 even with the solar gimmick, probably worse.",
+ "I have small wrists and still went with the 6X and glad I did.",
+ "I can deal with the looks, as Im now retired."
+ ],
+ "competitiveness": [
+ "Bought this to replace my vivoactive 3.",
+ "I like that this watch has so many features, especially those that monitor health like SP02, respiration, sleep, HRV status, stress, and heart rate.",
+ "I do not use it for sleep or heartrate monitoring so not sure how accurate they are.",
+ "I've avoided getting a smartwatch for so long due to short battery life on most of them."
+ ],
+ "packaging/appearance": ["I loved the minimalist cardboard packaging"]
+ }
+}
diff --git a/toolkit/components/shopping/test/xpcshell/data/analysis_status_completed_response.json b/toolkit/components/shopping/test/xpcshell/data/analysis_status_completed_response.json
new file mode 100644
index 0000000000..4c17e006ba
--- /dev/null
+++ b/toolkit/components/shopping/test/xpcshell/data/analysis_status_completed_response.json
@@ -0,0 +1,4 @@
+{
+ "status": "completed",
+ "progress": 100.0
+}
diff --git a/toolkit/components/shopping/test/xpcshell/data/analysis_status_in_progress_response.json b/toolkit/components/shopping/test/xpcshell/data/analysis_status_in_progress_response.json
new file mode 100644
index 0000000000..6f42fa495c
--- /dev/null
+++ b/toolkit/components/shopping/test/xpcshell/data/analysis_status_in_progress_response.json
@@ -0,0 +1,4 @@
+{
+ "status": "in_progress",
+ "progress": 50.0
+}
diff --git a/toolkit/components/shopping/test/xpcshell/data/analysis_status_pending_response.json b/toolkit/components/shopping/test/xpcshell/data/analysis_status_pending_response.json
new file mode 100644
index 0000000000..aa9233cff5
--- /dev/null
+++ b/toolkit/components/shopping/test/xpcshell/data/analysis_status_pending_response.json
@@ -0,0 +1,4 @@
+{
+ "status": "pending",
+ "progress": 0.0
+}
diff --git a/toolkit/components/shopping/test/xpcshell/data/analyze_pending.json b/toolkit/components/shopping/test/xpcshell/data/analyze_pending.json
new file mode 100644
index 0000000000..500a1fe0d4
--- /dev/null
+++ b/toolkit/components/shopping/test/xpcshell/data/analyze_pending.json
@@ -0,0 +1,3 @@
+{
+ "status": "pending"
+}
diff --git a/toolkit/components/shopping/test/xpcshell/data/attribution_response.json b/toolkit/components/shopping/test/xpcshell/data/attribution_response.json
new file mode 100644
index 0000000000..4d11d41ca4
--- /dev/null
+++ b/toolkit/components/shopping/test/xpcshell/data/attribution_response.json
@@ -0,0 +1,3 @@
+{
+ "1ALhiNLkZ2yR4al5lcP1Npbtlpl5toDfKRgJOATjeieAL6i5Dul99l9+ZTiIWyybUzGysChAdrOA6BWrMqr0EvjoymiH3veZ++XuOvJnC0y1NB/IQQtUzlYEO028XqVUJWJeJte47nPhnK2pSm2QhbdeKbxEnauKAty1cFQeEaBUP7LkvUgxh1GDzflwcVfuKcgMr7hOM3NzjYR2RN3vhmT385Ps4wUj--cv2ucc+1nozldFrl--i9GYyjuHYFFi+EgXXZ3ZsA==": null
+}
diff --git a/toolkit/components/shopping/test/xpcshell/data/bad_request.json b/toolkit/components/shopping/test/xpcshell/data/bad_request.json
new file mode 100644
index 0000000000..b6a9dff9e0
--- /dev/null
+++ b/toolkit/components/shopping/test/xpcshell/data/bad_request.json
@@ -0,0 +1,4 @@
+{
+ "status": 400,
+ "error": "Bad Request"
+}
diff --git a/toolkit/components/shopping/test/xpcshell/data/image.jpg b/toolkit/components/shopping/test/xpcshell/data/image.jpg
new file mode 100644
index 0000000000..78e77baed6
--- /dev/null
+++ b/toolkit/components/shopping/test/xpcshell/data/image.jpg
Binary files differ
diff --git a/toolkit/components/shopping/test/xpcshell/data/invalid_analysis_request.json b/toolkit/components/shopping/test/xpcshell/data/invalid_analysis_request.json
new file mode 100644
index 0000000000..008fce718c
--- /dev/null
+++ b/toolkit/components/shopping/test/xpcshell/data/invalid_analysis_request.json
@@ -0,0 +1,4 @@
+{
+ "product_id": 12345,
+ "website": "amazon.com"
+}
diff --git a/toolkit/components/shopping/test/xpcshell/data/invalid_analysis_response.json b/toolkit/components/shopping/test/xpcshell/data/invalid_analysis_response.json
new file mode 100644
index 0000000000..2210e97487
--- /dev/null
+++ b/toolkit/components/shopping/test/xpcshell/data/invalid_analysis_response.json
@@ -0,0 +1,7 @@
+{
+ "product_id": "B07W59LRL9",
+ "grade": 0.85,
+ "adjusted_rating": "4.7",
+ "needs_analysis": true,
+ "highlights": {}
+}
diff --git a/toolkit/components/shopping/test/xpcshell/data/invalid_recommendations_request.json b/toolkit/components/shopping/test/xpcshell/data/invalid_recommendations_request.json
new file mode 100644
index 0000000000..454cf49942
--- /dev/null
+++ b/toolkit/components/shopping/test/xpcshell/data/invalid_recommendations_request.json
@@ -0,0 +1,4 @@
+{
+ "product_id": 12345,
+ "website": ""
+}
diff --git a/toolkit/components/shopping/test/xpcshell/data/invalid_recommendations_response.json b/toolkit/components/shopping/test/xpcshell/data/invalid_recommendations_response.json
new file mode 100644
index 0000000000..7f3ffb8029
--- /dev/null
+++ b/toolkit/components/shopping/test/xpcshell/data/invalid_recommendations_response.json
@@ -0,0 +1,14 @@
+[
+ {
+ "name": "VIVO Electric 60 x 24 inch Stand Up Desk | Black Table Top, Black Frame, Height Adjustable Standing Workstation with Memory Preset Controller (DESK-KIT-1B6B)",
+ "url": "http://amazon.com/dp/B07V6ZSHF4",
+ "image_url": "https://i.fakespot.io/b6vx27xf3rgwr1a597q6qd3rutp6",
+ "price": 249.99,
+ "currency": "USD",
+ "grade": 0.5,
+ "adjusted_rating": "4.6",
+ "analysis_url": "https://www.fakespot.com/product/vivo-electric-60-x-24-inch-stand-up-desk-black-table-top-black-frame-height-adjustable-standing-workstation-with-memory-preset-controller-desk-kit-1b6b",
+ "sponsored": true,
+ "aid": "ELcC6OziGKu2jjQsCf5xMYHTpyPKFtjl/4mKPeygbSuQMyIqF/gkY3bTTznoMmNv0OsPV5uql0/NdzNsoguccIS0BujM3DwBADvkGIKLLF2WX0u3G+B2tvRpZmbTmC1NQW0ivS/KX7dTrRjae3Z84fs0i0PySM68buCo5JY848wvzdlyTfCrcT0B3/Ov0tJjZdy9FupF9skLuFtx0/lgElSRsnoGow/H--uLo2Tq7E++RNxgsl--YqlLhv5n8iGFYQwBD61VBg=="
+ }
+]
diff --git a/toolkit/components/shopping/test/xpcshell/data/needs_analysis_response.json b/toolkit/components/shopping/test/xpcshell/data/needs_analysis_response.json
new file mode 100644
index 0000000000..3c1cc93a3f
--- /dev/null
+++ b/toolkit/components/shopping/test/xpcshell/data/needs_analysis_response.json
@@ -0,0 +1,6 @@
+{
+ "product_id": null,
+ "grade": null,
+ "adjusted_rating": null,
+ "needs_analysis": true
+}
diff --git a/toolkit/components/shopping/test/xpcshell/data/recommendations_request.json b/toolkit/components/shopping/test/xpcshell/data/recommendations_request.json
new file mode 100644
index 0000000000..fed419f993
--- /dev/null
+++ b/toolkit/components/shopping/test/xpcshell/data/recommendations_request.json
@@ -0,0 +1,4 @@
+{
+ "product_id": "B0C2T6SQJC",
+ "website": "amazon.com"
+}
diff --git a/toolkit/components/shopping/test/xpcshell/data/recommendations_response.json b/toolkit/components/shopping/test/xpcshell/data/recommendations_response.json
new file mode 100644
index 0000000000..89dda89581
--- /dev/null
+++ b/toolkit/components/shopping/test/xpcshell/data/recommendations_response.json
@@ -0,0 +1,14 @@
+[
+ {
+ "name": "VIVO Electric 60 x 24 inch Stand Up Desk | Black Table Top, Black Frame, Height Adjustable Standing Workstation with Memory Preset Controller (DESK-KIT-1B6B)",
+ "url": "http://amazon.com/dp/B07V6ZSHF4",
+ "image_url": "http://example.com/api/image.jpg",
+ "price": "249.99",
+ "currency": "USD",
+ "grade": "A",
+ "adjusted_rating": 4.6,
+ "analysis_url": "https://www.fakespot.com/product/vivo-electric-60-x-24-inch-stand-up-desk-black-table-top-black-frame-height-adjustable-standing-workstation-with-memory-preset-controller-desk-kit-1b6b",
+ "sponsored": true,
+ "aid": "ELcC6OziGKu2jjQsCf5xMYHTpyPKFtjl/4mKPeygbSuQMyIqF/gkY3bTTznoMmNv0OsPV5uql0/NdzNsoguccIS0BujM3DwBADvkGIKLLF2WX0u3G+B2tvRpZmbTmC1NQW0ivS/KX7dTrRjae3Z84fs0i0PySM68buCo5JY848wvzdlyTfCrcT0B3/Ov0tJjZdy9FupF9skLuFtx0/lgElSRsnoGow/H--uLo2Tq7E++RNxgsl--YqlLhv5n8iGFYQwBD61VBg=="
+ }
+]
diff --git a/toolkit/components/shopping/test/xpcshell/data/report_response.json b/toolkit/components/shopping/test/xpcshell/data/report_response.json
new file mode 100644
index 0000000000..9f049555b1
--- /dev/null
+++ b/toolkit/components/shopping/test/xpcshell/data/report_response.json
@@ -0,0 +1,3 @@
+{
+ "message": "report created"
+}
diff --git a/toolkit/components/shopping/test/xpcshell/data/service_unavailable.json b/toolkit/components/shopping/test/xpcshell/data/service_unavailable.json
new file mode 100644
index 0000000000..c863e52262
--- /dev/null
+++ b/toolkit/components/shopping/test/xpcshell/data/service_unavailable.json
@@ -0,0 +1,4 @@
+{
+ "status": 503,
+ "error": "Service Unavailable"
+}
diff --git a/toolkit/components/shopping/test/xpcshell/data/too_many_requests.json b/toolkit/components/shopping/test/xpcshell/data/too_many_requests.json
new file mode 100644
index 0000000000..f2c5e48524
--- /dev/null
+++ b/toolkit/components/shopping/test/xpcshell/data/too_many_requests.json
@@ -0,0 +1,4 @@
+{
+ "status": 429,
+ "error": "Too Many Requests"
+}
diff --git a/toolkit/components/shopping/test/xpcshell/data/unprocessable_entity.json b/toolkit/components/shopping/test/xpcshell/data/unprocessable_entity.json
new file mode 100644
index 0000000000..8f3fdc745a
--- /dev/null
+++ b/toolkit/components/shopping/test/xpcshell/data/unprocessable_entity.json
@@ -0,0 +1,4 @@
+{
+ "status": 422,
+ "error": "Unprocessable entity"
+}
diff --git a/toolkit/components/shopping/test/xpcshell/head.js b/toolkit/components/shopping/test/xpcshell/head.js
new file mode 100644
index 0000000000..22ec9a9742
--- /dev/null
+++ b/toolkit/components/shopping/test/xpcshell/head.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+/* exported createHttpServer, loadJSONfromFile, readFile */
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+const { NetUtil } = ChromeUtils.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs"
+);
+
+const createHttpServer = (...args) => {
+ AddonTestUtils.maybeInit(this);
+ return AddonTestUtils.createHttpServer(...args);
+};
+
+async function loadJSONfromFile(path) {
+ let file = do_get_file(path);
+ let uri = Services.io.newFileURI(file);
+ return fetch(uri.spec).then(resp => {
+ if (!resp.ok) {
+ return undefined;
+ }
+ return resp.json();
+ });
+}
+
+function readFile(path) {
+ let file = do_get_file(path);
+ let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ fstream.init(file, -1, 0, 0);
+ let data = NetUtil.readInputStreamToString(fstream, fstream.available());
+ fstream.close();
+ return data;
+}
+
+/* These are constants but declared `var` so they can be used by the individual
+ * test files.
+ */
+var API_OHTTP_RELAY = "http://example.com/relay/";
+var API_OHTTP_CONFIG = "http://example.com/ohttp-config";
+
+function enableOHTTP(configURL = API_OHTTP_CONFIG) {
+ Services.prefs.setCharPref("toolkit.shopping.ohttpConfigURL", configURL);
+ Services.prefs.setCharPref("toolkit.shopping.ohttpRelayURL", API_OHTTP_RELAY);
+}
+
+function disableOHTTP() {
+ for (let pref of ["ohttpRelayURL", "ohttpConfigURL"]) {
+ Services.prefs.setCharPref(`toolkit.shopping.${pref}`, "");
+ }
+}
diff --git a/toolkit/components/shopping/test/xpcshell/test_fetchImage.js b/toolkit/components/shopping/test/xpcshell/test_fetchImage.js
new file mode 100644
index 0000000000..c0f6965904
--- /dev/null
+++ b/toolkit/components/shopping/test/xpcshell/test_fetchImage.js
@@ -0,0 +1,106 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { ShoppingProduct } = ChromeUtils.importESModule(
+ "chrome://global/content/shopping/ShoppingProduct.mjs"
+);
+const IMAGE_URL = "http://example.com/api/image.jpg";
+
+const server = createHttpServer({ hosts: ["example.com"] });
+server.registerDirectory("/api/", do_get_file("/data"));
+
+function BinaryHttpResponse(status, headerNames, headerValues, content) {
+ this.status = status;
+ this.headerNames = headerNames;
+ this.headerValues = headerValues;
+ this.content = content;
+}
+
+BinaryHttpResponse.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIBinaryHttpResponse"]),
+};
+
+let ohttp = Cc["@mozilla.org/network/oblivious-http;1"].getService(
+ Ci.nsIObliviousHttp
+);
+let ohttpServer = ohttp.server();
+
+server.registerPathHandler(
+ new URL(API_OHTTP_CONFIG).pathname,
+ (request, response) => {
+ let bstream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(
+ Ci.nsIBinaryOutputStream
+ );
+ bstream.setOutputStream(response.bodyOutputStream);
+ bstream.writeByteArray(ohttpServer.encodedConfig);
+ }
+);
+
+let gExpectedOHTTPMethod = "GET";
+server.registerPathHandler(
+ new URL(API_OHTTP_RELAY).pathname,
+ async (request, response) => {
+ let inputStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
+ Ci.nsIBinaryInputStream
+ );
+ inputStream.setInputStream(request.bodyInputStream);
+ let requestBody = inputStream.readByteArray(inputStream.available());
+ let ohttpRequest = ohttpServer.decapsulate(requestBody);
+ let bhttp = Cc["@mozilla.org/network/binary-http;1"].getService(
+ Ci.nsIBinaryHttp
+ );
+ let decodedRequest = bhttp.decodeRequest(ohttpRequest.request);
+ Assert.equal(
+ decodedRequest.method,
+ gExpectedOHTTPMethod,
+ "Should get expected HTTP method"
+ );
+ Assert.deepEqual(decodedRequest.headerNames.sort(), [
+ "Accept",
+ "Content-Type",
+ ]);
+ Assert.deepEqual(decodedRequest.headerValues, ["image/jpeg", "image/jpeg"]);
+
+ response.processAsync();
+ let innerResponse = await fetch("http://example.com" + decodedRequest.path);
+ let bytes = new Uint8Array(await innerResponse.arrayBuffer());
+ let binaryResponse = new BinaryHttpResponse(
+ innerResponse.status,
+ ["Content-Type"],
+ ["image/jpeg"],
+ bytes
+ );
+ let encResponse = ohttpRequest.encapsulate(
+ bhttp.encodeResponse(binaryResponse)
+ );
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "message/ohttp-res", false);
+
+ let bstream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(
+ Ci.nsIBinaryOutputStream
+ );
+ bstream.setOutputStream(response.bodyOutputStream);
+ bstream.writeByteArray(encResponse);
+ response.finish();
+ }
+);
+
+add_task(async function test_product_requestImageBlob() {
+ let uri = new URL("https://www.walmart.com/ip/926485654");
+ let product = new ShoppingProduct(uri, { allowValidationFailure: false });
+
+ Assert.ok(product.isProduct(), "Should recognize a valid product.");
+
+ let img = await ShoppingProduct.requestImageBlob(IMAGE_URL);
+
+ Assert.ok(Blob.isInstance(img), "Image is loaded and returned as a blob");
+
+ enableOHTTP();
+ img = await ShoppingProduct.requestImageBlob(IMAGE_URL);
+ disableOHTTP();
+
+ Assert.ok(Blob.isInstance(img), "Image is loaded and returned as a blob");
+});
diff --git a/toolkit/components/shopping/test/xpcshell/test_product.js b/toolkit/components/shopping/test/xpcshell/test_product.js
new file mode 100644
index 0000000000..ec11e502f6
--- /dev/null
+++ b/toolkit/components/shopping/test/xpcshell/test_product.js
@@ -0,0 +1,940 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+/* global createHttpServer, readFile */
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+function BinaryHttpResponse(status, headerNames, headerValues, content) {
+ this.status = status;
+ this.headerNames = headerNames;
+ this.headerValues = headerValues;
+ this.content = content;
+}
+
+BinaryHttpResponse.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIBinaryHttpResponse"]),
+};
+
+const {
+ ANALYSIS_RESPONSE_SCHEMA,
+ ANALYSIS_REQUEST_SCHEMA,
+ RECOMMENDATIONS_RESPONSE_SCHEMA,
+ RECOMMENDATIONS_REQUEST_SCHEMA,
+ ATTRIBUTION_RESPONSE_SCHEMA,
+ ATTRIBUTION_REQUEST_SCHEMA,
+ ANALYZE_RESPONSE_SCHEMA,
+ ANALYZE_REQUEST_SCHEMA,
+ ANALYSIS_STATUS_RESPONSE_SCHEMA,
+ ANALYSIS_STATUS_REQUEST_SCHEMA,
+} = ChromeUtils.importESModule(
+ "chrome://global/content/shopping/ProductConfig.mjs"
+);
+
+const { ShoppingProduct } = ChromeUtils.importESModule(
+ "chrome://global/content/shopping/ShoppingProduct.mjs"
+);
+
+const ANALYSIS_API_MOCK = "http://example.com/api/analysis_response.json";
+const RECOMMENDATIONS_API_MOCK =
+ "http://example.com/api/recommendations_response.json";
+const ATTRIBUTION_API_MOCK = "http://example.com/api/attribution_response.json";
+const ANALYSIS_API_MOCK_INVALID =
+ "http://example.com/api/invalid_analysis_response.json";
+const API_SERVICE_UNAVAILABLE =
+ "http://example.com/errors/service_unavailable.json";
+const API_ERROR_ONCE = "http://example.com/errors/error_once.json";
+const API_ERROR_BAD_REQUEST = "http://example.com/errors/bad_request.json";
+const API_ERROR_UNPROCESSABLE =
+ "http://example.com/errors/unprocessable_entity.json";
+const API_ERROR_TOO_MANY_REQUESTS =
+ "http://example.com/errors/too_many_requests.json";
+const API_POLL = "http://example.com/poll/poll_analysis_response.json";
+const API_ANALYSIS_IN_PROGRESS =
+ "http://example.com/poll/analysis_in_progress.json";
+const REPORTING_API_MOCK = "http://example.com/api/report_response.json";
+const ANALYZE_API_MOCK = "http://example.com/api/analyze_pending.json";
+
+const TEST_AID =
+ "1ALhiNLkZ2yR4al5lcP1Npbtlpl5toDfKRgJOATjeieAL6i5Dul99l9+ZTiIWyybUzGysChAdrOA6BWrMqr0EvjoymiH3veZ++XuOvJnC0y1NB/IQQtUzlYEO028XqVUJWJeJte47nPhnK2pSm2QhbdeKbxEnauKAty1cFQeEaBUP7LkvUgxh1GDzflwcVfuKcgMr7hOM3NzjYR2RN3vhmT385Ps4wUj--cv2ucc+1nozldFrl--i9GYyjuHYFFi+EgXXZ3ZsA==";
+
+const server = createHttpServer({ hosts: ["example.com"] });
+server.registerDirectory("/api/", do_get_file("/data"));
+
+// Path to test API call that will always fail.
+server.registerPathHandler(
+ new URL(API_SERVICE_UNAVAILABLE).pathname,
+ (request, response) => {
+ response.setStatusLine(request.httpVersion, 503, "Service Unavailable");
+ response.setHeader(
+ "Content-Type",
+ "application/json; charset=utf-8",
+ false
+ );
+ response.write(readFile("data/service_unavailable.json", false));
+ }
+);
+
+// Path to test API call that will fail once and then succeeded.
+let apiErrors = 0;
+server.registerPathHandler(
+ new URL(API_ERROR_ONCE).pathname,
+ (request, response) => {
+ response.setHeader(
+ "Content-Type",
+ "application/json; charset=utf-8",
+ false
+ );
+ if (apiErrors == 0) {
+ response.setStatusLine(request.httpVersion, 503, "Service Unavailable");
+ response.write(readFile("/data/service_unavailable.json"));
+ apiErrors++;
+ } else {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write(readFile("/data/analysis_response.json"));
+ apiErrors = 0;
+ }
+ }
+);
+
+// Request is missing required parameters.
+server.registerPathHandler(
+ new URL(API_ERROR_BAD_REQUEST).pathname,
+ (request, response) => {
+ response.setStatusLine(request.httpVersion, 400, "Bad Request");
+ response.setHeader(
+ "Content-Type",
+ "application/json; charset=utf-8",
+ false
+ );
+ response.write(readFile("data/bad_request.json", false));
+ }
+);
+
+// Request contains a nonsense product identifier or non supported website.
+server.registerPathHandler(
+ new URL(API_ERROR_UNPROCESSABLE).pathname,
+ (request, response) => {
+ response.setStatusLine(request.httpVersion, 422, "Unprocessable entity");
+ response.setHeader(
+ "Content-Type",
+ "application/json; charset=utf-8",
+ false
+ );
+ response.write(readFile("data/unprocessable_entity.json", false));
+ }
+);
+
+// Too many requests to the API.
+server.registerPathHandler(
+ new URL(API_ERROR_TOO_MANY_REQUESTS).pathname,
+ (request, response) => {
+ response.setStatusLine(request.httpVersion, 429, "Too many requests");
+ response.setHeader(
+ "Content-Type",
+ "application/json; charset=utf-8",
+ false
+ );
+ response.write(readFile("data/too_many_requests.json", false));
+ }
+);
+
+// Path to test API call that will still be processing twice and then succeeded.
+let pollingTries = 0;
+server.registerPathHandler(new URL(API_POLL).pathname, (request, response) => {
+ response.setHeader("Content-Type", "application/json; charset=utf-8", false);
+ if (pollingTries == 0) {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write(readFile("/data/analysis_status_pending_response.json"));
+ pollingTries++;
+ } else if (pollingTries == 1) {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write(readFile("/data/analysis_status_in_progress_response.json"));
+ pollingTries++;
+ } else {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write(readFile("/data/analysis_status_completed_response.json"));
+ pollingTries = 0;
+ }
+});
+
+// Path to test API call that will always need analysis.
+server.registerPathHandler(
+ new URL(API_ANALYSIS_IN_PROGRESS).pathname,
+ (request, response) => {
+ response.setHeader(
+ "Content-Type",
+ "application/json; charset=utf-8",
+ false
+ );
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write(readFile("/data/analysis_status_in_progress_response.json"));
+ }
+);
+
+let ohttp = Cc["@mozilla.org/network/oblivious-http;1"].getService(
+ Ci.nsIObliviousHttp
+);
+let ohttpServer = ohttp.server();
+
+server.registerPathHandler(
+ new URL(API_OHTTP_CONFIG).pathname,
+ (request, response) => {
+ let bstream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(
+ Ci.nsIBinaryOutputStream
+ );
+ bstream.setOutputStream(response.bodyOutputStream);
+ bstream.writeByteArray(ohttpServer.encodedConfig);
+ }
+);
+
+let gExpectedOHTTPMethod = "POST";
+let gExpectedProductDetails;
+server.registerPathHandler(
+ new URL(API_OHTTP_RELAY).pathname,
+ async (request, response) => {
+ let inputStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
+ Ci.nsIBinaryInputStream
+ );
+ inputStream.setInputStream(request.bodyInputStream);
+ let requestBody = inputStream.readByteArray(inputStream.available());
+ let ohttpRequest = ohttpServer.decapsulate(requestBody);
+ let bhttp = Cc["@mozilla.org/network/binary-http;1"].getService(
+ Ci.nsIBinaryHttp
+ );
+ let decodedRequest = bhttp.decodeRequest(ohttpRequest.request);
+ Assert.equal(
+ decodedRequest.method,
+ gExpectedOHTTPMethod,
+ "Should get expected HTTP method"
+ );
+ Assert.deepEqual(decodedRequest.headerNames.sort(), [
+ "Accept",
+ "Content-Type",
+ ]);
+ Assert.deepEqual(decodedRequest.headerValues, [
+ "application/json",
+ "application/json",
+ ]);
+ if (gExpectedOHTTPMethod == "POST") {
+ Assert.equal(
+ new TextDecoder().decode(new Uint8Array(decodedRequest.content)),
+ gExpectedProductDetails,
+ "Expected body content."
+ );
+ }
+
+ response.processAsync();
+ let innerResponse = await fetch("http://example.com" + decodedRequest.path);
+ let bytes = new Uint8Array(await innerResponse.arrayBuffer());
+ let binaryResponse = new BinaryHttpResponse(
+ innerResponse.status,
+ ["Content-Type"],
+ ["application/json"],
+ bytes
+ );
+ let encResponse = ohttpRequest.encapsulate(
+ bhttp.encodeResponse(binaryResponse)
+ );
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "message/ohttp-res", false);
+
+ let bstream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(
+ Ci.nsIBinaryOutputStream
+ );
+ bstream.setOutputStream(response.bodyOutputStream);
+ bstream.writeByteArray(encResponse);
+ response.finish();
+ }
+);
+
+add_task(async function test_product_requestAnalysis() {
+ let uri = new URL("https://www.walmart.com/ip/926485654");
+ let product = new ShoppingProduct(uri, { allowValidationFailure: false });
+
+ Assert.ok(product.isProduct(), "Should recognize a valid product.");
+
+ let analysis = await product.requestAnalysis(undefined, {
+ url: ANALYSIS_API_MOCK,
+ requestSchema: ANALYSIS_REQUEST_SCHEMA,
+ responseSchema: ANALYSIS_RESPONSE_SCHEMA,
+ });
+
+ Assert.ok(
+ typeof analysis == "object",
+ "Analysis object is loaded from JSON and validated"
+ );
+});
+
+add_task(async function test_product_requestAnalysis_OHTTP() {
+ let uri = new URL("https://www.walmart.com/ip/926485654");
+ let product = new ShoppingProduct(uri, { allowValidationFailure: false });
+
+ Assert.ok(product.isProduct(), "Should recognize a valid product.");
+
+ gExpectedProductDetails = JSON.stringify({
+ product_id: "926485654",
+ website: "walmart.com",
+ });
+
+ enableOHTTP();
+
+ let analysis = await product.requestAnalysis(undefined, {
+ url: ANALYSIS_API_MOCK,
+ requestSchema: ANALYSIS_REQUEST_SCHEMA,
+ responseSchema: ANALYSIS_RESPONSE_SCHEMA,
+ });
+
+ Assert.deepEqual(
+ analysis,
+ await fetch(ANALYSIS_API_MOCK).then(r => r.json()),
+ "Analysis object is loaded from JSON and validated"
+ );
+
+ disableOHTTP();
+});
+
+add_task(async function test_product_requestAnalysis_invalid() {
+ let uri = new URL("https://www.walmart.com/ip/926485654");
+ let product = new ShoppingProduct(uri, { allowValidationFailure: false });
+
+ Assert.ok(product.isProduct(), "Should recognize a valid product.");
+ let analysis = await product.requestAnalysis(undefined, {
+ url: ANALYSIS_API_MOCK_INVALID,
+ requestSchema: ANALYSIS_REQUEST_SCHEMA,
+ responseSchema: ANALYSIS_RESPONSE_SCHEMA,
+ });
+
+ Assert.equal(analysis, undefined, "Analysis object is invalidated");
+});
+
+add_task(async function test_product_requestAnalysis_invalid_allowed() {
+ let uri = new URL("https://www.walmart.com/ip/926485654");
+ let product = new ShoppingProduct(uri, { allowValidationFailure: true });
+
+ Assert.ok(product.isProduct(), "Should recognize a valid product.");
+ let analysis = await product.requestAnalysis(undefined, {
+ url: ANALYSIS_API_MOCK_INVALID,
+ requestSchema: ANALYSIS_REQUEST_SCHEMA,
+ responseSchema: ANALYSIS_RESPONSE_SCHEMA,
+ });
+
+ Assert.equal(analysis.grade, 0.85, "Analysis is invalid but allowed");
+});
+
+add_task(async function test_product_requestAnalysis_broken_config() {
+ let uri = new URL("https://www.walmart.com/ip/926485654");
+ let product = new ShoppingProduct(uri, { allowValidationFailure: false });
+
+ Assert.ok(product.isProduct(), "Should recognize a valid product.");
+
+ gExpectedProductDetails = JSON.stringify({
+ product_id: "926485654",
+ website: "walmart.com",
+ });
+
+ enableOHTTP("http://example.com/thisdoesntexist");
+
+ let analysis = await product.requestAnalysis(undefined, {
+ url: ANALYSIS_API_MOCK,
+ requestSchema: ANALYSIS_REQUEST_SCHEMA,
+ responseSchema: ANALYSIS_RESPONSE_SCHEMA,
+ });
+
+ // Because the config is missing, the OHTTP request can't be constructed,
+ // so we should fail.
+ Assert.equal(analysis, undefined, "Analysis object is invalidated");
+
+ disableOHTTP();
+});
+
+add_task(async function test_product_requestAnalysis_invalid_ohttp() {
+ let uri = new URL("https://www.walmart.com/ip/926485654");
+ let product = new ShoppingProduct(uri, { allowValidationFailure: false });
+
+ Assert.ok(product.isProduct(), "Should recognize a valid product.");
+
+ gExpectedProductDetails = JSON.stringify({
+ product_id: "926485654",
+ website: "walmart.com",
+ });
+
+ enableOHTTP();
+
+ let analysis = await product.requestAnalysis(undefined, {
+ url: ANALYSIS_API_MOCK_INVALID,
+ requestSchema: ANALYSIS_REQUEST_SCHEMA,
+ responseSchema: ANALYSIS_RESPONSE_SCHEMA,
+ });
+
+ Assert.equal(analysis, undefined, "Analysis object is invalidated");
+
+ disableOHTTP();
+});
+
+add_task(async function test_product_requestAnalysis_invalid_allowed_ohttp() {
+ let uri = new URL("https://www.walmart.com/ip/926485654");
+ let product = new ShoppingProduct(uri, { allowValidationFailure: true });
+
+ Assert.ok(product.isProduct(), "Should recognize a valid product.");
+
+ gExpectedProductDetails = JSON.stringify({
+ product_id: "926485654",
+ website: "walmart.com",
+ });
+
+ enableOHTTP();
+
+ let analysis = await product.requestAnalysis(undefined, {
+ url: ANALYSIS_API_MOCK_INVALID,
+ requestSchema: ANALYSIS_REQUEST_SCHEMA,
+ responseSchema: ANALYSIS_RESPONSE_SCHEMA,
+ });
+
+ Assert.equal(analysis.grade, 0.85, "Analysis is invalid but allowed");
+
+ disableOHTTP();
+});
+
+add_task(async function test_product_requestRecommendations() {
+ let uri = new URL("https://www.walmart.com/ip/926485654");
+ let product = new ShoppingProduct(uri, { allowValidationFailure: false });
+ if (product.isProduct()) {
+ let recommendations = await product.requestRecommendations(undefined, {
+ url: RECOMMENDATIONS_API_MOCK,
+ requestSchema: RECOMMENDATIONS_REQUEST_SCHEMA,
+ responseSchema: RECOMMENDATIONS_RESPONSE_SCHEMA,
+ });
+ Assert.ok(
+ Array.isArray(recommendations),
+ "Recommendations array is loaded from JSON and validated"
+ );
+ }
+});
+
+add_task(async function test_product_requestAnalysis_retry_failure() {
+ const TEST_TIMEOUT = 100;
+ const RETRIES = 3;
+ let uri = new URL("https://www.walmart.com/ip/926485654");
+ let product = new ShoppingProduct(uri, { allowValidationFailure: false });
+ let sandbox = sinon.createSandbox();
+ let spy = sandbox.spy(ShoppingProduct, "request");
+ let startTime = Cu.now();
+ let totalTime = TEST_TIMEOUT * Math.pow(2, RETRIES - 1);
+
+ if (product.isProduct()) {
+ let analysis = await product.requestAnalysis(undefined, {
+ url: API_SERVICE_UNAVAILABLE,
+ requestSchema: ANALYSIS_REQUEST_SCHEMA,
+ responseSchema: ANALYSIS_RESPONSE_SCHEMA,
+ });
+ Assert.equal(analysis, null, "Analysis object is null");
+ Assert.equal(
+ spy.callCount,
+ RETRIES + 1,
+ `Request was retried ${RETRIES} times after a failure`
+ );
+ Assert.ok(
+ Cu.now() - startTime >= totalTime,
+ `Waited for at least ${totalTime}ms`
+ );
+ }
+ sandbox.restore();
+});
+
+add_task(async function test_product_requestAnalysis_retry_success() {
+ let uri = new URL("https://www.walmart.com/ip/926485654");
+ let product = new ShoppingProduct(uri, { allowValidationFailure: false });
+ let sandbox = sinon.createSandbox();
+ let spy = sandbox.spy(ShoppingProduct, "request");
+ // Make sure API error count is reset
+ apiErrors = 0;
+ if (product.isProduct()) {
+ let analysis = await product.requestAnalysis(undefined, {
+ url: API_ERROR_ONCE,
+ requestSchema: ANALYSIS_REQUEST_SCHEMA,
+ responseSchema: ANALYSIS_RESPONSE_SCHEMA,
+ });
+ Assert.equal(spy.callCount, 2, `Request succeeded after a failure`);
+ Assert.ok(
+ typeof analysis == "object",
+ "Analysis object is loaded from JSON and validated"
+ );
+ }
+ sandbox.restore();
+});
+
+add_task(async function test_product_bad_request() {
+ let uri = new URL("https://www.walmart.com/ip/926485654");
+ let product = new ShoppingProduct(uri, { allowValidationFailure: false });
+
+ if (product.isProduct()) {
+ let errorResult = await product.requestAnalysis(undefined, {
+ url: API_ERROR_BAD_REQUEST,
+ requestSchema: ANALYSIS_REQUEST_SCHEMA,
+ responseSchema: ANALYSIS_RESPONSE_SCHEMA,
+ });
+ Assert.ok(
+ typeof errorResult == "object",
+ "Error object is loaded from JSON"
+ );
+ Assert.equal(errorResult.status, 400, "Error status is passed");
+ Assert.equal(errorResult.error, "Bad Request", "Error message is passed");
+ }
+});
+
+add_task(async function test_product_unprocessable_entity() {
+ let uri = new URL("https://www.walmart.com/ip/926485654");
+ let product = new ShoppingProduct(uri, { allowValidationFailure: false });
+
+ if (product.isProduct()) {
+ let errorResult = await product.requestAnalysis(undefined, {
+ url: API_ERROR_UNPROCESSABLE,
+ requestSchema: ANALYSIS_REQUEST_SCHEMA,
+ responseSchema: ANALYSIS_RESPONSE_SCHEMA,
+ });
+ Assert.ok(
+ typeof errorResult == "object",
+ "Error object is loaded from JSON"
+ );
+ Assert.equal(errorResult.status, 422, "Error status is passed");
+ Assert.equal(
+ errorResult.error,
+ "Unprocessable entity",
+ "Error message is passed"
+ );
+ }
+});
+
+add_task(async function test_ohttp_headers() {
+ let uri = new URL("https://www.walmart.com/ip/926485654");
+ let product = new ShoppingProduct(uri, { allowValidationFailure: false });
+
+ Assert.ok(product.isProduct(), "Should recognize a valid product.");
+
+ gExpectedProductDetails = JSON.stringify({
+ product_id: "926485654",
+ website: "walmart.com",
+ });
+
+ enableOHTTP();
+
+ let configURL = Services.prefs.getCharPref("toolkit.shopping.ohttpConfigURL");
+ let config = await ShoppingProduct.getOHTTPConfig(configURL);
+ Assert.ok(config, "Should have gotten a config.");
+ let ohttpDetails = await ShoppingProduct.ohttpRequest(
+ API_OHTTP_RELAY,
+ config,
+ ANALYSIS_API_MOCK,
+ {
+ method: "POST",
+ body: gExpectedProductDetails,
+ headers: {
+ Accept: "application/json",
+ "Content-Type": "application/json",
+ },
+ signal: new AbortController().signal,
+ }
+ );
+ Assert.equal(ohttpDetails.status, 200, "Request should return 200 OK.");
+ Assert.ok(ohttpDetails.ok, "Request should succeed.");
+ let responseHeaders = ohttpDetails.headers;
+ Assert.deepEqual(
+ responseHeaders,
+ { "content-type": "application/json" },
+ "Should have expected response headers."
+ );
+ disableOHTTP();
+});
+
+add_task(async function test_ohttp_too_many_requests() {
+ let uri = new URL("https://www.walmart.com/ip/926485654");
+ let product = new ShoppingProduct(uri, { allowValidationFailure: false });
+
+ Assert.ok(product.isProduct(), "Should recognize a valid product.");
+
+ gExpectedProductDetails = JSON.stringify({
+ product_id: "926485654",
+ website: "walmart.com",
+ });
+
+ enableOHTTP();
+
+ let configURL = Services.prefs.getCharPref("toolkit.shopping.ohttpConfigURL");
+ let config = await ShoppingProduct.getOHTTPConfig(configURL);
+ Assert.ok(config, "Should have gotten a config.");
+ let ohttpDetails = await ShoppingProduct.ohttpRequest(
+ API_OHTTP_RELAY,
+ config,
+ API_ERROR_TOO_MANY_REQUESTS,
+ {
+ method: "POST",
+ body: gExpectedProductDetails,
+ headers: {
+ Accept: "application/json",
+ "Content-Type": "application/json",
+ },
+ signal: new AbortController().signal,
+ }
+ );
+ Assert.equal(ohttpDetails.status, 429, "Request should return 429.");
+ Assert.equal(ohttpDetails.ok, false, "Request should not be ok.");
+
+ disableOHTTP();
+});
+
+add_task(async function test_product_uninit() {
+ let product = new ShoppingProduct();
+
+ Assert.equal(
+ product._abortController.signal.aborted,
+ false,
+ "Abort signal is false"
+ );
+
+ product.uninit();
+
+ Assert.equal(
+ product._abortController.signal.aborted,
+ true,
+ "Abort signal is given after uninit"
+ );
+});
+
+add_task(async function test_product_sendAttributionEvent_impression() {
+ let uri = new URL("https://www.walmart.com/ip/926485654");
+ let product = new ShoppingProduct(uri, { allowValidationFailure: false });
+ if (product.isProduct()) {
+ let event = await ShoppingProduct.sendAttributionEvent(
+ "impression",
+ TEST_AID,
+ "firefox_toolkit_tests",
+ {
+ url: ATTRIBUTION_API_MOCK,
+ requestSchema: ATTRIBUTION_REQUEST_SCHEMA,
+ responseSchema: ATTRIBUTION_RESPONSE_SCHEMA,
+ }
+ );
+ Assert.deepEqual(
+ event,
+ await fetch(ATTRIBUTION_API_MOCK).then(r => r.json()),
+ "Events object is loaded from JSON and validated"
+ );
+ }
+});
+
+add_task(async function test_product_sendAttributionEvent_click() {
+ let uri = new URL("https://www.walmart.com/ip/926485654");
+ let product = new ShoppingProduct(uri, { allowValidationFailure: false });
+ if (product.isProduct()) {
+ let event = await ShoppingProduct.sendAttributionEvent(
+ "click",
+ TEST_AID,
+ "firefox_toolkit_tests",
+ {
+ url: ATTRIBUTION_API_MOCK,
+ requestSchema: ATTRIBUTION_REQUEST_SCHEMA,
+ responseSchema: ATTRIBUTION_RESPONSE_SCHEMA,
+ }
+ );
+ Assert.deepEqual(
+ event,
+ await fetch(ATTRIBUTION_API_MOCK).then(r => r.json()),
+ "Events object is loaded from JSON and validated"
+ );
+ }
+});
+
+add_task(async function test_product_sendAttributionEvent_impression_OHTTP() {
+ let uri = new URL("https://www.walmart.com/ip/926485654");
+ let product = new ShoppingProduct(uri, { allowValidationFailure: false });
+
+ Assert.ok(product.isProduct(), "Should recognize a valid product.");
+
+ gExpectedProductDetails = JSON.stringify({
+ event_source: "firefox_toolkit_tests",
+ event_name: "trusted_deals_impression",
+ aidvs: [TEST_AID],
+ });
+
+ enableOHTTP();
+
+ let event = await ShoppingProduct.sendAttributionEvent(
+ "impression",
+ TEST_AID,
+ "firefox_toolkit_tests",
+ {
+ url: ATTRIBUTION_API_MOCK,
+ requestSchema: ATTRIBUTION_REQUEST_SCHEMA,
+ responseSchema: ATTRIBUTION_RESPONSE_SCHEMA,
+ }
+ );
+
+ Assert.deepEqual(
+ event,
+ await fetch(ATTRIBUTION_API_MOCK).then(r => r.json()),
+ "Events object is loaded from JSON and validated"
+ );
+
+ disableOHTTP();
+});
+
+add_task(async function test_product_sendAttributionEvent_click_OHTTP() {
+ let uri = new URL("https://www.walmart.com/ip/926485654");
+ let product = new ShoppingProduct(uri, { allowValidationFailure: false });
+
+ Assert.ok(product.isProduct(), "Should recognize a valid product.");
+
+ gExpectedProductDetails = JSON.stringify({
+ event_source: "firefox_toolkit_tests",
+ event_name: "trusted_deals_link_clicked",
+ aid: TEST_AID,
+ });
+
+ enableOHTTP();
+
+ let event = await ShoppingProduct.sendAttributionEvent(
+ "click",
+ TEST_AID,
+ "firefox_toolkit_tests",
+ {
+ url: ATTRIBUTION_API_MOCK,
+ requestSchema: ATTRIBUTION_REQUEST_SCHEMA,
+ responseSchema: ATTRIBUTION_RESPONSE_SCHEMA,
+ }
+ );
+
+ Assert.deepEqual(
+ event,
+ await fetch(ATTRIBUTION_API_MOCK).then(r => r.json()),
+ "Events object is loaded from JSON and validated"
+ );
+
+ disableOHTTP();
+});
+
+add_task(async function test_product_sendAttributionEvent_placement_OHTTP() {
+ let uri = new URL("https://www.walmart.com/ip/926485654");
+ let product = new ShoppingProduct(uri, { allowValidationFailure: false });
+
+ Assert.ok(product.isProduct(), "Should recognize a valid product.");
+
+ gExpectedProductDetails = JSON.stringify({
+ event_source: "firefox_toolkit_tests",
+ event_name: "trusted_deals_placement",
+ aidvs: [TEST_AID],
+ });
+
+ enableOHTTP();
+
+ let event = await ShoppingProduct.sendAttributionEvent(
+ "placement",
+ TEST_AID,
+ "firefox_toolkit_tests",
+ {
+ url: ATTRIBUTION_API_MOCK,
+ requestSchema: ATTRIBUTION_REQUEST_SCHEMA,
+ responseSchema: ATTRIBUTION_RESPONSE_SCHEMA,
+ }
+ );
+
+ Assert.deepEqual(
+ event,
+ await fetch(ATTRIBUTION_API_MOCK).then(r => r.json()),
+ "Events object is loaded from JSON and validated"
+ );
+
+ disableOHTTP();
+});
+
+add_task(async function test_product_requestAnalysis_poll() {
+ let uri = new URL("https://www.walmart.com/ip/926485654");
+ let product = new ShoppingProduct(uri, { allowValidationFailure: false });
+ let sandbox = sinon.createSandbox();
+ let spy = sandbox.spy(ShoppingProduct, "request");
+ let startTime = Cu.now();
+ const INITIAL_TIMEOUT = 100;
+ const TIMEOUT = 50;
+ const TRIES = 10;
+ let totalTime = INITIAL_TIMEOUT + TIMEOUT;
+
+ pollingTries = 0;
+ if (!product.isProduct()) {
+ return;
+ }
+ let analysis = await product.pollForAnalysisCompleted({
+ url: API_POLL,
+ requestSchema: ANALYSIS_STATUS_REQUEST_SCHEMA,
+ responseSchema: ANALYSIS_STATUS_RESPONSE_SCHEMA,
+ pollInitialWait: INITIAL_TIMEOUT,
+ pollTimeout: TIMEOUT,
+ pollAttempts: TRIES,
+ });
+
+ Assert.equal(spy.callCount, 3, "Request is done processing");
+ Assert.ok(
+ typeof analysis == "object",
+ "Analysis object is loaded from JSON and validated"
+ );
+ Assert.equal(analysis.status, "completed", "Analysis is completed");
+ Assert.equal(analysis.progress, 100.0, "Progress is 100%");
+ Assert.ok(
+ Cu.now() - startTime >= totalTime,
+ `Waited for at least ${totalTime}ms`
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_product_requestAnalysis_poll_max() {
+ let uri = new URL("https://www.walmart.com/ip/926485654");
+ let product = new ShoppingProduct(uri, { allowValidationFailure: false });
+ let sandbox = sinon.createSandbox();
+ let spy = sandbox.spy(ShoppingProduct, "request");
+ let startTime = Cu.now();
+
+ const INITIAL_TIMEOUT = 100;
+ const TIMEOUT = 50;
+ const TRIES = 4;
+ let totalTime = INITIAL_TIMEOUT + TIMEOUT * 3;
+
+ pollingTries = 0;
+ if (!product.isProduct()) {
+ return;
+ }
+ let analysis = await product.pollForAnalysisCompleted({
+ url: API_ANALYSIS_IN_PROGRESS,
+ requestSchema: ANALYSIS_STATUS_REQUEST_SCHEMA,
+ responseSchema: ANALYSIS_STATUS_RESPONSE_SCHEMA,
+ pollInitialWait: INITIAL_TIMEOUT,
+ pollTimeout: TIMEOUT,
+ pollAttempts: TRIES,
+ });
+
+ Assert.equal(spy.callCount, TRIES, "Request is done processing");
+ Assert.ok(
+ typeof analysis == "object",
+ "Analysis object is loaded from JSON and validated"
+ );
+ Assert.equal(analysis.status, "in_progress", "Analysis not done");
+ Assert.ok(
+ Cu.now() - startTime >= totalTime,
+ `Waited for at least ${totalTime}ms`
+ );
+ sandbox.restore();
+});
+
+add_task(async function test_product_requestAnalysisCreationStatus() {
+ let uri = new URL("https://www.walmart.com/ip/926485654");
+ let product = new ShoppingProduct(uri, { allowValidationFailure: false });
+ if (!product.isProduct()) {
+ return;
+ }
+ let analysis = await product.requestAnalysisCreationStatus(undefined, {
+ url: API_ANALYSIS_IN_PROGRESS,
+ requestSchema: ANALYSIS_STATUS_REQUEST_SCHEMA,
+ responseSchema: ANALYSIS_STATUS_RESPONSE_SCHEMA,
+ });
+ Assert.ok(
+ typeof analysis == "object",
+ "Analysis object is loaded from JSON and validated"
+ );
+ Assert.equal(analysis.status, "in_progress", "Analysis is in progress");
+ Assert.equal(analysis.progress, 50.0, "Progress is 50%");
+});
+
+add_task(async function test_product_requestCreateAnalysis() {
+ let uri = new URL("https://www.walmart.com/ip/926485654");
+ let product = new ShoppingProduct(uri, { allowValidationFailure: false });
+ if (!product.isProduct()) {
+ return;
+ }
+ let analysis = await product.requestCreateAnalysis(undefined, {
+ url: ANALYZE_API_MOCK,
+ requestSchema: ANALYZE_REQUEST_SCHEMA,
+ responseSchema: ANALYZE_RESPONSE_SCHEMA,
+ });
+ Assert.ok(
+ typeof analysis == "object",
+ "Analyze object is loaded from JSON and validated"
+ );
+ Assert.equal(analysis.status, "pending", "Analysis is pending");
+});
+
+add_task(async function test_product_sendReport() {
+ let uri = new URL("https://www.walmart.com/ip/926485654");
+ let product = new ShoppingProduct(uri, { allowValidationFailure: false });
+
+ Assert.ok(product.isProduct(), "Should recognize a valid product.");
+
+ let report = await product.sendReport(undefined, {
+ url: REPORTING_API_MOCK,
+ });
+
+ Assert.ok(
+ typeof report == "object",
+ "Report object is loaded from JSON and validated"
+ );
+ Assert.equal(report.message, "report created", "Report is created.");
+});
+
+add_task(async function test_product_sendReport_OHTTP() {
+ let uri = new URL("https://www.walmart.com/ip/926485654");
+ let product = new ShoppingProduct(uri, { allowValidationFailure: false });
+
+ Assert.ok(product.isProduct(), "Should recognize a valid product.");
+
+ gExpectedProductDetails = JSON.stringify({
+ product_id: "926485654",
+ website: "walmart.com",
+ });
+
+ enableOHTTP();
+
+ let report = await product.sendReport(undefined, {
+ url: REPORTING_API_MOCK,
+ });
+
+ Assert.ok(
+ typeof report == "object",
+ "Report object is loaded from JSON and validated"
+ );
+ Assert.equal(report.message, "report created", "Report is created.");
+ disableOHTTP();
+});
+
+add_task(async function test_product_analysisProgress_event() {
+ let uri = new URL("https://www.walmart.com/ip/926485654");
+ let product = new ShoppingProduct(uri, { allowValidationFailure: false });
+
+ const INITIAL_TIMEOUT = 0;
+ const TIMEOUT = 0;
+ const TRIES = 1;
+
+ if (!product.isProduct()) {
+ return;
+ }
+
+ let analysisProgressEventData;
+ product.on("analysis-progress", (eventName, progress) => {
+ analysisProgressEventData = progress;
+ });
+
+ await product.pollForAnalysisCompleted({
+ url: API_ANALYSIS_IN_PROGRESS,
+ requestSchema: ANALYSIS_STATUS_REQUEST_SCHEMA,
+ responseSchema: ANALYSIS_STATUS_RESPONSE_SCHEMA,
+ pollInitialWait: INITIAL_TIMEOUT,
+ pollTimeout: TIMEOUT,
+ pollAttempts: TRIES,
+ });
+
+ Assert.equal(
+ analysisProgressEventData,
+ 50,
+ "Analysis progress event data is emitted"
+ );
+});
diff --git a/toolkit/components/shopping/test/xpcshell/test_product_urls.js b/toolkit/components/shopping/test/xpcshell/test_product_urls.js
new file mode 100644
index 0000000000..ea3fc6da71
--- /dev/null
+++ b/toolkit/components/shopping/test/xpcshell/test_product_urls.js
@@ -0,0 +1,297 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { ShoppingProduct, isProductURL } = ChromeUtils.importESModule(
+ "chrome://global/content/shopping/ShoppingProduct.mjs"
+);
+
+add_task(function test_product_fromUrl() {
+ Assert.deepEqual(
+ ShoppingProduct.fromURL(),
+ { valid: false },
+ "Passing a nothing returns empty result object"
+ );
+
+ Assert.deepEqual(
+ ShoppingProduct.fromURL(12345),
+ { valid: false },
+ "Passing a number returns empty result object"
+ );
+
+ Assert.deepEqual(
+ ShoppingProduct.fromURL("https://www.walmart.com/ip/926485654"),
+ { valid: false },
+ "String urls returns empty result object"
+ );
+
+ Assert.deepEqual(
+ ShoppingProduct.fromURL(new URL("https://www.mozilla.org")),
+ { host: "mozilla.org", valid: false },
+ "Invalid Url returns a full result object"
+ );
+
+ Assert.deepEqual(
+ ShoppingProduct.fromURL(new URL("https://www.walmart.com/ip/926485654"))
+ .host,
+ "walmart.com",
+ "WWW in host is ignored"
+ );
+
+ Assert.deepEqual(
+ ShoppingProduct.fromURL(
+ new URL("https://staging.walmart.com/ip/926485654")
+ ),
+ { host: "staging.walmart.com", valid: false },
+ "Subdomain in valid Product Url returns partial result"
+ );
+
+ Assert.deepEqual(
+ ShoppingProduct.fromURL(new URL("https://walmart.co.uk/ip/926485654")),
+ { host: "walmart.co.uk", sitename: "walmart", valid: false },
+ "Invalid in Product TLD returns partial result"
+ );
+
+ Assert.deepEqual(
+ ShoppingProduct.fromURL(new URL("https://walmart.com")),
+ { host: "walmart.com", sitename: "walmart", tld: "com", valid: false },
+ "Non-Product page returns partial result"
+ );
+
+ Assert.deepEqual(
+ ShoppingProduct.fromURL(new URL("https://walmart.com/ip/926485654")),
+ {
+ host: "walmart.com",
+ sitename: "walmart",
+ tld: "com",
+ id: "926485654",
+ valid: true,
+ },
+ "Valid Product Url returns a full result object"
+ );
+
+ Assert.deepEqual(
+ ShoppingProduct.fromURL(new URL("http://walmart.com/ip/926485654")),
+ {
+ host: "walmart.com",
+ sitename: "walmart",
+ tld: "com",
+ id: "926485654",
+ valid: true,
+ },
+ "Protocol is not checked"
+ );
+
+ Assert.deepEqual(
+ ShoppingProduct.fromURL(new URL("https://amazon.fr/product/dp/ABCDEFG123")),
+ {
+ host: "amazon.fr",
+ sitename: "amazon",
+ tld: "fr",
+ id: "ABCDEFG123",
+ valid: true,
+ },
+ "Valid French Product Url returns a full result object"
+ );
+
+ Assert.deepEqual(
+ ShoppingProduct.fromURL(new URL("https://amazon.de/product/dp/ABCDEFG123")),
+ {
+ host: "amazon.de",
+ sitename: "amazon",
+ tld: "de",
+ id: "ABCDEFG123",
+ valid: true,
+ },
+ "Valid German Product Url returns a full result object"
+ );
+});
+
+add_task(function test_product_isProduct() {
+ let product = {
+ host: "walmart.com",
+ sitename: "walmart",
+ tld: "com",
+ id: "926485654",
+ valid: true,
+ };
+ Assert.equal(
+ ShoppingProduct.isProduct(product),
+ true,
+ "Passing a Product object returns true"
+ );
+ Assert.equal(
+ ShoppingProduct.isProduct({ host: "walmart.com", sitename: "walmart" }),
+ false,
+ "Passing an incomplete ShoppingProduct object returns false"
+ );
+ Assert.equal(
+ ShoppingProduct.isProduct(),
+ false,
+ "Passing nothing returns false"
+ );
+});
+
+add_task(function test_amazon_product_urls() {
+ let product;
+ let url_com = new URL(
+ "https://www.amazon.com/Furmax-Electric-Adjustable-Standing-Computer/dp/B09TJGHL5F/"
+ );
+ let url_ca = new URL(
+ "https://www.amazon.ca/JBL-Flip-Essential-Waterproof-Bluetooth/dp/B0C3NNGWFN/"
+ );
+ let url_uk = new URL(
+ "https://www.amazon.co.uk/placeholder_title/dp/B0B8KGPHS7/"
+ );
+ let url_content = new URL("https://www.amazon.com/stores/node/20648519011");
+
+ product = ShoppingProduct.fromURL(url_com);
+ Assert.equal(ShoppingProduct.isProduct(product), true, "Url is a product");
+ Assert.equal(product.id, "B09TJGHL5F", "Product id was found in Url");
+
+ product = ShoppingProduct.fromURL(url_ca);
+ Assert.equal(
+ ShoppingProduct.isProduct(product),
+ false,
+ "Url is not a supported tld"
+ );
+
+ product = ShoppingProduct.fromURL(url_uk);
+ Assert.equal(
+ ShoppingProduct.isProduct(product),
+ false,
+ "Url is not a supported tld"
+ );
+
+ product = ShoppingProduct.fromURL(url_content);
+ Assert.equal(
+ ShoppingProduct.isProduct(product),
+ false,
+ "Url is not a product"
+ );
+});
+
+add_task(function test_walmart_product_urls() {
+ let product;
+ let url_com = new URL(
+ "https://www.walmart.com/ip/Kent-Bicycles-29-Men-s-Trouvaille-Mountain-Bike-Medium-Black-and-Taupe/823391155"
+ );
+ let url_ca = new URL(
+ "https://www.walmart.ca/en/ip/cherries-jumbo/6000187473587"
+ );
+ let url_content = new URL(
+ "https://www.walmart.com/browse/food/grilling-foods/976759_1567409_8808777"
+ );
+
+ product = ShoppingProduct.fromURL(url_com);
+ Assert.equal(ShoppingProduct.isProduct(product), true, "Url is a product");
+ Assert.equal(product.id, "823391155", "Product id was found in Url");
+
+ product = ShoppingProduct.fromURL(url_ca);
+ Assert.equal(
+ ShoppingProduct.isProduct(product),
+ false,
+ "Url is not a valid tld"
+ );
+
+ product = ShoppingProduct.fromURL(url_content);
+ Assert.equal(
+ ShoppingProduct.isProduct(product),
+ false,
+ "Url is not a product"
+ );
+});
+
+add_task(function test_bestbuy_product_urls() {
+ let product;
+ let url_com = new URL(
+ "https://www.bestbuy.com/site/ge-profile-ultrafast-4-8-cu-ft-large-capacity-all-in-one-washer-dryer-combo-with-ventless-heat-pump-technology-carbon-graphite/6530134.p?skuId=6530134"
+ );
+ let url_ca = new URL(
+ "https://www.bestbuy.ca/en-ca/product/segway-ninebot-kickscooter-f40-electric-scooter-40km-range-30km-h-top-speed-dark-grey/15973012"
+ );
+ let url_content = new URL(
+ "https://www.bestbuy.com/site/home-appliances/major-appliances-sale-event/pcmcat321600050000.c"
+ );
+
+ product = ShoppingProduct.fromURL(url_com);
+ Assert.equal(ShoppingProduct.isProduct(product), true, "Url is a product");
+ Assert.equal(product.id, "6530134.p", "Product id was found in Url");
+
+ product = ShoppingProduct.fromURL(url_ca);
+ Assert.equal(
+ ShoppingProduct.isProduct(product),
+ false,
+ "Url is not a valid tld"
+ );
+
+ product = ShoppingProduct.fromURL(url_content);
+ Assert.equal(
+ ShoppingProduct.isProduct(product),
+ false,
+ "Url is not a product"
+ );
+});
+
+add_task(function test_isProductURL() {
+ let product_string =
+ "https://www.amazon.com/Furmax-Electric-Adjustable-Standing-Computer/dp/B09TJGHL5F/";
+ let product_url = new URL(product_string);
+ let product_uri = Services.io.newURI(product_string);
+ Assert.equal(
+ isProductURL(product_url),
+ true,
+ "Passing a product URL returns true"
+ );
+ Assert.equal(
+ isProductURL(product_uri),
+ true,
+ "Passing a product URI returns true"
+ );
+
+ let content_string =
+ "https://www.walmart.com/browse/food/grilling-foods/976759_1567409_8808777";
+ let content_url = new URL(content_string);
+ let content_uri = Services.io.newURI(content_string);
+ Assert.equal(
+ isProductURL(content_url),
+ false,
+ "Passing a content URL returns false"
+ );
+ Assert.equal(
+ isProductURL(content_uri),
+ false,
+ "Passing a content URI returns false"
+ );
+
+ Assert.equal(isProductURL(), false, "Passing nothing returns false");
+
+ Assert.equal(isProductURL(1234), false, "Passing a number returns false");
+
+ Assert.equal(
+ isProductURL("1234"),
+ false,
+ "Passing a junk string returns false"
+ );
+});
+
+add_task(function test_new_ShoppingProduct() {
+ let product_string =
+ "https://www.amazon.com/Furmax-Electric-Adjustable-Standing-Computer/dp/B09TJGHL5F/";
+ let product_url = new URL(product_string);
+ let product_uri = Services.io.newURI(product_string);
+ let productURL = new ShoppingProduct(product_url);
+ Assert.equal(
+ productURL.isProduct(),
+ true,
+ "Passing a product URL returns a valid product"
+ );
+ let productURI = new ShoppingProduct(product_uri);
+ Assert.equal(
+ productURI.isProduct(),
+ true,
+ "Passing a product URI returns a valid product"
+ );
+});
diff --git a/toolkit/components/shopping/test/xpcshell/test_product_validator.js b/toolkit/components/shopping/test/xpcshell/test_product_validator.js
new file mode 100644
index 0000000000..5aab07dbf5
--- /dev/null
+++ b/toolkit/components/shopping/test/xpcshell/test_product_validator.js
@@ -0,0 +1,91 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+/* global loadJSONfromFile */
+
+const {
+ ANALYSIS_RESPONSE_SCHEMA,
+ ANALYSIS_REQUEST_SCHEMA,
+ RECOMMENDATIONS_RESPONSE_SCHEMA,
+ RECOMMENDATIONS_REQUEST_SCHEMA,
+} = ChromeUtils.importESModule(
+ "chrome://global/content/shopping/ProductConfig.mjs"
+);
+
+const { ProductValidator } = ChromeUtils.importESModule(
+ "chrome://global/content/shopping/ProductValidator.sys.mjs"
+);
+
+add_task(async function test_validate_analysis() {
+ const json = await loadJSONfromFile("data/analysis_response.json");
+ let valid = await ProductValidator.validate(json, ANALYSIS_RESPONSE_SCHEMA);
+
+ Assert.equal(valid, true, "Analysis JSON is valid");
+});
+
+add_task(async function test_validate_analysis_invalid() {
+ const json = await loadJSONfromFile("data/invalid_analysis_response.json");
+ let valid = await ProductValidator.validate(json, ANALYSIS_RESPONSE_SCHEMA);
+
+ Assert.equal(valid, false, "Analysis JSON is invalid");
+});
+
+add_task(async function test_validate_recommendations() {
+ const json = await loadJSONfromFile("data/recommendations_response.json");
+ let valid = await ProductValidator.validate(
+ json,
+ RECOMMENDATIONS_RESPONSE_SCHEMA
+ );
+
+ Assert.equal(valid, true, "Recommendations JSON is valid");
+});
+
+add_task(async function test_validate_recommendations_invalid() {
+ const json = await loadJSONfromFile(
+ "data/invalid_recommendations_response.json"
+ );
+ let valid = await ProductValidator.validate(
+ json,
+ RECOMMENDATIONS_RESPONSE_SCHEMA
+ );
+
+ Assert.equal(valid, false, "Recommendations JSON is invalid");
+});
+
+add_task(async function test_validate_analysis() {
+ const json = await loadJSONfromFile("data/analysis_request.json");
+ let valid = await ProductValidator.validate(json, ANALYSIS_REQUEST_SCHEMA);
+
+ Assert.equal(valid, true, "Analysis JSON is valid");
+});
+
+add_task(async function test_validate_analysis_invalid() {
+ const json = await loadJSONfromFile("data/invalid_analysis_request.json");
+ let valid = await ProductValidator.validate(json, ANALYSIS_REQUEST_SCHEMA);
+
+ Assert.equal(valid, false, "Analysis JSON is invalid");
+});
+
+add_task(async function test_validate_recommendations() {
+ const json = await loadJSONfromFile("data/recommendations_request.json");
+ let valid = await ProductValidator.validate(
+ json,
+ RECOMMENDATIONS_REQUEST_SCHEMA
+ );
+
+ Assert.equal(valid, true, "Recommendations JSON is valid");
+});
+
+add_task(async function test_validate_recommendations_invalid() {
+ const json = await loadJSONfromFile(
+ "data/invalid_recommendations_request.json"
+ );
+ let valid = await ProductValidator.validate(
+ json,
+ RECOMMENDATIONS_REQUEST_SCHEMA
+ );
+
+ Assert.equal(valid, false, "Recommendations JSON is invalid");
+});
diff --git a/toolkit/components/shopping/test/xpcshell/xpcshell.toml b/toolkit/components/shopping/test/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..a95d53dca3
--- /dev/null
+++ b/toolkit/components/shopping/test/xpcshell/xpcshell.toml
@@ -0,0 +1,38 @@
+[DEFAULT]
+head = "head.js"
+prefs = [
+ "toolkit.shopping.environment='test'",
+ "toolkit.shopping.ohttpConfigURL=''",
+ "toolkit.shopping.ohttpRelayURL=''",
+]
+
+support-files = [
+ "data/analysis_response.json",
+ "data/recommendations_response.json",
+ "data/invalid_analysis_response.json",
+ "data/invalid_recommendations_response.json",
+ "data/analysis_request.json",
+ "data/recommendations_request.json",
+ "data/invalid_analysis_request.json",
+ "data/invalid_recommendations_request.json",
+ "data/service_unavailable.json",
+ "data/bad_request.json",
+ "data/unprocessable_entity.json",
+ "data/needs_analysis_response.json",
+ "data/attribution_response.json",
+ "data/image.jpg",
+ "data/report_response.json",
+ "data/analysis_status_completed_response.json",
+ "data/analysis_status_in_progress_response.json",
+ "data/analysis_status_pending_response.json",
+ "data/analyze_pending.json",
+ "data/too_many_requests.json",
+]
+
+["test_fetchImage.js"]
+
+["test_product.js"]
+
+["test_product_urls.js"]
+
+["test_product_validator.js"]