diff options
Diffstat (limited to 'netwerk/test/unit/test_cookies_async_failure.js')
-rw-r--r-- | netwerk/test/unit/test_cookies_async_failure.js | 508 |
1 files changed, 508 insertions, 0 deletions
diff --git a/netwerk/test/unit/test_cookies_async_failure.js b/netwerk/test/unit/test_cookies_async_failure.js new file mode 100644 index 0000000000..c7a455cb53 --- /dev/null +++ b/netwerk/test/unit/test_cookies_async_failure.js @@ -0,0 +1,508 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test the various ways opening a cookie database can fail in an asynchronous +// (i.e. after synchronous initialization) manner, and that the database is +// renamed and recreated under each circumstance. These circumstances are, in no +// particular order: +// +// 1) A write operation failing after the database has been read in. +// 2) Asynchronous read failure due to a corrupt database. +// 3) Synchronous read failure due to a corrupt database, when reading: +// a) a single base domain; +// b) the entire database. +// 4) Asynchronous read failure, followed by another failure during INSERT but +// before the database closes for rebuilding. (The additional error should be +// ignored.) +// 5) Asynchronous read failure, followed by an INSERT failure during rebuild. +// This should result in an abort of the database rebuild; the partially- +// built database should be moved to 'cookies.sqlite.bak-rebuild'. + +"use strict"; + +let profile; +let cookie; + +add_task(async () => { + // Set up a profile. + profile = do_get_profile(); + + // Allow all cookies. + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + + // The server. + const hosts = ["foo.com", "hither.com", "haithur.com", "bar.com"]; + for (let i = 0; i < 3000; ++i) { + hosts.push(i + ".com"); + } + CookieXPCShellUtils.createServer({ hosts }); + + // Get the cookie file and the backup file. + Assert.ok(!do_get_cookie_file(profile).exists()); + Assert.ok(!do_get_backup_file(profile).exists()); + + // Create a cookie object for testing. + let now = Date.now() * 1000; + let futureExpiry = Math.round(now / 1e6 + 1000); + cookie = new Cookie( + "oh", + "hai", + "bar.com", + "/", + futureExpiry, + now, + now, + false, + false, + false + ); + + await run_test_1(); + await run_test_2(); + await run_test_3(); + await run_test_4(); + await run_test_5(); +}); + +function do_get_backup_file(profile) { + let file = profile.clone(); + file.append("cookies.sqlite.bak"); + return file; +} + +function do_get_rebuild_backup_file(profile) { + let file = profile.clone(); + file.append("cookies.sqlite.bak-rebuild"); + return file; +} + +function do_corrupt_db(file) { + // Sanity check: the database size should be larger than 320k, since we've + // written about 460k of data. If it's not, let's make it obvious now. + let size = file.fileSize; + Assert.ok(size > 320e3); + + // Corrupt the database by writing bad data to the end of the file. We + // assume that the important metadata -- table structure etc -- is stored + // elsewhere, and that doing this will not cause synchronous failure when + // initializing the database connection. This is totally empirical -- + // overwriting between 1k and 100k of live data seems to work. (Note that the + // database file will be larger than the actual content requires, since the + // cookie service uses a large growth increment. So we calculate the offset + // based on the expected size of the content, not just the file size.) + let ostream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance( + Ci.nsIFileOutputStream + ); + ostream.init(file, 2, -1, 0); + let sstream = ostream.QueryInterface(Ci.nsISeekableStream); + let n = size - 320e3 + 20e3; + sstream.seek(Ci.nsISeekableStream.NS_SEEK_SET, size - n); + for (let i = 0; i < n; ++i) { + ostream.write("a", 1); + } + ostream.flush(); + ostream.close(); + + Assert.equal(file.clone().fileSize, size); + return size; +} + +async function run_test_1() { + // Load the profile and populate it. + await CookieXPCShellUtils.setCookieToDocument( + "http://foo.com/", + "oh=hai; max-age=1000" + ); + + // Close the profile. + await promise_close_profile(); + + // Open a database connection now, before we load the profile and begin + // asynchronous write operations. + let db = new CookieDatabaseConnection(do_get_cookie_file(profile), 12); + Assert.equal(do_count_cookies_in_db(db.db), 1); + + // Load the profile, and wait for async read completion... + await promise_load_profile(); + + // Insert a row. + db.insertCookie(cookie); + db.close(); + + // Attempt to insert a cookie with the same (name, host, path) triplet. + Services.cookiemgr.add( + cookie.host, + cookie.path, + cookie.name, + "hallo", + cookie.isSecure, + cookie.isHttpOnly, + cookie.isSession, + cookie.expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + + // Check that the cookie service accepted the new cookie. + Assert.equal(Services.cookiemgr.countCookiesFromHost(cookie.host), 1); + + let isRebuildingDone = false; + let rebuildingObserve = function(subject, topic, data) { + isRebuildingDone = true; + Services.obs.removeObserver(rebuildingObserve, "cookie-db-rebuilding"); + }; + Services.obs.addObserver(rebuildingObserve, "cookie-db-rebuilding"); + + // Crash test: we're going to rebuild the cookie database. Close all the db + // connections in the main thread and initialize a new database file in the + // cookie thread. Trigger some access of cookies to ensure we won't crash in + // the chaos status. + for (let i = 0; i < 10; ++i) { + Assert.equal(Services.cookiemgr.countCookiesFromHost(cookie.host), 1); + await new Promise(resolve => executeSoon(resolve)); + } + + // Wait for the cookie service to rename the old database and rebuild if not yet. + if (!isRebuildingDone) { + Services.obs.removeObserver(rebuildingObserve, "cookie-db-rebuilding"); + await new _promise_observer("cookie-db-rebuilding"); + } + + await new Promise(resolve => executeSoon(resolve)); + + // At this point, the cookies should still be in memory. + Assert.equal(Services.cookiemgr.countCookiesFromHost("foo.com"), 1); + Assert.equal(Services.cookiemgr.countCookiesFromHost(cookie.host), 1); + Assert.equal(do_count_cookies(), 2); + + // Close the profile. + await promise_close_profile(); + + // Check that the original database was renamed, and that it contains the + // original cookie. + Assert.ok(do_get_backup_file(profile).exists()); + let backupdb = Services.storage.openDatabase(do_get_backup_file(profile)); + Assert.equal(do_count_cookies_in_db(backupdb, "foo.com"), 1); + backupdb.close(); + + // Load the profile, and check that it contains the new cookie. + do_load_profile(); + + Assert.equal(Services.cookiemgr.countCookiesFromHost("foo.com"), 1); + let cookies = Services.cookiemgr.getCookiesFromHost(cookie.host, {}); + Assert.equal(cookies.length, 1); + let dbcookie = cookies[0]; + Assert.equal(dbcookie.value, "hallo"); + + // Close the profile. + await promise_close_profile(); + + // Clean up. + do_get_cookie_file(profile).remove(false); + do_get_backup_file(profile).remove(false); + Assert.ok(!do_get_cookie_file(profile).exists()); + Assert.ok(!do_get_backup_file(profile).exists()); +} + +async function run_test_2() { + // Load the profile and populate it. + do_load_profile(); + + Services.cookiesvc.runInTransaction(_ => { + let uri = NetUtil.newURI("http://foo.com/"); + const channel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + + for (let i = 0; i < 3000; ++i) { + let uri = NetUtil.newURI("http://" + i + ".com/"); + Services.cookiesvc.setCookieStringFromHttp( + uri, + "oh=hai; max-age=1000", + channel + ); + } + }); + + // Close the profile. + await promise_close_profile(); + + // Corrupt the database file. + let size = do_corrupt_db(do_get_cookie_file(profile)); + + // Load the profile. + do_load_profile(); + + // At this point, the database connection should be open. Ensure that it + // succeeded. + Assert.ok(!do_get_backup_file(profile).exists()); + + // Recreate a new database since it was corrupted + Assert.equal(Services.cookiemgr.countCookiesFromHost("0.com"), 0); + Assert.equal(do_count_cookies(), 0); + + // Close the profile. + await promise_close_profile(); + + // Check that the original database was renamed. + Assert.ok(do_get_backup_file(profile).exists()); + Assert.equal(do_get_backup_file(profile).fileSize, size); + let db = Services.storage.openDatabase(do_get_cookie_file(profile)); + db.close(); + + do_load_profile(); + Assert.equal(Services.cookiemgr.countCookiesFromHost("0.com"), 0); + Assert.equal(do_count_cookies(), 0); + + // Close the profile. + await promise_close_profile(); + + // Clean up. + do_get_cookie_file(profile).remove(false); + do_get_backup_file(profile).remove(false); + Assert.ok(!do_get_cookie_file(profile).exists()); + Assert.ok(!do_get_backup_file(profile).exists()); +} + +async function run_test_3() { + // Set the maximum cookies per base domain limit to a large value, so that + // corrupting the database is easier. + Services.prefs.setIntPref("network.cookie.maxPerHost", 3000); + + // Load the profile and populate it. + do_load_profile(); + Services.cookiesvc.runInTransaction(_ => { + let uri = NetUtil.newURI("http://hither.com/"); + let channel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + for (let i = 0; i < 10; ++i) { + Services.cookiesvc.setCookieStringFromHttp( + uri, + "oh" + i + "=hai; max-age=1000", + channel + ); + } + uri = NetUtil.newURI("http://haithur.com/"); + channel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + for (let i = 10; i < 3000; ++i) { + Services.cookiesvc.setCookieStringFromHttp( + uri, + "oh" + i + "=hai; max-age=1000", + channel + ); + } + }); + + // Close the profile. + await promise_close_profile(); + + // Corrupt the database file. + let size = do_corrupt_db(do_get_cookie_file(profile)); + + // Load the profile. + do_load_profile(); + + // At this point, the database connection should be open. Ensure that it + // succeeded. + Assert.ok(!do_get_backup_file(profile).exists()); + + // Recreate a new database since it was corrupted + Assert.equal(Services.cookiemgr.countCookiesFromHost("hither.com"), 0); + Assert.equal(Services.cookiemgr.countCookiesFromHost("haithur.com"), 0); + + // Close the profile. + await promise_close_profile(); + + let db = Services.storage.openDatabase(do_get_cookie_file(profile)); + Assert.equal(do_count_cookies_in_db(db, "hither.com"), 0); + Assert.equal(do_count_cookies_in_db(db), 0); + db.close(); + + // Check that the original database was renamed. + Assert.ok(do_get_backup_file(profile).exists()); + Assert.equal(do_get_backup_file(profile).fileSize, size); + + // Rename it back, and try loading the entire database synchronously. + do_get_backup_file(profile).moveTo(null, "cookies.sqlite"); + do_load_profile(); + + // At this point, the database connection should be open. Ensure that it + // succeeded. + Assert.ok(!do_get_backup_file(profile).exists()); + + // Synchronously read in everything. + Assert.equal(do_count_cookies(), 0); + + // Close the profile. + await promise_close_profile(); + + db = Services.storage.openDatabase(do_get_cookie_file(profile)); + Assert.equal(do_count_cookies_in_db(db), 0); + db.close(); + + // Check that the original database was renamed. + Assert.ok(do_get_backup_file(profile).exists()); + Assert.equal(do_get_backup_file(profile).fileSize, size); + + // Clean up. + do_get_cookie_file(profile).remove(false); + do_get_backup_file(profile).remove(false); + Assert.ok(!do_get_cookie_file(profile).exists()); + Assert.ok(!do_get_backup_file(profile).exists()); +} + +async function run_test_4() { + // Load the profile and populate it. + do_load_profile(); + Services.cookiesvc.runInTransaction(_ => { + let uri = NetUtil.newURI("http://foo.com/"); + let channel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + for (let i = 0; i < 3000; ++i) { + let uri = NetUtil.newURI("http://" + i + ".com/"); + Services.cookiesvc.setCookieStringFromHttp( + uri, + "oh=hai; max-age=1000", + channel + ); + } + }); + + // Close the profile. + await promise_close_profile(); + + // Corrupt the database file. + let size = do_corrupt_db(do_get_cookie_file(profile)); + + // Load the profile. + do_load_profile(); + + // At this point, the database connection should be open. Ensure that it + // succeeded. + Assert.ok(!do_get_backup_file(profile).exists()); + + // Recreate a new database since it was corrupted + Assert.equal(Services.cookiemgr.countCookiesFromHost("0.com"), 0); + + // Queue up an INSERT for the same base domain. This should also go into + // memory and be written out during database rebuild. + await CookieXPCShellUtils.setCookieToDocument( + "http://0.com/", + "oh2=hai; max-age=1000" + ); + + // At this point, the cookies should still be in memory. + Assert.equal(Services.cookiemgr.countCookiesFromHost("0.com"), 1); + Assert.equal(do_count_cookies(), 1); + + // Close the profile. + await promise_close_profile(); + + // Check that the original database was renamed. + Assert.ok(do_get_backup_file(profile).exists()); + Assert.equal(do_get_backup_file(profile).fileSize, size); + + // Load the profile, and check that it contains the new cookie. + do_load_profile(); + Assert.equal(Services.cookiemgr.countCookiesFromHost("0.com"), 1); + Assert.equal(do_count_cookies(), 1); + + // Close the profile. + await promise_close_profile(); + + // Clean up. + do_get_cookie_file(profile).remove(false); + do_get_backup_file(profile).remove(false); + Assert.ok(!do_get_cookie_file(profile).exists()); + Assert.ok(!do_get_backup_file(profile).exists()); +} + +async function run_test_5() { + // Load the profile and populate it. + do_load_profile(); + Services.cookiesvc.runInTransaction(_ => { + let uri = NetUtil.newURI("http://bar.com/"); + const channel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + Services.cookiesvc.setCookieStringFromHttp( + uri, + "oh=hai; path=/; max-age=1000", + channel + ); + for (let i = 0; i < 3000; ++i) { + let uri = NetUtil.newURI("http://" + i + ".com/"); + Services.cookiesvc.setCookieStringFromHttp( + uri, + "oh=hai; max-age=1000", + channel + ); + } + }); + + // Close the profile. + await promise_close_profile(); + + // Corrupt the database file. + let size = do_corrupt_db(do_get_cookie_file(profile)); + + // Load the profile. + do_load_profile(); + + // At this point, the database connection should be open. Ensure that it + // succeeded. + Assert.ok(!do_get_backup_file(profile).exists()); + + // Recreate a new database since it was corrupted + Assert.equal(Services.cookiemgr.countCookiesFromHost("bar.com"), 0); + Assert.equal(Services.cookiemgr.countCookiesFromHost("0.com"), 0); + Assert.equal(do_count_cookies(), 0); + Assert.ok(do_get_backup_file(profile).exists()); + Assert.equal(do_get_backup_file(profile).fileSize, size); + Assert.ok(!do_get_rebuild_backup_file(profile).exists()); + + // Open a database connection, and write a row that will trigger a constraint + // violation. + let db = new CookieDatabaseConnection(do_get_cookie_file(profile), 12); + db.insertCookie(cookie); + Assert.equal(do_count_cookies_in_db(db.db, "bar.com"), 1); + Assert.equal(do_count_cookies_in_db(db.db), 1); + db.close(); + + // Check that the original backup and the database itself are gone. + Assert.ok(do_get_backup_file(profile).exists()); + Assert.equal(do_get_backup_file(profile).fileSize, size); + + Assert.equal(Services.cookiemgr.countCookiesFromHost("bar.com"), 0); + Assert.equal(Services.cookiemgr.countCookiesFromHost("0.com"), 0); + Assert.equal(do_count_cookies(), 0); + + // Close the profile. We do not need to wait for completion, because the + // database has already been closed. Ensure the cookie file is unlocked. + await promise_close_profile(); + + // Clean up. + do_get_cookie_file(profile).remove(false); + do_get_backup_file(profile).remove(false); + Assert.ok(!do_get_cookie_file(profile).exists()); + Assert.ok(!do_get_backup_file(profile).exists()); +} |