From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- toolkit/profile/ProfileUnlockerAndroid.cpp | 37 + toolkit/profile/ProfileUnlockerAndroid.h | 26 + toolkit/profile/ProfileUnlockerWin.cpp | 245 +++ toolkit/profile/ProfileUnlockerWin.h | 56 + toolkit/profile/content/createProfileWizard.js | 266 +++ toolkit/profile/content/createProfileWizard.xhtml | 90 + toolkit/profile/content/profileDowngrade.js | 39 + toolkit/profile/content/profileDowngrade.xhtml | 42 + toolkit/profile/content/profileSelection.js | 355 ++++ toolkit/profile/content/profileSelection.xhtml | 104 + toolkit/profile/gtest/TestProfileLock.cpp | 35 + toolkit/profile/gtest/TestProfileLockRetry.cpp | 74 + toolkit/profile/gtest/moz.build | 20 + toolkit/profile/jar.mn | 13 + toolkit/profile/moz.build | 49 + toolkit/profile/notifications.txt | 52 + toolkit/profile/nsIProfileMigrator.idl | 69 + toolkit/profile/nsIProfileUnlocker.idl | 21 + toolkit/profile/nsIToolkitProfile.idl | 99 + toolkit/profile/nsIToolkitProfileService.idl | 147 ++ toolkit/profile/nsProfileLock.cpp | 578 +++++ toolkit/profile/nsProfileLock.h | 106 + toolkit/profile/nsToolkitProfileService.cpp | 2240 ++++++++++++++++++++ toolkit/profile/nsToolkitProfileService.h | 180 ++ toolkit/profile/test/chrome.toml | 3 + toolkit/profile/test/test_create_profile.xhtml | 126 ++ toolkit/profile/xpcshell/head.js | 623 ++++++ toolkit/profile/xpcshell/test_check_backup.js | 56 + toolkit/profile/xpcshell/test_claim_locked.js | 68 + toolkit/profile/xpcshell/test_clean.js | 165 ++ toolkit/profile/xpcshell/test_conflict_installs.js | 40 + toolkit/profile/xpcshell/test_conflict_profiles.js | 57 + toolkit/profile/xpcshell/test_create_default.js | 52 + .../profile/xpcshell/test_fix_directory_case.js | 113 + .../xpcshell/test_ignore_legacy_directory.js | 129 ++ .../profile/xpcshell/test_invalid_descriptor.js | 55 + toolkit/profile/xpcshell/test_legacy_empty.js | 28 + toolkit/profile/xpcshell/test_legacy_select.js | 67 + toolkit/profile/xpcshell/test_lock.js | 72 + .../profile/xpcshell/test_missing_profilesini.js | 67 + toolkit/profile/xpcshell/test_new_default.js | 129 ++ .../profile/xpcshell/test_previous_dedicated.js | 62 + toolkit/profile/xpcshell/test_profile_reset.js | 59 + toolkit/profile/xpcshell/test_remove.js | 103 + toolkit/profile/xpcshell/test_remove_default.js | 79 + .../test_select_backgroundtasks_ephemeral.js | 30 + ..._select_backgroundtasks_not_ephemeral_create.js | 78 + ..._select_backgroundtasks_not_ephemeral_exists.js | 66 + toolkit/profile/xpcshell/test_select_default.js | 71 + .../profile/xpcshell/test_select_environment.js | 45 + .../xpcshell/test_select_environment_named.js | 61 + toolkit/profile/xpcshell/test_select_missing.js | 34 + toolkit/profile/xpcshell/test_select_named.js | 43 + toolkit/profile/xpcshell/test_select_noname.js | 35 + .../xpcshell/test_select_profile_argument.js | 50 + .../xpcshell/test_select_profile_argument_new.js | 43 + .../profile/xpcshell/test_select_profilemanager.js | 34 + .../xpcshell/test_single_profile_selected.js | 73 + .../xpcshell/test_single_profile_unselected.js | 74 + .../xpcshell/test_skip_locked_environment.js | 131 ++ toolkit/profile/xpcshell/test_snap.js | 65 + toolkit/profile/xpcshell/test_snap_empty.js | 26 + .../profile/xpcshell/test_snatch_environment.js | 101 + .../xpcshell/test_snatch_environment_default.js | 101 + toolkit/profile/xpcshell/test_startswithlast.js | 28 + toolkit/profile/xpcshell/test_steal_inuse.js | 77 + .../xpcshell/test_update_selected_dedicated.js | 80 + .../xpcshell/test_update_unknown_dedicated.js | 85 + .../xpcshell/test_update_unselected_dedicated.js | 89 + toolkit/profile/xpcshell/test_use_dedicated.js | 107 + toolkit/profile/xpcshell/xpcshell.toml | 97 + 71 files changed, 8820 insertions(+) create mode 100644 toolkit/profile/ProfileUnlockerAndroid.cpp create mode 100644 toolkit/profile/ProfileUnlockerAndroid.h create mode 100644 toolkit/profile/ProfileUnlockerWin.cpp create mode 100644 toolkit/profile/ProfileUnlockerWin.h create mode 100644 toolkit/profile/content/createProfileWizard.js create mode 100644 toolkit/profile/content/createProfileWizard.xhtml create mode 100644 toolkit/profile/content/profileDowngrade.js create mode 100644 toolkit/profile/content/profileDowngrade.xhtml create mode 100644 toolkit/profile/content/profileSelection.js create mode 100644 toolkit/profile/content/profileSelection.xhtml create mode 100644 toolkit/profile/gtest/TestProfileLock.cpp create mode 100644 toolkit/profile/gtest/TestProfileLockRetry.cpp create mode 100644 toolkit/profile/gtest/moz.build create mode 100644 toolkit/profile/jar.mn create mode 100644 toolkit/profile/moz.build create mode 100644 toolkit/profile/notifications.txt create mode 100644 toolkit/profile/nsIProfileMigrator.idl create mode 100644 toolkit/profile/nsIProfileUnlocker.idl create mode 100644 toolkit/profile/nsIToolkitProfile.idl create mode 100644 toolkit/profile/nsIToolkitProfileService.idl create mode 100644 toolkit/profile/nsProfileLock.cpp create mode 100644 toolkit/profile/nsProfileLock.h create mode 100644 toolkit/profile/nsToolkitProfileService.cpp create mode 100644 toolkit/profile/nsToolkitProfileService.h create mode 100644 toolkit/profile/test/chrome.toml create mode 100644 toolkit/profile/test/test_create_profile.xhtml create mode 100644 toolkit/profile/xpcshell/head.js create mode 100644 toolkit/profile/xpcshell/test_check_backup.js create mode 100644 toolkit/profile/xpcshell/test_claim_locked.js create mode 100644 toolkit/profile/xpcshell/test_clean.js create mode 100644 toolkit/profile/xpcshell/test_conflict_installs.js create mode 100644 toolkit/profile/xpcshell/test_conflict_profiles.js create mode 100644 toolkit/profile/xpcshell/test_create_default.js create mode 100644 toolkit/profile/xpcshell/test_fix_directory_case.js create mode 100644 toolkit/profile/xpcshell/test_ignore_legacy_directory.js create mode 100644 toolkit/profile/xpcshell/test_invalid_descriptor.js create mode 100644 toolkit/profile/xpcshell/test_legacy_empty.js create mode 100644 toolkit/profile/xpcshell/test_legacy_select.js create mode 100644 toolkit/profile/xpcshell/test_lock.js create mode 100644 toolkit/profile/xpcshell/test_missing_profilesini.js create mode 100644 toolkit/profile/xpcshell/test_new_default.js create mode 100644 toolkit/profile/xpcshell/test_previous_dedicated.js create mode 100644 toolkit/profile/xpcshell/test_profile_reset.js create mode 100644 toolkit/profile/xpcshell/test_remove.js create mode 100644 toolkit/profile/xpcshell/test_remove_default.js create mode 100644 toolkit/profile/xpcshell/test_select_backgroundtasks_ephemeral.js create mode 100644 toolkit/profile/xpcshell/test_select_backgroundtasks_not_ephemeral_create.js create mode 100644 toolkit/profile/xpcshell/test_select_backgroundtasks_not_ephemeral_exists.js create mode 100644 toolkit/profile/xpcshell/test_select_default.js create mode 100644 toolkit/profile/xpcshell/test_select_environment.js create mode 100644 toolkit/profile/xpcshell/test_select_environment_named.js create mode 100644 toolkit/profile/xpcshell/test_select_missing.js create mode 100644 toolkit/profile/xpcshell/test_select_named.js create mode 100644 toolkit/profile/xpcshell/test_select_noname.js create mode 100644 toolkit/profile/xpcshell/test_select_profile_argument.js create mode 100644 toolkit/profile/xpcshell/test_select_profile_argument_new.js create mode 100644 toolkit/profile/xpcshell/test_select_profilemanager.js create mode 100644 toolkit/profile/xpcshell/test_single_profile_selected.js create mode 100644 toolkit/profile/xpcshell/test_single_profile_unselected.js create mode 100644 toolkit/profile/xpcshell/test_skip_locked_environment.js create mode 100644 toolkit/profile/xpcshell/test_snap.js create mode 100644 toolkit/profile/xpcshell/test_snap_empty.js create mode 100644 toolkit/profile/xpcshell/test_snatch_environment.js create mode 100644 toolkit/profile/xpcshell/test_snatch_environment_default.js create mode 100644 toolkit/profile/xpcshell/test_startswithlast.js create mode 100644 toolkit/profile/xpcshell/test_steal_inuse.js create mode 100644 toolkit/profile/xpcshell/test_update_selected_dedicated.js create mode 100644 toolkit/profile/xpcshell/test_update_unknown_dedicated.js create mode 100644 toolkit/profile/xpcshell/test_update_unselected_dedicated.js create mode 100644 toolkit/profile/xpcshell/test_use_dedicated.js create mode 100644 toolkit/profile/xpcshell/xpcshell.toml (limited to 'toolkit/profile') diff --git a/toolkit/profile/ProfileUnlockerAndroid.cpp b/toolkit/profile/ProfileUnlockerAndroid.cpp new file mode 100644 index 0000000000..58836c7b8e --- /dev/null +++ b/toolkit/profile/ProfileUnlockerAndroid.cpp @@ -0,0 +1,37 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ProfileUnlockerAndroid.h" +#include "nsPrintfCString.h" +#include + +namespace mozilla { + +ProfileUnlockerAndroid::ProfileUnlockerAndroid(const pid_t aPid) : mPid(aPid) {} + +ProfileUnlockerAndroid::~ProfileUnlockerAndroid() {} + +NS_IMPL_ISUPPORTS(ProfileUnlockerAndroid, nsIProfileUnlocker) + +NS_IMETHODIMP +ProfileUnlockerAndroid::Unlock(uint32_t aSeverity) { + if (aSeverity != FORCE_QUIT) { + return NS_ERROR_NOT_IMPLEMENTED; + } + + NS_WARNING(nsPrintfCString("Process %d has the profile " + "lock, will try to kill it.", + mPid) + .get()); + if (mPid == getpid()) { + NS_ERROR("Lock held by current process !?"); + return NS_ERROR_FAILURE; + } + if (kill(mPid, SIGKILL) != 0) { + NS_WARNING("Could not kill process."); + } + return NS_OK; +} + +} // namespace mozilla diff --git a/toolkit/profile/ProfileUnlockerAndroid.h b/toolkit/profile/ProfileUnlockerAndroid.h new file mode 100644 index 0000000000..a8dc33bcb5 --- /dev/null +++ b/toolkit/profile/ProfileUnlockerAndroid.h @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef ProfileUnlockerAndroid_h +#define ProfileUnlockerAndroid_h + +#include "nsIProfileUnlocker.h" + +namespace mozilla { + +class ProfileUnlockerAndroid final : public nsIProfileUnlocker { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPROFILEUNLOCKER + + explicit ProfileUnlockerAndroid(const pid_t aPid); + + private: + ~ProfileUnlockerAndroid(); + pid_t mPid; +}; + +} // namespace mozilla + +#endif // ProfileUnlockerAndroid_h diff --git a/toolkit/profile/ProfileUnlockerWin.cpp b/toolkit/profile/ProfileUnlockerWin.cpp new file mode 100644 index 0000000000..d7ab5e0a30 --- /dev/null +++ b/toolkit/profile/ProfileUnlockerWin.cpp @@ -0,0 +1,245 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ProfileUnlockerWin.h" +#include "nsCOMPtr.h" +#include "nsIFile.h" +#include "nsTArray.h" +#include "nsXPCOM.h" + +namespace mozilla { + +/** + * RAII class to obtain and manage a handle to a Restart Manager session. + * It opens a new handle upon construction and releases it upon destruction. + */ +class MOZ_STACK_CLASS ScopedRestartManagerSession { + public: + explicit ScopedRestartManagerSession(ProfileUnlockerWin& aUnlocker) + : mError(ERROR_INVALID_HANDLE), + mHandle((DWORD)-1) // 0 is a valid restart manager handle + , + mUnlocker(aUnlocker) { + mError = mUnlocker.StartSession(mHandle); + } + + ~ScopedRestartManagerSession() { + if (mError == ERROR_SUCCESS) { + mUnlocker.EndSession(mHandle); + } + } + + /** + * @return true if the handle is a valid Restart Ranager handle. + */ + inline bool ok() { return mError == ERROR_SUCCESS; } + + /** + * @return the Restart Manager handle to pass to other Restart Manager APIs. + */ + inline DWORD handle() { return mHandle; } + + private: + DWORD mError; + DWORD mHandle; + ProfileUnlockerWin& mUnlocker; +}; + +ProfileUnlockerWin::ProfileUnlockerWin(const nsAString& aFileName) + : mRmStartSession(nullptr), + mRmRegisterResources(nullptr), + mRmGetList(nullptr), + mRmEndSession(nullptr), + mFileName(aFileName) {} + +ProfileUnlockerWin::~ProfileUnlockerWin() {} + +NS_IMPL_ISUPPORTS(ProfileUnlockerWin, nsIProfileUnlocker) + +nsresult ProfileUnlockerWin::Init() { + MOZ_ASSERT(!mRestartMgrModule); + if (mFileName.IsEmpty()) { + return NS_ERROR_ILLEGAL_VALUE; + } + + nsModuleHandle module(::LoadLibraryW(L"Rstrtmgr.dll")); + if (!module) { + return NS_ERROR_NOT_AVAILABLE; + } + mRmStartSession = reinterpret_cast( + ::GetProcAddress(module, "RmStartSession")); + if (!mRmStartSession) { + return NS_ERROR_UNEXPECTED; + } + mRmRegisterResources = reinterpret_cast( + ::GetProcAddress(module, "RmRegisterResources")); + if (!mRmRegisterResources) { + return NS_ERROR_UNEXPECTED; + } + mRmGetList = + reinterpret_cast(::GetProcAddress(module, "RmGetList")); + if (!mRmGetList) { + return NS_ERROR_UNEXPECTED; + } + mRmEndSession = + reinterpret_cast(::GetProcAddress(module, "RmEndSession")); + if (!mRmEndSession) { + return NS_ERROR_UNEXPECTED; + } + + mRestartMgrModule.steal(module); + return NS_OK; +} + +DWORD +ProfileUnlockerWin::StartSession(DWORD& aHandle) { + WCHAR sessionKey[CCH_RM_SESSION_KEY + 1] = {0}; + return mRmStartSession(&aHandle, 0, sessionKey); +} + +void ProfileUnlockerWin::EndSession(DWORD aHandle) { mRmEndSession(aHandle); } + +NS_IMETHODIMP +ProfileUnlockerWin::Unlock(uint32_t aSeverity) { + if (!mRestartMgrModule) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (aSeverity != FORCE_QUIT) { + return NS_ERROR_NOT_IMPLEMENTED; + } + + ScopedRestartManagerSession session(*this); + if (!session.ok()) { + return NS_ERROR_FAILURE; + } + + LPCWSTR resources[] = {mFileName.get()}; + DWORD error = mRmRegisterResources(session.handle(), 1, resources, 0, nullptr, + 0, nullptr); + if (error != ERROR_SUCCESS) { + return NS_ERROR_FAILURE; + } + + // Using a AutoTArray here because we expect the required size to be 1. + AutoTArray info; + UINT numEntries; + UINT numEntriesNeeded = 1; + error = ERROR_MORE_DATA; + DWORD reason = RmRebootReasonNone; + while (error == ERROR_MORE_DATA) { + info.SetLength(numEntriesNeeded); + numEntries = numEntriesNeeded; + error = mRmGetList(session.handle(), &numEntriesNeeded, &numEntries, + &info[0], &reason); + } + if (error != ERROR_SUCCESS) { + return NS_ERROR_FAILURE; + } + if (numEntries == 0) { + // Nobody else is locking the file; the other process must have terminated + return NS_OK; + } + + nsresult rv = NS_ERROR_FAILURE; + for (UINT i = 0; i < numEntries; ++i) { + rv = TryToTerminate(info[i].Process); + if (NS_SUCCEEDED(rv)) { + return NS_OK; + } + } + + // If nothing could be unlocked then we return the error code of the last + // failure that was returned. + return rv; +} + +nsresult ProfileUnlockerWin::TryToTerminate(RM_UNIQUE_PROCESS& aProcess) { + // Subtle: If the target process terminated before this call to OpenProcess, + // this call will still succeed. This is because the restart manager session + // internally retains a handle to the target process. The rules for Windows + // PIDs state that the PID of a terminated process remains valid as long as + // at least one handle to that process remains open, so when we reach this + // point the PID is still valid and the process will open successfully. + DWORD accessRights = PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_TERMINATE; + nsAutoHandle otherProcess( + ::OpenProcess(accessRights, FALSE, aProcess.dwProcessId)); + if (!otherProcess) { + return NS_ERROR_FAILURE; + } + + FILETIME creationTime, exitTime, kernelTime, userTime; + if (!::GetProcessTimes(otherProcess, &creationTime, &exitTime, &kernelTime, + &userTime)) { + return NS_ERROR_FAILURE; + } + if (::CompareFileTime(&aProcess.ProcessStartTime, &creationTime)) { + return NS_ERROR_NOT_AVAILABLE; + } + + WCHAR imageName[MAX_PATH]; + DWORD imageNameLen = MAX_PATH; + if (!::QueryFullProcessImageNameW(otherProcess, 0, imageName, + &imageNameLen)) { + // The error codes for this function are not very descriptive. There are + // actually two failure cases here: Either the call failed because the + // process is no longer running, or it failed for some other reason. We + // need to know which case that is. + DWORD otherProcessExitCode; + if (!::GetExitCodeProcess(otherProcess, &otherProcessExitCode) || + otherProcessExitCode == STILL_ACTIVE) { + // The other process is still running. + return NS_ERROR_FAILURE; + } + // The other process must have terminated. We should return NS_OK so that + // this process may proceed with startup. + return NS_OK; + } + nsCOMPtr otherProcessImageName; + if (NS_FAILED(NS_NewLocalFile(nsDependentString(imageName, imageNameLen), + false, + getter_AddRefs(otherProcessImageName)))) { + return NS_ERROR_FAILURE; + } + nsAutoString otherProcessLeafName; + if (NS_FAILED(otherProcessImageName->GetLeafName(otherProcessLeafName))) { + return NS_ERROR_FAILURE; + } + + imageNameLen = MAX_PATH; + if (!::QueryFullProcessImageNameW(::GetCurrentProcess(), 0, imageName, + &imageNameLen)) { + return NS_ERROR_FAILURE; + } + nsCOMPtr thisProcessImageName; + if (NS_FAILED(NS_NewLocalFile(nsDependentString(imageName, imageNameLen), + false, getter_AddRefs(thisProcessImageName)))) { + return NS_ERROR_FAILURE; + } + nsAutoString thisProcessLeafName; + if (NS_FAILED(thisProcessImageName->GetLeafName(thisProcessLeafName))) { + return NS_ERROR_FAILURE; + } + + // Make sure the image leaf names match + if (_wcsicmp(otherProcessLeafName.get(), thisProcessLeafName.get())) { + return NS_ERROR_NOT_AVAILABLE; + } + + // We know that another process holds the lock and that it shares the same + // image name as our process. Let's kill it. + // Subtle: TerminateProcess returning ERROR_ACCESS_DENIED is actually an + // indicator that the target process managed to shut down on its own. In that + // case we should return NS_OK since we may proceed with startup. + if (!::TerminateProcess(otherProcess, 1) && + ::GetLastError() != ERROR_ACCESS_DENIED) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +} // namespace mozilla diff --git a/toolkit/profile/ProfileUnlockerWin.h b/toolkit/profile/ProfileUnlockerWin.h new file mode 100644 index 0000000000..6429301394 --- /dev/null +++ b/toolkit/profile/ProfileUnlockerWin.h @@ -0,0 +1,56 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef ProfileUnlockerWin_h +#define ProfileUnlockerWin_h + +#include +#include + +#include "nsIProfileUnlocker.h" +#include "nsString.h" +#include "nsWindowsHelpers.h" + +namespace mozilla { + +class ProfileUnlockerWin final : public nsIProfileUnlocker { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPROFILEUNLOCKER + + explicit ProfileUnlockerWin(const nsAString& aFileName); + + nsresult Init(); + + DWORD StartSession(DWORD& aHandle); + void EndSession(DWORD aHandle); + + private: + ~ProfileUnlockerWin(); + nsresult TryToTerminate(RM_UNIQUE_PROCESS& aProcess); + + private: + typedef DWORD(WINAPI* RMSTARTSESSION)(DWORD*, DWORD, WCHAR[]); + typedef DWORD(WINAPI* RMREGISTERRESOURCES)(DWORD, UINT, LPCWSTR[], UINT, + RM_UNIQUE_PROCESS[], UINT, + LPCWSTR[]); + typedef DWORD(WINAPI* RMGETLIST)(DWORD, UINT*, UINT*, RM_PROCESS_INFO[], + LPDWORD); + typedef DWORD(WINAPI* RMENDSESSION)(DWORD); + + private: + nsModuleHandle mRestartMgrModule; + RMSTARTSESSION mRmStartSession; + RMREGISTERRESOURCES mRmRegisterResources; + RMGETLIST mRmGetList; + RMENDSESSION mRmEndSession; + + nsString mFileName; +}; + +} // namespace mozilla + +#endif // ProfileUnlockerWin_h diff --git a/toolkit/profile/content/createProfileWizard.js b/toolkit/profile/content/createProfileWizard.js new file mode 100644 index 0000000000..9e87fb4220 --- /dev/null +++ b/toolkit/profile/content/createProfileWizard.js @@ -0,0 +1,266 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const C = Cc; +const I = Ci; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +const ToolkitProfileService = "@mozilla.org/toolkit/profile-service;1"; + +var gProfileService; +var gProfileManagerBundle; + +var gDefaultProfileParent; + +// The directory where the profile will be created. +var gProfileRoot; + +// Text node to display the location and name of the profile to create. +var gProfileDisplay; + +// Called once when the wizard is opened. +function initWizard() { + try { + gProfileService = C[ToolkitProfileService].getService( + I.nsIToolkitProfileService + ); + gProfileManagerBundle = document.getElementById("bundle_profileManager"); + + gDefaultProfileParent = Services.dirsvc.get("DefProfRt", I.nsIFile); + + // Initialize the profile location display. + gProfileDisplay = document.getElementById("profileDisplay").firstChild; + document.addEventListener("wizardfinish", onFinish); + document + .getElementById("explanation") + .addEventListener("pageshow", enableNextButton); + document + .getElementById("createProfile") + .addEventListener("pageshow", initSecondWizardPage); + setDisplayToDefaultFolder(); + } catch (e) { + window.close(); + throw e; + } +} + +// Called every time the second wizard page is displayed. +function initSecondWizardPage() { + var profileName = document.getElementById("profileName"); + profileName.select(); + profileName.focus(); + + // Initialize profile name validation. + checkCurrentInput(profileName.value); +} + +const kSaltTable = [ + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "0", +]; + +var kSaltString = ""; +for (var i = 0; i < 8; ++i) { + kSaltString += kSaltTable[Math.floor(Math.random() * kSaltTable.length)]; +} + +function saltName(aName) { + return kSaltString + "." + aName; +} + +function setDisplayToDefaultFolder() { + var defaultProfileDir = gDefaultProfileParent.clone(); + defaultProfileDir.append( + saltName(document.getElementById("profileName").value) + ); + gProfileRoot = defaultProfileDir; + document.getElementById("useDefault").disabled = true; +} + +function updateProfileDisplay() { + gProfileDisplay.data = gProfileRoot.path; +} + +// Invoke a folder selection dialog for choosing the directory of profile storage. +function chooseProfileFolder() { + var newProfileRoot; + + var dirChooser = C["@mozilla.org/filepicker;1"].createInstance( + I.nsIFilePicker + ); + dirChooser.init( + window, + gProfileManagerBundle.getString("chooseFolder"), + I.nsIFilePicker.modeGetFolder + ); + dirChooser.appendFilters(I.nsIFilePicker.filterAll); + + // default to the Profiles folder + dirChooser.displayDirectory = gDefaultProfileParent; + + dirChooser.open(() => { + newProfileRoot = dirChooser.file; + + // Disable the "Default Folder..." button when the default profile folder + // was selected manually in the File Picker. + document.getElementById("useDefault").disabled = + newProfileRoot.parent.equals(gDefaultProfileParent); + + gProfileRoot = newProfileRoot; + updateProfileDisplay(); + }); +} + +// Checks the current user input for validity and triggers an error message accordingly. +function checkCurrentInput(currentInput) { + let wizard = document.querySelector("wizard"); + var finishButton = wizard.getButton("finish"); + var finishText = document.getElementById("finishText"); + var canAdvance; + + var errorMessage = checkProfileName(currentInput); + + if (!errorMessage) { + finishText.className = ""; + if (AppConstants.platform == "macosx") { + finishText.firstChild.data = gProfileManagerBundle.getString( + "profileFinishTextMac" + ); + } else { + finishText.firstChild.data = + gProfileManagerBundle.getString("profileFinishText"); + } + canAdvance = true; + } else { + finishText.className = "error"; + finishText.firstChild.data = errorMessage; + canAdvance = false; + } + + wizard.canAdvance = canAdvance; + finishButton.disabled = !canAdvance; + + updateProfileDisplay(); + + return canAdvance; +} + +function updateProfileName(aNewName) { + if (checkCurrentInput(aNewName)) { + gProfileRoot.leafName = saltName(aNewName); + updateProfileDisplay(); + } +} + +// Checks whether the given string is a valid profile name. +// Returns an error message describing the error in the name or "" when it's valid. +function checkProfileName(profileNameToCheck) { + // Check for emtpy profile name. + if (!/\S/.test(profileNameToCheck)) { + return gProfileManagerBundle.getString("profileNameEmpty"); + } + + // Check whether all characters in the profile name are allowed. + if (/([\\*:?<>|\/\"])/.test(profileNameToCheck)) { + return gProfileManagerBundle.getFormattedString("invalidChar", [RegExp.$1]); + } + + // Check whether a profile with the same name already exists. + if (profileExists(profileNameToCheck)) { + return gProfileManagerBundle.getString("profileExists"); + } + + // profileNameToCheck is valid. + return ""; +} + +function profileExists(aName) { + for (let profile of gProfileService.profiles) { + if (profile.name.toLowerCase() == aName.toLowerCase()) { + return true; + } + } + + return false; +} + +// Called when the first wizard page is shown. +function enableNextButton() { + document.querySelector("wizard").canAdvance = true; +} + +function onFinish(event) { + var profileName = document.getElementById("profileName").value; + var profile; + + // Create profile named profileName in profileRoot. + try { + profile = gProfileService.createProfile(gProfileRoot, profileName); + } catch (e) { + var profileCreationFailed = gProfileManagerBundle.getString( + "profileCreationFailed" + ); + var profileCreationFailedTitle = gProfileManagerBundle.getString( + "profileCreationFailedTitle" + ); + Services.prompt.alert( + window, + profileCreationFailedTitle, + profileCreationFailed + "\n" + e + ); + + event.preventDefault(); + return; + } + + if (window.arguments && window.arguments[1]) { + // Add new profile to the list in the Profile Manager. + window.arguments[1].CreateProfile(profile); + } else { + // Use the newly created Profile. + var profileLock = profile.lock(null); + + var dialogParams = window.arguments[0].QueryInterface( + I.nsIDialogParamBlock + ); + dialogParams.objects.insertElementAt(profileLock, 0); + } +} diff --git a/toolkit/profile/content/createProfileWizard.xhtml b/toolkit/profile/content/createProfileWizard.xhtml new file mode 100644 index 0000000000..dc984a0432 --- /dev/null +++ b/toolkit/profile/content/createProfileWizard.xhtml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + Mozilla Bug 543854 + + + + + diff --git a/toolkit/profile/xpcshell/head.js b/toolkit/profile/xpcshell/head.js new file mode 100644 index 0000000000..dc354f5b62 --- /dev/null +++ b/toolkit/profile/xpcshell/head.js @@ -0,0 +1,623 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { NetUtil } = ChromeUtils.importESModule( + "resource://gre/modules/NetUtil.sys.mjs" +); +const { FileUtils } = ChromeUtils.importESModule( + "resource://gre/modules/FileUtils.sys.mjs" +); +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +const NS_ERROR_START_PROFILE_MANAGER = 0x805800c9; + +const UPDATE_CHANNEL = AppConstants.MOZ_UPDATE_CHANNEL; + +let gProfD = do_get_profile(); +let gDataHome = gProfD.clone(); +gDataHome.append("data"); +gDataHome.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755); +let gDataHomeLocal = gProfD.clone(); +gDataHomeLocal.append("local"); +gDataHomeLocal.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + +let xreDirProvider = Cc["@mozilla.org/xre/directory-provider;1"].getService( + Ci.nsIXREDirProvider +); +xreDirProvider.setUserDataDirectory(gDataHome, false); +xreDirProvider.setUserDataDirectory(gDataHomeLocal, true); + +let gIsDefaultApp = false; + +const ShellService = { + register() { + let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + + let factory = { + createInstance(iid) { + return ShellService.QueryInterface(iid); + }, + }; + + registrar.registerFactory( + this.ID, + "ToolkitShellService", + this.CONTRACT, + factory + ); + }, + + isDefaultApplication() { + return gIsDefaultApp; + }, + + QueryInterface: ChromeUtils.generateQI(["nsIToolkitShellService"]), + ID: Components.ID("{ce724e0c-ed70-41c9-ab31-1033b0b591be}"), + CONTRACT: "@mozilla.org/toolkit/shell-service;1", +}; + +ShellService.register(); + +let gIsLegacy = false; + +function enableLegacyProfiles() { + Services.env.set("MOZ_LEGACY_PROFILES", "1"); + + gIsLegacy = true; +} + +function getProfileService() { + return Cc["@mozilla.org/toolkit/profile-service;1"].getService( + Ci.nsIToolkitProfileService + ); +} + +let PROFILE_DEFAULT = "default"; +let DEDICATED_NAME = `default-${UPDATE_CHANNEL}`; +if (AppConstants.MOZ_DEV_EDITION) { + DEDICATED_NAME = PROFILE_DEFAULT = "dev-edition-default"; +} + +// Shared data for backgroundtasks tests. +const BACKGROUNDTASKS_PROFILE_DATA = (() => { + let hash = xreDirProvider.getInstallHash(); + let profileData = { + options: { + startWithLastProfile: true, + }, + profiles: [ + { + name: "Profile1", + path: "Path1", + default: false, + }, + { + name: "Profile3", + path: "Path3", + default: false, + }, + ], + installs: { + [hash]: { + default: "Path1", + }, + }, + backgroundTasksProfiles: [ + { + name: `MozillaBackgroundTask-${hash}-unrelated_task`, + path: `saltsalt.MozillaBackgroundTask-${hash}-unrelated_task`, + }, + ], + }; + return profileData; +})(); + +/** + * Creates a random profile path for use. + */ +function makeRandomProfileDir(name) { + let file = gDataHome.clone(); + file.append(name); + file.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + return file; +} + +/** + * A wrapper around nsIToolkitProfileService.selectStartupProfile to make it + * a bit nicer to use from JS. + */ +function selectStartupProfile(args = [], isResetting = false, legacyHash = "") { + let service = getProfileService(); + let rootDir = {}; + let localDir = {}; + let profile = {}; + let didCreate = service.selectStartupProfile( + ["xpcshell", ...args], + isResetting, + UPDATE_CHANNEL, + legacyHash, + rootDir, + localDir, + profile + ); + + if (profile.value) { + Assert.ok( + rootDir.value.equals(profile.value.rootDir), + "Should have matched the root dir." + ); + Assert.ok( + localDir.value.equals(profile.value.localDir), + "Should have matched the local dir." + ); + Assert.ok( + service.currentProfile === profile.value, + "Should have marked the profile as the current profile." + ); + } else { + Assert.ok(!service.currentProfile, "Should be no current profile."); + } + + return { + rootDir: rootDir.value, + localDir: localDir.value, + profile: profile.value, + didCreate, + }; +} + +function testStartsProfileManager(args = [], isResetting = false) { + try { + selectStartupProfile(args, isResetting); + Assert.ok(false, "Should have started the profile manager"); + checkStartupReason(); + } catch (e) { + Assert.equal( + e.result, + NS_ERROR_START_PROFILE_MANAGER, + "Should have started the profile manager" + ); + } +} + +function safeGet(ini, section, key) { + try { + return ini.getString(section, key); + } catch (e) { + return null; + } +} + +/** + * Writes a compatibility.ini file that marks the give profile directory as last + * used by the given install path. + */ +function writeCompatibilityIni( + dir, + appDir = FileUtils.getDir("CurProcD", []), + greDir = FileUtils.getDir("GreD", []) +) { + let target = dir.clone(); + target.append("compatibility.ini"); + + let factory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"].getService( + Ci.nsIINIParserFactory + ); + let ini = factory.createINIParser().QueryInterface(Ci.nsIINIParserWriter); + + // The profile service doesn't care about these so just use fixed values + ini.setString( + "Compatibility", + "LastVersion", + "64.0a1_20180919123806/20180919123806" + ); + ini.setString("Compatibility", "LastOSABI", "Darwin_x86_64-gcc3"); + + ini.setString( + "Compatibility", + "LastPlatformDir", + greDir.persistentDescriptor + ); + ini.setString("Compatibility", "LastAppDir", appDir.persistentDescriptor); + + ini.writeFile(target); +} + +/** + * Writes a profiles.ini based on the passed profile data. + * profileData should contain two properties, options and profiles. + * options contains a single property, startWithLastProfile. + * profiles is an array of profiles each containing name, path and default + * properties. + */ +function writeProfilesIni(profileData) { + let target = gDataHome.clone(); + target.append("profiles.ini"); + + let factory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"].getService( + Ci.nsIINIParserFactory + ); + let ini = factory.createINIParser().QueryInterface(Ci.nsIINIParserWriter); + + const { + options = {}, + profiles = [], + installs = null, + backgroundTasksProfiles = null, + } = profileData; + + let { startWithLastProfile = true } = options; + ini.setString( + "General", + "StartWithLastProfile", + startWithLastProfile ? "1" : "0" + ); + + for (let i = 0; i < profiles.length; i++) { + let profile = profiles[i]; + let section = `Profile${i}`; + + ini.setString(section, "Name", profile.name); + ini.setString(section, "IsRelative", 1); + ini.setString(section, "Path", profile.path); + + if (profile.default) { + ini.setString(section, "Default", "1"); + } + } + + if (backgroundTasksProfiles) { + let section = "BackgroundTasksProfiles"; + for (let backgroundTasksProfile of backgroundTasksProfiles) { + ini.setString( + section, + backgroundTasksProfile.name, + backgroundTasksProfile.path + ); + } + } + + if (installs) { + ini.setString("General", "Version", "2"); + + for (let hash of Object.keys(installs)) { + ini.setString(`Install${hash}`, "Default", installs[hash].default); + if ("locked" in installs[hash]) { + ini.setString( + `Install${hash}`, + "Locked", + installs[hash].locked ? "1" : "0" + ); + } + } + + writeInstallsIni({ installs }); + } else { + writeInstallsIni(null); + } + + ini.writeFile(target); +} + +/** + * Reads the existing profiles.ini into the same structure as that accepted by + * writeProfilesIni above. The profiles property is sorted according to name + * because the order is irrelevant and it makes testing easier if we can make + * that assumption. + */ +function readProfilesIni() { + let target = gDataHome.clone(); + target.append("profiles.ini"); + + let profileData = { + options: { + startWithLastProfile: true, + }, + profiles: [], + installs: null, + }; + + if (!target.exists()) { + return profileData; + } + + let factory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"].getService( + Ci.nsIINIParserFactory + ); + let ini = factory.createINIParser(target); + + profileData.options.startWithLastProfile = + safeGet(ini, "General", "StartWithLastProfile") == "1"; + if (safeGet(ini, "General", "Version") == "2") { + profileData.installs = {}; + } + + let sections = ini.getSections(); + while (sections.hasMore()) { + let section = sections.getNext(); + + if (section == "General") { + continue; + } + + if (section.startsWith("Profile")) { + let isRelative = safeGet(ini, section, "IsRelative"); + if (isRelative === null) { + break; + } + Assert.equal( + isRelative, + "1", + "Paths should always be relative in these tests." + ); + + let profile = { + name: safeGet(ini, section, "Name"), + path: safeGet(ini, section, "Path"), + }; + + try { + profile.default = ini.getString(section, "Default") == "1"; + Assert.ok( + profile.default, + "The Default value is only written when true." + ); + } catch (e) { + profile.default = false; + } + + profileData.profiles.push(profile); + } + + if (section.startsWith("Install")) { + Assert.ok( + profileData.installs, + "Should only see an install section if the ini version was correct." + ); + + profileData.installs[section.substring(7)] = { + default: safeGet(ini, section, "Default"), + }; + + let locked = safeGet(ini, section, "Locked"); + if (locked !== null) { + profileData.installs[section.substring(7)].locked = locked; + } + } + + if (section == "BackgroundTasksProfiles") { + profileData.backgroundTasksProfiles = []; + let backgroundTasksProfiles = ini.getKeys(section); + while (backgroundTasksProfiles.hasMore()) { + let name = backgroundTasksProfiles.getNext(); + let path = ini.getString(section, name); + profileData.backgroundTasksProfiles.push({ name, path }); + } + profileData.backgroundTasksProfiles.sort((a, b) => + a.name.localeCompare(b.name) + ); + } + } + + profileData.profiles.sort((a, b) => a.name.localeCompare(b.name)); + + return profileData; +} + +/** + * Writes an installs.ini based on the supplied data. Should be an object with + * keys for every installation hash each mapping to an object. Each object + * should have a default property for the relative path to the profile. + */ +function writeInstallsIni(installData) { + let target = gDataHome.clone(); + target.append("installs.ini"); + + if (!installData) { + try { + target.remove(false); + } catch (e) {} + return; + } + + const { installs = {} } = installData; + + let factory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"].getService( + Ci.nsIINIParserFactory + ); + let ini = factory.createINIParser(null).QueryInterface(Ci.nsIINIParserWriter); + + for (let hash of Object.keys(installs)) { + ini.setString(hash, "Default", installs[hash].default); + if ("locked" in installs[hash]) { + ini.setString(hash, "Locked", installs[hash].locked ? "1" : "0"); + } + } + + ini.writeFile(target); +} + +/** + * Reads installs.ini into a structure like that used in the above function. + */ +function readInstallsIni() { + let target = gDataHome.clone(); + target.append("installs.ini"); + + let installData = { + installs: {}, + }; + + if (!target.exists()) { + return installData; + } + + let factory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"].getService( + Ci.nsIINIParserFactory + ); + let ini = factory.createINIParser(target); + + let sections = ini.getSections(); + while (sections.hasMore()) { + let hash = sections.getNext(); + if (hash != "General") { + installData.installs[hash] = { + default: safeGet(ini, hash, "Default"), + }; + + let locked = safeGet(ini, hash, "Locked"); + if (locked !== null) { + installData.installs[hash].locked = locked; + } + } + } + + return installData; +} + +/** + * Check that the backup data in installs.ini matches the install data in + * profiles.ini. + */ +function checkBackup( + profileData = readProfilesIni(), + installData = readInstallsIni() +) { + if (!profileData.installs) { + // If the profiles db isn't of the right version we wouldn't expect the + // backup to be accurate. + return; + } + + Assert.deepEqual( + profileData.installs, + installData.installs, + "Backup installs.ini should match installs in profiles.ini" + ); +} + +/** + * Checks that the profile service seems to have the right data in it compared + * to profile and install data structured as in the above functions. + */ +function checkProfileService( + profileData = readProfilesIni(), + verifyBackup = true +) { + let service = getProfileService(); + + let expectedStartWithLast = true; + if ("options" in profileData) { + expectedStartWithLast = profileData.options.startWithLastProfile; + } + + Assert.equal( + service.startWithLastProfile, + expectedStartWithLast, + "Start with last profile should match." + ); + + let serviceProfiles = Array.from(service.profiles); + + Assert.equal( + serviceProfiles.length, + profileData.profiles.length, + "Should be the same number of profiles." + ); + + // Sort to make matching easy. + serviceProfiles.sort((a, b) => a.name.localeCompare(b.name)); + profileData.profiles.sort((a, b) => a.name.localeCompare(b.name)); + + let hash = xreDirProvider.getInstallHash(); + let defaultPath = + profileData.installs && hash in profileData.installs + ? profileData.installs[hash].default + : null; + let dedicatedProfile = null; + let legacyProfile = null; + + for (let i = 0; i < serviceProfiles.length; i++) { + let serviceProfile = serviceProfiles[i]; + let expectedProfile = profileData.profiles[i]; + + Assert.equal( + serviceProfile.name, + expectedProfile.name, + "Should have the same name." + ); + + let expectedPath = Cc["@mozilla.org/file/local;1"].createInstance( + Ci.nsIFile + ); + expectedPath.setRelativeDescriptor(gDataHome, expectedProfile.path); + Assert.equal( + serviceProfile.rootDir.path, + expectedPath.path, + "Should have the same path." + ); + + if (expectedProfile.path == defaultPath) { + dedicatedProfile = serviceProfile; + } + + if (AppConstants.MOZ_DEV_EDITION) { + if (expectedProfile.name == PROFILE_DEFAULT) { + legacyProfile = serviceProfile; + } + } else if (expectedProfile.default) { + legacyProfile = serviceProfile; + } + } + + if (gIsLegacy || Services.env.get("SNAP_NAME")) { + Assert.equal( + service.defaultProfile, + legacyProfile, + "Should have seen the right profile selected." + ); + } else { + Assert.equal( + service.defaultProfile, + dedicatedProfile, + "Should have seen the right profile selected." + ); + } + + if (verifyBackup) { + checkBackup(profileData); + } +} + +// Maps the interesting scalar IDs to simple names that can be used as JS variables. +const SCALARS = { + selectionReason: "startup.profile_selection_reason", + databaseVersion: "startup.profile_database_version", + profileCount: "startup.profile_count", +}; + +function getTelemetryScalars() { + let scalars = TelemetryTestUtils.getProcessScalars("parent"); + + let results = {}; + for (let [prop, scalarId] of Object.entries(SCALARS)) { + results[prop] = scalars[scalarId]; + } + + return results; +} + +function checkStartupReason(expected = undefined) { + let { selectionReason } = getTelemetryScalars(); + + Assert.equal( + selectionReason, + expected, + "Should have seen the right startup reason." + ); +} diff --git a/toolkit/profile/xpcshell/test_check_backup.js b/toolkit/profile/xpcshell/test_check_backup.js new file mode 100644 index 0000000000..2f5a9dbf18 --- /dev/null +++ b/toolkit/profile/xpcshell/test_check_backup.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests that when the profiles DB is missing the install data we reload it. + */ + +add_task(async () => { + let hash = xreDirProvider.getInstallHash(); + + let profileData = { + options: { + startWithLastProfile: true, + }, + profiles: [ + { + name: "Profile1", + path: "Path1", + }, + { + name: "Profile2", + path: "Path2", + }, + ], + }; + + let installs = { + [hash]: { + default: "Path2", + }, + }; + + writeProfilesIni(profileData); + writeInstallsIni({ installs }); + + let { profile, didCreate } = selectStartupProfile(); + checkStartupReason("default"); + let { databaseVersion, profileCount } = getTelemetryScalars(); + Assert.equal( + databaseVersion, + "1", + "Old database file was present at startup." + ); + Assert.equal(profileCount, 2, "Should be two profiles."); + + // Should have added the backup data to the service, check that is true. + profileData.installs = installs; + checkProfileService(profileData); + + Assert.ok(!didCreate, "Should not have created a new profile."); + Assert.equal( + profile.name, + "Profile2", + "Should have selected the right profile" + ); +}); diff --git a/toolkit/profile/xpcshell/test_claim_locked.js b/toolkit/profile/xpcshell/test_claim_locked.js new file mode 100644 index 0000000000..bffdea39dc --- /dev/null +++ b/toolkit/profile/xpcshell/test_claim_locked.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests that an old-style default profile already locked to a different install + * isn't claimed by this install. + */ + +add_task(async () => { + let defaultProfile = makeRandomProfileDir("default"); + + writeCompatibilityIni(defaultProfile); + + writeProfilesIni({ + profiles: [ + { + name: "Foo", + path: defaultProfile.leafName, + default: true, + }, + ], + installs: { + other: { + default: defaultProfile.leafName, + locked: true, + }, + }, + }); + + let { profile: selectedProfile, didCreate } = selectStartupProfile(); + + let profileData = readProfilesIni(); + + Assert.ok( + profileData.options.startWithLastProfile, + "Should be set to start with the last profile." + ); + Assert.equal( + profileData.profiles.length, + 2, + "Should have the right number of profiles." + ); + + let hash = xreDirProvider.getInstallHash(); + Assert.equal( + Object.keys(profileData.installs).length, + 2, + "Should be two known installs." + ); + Assert.notEqual( + profileData.installs[hash].default, + defaultProfile.leafName, + "Should not have marked the original default profile as the default for this install." + ); + Assert.ok( + profileData.installs[hash].locked, + "Should have locked as we created this profile for this install." + ); + + checkProfileService(profileData); + + Assert.ok(didCreate, "Should have created a new profile."); + Assert.ok( + !selectedProfile.rootDir.equals(defaultProfile), + "Should be using a different directory." + ); + Assert.equal(selectedProfile.name, DEDICATED_NAME); +}); diff --git a/toolkit/profile/xpcshell/test_clean.js b/toolkit/profile/xpcshell/test_clean.js new file mode 100644 index 0000000000..3132d5457d --- /dev/null +++ b/toolkit/profile/xpcshell/test_clean.js @@ -0,0 +1,165 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests from a clean state. + * Then does some testing that creating new profiles and marking them as + * selected works. + */ + +add_task(async () => { + let service = getProfileService(); + + let target = gDataHome.clone(); + target.append("profiles.ini"); + Assert.ok(!target.exists(), "profiles.ini should not exist yet."); + target.leafName = "installs.ini"; + Assert.ok(!target.exists(), "installs.ini should not exist yet."); + + // Create a new profile to use. + let newProfile = service.createProfile(null, "dedicated"); + service.flush(); + + let profileData = readProfilesIni(); + + Assert.equal( + profileData.profiles.length, + 1, + "Should have the right number of profiles." + ); + + let profile = profileData.profiles[0]; + Assert.equal(profile.name, "dedicated", "Should have the right name."); + Assert.ok(!profile.default, "Should not be marked as the old-style default."); + + // The new profile hasn't been marked as the default yet! + Assert.equal( + Object.keys(profileData.installs).length, + 0, + "Should be no defaults for installs yet." + ); + + checkProfileService(profileData); + + Assert.ok( + service.startWithLastProfile, + "Should be set to start with the last profile." + ); + service.startWithLastProfile = false; + Assert.ok( + !service.startWithLastProfile, + "Should be set to not start with the last profile." + ); + + service.defaultProfile = newProfile; + service.flush(); + + profileData = readProfilesIni(); + + Assert.equal( + profileData.profiles.length, + 1, + "Should have the right number of profiles." + ); + + profile = profileData.profiles[0]; + Assert.equal(profile.name, "dedicated", "Should have the right name."); + Assert.ok(!profile.default, "Should not be marked as the old-style default."); + + let hash = xreDirProvider.getInstallHash(); + Assert.equal( + Object.keys(profileData.installs).length, + 1, + "Should be only one known install." + ); + Assert.equal( + profileData.installs[hash].default, + profileData.profiles[0].path, + "Should have marked the new profile as the default for this install." + ); + + checkProfileService(profileData); + + let otherProfile = service.createProfile(null, "another"); + service.defaultProfile = otherProfile; + + service.flush(); + + profileData = readProfilesIni(); + + Assert.equal( + profileData.profiles.length, + 2, + "Should have the right number of profiles." + ); + + profile = profileData.profiles[0]; + Assert.equal(profile.name, "another", "Should have the right name."); + Assert.ok(!profile.default, "Should not be marked as the old-style default."); + + profile = profileData.profiles[1]; + Assert.equal(profile.name, "dedicated", "Should have the right name."); + Assert.ok(!profile.default, "Should not be marked as the old-style default."); + + Assert.equal( + Object.keys(profileData.installs).length, + 1, + "Should be only one known install." + ); + Assert.equal( + profileData.installs[hash].default, + profileData.profiles[0].path, + "Should have marked the new profile as the default for this install." + ); + + checkProfileService(profileData); + + newProfile.remove(true); + service.flush(); + + profileData = readProfilesIni(); + + Assert.equal( + profileData.profiles.length, + 1, + "Should have the right number of profiles." + ); + + profile = profileData.profiles[0]; + Assert.equal(profile.name, "another", "Should have the right name."); + Assert.ok(!profile.default, "Should not be marked as the old-style default."); + + Assert.equal( + Object.keys(profileData.installs).length, + 1, + "Should be only one known install." + ); + Assert.equal( + profileData.installs[hash].default, + profileData.profiles[0].path, + "Should have marked the new profile as the default for this install." + ); + + checkProfileService(profileData); + + otherProfile.remove(true); + service.flush(); + + profileData = readProfilesIni(); + + Assert.equal( + profileData.profiles.length, + 0, + "Should have the right number of profiles." + ); + + // We leave a reference to the missing profile to stop us trying to steal the + // old-style default profile on next startup. + Assert.equal( + Object.keys(profileData.installs).length, + 1, + "Should be only one known install." + ); + + checkProfileService(profileData); +}); diff --git a/toolkit/profile/xpcshell/test_conflict_installs.js b/toolkit/profile/xpcshell/test_conflict_installs.js new file mode 100644 index 0000000000..741e7ed70f --- /dev/null +++ b/toolkit/profile/xpcshell/test_conflict_installs.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests that the profile service refuses to flush when the install.ini file + * has been modified. + */ + +function check_unchanged(service) { + Assert.ok( + !service.isListOutdated, + "Should not have detected a modification." + ); + try { + service.flush(); + Assert.ok(true, "Should have flushed."); + } catch (e) { + Assert.ok(false, "Should have succeeded flushing."); + } +} + +add_task(async () => { + let service = getProfileService(); + + Assert.ok(!service.isListOutdated, "Should not be modified yet."); + + let installsini = gDataHome.clone(); + installsini.append("installs.ini"); + + Assert.ok(!installsini.exists(), "File should not exist yet."); + installsini.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644); + + installsini.remove(false); + // We have to do profile selection to actually have any install data. + selectStartupProfile(); + check_unchanged(service); + + // We can't reset the modification time back to exactly what it was, so I + // guess we can't do much more here :( +}); diff --git a/toolkit/profile/xpcshell/test_conflict_profiles.js b/toolkit/profile/xpcshell/test_conflict_profiles.js new file mode 100644 index 0000000000..7028c1d5c2 --- /dev/null +++ b/toolkit/profile/xpcshell/test_conflict_profiles.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests that the profile service refuses to flush when the profiles.ini file + * has been modified. + */ + +function check_unchanged(service) { + Assert.ok( + !service.isListOutdated, + "Should not have detected a modification." + ); + try { + service.flush(); + Assert.ok(true, "Should have flushed."); + } catch (e) { + Assert.ok(false, "Should have succeeded flushing."); + } +} + +function check_outdated(service) { + Assert.ok(service.isListOutdated, "Should have detected a modification."); + try { + service.flush(); + Assert.ok(false, "Should have failed to flush."); + } catch (e) { + Assert.equal( + e.result, + Cr.NS_ERROR_DATABASE_CHANGED, + "Should have refused to flush." + ); + } +} + +add_task(async () => { + let service = getProfileService(); + + Assert.ok(!service.isListOutdated, "Should not be modified yet."); + + let profilesini = gDataHome.clone(); + profilesini.append("profiles.ini"); + + Assert.ok(!profilesini.exists(), "File should not exist yet."); + profilesini.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644); + check_outdated(service); + + profilesini.remove(false); + check_unchanged(service); + + let oldTime = profilesini.lastModifiedTime; + profilesini.lastModifiedTime = oldTime - 10000; + check_outdated(service); + + // We can't reset the modification time back to exactly what it was, so I + // guess we can't do much more here :( +}); diff --git a/toolkit/profile/xpcshell/test_create_default.js b/toolkit/profile/xpcshell/test_create_default.js new file mode 100644 index 0000000000..cdac1832bf --- /dev/null +++ b/toolkit/profile/xpcshell/test_create_default.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests that from an empty database a default profile is created. + */ + +add_task(async () => { + let service = getProfileService(); + let { profile, didCreate } = selectStartupProfile(); + + checkStartupReason("firstrun-created-default"); + + let profileData = readProfilesIni(); + checkProfileService(profileData); + + Assert.ok(didCreate, "Should have created a new profile."); + Assert.equal( + profile, + service.defaultProfile, + "Should now be the default profile." + ); + Assert.equal( + profile.name, + DEDICATED_NAME, + "Should have created a new profile with the right name." + ); + + Assert.ok( + profileData.options.startWithLastProfile, + "Should be set to start with the last profile." + ); + Assert.equal( + profileData.profiles.length, + 2, + "Should have the right number of profiles." + ); + + profile = profileData.profiles[0]; + Assert.equal(profile.name, "default", "Should have the right name."); + Assert.ok(profile.default, "Should be marked as the old-style default."); + + profile = profileData.profiles[1]; + Assert.equal(profile.name, DEDICATED_NAME, "Should have the right name."); + Assert.ok(!profile.default, "Should not be marked as the old-style default."); + + let hash = xreDirProvider.getInstallHash(); + Assert.ok( + profileData.installs[hash].locked, + "Should have locked the profile" + ); +}); diff --git a/toolkit/profile/xpcshell/test_fix_directory_case.js b/toolkit/profile/xpcshell/test_fix_directory_case.js new file mode 100644 index 0000000000..60e3acbe4e --- /dev/null +++ b/toolkit/profile/xpcshell/test_fix_directory_case.js @@ -0,0 +1,113 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests the case where the user has an default profile set for the legacy + * install hash. This should be switched to the new hash and correctly used as + * the default. + */ + +add_task(async () => { + let currentHash = xreDirProvider.getInstallHash(); + let legacyHash = "F87E39E944FE466E"; + + let defaultProfile = makeRandomProfileDir("default"); + let dedicatedProfile = makeRandomProfileDir("dedicated"); + let devProfile = makeRandomProfileDir("devedition"); + + // Make sure we don't steal the old-style default. + writeCompatibilityIni(defaultProfile); + + writeProfilesIni({ + profiles: [ + { + name: "default", + path: defaultProfile.leafName, + default: true, + }, + { + name: "dedicated", + path: dedicatedProfile.leafName, + }, + { + name: "dev-edition-default", + path: devProfile.leafName, + }, + ], + installs: { + [legacyHash]: { + default: dedicatedProfile.leafName, + }, + otherhash: { + default: "foobar", + }, + }, + }); + + let { profile: selectedProfile, didCreate } = selectStartupProfile( + [], + false, + legacyHash + ); + checkStartupReason("default"); + + let profileData = readProfilesIni(); + + Assert.ok( + profileData.options.startWithLastProfile, + "Should be set to start with the last profile." + ); + Assert.equal( + profileData.profiles.length, + 3, + "Should have the right number of profiles." + ); + + let profile = profileData.profiles[0]; + Assert.equal(profile.name, `dedicated`, "Should have the right name."); + Assert.equal( + profile.path, + dedicatedProfile.leafName, + "Should be the expected dedicated profile." + ); + Assert.ok(!profile.default, "Should not be marked as the old-style default."); + + profile = profileData.profiles[1]; + Assert.equal(profile.name, "default", "Should have the right name."); + Assert.equal( + profile.path, + defaultProfile.leafName, + "Should be the original default profile." + ); + Assert.ok(profile.default, "Should be marked as the old-style default."); + + Assert.equal( + Object.keys(profileData.installs).length, + 3, + "Should be three known installs." + ); + Assert.equal( + profileData.installs[currentHash].default, + dedicatedProfile.leafName, + "Should have switched to the new install hash." + ); + Assert.equal( + profileData.installs[legacyHash].default, + dedicatedProfile.leafName, + "Should have kept the old install hash." + ); + Assert.equal( + profileData.installs.otherhash.default, + "foobar", + "Should have kept the default for the other install." + ); + + checkProfileService(profileData); + + Assert.ok(!didCreate, "Should not have created a new profile."); + Assert.ok( + selectedProfile.rootDir.equals(dedicatedProfile), + "Should be using the right directory." + ); + Assert.equal(selectedProfile.name, "dedicated"); +}); diff --git a/toolkit/profile/xpcshell/test_ignore_legacy_directory.js b/toolkit/profile/xpcshell/test_ignore_legacy_directory.js new file mode 100644 index 0000000000..29b1601cc5 --- /dev/null +++ b/toolkit/profile/xpcshell/test_ignore_legacy_directory.js @@ -0,0 +1,129 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests the case where the user has an default profile set for both the legacy + * and new install hash. This should just use the default for the new install + * hash. + */ + +add_task(async () => { + let currentHash = xreDirProvider.getInstallHash(); + let legacyHash = "F87E39E944FE466E"; + + let defaultProfile = makeRandomProfileDir("default"); + let dedicatedProfile = makeRandomProfileDir("dedicated"); + let devProfile = makeRandomProfileDir("devedition"); + + // Make sure we don't steal the old-style default. + writeCompatibilityIni(defaultProfile); + + writeProfilesIni({ + profiles: [ + { + name: "default", + path: defaultProfile.leafName, + default: true, + }, + { + name: "dedicated", + path: dedicatedProfile.leafName, + }, + { + name: "dev-edition-default", + path: devProfile.leafName, + }, + ], + installs: { + [legacyHash]: { + default: defaultProfile.leafName, + }, + [currentHash]: { + default: dedicatedProfile.leafName, + }, + otherhash: { + default: "foobar", + }, + }, + }); + + let { profile: selectedProfile, didCreate } = selectStartupProfile( + [], + false, + legacyHash + ); + checkStartupReason("default"); + + let profileData = readProfilesIni(); + + Assert.ok( + profileData.options.startWithLastProfile, + "Should be set to start with the last profile." + ); + Assert.equal( + profileData.profiles.length, + 3, + "Should have the right number of profiles." + ); + + let profile = profileData.profiles[0]; + Assert.equal(profile.name, `dedicated`, "Should have the right name."); + Assert.equal( + profile.path, + dedicatedProfile.leafName, + "Should be the expected dedicated profile." + ); + Assert.ok(!profile.default, "Should not be marked as the old-style default."); + + profile = profileData.profiles[1]; + Assert.equal(profile.name, "default", "Should have the right name."); + Assert.equal( + profile.path, + defaultProfile.leafName, + "Should be the original default profile." + ); + Assert.ok(profile.default, "Should be marked as the old-style default."); + + profile = profileData.profiles[2]; + Assert.equal( + profile.name, + "dev-edition-default", + "Should have the right name." + ); + Assert.equal( + profile.path, + devProfile.leafName, + "Should not be the original default profile." + ); + Assert.ok(!profile.default, "Should not be marked as the old-style default."); + + Assert.equal( + Object.keys(profileData.installs).length, + 3, + "Should be three known installs." + ); + Assert.equal( + profileData.installs[currentHash].default, + dedicatedProfile.leafName, + "Should have switched to the new install hash." + ); + Assert.equal( + profileData.installs[legacyHash].default, + defaultProfile.leafName, + "Should have ignored old install hash." + ); + Assert.equal( + profileData.installs.otherhash.default, + "foobar", + "Should have kept the default for the other install." + ); + + checkProfileService(profileData); + + Assert.ok(!didCreate, "Should not have created a new profile."); + Assert.ok( + selectedProfile.rootDir.equals(dedicatedProfile), + "Should be using the right directory." + ); + Assert.equal(selectedProfile.name, "dedicated"); +}); diff --git a/toolkit/profile/xpcshell/test_invalid_descriptor.js b/toolkit/profile/xpcshell/test_invalid_descriptor.js new file mode 100644 index 0000000000..87638d301d --- /dev/null +++ b/toolkit/profile/xpcshell/test_invalid_descriptor.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * If a user has modified a relative profile path then there may be issues where + * the profile default setting doesn't match. + */ + +add_task(async () => { + let hash = xreDirProvider.getInstallHash(); + + let profileData = { + options: { + startWithLastProfile: true, + }, + profiles: [ + { + name: "Profile1", + path: "../data/test", + }, + { + name: "Profile2", + path: "Path2", + }, + ], + installs: { + [hash]: { + default: "test", + }, + }, + }; + + writeProfilesIni(profileData); + + let { profile, didCreate } = selectStartupProfile(); + checkStartupReason("default"); + + let service = getProfileService(); + + Assert.ok(!didCreate, "Should not have created a new profile."); + Assert.equal( + profile.name, + "Profile1", + "Should have selected the expected profile" + ); + + Assert.equal( + profile.name, + service.defaultProfile.name, + "Should have selected the right default." + ); + + service.flush(); + checkProfileService(); +}); diff --git a/toolkit/profile/xpcshell/test_legacy_empty.js b/toolkit/profile/xpcshell/test_legacy_empty.js new file mode 100644 index 0000000000..7348006f8b --- /dev/null +++ b/toolkit/profile/xpcshell/test_legacy_empty.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests that setting MOZ_LEGACY_PROFILES disables dedicated profiles. + */ + +add_task(async () => { + enableLegacyProfiles(); + + let service = getProfileService(); + let { profile, didCreate } = selectStartupProfile(); + checkStartupReason("firstrun-created-default"); + + Assert.ok(didCreate, "Should have created a new profile."); + Assert.equal( + profile.name, + PROFILE_DEFAULT, + "Should have used the normal name." + ); + if (AppConstants.MOZ_DEV_EDITION) { + Assert.equal(service.profileCount, 2, "Should be two profiles."); + } else { + Assert.equal(service.profileCount, 1, "Should be only one profile."); + } + + checkProfileService(); +}); diff --git a/toolkit/profile/xpcshell/test_legacy_select.js b/toolkit/profile/xpcshell/test_legacy_select.js new file mode 100644 index 0000000000..e919169d59 --- /dev/null +++ b/toolkit/profile/xpcshell/test_legacy_select.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests that an old-style default profile not previously used by this build + * gets selected when configured for legacy profiles. + */ + +add_task(async () => { + let defaultProfile = makeRandomProfileDir("default"); + + // Just pretend this profile was last used by something in the profile dir. + let greDir = gProfD.clone(); + greDir.append("app"); + writeCompatibilityIni(defaultProfile, greDir, greDir); + + writeProfilesIni({ + profiles: [ + { + name: PROFILE_DEFAULT, + path: defaultProfile.leafName, + default: true, + }, + ], + }); + + enableLegacyProfiles(); + + let { profile: selectedProfile, didCreate } = selectStartupProfile(); + checkStartupReason("default"); + + let profileData = readProfilesIni(); + let installsINI = gDataHome.clone(); + installsINI.append("installs.ini"); + Assert.ok( + !installsINI.exists(), + "Installs database should not have been created." + ); + + Assert.ok( + profileData.options.startWithLastProfile, + "Should be set to start with the last profile." + ); + Assert.equal( + profileData.profiles.length, + 1, + "Should have the right number of profiles." + ); + + let profile = profileData.profiles[0]; + Assert.equal(profile.name, PROFILE_DEFAULT, "Should have the right name."); + Assert.equal( + profile.path, + defaultProfile.leafName, + "Should be the original default profile." + ); + Assert.ok(profile.default, "Should be marked as the old-style default."); + + checkProfileService(profileData); + + Assert.ok(!didCreate, "Should not have created a new profile."); + Assert.ok( + selectedProfile.rootDir.equals(defaultProfile), + "Should be using the right directory." + ); + Assert.equal(selectedProfile.name, PROFILE_DEFAULT); +}); diff --git a/toolkit/profile/xpcshell/test_lock.js b/toolkit/profile/xpcshell/test_lock.js new file mode 100644 index 0000000000..7283406e00 --- /dev/null +++ b/toolkit/profile/xpcshell/test_lock.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests that when the default application claims the old-style default profile + * it locks it to itself. + */ + +add_task(async () => { + gIsDefaultApp = true; + let defaultProfile = makeRandomProfileDir("default"); + + writeCompatibilityIni(defaultProfile); + + writeProfilesIni({ + profiles: [ + { + name: PROFILE_DEFAULT, + path: defaultProfile.leafName, + default: true, + }, + ], + }); + + let { profile: selectedProfile, didCreate } = selectStartupProfile(); + + let hash = xreDirProvider.getInstallHash(); + let profileData = readProfilesIni(); + + Assert.ok( + profileData.options.startWithLastProfile, + "Should be set to start with the last profile." + ); + Assert.equal( + profileData.profiles.length, + 1, + "Should have the right number of profiles." + ); + + let profile = profileData.profiles[0]; + Assert.equal(profile.name, PROFILE_DEFAULT, "Should have the right name."); + Assert.equal( + profile.path, + defaultProfile.leafName, + "Should be the original default profile." + ); + Assert.ok(profile.default, "Should be marked as the old-style default."); + + Assert.equal( + Object.keys(profileData.installs).length, + 1, + "Should be only one known install." + ); + Assert.equal( + profileData.installs[hash].default, + defaultProfile.leafName, + "Should have marked the original default profile as the default for this install." + ); + Assert.ok( + profileData.installs[hash].locked, + "Should have locked as we're the default app." + ); + + checkProfileService(profileData); + + Assert.ok(!didCreate, "Should not have created a new profile."); + Assert.ok( + selectedProfile.rootDir.equals(defaultProfile), + "Should be using the right directory." + ); + Assert.equal(selectedProfile.name, PROFILE_DEFAULT); +}); diff --git a/toolkit/profile/xpcshell/test_missing_profilesini.js b/toolkit/profile/xpcshell/test_missing_profilesini.js new file mode 100644 index 0000000000..74ab7584fa --- /dev/null +++ b/toolkit/profile/xpcshell/test_missing_profilesini.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * When profiles.ini is missing there isn't any point in restoring from any + * installs.ini, the profiles it refers to are gone anyway. + */ + +add_task(async () => { + let hash = xreDirProvider.getInstallHash(); + + let installs = { + [hash]: { + default: "Path2", + }, + otherhash: { + default: "foo", + }, + anotherhash: { + default: "bar", + }, + }; + + writeInstallsIni({ installs }); + + let { profile, didCreate } = selectStartupProfile(); + checkStartupReason("firstrun-created-default"); + + Assert.ok(didCreate, "Should have created a new profile."); + Assert.equal( + profile.name, + DEDICATED_NAME, + "Should have created the right profile" + ); + + let profilesData = readProfilesIni(); + Assert.equal( + Object.keys(profilesData.installs).length, + 1, + "Should be only one known install" + ); + Assert.ok(hash in profilesData.installs, "Should be the expected install."); + Assert.notEqual( + profilesData.installs[hash].default, + "Path2", + "Didn't import the previous data." + ); + Assert.equal( + profilesData.profiles.length, + 2, + "Should be two profiles (old-style default and dedicated default)." + ); + + let { databaseVersion, profileCount } = getTelemetryScalars(); + Assert.equal( + databaseVersion, + "0", + "Database file was not present at startup." + ); + Assert.equal( + profileCount, + 2, + "Should be two profiles (old-style default and dedicated default)." + ); + + checkProfileService(profilesData); +}); diff --git a/toolkit/profile/xpcshell/test_new_default.js b/toolkit/profile/xpcshell/test_new_default.js new file mode 100644 index 0000000000..552433ab95 --- /dev/null +++ b/toolkit/profile/xpcshell/test_new_default.js @@ -0,0 +1,129 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests that an old-style default profile previously used by this build gets + * updated to a dedicated profile for this build. + */ + +add_task(async () => { + let mydefaultProfile = makeRandomProfileDir("mydefault"); + let defaultProfile = makeRandomProfileDir("default"); + let devDefaultProfile = makeRandomProfileDir("devedition"); + + writeCompatibilityIni(mydefaultProfile); + writeCompatibilityIni(devDefaultProfile); + + writeProfilesIni({ + profiles: [ + { + name: "mydefault", + path: mydefaultProfile.leafName, + default: true, + }, + { + name: "default", + path: defaultProfile.leafName, + }, + { + name: "dev-edition-default", + path: devDefaultProfile.leafName, + }, + ], + }); + + let { profile: selectedProfile, didCreate } = selectStartupProfile(); + checkStartupReason("firstrun-claimed-default"); + let { databaseVersion, profileCount } = getTelemetryScalars(); + Assert.equal( + databaseVersion, + "1", + "Old database file was present at startup." + ); + Assert.equal(profileCount, 3, "Should be three profiles."); + + let hash = xreDirProvider.getInstallHash(); + let profileData = readProfilesIni(); + + Assert.ok( + profileData.options.startWithLastProfile, + "Should be set to start with the last profile." + ); + Assert.equal( + profileData.profiles.length, + 3, + "Should have the right number of profiles." + ); + + let profile = profileData.profiles[0]; + Assert.equal(profile.name, "default", "Should have the right name."); + Assert.equal( + profile.path, + defaultProfile.leafName, + "Should be the original non-default profile." + ); + Assert.ok(!profile.default, "Should not be marked as the old-style default."); + + profile = profileData.profiles[1]; + Assert.equal( + profile.name, + "dev-edition-default", + "Should have the right name." + ); + Assert.equal( + profile.path, + devDefaultProfile.leafName, + "Should be the original dev default profile." + ); + Assert.ok(!profile.default, "Should not be marked as the old-style default."); + + profile = profileData.profiles[2]; + Assert.equal(profile.name, "mydefault", "Should have the right name."); + Assert.equal( + profile.path, + mydefaultProfile.leafName, + "Should be the original default profile." + ); + Assert.ok(profile.default, "Should be marked as the old-style default."); + + Assert.equal( + Object.keys(profileData.installs).length, + 1, + "Should be only one known install." + ); + if (AppConstants.MOZ_DEV_EDITION) { + Assert.equal( + profileData.installs[hash].default, + devDefaultProfile.leafName, + "Should have marked the original dev default profile as the default for this install." + ); + } else { + Assert.equal( + profileData.installs[hash].default, + mydefaultProfile.leafName, + "Should have marked the original default profile as the default for this install." + ); + } + + Assert.ok( + !profileData.installs[hash].locked, + "Should not be locked as we're not the default app." + ); + + checkProfileService(profileData); + + Assert.ok(!didCreate, "Should not have created a new profile."); + if (AppConstants.MOZ_DEV_EDITION) { + Assert.ok( + selectedProfile.rootDir.equals(devDefaultProfile), + "Should be using the right directory." + ); + Assert.equal(selectedProfile.name, "dev-edition-default"); + } else { + Assert.ok( + selectedProfile.rootDir.equals(mydefaultProfile), + "Should be using the right directory." + ); + Assert.equal(selectedProfile.name, "mydefault"); + } +}); diff --git a/toolkit/profile/xpcshell/test_previous_dedicated.js b/toolkit/profile/xpcshell/test_previous_dedicated.js new file mode 100644 index 0000000000..6dd80b4028 --- /dev/null +++ b/toolkit/profile/xpcshell/test_previous_dedicated.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * If install.ini lists a default profile for this build but that profile no + * longer exists don't try to steal the old-style default even if it was used + * by this build. It means this install has previously used dedicated profiles. + */ + +add_task(async () => { + let hash = xreDirProvider.getInstallHash(); + let defaultProfile = makeRandomProfileDir("default"); + + writeCompatibilityIni(defaultProfile); + + writeProfilesIni({ + profiles: [ + { + name: "default", + path: defaultProfile.leafName, + default: true, + }, + ], + installs: { + [hash]: { + default: "foobar", + }, + }, + }); + + testStartsProfileManager(); + + let profileData = readProfilesIni(); + + Assert.ok( + profileData.options.startWithLastProfile, + "Should be set to start with the last profile." + ); + Assert.equal( + profileData.profiles.length, + 1, + "Should have the right number of profiles." + ); + + let profile = profileData.profiles[0]; + Assert.equal(profile.name, "default", "Should have the right name."); + Assert.equal( + profile.path, + defaultProfile.leafName, + "Should be the original default profile." + ); + Assert.ok(profile.default, "Should be marked as the old-style default."); + + // We keep the data here so we don't steal on the next reboot... + Assert.equal( + Object.keys(profileData.installs).length, + 1, + "Still list the broken reference." + ); + + checkProfileService(profileData); +}); diff --git a/toolkit/profile/xpcshell/test_profile_reset.js b/toolkit/profile/xpcshell/test_profile_reset.js new file mode 100644 index 0000000000..7cfb5ed3d1 --- /dev/null +++ b/toolkit/profile/xpcshell/test_profile_reset.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests that from an empty database with profile reset requested a new profile + * is still created. + */ + +add_task(async () => { + let { profile: selectedProfile, didCreate } = selectStartupProfile([], true); + // With no profile we're just create a new profile and skip resetting it. + checkStartupReason("firstrun-created-default"); + checkProfileService(); + + let hash = xreDirProvider.getInstallHash(); + let profileData = readProfilesIni(); + + Assert.ok( + profileData.options.startWithLastProfile, + "Should be set to start with the last profile." + ); + Assert.equal( + profileData.profiles.length, + 2, + "Should have the right number of profiles, ours and the old-style default." + ); + + let profile = profileData.profiles[0]; + Assert.equal(profile.name, "default", "Should have the right name."); + Assert.ok(profile.default, "Should be marked as the old-style default."); + + profile = profileData.profiles[1]; + Assert.equal(profile.name, DEDICATED_NAME, "Should have the right name."); + Assert.ok(!profile.default, "Should not be marked as the old-style default."); + + Assert.equal( + Object.keys(profileData.installs).length, + 1, + "Should only be one known installs." + ); + Assert.equal( + profileData.installs[hash].default, + profile.path, + "Should have taken the new profile as the default for the current install." + ); + Assert.ok( + profileData.installs[hash].locked, + "Should have locked as we created this profile." + ); + + checkProfileService(profileData); + + Assert.ok(didCreate, "Should have created a new profile."); + Assert.equal( + selectedProfile.name, + profile.name, + "Should be using the right profile." + ); +}); diff --git a/toolkit/profile/xpcshell/test_remove.js b/toolkit/profile/xpcshell/test_remove.js new file mode 100644 index 0000000000..8b5025d612 --- /dev/null +++ b/toolkit/profile/xpcshell/test_remove.js @@ -0,0 +1,103 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests adding and removing functions correctly. + */ + +function compareLists(service, knownProfiles) { + Assert.equal( + service.profileCount, + knownProfiles.length, + "profileCount should be correct." + ); + let serviceProfiles = Array.from(service.profiles); + Assert.equal( + serviceProfiles.length, + knownProfiles.length, + "Enumerator length should be correct." + ); + + for (let i = 0; i < knownProfiles.length; i++) { + // Cannot use strictEqual here, it attempts to print out a string + // representation of the profile objects and on some platforms that recurses + // infinitely. + Assert.ok( + serviceProfiles[i] === knownProfiles[i], + `Should have the right profile in position ${i}.` + ); + } +} + +function removeProfile(profiles, position) { + dump(`Removing profile in position ${position}.`); + Assert.greaterOrEqual(position, 0, "Should be removing a valid position."); + Assert.less( + position, + profiles.length, + "Should be removing a valid position." + ); + + let last = profiles.pop(); + + if (profiles.length == position) { + // We were asked to remove the last profile. + last.remove(false); + return; + } + + profiles[position].remove(false); + profiles[position] = last; +} + +add_task(async () => { + let service = getProfileService(); + let profiles = []; + compareLists(service, profiles); + + profiles.push(service.createProfile(null, "profile1")); + profiles.push(service.createProfile(null, "profile2")); + profiles.push(service.createProfile(null, "profile3")); + profiles.push(service.createProfile(null, "profile4")); + profiles.push(service.createProfile(null, "profile5")); + profiles.push(service.createProfile(null, "profile6")); + profiles.push(service.createProfile(null, "profile7")); + profiles.push(service.createProfile(null, "profile8")); + profiles.push(service.createProfile(null, "profile9")); + compareLists(service, profiles); + + // Test removing the first profile. + removeProfile(profiles, 0); + compareLists(service, profiles); + + // And the last profile. + removeProfile(profiles, profiles.length - 1); + compareLists(service, profiles); + + // Last but one... + removeProfile(profiles, profiles.length - 2); + compareLists(service, profiles); + + // Second one... + removeProfile(profiles, 1); + compareLists(service, profiles); + + // Something in the middle. + removeProfile(profiles, 2); + compareLists(service, profiles); + + let expectedNames = ["profile9", "profile7", "profile5", "profile4"]; + + let serviceProfiles = Array.from(service.profiles); + for (let i = 0; i < expectedNames.length; i++) { + Assert.equal(serviceProfiles[i].name, expectedNames[i]); + } + + removeProfile(profiles, 0); + removeProfile(profiles, 0); + removeProfile(profiles, 0); + removeProfile(profiles, 0); + + Assert.equal(Array.from(service.profiles).length, 0, "All profiles gone."); + Assert.equal(service.profileCount, 0, "All profiles gone."); +}); diff --git a/toolkit/profile/xpcshell/test_remove_default.js b/toolkit/profile/xpcshell/test_remove_default.js new file mode 100644 index 0000000000..f77f2d87d9 --- /dev/null +++ b/toolkit/profile/xpcshell/test_remove_default.js @@ -0,0 +1,79 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that calling nsIToolkitProfile.remove on the default profile correctly + * removes the profile. + */ + +add_task(async () => { + let hash = xreDirProvider.getInstallHash(); + let defaultProfile = makeRandomProfileDir("default"); + + let profilesIni = { + profiles: [ + { + name: "default", + path: defaultProfile.leafName, + default: true, + }, + ], + installs: { + [hash]: { + default: defaultProfile.leafName, + }, + }, + }; + writeProfilesIni(profilesIni); + + let service = getProfileService(); + checkProfileService(profilesIni); + + let { profile, didCreate } = selectStartupProfile(); + Assert.ok(!didCreate, "Should have not created a new profile."); + Assert.equal( + profile.name, + "default", + "Should have selected the default profile." + ); + Assert.equal( + profile, + service.defaultProfile, + "Should have selected the default profile." + ); + + checkProfileService(profilesIni); + + // In an actual run of Firefox we wouldn't be able to delete the profile in + // use because it would be locked. But we don't actually lock the profile in + // tests. + profile.remove(false); + + Assert.ok(!service.defaultProfile, "Should no longer be a default profile."); + Assert.equal( + profile, + service.currentProfile, + "Should still be the profile in use." + ); + + // These are the modifications that should have been made. + profilesIni.profiles.pop(); + profilesIni.installs[hash].default = ""; + + // The data isn't flushed to disk so don't check the backup here. + checkProfileService(profilesIni, false); + + service.flush(); + + // And that should have flushed to disk correctly. + checkProfileService(); + + // checkProfileService doesn't differentiate between a blank default profile + // for the install and a missing install. + profilesIni = readProfilesIni(); + Assert.equal( + profilesIni.installs[hash].default, + "", + "Should be a blank default profile." + ); +}); diff --git a/toolkit/profile/xpcshell/test_select_backgroundtasks_ephemeral.js b/toolkit/profile/xpcshell/test_select_backgroundtasks_ephemeral.js new file mode 100644 index 0000000000..982df276db --- /dev/null +++ b/toolkit/profile/xpcshell/test_select_backgroundtasks_ephemeral.js @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Verify that background tasks don't touch `profiles.ini` for ephemeral profile + * tasks. + */ + +let condition = { + skip_if: () => !AppConstants.MOZ_BACKGROUNDTASKS, +}; + +add_task(condition, async () => { + writeProfilesIni(BACKGROUNDTASKS_PROFILE_DATA); + + // Pretend that this is a background task. For a task that uses an ephemeral + // profile, `profiles.ini` is untouched. + const bts = Cc["@mozilla.org/backgroundtasks;1"].getService( + Ci.nsIBackgroundTasks + ); + bts.overrideBackgroundTaskNameForTesting("ephemeral_profile"); + + let { didCreate } = selectStartupProfile(); + checkStartupReason("backgroundtask-ephemeral"); + + Assert.equal(didCreate, true, "Created new ephemeral profile"); + + let profileData = readProfilesIni(); + Assert.deepEqual(BACKGROUNDTASKS_PROFILE_DATA, profileData); +}); diff --git a/toolkit/profile/xpcshell/test_select_backgroundtasks_not_ephemeral_create.js b/toolkit/profile/xpcshell/test_select_backgroundtasks_not_ephemeral_create.js new file mode 100644 index 0000000000..ebed889360 --- /dev/null +++ b/toolkit/profile/xpcshell/test_select_backgroundtasks_not_ephemeral_create.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Verify that background tasks that create non-ephemeral profiles update + * `profiles.ini` with a salted profile location. + */ + +let condition = { + skip_if: () => !AppConstants.MOZ_BACKGROUNDTASKS, +}; + +// MOZ_APP_VENDOR is empty on Thunderbird. +let vendor = AppConstants.MOZ_APP_NAME == "thunderbird" ? "" : "Mozilla"; + +add_task(condition, async () => { + let hash = xreDirProvider.getInstallHash(); + + writeProfilesIni(BACKGROUNDTASKS_PROFILE_DATA); + + // Pretend that this is a background task. For a task that does *not* use an + // ephemeral profile, i.e., that uses a persistent profile, `profiles.ini` is + // updated with a new entry in section `BackgroundTaskProfiles`. + const bts = Cc["@mozilla.org/backgroundtasks;1"].getService( + Ci.nsIBackgroundTasks + ); + // "not_ephemeral_profile" is a special name recognized by the + // background task system. + bts.overrideBackgroundTaskNameForTesting("not_ephemeral_profile"); + + let { didCreate, rootDir } = selectStartupProfile(); + checkStartupReason("backgroundtask-not-ephemeral"); + + Assert.equal(didCreate, true, "Created new non-ephemeral profile"); + + let profileData = readProfilesIni(); + + // Profile names are lexicographically ordered, and `not_ephemeral_profile` + // sorts before `unrelated_task`. + Assert.equal(profileData.backgroundTasksProfiles.length, 2); + Assert.deepEqual( + [profileData.backgroundTasksProfiles[1]], + BACKGROUNDTASKS_PROFILE_DATA.backgroundTasksProfiles + ); + + let saltedPath = profileData.backgroundTasksProfiles[0].path; + Assert.ok( + saltedPath.endsWith( + `.${vendor}BackgroundTask-${hash}-not_ephemeral_profile` + ), + `${saltedPath} ends with ".${vendor}BackgroundTask-${hash}-not_ephemeral_profile"` + ); + Assert.ok( + !saltedPath.startsWith( + `.${vendor}BackgroundTask-${hash}-not_ephemeral_profile` + ), + `${saltedPath} is really salted` + ); + Assert.deepEqual(profileData.backgroundTasksProfiles[0], { + name: `${vendor}BackgroundTask-${hash}-not_ephemeral_profile`, + path: saltedPath, + }); + + Assert.ok( + rootDir.path.endsWith(saltedPath), + `rootDir "${rootDir.path}" ends with salted path "${saltedPath}"` + ); + + // Really, "UAppData", but this is xpcshell. + let backgroundTasksProfilesPath = gDataHome; + if (!AppConstants.XP_UNIX || AppConstants.platform === "macosx") { + backgroundTasksProfilesPath.append("Background Tasks Profiles"); + } + Assert.ok( + rootDir.path.startsWith(backgroundTasksProfilesPath.path), + `rootDir "${rootDir.path}" is sibling to user profiles directory ${backgroundTasksProfilesPath}` + ); +}); diff --git a/toolkit/profile/xpcshell/test_select_backgroundtasks_not_ephemeral_exists.js b/toolkit/profile/xpcshell/test_select_backgroundtasks_not_ephemeral_exists.js new file mode 100644 index 0000000000..6fa3ec5882 --- /dev/null +++ b/toolkit/profile/xpcshell/test_select_backgroundtasks_not_ephemeral_exists.js @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Verify that background tasks that use non-ephemeral profiles re-use existing + * salted profile locations from `profiles.ini`. + */ + +let condition = { + skip_if: () => !AppConstants.MOZ_BACKGROUNDTASKS, +}; + +// MOZ_APP_VENDOR is empty on Thunderbird. +let vendor = AppConstants.MOZ_APP_NAME == "thunderbird" ? "" : "Mozilla"; + +add_task(condition, async () => { + let hash = xreDirProvider.getInstallHash(); + + let saltedPath = `saltSALT.${vendor}BackgroundTask-${hash}-not_ephemeral_profile`; + + // See note about ordering below. + BACKGROUNDTASKS_PROFILE_DATA.backgroundTasksProfiles.splice(0, 0, { + name: `${vendor}BackgroundTask-${hash}-not_ephemeral_profile`, + path: saltedPath, + }); + + writeProfilesIni(BACKGROUNDTASKS_PROFILE_DATA); + + // Pretend that this is a background task. For a task that does *not* use an + // ephemeral profile, i.e., that uses a persistent profile, when + // `profiles.ini` section `BackgroundTasksProfiles` contains the relevant + // entry, that profile path is re-used. + const bts = Cc["@mozilla.org/backgroundtasks;1"].getService( + Ci.nsIBackgroundTasks + ); + // "not_ephemeral_profile" is a special name recognized by the + // background task system. + bts.overrideBackgroundTaskNameForTesting("not_ephemeral_profile"); + + let { didCreate, rootDir } = selectStartupProfile(); + checkStartupReason("backgroundtask-not-ephemeral"); + + Assert.equal(didCreate, false, "Re-used existing non-ephemeral profile"); + + let profileData = readProfilesIni(); + + // Profile names are lexicographically ordered, and `not_ephemeral_profile` + // sorts before `unrelated_task`. + Assert.equal(profileData.backgroundTasksProfiles.length, 2); + Assert.deepEqual(profileData, BACKGROUNDTASKS_PROFILE_DATA); + + Assert.ok( + rootDir.path.endsWith(saltedPath), + `rootDir "${rootDir.path}" ends with salted path "${saltedPath}"` + ); + + // Really, "UAppData", but this is xpcshell. + let backgroundTasksProfilesPath = gDataHome; + if (!AppConstants.XP_UNIX || AppConstants.platform === "macosx") { + backgroundTasksProfilesPath.append("Background Tasks Profiles"); + } + Assert.ok( + rootDir.path.startsWith(backgroundTasksProfilesPath.path), + `rootDir "${rootDir.path}" is sibling to user profiles directory ${backgroundTasksProfilesPath}` + ); +}); diff --git a/toolkit/profile/xpcshell/test_select_default.js b/toolkit/profile/xpcshell/test_select_default.js new file mode 100644 index 0000000000..df4b27df7e --- /dev/null +++ b/toolkit/profile/xpcshell/test_select_default.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests that from a database of profiles the default profile is selected. + */ + +add_task(async () => { + let hash = xreDirProvider.getInstallHash(); + + let profileData = { + options: { + startWithLastProfile: true, + }, + profiles: [ + { + name: "Profile1", + path: "Path1", + }, + { + name: "Profile3", + path: "Path3", + }, + ], + installs: { + [hash]: { + default: "Path2", + }, + }, + }; + + if (AppConstants.MOZ_DEV_EDITION) { + profileData.profiles.push( + { + name: "default", + path: "Path2", + default: true, + }, + { + name: PROFILE_DEFAULT, + path: "Path4", + } + ); + } else { + profileData.profiles.push({ + name: PROFILE_DEFAULT, + path: "Path2", + default: true, + }); + } + + writeProfilesIni(profileData); + + let { profile, didCreate } = selectStartupProfile(); + checkStartupReason("default"); + + let service = getProfileService(); + checkProfileService(profileData); + + Assert.ok(!didCreate, "Should not have created a new profile."); + Assert.equal( + profile, + service.defaultProfile, + "Should have returned the default profile." + ); + Assert.equal( + profile.name, + "default", + "Should have selected the right profile" + ); +}); diff --git a/toolkit/profile/xpcshell/test_select_environment.js b/toolkit/profile/xpcshell/test_select_environment.js new file mode 100644 index 0000000000..f7e3138191 --- /dev/null +++ b/toolkit/profile/xpcshell/test_select_environment.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests that the environment variables are used to select a profile. + */ + +add_task(async () => { + let dir = makeRandomProfileDir("foo"); + + let profileData = { + options: { + startWithLastProfile: true, + }, + profiles: [ + { + name: "Profile1", + path: dir.leafName, + }, + { + name: "Profile2", + path: "Path2", + default: true, + }, + { + name: "Profile3", + path: "Path3", + }, + ], + }; + + writeProfilesIni(profileData); + checkProfileService(profileData); + + Services.env.set("XRE_PROFILE_PATH", dir.path); + Services.env.set("XRE_PROFILE_LOCAL_PATH", dir.path); + + let { rootDir, localDir, profile, didCreate } = selectStartupProfile(); + checkStartupReason("restart"); + + Assert.ok(!didCreate, "Should not have created a new profile."); + Assert.ok(rootDir.equals(dir), "Should have selected the right root dir."); + Assert.ok(localDir.equals(dir), "Should have selected the right local dir."); + Assert.ok(!profile, "No named profile matches this."); +}); diff --git a/toolkit/profile/xpcshell/test_select_environment_named.js b/toolkit/profile/xpcshell/test_select_environment_named.js new file mode 100644 index 0000000000..348ce0ff0a --- /dev/null +++ b/toolkit/profile/xpcshell/test_select_environment_named.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests that the environment variables are used to select a profile. + */ + +add_task(async () => { + let root = makeRandomProfileDir("foo"); + let local = gDataHomeLocal.clone(); + local.append("foo"); + + let profileData = { + options: { + startWithLastProfile: true, + }, + profiles: [ + { + name: "Profile1", + path: root.leafName, + }, + { + name: "Profile2", + path: "Path2", + default: true, + }, + { + name: "Profile3", + path: "Path3", + }, + ], + }; + + writeProfilesIni(profileData); + checkProfileService(profileData); + + Services.env.set("XRE_PROFILE_PATH", root.path); + Services.env.set("XRE_PROFILE_LOCAL_PATH", local.path); + + let { rootDir, localDir, profile, didCreate } = selectStartupProfile([ + "-P", + "Profile3", + ]); + checkStartupReason("restart"); + + Assert.ok(!didCreate, "Should not have created a new profile."); + Assert.ok(rootDir.equals(root), "Should have selected the right root dir."); + Assert.ok( + localDir.equals(local), + "Should have selected the right local dir." + ); + Assert.ok(profile, "A named profile matches this."); + Assert.equal(profile.name, "Profile1", "The right profile was matched."); + + let service = getProfileService(); + Assert.notEqual( + service.defaultProfile, + profile, + "Should not be the default profile." + ); +}); diff --git a/toolkit/profile/xpcshell/test_select_missing.js b/toolkit/profile/xpcshell/test_select_missing.js new file mode 100644 index 0000000000..97da6d76b3 --- /dev/null +++ b/toolkit/profile/xpcshell/test_select_missing.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests that when choosing an unknown profile the profile manager is shown. + */ + +add_task(async () => { + let profileData = { + options: { + startWithLastProfile: true, + }, + profiles: [ + { + name: "Profile1", + path: "Path1", + }, + { + name: "Profile2", + path: "Path2", + default: true, + }, + { + name: "Profile3", + path: "Path3", + }, + ], + }; + + writeProfilesIni(profileData); + checkProfileService(profileData); + + testStartsProfileManager(["-P", "foo"]); +}); diff --git a/toolkit/profile/xpcshell/test_select_named.js b/toolkit/profile/xpcshell/test_select_named.js new file mode 100644 index 0000000000..abecba1f4d --- /dev/null +++ b/toolkit/profile/xpcshell/test_select_named.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests that from a database of profiles the correct profile is selected. + */ + +add_task(async () => { + let profileData = { + options: { + startWithLastProfile: true, + }, + profiles: [ + { + name: "Profile1", + path: "Path1", + }, + { + name: "Profile2", + path: "Path2", + default: true, + }, + { + name: "Profile3", + path: "Path3", + }, + ], + }; + + writeProfilesIni(profileData); + + checkProfileService(profileData); + + let { profile, didCreate } = selectStartupProfile(["-P", "Profile1"]); + checkStartupReason("argument-p"); + + Assert.ok(!didCreate, "Should not have created a new profile."); + Assert.equal( + profile.name, + "Profile1", + "Should have chosen the right profile" + ); +}); diff --git a/toolkit/profile/xpcshell/test_select_noname.js b/toolkit/profile/xpcshell/test_select_noname.js new file mode 100644 index 0000000000..278b38089c --- /dev/null +++ b/toolkit/profile/xpcshell/test_select_noname.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests that when passing the -P command line argument and not passing a + * profile name the profile manager is opened. + */ + +add_task(async () => { + let profileData = { + options: { + startWithLastProfile: true, + }, + profiles: [ + { + name: "Profile1", + path: "Path1", + }, + { + name: "Profile2", + path: "Path2", + default: true, + }, + { + name: "Profile3", + path: "Path3", + }, + ], + }; + + writeProfilesIni(profileData); + checkProfileService(profileData); + + testStartsProfileManager(["-P"]); +}); diff --git a/toolkit/profile/xpcshell/test_select_profile_argument.js b/toolkit/profile/xpcshell/test_select_profile_argument.js new file mode 100644 index 0000000000..70be506538 --- /dev/null +++ b/toolkit/profile/xpcshell/test_select_profile_argument.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests that selecting a profile directory with the "profile" argument finds + * the matching profile. + */ + +add_task(async () => { + let root = makeRandomProfileDir("foo"); + let local = gDataHomeLocal.clone(); + local.append("foo"); + + let profileData = { + options: { + startWithLastProfile: true, + }, + profiles: [ + { + name: "Profile1", + path: root.leafName, + }, + ], + }; + + writeProfilesIni(profileData); + checkProfileService(profileData); + + let { rootDir, localDir, profile, didCreate } = selectStartupProfile([ + "-profile", + root.path, + ]); + checkStartupReason("argument-profile"); + + Assert.ok(!didCreate, "Should not have created a new profile."); + Assert.ok(rootDir.equals(root), "Should have selected the right root dir."); + Assert.ok( + localDir.equals(local), + "Should have selected the right local dir." + ); + Assert.ok(profile, "A named profile matches this."); + Assert.equal(profile.name, "Profile1", "The right profile was matched."); + + let service = getProfileService(); + Assert.notEqual( + service.defaultProfile, + profile, + "Should not be the default profile." + ); +}); diff --git a/toolkit/profile/xpcshell/test_select_profile_argument_new.js b/toolkit/profile/xpcshell/test_select_profile_argument_new.js new file mode 100644 index 0000000000..570abbd19f --- /dev/null +++ b/toolkit/profile/xpcshell/test_select_profile_argument_new.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests that selecting a profile directory with the "profile" argument finds + * doesn't match the incorrect profile. + */ + +add_task(async () => { + let root = makeRandomProfileDir("foo"); + let local = gDataHomeLocal.clone(); + local.append("foo"); + let empty = makeRandomProfileDir("empty"); + + let profileData = { + options: { + startWithLastProfile: true, + }, + profiles: [ + { + name: "Profile1", + path: root.leafName, + }, + ], + }; + + writeProfilesIni(profileData); + checkProfileService(profileData); + + let { rootDir, localDir, profile, didCreate } = selectStartupProfile([ + "-profile", + empty.path, + ]); + checkStartupReason("argument-profile"); + + Assert.ok(!didCreate, "Should not have created a new profile."); + Assert.ok(rootDir.equals(empty), "Should have selected the right root dir."); + Assert.ok( + localDir.equals(empty), + "Should have selected the right local dir." + ); + Assert.ok(!profile, "No named profile matches this."); +}); diff --git a/toolkit/profile/xpcshell/test_select_profilemanager.js b/toolkit/profile/xpcshell/test_select_profilemanager.js new file mode 100644 index 0000000000..7c8bdd820e --- /dev/null +++ b/toolkit/profile/xpcshell/test_select_profilemanager.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests that when requested the profile manager is shown. + */ + +add_task(async () => { + let profileData = { + options: { + startWithLastProfile: true, + }, + profiles: [ + { + name: "Profile1", + path: "Path1", + }, + { + name: "Profile2", + path: "Path2", + default: true, + }, + { + name: "Profile3", + path: "Path3", + }, + ], + }; + + writeProfilesIni(profileData); + checkProfileService(profileData); + + testStartsProfileManager(["-profilemanager"]); +}); diff --git a/toolkit/profile/xpcshell/test_single_profile_selected.js b/toolkit/profile/xpcshell/test_single_profile_selected.js new file mode 100644 index 0000000000..1dc1f5c872 --- /dev/null +++ b/toolkit/profile/xpcshell/test_single_profile_selected.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Previous versions of Firefox automatically used a single profile even if it + * wasn't marked as the default. So we should try to upgrade that one if it was + * last used by this build. This test checks the case where it was. + */ + +add_task(async () => { + let defaultProfile = makeRandomProfileDir("default"); + + writeCompatibilityIni(defaultProfile); + + writeProfilesIni({ + profiles: [ + { + name: "default", + path: defaultProfile.leafName, + default: false, + }, + ], + }); + + let { profile: selectedProfile, didCreate } = selectStartupProfile(); + checkStartupReason("firstrun-claimed-default"); + + let hash = xreDirProvider.getInstallHash(); + let profileData = readProfilesIni(); + + Assert.ok( + profileData.options.startWithLastProfile, + "Should be set to start with the last profile." + ); + Assert.equal( + profileData.profiles.length, + 1, + "Should have the right number of profiles." + ); + + let profile = profileData.profiles[0]; + Assert.equal(profile.name, "default", "Should have the right name."); + Assert.equal( + profile.path, + defaultProfile.leafName, + "Should be the original default profile." + ); + Assert.ok(profile.default, "Should be marked as the old-style default."); + + Assert.equal( + Object.keys(profileData.installs).length, + 1, + "Should be only one known install." + ); + Assert.equal( + profileData.installs[hash].default, + defaultProfile.leafName, + "Should have marked the original default profile as the default for this install." + ); + Assert.ok( + !profileData.installs[hash].locked, + "Should not have locked as we're not the default app." + ); + + checkProfileService(profileData); + + Assert.ok(!didCreate, "Should not have created a new profile."); + Assert.ok( + selectedProfile.rootDir.equals(defaultProfile), + "Should be using the right directory." + ); + Assert.equal(selectedProfile.name, "default"); +}); diff --git a/toolkit/profile/xpcshell/test_single_profile_unselected.js b/toolkit/profile/xpcshell/test_single_profile_unselected.js new file mode 100644 index 0000000000..3ad36de387 --- /dev/null +++ b/toolkit/profile/xpcshell/test_single_profile_unselected.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Previous versions of Firefox automatically used a single profile even if it + * wasn't marked as the default. So we should try to upgrade that one if it was + * last used by this build. This test checks the case where it wasn't. + */ + +add_task(async () => { + let defaultProfile = makeRandomProfileDir("default"); + + // Just pretend this profile was last used by something in the profile dir. + let greDir = gProfD.clone(); + greDir.append("app"); + writeCompatibilityIni(defaultProfile, greDir, greDir); + + writeProfilesIni({ + profiles: [ + { + name: "default", + path: defaultProfile.leafName, + default: false, + }, + ], + }); + + let profileData = readProfilesIni(); + + Assert.ok( + profileData.options.startWithLastProfile, + "Should be set to start with the last profile." + ); + Assert.equal( + profileData.profiles.length, + 1, + "Should have the right number of profiles." + ); + + let profile = profileData.profiles[0]; + Assert.equal(profile.name, "default", "Should have the right name."); + Assert.equal( + profile.path, + defaultProfile.leafName, + "Should be the original default profile." + ); + Assert.ok(!profile.default, "Should not be marked as the old-style default."); + + Assert.ok(!profileData.installs, "Should be no defaults for installs yet."); + + checkProfileService(profileData); + + let { profile: selectedProfile, didCreate } = selectStartupProfile(); + checkStartupReason("firstrun-skipped-default"); + Assert.ok(didCreate, "Should have created a new profile."); + Assert.ok( + !selectedProfile.rootDir.equals(defaultProfile), + "Should be using the right directory." + ); + Assert.equal(selectedProfile.name, DEDICATED_NAME); + + profileData = readProfilesIni(); + + profile = profileData.profiles[0]; + Assert.equal(profile.name, "default", "Should have the right name."); + Assert.equal( + profile.path, + defaultProfile.leafName, + "Should be the original default profile." + ); + Assert.ok(profile.default, "Should now be marked as the old-style default."); + + checkProfileService(profileData); +}); diff --git a/toolkit/profile/xpcshell/test_skip_locked_environment.js b/toolkit/profile/xpcshell/test_skip_locked_environment.js new file mode 100644 index 0000000000..f98f6dd88f --- /dev/null +++ b/toolkit/profile/xpcshell/test_skip_locked_environment.js @@ -0,0 +1,131 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests that the environment variables are used to select a profile and that + * on the first run of a dedicated profile build we don't snatch it if it is + * locked by another install. + */ + +add_task(async () => { + let root = makeRandomProfileDir("foo"); + let local = gDataHomeLocal.clone(); + local.append("foo"); + + writeCompatibilityIni(root); + + let profileData = { + options: { + startWithLastProfile: true, + }, + profiles: [ + { + name: PROFILE_DEFAULT, + path: root.leafName, + default: true, + }, + { + name: "Profile2", + path: "Path2", + }, + { + name: "Profile3", + path: "Path3", + }, + ], + // Another install is using the profile and it is locked. + installs: { + otherinstall: { + default: root.leafName, + locked: true, + }, + }, + }; + + writeProfilesIni(profileData); + checkProfileService(profileData); + + Services.env.set("XRE_PROFILE_PATH", root.path); + Services.env.set("XRE_PROFILE_LOCAL_PATH", local.path); + + let { rootDir, localDir, profile, didCreate } = selectStartupProfile(); + checkStartupReason("restart-skipped-default"); + + // Since there is already a profile with the desired name on dev-edition, a + // unique version will be used. + let expectedName = AppConstants.MOZ_DEV_EDITION + ? `${DEDICATED_NAME}-1` + : DEDICATED_NAME; + + Assert.ok(didCreate, "Should have created a new profile."); + Assert.ok(!rootDir.equals(root), "Should have selected the right root dir."); + Assert.ok( + !localDir.equals(local), + "Should have selected the right local dir." + ); + Assert.ok(profile, "A named profile was returned."); + Assert.equal(profile.name, expectedName, "The right profile name was used."); + + let service = getProfileService(); + Assert.equal( + service.defaultProfile, + profile, + "Should be the default profile." + ); + Assert.equal( + service.currentProfile, + profile, + "Should be the current profile." + ); + + profileData = readProfilesIni(); + + Assert.equal( + profileData.profiles[0].name, + PROFILE_DEFAULT, + "Should be the right profile." + ); + Assert.ok( + profileData.profiles[0].default, + "Should be the old default profile." + ); + Assert.equal( + profileData.profiles[0].path, + root.leafName, + "Should be the correct path." + ); + Assert.equal( + profileData.profiles[1].name, + expectedName, + "Should be the right profile." + ); + Assert.ok( + !profileData.profiles[1].default, + "Should not be the old default profile." + ); + + let hash = xreDirProvider.getInstallHash(); + Assert.equal( + Object.keys(profileData.installs).length, + 2, + "Should be one known install." + ); + Assert.notEqual( + profileData.installs[hash].default, + root.leafName, + "Should have marked the original default profile as the default for this install." + ); + Assert.ok( + profileData.installs[hash].locked, + "Should have locked as we created the profile for this install." + ); + Assert.equal( + profileData.installs.otherinstall.default, + root.leafName, + "Should have left the other profile as the default for the other install." + ); + Assert.ok( + profileData.installs[hash].locked, + "Should still be locked to the other install." + ); +}); diff --git a/toolkit/profile/xpcshell/test_snap.js b/toolkit/profile/xpcshell/test_snap.js new file mode 100644 index 0000000000..c29c85089e --- /dev/null +++ b/toolkit/profile/xpcshell/test_snap.js @@ -0,0 +1,65 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests that an old-style default profile not previously used by this build gets + * used in a snap environment. + */ + +add_task(async () => { + let defaultProfile = makeRandomProfileDir("default"); + + // Just pretend this profile was last used by something in the profile dir. + let greDir = gProfD.clone(); + greDir.append("app"); + writeCompatibilityIni(defaultProfile, greDir, greDir); + + writeProfilesIni({ + profiles: [ + { + name: PROFILE_DEFAULT, + path: defaultProfile.leafName, + default: true, + }, + ], + }); + + let { profile: selectedProfile, didCreate } = selectStartupProfile(); + checkStartupReason("default"); + + let profileData = readProfilesIni(); + let installsINI = gDataHome.clone(); + installsINI.append("installs.ini"); + Assert.ok( + !installsINI.exists(), + "Installs database should not have been created." + ); + + Assert.ok( + profileData.options.startWithLastProfile, + "Should be set to start with the last profile." + ); + Assert.equal( + profileData.profiles.length, + 1, + "Should have the right number of profiles." + ); + + let profile = profileData.profiles[0]; + Assert.equal(profile.name, PROFILE_DEFAULT, "Should have the right name."); + Assert.equal( + profile.path, + defaultProfile.leafName, + "Should be the original default profile." + ); + Assert.ok(profile.default, "Should be marked as the old-style default."); + + checkProfileService(profileData); + + Assert.ok(!didCreate, "Should not have created a new profile."); + Assert.ok( + selectedProfile.rootDir.equals(defaultProfile), + "Should be using the right directory." + ); + Assert.equal(selectedProfile.name, PROFILE_DEFAULT); +}); diff --git a/toolkit/profile/xpcshell/test_snap_empty.js b/toolkit/profile/xpcshell/test_snap_empty.js new file mode 100644 index 0000000000..80b1de9569 --- /dev/null +++ b/toolkit/profile/xpcshell/test_snap_empty.js @@ -0,0 +1,26 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests that from a clean slate snap builds create an appropriate profile. + */ + +add_task(async () => { + let service = getProfileService(); + let { profile, didCreate } = selectStartupProfile(); + checkStartupReason("firstrun-created-default"); + + Assert.ok(didCreate, "Should have created a new profile."); + Assert.equal( + profile.name, + PROFILE_DEFAULT, + "Should have used the normal name." + ); + if (AppConstants.MOZ_DEV_EDITION) { + Assert.equal(service.profileCount, 2, "Should be two profiles."); + } else { + Assert.equal(service.profileCount, 1, "Should be only one profile."); + } + + checkProfileService(); +}); diff --git a/toolkit/profile/xpcshell/test_snatch_environment.js b/toolkit/profile/xpcshell/test_snatch_environment.js new file mode 100644 index 0000000000..2241fb691c --- /dev/null +++ b/toolkit/profile/xpcshell/test_snatch_environment.js @@ -0,0 +1,101 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests that the environment variables are used to select a profile and that + * on the first run of a dedicated profile build we snatch it if it was the + * default profile. + */ + +add_task(async () => { + let root = makeRandomProfileDir("foo"); + let local = gDataHomeLocal.clone(); + local.append("foo"); + + writeCompatibilityIni(root); + + let profileData = { + options: { + startWithLastProfile: true, + }, + profiles: [ + { + name: PROFILE_DEFAULT, + path: root.leafName, + default: true, + }, + { + name: "Profile2", + path: "Path2", + }, + { + name: "Profile3", + path: "Path3", + }, + ], + // Another install is using the profile but it isn't locked. + installs: { + otherinstall: { + default: root.leafName, + }, + }, + }; + + writeProfilesIni(profileData); + checkProfileService(profileData); + + Services.env.set("XRE_PROFILE_PATH", root.path); + Services.env.set("XRE_PROFILE_LOCAL_PATH", local.path); + + let { rootDir, localDir, profile, didCreate } = selectStartupProfile(); + checkStartupReason("restart-claimed-default"); + + Assert.ok(!didCreate, "Should not have created a new profile."); + Assert.ok(rootDir.equals(root), "Should have selected the right root dir."); + Assert.ok( + localDir.equals(local), + "Should have selected the right local dir." + ); + Assert.ok(profile, "A named profile matches this."); + Assert.equal(profile.name, PROFILE_DEFAULT, "The right profile was matched."); + + let service = getProfileService(); + Assert.equal( + service.defaultProfile, + profile, + "Should be the default profile." + ); + Assert.equal( + service.currentProfile, + profile, + "Should be the current profile." + ); + + profileData = readProfilesIni(); + Assert.equal( + profileData.profiles[0].name, + PROFILE_DEFAULT, + "Should be the right profile." + ); + Assert.ok( + profileData.profiles[0].default, + "Should still be the old default profile." + ); + + let hash = xreDirProvider.getInstallHash(); + // The info about the other install will have been removed so it goes through first run on next startup. + Assert.equal( + Object.keys(profileData.installs).length, + 1, + "Should be one known install." + ); + Assert.equal( + profileData.installs[hash].default, + root.leafName, + "Should have marked the original default profile as the default for this install." + ); + Assert.ok( + !profileData.installs[hash].locked, + "Should not have locked as we're not the default app." + ); +}); diff --git a/toolkit/profile/xpcshell/test_snatch_environment_default.js b/toolkit/profile/xpcshell/test_snatch_environment_default.js new file mode 100644 index 0000000000..d93a0cfc12 --- /dev/null +++ b/toolkit/profile/xpcshell/test_snatch_environment_default.js @@ -0,0 +1,101 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests that the environment variables are used to select a profile and that + * on the first run of a dedicated profile build we snatch it if it was the + * default profile and lock it when we're the default app. + */ + +add_task(async () => { + gIsDefaultApp = true; + + let root = makeRandomProfileDir("foo"); + let local = gDataHomeLocal.clone(); + local.append("foo"); + + writeCompatibilityIni(root); + + let profileData = { + options: { + startWithLastProfile: true, + }, + profiles: [ + { + name: PROFILE_DEFAULT, + path: root.leafName, + default: true, + }, + { + name: "Profile2", + path: "Path2", + }, + { + name: "Profile3", + path: "Path3", + }, + ], + // Another install is using the profile but it isn't locked. + installs: { + otherinstall: { + default: root.leafName, + }, + }, + }; + + writeProfilesIni(profileData); + checkProfileService(profileData); + + Services.env.set("XRE_PROFILE_PATH", root.path); + Services.env.set("XRE_PROFILE_LOCAL_PATH", local.path); + + let { rootDir, localDir, profile, didCreate } = selectStartupProfile(); + checkStartupReason("restart-claimed-default"); + + Assert.ok(!didCreate, "Should not have created a new profile."); + Assert.ok(rootDir.equals(root), "Should have selected the right root dir."); + Assert.ok( + localDir.equals(local), + "Should have selected the right local dir." + ); + Assert.ok(!!profile, "A named profile matches this."); + Assert.equal(profile.name, PROFILE_DEFAULT, "The right profile was matched."); + + let service = getProfileService(); + Assert.ok( + service.defaultProfile === profile, + "Should be the default profile." + ); + Assert.ok( + service.currentProfile === profile, + "Should be the current profile." + ); + + profileData = readProfilesIni(); + Assert.equal( + profileData.profiles[0].name, + PROFILE_DEFAULT, + "Should be the right profile." + ); + Assert.ok( + profileData.profiles[0].default, + "Should still be the old default profile." + ); + + let hash = xreDirProvider.getInstallHash(); + // The info about the other install will have been removed so it goes through first run on next startup. + Assert.equal( + Object.keys(profileData.installs).length, + 1, + "Should be one known install." + ); + Assert.equal( + profileData.installs[hash].default, + root.leafName, + "Should have marked the original default profile as the default for this install." + ); + Assert.ok( + profileData.installs[hash].locked, + "Should have locked as we're the default app." + ); +}); diff --git a/toolkit/profile/xpcshell/test_startswithlast.js b/toolkit/profile/xpcshell/test_startswithlast.js new file mode 100644 index 0000000000..1b1fef4415 --- /dev/null +++ b/toolkit/profile/xpcshell/test_startswithlast.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests that if profiles.ini is set to not start with the last profile then + * we show the profile manager in preference to assigning the old default. + */ + +add_task(async () => { + let defaultProfile = makeRandomProfileDir("default"); + + writeCompatibilityIni(defaultProfile); + + writeProfilesIni({ + options: { + startWithLastProfile: false, + }, + profiles: [ + { + name: PROFILE_DEFAULT, + path: defaultProfile.leafName, + default: true, + }, + ], + }); + + testStartsProfileManager(); +}); diff --git a/toolkit/profile/xpcshell/test_steal_inuse.js b/toolkit/profile/xpcshell/test_steal_inuse.js new file mode 100644 index 0000000000..68263ceae1 --- /dev/null +++ b/toolkit/profile/xpcshell/test_steal_inuse.js @@ -0,0 +1,77 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests that an old-style default profile previously used by this build but + * that has already been claimed by a different build gets stolen by this build. + */ + +add_task(async () => { + let defaultProfile = makeRandomProfileDir("default"); + + writeCompatibilityIni(defaultProfile); + + writeProfilesIni({ + profiles: [ + { + name: PROFILE_DEFAULT, + path: defaultProfile.leafName, + default: true, + }, + ], + installs: { + otherhash: { + default: defaultProfile.leafName, + }, + }, + }); + + let { profile: selectedProfile, didCreate } = selectStartupProfile(); + checkStartupReason("firstrun-claimed-default"); + + let hash = xreDirProvider.getInstallHash(); + let profileData = readProfilesIni(); + + Assert.ok( + profileData.options.startWithLastProfile, + "Should be set to start with the last profile." + ); + Assert.equal( + profileData.profiles.length, + 1, + "Should have the right number of profiles." + ); + + let profile = profileData.profiles[0]; + Assert.equal(profile.name, PROFILE_DEFAULT, "Should have the right name."); + Assert.equal( + profile.path, + defaultProfile.leafName, + "Should be the original default profile." + ); + Assert.ok(profile.default, "Should be marked as the old-style default."); + + Assert.equal( + Object.keys(profileData.installs).length, + 1, + "Should only be one known installs." + ); + Assert.equal( + profileData.installs[hash].default, + defaultProfile.leafName, + "Should have taken the original default profile as the default for the current install." + ); + Assert.ok( + !profileData.installs[hash].locked, + "Should not have locked as we're not the default app." + ); + + checkProfileService(profileData); + + Assert.ok(!didCreate, "Should not have created a new profile."); + Assert.ok( + selectedProfile.rootDir.equals(defaultProfile), + "Should be using the right directory." + ); + Assert.equal(selectedProfile.name, PROFILE_DEFAULT); +}); diff --git a/toolkit/profile/xpcshell/test_update_selected_dedicated.js b/toolkit/profile/xpcshell/test_update_selected_dedicated.js new file mode 100644 index 0000000000..5a7c734e90 --- /dev/null +++ b/toolkit/profile/xpcshell/test_update_selected_dedicated.js @@ -0,0 +1,80 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests that an old-style default profile previously used by this build gets + * updated to a dedicated profile for this build. + */ + +add_task(async () => { + let defaultProfile = makeRandomProfileDir("default"); + + writeCompatibilityIni(defaultProfile); + + writeProfilesIni({ + profiles: [ + { + name: PROFILE_DEFAULT, + path: defaultProfile.leafName, + default: true, + }, + ], + }); + + let { profile: selectedProfile, didCreate } = selectStartupProfile(); + checkStartupReason("firstrun-claimed-default"); + + let { databaseVersion, profileCount } = getTelemetryScalars(); + Assert.equal( + databaseVersion, + "1", + "Old database file was present at startup." + ); + Assert.equal(profileCount, 1, "Should be one profile."); + + let hash = xreDirProvider.getInstallHash(); + let profileData = readProfilesIni(); + + Assert.ok( + profileData.options.startWithLastProfile, + "Should be set to start with the last profile." + ); + Assert.equal( + profileData.profiles.length, + 1, + "Should have the right number of profiles." + ); + + let profile = profileData.profiles[0]; + Assert.equal(profile.name, PROFILE_DEFAULT, "Should have the right name."); + Assert.equal( + profile.path, + defaultProfile.leafName, + "Should be the original default profile." + ); + Assert.ok(profile.default, "Should be marked as the old-style default."); + + Assert.equal( + Object.keys(profileData.installs).length, + 1, + "Should be only one known install." + ); + Assert.equal( + profileData.installs[hash].default, + defaultProfile.leafName, + "Should have marked the original default profile as the default for this install." + ); + Assert.ok( + !profileData.installs[hash].locked, + "Should not have locked as we're not the default app." + ); + + checkProfileService(profileData); + + Assert.ok(!didCreate, "Should not have created a new profile."); + Assert.ok( + selectedProfile.rootDir.equals(defaultProfile), + "Should be using the right directory." + ); + Assert.equal(selectedProfile.name, PROFILE_DEFAULT); +}); diff --git a/toolkit/profile/xpcshell/test_update_unknown_dedicated.js b/toolkit/profile/xpcshell/test_update_unknown_dedicated.js new file mode 100644 index 0000000000..1fd8c30fbf --- /dev/null +++ b/toolkit/profile/xpcshell/test_update_unknown_dedicated.js @@ -0,0 +1,85 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests that an old-style default profile not previously used by any build + * doesn't get updated to a dedicated profile for this build and we don't set + * the flag to show the user info about dedicated profiles. + */ + +add_task(async () => { + let hash = xreDirProvider.getInstallHash(); + let defaultProfile = makeRandomProfileDir("default"); + + writeProfilesIni({ + profiles: [ + { + name: PROFILE_DEFAULT, + path: defaultProfile.leafName, + default: true, + }, + ], + }); + + let { profile: selectedProfile, didCreate } = selectStartupProfile(); + checkStartupReason("firstrun-created-default"); + + let profileData = readProfilesIni(); + + Assert.ok( + profileData.options.startWithLastProfile, + "Should be set to start with the last profile." + ); + Assert.equal( + profileData.profiles.length, + 2, + "Should have the right number of profiles." + ); + + // Since there is already a profile with the desired name on dev-edition, a + // unique version will be used. + let expectedName = AppConstants.MOZ_DEV_EDITION + ? `${DEDICATED_NAME}-1` + : DEDICATED_NAME; + + let profile = profileData.profiles[0]; + Assert.equal(profile.name, PROFILE_DEFAULT, "Should have the right name."); + Assert.equal( + profile.path, + defaultProfile.leafName, + "Should be the original default profile." + ); + Assert.ok(profile.default, "Should be marked as the old-style default."); + profile = profileData.profiles[1]; + Assert.equal(profile.name, expectedName, "Should have the right name."); + Assert.notEqual( + profile.path, + defaultProfile.leafName, + "Should not be the original default profile." + ); + Assert.ok(!profile.default, "Should not be marked as the old-style default."); + + Assert.equal( + Object.keys(profileData.installs).length, + 1, + "Should be a default for installs." + ); + Assert.equal( + profileData.installs[hash].default, + profile.path, + "Should have the right default profile." + ); + Assert.ok( + profileData.installs[hash].locked, + "Should have locked as we created this profile for this install." + ); + + checkProfileService(profileData); + + Assert.ok(didCreate, "Should have created a new profile."); + Assert.ok( + !selectedProfile.rootDir.equals(defaultProfile), + "Should not be using the old directory." + ); + Assert.equal(selectedProfile.name, expectedName); +}); diff --git a/toolkit/profile/xpcshell/test_update_unselected_dedicated.js b/toolkit/profile/xpcshell/test_update_unselected_dedicated.js new file mode 100644 index 0000000000..4aa56eaaac --- /dev/null +++ b/toolkit/profile/xpcshell/test_update_unselected_dedicated.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests that an old-style default profile not previously used by this build gets + * ignored. + */ + +add_task(async () => { + let hash = xreDirProvider.getInstallHash(); + let defaultProfile = makeRandomProfileDir("default"); + + // Just pretend this profile was last used by something in the profile dir. + let greDir = gProfD.clone(); + greDir.append("app"); + writeCompatibilityIni(defaultProfile, greDir, greDir); + + writeProfilesIni({ + profiles: [ + { + name: PROFILE_DEFAULT, + path: defaultProfile.leafName, + default: true, + }, + ], + }); + + let { profile: selectedProfile, didCreate } = selectStartupProfile(); + checkStartupReason("firstrun-skipped-default"); + + let profileData = readProfilesIni(); + + Assert.ok( + profileData.options.startWithLastProfile, + "Should be set to start with the last profile." + ); + Assert.equal( + profileData.profiles.length, + 2, + "Should have the right number of profiles." + ); + + // Since there is already a profile with the desired name on dev-edition, a + // unique version will be used. + let expectedName = AppConstants.MOZ_DEV_EDITION + ? `${DEDICATED_NAME}-1` + : DEDICATED_NAME; + + let profile = profileData.profiles[0]; + Assert.equal(profile.name, PROFILE_DEFAULT, "Should have the right name."); + Assert.equal( + profile.path, + defaultProfile.leafName, + "Should be the original default profile." + ); + Assert.ok(profile.default, "Should be marked as the old-style default."); + profile = profileData.profiles[1]; + Assert.equal(profile.name, expectedName, "Should have the right name."); + Assert.notEqual( + profile.path, + defaultProfile.leafName, + "Should not be the original default profile." + ); + Assert.ok(!profile.default, "Should not be marked as the old-style default."); + + Assert.equal( + Object.keys(profileData.installs).length, + 1, + "Should be a default for this install." + ); + Assert.equal( + profileData.installs[hash].default, + profile.path, + "Should have marked the new profile as the default for this install." + ); + Assert.ok( + profileData.installs[hash].locked, + "Should have locked as we created this profile for this install." + ); + + checkProfileService(profileData); + + Assert.ok(didCreate, "Should have created a new profile."); + Assert.ok( + !selectedProfile.rootDir.equals(defaultProfile), + "Should be using the right directory." + ); + Assert.equal(selectedProfile.name, expectedName); +}); diff --git a/toolkit/profile/xpcshell/test_use_dedicated.js b/toolkit/profile/xpcshell/test_use_dedicated.js new file mode 100644 index 0000000000..d83b6fc3a7 --- /dev/null +++ b/toolkit/profile/xpcshell/test_use_dedicated.js @@ -0,0 +1,107 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that if installs.ini lists a profile we use it as the default. + */ + +add_task(async () => { + let hash = xreDirProvider.getInstallHash(); + let defaultProfile = makeRandomProfileDir("default"); + let dedicatedProfile = makeRandomProfileDir("dedicated"); + let devProfile = makeRandomProfileDir("devedition"); + + // Make sure we don't steal the old-style default. + writeCompatibilityIni(defaultProfile); + + writeProfilesIni({ + profiles: [ + { + name: "default", + path: defaultProfile.leafName, + default: true, + }, + { + name: "dedicated", + path: dedicatedProfile.leafName, + }, + { + name: "dev-edition-default", + path: devProfile.leafName, + }, + ], + installs: { + [hash]: { + default: dedicatedProfile.leafName, + }, + otherhash: { + default: "foobar", + }, + }, + }); + + let { profile: selectedProfile, didCreate } = selectStartupProfile(); + checkStartupReason("default"); + let { databaseVersion, profileCount } = getTelemetryScalars(); + Assert.equal( + databaseVersion, + "2", + "New database file was present at startup." + ); + Assert.equal(profileCount, 3, "Should be three profiles present."); + + let profileData = readProfilesIni(); + + Assert.ok( + profileData.options.startWithLastProfile, + "Should be set to start with the last profile." + ); + Assert.equal( + profileData.profiles.length, + 3, + "Should have the right number of profiles." + ); + + let profile = profileData.profiles[0]; + Assert.equal(profile.name, `dedicated`, "Should have the right name."); + Assert.equal( + profile.path, + dedicatedProfile.leafName, + "Should be the expected dedicated profile." + ); + Assert.ok(!profile.default, "Should not be marked as the old-style default."); + + profile = profileData.profiles[1]; + Assert.equal(profile.name, "default", "Should have the right name."); + Assert.equal( + profile.path, + defaultProfile.leafName, + "Should be the original default profile." + ); + Assert.ok(profile.default, "Should be marked as the old-style default."); + + Assert.equal( + Object.keys(profileData.installs).length, + 2, + "Should be two known installs." + ); + Assert.equal( + profileData.installs[hash].default, + dedicatedProfile.leafName, + "Should have kept the default for this install." + ); + Assert.equal( + profileData.installs.otherhash.default, + "foobar", + "Should have kept the default for the other install." + ); + + checkProfileService(profileData); + + Assert.ok(!didCreate, "Should not have created a new profile."); + Assert.ok( + selectedProfile.rootDir.equals(dedicatedProfile), + "Should be using the right directory." + ); + Assert.equal(selectedProfile.name, "dedicated"); +}); diff --git a/toolkit/profile/xpcshell/xpcshell.toml b/toolkit/profile/xpcshell/xpcshell.toml new file mode 100644 index 0000000000..00c00cc6c4 --- /dev/null +++ b/toolkit/profile/xpcshell/xpcshell.toml @@ -0,0 +1,97 @@ +[DEFAULT] +head = "head.js" +skip-if = ["os == 'android'"] + +["test_check_backup.js"] + +["test_claim_locked.js"] + +["test_clean.js"] + +["test_conflict_installs.js"] + +["test_conflict_profiles.js"] + +["test_create_default.js"] + +["test_fix_directory_case.js"] + +["test_ignore_legacy_directory.js"] + +["test_invalid_descriptor.js"] + +["test_legacy_empty.js"] + +["test_legacy_select.js"] + +["test_lock.js"] + +["test_missing_profilesini.js"] + +["test_new_default.js"] + +["test_previous_dedicated.js"] + +["test_profile_reset.js"] + +["test_remove.js"] + +["test_remove_default.js"] + +["test_select_backgroundtasks_ephemeral.js"] + +["test_select_backgroundtasks_not_ephemeral_create.js"] + +["test_select_backgroundtasks_not_ephemeral_exists.js"] + +["test_select_default.js"] + +["test_select_environment.js"] + +["test_select_environment_named.js"] + +["test_select_missing.js"] + +["test_select_named.js"] + +["test_select_noname.js"] + +["test_select_profile_argument.js"] + +["test_select_profile_argument_new.js"] + +["test_select_profilemanager.js"] + +["test_single_profile_selected.js"] +skip-if = ["devedition"] + +["test_single_profile_unselected.js"] +skip-if = ["devedition"] + +["test_skip_locked_environment.js"] + +["test_snap.js"] +snap = true +run-if = ["os == 'linux'"] +skip-if = ["appname == 'thunderbird'"] + +["test_snap_empty.js"] +snap = true +run-if = ["os == 'linux'"] +skip-if = ["appname == 'thunderbird'"] + +["test_snatch_environment.js"] + +["test_snatch_environment_default.js"] + +["test_startswithlast.js"] + +["test_steal_inuse.js"] + +["test_update_selected_dedicated.js"] + +["test_update_unknown_dedicated.js"] + +["test_update_unselected_dedicated.js"] + +["test_use_dedicated.js"] -- cgit v1.2.3