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 | |
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')
40 files changed, 2949 insertions, 0 deletions
diff --git a/toolkit/components/shopping/test/browser/browser.toml b/toolkit/components/shopping/test/browser/browser.toml new file mode 100644 index 0000000000..878080690d --- /dev/null +++ b/toolkit/components/shopping/test/browser/browser.toml @@ -0,0 +1,31 @@ +[DEFAULT] +prefs = [ + "browser.shopping.experience2023.enabled=true", + "browser.shopping.experience2023.optedIn=1", + # Disable the fakespot feature callouts to avoid interference. Individual tests + # that need them can re-enable them as needed. + "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features=false", + "toolkit.shopping.environment=test", + "browser.shopping.experience2023.autoOpen.enabled=false", + "browser.shopping.experience2023.autoOpen.userEnabled=true", +] +support-files = [ + "head.js", + "../mockapis/server_helper.js", + "../mockapis/analysis_status.sjs", + "../mockapis/analysis.sjs", + "../mockapis/analyze.sjs", + "../mockapis/attribution.sjs", + "../mockapis/recommendations.sjs", + "../mockapis/reporting.sjs", +] + +["browser_shopping_ad_not_available.js"] + +["browser_shopping_ads_test.js"] + +["browser_shopping_integration.js"] + +["browser_shopping_request_telemetry.js"] + +["browser_shopping_sidebar_messages.js"] diff --git a/toolkit/components/shopping/test/browser/browser_shopping_ad_not_available.js b/toolkit/components/shopping/test/browser/browser_shopping_ad_not_available.js new file mode 100644 index 0000000000..c4b9c69277 --- /dev/null +++ b/toolkit/components/shopping/test/browser/browser_shopping_ad_not_available.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(async function () { + await Services.fog.testFlushAllChildren(); + Services.fog.testResetFOG(); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["toolkit.shopping.ohttpRelayURL", ""], + ["toolkit.shopping.ohttpConfigURL", ""], + ["browser.shopping.experience2023.ads.enabled", true], + ["browser.shopping.experience2023.ads.userEnabled", true], + ], + }); +}); + +/** + * Check that we send the right telemetry if no ad is available. + */ +add_task(async function test_no_ad_available_telemetry() { + await BrowserTestUtils.withNewTab(OTHER_PRODUCT_TEST_URL, async browser => { + let sidebar = gBrowser.getPanel(browser).querySelector("shopping-sidebar"); + Assert.ok(sidebar, "Sidebar should exist"); + Assert.ok( + BrowserTestUtils.isVisible(sidebar), + "Sidebar should be visible." + ); + + info("Waiting for sidebar to update."); + await promiseSidebarAdsUpdated(sidebar, OTHER_PRODUCT_TEST_URL); + // Test the lack of ad was recorded by telemetry + await Services.fog.testFlushAllChildren(); + let noAdsAvailableEvents = + Glean.shopping.surfaceNoAdsAvailable.testGetValue(); + Assert.equal( + noAdsAvailableEvents?.length, + 1, + "Should have recorded lack of ads." + ); + let noAdsEvent = noAdsAvailableEvents?.[0]; + Assert.equal(noAdsEvent?.category, "shopping"); + Assert.equal(noAdsEvent?.name, "surface_no_ads_available"); + }); +}); diff --git a/toolkit/components/shopping/test/browser/browser_shopping_ads_test.js b/toolkit/components/shopping/test/browser/browser_shopping_ads_test.js new file mode 100644 index 0000000000..1061cf7fa6 --- /dev/null +++ b/toolkit/components/shopping/test/browser/browser_shopping_ads_test.js @@ -0,0 +1,236 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); + +function recommendedAdsEventListener(eventName, sidebar) { + return SpecialPowers.spawn( + sidebar.querySelector("browser"), + [eventName], + name => { + let shoppingContainer = + content.document.querySelector("shopping-container").wrappedJSObject; + let adEl = shoppingContainer.recommendedAdEl; + return ContentTaskUtils.waitForEvent(adEl, name, false, null, true).then( + ev => null + ); + } + ); +} + +function recommendedAdVisible(sidebar) { + return SpecialPowers.spawn(sidebar.querySelector("browser"), [], async () => { + await ContentTaskUtils.waitForCondition(() => { + let shoppingContainer = + content.document.querySelector("shopping-container").wrappedJSObject; + return ( + shoppingContainer?.recommendedAdEl && + ContentTaskUtils.isVisible(shoppingContainer?.recommendedAdEl) + ); + }); + }); +} + +add_setup(async function () { + await Services.fog.testFlushAllChildren(); + Services.fog.testResetFOG(); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["toolkit.shopping.ohttpRelayURL", ""], + ["toolkit.shopping.ohttpConfigURL", ""], + ["browser.shopping.experience2023.ads.enabled", true], + ["browser.shopping.experience2023.ads.userEnabled", true], + ], + }); +}); + +add_task(async function test_ad_attribution() { + await BrowserTestUtils.withNewTab(PRODUCT_TEST_URL, async browser => { + // Test that impression event is fired when opening sidebar + let sidebar = gBrowser.getPanel(browser).querySelector("shopping-sidebar"); + Assert.ok(sidebar, "Sidebar should exist"); + Assert.ok( + BrowserTestUtils.isVisible(sidebar), + "Sidebar should be visible." + ); + let shoppingButton = document.getElementById("shopping-sidebar-button"); + ok( + BrowserTestUtils.isVisible(shoppingButton), + "Shopping Button should be visible on a product page" + ); + + info("Waiting for sidebar to update."); + await promiseSidebarUpdated(sidebar, PRODUCT_TEST_URL); + await recommendedAdVisible(sidebar); + + info("Verifying product info for initial product."); + await verifyProductInfo(sidebar, { + productURL: PRODUCT_TEST_URL, + adjustedRating: "4.1", + letterGrade: "B", + }); + + // Test placement was recorded by telemetry + info("Verifying ad placement event."); + await Services.fog.testFlushAllChildren(); + var adsPlacementEvents = Glean.shopping.surfaceAdsPlacement.testGetValue(); + Assert.equal(adsPlacementEvents.length, 1, "should have recorded an event"); + Assert.equal(adsPlacementEvents[0].category, "shopping"); + Assert.equal(adsPlacementEvents[0].name, "surface_ads_placement"); + + let impressionEvent = recommendedAdsEventListener("AdImpression", sidebar); + + info("Waiting for ad impression event."); + await impressionEvent; + Assert.ok(true, "Got ad impression event"); + + // Test the impression was recorded by telemetry + await Services.fog.testFlushAllChildren(); + var adsImpressionEvents = + Glean.shopping.surfaceAdsImpression.testGetValue(); + Assert.equal( + adsImpressionEvents.length, + 1, + "should have recorded an event" + ); + Assert.equal(adsImpressionEvents[0].category, "shopping"); + Assert.equal(adsImpressionEvents[0].name, "surface_ads_impression"); + + // + // Test that impression event is fired after switching to a tab that was + // opened in the background + + let tab = BrowserTestUtils.addTab(gBrowser, PRODUCT_TEST_URL); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + let tabSidebar = gBrowser + .getPanel(tab.linkedBrowser) + .querySelector("shopping-sidebar"); + Assert.ok(tabSidebar, "Sidebar should exist"); + + info("Waiting for sidebar to update."); + await promiseSidebarUpdated(tabSidebar, PRODUCT_TEST_URL); + await recommendedAdVisible(tabSidebar); + + // Need to wait the impression timeout to confirm that no impression event + // has been dispatched + // Bug 1859029 should update this to use sinon fake timers instead of using + // setTimeout + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(r => setTimeout(r, 2000)); + + let hasImpressed = await SpecialPowers.spawn( + tabSidebar.querySelector("browser"), + [], + () => { + let shoppingContainer = + content.document.querySelector("shopping-container").wrappedJSObject; + let adEl = shoppingContainer.recommendedAdEl; + return adEl.hasImpressed; + } + ); + Assert.ok(!hasImpressed, "We haven't seend the ad yet"); + + impressionEvent = recommendedAdsEventListener("AdImpression", tabSidebar); + await BrowserTestUtils.switchTab(gBrowser, tab); + await recommendedAdVisible(tabSidebar); + + info("Waiting for ad impression event."); + await impressionEvent; + Assert.ok(true, "Got ad impression event"); + + // + // Test that the impression event is fired after opening foreground tab, + // switching away and the event is not fired, then switching back and the + // event does fire + + gBrowser.removeTab(tab); + + tab = BrowserTestUtils.addTab(gBrowser, PRODUCT_TEST_URL); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + tabSidebar = gBrowser + .getPanel(tab.linkedBrowser) + .querySelector("shopping-sidebar"); + Assert.ok(tabSidebar, "Sidebar should exist"); + + info("Waiting for sidebar to update."); + await promiseSidebarUpdated(tabSidebar, PRODUCT_TEST_URL); + await recommendedAdVisible(tabSidebar); + + // Switch to new sidebar tab + await BrowserTestUtils.switchTab(gBrowser, tab); + // switch back to original tab + await BrowserTestUtils.switchTab( + gBrowser, + gBrowser.getTabForBrowser(browser) + ); + + // Need to wait the impression timeout to confirm that no impression event + // has been dispatched + // Bug 1859029 should update this to use sinon fake timers instead of using + // setTimeout + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(r => setTimeout(r, 2000)); + + hasImpressed = await SpecialPowers.spawn( + tabSidebar.querySelector("browser"), + [], + () => { + let shoppingContainer = + content.document.querySelector("shopping-container").wrappedJSObject; + let adEl = shoppingContainer.recommendedAdEl; + return adEl.hasImpressed; + } + ); + Assert.ok(!hasImpressed, "We haven't seend the ad yet"); + + impressionEvent = recommendedAdsEventListener("AdImpression", tabSidebar); + await BrowserTestUtils.switchTab(gBrowser, tab); + await recommendedAdVisible(tabSidebar); + + info("Waiting for ad impression event."); + await impressionEvent; + Assert.ok(true, "Got ad impression event"); + + gBrowser.removeTab(tab); + + // + // Test ad clicked event + + let adOpenedTabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + PRODUCT_TEST_URL, + true + ); + + let clickedEvent = recommendedAdsEventListener("AdClicked", sidebar); + await SpecialPowers.spawn(sidebar.querySelector("browser"), [], () => { + let shoppingContainer = + content.document.querySelector("shopping-container").wrappedJSObject; + let adEl = shoppingContainer.recommendedAdEl; + adEl.linkEl.click(); + }); + + let adTab = await adOpenedTabPromise; + + info("Waiting for ad clicked event."); + await clickedEvent; + Assert.ok(true, "Got ad clicked event"); + + // Test the click was recorded by telemetry + await Services.fog.testFlushAllChildren(); + var adsClickedEvents = Glean.shopping.surfaceAdsClicked.testGetValue(); + Assert.equal(adsClickedEvents.length, 1, "should have recorded a click"); + Assert.equal(adsClickedEvents[0].category, "shopping"); + Assert.equal(adsClickedEvents[0].name, "surface_ads_clicked"); + + gBrowser.removeTab(adTab); + Services.fog.testResetFOG(); + }); +}); diff --git a/toolkit/components/shopping/test/browser/browser_shopping_integration.js b/toolkit/components/shopping/test/browser/browser_shopping_integration.js new file mode 100644 index 0000000000..d16b021eb3 --- /dev/null +++ b/toolkit/components/shopping/test/browser/browser_shopping_integration.js @@ -0,0 +1,277 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_sidebar_navigation() { + // Disable OHTTP for now to get this landed; we'll re-enable with proper + // mocking in the near future. + await SpecialPowers.pushPrefEnv({ + set: [ + ["toolkit.shopping.ohttpRelayURL", ""], + ["toolkit.shopping.ohttpConfigURL", ""], + ], + }); + await BrowserTestUtils.withNewTab(PRODUCT_TEST_URL, async browser => { + let sidebar = gBrowser.getPanel(browser).querySelector("shopping-sidebar"); + Assert.ok(sidebar, "Sidebar should exist"); + Assert.ok( + BrowserTestUtils.isVisible(sidebar), + "Sidebar should be visible." + ); + info("Waiting for sidebar to update."); + await promiseSidebarUpdated(sidebar, PRODUCT_TEST_URL); + info("Verifying product info for initial product."); + await verifyProductInfo(sidebar, { + productURL: PRODUCT_TEST_URL, + adjustedRating: "4.1", + letterGrade: "B", + }); + + // Navigate the browser from the parent: + let loadedPromise = Promise.all([ + BrowserTestUtils.browserLoaded(browser, false, OTHER_PRODUCT_TEST_URL), + promiseSidebarUpdated(sidebar, OTHER_PRODUCT_TEST_URL), + ]); + BrowserTestUtils.startLoadingURIString(browser, OTHER_PRODUCT_TEST_URL); + info("Loading another product."); + await loadedPromise; + Assert.ok(sidebar, "Sidebar should exist."); + Assert.ok( + BrowserTestUtils.isVisible(sidebar), + "Sidebar should be visible." + ); + info("Verifying another product."); + await verifyProductInfo(sidebar, { + productURL: OTHER_PRODUCT_TEST_URL, + adjustedRating: "1", + letterGrade: "F", + }); + + // Navigate to a non-product URL: + loadedPromise = BrowserTestUtils.browserLoaded( + browser, + false, + "https://example.com/1" + ); + BrowserTestUtils.startLoadingURIString(browser, "https://example.com/1"); + info("Go to a non-product."); + await loadedPromise; + Assert.ok(BrowserTestUtils.isHidden(sidebar)); + + // Navigate using pushState: + loadedPromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + PRODUCT_TEST_URL + ); + info("Navigate to the first product using pushState."); + await SpecialPowers.spawn(browser, [PRODUCT_TEST_URL], urlToUse => { + content.history.pushState({}, null, urlToUse); + }); + info("Waiting to load first product again."); + await loadedPromise; + info("Waiting for the sidebar to have updated."); + await promiseSidebarUpdated(sidebar, PRODUCT_TEST_URL); + Assert.ok(sidebar, "Sidebar should exist"); + Assert.ok( + BrowserTestUtils.isVisible(sidebar), + "Sidebar should be visible." + ); + + info("Waiting to verify the first product a second time."); + await verifyProductInfo(sidebar, { + productURL: PRODUCT_TEST_URL, + adjustedRating: "4.1", + letterGrade: "B", + }); + + // Navigate to a product URL with query params: + loadedPromise = BrowserTestUtils.browserLoaded( + browser, + false, + PRODUCT_TEST_URL + "?th=1" + ); + // Navigate to the same product, but with a th=1 added. + BrowserTestUtils.startLoadingURIString(browser, PRODUCT_TEST_URL + "?th=1"); + // When just comparing URLs product info would be cleared out, + // but when comparing the parsed product ids, we do nothing as the product + // has not changed. + info("Verifying product has not changed before load."); + await verifyProductInfo(sidebar, { + productURL: PRODUCT_TEST_URL, + adjustedRating: "4.1", + letterGrade: "B", + }); + // Wait for the page to load, but don't wait for the sidebar to update so + // we can be sure we still have the previous product info. + await loadedPromise; + info("Verifying product has not changed after load."); + await verifyProductInfo(sidebar, { + productURL: PRODUCT_TEST_URL, + adjustedRating: "4.1", + letterGrade: "B", + }); + }); +}); + +add_task(async function test_button_visible_when_opted_out() { + await BrowserTestUtils.withNewTab( + { + url: PRODUCT_TEST_URL, + gBrowser, + }, + async browser => { + let shoppingBrowser = gBrowser.ownerDocument.querySelector( + "browser.shopping-sidebar" + ); + + let shoppingButton = document.getElementById("shopping-sidebar-button"); + + ok( + BrowserTestUtils.isVisible(shoppingButton), + "Shopping Button should be visible on a product page" + ); + + let sidebar = gBrowser + .getPanel(browser) + .querySelector("shopping-sidebar"); + Assert.ok(sidebar, "Sidebar should exist"); + Assert.ok( + BrowserTestUtils.isVisible(sidebar), + "Sidebar should be visible." + ); + info("Waiting for sidebar to update."); + await promiseSidebarUpdated(sidebar, PRODUCT_TEST_URL); + + await SpecialPowers.spawn(shoppingBrowser, [], async () => { + let shoppingContainer = + content.document.querySelector("shopping-container").wrappedJSObject; + await shoppingContainer.updateComplete; + let shoppingSettings = shoppingContainer.settingsEl; + await shoppingSettings.updateComplete; + + shoppingSettings.shoppingCardEl.detailsEl.open = true; + let optOutButton = shoppingSettings.optOutButtonEl; + optOutButton.click(); + }); + + await BrowserTestUtils.waitForMutationCondition( + shoppingButton, + { attributes: false, attributeFilter: ["shoppingsidebaropen"] }, + () => shoppingButton.getAttribute("shoppingsidebaropen") + ); + + ok( + !Services.prefs.getBoolPref("browser.shopping.experience2023.active"), + "Shopping sidebar is no longer active" + ); + is( + Services.prefs.getIntPref("browser.shopping.experience2023.optedIn"), + 2, + "Opted out of shopping experience" + ); + + ok( + BrowserTestUtils.isVisible(shoppingButton), + "Shopping Button should be visible after opting out" + ); + + Services.prefs.setBoolPref( + "browser.shopping.experience2023.active", + true + ); + Services.prefs.setIntPref("browser.shopping.experience2023.optedIn", 1); + } + ); +}); + +add_task(async function test_sidebar_button_open_close() { + // Disable OHTTP for now to get this landed; we'll re-enable with proper + // mocking in the near future. + await SpecialPowers.pushPrefEnv({ + set: [ + ["toolkit.shopping.ohttpRelayURL", ""], + ["toolkit.shopping.ohttpConfigURL", ""], + ], + }); + await BrowserTestUtils.withNewTab(PRODUCT_TEST_URL, async browser => { + let sidebar = gBrowser.getPanel(browser).querySelector("shopping-sidebar"); + Assert.ok(sidebar, "Sidebar should exist"); + Assert.ok( + BrowserTestUtils.isVisible(sidebar), + "Sidebar should be visible." + ); + let shoppingButton = document.getElementById("shopping-sidebar-button"); + ok( + BrowserTestUtils.isVisible(shoppingButton), + "Shopping Button should be visible on a product page" + ); + + info("Waiting for sidebar to update."); + await promiseSidebarUpdated(sidebar, PRODUCT_TEST_URL); + + info("Verifying product info for initial product."); + await verifyProductInfo(sidebar, { + productURL: PRODUCT_TEST_URL, + adjustedRating: "4.1", + letterGrade: "B", + }); + + // close the sidebar + shoppingButton.click(); + ok(BrowserTestUtils.isHidden(sidebar), "Sidebar should be hidden"); + + // reopen the sidebar + shoppingButton.click(); + Assert.ok( + BrowserTestUtils.isVisible(sidebar), + "Sidebar should be visible." + ); + + info("Waiting for sidebar to update."); + await promiseSidebarUpdated(sidebar, PRODUCT_TEST_URL); + + info("Verifying product info for has not changed."); + await verifyProductInfo(sidebar, { + productURL: PRODUCT_TEST_URL, + adjustedRating: "4.1", + letterGrade: "B", + }); + }); +}); + +add_task(async function test_no_reliability_available() { + Services.fog.testResetFOG(); + await Services.fog.testFlushAllChildren(); + // Disable OHTTP for now to get this landed; we'll re-enable with proper + // mocking in the near future. + await SpecialPowers.pushPrefEnv({ + set: [ + ["toolkit.shopping.ohttpRelayURL", ""], + ["toolkit.shopping.ohttpConfigURL", ""], + ], + }); + await BrowserTestUtils.withNewTab(NEEDS_ANALYSIS_TEST_URL, async browser => { + let sidebar = gBrowser.getPanel(browser).querySelector("shopping-sidebar"); + + Assert.ok(sidebar, "Sidebar should exist"); + + Assert.ok( + BrowserTestUtils.isVisible(sidebar), + "Sidebar should be visible." + ); + info("Waiting for sidebar to update."); + await promiseSidebarUpdated(sidebar, NEEDS_ANALYSIS_TEST_URL); + }); + + await Services.fog.testFlushAllChildren(); + var sawPageEvents = + Glean.shopping.surfaceNoReviewReliabilityAvailable.testGetValue(); + + Assert.equal(sawPageEvents.length, 1); + Assert.equal(sawPageEvents[0].category, "shopping"); + Assert.equal( + sawPageEvents[0].name, + "surface_no_review_reliability_available" + ); +}); diff --git a/toolkit/components/shopping/test/browser/browser_shopping_request_telemetry.js b/toolkit/components/shopping/test/browser/browser_shopping_request_telemetry.js new file mode 100644 index 0000000000..d4b5147e48 --- /dev/null +++ b/toolkit/components/shopping/test/browser/browser_shopping_request_telemetry.js @@ -0,0 +1,149 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const INVALID_RESPONSE = "https://example.com/Some-Product/dp/INVALID123"; +const SERVICE_UNAVAILABLE = "https://example.com/Some-Product/dp/HTTPERR503"; +const TOO_MANY_REQUESTS = "https://example.com/Some-Product/dp/HTTPERR429"; + +function assertEventMatches(gleanEvents, requiredValues) { + if (!gleanEvents?.length) { + return Assert.ok( + !!gleanEvents?.length, + `${requiredValues?.name} event recorded` + ); + } + let limitedEvent = Object.assign({}, gleanEvents[0]); + for (let k of Object.keys(limitedEvent)) { + if (!requiredValues.hasOwnProperty(k)) { + delete limitedEvent[k]; + } + } + return Assert.deepEqual(limitedEvent, requiredValues); +} + +async function testProductURL(url) { + await BrowserTestUtils.withNewTab( + { + url, + gBrowser, + }, + async browser => { + let sidebar = gBrowser + .getPanel(browser) + .querySelector("shopping-sidebar"); + + await promiseSidebarUpdated(sidebar, url); + } + ); +} + +add_task(async function test_shopping_server_failure_telemetry() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["toolkit.shopping.ohttpRelayURL", ""], + ["toolkit.shopping.ohttpConfigURL", ""], + ], + }); + Services.fog.testResetFOG(); + + await testProductURL(SERVICE_UNAVAILABLE); + + await Services.fog.testFlushAllChildren(); + + const events = Glean.shoppingProduct.serverFailure.testGetValue(); + assertEventMatches(events, { + category: "shopping_product", + name: "server_failure", + }); + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_shopping_request_failure_telemetry() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["toolkit.shopping.ohttpRelayURL", ""], + ["toolkit.shopping.ohttpConfigURL", ""], + ], + }); + Services.fog.testResetFOG(); + + await testProductURL(TOO_MANY_REQUESTS); + + await Services.fog.testFlushAllChildren(); + + const events = Glean.shoppingProduct.requestFailure.testGetValue(); + assertEventMatches(events, { + category: "shopping_product", + name: "request_failure", + }); + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_shopping_request_retried_telemetry() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["toolkit.shopping.ohttpRelayURL", ""], + ["toolkit.shopping.ohttpConfigURL", ""], + ], + }); + Services.fog.testResetFOG(); + + await testProductURL(SERVICE_UNAVAILABLE); + + await Services.fog.testFlushAllChildren(); + + const events = Glean.shoppingProduct.requestRetried.testGetValue(); + assertEventMatches(events, { + category: "shopping_product", + name: "request_retried", + }); + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_shopping_response_invalid_telemetry() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["toolkit.shopping.ohttpRelayURL", ""], + ["toolkit.shopping.ohttpConfigURL", ""], + ], + }); + Services.fog.testResetFOG(); + + await testProductURL(INVALID_RESPONSE); + + await Services.fog.testFlushAllChildren(); + const events = Glean.shoppingProduct.invalidResponse.testGetValue(); + assertEventMatches(events, { + category: "shopping_product", + name: "invalid_response", + }); + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_shopping_ohttp_invalid_telemetry() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["toolkit.shopping.ohttpRelayURL", "https://example.com/api"], + ["toolkit.shopping.ohttpConfigURL", "https://example.com/config"], + ], + }); + Services.fog.testResetFOG(); + + await testProductURL(PRODUCT_TEST_URL); + + await Services.fog.testFlushAllChildren(); + + const events = Glean.shoppingProduct.invalidOhttpConfig.testGetValue(); + assertEventMatches(events, { + category: "shopping_product", + name: "invalid_ohttp_config", + }); + + await SpecialPowers.popPrefEnv(); +}); diff --git a/toolkit/components/shopping/test/browser/browser_shopping_sidebar_messages.js b/toolkit/components/shopping/test/browser/browser_shopping_sidebar_messages.js new file mode 100644 index 0000000000..969b49481d --- /dev/null +++ b/toolkit/components/shopping/test/browser/browser_shopping_sidebar_messages.js @@ -0,0 +1,195 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const NOT_ENOUGH_REVIEWS_TEST_URL = + "https://example.com/Bad-Product/dp/N0T3NOUGHR"; +const NOT_SUPPORTED_TEST_URL = "https://example.com/Bad-Product/dp/PAG3N0TSUP"; +const UNPROCESSABLE_TEST_URL = "https://example.com/Bad-Product/dp/UNPR0C3SSA"; + +add_task(async function test_sidebar_error() { + // Disable OHTTP for now to get this landed; we'll re-enable with proper + // mocking in the near future. + await SpecialPowers.pushPrefEnv({ + set: [ + ["toolkit.shopping.ohttpRelayURL", ""], + ["toolkit.shopping.ohttpConfigURL", ""], + ], + }); + await BrowserTestUtils.withNewTab(BAD_PRODUCT_TEST_URL, async browser => { + let sidebar = gBrowser.getPanel(browser).querySelector("shopping-sidebar"); + + Assert.ok(sidebar, "Sidebar should exist"); + + Assert.ok( + BrowserTestUtils.isVisible(sidebar), + "Sidebar should be visible." + ); + info("Waiting for sidebar to update."); + await promiseSidebarUpdated(sidebar, BAD_PRODUCT_TEST_URL); + + info("Verifying a generic error is shown."); + await SpecialPowers.spawn( + sidebar.querySelector("browser"), + [], + async prodInfo => { + let doc = content.document; + let shoppingContainer = + doc.querySelector("shopping-container").wrappedJSObject; + + ok( + shoppingContainer.shoppingMessageBarEl, + "Got shopping-message-bar element" + ); + is( + shoppingContainer.shoppingMessageBarEl.getAttribute("type"), + "generic-error", + "generic-error type should be correct" + ); + } + ); + }); +}); + +add_task(async function test_sidebar_analysis_status_page_not_supported() { + // Disable OHTTP for now to get this landed; we'll re-enable with proper + // mocking in the near future. + await SpecialPowers.pushPrefEnv({ + set: [ + ["toolkit.shopping.ohttpRelayURL", ""], + ["toolkit.shopping.ohttpConfigURL", ""], + ], + }); + + // Product not supported status + await BrowserTestUtils.withNewTab(NOT_SUPPORTED_TEST_URL, async browser => { + let sidebar = gBrowser.getPanel(browser).querySelector("shopping-sidebar"); + + Assert.ok(sidebar, "Sidebar should exist"); + + Assert.ok( + BrowserTestUtils.isVisible(sidebar), + "Sidebar should be visible." + ); + info("Waiting for sidebar to update."); + await promiseSidebarUpdated(sidebar, NOT_SUPPORTED_TEST_URL); + + info("Verifying a generic error is shown."); + await SpecialPowers.spawn( + sidebar.querySelector("browser"), + [], + async prodInfo => { + let doc = content.document; + let shoppingContainer = + doc.querySelector("shopping-container").wrappedJSObject; + + ok( + shoppingContainer.shoppingMessageBarEl, + "Got shopping-message-bar element" + ); + is( + shoppingContainer.shoppingMessageBarEl.getAttribute("type"), + "page-not-supported", + "message type should be correct" + ); + } + ); + }); +}); + +add_task(async function test_sidebar_analysis_status_unprocessable() { + // Disable OHTTP for now to get this landed; we'll re-enable with proper + // mocking in the near future. + await SpecialPowers.pushPrefEnv({ + set: [ + ["toolkit.shopping.ohttpRelayURL", ""], + ["toolkit.shopping.ohttpConfigURL", ""], + ], + }); + + // Unprocessable status + await BrowserTestUtils.withNewTab(UNPROCESSABLE_TEST_URL, async browser => { + let sidebar = gBrowser.getPanel(browser).querySelector("shopping-sidebar"); + + Assert.ok(sidebar, "Sidebar should exist"); + + Assert.ok( + BrowserTestUtils.isVisible(sidebar), + "Sidebar should be visible." + ); + info("Waiting for sidebar to update."); + await promiseSidebarUpdated(sidebar, UNPROCESSABLE_TEST_URL); + + info("Verifying a generic error is shown."); + await SpecialPowers.spawn( + sidebar.querySelector("browser"), + [], + async prodInfo => { + let doc = content.document; + let shoppingContainer = + doc.querySelector("shopping-container").wrappedJSObject; + + ok( + shoppingContainer.shoppingMessageBarEl, + "Got shopping-message-bar element" + ); + is( + shoppingContainer.shoppingMessageBarEl.getAttribute("type"), + "generic-error", + "message type should be correct" + ); + } + ); + }); +}); + +add_task(async function test_sidebar_analysis_status_not_enough_reviews() { + // Disable OHTTP for now to get this landed; we'll re-enable with proper + // mocking in the near future. + await SpecialPowers.pushPrefEnv({ + set: [ + ["toolkit.shopping.ohttpRelayURL", ""], + ["toolkit.shopping.ohttpConfigURL", ""], + ], + }); + // Not enough reviews status + await BrowserTestUtils.withNewTab( + NOT_ENOUGH_REVIEWS_TEST_URL, + async browser => { + let sidebar = gBrowser + .getPanel(browser) + .querySelector("shopping-sidebar"); + + Assert.ok(sidebar, "Sidebar should exist"); + + Assert.ok( + BrowserTestUtils.isVisible(sidebar), + "Sidebar should be visible." + ); + info("Waiting for sidebar to update."); + await promiseSidebarUpdated(sidebar, NOT_ENOUGH_REVIEWS_TEST_URL); + + info("Verifying a generic error is shown."); + await SpecialPowers.spawn( + sidebar.querySelector("browser"), + [], + async prodInfo => { + let doc = content.document; + let shoppingContainer = + doc.querySelector("shopping-container").wrappedJSObject; + + ok( + shoppingContainer.shoppingMessageBarEl, + "Got shopping-message-bar element" + ); + is( + shoppingContainer.shoppingMessageBarEl.getAttribute("type"), + "not-enough-reviews", + "message type should be correct" + ); + } + ); + } + ); +}); diff --git a/toolkit/components/shopping/test/browser/head.js b/toolkit/components/shopping/test/browser/head.js new file mode 100644 index 0000000000..af676bbc33 --- /dev/null +++ b/toolkit/components/shopping/test/browser/head.js @@ -0,0 +1,106 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const PRODUCT_TEST_URL = "https://example.com/Some-Product/dp/ABCDEFG123"; +const OTHER_PRODUCT_TEST_URL = + "https://example.com/Another-Product/dp/HIJKLMN456"; +const BAD_PRODUCT_TEST_URL = "https://example.com/Bad-Product/dp/0000000000"; +const NEEDS_ANALYSIS_TEST_URL = "https://example.com/Bad-Product/dp/OPQRSTU789"; + +async function promiseSidebarUpdated(sidebar, expectedProduct) { + let browser = sidebar.querySelector("browser"); + if ( + !browser.currentURI?.equals(Services.io.newURI("about:shoppingsidebar")) + ) { + await BrowserTestUtils.browserLoaded( + browser, + false, + "about:shoppingsidebar" + ); + } + return SpecialPowers.spawn(browser, [expectedProduct], prod => { + function isProductCurrent() { + let actor = content.windowGlobalChild.getExistingActor("ShoppingSidebar"); + return actor?.getProductURI()?.spec == prod; + } + if ( + isProductCurrent() && + !!content.document.querySelector("shopping-container").wrappedJSObject + .data + ) { + info("Product already loaded."); + return true; + } + info( + "Waiting for product to be updated. Document: " + + content.document.location.href + ); + return ContentTaskUtils.waitForEvent( + content.document, + "Update", + true, + e => { + info("Sidebar updated for product: " + JSON.stringify(e.detail)); + return !!e.detail.data && isProductCurrent(); + }, + true + ).then(e => true); + }); +} + +async function promiseSidebarAdsUpdated(sidebar, expectedProduct) { + await promiseSidebarUpdated(sidebar, expectedProduct); + let browser = sidebar.querySelector("browser"); + return SpecialPowers.spawn(browser, [], () => { + let container = + content.document.querySelector("shopping-container").wrappedJSObject; + if (container.recommendationData) { + return true; + } + return ContentTaskUtils.waitForEvent( + content.document, + "UpdateRecommendations", + true, + null, + true + ).then(e => true); + }); +} + +async function verifyProductInfo(sidebar, expectedProductInfo) { + await SpecialPowers.spawn( + sidebar.querySelector("browser"), + [expectedProductInfo], + async prodInfo => { + let doc = content.document; + let container = doc.querySelector("shopping-container"); + let root = container.shadowRoot; + let reviewReliability = root.querySelector("review-reliability"); + // The async fetch could take some time. + while (!reviewReliability) { + info("Waiting for update."); + await container.updateComplete; + } + let adjustedRating = root.querySelector("adjusted-rating"); + Assert.equal( + reviewReliability.getAttribute("letter"), + prodInfo.letterGrade, + `Should have correct letter grade for product ${prodInfo.id}.` + ); + Assert.equal( + adjustedRating.getAttribute("rating"), + prodInfo.adjustedRating, + `Should have correct adjusted rating for product ${prodInfo.id}.` + ); + Assert.equal( + content.windowGlobalChild + .getExistingActor("ShoppingSidebar") + ?.getProductURI()?.spec, + prodInfo.productURL, + `Should have correct url in the child.` + ); + } + ); +} diff --git a/toolkit/components/shopping/test/mockapis/analysis.sjs b/toolkit/components/shopping/test/mockapis/analysis.sjs new file mode 100644 index 0000000000..2cc154803e --- /dev/null +++ b/toolkit/components/shopping/test/mockapis/analysis.sjs @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function loadHelperScript(path) { + let scriptFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + scriptFile.initWithPath(getState("__LOCATION__")); + scriptFile = scriptFile.parent; + scriptFile.append(path); + let scriptSpec = Services.io.newFileURI(scriptFile).spec; + Services.scriptloader.loadSubScript(scriptSpec, this); +} +/* import-globals-from ./server_helper.js */ +loadHelperScript("server_helper.js"); + +let gResponses = new Map( + Object.entries({ + ABCDEFG123: { needs_analysis: false, grade: "B", adjusted_rating: 4.1 }, + HIJKLMN456: { needs_analysis: false, grade: "F", adjusted_rating: 1.0 }, + OPQRSTU789: { needs_analysis: true }, + INVALID123: { needs_analysis: false, grade: 0.85, adjusted_rating: 1.0 }, + HTTPERR503: { status: 503, error: "Service Unavailable" }, + HTTPERR429: { status: 429, error: "Too Many Requests" }, + }) +); + +function handleRequest(request, response) { + var body = getPostBody(request.bodyInputStream); + let requestData = JSON.parse(body); + let productDetails = gResponses.get(requestData.product_id); + if (!productDetails) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + productDetails = { + status: 400, + error: "Bad Request", + }; + } + if (productDetails?.status) { + response.setStatusLine( + request.httpVersion, + productDetails.status, + productDetails.error + ); + } + response.write(JSON.stringify(productDetails)); +} diff --git a/toolkit/components/shopping/test/mockapis/analysis_status.sjs b/toolkit/components/shopping/test/mockapis/analysis_status.sjs new file mode 100644 index 0000000000..4d943a9476 --- /dev/null +++ b/toolkit/components/shopping/test/mockapis/analysis_status.sjs @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function loadHelperScript(path) { + let scriptFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + scriptFile.initWithPath(getState("__LOCATION__")); + scriptFile = scriptFile.parent; + scriptFile.append(path); + let scriptSpec = Services.io.newFileURI(scriptFile).spec; + Services.scriptloader.loadSubScript(scriptSpec, this); +} +/* import-globals-from ./server_helper.js */ +loadHelperScript("server_helper.js"); + +let gResponses = new Map( + Object.entries({ + N0T3NOUGHR: { status: "not_enough_reviews", progress: 100.0 }, + PAG3N0TSUP: { status: "page_not_supported", progress: 100.0 }, + UNPR0C3SSA: { status: "unprocessable", progress: 100.0 }, + }) +); + +function handleRequest(request, response) { + let body = getPostBody(request.bodyInputStream); + let requestData = JSON.parse(body); + let responseData = gResponses.get(requestData.product_id); + if (!responseData) { + // We want the status to be completed for most tests. + responseData = { + status: "completed", + progress: 100.0, + }; + } + response.write(JSON.stringify(responseData)); +} diff --git a/toolkit/components/shopping/test/mockapis/analyze.sjs b/toolkit/components/shopping/test/mockapis/analyze.sjs new file mode 100644 index 0000000000..6f9a8cbcee --- /dev/null +++ b/toolkit/components/shopping/test/mockapis/analyze.sjs @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function loadHelperScript(path) { + let scriptFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + scriptFile.initWithPath(getState("__LOCATION__")); + scriptFile = scriptFile.parent; + scriptFile.append(path); + let scriptSpec = Services.io.newFileURI(scriptFile).spec; + Services.scriptloader.loadSubScript(scriptSpec, this); +} +/* import-globals-from ./server_helper.js */ +loadHelperScript("server_helper.js"); + +function handleRequest(_request, response) { + // We always want the status to be pending for the current tests. + let status = { + status: "pending", + }; + response.write(JSON.stringify(status)); +} diff --git a/toolkit/components/shopping/test/mockapis/attribution.sjs b/toolkit/components/shopping/test/mockapis/attribution.sjs new file mode 100644 index 0000000000..b25cf78b3b --- /dev/null +++ b/toolkit/components/shopping/test/mockapis/attribution.sjs @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function loadHelperScript(path) { + let scriptFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + scriptFile.initWithPath(getState("__LOCATION__")); + scriptFile = scriptFile.parent; + scriptFile.append(path); + let scriptSpec = Services.io.newFileURI(scriptFile).spec; + Services.scriptloader.loadSubScript(scriptSpec, this); +} +/* import-globals-from ./server_helper.js */ +loadHelperScript("server_helper.js"); + +function getPostBody(stream) { + let binaryStream = new BinaryInputStream(stream); + let count = binaryStream.available(); + let arrayBuffer = new ArrayBuffer(count); + while (count > 0) { + let actuallyRead = binaryStream.readArrayBuffer(count, arrayBuffer); + if (!actuallyRead) { + throw new Error("Nothing read from input stream!"); + } + count -= actuallyRead; + } + return new TextDecoder().decode(arrayBuffer); +} + +function handleRequest(request, response) { + var body = getPostBody(request.bodyInputStream); + let requestData = JSON.parse(body); + + let key = requestData.aid || (requestData.aidvs && requestData.aidvs[0]); + let responseObj = { + [key]: null, + }; + + response.write(JSON.stringify(responseObj)); +} diff --git a/toolkit/components/shopping/test/mockapis/recommendations.sjs b/toolkit/components/shopping/test/mockapis/recommendations.sjs new file mode 100644 index 0000000000..2c4abc23d2 --- /dev/null +++ b/toolkit/components/shopping/test/mockapis/recommendations.sjs @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function loadHelperScript(path) { + let scriptFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + scriptFile.initWithPath(getState("__LOCATION__")); + scriptFile = scriptFile.parent; + scriptFile.append(path); + let scriptSpec = Services.io.newFileURI(scriptFile).spec; + Services.scriptloader.loadSubScript(scriptSpec, this); +} +/* import-globals-from ./server_helper.js */ +loadHelperScript("server_helper.js"); + +let gResponses = new Map( + Object.entries({ + ABCDEFG123: [ + { + 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: "https://example.com/Some-Product/dp/ABCDEFG123", + image_url: "https://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==", + }, + ], + HIJKLMN456: [], + OPQRSTU789: [], + }) +); + +function handleRequest(request, response) { + var body = getPostBody(request.bodyInputStream); + let requestData = JSON.parse(body); + let recommendation = gResponses.get(requestData.product_id); + + response.write(JSON.stringify(recommendation)); +} diff --git a/toolkit/components/shopping/test/mockapis/reporting.sjs b/toolkit/components/shopping/test/mockapis/reporting.sjs new file mode 100644 index 0000000000..1f4a4115e5 --- /dev/null +++ b/toolkit/components/shopping/test/mockapis/reporting.sjs @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const BinaryInputStream = Components.Constructor( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +function getPostBody(stream) { + let binaryStream = new BinaryInputStream(stream); + let count = binaryStream.available(); + let arrayBuffer = new ArrayBuffer(count); + while (count > 0) { + let actuallyRead = binaryStream.readArrayBuffer(count, arrayBuffer); + if (!actuallyRead) { + throw new Error("Nothing read from input stream!"); + } + count -= actuallyRead; + } + return new TextDecoder().decode(arrayBuffer); +} + +let gResponses = new Map( + Object.entries({ + ABCDEFG123: { message: "report created" }, + HIJKLMN456: { message: "already reported" }, + OPQRSTU789: { message: "not deleted" }, + }) +); + +function handleRequest(request, response) { + var body = getPostBody(request.bodyInputStream); + let requestData = JSON.parse(body); + let report = gResponses.get(requestData.product_id); + + response.write(JSON.stringify(report)); +} diff --git a/toolkit/components/shopping/test/mockapis/server_helper.js b/toolkit/components/shopping/test/mockapis/server_helper.js new file mode 100644 index 0000000000..e5d3a1d591 --- /dev/null +++ b/toolkit/components/shopping/test/mockapis/server_helper.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; +/* exported getPostBody */ + +const BinaryInputStream = Components.Constructor( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +// eslint-disable-next-line mozilla/reject-importGlobalProperties +Cu.importGlobalProperties(["TextDecoder"]); + +function getPostBody(stream) { + let binaryStream = new BinaryInputStream(stream); + let count = binaryStream.available(); + let arrayBuffer = new ArrayBuffer(count); + while (count > 0) { + let actuallyRead = binaryStream.readArrayBuffer(count, arrayBuffer); + if (!actuallyRead) { + throw new Error("Nothing read from input stream!"); + } + count -= actuallyRead; + } + return new TextDecoder().decode(arrayBuffer); +} 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"] |