/* 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" ); }); })();