/* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; ChromeUtils.defineESModuleGetters(this, { AdsFeed: "resource://newtab/lib/AdsFeed.sys.mjs", actionCreators: "resource://newtab/common/Actions.mjs", actionTypes: "resource://newtab/common/Actions.mjs", ContextId: "moz-src:///browser/modules/ContextId.sys.mjs", sinon: "resource://testing-common/Sinon.sys.mjs", }); const { ObliviousHTTP } = ChromeUtils.importESModule( "resource://gre/modules/ObliviousHTTP.sys.mjs" ); // Prefs to enable UAPI ad types const PREF_UNIFIED_ADS_ADSFEED_ENABLED = "unifiedAds.adsFeed.enabled"; const PREF_UNIFIED_ADS_TILES_ENABLED = "unifiedAds.tiles.enabled"; const PREF_UNIFIED_ADS_SPOCS_ENABLED = "unifiedAds.spocs.enabled"; const PREF_UNIFIED_ADS_ENDPOINT = "unifiedAds.endpoint"; const PREF_TILES_PLACEMENTS = "discoverystream.placements.tiles"; const PREF_TILES_COUNTS = "discoverystream.placements.tiles.counts"; const PREF_SPOC_COUNTS = "discoverystream.placements.spocs.counts"; const PREF_SPOC_PLACEMENTS = "discoverystream.placements.spocs"; const PREF_UNIFIED_ADS_BLOCKED_LIST = "unifiedAds.blockedAds"; // Note: Full pref path required by Services.prefs.setBoolPref const PREF_UNIFIED_ADS_OHTTP_ENABLED = "browser.newtabpage.activity-stream.unifiedAds.ohttp.enabled"; const PREF_UNIFIED_ADS_OHTTP_RELAY_URL = "browser.newtabpage.activity-stream.discoverystream.ohttp.relayURL"; const PREF_UNIFIED_ADS_OHTTP_CONFIG_URL = "browser.newtabpage.activity-stream.discoverystream.ohttp.configURL"; // Primary pref that is toggled when enabling top site sponsored tiles const PREF_FEED_TOPSITES = "feeds.topsites"; const PREF_SYSTEM_TOPSITES = "feeds.system.topsites"; const PREF_SHOW_SPONSORED_TOPSITES = "showSponsoredTopSites"; // Primary pref that is toggled when enabling sponsored stories const PREF_FEED_SECTIONS_TOPSTORIES = "feeds.section.topstories"; const PREF_SYSTEM_TOPSTORIES = "feeds.system.topstories"; const PREF_SHOW_SPONSORED = "showSponsored"; const PREF_SYSTEM_SHOW_SPONSORED = "system.showSponsored"; const mockedTileData = [ { url: "https://www.test.com", image_url: "images/test-com.png", click_url: "https://www.test-click.com", impression_url: "https://www.test-impression.com", name: "test", }, { url: "https://www.test1.com", image_url: "images/test1-com.png", click_url: "https://www.test1-click.com", impression_url: "https://www.test1-impression.com", name: "test1", }, ]; const mockedFetchTileData = { newtab_tile_1: [ { format: "tile", url: "https://www.test1.com", callbacks: { click: "https://www.test1-click.com", impression: "https://www.test1-impression.com", report: "https://www.test1-report.com", }, image_url: "images/test1-com.png", name: "test1", block_key: "test1", }, ], newtab_tile_2: [ { format: "tile", url: "https://www.test2.com", callbacks: { click: "https://www.test2-click.com", impression: "https://www.test2-impression.com", report: "https://www.test2-report.com", }, image_url: "images/test2-com.png", name: "test2", block_key: "test2", }, ], newtab_tile_3: [ { format: "tile", url: "https://www.test3.com", callbacks: { click: "https://www.test3-click.com", impression: "https://www.test3-impression.com", report: "https://www.test3-report.com", }, image_url: "images/test3-com.png", name: "test3", block_key: "test3", }, ], }; const mockedSpocPlacements = [ { placement: "newtab_spocs", count: 6, }, ]; const mockedSpocsData = [ { format: "spoc", url: "https://www.test.com", callbacks: { click: "https://www.test-click.com", impression: "https://www.test-impression.com", report: "https://www.test-report.com", }, image_url: "images/test-com.png", title: "test", domain: "test.com", excerpt: "Lorem ipsum dolor sit amet.", sponsor: "Test", block_key: "test", caps: { cap_key: "123456789", day: 10, }, ranking: { priority: 9, personalization_models: {}, item_score: 1, }, }, { format: "spoc", url: "https://www.test1.com", callbacks: { click: "https://www.test1-click.com", impression: "https://www.test1-impression.com", report: "https://www.test1-report.com", }, image_url: "images/test1-com.png", title: "test1", domain: "test1.com", excerpt: "Lorem ipsum dolor sit amet.", sponsor: "Test1", block_key: "test1", caps: { cap_key: "234567891", day: 10, }, ranking: { priority: 9, personalization_models: {}, item_score: 1, }, }, { format: "spoc", url: "https://www.test2.com", callbacks: { click: "https://www.test2-click.com", impression: "https://www.test2-impression.com", report: "https://www.test2-report.com", }, image_url: "images/test2-com.png", title: "test2", domain: "test2.com", excerpt: "Lorem ipsum dolor sit amet.", sponsor: "Test2", block_key: "test2", caps: { cap_key: "345678912", day: 10, }, ranking: { priority: 9, personalization_models: {}, item_score: 1, }, }, { format: "spoc", url: "https://www.test3.com", callbacks: { click: "https://www.test3-click.com", impression: "https://www.test3-impression.com", report: "https://www.test3-report.com", }, image_url: "images/test3-com.png", title: "test3", domain: "test3.com", excerpt: "Lorem ipsum dolor sit amet.", sponsor: "Test3", block_key: "test3", caps: { cap_key: "456789123", day: 10, }, ranking: { priority: 9, personalization_models: {}, item_score: 1, }, }, { format: "spoc", url: "https://www.test4.com", callbacks: { click: "https://www.test4-click.com", impression: "https://www.test4-impression.com", report: "https://www.test4-report.com", }, image_url: "images/test4-com.png", title: "test4", domain: "test4.com", excerpt: "Lorem ipsum dolor sit amet.", sponsor: "Test4", block_key: "test4", caps: { cap_key: "567891234", day: 10, }, ranking: { priority: 9, personalization_models: {}, item_score: 1, }, }, { format: "spoc", url: "https://www.test5.com", callbacks: { click: "https://www.test5-click.com", impression: "https://www.test5-impression.com", report: "https://www.test5-report.com", }, image_url: "images/test5-com.png", title: "test5", domain: "test5.com", excerpt: "Lorem ipsum dolor sit amet.", sponsor: "Test5", block_key: "test5", caps: { cap_key: "678912345", day: 10, }, ranking: { priority: 9, personalization_models: {}, item_score: 1, }, }, ]; function getAdsFeedForTest() { let feed = new AdsFeed(); let tiles = mockedTileData; let spocs = mockedSpocsData; let spocPlacements = mockedSpocPlacements; feed.store = { dispatch: sinon.spy(), getState() { return this.state; }, state: { tiles, spocs, spocPlacements, lastUpdated: 1, Prefs: { values: { [PREF_TILES_PLACEMENTS]: "newtab_tile_1, newtab_tile_2, newtab_tile_3", [PREF_TILES_COUNTS]: "1, 1, 1", [PREF_SPOC_PLACEMENTS]: "newtab_spocs", [PREF_SPOC_COUNTS]: "6", [PREF_UNIFIED_ADS_BLOCKED_LIST]: "", [PREF_UNIFIED_ADS_ENDPOINT]: "https://example.com/", // AdsFeed/UAPI specific prefs to test [PREF_UNIFIED_ADS_TILES_ENABLED]: false, [PREF_UNIFIED_ADS_SPOCS_ENABLED]: false, [PREF_UNIFIED_ADS_ADSFEED_ENABLED]: false, // Default display prefs for tiles/spocs [PREF_FEED_TOPSITES]: true, [PREF_SYSTEM_TOPSITES]: true, [PREF_SHOW_SPONSORED_TOPSITES]: true, [PREF_FEED_SECTIONS_TOPSTORIES]: true, [PREF_SYSTEM_TOPSTORIES]: true, [PREF_SHOW_SPONSORED]: true, [PREF_SYSTEM_SHOW_SPONSORED]: true, }, }, }, }; return feed; } add_task(async function test_construction() { let sandbox = sinon.createSandbox(); sandbox.stub(AdsFeed.prototype, "PersistentCache").returns({ set: () => {}, get: () => {}, }); let feed = new AdsFeed(); info("AdsFeed constructor should create initial values"); Assert.ok(feed, "Could construct a AdsFeed"); Assert.strictEqual(feed.loaded, false, "AdsFeed is not loaded"); Assert.strictEqual(feed.enabled, false, "AdsFeed is not enabled"); Assert.strictEqual( feed.lastUpdated, null, "AdsFeed has no lastUpdated record" ); Assert.strictEqual( feed.tiles.length, 0, "tiles is initialized as a array with length of 0" ); sandbox.restore(); }); add_task(async function test_isEnabled_tiles() { let sandbox = sinon.createSandbox(); sandbox.stub(AdsFeed.prototype, "PersistentCache").returns({ set: () => {}, get: () => {}, }); const dateNowTestValue = 1; sandbox.stub(AdsFeed.prototype, "Date").returns({ now: () => dateNowTestValue, }); let feed = getAdsFeedForTest(sandbox); feed.store.state.Prefs.values[PREF_UNIFIED_ADS_ADSFEED_ENABLED] = true; feed.store.state.Prefs.values[PREF_UNIFIED_ADS_TILES_ENABLED] = true; feed.store.state.Prefs.values[PREF_UNIFIED_ADS_SPOCS_ENABLED] = true; feed.store.state.Prefs.values[PREF_FEED_TOPSITES] = true; feed.store.state.Prefs.values[PREF_SYSTEM_TOPSITES] = true; feed.store.state.Prefs.values[PREF_SHOW_SPONSORED_TOPSITES] = true; feed.store.state.Prefs.values[PREF_FEED_SECTIONS_TOPSTORIES] = true; feed.store.state.Prefs.values[PREF_SYSTEM_TOPSTORIES] = true; feed.store.state.Prefs.values[PREF_SHOW_SPONSORED] = true; feed.store.state.Prefs.values[PREF_SYSTEM_SHOW_SPONSORED] = true; Assert.ok(feed.isEnabled()); sandbox.restore(); }); add_task(async function test_onAction_INIT_tiles_and_spocs() { let sandbox = sinon.createSandbox(); sandbox.stub(AdsFeed.prototype, "PersistentCache").returns({ set: () => {}, get: () => {}, }); const dateNowTestValue = 1; sandbox.stub(AdsFeed.prototype, "Date").returns({ now: () => dateNowTestValue, }); let feed = getAdsFeedForTest(sandbox); feed.store.state.Prefs.values[PREF_UNIFIED_ADS_ADSFEED_ENABLED] = true; feed.store.state.Prefs.values[PREF_UNIFIED_ADS_TILES_ENABLED] = true; feed.store.state.Prefs.values[PREF_UNIFIED_ADS_SPOCS_ENABLED] = true; sandbox.stub(feed, "isEnabled").returns(true); sandbox.stub(feed, "fetchData").returns({ tiles: mockedTileData, spocs: mockedSpocsData, spocPlacements: mockedSpocPlacements, lastUpdated: dateNowTestValue, }); info("AdsFeed.onAction INIT should initialize Ads"); await feed.onAction({ type: actionTypes.INIT, }); Assert.ok(feed.store.dispatch.calledTwice); Assert.ok( feed.store.dispatch.firstCall.calledWithExactly( actionCreators.BroadcastToContent({ type: "ADS_UPDATE_TILES", data: { tiles: mockedTileData, }, meta: { isStartup: true, }, }) ) ); Assert.ok( feed.store.dispatch.secondCall.calledWithExactly( actionCreators.BroadcastToContent({ type: "ADS_UPDATE_SPOCS", data: { spocs: mockedSpocsData, spocPlacements: mockedSpocPlacements, }, meta: { isStartup: true, }, }) ) ); sandbox.restore(); }); add_task(async function test_onAction_INIT_spocs_only() { let sandbox = sinon.createSandbox(); sandbox.stub(AdsFeed.prototype, "PersistentCache").returns({ set: () => {}, get: () => {}, }); const dateNowTestValue = 1; sandbox.stub(AdsFeed.prototype, "Date").returns({ now: () => dateNowTestValue, }); let feed = getAdsFeedForTest(sandbox); feed.store.state.Prefs.values[PREF_UNIFIED_ADS_ADSFEED_ENABLED] = true; feed.store.state.Prefs.values[PREF_UNIFIED_ADS_SPOCS_ENABLED] = true; sandbox.stub(feed, "isEnabled").returns(true); sandbox.stub(feed, "fetchData").returns({ tiles: [], spocs: mockedSpocsData, spocPlacements: mockedSpocPlacements, lastUpdated: dateNowTestValue, }); info( "AdsFeed.onAction INIT should return only spocs when tiles are disabled" ); await feed.onAction({ type: actionTypes.INIT, }); Assert.ok(feed.store.dispatch.calledOnce); Assert.ok( feed.store.dispatch.calledWithExactly( actionCreators.BroadcastToContent({ type: "ADS_UPDATE_SPOCS", data: { spocs: mockedSpocsData, spocPlacements: mockedSpocPlacements, }, meta: { isStartup: true, }, }) ) ); sandbox.restore(); }); add_task(async function test_onAction_INIT_tiles_only() { let sandbox = sinon.createSandbox(); sandbox.stub(AdsFeed.prototype, "PersistentCache").returns({ set: () => {}, get: () => {}, }); const dateNowTestValue = 1; sandbox.stub(AdsFeed.prototype, "Date").returns({ now: () => dateNowTestValue, }); let feed = getAdsFeedForTest(sandbox); feed.store.state.Prefs.values[PREF_UNIFIED_ADS_ADSFEED_ENABLED] = true; feed.store.state.Prefs.values[PREF_UNIFIED_ADS_TILES_ENABLED] = true; sandbox.stub(feed, "isEnabled").returns(true); sandbox.stub(feed, "fetchData").returns({ tiles: mockedTileData, spocs: [], spocPlacements: [], lastUpdated: dateNowTestValue, }); info( "AdsFeed.onAction INIT should return only tiles when spocs are disabled" ); await feed.onAction({ type: actionTypes.INIT, }); Assert.ok(feed.store.dispatch.calledOnce); Assert.ok( feed.store.dispatch.calledWithExactly( actionCreators.BroadcastToContent({ type: "ADS_UPDATE_TILES", data: { tiles: mockedTileData, }, meta: { isStartup: true, }, }) ) ); sandbox.restore(); }); add_task(async function test_fetchData_noOHTTP() { const sandbox = sinon.createSandbox(); const feed = getAdsFeedForTest(); sandbox .stub(AdsFeed.prototype, "PersistentCache") .returns({ get: () => {}, set: () => {} }); sandbox.stub(feed, "fetch").resolves( new Response( JSON.stringify({ tile1: [ { block_key: "foo", name: "bar", url: "https://test.com", image_url: "image.png", callbacks: { click: "click", impression: "impression" }, }, ], }) ) ); sandbox.stub(feed, "Date").returns({ now: () => 123 }); // Simulate OHTTP being disabled Services.prefs.setBoolPref(PREF_UNIFIED_ADS_OHTTP_ENABLED, false); const supportedAdTypes = { tiles: true, spocs: false }; await feed.fetchData(supportedAdTypes); info("AdsFeed: fetchData() should fetch normally via HTTP"); Assert.ok(feed.fetch.calledOnce, "Fallback fetch called"); sandbox.restore(); }); add_task(async function test_fetchData_OHTTP() { const sandbox = sinon.createSandbox(); const CONTEXT_ID = "ContextId"; sandbox.stub(ContextId, "request").returns(CONTEXT_ID); const feed = getAdsFeedForTest(); Services.prefs.setBoolPref(PREF_UNIFIED_ADS_OHTTP_ENABLED, true); Services.prefs.setStringPref( PREF_UNIFIED_ADS_OHTTP_RELAY_URL, "https://relay.test" ); Services.prefs.setStringPref( PREF_UNIFIED_ADS_OHTTP_CONFIG_URL, "https://config.test" ); const mockConfig = { config: "mocked" }; sandbox .stub(AdsFeed.prototype, "PersistentCache") .returns({ get: () => {}, set: () => {} }); sandbox.stub(feed, "Date").returns({ now: () => 123 }); sandbox.stub(ObliviousHTTP, "getOHTTPConfig").resolves(mockConfig); sandbox.stub(ObliviousHTTP, "ohttpRequest").resolves({ status: 200, json: () => { return Promise.resolve(mockedFetchTileData); }, }); const result = await feed.fetchData({ tiles: true, spocs: false }); info("AdsFeed: fetchData() should fetch via OHTTP when enabled"); Assert.ok(ObliviousHTTP.getOHTTPConfig.calledOnce); Assert.ok(ObliviousHTTP.ohttpRequest.calledOnce); Assert.deepEqual(result.tiles[0].id, "test1"); info("AdsFeed: fetchData() should not send cookies"); Assert.equal( ObliviousHTTP.ohttpRequest.firstCall.args[3].credentials, "omit", "should not send cookies" ); info("AdsFeed: fetchData() should construct request body"); Assert.equal( ObliviousHTTP.ohttpRequest.firstCall.args[3].body, JSON.stringify({ context_id: CONTEXT_ID, flags: {}, placements: [ { placement: "newtab_tile_1", count: 1, }, { placement: "newtab_tile_2", count: 1, }, { placement: "newtab_tile_3", count: 1, }, ], blocks: [""], }) ); sandbox.restore(); }); add_task(async function test_fetchData_OHTTP_with_adsBackendConfig() { const sandbox = sinon.createSandbox(); const CONTEXT_ID = "ContextId"; sandbox.stub(ContextId, "request").returns(CONTEXT_ID); const feed = getAdsFeedForTest(); Services.prefs.setBoolPref(PREF_UNIFIED_ADS_OHTTP_ENABLED, true); Services.prefs.setStringPref( PREF_UNIFIED_ADS_OHTTP_RELAY_URL, "https://relay.test" ); Services.prefs.setStringPref( PREF_UNIFIED_ADS_OHTTP_CONFIG_URL, "https://config.test" ); feed.store.state.Prefs.values.adsBackendConfig = { feature1: false, feature2: true, }; const mockConfig = { config: "mocked" }; sandbox .stub(AdsFeed.prototype, "PersistentCache") .returns({ get: () => {}, set: () => {} }); sandbox.stub(feed, "Date").returns({ now: () => 123 }); sandbox.stub(ObliviousHTTP, "getOHTTPConfig").resolves(mockConfig); sandbox.stub(ObliviousHTTP, "ohttpRequest").resolves({ status: 200, json: () => { return Promise.resolve(mockedFetchTileData); }, }); const result = await feed.fetchData({ tiles: true, spocs: false }); info("AdsFeed: fetchData() should fetch via OHTTP when enabled"); Assert.ok(ObliviousHTTP.getOHTTPConfig.calledOnce); Assert.ok(ObliviousHTTP.ohttpRequest.calledOnce); Assert.deepEqual(result.tiles[0].id, "test1"); info("AdsFeed: fetchData() should not send cookies"); Assert.equal( ObliviousHTTP.ohttpRequest.firstCall.args[3].credentials, "omit", "should not send cookies" ); info("AdsFeed: fetchData() should construct request body"); Assert.equal( ObliviousHTTP.ohttpRequest.firstCall.args[3].body, JSON.stringify({ context_id: CONTEXT_ID, flags: { feature1: false, feature2: true, }, placements: [ { placement: "newtab_tile_1", count: 1, }, { placement: "newtab_tile_2", count: 1, }, { placement: "newtab_tile_3", count: 1, }, ], blocks: [""], }) ); sandbox.restore(); });