summaryrefslogtreecommitdiffstats
path: root/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_purge.js
blob: 5ede57a08b54e6d3380bc180232a11bf728f1623 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

const { SiteDataTestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/SiteDataTestUtils.sys.mjs"
);

let btp;
let bounceTrackingGracePeriodSec;
let bounceTrackingActivationLifetimeSec;

/**
 * Adds brackets to a host if it's an IPv6 address.
 * @param {string} host - Host which may be an IPv6.
 * @returns {string} bracketed IPv6 or host if host is not an IPv6.
 */
function maybeFixupIpv6(host) {
  if (!host.includes(":")) {
    return host;
  }
  return `[${host}]`;
}

/**
 * Adds cookies and indexedDB test data for the given host.
 * @param {string} host
 */
async function addStateForHost(host) {
  info(`adding state for host ${host}`);
  SiteDataTestUtils.addToCookies({ host });
  await SiteDataTestUtils.addToIndexedDB(`https://${maybeFixupIpv6(host)}`);
}

/**
 * Checks if the given host as cookies or indexedDB data.
 * @param {string} host
 * @returns {boolean}
 */
async function hasStateForHost(host) {
  let origin = `https://${maybeFixupIpv6(host)}`;
  if (SiteDataTestUtils.hasCookies(origin)) {
    return true;
  }
  return SiteDataTestUtils.hasIndexedDB(origin);
}

/**
 * Assert that there are no bounce tracker candidates or user activations
 * recorded.
 */
function assertEmpty() {
  Assert.equal(
    btp.testGetBounceTrackerCandidateHosts({}).length,
    0,
    "No tracker candidates."
  );
  Assert.equal(
    btp.testGetUserActivationHosts({}).length,
    0,
    "No user activation hosts."
  );
}

add_setup(function () {
  // Need a profile to data clearing calls.
  do_get_profile();

  btp = Cc["@mozilla.org/bounce-tracking-protection;1"].getService(
    Ci.nsIBounceTrackingProtection
  );

  // Reset global bounce tracking state.
  btp.clearAll();

  bounceTrackingGracePeriodSec = Services.prefs.getIntPref(
    "privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec"
  );
  bounceTrackingActivationLifetimeSec = Services.prefs.getIntPref(
    "privacy.bounceTrackingProtection.bounceTrackingActivationLifetimeSec"
  );
});

/**
 * When both maps are empty running PurgeBounceTrackers should be a no-op.
 */
add_task(async function test_empty() {
  assertEmpty();

  info("Run PurgeBounceTrackers");
  await btp.testRunPurgeBounceTrackers();

  assertEmpty();
});

/**
 * Tests that the PurgeBounceTrackers behaves as expected by adding site state
 * and adding simulated bounce state and user activations.
 */
add_task(async function test_purge() {
  let now = Date.now();

  // Epoch in MS.
  let timestampWithinGracePeriod =
    now - (bounceTrackingGracePeriodSec * 1000) / 2;
  let timestampWithinGracePeriod2 =
    now - (bounceTrackingGracePeriodSec * 1000) / 4;
  let timestampOutsideGracePeriodFiveSeconds =
    now - (bounceTrackingGracePeriodSec + 5) * 1000;
  let timestampOutsideGracePeriodThreeDays =
    now - (bounceTrackingGracePeriodSec + 60 * 60 * 24 * 3) * 1000;
  let timestampFuture = now + bounceTrackingGracePeriodSec * 1000 * 2;

  let timestampValidUserActivation =
    now - (bounceTrackingActivationLifetimeSec * 1000) / 2;
  let timestampExpiredUserActivationFourSeconds =
    now - (bounceTrackingActivationLifetimeSec + 4) * 1000;
  let timestampExpiredUserActivationTenDays =
    now - (bounceTrackingActivationLifetimeSec + 60 * 60 * 24 * 10) * 1000;

  const TEST_TRACKERS = {
    "example.com": {
      bounceTime: timestampWithinGracePeriod,
      userActivationTime: null,
      message: "Should not purge within grace period.",
      shouldPurge: bounceTrackingGracePeriodSec == 0,
    },
    "example2.com": {
      bounceTime: timestampWithinGracePeriod2,
      userActivationTime: null,
      message: "Should not purge within grace period (2).",
      shouldPurge: bounceTrackingGracePeriodSec == 0,
    },
    "example.net": {
      bounceTime: timestampOutsideGracePeriodFiveSeconds,
      userActivationTime: null,
      message: "Should purge after grace period.",
      shouldPurge: true,
    },
    // Also ensure that clear data calls with IP sites succeed.
    "1.2.3.4": {
      bounceTime: timestampOutsideGracePeriodThreeDays,
      userActivationTime: null,
      message: "Should purge after grace period (2).",
      shouldPurge: true,
    },
    "2606:4700:4700::1111": {
      bounceTime: timestampOutsideGracePeriodThreeDays,
      userActivationTime: null,
      message: "Should purge after grace period (3).",
      shouldPurge: true,
    },
    "example.org": {
      bounceTime: timestampWithinGracePeriod,
      userActivationTime: null,
      message: "Should not purge within grace period.",
      shouldPurge: false,
    },
    "example2.org": {
      bounceTime: timestampFuture,
      userActivationTime: null,
      message: "Should not purge for future bounce time (within grace period).",
      shouldPurge: false,
    },
    "1.1.1.1": {
      bounceTime: null,
      userActivationTime: timestampValidUserActivation,
      message: "Should not purge without bounce (valid user activation).",
      shouldPurge: false,
    },
    // Also testing domains with trailing ".".
    "mozilla.org.": {
      bounceTime: null,
      userActivationTime: timestampExpiredUserActivationFourSeconds,
      message: "Should not purge without bounce (expired user activation).",
      shouldPurge: false,
    },
    "firefox.com": {
      bounceTime: null,
      userActivationTime: timestampExpiredUserActivationTenDays,
      message: "Should not purge without bounce (expired user activation) (2).",
      shouldPurge: false,
    },
  };

  info("Assert empty initially.");
  assertEmpty();

  info("Populate bounce and user activation sets.");

  let expectedBounceTrackerHosts = [];
  let expectedUserActivationHosts = [];

  let expiredUserActivationHosts = [];
  let expectedPurgedHosts = [];

  // This would normally happen over time while browsing.
  let initPromises = Object.entries(TEST_TRACKERS).map(
    async ([siteHost, { bounceTime, userActivationTime, shouldPurge }]) => {
      // Add site state so we can later assert it has been purged.
      await addStateForHost(siteHost);

      if (bounceTime != null) {
        if (userActivationTime != null) {
          throw new Error(
            "Attempting to construct invalid map state. testGetBounceTrackerCandidateHosts({}) and testGetUserActivationHosts({}) must be disjoint."
          );
        }

        expectedBounceTrackerHosts.push(siteHost);

        // Convert bounceTime timestamp to nanoseconds (PRTime).
        info(
          `Adding bounce. siteHost: ${siteHost}, bounceTime: ${bounceTime} ms`
        );
        btp.testAddBounceTrackerCandidate({}, siteHost, bounceTime * 1000);
      }

      if (userActivationTime != null) {
        if (bounceTime != null) {
          throw new Error(
            "Attempting to construct invalid map state. testGetBounceTrackerCandidateHosts({}) and testGetUserActivationHosts({}) must be disjoint."
          );
        }

        expectedUserActivationHosts.push(siteHost);
        if (
          userActivationTime + bounceTrackingActivationLifetimeSec * 1000 >
          now
        ) {
          expiredUserActivationHosts.push(siteHost);
        }

        // Convert userActivationTime timestamp to nanoseconds (PRTime).
        info(
          `Adding user interaction. siteHost: ${siteHost}, userActivationTime: ${userActivationTime} ms`
        );
        btp.testAddUserActivation({}, siteHost, userActivationTime * 1000);
      }

      if (shouldPurge) {
        expectedPurgedHosts.push(siteHost);
      }
    }
  );
  await Promise.all(initPromises);

  info(
    "Check that bounce and user activation data has been correctly recorded."
  );
  Assert.deepEqual(
    btp.testGetBounceTrackerCandidateHosts({}).sort(),
    expectedBounceTrackerHosts.sort(),
    "Has added bounce tracker hosts."
  );
  Assert.deepEqual(
    btp.testGetUserActivationHosts({}).sort(),
    expectedUserActivationHosts.sort(),
    "Has added user activation hosts."
  );

  info("Run PurgeBounceTrackers");
  let actualPurgedHosts = await btp.testRunPurgeBounceTrackers();

  Assert.deepEqual(
    actualPurgedHosts.sort(),
    expectedPurgedHosts.sort(),
    "Should have purged all expected hosts."
  );

  let expectedBounceTrackerHostsAfterPurge = expectedBounceTrackerHosts
    .filter(host => !expectedPurgedHosts.includes(host))
    .sort();
  Assert.deepEqual(
    btp.testGetBounceTrackerCandidateHosts({}).sort(),
    expectedBounceTrackerHostsAfterPurge.sort(),
    "After purge the bounce tracker candidate host set should be updated correctly."
  );

  Assert.deepEqual(
    btp.testGetUserActivationHosts({}).sort(),
    expiredUserActivationHosts.sort(),
    "After purge any expired user activation records should have been removed"
  );

  info("Test that we actually purged the correct sites.");
  for (let siteHost of expectedPurgedHosts) {
    Assert.ok(
      !(await hasStateForHost(siteHost)),
      `Site ${siteHost} should no longer have state.`
    );
  }
  for (let siteHost of expectedBounceTrackerHostsAfterPurge) {
    Assert.ok(
      await hasStateForHost(siteHost),
      `Site ${siteHost} should still have state.`
    );
  }

  info("Reset bounce tracking state.");
  btp.clearAll();
  assertEmpty();

  info("Clean up site data.");
  await SiteDataTestUtils.clear();
});