diff options
Diffstat (limited to 'browser/modules/test/xpcshell/test_DirectoryLinksProvider.js')
-rw-r--r-- | browser/modules/test/xpcshell/test_DirectoryLinksProvider.js | 1854 |
1 files changed, 1854 insertions, 0 deletions
diff --git a/browser/modules/test/xpcshell/test_DirectoryLinksProvider.js b/browser/modules/test/xpcshell/test_DirectoryLinksProvider.js new file mode 100644 index 000000000..712f52fa6 --- /dev/null +++ b/browser/modules/test/xpcshell/test_DirectoryLinksProvider.js @@ -0,0 +1,1854 @@ +/* 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 <form><button formaction='javascript:alert(1)''>X</button></form> muhahaha", + adgroup_name: "WE ARE EVIL <link rel='import' href='test.svg'/>", + 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; +}); |