summaryrefslogtreecommitdiffstats
path: root/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_fetch.js
blob: 92bf61dbde36daf8020673232a39abe5123f7590 (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
/* Any copyright is dedicated to the Public Domain.
 * https://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

/**
 * @fileOverview Tests the MLBF and RemoteSettings synchronization logic.
 */

Services.prefs.setBoolPref("extensions.blocklist.useMLBF", true);

const { Downloader } = ChromeUtils.importESModule(
  "resource://services-settings/Attachments.sys.mjs"
);

const ExtensionBlocklistMLBF = getExtensionBlocklistMLBF();

// This test needs to interact with the RemoteSettings client.
ExtensionBlocklistMLBF.ensureInitialized();

add_task(async function fetch_invalid_mlbf_record() {
  let invalidRecord = {
    attachment: { size: 1, hash: "definitely not valid" },
    generation_time: 1,
  };

  // _fetchMLBF(invalidRecord) may succeed if there is a MLBF dump packaged with
  // the application. This test intentionally hides the actual path to get
  // deterministic results. To check whether the dump is correctly registered,
  // run test_blocklist_mlbf_dump.js

  // Forget about the packaged attachment.
  Downloader._RESOURCE_BASE_URL = "invalid://bogus";
  // NetworkError is expected here. The JSON.parse error could be triggered via
  // _baseAttachmentsURL < downloadAsBytes < download < download < _fetchMLBF if
  // the request to  services.settings.server ("data:,#remote-settings-dummy/v1")
  // is fulfilled (but with invalid JSON). That request is not expected to be
  // fulfilled in the first place, but that is not a concern of this test.
  // This test passes if _fetchMLBF() rejects when given an invalid record.
  await Assert.rejects(
    ExtensionBlocklistMLBF._fetchMLBF(invalidRecord),
    /NetworkError|SyntaxError: JSON\.parse/,
    "record not found when there is no packaged MLBF"
  );
});

// Other tests can mock _testMLBF, so let's verify that it works as expected.
add_task(async function fetch_valid_mlbf() {
  await ExtensionBlocklistMLBF._client.db.saveAttachment(
    ExtensionBlocklistMLBF.RS_ATTACHMENT_ID,
    { record: MLBF_RECORD, blob: await load_mlbf_record_as_blob() }
  );

  const result = await ExtensionBlocklistMLBF._fetchMLBF(MLBF_RECORD);
  Assert.equal(result.cascadeHash, MLBF_RECORD.attachment.hash, "hash OK");
  Assert.equal(result.generationTime, MLBF_RECORD.generation_time, "time OK");
  Assert.ok(result.cascadeFilter.has("@blocked:1"), "item blocked");
  Assert.ok(!result.cascadeFilter.has("@unblocked:2"), "item not blocked");

  const result2 = await ExtensionBlocklistMLBF._fetchMLBF({
    attachment: { size: 1, hash: "invalid" },
    generation_time: Date.now(),
  });
  Assert.equal(
    result2.cascadeHash,
    MLBF_RECORD.attachment.hash,
    "The cached MLBF should be used when the attachment is invalid"
  );

  // The attachment is kept in the database for use by the next test task.
});

// Test that results of the public API are consistent with the MLBF file.
add_task(async function public_api_uses_mlbf() {
  createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
  await promiseStartupManager();

  const blockedAddon = {
    id: "@blocked",
    version: "1",
    signedDate: new Date(0), // a date in the past, before MLBF's generationTime.
    signedState: AddonManager.SIGNEDSTATE_SIGNED,
  };
  const nonBlockedAddon = {
    id: "@unblocked",
    version: "2",
    signedDate: new Date(0), // a date in the past, before MLBF's generationTime.
    signedState: AddonManager.SIGNEDSTATE_SIGNED,
  };

  await AddonTestUtils.loadBlocklistRawData({ extensionsMLBF: [MLBF_RECORD] });

  Assert.deepEqual(
    await Blocklist.getAddonBlocklistEntry(blockedAddon),
    {
      state: Ci.nsIBlocklistService.STATE_BLOCKED,
      url: "https://addons.mozilla.org/en-US/xpcshell/blocked-addon/@blocked/1/",
    },
    "Blocked addon should have blocked entry"
  );

  Assert.deepEqual(
    await Blocklist.getAddonBlocklistEntry(nonBlockedAddon),
    null,
    "Non-blocked addon should not be blocked"
  );

  Assert.equal(
    await Blocklist.getAddonBlocklistState(blockedAddon),
    Ci.nsIBlocklistService.STATE_BLOCKED,
    "Blocked entry should have blocked state"
  );

  Assert.equal(
    await Blocklist.getAddonBlocklistState(nonBlockedAddon),
    Ci.nsIBlocklistService.STATE_NOT_BLOCKED,
    "Non-blocked entry should have unblocked state"
  );

  // Note: Blocklist collection and attachment carries over to the next test.
});

// Verifies that the metadata (time of validity) of an updated MLBF record is
// correctly used, even if the MLBF itself has not changed.
add_task(async function fetch_updated_mlbf_same_hash() {
  const recordUpdate = {
    ...MLBF_RECORD,
    generation_time: MLBF_RECORD.generation_time + 1,
  };
  const blockedAddonUpdate = {
    id: "@blocked",
    version: "1",
    signedDate: new Date(recordUpdate.generation_time),
    signedState: AddonManager.SIGNEDSTATE_SIGNED,
  };

  // The blocklist already includes "@blocked:1", but the last specified
  // generation time is MLBF_RECORD.generation_time. So the addon cannot be
  // blocked, because the block decision could be a false positive.
  Assert.equal(
    await Blocklist.getAddonBlocklistState(blockedAddonUpdate),
    Ci.nsIBlocklistService.STATE_NOT_BLOCKED,
    "Add-on not blocked before blocklist update"
  );

  await AddonTestUtils.loadBlocklistRawData({ extensionsMLBF: [recordUpdate] });
  // The MLBF is now known to apply to |blockedAddonUpdate|.

  Assert.equal(
    await Blocklist.getAddonBlocklistState(blockedAddonUpdate),
    Ci.nsIBlocklistService.STATE_BLOCKED,
    "Add-on blocked after update"
  );

  // Note: Blocklist collection and attachment carries over to the next test.
});

// Checks the remaining cases of database corruption that haven't been handled
// before.
add_task(async function handle_database_corruption() {
  const blockedAddon = {
    id: "@blocked",
    version: "1",
    signedDate: new Date(0), // a date in the past, before MLBF's generationTime.
    signedState: AddonManager.SIGNEDSTATE_SIGNED,
  };
  async function checkBlocklistWorks() {
    Assert.equal(
      await Blocklist.getAddonBlocklistState(blockedAddon),
      Ci.nsIBlocklistService.STATE_BLOCKED,
      "Add-on should be blocked by the blocklist"
    );
  }

  let fetchCount = 0;
  const originalFetchMLBF = ExtensionBlocklistMLBF._fetchMLBF;
  ExtensionBlocklistMLBF._fetchMLBF = function () {
    ++fetchCount;
    return originalFetchMLBF.apply(this, arguments);
  };

  // In the fetch_invalid_mlbf_record we checked that a cached / packaged MLBF
  // attachment is used as a fallback when the record is invalid. Here we also
  // check that there is a fallback when there is no record at all.

  // Include a dummy record in the list, to prevent RemoteSettings from
  // importing a JSON dump with unexpected records.
  await AddonTestUtils.loadBlocklistRawData({ extensionsMLBF: [{}] });
  Assert.equal(fetchCount, 1, "MLBF read once despite bad record");
  // When the collection is empty, the last known MLBF should be used anyway.
  await checkBlocklistWorks();
  Assert.equal(fetchCount, 1, "MLBF not read again by blocklist query");

  // Now we also remove the cached file...
  await ExtensionBlocklistMLBF._client.db.saveAttachment(
    ExtensionBlocklistMLBF.RS_ATTACHMENT_ID,
    null
  );
  Assert.equal(fetchCount, 1, "MLBF not read again after attachment deletion");
  // Deleting the file shouldn't cause issues because the MLBF is loaded once
  // and then kept in memory.
  await checkBlocklistWorks();
  Assert.equal(fetchCount, 1, "MLBF not read again by blocklist query 2");

  // Force an update while we don't have any blocklist data nor cache.
  await ExtensionBlocklistMLBF._onUpdate();
  Assert.equal(fetchCount, 2, "MLBF read again at forced update");
  // As a fallback, continue to use the in-memory version of the blocklist.
  await checkBlocklistWorks();
  Assert.equal(fetchCount, 2, "MLBF not read again by blocklist query 3");

  // Memory gone, e.g. after a browser restart.
  delete ExtensionBlocklistMLBF._mlbfData;
  delete ExtensionBlocklistMLBF._stashes;
  Assert.equal(
    await Blocklist.getAddonBlocklistState(blockedAddon),
    Ci.nsIBlocklistService.STATE_NOT_BLOCKED,
    "Blocklist can't work if all blocklist data is gone"
  );
  Assert.equal(fetchCount, 3, "MLBF read again after restart/cleared cache");
  Assert.equal(
    await Blocklist.getAddonBlocklistState(blockedAddon),
    Ci.nsIBlocklistService.STATE_NOT_BLOCKED,
    "Blocklist can still not work if all blocklist data is gone"
  );
  // Ideally, the client packages a dump. But if the client did not package the
  // dump, then it should not be trying to read the data over and over again.
  Assert.equal(fetchCount, 3, "MLBF not read again despite absence of MLBF");

  ExtensionBlocklistMLBF._fetchMLBF = originalFetchMLBF;
});