1from __future__ import absolute_import, print_function 2import os 3import time 4 5from marionette_harness import MarionetteTestCase 6from marionette_driver.errors import NoAlertPresentException 7 8 9# Holds info about things we need to cleanup after the tests are done. 10class PendingCleanup: 11 desktop_backup_path = None 12 reset_profile_path = None 13 reset_profile_local_path = None 14 15 def __init__(self, profile_name_to_remove): 16 self.profile_name_to_remove = profile_name_to_remove 17 18 19class TestFirefoxRefresh(MarionetteTestCase): 20 _sandbox = "firefox-refresh" 21 22 _username = "marionette-test-login" 23 _password = "marionette-test-password" 24 _bookmarkURL = "about:mozilla" 25 _bookmarkText = "Some bookmark from Marionette" 26 27 _cookieHost = "firefox-refresh.marionette-test.mozilla.org" 28 _cookiePath = "some/cookie/path" 29 _cookieName = "somecookie" 30 _cookieValue = "some cookie value" 31 32 _historyURL = "http://firefox-refresh.marionette-test.mozilla.org/" 33 _historyTitle = "Test visit for Firefox Reset" 34 35 _formHistoryFieldName = "some-very-unique-marionette-only-firefox-reset-field" 36 _formHistoryValue = "special-pumpkin-value" 37 38 _formAutofillAvailable = False 39 _formAutofillAddressGuid = None 40 41 _expectedURLs = ["about:robots", "about:mozilla"] 42 43 def savePassword(self): 44 self.runCode( 45 """ 46 let myLogin = new global.LoginInfo( 47 "test.marionette.mozilla.com", 48 "http://test.marionette.mozilla.com/some/form/", 49 null, 50 arguments[0], 51 arguments[1], 52 "username", 53 "password" 54 ); 55 Services.logins.addLogin(myLogin) 56 """, 57 script_args=(self._username, self._password), 58 ) 59 60 def createBookmarkInMenu(self): 61 error = self.runAsyncCode( 62 """ 63 // let url = arguments[0]; 64 // let title = arguments[1]; 65 // let resolve = arguments[arguments.length - 1]; 66 let [url, title, resolve] = arguments; 67 PlacesUtils.bookmarks.insert({ 68 parentGuid: PlacesUtils.bookmarks.menuGuid, url, title 69 }).then(() => resolve(false), resolve); 70 """, 71 script_args=(self._bookmarkURL, self._bookmarkText), 72 ) 73 if error: 74 print(error) 75 76 def createBookmarksOnToolbar(self): 77 error = self.runAsyncCode( 78 """ 79 let resolve = arguments[arguments.length - 1]; 80 let children = []; 81 for (let i = 1; i <= 5; i++) { 82 children.push({url: `about:rights?p=${i}`, title: `Bookmark ${i}`}); 83 } 84 PlacesUtils.bookmarks.insertTree({ 85 guid: PlacesUtils.bookmarks.toolbarGuid, 86 children 87 }).then(() => resolve(false), resolve); 88 """ 89 ) 90 if error: 91 print(error) 92 93 def createHistory(self): 94 error = self.runAsyncCode( 95 """ 96 let resolve = arguments[arguments.length - 1]; 97 PlacesUtils.history.insert({ 98 url: arguments[0], 99 title: arguments[1], 100 visits: [{ 101 date: new Date(Date.now() - 5000), 102 referrer: "about:mozilla" 103 }] 104 }).then(() => resolve(false), 105 ex => resolve("Unexpected error in adding visit: " + ex)); 106 """, 107 script_args=(self._historyURL, self._historyTitle), 108 ) 109 if error: 110 print(error) 111 112 def createFormHistory(self): 113 error = self.runAsyncCode( 114 """ 115 let updateDefinition = { 116 op: "add", 117 fieldname: arguments[0], 118 value: arguments[1], 119 firstUsed: (Date.now() - 5000) * 1000, 120 }; 121 let finished = false; 122 let resolve = arguments[arguments.length - 1]; 123 global.FormHistory.update(updateDefinition, { 124 handleError(error) { 125 finished = true; 126 resolve(error); 127 }, 128 handleCompletion() { 129 if (!finished) { 130 resolve(false); 131 } 132 } 133 }); 134 """, 135 script_args=(self._formHistoryFieldName, self._formHistoryValue), 136 ) 137 if error: 138 print(error) 139 140 def createFormAutofill(self): 141 if not self._formAutofillAvailable: 142 return 143 self._formAutofillAddressGuid = self.runAsyncCode( 144 """ 145 let resolve = arguments[arguments.length - 1]; 146 const TEST_ADDRESS_1 = { 147 "given-name": "John", 148 "additional-name": "R.", 149 "family-name": "Smith", 150 organization: "World Wide Web Consortium", 151 "street-address": "32 Vassar Street\\\nMIT Room 32-G524", 152 "address-level2": "Cambridge", 153 "address-level1": "MA", 154 "postal-code": "02139", 155 country: "US", 156 tel: "+15195555555", 157 email: "user@example.com", 158 }; 159 return global.formAutofillStorage.initialize().then(() => { 160 return global.formAutofillStorage.addresses.add(TEST_ADDRESS_1); 161 }).then(resolve); 162 """ 163 ) 164 165 def createCookie(self): 166 self.runCode( 167 """ 168 // Expire in 15 minutes: 169 let expireTime = Math.floor(Date.now() / 1000) + 15 * 60; 170 Services.cookies.add(arguments[0], arguments[1], arguments[2], arguments[3], 171 true, false, false, expireTime, {}, 172 Ci.nsICookie.SAMESITE_NONE, Ci.nsICookie.SCHEME_UNSET); 173 """, 174 script_args=( 175 self._cookieHost, 176 self._cookiePath, 177 self._cookieName, 178 self._cookieValue, 179 ), 180 ) 181 182 def createSession(self): 183 self.runAsyncCode( 184 """ 185 let resolve = arguments[arguments.length - 1]; 186 const COMPLETE_STATE = Ci.nsIWebProgressListener.STATE_STOP + 187 Ci.nsIWebProgressListener.STATE_IS_NETWORK; 188 let {TabStateFlusher} = Cu.import("resource:///modules/sessionstore/TabStateFlusher.jsm", {}); 189 let expectedURLs = Array.from(arguments[0]) 190 gBrowser.addTabsProgressListener({ 191 onStateChange(browser, webprogress, request, flags, status) { 192 try { 193 request && request.QueryInterface(Ci.nsIChannel); 194 } catch (ex) {} 195 let uriLoaded = request.originalURI && request.originalURI.spec; 196 if ((flags & COMPLETE_STATE == COMPLETE_STATE) && uriLoaded && 197 expectedURLs.includes(uriLoaded)) { 198 TabStateFlusher.flush(browser).then(function() { 199 expectedURLs.splice(expectedURLs.indexOf(uriLoaded), 1); 200 if (!expectedURLs.length) { 201 gBrowser.removeTabsProgressListener(this); 202 resolve(); 203 } 204 }); 205 } 206 } 207 }); 208 let expectedTabs = new Set(); 209 for (let url of expectedURLs) { 210 expectedTabs.add(gBrowser.addTab(url, { 211 triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), 212 })); 213 } 214 // Close any other tabs that might be open: 215 let allTabs = Array.from(gBrowser.tabs); 216 for (let tab of allTabs) { 217 if (!expectedTabs.has(tab)) { 218 gBrowser.removeTab(tab); 219 } 220 } 221 """, # NOQA: E501 222 script_args=(self._expectedURLs,), 223 ) 224 225 def createFxa(self): 226 # This script will write an entry to the login manager and create 227 # a signedInUser.json in the profile dir. 228 self.runAsyncCode( 229 """ 230 let resolve = arguments[arguments.length - 1]; 231 Cu.import("resource://gre/modules/FxAccountsStorage.jsm"); 232 let storage = new FxAccountsStorageManager(); 233 let data = {email: "test@test.com", uid: "uid", keyFetchToken: "top-secret"}; 234 storage.initialize(data); 235 storage.finalize().then(resolve); 236 """ 237 ) 238 239 def createSync(self): 240 # This script will write the canonical preference which indicates a user 241 # is signed into sync. 242 self.marionette.execute_script( 243 """ 244 Services.prefs.setStringPref("services.sync.username", "test@test.com"); 245 """ 246 ) 247 248 def checkPassword(self): 249 loginInfo = self.marionette.execute_script( 250 """ 251 let ary = Services.logins.findLogins( 252 "test.marionette.mozilla.com", 253 "http://test.marionette.mozilla.com/some/form/", 254 null, {}); 255 return ary.length ? ary : {username: "null", password: "null"}; 256 """ 257 ) 258 self.assertEqual(len(loginInfo), 1) 259 self.assertEqual(loginInfo[0]["username"], self._username) 260 self.assertEqual(loginInfo[0]["password"], self._password) 261 262 loginCount = self.marionette.execute_script( 263 """ 264 return Services.logins.getAllLogins().length; 265 """ 266 ) 267 # Note that we expect 2 logins - one from us, one from sync. 268 self.assertEqual(loginCount, 2, "No other logins are present") 269 270 def checkBookmarkInMenu(self): 271 titleInBookmarks = self.runAsyncCode( 272 """ 273 let [url, resolve] = arguments; 274 PlacesUtils.bookmarks.fetch({url}).then( 275 bookmark => resolve(bookmark ? bookmark.title : ""), 276 ex => resolve(ex) 277 ); 278 """, 279 script_args=(self._bookmarkURL,), 280 ) 281 self.assertEqual(titleInBookmarks, self._bookmarkText) 282 283 def checkBookmarkToolbarVisibility(self): 284 toolbarVisible = self.marionette.execute_script( 285 """ 286 const BROWSER_DOCURL = AppConstants.BROWSER_CHROME_URL; 287 return Services.xulStore.getValue(BROWSER_DOCURL, "PersonalToolbar", "collapsed"); 288 """ 289 ) 290 if toolbarVisible == "": 291 toolbarVisible = "false" 292 self.assertEqual(toolbarVisible, "false") 293 294 def checkHistory(self): 295 historyResult = self.runAsyncCode( 296 """ 297 let resolve = arguments[arguments.length - 1]; 298 PlacesUtils.history.fetch(arguments[0]).then(pageInfo => { 299 if (!pageInfo) { 300 resolve("No visits found"); 301 } else { 302 resolve(pageInfo); 303 } 304 }).catch(e => { 305 resolve("Unexpected error in fetching page: " + e); 306 }); 307 """, 308 script_args=(self._historyURL,), 309 ) 310 if type(historyResult) == str: 311 self.fail(historyResult) 312 return 313 314 self.assertEqual(historyResult["title"], self._historyTitle) 315 316 def checkFormHistory(self): 317 formFieldResults = self.runAsyncCode( 318 """ 319 let resolve = arguments[arguments.length - 1]; 320 let results = []; 321 global.FormHistory.search(["value"], {fieldname: arguments[0]}, { 322 handleError(error) { 323 results = error; 324 }, 325 handleResult(result) { 326 results.push(result); 327 }, 328 handleCompletion() { 329 resolve(results); 330 }, 331 }); 332 """, 333 script_args=(self._formHistoryFieldName,), 334 ) 335 if type(formFieldResults) == str: 336 self.fail(formFieldResults) 337 return 338 339 formFieldResultCount = len(formFieldResults) 340 self.assertEqual( 341 formFieldResultCount, 342 1, 343 "Should have exactly 1 entry for this field, got %d" % formFieldResultCount, 344 ) 345 if formFieldResultCount == 1: 346 self.assertEqual(formFieldResults[0]["value"], self._formHistoryValue) 347 348 formHistoryCount = self.runAsyncCode( 349 """ 350 let [resolve] = arguments; 351 let count; 352 let callbacks = { 353 handleResult: rv => count = rv, 354 handleCompletion() { 355 resolve(count); 356 }, 357 }; 358 global.FormHistory.count({}, callbacks); 359 """ 360 ) 361 self.assertEqual( 362 formHistoryCount, 1, "There should be only 1 entry in the form history" 363 ) 364 365 def checkFormAutofill(self): 366 if not self._formAutofillAvailable: 367 return 368 369 formAutofillResults = self.runAsyncCode( 370 """ 371 let resolve = arguments[arguments.length - 1]; 372 return global.formAutofillStorage.initialize().then(() => { 373 return global.formAutofillStorage.addresses.getAll() 374 }).then(resolve); 375 """, 376 ) 377 if type(formAutofillResults) == str: 378 self.fail(formAutofillResults) 379 return 380 381 formAutofillAddressCount = len(formAutofillResults) 382 self.assertEqual( 383 formAutofillAddressCount, 384 1, 385 "Should have exactly 1 saved address, got %d" % formAutofillAddressCount, 386 ) 387 if formAutofillAddressCount == 1: 388 self.assertEqual( 389 formAutofillResults[0]["guid"], self._formAutofillAddressGuid 390 ) 391 392 def checkCookie(self): 393 cookieInfo = self.runCode( 394 """ 395 try { 396 let cookies = Services.cookies.getCookiesFromHost(arguments[0], {}); 397 let cookie = null; 398 for (let hostCookie of cookies) { 399 // getCookiesFromHost returns any cookie from the BASE host. 400 if (hostCookie.rawHost != arguments[0]) 401 continue; 402 if (cookie != null) { 403 return "more than 1 cookie! That shouldn't happen!"; 404 } 405 cookie = hostCookie; 406 } 407 return {path: cookie.path, name: cookie.name, value: cookie.value}; 408 } catch (ex) { 409 return "got exception trying to fetch cookie: " + ex; 410 } 411 """, 412 script_args=(self._cookieHost,), 413 ) 414 if not isinstance(cookieInfo, dict): 415 self.fail(cookieInfo) 416 return 417 self.assertEqual(cookieInfo["path"], self._cookiePath) 418 self.assertEqual(cookieInfo["value"], self._cookieValue) 419 self.assertEqual(cookieInfo["name"], self._cookieName) 420 421 def checkSession(self): 422 tabURIs = self.runCode( 423 """ 424 return [... gBrowser.browsers].map(b => b.currentURI && b.currentURI.spec) 425 """ 426 ) 427 self.assertSequenceEqual(tabURIs, ["about:welcomeback"]) 428 429 # Dismiss modal dialog if any. This is mainly to dismiss the check for 430 # default browser dialog if it shows up. 431 try: 432 alert = self.marionette.switch_to_alert() 433 alert.dismiss() 434 except NoAlertPresentException: 435 pass 436 437 tabURIs = self.runAsyncCode( 438 """ 439 let resolve = arguments[arguments.length - 1] 440 let mm = gBrowser.selectedBrowser.messageManager; 441 442 let {TabStateFlusher} = Cu.import("resource:///modules/sessionstore/TabStateFlusher.jsm", {}); 443 window.addEventListener("SSWindowStateReady", function testSSPostReset() { 444 window.removeEventListener("SSWindowStateReady", testSSPostReset, false); 445 Promise.all(gBrowser.browsers.map(b => TabStateFlusher.flush(b))).then(function() { 446 resolve([... gBrowser.browsers].map(b => b.currentURI && b.currentURI.spec)); 447 }); 448 }, false); 449 450 let fs = function() { 451 if (content.document.readyState === "complete") { 452 content.document.getElementById("errorTryAgain").click(); 453 } else { 454 content.window.addEventListener("load", function(event) { 455 content.document.getElementById("errorTryAgain").click(); 456 }, { once: true }); 457 } 458 }; 459 460 mm.loadFrameScript("data:application/javascript,(" + fs.toString() + ")()", true); 461 """ # NOQA: E501 462 ) 463 self.assertSequenceEqual(tabURIs, self._expectedURLs) 464 465 def checkFxA(self): 466 result = self.runAsyncCode( 467 """ 468 Cu.import("resource://gre/modules/FxAccountsStorage.jsm"); 469 let resolve = arguments[arguments.length - 1]; 470 let storage = new FxAccountsStorageManager(); 471 let result = {}; 472 storage.initialize(); 473 storage.getAccountData().then(data => { 474 result.accountData = data; 475 return storage.finalize(); 476 }).then(() => { 477 resolve(result); 478 }).catch(err => { 479 resolve(err.toString()); 480 }); 481 """ 482 ) 483 if type(result) != dict: 484 self.fail(result) 485 return 486 self.assertEqual(result["accountData"]["email"], "test@test.com") 487 self.assertEqual(result["accountData"]["uid"], "uid") 488 self.assertEqual(result["accountData"]["keyFetchToken"], "top-secret") 489 490 def checkSync(self, expect_sync_user): 491 pref_value = self.marionette.execute_script( 492 """ 493 return Services.prefs.getStringPref("services.sync.username", null); 494 """ 495 ) 496 expected_value = "test@test.com" if expect_sync_user else None 497 self.assertEqual(pref_value, expected_value) 498 499 def checkProfile(self, has_migrated=False, expect_sync_user=True): 500 self.checkPassword() 501 self.checkBookmarkInMenu() 502 self.checkHistory() 503 self.checkFormHistory() 504 self.checkFormAutofill() 505 self.checkCookie() 506 self.checkFxA() 507 self.checkSync(expect_sync_user) 508 if has_migrated: 509 self.checkBookmarkToolbarVisibility() 510 self.checkSession() 511 512 def createProfileData(self): 513 self.savePassword() 514 self.createBookmarkInMenu() 515 self.createBookmarksOnToolbar() 516 self.createHistory() 517 self.createFormHistory() 518 self.createFormAutofill() 519 self.createCookie() 520 self.createSession() 521 self.createFxa() 522 self.createSync() 523 524 def setUpScriptData(self): 525 self.marionette.set_context(self.marionette.CONTEXT_CHROME) 526 self.runCode( 527 """ 528 window.global = {}; 529 global.LoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1", "nsILoginInfo", "init"); 530 global.profSvc = Cc["@mozilla.org/toolkit/profile-service;1"].getService(Ci.nsIToolkitProfileService); 531 global.Preferences = Cu.import("resource://gre/modules/Preferences.jsm", {}).Preferences; 532 global.FormHistory = Cu.import("resource://gre/modules/FormHistory.jsm", {}).FormHistory; 533 """ # NOQA: E501 534 ) 535 self._formAutofillAvailable = self.runCode( 536 """ 537 try { 538 global.formAutofillStorage = Cu.import("resource://formautofill/FormAutofillStorage.jsm", {}).formAutofillStorage; 539 } catch(e) { 540 return false; 541 } 542 return true; 543 """ # NOQA: E501 544 ) 545 546 def runCode(self, script, *args, **kwargs): 547 return self.marionette.execute_script( 548 script, new_sandbox=False, sandbox=self._sandbox, *args, **kwargs 549 ) 550 551 def runAsyncCode(self, script, *args, **kwargs): 552 return self.marionette.execute_async_script( 553 script, new_sandbox=False, sandbox=self._sandbox, *args, **kwargs 554 ) 555 556 def setUp(self): 557 MarionetteTestCase.setUp(self) 558 self.setUpScriptData() 559 560 self.cleanups = [] 561 562 def tearDown(self): 563 # Force yet another restart with a clean profile to disconnect from the 564 # profile and environment changes we've made, to leave a more or less 565 # blank slate for the next person. 566 self.marionette.restart(clean=True, in_app=False) 567 self.setUpScriptData() 568 569 # Super 570 MarionetteTestCase.tearDown(self) 571 572 # A helper to deal with removing a load of files 573 import mozfile 574 575 for cleanup in self.cleanups: 576 if cleanup.desktop_backup_path: 577 mozfile.remove(cleanup.desktop_backup_path) 578 579 if cleanup.reset_profile_path: 580 # Remove ourselves from profiles.ini 581 self.runCode( 582 """ 583 let name = arguments[0]; 584 let profile = global.profSvc.getProfileByName(name); 585 profile.remove(false) 586 global.profSvc.flush(); 587 """, 588 script_args=(cleanup.profile_name_to_remove,), 589 ) 590 # Remove the local profile dir if it's not the same as the profile dir: 591 different_path = ( 592 cleanup.reset_profile_local_path != cleanup.reset_profile_path 593 ) 594 if cleanup.reset_profile_local_path and different_path: 595 mozfile.remove(cleanup.reset_profile_local_path) 596 597 # And delete all the files. 598 mozfile.remove(cleanup.reset_profile_path) 599 600 def doReset(self): 601 profileName = "marionette-test-profile-" + str(int(time.time() * 1000)) 602 cleanup = PendingCleanup(profileName) 603 self.runCode( 604 """ 605 // Ensure the current (temporary) profile is in profiles.ini: 606 let profD = Services.dirsvc.get("ProfD", Ci.nsIFile); 607 let profileName = arguments[1]; 608 let myProfile = global.profSvc.createProfile(profD, profileName); 609 global.profSvc.flush() 610 611 // Now add the reset parameters: 612 let env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment); 613 let prefsToKeep = Array.from(Services.prefs.getChildList("marionette.")); 614 prefsToKeep.push("datareporting.policy.dataSubmissionPolicyBypassNotification"); 615 let prefObj = {}; 616 for (let pref of prefsToKeep) { 617 prefObj[pref] = global.Preferences.get(pref); 618 } 619 env.set("MOZ_MARIONETTE_PREF_STATE_ACROSS_RESTARTS", JSON.stringify(prefObj)); 620 env.set("MOZ_RESET_PROFILE_RESTART", "1"); 621 env.set("XRE_PROFILE_PATH", arguments[0]); 622 """, 623 script_args=( 624 self.marionette.instance.profile.profile, 625 profileName, 626 ), 627 ) 628 629 profileLeafName = os.path.basename( 630 os.path.normpath(self.marionette.instance.profile.profile) 631 ) 632 633 # Now restart the browser to get it reset: 634 self.marionette.restart(clean=False, in_app=True) 635 self.setUpScriptData() 636 637 # Determine the new profile path (we'll need to remove it when we're done) 638 [cleanup.reset_profile_path, cleanup.reset_profile_local_path] = self.runCode( 639 """ 640 let profD = Services.dirsvc.get("ProfD", Ci.nsIFile); 641 let localD = Services.dirsvc.get("ProfLD", Ci.nsIFile); 642 return [profD.path, localD.path]; 643 """ 644 ) 645 646 # Determine the backup path 647 cleanup.desktop_backup_path = self.runCode( 648 """ 649 let container; 650 try { 651 container = Services.dirsvc.get("Desk", Ci.nsIFile); 652 } catch (ex) { 653 container = Services.dirsvc.get("Home", Ci.nsIFile); 654 } 655 let bundle = Services.strings.createBundle("chrome://mozapps/locale/profile/profileSelection.properties"); 656 let dirName = bundle.formatStringFromName("resetBackupDirectory", [Services.appinfo.name]); 657 container.append(dirName); 658 container.append(arguments[0]); 659 return container.path; 660 """, # NOQA: E501 661 script_args=(profileLeafName,), 662 ) 663 664 self.assertTrue( 665 os.path.isdir(cleanup.reset_profile_path), 666 "Reset profile path should be present", 667 ) 668 self.assertTrue( 669 os.path.isdir(cleanup.desktop_backup_path), 670 "Backup profile path should be present", 671 ) 672 self.assertIn(cleanup.profile_name_to_remove, cleanup.reset_profile_path) 673 return cleanup 674 675 def testResetEverything(self): 676 self.createProfileData() 677 678 self.checkProfile(expect_sync_user=True) 679 680 this_cleanup = self.doReset() 681 self.cleanups.append(this_cleanup) 682 683 # Now check that we're doing OK... 684 self.checkProfile(has_migrated=True, expect_sync_user=True) 685 686 def testFxANoSync(self): 687 # This test doesn't need to repeat all the non-sync tests... 688 # Setup FxA but *not* sync 689 self.createFxa() 690 691 self.checkFxA() 692 self.checkSync(False) 693 694 this_cleanup = self.doReset() 695 self.cleanups.append(this_cleanup) 696 697 self.checkFxA() 698 self.checkSync(False) 699