diff options
Diffstat (limited to '')
-rw-r--r-- | src/tests/tests/background.js | 467 | ||||
-rw-r--r-- | src/tests/tests/baseDomain.js | 255 | ||||
-rw-r--r-- | src/tests/tests/firstparties.js | 167 | ||||
-rw-r--r-- | src/tests/tests/heuristic.js | 165 | ||||
-rw-r--r-- | src/tests/tests/htmlutils.js | 241 | ||||
-rw-r--r-- | src/tests/tests/multiDomainFirstParties.js | 73 | ||||
-rw-r--r-- | src/tests/tests/options.js | 105 | ||||
-rw-r--r-- | src/tests/tests/storage.js | 638 | ||||
-rw-r--r-- | src/tests/tests/tabData.js | 310 | ||||
-rw-r--r-- | src/tests/tests/utils.js | 550 | ||||
-rw-r--r-- | src/tests/tests/yellowlist.js | 424 |
11 files changed, 3395 insertions, 0 deletions
diff --git a/src/tests/tests/background.js b/src/tests/tests/background.js new file mode 100644 index 0000000..149db42 --- /dev/null +++ b/src/tests/tests/background.js @@ -0,0 +1,467 @@ +/* globals badger:false */ + +(function () { + +const DNT_COMPLIANT_DOMAIN = 'eff.org', + DNT_DOMAINS = [ + DNT_COMPLIANT_DOMAIN, + 'dnt2.example', + 'dnt3.example', + 'dnt4.example', + 'dnt5.example', + ], + POLICY_URL = chrome.runtime.getURL('data/dnt-policy.txt'); + +let utils = require('utils'), + constants = require('constants'), + migrations = require('migrations').Migrations, + mdfp = require('multiDomainFP'); + +let clock, + server, + xhrSpy, + dnt_policy_txt; + +function setupBadgerStorage(badger) { + // add foo.com, allowed as seen tracking on only one site + badger.storage.action_map.setItem('foo.com', { + dnt: false, + heuristicAction: constants.ALLOW, + nextUpdateTime: 100, + userAction: "" + }); + badger.storage.snitch_map.setItem('foo.com', ['a.co']); + + // add sub.bar.com, + // blocked after having been recorded tracking on three sites + badger.storage.action_map.setItem('bar.com', { + dnt: false, + heuristicAction: constants.BLOCK, + nextUpdateTime: 100, + userAction: "" + }); + badger.storage.action_map.setItem('sub.bar.com', { + dnt: false, + heuristicAction: constants.BLOCK, + nextUpdateTime: 100, + userAction: "" + }); + badger.storage.snitch_map.setItem('bar.com', ['a.co', 'b.co', 'c.co']); +} + +QUnit.module("Background", { + before: (assert) => { + let done = assert.async(); + + // fetch locally stored DNT policy + utils.xhrRequest(POLICY_URL, function (err, data) { + dnt_policy_txt = data; + + // set up fake server to simulate XMLHttpRequests + server = sinon.fakeServer.create({ + respondImmediately: true + }); + DNT_DOMAINS.forEach(domain => { + server.respondWith( + "GET", + "https://" + domain + "/.well-known/dnt-policy.txt", + [200, {}, dnt_policy_txt] + ); + }); + + // set up fake timers to simulate window.setTimeout and co. + clock = sinon.useFakeTimers(+new Date()); + + done(); + }); + }, + + beforeEach: (/*assert*/) => { + // spy on utils.xhrRequest + xhrSpy = sinon.spy(utils, "xhrRequest"); + }, + + afterEach: (/*assert*/) => { + // reset call counts, etc. after each test + utils.xhrRequest.restore(); + }, + + after: (/*assert*/) => { + clock.restore(); + server.restore(); + } +}); + +QUnit.test("DNT policy checking", (assert) => { + const NUM_TESTS = 2, + done = assert.async(NUM_TESTS); + + assert.expect(NUM_TESTS); + + badger.checkForDNTPolicy(DNT_COMPLIANT_DOMAIN, function (successStatus) { + assert.ok(successStatus, "Domain returns good DNT policy"); + done(); + }); + + badger.checkForDNTPolicy('ecorp.example', function (successStatus) { + assert.notOk(successStatus, "Domain returns 200 but no valid policy"); + done(); + }); + + // advance the clock enough to trigger all rate-limited calls + clock.tick(constants.DNT_POLICY_CHECK_INTERVAL * NUM_TESTS); +}); + +QUnit.test("Several checks for same domain resolve to one XHR", (assert) => { + const NUM_CHECKS = 5; + + // set recheck time to now + badger.storage.touchDNTRecheckTime(DNT_COMPLIANT_DOMAIN, +new Date()); + + for (let i = 0; i < NUM_CHECKS; i++) { + badger.checkForDNTPolicy(DNT_COMPLIANT_DOMAIN); + } + + // advance the clock + clock.tick(constants.DNT_POLICY_CHECK_INTERVAL * NUM_CHECKS); + + assert.equal(xhrSpy.callCount, 1, "XHR method gets called exactly once"); + assert.equal( + xhrSpy.getCall(0).args[0], + "https://" + DNT_COMPLIANT_DOMAIN + "/.well-known/dnt-policy.txt", + "XHR method gets called with expected DNT URL" + ); +}); + +QUnit.test("DNT checking is rate limited", (assert) => { + const NUM_TESTS = DNT_DOMAINS.length; + + let done = assert.async(NUM_TESTS); + + assert.expect(NUM_TESTS); + + for (let i = 0; i < NUM_TESTS; i++) { + badger.checkForDNTPolicy( + DNT_DOMAINS[i], + function () { // eslint-disable-line no-loop-func + assert.equal(xhrSpy.callCount, i+1); + clock.tick(constants.DNT_POLICY_CHECK_INTERVAL); + done(); + } + ); + } +}); + +QUnit.test("DNT checking obeys user setting", (assert) => { + const NUM_TESTS = DNT_DOMAINS.length; + + let done = assert.async(NUM_TESTS); + let old_dnt_check_func = badger.isCheckingDNTPolicyEnabled; + + assert.expect(NUM_TESTS); + badger.isCheckingDNTPolicyEnabled = () => false; + + for (let i = 0; i < NUM_TESTS; i++) { + badger.checkForDNTPolicy(DNT_DOMAINS[i]); + clock.tick(constants.DNT_POLICY_CHECK_INTERVAL); + assert.equal(xhrSpy.callCount, 0); + done(); + } + + badger.isCheckingDNTPolicyEnabled = old_dnt_check_func; +}); + +// test #1972 +QUnit.test("mergeUserData does not unblock formerly blocked domains", (assert) => { + setupBadgerStorage(badger); + + const SITE_DOMAINS = ['a.co', 'b.co', 'c.co'], + USER_DATA = { + action_map: { + 'foo.com': { + dnt: false, + heuristicAction: constants.BLOCK, + nextUpdateTime: 100, + userAction: "" + } + }, + snitch_map: { + 'foo.com': SITE_DOMAINS + }, + settings_map: { + migrationLevel: 0 + } + }; + + badger.mergeUserData(USER_DATA); + + assert.equal( + badger.storage.action_map.getItem('foo.com').heuristicAction, + constants.BLOCK, + "foo.com was blocked" + ); + assert.deepEqual( + badger.storage.snitch_map.getItem('foo.com'), + SITE_DOMAINS, + "snitch map was migrated" + ); + + badger.runMigrations(); + + assert.equal( + badger.storage.action_map.getItem('foo.com').heuristicAction, + constants.BLOCK, + "foo.com is still blocked after running migrations" + ); +}); + +QUnit.test("user-blocked domains keep their tracking history", (assert) => { + const SITE_DOMAINS = ['a.co', 'b.co'], + USER_DATA = { + action_map: { + 'foo.com': { + dnt: false, + heuristicAction: constants.ALLOW, + nextUpdateTime: 100, + userAction: constants.USER_BLOCK + } + }, + snitch_map: { + 'foo.com': SITE_DOMAINS + } + }; + + badger.mergeUserData(USER_DATA); + + assert.equal( + badger.storage.getAction('foo.com'), + constants.USER_BLOCK, + "foo.com was blocked" + ); + assert.deepEqual( + badger.storage.snitch_map.getItem('foo.com'), + SITE_DOMAINS, + "snitch map was migrated" + ); +}); + +QUnit.test("merging snitch maps results in a blocked domain", (assert) => { + setupBadgerStorage(badger); + + // https://github.com/EFForg/privacybadger/pull/2082#issuecomment-401942070 + const USER_DATA = { + action_map: { + 'foo.com': { + dnt: false, + heuristicAction: constants.ALLOW, + nextUpdateTime: 100, + userAction: "" + } + }, + snitch_map: {'foo.com': ['b.co', 'c.co']}, + }; + + badger.mergeUserData(USER_DATA); + + assert.equal( + badger.storage.action_map.getItem('foo.com').heuristicAction, + constants.BLOCK, + "foo.com was blocked" + ); + assert.deepEqual( + badger.storage.snitch_map.getItem('foo.com'), + ['a.co', 'b.co', 'c.co'], + "snitch map was combined" + ); +}); + +QUnit.test("subdomain that is not blocked does not override subdomain that is", (assert) => { + setupBadgerStorage(badger); + + const USER_DATA = { + action_map: { + 'sub.bar.com': { + dnt: false, + heuristicAction: constants.ALLOW, + nextUpdateTime: 100, + userAction: "" + } + }, + snitch_map: {'bar.com': ['a.co']} + }; + + badger.mergeUserData(USER_DATA); + + assert.equal( + badger.storage.action_map.getItem('sub.bar.com').heuristicAction, + constants.BLOCK, + "sub.bar.com is still blocked" + ); + assert.deepEqual( + badger.storage.snitch_map.getItem('bar.com'), + ['a.co', 'b.co', 'c.co'], + "snitch map was preserved" + ); +}); + +QUnit.test("subdomains on the yellowlist are preserved", (assert) => { + const DOMAIN = "example.com", + SUBDOMAIN = "cdn.example.com", + USER_DATA = { + action_map: { + [DOMAIN]: { + dnt: false, + heuristicAction: constants.BLOCK, + nextUpdateTime: 100, + userAction: '' + }, + [SUBDOMAIN]: { + dnt: false, + heuristicAction: constants.ALLOW, + nextUpdateTime: 0, + userAction: '' + } + }, + snitch_map: { + [DOMAIN]: ['a.co', 'b.co', 'c.co'], + } + }; + + const actionMap = badger.storage.getStore('action_map'), + snitchMap = badger.storage.getStore('snitch_map'); + + // merge in a blocked parent domain and a subdomain + badger.mergeUserData(USER_DATA); + + assert.notOk(actionMap.getItem(SUBDOMAIN), + SUBDOMAIN + " should have been discarded during merge" + ); + + // clean up + actionMap.deleteItem(DOMAIN); + actionMap.deleteItem(SUBDOMAIN); + snitchMap.deleteItem(DOMAIN); + + // now add subdomain to yellowlist + badger.storage.getStore('cookieblock_list') + .setItem(SUBDOMAIN, true); + + // and do the merge again + badger.mergeUserData(USER_DATA); + + assert.ok(actionMap.getItem(SUBDOMAIN), + SUBDOMAIN + " should be present in action_map" + ); + assert.equal( + actionMap.getItem(SUBDOMAIN).heuristicAction, + constants.COOKIEBLOCK, + SUBDOMAIN + " should be cookieblocked" + ); +}); + +QUnit.test("forgetFirstPartySnitches migration properly handles snitch entries with no MDFP entries", (assert) => { + const actionMap = badger.storage.getStore('action_map'), + snitchMap = badger.storage.getStore('snitch_map'); + + let snitchNoMDFP = { + 'amazon.com': ['amazonads.com', 'amazing.com', 'amazonrainforest.com'] + }; + + let actionNoMDFP = { + 'amazon.com': { + heuristicAction: "cookieblock", + userAction: "", + dnt: false, + nextUpdateTime: 0, + } + }; + + snitchMap.updateObject(snitchNoMDFP); + actionMap.updateObject(actionNoMDFP); + migrations.forgetFirstPartySnitches(badger); + + assert.deepEqual( + actionMap.getItem('amazon.com'), + actionNoMDFP['amazon.com'], + "action map preserved for domain with no MDFP snitch entries" + ); + + assert.deepEqual( + snitchMap.getItem('amazon.com'), + snitchNoMDFP['amazon.com'], + "snitch map entry with no MDFP domains remains the same after migration runs" + ); +}); + +QUnit.test("forgetFirstPartySnitches migration properly handles snitch entries with some MDFP entries", (assert) => { + const actionMap = badger.storage.getStore('action_map'), + snitchMap = badger.storage.getStore('snitch_map'); + + let snitchSomeMDFP = { + 'amazon.com': ['amazon.ca', 'amazon.co.jp', 'amazing.com'] + }; + + let actionSomeMDFP = { + 'amazon.com': { + heuristicAction: "cookieblock", + userAction: "", + dnt: false, + nextUpdateTime: 0, + } + }; + + snitchMap.updateObject(snitchSomeMDFP); + actionMap.updateObject(actionSomeMDFP); + migrations.forgetFirstPartySnitches(badger); + + assert.equal( + badger.storage.getAction('amazon.com'), + constants.ALLOW, + "Action downgraded for partial MDFP domain" + ); + + assert.deepEqual(snitchMap.getItem('amazon.com'), + ["amazing.com"], + 'forget first party migration properly removes MDFP domains and leaves regular domains'); +}); + +QUnit.test("forgetFirstPartySnitches migration properly handles snitch entries with all MDFP entries", (assert) => { + const actionMap = badger.storage.getStore('action_map'), + snitchMap = badger.storage.getStore('snitch_map'); + + let snitchAllMDFP = { + 'amazon.com': ['amazon.ca', 'amazon.co.jp', 'amazon.es'] + }; + + let actionAllMDFP = { + 'amazon.com': { + heuristicAction: "cookieblock", + userAction: "", + dnt: false, + nextUpdateTime: 0, + } + }; + + // confirm all entries are MDFP + snitchAllMDFP["amazon.com"].forEach((domain) => { + assert.ok( + mdfp.isMultiDomainFirstParty('amazon.com', domain), + domain + " is indeed MDFP to amazon.com" + ); + }); + + snitchMap.updateObject(snitchAllMDFP); + actionMap.updateObject(actionAllMDFP); + migrations.forgetFirstPartySnitches(badger); + + assert.notOk(snitchMap.getItem('amazon.com'), + 'forget first party migration properly removes a snitch map entry with all MDFP domains attributed to it'); + + assert.equal( + badger.storage.getAction('amazon.com'), + constants.NO_TRACKING, + "Action downgraded for all MDFP domain" + ); +}); + +}()); diff --git a/src/tests/tests/baseDomain.js b/src/tests/tests/baseDomain.js new file mode 100644 index 0000000..b39d757 --- /dev/null +++ b/src/tests/tests/baseDomain.js @@ -0,0 +1,255 @@ +/* * This file is part of Adblock Plus <http://adblockplus.org/>, * Copyright (C) 2006-2013 Eyeo GmbH * + * Adblock Plus is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * Adblock Plus is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. + */ + +/* global +extractHostFromURL:false, +getBaseDomain: false, +ipAddressToNumber: false, +isPrivateDomain:false, +isThirdParty:false, +URI:false, +*/ + +(function () { + +QUnit.module("URL/host tools"); + +QUnit.test("Host name extraction", function (assert) { + var tests = [ + [null, ""], + ["/foo/bar", ""], + ["http://example.com/", "example.com"], + ["http://example.com:8000/", "example.com"], + ["http://foo:bar@example.com:8000/foo:bar/bas", "example.com"], + ["ftp://example.com/", "example.com"], + ["http://1.2.3.4:8000/", "1.2.3.4"], + ["http://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]/", "2001:0db8:85a3:0000:0000:8a2e:0370:7334"], + ["http://[2001::7334]:8000/test@foo.example.com/bar", "2001::7334"], + ]; + + for (var i = 0; i < tests.length; i++) { + assert.equal(extractHostFromURL(tests[i][0]), tests[i][1], tests[i][0]); + } +}); + +QUnit.test("Invalid URI recognition", function (assert) { + var tests = [ + null, + "", + "http:", + "http:foo.bar/", + "http://foo.bar" + ]; + for (var i = 0; i < tests.length; i++) { + assert.raises(function() { // eslint-disable-line no-loop-func + return new URI(tests[i]); + }, Error, "Invalid URI recognition."); + } +}); + +QUnit.test("URI parsing", function (assert) { + var tests = [ + ["http://example.com/", { + scheme: "http", + host: "example.com", + asciiHost: "example.com", + hostPort: "example.com", + port: -1, + path: "/", + prePath: "http://example.com" + }], + ["http://example.com:8000/", { + scheme: "http", + host: "example.com", + asciiHost: "example.com", + hostPort: "example.com:8000", + port: 8000, + path: "/", + prePath: "http://example.com:8000" + }], + ["http://foo:bar@\u0440\u043E\u0441\u0441\u0438\u044F.\u0440\u0444:8000/foo:bar/bas", { + scheme: "http", + host: "\u0440\u043E\u0441\u0441\u0438\u044F.\u0440\u0444", + asciiHost: "xn--h1alffa9f.xn--p1ai", + hostPort: "\u0440\u043E\u0441\u0441\u0438\u044F.\u0440\u0444:8000", + port: 8000, + path: "/foo:bar/bas", + prePath: "http://foo:bar@\u0440\u043E\u0441\u0441\u0438\u044F.\u0440\u0444:8000" + }], + ["ftp://m\xFCller.de/", { + scheme: "ftp", + host: "m\xFCller.de", + asciiHost: "xn--mller-kva.de", + hostPort: "m\xFCller.de", + port: -1, + path: "/", + prePath: "ftp://m\xFCller.de" + }], + ["http://1.2.3.4:8000/", { + scheme: "http", + host: "1.2.3.4", + asciiHost: "1.2.3.4", + hostPort: "1.2.3.4:8000", + port: 8000, + path: "/", + prePath: "http://1.2.3.4:8000" + }], + ["http://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]/", { + scheme: "http", + host: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + asciiHost: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + hostPort: "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]", + port: -1, + path: "/", + prePath: "http://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]" + }], + ["http://[2001::7334]:8000/test@foo.example.com/bar", { + scheme: "http", + host: "2001::7334", + asciiHost: "2001::7334", + hostPort: "[2001::7334]:8000", + port: 8000, + path: "/test@foo.example.com/bar", + prePath: "http://[2001::7334]:8000" + }], + ["filesystem:http://example.com/temporary/myfile.png", { + scheme: "filesystem:http", + host: "example.com", + asciiHost: "example.com", + hostPort: "example.com", + port: -1, + path: "/temporary/myfile.png", + prePath: "filesystem:http://example.com" + }], + ["blob:https://www.daringgourmet.com/69587cd0-01e1-417b-819d-8e2ecbefc1f9", { + scheme: "blob:https", + host: "www.daringgourmet.com", + asciiHost: "www.daringgourmet.com", + hostPort: "www.daringgourmet.com", + port: -1, + path: "/69587cd0-01e1-417b-819d-8e2ecbefc1f9", + prePath: "blob:https://www.daringgourmet.com" + }], + ]; + + for (var i = 0; i < tests.length; i++) { + var url = tests[i][0]; + var uri = new URI(url); + assert.equal(uri.spec, url, "URI(" + url + ").spec"); + for (var k in tests[i][1]) { + assert.equal(uri[k], tests[i][1][k], "URI(" + url + ")." + k); + } + } +}); + +QUnit.test("Determining base domain", function (assert) { + var tests = [ + ["com", "com"], + ["example.com", "example.com"], + ["www.example.com", "example.com"], + ["www.example.com.", "example.com"], + ["www.example.co.uk", "example.co.uk"], + ["www.example.co.uk.", "example.co.uk"], + ["www.example.bl.uk", "bl.uk"], + ["foo.bar.example.co.uk", "example.co.uk"], + ["1.2.3.4.com", "4.com"], + ["1.2.3.4.bg", "3.4.bg"], + ["1.2.3.4", "1.2.3.4"], + ["1.2.0x3.0x4", "1.2.0x3.0x4"], + ["1.2.3", "2.3"], + ["1.2.0x3g.0x4", "0x3g.0x4"], + ["2001:0db8:85a3:0000:0000:8a2e:0370:7334", "2001:0db8:85a3:0000:0000:8a2e:0370:7334"], + ["2001::7334", "2001::7334"], + ["::ffff:1.2.3.4", "::ffff:1.2.3.4"], + ["foo.bar.2001::7334", "bar.2001::7334"], + ["test.xn--e1aybc.xn--p1ai", "тест.рф"], + ]; + + for (var i = 0; i < tests.length; i++) { + assert.equal(getBaseDomain(tests[i][0]), tests[i][1], tests[i][0]); + } +}); + +QUnit.test("Converting IP address to number checks", function (assert) { + var testResults = { + "127.0.0.1": 2130706433, + "8.8.8.8": 134744072, + "192.168.0.1": 3232235521, + "256.0.0.1": 0, + "privacybadger.org": 0, + }; + + for (var ip in testResults) { + // Ignore object properties. + if (!testResults.hasOwnProperty(ip)) { + continue; + } + + assert.equal(ipAddressToNumber(ip), testResults[ip], ip); + } +}); + +QUnit.test("Private domain checks", function (assert) { + var testResults = { + localhost: true, + "126.0.0.13": false, + "127.0.0.1": true, + "128.0.2.27": false, + "9.4.201.150": false, + "10.3.0.99": true, + "11.240.84.107": false, + "171.20.103.65": false, + "172.15.2.0": false, + "172.16.25.30": true, + "172.31.16.2": true, + "172.32.3.4": false, + "173.28.86.211": false, + "191.168.33.41": false, + "192.167.101.111": false, + "192.168.1.5": true, + "192.169.204.154": false, + "193.168.28.139": false, + "privacybadger.org": false, + }; + + for (var domain in testResults) { + // Ignore object properties. + if (!testResults.hasOwnProperty(domain)) { + continue; + } + + assert.equal(isPrivateDomain(domain), testResults[domain], domain); + } +}); + +QUnit.test("Third party checks", function (assert) { + var tests = [ + ["foo", "foo", false], + ["foo", "bar", true], + ["foo.com", "bar.com", true], + ["foo.com", "foo.com", false], + ["foo.com", "www.foo.com", false], + ["foo.example.com", "bar.example.com", false], + ["foo.uk", "bar.uk", true], + ["foo.co.uk", "bar.co.uk", true], + ["foo.example.co.uk", "bar.example.co.uk", false], + ["1.2.3.4", "2.2.3.4", true], + ]; + + for (var i = 0; i < tests.length; i++) { + assert.equal(isThirdParty(tests[i][0], tests[i][1]), tests[i][2], tests[i][0] + " and " + tests[i][1]); + } +}); + +}()); diff --git a/src/tests/tests/firstparties.js b/src/tests/tests/firstparties.js new file mode 100644 index 0000000..3560e04 --- /dev/null +++ b/src/tests/tests/firstparties.js @@ -0,0 +1,167 @@ +(function () { + +let destination = 'https://the.beach/'; +let fb_wrap = 'https://facebook.com/l.php?u=' + destination; +let fb_xss = 'https://facebook.com/l.php?u=javascript://bad.site/%250Aalert(1)'; +let g_wrap = 'https://www.google.com/url?q=' + destination; +let g_ping = '/url?url=' + destination; + +function makeLink(href) { + let element = document.createElement('a'); + element.href = href; + element.rel = ''; + return element; +} + +function stub(elts, selector) { + document.querySelectorAllBefore = document.querySelectorAll; + window.setIntervalBefore = window.setInterval; + chrome.runtime.sendMessageBefore = chrome.runtime.sendMessage; + + // Stub querySelectorAll so that any selector that includes `selector` will + // match all the elements in `elts`. + document.querySelectorAll = function (query) { + if (query.includes(selector)) { + return elts; + } else { + return document.querySelectorAllBefore(query); + } + }; + + // Stub runtime.sendMessage so that it returns `true` in response to the + // `checkEnabled` query. + chrome.runtime.sendMessage = function (message, callback) { + if (message.type == "checkEnabled") { + callback(true); + } else { + chrome.runtime.sendMessageBefore(message, callback); + } + }; + window.setInterval = function () {}; + +} + +function unstub() { + document.querySelectorAll = document.querySelectorAllBefore; + window.setInterval = window.setIntervalBefore; + chrome.runtime.sendMessage = chrome.runtime.sendMessageBefore; +} + +QUnit.module('First parties'); + +QUnit.test('facebook script unwraps valid links', (assert) => { + const NUM_CHECKS = 4, + done = assert.async(); + assert.expect(NUM_CHECKS); + + let fixture = document.getElementById('qunit-fixture'); + let good_link = makeLink(fb_wrap); + let bad_link = makeLink(fb_xss); + + // create first-party utility script + let util_script = document.createElement('script'); + util_script.src = '../js/firstparties/lib/utils.js'; + + // create the content script + let fb_script = document.createElement('script'); + fb_script.src = '../js/firstparties/facebook.js'; + fb_script.onload = function() { + assert.equal(good_link.href, destination, 'unwrapped good link'); + assert.ok(good_link.rel.includes('noreferrer'), + 'added noreferrer to good link'); + + assert.equal(bad_link.href, fb_xss, 'did not unwrap the XSS link'); + assert.notOk(bad_link.rel.includes('noreferrer'), + 'did not change rel of XSS link'); + + unstub(); + done(); + }; + + // after the utility script has finished loading, add the content script + util_script.onload = function() { + fixture.append(fb_script); + }; + + stub([good_link, bad_link], '/l.php?'); + fixture.appendChild(good_link); + fixture.appendChild(bad_link); + fixture.appendChild(util_script); +}); + + +QUnit.test('google shim link unwrapping', (assert) => { + const NUM_CHECKS = 2, + done = assert.async(); + assert.expect(NUM_CHECKS); + + let fixture = document.getElementById('qunit-fixture'); + let shim_link = makeLink(g_wrap); + + // create first-party utility script + let util_script = document.createElement('script'); + util_script.src = '../js/firstparties/lib/utils.js'; + + // create the content script + let g_script = document.createElement('script'); + g_script.src = '../js/firstparties/google-static.js'; + g_script.onload = function() { + assert.equal(shim_link.href, destination, 'unwrapped shim link'); + assert.ok(shim_link.rel.includes('noreferrer'), + 'added noreferrer to shim link'); + + unstub(); + done(); + }; + + // after the utility script has finished loading, add the content script + util_script.onload = function() { + fixture.append(g_script); + }; + + stub([shim_link], '/url?'); + fixture.appendChild(shim_link); + fixture.appendChild(util_script); +}); + + +QUnit.test('google search de-instrumentation', (assert) => { + const NUM_CHECKS = 3, + done = assert.async(); + assert.expect(NUM_CHECKS); + + let fixture = document.getElementById('qunit-fixture'); + let ff_link = makeLink(destination); + ff_link.onmousedown = 'return rwt(this, foobar);'; + let chrome_link = makeLink(destination); + chrome_link.ping = g_ping; + + // create first-party utility script + let util_script = document.createElement('script'); + util_script.src = '../js/firstparties/lib/utils.js'; + + // create the content script + let g_script = document.createElement('script'); + g_script.src = '../js/firstparties/google-search.js'; + g_script.onload = function() { + assert.notOk(ff_link.onmousedown, 'removed mouseDown event from ff link'); + assert.ok(ff_link.rel.includes('noreferrer'), 'added noreferrer to link'); + + assert.notOk(chrome_link.ping, 'removed ping attr from chrome link'); + + unstub(); + done(); + }; + + // after the utility script has finished loading, add the content script + util_script.onload = function() { + fixture.append(g_script); + }; + + stub([ff_link, chrome_link], 'onmousedown^='); + fixture.appendChild(ff_link); + fixture.appendChild(chrome_link); + fixture.appendChild(util_script); +}); + +}()); diff --git a/src/tests/tests/heuristic.js b/src/tests/tests/heuristic.js new file mode 100644 index 0000000..0eed2bb --- /dev/null +++ b/src/tests/tests/heuristic.js @@ -0,0 +1,165 @@ +(function () { + +let hb = require('heuristicblocking'); + +let chromeDetails = { + frameId: 35, + method: "GET", + requestHeaders: [ + { + name: "User-Agent", + value: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36" + }, { + name: "Accept", + value: "*/*" + }, { + name: "Referer", + value: "http://eff-tracker-test.s3-website-us-west-2.amazonaws.com/third-party.html" + }, { + name: "Accept-Encoding", + value: "gzip, deflate, sdch" + }, { + name: "Accept-Language", + value: "en-US,en;q=0.8" + }, { + name: "Cookie", + value: "thirdpartytest=1234567890" + } + ], + requestId: "502", + tabId: 15, + timeStamp: 1490117784939.147, + type: "script", + url: "http://eff-tracker-test.s3-website-us-west-2.amazonaws.com/third-party.js" +}; +const CHROME_COOKIE_INDEX = chromeDetails.requestHeaders.findIndex( + i => i.name == "Cookie" +); + +let firefoxDetails = { + requestId: "13", + url: "http://eff-tracker-test.s3-website-us-west-2.amazonaws.com/third-party.js", + originUrl: "http://eff-tracker-test.s3-website-us-west-2.amazonaws.com/third-party.html", + method: "GET", + type: "script", + timeStamp: 1490118778473, + frameId: 4294967303, + tabId: 2, + requestHeaders: [ + { + name: "host", + value: "eff-tracker-test.s3-website-us-west-2.amazonaws.com" + }, { + name: "user-agent", + value: "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0" + }, { + name: "accept", + value: "*/*" + }, { + name: "accept-language", + value: "en-US,en;q=0.5" + }, { + name: "accept-encoding", + value: "gzip, deflate" + }, { + name: "referer", + value: "http://eff-tracker-test.s3-website-us-west-2.amazonaws.com/third-party.html" + }, { + name: "cookie", + value: "thirdpartytest=1234567890" + }, { + name: "connection", + value: "keep-alive" + } + ] +}; + +QUnit.module("Heuristic", { + before: (/*assert*/) => { + }, + + beforeEach: (/*assert*/) => { + }, + + afterEach: (/*assert*/) => { + }, + + after: (/*assert*/) => { + } +}); + +QUnit.test("HTTP cookie tracking detection", (assert) => { + let details = JSON.parse(JSON.stringify(chromeDetails)); + + // remove cookie header + let cookieHeader = details.requestHeaders.splice(CHROME_COOKIE_INDEX, 1); + assert.notOk(hb.hasCookieTracking(details), "No cookie header"); + + // restore it + details.requestHeaders.push(cookieHeader[0]); + assert.ok(hb.hasCookieTracking(details), "High-entropy cookie header"); + + // set it to a low-entropy value + details.requestHeaders[CHROME_COOKIE_INDEX] = { + name: "Cookie", + value: "key=ab" + }; + assert.notOk(hb.hasCookieTracking(details), "Low-entropy cookie header"); + + // check when individual entropy is low but overall entropy is over threshold + // add another low entropy cookie + details.requestHeaders.push({ + name: "Cookie", + value: "key=ab" + }); + assert.ok(hb.hasCookieTracking(details), + "Two low-entropy cookies combine to cross tracking threshold"); +}); + +QUnit.test("HTTP header names are case-insensitive", (assert) => { + assert.ok( + hb.hasCookieTracking(chromeDetails), + "Cookie tracking detected with capitalized (Chrome) headers" + ); + assert.ok( + hb.hasCookieTracking(firefoxDetails), + "Cookie tracking detected with lowercase (Firefox) headers" + ); +}); + +QUnit.test("Cookie attributes shouldn't add to entropy", (assert) => { + let ATTR_COOKIES = [ + 'test-cookie=true; Expires=Thu, 01-Jan-1970 00:00:01 GMT; Path=/; Domain=.parrable.com', + '__usd_latimes.com=; expires=Wed, 03-May-2017 01:20:20 GMT; domain=.go.sonobi.com; path=/', + 'ses55=; Domain=.rubiconproject.com; Path=/; Expires=Wed, 03-May-2017 11:59:59 GMT; Max-Age=38407', + 'vf=5;Version=1;Comment=;Domain=.contextweb.com;Path=/;Max-Age=9583', + 'PUBMDCID=2; domain=pubmatic.com; expires=Tue, 01-Aug-2017 01:20:21 GMT; path=/', + 'tc=; path=/; Max-Age=31536000; expires=Thu, 03 May 2018 01:20:21 GMT', + 'uid=; path=/; expires=Wed, 03 May 2017 01:20:31 GMT; domain=medium.com; secure; httponly', + ]; + + let details = JSON.parse(JSON.stringify(chromeDetails)); + for (let i = 0; i < ATTR_COOKIES.length; i++) { + details.requestHeaders[CHROME_COOKIE_INDEX].value = ATTR_COOKIES[i]; + assert.notOk(hb.hasCookieTracking(details), + "cookie attributes test #" + i); + } +}); + +QUnit.test("CloudFlare cookies should get ignored", (assert) => { + let CLOUDFLARE_COOKIES = [ + '__cfduid=d3c728f97e01b1ab6969828f24b42ab111493693758', + '__cfduid=d9758e8613dd4acbba3248dde15e74f8d1493774432; expires=Thu, 03-May-18 01:20:32 GMT; path=/; domain=.medium.com; HttpOnly', + '__cfduid=de8a1734f91060dba20e2833705018b911493771353; expires=Thu, 03-May-18 02:25:53 GMT; path=/; domain=.fightforthefuture.org; HttpOnly', + '__cfduid=d712bcfe8e20469cc4b9129a4ab89b7501576598707; expires=Thu, 16-Jan-20 16:05:07 GMT; path=/; domain=.githack.com; HttpOnly; SameSite=Lax', + ]; + + let details = JSON.parse(JSON.stringify(chromeDetails)); + for (let i = 0; i < CLOUDFLARE_COOKIES.length; i++) { + details.requestHeaders[CHROME_COOKIE_INDEX].value = CLOUDFLARE_COOKIES[i]; + assert.notOk(hb.hasCookieTracking(details), + "CloudFlare cookie test #" + i); + } +}); + +}()); diff --git a/src/tests/tests/htmlutils.js b/src/tests/tests/htmlutils.js new file mode 100644 index 0000000..5ff0d89 --- /dev/null +++ b/src/tests/tests/htmlutils.js @@ -0,0 +1,241 @@ +(function () { + +QUnit.module("HTML Utils"); + +let constants = require('constants'), + htmlUtils = require("htmlutils").htmlUtils; + +QUnit.test("getActionDescription", (assert) => { + // Test parameters + const getMessage = chrome.i18n.getMessage, + origin = "pbtest.org"; + const tests = [ + { + action: "block", + origin, + expectedResult: getMessage('badger_status_block', origin) + }, + { + action: "cookieblock", + origin, + expectedResult: getMessage('badger_status_cookieblock', origin) + }, + { + action: "allow", + origin, + expectedResult: getMessage('badger_status_allow', origin) + }, + { + action: "dnt", + origin, + expectedResult: getMessage('dnt_tooltip') + }, + ]; + + // Run each test. + for (let i = 0; i < tests.length; i++) { + const test = tests[i], + message = `Inputs: '${test.action}' and '${test.origin}'`; + + assert.equal( + htmlUtils.getActionDescription(test.action, test.origin), + test.expectedResult, + message + ); + } +}); + +QUnit.test("getToggleHtml", function (assert) { + // Test parameters + const origin = "pbtest.org"; + const tests = [ + { + action: constants.BLOCK, + expectedResult: constants.BLOCK, + }, + { + action: constants.COOKIEBLOCK, + expectedResult: constants.COOKIEBLOCK, + }, + { + action: constants.ALLOW, + expectedResult: constants.ALLOW, + }, + { + action: constants.DNT, + expectedResult: constants.ALLOW, + }, + ]; + + // Run each test. + for (let test of tests) { + let message = `Inputs: '${origin}' and '${test.action}'`; + let html = htmlUtils.getToggleHtml(origin, test.action); + let input_val = $('input[name="' + origin + '"]:checked', html).val(); + assert.equal(input_val, test.expectedResult, message); + } +}); + +QUnit.test("getOriginHtml", function (assert) { + // Test parameters + var tests = [ + { + existingHtml: '<div id="existinghtml"></div>', + origin: "pbtest.org", + action: constants.ALLOW, + }, + { + existingHtml: '<div id="existinghtml"></div>', + origin: "pbtest.org", + action: constants.DNT, + }, + ]; + + // Run each test. + for (var i = 0; i < tests.length; i++) { + var existingHtml = tests[i].existingHtml; + var origin = tests[i].origin; + var action = tests[i].action; + + var htmlResult = existingHtml + htmlUtils.getOriginHtml(origin, action); + + // Make sure existing HTML is present. + var existingHtmlExists = htmlResult.indexOf(existingHtml) > -1; + assert.ok(existingHtmlExists, "Existing HTML should be present"); + + // Make sure origin is set. + var originDataExists = htmlResult.indexOf('data-origin="' + origin + '"') > -1; + assert.ok(originDataExists, "Origin should be set"); + + // Check for presence of DNT content. + var dntExists = htmlResult.indexOf('id="dnt-compliant"') > -1; + assert.equal(dntExists, action == constants.DNT, + "DNT div should " + (dntExists ? "" : "not ") + "be present"); + } +}); + +QUnit.test("makeSortable", (assert) => { + const tests = [ + ["bbc.co.uk", "bbc."], + ["s3.amazonaws.com", "s3."], + ["01234.global.ssl.fastly.net", "01234."], + ["api.nextgen.guardianapps.co.uk", "guardianapps.nextgen.api"], + ["localhost", "localhost."], + ["127.0.0.1", "127.0.0.1."], + ]; + tests.forEach((test) => { + assert.equal( + htmlUtils.makeSortable(test[0]), + test[1], + test[0] + ); + }); +}); + +QUnit.test("sortDomains", (assert) => { + const DOMAINS = [ + "ajax.cloudflare.com", + "betrad.com", + "c.betrad.com", + "cloudflare.com", + "condenastdigital.com", + "weather.com" + ]; + const tests = [ + { + msg: "disquscdn.com was getting sorted with the Cs", + domains: [ + "a.disquscdn.com", + "caradvice.disqus.com", + "carscoop.disqus.com", + "c.disquscdn.com", + "celebstoner.disqus.com", + "changemon.disqus.com", + "disqusads.com", + "disquscdn.com", + "disqus.com", + "uploads.disquscdn.com", + "wired.disqus.com", + ], + expected: [ + "disqus.com", + "caradvice.disqus.com", + "carscoop.disqus.com", + "celebstoner.disqus.com", + "changemon.disqus.com", + "wired.disqus.com", + "disqusads.com", + "disquscdn.com", + "a.disquscdn.com", + "c.disquscdn.com", + "uploads.disquscdn.com", + ] + }, + { + msg: "bbc.co.uk was getting sorted with the Cs", + domains: DOMAINS.concat([ + "baidu.com", + "bbc.co.uk", + "static.bbc.co.uk", + ]), + expected: [ + "baidu.com", + "bbc.co.uk", + "static.bbc.co.uk", + "betrad.com", + "c.betrad.com", + "cloudflare.com", + "ajax.cloudflare.com", + "condenastdigital.com", + "weather.com", + ] + }, + { + msg: "googleapis.com is a PSL TLD", + domains: DOMAINS.concat([ + "ajax.googleapis.com", + "maps.googleapis.com", + "google.com", + ]), + expected: [ + "ajax.googleapis.com", + "betrad.com", + "c.betrad.com", + "cloudflare.com", + "ajax.cloudflare.com", + "condenastdigital.com", + "google.com", + "maps.googleapis.com", + "weather.com", + ] + }, + { + msg: "non-TLD addresses", + domains: DOMAINS.concat([ + "localhost", + "127.0.0.1", + ]), + expected: [ + "127.0.0.1", + "betrad.com", + "c.betrad.com", + "cloudflare.com", + "ajax.cloudflare.com", + "condenastdigital.com", + "localhost", + "weather.com", + ] + }, + + ]; + + tests.forEach((test) => { + assert.deepEqual( + htmlUtils.sortDomains(test.domains), + test.expected, + test.msg + ); + }); +}); + +}()); diff --git a/src/tests/tests/multiDomainFirstParties.js b/src/tests/tests/multiDomainFirstParties.js new file mode 100644 index 0000000..4961b03 --- /dev/null +++ b/src/tests/tests/multiDomainFirstParties.js @@ -0,0 +1,73 @@ +(function () { + +QUnit.module("Multi-domain first parties"); + +let mdfp = require('multiDomainFP'); + +QUnit.test('isMultiDomainFirstParty test', function (assert) { + let testData = [ + ['foo.bar', 'yep.com', 'maybe.idk'], + ['related.com', 'larry.com'], + ]; + + let isMdfp = mdfp.makeIsMultiDomainFirstParty(mdfp.makeDomainLookup(testData)); + + assert.ok( + isMdfp('yep.com', 'maybe.idk'), + "these are related domains according to test data" + ); + assert.ok( + isMdfp('maybe.idk', 'yep.com'), + "the domains are related regardless of ordering" + ); + assert.ok( + isMdfp('related.com', 'larry.com'), + "these should also be related domains, from a different set in test data" + ); + assert.notOk( + isMdfp('yep.com', 'related.com'), + "these domains are both present in test data but should not be related" + ); + assert.notOk( + isMdfp('larry.com', 'yep.com'), + "these domains are also both present but should be unrelated" + ); + assert.notOk( + isMdfp('yep.com', 'google.com'), + "one of these domains is not in test data" + ); + assert.notOk( + isMdfp('reddit.com', 'eff.org'), + "both domains are not in test data" + ); +}); + +// "lint" our MDFP definitions to avoid accidentally adding PSL domains +// for example: +// https://github.com/EFForg/privacybadger/pull/1550#pullrequestreview-54480652 +QUnit.test('MDFP domains are all base domains', (assert) => { + for (let group of mdfp.multiDomainFirstPartiesArray) { + for (let domain of group) { + assert.ok( + window.getBaseDomain('fakesubdomain.' + domain) == domain, + domain + ' is a base domain (eTLD+1)' + ); + } + } +}); + +// lint for duplicates +QUnit.test('MDFP domains do not contain duplicates', (assert) => { + let domains = new Set(); + for (let group of mdfp.multiDomainFirstPartiesArray) { + for (let domain of group) { + assert.notOk( + domains.has(domain), + domain + ' does not appear more than once' + ); + domains.add(domain); + } + } +}); + +}()); diff --git a/src/tests/tests/options.js b/src/tests/tests/options.js new file mode 100644 index 0000000..2abeba8 --- /dev/null +++ b/src/tests/tests/options.js @@ -0,0 +1,105 @@ +(function () { + +QUnit.module("Options page utils"); + +let { getOriginsArray } = require("optionslib"); + +QUnit.test("getOriginsArray", (assert) => { + const origins = { + "allowed.com": "allow", + "blocked.org": "block", + "alsoblocked.org": "block", + "cookieblocked.biz": "cookieblock", + "UserAllowed.net": "user_allow", + "uuuserblocked.nyc": "user_block", + "dntDomain.co.uk": "dnt", + "another.allowed.domain.example": "allow", + }; + const originsSansAllowed = _.reduce( + origins, (memo, val, key) => { + if (val != "allow") { + memo[key] = val; + } + return memo; + }, {} + ); + + const tests = [ + { + msg: "Empty, no filters", + args: [{},], + expected: [] + }, + { + msg: "No filters (allowed domains are filtered out)", + args: [origins,], + expected: Object.keys(originsSansAllowed) + }, + { + msg: "Not-yet-blocked domains are shown", + args: [origins, null, null, null, true], + expected: Object.keys(origins) + }, + { + msg: "Type filter", + args: [origins, "", "user"], + expected: ["UserAllowed.net", "uuuserblocked.nyc"] + }, + { + msg: "Status filter", + args: [origins, "", "", "allow"], + expected: ["UserAllowed.net", "dntDomain.co.uk"] + }, + { + msg: "Text filter", + args: [origins, ".org"], + expected: ["blocked.org", "alsoblocked.org"] + }, + { + msg: "Text filter and domain case insensitivity", + args: [origins, "uSER"], + expected: ["UserAllowed.net", "uuuserblocked.nyc"] + }, + { + msg: "Text filter with extra space", + args: [origins, " .org"], + expected: ["blocked.org", "alsoblocked.org"] + }, + { + msg: "Negative text filter", + args: [origins, "-.org"], + expected: [ + "cookieblocked.biz", + "UserAllowed.net", + "uuuserblocked.nyc", + "dntDomain.co.uk", + ] + }, + { + msg: "Multiple negative text filter", + args: [origins, "-.net -cookie -.co.uk"], + expected: ["blocked.org", "alsoblocked.org", "uuuserblocked.nyc"] + }, + { + msg: "Multiple text filters", + args: [origins, " -also .biz .org "], + expected: ["blocked.org", "cookieblocked.biz"] + }, + { + msg: "All filters together", + args: [origins, ".net", "user", "allow", true], + expected: ["UserAllowed.net"] + }, + ]; + + tests.forEach((test) => { + assert.deepEqual( + getOriginsArray.apply(window, test.args), + test.expected, + test.msg + ); + }); + +}); + +}()); diff --git a/src/tests/tests/storage.js b/src/tests/tests/storage.js new file mode 100644 index 0000000..cb4042d --- /dev/null +++ b/src/tests/tests/storage.js @@ -0,0 +1,638 @@ +/* globals badger:false, constants:false */ + +(function () { + +const DOMAIN = "example.com", + SUBDOMAIN = "widgets." + DOMAIN, + SUBSUBDOMAIN = "cdn." + SUBDOMAIN; + +let storage = badger.storage, + actionMap, + snitchMap; + +QUnit.module("Storage", { + before: (assert) => { + // can't initialize globally above + // as they get initialized too early when run by Selenium + actionMap = storage.getStore('action_map'); + snitchMap = storage.getStore('snitch_map'); + + assert.notOk(actionMap.getItem(DOMAIN), + "test domain is not yet in action_map"); + assert.notOk(snitchMap.getItem(DOMAIN), + "test domain is not yet in snitch_map"); + } +}); + +QUnit.test("testGetBadgerStorage", function (assert) { + assert.ok(actionMap.updateObject instanceof Function, "actionMap is a pbstorage"); +}); + +QUnit.test("test BadgerStorage methods", function (assert) { + actionMap.setItem('foo', 'bar'); + assert.equal(actionMap.getItem('foo'), 'bar'); + assert.ok(actionMap.hasItem('foo')); + actionMap.deleteItem('foo'); + assert.notOk(actionMap.hasItem('foo')); +}); + +QUnit.test("test user override of default action for domain", function (assert) { + badger.saveAction("allow", "pbtest.org"); + assert.equal(storage.getAction("pbtest.org"), constants.USER_ALLOW); + badger.saveAction("block", "pbtest.org"); + assert.equal(storage.getAction("pbtest.org"), constants.USER_BLOCK); + badger.saveAction("allow", "pbtest.org"); + assert.equal(storage.getAction("pbtest.org"), constants.USER_ALLOW); + storage.revertUserAction("pbtest.org"); + assert.equal(storage.getAction("pbtest.org"), constants.NO_TRACKING); +}); + +QUnit.test("settings map merging", (assert) => { + let settings_map = storage.getStore('settings_map'); + + // overwrite settings with test values + settings_map.setItem('disabledSites', ['example.com']); + settings_map.setItem('showCounter', true); + + // merge settings + settings_map.merge({ + disabledSites: ['www.nytimes.com'], + showCounter: false, + }); + + // verify + assert.deepEqual( + settings_map.getItem('disabledSites'), + ['example.com', 'www.nytimes.com'], + "disabled site lists are combined when merging settings" + ); + assert.ok(!settings_map.getItem('showCounter'), "other settings are overwritten"); +}); + +// previously: +// https://github.com/EFForg/privacybadger/pull/1911#issuecomment-379896911 +QUnit.test("action map merge copies/breaks references", (assert) => { + let data = { + dnt: false, + heuristicAction: '', + nextUpdateTime: 100, + userAction: 'user_block' + }; + + actionMap.merge({[DOMAIN]: data}); + assert.deepEqual( + actionMap.getItem(DOMAIN), + data, + "test domain was imported"); + + // set a property on the original object + data.userAction = "user_allow"; + + // this should not affect data in storage + assert.equal(actionMap.getItem(DOMAIN).userAction, + "user_block", + "already imported data should be left alone " + + "when modifying object used for import"); +}); + +QUnit.test("action map merge only updates user action", (assert) => { + actionMap.setItem(DOMAIN, + {dnt: false, heuristicAction: '', nextUpdateTime: 100, userAction: ''}); + assert.equal(actionMap.getItem(DOMAIN).nextUpdateTime, 100); + + let newValue = {dnt: true, heuristicAction: constants.BLOCK, + nextUpdateTime: 99, userAction: constants.USER_BLOCK}; + actionMap.merge({[DOMAIN]: newValue}); + assert.equal(actionMap.getItem(DOMAIN).userAction, + constants.USER_BLOCK, + "userAction should be merged if it's set"); + assert.equal(actionMap.getItem(DOMAIN).heuristicAction, '', + 'heuristicAction should never be overwritten'); + + newValue.userAction = ''; + actionMap.merge({[DOMAIN]: newValue}); + assert.equal(actionMap.getItem(DOMAIN).userAction, + constants.USER_BLOCK, + 'blank userAction should not overwrite anything'); +}); + +QUnit.test("action map merge creates new entry if necessary", (assert) => { + assert.notOk(actionMap.hasItem('newsite.com')); + + let newValue = {dnt: false, heuristicAction: constants.BLOCK, + nextUpdateTime: 100, userAction: ''}; + actionMap.merge({'newsite.com': newValue}); + assert.notOk(actionMap.hasItem('newsite.com'), + 'action map entry should not be created for heuristicAction alone'); + + newValue.userAction = constants.USER_BLOCK; + actionMap.merge({'newsite.com': newValue}); + assert.ok(actionMap.hasItem('newsite.com'), + 'action map entry should be created if userAction is set'); + + actionMap.deleteItem('newsite.com'); + + newValue.userAction = ''; + newValue.dnt = true; + actionMap.merge({'newsite.com': newValue}); + assert.ok(actionMap.hasItem('newsite.com'), + 'action map entry should be created if DNT is set'); +}); + +QUnit.test("action map merge updates with latest DNT info", (assert) => { + actionMap.setItem(DOMAIN, + {dnt: false, heuristicAction: '', nextUpdateTime: 100, userAction: ''}); + + // DNT should not be merged if nextUpdateTime is earlier + let newValue = {dnt: true, heuristicAction: '', nextUpdateTime: 99, userAction: ''}; + actionMap.merge({[DOMAIN]: newValue}); + assert.equal(actionMap.getItem(DOMAIN).nextUpdateTime, 100, + 'nextUpdateTime should not be changed to an earlier time'); + assert.notOk(actionMap.getItem(DOMAIN).dnt, + 'DNT value should not be updated by out-of-date information'); + + // DNT should be merged if it's more up-to-date + newValue.nextUpdateTime = 101; + actionMap.merge({[DOMAIN]: newValue}); + assert.equal(actionMap.getItem(DOMAIN).nextUpdateTime, 101, + 'nextUpdateTime should be updated to later time'); + assert.ok(actionMap.getItem(DOMAIN).dnt, + 'DNT value should be updated with more recent information'); +}); + +QUnit.test("action map merge handles missing nextUpdateTime", (assert) => { + let newValue = { + dnt: true, + heuristicAction: '', + userAction: '' + }; + + assert.notOk(newValue.hasOwnProperty('nextUpdateTime'), + "nextUpdateTime is indeed missing from the import"); + + // new DNT domain should be imported + actionMap.merge({[DOMAIN]: newValue}); + assert.deepEqual( + actionMap.getItem(DOMAIN), + Object.assign({ nextUpdateTime: 0 }, newValue), + "test domain was imported and nextUpdateTime got initialized"); + + // existing DNT domain should be left alone + // as we don't know how fresh the import is + newValue.dnt = false; + actionMap.merge({[DOMAIN]: newValue}); + assert.ok(actionMap.getItem(DOMAIN).dnt, + "existing data should be left alone " + + "when unable to determine recency of new data"); + + // now set the timestamp and try again + newValue.nextUpdateTime = 200; + actionMap.merge({[DOMAIN]: newValue}); + assert.notOk(actionMap.getItem(DOMAIN).dnt, + "DNT got overriden now that new data seems fresher"); +}); + +QUnit.test("action map merge handles missing userAction", (assert) => { + let newValue = { + heuristicAction: 'allow', + dnt: true, + nextUpdateTime: 100 + }; + + // import and check that userAction got initialized + actionMap.merge({[DOMAIN]: newValue}); + assert.deepEqual( + actionMap.getItem(DOMAIN), + Object.assign({ userAction: '' }, newValue), + "test domain was imported and userAction got initialized"); +}); + +QUnit.test("action map merge handles missing dnt", (assert) => { + let newValue = { + heuristicAction: 'block', + userAction: 'user_allow' + }; + + // import and check that userAction got initialized + actionMap.merge({[DOMAIN]: newValue}); + assert.deepEqual( + actionMap.getItem(DOMAIN), + Object.assign({ dnt: false, nextUpdateTime: 0 }, newValue), + "test domain was imported and DNT got initialized"); +}); + +QUnit.test("action map merge handles subdomains correctly", (assert) => { + actionMap.setItem('testsite.com', + {dnt: false, heuristicAction: '', nextUpdateTime: 100, userAction: ''}); + + let newValue = {dnt: true, heuristicAction: '', nextUpdateTime: 100, userAction: ''}; + + actionMap.merge({'s1.testsite.com': newValue}); + assert.ok(actionMap.hasItem('s1.testsite.com'), + 'Subdomains should be merged if they honor DNT'); + + newValue.dnt = false; + actionMap.merge({'s2.testsite.com': newValue}); + assert.notOk(actionMap.hasItem('s2.testsite.com'), + "Subdomains should not be merged if they don't honor DNT"); +}); + +QUnit.test("snitch map merging", (assert) => { + snitchMap.merge({[DOMAIN]: ['firstparty.org']}); + assert.ok(snitchMap.getItem(DOMAIN).indexOf('firstparty.org') > -1); + + // Check to make sure existing and new domain are present + snitchMap.merge({[DOMAIN]: ['firstparty2.org']}); + assert.ok(snitchMap.getItem(DOMAIN).indexOf('firstparty.org') > -1); + assert.ok(snitchMap.getItem(DOMAIN).indexOf('firstparty2.org') > -1); + + // Verify 'block' status is triggered once TRACKING_THRESHOLD is hit + assert.equal(actionMap.getItem(DOMAIN).heuristicAction, "allow"); + snitchMap.merge({[DOMAIN]: ["firstparty3.org"]}); + assert.equal(actionMap.getItem(DOMAIN).heuristicAction, "block"); +}); + +QUnit.test("blocking cascades", (assert) => { + // mark domain for blocking + storage.setupHeuristicAction(DOMAIN, constants.BLOCK); + + // check domain itself + assert.equal( + storage.getAction(DOMAIN), + constants.BLOCK, + "domain is marked for blocking directly" + ); + assert.equal( + storage.getBestAction(DOMAIN), + constants.BLOCK, + "domain is marked for blocking" + ); + + // check that subdomain inherits blocking + assert.equal( + storage.getAction(SUBDOMAIN), + constants.NO_TRACKING, + "subdomain is not marked for blocking directly" + ); + assert.equal( + storage.getBestAction(SUBDOMAIN), + constants.BLOCK, + "subdomain is marked for blocking (via parent domain)" + ); + + // check that subsubdomain inherits blocking + assert.equal( + storage.getAction(SUBSUBDOMAIN), + constants.NO_TRACKING, + "subsubdomain is not marked for blocking directly" + ); + assert.equal( + storage.getBestAction(SUBSUBDOMAIN), + constants.BLOCK, + "subsubdomain is marked for blocking (via grandparent domain)" + ); +}); + +QUnit.test("DNT does not cascade", (assert) => { + storage.setupDNT(DOMAIN); + + // check domain itself + assert.equal( + storage.getAction(DOMAIN), + constants.DNT, + "domain is marked as DNT directly" + ); + assert.equal( + storage.getBestAction(DOMAIN), + constants.DNT, + "domain is marked as DNT" + ); + + // check that subdomain does not inherit DNT + assert.equal( + storage.getAction(SUBDOMAIN), + constants.NO_TRACKING, + "subdomain is not marked as DNT directly" + ); + assert.equal( + storage.getBestAction(SUBDOMAIN), + constants.NO_TRACKING, + "subdomain is not marked as DNT (via parent domain)" + ); +}); + +QUnit.test("DNT does not return as an action if user has chosen not to", (assert) => { + let settings_map = storage.getStore('settings_map'); + settings_map.setItem("checkForDNTPolicy", false); + storage.setupDNT(DOMAIN); + + assert.equal( + storage.getAction(DOMAIN), + constants.NO_TRACKING, + "domain is marked as DNT directly, but returns as NO_TRACKING because user has disabled DNT" + ); + assert.equal( + storage.getBestAction(DOMAIN), + constants.NO_TRACKING, + "domain is marked as DNT, but returns as NO_TRACKING because user has disabled DNT" + ); +}); + +QUnit.test("blocking still cascades after domain declares DNT", (assert) => { + storage.setupHeuristicAction(DOMAIN, constants.BLOCK); + storage.setupDNT(DOMAIN); + + // check domain itself + assert.equal( + storage.getAction(DOMAIN, true), + constants.BLOCK, + "domain is marked for blocking directly" + ); + assert.equal( + storage.getBestAction(DOMAIN), + constants.DNT, + "domain is marked as DNT" + ); + + // check that subdomain inherits blocking + assert.equal( + storage.getAction(SUBDOMAIN), + constants.NO_TRACKING, + "subdomain is not marked for blocking directly" + ); + assert.equal( + storage.getBestAction(SUBDOMAIN), + constants.BLOCK, + "subdomain is marked for blocking (via parent domain)" + ); +}); + +QUnit.test("cascading doesn't work the other way", (assert) => { + // mark subdomain for blocking + storage.setupHeuristicAction(SUBDOMAIN, constants.BLOCK); + + // check subdomain itself + assert.equal( + storage.getAction(SUBDOMAIN), + constants.BLOCK, + "subdomain is marked for blocking directly" + ); + assert.equal( + storage.getBestAction(SUBDOMAIN), + constants.BLOCK, + "subdomain is marked for blocking" + ); + + // check that parent domain does not inherit blocking + assert.equal( + storage.getAction(DOMAIN), + constants.NO_TRACKING, + "domain is not marked for blocking directly" + ); + assert.equal( + storage.getBestAction(DOMAIN), + constants.NO_TRACKING, + "domain is not marked for blocking" + ); +}); + +QUnit.test("blocking overrules allowing", (assert) => { + // mark domain for blocking + storage.setupHeuristicAction(DOMAIN, constants.BLOCK); + // mark subsubdomain as "allow" (not-yet-over-the-threshold tracker) + storage.setupHeuristicAction(SUBSUBDOMAIN, constants.ALLOW); + + // check domain itself + assert.equal( + storage.getAction(DOMAIN), + constants.BLOCK, + "domain is marked for blocking directly" + ); + assert.equal( + storage.getBestAction(DOMAIN), + constants.BLOCK, + "domain is marked for blocking" + ); + + // check that subsubdomain inherits blocking + assert.equal( + storage.getAction(SUBSUBDOMAIN), + constants.ALLOW, + "subdomain is marked as 'allow' directly" + ); + assert.equal( + storage.getBestAction(SUBSUBDOMAIN), + constants.BLOCK, + "subsubdomain is marked for blocking (via grandparent domain)" + ); +}); + +QUnit.test("cookieblocking overrules blocking", (assert) => { + // mark domain for cookieblocking + storage.setupHeuristicAction(DOMAIN, constants.COOKIEBLOCK); + // mark subdomain for blocking + storage.setupHeuristicAction(SUBDOMAIN, constants.BLOCK); + + // check domain itself + assert.equal( + storage.getAction(DOMAIN), + constants.COOKIEBLOCK, + "domain is marked for cookieblocking directly" + ); + assert.equal( + storage.getBestAction(DOMAIN), + constants.COOKIEBLOCK, + "domain is marked for cookieblocking" + ); + + // check that subdomain inherits cookieblocking + assert.equal( + storage.getAction(SUBDOMAIN), + constants.BLOCK, + "subdomain is marked for blocking directly" + ); + assert.equal( + storage.getBestAction(SUBDOMAIN), + constants.COOKIEBLOCK, + "subdomain is marked for cookieblocking (via parent domain)" + ); +}); + +QUnit.test("user actions overrule everything else", (assert) => { + storage.setupUserAction(DOMAIN, constants.USER_BLOCK); + storage.setupHeuristicAction(SUBDOMAIN, constants.COOKIEBLOCK); + storage.setupDNT(SUBSUBDOMAIN); + + // check domain itself + assert.equal( + storage.getAction(DOMAIN), + constants.USER_BLOCK, + "domain is marked as userblock directly" + ); + assert.equal( + storage.getBestAction(DOMAIN), + constants.USER_BLOCK, + "domain is marked as userblock" + ); + + // check subdomain + assert.equal( + storage.getAction(SUBDOMAIN), + constants.COOKIEBLOCK, + "subdomain is marked for cookie blocking directly" + ); + assert.equal( + storage.getBestAction(SUBDOMAIN), + constants.USER_BLOCK, + "subdomain is marked as userblock" + ); + + // check subsubdomain + assert.equal( + storage.getAction(SUBSUBDOMAIN), + constants.DNT, + "subsubdomain is marked as DNT directly" + ); + assert.equal( + storage.getBestAction(SUBSUBDOMAIN), + constants.USER_BLOCK, + "subsubdomain is marked as userblock" + ); +}); + +// all three user actions are equally important +// but the one closest to the FQDN being checked should win +QUnit.test("specificity of rules of equal priority", (assert) => { + storage.setupUserAction(DOMAIN, constants.USER_BLOCK); + storage.setupUserAction(SUBDOMAIN, constants.USER_ALLOW); + storage.setupUserAction(SUBSUBDOMAIN, constants.USER_COOKIEBLOCK); + + // check domain itself + assert.equal( + storage.getAction(DOMAIN), + constants.USER_BLOCK, + "domain is marked as userblock directly" + ); + assert.equal( + storage.getBestAction(DOMAIN), + constants.USER_BLOCK, + "domain is marked as userblock" + ); + + // check subdomain + assert.equal( + storage.getAction(SUBDOMAIN), + constants.USER_ALLOW, + "subdomain is marked as userallow directly" + ); + assert.equal( + storage.getBestAction(SUBDOMAIN), + constants.USER_ALLOW, + "subdomain is marked as userallow" + ); + + // check subsubdomain + assert.equal( + storage.getAction(SUBSUBDOMAIN), + constants.USER_COOKIEBLOCK, + "subsubdomain is marked as usercookieblock directly" + ); + assert.equal( + storage.getBestAction(SUBSUBDOMAIN), + constants.USER_COOKIEBLOCK, + "subsubdomain is marked as usercookieblock" + ); +}); + +QUnit.test("unexpected heuristic actions are ignored", (assert) => { + storage.setupHeuristicAction(DOMAIN, "foo"); + storage.setupHeuristicAction(SUBDOMAIN, constants.ALLOW); + storage.setupHeuristicAction(SUBSUBDOMAIN, "bar"); + + // check domain itself + assert.equal( + storage.getAction(DOMAIN), + "foo", + "domain is marked as 'foo' directly" + ); + assert.equal( + storage.getBestAction(DOMAIN), + constants.NO_TRACKING, + "best action for domain is 'no tracking'" + ); + + // check subdomain + assert.equal( + storage.getAction(SUBDOMAIN), + constants.ALLOW, + "subdomain is marked as 'allow' directly" + ); + assert.equal( + storage.getBestAction(SUBDOMAIN), + constants.ALLOW, + "best action for subdomain is 'allow'" + ); + + // check subsubdomain + assert.equal( + storage.getAction(SUBSUBDOMAIN), + "bar", + "subsubdomain is marked as 'bar' directly" + ); + assert.equal( + storage.getBestAction(SUBSUBDOMAIN), + constants.ALLOW, + "best action for subsubdomain is 'allow'" + ); +}); + +function checkCookieblocking(assert) { + assert.equal( + storage.getBestAction(SUBDOMAIN), + constants.NO_TRACKING, + "subdomain is not yet (cookie)blocked" + ); + assert.ok( + storage.wouldGetCookieblocked(SUBDOMAIN), + "subdomain would get cookieblocked if blocked" + ); + + // block the subdomain + badger.heuristicBlocking.blocklistOrigin(DOMAIN, SUBDOMAIN); + + assert.equal( + storage.getBestAction(SUBDOMAIN), + constants.COOKIEBLOCK, + "subdomain is cookieblocked" + ); + assert.ok( + storage.wouldGetCookieblocked(SUBDOMAIN), + "subdomain would get/is cookieblocked" + ); +} + +QUnit.test("checking cookieblock potential for yellowlisted subdomain", (assert) => { + assert.notOk( + storage.wouldGetCookieblocked(SUBDOMAIN), + "subdomain wouldn't get cookieblocked if blocked" + ); + + // add subdomain to yellowlist + storage.getStore('cookieblock_list').setItem(SUBDOMAIN, true); + + checkCookieblocking(assert); +}); + +QUnit.test("checking cookieblock potential for subdomain with yellowlisted base domain", (assert) => { + assert.notOk( + storage.wouldGetCookieblocked(SUBDOMAIN), + "subdomain wouldn't get cookieblocked if blocked" + ); + + // add base domain to yellowlist + storage.getStore('cookieblock_list').setItem(DOMAIN, true); + + checkCookieblocking(assert); +}); + +}()); diff --git a/src/tests/tests/tabData.js b/src/tests/tests/tabData.js new file mode 100644 index 0000000..c578cd1 --- /dev/null +++ b/src/tests/tests/tabData.js @@ -0,0 +1,310 @@ +/* globals badger:false */ + +(function () { + +let constants = require('constants'); + +QUnit.module("tabData", { + beforeEach: function () { + + this.SITE_URL = "http://example.com/"; + this.tabId = 9999; + + badger.recordFrame(this.tabId, 0, this.SITE_URL); + + // stub chrome.tabs.get manually as we have some sort of issue stubbing with Sinon in Firefox + this.chromeTabsGet = chrome.tabs.get; + chrome.tabs.get = (tab_id, callback) => { + return callback({ + active: true + }); + }; + }, + + afterEach: function () { + chrome.tabs.get = this.chromeTabsGet; + delete badger.tabData[this.tabId]; + } +}, +function() { + QUnit.module("logThirdPartyOriginOnTab", { + beforeEach: function () { + this.clock = sinon.useFakeTimers(); + sinon.stub(chrome.browserAction, "setBadgeText"); + }, + afterEach: function () { + chrome.browserAction.setBadgeText.restore(); + this.clock.restore(); + }, + }); + + QUnit.test("logging blocked domain", function (assert) { + const DOMAIN = "example.com"; + + assert.equal( + badger.getTrackerCount(this.tabId), 0, "count starts at zero" + ); + + // set up domain blocking (used by getTrackerCount) + badger.storage.setupHeuristicAction(DOMAIN, constants.BLOCK); + + // log blocked domain + badger.logThirdPartyOriginOnTab(this.tabId, DOMAIN, constants.BLOCK); + this.clock.tick(1); + assert.equal( + badger.getTrackerCount(this.tabId), 1, "count gets incremented" + ); + assert.ok( + chrome.browserAction.setBadgeText.calledOnce, + "updateBadge gets called when we see a blocked domain" + ); + assert.ok(chrome.browserAction.setBadgeText.calledWithExactly({ + tabId: this.tabId, + text: "1" + }), "setBadgeText was called with expected args"); + }); + + QUnit.test("logging unblocked domain", function (assert) { + badger.logThirdPartyOriginOnTab(this.tabId, "example.com", constants.ALLOW); + this.clock.tick(1); + assert.equal( + badger.getTrackerCount(this.tabId), 0, "count stays at zero" + ); + assert.ok( + chrome.browserAction.setBadgeText.notCalled, + "updateBadge does not get called when we see a hasn't-decided-yet-to-block domain" + ); + }); + + QUnit.test("logging DNT-compliant domain", function (assert) { + badger.logThirdPartyOriginOnTab(this.tabId, "example.com", constants.DNT); + this.clock.tick(1); + assert.equal( + badger.getTrackerCount(this.tabId), 0, "count stays at zero" + ); + assert.ok( + chrome.browserAction.setBadgeText.notCalled, + "updateBadge does not get called when we see a DNT-compliant domain" + ); + }); + + QUnit.test("logging as unblocked then as blocked", function (assert) { + const DOMAIN = "example.com"; + + // log unblocked domain + badger.logThirdPartyOriginOnTab(this.tabId, DOMAIN, constants.ALLOW); + this.clock.tick(1); + + // set up domain blocking (used by getTrackerCount) + badger.storage.setupHeuristicAction(DOMAIN, constants.BLOCK); + + // log the same domain, this time as blocked + badger.logThirdPartyOriginOnTab(this.tabId, DOMAIN, constants.BLOCK); + this.clock.tick(1); + assert.equal( + badger.getTrackerCount(this.tabId), 1, "count gets incremented" + ); + assert.equal( + chrome.browserAction.setBadgeText.callCount, + "1", + "updateBadge gets called when we see a blocked domain" + ); + assert.ok(chrome.browserAction.setBadgeText.calledWithExactly({ + tabId: this.tabId, + text: "1" + }), "setBadgeText was called with expected args"); + }); + + QUnit.test("logging blocked domain twice", function (assert) { + const DOMAIN = "example.com"; + + // set up domain blocking (used by getTrackerCount) + badger.storage.setupHeuristicAction(DOMAIN, constants.BLOCK); + + // log blocked domain + badger.logThirdPartyOriginOnTab(this.tabId, DOMAIN, constants.BLOCK); + this.clock.tick(1); + assert.equal( + badger.getTrackerCount(this.tabId), 1, "count gets incremented" + ); + assert.ok( + chrome.browserAction.setBadgeText.calledOnce, + "updateBadge gets called when we see a blocked domain" + ); + assert.ok(chrome.browserAction.setBadgeText.calledWithExactly({ + tabId: this.tabId, + text: "1" + }), "setBadgeText was called with expected args"); + + // log the same blocked domain again + badger.logThirdPartyOriginOnTab(this.tabId, DOMAIN, constants.BLOCK); + this.clock.tick(1); + assert.equal( + badger.getTrackerCount(this.tabId), + 1, + "count does not get incremented" + ); + assert.ok( + chrome.browserAction.setBadgeText.calledOnce, + "updateBadge not called when we see the same blocked domain again" + ); + }); + + QUnit.test("logging 2x unblocked then 2x blocked", function (assert) { + const DOMAIN = "example.com"; + + // log unblocked domain twice + badger.logThirdPartyOriginOnTab(this.tabId, DOMAIN, constants.ALLOW); + this.clock.tick(1); + badger.logThirdPartyOriginOnTab(this.tabId, DOMAIN, constants.ALLOW); + this.clock.tick(1); + + // set up domain blocking (used by getTrackerCount) + badger.storage.setupHeuristicAction(DOMAIN, constants.BLOCK); + + // log blocked domain + badger.logThirdPartyOriginOnTab(this.tabId, DOMAIN, constants.BLOCK); + this.clock.tick(1); + assert.equal( + badger.getTrackerCount(this.tabId), 1, "count gets incremented" + ); + assert.ok( + chrome.browserAction.setBadgeText.calledOnce, + "updateBadge gets called when we see a blocked domain" + ); + assert.deepEqual(chrome.browserAction.setBadgeText.getCall(0).args[0], { + tabId: this.tabId, + text: "1" + }, "setBadgeText was called with expected args"); + + // log the same blocked domain again + badger.logThirdPartyOriginOnTab(this.tabId, DOMAIN, constants.BLOCK); + this.clock.tick(1); + assert.equal( + badger.getTrackerCount(this.tabId), + 1, + "count does not get incremented" + ); + assert.ok( + chrome.browserAction.setBadgeText.calledOnce, + "updateBadge not called when we see the same blocked domain again" + ); + }); + + QUnit.test("logging cookieblocked domain", function (assert) { + const DOMAIN = "example.com"; + + // set up domain blocking (used by getTrackerCount) + badger.storage.setupHeuristicAction(DOMAIN, constants.COOKIEBLOCK); + + // log cookieblocked domain + badger.logThirdPartyOriginOnTab(this.tabId, DOMAIN, constants.COOKIEBLOCK); + this.clock.tick(1); + assert.equal( + badger.getTrackerCount(this.tabId), 1, "count gets incremented" + ); + assert.ok( + chrome.browserAction.setBadgeText.calledOnce, + "updateBadge gets called when we see a cookieblocked domain" + ); + assert.ok(chrome.browserAction.setBadgeText.calledWithExactly({ + tabId: this.tabId, + text: "1" + }), "setBadgeText was called with expected args"); + }); + + QUnit.test("logging several domains", function (assert) { + const DOMAIN1 = "example.com", + DOMAIN2 = "example.net"; + + // set up domain blocking (used by getTrackerCount) + badger.storage.setupHeuristicAction(DOMAIN1, constants.BLOCK); + badger.storage.setupHeuristicAction(DOMAIN2, constants.COOKIEBLOCK); + + // log blocked domain + badger.logThirdPartyOriginOnTab(this.tabId, DOMAIN1, constants.BLOCK); + this.clock.tick(1); + assert.equal( + badger.getTrackerCount(this.tabId), 1, "count gets incremented" + ); + assert.ok( + chrome.browserAction.setBadgeText.calledOnce, + "updateBadge gets called when we see a blocked domain" + ); + assert.ok(chrome.browserAction.setBadgeText.calledWithExactly({ + tabId: this.tabId, + text: "1" + }), "setBadgeText was called with expected args"); + + // log cookieblocked domain + badger.logThirdPartyOriginOnTab(this.tabId, DOMAIN2, constants.COOKIEBLOCK); + this.clock.tick(1); + assert.equal( + badger.getTrackerCount(this.tabId), 2, "count gets incremented again" + ); + assert.ok( + chrome.browserAction.setBadgeText.calledTwice, + "updateBadge gets called when we see a cookieblocked domain" + ); + assert.ok(chrome.browserAction.setBadgeText.calledWithExactly({ + tabId: this.tabId, + text: "2" + }), "setBadgeText was called with expected args"); + }); + + QUnit.module('updateBadge', { + beforeEach: function() { + this.setBadgeText = sinon.stub(chrome.browserAction, "setBadgeText"); + + // another Firefox workaround: setBadgeText gets stubbed fine but setBadgeBackgroundColor doesn't + this.setBadgeBackgroundColor = chrome.browserAction.setBadgeBackgroundColor; + }, + afterEach: function() { + this.setBadgeText.restore(); + chrome.browserAction.setBadgeBackgroundColor = this.setBadgeBackgroundColor; + }, + }); + + QUnit.test("disabled", function(assert) { + let done = assert.async(2), + called = false; + + badger.disablePrivacyBadgerForOrigin(window.extractHostFromURL(this.SITE_URL)); + + this.setBadgeText.callsFake((obj) => { + assert.deepEqual(obj, {tabId: this.tabId, text: ''}); + done(); + }); + chrome.browserAction.setBadgeBackgroundColor = () => {called = true;}; + + badger.updateBadge(this.tabId); + + assert.notOk(called, "setBadgeBackgroundColor does not get called"); + + done(); + }); + + QUnit.test("numblocked zero", function(assert) { + let done = assert.async(2), + called = false; + + this.setBadgeText.callsFake((obj) => { + assert.deepEqual( + obj, + {tabId: this.tabId, text: ""}, + "setBadgeText called with expected args" + ); + done(); + }); + chrome.browserAction.setBadgeBackgroundColor = () => {called = true;}; + + badger.updateBadge(this.tabId); + + assert.notOk(called, "setBadgeBackgroundColor does not get called"); + + done(); + }); + +}); + +}()); diff --git a/src/tests/tests/utils.js b/src/tests/tests/utils.js new file mode 100644 index 0000000..c884374 --- /dev/null +++ b/src/tests/tests/utils.js @@ -0,0 +1,550 @@ +/* globals badger:false */ + +(function() { + +QUnit.module("Utils"); + +var utils = require('utils'); +var getSurrogateURI = require('surrogates').getSurrogateURI; + +QUnit.test("explodeSubdomains", function (assert) { + var fqdn = "test.what.yea.eff.org"; + var subs = utils.explodeSubdomains(fqdn); + assert.equal(subs.length, 4); + assert.equal(subs[0], fqdn); + assert.equal(subs[3], "eff.org"); +}); + +QUnit.test("xhrRequest", function (assert) { + // set up fake server to simulate XMLHttpRequests + let server = sinon.fakeServer.create({ + respondImmediately: true + }); + server.respondWith("GET", "https://www.eff.org/files/badgertest.txt", + [200, {}, "test passed\n"]); + + let done = assert.async(); + assert.expect(4); + + utils.xhrRequest("https://www.eff.org/files/badgertest.txt", function (err1, resp) { + assert.strictEqual(err1, null, "there was no error"); + assert.equal(resp, "test passed\n", "got expected response text"); + + utils.xhrRequest("https://www.eff.org/nonexistent-page", function(err2/*, resp*/) { + assert.ok(err2, "there was an error"); + assert.equal(err2.status, 404, "error was 404"); + + server.restore(); + done(); + }); + }); +}); + +QUnit.test("isPrivacyBadgerEnabled basic tests", function (assert) { + assert.ok(badger.isPrivacyBadgerEnabled("example.com"), + "Domain starts out as enabled."); + + badger.disablePrivacyBadgerForOrigin("example.com"); + assert.notOk(badger.isPrivacyBadgerEnabled("example.com"), + "Disabling the domain works."); + + badger.enablePrivacyBadgerForOrigin("example.com"); + assert.ok(badger.isPrivacyBadgerEnabled("example.com"), + "Re-enabling the domain works."); +}); + +QUnit.test("isPrivacyBadgerEnabled wildcard tests", function (assert) { + badger.disablePrivacyBadgerForOrigin('*.mail.example.com'); + assert.ok( + badger.isPrivacyBadgerEnabled('www.example.com'), + "Ignores cases without as many subdomains as the wildcard." + ); + assert.ok( + badger.isPrivacyBadgerEnabled('web.stuff.example.com'), + "Ignores cases where subdomains do not match the wildcard." + ); + assert.notOk( + badger.isPrivacyBadgerEnabled('web.mail.example.com'), + "Website matches wildcard pattern." + ); + assert.notOk( + badger.isPrivacyBadgerEnabled('stuff.fakedomain.web.mail.example.com'), + "Wildcard catches all prefacing subdomains." + ); + assert.ok( + badger.isPrivacyBadgerEnabled('mail.example.com'), + "Checks against URLs that lack a starting dot." + ); + + const PSL_TLD = "example.googlecode.com"; + assert.equal(window.getBaseDomain(PSL_TLD), PSL_TLD, + PSL_TLD + " is a PSL TLD"); + badger.disablePrivacyBadgerForOrigin('*.googlecode.com'); + assert.notOk(badger.isPrivacyBadgerEnabled(PSL_TLD), + "PSL TLDs work with wildcards as expected."); +}); + +QUnit.test("disable/enable privacy badger for origin", function (assert) { + function parsed() { + return badger.storage.getStore('settings_map').getItem('disabledSites'); + } + + let origLength = parsed() && parsed().length || 0; + + badger.disablePrivacyBadgerForOrigin('foo.com'); + assert.ok(parsed().length == (origLength + 1), "one more disabled site"); + + badger.enablePrivacyBadgerForOrigin('foo.com'); + assert.ok(parsed().length == origLength, "one less disabled site"); +}); + +QUnit.test("surrogate script URL lookups", function (assert) { + const NOOP = function () {}; + const surrogatedb = require('surrogatedb'); + const SURROGATE_PREFIX = 'data:application/javascript;base64,'; + const GA_JS_TESTS = [ + { + url: 'http://www.google-analytics.com/ga.js', + msg: "Google Analytics ga.js http URL should match" + }, + { + url: 'https://www.google-analytics.com/ga.js', + msg: "Google Analytics ga.js https URL should match" + }, + { + url: 'https://www.google-analytics.com/ga.js?foo=bar', + msg: "Google Analytics ga.js querystring URL should match" + }, + ]; + const NYT_SCRIPT_PATH = '/assets/homepage/20160920-111441/js/foundation/lib/framework.js'; + const NYT_URL = 'https://a1.nyt.com' + NYT_SCRIPT_PATH; + + let ga_js_surrogate; + + for (let i = 0; i < GA_JS_TESTS.length; i++) { + ga_js_surrogate = getSurrogateURI( + GA_JS_TESTS[i].url, + 'www.google-analytics.com' + ); + assert.ok(ga_js_surrogate, GA_JS_TESTS[i].msg); + } + + assert.ok( + ga_js_surrogate.startsWith(SURROGATE_PREFIX), + "The returned ga.js surrogate is a base64-encoded JavaScript data URI" + ); + + // test negative match + assert.notOk( + getSurrogateURI(NYT_URL, window.extractHostFromURL(NYT_URL)), + "New York Times script URL should not match any surrogates" + ); + + // test surrogate suffix token response contents + surrogatedb.hostnames[window.extractHostFromURL(NYT_URL)] = [ + NYT_SCRIPT_PATH + ]; + surrogatedb.surrogates[NYT_SCRIPT_PATH] = NOOP; + assert.equal( + getSurrogateURI(NYT_URL, window.extractHostFromURL(NYT_URL)), + SURROGATE_PREFIX + btoa(NOOP), + "New York Times script URL should now match the noop surrogate" + ); + + // set up test data for wildcard token tests + surrogatedb.hostnames['cdn.example.com'] = 'noop'; + surrogatedb.surrogates.noop = NOOP; + + // test wildcard tokens + for (let i = 0; i < 25; i++) { + let url = 'http://cdn.example.com/' + _.sample( + 'abcdefghijklmnopqrstuvwxyz0123456789'.split(''), + _.random(5, 15) + ).join(''); + + assert.equal( + getSurrogateURI(url, window.extractHostFromURL(url)), + SURROGATE_PREFIX + btoa(NOOP), + "A wildcard token should match all URLs for the hostname: " + url + ); + } +}); + +QUnit.test("rateLimit", (assert) => { + const INTERVAL = 100, + NUM_TESTS = 5; + + let clock = sinon.useFakeTimers(+new Date()); + + let callback = sinon.spy(function (password, i) { + // check args + assert.equal(password, "qwerty", + "rateLimit should preserve args"); + assert.equal(i + 1, callback.callCount, + "rateLimit should preserve args and call order"); + + // check context + assert.ok(this.foo == "bar", "rateLimit should preserve context"); + }); + + let fn = utils.rateLimit(callback, INTERVAL, {foo:"bar"}); + + for (let i = 0; i < NUM_TESTS; i++) { + fn("qwerty", i); + } + + for (let i = 0; i < NUM_TESTS; i++) { + // check rate limiting + assert.equal(callback.callCount, i + 1, + "rateLimit should allow only one call per interval"); + + // advance the clock + clock.tick(INTERVAL); + } + + clock.restore(); +}); + +// the following cookie parsing tests are derived from +// https://github.com/jshttp/cookie/blob/81bd3c77db6a8dcb23567de94b3beaef6c03e97a/test/parse.js +QUnit.test("cookie parsing", function (assert) { + + assert.deepEqual(utils.parseCookie('foo=bar'), { foo: 'bar' }, + "simple cookie"); + + assert.deepEqual( + utils.parseCookie('foo=bar;bar=123'), + { + foo: 'bar', + bar: '123' + }, + "simple cookie with two values" + ); + + assert.deepEqual( + utils.parseCookie('FOO = bar; baz = raz'), + { + FOO: 'bar', + baz: 'raz' + }, + "ignore spaces" + ); + + assert.deepEqual( + utils.parseCookie('foo="bar=123456789&name=Magic+Mouse"'), + { foo: 'bar=123456789&name=Magic+Mouse' }, + "escaped value" + ); + + assert.deepEqual( + utils.parseCookie('email=%20%22%2c%3b%2f'), + { email: ' ",;/' }, + "encoded value" + ); + + assert.deepEqual( + utils.parseCookie('foo=%1;bar=bar'), + { + foo: '%1', + bar: 'bar' + }, + "ignore escaping error and return original value" + ); + + assert.deepEqual( + utils.parseCookie('foo=%1;bar=bar;HttpOnly;Secure', { skipNonValues: true }), + { + foo: '%1', + bar: 'bar' + }, + "ignore non values" + ); + + assert.deepEqual( + utils.parseCookie('priority=true; expires=Wed, 29 Jan 2014 17:43:25 GMT; Path=/'), + { + priority: 'true', + expires: 'Wed, 29 Jan 2014 17:43:25 GMT', + Path: '/' + }, + "dates" + ); + + assert.deepEqual( + utils.parseCookie('foo=%1;bar=bar;foo=boo', { noOverwrite: true}), + { + foo: '%1', + bar: 'bar' + }, + "duplicate names #1" + ); + + assert.deepEqual( + utils.parseCookie('foo=false;bar=bar;foo=true', { noOverwrite: true}), + { + foo: 'false', + bar: 'bar' + }, + "duplicate names #2" + ); + + assert.deepEqual( + utils.parseCookie('foo=;bar=bar;foo=boo', { noOverwrite: true}), + { + foo: '', + bar: 'bar' + }, + "duplicate names #3" + ); + + // SameSite attribute + let SAMESITE_COOKIE = 'abc=123; path=/; domain=.githack.com; HttpOnly; SameSite=Lax'; + assert.deepEqual( + utils.parseCookie(SAMESITE_COOKIE), + { + abc: '123', + SameSite: 'Lax', + path: '/', + domain: '.githack.com', + HttpOnly: '', + }, + "SameSite is parsed" + ); + assert.deepEqual( + utils.parseCookie(SAMESITE_COOKIE, { skipAttributes: true }), + { abc: '123' }, + "SameSite is ignored when ignoring attributes" + ); + +}); + +QUnit.test("cookie parsing (legacy Firefox add-on)", function (assert) { + // raw cookies (input) + let optimizelyCookie = 'optimizelyEndUserId=oeu1394241144653r0.538161732205'+ + '5392; optimizelySegments=%7B%22237061344%22%3A%22none%22%2C%22237321400%'+ + '22%3A%22ff%22%2C%22237335298%22%3A%22search%22%2C%22237485170%22%3A%22fa'+ + 'lse%22%7D; optimizelyBuckets=%7B%7D'; + let googleCookie = 'PREF=ID=d93d4e842d10e12a:U=3838eaea5cd40d37:FF=0:TM=139'+ + '4232126:LM=1394235924:S=rKP367ac3aAdDzAS; NID=67=VwhHOGQunRmNsm9WwJyK571'+ + 'OGqb3RtvUmH987K5DXFgKFAxFwafA_5VPF5_bsjhrCoM0BjyQdxyL2b-qs9b-fmYCQ_1Uqjt'+ + 'qTeidAJBnc2ecjewJia6saHrcJ6yOVVgv'; + let hackpadCookie = 'acctIds=%5B%22mIqZhIPMu7j%22%2C%221394477194%22%2C%22u'+ + 'T/ayZECO0g/+hHtQnjrdEZivWA%3D%22%5D; expires=Wed, 01-Jan-3000 08:00:01 G'+ + 'MT; domain=.hackpad.com; path=/; secure; httponly\nacctIds=%5B%22mIqZhIP'+ + 'Mu7j%22%2C%221394477194%22%2C%22uT/ayZECO0g/+hHtQnjrdEZivWA%3D%22%5D; ex'+ + 'pires=Wed, 01-Jan-3000 08:00:00 GMT; domain=.hackpad.com; path=/; secure'+ + '; httponly\n1ASIE=T; expires=Wed, 01-Jan-3000 08:00:00 GMT; domain=hackp'+ + 'ad.com; path=/\nPUAS3=3186efa7f8bca99c; expires=Wed, 01-Jan-3000 08:00:0'+ + '0 GMT; path=/; secure; httponly'; + let emptyCookie = ''; + let testCookie = ' notacookiestring; abc=123 '; + + // parsed cookies (expected output) + let COOKIES = {}; + COOKIES[optimizelyCookie] = { + optimizelyEndUserId: 'oeu1394241144653r0.5381617322055392', + optimizelySegments: '%7B%22237061344%22%3A%22none%22%2C%22237321400%2' + + '2%3A%22ff%22%2C%22237335298%22%3A%22search%22%2C%22237485170%22%3A%2' + + '2false%22%7D', + optimizelyBuckets: '%7B%7D' + }; + COOKIES[emptyCookie] = {}; + COOKIES[testCookie] = {abc: '123'}; + COOKIES[googleCookie] = { + PREF: 'ID=d93d4e842d10e12a:U=3838eaea5cd40d37:FF=0:TM=1394232126:LM=1'+ + '394235924:S=rKP367ac3aAdDzAS', + NID: '67=VwhHOGQunRmNsm9WwJyK571OGqb3RtvUmH987K5DXFgKFAxFwafA_5VPF5_b'+ + 'sjhrCoM0BjyQdxyL2b-qs9b-fmYCQ_1UqjtqTeidAJBnc2ecjewJia6saHrcJ6yOVVgv' + }; + COOKIES[hackpadCookie] = { + acctIds: '%5B%22mIqZhIPMu7j%22%2C%221394477194%22%2C%22uT/ayZECO0g/+h'+ + 'HtQnjrdEZivWA%3D%22%5D', + PUAS3: '3186efa7f8bca99c', + '1ASIE': 'T' + }; + + // compare actual to expected + let test_number = 0; + for (let cookieString in COOKIES) { + if (COOKIES.hasOwnProperty(cookieString)) { + test_number++; + + let expected = COOKIES[cookieString]; + + let actual = utils.parseCookie( + cookieString, { + noDecode: true, + skipAttributes: true, + skipNonValues: true + } + ); + + assert.deepEqual(actual, expected, "cookie test #" + test_number); + } + } +}); + +// the following cookie parsing tests are derived from +// https://github.com/yui/yui3/blob/25264e3629b1c07fb779d203c4a25c0879ec862c/src/cookie/tests/cookie-tests.js +QUnit.test("cookie parsing (YUI3)", function (assert) { + + let cookieString = "a=b"; + let cookies = utils.parseCookie(cookieString); + assert.ok(cookies.hasOwnProperty("a"), "Cookie 'a' is present."); + assert.equal(cookies.a, "b", "Cookie 'a' should have value 'b'."); + + cookieString = "12345=b"; + cookies = utils.parseCookie(cookieString); + assert.ok(cookies.hasOwnProperty("12345"), "Cookie '12345' is present."); + assert.equal(cookies["12345"], "b", "Cookie '12345' should have value 'b'."); + + cookieString = "a=b; c=d; e=f; g=h"; + cookies = utils.parseCookie(cookieString); + assert.ok(cookies.hasOwnProperty("a"), "Cookie 'a' is present."); + assert.ok(cookies.hasOwnProperty("c"), "Cookie 'c' is present."); + assert.ok(cookies.hasOwnProperty("e"), "Cookie 'e' is present."); + assert.ok(cookies.hasOwnProperty("g"), "Cookie 'g' is present."); + assert.equal(cookies.a, "b", "Cookie 'a' should have value 'b'."); + assert.equal(cookies.c, "d", "Cookie 'c' should have value 'd'."); + assert.equal(cookies.e, "f", "Cookie 'e' should have value 'f'."); + assert.equal(cookies.g, "h", "Cookie 'g' should have value 'h'."); + + cookieString = "name=Nicholas%20Zakas; title=front%20end%20engineer"; + cookies = utils.parseCookie(cookieString); + assert.ok(cookies.hasOwnProperty("name"), "Cookie 'name' is present."); + assert.ok(cookies.hasOwnProperty("title"), "Cookie 'title' is present."); + assert.equal(cookies.name, "Nicholas Zakas", "Cookie 'name' should have value 'Nicholas Zakas'."); + assert.equal(cookies.title, "front end engineer", "Cookie 'title' should have value 'front end engineer'."); + + cookieString = "B=2nk0a3t3lj7cr&b=3&s=13; LYC=l_v=2&l_lv=10&l_l=94ddoa70d&l_s=qz54t4qwrsqquyv51w0z4xxwtx31x1t0&l_lid=146p1u6&l_r=4q&l_lc=0_0_0_0_0&l_mpr=50_0_0&l_um=0_0_1_0_0;YMRAD=1215072198*0_0_7318647_1_0_40123839_1; l%5FPD3=840"; + cookies = utils.parseCookie(cookieString); + assert.ok(cookies.hasOwnProperty("B"), "Cookie 'B' is present."); + assert.ok(cookies.hasOwnProperty("LYC"), "Cookie 'LYC' is present."); + assert.ok(cookies.hasOwnProperty("l_PD3"), "Cookie 'l_PD3' is present."); + + let cookieName = "something[1]"; + let cookieValue = "123"; + cookieString = encodeURIComponent(cookieName) + "=" + encodeURIComponent(cookieValue); + cookies = utils.parseCookie(cookieString); + assert.ok(cookies.hasOwnProperty(cookieName), "Cookie '" + cookieName + "' is present."); + assert.equal(cookies[cookieName], cookieValue, "Cookie value for '" + cookieName + "' is " + cookieValue + "."); + + cookieString = "SESSION=27bedbdf3d35252d0db07f34d81dcca6; STATS=OK; SCREEN=1280x1024; undefined; ys-bottom-preview=o%3Aheight%3Dn%253A389"; + cookies = utils.parseCookie(cookieString); + assert.ok(cookies.hasOwnProperty("SCREEN"), "Cookie 'SCREEN' is present."); + assert.ok(cookies.hasOwnProperty("STATS"), "Cookie 'STATS' is present."); + assert.ok(cookies.hasOwnProperty("SESSION"), "Cookie 'SESSION' is present."); + assert.ok(cookies.hasOwnProperty("ys-bottom-preview"), "Cookie 'ys-bottom-preview' is present."); + assert.ok(cookies.hasOwnProperty("undefined"), "Cookie 'undefined' is present."); + + // Tests that cookie parsing deals with cookies that contain an invalid + // encoding. It shouldn't error, but should treat the cookie as if it + // doesn't exist (return null). + cookieString = "DetailInfoList=CPN03022194=@|@=CPN03#|#%B4%EB%C3%B5%C7%D8%BC%F6%BF%E5%C0%E5#|#1016026000#|#%BD%C5%C8%E6%B5%BF#|##|#"; + cookies = utils.parseCookie(cookieString, { skipInvalid: true }); + assert.equal(cookies.DetailInfoList, null, "Cookie 'DetailInfoList' should not have a value."); + + // Tests that a Boolean cookie, one without an equals sign of value, + // is represented as an empty string. + cookieString = "info"; + cookies = utils.parseCookie(cookieString); + assert.equal(cookies.info, "", "Cookie 'info' should be an empty string."); + + cookieString = "name=Nicholas%20Zakas; hash=a=b&c=d&e=f&g=h; title=front%20end%20engineer"; + cookies = utils.parseCookie(cookieString); + assert.ok(cookies.hasOwnProperty("name"), "Cookie 'name' is present."); + assert.ok(cookies.hasOwnProperty("hash"), "Cookie 'hash' is present."); + assert.ok(cookies.hasOwnProperty("title"), "Cookie 'title' is present."); + assert.equal(cookies.name, "Nicholas Zakas", "Cookie 'name' should have value 'Nicholas Zakas'."); + assert.equal(cookies.hash, "a=b&c=d&e=f&g=h", "Cookie 'hash' should have value 'a=b&c=d&e=f&g=h'."); + assert.equal(cookies.title, "front end engineer", "Cookie 'title' should have value 'front end engineer'."); + +}); + +QUnit.test("getHostFromDomainInput", assert => { + assert.equal( + utils.getHostFromDomainInput("www.spiegel.de"), + "www.spiegel.de", + "Valid domains are accepted" + ); + + assert.equal( + utils.getHostFromDomainInput("http://www.spiegel.de/"), + "www.spiegel.de", + "URLs get transformed into domains" + ); + + assert.equal( + utils.getHostFromDomainInput("http://www.spiegel.de"), + "www.spiegel.de", + "Trailing slashes are not required" + ); + + assert.equal( + utils.getHostFromDomainInput("@"), + false, + "Valid URIs with empty hosts are rejected." + ); +}); + +// used in pixel tracking heuristic, given a string the estimateMaxEntropy function +// will return the estimated entropy value from it, based on logic parsing the string's length, +// and classes of character complication included in the string +QUnit.test("estimateMaxEntropy", assert => { + assert.equal( + utils.estimateMaxEntropy("google.com/analytics.google/analytics.google/google.com/analytics.google/analytics.google/google.com/analytics.google/analytics.google/google.com/analytics.google/analytics.google/google.com/analytics.google/analytics.google/google.com/analytics.google/anal"), + 257, + "returns length of string if it's above 256 (MAX_LS_LEN_FOR_ENTROPY_EST)" + ); + + assert.equal( + utils.estimateMaxEntropy("googlecomanalytics"), + utils.estimateMaxEntropy("GOOGLECOMANALYTICS"), + "if the same string is all lower case or all upper case, the returned estimated entropy value is the same" + ); + + assert.notEqual( + utils.estimateMaxEntropy('analytics.GOOGLE1234_'), + utils.estimateMaxEntropy('ANALYTICS.google1234'), + "two nearly identical strings of mixed character classes and different cases will return different values" + ); + + assert.notEqual( + utils.estimateMaxEntropy('google.com/analytics'), + utils.estimateMaxEntropy('0191/_-goo~le9x+xzxo'), + "strings of the same length but from different character classes will estimate different entropy values" + ); + + assert.equal( + utils.estimateMaxEntropy("google.com/0191/_-google/analytics.fizz?buzz=foobar"), + 320.55551316197466, + "entropy for complex string of varying character classes is correctly estimated" + ); + + assert.equal( + utils.estimateMaxEntropy("03899029.01_293"), + 49.82892142331044, + "entropy for string from the common classes of characters is correctly estimated" + ); + + assert.equal( + utils.estimateMaxEntropy("fizzBUZZ012345"), + 84, + "entropy for string from the case-insensitive class of characters is correctly estimated" + ); + + assert.equal( + utils.estimateMaxEntropy("fizz/buzz+fizzy~buzzy%"), + 142.82076811925285, + "entropy for string from the case-sensitive class of characters is correctly estimated" + ); + + assert.equal( + utils.estimateMaxEntropy("1280x720") < 32, + true, + "resolution strings with 'x' char from SEPS class are correctly estimated as low entropy" + ); + +}); + +})(); diff --git a/src/tests/tests/yellowlist.js b/src/tests/tests/yellowlist.js new file mode 100644 index 0000000..43fa869 --- /dev/null +++ b/src/tests/tests/yellowlist.js @@ -0,0 +1,424 @@ +/* globals badger:false */ + +(function () { + +function get_ylist() { + return badger.storage.getStore('cookieblock_list').getItemClones(); +} + +let constants = require('constants'); + +// fake server to simulate XMLHttpRequests +let server; + +QUnit.module("Yellowlist", (hooks) => { + hooks.before((/*assert*/) => { + server = sinon.fakeServer.create({ + respondImmediately: true + }); + }); + + hooks.after((/*assert*/) => { + server.restore(); + }); + + QUnit.test("Updating to a valid list", (assert) => { + let done = assert.async(); + assert.expect(3); + + let ylist = get_ylist(); + assert.ok(!!Object.keys(ylist).length, "yellowlist is not empty"); + + // remove a domain + let removed_domain = Object.keys(ylist)[0]; + delete ylist[removed_domain]; + + // add a domain + const NEW_YLIST_DOMAIN = "widgets.example.com"; + ylist[NEW_YLIST_DOMAIN] = true; + + // respond with the modified list + server.respondWith("GET", constants.YELLOWLIST_URL, + [200, {}, Object.keys(ylist).join("\n")]); + + badger.updateYellowlist(function (err) { + assert.notOk(err, "callback status indicates success"); + assert.deepEqual(get_ylist(), ylist, "list got updated"); + done(); + }); + }); + + QUnit.test("Updating receives a blank response", (assert) => { + let done = assert.async(); + assert.expect(3); + + let ylist = get_ylist(); + assert.ok(!!Object.keys(ylist).length, "yellowlist is not empty"); + + // respond with no content + server.respondWith("GET", constants.YELLOWLIST_URL, + [200, {}, ""]); + + badger.updateYellowlist(function (err) { + assert.ok(err, "callback status indicates failure"); + assert.deepEqual(get_ylist(), ylist, "list did not get updated"); + done(); + }); + }); + + QUnit.test("Updating receives an invalid response", (assert) => { + let BAD_RESPONSES = [ + "page not found", + "page\nnot\nfound", + "pagenotfound", + "eff.org\n...\n", + "...eff.org...", + "<html><body>eff.org</body></html>", + ]; + + let done = assert.async(BAD_RESPONSES.length); + assert.expect(1 + (2 * BAD_RESPONSES.length)); + + let ylist = get_ylist(); + assert.ok(!!Object.keys(ylist).length, "yellowlist is not empty"); + + BAD_RESPONSES.forEach(response => { + // respond with stuff that may look like the yellowlist but is not + server.respondWith("GET", constants.YELLOWLIST_URL, + [200, {}, response]); + + badger.updateYellowlist(function (err) { + assert.ok(err, + "callback status indicates failure for " + JSON.stringify(response)); + assert.deepEqual(get_ylist(), ylist, + "list did not get updated for " + JSON.stringify(response)); + done(); + }); + }); + }); + + QUnit.test("Updating gets a server error", (assert) => { + let done = assert.async(); + assert.expect(1); + + // respond with a 404 error + server.respondWith("GET", constants.YELLOWLIST_URL, + [404, {}, "page not found"]); + + badger.updateYellowlist(function (err) { + assert.ok(err, "callback status indicates failure"); + done(); + }); + }); + + QUnit.test("added domains get cookieblocked", (assert) => { + const DOMAIN = "example.com"; + + let done = assert.async(); + assert.expect(2); + + // mark domain for blocking + badger.storage.setupHeuristicAction(DOMAIN, constants.BLOCK); + + // respond with this domain added + let ylist = get_ylist(); + ylist[DOMAIN] = true; + server.respondWith("GET", constants.YELLOWLIST_URL, + [200, {}, Object.keys(ylist).join("\n")]); + + // update yellowlist + badger.updateYellowlist(function (err) { + assert.notOk(err, "callback status indicates success"); + + // check that the domain got cookieblocked + assert.equal( + badger.storage.getAction(DOMAIN), + constants.COOKIEBLOCK, + "domain is marked for cookieblocking" + ); + + done(); + }); + }); + + QUnit.test("Reapplying yellowlist updates", (assert) => { + // these are all on the yellowlist + let DOMAINS = [ + // domain, action + ["books.google.com", null], // null means do not record + ["clients6.google.com", ""], + ["storage.googleapis.com", constants.BLOCK], + ]; + + // set up test data + for (let i = 0; i < DOMAINS.length; i++) { + let [domain, action] = DOMAINS[i]; + if (action !== null) { + // record the domain with specified action + badger.storage.setupHeuristicAction(domain, action); + + // block the base domain + badger.storage.setupHeuristicAction( + window.getBaseDomain(domain), constants.BLOCK); + } + } + + // (re)apply yellowlist updates + require("migrations").Migrations.reapplyYellowlist(badger); + + // all test domains should be now set to "cookieblock" + for (let i = 0; i < DOMAINS.length; i++) { + let [domain,] = DOMAINS[i]; + assert.equal( + badger.storage.getBestAction(domain), + constants.COOKIEBLOCK, + domain + " is cookieblocked" + ); + } + }); + + QUnit.module("Removing domains", () => { + let TESTS = [ + { + name: "Basic scenario", + domains: { + 'example.com': { + yellowlist: true, + remove: true, + initial: constants.COOKIEBLOCK, + expected: constants.BLOCK, + expectedBest: constants.BLOCK + }, + } + }, + + { + name: "Parent is on yellowlist", + domains: { + 'widgets.example.com': { + yellowlist: true, + initial: constants.COOKIEBLOCK + }, + 'cdn.widgets.example.com': { + yellowlist: true, + remove: true, + initial: constants.COOKIEBLOCK, + expected: constants.COOKIEBLOCK, + expectedBest: constants.COOKIEBLOCK + }, + } + }, + + // scenario from https://github.com/EFForg/privacybadger/issues/1474 + { + name: "Parent is on yellowlist and is a PSL TLD (not in action map)", + domains: { + 'googleapis.com': { + yellowlist: true, + expected: constants.NO_TRACKING, + expectedBest: constants.NO_TRACKING, + }, + 'ajax.googleapis.com': { + yellowlist: true, + remove: true, + initial: constants.COOKIEBLOCK, + expected: constants.COOKIEBLOCK, + expectedBest: constants.COOKIEBLOCK + }, + } + }, + + { + name: "Child is on yellowlist", + domains: { + 'widgets.example.com': { + yellowlist: true, + remove: true, + initial: constants.COOKIEBLOCK, + expected: constants.BLOCK, + expectedBest: constants.BLOCK + }, + 'cdn.widgets.example.com': { + yellowlist: true, + expected: constants.COOKIEBLOCK, + expectedBest: constants.COOKIEBLOCK + }, + } + }, + + { + name: "Removing parent blocks subdomains", + domains: { + // parent domain is yellowlisted and cookieblocked + 'example.com': { + yellowlist: true, + remove: true, + initial: constants.COOKIEBLOCK, + expected: constants.BLOCK, + expectedBest: constants.BLOCK + }, + // non-yellowlisted subdomain + 'cdn1.example.com': { + initial: constants.COOKIEBLOCK, + expected: constants.BLOCK, + expectedBest: constants.BLOCK + }, + // another non-yellowlisted subdomain + 'cdn2.example.com': { + initial: constants.COOKIEBLOCK, + expected: constants.BLOCK, + expectedBest: constants.BLOCK + }, + } + }, + + { + name: "Parent is blocked", + domains: { + 'example.com': { + initial: constants.BLOCK, + }, + // removing from yellowlist will get this blocked + 'www.example.com': { + yellowlist: true, + remove: true, + initial: constants.COOKIEBLOCK, + expected: constants.BLOCK, + expectedBest: constants.BLOCK + }, + // removing from yellowlist will get this blocked + 's-static.ak.example.com': { + yellowlist: true, + remove: true, + initial: constants.COOKIEBLOCK, + expected: constants.BLOCK, + expectedBest: constants.BLOCK + }, + // yellowlisted and cookieblocked, should stay the same + 'video.example.com': { + yellowlist: true, + initial: constants.COOKIEBLOCK, + expected: constants.COOKIEBLOCK, + expectedBest: constants.COOKIEBLOCK + }, + // non-tracking, should stay the same + 'ampcid.example.com': { + initial: "", + expected: constants.NO_TRACKING, + expectedBest: constants.BLOCK + }, + } + }, + + // scenario from https://github.com/EFForg/privacybadger/issues/1474: + // using endsWith() and removing "" blocked all domains in action map + // that were also on the yellowlist, regardless of their status + { + name: "Removing blank domain does not block entire yellowlist", + domains: { + '': { + yellowlist: true, + remove: true + }, + // on yellowlist and in action map as non-tracking + 'avatars0.example.com': { + yellowlist: true, + initial: "", + expected: constants.NO_TRACKING, + expectedBest: constants.NO_TRACKING + }, + // on yellowlist and in action map but not yet blocked + 'api.example.net': { + yellowlist: true, + initial: constants.ALLOW, + expected: constants.ALLOW, + expectedBest: constants.ALLOW + } + } + } + ]; + + QUnit.test("googleapis.com is still a PSL TLD", (assert) => { + assert.notEqual( + window.getBaseDomain("ajax.googleapis.com"), + "googleapis.com", + "PSL yellowlist test depends on googleapis.com remaining a PSL TLD" + ); + }); + + TESTS.forEach(test => { + QUnit.test(test.name, (assert) => { + + let done = assert.async(); + + // to get num. of assertions, tally the expected/expectedBest props, + // and add one for the yellowlist update assertion + assert.expect(1 + Object.keys(test.domains).reduce((memo, domain) => { + let data = test.domains[domain]; + if (data.hasOwnProperty('expected')) { + memo++; + } + if (data.hasOwnProperty('expectedBest')) { + memo++; + } + return memo; + }, 0)); + + let ylistStorage = badger.storage.getStore('cookieblock_list'); + + // set up cookieblocking + for (let domain in test.domains) { + let conf = test.domains[domain]; + if (conf.yellowlist) { + ylistStorage.setItem(domain, true); + } + if (conf.hasOwnProperty("initial")) { + badger.storage.setupHeuristicAction(domain, conf.initial); + } + } + + // update the yellowlist making sure removed domains aren't on it + const ylist = ylistStorage.getItemClones(); + for (let domain in test.domains) { + if (test.domains[domain].remove) { + delete ylist[domain]; + } + } + server.respondWith("GET", constants.YELLOWLIST_URL, + [200, {}, Object.keys(ylist).join("\n")]); + + badger.updateYellowlist(err => { + assert.notOk(err, "callback status indicates success"); + + for (let domain in test.domains) { + let expected, data = test.domains[domain]; + + if (data.hasOwnProperty('expected')) { + expected = data.expected; + assert.equal( + badger.storage.getAction(domain), + expected, + `action on ${domain} should be "${expected}"` + ); + } + + if (data.hasOwnProperty('expectedBest')) { + expected = data.expectedBest; + assert.equal( + badger.storage.getBestAction(domain), + expected, + `best action for ${domain} should be "${expected}"` + ); + } + } + + done(); + }); + + }); + }); + }); + +}); + +}()); |