head.js (15325B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 ChromeUtils.defineESModuleGetters(this, { 8 BackupService: "resource:///modules/backup/BackupService.sys.mjs", 9 BackupResource: "resource:///modules/backup/BackupResource.sys.mjs", 10 MeasurementUtils: "resource:///modules/backup/MeasurementUtils.sys.mjs", 11 TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs", 12 Sqlite: "resource://gre/modules/Sqlite.sys.mjs", 13 sinon: "resource://testing-common/Sinon.sys.mjs", 14 OSKeyStoreTestUtils: "resource://testing-common/OSKeyStoreTestUtils.sys.mjs", 15 MockRegistrar: "resource://testing-common/MockRegistrar.sys.mjs", 16 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", 17 AppConstants: "resource://gre/modules/AppConstants.sys.mjs", 18 setTimeout: "resource://gre/modules/Timer.sys.mjs", 19 }); 20 21 const HISTORY_ENABLED_PREF = "places.history.enabled"; 22 const SANITIZE_ON_SHUTDOWN_PREF = "privacy.sanitize.sanitizeOnShutdown"; 23 const USE_OLD_CLEAR_HISTORY_DIALOG_PREF = 24 "privacy.sanitize.useOldClearHistoryDialog"; 25 const FORM_HISTORY_CLEARED_ON_SHUTDOWN_PREF = 26 "privacy.clearOnShutdown_v2.formdata"; 27 const HISTORY_CLEARED_ON_SHUTDOWN_PREF = 28 "privacy.clearOnShutdown_v2.browsingHistoryAndDownloads"; 29 const SITE_SETTINGS_CLEARED_ON_SHUTDOWN_PREF = 30 "privacy.clearOnShutdown_v2.siteSettings"; 31 const SITE_SETTINGS_CLEARED_ON_SHUTDOWN_OLD_PREF = 32 "privacy.clearOnShutdown.siteSettings"; 33 34 let gFakeOSKeyStore; 35 36 add_setup(async () => { 37 // During our unit tests, we're not interested in showing OSKeyStore 38 // authentication dialogs, nor are we interested in actually using the "real" 39 // OSKeyStore. We instead swap in our own implementation of nsIOSKeyStore 40 // which provides some stubbed out values. We also set up OSKeyStoreTestUtils 41 // which will suppress any reauthentication dialogs. 42 gFakeOSKeyStore = { 43 asyncEncryptBytes: sinon.stub(), 44 asyncDecryptBytes: sinon.stub(), 45 asyncDeleteSecret: sinon.stub().resolves(), 46 asyncSecretAvailable: sinon.stub().resolves(true), 47 asyncGetRecoveryPhrase: sinon.stub().resolves("SomeRecoveryPhrase"), 48 asyncRecoverSecret: sinon.stub().resolves(), 49 QueryInterface: ChromeUtils.generateQI([Ci.nsIOSKeyStore]), 50 }; 51 let osKeyStoreCID = MockRegistrar.register( 52 "@mozilla.org/security/oskeystore;1", 53 gFakeOSKeyStore 54 ); 55 56 OSKeyStoreTestUtils.setup(); 57 registerCleanupFunction(async () => { 58 await OSKeyStoreTestUtils.cleanup(); 59 MockRegistrar.unregister(osKeyStoreCID); 60 }); 61 62 // This will set gDirServiceProvider, which doesn't happen automatically 63 // in xpcshell tests, but is needed by BackupService. 64 Cc["@mozilla.org/xre/directory-provider;1"].getService(Ci.nsIXREDirProvider); 65 }); 66 67 const BYTES_IN_KB = 1000; 68 69 const FAKE_METADATA = { 70 date: "2024-06-07T00:00:00+00:00", 71 appName: "firefox", 72 appVersion: "128.0", 73 buildID: "20240604133346", 74 profileName: "profile-default", 75 machineName: "A super cool machine", 76 osName: "Windows_NT", 77 osVersion: "10.0", 78 legacyClientID: "decafbad-0cd1-0cd2-0cd3-decafbad1000", 79 profileGroupID: "decafbad-0cd1-0cd2-0cd3-decafbad2000", 80 accountID: "", 81 accountEmail: "", 82 }; 83 84 do_get_profile(); 85 86 // Configure any backup files to get written into a temporary folder. 87 Services.prefs.setStringPref("browser.backup.location", PathUtils.tempDir); 88 89 /** 90 * Some fake backup resource classes to test with. 91 */ 92 class FakeBackupResource1 extends BackupResource { 93 static get key() { 94 return "fake1"; 95 } 96 static get requiresEncryption() { 97 return false; 98 } 99 } 100 101 /** 102 * Another fake backup resource class to test with. 103 */ 104 class FakeBackupResource2 extends BackupResource { 105 static get key() { 106 return "fake2"; 107 } 108 static get requiresEncryption() { 109 return false; 110 } 111 static get priority() { 112 return 1; 113 } 114 } 115 116 /** 117 * Yet another fake backup resource class to test with. 118 */ 119 class FakeBackupResource3 extends BackupResource { 120 static get key() { 121 return "fake3"; 122 } 123 static get requiresEncryption() { 124 return false; 125 } 126 static get priority() { 127 return 2; 128 } 129 } 130 131 /** 132 * Create a file of a given size in kilobytes. 133 * 134 * @param {string} path the path where the file will be created. 135 * @param {number} sizeInKB size file in Kilobytes. 136 * @returns {Promise<undefined>} 137 */ 138 async function createKilobyteSizedFile(path, sizeInKB) { 139 let bytes = new Uint8Array(sizeInKB * BYTES_IN_KB); 140 await IOUtils.write(path, bytes); 141 } 142 143 /** 144 * @typedef {object} TestFileObject 145 * @property {(string|Array.<string>)} path 146 * The relative path of the file. It can be a string or an array of strings 147 * in the event that directories need to be created. For example, this is 148 * an array of valid TestFileObjects. 149 * 150 * [ 151 * { path: "file1.txt" }, 152 * { path: ["dir1", "file2.txt"] }, 153 * { path: ["dir2", "dir3", "file3.txt"], sizeInKB: 25 }, 154 * { path: "file4.txt" }, 155 * ] 156 * 157 * @property {number} [sizeInKB=10] 158 * The size of the created file in kilobytes. Defaults to 10. 159 */ 160 161 /** 162 * Easily creates a series of test files and directories under parentPath. 163 * 164 * @param {string} parentPath 165 * The path to the parent directory where the files will be created. 166 * @param {TestFileObject[]} testFilesArray 167 * An array of TestFileObjects describing what test files to create within 168 * the parentPath. 169 * @see TestFileObject 170 * @returns {Promise<undefined>} 171 */ 172 async function createTestFiles(parentPath, testFilesArray) { 173 for (let { path, sizeInKB } of testFilesArray) { 174 if (Array.isArray(path)) { 175 // Make a copy of the array of path elements, chopping off the last one. 176 // We'll assume the unchopped items are directories, and make sure they 177 // exist first. 178 let folders = path.slice(0, -1); 179 await IOUtils.getDirectory(PathUtils.join(parentPath, ...folders)); 180 } 181 182 if (sizeInKB === undefined) { 183 sizeInKB = 10; 184 } 185 186 // This little piece of cleverness coerces a string into an array of one 187 // if path is a string, or just leaves it alone if it's already an array. 188 let filePath = PathUtils.join(parentPath, ...[].concat(path)); 189 await createKilobyteSizedFile(filePath, sizeInKB); 190 } 191 } 192 193 /** 194 * Checks that files exist within a particular folder. The filesize is not 195 * checked. 196 * 197 * @param {string} parentPath 198 * The path to the parent directory where the files should exist. 199 * @param {TestFileObject[]} testFilesArray 200 * An array of TestFileObjects describing what test files to search for within 201 * parentPath. 202 * @see TestFileObject 203 * @returns {Promise<undefined>} 204 */ 205 async function assertFilesExist(parentPath, testFilesArray) { 206 for (let { path } of testFilesArray) { 207 let copiedFileName = PathUtils.join(parentPath, ...[].concat(path)); 208 Assert.ok( 209 await IOUtils.exists(copiedFileName), 210 `${copiedFileName} should exist in the staging folder` 211 ); 212 } 213 } 214 215 /** 216 * Perform removalFn, potentially a few times, with special consideration for 217 * Windows, which has issues with file removal. Sometimes remove() throws 218 * ERROR_LOCK_VIOLATION when the file is not unlocked soon enough. This can 219 * happen because kernel operations still hold a handle to it, or because e.g. 220 * Windows Defender started a scan, or whatever. Some applications (for 221 * example SQLite) retry deletes with a delay on Windows. We emulate that 222 * here, since this happens very frequently in tests. 223 * 224 * This registers a test failure if it does not succeed. 225 * 226 * @param {Function} removalFn Async function to (potentially repeatedly) 227 * call. 228 * @throws The resultant file system exception if removal fails, despite the 229 * special considerations. 230 */ 231 async function doFileRemovalOperation(removalFn) { 232 const kMaxRetries = 5; 233 let nRetries = 0; 234 let lastException; 235 while (nRetries < kMaxRetries) { 236 nRetries++; 237 try { 238 await removalFn(); 239 // Success! 240 return; 241 } catch (error) { 242 if ( 243 AppConstants.platform !== "win" || 244 !/NS_ERROR_FILE_IS_LOCKED/.test(error.message) 245 ) { 246 throw error; 247 } 248 lastException = error; 249 } 250 await new Promise(res => setTimeout(res, 100)); 251 } 252 253 // All retries failed. Re-throw last exception. 254 Assert.ok(false, `doRemovalOperation failed after ${nRetries} attempts`); 255 throw lastException; 256 } 257 258 /** 259 * Remove a file or directory at a path if it exists and files are unlocked. 260 * Prefer this to IOUtils.remove in tests to avoid intermittent failures 261 * because of the Windows-ism mentioned in doFileRemovalOperation. 262 * 263 * @param {string} path path to remove. 264 */ 265 async function maybeRemovePath(path) { 266 let nAttempts = 0; 267 await doFileRemovalOperation(async () => { 268 nAttempts++; 269 await IOUtils.remove(path, { ignoreAbsent: true, recursive: true }); 270 Assert.ok(true, `Removed ${path} on attempt #${nAttempts}`); 271 }); 272 } 273 274 /** 275 * A generator function for deterministically generating a sequence of 276 * pseudo-random numbers between 0 and 255 with a fixed seed. This means we can 277 * generate an arbitrary amount of nonsense information, but that generation 278 * will be consistent between test runs. It's definitely not a cryptographically 279 * secure random number generator! Please don't use it for that! 280 * 281 * @yields {number} 282 * The next number in the sequence. 283 */ 284 function* seededRandomNumberGenerator() { 285 // This is a verbatim copy of the public domain-licensed code in 286 // https://github.com/bryc/code/blob/master/jshash/PRNGs.md for the sfc32 287 // PRNG (see https://pracrand.sourceforge.net/RNG_engines.txt) 288 let sfc32 = function (a, b, c, d) { 289 return function () { 290 a |= 0; 291 b |= 0; 292 c |= 0; 293 d |= 0; 294 var t = (((a + b) | 0) + d) | 0; 295 d = (d + 1) | 0; 296 a = b ^ (b >>> 9); 297 b = (c + (c << 3)) | 0; 298 c = (c << 21) | (c >>> 11); 299 c = (c + t) | 0; 300 return (t >>> 0) / 4294967296; 301 }; 302 }; 303 304 // The seeds don't need to make sense, they just need to be the same from 305 // test run to test run to give us a consistent stream of nonsense. 306 const SEED1 = 123; 307 const SEED2 = 456; 308 const SEED3 = 789; 309 const SEED4 = 101; 310 311 let generator = sfc32(SEED1, SEED2, SEED3, SEED4); 312 313 while (true) { 314 yield Math.round(generator() * 1000) % 255; 315 } 316 } 317 318 /** 319 * Compares 2 Uint8Arrays and checks their similarity. This is mainly used to 320 * test that the encrypted bytes passed through ArchiveEncryptor actually get 321 * changed, and that the bytes that come out of ArchiveDecryptor match the 322 * source bytes. This doesn't test that the resulting bytes have gone through 323 * the Web Crypto API. 324 * 325 * When expecting similar arrays, we expect the byte lengths to be the same, and 326 * for the bytes to match. When expecting dissimilar arrays, we expect the byte 327 * lengths to be different and for at least one byte to be dissimilar between 328 * the two arrays. 329 * 330 * @param {Uint8Array} uint8ArrayA 331 * The left-side of the Uint8Array comparison. 332 * @param {Uint8Array} uint8ArrayB 333 * The right-side fo the Uint8Array comparison. 334 * @param {boolean} expectSimilar 335 * True if the caller expects the two arrays to be similar, false otherwise. 336 */ 337 function assertUint8ArraysSimilarity(uint8ArrayA, uint8ArrayB, expectSimilar) { 338 let lengthToCheck; 339 if (expectSimilar) { 340 Assert.equal( 341 uint8ArrayA.byteLength, 342 uint8ArrayB.byteLength, 343 "Uint8Arrays have the same byteLength" 344 ); 345 lengthToCheck = uint8ArrayA.byteLength; 346 } else { 347 Assert.notEqual( 348 uint8ArrayA.byteLength, 349 uint8ArrayB.byteLength, 350 "Uint8Arrays have differing byteLength" 351 ); 352 lengthToCheck = Math.min(uint8ArrayA.byteLength, uint8ArrayB.byteLength); 353 } 354 355 let foundDifference = false; 356 for (let i = 0; i < lengthToCheck; ++i) { 357 if (uint8ArrayA[i] != uint8ArrayB[i]) { 358 foundDifference = true; 359 break; 360 } 361 } 362 363 if (expectSimilar) { 364 Assert.ok(!foundDifference, "Arrays contain the same bytes."); 365 } else { 366 Assert.ok(foundDifference, "Arrays contain different bytes."); 367 } 368 } 369 370 /** 371 * Returns the total number of measurements taken for this histogram, regardless 372 * of the values of the measurements themselves. 373 * 374 * @param {object} histogram 375 * Telemetry histogram object, like from `getHistogramById` 376 * @returns {number} 377 * Number of measurements in the latest snapshot of the histogram 378 */ 379 function countHistogramMeasurements(histogram) { 380 const snapshot = histogram.snapshot(); 381 const countsPerBucket = Object.values(snapshot.values); 382 return countsPerBucket.reduce((sum, count) => sum + count, 0); 383 } 384 385 function setupProfile() { 386 // FOG needs to be initialized in order for data to flow. 387 Services.fog.initializeFOG(); 388 389 // Much of this setup is copied from toolkit/profile/xpcshell/head.js. It is 390 // needed in order to put the xpcshell test environment into the state where 391 // it thinks its profile is the one pointed at by 392 // nsIToolkitProfileService.currentProfile. 393 let gProfD = do_get_profile(); 394 let gDataHome = gProfD.clone(); 395 gDataHome.append("data"); 396 gDataHome.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755); 397 let gDataHomeLocal = gProfD.clone(); 398 gDataHomeLocal.append("local"); 399 gDataHomeLocal.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755); 400 401 let xreDirProvider = Cc["@mozilla.org/xre/directory-provider;1"].getService( 402 Ci.nsIXREDirProvider 403 ); 404 xreDirProvider.setUserDataDirectory(gDataHome, false); 405 xreDirProvider.setUserDataDirectory(gDataHomeLocal, true); 406 407 let profileSvc = Cc["@mozilla.org/toolkit/profile-service;1"].getService( 408 Ci.nsIToolkitProfileService 409 ); 410 411 let createdProfile = {}; 412 let didCreate = profileSvc.selectStartupProfile( 413 ["xpcshell"], 414 false, 415 AppConstants.UPDATE_CHANNEL, 416 "", 417 {}, 418 {}, 419 createdProfile 420 ); 421 Assert.ok(didCreate, "Created a testing profile and set it to current."); 422 Assert.equal( 423 profileSvc.currentProfile, 424 createdProfile.value, 425 "Profile set to current" 426 ); 427 428 return createdProfile.value; 429 } 430 431 /** 432 * Asserts that a histogram received a certain number of measurements, regardless 433 * of the values of the measurements themselves. 434 * 435 * @param {object} histogram 436 * Telemetry histogram object, like from `getHistogramById` 437 * @param {number} expected 438 * Expected number of measurements to have been taken 439 * @param {string?} message 440 * Optional message for test report 441 * @returns {void} 442 * No return value; only runs assertions 443 */ 444 function assertHistogramMeasurementQuantity( 445 histogram, 446 expected, 447 message = "Should have taken a specific number of measurements in the histogram" 448 ) { 449 const totalCount = countHistogramMeasurements(histogram); 450 Assert.equal(totalCount, expected, message); 451 } 452 453 /** 454 * @param {GleanDistributionData?} timerTestValue 455 * Glean timer from `testGetValue` 456 * @returns {void} 457 * No return value; only runs assertions 458 */ 459 function assertSingleTimeMeasurement(timerTestValue) { 460 Assert.notEqual(timerTestValue, null, "Timer should have something recorded"); 461 Assert.equal( 462 timerTestValue.count, 463 1, 464 "Timer should have a single measurement" 465 ); 466 Assert.greater(timerTestValue.sum, 0, "Timer measurement should be non-zero"); 467 }