diff options
Diffstat (limited to 'toolkit/components/antitracking/test/xpcshell/test_tracking_db_service.js')
-rw-r--r-- | toolkit/components/antitracking/test/xpcshell/test_tracking_db_service.js | 492 |
1 files changed, 492 insertions, 0 deletions
diff --git a/toolkit/components/antitracking/test/xpcshell/test_tracking_db_service.js b/toolkit/components/antitracking/test/xpcshell/test_tracking_db_service.js new file mode 100644 index 0000000000..0f6ded1218 --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_tracking_db_service.js @@ -0,0 +1,492 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Note: This test may cause intermittents if run at exactly midnight. + +"use strict"; + +const { Sqlite } = ChromeUtils.importESModule( + "resource://gre/modules/Sqlite.sys.mjs" +); +XPCOMUtils.defineLazyServiceGetter( + this, + "TrackingDBService", + "@mozilla.org/tracking-db-service;1", + "nsITrackingDBService" +); + +XPCOMUtils.defineLazyGetter(this, "DB_PATH", function () { + return PathUtils.join(PathUtils.profileDir, "protections.sqlite"); +}); + +const SQL = { + insertCustomTimeEvent: + "INSERT INTO events (type, count, timestamp)" + + "VALUES (:type, :count, date(:timestamp));", + + selectAllEntriesOfType: "SELECT * FROM events WHERE type = :type;", + + selectAll: "SELECT * FROM events", +}; + +// Emulate the content blocking log. We do not record the url key, nor +// do we use the aggregated event number (the last element in the array). +const LOG = { + "https://1.example.com": [ + [Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT, true, 1], + ], + "https://2.example.com": [ + [Ci.nsIWebProgressListener.STATE_BLOCKED_FINGERPRINTING_CONTENT, true, 1], + ], + "https://3.example.com": [ + [Ci.nsIWebProgressListener.STATE_BLOCKED_CRYPTOMINING_CONTENT, true, 2], + ], + "https://4.example.com": [ + [Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, true, 3], + ], + "https://5.example.com": [ + [Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, true, 1], + ], + // Cookie blocked for other reason, then identified as a tracker + "https://6.example.com": [ + [ + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL | + Ci.nsIWebProgressListener.STATE_LOADED_LEVEL_1_TRACKING_CONTENT, + true, + 4, + ], + ], + "https://7.example.com": [ + [Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_SOCIALTRACKER, true, 1], + ], + "https://8.example.com": [ + [Ci.nsIWebProgressListener.STATE_BLOCKED_SOCIALTRACKING_CONTENT, true, 1], + ], + + // The contents below should not add to the database. + // Cookie loaded but not blocked. + "https://10.example.com": [ + [Ci.nsIWebProgressListener.STATE_COOKIES_LOADED, true, 1], + ], + // Tracker cookie loaded but not blocked. + "https://11.unblocked.example.com": [ + [Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_TRACKER, true, 1], + ], + // Social tracker cookie loaded but not blocked. + "https://12.example.com": [ + [Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_SOCIALTRACKER, true, 1], + ], + // Cookie blocked for other reason (not a tracker) + "https://13.example.com": [ + [Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_BY_PERMISSION, true, 2], + ], + // Fingerprinters set to block, but this one has an exception + "https://14.example.com": [ + [Ci.nsIWebProgressListener.STATE_BLOCKED_FINGERPRINTING_CONTENT, false, 1], + ], + // Two fingerprinters replaced with a shims script, should be treated as blocked + // and increment the counter. + "https://15.example.com": [ + [Ci.nsIWebProgressListener.STATE_REPLACED_FINGERPRINTING_CONTENT, true, 1], + ], + "https://16.example.com": [ + [Ci.nsIWebProgressListener.STATE_REPLACED_FINGERPRINTING_CONTENT, true, 1], + ], +}; + +do_get_profile(); + +Services.prefs.setBoolPref("browser.contentblocking.database.enabled", true); +Services.prefs.setBoolPref( + "privacy.socialtracking.block_cookies.enabled", + true +); +Services.prefs.setBoolPref( + "privacy.trackingprotection.fingerprinting.enabled", + true +); +Services.prefs.setBoolPref( + "browser.contentblocking.cfr-milestone.enabled", + true +); +Services.prefs.setIntPref( + "browser.contentblocking.cfr-milestone.update-interval", + 0 +); +Services.prefs.setStringPref( + "browser.contentblocking.cfr-milestone.milestones", + "[1000, 5000, 10000, 25000, 100000, 500000]" +); + +registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.contentblocking.database.enabled"); + Services.prefs.clearUserPref("privacy.socialtracking.block_cookies.enabled"); + Services.prefs.clearUserPref( + "privacy.trackingprotection.fingerprinting.enabled" + ); + Services.prefs.clearUserPref("browser.contentblocking.cfr-milestone.enabled"); + Services.prefs.clearUserPref( + "browser.contentblocking.cfr-milestone.update-interval" + ); + Services.prefs.clearUserPref( + "browser.contentblocking.cfr-milestone.milestones" + ); +}); + +// This tests that data is added successfully, different types of events should get +// their own entries, when the type is the same they should be aggregated. Events +// that are not blocking events should not be recorded. Cookie blocking events +// should only be recorded if we can identify the cookie as a tracking cookie. +add_task(async function test_save_and_delete() { + await TrackingDBService.saveEvents(JSON.stringify(LOG)); + + // Peek in the DB to make sure we have the right data. + let db = await Sqlite.openConnection({ path: DB_PATH }); + // Make sure the items table was created. + ok(await db.tableExists("events"), "events table exists"); + + // make sure we have the correct contents in the database + let rows = await db.execute(SQL.selectAll); + equal( + rows.length, + 5, + "Events that should not be saved have not been, length is 4" + ); + rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.TRACKERS_ID, + }); + equal(rows.length, 1, "Only one day has had tracker entries, length is 1"); + let count = rows[0].getResultByName("count"); + equal(count, 1, "there is only one tracker entry"); + + rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.TRACKING_COOKIES_ID, + }); + equal(rows.length, 1, "Only one day has had cookies entries, length is 1"); + count = rows[0].getResultByName("count"); + equal(count, 3, "Cookie entries were aggregated"); + + rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.CRYPTOMINERS_ID, + }); + equal( + rows.length, + 1, + "Only one day has had cryptominer entries, length is 1" + ); + count = rows[0].getResultByName("count"); + equal(count, 1, "there is only one cryptominer entry"); + + rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.FINGERPRINTERS_ID, + }); + equal( + rows.length, + 1, + "Only one day has had fingerprinters entries, length is 1" + ); + count = rows[0].getResultByName("count"); + equal(count, 3, "there are three fingerprinter entries"); + + rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.SOCIAL_ID, + }); + equal(rows.length, 1, "Only one day has had social entries, length is 1"); + count = rows[0].getResultByName("count"); + equal(count, 2, "there are two social entries"); + + // Use the TrackingDBService API to delete the data. + await TrackingDBService.clearAll(); + // Make sure the data was deleted. + rows = await db.execute(SQL.selectAll); + equal(rows.length, 0, "length is 0"); + await db.close(); +}); + +// This tests that content blocking events encountered on the same day get aggregated, +// and those on different days get seperate entries +add_task(async function test_timestamp_aggragation() { + // This creates the schema. + await TrackingDBService.saveEvents(JSON.stringify({})); + let db = await Sqlite.openConnection({ path: DB_PATH }); + + let yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); + let today = new Date().toISOString(); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKERS_ID, + count: 4, + timestamp: yesterday, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.CRYPTOMINERS_ID, + count: 3, + timestamp: yesterday, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.FINGERPRINTERS_ID, + count: 2, + timestamp: yesterday, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKING_COOKIES_ID, + count: 1, + timestamp: yesterday, + }); + + // Add some events for today which must get aggregated + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKERS_ID, + count: 2, + timestamp: today, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.CRYPTOMINERS_ID, + count: 2, + timestamp: today, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.FINGERPRINTERS_ID, + count: 2, + timestamp: today, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKING_COOKIES_ID, + count: 2, + timestamp: today, + }); + + // Add new events, they will have today's timestamp. + await TrackingDBService.saveEvents(JSON.stringify(LOG)); + + // Ensure events that are inserted today are not aggregated with past events. + let rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.TRACKERS_ID, + }); + equal(rows.length, 2, "Tracker entries for today and yesterday, length is 2"); + for (let i = 0; i < rows.length; i++) { + let count = rows[i].getResultByName("count"); + if (i == 0) { + equal(count, 4, "Yesterday's count is 4"); + } else if (i == 1) { + equal(count, 3, "Today's count is 3, new entries were aggregated"); + } + } + + rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.CRYPTOMINERS_ID, + }); + equal( + rows.length, + 2, + "Cryptominer entries for today and yesterday, length is 2" + ); + for (let i = 0; i < rows.length; i++) { + let count = rows[i].getResultByName("count"); + if (i == 0) { + equal(count, 3, "Yesterday's count is 3"); + } else if (i == 1) { + equal(count, 3, "Today's count is 3, new entries were aggregated"); + } + } + + rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.FINGERPRINTERS_ID, + }); + equal( + rows.length, + 2, + "Fingerprinter entries for today and yesterday, length is 2" + ); + for (let i = 0; i < rows.length; i++) { + let count = rows[i].getResultByName("count"); + if (i == 0) { + equal(count, 2, "Yesterday's count is 2"); + } else if (i == 1) { + equal(count, 5, "Today's count is 5, new entries were aggregated"); + } + } + + rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.TRACKING_COOKIES_ID, + }); + equal( + rows.length, + 2, + "Tracking Cookies entries for today and yesterday, length is 2" + ); + for (let i = 0; i < rows.length; i++) { + let count = rows[i].getResultByName("count"); + if (i == 0) { + equal(count, 1, "Yesterday's count is 1"); + } else if (i == 1) { + equal(count, 5, "Today's count is 5, new entries were aggregated"); + } + } + + // Use the TrackingDBService API to delete the data. + await TrackingDBService.clearAll(); + // Make sure the data was deleted. + rows = await db.execute(SQL.selectAll); + equal(rows.length, 0, "length is 0"); + await db.close(); +}); + +let addEventsToDB = async db => { + let d = new Date(1521009000000); + let date = d.toISOString(); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.CRYPTOMINERS_ID, + count: 3, + timestamp: date, + }); + + date = new Date(d - 2 * 24 * 60 * 60 * 1000).toISOString(); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKERS_ID, + count: 2, + timestamp: date, + }); + + date = new Date(d - 3 * 24 * 60 * 60 * 1000).toISOString(); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKING_COOKIES_ID, + count: 2, + timestamp: date, + }); + + date = new Date(d - 4 * 24 * 60 * 60 * 1000).toISOString(); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKING_COOKIES_ID, + count: 2, + timestamp: date, + }); + + date = new Date(d - 9 * 24 * 60 * 60 * 1000).toISOString(); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.FINGERPRINTERS_ID, + count: 2, + timestamp: date, + }); +}; + +// This tests that TrackingDBService.getEventsByDateRange can accept two timestamps in unix epoch time +// and return entries that occur within the timestamps, rounded to the nearest day and inclusive. +add_task(async function test_getEventsByDateRange() { + // This creates the schema. + await TrackingDBService.saveEvents(JSON.stringify({})); + let db = await Sqlite.openConnection({ path: DB_PATH }); + await addEventsToDB(db); + + let d = new Date(1521009000000); + let daysBefore1 = new Date(d - 24 * 60 * 60 * 1000); + let daysBefore4 = new Date(d - 4 * 24 * 60 * 60 * 1000); + let daysBefore9 = new Date(d - 9 * 24 * 60 * 60 * 1000); + + let events = await TrackingDBService.getEventsByDateRange(daysBefore1, d); + equal( + events.length, + 1, + "There is 1 event entry between the date and one day before, inclusive" + ); + + events = await TrackingDBService.getEventsByDateRange(daysBefore4, d); + equal( + events.length, + 4, + "There is 4 event entries between the date and four days before, inclusive" + ); + + events = await TrackingDBService.getEventsByDateRange( + daysBefore9, + daysBefore4 + ); + equal( + events.length, + 2, + "There is 2 event entries between nine and four days before, inclusive" + ); + + await TrackingDBService.clearAll(); + await db.close(); +}); + +// This tests that TrackingDBService.sumAllEvents returns the number of +// tracking events in the database, and can handle 0 entries. +add_task(async function test_sumAllEvents() { + // This creates the schema. + await TrackingDBService.saveEvents(JSON.stringify({})); + let db = await Sqlite.openConnection({ path: DB_PATH }); + + let sum = await TrackingDBService.sumAllEvents(); + equal(sum, 0, "There have been 0 events recorded"); + + // populate the database + await addEventsToDB(db); + + sum = await TrackingDBService.sumAllEvents(); + equal(sum, 11, "There have been 11 events recorded"); + + await TrackingDBService.clearAll(); + await db.close(); +}); + +// This tests that TrackingDBService.getEarliestRecordedDate returns the +// earliest date recorded and can handle 0 entries. +add_task(async function test_getEarliestRecordedDate() { + // This creates the schema. + await TrackingDBService.saveEvents(JSON.stringify({})); + let db = await Sqlite.openConnection({ path: DB_PATH }); + + let timestamp = await TrackingDBService.getEarliestRecordedDate(); + equal(timestamp, null, "There is no earliest recorded date"); + + // populate the database + await addEventsToDB(db); + let d = new Date(1521009000000); + let daysBefore9 = new Date(d - 9 * 24 * 60 * 60 * 1000) + .toISOString() + .split("T")[0]; + + timestamp = await TrackingDBService.getEarliestRecordedDate(); + let date = new Date(timestamp).toISOString().split("T")[0]; + equal(date, daysBefore9, "The earliest recorded event is nine days before."); + + await TrackingDBService.clearAll(); + await db.close(); +}); + +// This tests that a message to CFR is sent when the amount of saved trackers meets a milestone +add_task(async function test_sendMilestoneNotification() { + let milestones = JSON.parse( + Services.prefs.getStringPref( + "browser.contentblocking.cfr-milestone.milestones" + ) + ); + // This creates the schema. + await TrackingDBService.saveEvents(JSON.stringify({})); + let db = await Sqlite.openConnection({ path: DB_PATH }); + // save number of trackers equal to the first milestone + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.CRYPTOMINERS_ID, + count: milestones[0], + timestamp: new Date().toISOString(), + }); + + let awaitNotification = TestUtils.topicObserved( + "SiteProtection:ContentBlockingMilestone" + ); + + // trigger a "save" event to compare the trackers with the milestone. + await TrackingDBService.saveEvents( + JSON.stringify({ + "https://1.example.com": [ + [Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT, true, 1], + ], + }) + ); + await awaitNotification; + + await TrackingDBService.clearAll(); + await db.close(); +}); |