test_BackupService_enabled.js (13416B)
1 /* Any copyright is dedicated to the Public Domain. 2 https://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 const { ExperimentAPI } = ChromeUtils.importESModule( 7 "resource://nimbus/ExperimentAPI.sys.mjs" 8 ); 9 const { NimbusTestUtils } = ChromeUtils.importESModule( 10 "resource://testing-common/NimbusTestUtils.sys.mjs" 11 ); 12 13 const BACKUP_DIR_PREF_NAME = "browser.backup.location"; 14 const BACKUP_ARCHIVE_ENABLED_PREF_NAME = "browser.backup.archive.enabled"; 15 const BACKUP_ARCHIVE_ENABLED_OVERRIDE_PREF_NAME = 16 "browser.backup.archive.overridePlatformCheck"; 17 const BACKUP_RESTORE_ENABLED_PREF_NAME = "browser.backup.restore.enabled"; 18 const BACKUP_RESTORE_ENABLED_OVERRIDE_PREF_NAME = 19 "browser.backup.restore.overridePlatformCheck"; 20 const SELECTABLE_PROFILES_CREATED_PREF_NAME = "browser.profiles.created"; 21 22 add_setup(async () => { 23 setupProfile(); 24 25 NimbusTestUtils.init(this); 26 27 const { cleanup: nimbusCleanup } = await NimbusTestUtils.setupTest(); 28 29 await ExperimentAPI.ready(); 30 31 let backupDir = await IOUtils.createUniqueDirectory( 32 PathUtils.profileDir, 33 "backup" 34 ); 35 36 // Use temporary directory for backups. 37 Services.prefs.setStringPref(BACKUP_DIR_PREF_NAME, backupDir); 38 39 registerCleanupFunction(async () => { 40 const nimbusCleanupPromise = nimbusCleanup(); 41 const backupDirCleanupPromise = IOUtils.remove(backupDir, { 42 recursive: true, 43 }); 44 45 await Promise.all([nimbusCleanupPromise, backupDirCleanupPromise]); 46 47 Services.prefs.clearUserPref(BACKUP_DIR_PREF_NAME); 48 }); 49 }); 50 51 add_task(async function test_archive_killswitch_enrollment() { 52 let cleanupExperiment; 53 const savedPref = Services.prefs.getBoolPref( 54 BACKUP_ARCHIVE_ENABLED_OVERRIDE_PREF_NAME, 55 false 56 ); 57 await archiveTemplate({ 58 internalReason: "nimbus", 59 async disable() { 60 Services.prefs.setBoolPref( 61 BACKUP_ARCHIVE_ENABLED_OVERRIDE_PREF_NAME, 62 false 63 ); 64 cleanupExperiment = await NimbusTestUtils.enrollWithFeatureConfig({ 65 featureId: "backupService", 66 value: { archiveKillswitch: true }, 67 }); 68 }, 69 async enable() { 70 Services.prefs.setBoolPref( 71 BACKUP_ARCHIVE_ENABLED_OVERRIDE_PREF_NAME, 72 savedPref 73 ); 74 await cleanupExperiment(); 75 }, 76 // Nimbus calls onUpdate if any experiments are running, meaning that the 77 // observer service will be notified twice, i.e. one spurious call. 78 startup: 0, 79 }); 80 }); 81 82 add_task(async function test_archive_enabled_pref() { 83 await archiveTemplate({ 84 internalReason: "pref", 85 async disable() { 86 Services.prefs.unlockPref(BACKUP_ARCHIVE_ENABLED_PREF_NAME); 87 Services.prefs.setBoolPref(BACKUP_ARCHIVE_ENABLED_PREF_NAME, false); 88 }, 89 async enable() { 90 Services.prefs.setBoolPref(BACKUP_ARCHIVE_ENABLED_PREF_NAME, true); 91 }, 92 }); 93 }); 94 95 add_task(async function test_archive_policy() { 96 let storedDefault; 97 await archiveTemplate({ 98 internalReason: "policy", 99 disable: () => { 100 Services.prefs.unlockPref(BACKUP_ARCHIVE_ENABLED_PREF_NAME); 101 const defaults = Services.prefs.getDefaultBranch(""); 102 storedDefault = defaults.getBoolPref(BACKUP_ARCHIVE_ENABLED_PREF_NAME); 103 defaults.setBoolPref(BACKUP_ARCHIVE_ENABLED_PREF_NAME, false); 104 defaults.lockPref(BACKUP_ARCHIVE_ENABLED_PREF_NAME); 105 return 0; 106 }, 107 enable: () => { 108 const defaults = Services.prefs.getDefaultBranch(""); 109 defaults.setBoolPref(BACKUP_ARCHIVE_ENABLED_PREF_NAME, storedDefault); 110 Services.prefs.unlockPref(BACKUP_ARCHIVE_ENABLED_PREF_NAME); 111 Services.prefs.setBoolPref(BACKUP_ARCHIVE_ENABLED_PREF_NAME, true); 112 return 0; 113 }, 114 // At startup, there wouldn't have been a spurious call. 115 startup: 0, 116 }); 117 }); 118 119 add_task(async function test_archive_selectable_profiles() { 120 await archiveTemplate({ 121 internalReason: "selectable profiles", 122 async disable() { 123 Services.prefs.setBoolPref(SELECTABLE_PROFILES_CREATED_PREF_NAME, true); 124 }, 125 async enable() { 126 Services.prefs.setBoolPref(SELECTABLE_PROFILES_CREATED_PREF_NAME, false); 127 }, 128 }); 129 }); 130 131 add_task(async function test_archive_disabled_unsupported_os() { 132 const sandbox = sinon.createSandbox(); 133 const archiveWasEnabled = Services.prefs.getBoolPref( 134 BACKUP_ARCHIVE_ENABLED_OVERRIDE_PREF_NAME, 135 false 136 ); 137 Services.prefs.setBoolPref(BACKUP_ARCHIVE_ENABLED_OVERRIDE_PREF_NAME, false); 138 sandbox.stub(BackupService, "checkOsSupportsBackup").returns(false); 139 const cleanupExperiment = await NimbusTestUtils.enrollWithFeatureConfig({ 140 featureId: "backupService", 141 value: { archiveKillswitch: false }, 142 }); 143 144 try { 145 const bs = new BackupService(); 146 const status = bs.archiveEnabledStatus; 147 Assert.equal(false, status.enabled); 148 Assert.equal("os version", status.internalReason); 149 } finally { 150 sandbox.restore(); 151 Services.prefs.setBoolPref( 152 BACKUP_ARCHIVE_ENABLED_OVERRIDE_PREF_NAME, 153 archiveWasEnabled 154 ); 155 await cleanupExperiment(); 156 } 157 }); 158 159 add_task(async function test_archive_enabled_supported_os() { 160 const sandbox = sinon.createSandbox(); 161 const archiveWasEnabled = Services.prefs.getBoolPref( 162 BACKUP_ARCHIVE_ENABLED_OVERRIDE_PREF_NAME, 163 false 164 ); 165 Services.prefs.setBoolPref(BACKUP_ARCHIVE_ENABLED_OVERRIDE_PREF_NAME, false); 166 sandbox.stub(BackupService, "checkOsSupportsBackup").returns(true); 167 const cleanupExperiment = await NimbusTestUtils.enrollWithFeatureConfig({ 168 featureId: "backupService", 169 value: { archiveKillswitch: false }, 170 }); 171 try { 172 const bs = new BackupService(); 173 const status = bs.archiveEnabledStatus; 174 Assert.equal(true, status.enabled); 175 } finally { 176 sandbox.restore(); 177 Services.prefs.setBoolPref( 178 BACKUP_ARCHIVE_ENABLED_OVERRIDE_PREF_NAME, 179 archiveWasEnabled 180 ); 181 await cleanupExperiment(); 182 } 183 }); 184 185 add_task(async function test_restore_killswitch_enrollment() { 186 let cleanupExperiment; 187 const savedPref = Services.prefs.getBoolPref( 188 BACKUP_RESTORE_ENABLED_OVERRIDE_PREF_NAME, 189 false 190 ); 191 await restoreTemplate({ 192 internalReason: "nimbus", 193 async disable() { 194 Services.prefs.setBoolPref( 195 BACKUP_RESTORE_ENABLED_OVERRIDE_PREF_NAME, 196 false 197 ); 198 cleanupExperiment = await NimbusTestUtils.enrollWithFeatureConfig({ 199 featureId: "backupService", 200 value: { restoreKillswitch: true }, 201 }); 202 }, 203 async enable() { 204 Services.prefs.setBoolPref( 205 BACKUP_RESTORE_ENABLED_OVERRIDE_PREF_NAME, 206 savedPref 207 ); 208 await cleanupExperiment(); 209 }, 210 // Nimbus calls onUpdate if any experiments are running, meaning that the 211 // observer service will be notified twice, i.e. one spurious call. 212 startup: 0, 213 }); 214 }); 215 216 add_task(async function test_restore_enabled_pref() { 217 await restoreTemplate({ 218 internalReason: "pref", 219 async disable() { 220 Services.prefs.setBoolPref(BACKUP_RESTORE_ENABLED_PREF_NAME, false); 221 }, 222 async enable() { 223 Services.prefs.setBoolPref(BACKUP_RESTORE_ENABLED_PREF_NAME, true); 224 }, 225 }); 226 }); 227 228 add_task(async function test_restore_policy() { 229 let storedDefault; 230 await restoreTemplate({ 231 internalReason: "policy", 232 async disable() { 233 const defaults = Services.prefs.getDefaultBranch(""); 234 storedDefault = defaults.getBoolPref(BACKUP_RESTORE_ENABLED_PREF_NAME); 235 defaults.setBoolPref(BACKUP_RESTORE_ENABLED_PREF_NAME, false); 236 Services.prefs.lockPref(BACKUP_RESTORE_ENABLED_PREF_NAME); 237 return 0; 238 }, 239 async enable() { 240 Services.prefs.unlockPref(BACKUP_RESTORE_ENABLED_PREF_NAME); 241 const defaults = Services.prefs.getDefaultBranch(""); 242 defaults.setBoolPref(BACKUP_RESTORE_ENABLED_PREF_NAME, storedDefault); 243 return 0; 244 }, 245 // At startup, there wouldn't have been a spurious call. 246 startup: 0, 247 }); 248 }); 249 250 add_task(async function test_restore_selectable_profiles() { 251 await restoreTemplate({ 252 internalReason: "selectable profiles", 253 async disable() { 254 Services.prefs.setBoolPref(SELECTABLE_PROFILES_CREATED_PREF_NAME, true); 255 }, 256 async enable() { 257 Services.prefs.setBoolPref(SELECTABLE_PROFILES_CREATED_PREF_NAME, false); 258 }, 259 }); 260 }); 261 262 async function archiveTemplate({ internalReason, disable, enable, startup }) { 263 Services.telemetry.clearScalars(); 264 Services.fog.testResetFOG(); 265 266 let bs = new BackupService(); 267 bs.initStatusObservers(); 268 assertStatus("archive", bs.archiveEnabledStatus, true, null); 269 270 let calledCount = 0; 271 let callback = () => calledCount++; 272 Services.obs.addObserver(callback, "backup-service-status-updated"); 273 274 let spurious = (await disable()) ?? 0; 275 Assert.equal(calledCount, 1 + spurious, "Observers were notified on disable"); 276 assertStatus("archive", bs.archiveEnabledStatus, false, internalReason); 277 278 let backup = await bs.createBackup(); 279 Assert.ok( 280 !backup, 281 "Creating a backup should fail when archiving is disabled." 282 ); 283 284 spurious += (await enable()) ?? 0; 285 Assert.equal( 286 calledCount, 287 2 + spurious, 288 "Observers were notified on re-enable" 289 ); 290 assertStatus("archive", bs.archiveEnabledStatus, true, "reenabled"); 291 292 backup = await bs.createBackup(); 293 ok( 294 backup, 295 "Creating a backup should succeed once the archive killswitch experiment ends." 296 ); 297 ok( 298 await IOUtils.exists(backup.archivePath), 299 "Archive file should exist on disk." 300 ); 301 302 await IOUtils.remove(backup.archivePath); 303 bs.uninitStatusObservers(); 304 305 // Also check that it works at startup. 306 spurious += (startup ?? 0) + ((await disable()) ?? 0); 307 bs = new BackupService(); 308 bs.initStatusObservers(); 309 Assert.equal(calledCount, 3 + spurious, "Observers were notified at startup"); 310 assertStatus("archive", bs.archiveEnabledStatus, false, internalReason); 311 await enable(); 312 bs.uninitStatusObservers(); 313 314 Services.obs.removeObserver(callback, "backup-service-status-updated"); 315 } 316 317 async function restoreTemplate({ internalReason, disable, enable, startup }) { 318 Services.telemetry.clearScalars(); 319 Services.fog.testResetFOG(); 320 321 let bs = new BackupService(); 322 bs.initStatusObservers(); 323 assertStatus("restore", bs.restoreEnabledStatus, true, null); 324 325 const backup = await bs.createBackup(); 326 Assert.ok( 327 backup && backup.archivePath, 328 "Archive should have been created on disk." 329 ); 330 331 let calledCount = 0; 332 let callback = () => calledCount++; 333 Services.obs.addObserver(callback, "backup-service-status-updated"); 334 335 let spurious = (await disable()) ?? 0; 336 Assert.equal(calledCount, 1 + spurious, "Observers were notified on disable"); 337 assertStatus("restore", bs.restoreEnabledStatus, false, internalReason); 338 339 const recoveryDir = await IOUtils.createUniqueDirectory( 340 PathUtils.profileDir, 341 "recovered-profiles" 342 ); 343 344 await Assert.rejects( 345 bs.recoverFromBackupArchive( 346 backup.archivePath, 347 null, 348 false, 349 PathUtils.profileDir, 350 recoveryDir 351 ), 352 /.*disabled.*/, 353 "Recovery should throw when the restore is disabled." 354 ); 355 356 spurious += (await enable()) ?? 0; 357 Assert.equal( 358 calledCount, 359 2 + spurious, 360 "Observers were notified on re-enable" 361 ); 362 assertStatus("restore", bs.restoreEnabledStatus, true, "reenabled"); 363 364 let recoveredProfile = await bs.recoverFromBackupArchive( 365 backup.archivePath, 366 null, 367 false, 368 PathUtils.profileDir, 369 recoveryDir 370 ); 371 Assert.ok( 372 recoveredProfile, 373 "Recovery should succeed once restore is re-enabled." 374 ); 375 Assert.ok( 376 await IOUtils.exists(recoveredProfile.rootDir.path), 377 "Recovered profile directory should exist on disk." 378 ); 379 380 bs.uninitStatusObservers(); 381 382 // Also check that it works at startup. 383 spurious += (startup ?? 0) + ((await disable()) ?? 0); 384 bs = new BackupService(); 385 bs.initStatusObservers(); 386 Assert.equal(calledCount, 3 + spurious, "Observers were notified at startup"); 387 assertStatus("restore", bs.restoreEnabledStatus, false, internalReason); 388 await enable(); 389 bs.uninitStatusObservers(); 390 391 Services.obs.removeObserver(callback, "backup-service-status-updated"); 392 } 393 394 /** 395 * Checks that the status object matches the expected values, and that the 396 * telemetry matches the object. 397 * 398 * @param {string} kind 399 * The kind of status object. 400 * @param {string} status 401 * The status object given. 402 * @param {boolean} enabled 403 * Whether the feature should be enabled or disabled. 404 * @param {string?} internalReason 405 * The internal reason that should be given. 406 */ 407 function assertStatus(kind, status, enabled, internalReason) { 408 Assert.equal( 409 status.enabled, 410 enabled, 411 `${kind} status is ${enabled ? "" : "not "}enabled.` 412 ); 413 Assert.equal( 414 status.internalReason ?? null, 415 // 'reenabled' is only for telemetry, the status is null 416 internalReason == "reenabled" ? null : internalReason, 417 `${kind} status has the expected internal reason.` 418 ); 419 420 Assert.equal( 421 Glean.browserBackup[kind + "Enabled"].testGetValue(), 422 enabled, 423 `Glean ${kind}_enabled metric should be ${enabled}.` 424 ); 425 TelemetryTestUtils.assertScalar( 426 TelemetryTestUtils.getProcessScalars("parent", false, true), 427 `browser.backup.${kind}_enabled`, 428 enabled, 429 `Legacy ${kind}_enabled metric should be ${enabled}.` 430 ); 431 432 Assert.equal( 433 Glean.browserBackup[kind + "DisabledReason"].testGetValue(), 434 internalReason, 435 `Glean ${kind}_disabled_reason metric is ${internalReason}.` 436 ); 437 }