Bug 1857386 [wpt PR 42383] - Update wpt metadata, a=testonly
[gecko.git] / netwerk / test / unit / test_cookies_async_failure.js
blobc61da23f9943535b83a9243cbb4fd494a26d2512
1 /* Any copyright is dedicated to the Public Domain.
2    http://creativecommons.org/publicdomain/zero/1.0/ */
4 // Test the various ways opening a cookie database can fail in an asynchronous
5 // (i.e. after synchronous initialization) manner, and that the database is
6 // renamed and recreated under each circumstance. These circumstances are, in no
7 // particular order:
8 //
9 // 1) A write operation failing after the database has been read in.
10 // 2) Asynchronous read failure due to a corrupt database.
11 // 3) Synchronous read failure due to a corrupt database, when reading:
12 //    a) a single base domain;
13 //    b) the entire database.
14 // 4) Asynchronous read failure, followed by another failure during INSERT but
15 //    before the database closes for rebuilding. (The additional error should be
16 //    ignored.)
17 // 5) Asynchronous read failure, followed by an INSERT failure during rebuild.
18 //    This should result in an abort of the database rebuild; the partially-
19 //    built database should be moved to 'cookies.sqlite.bak-rebuild'.
21 "use strict";
23 let profile;
24 let cookie;
26 add_task(async () => {
27   // Set up a profile.
28   profile = do_get_profile();
29   Services.prefs.setBoolPref("dom.security.https_first", false);
31   // Allow all cookies.
32   Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
33   Services.prefs.setBoolPref(
34     "network.cookieJarSettings.unblocked_for_testing",
35     true
36   );
38   // Bug 1617611 - Fix all the tests broken by "cookies SameSite=Lax by default"
39   Services.prefs.setBoolPref("network.cookie.sameSite.laxByDefault", false);
41   // The server.
42   const hosts = ["foo.com", "hither.com", "haithur.com", "bar.com"];
43   for (let i = 0; i < 3000; ++i) {
44     hosts.push(i + ".com");
45   }
46   CookieXPCShellUtils.createServer({ hosts });
48   // Get the cookie file and the backup file.
49   Assert.ok(!do_get_cookie_file(profile).exists());
50   Assert.ok(!do_get_backup_file().exists());
52   // Create a cookie object for testing.
53   let now = Date.now() * 1000;
54   let futureExpiry = Math.round(now / 1e6 + 1000);
55   cookie = new Cookie(
56     "oh",
57     "hai",
58     "bar.com",
59     "/",
60     futureExpiry,
61     now,
62     now,
63     false,
64     false,
65     false
66   );
68   await run_test_1();
69   await run_test_2();
70   await run_test_3();
71   await run_test_4();
72   await run_test_5();
73   Services.prefs.clearUserPref("dom.security.https_first");
74   Services.prefs.clearUserPref("network.cookie.sameSite.laxByDefault");
75 });
77 function do_get_backup_file() {
78   let file = profile.clone();
79   file.append("cookies.sqlite.bak");
80   return file;
83 function do_get_rebuild_backup_file() {
84   let file = profile.clone();
85   file.append("cookies.sqlite.bak-rebuild");
86   return file;
89 function do_corrupt_db(file) {
90   // Sanity check: the database size should be larger than 320k, since we've
91   // written about 460k of data. If it's not, let's make it obvious now.
92   let size = file.fileSize;
93   Assert.ok(size > 320e3);
95   // Corrupt the database by writing bad data to the end of the file. We
96   // assume that the important metadata -- table structure etc -- is stored
97   // elsewhere, and that doing this will not cause synchronous failure when
98   // initializing the database connection. This is totally empirical --
99   // overwriting between 1k and 100k of live data seems to work. (Note that the
100   // database file will be larger than the actual content requires, since the
101   // cookie service uses a large growth increment. So we calculate the offset
102   // based on the expected size of the content, not just the file size.)
103   let ostream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(
104     Ci.nsIFileOutputStream
105   );
106   ostream.init(file, 2, -1, 0);
107   let sstream = ostream.QueryInterface(Ci.nsISeekableStream);
108   let n = size - 320e3 + 20e3;
109   sstream.seek(Ci.nsISeekableStream.NS_SEEK_SET, size - n);
110   for (let i = 0; i < n; ++i) {
111     ostream.write("a", 1);
112   }
113   ostream.flush();
114   ostream.close();
116   Assert.equal(file.clone().fileSize, size);
117   return size;
120 async function run_test_1() {
121   // Load the profile and populate it.
122   await CookieXPCShellUtils.setCookieToDocument(
123     "http://foo.com/",
124     "oh=hai; max-age=1000"
125   );
127   // Close the profile.
128   await promise_close_profile();
130   // Open a database connection now, before we load the profile and begin
131   // asynchronous write operations.
132   let db = new CookieDatabaseConnection(do_get_cookie_file(profile), 12);
133   Assert.equal(do_count_cookies_in_db(db.db), 1);
135   // Load the profile, and wait for async read completion...
136   await promise_load_profile();
138   // Insert a row.
139   db.insertCookie(cookie);
140   db.close();
142   // Attempt to insert a cookie with the same (name, host, path) triplet.
143   Services.cookies.add(
144     cookie.host,
145     cookie.path,
146     cookie.name,
147     "hallo",
148     cookie.isSecure,
149     cookie.isHttpOnly,
150     cookie.isSession,
151     cookie.expiry,
152     {},
153     Ci.nsICookie.SAMESITE_NONE,
154     Ci.nsICookie.SCHEME_HTTPS
155   );
157   // Check that the cookie service accepted the new cookie.
158   Assert.equal(Services.cookies.countCookiesFromHost(cookie.host), 1);
160   let isRebuildingDone = false;
161   let rebuildingObserve = function (subject, topic, data) {
162     isRebuildingDone = true;
163     Services.obs.removeObserver(rebuildingObserve, "cookie-db-rebuilding");
164   };
165   Services.obs.addObserver(rebuildingObserve, "cookie-db-rebuilding");
167   // Crash test: we're going to rebuild the cookie database. Close all the db
168   // connections in the main thread and initialize a new database file in the
169   // cookie thread. Trigger some access of cookies to ensure we won't crash in
170   // the chaos status.
171   for (let i = 0; i < 10; ++i) {
172     Assert.equal(Services.cookies.countCookiesFromHost(cookie.host), 1);
173     await new Promise(resolve => executeSoon(resolve));
174   }
176   // Wait for the cookie service to rename the old database and rebuild if not yet.
177   if (!isRebuildingDone) {
178     Services.obs.removeObserver(rebuildingObserve, "cookie-db-rebuilding");
179     await new _promise_observer("cookie-db-rebuilding");
180   }
182   await new Promise(resolve => executeSoon(resolve));
184   // At this point, the cookies should still be in memory.
185   Assert.equal(Services.cookies.countCookiesFromHost("foo.com"), 1);
186   Assert.equal(Services.cookies.countCookiesFromHost(cookie.host), 1);
187   Assert.equal(do_count_cookies(), 2);
189   // Close the profile.
190   await promise_close_profile();
192   // Check that the original database was renamed, and that it contains the
193   // original cookie.
194   Assert.ok(do_get_backup_file().exists());
195   let backupdb = Services.storage.openDatabase(do_get_backup_file());
196   Assert.equal(do_count_cookies_in_db(backupdb, "foo.com"), 1);
197   backupdb.close();
199   // Load the profile, and check that it contains the new cookie.
200   do_load_profile();
202   Assert.equal(Services.cookies.countCookiesFromHost("foo.com"), 1);
203   let cookies = Services.cookies.getCookiesFromHost(cookie.host, {});
204   Assert.equal(cookies.length, 1);
205   let dbcookie = cookies[0];
206   Assert.equal(dbcookie.value, "hallo");
208   // Close the profile.
209   await promise_close_profile();
211   // Clean up.
212   do_get_cookie_file(profile).remove(false);
213   do_get_backup_file().remove(false);
214   Assert.ok(!do_get_cookie_file(profile).exists());
215   Assert.ok(!do_get_backup_file().exists());
218 async function run_test_2() {
219   // Load the profile and populate it.
220   do_load_profile();
222   Services.cookies.runInTransaction(_ => {
223     let uri = NetUtil.newURI("http://foo.com/");
224     const channel = NetUtil.newChannel({
225       uri,
226       loadUsingSystemPrincipal: true,
227       contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
228     });
230     for (let i = 0; i < 3000; ++i) {
231       uri = NetUtil.newURI("http://" + i + ".com/");
232       Services.cookies.setCookieStringFromHttp(
233         uri,
234         "oh=hai; max-age=1000",
235         channel
236       );
237     }
238   });
240   // Close the profile.
241   await promise_close_profile();
243   // Corrupt the database file.
244   let size = do_corrupt_db(do_get_cookie_file(profile));
246   // Load the profile.
247   do_load_profile();
249   // At this point, the database connection should be open. Ensure that it
250   // succeeded.
251   Assert.ok(!do_get_backup_file().exists());
253   // Recreate a new database since it was corrupted
254   Assert.equal(Services.cookies.countCookiesFromHost("0.com"), 0);
255   Assert.equal(do_count_cookies(), 0);
257   // Close the profile.
258   await promise_close_profile();
260   // Check that the original database was renamed.
261   Assert.ok(do_get_backup_file().exists());
262   Assert.equal(do_get_backup_file().fileSize, size);
263   let db = Services.storage.openDatabase(do_get_cookie_file(profile));
264   db.close();
266   do_load_profile();
267   Assert.equal(Services.cookies.countCookiesFromHost("0.com"), 0);
268   Assert.equal(do_count_cookies(), 0);
270   // Close the profile.
271   await promise_close_profile();
273   // Clean up.
274   do_get_cookie_file(profile).remove(false);
275   do_get_backup_file().remove(false);
276   Assert.ok(!do_get_cookie_file(profile).exists());
277   Assert.ok(!do_get_backup_file().exists());
280 async function run_test_3() {
281   // Set the maximum cookies per base domain limit to a large value, so that
282   // corrupting the database is easier.
283   Services.prefs.setIntPref("network.cookie.maxPerHost", 3000);
285   // Load the profile and populate it.
286   do_load_profile();
287   Services.cookies.runInTransaction(_ => {
288     let uri = NetUtil.newURI("http://hither.com/");
289     let channel = NetUtil.newChannel({
290       uri,
291       loadUsingSystemPrincipal: true,
292       contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
293     });
294     for (let i = 0; i < 10; ++i) {
295       Services.cookies.setCookieStringFromHttp(
296         uri,
297         "oh" + i + "=hai; max-age=1000",
298         channel
299       );
300     }
301     uri = NetUtil.newURI("http://haithur.com/");
302     channel = NetUtil.newChannel({
303       uri,
304       loadUsingSystemPrincipal: true,
305       contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
306     });
307     for (let i = 10; i < 3000; ++i) {
308       Services.cookies.setCookieStringFromHttp(
309         uri,
310         "oh" + i + "=hai; max-age=1000",
311         channel
312       );
313     }
314   });
316   // Close the profile.
317   await promise_close_profile();
319   // Corrupt the database file.
320   let size = do_corrupt_db(do_get_cookie_file(profile));
322   // Load the profile.
323   do_load_profile();
325   // At this point, the database connection should be open. Ensure that it
326   // succeeded.
327   Assert.ok(!do_get_backup_file().exists());
329   // Recreate a new database since it was corrupted
330   Assert.equal(Services.cookies.countCookiesFromHost("hither.com"), 0);
331   Assert.equal(Services.cookies.countCookiesFromHost("haithur.com"), 0);
333   // Close the profile.
334   await promise_close_profile();
336   let db = Services.storage.openDatabase(do_get_cookie_file(profile));
337   Assert.equal(do_count_cookies_in_db(db, "hither.com"), 0);
338   Assert.equal(do_count_cookies_in_db(db), 0);
339   db.close();
341   // Check that the original database was renamed.
342   Assert.ok(do_get_backup_file().exists());
343   Assert.equal(do_get_backup_file().fileSize, size);
345   // Rename it back, and try loading the entire database synchronously.
346   do_get_backup_file().moveTo(null, "cookies.sqlite");
347   do_load_profile();
349   // At this point, the database connection should be open. Ensure that it
350   // succeeded.
351   Assert.ok(!do_get_backup_file().exists());
353   // Synchronously read in everything.
354   Assert.equal(do_count_cookies(), 0);
356   // Close the profile.
357   await promise_close_profile();
359   db = Services.storage.openDatabase(do_get_cookie_file(profile));
360   Assert.equal(do_count_cookies_in_db(db), 0);
361   db.close();
363   // Check that the original database was renamed.
364   Assert.ok(do_get_backup_file().exists());
365   Assert.equal(do_get_backup_file().fileSize, size);
367   // Clean up.
368   do_get_cookie_file(profile).remove(false);
369   do_get_backup_file().remove(false);
370   Assert.ok(!do_get_cookie_file(profile).exists());
371   Assert.ok(!do_get_backup_file().exists());
374 async function run_test_4() {
375   // Load the profile and populate it.
376   do_load_profile();
377   Services.cookies.runInTransaction(_ => {
378     let uri = NetUtil.newURI("http://foo.com/");
379     let channel = NetUtil.newChannel({
380       uri,
381       loadUsingSystemPrincipal: true,
382       contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
383     });
384     for (let i = 0; i < 3000; ++i) {
385       uri = NetUtil.newURI("http://" + i + ".com/");
386       Services.cookies.setCookieStringFromHttp(
387         uri,
388         "oh=hai; max-age=1000",
389         channel
390       );
391     }
392   });
394   // Close the profile.
395   await promise_close_profile();
397   // Corrupt the database file.
398   let size = do_corrupt_db(do_get_cookie_file(profile));
400   // Load the profile.
401   do_load_profile();
403   // At this point, the database connection should be open. Ensure that it
404   // succeeded.
405   Assert.ok(!do_get_backup_file().exists());
407   // Recreate a new database since it was corrupted
408   Assert.equal(Services.cookies.countCookiesFromHost("0.com"), 0);
410   // Queue up an INSERT for the same base domain. This should also go into
411   // memory and be written out during database rebuild.
412   await CookieXPCShellUtils.setCookieToDocument(
413     "http://0.com/",
414     "oh2=hai; max-age=1000"
415   );
417   // At this point, the cookies should still be in memory.
418   Assert.equal(Services.cookies.countCookiesFromHost("0.com"), 1);
419   Assert.equal(do_count_cookies(), 1);
421   // Close the profile.
422   await promise_close_profile();
424   // Check that the original database was renamed.
425   Assert.ok(do_get_backup_file().exists());
426   Assert.equal(do_get_backup_file().fileSize, size);
428   // Load the profile, and check that it contains the new cookie.
429   do_load_profile();
430   Assert.equal(Services.cookies.countCookiesFromHost("0.com"), 1);
431   Assert.equal(do_count_cookies(), 1);
433   // Close the profile.
434   await promise_close_profile();
436   // Clean up.
437   do_get_cookie_file(profile).remove(false);
438   do_get_backup_file().remove(false);
439   Assert.ok(!do_get_cookie_file(profile).exists());
440   Assert.ok(!do_get_backup_file().exists());
443 async function run_test_5() {
444   // Load the profile and populate it.
445   do_load_profile();
446   Services.cookies.runInTransaction(_ => {
447     let uri = NetUtil.newURI("http://bar.com/");
448     const channel = NetUtil.newChannel({
449       uri,
450       loadUsingSystemPrincipal: true,
451       contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
452     });
453     Services.cookies.setCookieStringFromHttp(
454       uri,
455       "oh=hai; path=/; max-age=1000",
456       channel
457     );
458     for (let i = 0; i < 3000; ++i) {
459       uri = NetUtil.newURI("http://" + i + ".com/");
460       Services.cookies.setCookieStringFromHttp(
461         uri,
462         "oh=hai; max-age=1000",
463         channel
464       );
465     }
466   });
468   // Close the profile.
469   await promise_close_profile();
471   // Corrupt the database file.
472   let size = do_corrupt_db(do_get_cookie_file(profile));
474   // Load the profile.
475   do_load_profile();
477   // At this point, the database connection should be open. Ensure that it
478   // succeeded.
479   Assert.ok(!do_get_backup_file().exists());
481   // Recreate a new database since it was corrupted
482   Assert.equal(Services.cookies.countCookiesFromHost("bar.com"), 0);
483   Assert.equal(Services.cookies.countCookiesFromHost("0.com"), 0);
484   Assert.equal(do_count_cookies(), 0);
485   Assert.ok(do_get_backup_file().exists());
486   Assert.equal(do_get_backup_file().fileSize, size);
487   Assert.ok(!do_get_rebuild_backup_file().exists());
489   // Open a database connection, and write a row that will trigger a constraint
490   // violation.
491   let db = new CookieDatabaseConnection(do_get_cookie_file(profile), 12);
492   db.insertCookie(cookie);
493   Assert.equal(do_count_cookies_in_db(db.db, "bar.com"), 1);
494   Assert.equal(do_count_cookies_in_db(db.db), 1);
495   db.close();
497   // Check that the original backup and the database itself are gone.
498   Assert.ok(do_get_backup_file().exists());
499   Assert.equal(do_get_backup_file().fileSize, size);
501   Assert.equal(Services.cookies.countCookiesFromHost("bar.com"), 0);
502   Assert.equal(Services.cookies.countCookiesFromHost("0.com"), 0);
503   Assert.equal(do_count_cookies(), 0);
505   // Close the profile. We do not need to wait for completion, because the
506   // database has already been closed. Ensure the cookie file is unlocked.
507   await promise_close_profile();
509   // Clean up.
510   do_get_cookie_file(profile).remove(false);
511   do_get_backup_file().remove(false);
512   Assert.ok(!do_get_cookie_file(profile).exists());
513   Assert.ok(!do_get_backup_file().exists());