// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <stddef.h>
#include "base/one_shot_event.h"
#include "base/run_loop.h"
#include "build/build_config.h"
#include "chrome/browser/ash/app_list/app_list_syncable_service.h"
#include "chrome/browser/ash/app_list/app_list_syncable_service_factory.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/sync/test/integration/apps_helper.h"
#include "chrome/browser/sync/test/integration/extensions_helper.h"
#include "chrome/browser/sync/test/integration/sync_app_list_helper.h"
#include "chrome/browser/sync/test/integration/sync_service_impl_harness.h"
#include "chrome/browser/sync/test/integration/sync_test.h"
#include "chrome/browser/sync/test/integration/updated_progress_marker_checker.h"
#include "components/prefs/pref_service.h"
#include "components/sync/base/pref_names.h"
#include "components/sync/base/user_selectable_type.h"
#include "components/sync/service/sync_user_settings.h"
#include "content/public/test/browser_test.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_system.h"
using apps_helper::DisableApp;
using apps_helper::EnableApp;
using apps_helper::IncognitoDisableApp;
using apps_helper::IncognitoEnableApp;
using apps_helper::InstallAppsPendingForSync;
using apps_helper::InstallHostedApp;
using apps_helper::IsAppEnabled;
using apps_helper::IsIncognitoEnabled;
using apps_helper::UninstallApp;
using syncer::SyncUserSettings;
using syncer::UserSelectableOsType;
using syncer::UserSelectableOsTypeSet;
using syncer::UserSelectableType;
using syncer::UserSelectableTypeSet;
namespace {
const size_t kNumDefaultApps = 2;
bool AllProfilesHaveSameAppList(size_t* size_out = nullptr) {
return SyncAppListHelper::GetInstance()->AllProfilesHaveSameAppList(size_out);
}
const app_list::AppListSyncableService::SyncItem* GetSyncItem(
Profile* profile,
const std::string& app_id) {
app_list::AppListSyncableService* service =
app_list::AppListSyncableServiceFactory::GetForProfile(profile);
return service->GetSyncItem(app_id);
}
} // namespace
class TwoClientAppListSyncTest : public SyncTest {
public:
TwoClientAppListSyncTest() : SyncTest(TWO_CLIENT) {}
~TwoClientAppListSyncTest() override = default;
// SyncTest
bool SetupClients() override {
if (!SyncTest::SetupClients()) {
return false;
}
// Init SyncAppListHelper to ensure that the extension system is initialized
// for each Profile.
SyncAppListHelper::GetInstance();
return true;
}
void AwaitQuiescenceAndInstallAppsPendingForSync() {
ASSERT_TRUE(AwaitQuiescence());
InstallAppsPendingForSync(GetProfile(0));
InstallAppsPendingForSync(GetProfile(1));
}
void WaitForExtensionServicesToLoad() {
for (int i = 0; i < num_clients(); ++i) {
WaitForExtensionsServiceToLoadForProfile(GetProfile(i));
}
}
private:
void WaitForExtensionsServiceToLoadForProfile(Profile* profile) {
extensions::ExtensionSystem* extension_system =
extensions::ExtensionSystem::Get(profile);
base::RunLoop run_loop;
extension_system->ready().Post(FROM_HERE, run_loop.QuitClosure());
run_loop.Run();
}
};
class RemoveDefaultAppSyncTest : public testing::WithParamInterface<bool>,
public TwoClientAppListSyncTest {
public:
RemoveDefaultAppSyncTest() = default;
RemoveDefaultAppSyncTest(const RemoveDefaultAppSyncTest&) = delete;
RemoveDefaultAppSyncTest& operator=(const RemoveDefaultAppSyncTest&) = delete;
~RemoveDefaultAppSyncTest() override = default;
bool MarkAppAsDefaultApp() { return GetParam(); }
};
IN_PROC_BROWSER_TEST_F(TwoClientAppListSyncTest, StartWithNoApps) {
ASSERT_TRUE(SetupSync());
WaitForExtensionServicesToLoad();
ASSERT_TRUE(AllProfilesHaveSameAppList());
}
IN_PROC_BROWSER_TEST_F(TwoClientAppListSyncTest, StartWithSameApps) {
ASSERT_TRUE(SetupClients());
const int kNumApps = 5;
for (int i = 0; i < kNumApps; ++i) {
InstallHostedApp(GetProfile(0), i);
InstallHostedApp(GetProfile(1), i);
}
ASSERT_TRUE(SetupSync());
WaitForExtensionServicesToLoad();
ASSERT_TRUE(AwaitQuiescence());
ASSERT_TRUE(AllProfilesHaveSameAppList());
}
// Install some apps on both clients, some on only one client, some on only the
// other, and sync. Both clients should end up with all apps, and the app and
// page ordinals should be identical.
IN_PROC_BROWSER_TEST_F(TwoClientAppListSyncTest, StartWithDifferentApps) {
ASSERT_TRUE(SetupClients());
int i = 0;
const int kNumCommonApps = 5;
for (int j = 0; j < kNumCommonApps; ++i, ++j) {
InstallHostedApp(GetProfile(0), i);
InstallHostedApp(GetProfile(1), i);
}
const int kNumProfile0Apps = 10;
for (int j = 0; j < kNumProfile0Apps; ++i, ++j) {
std::string id = InstallHostedApp(GetProfile(0), i);
}
const int kNumProfile1Apps = 10;
for (int j = 0; j < kNumProfile1Apps; ++i, ++j) {
std::string id = InstallHostedApp(GetProfile(1), i);
}
ASSERT_TRUE(SetupSync());
WaitForExtensionServicesToLoad();
AwaitQuiescenceAndInstallAppsPendingForSync();
// Verify the app lists, but ignore absolute position values, checking only
// relative positions (see note in app_list_syncable_service.h).
ASSERT_TRUE(AllProfilesHaveSameAppList());
}
// Install some apps on both clients, then sync. Then install some apps on only
// one client, some on only the other, and then sync again. Both clients should
// end up with all apps, and the app and page ordinals should be identical.
IN_PROC_BROWSER_TEST_F(TwoClientAppListSyncTest, InstallDifferentApps) {
ASSERT_TRUE(SetupClients());
int i = 0;
const int kNumCommonApps = 5;
for (int j = 0; j < kNumCommonApps; ++i, ++j) {
InstallHostedApp(GetProfile(0), i);
InstallHostedApp(GetProfile(1), i);
}
ASSERT_TRUE(SetupSync());
WaitForExtensionServicesToLoad();
ASSERT_TRUE(AwaitQuiescence());
const int kNumProfile0Apps = 10;
for (int j = 0; j < kNumProfile0Apps; ++i, ++j) {
std::string id = InstallHostedApp(GetProfile(0), i);
}
const int kNumProfile1Apps = 10;
for (int j = 0; j < kNumProfile1Apps; ++i, ++j) {
std::string id = InstallHostedApp(GetProfile(1), i);
}
AwaitQuiescenceAndInstallAppsPendingForSync();
// Verify the app lists, but ignore absolute position values, checking only
// relative positions (see note in app_list_syncable_service.h).
ASSERT_TRUE(AllProfilesHaveSameAppList());
}
IN_PROC_BROWSER_TEST_F(TwoClientAppListSyncTest, Install) {
ASSERT_TRUE(SetupSync());
WaitForExtensionServicesToLoad();
ASSERT_TRUE(AllProfilesHaveSameAppList());
InstallHostedApp(GetProfile(0), 0);
AwaitQuiescenceAndInstallAppsPendingForSync();
ASSERT_TRUE(AllProfilesHaveSameAppList());
}
IN_PROC_BROWSER_TEST_F(TwoClientAppListSyncTest, Uninstall) {
ASSERT_TRUE(SetupSync());
WaitForExtensionServicesToLoad();
ASSERT_TRUE(AllProfilesHaveSameAppList());
InstallHostedApp(GetProfile(0), 0);
AwaitQuiescenceAndInstallAppsPendingForSync();
ASSERT_TRUE(AllProfilesHaveSameAppList());
UninstallApp(GetProfile(0), 0);
ASSERT_TRUE(AwaitQuiescence());
ASSERT_TRUE(AllProfilesHaveSameAppList());
}
// Install an app on one client, then sync. Then uninstall the app on the first
// client and sync again. Now install a new app on the first client and sync.
// Both client should only have the second app, with identical app and page
// ordinals.
IN_PROC_BROWSER_TEST_F(TwoClientAppListSyncTest, UninstallThenInstall) {
ASSERT_TRUE(SetupSync());
WaitForExtensionServicesToLoad();
ASSERT_TRUE(AllProfilesHaveSameAppList());
InstallHostedApp(GetProfile(0), 0);
AwaitQuiescenceAndInstallAppsPendingForSync();
ASSERT_TRUE(AllProfilesHaveSameAppList());
UninstallApp(GetProfile(0), 0);
ASSERT_TRUE(AwaitQuiescence());
ASSERT_TRUE(AllProfilesHaveSameAppList());
InstallHostedApp(GetProfile(0), 1);
AwaitQuiescenceAndInstallAppsPendingForSync();
ASSERT_TRUE(AllProfilesHaveSameAppList());
}
IN_PROC_BROWSER_TEST_F(TwoClientAppListSyncTest, Merge) {
ASSERT_TRUE(SetupSync());
WaitForExtensionServicesToLoad();
ASSERT_TRUE(AllProfilesHaveSameAppList());
InstallHostedApp(GetProfile(0), 0);
InstallHostedApp(GetProfile(1), 0);
ASSERT_TRUE(AwaitQuiescence());
UninstallApp(GetProfile(0), 0);
InstallHostedApp(GetProfile(0), 1);
InstallHostedApp(GetProfile(0), 2);
InstallHostedApp(GetProfile(1), 2);
InstallHostedApp(GetProfile(1), 3);
AwaitQuiescenceAndInstallAppsPendingForSync();
ASSERT_TRUE(AllProfilesHaveSameAppList());
}
IN_PROC_BROWSER_TEST_F(TwoClientAppListSyncTest, UpdateEnableDisableApp) {
ASSERT_TRUE(SetupSync());
WaitForExtensionServicesToLoad();
ASSERT_TRUE(AllProfilesHaveSameAppList());
InstallHostedApp(GetProfile(0), 0);
InstallHostedApp(GetProfile(1), 0);
ASSERT_TRUE(AwaitQuiescence());
ASSERT_TRUE(AllProfilesHaveSameAppList());
ASSERT_TRUE(IsAppEnabled(GetProfile(0), 0));
ASSERT_TRUE(IsAppEnabled(GetProfile(1), 0));
DisableApp(GetProfile(0), 0);
ASSERT_TRUE(AwaitQuiescence());
ASSERT_TRUE(AllProfilesHaveSameAppList());
ASSERT_FALSE(IsAppEnabled(GetProfile(0), 0));
ASSERT_FALSE(IsAppEnabled(GetProfile(1), 0));
EnableApp(GetProfile(1), 0);
ASSERT_TRUE(AwaitQuiescence());
ASSERT_TRUE(AllProfilesHaveSameAppList());
ASSERT_TRUE(IsAppEnabled(GetProfile(0), 0));
ASSERT_TRUE(IsAppEnabled(GetProfile(1), 0));
}
IN_PROC_BROWSER_TEST_F(TwoClientAppListSyncTest, UpdateIncognitoEnableDisable) {
ASSERT_TRUE(SetupSync());
WaitForExtensionServicesToLoad();
ASSERT_TRUE(AllProfilesHaveSameAppList());
InstallHostedApp(GetProfile(0), 0);
InstallHostedApp(GetProfile(1), 0);
ASSERT_TRUE(AwaitQuiescence());
ASSERT_TRUE(AllProfilesHaveSameAppList());
ASSERT_FALSE(IsIncognitoEnabled(GetProfile(0), 0));
ASSERT_FALSE(IsIncognitoEnabled(GetProfile(1), 0));
IncognitoEnableApp(GetProfile(0), 0);
ASSERT_TRUE(AwaitQuiescence());
ASSERT_TRUE(AllProfilesHaveSameAppList());
ASSERT_TRUE(IsIncognitoEnabled(GetProfile(0), 0));
ASSERT_TRUE(IsIncognitoEnabled(GetProfile(1), 0));
IncognitoDisableApp(GetProfile(1), 0);
ASSERT_TRUE(AwaitQuiescence());
ASSERT_TRUE(AllProfilesHaveSameAppList());
ASSERT_FALSE(IsIncognitoEnabled(GetProfile(0), 0));
ASSERT_FALSE(IsIncognitoEnabled(GetProfile(1), 0));
}
IN_PROC_BROWSER_TEST_F(TwoClientAppListSyncTest, DisableApps) {
ASSERT_TRUE(SetupSync());
WaitForExtensionServicesToLoad();
ASSERT_TRUE(AllProfilesHaveSameAppList());
SyncUserSettings* settings = GetClient(1)->service()->GetUserSettings();
{
// Disable APP_LIST by disabling apps sync.
UserSelectableOsTypeSet types = settings->GetSelectedOsTypes();
types.Remove(UserSelectableOsType::kOsApps);
settings->SetSelectedOsTypes(/*sync_all_os_types=*/false, types);
InstallHostedApp(GetProfile(0), 0);
ASSERT_TRUE(UpdatedProgressMarkerChecker(GetSyncService(0)).Wait());
ASSERT_FALSE(AllProfilesHaveSameAppList());
}
{
// Enable APP_LIST by enabling apps sync.
UserSelectableOsTypeSet types = settings->GetSelectedOsTypes();
types.Put(UserSelectableOsType::kOsApps);
settings->SetSelectedOsTypes(/*sync_all_os_types=*/false, types);
AwaitQuiescenceAndInstallAppsPendingForSync();
}
ASSERT_TRUE(AllProfilesHaveSameAppList());
}
// Disable sync for the second client and then install an app on the first
// client, then enable sync on the second client. Both clients should have the
// same app with identical app and page ordinals.
IN_PROC_BROWSER_TEST_F(TwoClientAppListSyncTest, DisableSync) {
ASSERT_TRUE(SetupSync());
WaitForExtensionServicesToLoad();
ASSERT_TRUE(AllProfilesHaveSameAppList());
ASSERT_TRUE(GetClient(1)->DisableSyncForAllDatatypes());
InstallHostedApp(GetProfile(0), 0);
ASSERT_TRUE(UpdatedProgressMarkerChecker(GetSyncService(0)).Wait());
ASSERT_FALSE(AllProfilesHaveSameAppList());
ASSERT_TRUE(GetClient(1)->EnableSyncForRegisteredDatatypes());
AwaitQuiescenceAndInstallAppsPendingForSync();
ASSERT_TRUE(AllProfilesHaveSameAppList());
}
// Install some apps on both clients, then sync. Move an app on one client
// and sync. Both clients should have the updated position for the app.
IN_PROC_BROWSER_TEST_F(TwoClientAppListSyncTest, Move) {
ASSERT_TRUE(SetupSync());
WaitForExtensionServicesToLoad();
ASSERT_TRUE(AllProfilesHaveSameAppList());
const int kNumApps = 5;
for (int i = 0; i < kNumApps; ++i) {
InstallHostedApp(GetProfile(1), i);
}
AwaitQuiescenceAndInstallAppsPendingForSync();
ASSERT_TRUE(AllProfilesHaveSameAppList());
}
// Install a Default App on both clients, then sync. Remove the app on one
// client and sync. Ensure that the app is removed on the other client and
// that a TYPE_REMOVE_DEFAULT_APP entry exists.
//
// This test largely checks mechanism (the How), not policy (the What or Why).
// "How" is user-invisible implementation detail, such as whether or not a
// SyncItem exists and what its code is (e.g. TYPE_REMOVE_DEFAULT_APP). "What"
// is user-visible effects, such as whether or not an app is (still) installed.
//
// To be clear, "policy" in this comment means something different than the
// "policy" in "enterprise administrative policy can install apps". Similarly,
// "default apps" means hardware-specific apps from OEMs, such as an "HP
// [Hewlett Packard]" app installed by default on a HP laptop. "Default apps"
// doesn't refer to built-in apps like the Camera or Files apps.
//
// This test is run twice, with MarkAppAsDefaultApp() returning either false or
// true. Either way, the What (what's user visible, i.e. whether or not apps
// are installed or uninstalled after synchronizing) is unaffected by whether
// or not we mark an app as default-installed in Profile1 before we sync the
// two profiles. It's unclear whether this non-difference is deliberate.
//
// This isn't ideal, but probably still better than nothing. Obviously, it
// would be better to confirm some user-visible effect happens for
// default-installed apps and does not happen otherwise, but it's not certain
// what such an effect would be, or at least how to easily test it. There is
// some discussion at https://crrev.com/c/1732092 and
// https://crrev.com/c/1720229/1/chrome/browser/sync/test/integration/two_client_app_list_sync_test.cc#402
// although the desired What is possibly lost to the mists of time.
IN_PROC_BROWSER_TEST_P(RemoveDefaultAppSyncTest, Remove) {
ASSERT_TRUE(SetupClients());
ASSERT_TRUE(SetupSync());
WaitForExtensionServicesToLoad();
// Install a non-default app in two synchronized Profiles. We should end up
// with a certain number of apps, lets say N.
InstallHostedApp(GetProfile(0), 0);
InstallHostedApp(GetProfile(1), 0);
size_t number_of_apps = 0;
ASSERT_TRUE(AllProfilesHaveSameAppList(&number_of_apps));
const size_t initial_number_of_apps = number_of_apps;
// Install an app in Profile 0 only. At a later point, we'll mark it as
// default-installed, but for now, it's just a regular app.
//
// After sync'ing, we should have N+1 apps in both Profiles.
const int default_app_index = 1;
std::string default_app_id =
InstallHostedApp(GetProfile(0), default_app_index);
AwaitQuiescenceAndInstallAppsPendingForSync();
ASSERT_TRUE(AllProfilesHaveSameAppList(&number_of_apps));
EXPECT_EQ(number_of_apps, initial_number_of_apps + 1);
// Mark that app as a default app, in Profile 1 only.
using ALSS = app_list::AppListSyncableService;
EXPECT_FALSE(ALSS::AppIsDefaultForTest(GetProfile(1), default_app_id));
if (MarkAppAsDefaultApp()) {
ALSS::SetAppIsDefaultForTest(GetProfile(1), default_app_id);
EXPECT_TRUE(ALSS::AppIsDefaultForTest(GetProfile(1), default_app_id));
}
// Remove that app in Profile 0. After sync'ing, it should also be removed
// from Profile 1: we should go back to having N apps in both Profiles.
UninstallApp(GetProfile(0), default_app_index);
ASSERT_TRUE(AwaitQuiescence());
ASSERT_TRUE(AllProfilesHaveSameAppList(&number_of_apps));
EXPECT_EQ(number_of_apps, initial_number_of_apps);
// Ensure that a TYPE_REMOVE_DEFAULT_APP SyncItem exists in both Profiles, if
// it was marked as a default app in Profile 1. This tests the How, not the
// What, but it's better than nothing.
for (int i = 0; i < 2; i++) {
const ALSS::SyncItem* sync_item =
GetSyncItem(GetProfile(i), default_app_id);
if (MarkAppAsDefaultApp()) {
ASSERT_TRUE(sync_item);
EXPECT_EQ(sync_pb::AppListSpecifics::TYPE_REMOVE_DEFAULT_APP,
sync_item->item_type);
} else {
ASSERT_FALSE(sync_item);
}
}
// Re-Install the same app in Profile 0.
std::string app_id2 = InstallHostedApp(GetProfile(0), default_app_index);
EXPECT_EQ(default_app_id, app_id2);
// Ensure that the TYPE_REMOVE_DEFAULT_APP SyncItem (if present) was replaced
// with an TYPE_APP entry, for at least Profile 0.
{
const ALSS::SyncItem* sync_item = GetSyncItem(GetProfile(0), app_id2);
ASSERT_TRUE(sync_item);
EXPECT_EQ(sync_pb::AppListSpecifics::TYPE_APP, sync_item->item_type);
}
// After sync'ing, we should have N+1 apps in both Profiles.
AwaitQuiescenceAndInstallAppsPendingForSync();
ASSERT_TRUE(AllProfilesHaveSameAppList(&number_of_apps));
EXPECT_EQ(number_of_apps, initial_number_of_apps + 1);
// Ensure that the TYPE_REMOVE_DEFAULT_APP SyncItem was replaced with an
// TYPE_APP entry, for both Profiles.
for (int i = 0; i < 2; i++) {
const ALSS::SyncItem* sync_item = GetSyncItem(GetProfile(i), app_id2);
ASSERT_TRUE(sync_item);
EXPECT_EQ(sync_pb::AppListSpecifics::TYPE_APP, sync_item->item_type);
}
}
INSTANTIATE_TEST_SUITE_P(All, RemoveDefaultAppSyncTest, ::testing::Bool());
// Install some apps on both clients, then sync. Move an app on one client
// to a folder and sync. The app lists, including folders, should match.
IN_PROC_BROWSER_TEST_F(TwoClientAppListSyncTest, MoveToFolder) {
ASSERT_TRUE(SetupSync());
WaitForExtensionServicesToLoad();
ASSERT_TRUE(AllProfilesHaveSameAppList());
const int kNumApps = 5;
std::vector<std::string> app_ids;
for (int i = 0; i < kNumApps; ++i) {
app_ids.push_back(InstallHostedApp(GetProfile(0), i));
InstallHostedApp(GetProfile(1), i);
}
ASSERT_TRUE(AwaitQuiescence());
ASSERT_TRUE(AllProfilesHaveSameAppList());
size_t index = 2u;
std::string folder_id = "Folder 0";
SyncAppListHelper::GetInstance()->MoveAppToFolder(GetProfile(0),
app_ids[index], folder_id);
ASSERT_TRUE(AwaitQuiescence());
ASSERT_TRUE(AllProfilesHaveSameAppList());
}
IN_PROC_BROWSER_TEST_F(TwoClientAppListSyncTest, FolderAddRemove) {
ASSERT_TRUE(SetupSync());
WaitForExtensionServicesToLoad();
ASSERT_TRUE(AllProfilesHaveSameAppList());
const int kNumApps = 10;
std::vector<std::string> app_ids;
for (int i = 0; i < kNumApps; ++i) {
app_ids.push_back(InstallHostedApp(GetProfile(0), i));
InstallHostedApp(GetProfile(1), i);
}
ASSERT_TRUE(AwaitQuiescence());
ASSERT_TRUE(AllProfilesHaveSameAppList());
// Move a few apps to a folder.
const size_t kNumAppsToMove = 3;
std::string folder_id = "Folder 0";
// The folder will be created at the end of the list; always move the
// non default items in the list.
// Note: We don't care about the order of items in Chrome, so when we
// changes a file's folder, its index in the list remains unchanged.
// The |kNumAppsToMove| items to move are
// app_ids[item_index..(item_index+kNumAppsToMove-1)].
size_t item_index = kNumDefaultApps;
for (size_t i = 0; i < kNumAppsToMove; ++i) {
SyncAppListHelper::GetInstance()->MoveAppToFolder(
GetProfile(0), app_ids[item_index + i], folder_id);
}
ASSERT_TRUE(AwaitQuiescence());
ASSERT_TRUE(AllProfilesHaveSameAppList());
// Remove one app from the folder.
SyncAppListHelper::GetInstance()->MoveAppFromFolder(GetProfile(0), app_ids[0],
folder_id);
ASSERT_TRUE(AwaitQuiescence());
ASSERT_TRUE(AllProfilesHaveSameAppList());
// Remove remaining apps from the folder (deletes folder).
for (size_t i = 1; i < kNumAppsToMove; ++i) {
SyncAppListHelper::GetInstance()->MoveAppFromFolder(GetProfile(0),
app_ids[0], folder_id);
}
ASSERT_TRUE(AwaitQuiescence());
ASSERT_TRUE(AllProfilesHaveSameAppList());
// Move apps back to a (new) folder.
for (size_t i = 0; i < kNumAppsToMove; ++i) {
SyncAppListHelper::GetInstance()->MoveAppToFolder(
GetProfile(0), app_ids[item_index], folder_id);
}
ASSERT_TRUE(AwaitQuiescence());
ASSERT_TRUE(AllProfilesHaveSameAppList());
}