summaryrefslogtreecommitdiffstats
path: root/src/tests/tests
diff options
context:
space:
mode:
Diffstat (limited to 'src/tests/tests')
-rw-r--r--src/tests/tests/background.js467
-rw-r--r--src/tests/tests/baseDomain.js255
-rw-r--r--src/tests/tests/firstparties.js167
-rw-r--r--src/tests/tests/heuristic.js165
-rw-r--r--src/tests/tests/htmlutils.js241
-rw-r--r--src/tests/tests/multiDomainFirstParties.js73
-rw-r--r--src/tests/tests/options.js105
-rw-r--r--src/tests/tests/storage.js638
-rw-r--r--src/tests/tests/tabData.js310
-rw-r--r--src/tests/tests/utils.js550
-rw-r--r--src/tests/tests/yellowlist.js424
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();
+ });
+
+ });
+ });
+ });
+
+});
+
+}());