test_MiscDataBackupResource.js (9508B)
1 /* Any copyright is dedicated to the Public Domain. 2 https://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 const { MiscDataBackupResource } = ChromeUtils.importESModule( 7 "resource:///modules/backup/MiscDataBackupResource.sys.mjs" 8 ); 9 10 const { ASRouterStorage } = ChromeUtils.importESModule( 11 "resource:///modules/asrouter/ASRouterStorage.sys.mjs" 12 ); 13 14 const { ProfileAge } = ChromeUtils.importESModule( 15 "resource://gre/modules/ProfileAge.sys.mjs" 16 ); 17 18 /** 19 * Tests that we can measure miscellaneous files in the profile directory. 20 */ 21 add_task(async function test_measure() { 22 Services.fog.testResetFOG(); 23 24 const EXPECTED_MISC_KILOBYTES_SIZE = 231; 25 const tempDir = await IOUtils.createUniqueDirectory( 26 PathUtils.tempDir, 27 "MiscDataBackupResource-measurement-test" 28 ); 29 30 const mockFiles = [ 31 { path: "enumerate_devices.txt", sizeInKB: 1 }, 32 { path: "protections.sqlite", sizeInKB: 100 }, 33 { path: "SiteSecurityServiceState.bin", sizeInKB: 10 }, 34 { path: ["storage", "permanent", "chrome", "123ABC.sqlite"], sizeInKB: 40 }, 35 { path: ["storage", "permanent", "chrome", "456DEF.sqlite"], sizeInKB: 40 }, 36 { 37 path: ["storage", "permanent", "chrome", "mockIDBDir", "890HIJ.sqlite"], 38 sizeInKB: 40, 39 }, 40 ]; 41 42 await createTestFiles(tempDir, mockFiles); 43 44 let miscDataBackupResource = new MiscDataBackupResource(); 45 await miscDataBackupResource.measure(tempDir); 46 47 let measurement = Glean.browserBackup.miscDataSize.testGetValue(); 48 let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false); 49 50 TelemetryTestUtils.assertScalar( 51 scalars, 52 "browser.backup.misc_data_size", 53 measurement, 54 "Glean and telemetry measurements for misc data should be equal" 55 ); 56 Assert.equal( 57 measurement, 58 EXPECTED_MISC_KILOBYTES_SIZE, 59 "Should have collected the correct glean measurement for misc files" 60 ); 61 62 await maybeRemovePath(tempDir); 63 }); 64 65 add_task(async function test_backup() { 66 let sandbox = sinon.createSandbox(); 67 68 let miscDataBackupResource = new MiscDataBackupResource(); 69 let sourcePath = await IOUtils.createUniqueDirectory( 70 PathUtils.tempDir, 71 "MiscDataBackupResource-source-test" 72 ); 73 let stagingPath = await IOUtils.createUniqueDirectory( 74 PathUtils.tempDir, 75 "MiscDataBackupResource-staging-test" 76 ); 77 78 const simpleCopyFiles = [ 79 { path: "enumerate_devices.txt" }, 80 { path: "SiteSecurityServiceState.bin" }, 81 ]; 82 await createTestFiles(sourcePath, simpleCopyFiles); 83 84 // Create our fake database files. We don't expect this to be copied to the 85 // staging directory in this test due to our stubbing of the backup method, so 86 // we don't include it in `simpleCopyFiles`. 87 await createTestFiles(sourcePath, [{ path: "protections.sqlite" }]); 88 89 // We have no need to test that Sqlite.sys.mjs's backup method is working - 90 // this is something that is tested in Sqlite's own tests. We can just make 91 // sure that it's being called using sinon. Unfortunately, we cannot do the 92 // same thing with IOUtils.copy, as its methods are not stubbable. 93 let fakeConnection = { 94 backup: sandbox.stub().resolves(true), 95 close: sandbox.stub().resolves(true), 96 }; 97 sandbox.stub(Sqlite, "openConnection").returns(fakeConnection); 98 99 let snippetsTableStub = { 100 getAllKeys: sandbox.stub().resolves(["key1", "key2"]), 101 get: sandbox.stub().callsFake(key => { 102 return { key: `value for ${key}` }; 103 }), 104 }; 105 106 sandbox 107 .stub(ASRouterStorage.prototype, "getDbTable") 108 .withArgs("snippets") 109 .resolves(snippetsTableStub); 110 111 let manifestEntry = await miscDataBackupResource.backup( 112 stagingPath, 113 sourcePath 114 ); 115 Assert.equal( 116 manifestEntry, 117 null, 118 "MiscDataBackupResource.backup should return null as its ManifestEntry" 119 ); 120 121 await assertFilesExist(stagingPath, simpleCopyFiles); 122 123 // Next, we'll make sure that the Sqlite connection had `backup` called on it 124 // with the right arguments. 125 Assert.ok( 126 fakeConnection.backup.calledOnce, 127 "Called backup the expected number of times for all connections" 128 ); 129 Assert.ok( 130 fakeConnection.backup.firstCall.calledWith( 131 PathUtils.join(stagingPath, "protections.sqlite") 132 ), 133 "Called backup on the protections.sqlite Sqlite connection" 134 ); 135 136 // Bug 1890585 - we don't currently have the generalized ability to copy the 137 // chrome-privileged IndexedDB databases under storage/permanent/chrome, but 138 // we do support copying individual IndexedDB databases by manually exporting 139 // and re-importing their contents. 140 let snippetsBackupPath = PathUtils.join( 141 stagingPath, 142 "activity-stream-snippets.json" 143 ); 144 Assert.ok( 145 await IOUtils.exists(snippetsBackupPath), 146 "The activity-stream-snippets.json file should exist" 147 ); 148 let snippetsBackupContents = await IOUtils.readJSON(snippetsBackupPath); 149 Assert.deepEqual( 150 snippetsBackupContents, 151 { 152 key1: { key: "value for key1" }, 153 key2: { key: "value for key2" }, 154 }, 155 "The contents of the activity-stream-snippets.json file should be as expected" 156 ); 157 158 await maybeRemovePath(stagingPath); 159 await maybeRemovePath(sourcePath); 160 161 sandbox.restore(); 162 }); 163 164 /** 165 * Test that the recover method correctly copies items from the recovery 166 * directory into the destination profile directory. 167 */ 168 add_task(async function test_recover() { 169 let miscBackupResource = new MiscDataBackupResource(); 170 let recoveryPath = await IOUtils.createUniqueDirectory( 171 PathUtils.tempDir, 172 "MiscDataBackupResource-recovery-test" 173 ); 174 let destProfilePath = await IOUtils.createUniqueDirectory( 175 PathUtils.tempDir, 176 "MiscDataBackupResource-test-profile" 177 ); 178 179 // Write a dummy times.json into the xpcshell test profile directory. We 180 // expect it to be copied into the destination profile. 181 let originalProfileAge = await ProfileAge(PathUtils.profileDir); 182 await originalProfileAge.computeAndPersistCreated(); 183 Assert.ok( 184 await IOUtils.exists(PathUtils.join(PathUtils.profileDir, "times.json")) 185 ); 186 187 const simpleCopyFiles = [ 188 { path: "enumerate_devices.txt" }, 189 { path: "protections.sqlite" }, 190 { path: "SiteSecurityServiceState.bin" }, 191 ]; 192 await createTestFiles(recoveryPath, simpleCopyFiles); 193 194 const SNIPPETS_BACKUP_FILE = "activity-stream-snippets.json"; 195 196 // We'll also separately create the activity-stream-snippets.json file, which 197 // is not expected to be copied into the profile directory, but is expected 198 // to exist in the recovery path. 199 await createTestFiles(recoveryPath, [{ path: SNIPPETS_BACKUP_FILE }]); 200 201 // The backup method is expected to have returned a null ManifestEntry 202 let postRecoveryEntry = await miscBackupResource.recover( 203 null /* manifestEntry */, 204 recoveryPath, 205 destProfilePath 206 ); 207 Assert.deepEqual( 208 postRecoveryEntry, 209 { 210 snippetsBackupFile: PathUtils.join(recoveryPath, SNIPPETS_BACKUP_FILE), 211 }, 212 "MiscDataBackupResource.recover should return the snippets backup data " + 213 "path as its post recovery entry" 214 ); 215 216 await assertFilesExist(destProfilePath, simpleCopyFiles); 217 218 // The activity-stream-snippets.json path should _not_ have been written to 219 // the profile path. 220 Assert.ok( 221 !(await IOUtils.exists( 222 PathUtils.join(destProfilePath, SNIPPETS_BACKUP_FILE) 223 )), 224 "Snippets backup data should not have gone into the profile directory" 225 ); 226 227 // The times.json file should have been copied over and a backup recovery 228 // time written into it. 229 Assert.ok( 230 await IOUtils.exists(PathUtils.join(destProfilePath, "times.json")) 231 ); 232 let copiedProfileAge = await ProfileAge(destProfilePath); 233 Assert.equal( 234 await originalProfileAge.created, 235 await copiedProfileAge.created, 236 "Created timestamp should match." 237 ); 238 Assert.equal( 239 await originalProfileAge.firstUse, 240 await copiedProfileAge.firstUse, 241 "First use timestamp should match." 242 ); 243 Assert.ok( 244 await copiedProfileAge.recoveredFromBackup, 245 "Backup recovery timestamp should have been set." 246 ); 247 248 await maybeRemovePath(recoveryPath); 249 await maybeRemovePath(destProfilePath); 250 }); 251 252 /** 253 * Test that the postRecovery method correctly writes the snippets backup data 254 * into the snippets IndexedDB table. 255 */ 256 add_task(async function test_postRecovery() { 257 let sandbox = sinon.createSandbox(); 258 259 let fakeProfilePath = await IOUtils.createUniqueDirectory( 260 PathUtils.tempDir, 261 "MiscDataBackupResource-test-profile" 262 ); 263 let fakeSnippetsData = { 264 key1: "value1", 265 key2: "value2", 266 }; 267 const SNIPPEST_BACKUP_FILE = PathUtils.join( 268 fakeProfilePath, 269 "activity-stream-snippets.json" 270 ); 271 272 await IOUtils.writeJSON(SNIPPEST_BACKUP_FILE, fakeSnippetsData); 273 274 let snippetsTableStub = { 275 set: sandbox.stub(), 276 }; 277 278 sandbox 279 .stub(ASRouterStorage.prototype, "getDbTable") 280 .withArgs("snippets") 281 .resolves(snippetsTableStub); 282 283 let miscBackupResource = new MiscDataBackupResource(); 284 await miscBackupResource.postRecovery({ 285 snippetsBackupFile: SNIPPEST_BACKUP_FILE, 286 }); 287 288 Assert.ok( 289 snippetsTableStub.set.calledTwice, 290 "The snippets table's set method was called twice" 291 ); 292 Assert.ok( 293 snippetsTableStub.set.firstCall.calledWith("key1", "value1"), 294 "The snippets table's set method was called with the first key-value pair" 295 ); 296 Assert.ok( 297 snippetsTableStub.set.secondCall.calledWith("key2", "value2"), 298 "The snippets table's set method was called with the second key-value pair" 299 ); 300 301 sandbox.restore(); 302 });