diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /toolkit/components/shopping/test/xpcshell | |
parent | Initial commit. (diff) | |
download | firefox-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')
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 Binary files differnew file mode 100644 index 0000000000..78e77baed6 --- /dev/null +++ b/toolkit/components/shopping/test/xpcshell/data/image.jpg 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"] |