|
|
1
|
+"use strict";
|
|
|
2
|
+
|
|
|
3
|
+const { Downloader } = ChromeUtils.importESModule(
|
|
|
4
|
+ "resource://services-settings/Attachments.sys.mjs"
|
|
|
5
|
+);
|
|
|
6
|
+const { sinon } = ChromeUtils.importESModule(
|
|
|
7
|
+ "resource://testing-common/Sinon.sys.mjs"
|
|
|
8
|
+);
|
|
|
9
|
+
|
|
|
10
|
+// A collection with a dump that's packaged on all test builds.
|
|
|
11
|
+const TEST_BUCKET = "main";
|
|
|
12
|
+const TEST_COLLECTION = "password-recipes";
|
|
|
13
|
+
|
|
|
14
|
+let client;
|
|
|
15
|
+let DUMP_RECORDS;
|
|
|
16
|
+let DUMP_LAST_MODIFIED;
|
|
|
17
|
+let SERVER_LAST_MODIFIED;
|
|
|
18
|
+let server;
|
|
|
19
|
+let serverRequests = [];
|
|
|
20
|
+
|
|
|
21
|
+function assertNoServerRequests() {
|
|
|
22
|
+ Assert.deepEqual(serverRequests, [], "the server received no requests");
|
|
|
23
|
+ serverRequests = [];
|
|
|
24
|
+}
|
|
|
25
|
+
|
|
|
26
|
+async function importData(records) {
|
|
|
27
|
+ await RemoteSettingsWorker._execute("_test_only_import", [
|
|
|
28
|
+ TEST_BUCKET,
|
|
|
29
|
+ TEST_COLLECTION,
|
|
|
30
|
+ records,
|
|
|
31
|
+ records[0]?.last_modified || 0,
|
|
|
32
|
+ ]);
|
|
|
33
|
+}
|
|
|
34
|
+
|
|
|
35
|
+async function pollChangesFromOutdatedData() {
|
|
|
36
|
+ await importData([{ id: "dummy", last_modified: 1 }]);
|
|
|
37
|
+ const seenNotification = TestUtils.topicObserved(
|
|
|
38
|
+ "remote-settings:changes-poll-end"
|
|
|
39
|
+ );
|
|
|
40
|
+ await RemoteSettings.pollChanges({ trigger: "startup" });
|
|
|
41
|
+ await seenNotification;
|
|
|
42
|
+}
|
|
|
43
|
+
|
|
|
44
|
+add_setup(async () => {
|
|
|
45
|
+ Assert.ok(
|
|
|
46
|
+ AppConstants.BASE_BROWSER_VERSION,
|
|
|
47
|
+ "This test only makes sense when built with --with-base-browser-version"
|
|
|
48
|
+ );
|
|
|
49
|
+
|
|
|
50
|
+ client = RemoteSettings(TEST_COLLECTION, { bucketName: TEST_BUCKET });
|
|
|
51
|
+ // Opening the DB from the main thread creates/upgrades it as needed.
|
|
|
52
|
+ // The worker-based import used by importData() below opens it with
|
|
|
53
|
+ // allowUpgrades=false, so it requires the DB to already exist.
|
|
|
54
|
+ await client.db.getLastModified();
|
|
|
55
|
+
|
|
|
56
|
+ const dump = await SharedUtils.loadJSONDump(TEST_BUCKET, TEST_COLLECTION);
|
|
|
57
|
+ DUMP_RECORDS = dump.data;
|
|
|
58
|
+ DUMP_LAST_MODIFIED = dump.timestamp;
|
|
|
59
|
+ Assert.greater(
|
|
|
60
|
+ DUMP_LAST_MODIFIED,
|
|
|
61
|
+ 1,
|
|
|
62
|
+ "the dump is newer than the outdated data used by the tests"
|
|
|
63
|
+ );
|
|
|
64
|
+
|
|
|
65
|
+ // The server offers data that is newer than the dump, so that processing
|
|
|
66
|
+ // anything from it would be observable.
|
|
|
67
|
+ SERVER_LAST_MODIFIED = DUMP_LAST_MODIFIED + 1000;
|
|
|
68
|
+
|
|
|
69
|
+ server = new HttpServer();
|
|
|
70
|
+ server.start(-1);
|
|
|
71
|
+ registerCleanupFunction(() => server.stop(() => {}));
|
|
|
72
|
+ const origin = `http://localhost:${server.identity.primaryPort}`;
|
|
|
73
|
+ const attachment = await IOUtils.readUTF8(
|
|
|
74
|
+ do_get_file(
|
|
|
75
|
+ "test_attachments_downloader/65650a0f-7c22-4c10-9744-2d67e301f5f4.pem"
|
|
|
76
|
+ ).path
|
|
|
77
|
+ );
|
|
|
78
|
+ const responses = {
|
|
|
79
|
+ "/v1/": {
|
|
|
80
|
+ capabilities: { attachments: { base_url: `${origin}/cdn/` } },
|
|
|
81
|
+ },
|
|
|
82
|
+ "/v1/buckets/monitor/collections/changes/changeset": {
|
|
|
83
|
+ timestamp: SERVER_LAST_MODIFIED,
|
|
|
84
|
+ changes: [
|
|
|
85
|
+ {
|
|
|
86
|
+ bucket: TEST_BUCKET,
|
|
|
87
|
+ collection: TEST_COLLECTION,
|
|
|
88
|
+ last_modified: SERVER_LAST_MODIFIED,
|
|
|
89
|
+ },
|
|
|
90
|
+ ],
|
|
|
91
|
+ },
|
|
|
92
|
+ [`/v1/buckets/${TEST_BUCKET}/collections/${TEST_COLLECTION}/changeset`]: {
|
|
|
93
|
+ timestamp: SERVER_LAST_MODIFIED,
|
|
|
94
|
+ metadata: {},
|
|
|
95
|
+ changes: [{ id: "from-server", last_modified: SERVER_LAST_MODIFIED }],
|
|
|
96
|
+ },
|
|
|
97
|
+ };
|
|
|
98
|
+ server.registerPrefixHandler("/", (request, response) => {
|
|
|
99
|
+ serverRequests.push(request.path);
|
|
|
100
|
+ if (request.path.startsWith("/cdn/")) {
|
|
|
101
|
+ response.write(attachment);
|
|
|
102
|
+ } else if (request.path in responses) {
|
|
|
103
|
+ response.setHeader("Content-Type", "application/json; charset=UTF-8");
|
|
|
104
|
+ response.write(JSON.stringify(responses[request.path]));
|
|
|
105
|
+ } else {
|
|
|
106
|
+ response.setStatusLine(null, 404, "Not Found");
|
|
|
107
|
+ }
|
|
|
108
|
+ });
|
|
|
109
|
+ Services.prefs.setStringPref("services.settings.server", `${origin}/v1`);
|
|
|
110
|
+ // Tor browser routes everything through a SOCKS proxy by default, which
|
|
|
111
|
+ // would prevent requests from ever reaching the server.
|
|
|
112
|
+ Services.prefs.setIntPref("network.proxy.type", 0);
|
|
|
113
|
+});
|
|
|
114
|
+
|
|
|
115
|
+add_task(function test_shouldSkipRemoteActivity_is_always_true() {
|
|
|
116
|
+ Assert.ok(
|
|
|
117
|
+ Utils.shouldSkipRemoteActivity,
|
|
|
118
|
+ "Remote activity is always skipped for base browser builds"
|
|
|
119
|
+ );
|
|
|
120
|
+});
|
|
|
121
|
+
|
|
|
122
|
+add_task(async function test_appconstants_disable_remote_settings_server() {
|
|
|
123
|
+ Assert.deepEqual(
|
|
|
124
|
+ AppConstants.REMOTE_SETTINGS_SERVER_URLS,
|
|
|
125
|
+ [""],
|
|
|
126
|
+ "No real Remote Settings server is configured"
|
|
|
127
|
+ );
|
|
|
128
|
+ Assert.equal(
|
|
|
129
|
+ AppConstants.REMOTE_SETTINGS_VERIFY_SIGNATURE,
|
|
|
130
|
+ false,
|
|
|
131
|
+ "Signature verification is disabled, since we never fetch from a server"
|
|
|
132
|
+ );
|
|
|
133
|
+ Assert.equal(
|
|
|
134
|
+ Services.prefs
|
|
|
135
|
+ .getDefaultBranch("")
|
|
|
136
|
+ .getStringPref("services.settings.server", ""),
|
|
|
137
|
+ "",
|
|
|
138
|
+ "The services.settings.server pref has no default value"
|
|
|
139
|
+ );
|
|
|
140
|
+});
|
|
|
141
|
+
|
|
|
142
|
+add_task(async function test_sync_never_contacts_server() {
|
|
|
143
|
+ await importData([{ id: "dummy", last_modified: 1 }]);
|
|
|
144
|
+
|
|
|
145
|
+ await client.sync();
|
|
|
146
|
+
|
|
|
147
|
+ Assert.equal(
|
|
|
148
|
+ await client.getLastModified(),
|
|
|
149
|
+ 1,
|
|
|
150
|
+ "sync() did not touch local data"
|
|
|
151
|
+ );
|
|
|
152
|
+ assertNoServerRequests();
|
|
|
153
|
+
|
|
|
154
|
+ // Sanity check: the server does offer newer data for this collection.
|
|
|
155
|
+ const { remoteTimestamp } = await client._fetchChangeset();
|
|
|
156
|
+ Assert.equal(
|
|
|
157
|
+ remoteTimestamp,
|
|
|
158
|
+ SERVER_LAST_MODIFIED,
|
|
|
159
|
+ "the server offers newer data"
|
|
|
160
|
+ );
|
|
|
161
|
+ serverRequests = [];
|
|
|
162
|
+});
|
|
|
163
|
+
|
|
|
164
|
+add_task(async function test_pollChanges_imports_from_newer_local_dump() {
|
|
|
165
|
+ RemoteSettings._initialized = false;
|
|
|
166
|
+
|
|
|
167
|
+ await pollChangesFromOutdatedData();
|
|
|
168
|
+
|
|
|
169
|
+ Assert.equal(
|
|
|
170
|
+ await client.getLastModified(),
|
|
|
171
|
+ DUMP_LAST_MODIFIED,
|
|
|
172
|
+ "local data was updated from the packaged dump"
|
|
|
173
|
+ );
|
|
|
174
|
+ const current = await client.get({ loadDumpIfNewer: false });
|
|
|
175
|
+ Assert.equal(
|
|
|
176
|
+ current.length,
|
|
|
177
|
+ DUMP_RECORDS.length,
|
|
|
178
|
+ "all dump records imported"
|
|
|
179
|
+ );
|
|
|
180
|
+ assertNoServerRequests();
|
|
|
181
|
+
|
|
|
182
|
+ // Sanity check: the server does advertise newer changes.
|
|
|
183
|
+ const { changes } = await Utils.fetchLatestChanges(Utils.SERVER_URL);
|
|
|
184
|
+ Assert.deepEqual(
|
|
|
185
|
+ changes.map(c => c.last_modified),
|
|
|
186
|
+ [SERVER_LAST_MODIFIED],
|
|
|
187
|
+ "the server advertises newer changes"
|
|
|
188
|
+ );
|
|
|
189
|
+ serverRequests = [];
|
|
|
190
|
+});
|
|
|
191
|
+
|
|
|
192
|
+add_task(async function test_pollChanges_is_noop_after_first_call() {
|
|
|
193
|
+ RemoteSettings._initialized = false;
|
|
|
194
|
+ await pollChangesFromOutdatedData();
|
|
|
195
|
+
|
|
|
196
|
+ // The local dump import is not awaited by pollChanges(), but it calls
|
|
|
197
|
+ // hasLocalDump() synchronously, so this spy tells us deterministically
|
|
|
198
|
+ // whether an import was started.
|
|
|
199
|
+ const hasLocalDumpSpy = sinon.spy(Utils, "hasLocalDump");
|
|
|
200
|
+
|
|
|
201
|
+ // This guards against bug 1730026, where GeckoView calls this twice.
|
|
|
202
|
+ await RemoteSettings.pollChanges({ trigger: "timer" });
|
|
|
203
|
+ Assert.ok(
|
|
|
204
|
+ hasLocalDumpSpy.notCalled,
|
|
|
205
|
+ "second call to pollChanges() is a no-op"
|
|
|
206
|
+ );
|
|
|
207
|
+
|
|
|
208
|
+ // Sanity check: without the guard, the same call starts an import.
|
|
|
209
|
+ RemoteSettings._initialized = false;
|
|
|
210
|
+ await pollChangesFromOutdatedData();
|
|
|
211
|
+ Assert.ok(
|
|
|
212
|
+ hasLocalDumpSpy.called,
|
|
|
213
|
+ "pollChanges() starts an import when unguarded"
|
|
|
214
|
+ );
|
|
|
215
|
+
|
|
|
216
|
+ hasLocalDumpSpy.restore();
|
|
|
217
|
+ assertNoServerRequests();
|
|
|
218
|
+});
|
|
|
219
|
+
|
|
|
220
|
+add_task(async function test_pollChanges_never_records_sync_history() {
|
|
|
221
|
+ RemoteSettings._initialized = false;
|
|
|
222
|
+ const syncHistory = new SyncHistory("settings-sync");
|
|
|
223
|
+ await syncHistory.clear();
|
|
|
224
|
+
|
|
|
225
|
+ await pollChangesFromOutdatedData();
|
|
|
226
|
+
|
|
|
227
|
+ const history = await syncHistory.list();
|
|
|
228
|
+ Assert.equal(history.length, 0, "no sync history was recorded");
|
|
|
229
|
+ assertNoServerRequests();
|
|
|
230
|
+});
|
|
|
231
|
+
|
|
|
232
|
+add_task(async function test_downloader_never_downloads_from_server() {
|
|
|
233
|
+ // Same fixture as test_attachments_downloader.js: a record whose attachment
|
|
|
234
|
+ // is not available from any dump or cache.
|
|
|
235
|
+ const record = {
|
|
|
236
|
+ id: "1f3a0802-648d-11ea-bd79-876a8b69c377",
|
|
|
237
|
+ attachment: {
|
|
|
238
|
+ hash: "f41ed47d0f43325c9f089d03415c972ce1d3f1ecab6e4d6260665baf3db3ccee",
|
|
|
239
|
+ size: 1597,
|
|
|
240
|
+ filename: "test_file.pem",
|
|
|
241
|
+ location:
|
|
|
242
|
+ "main-workspace/some-collection/65650a0f-7c22-4c10-9744-2d67e301f5f4.pem",
|
|
|
243
|
+ mimetype: "application/x-pem-file",
|
|
|
244
|
+ },
|
|
|
245
|
+ };
|
|
|
246
|
+ const downloader = new Downloader(TEST_BUCKET, "some-collection");
|
|
|
247
|
+
|
|
|
248
|
+ // There's no local dump or cache for this attachment, so download must fail
|
|
|
249
|
+ // rather than fall back to the network.
|
|
|
250
|
+ await Assert.rejects(
|
|
|
251
|
+ downloader.downloadAsBytes(record),
|
|
|
252
|
+ e => e instanceof Downloader.NotFoundError,
|
|
|
253
|
+ "download never falls back to the network"
|
|
|
254
|
+ );
|
|
|
255
|
+ assertNoServerRequests();
|
|
|
256
|
+
|
|
|
257
|
+ // Sanity check: the server does serve this attachment.
|
|
|
258
|
+ const { size, hash, location } = record.attachment;
|
|
|
259
|
+ const buffer = await downloader._fetchAttachment(
|
|
|
260
|
+ (await Utils.baseAttachmentsURL()) + location
|
|
|
261
|
+ );
|
|
|
262
|
+ Assert.ok(
|
|
|
263
|
+ await RemoteSettingsWorker.checkContentHash(buffer, size, hash),
|
|
|
264
|
+ "the server serves the attachment"
|
|
|
265
|
+ );
|
|
|
266
|
+ serverRequests = [];
|
|
|
267
|
+}); |