test_PreferencesBackupResource.js (11322B)
1 /* Any copyright is dedicated to the Public Domain. 2 https://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 const { PreferencesBackupResource } = ChromeUtils.importESModule( 7 "resource:///modules/backup/PreferencesBackupResource.sys.mjs" 8 ); 9 const { SearchUtils } = ChromeUtils.importESModule( 10 "moz-src:///toolkit/components/search/SearchUtils.sys.mjs" 11 ); 12 13 /** 14 * Test that the measure method correctly collects the disk-sizes of things that 15 * the PreferencesBackupResource is meant to back up. 16 */ 17 add_task(async function test_measure() { 18 Services.fog.testResetFOG(); 19 20 const EXPECTED_PREFERENCES_KILOBYTES_SIZE = 55; 21 const tempDir = await IOUtils.createUniqueDirectory( 22 PathUtils.tempDir, 23 "PreferencesBackupResource-measure-test" 24 ); 25 const mockFiles = [ 26 { path: "prefs.js", sizeInKB: 20 }, 27 { path: "xulstore.json", sizeInKB: 1 }, 28 { path: "containers.json", sizeInKB: 1 }, 29 { path: "handlers.json", sizeInKB: 1 }, 30 { path: "search.json.mozlz4", sizeInKB: 1 }, 31 { path: "user.js", sizeInKB: 2 }, 32 { path: ["chrome", "userChrome.css"], sizeInKB: 5 }, 33 { path: ["chrome", "userContent.css"], sizeInKB: 5 }, 34 { path: ["chrome", "css", "mockStyles.css"], sizeInKB: 5 }, 35 ]; 36 37 await createTestFiles(tempDir, mockFiles); 38 39 let preferencesBackupResource = new PreferencesBackupResource(); 40 41 await preferencesBackupResource.measure(tempDir); 42 43 let measurement = Glean.browserBackup.preferencesSize.testGetValue(); 44 let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false); 45 46 TelemetryTestUtils.assertScalar( 47 scalars, 48 "browser.backup.preferences_size", 49 measurement, 50 "Glean and telemetry measurements for preferences data should be equal" 51 ); 52 Assert.equal( 53 measurement, 54 EXPECTED_PREFERENCES_KILOBYTES_SIZE, 55 "Should have collected the correct glean measurement for preferences files" 56 ); 57 58 await maybeRemovePath(tempDir); 59 }); 60 61 /** 62 * Test that the backup method correctly copies items from the profile directory 63 * into the staging directory. 64 */ 65 add_task(async function test_backup() { 66 let sandbox = sinon.createSandbox(); 67 68 let preferencesBackupResource = new PreferencesBackupResource(); 69 let sourcePath = await IOUtils.createUniqueDirectory( 70 PathUtils.tempDir, 71 "PreferencesBackupResource-source-test" 72 ); 73 let stagingPath = await IOUtils.createUniqueDirectory( 74 PathUtils.tempDir, 75 "PreferencesBackupResource-staging-test" 76 ); 77 78 const simpleCopyFiles = [ 79 { path: "xulstore.json" }, 80 { path: "containers.json" }, 81 { path: "handlers.json" }, 82 { path: "search.json.mozlz4" }, 83 { path: "user.js" }, 84 { path: ["chrome", "userChrome.css"] }, 85 { path: ["chrome", "userContent.css"] }, 86 { path: ["chrome", "childFolder", "someOtherStylesheet.css"] }, 87 ]; 88 await createTestFiles(sourcePath, simpleCopyFiles); 89 90 // We have no need to test that Sqlite.sys.mjs's backup method is working - 91 // this is something that is tested in Sqlite's own tests. We can just make 92 // sure that it's being called using sinon. Unfortunately, we cannot do the 93 // same thing with IOUtils.copy, as its methods are not stubbable. 94 let fakeConnection = { 95 backup: sandbox.stub().resolves(true), 96 close: sandbox.stub().resolves(true), 97 }; 98 sandbox.stub(Sqlite, "openConnection").returns(fakeConnection); 99 100 let manifestEntry = await preferencesBackupResource.backup( 101 stagingPath, 102 sourcePath 103 ); 104 Assert.deepEqual( 105 manifestEntry, 106 { profilePath: sourcePath }, 107 "PreferencesBackupResource.backup should return the original profile path " + 108 "in its ManifestEntry" 109 ); 110 111 await assertFilesExist(stagingPath, simpleCopyFiles); 112 113 Assert.ok( 114 fakeConnection.backup.notCalled, 115 "No sqlite connections should have been made" 116 ); 117 118 // And we'll make sure that preferences were properly written out. 119 Assert.ok( 120 await IOUtils.exists(PathUtils.join(stagingPath, "prefs.js")), 121 "prefs.js should exist in the staging folder" 122 ); 123 124 await maybeRemovePath(stagingPath); 125 await maybeRemovePath(sourcePath); 126 127 sandbox.restore(); 128 }); 129 130 /** 131 * Check that prefs.js has "browser.backup.profile-restoration-date". Due to 132 * concerns over potential time skips in automation, we only check that the 133 * timestamp is not more than a week before/after now (we would expect the 134 * difference to be more like a few milliseconds). 135 * 136 * @param {string} prefsJsPath 137 */ 138 async function checkPrefsJsHasValidRecoveryTime(prefsJsPath) { 139 Assert.equal( 140 Services.prefs.getPrefType("browser.backup.profile-restoration-date"), 141 Services.prefs.PREF_INVALID, 142 "Restoration pref not set since current profile was not restored" 143 ); 144 145 // NB: The non-profile-restoration-date part of the prefs file is junk made 146 // by `createTestFiles`. We don't care about that here. 147 const contents = await IOUtils.readUTF8(prefsJsPath); 148 const dateRegex = 149 /pref\("browser\.backup\.profile-restoration-date", (\d+)\);/; 150 let restoreDate = contents.match(dateRegex); 151 Assert.equal(restoreDate.length, 2, "found the restoration date"); 152 153 const kOneWeekAgoInSec = 154 60 /* sec/min */ * 60 /* min/hr */ * 24 /* hr/day */ * 7; /* day/wk */ 155 const nowInSeconds = Math.round(Date.now() / 1000); 156 Assert.lessOrEqual( 157 Math.abs(nowInSeconds - Number(restoreDate[1])), 158 kOneWeekAgoInSec, 159 "timestamp was within one week of now" 160 ); 161 } 162 163 /** 164 * Test that the recover method correctly copies items from the recovery 165 * directory into the destination profile directory. 166 */ 167 add_task(async function test_recover() { 168 let sandbox = sinon.createSandbox(); 169 let preferencesBackupResource = new PreferencesBackupResource(); 170 let recoveryPath = await IOUtils.createUniqueDirectory( 171 PathUtils.tempDir, 172 "PreferencesBackupResource-recovery-test" 173 ); 174 let destProfilePath = await IOUtils.createUniqueDirectory( 175 PathUtils.tempDir, 176 "PreferencesBackupResource-test-profile" 177 ); 178 179 const simpleCopyFiles = [ 180 { path: "prefs.js" }, 181 { path: "xulstore.json" }, 182 { path: "containers.json" }, 183 { path: "handlers.json" }, 184 { path: "user.js" }, 185 { path: ["chrome", "userChrome.css"] }, 186 { path: ["chrome", "userContent.css"] }, 187 { path: ["chrome", "childFolder", "someOtherStylesheet.css"] }, 188 ]; 189 await createTestFiles(recoveryPath, simpleCopyFiles); 190 191 // We'll now hand-prepare enough of a search.json.mozlz4 file that we can 192 // ensure that PreferencesBackupResource knows how to update the 193 // verification hashes for non-default engines. 194 const TEST_SEARCH_ENGINE_LOAD_PATH = "some/path/on/disk"; 195 const TEST_SEARCH_ENGINE_LOAD_PATH_HASH = "some pre-existing hash"; 196 const TEST_DEFAULT_ENGINE_ID = "bugle"; 197 const TEST_DEFAULT_ENGINE_ID_HASH = "default engine original hash"; 198 const TEST_PRIVATE_DEFAULT_ENGINE_ID = "goose"; 199 const TEST_PRIVATE_DEFAULT_ENGINE_ID_HASH = 200 "private default engine original hash"; 201 202 let fakeSearchPrefs = { 203 metaData: { 204 defaultEngineId: TEST_DEFAULT_ENGINE_ID, 205 defaultEngineIdHash: TEST_DEFAULT_ENGINE_ID_HASH, 206 privateDefaultEngineId: TEST_PRIVATE_DEFAULT_ENGINE_ID, 207 privateDefaultEngineIdHash: TEST_PRIVATE_DEFAULT_ENGINE_ID_HASH, 208 }, 209 engines: [ 210 { 211 _loadPath: TEST_SEARCH_ENGINE_LOAD_PATH, 212 _metaData: { 213 loadPathHash: TEST_SEARCH_ENGINE_LOAD_PATH_HASH, 214 }, 215 }, 216 ], 217 }; 218 219 const SEARCH_PREFS_FILENAME = "search.json.mozlz4"; 220 await IOUtils.writeJSON( 221 PathUtils.join(recoveryPath, SEARCH_PREFS_FILENAME), 222 fakeSearchPrefs, 223 { 224 compress: true, 225 } 226 ); 227 228 const EXPECTED_HASH = "this is some newly generated hash"; 229 sandbox 230 .stub(SearchUtils, "getVerificationHash") 231 .onCall(0) 232 .returns(TEST_SEARCH_ENGINE_LOAD_PATH_HASH) 233 .onCall(1) 234 .returns(EXPECTED_HASH) 235 .onCall(2) 236 .returns(TEST_DEFAULT_ENGINE_ID_HASH) 237 .onCall(3) 238 .returns(EXPECTED_HASH) 239 .onCall(4) 240 .returns(TEST_PRIVATE_DEFAULT_ENGINE_ID_HASH) 241 .onCall(5) 242 .returns(EXPECTED_HASH); 243 244 const PRETEND_ORIGINAL_PATH = "some/original/path"; 245 246 // The backup method is expected to have returned a null ManifestEntry 247 let postRecoveryEntry = await preferencesBackupResource.recover( 248 { profilePath: PRETEND_ORIGINAL_PATH }, 249 recoveryPath, 250 destProfilePath 251 ); 252 Assert.equal( 253 postRecoveryEntry, 254 null, 255 "PreferencesBackupResource.recover should return null as its post recovery entry" 256 ); 257 258 await assertFilesExist(destProfilePath, simpleCopyFiles); 259 await checkPrefsJsHasValidRecoveryTime( 260 PathUtils.join(destProfilePath, "prefs.js") 261 ); 262 263 // Now ensure that the verification was properly recomputed. We should 264 // Have called getVerificationHash 6 times - twice each for: 265 // 266 // - The single engine in the engines list 267 // - The defaultEngineId 268 // - The privateDefaultEngineId 269 // 270 // The first call is to verify the hash against the original profile path, 271 // and the second call is to generate the hash for the new profile path. 272 Assert.equal( 273 SearchUtils.getVerificationHash.callCount, 274 6, 275 "SearchUtils.getVerificationHash was called the right number of times." 276 ); 277 Assert.ok( 278 SearchUtils.getVerificationHash 279 .getCall(0) 280 .calledWith(TEST_SEARCH_ENGINE_LOAD_PATH, PRETEND_ORIGINAL_PATH), 281 "SearchUtils.getVerificationHash first call called with the right arguments." 282 ); 283 Assert.ok( 284 SearchUtils.getVerificationHash 285 .getCall(1) 286 .calledWith(TEST_SEARCH_ENGINE_LOAD_PATH, destProfilePath), 287 "SearchUtils.getVerificationHash second call called with the right arguments." 288 ); 289 Assert.ok( 290 SearchUtils.getVerificationHash 291 .getCall(2) 292 .calledWith(TEST_DEFAULT_ENGINE_ID, PRETEND_ORIGINAL_PATH), 293 "SearchUtils.getVerificationHash third call called with the right arguments." 294 ); 295 Assert.ok( 296 SearchUtils.getVerificationHash 297 .getCall(3) 298 .calledWith(TEST_DEFAULT_ENGINE_ID, destProfilePath), 299 "SearchUtils.getVerificationHash fourth call called with the right arguments." 300 ); 301 Assert.ok( 302 SearchUtils.getVerificationHash 303 .getCall(4) 304 .calledWith(TEST_PRIVATE_DEFAULT_ENGINE_ID, PRETEND_ORIGINAL_PATH), 305 "SearchUtils.getVerificationHash fifth call called with the right arguments." 306 ); 307 Assert.ok( 308 SearchUtils.getVerificationHash 309 .getCall(5) 310 .calledWith(TEST_PRIVATE_DEFAULT_ENGINE_ID, destProfilePath), 311 "SearchUtils.getVerificationHash sixth call called with the right arguments." 312 ); 313 314 let recoveredSearchPrefs = await IOUtils.readJSON( 315 PathUtils.join(destProfilePath, SEARCH_PREFS_FILENAME), 316 { decompress: true } 317 ); 318 Assert.equal( 319 recoveredSearchPrefs.engines.length, 320 1, 321 "Should still have 1 search engine" 322 ); 323 Assert.equal( 324 recoveredSearchPrefs.engines[0]._metaData.loadPathHash, 325 EXPECTED_HASH, 326 "The expected hash was written for the single engine." 327 ); 328 Assert.equal( 329 recoveredSearchPrefs.metaData.defaultEngineIdHash, 330 EXPECTED_HASH, 331 "The expected hash was written for the default engine." 332 ); 333 Assert.equal( 334 recoveredSearchPrefs.metaData.privateDefaultEngineIdHash, 335 EXPECTED_HASH, 336 "The expected hash was written for the private default engine." 337 ); 338 339 await maybeRemovePath(recoveryPath); 340 await maybeRemovePath(destProfilePath); 341 sandbox.restore(); 342 });