blob: 95b80764353324f03ae357eed85725ad343a633d [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/files/file_path.h"
#include "base/json/json_reader.h"
#include "base/path_service.h"
#include "base/strings/strcat.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/external_provider_impl.h"
#include "chrome/browser/extensions/external_testing_loader.h"
#include "chrome/browser/extensions/launch_util.h"
#include "chrome/browser/extensions/updater/extension_updater.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/web_applications/test/ssl_test_utils.h"
#include "chrome/browser/web_applications/components/os_integration_manager.h"
#include "chrome/browser/web_applications/components/preinstalled_app_install_features.h"
#include "chrome/browser/web_applications/components/web_app_helpers.h"
#include "chrome/browser/web_applications/preinstalled_web_app_manager.h"
#include "chrome/browser/web_applications/preinstalled_web_apps/preinstalled_web_apps.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/url_loader_interceptor.h"
#include "extensions/browser/app_sorting.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/test_extension_registry_observer.h"
#include "extensions/browser/updater/extension_cache_fake.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "ash/public/cpp/app_list/app_list_types.h"
#include "chrome/browser/ui/app_list/app_list_model_updater.h"
#include "chrome/browser/ui/app_list/app_list_syncable_service.h"
#include "chrome/browser/ui/app_list/app_list_syncable_service_factory.h"
#include "chrome/browser/ui/app_list/chrome_app_list_item.h"
#endif
namespace {
constexpr char kMigrationFlag[] = "MigrationTest";
constexpr char kExtensionUpdatePath[] = "/update_extension";
constexpr char kExtensionId[] = "kbmnembihfiondgfjekmnmcbddelicoi";
constexpr char kExtensionVersion[] = "1.0.0.0";
constexpr char kExtensionCrxPath[] = "/extensions/hosted_app.crx";
constexpr char kWebAppPath[] = "/web_apps/basic.html";
} // namespace
namespace web_app {
class PreinstalledWebAppMigrationBrowserTest : public InProcessBrowserTest {
public:
PreinstalledWebAppMigrationBrowserTest() {
PreinstalledWebAppManager::SkipStartupForTesting();
PreinstalledWebAppManager::BypassOfflineManifestRequirementForTesting();
disable_external_extensions_scope_ =
extensions::ExtensionService::DisableExternalUpdatesForTesting();
}
~PreinstalledWebAppMigrationBrowserTest() override = default;
Profile* profile() { return browser()->profile(); }
extensions::ExtensionService& extension_service() {
return *extensions::ExtensionSystem::Get(profile())->extension_service();
}
GURL GetWebAppUrl() const {
return embedded_test_server()->GetURL(kWebAppPath);
}
AppId GetWebAppId() const { return GenerateAppIdFromURL(GetWebAppUrl()); }
// InProcessBrowserTest:
void SetUp() override {
embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
&PreinstalledWebAppMigrationBrowserTest::RequestHandlerOverride,
base::Unretained(this)));
ASSERT_TRUE(embedded_test_server()->Start());
InProcessBrowserTest::SetUp();
}
void SetUpOnMainThread() override {
SetUpExtensionTestExternalProvider();
InProcessBrowserTest::SetUpOnMainThread();
os_hooks_suppress_ =
OsIntegrationManager::ScopedSuppressOsHooksForTesting();
}
std::unique_ptr<net::test_server::HttpResponse> RequestHandlerOverride(
const net::test_server::HttpRequest& request) {
std::string request_path = request.GetURL().path();
if (request_path == kExtensionUpdatePath) {
auto response = std::make_unique<net::test_server::BasicHttpResponse>();
response->set_code(net::HTTP_OK);
response->set_content(base::ReplaceStringPlaceholders(
R"(<?xml version='1.0' encoding='UTF-8'?>
<gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'>
<app appid='$1'>
<updatecheck codebase='$2' version='$3' />
</app>
</gupdate>
)",
{kExtensionId,
embedded_test_server()->GetURL(kExtensionCrxPath).spec(),
kExtensionVersion},
nullptr));
response->set_content_type("text/xml");
return std::move(response);
}
return nullptr;
}
void SetUpExtensionTestExternalProvider() {
extension_service().ClearProvidersForTesting();
extension_service().updater()->SetExtensionCacheForTesting(
test_extension_cache_.get());
std::string external_extension_config = base::ReplaceStringPlaceholders(
R"({
"$1": {
"external_update_url": "$2",
"web_app_migration_flag": "$3"
}
})",
{kExtensionId,
embedded_test_server()->GetURL(kExtensionUpdatePath).spec(),
kMigrationFlag},
nullptr);
extension_service().AddProviderForTesting(
std::make_unique<extensions::ExternalProviderImpl>(
&extension_service(),
base::MakeRefCounted<extensions::ExternalTestingLoader>(
external_extension_config,
base::FilePath(FILE_PATH_LITERAL("//absolute/path"))),
profile(), extensions::mojom::ManifestLocation::kExternalPref,
extensions::mojom::ManifestLocation::kExternalPrefDownload,
// Matches |bundled_extension_creation_flags| in
// ExternalProviderImpl::CreateExternalProviders().
extensions::Extension::WAS_INSTALLED_BY_DEFAULT |
extensions::Extension::FROM_WEBSTORE));
disable_external_extensions_scope_.reset();
}
void SyncExternalExtensions() {
base::RunLoop run_loop;
extension_service().set_external_updates_finished_callback_for_test(
run_loop.QuitWhenIdleClosure());
extension_service().CheckForExternalUpdates();
run_loop.Run();
}
void SyncExternalWebApps(bool expect_install,
bool expect_uninstall,
bool pass_config = true) {
base::RunLoop run_loop;
base::Optional<InstallResultCode> code;
auto callback = base::BindLambdaForTesting(
[&](std::map<GURL, ExternallyManagedAppManager::InstallResult>
install_results,
std::map<GURL, bool> uninstall_results) {
if (expect_install) {
code = install_results.at(GetWebAppUrl()).code;
EXPECT_TRUE(*code == InstallResultCode::kSuccessNewInstall ||
*code == InstallResultCode::kSuccessOfflineOnlyInstall);
} else {
EXPECT_EQ(install_results.find(GetWebAppUrl()),
install_results.end());
}
EXPECT_EQ(uninstall_results[GetWebAppUrl()], expect_uninstall);
run_loop.Quit();
});
std::vector<base::Value> app_configs;
if (pass_config) {
std::string app_config_string = base::ReplaceStringPlaceholders(
R"({
"app_url": "$1",
"launch_container": "window",
"user_type": ["unmanaged"],
"feature_name": "$2",
"uninstall_and_replace": ["$3"]
})",
{GetWebAppUrl().spec(), kMigrationFlag, kExtensionId}, nullptr);
app_configs.push_back(*base::JSONReader::Read(app_config_string));
}
PreinstalledWebAppManager::SetConfigsForTesting(&app_configs);
WebAppProvider::Get(profile())
->preinstalled_web_app_manager()
.LoadAndSynchronizeForTesting(std::move(callback));
run_loop.Run();
PreinstalledWebAppManager::SetConfigsForTesting(nullptr);
}
bool IsWebAppInstalled() {
return WebAppProvider::Get(profile())->registrar().IsLocallyInstalled(
GetWebAppId());
}
bool IsExtensionAppInstalled() {
return extensions::ExtensionRegistry::Get(profile())->GetExtensionById(
kExtensionId, extensions::ExtensionRegistry::EVERYTHING);
}
void FlushAppService() {
apps::AppServiceProxyFactory::GetForProfile(profile())
->FlushMojoCallsForTesting();
}
private:
base::test::ScopedFeatureList features_;
base::Optional<base::AutoReset<bool>> disable_external_extensions_scope_;
std::unique_ptr<extensions::ExtensionCacheFake> test_extension_cache_;
ScopedOsHooksSuppress os_hooks_suppress_;
};
IN_PROC_BROWSER_TEST_F(PreinstalledWebAppMigrationBrowserTest,
MigrateRevertMigrate) {
#if BUILDFLAG(IS_CHROMEOS_ASH)
// Grab handles to the app list to update shelf/list state for apps later on.
app_list::AppListSyncableService* app_list_syncable_service =
app_list::AppListSyncableServiceFactory::GetForProfile(profile());
AppListModelUpdater* app_list_model_updater =
app_list_syncable_service->GetModelUpdater();
app_list_model_updater->SetActive(true);
#endif
// Set up pre-migration state.
{
ASSERT_FALSE(IsPreinstalledAppInstallFeatureEnabled(kMigrationFlag));
SyncExternalExtensions();
SyncExternalWebApps(/*expect_install=*/false, /*expect_uninstall=*/false);
FlushAppService();
EXPECT_FALSE(IsWebAppInstalled());
EXPECT_TRUE(IsExtensionAppInstalled());
#if BUILDFLAG(IS_CHROMEOS_ASH)
ChromeAppListItem* app_list_item =
app_list_model_updater->FindItem(kExtensionId);
app_list_item->SetPosition(syncer::StringOrdinal("testapplistposition"));
app_list_model_updater->OnItemUpdated(app_list_item->CloneMetadata());
app_list_syncable_service->SetPinPosition(
kExtensionId, syncer::StringOrdinal("testpinposition"));
EXPECT_EQ(app_list_syncable_service->GetSyncItem(kExtensionId)->ToString(),
"kbmnembi { Nothing } [testapplistposition] [testpinposition]");
#endif
}
// Migrate extension app to web app.
{
base::AutoReset<bool> testing_scope =
SetPreinstalledAppInstallFeatureAlwaysEnabledForTesting();
ASSERT_TRUE(IsPreinstalledAppInstallFeatureEnabled(kMigrationFlag));
SyncExternalExtensions();
// Extension sticks around to be uninstalled by the replacement web app.
EXPECT_TRUE(IsExtensionAppInstalled());
{
base::HistogramTester histograms;
extensions::TestExtensionRegistryObserver uninstall_observer(
extensions::ExtensionRegistry::Get(profile()));
SyncExternalWebApps(/*expect_install=*/true, /*expect_uninstall=*/false);
scoped_refptr<const extensions::Extension> uninstalled_app =
uninstall_observer.WaitForExtensionUninstalled();
EXPECT_EQ(uninstalled_app->id(), kExtensionId);
FlushAppService();
EXPECT_TRUE(IsWebAppInstalled());
EXPECT_FALSE(IsExtensionAppInstalled());
histograms.ExpectUniqueSample(
PreinstalledWebAppManager::kHistogramInstallResult,
InstallResultCode::kSuccessNewInstall, 1);
histograms.ExpectUniqueSample(
PreinstalledWebAppManager::kHistogramUninstallAndReplaceCount, 1, 1);
#if BUILDFLAG(IS_CHROMEOS_ASH)
// Chrome OS shelf/list position should migrate.
EXPECT_EQ(
app_list_syncable_service->GetSyncItem(GetWebAppId())->ToString(),
base::StringPrintf(
"%s { Basic web app } [testapplistposition] [testpinposition]",
GetWebAppId().substr(0, 8).c_str()));
// Old Chrome app is unpinned.
EXPECT_EQ(
app_list_syncable_service->GetSyncItem(kExtensionId)->ToString(),
"kbmnembi { Nothing } [testapplistposition] [INVALID[]]");
#endif
}
}
// Revert migration.
{
ASSERT_FALSE(IsPreinstalledAppInstallFeatureEnabled(kMigrationFlag));
SyncExternalExtensions();
SyncExternalWebApps(/*expect_install=*/false, /*expect_uninstall=*/true);
FlushAppService();
EXPECT_TRUE(IsExtensionAppInstalled());
EXPECT_FALSE(IsWebAppInstalled());
#if BUILDFLAG(IS_CHROMEOS_ASH)
// Re-pin the old Chrome app.
app_list_syncable_service->SetPinPosition(
kExtensionId, syncer::StringOrdinal("testpinposition"));
EXPECT_EQ(app_list_syncable_service->GetSyncItem(kExtensionId)->ToString(),
"kbmnembi { Nothing } [testapplistposition] [testpinposition]");
#endif
}
// Re-run migration.
{
base::HistogramTester histograms;
base::AutoReset<bool> testing_scope =
SetPreinstalledAppInstallFeatureAlwaysEnabledForTesting();
ASSERT_TRUE(IsPreinstalledAppInstallFeatureEnabled(kMigrationFlag));
extensions::TestExtensionRegistryObserver uninstall_observer(
extensions::ExtensionRegistry::Get(profile()));
SyncExternalWebApps(/*expect_install=*/true, /*expect_uninstall=*/false);
scoped_refptr<const extensions::Extension> uninstalled_app =
uninstall_observer.WaitForExtensionUninstalled();
EXPECT_EQ(uninstalled_app->id(), kExtensionId);
FlushAppService();
EXPECT_TRUE(IsWebAppInstalled());
EXPECT_FALSE(IsExtensionAppInstalled());
histograms.ExpectUniqueSample(
PreinstalledWebAppManager::kHistogramInstallResult,
InstallResultCode::kSuccessNewInstall, 1);
histograms.ExpectUniqueSample(
PreinstalledWebAppManager::kHistogramUninstallAndReplaceCount, 1, 1);
#if BUILDFLAG(IS_CHROMEOS_ASH)
// Chrome OS shelf/list position should re-migrate.
EXPECT_EQ(
app_list_syncable_service->GetSyncItem(GetWebAppId())->ToString(),
base::StringPrintf(
"%s { Basic web app } [testapplistposition] [testpinposition]",
GetWebAppId().substr(0, 8).c_str()));
// Old Chrome app should get unpinned again.
EXPECT_EQ(app_list_syncable_service->GetSyncItem(kExtensionId)->ToString(),
"kbmnembi { Nothing } [testapplistposition] [INVALID[]]");
#endif
}
}
IN_PROC_BROWSER_TEST_F(PreinstalledWebAppMigrationBrowserTest,
MigratePreferences) {
#if BUILDFLAG(IS_CHROMEOS_ASH)
app_list::AppListSyncableService* app_list_syncable_service =
app_list::AppListSyncableServiceFactory::GetForProfile(profile());
AppListModelUpdater* app_list_model_updater =
app_list_syncable_service->GetModelUpdater();
app_list_model_updater->SetActive(true);
#endif
extensions::AppSorting* app_sorting =
extensions::ExtensionSystem::Get(profile())->app_sorting();
// Set up pre-migration state.
{
ASSERT_FALSE(IsPreinstalledAppInstallFeatureEnabled(kMigrationFlag));
SyncExternalExtensions();
SyncExternalWebApps(/*expect_install=*/false, /*expect_uninstall=*/false);
EXPECT_FALSE(IsWebAppInstalled());
EXPECT_TRUE(IsExtensionAppInstalled());
#if BUILDFLAG(IS_CHROMEOS_ASH)
ChromeAppListItem* app_list_item =
app_list_model_updater->FindItem(kExtensionId);
app_list_item->SetPosition(syncer::StringOrdinal("testapplistposition"));
app_list_model_updater->OnItemUpdated(app_list_item->CloneMetadata());
app_list_syncable_service->SetPinPosition(
kExtensionId, syncer::StringOrdinal("testpinposition"));
EXPECT_EQ(app_list_syncable_service->GetSyncItem(kExtensionId)->ToString(),
"kbmnembi { Nothing } [testapplistposition] [testpinposition]");
#endif
// Set chrome://apps position.
app_sorting->SetAppLaunchOrdinal(kExtensionId,
syncer::StringOrdinal("testapplaunch"));
app_sorting->SetPageOrdinal(kExtensionId,
syncer::StringOrdinal("testpageordinal"));
// Set user preference to launch as browser tab.
extensions::SetLaunchType(profile(), kExtensionId,
extensions::LAUNCH_TYPE_REGULAR);
}
// Migrate extension app to web app.
{
base::AutoReset<bool> testing_scope =
SetPreinstalledAppInstallFeatureAlwaysEnabledForTesting();
ASSERT_TRUE(IsPreinstalledAppInstallFeatureEnabled(kMigrationFlag));
SyncExternalExtensions();
// Extension sticks around to be uninstalled by the replacement web app.
EXPECT_TRUE(IsExtensionAppInstalled());
{
base::HistogramTester histograms;
extensions::TestExtensionRegistryObserver uninstall_observer(
extensions::ExtensionRegistry::Get(profile()));
SyncExternalWebApps(/*expect_install=*/true, /*expect_uninstall=*/false);
EXPECT_TRUE(IsWebAppInstalled());
scoped_refptr<const extensions::Extension> uninstalled_app =
uninstall_observer.WaitForExtensionUninstalled();
EXPECT_EQ(uninstalled_app->id(), kExtensionId);
EXPECT_FALSE(IsExtensionAppInstalled());
histograms.ExpectUniqueSample(
PreinstalledWebAppManager::kHistogramInstallResult,
InstallResultCode::kSuccessNewInstall, 1);
histograms.ExpectUniqueSample(
PreinstalledWebAppManager::kHistogramUninstallAndReplaceCount, 1, 1);
}
}
// Check UI preferences have migrated across.
{
const AppId web_app_id = GetWebAppId();
#if BUILDFLAG(IS_CHROMEOS_ASH)
// Chrome OS shelf/list position should migrate.
EXPECT_EQ(
app_list_syncable_service->GetSyncItem(GetWebAppId())->ToString(),
base::StringPrintf(
"%s { Basic web app } [testapplistposition] [testpinposition]",
GetWebAppId().substr(0, 8).c_str()));
// Chrome app shelf/list position should be retained.
EXPECT_EQ(app_list_syncable_service->GetSyncItem(kExtensionId)->ToString(),
"kbmnembi { Nothing } [testapplistposition] [testpinposition]");
#endif
// chrome://apps position should migrate.
EXPECT_EQ(app_sorting->GetAppLaunchOrdinal(web_app_id).ToDebugString(),
"testapplaunch");
EXPECT_EQ(app_sorting->GetPageOrdinal(web_app_id).ToDebugString(),
"testpageordinal");
// User launch preference should migrate across and override
// "launch_container": "window" in the JSON config.
EXPECT_EQ(WebAppProvider::Get(profile())->registrar().GetAppUserDisplayMode(
web_app_id),
DisplayMode::kBrowser);
}
}
IN_PROC_BROWSER_TEST_F(PreinstalledWebAppMigrationBrowserTest,
UserUninstalledExtensionApp) {
// Set up pre-migration state.
{
ASSERT_FALSE(IsPreinstalledAppInstallFeatureEnabled(kMigrationFlag));
SyncExternalExtensions();
SyncExternalWebApps(/*expect_install=*/false, /*expect_uninstall=*/false);
EXPECT_FALSE(IsWebAppInstalled());
EXPECT_TRUE(IsExtensionAppInstalled());
}
{
extensions::TestExtensionRegistryObserver uninstall_observer(
extensions::ExtensionRegistry::Get(profile()), kExtensionId);
extensions::ExtensionSystem::Get(profile())
->extension_service()
->UninstallExtension(kExtensionId,
extensions::UNINSTALL_REASON_FOR_TESTING, nullptr);
uninstall_observer.WaitForExtensionUninstalled();
EXPECT_FALSE(IsWebAppInstalled());
EXPECT_FALSE(IsExtensionAppInstalled());
}
// Migrate extension app to web app.
{
base::AutoReset<bool> testing_scope =
SetPreinstalledAppInstallFeatureAlwaysEnabledForTesting();
ASSERT_TRUE(IsPreinstalledAppInstallFeatureEnabled(kMigrationFlag));
SyncExternalExtensions();
EXPECT_FALSE(IsExtensionAppInstalled());
SyncExternalWebApps(/*expect_install=*/false, /*expect_uninstall=*/false);
EXPECT_FALSE(IsWebAppInstalled());
}
}
// Tests the migration from an extension-app to a preinstalled web app provided
// by the preinstalled apps (rather than an external config).
IN_PROC_BROWSER_TEST_F(PreinstalledWebAppMigrationBrowserTest,
MigrateToPreinstalledWebApp) {
ScopedTestingPreinstalledAppData preinstalled_apps;
ExternalInstallOptions options(GetWebAppUrl(), DisplayMode::kBrowser,
ExternalInstallSource::kExternalDefault);
options.gate_on_feature = kMigrationFlag;
options.user_type_allowlist = {"unmanaged"};
options.uninstall_and_replace.push_back(kExtensionId);
options.only_use_app_info_factory = true;
options.app_info_factory = base::BindLambdaForTesting([&]() {
auto info = std::make_unique<WebApplicationInfo>();
info->start_url = GetWebAppUrl();
info->title = u"Test app";
return info;
});
preinstalled_apps.apps.push_back(std::move(options));
EXPECT_EQ(1u, GetPreinstalledWebApps().size());
// Set up pre-migration state.
{
base::HistogramTester histograms;
ASSERT_FALSE(IsPreinstalledAppInstallFeatureEnabled(kMigrationFlag));
SyncExternalExtensions();
SyncExternalWebApps(/*expect_install=*/false, /*expect_uninstall=*/false,
/*pass_config=*/false);
EXPECT_FALSE(IsWebAppInstalled());
EXPECT_TRUE(IsExtensionAppInstalled());
histograms.ExpectUniqueSample(
PreinstalledWebAppManager::kHistogramEnabledCount, 0, 1);
histograms.ExpectUniqueSample(
PreinstalledWebAppManager::kHistogramDisabledCount, 1, 1);
histograms.ExpectUniqueSample(
PreinstalledWebAppManager::kHistogramConfigErrorCount, 0, 1);
}
// Migrate extension app to web app.
{
base::AutoReset<bool> testing_scope =
SetPreinstalledAppInstallFeatureAlwaysEnabledForTesting();
ASSERT_TRUE(IsPreinstalledAppInstallFeatureEnabled(kMigrationFlag));
SyncExternalExtensions();
// Extension sticks around to be uninstalled by the replacement web app.
EXPECT_TRUE(IsExtensionAppInstalled());
{
base::HistogramTester histograms;
extensions::TestExtensionRegistryObserver uninstall_observer(
extensions::ExtensionRegistry::Get(profile()));
SyncExternalWebApps(/*expect_install=*/true, /*expect_uninstall=*/false,
/*pass_config=*/false);
EXPECT_TRUE(IsWebAppInstalled());
scoped_refptr<const extensions::Extension> uninstalled_app =
uninstall_observer.WaitForExtensionUninstalled();
EXPECT_EQ(uninstalled_app->id(), kExtensionId);
EXPECT_FALSE(IsExtensionAppInstalled());
histograms.ExpectUniqueSample(
PreinstalledWebAppManager::kHistogramEnabledCount, 1, 1);
histograms.ExpectUniqueSample(
PreinstalledWebAppManager::kHistogramDisabledCount, 0, 1);
histograms.ExpectUniqueSample(
PreinstalledWebAppManager::kHistogramConfigErrorCount, 0, 1);
histograms.ExpectUniqueSample(
PreinstalledWebAppManager::kHistogramInstallResult,
InstallResultCode::kSuccessOfflineOnlyInstall, 1);
histograms.ExpectUniqueSample(
PreinstalledWebAppManager::kHistogramUninstallAndReplaceCount, 1, 1);
}
}
}
} // namespace web_app