/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; /** * This file tests the DirectoryLinksProvider singleton in the DirectoryLinksProvider.jsm module. */ var { classes: Cc, interfaces: Ci, results: Cr, utils: Cu, Constructor: CC } = Components; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource:///modules/DirectoryLinksProvider.jsm"); Cu.import("resource://gre/modules/Promise.jsm"); Cu.import("resource://gre/modules/Http.jsm"); Cu.import("resource://testing-common/httpd.js"); Cu.import("resource://gre/modules/osfile.jsm"); Cu.import("resource://gre/modules/Task.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/PlacesUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils", "resource://gre/modules/NewTabUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils", "resource://testing-common/PlacesTestUtils.jsm"); do_get_profile(); const DIRECTORY_LINKS_FILE = "directoryLinks.json"; const DIRECTORY_FRECENCY = 1000; const SUGGESTED_FRECENCY = Infinity; const kURLData = {"directory": [{"url":"http://example.com", "title":"LocalSource"}]}; const kTestURL = 'data:application/json,' + JSON.stringify(kURLData); // DirectoryLinksProvider preferences const kLocalePref = DirectoryLinksProvider._observedPrefs.prefSelectedLocale; const kSourceUrlPref = DirectoryLinksProvider._observedPrefs.linksURL; const kPingUrlPref = "browser.newtabpage.directory.ping"; const kNewtabEnhancedPref = "browser.newtabpage.enhanced"; // httpd settings var server; const kDefaultServerPort = 9000; const kBaseUrl = "http://localhost:" + kDefaultServerPort; const kExamplePath = "/exampleTest/"; const kFailPath = "/fail/"; const kPingPath = "/ping/"; const kExampleURL = kBaseUrl + kExamplePath; const kFailURL = kBaseUrl + kFailPath; const kPingUrl = kBaseUrl + kPingPath; // app/profile/firefox.js are not avaialble in xpcshell: hence, preset them Services.prefs.setCharPref(kLocalePref, "en-US"); Services.prefs.setCharPref(kSourceUrlPref, kTestURL); Services.prefs.setCharPref(kPingUrlPref, kPingUrl); Services.prefs.setBoolPref(kNewtabEnhancedPref, true); const kHttpHandlerData = {}; kHttpHandlerData[kExamplePath] = {"directory": [{"url":"http://example.com", "title":"RemoteSource"}]}; const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", "nsIBinaryInputStream", "setInputStream"); var gLastRequestPath; var suggestedTile1 = { url: "http://turbotax.com", type: "affiliate", lastVisitDate: 4, adgroup_name: "Adgroup1", frecent_sites: [ "taxact.com", "hrblock.com", "1040.com", "taxslayer.com" ] }; var suggestedTile2 = { url: "http://irs.gov", type: "affiliate", lastVisitDate: 3, adgroup_name: "Adgroup2", frecent_sites: [ "taxact.com", "hrblock.com", "freetaxusa.com", "taxslayer.com" ] }; var suggestedTile3 = { url: "http://hrblock.com", type: "affiliate", lastVisitDate: 2, adgroup_name: "Adgroup3", frecent_sites: [ "taxact.com", "freetaxusa.com", "1040.com", "taxslayer.com" ] }; var suggestedTile4 = { url: "http://sponsoredtile.com", type: "sponsored", lastVisitDate: 1, adgroup_name: "Adgroup4", frecent_sites: [ "sponsoredtarget.com" ] } var suggestedTile5 = { url: "http://eviltile.com", type: "affiliate", lastVisitDate: 5, explanation: "This is an evil tile
muhahaha", adgroup_name: "WE ARE EVIL ", frecent_sites: [ "eviltarget.com" ] } var someOtherSite = {url: "http://someothersite.com", title: "Not_A_Suggested_Site"}; function getHttpHandler(path) { let code = 200; let body = JSON.stringify(kHttpHandlerData[path]); if (path == kFailPath) { code = 204; } return function(aRequest, aResponse) { gLastRequestPath = aRequest.path; aResponse.setStatusLine(null, code); aResponse.setHeader("Content-Type", "application/json"); aResponse.write(body); }; } function isIdentical(actual, expected) { if (expected == null) { do_check_eq(actual, expected); } else if (typeof expected == "object") { // Make sure all the keys match up do_check_eq(Object.keys(actual).sort() + "", Object.keys(expected).sort()); // Recursively check each value individually Object.keys(expected).forEach(key => { isIdentical(actual[key], expected[key]); }); } else { do_check_eq(actual, expected); } } function fetchData() { let deferred = Promise.defer(); DirectoryLinksProvider.getLinks(linkData => { deferred.resolve(linkData); }); return deferred.promise; } function readJsonFile(jsonFile = DIRECTORY_LINKS_FILE) { let decoder = new TextDecoder(); let directoryLinksFilePath = OS.Path.join(OS.Constants.Path.localProfileDir, jsonFile); return OS.File.read(directoryLinksFilePath).then(array => { let json = decoder.decode(array); return JSON.parse(json); }, () => { return "" }); } function cleanJsonFile(jsonFile = DIRECTORY_LINKS_FILE) { let directoryLinksFilePath = OS.Path.join(OS.Constants.Path.localProfileDir, jsonFile); return OS.File.remove(directoryLinksFilePath); } function LinksChangeObserver() { this.deferred = Promise.defer(); this.onManyLinksChanged = () => this.deferred.resolve(); this.onDownloadFail = this.onManyLinksChanged; } function promiseDirectoryDownloadOnPrefChange(pref, newValue) { let oldValue = Services.prefs.getCharPref(pref); if (oldValue != newValue) { // if the preference value is already equal to newValue // the pref service will not call our observer and we // deadlock. Hence only setup observer if values differ let observer = new LinksChangeObserver(); DirectoryLinksProvider.addObserver(observer); Services.prefs.setCharPref(pref, newValue); return observer.deferred.promise.then(() => { DirectoryLinksProvider.removeObserver(observer); }); } return Promise.resolve(); } function promiseSetupDirectoryLinksProvider(options = {}) { return Task.spawn(function*() { let linksURL = options.linksURL || kTestURL; yield DirectoryLinksProvider.init(); yield promiseDirectoryDownloadOnPrefChange(kLocalePref, options.locale || "en-US"); yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, linksURL); do_check_eq(DirectoryLinksProvider._linksURL, linksURL); DirectoryLinksProvider._lastDownloadMS = options.lastDownloadMS || 0; }); } function promiseCleanDirectoryLinksProvider() { return Task.spawn(function*() { yield promiseDirectoryDownloadOnPrefChange(kLocalePref, "en-US"); yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, kTestURL); yield DirectoryLinksProvider._clearFrequencyCap(); yield DirectoryLinksProvider._loadInadjacentSites(); DirectoryLinksProvider._lastDownloadMS = 0; DirectoryLinksProvider.reset(); }); } function run_test() { // Set up a mock HTTP server to serve a directory page server = new HttpServer(); server.registerPrefixHandler(kExamplePath, getHttpHandler(kExamplePath)); server.registerPrefixHandler(kFailPath, getHttpHandler(kFailPath)); server.start(kDefaultServerPort); NewTabUtils.init(); run_next_test(); // Teardown. do_register_cleanup(function() { server.stop(function() { }); DirectoryLinksProvider.reset(); Services.prefs.clearUserPref(kLocalePref); Services.prefs.clearUserPref(kSourceUrlPref); Services.prefs.clearUserPref(kPingUrlPref); Services.prefs.clearUserPref(kNewtabEnhancedPref); }); } function setTimeout(fun, timeout) { let timer = Components.classes["@mozilla.org/timer;1"] .createInstance(Components.interfaces.nsITimer); var event = { notify: function () { fun(); } }; timer.initWithCallback(event, timeout, Components.interfaces.nsITimer.TYPE_ONE_SHOT); return timer; } add_task(function test_shouldUpdateSuggestedTile() { let suggestedLink = { targetedSite: "somesite.com" }; // DirectoryLinksProvider has no suggested tile and no top sites => no need to update do_check_eq(DirectoryLinksProvider._getCurrentTopSiteCount(), 0); isIdentical(NewTabUtils.getProviderLinks(), []); do_check_eq(DirectoryLinksProvider._shouldUpdateSuggestedTile(), false); // DirectoryLinksProvider has a suggested tile and no top sites => need to update let origGetProviderLinks = NewTabUtils.getProviderLinks; NewTabUtils.getProviderLinks = (provider) => [suggestedLink]; do_check_eq(DirectoryLinksProvider._getCurrentTopSiteCount(), 0); isIdentical(NewTabUtils.getProviderLinks(), [suggestedLink]); do_check_eq(DirectoryLinksProvider._shouldUpdateSuggestedTile(), true); // DirectoryLinksProvider has a suggested tile and 8 top sites => no need to update let origCurrentTopSiteCount = DirectoryLinksProvider._getCurrentTopSiteCount; DirectoryLinksProvider._getCurrentTopSiteCount = () => 8; do_check_eq(DirectoryLinksProvider._getCurrentTopSiteCount(), 8); isIdentical(NewTabUtils.getProviderLinks(), [suggestedLink]); do_check_eq(DirectoryLinksProvider._shouldUpdateSuggestedTile(), false); // DirectoryLinksProvider has no suggested tile and 8 top sites => need to update NewTabUtils.getProviderLinks = origGetProviderLinks; do_check_eq(DirectoryLinksProvider._getCurrentTopSiteCount(), 8); isIdentical(NewTabUtils.getProviderLinks(), []); do_check_eq(DirectoryLinksProvider._shouldUpdateSuggestedTile(), true); // Cleanup DirectoryLinksProvider._getCurrentTopSiteCount = origCurrentTopSiteCount; }); add_task(function* test_updateSuggestedTile() { let topSites = ["site0.com", "1040.com", "site2.com", "hrblock.com", "site4.com", "freetaxusa.com", "site6.com"]; // Initial setup let data = {"suggested": [suggestedTile1, suggestedTile2, suggestedTile3], "directory": [someOtherSite]}; let dataURI = 'data:application/json,' + JSON.stringify(data); let testObserver = new TestFirstRun(); DirectoryLinksProvider.addObserver(testObserver); yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); let links = yield fetchData(); let origIsTopPlacesSite = NewTabUtils.isTopPlacesSite; NewTabUtils.isTopPlacesSite = function(site) { return topSites.indexOf(site) >= 0; } let origGetProviderLinks = NewTabUtils.getProviderLinks; NewTabUtils.getProviderLinks = function(provider) { return links; } let origCurrentTopSiteCount = DirectoryLinksProvider._getCurrentTopSiteCount; DirectoryLinksProvider._getCurrentTopSiteCount = () => 8; do_check_eq(DirectoryLinksProvider._updateSuggestedTile(), undefined); function TestFirstRun() { this.promise = new Promise(resolve => { this.onLinkChanged = (directoryLinksProvider, link) => { links.unshift(link); let possibleLinks = [suggestedTile1.url, suggestedTile2.url, suggestedTile3.url]; isIdentical([...DirectoryLinksProvider._topSitesWithSuggestedLinks], ["hrblock.com", "1040.com", "freetaxusa.com"]); do_check_true(possibleLinks.indexOf(link.url) > -1); do_check_eq(link.frecency, SUGGESTED_FRECENCY); do_check_eq(link.type, "affiliate"); resolve(); }; }); } function TestChangingSuggestedTile() { this.count = 0; this.promise = new Promise(resolve => { this.onLinkChanged = (directoryLinksProvider, link) => { this.count++; let possibleLinks = [suggestedTile1.url, suggestedTile2.url, suggestedTile3.url]; do_check_true(possibleLinks.indexOf(link.url) > -1); do_check_eq(link.type, "affiliate"); do_check_true(this.count <= 2); if (this.count == 1) { // The removed suggested link is the one we added initially. do_check_eq(link.url, links.shift().url); do_check_eq(link.frecency, SUGGESTED_FRECENCY); } else { links.unshift(link); do_check_eq(link.frecency, SUGGESTED_FRECENCY); } isIdentical([...DirectoryLinksProvider._topSitesWithSuggestedLinks], ["hrblock.com", "freetaxusa.com"]); resolve(); } }); } function TestRemovingSuggestedTile() { this.count = 0; this.promise = new Promise(resolve => { this.onLinkChanged = (directoryLinksProvider, link) => { this.count++; do_check_eq(link.type, "affiliate"); do_check_eq(this.count, 1); do_check_eq(link.frecency, SUGGESTED_FRECENCY); do_check_eq(link.url, links.shift().url); isIdentical([...DirectoryLinksProvider._topSitesWithSuggestedLinks], []); resolve(); } }); } // Test first call to '_updateSuggestedTile()', called when fetching directory links. yield testObserver.promise; DirectoryLinksProvider.removeObserver(testObserver); // Removing a top site that doesn't have a suggested link should // not change the current suggested tile. let removedTopsite = topSites.shift(); do_check_eq(removedTopsite, "site0.com"); do_check_false(NewTabUtils.isTopPlacesSite(removedTopsite)); let updateSuggestedTile = DirectoryLinksProvider._handleLinkChanged({ url: "http://" + removedTopsite, type: "history", }); do_check_false(updateSuggestedTile); // Removing a top site that has a suggested link should // remove any current suggested tile and add a new one. testObserver = new TestChangingSuggestedTile(); DirectoryLinksProvider.addObserver(testObserver); removedTopsite = topSites.shift(); do_check_eq(removedTopsite, "1040.com"); do_check_false(NewTabUtils.isTopPlacesSite(removedTopsite)); DirectoryLinksProvider.onLinkChanged(DirectoryLinksProvider, { url: "http://" + removedTopsite, type: "history", }); yield testObserver.promise; do_check_eq(testObserver.count, 2); DirectoryLinksProvider.removeObserver(testObserver); // Removing all top sites with suggested links should remove // the current suggested link and not replace it. topSites = []; testObserver = new TestRemovingSuggestedTile(); DirectoryLinksProvider.addObserver(testObserver); DirectoryLinksProvider.onManyLinksChanged(); yield testObserver.promise; // Cleanup yield promiseCleanDirectoryLinksProvider(); NewTabUtils.isTopPlacesSite = origIsTopPlacesSite; NewTabUtils.getProviderLinks = origGetProviderLinks; DirectoryLinksProvider._getCurrentTopSiteCount = origCurrentTopSiteCount; }); add_task(function* test_suggestedLinksMap() { let data = {"suggested": [suggestedTile1, suggestedTile2, suggestedTile3, suggestedTile4], "directory": [someOtherSite]}; let dataURI = 'data:application/json,' + JSON.stringify(data); yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); let links = yield fetchData(); // Ensure the suggested tiles were not considered directory tiles. do_check_eq(links.length, 1); let expected_data = [{url: "http://someothersite.com", title: "Not_A_Suggested_Site", frecency: DIRECTORY_FRECENCY, lastVisitDate: 1}]; isIdentical(links, expected_data); // Check for correctly saved suggested tiles data. expected_data = { "taxact.com": [suggestedTile1, suggestedTile2, suggestedTile3], "hrblock.com": [suggestedTile1, suggestedTile2], "1040.com": [suggestedTile1, suggestedTile3], "taxslayer.com": [suggestedTile1, suggestedTile2, suggestedTile3], "freetaxusa.com": [suggestedTile2, suggestedTile3], "sponsoredtarget.com": [suggestedTile4], }; let suggestedSites = [...DirectoryLinksProvider._suggestedLinks.keys()]; do_check_eq(suggestedSites.indexOf("sponsoredtarget.com"), 5); do_check_eq(suggestedSites.length, Object.keys(expected_data).length); DirectoryLinksProvider._suggestedLinks.forEach((suggestedLinks, site) => { let suggestedLinksItr = suggestedLinks.values(); for (let link of expected_data[site]) { let linkCopy = JSON.parse(JSON.stringify(link)); linkCopy.targetedName = link.adgroup_name; linkCopy.explanation = ""; isIdentical(suggestedLinksItr.next().value, linkCopy); } }) yield promiseCleanDirectoryLinksProvider(); }); add_task(function* test_topSitesWithSuggestedLinks() { let topSites = ["site0.com", "1040.com", "site2.com", "hrblock.com", "site4.com", "freetaxusa.com", "site6.com"]; let origIsTopPlacesSite = NewTabUtils.isTopPlacesSite; NewTabUtils.isTopPlacesSite = function(site) { return topSites.indexOf(site) >= 0; } // Mock out getProviderLinks() so we don't have to populate cache in NewTabUtils let origGetProviderLinks = NewTabUtils.getProviderLinks; NewTabUtils.getProviderLinks = function(provider) { return []; } // We start off with no top sites with suggested links. do_check_eq(DirectoryLinksProvider._topSitesWithSuggestedLinks.size, 0); let data = {"suggested": [suggestedTile1, suggestedTile2, suggestedTile3], "directory": [someOtherSite]}; let dataURI = 'data:application/json,' + JSON.stringify(data); yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); yield fetchData(); // Check we've populated suggested links as expected. do_check_eq(DirectoryLinksProvider._suggestedLinks.size, 5); // When many sites change, we update _topSitesWithSuggestedLinks as expected. let expectedTopSitesWithSuggestedLinks = ["hrblock.com", "1040.com", "freetaxusa.com"]; DirectoryLinksProvider._handleManyLinksChanged(); isIdentical([...DirectoryLinksProvider._topSitesWithSuggestedLinks], expectedTopSitesWithSuggestedLinks); // Removing site6.com as a topsite has no impact on _topSitesWithSuggestedLinks. let popped = topSites.pop(); DirectoryLinksProvider._handleLinkChanged({url: "http://" + popped}); isIdentical([...DirectoryLinksProvider._topSitesWithSuggestedLinks], expectedTopSitesWithSuggestedLinks); // Removing freetaxusa.com as a topsite will remove it from _topSitesWithSuggestedLinks. popped = topSites.pop(); expectedTopSitesWithSuggestedLinks.pop(); DirectoryLinksProvider._handleLinkChanged({url: "http://" + popped}); isIdentical([...DirectoryLinksProvider._topSitesWithSuggestedLinks], expectedTopSitesWithSuggestedLinks); // Re-adding freetaxusa.com as a topsite will add it to _topSitesWithSuggestedLinks. topSites.push(popped); expectedTopSitesWithSuggestedLinks.push(popped); DirectoryLinksProvider._handleLinkChanged({url: "http://" + popped}); isIdentical([...DirectoryLinksProvider._topSitesWithSuggestedLinks], expectedTopSitesWithSuggestedLinks); // Cleanup. NewTabUtils.isTopPlacesSite = origIsTopPlacesSite; NewTabUtils.getProviderLinks = origGetProviderLinks; }); add_task(function* test_suggestedAttributes() { let origIsTopPlacesSite = NewTabUtils.isTopPlacesSite; NewTabUtils.isTopPlacesSite = () => true; let origCurrentTopSiteCount = DirectoryLinksProvider._getCurrentTopSiteCount; DirectoryLinksProvider._getCurrentTopSiteCount = () => 8; let frecent_sites = "addons.mozilla.org,air.mozilla.org,blog.mozilla.org,bugzilla.mozilla.org,developer.mozilla.org,etherpad.mozilla.org,forums.mozillazine.org,hacks.mozilla.org,hg.mozilla.org,mozilla.org,planet.mozilla.org,quality.mozilla.org,support.mozilla.org,treeherder.mozilla.org,wiki.mozilla.org".split(","); let imageURI = "https://image/"; let title = "the title"; let type = "affiliate"; let url = "http://test.url/"; let adgroup_name = "Mozilla"; let data = { suggested: [{ frecent_sites, imageURI, title, type, url, adgroup_name }] }; let dataURI = "data:application/json," + escape(JSON.stringify(data)); yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); // Wait for links to get loaded let gLinks = NewTabUtils.links; gLinks.addProvider(DirectoryLinksProvider); gLinks.populateCache(); yield new Promise(resolve => { NewTabUtils.allPages.register({ observe: _ => _, update() { NewTabUtils.allPages.unregister(this); resolve(); } }); }); // Make sure we get the expected attributes on the suggested tile let link = gLinks.getLinks()[0]; do_check_eq(link.imageURI, imageURI); do_check_eq(link.targetedName, "Mozilla"); do_check_eq(link.targetedSite, frecent_sites[0]); do_check_eq(link.title, title); do_check_eq(link.type, type); do_check_eq(link.url, url); // Cleanup. NewTabUtils.isTopPlacesSite = origIsTopPlacesSite; DirectoryLinksProvider._getCurrentTopSiteCount = origCurrentTopSiteCount; gLinks.removeProvider(DirectoryLinksProvider); DirectoryLinksProvider.removeObserver(gLinks); }); add_task(function* test_frequencyCappedSites_views() { Services.prefs.setCharPref(kPingUrlPref, ""); let origIsTopPlacesSite = NewTabUtils.isTopPlacesSite; NewTabUtils.isTopPlacesSite = () => true; let origCurrentTopSiteCount = DirectoryLinksProvider._getCurrentTopSiteCount; DirectoryLinksProvider._getCurrentTopSiteCount = () => 8; let testUrl = "http://frequency.capped/link"; let targets = ["top.site.com"]; let data = { suggested: [{ type: "affiliate", frecent_sites: targets, url: testUrl, frequency_caps: {daily: 5}, adgroup_name: "Test" }], directory: [{ type: "organic", url: "http://directory.site/" }] }; let dataURI = "data:application/json," + JSON.stringify(data); yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); // Wait for links to get loaded let gLinks = NewTabUtils.links; gLinks.addProvider(DirectoryLinksProvider); gLinks.populateCache(); yield new Promise(resolve => { NewTabUtils.allPages.register({ observe: _ => _, update() { NewTabUtils.allPages.unregister(this); resolve(); } }); }); function synthesizeAction(action) { DirectoryLinksProvider.reportSitesAction([{ link: { targetedSite: targets[0], url: testUrl } }], action, 0); } function checkFirstTypeAndLength(type, length) { let links = gLinks.getLinks(); do_check_eq(links[0].type, type); do_check_eq(links.length, length); } // Make sure we get 5 views of the link before it is removed checkFirstTypeAndLength("affiliate", 2); synthesizeAction("view"); checkFirstTypeAndLength("affiliate", 2); synthesizeAction("view"); checkFirstTypeAndLength("affiliate", 2); synthesizeAction("view"); checkFirstTypeAndLength("affiliate", 2); synthesizeAction("view"); checkFirstTypeAndLength("affiliate", 2); synthesizeAction("view"); checkFirstTypeAndLength("organic", 1); // Cleanup. NewTabUtils.isTopPlacesSite = origIsTopPlacesSite; DirectoryLinksProvider._getCurrentTopSiteCount = origCurrentTopSiteCount; gLinks.removeProvider(DirectoryLinksProvider); DirectoryLinksProvider.removeObserver(gLinks); Services.prefs.setCharPref(kPingUrlPref, kPingUrl); }); add_task(function* test_frequencyCappedSites_click() { Services.prefs.setCharPref(kPingUrlPref, ""); let origIsTopPlacesSite = NewTabUtils.isTopPlacesSite; NewTabUtils.isTopPlacesSite = () => true; let origCurrentTopSiteCount = DirectoryLinksProvider._getCurrentTopSiteCount; DirectoryLinksProvider._getCurrentTopSiteCount = () => 8; let testUrl = "http://frequency.capped/link"; let targets = ["top.site.com"]; let data = { suggested: [{ type: "affiliate", frecent_sites: targets, url: testUrl, adgroup_name: "Test" }], directory: [{ type: "organic", url: "http://directory.site/" }] }; let dataURI = "data:application/json," + JSON.stringify(data); yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); // Wait for links to get loaded let gLinks = NewTabUtils.links; gLinks.addProvider(DirectoryLinksProvider); gLinks.populateCache(); yield new Promise(resolve => { NewTabUtils.allPages.register({ observe: _ => _, update() { NewTabUtils.allPages.unregister(this); resolve(); } }); }); function synthesizeAction(action) { DirectoryLinksProvider.reportSitesAction([{ link: { targetedSite: targets[0], url: testUrl } }], action, 0); } function checkFirstTypeAndLength(type, length) { let links = gLinks.getLinks(); do_check_eq(links[0].type, type); do_check_eq(links.length, length); } // Make sure the link disappears after the first click checkFirstTypeAndLength("affiliate", 2); synthesizeAction("view"); checkFirstTypeAndLength("affiliate", 2); synthesizeAction("click"); checkFirstTypeAndLength("organic", 1); // Cleanup. NewTabUtils.isTopPlacesSite = origIsTopPlacesSite; DirectoryLinksProvider._getCurrentTopSiteCount = origCurrentTopSiteCount; gLinks.removeProvider(DirectoryLinksProvider); DirectoryLinksProvider.removeObserver(gLinks); Services.prefs.setCharPref(kPingUrlPref, kPingUrl); }); add_task(function* test_fetchAndCacheLinks_local() { yield DirectoryLinksProvider.init(); yield cleanJsonFile(); // Trigger cache of data or chrome uri files in profD yield DirectoryLinksProvider._fetchAndCacheLinks(kTestURL); let data = yield readJsonFile(); isIdentical(data, kURLData); }); add_task(function* test_fetchAndCacheLinks_remote() { yield DirectoryLinksProvider.init(); yield cleanJsonFile(); // this must trigger directory links json download and save it to cache file yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, kExampleURL + "%LOCALE%"); do_check_eq(gLastRequestPath, kExamplePath + "en-US"); let data = yield readJsonFile(); isIdentical(data, kHttpHandlerData[kExamplePath]); }); add_task(function* test_fetchAndCacheLinks_malformedURI() { yield DirectoryLinksProvider.init(); yield cleanJsonFile(); let someJunk = "some junk"; try { yield DirectoryLinksProvider._fetchAndCacheLinks(someJunk); do_throw("Malformed URIs should fail") } catch (e) { do_check_eq(e, "Error fetching " + someJunk) } // File should be empty. let data = yield readJsonFile(); isIdentical(data, ""); }); add_task(function* test_fetchAndCacheLinks_unknownHost() { yield DirectoryLinksProvider.init(); yield cleanJsonFile(); let nonExistentServer = "http://localhost:56789/"; try { yield DirectoryLinksProvider._fetchAndCacheLinks(nonExistentServer); do_throw("BAD URIs should fail"); } catch (e) { do_check_true(e.startsWith("Fetching " + nonExistentServer + " results in error code: ")) } // File should be empty. let data = yield readJsonFile(); isIdentical(data, ""); }); add_task(function* test_fetchAndCacheLinks_non200Status() { yield DirectoryLinksProvider.init(); yield cleanJsonFile(); yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, kFailURL); do_check_eq(gLastRequestPath, kFailPath); let data = yield readJsonFile(); isIdentical(data, {}); }); // To test onManyLinksChanged observer, trigger a fetch add_task(function* test_DirectoryLinksProvider__linkObservers() { yield DirectoryLinksProvider.init(); let testObserver = new LinksChangeObserver(); DirectoryLinksProvider.addObserver(testObserver); do_check_eq(DirectoryLinksProvider._observers.size, 1); DirectoryLinksProvider._fetchAndCacheLinksIfNecessary(true); yield testObserver.deferred.promise; DirectoryLinksProvider._removeObservers(); do_check_eq(DirectoryLinksProvider._observers.size, 0); yield promiseCleanDirectoryLinksProvider(); }); add_task(function* test_DirectoryLinksProvider__prefObserver_url() { yield promiseSetupDirectoryLinksProvider({linksURL: kTestURL}); let links = yield fetchData(); do_check_eq(links.length, 1); let expectedData = [{url: "http://example.com", title: "LocalSource", frecency: DIRECTORY_FRECENCY, lastVisitDate: 1}]; isIdentical(links, expectedData); // tests these 2 things: // 1. _linksURL is properly set after the pref change // 2. invalid source url is correctly handled let exampleUrl = 'http://localhost:56789/bad'; yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, exampleUrl); do_check_eq(DirectoryLinksProvider._linksURL, exampleUrl); // since the download fail, the directory file must remain the same let newLinks = yield fetchData(); isIdentical(newLinks, expectedData); // now remove the file, and re-download yield cleanJsonFile(); yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, exampleUrl + " "); // we now should see empty links newLinks = yield fetchData(); isIdentical(newLinks, []); yield promiseCleanDirectoryLinksProvider(); }); add_task(function* test_DirectoryLinksProvider_getLinks_noDirectoryData() { let data = { "directory": [], }; let dataURI = 'data:application/json,' + JSON.stringify(data); yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); let links = yield fetchData(); do_check_eq(links.length, 0); yield promiseCleanDirectoryLinksProvider(); }); add_task(function* test_DirectoryLinksProvider_getLinks_badData() { let data = { "en-US": { "en-US": [{url: "http://example.com", title: "US"}], }, }; let dataURI = 'data:application/json,' + JSON.stringify(data); yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); // Make sure we get nothing for incorrectly formatted data let links = yield fetchData(); do_check_eq(links.length, 0); yield promiseCleanDirectoryLinksProvider(); }); add_task(function test_DirectoryLinksProvider_needsDownload() { // test timestamping DirectoryLinksProvider._lastDownloadMS = 0; do_check_true(DirectoryLinksProvider._needsDownload); DirectoryLinksProvider._lastDownloadMS = Date.now(); do_check_false(DirectoryLinksProvider._needsDownload); DirectoryLinksProvider._lastDownloadMS = Date.now() - (60*60*24 + 1)*1000; do_check_true(DirectoryLinksProvider._needsDownload); DirectoryLinksProvider._lastDownloadMS = 0; }); add_task(function* test_DirectoryLinksProvider_fetchAndCacheLinksIfNecessary() { yield DirectoryLinksProvider.init(); yield cleanJsonFile(); // explicitly change source url to cause the download during setup yield promiseSetupDirectoryLinksProvider({linksURL: kTestURL+" "}); yield DirectoryLinksProvider._fetchAndCacheLinksIfNecessary(); // inspect lastDownloadMS timestamp which should be 5 seconds less then now() let lastDownloadMS = DirectoryLinksProvider._lastDownloadMS; do_check_true((Date.now() - lastDownloadMS) < 5000); // we should have fetched a new file during setup let data = yield readJsonFile(); isIdentical(data, kURLData); // attempt to download again - the timestamp should not change yield DirectoryLinksProvider._fetchAndCacheLinksIfNecessary(); do_check_eq(DirectoryLinksProvider._lastDownloadMS, lastDownloadMS); // clean the file and force the download yield cleanJsonFile(); yield DirectoryLinksProvider._fetchAndCacheLinksIfNecessary(true); data = yield readJsonFile(); isIdentical(data, kURLData); // make sure that failed download does not corrupt the file, nor changes lastDownloadMS lastDownloadMS = DirectoryLinksProvider._lastDownloadMS; yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, "http://"); yield DirectoryLinksProvider._fetchAndCacheLinksIfNecessary(true); data = yield readJsonFile(); isIdentical(data, kURLData); do_check_eq(DirectoryLinksProvider._lastDownloadMS, lastDownloadMS); // _fetchAndCacheLinksIfNecessary must return same promise if download is in progress let downloadPromise = DirectoryLinksProvider._fetchAndCacheLinksIfNecessary(true); let anotherPromise = DirectoryLinksProvider._fetchAndCacheLinksIfNecessary(true); do_check_true(downloadPromise === anotherPromise); yield downloadPromise; yield promiseCleanDirectoryLinksProvider(); }); add_task(function* test_DirectoryLinksProvider_fetchDirectoryOnPrefChange() { yield DirectoryLinksProvider.init(); let testObserver = new LinksChangeObserver(); DirectoryLinksProvider.addObserver(testObserver); yield cleanJsonFile(); // ensure that provider does not think it needs to download do_check_false(DirectoryLinksProvider._needsDownload); // change the source URL, which should force directory download yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, kExampleURL); // then wait for testObserver to fire and test that json is downloaded yield testObserver.deferred.promise; do_check_eq(gLastRequestPath, kExamplePath); let data = yield readJsonFile(); isIdentical(data, kHttpHandlerData[kExamplePath]); yield promiseCleanDirectoryLinksProvider(); }); add_task(function* test_DirectoryLinksProvider_fetchDirectoryOnShow() { yield promiseSetupDirectoryLinksProvider(); // set lastdownload to 0 to make DirectoryLinksProvider want to download DirectoryLinksProvider._lastDownloadMS = 0; do_check_true(DirectoryLinksProvider._needsDownload); // download should happen on view yield DirectoryLinksProvider.reportSitesAction([], "view"); do_check_true(DirectoryLinksProvider._lastDownloadMS != 0); yield promiseCleanDirectoryLinksProvider(); }); add_task(function* test_DirectoryLinksProvider_fetchDirectoryOnInit() { // ensure preferences are set to defaults yield promiseSetupDirectoryLinksProvider(); // now clean to provider, so we can init it again yield promiseCleanDirectoryLinksProvider(); yield cleanJsonFile(); yield DirectoryLinksProvider.init(); let data = yield readJsonFile(); isIdentical(data, kURLData); yield promiseCleanDirectoryLinksProvider(); }); add_task(function* test_DirectoryLinksProvider_getLinksFromCorruptedFile() { yield promiseSetupDirectoryLinksProvider(); // write bogus json to a file and attempt to fetch from it let directoryLinksFilePath = OS.Path.join(OS.Constants.Path.profileDir, DIRECTORY_LINKS_FILE); yield OS.File.writeAtomic(directoryLinksFilePath, '{"en-US":'); let data = yield fetchData(); isIdentical(data, []); yield promiseCleanDirectoryLinksProvider(); }); add_task(function* test_DirectoryLinksProvider_getAllowedLinks() { let data = {"directory": [ {url: "ftp://example.com"}, {url: "http://example.net"}, {url: "javascript:5"}, {url: "https://example.com"}, {url: "httpJUNKjavascript:42"}, {url: "data:text/plain,hi"}, {url: "http/bork:eh"}, ]}; let dataURI = 'data:application/json,' + JSON.stringify(data); yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); let links = yield fetchData(); do_check_eq(links.length, 2); // The only remaining url should be http and https do_check_eq(links[0].url, data["directory"][1].url); do_check_eq(links[1].url, data["directory"][3].url); }); add_task(function* test_DirectoryLinksProvider_getAllowedImages() { let data = {"directory": [ {url: "http://example.com", imageURI: "ftp://example.com"}, {url: "http://example.com", imageURI: "http://example.net"}, {url: "http://example.com", imageURI: "javascript:5"}, {url: "http://example.com", imageURI: "https://example.com"}, {url: "http://example.com", imageURI: "httpJUNKjavascript:42"}, {url: "http://example.com", imageURI: "data:text/plain,hi"}, {url: "http://example.com", imageURI: "http/bork:eh"}, ]}; let dataURI = 'data:application/json,' + JSON.stringify(data); yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); let links = yield fetchData(); do_check_eq(links.length, 2); // The only remaining images should be https and data do_check_eq(links[0].imageURI, data["directory"][3].imageURI); do_check_eq(links[1].imageURI, data["directory"][5].imageURI); }); add_task(function* test_DirectoryLinksProvider_getAllowedImages_base() { let data = {"directory": [ {url: "http://example1.com", imageURI: "https://example.com"}, {url: "http://example2.com", imageURI: "https://tiles.cdn.mozilla.net"}, {url: "http://example3.com", imageURI: "https://tiles2.cdn.mozilla.net"}, {url: "http://example4.com", enhancedImageURI: "https://mozilla.net"}, {url: "http://example5.com", imageURI: "data:text/plain,hi"}, ]}; let dataURI = 'data:application/json,' + JSON.stringify(data); yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); // Pretend we're using the default pref to trigger base matching DirectoryLinksProvider.__linksURLModified = false; let links = yield fetchData(); do_check_eq(links.length, 4); // The only remaining images should be https with mozilla.net or data URI do_check_eq(links[0].url, data["directory"][1].url); do_check_eq(links[1].url, data["directory"][2].url); do_check_eq(links[2].url, data["directory"][3].url); do_check_eq(links[3].url, data["directory"][4].url); }); add_task(function* test_DirectoryLinksProvider_getAllowedEnhancedImages() { let data = {"directory": [ {url: "http://example.com", enhancedImageURI: "ftp://example.com"}, {url: "http://example.com", enhancedImageURI: "http://example.net"}, {url: "http://example.com", enhancedImageURI: "javascript:5"}, {url: "http://example.com", enhancedImageURI: "https://example.com"}, {url: "http://example.com", enhancedImageURI: "httpJUNKjavascript:42"}, {url: "http://example.com", enhancedImageURI: "data:text/plain,hi"}, {url: "http://example.com", enhancedImageURI: "http/bork:eh"}, ]}; let dataURI = 'data:application/json,' + JSON.stringify(data); yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); let links = yield fetchData(); do_check_eq(links.length, 2); // The only remaining enhancedImages should be http and https and data do_check_eq(links[0].enhancedImageURI, data["directory"][3].enhancedImageURI); do_check_eq(links[1].enhancedImageURI, data["directory"][5].enhancedImageURI); }); add_task(function* test_DirectoryLinksProvider_getEnhancedLink() { let data = {"enhanced": [ {url: "http://example.net", enhancedImageURI: "data:,net1"}, {url: "http://example.com", enhancedImageURI: "data:,com1"}, {url: "http://example.com", enhancedImageURI: "data:,com2"}, ]}; let dataURI = 'data:application/json,' + JSON.stringify(data); yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); let links = yield fetchData(); do_check_eq(links.length, 0); // There are no directory links. function checkEnhanced(url, image) { let enhanced = DirectoryLinksProvider.getEnhancedLink({url: url}); do_check_eq(enhanced && enhanced.enhancedImageURI, image); } // Get the expected image for the same site checkEnhanced("http://example.net/", "data:,net1"); checkEnhanced("http://example.net/path", "data:,net1"); checkEnhanced("https://www.example.net/", "data:,net1"); checkEnhanced("https://www3.example.net/", "data:,net1"); // Get the image of the last entry checkEnhanced("http://example.com", "data:,com2"); // Get the inline enhanced image let inline = DirectoryLinksProvider.getEnhancedLink({ url: "http://example.com/echo", enhancedImageURI: "data:,echo", }); do_check_eq(inline.enhancedImageURI, "data:,echo"); do_check_eq(inline.url, "http://example.com/echo"); // Undefined for not enhanced checkEnhanced("http://sub.example.net/", undefined); checkEnhanced("http://example.org", undefined); checkEnhanced("http://localhost", undefined); checkEnhanced("http://127.0.0.1", undefined); // Make sure old data is not cached data = {"enhanced": [ {url: "http://example.com", enhancedImageURI: "data:,fresh"}, ]}; dataURI = 'data:application/json,' + JSON.stringify(data); yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); links = yield fetchData(); do_check_eq(links.length, 0); // There are no directory links. checkEnhanced("http://example.net", undefined); checkEnhanced("http://example.com", "data:,fresh"); }); add_task(function* test_DirectoryLinksProvider_enhancedURIs() { let origIsTopPlacesSite = NewTabUtils.isTopPlacesSite; NewTabUtils.isTopPlacesSite = () => true; let origCurrentTopSiteCount = DirectoryLinksProvider._getCurrentTopSiteCount; DirectoryLinksProvider._getCurrentTopSiteCount = () => 8; let data = { "suggested": [ {url: "http://example.net", enhancedImageURI: "data:,net1", title:"SuggestedTitle", adgroup_name: "Test", frecent_sites: ["test.com"]} ], "directory": [ {url: "http://example.net", enhancedImageURI: "data:,net2", title:"DirectoryTitle"} ] }; let dataURI = 'data:application/json,' + JSON.stringify(data); yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); // Wait for links to get loaded let gLinks = NewTabUtils.links; gLinks.addProvider(DirectoryLinksProvider); gLinks.populateCache(); yield new Promise(resolve => { NewTabUtils.allPages.register({ observe: _ => _, update() { NewTabUtils.allPages.unregister(this); resolve(); } }); }); // Check that we've saved the directory tile. let links = yield fetchData(); do_check_eq(links.length, 1); do_check_eq(links[0].title, "DirectoryTitle"); do_check_eq(links[0].enhancedImageURI, "data:,net2"); // Check that the suggested tile with the same URL replaces the directory tile. links = gLinks.getLinks(); do_check_eq(links.length, 1); do_check_eq(links[0].title, "SuggestedTitle"); do_check_eq(links[0].enhancedImageURI, "data:,net1"); // Cleanup. NewTabUtils.isTopPlacesSite = origIsTopPlacesSite; DirectoryLinksProvider._getCurrentTopSiteCount = origCurrentTopSiteCount; gLinks.removeProvider(DirectoryLinksProvider); }); add_task(function test_DirectoryLinksProvider_setDefaultEnhanced() { function checkDefault(expected) { Services.prefs.clearUserPref(kNewtabEnhancedPref); do_check_eq(Services.prefs.getBoolPref(kNewtabEnhancedPref), expected); } // Use the default donottrack prefs (enabled = false) Services.prefs.clearUserPref("privacy.donottrackheader.enabled"); checkDefault(true); // Turn on DNT - no track Services.prefs.setBoolPref("privacy.donottrackheader.enabled", true); checkDefault(false); // Turn off DNT header Services.prefs.clearUserPref("privacy.donottrackheader.enabled"); checkDefault(true); // Clean up Services.prefs.clearUserPref("privacy.donottrackheader.value"); }); add_task(function* test_timeSensetiveSuggestedTiles() { // make tile json with start and end dates let testStartTime = Date.now(); // start date is now + 1 seconds let startDate = new Date(testStartTime + 1000); // end date is now + 3 seconds let endDate = new Date(testStartTime + 3000); let suggestedTile = Object.assign({ time_limits: { start: startDate.toISOString(), end: endDate.toISOString(), } }, suggestedTile1); // Initial setup let topSites = ["site0.com", "1040.com", "site2.com", "hrblock.com", "site4.com", "freetaxusa.com", "site6.com"]; let data = {"suggested": [suggestedTile], "directory": [someOtherSite]}; let dataURI = 'data:application/json,' + JSON.stringify(data); let testObserver = new TestTimingRun(); DirectoryLinksProvider.addObserver(testObserver); yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); let links = yield fetchData(); let origIsTopPlacesSite = NewTabUtils.isTopPlacesSite; NewTabUtils.isTopPlacesSite = function(site) { return topSites.indexOf(site) >= 0; } let origGetProviderLinks = NewTabUtils.getProviderLinks; NewTabUtils.getProviderLinks = function(provider) { return links; } let origCurrentTopSiteCount = DirectoryLinksProvider._getCurrentTopSiteCount; DirectoryLinksProvider._getCurrentTopSiteCount = () => 8; do_check_eq(DirectoryLinksProvider._updateSuggestedTile(), undefined); // this tester will fire twice: when start limit is reached and when tile link // is removed upon end of the campaign, in which case deleteFlag will be set function TestTimingRun() { this.promise = new Promise(resolve => { this.onLinkChanged = (directoryLinksProvider, link, ignoreFlag, deleteFlag) => { // if we are not deleting, add link to links, so we can catch it's removal if (!deleteFlag) { links.unshift(link); } isIdentical([...DirectoryLinksProvider._topSitesWithSuggestedLinks], ["hrblock.com", "1040.com"]); do_check_eq(link.frecency, SUGGESTED_FRECENCY); do_check_eq(link.type, "affiliate"); do_check_eq(link.url, suggestedTile.url); let timeDelta = Date.now() - testStartTime; if (!deleteFlag) { // this is start timeout corresponding to campaign start // a seconds must pass and targetedSite must be set do_print("TESTING START timeDelta: " + timeDelta); do_check_true(timeDelta >= 1000 / 2); // check for at least half time do_check_eq(link.targetedSite, "hrblock.com"); do_check_true(DirectoryLinksProvider._campaignTimeoutID); } else { // this is the campaign end timeout, so 3 seconds must pass // and timeout should be cleared do_print("TESTING END timeDelta: " + timeDelta); do_check_true(timeDelta >= 3000 / 2); // check for at least half time do_check_false(link.targetedSite); do_check_false(DirectoryLinksProvider._campaignTimeoutID); resolve(); } }; }); } // _updateSuggestedTile() is called when fetching directory links. yield testObserver.promise; DirectoryLinksProvider.removeObserver(testObserver); // shoudl suggest nothing do_check_eq(DirectoryLinksProvider._updateSuggestedTile(), undefined); // set links back to contain directory tile only links.shift(); // drop the end time - we should pick up the tile suggestedTile.time_limits.end = null; data = {"suggested": [suggestedTile], "directory": [someOtherSite]}; dataURI = 'data:application/json,' + JSON.stringify(data); // redownload json and getLinks to force time recomputation yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, dataURI); // ensure that there's a link returned by _updateSuggestedTile and no timeout let deferred = Promise.defer(); DirectoryLinksProvider.getLinks(() => { let link = DirectoryLinksProvider._updateSuggestedTile(); // we should have a suggested tile and no timeout do_check_eq(link.type, "affiliate"); do_check_eq(link.url, suggestedTile.url); do_check_false(DirectoryLinksProvider._campaignTimeoutID); deferred.resolve(); }); yield deferred.promise; // repeat the test for end time only suggestedTile.time_limits.start = null; suggestedTile.time_limits.end = (new Date(Date.now() + 3000)).toISOString(); data = {"suggested": [suggestedTile], "directory": [someOtherSite]}; dataURI = 'data:application/json,' + JSON.stringify(data); // redownload json and call getLinks() to force time recomputation yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, dataURI); // ensure that there's a link returned by _updateSuggestedTile and timeout set deferred = Promise.defer(); DirectoryLinksProvider.getLinks(() => { let link = DirectoryLinksProvider._updateSuggestedTile(); // we should have a suggested tile and timeout set do_check_eq(link.type, "affiliate"); do_check_eq(link.url, suggestedTile.url); do_check_true(DirectoryLinksProvider._campaignTimeoutID); DirectoryLinksProvider._clearCampaignTimeout(); deferred.resolve(); }); yield deferred.promise; // Cleanup yield promiseCleanDirectoryLinksProvider(); NewTabUtils.isTopPlacesSite = origIsTopPlacesSite; NewTabUtils.getProviderLinks = origGetProviderLinks; DirectoryLinksProvider._getCurrentTopSiteCount = origCurrentTopSiteCount; }); add_task(function test_setupStartEndTime() { let currentTime = Date.now(); let dt = new Date(currentTime); let link = { time_limits: { start: dt.toISOString() } }; // test ISO translation DirectoryLinksProvider._setupStartEndTime(link); do_check_eq(link.startTime, currentTime); // test localtime translation let shiftedDate = new Date(currentTime - dt.getTimezoneOffset()*60*1000); link.time_limits.start = shiftedDate.toISOString().replace(/Z$/, ""); DirectoryLinksProvider._setupStartEndTime(link); do_check_eq(link.startTime, currentTime); // throw some garbage into date string delete link.startTime; link.time_limits.start = "no date" DirectoryLinksProvider._setupStartEndTime(link); do_check_false(link.startTime); link.time_limits.start = "2015-99999-01T00:00:00" DirectoryLinksProvider._setupStartEndTime(link); do_check_false(link.startTime); link.time_limits.start = "20150501T00:00:00" DirectoryLinksProvider._setupStartEndTime(link); do_check_false(link.startTime); }); add_task(function* test_DirectoryLinksProvider_frequencyCapSetup() { yield promiseSetupDirectoryLinksProvider(); yield DirectoryLinksProvider.init(); yield promiseCleanDirectoryLinksProvider(); yield DirectoryLinksProvider._readFrequencyCapFile(); isIdentical(DirectoryLinksProvider._frequencyCaps, {}); // setup few links DirectoryLinksProvider._updateFrequencyCapSettings({ url: "1", }); DirectoryLinksProvider._updateFrequencyCapSettings({ url: "2", frequency_caps: {daily: 1, total: 2} }); DirectoryLinksProvider._updateFrequencyCapSettings({ url: "3", frequency_caps: {total: 2} }); DirectoryLinksProvider._updateFrequencyCapSettings({ url: "4", frequency_caps: {daily: 1} }); let freqCapsObject = DirectoryLinksProvider._frequencyCaps; let capObject = freqCapsObject["1"]; let defaultDaily = capObject.dailyCap; let defaultTotal = capObject.totalCap; // check if we have defaults set do_check_true(capObject.dailyCap > 0); do_check_true(capObject.totalCap > 0); // check if defaults are properly handled do_check_eq(freqCapsObject["2"].dailyCap, 1); do_check_eq(freqCapsObject["2"].totalCap, 2); do_check_eq(freqCapsObject["3"].dailyCap, defaultDaily); do_check_eq(freqCapsObject["3"].totalCap, 2); do_check_eq(freqCapsObject["4"].dailyCap, 1); do_check_eq(freqCapsObject["4"].totalCap, defaultTotal); // write object to file yield DirectoryLinksProvider._writeFrequencyCapFile(); // empty out freqCapsObject and read file back DirectoryLinksProvider._frequencyCaps = {}; yield DirectoryLinksProvider._readFrequencyCapFile(); // re-ran tests - they should all pass do_check_eq(freqCapsObject["2"].dailyCap, 1); do_check_eq(freqCapsObject["2"].totalCap, 2); do_check_eq(freqCapsObject["3"].dailyCap, defaultDaily); do_check_eq(freqCapsObject["3"].totalCap, 2); do_check_eq(freqCapsObject["4"].dailyCap, 1); do_check_eq(freqCapsObject["4"].totalCap, defaultTotal); // wait a second and prune frequency caps yield new Promise(resolve => { setTimeout(resolve, 1100); }); // update one link and create another DirectoryLinksProvider._updateFrequencyCapSettings({ url: "3", frequency_caps: {daily: 1, total: 2} }); DirectoryLinksProvider._updateFrequencyCapSettings({ url: "7", frequency_caps: {daily: 1, total: 2} }); // now prune the ones that have been in the object longer than 1 second DirectoryLinksProvider._pruneFrequencyCapUrls(1000); // make sure all keys but "3" and "7" are deleted Object.keys(DirectoryLinksProvider._frequencyCaps).forEach(key => { do_check_true(key == "3" || key == "7"); }); yield promiseCleanDirectoryLinksProvider(); }); add_task(function* test_DirectoryLinksProvider_getFrequencyCapLogic() { yield promiseSetupDirectoryLinksProvider(); yield DirectoryLinksProvider.init(); // setup suggested links DirectoryLinksProvider._updateFrequencyCapSettings({ url: "1", frequency_caps: {daily: 2, total: 4} }); do_check_true(DirectoryLinksProvider._testFrequencyCapLimits("1")); // exhaust daily views DirectoryLinksProvider._addFrequencyCapView("1") do_check_true(DirectoryLinksProvider._testFrequencyCapLimits("1")); DirectoryLinksProvider._addFrequencyCapView("1") do_check_false(DirectoryLinksProvider._testFrequencyCapLimits("1")); // now step into the furture let _wasTodayOrig = DirectoryLinksProvider._wasToday; DirectoryLinksProvider._wasToday = function () { return false; } // exhaust total views DirectoryLinksProvider._addFrequencyCapView("1") do_check_true(DirectoryLinksProvider._testFrequencyCapLimits("1")); DirectoryLinksProvider._addFrequencyCapView("1") // reached totalViews 4, should return false do_check_false(DirectoryLinksProvider._testFrequencyCapLimits("1")); // add more views by updating configuration DirectoryLinksProvider._updateFrequencyCapSettings({ url: "1", frequency_caps: {daily: 5, total: 10} }); // should be true, since we have more total views do_check_true(DirectoryLinksProvider._testFrequencyCapLimits("1")); // set click flag DirectoryLinksProvider._setFrequencyCapClick("1"); // always false after click do_check_false(DirectoryLinksProvider._testFrequencyCapLimits("1")); // use unknown urls and ensure nothing breaks DirectoryLinksProvider._addFrequencyCapView("nosuch.url"); DirectoryLinksProvider._setFrequencyCapClick("nosuch.url"); // testing unknown url should always return false do_check_false(DirectoryLinksProvider._testFrequencyCapLimits("nosuch.url")); // reset _wasToday back to original function DirectoryLinksProvider._wasToday = _wasTodayOrig; yield promiseCleanDirectoryLinksProvider(); }); add_task(function* test_DirectoryLinksProvider_getFrequencyCapReportSiteAction() { yield promiseSetupDirectoryLinksProvider(); yield DirectoryLinksProvider.init(); // setup suggested links DirectoryLinksProvider._updateFrequencyCapSettings({ url: "bar.com", frequency_caps: {daily: 2, total: 4} }); do_check_true(DirectoryLinksProvider._testFrequencyCapLimits("bar.com")); // report site action yield DirectoryLinksProvider.reportSitesAction([{ link: { targetedSite: "foo.com", url: "bar.com" }, isPinned: function() { return false; }, }], "view", 0); // read file content and ensure that view counters are updated let data = yield readJsonFile(DirectoryLinksProvider._frequencyCapFilePath); do_check_eq(data["bar.com"].dailyViews, 1); do_check_eq(data["bar.com"].totalViews, 1); yield promiseCleanDirectoryLinksProvider(); }); add_task(function* test_DirectoryLinksProvider_ClickRemoval() { yield promiseSetupDirectoryLinksProvider(); yield DirectoryLinksProvider.init(); let landingUrl = "http://foo.com"; // setup suggested links DirectoryLinksProvider._updateFrequencyCapSettings({ url: landingUrl, frequency_caps: {daily: 2, total: 4} }); // add views DirectoryLinksProvider._addFrequencyCapView(landingUrl) DirectoryLinksProvider._addFrequencyCapView(landingUrl) // make a click DirectoryLinksProvider._setFrequencyCapClick(landingUrl); // views must be 2 and click must be set do_check_eq(DirectoryLinksProvider._frequencyCaps[landingUrl].totalViews, 2); do_check_true(DirectoryLinksProvider._frequencyCaps[landingUrl].clicked); // now insert a visit into places yield new Promise(resolve => { PlacesUtils.asyncHistory.updatePlaces( { uri: NetUtil.newURI(landingUrl), title: "HELLO", visits: [{ visitDate: Date.now()*1000, transitionType: Ci.nsINavHistoryService.TRANSITION_LINK }] }, { handleError: function () { do_check_true(false); }, handleResult: function () {}, handleCompletion: function () { resolve(); } } ); }); function UrlDeletionTester() { this.promise = new Promise(resolve => { this.onDeleteURI = (directoryLinksProvider, link) => { resolve(); }; this.onClearHistory = (directoryLinksProvider) => { resolve(); }; }); } let testObserver = new UrlDeletionTester(); DirectoryLinksProvider.addObserver(testObserver); PlacesUtils.bhistory.removePage(NetUtil.newURI(landingUrl)); yield testObserver.promise; DirectoryLinksProvider.removeObserver(testObserver); // views must be 2 and click should not exist do_check_eq(DirectoryLinksProvider._frequencyCaps[landingUrl].totalViews, 2); do_check_false(DirectoryLinksProvider._frequencyCaps[landingUrl].hasOwnProperty("clicked")); // verify that disk written data is kosher let data = yield readJsonFile(DirectoryLinksProvider._frequencyCapFilePath); do_check_eq(data[landingUrl].totalViews, 2); do_check_false(data[landingUrl].hasOwnProperty("clicked")); // now test clear history DirectoryLinksProvider._updateFrequencyCapSettings({ url: landingUrl, frequency_caps: {daily: 2, total: 4} }); DirectoryLinksProvider._updateFrequencyCapSettings({ url: "http://bar.com", frequency_caps: {daily: 2, total: 4} }); DirectoryLinksProvider._setFrequencyCapClick(landingUrl); DirectoryLinksProvider._setFrequencyCapClick("http://bar.com"); // both tiles must have clicked do_check_true(DirectoryLinksProvider._frequencyCaps[landingUrl].clicked); do_check_true(DirectoryLinksProvider._frequencyCaps["http://bar.com"].clicked); testObserver = new UrlDeletionTester(); DirectoryLinksProvider.addObserver(testObserver); yield PlacesTestUtils.clearHistory(); yield testObserver.promise; DirectoryLinksProvider.removeObserver(testObserver); // no clicks should remain in the cap object do_check_false(DirectoryLinksProvider._frequencyCaps[landingUrl].hasOwnProperty("clicked")); do_check_false(DirectoryLinksProvider._frequencyCaps["http://bar.com"].hasOwnProperty("clicked")); // verify that disk written data is kosher data = yield readJsonFile(DirectoryLinksProvider._frequencyCapFilePath); do_check_false(data[landingUrl].hasOwnProperty("clicked")); do_check_false(data["http://bar.com"].hasOwnProperty("clicked")); yield promiseCleanDirectoryLinksProvider(); }); add_task(function test_DirectoryLinksProvider_anonymous() { do_check_true(DirectoryLinksProvider._newXHR().mozAnon); }); add_task(function* test_sanitizeExplanation() { // Note: this is a basic test to ensure we applied sanitization to the link explanation. // Full testing for appropriate sanitization is done in parser/xml/test/unit/test_sanitizer.js. let data = {"suggested": [suggestedTile5]}; let dataURI = 'data:application/json,' + encodeURIComponent(JSON.stringify(data)); yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); yield fetchData(); let suggestedSites = [...DirectoryLinksProvider._suggestedLinks.keys()]; do_check_eq(suggestedSites.indexOf("eviltarget.com"), 0); do_check_eq(suggestedSites.length, 1); let suggestedLink = [...DirectoryLinksProvider._suggestedLinks.get(suggestedSites[0]).values()][0]; do_check_eq(suggestedLink.explanation, "This is an evil tile X muhahaha"); do_check_eq(suggestedLink.targetedName, "WE ARE EVIL "); }); add_task(function* test_inadjecentSites() { let suggestedTile = Object.assign({ check_inadjacency: true }, suggestedTile1); // Initial setup let topSites = ["1040.com", "site2.com", "hrblock.com", "site4.com", "freetaxusa.com", "site6.com"]; let data = {"suggested": [suggestedTile], "directory": [someOtherSite]}; let dataURI = 'data:application/json,' + JSON.stringify(data); let testObserver = new TestFirstRun(); DirectoryLinksProvider.addObserver(testObserver); yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); let links = yield fetchData(); let origIsTopPlacesSite = NewTabUtils.isTopPlacesSite; NewTabUtils.isTopPlacesSite = function(site) { return topSites.indexOf(site) >= 0; } let origGetProviderLinks = NewTabUtils.getProviderLinks; NewTabUtils.getProviderLinks = function(provider) { return links; } let origCurrentTopSiteCount = DirectoryLinksProvider._getCurrentTopSiteCount; DirectoryLinksProvider._getCurrentTopSiteCount = () => { origCurrentTopSiteCount.apply(DirectoryLinksProvider); return 8; }; // store oroginal inadjacent sites url let origInadjacentSitesUrl = DirectoryLinksProvider._inadjacentSitesUrl; // loading inadjacent sites list function function setInadjacentSites(sites) { let badSiteB64 = []; sites.forEach(site => { badSiteB64.push(DirectoryLinksProvider._generateHash(site)); }); let theList = {"domains": badSiteB64}; let uri = 'data:application/json,' + JSON.stringify(theList); DirectoryLinksProvider._inadjacentSitesUrl = uri; return DirectoryLinksProvider._loadInadjacentSites(); } // setup gLinks loader let gLinks = NewTabUtils.links; gLinks.addProvider(DirectoryLinksProvider); function updateNewTabCache() { gLinks.populateCache(); return new Promise(resolve => { NewTabUtils.allPages.register({ observe: _ => _, update() { NewTabUtils.allPages.unregister(this); resolve(); } }); }); } // no suggested file do_check_eq(DirectoryLinksProvider._updateSuggestedTile(), undefined); // _avoidInadjacentSites should be set, since link.check_inadjacency is on do_check_true(DirectoryLinksProvider._avoidInadjacentSites); // make sure example.com is included in inadjacent sites list do_check_true(DirectoryLinksProvider._isInadjacentLink({baseDomain: "example.com"})); function TestFirstRun() { this.promise = new Promise(resolve => { this.onLinkChanged = (directoryLinksProvider, link) => { do_check_eq(link.url, suggestedTile.url); do_check_eq(link.type, "affiliate"); resolve(); }; }); } // Test first call to '_updateSuggestedTile()', called when fetching directory links. yield testObserver.promise; DirectoryLinksProvider.removeObserver(testObserver); // update newtab cache yield updateNewTabCache(); // this should have set do_check_true(DirectoryLinksProvider._avoidInadjacentSites); // there should be siggested link let link = DirectoryLinksProvider._updateSuggestedTile(); do_check_eq(link.url, "http://turbotax.com"); // and it should have avoidInadjacentSites flag do_check_true(link.check_inadjacency); // make someothersite.com inadjacent yield setInadjacentSites(["someothersite.com"]); // there should be no suggested link link = DirectoryLinksProvider._updateSuggestedTile(); do_check_false(link); do_check_true(DirectoryLinksProvider._newTabHasInadjacentSite); // _handleLinkChanged must return true on inadjacent site do_check_true(DirectoryLinksProvider._handleLinkChanged({ url: "http://someothersite.com", type: "history", })); // _handleLinkChanged must return false on ok site do_check_false(DirectoryLinksProvider._handleLinkChanged({ url: "http://foobar.com", type: "history", })); // change inadjacent list to sites not on newtab page yield setInadjacentSites(["foo.com", "bar.com"]); link = DirectoryLinksProvider._updateSuggestedTile(); // we should now have a link do_check_true(link); do_check_eq(link.url, "http://turbotax.com"); // make newtab offending again yield setInadjacentSites(["someothersite.com", "foo.com"]); // there should be no suggested link link = DirectoryLinksProvider._updateSuggestedTile(); do_check_false(link); do_check_true(DirectoryLinksProvider._newTabHasInadjacentSite); // remove avoidInadjacentSites flag from suggested tile and reload json delete suggestedTile.check_inadjacency; data = {"suggested": [suggestedTile], "directory": [someOtherSite]}; dataURI = 'data:application/json,' + JSON.stringify(data); yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, dataURI); yield fetchData(); // inadjacent checking should be disabled do_check_false(DirectoryLinksProvider._avoidInadjacentSites); link = DirectoryLinksProvider._updateSuggestedTile(); do_check_true(link); do_check_eq(link.url, "http://turbotax.com"); do_check_false(DirectoryLinksProvider._newTabHasInadjacentSite); // _handleLinkChanged should return false now, even if newtab has bad site do_check_false(DirectoryLinksProvider._handleLinkChanged({ url: "http://someothersite.com", type: "history", })); // test _isInadjacentLink do_check_true(DirectoryLinksProvider._isInadjacentLink({baseDomain: "someothersite.com"})); do_check_false(DirectoryLinksProvider._isInadjacentLink({baseDomain: "bar.com"})); do_check_true(DirectoryLinksProvider._isInadjacentLink({url: "http://www.someothersite.com"})); do_check_false(DirectoryLinksProvider._isInadjacentLink({url: "http://www.bar.com"})); // try to crash _isInadjacentLink do_check_false(DirectoryLinksProvider._isInadjacentLink({baseDomain: ""})); do_check_false(DirectoryLinksProvider._isInadjacentLink({url: ""})); do_check_false(DirectoryLinksProvider._isInadjacentLink({url: "http://localhost:8081/"})); do_check_false(DirectoryLinksProvider._isInadjacentLink({url: "abracodabra"})); do_check_false(DirectoryLinksProvider._isInadjacentLink({})); // test _checkForInadjacentSites do_check_true(DirectoryLinksProvider._checkForInadjacentSites()); // Cleanup gLinks.removeProvider(DirectoryLinksProvider); DirectoryLinksProvider._inadjacentSitesUrl = origInadjacentSitesUrl; NewTabUtils.isTopPlacesSite = origIsTopPlacesSite; NewTabUtils.getProviderLinks = origGetProviderLinks; DirectoryLinksProvider._getCurrentTopSiteCount = origCurrentTopSiteCount; yield promiseCleanDirectoryLinksProvider(); }); add_task(function* test_blockSuggestedTiles() { // Initial setup let topSites = ["site0.com", "1040.com", "site2.com", "hrblock.com", "site4.com", "freetaxusa.com", "site6.com"]; let data = {"suggested": [suggestedTile1, suggestedTile2, suggestedTile3], "directory": [someOtherSite]}; let dataURI = 'data:application/json,' + JSON.stringify(data); yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); let links = yield fetchData(); let origIsTopPlacesSite = NewTabUtils.isTopPlacesSite; NewTabUtils.isTopPlacesSite = function(site) { return topSites.indexOf(site) >= 0; } let origGetProviderLinks = NewTabUtils.getProviderLinks; NewTabUtils.getProviderLinks = function(provider) { return links; } let origCurrentTopSiteCount = DirectoryLinksProvider._getCurrentTopSiteCount; DirectoryLinksProvider._getCurrentTopSiteCount = () => 8; // load the links yield new Promise(resolve => { DirectoryLinksProvider.getLinks(resolve); }); // ensure that tile is suggested let suggestedLink = DirectoryLinksProvider._updateSuggestedTile(); do_check_true(suggestedLink.frecent_sites); // block suggested tile in a regular way DirectoryLinksProvider.reportSitesAction([{ isPinned: function() { return false; }, link: Object.assign({frecency: 1000}, suggestedLink) }], "block", 0); // suggested tile still must be recommended suggestedLink = DirectoryLinksProvider._updateSuggestedTile(); do_check_true(suggestedLink.frecent_sites); // timestamp suggested_block in the frequency cap object DirectoryLinksProvider.handleSuggestedTileBlock(); // no more recommendations should be seen do_check_eq(DirectoryLinksProvider._updateSuggestedTile(), undefined); // move lastUpdated for suggested tile into the past DirectoryLinksProvider._frequencyCaps["ignore://suggested_block"].lastUpdated = Date.now() - 25*60*60*1000; // ensure that suggested tile updates again suggestedLink = DirectoryLinksProvider._updateSuggestedTile(); do_check_true(suggestedLink.frecent_sites); // Cleanup yield promiseCleanDirectoryLinksProvider(); NewTabUtils.isTopPlacesSite = origIsTopPlacesSite; NewTabUtils.getProviderLinks = origGetProviderLinks; DirectoryLinksProvider._getCurrentTopSiteCount = origCurrentTopSiteCount; });