Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
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 let test_generator = do_run_test();
23 function run_test() {
24 do_test_pending();
25 do_run_generator(test_generator);
26 }
28 function finish_test() {
29 do_execute_soon(function() {
30 test_generator.close();
31 do_test_finished();
32 });
33 }
35 function do_run_test() {
36 // Set up a profile.
37 this.profile = do_get_profile();
39 // Allow all cookies.
40 Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
42 // Get the cookie file and the backup file.
43 do_check_false(do_get_cookie_file(profile).exists());
44 do_check_false(do_get_backup_file(profile).exists());
46 // Create a cookie object for testing.
47 this.now = Date.now() * 1000;
48 this.futureExpiry = Math.round(this.now / 1e6 + 1000);
49 this.cookie = new Cookie("oh", "hai", "bar.com", "/", this.futureExpiry,
50 this.now, this.now, false, false, false);
52 this.sub_generator = run_test_1(test_generator);
53 sub_generator.next();
54 yield;
56 this.sub_generator = run_test_2(test_generator);
57 sub_generator.next();
58 yield;
60 this.sub_generator = run_test_3(test_generator);
61 sub_generator.next();
62 yield;
64 this.sub_generator = run_test_4(test_generator);
65 sub_generator.next();
66 yield;
68 this.sub_generator = run_test_5(test_generator);
69 sub_generator.next();
70 yield;
72 finish_test();
73 return;
74 }
76 function do_get_backup_file(profile)
77 {
78 let file = profile.clone();
79 file.append("cookies.sqlite.bak");
80 return file;
81 }
83 function do_get_rebuild_backup_file(profile)
84 {
85 let file = profile.clone();
86 file.append("cookies.sqlite.bak-rebuild");
87 return file;
88 }
90 function do_corrupt_db(file)
91 {
92 // Sanity check: the database size should be larger than 450k, since we've
93 // written about 460k of data. If it's not, let's make it obvious now.
94 let size = file.fileSize;
95 do_check_true(size > 450e3);
97 // Corrupt the database by writing bad data to the end of the file. We
98 // assume that the important metadata -- table structure etc -- is stored
99 // elsewhere, and that doing this will not cause synchronous failure when
100 // initializing the database connection. This is totally empirical --
101 // overwriting between 1k and 100k of live data seems to work. (Note that the
102 // database file will be larger than the actual content requires, since the
103 // cookie service uses a large growth increment. So we calculate the offset
104 // based on the expected size of the content, not just the file size.)
105 let ostream = Cc["@mozilla.org/network/file-output-stream;1"].
106 createInstance(Ci.nsIFileOutputStream);
107 ostream.init(file, 2, -1, 0);
108 let sstream = ostream.QueryInterface(Ci.nsISeekableStream);
109 let n = size - 450e3 + 20e3;
110 sstream.seek(Ci.nsISeekableStream.NS_SEEK_SET, size - n);
111 for (let i = 0; i < n; ++i) {
112 ostream.write("a", 1);
113 }
114 ostream.flush();
115 ostream.close();
117 do_check_eq(file.clone().fileSize, size);
118 return size;
119 }
121 function run_test_1(generator)
122 {
123 // Load the profile and populate it.
124 let uri = NetUtil.newURI("http://foo.com/");
125 Services.cookies.setCookieString(uri, null, "oh=hai; max-age=1000", null);
127 // Close the profile.
128 do_close_profile(sub_generator);
129 yield;
131 // Open a database connection now, before we load the profile and begin
132 // asynchronous write operations. In order to tell when the async delete
133 // statement has completed, we do something tricky: open a schema 2 connection
134 // and add a cookie with null baseDomain. We can then wait until we see it
135 // deleted in the new database.
136 let db2 = new CookieDatabaseConnection(do_get_cookie_file(profile), 2);
137 db2.db.executeSimpleSQL("INSERT INTO moz_cookies (baseDomain) VALUES (NULL)");
138 db2.close();
139 let db = new CookieDatabaseConnection(do_get_cookie_file(profile), 4);
140 do_check_eq(do_count_cookies_in_db(db.db), 2);
142 // Load the profile, and wait for async read completion...
143 do_load_profile(sub_generator);
144 yield;
146 // ... and the DELETE statement to finish.
147 while (do_count_cookies_in_db(db.db) == 2) {
148 do_execute_soon(function() {
149 do_run_generator(sub_generator);
150 });
151 yield;
152 }
153 do_check_eq(do_count_cookies_in_db(db.db), 1);
155 // Insert a row.
156 db.insertCookie(cookie);
157 db.close();
159 // Attempt to insert a cookie with the same (name, host, path) triplet.
160 Services.cookiemgr.add(cookie.host, cookie.path, cookie.name, "hallo",
161 cookie.isSecure, cookie.isHttpOnly, cookie.isSession, cookie.expiry);
163 // Check that the cookie service accepted the new cookie.
164 do_check_eq(Services.cookiemgr.countCookiesFromHost(cookie.host), 1);
166 // Wait for the cookie service to rename the old database and rebuild.
167 new _observer(sub_generator, "cookie-db-rebuilding");
168 yield;
169 do_execute_soon(function() { do_run_generator(sub_generator); });
170 yield;
172 // At this point, the cookies should still be in memory.
173 do_check_eq(Services.cookiemgr.countCookiesFromHost("foo.com"), 1);
174 do_check_eq(Services.cookiemgr.countCookiesFromHost(cookie.host), 1);
175 do_check_eq(do_count_cookies(), 2);
177 // Close the profile.
178 do_close_profile(sub_generator);
179 yield;
181 // Check that the original database was renamed, and that it contains the
182 // original cookie.
183 do_check_true(do_get_backup_file(profile).exists());
184 let backupdb = Services.storage.openDatabase(do_get_backup_file(profile));
185 do_check_eq(do_count_cookies_in_db(backupdb, "foo.com"), 1);
186 backupdb.close();
188 // Load the profile, and check that it contains the new cookie.
189 do_load_profile();
191 do_check_eq(Services.cookiemgr.countCookiesFromHost("foo.com"), 1);
192 let enumerator = Services.cookiemgr.getCookiesFromHost(cookie.host);
193 do_check_true(enumerator.hasMoreElements());
194 let dbcookie = enumerator.getNext().QueryInterface(Ci.nsICookie2);
195 do_check_eq(dbcookie.value, "hallo");
196 do_check_false(enumerator.hasMoreElements());
198 // Close the profile.
199 do_close_profile(sub_generator);
200 yield;
202 // Clean up.
203 do_get_cookie_file(profile).remove(false);
204 do_get_backup_file(profile).remove(false);
205 do_check_false(do_get_cookie_file(profile).exists());
206 do_check_false(do_get_backup_file(profile).exists());
207 do_run_generator(generator);
208 }
210 function run_test_2(generator)
211 {
212 // Load the profile and populate it.
213 do_load_profile();
214 for (let i = 0; i < 3000; ++i) {
215 let uri = NetUtil.newURI("http://" + i + ".com/");
216 Services.cookies.setCookieString(uri, null, "oh=hai; max-age=1000", null);
217 }
219 // Close the profile.
220 do_close_profile(sub_generator);
221 yield;
223 // Corrupt the database file.
224 let size = do_corrupt_db(do_get_cookie_file(profile));
226 // Load the profile.
227 do_load_profile();
229 // At this point, the database connection should be open. Ensure that it
230 // succeeded.
231 do_check_false(do_get_backup_file(profile).exists());
233 // Synchronously read in the first cookie. This will cause it to go into the
234 // cookie table, whereupon it will be written out during database rebuild.
235 do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 1);
237 // Wait for the asynchronous read to choke, at which point the backup file
238 // will be created and the database rebuilt.
239 new _observer(sub_generator, "cookie-db-rebuilding");
240 yield;
241 do_execute_soon(function() { do_run_generator(sub_generator); });
242 yield;
244 // At this point, the cookies should still be in memory.
245 do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 1);
246 do_check_eq(do_count_cookies(), 1);
248 // Close the profile.
249 do_close_profile(sub_generator);
250 yield;
252 // Check that the original database was renamed.
253 do_check_true(do_get_backup_file(profile).exists());
254 do_check_eq(do_get_backup_file(profile).fileSize, size);
255 let db = Services.storage.openDatabase(do_get_cookie_file(profile));
256 do_check_eq(do_count_cookies_in_db(db, "0.com"), 1);
257 db.close();
259 // Load the profile, and check that it contains the new cookie.
260 do_load_profile();
261 do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 1);
262 do_check_eq(do_count_cookies(), 1);
264 // Close the profile.
265 do_close_profile(sub_generator);
266 yield;
268 // Clean up.
269 do_get_cookie_file(profile).remove(false);
270 do_get_backup_file(profile).remove(false);
271 do_check_false(do_get_cookie_file(profile).exists());
272 do_check_false(do_get_backup_file(profile).exists());
273 do_run_generator(generator);
274 }
276 function run_test_3(generator)
277 {
278 // Set the maximum cookies per base domain limit to a large value, so that
279 // corrupting the database is easier.
280 Services.prefs.setIntPref("network.cookie.maxPerHost", 3000);
282 // Load the profile and populate it.
283 do_load_profile();
284 for (let i = 0; i < 10; ++i) {
285 let uri = NetUtil.newURI("http://hither.com/");
286 Services.cookies.setCookieString(uri, null, "oh" + i + "=hai; max-age=1000",
287 null);
288 }
289 for (let i = 10; i < 3000; ++i) {
290 let uri = NetUtil.newURI("http://haithur.com/");
291 Services.cookies.setCookieString(uri, null, "oh" + i + "=hai; max-age=1000",
292 null);
293 }
295 // Close the profile.
296 do_close_profile(sub_generator);
297 yield;
299 // Corrupt the database file.
300 let size = do_corrupt_db(do_get_cookie_file(profile));
302 // Load the profile.
303 do_load_profile();
305 // At this point, the database connection should be open. Ensure that it
306 // succeeded.
307 do_check_false(do_get_backup_file(profile).exists());
309 // Synchronously read in the cookies for our two domains. The first should
310 // succeed, but the second should fail midway through, resulting in none of
311 // those cookies being present.
312 do_check_eq(Services.cookiemgr.countCookiesFromHost("hither.com"), 10);
313 do_check_eq(Services.cookiemgr.countCookiesFromHost("haithur.com"), 0);
315 // Wait for the backup file to be created and the database rebuilt.
316 do_check_false(do_get_backup_file(profile).exists());
317 new _observer(sub_generator, "cookie-db-rebuilding");
318 yield;
319 do_execute_soon(function() { do_run_generator(sub_generator); });
320 yield;
322 // Close the profile.
323 do_close_profile(sub_generator);
324 yield;
325 let db = Services.storage.openDatabase(do_get_cookie_file(profile));
326 do_check_eq(do_count_cookies_in_db(db, "hither.com"), 10);
327 do_check_eq(do_count_cookies_in_db(db), 10);
328 db.close();
330 // Check that the original database was renamed.
331 do_check_true(do_get_backup_file(profile).exists());
332 do_check_eq(do_get_backup_file(profile).fileSize, size);
334 // Rename it back, and try loading the entire database synchronously.
335 do_get_backup_file(profile).moveTo(null, "cookies.sqlite");
336 do_load_profile();
338 // At this point, the database connection should be open. Ensure that it
339 // succeeded.
340 do_check_false(do_get_backup_file(profile).exists());
342 // Synchronously read in everything.
343 do_check_eq(do_count_cookies(), 0);
345 // Wait for the backup file to be created and the database rebuilt.
346 do_check_false(do_get_backup_file(profile).exists());
347 new _observer(sub_generator, "cookie-db-rebuilding");
348 yield;
349 do_execute_soon(function() { do_run_generator(sub_generator); });
350 yield;
352 // Close the profile.
353 do_close_profile(sub_generator);
354 yield;
355 let db = Services.storage.openDatabase(do_get_cookie_file(profile));
356 do_check_eq(do_count_cookies_in_db(db), 0);
357 db.close();
359 // Check that the original database was renamed.
360 do_check_true(do_get_backup_file(profile).exists());
361 do_check_eq(do_get_backup_file(profile).fileSize, size);
363 // Clean up.
364 do_get_cookie_file(profile).remove(false);
365 do_get_backup_file(profile).remove(false);
366 do_check_false(do_get_cookie_file(profile).exists());
367 do_check_false(do_get_backup_file(profile).exists());
368 do_run_generator(generator);
369 }
371 function run_test_4(generator)
372 {
373 // Load the profile and populate it.
374 do_load_profile();
375 for (let i = 0; i < 3000; ++i) {
376 let uri = NetUtil.newURI("http://" + i + ".com/");
377 Services.cookies.setCookieString(uri, null, "oh=hai; max-age=1000", null);
378 }
380 // Close the profile.
381 do_close_profile(sub_generator);
382 yield;
384 // Corrupt the database file.
385 let size = do_corrupt_db(do_get_cookie_file(profile));
387 // Load the profile.
388 do_load_profile();
390 // At this point, the database connection should be open. Ensure that it
391 // succeeded.
392 do_check_false(do_get_backup_file(profile).exists());
394 // Synchronously read in the first cookie. This will cause it to go into the
395 // cookie table, whereupon it will be written out during database rebuild.
396 do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 1);
398 // Queue up an INSERT for the same base domain. This should also go into
399 // memory and be written out during database rebuild.
400 let uri = NetUtil.newURI("http://0.com/");
401 Services.cookies.setCookieString(uri, null, "oh2=hai; max-age=1000", null);
403 // Wait for the asynchronous read to choke and the insert to fail shortly
404 // thereafter, at which point the backup file will be created and the database
405 // rebuilt.
406 new _observer(sub_generator, "cookie-db-rebuilding");
407 yield;
408 do_execute_soon(function() { do_run_generator(sub_generator); });
409 yield;
411 // Close the profile.
412 do_close_profile(sub_generator);
413 yield;
415 // Check that the original database was renamed.
416 do_check_true(do_get_backup_file(profile).exists());
417 do_check_eq(do_get_backup_file(profile).fileSize, size);
418 let db = Services.storage.openDatabase(do_get_cookie_file(profile));
419 do_check_eq(do_count_cookies_in_db(db, "0.com"), 2);
420 db.close();
422 // Load the profile, and check that it contains the new cookie.
423 do_load_profile();
424 do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 2);
425 do_check_eq(do_count_cookies(), 2);
427 // Close the profile.
428 do_close_profile(sub_generator);
429 yield;
431 // Clean up.
432 do_get_cookie_file(profile).remove(false);
433 do_get_backup_file(profile).remove(false);
434 do_check_false(do_get_cookie_file(profile).exists());
435 do_check_false(do_get_backup_file(profile).exists());
436 do_run_generator(generator);
437 }
439 function run_test_4(generator)
440 {
441 // Load the profile and populate it.
442 do_load_profile();
443 for (let i = 0; i < 3000; ++i) {
444 let uri = NetUtil.newURI("http://" + i + ".com/");
445 Services.cookies.setCookieString(uri, null, "oh=hai; max-age=1000", null);
446 }
448 // Close the profile.
449 do_close_profile(sub_generator);
450 yield;
452 // Corrupt the database file.
453 let size = do_corrupt_db(do_get_cookie_file(profile));
455 // Load the profile.
456 do_load_profile();
458 // At this point, the database connection should be open. Ensure that it
459 // succeeded.
460 do_check_false(do_get_backup_file(profile).exists());
462 // Synchronously read in the first cookie. This will cause it to go into the
463 // cookie table, whereupon it will be written out during database rebuild.
464 do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 1);
466 // Queue up an INSERT for the same base domain. This should also go into
467 // memory and be written out during database rebuild.
468 let uri = NetUtil.newURI("http://0.com/");
469 Services.cookies.setCookieString(uri, null, "oh2=hai; max-age=1000", null);
471 // Wait for the asynchronous read to choke and the insert to fail shortly
472 // thereafter, at which point the backup file will be created and the database
473 // rebuilt.
474 new _observer(sub_generator, "cookie-db-rebuilding");
475 yield;
476 do_execute_soon(function() { do_run_generator(sub_generator); });
477 yield;
479 // At this point, the cookies should still be in memory.
480 do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 2);
481 do_check_eq(do_count_cookies(), 2);
483 // Close the profile.
484 do_close_profile(sub_generator);
485 yield;
487 // Check that the original database was renamed.
488 do_check_true(do_get_backup_file(profile).exists());
489 do_check_eq(do_get_backup_file(profile).fileSize, size);
490 let db = Services.storage.openDatabase(do_get_cookie_file(profile));
491 do_check_eq(do_count_cookies_in_db(db, "0.com"), 2);
492 db.close();
494 // Load the profile, and check that it contains the new cookie.
495 do_load_profile();
496 do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 2);
497 do_check_eq(do_count_cookies(), 2);
499 // Close the profile.
500 do_close_profile(sub_generator);
501 yield;
503 // Clean up.
504 do_get_cookie_file(profile).remove(false);
505 do_get_backup_file(profile).remove(false);
506 do_check_false(do_get_cookie_file(profile).exists());
507 do_check_false(do_get_backup_file(profile).exists());
508 do_run_generator(generator);
509 }
511 function run_test_5(generator)
512 {
513 // Load the profile and populate it.
514 do_load_profile();
515 let uri = NetUtil.newURI("http://bar.com/");
516 Services.cookies.setCookieString(uri, null, "oh=hai; path=/; max-age=1000",
517 null);
518 for (let i = 0; i < 3000; ++i) {
519 let uri = NetUtil.newURI("http://" + i + ".com/");
520 Services.cookies.setCookieString(uri, null, "oh=hai; max-age=1000", null);
521 }
523 // Close the profile.
524 do_close_profile(sub_generator);
525 yield;
527 // Corrupt the database file.
528 let size = do_corrupt_db(do_get_cookie_file(profile));
530 // Load the profile.
531 do_load_profile();
533 // At this point, the database connection should be open. Ensure that it
534 // succeeded.
535 do_check_false(do_get_backup_file(profile).exists());
537 // Synchronously read in the first two cookies. This will cause them to go
538 // into the cookie table, whereupon it will be written out during database
539 // rebuild.
540 do_check_eq(Services.cookiemgr.countCookiesFromHost("bar.com"), 1);
541 do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 1);
543 // Wait for the asynchronous read to choke, at which point the backup file
544 // will be created and a new connection opened.
545 new _observer(sub_generator, "cookie-db-rebuilding");
546 yield;
548 // At this point, the cookies should still be in memory. (Note that these
549 // calls are re-entrant into the cookie service, but it's OK!)
550 do_check_eq(Services.cookiemgr.countCookiesFromHost("bar.com"), 1);
551 do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 1);
552 do_check_eq(do_count_cookies(), 2);
553 do_check_true(do_get_backup_file(profile).exists());
554 do_check_eq(do_get_backup_file(profile).fileSize, size);
555 do_check_false(do_get_rebuild_backup_file(profile).exists());
557 // Open a database connection, and write a row that will trigger a constraint
558 // violation.
559 let db = new CookieDatabaseConnection(do_get_cookie_file(profile), 4);
560 db.insertCookie(cookie);
561 do_check_eq(do_count_cookies_in_db(db.db, "bar.com"), 1);
562 do_check_eq(do_count_cookies_in_db(db.db), 1);
563 db.close();
565 // Wait for the rebuild to bail and the database to be closed.
566 new _observer(sub_generator, "cookie-db-closed");
567 yield;
569 // Check that the original backup and the database itself are gone.
570 do_check_true(do_get_rebuild_backup_file(profile).exists());
571 do_check_true(do_get_backup_file(profile).exists());
572 do_check_eq(do_get_backup_file(profile).fileSize, size);
573 do_check_false(do_get_cookie_file(profile).exists());
575 // Check that the rebuild backup has the original bar.com cookie, and possibly
576 // a 0.com cookie depending on whether it got written out first or second.
577 db = new CookieDatabaseConnection(do_get_rebuild_backup_file(profile), 4);
578 do_check_eq(do_count_cookies_in_db(db.db, "bar.com"), 1);
579 let count = do_count_cookies_in_db(db.db);
580 do_check_true(count == 1 ||
581 count == 2 && do_count_cookies_in_db(db.db, "0.com") == 1);
582 db.close();
584 do_check_eq(Services.cookiemgr.countCookiesFromHost("bar.com"), 1);
585 do_check_eq(Services.cookiemgr.countCookiesFromHost("0.com"), 1);
586 do_check_eq(do_count_cookies(), 2);
588 // Close the profile. We do not need to wait for completion, because the
589 // database has already been closed.
590 do_close_profile();
592 // Clean up.
593 do_get_backup_file(profile).remove(false);
594 do_get_rebuild_backup_file(profile).remove(false);
595 do_check_false(do_get_cookie_file(profile).exists());
596 do_check_false(do_get_backup_file(profile).exists());
597 do_check_false(do_get_rebuild_backup_file(profile).exists());
598 do_run_generator(generator);
599 }