blob: 34e1210eba15ab819be13bad7e440d3b70090717 [file] [log] [blame]
// Copyright 2018 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 "chrome/browser/web_applications/web_app_registrar.h"
#include <memory>
#include <set>
#include <string>
#include <utility>
#include "base/bind_helpers.h"
#include "base/optional.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/bind_test_util.h"
#include "chrome/browser/web_applications/components/web_app_constants.h"
#include "chrome/browser/web_applications/components/web_app_helpers.h"
#include "chrome/browser/web_applications/test/test_web_app_database_factory.h"
#include "chrome/browser/web_applications/test/test_web_app_registry_controller.h"
#include "chrome/browser/web_applications/test/web_app_test.h"
#include "chrome/browser/web_applications/web_app.h"
#include "chrome/browser/web_applications/web_app_registry_update.h"
#include "chrome/browser/web_applications/web_app_sync_bridge.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
namespace web_app {
namespace {
Registry CreateRegistryForTesting(const std::string& base_url, int num_apps) {
Registry registry;
for (int i = 0; i < num_apps; ++i) {
const auto url = base_url + base::NumberToString(i);
const AppId app_id = GenerateAppIdFromURL(GURL(url));
auto web_app = std::make_unique<WebApp>(app_id);
web_app->AddSource(Source::kSync);
web_app->SetLaunchUrl(GURL(url));
web_app->SetDisplayMode(DisplayMode::kBrowser);
web_app->SetUserDisplayMode(DisplayMode::kBrowser);
registry.emplace(app_id, std::move(web_app));
}
return registry;
}
} // namespace
class WebAppRegistrarTest : public WebAppTest {
public:
void SetUp() override {
WebAppTest::SetUp();
test_registry_controller_ =
std::make_unique<TestWebAppRegistryController>();
test_registry_controller_->SetUp(profile());
}
protected:
TestWebAppRegistryController& controller() {
return *test_registry_controller_;
}
TestWebAppDatabaseFactory& database_factory() {
return controller().database_factory();
}
WebAppRegistrar& registrar() { return controller().registrar(); }
WebAppSyncBridge& sync_bridge() { return controller().sync_bridge(); }
std::set<AppId> RegisterAppsForTesting(Registry registry) {
std::set<AppId> ids;
ScopedRegistryUpdate update(&sync_bridge());
for (auto& kv : registry) {
ids.insert(kv.first);
update->CreateApp(std::move(kv.second));
}
return ids;
}
void RegisterApp(std::unique_ptr<WebApp> web_app) {
controller().RegisterApp(std::move(web_app));
}
void UnregisterApp(const AppId& app_id) {
controller().UnregisterApp(app_id);
}
void UnregisterAll() { controller().UnregisterAll(); }
AppId InitRegistrarWithApp(std::unique_ptr<WebApp> app) {
DCHECK(registrar().is_empty());
const AppId app_id = app->app_id();
Registry registry;
registry.emplace(app_id, std::move(app));
InitRegistrarWithRegistry(registry);
return app_id;
}
std::set<AppId> InitRegistrarWithApps(const std::string& base_url,
int num_apps) {
DCHECK(registrar().is_empty());
Registry registry = CreateRegistryForTesting(base_url, num_apps);
return InitRegistrarWithRegistry(registry);
}
std::set<AppId> InitRegistrarWithRegistry(const Registry& registry) {
std::set<AppId> app_ids;
for (auto& kv : registry)
app_ids.insert(kv.second->app_id());
database_factory().WriteRegistry(registry);
controller().Init();
return app_ids;
}
std::unique_ptr<WebApp> CreateWebAppWithSource(const std::string& url,
Source::Type source) {
const GURL launch_url(url);
const AppId app_id = GenerateAppIdFromURL(launch_url);
auto web_app = std::make_unique<WebApp>(app_id);
web_app->AddSource(source);
web_app->SetDisplayMode(DisplayMode::kStandalone);
web_app->SetUserDisplayMode(DisplayMode::kStandalone);
web_app->SetName("Name");
web_app->SetLaunchUrl(launch_url);
return web_app;
}
std::unique_ptr<WebApp> CreateWebApp(const std::string& url) {
return CreateWebAppWithSource(url, Source::kSync);
}
void SyncBridgeCommitUpdate(std::unique_ptr<WebAppRegistryUpdate> update) {
base::RunLoop run_loop;
sync_bridge().CommitUpdate(std::move(update),
base::BindLambdaForTesting([&](bool success) {
EXPECT_TRUE(success);
run_loop.Quit();
}));
run_loop.Run();
}
private:
std::unique_ptr<TestWebAppRegistryController> test_registry_controller_;
};
TEST_F(WebAppRegistrarTest, CreateRegisterUnregister) {
controller().Init();
EXPECT_EQ(nullptr, registrar().GetAppById(AppId()));
EXPECT_FALSE(registrar().GetAppById(AppId()));
const GURL launch_url = GURL("https://example.com/path");
const AppId app_id = GenerateAppIdFromURL(launch_url);
const std::string name = "Name";
const std::string description = "Description";
const GURL scope = GURL("https://example.com/scope");
const base::Optional<SkColor> theme_color = 0xAABBCCDD;
const GURL launch_url2 = GURL("https://example.com/path2");
const AppId app_id2 = GenerateAppIdFromURL(launch_url2);
auto web_app = std::make_unique<WebApp>(app_id);
auto web_app2 = std::make_unique<WebApp>(app_id2);
web_app->AddSource(Source::kSync);
web_app->SetDisplayMode(DisplayMode::kStandalone);
web_app->SetUserDisplayMode(DisplayMode::kStandalone);
web_app->SetName(name);
web_app->SetDescription(description);
web_app->SetLaunchUrl(launch_url);
web_app->SetScope(scope);
web_app->SetThemeColor(theme_color);
web_app2->AddSource(Source::kDefault);
web_app2->SetDisplayMode(DisplayMode::kBrowser);
web_app2->SetUserDisplayMode(DisplayMode::kBrowser);
web_app2->SetLaunchUrl(launch_url2);
EXPECT_EQ(nullptr, registrar().GetAppById(app_id));
EXPECT_EQ(nullptr, registrar().GetAppById(app_id2));
EXPECT_TRUE(registrar().is_empty());
RegisterApp(std::move(web_app));
EXPECT_TRUE(registrar().IsInstalled(app_id));
const WebApp* app = registrar().GetAppById(app_id);
EXPECT_EQ(app_id, app->app_id());
EXPECT_EQ(name, app->name());
EXPECT_EQ(description, app->description());
EXPECT_EQ(launch_url, app->launch_url());
EXPECT_EQ(scope, app->scope());
EXPECT_EQ(theme_color, app->theme_color());
EXPECT_EQ(nullptr, registrar().GetAppById(app_id2));
EXPECT_FALSE(registrar().is_empty());
RegisterApp(std::move(web_app2));
EXPECT_TRUE(registrar().IsInstalled(app_id2));
const WebApp* app2 = registrar().GetAppById(app_id2);
EXPECT_EQ(app_id2, app2->app_id());
EXPECT_FALSE(registrar().is_empty());
UnregisterApp(app_id);
EXPECT_FALSE(registrar().IsInstalled(app_id));
EXPECT_EQ(nullptr, registrar().GetAppById(app_id));
EXPECT_FALSE(registrar().is_empty());
// Check that app2 is still registered.
app2 = registrar().GetAppById(app_id2);
EXPECT_TRUE(registrar().IsInstalled(app_id2));
EXPECT_EQ(app_id2, app2->app_id());
UnregisterApp(app_id2);
EXPECT_FALSE(registrar().IsInstalled(app_id2));
EXPECT_EQ(nullptr, registrar().GetAppById(app_id2));
EXPECT_TRUE(registrar().is_empty());
}
TEST_F(WebAppRegistrarTest, DestroyRegistrarOwningRegisteredApps) {
controller().Init();
auto web_app = CreateWebApp("https://example.com/path");
RegisterApp(std::move(web_app));
auto web_app2 = CreateWebApp("https://example.com/path2");
RegisterApp(std::move(web_app2));
controller().DestroySubsystems();
}
TEST_F(WebAppRegistrarTest, InitRegistrarAndDoForEachApp) {
std::set<AppId> ids = InitRegistrarWithApps("https://example.com/path", 100);
for (const WebApp& web_app : registrar().AllApps()) {
const size_t num_removed = ids.erase(web_app.app_id());
EXPECT_EQ(1U, num_removed);
}
EXPECT_TRUE(ids.empty());
}
TEST_F(WebAppRegistrarTest, AllAppsMutable) {
std::set<AppId> ids = InitRegistrarWithApps("https://example.com/path", 10);
for (WebApp& web_app : controller().mutable_registrar().AllAppsMutable()) {
web_app.SetDisplayMode(DisplayMode::kStandalone);
const size_t num_removed = ids.erase(web_app.app_id());
EXPECT_EQ(1U, num_removed);
}
EXPECT_TRUE(ids.empty());
}
TEST_F(WebAppRegistrarTest, DoForEachAndUnregisterAllApps) {
controller().Init();
Registry registry = CreateRegistryForTesting("https://example.com/path", 100);
auto ids = RegisterAppsForTesting(std::move(registry));
EXPECT_EQ(100UL, ids.size());
for (const WebApp& web_app : registrar().AllApps()) {
const size_t num_removed = ids.erase(web_app.app_id());
EXPECT_EQ(1U, num_removed);
}
EXPECT_TRUE(ids.empty());
EXPECT_FALSE(registrar().is_empty());
UnregisterAll();
EXPECT_TRUE(registrar().is_empty());
}
TEST_F(WebAppRegistrarTest, WebAppSyncBridge) {
std::set<AppId> ids = InitRegistrarWithApps("https://example.com/path", 100);
// Add 1 app after Init.
auto web_app = CreateWebApp("https://example.com/path");
const AppId app_id = web_app->app_id();
RegisterApp(std::move(web_app));
EXPECT_EQ(101UL, database_factory().ReadAllAppIds().size());
EXPECT_EQ(101UL, controller().mutable_registrar().registry().size());
// Remove 1 app after Init.
UnregisterApp(app_id);
EXPECT_EQ(100UL, controller().mutable_registrar().registry().size());
EXPECT_EQ(100UL, database_factory().ReadAllAppIds().size());
// Remove 100 apps after Init.
UnregisterAll();
EXPECT_TRUE(database_factory().ReadAllAppIds().empty());
EXPECT_TRUE(registrar().is_empty());
}
TEST_F(WebAppRegistrarTest, GetAppDataFields) {
controller().Init();
const GURL launch_url = GURL("https://example.com/path");
const AppId app_id = GenerateAppIdFromURL(launch_url);
const std::string name = "Name";
const std::string description = "Description";
const base::Optional<SkColor> theme_color = 0xAABBCCDD;
const auto display_mode = DisplayMode::kMinimalUi;
const auto user_display_mode = DisplayMode::kStandalone;
EXPECT_EQ(std::string(), registrar().GetAppShortName(app_id));
EXPECT_EQ(GURL(), registrar().GetAppLaunchURL(app_id));
auto web_app = std::make_unique<WebApp>(app_id);
WebApp* web_app_ptr = web_app.get();
web_app->AddSource(Source::kSync);
web_app->SetName(name);
web_app->SetDescription(description);
web_app->SetThemeColor(theme_color);
web_app->SetLaunchUrl(launch_url);
web_app->SetDisplayMode(display_mode);
web_app->SetUserDisplayMode(user_display_mode);
web_app->SetIsLocallyInstalled(/*is_locally_installed*/ false);
RegisterApp(std::move(web_app));
EXPECT_EQ(name, registrar().GetAppShortName(app_id));
EXPECT_EQ(description, registrar().GetAppDescription(app_id));
EXPECT_EQ(theme_color, registrar().GetAppThemeColor(app_id));
EXPECT_EQ(launch_url, registrar().GetAppLaunchURL(app_id));
EXPECT_EQ(DisplayMode::kStandalone,
registrar().GetAppUserDisplayMode(app_id));
{
EXPECT_FALSE(registrar().IsLocallyInstalled(app_id));
EXPECT_FALSE(registrar().IsLocallyInstalled("unknown"));
web_app_ptr->SetIsLocallyInstalled(/*is_locally_installed*/ true);
EXPECT_TRUE(registrar().IsLocallyInstalled(app_id));
}
{
EXPECT_EQ(DisplayMode::kUndefined,
registrar().GetAppUserDisplayMode("unknown"));
web_app_ptr->SetUserDisplayMode(DisplayMode::kBrowser);
EXPECT_EQ(DisplayMode::kBrowser, registrar().GetAppUserDisplayMode(app_id));
sync_bridge().SetAppUserDisplayMode(app_id, DisplayMode::kStandalone);
EXPECT_EQ(DisplayMode::kStandalone, web_app_ptr->user_display_mode());
EXPECT_EQ(DisplayMode::kMinimalUi, web_app_ptr->display_mode());
}
}
TEST_F(WebAppRegistrarTest, CanFindAppsInScope) {
controller().Init();
const GURL origin_scope("https://example.com/");
const GURL app1_scope("https://example.com/app");
const GURL app2_scope("https://example.com/app-two");
const GURL app3_scope("https://not-example.com/app");
const AppId app1_id = GenerateAppIdFromURL(app1_scope);
const AppId app2_id = GenerateAppIdFromURL(app2_scope);
const AppId app3_id = GenerateAppIdFromURL(app3_scope);
std::vector<AppId> in_scope = registrar().FindAppsInScope(origin_scope);
EXPECT_EQ(0u, in_scope.size());
auto app1 = CreateWebApp(app1_scope.spec());
app1->SetScope(app1_scope);
RegisterApp(std::move(app1));
in_scope = registrar().FindAppsInScope(origin_scope);
EXPECT_THAT(in_scope, testing::UnorderedElementsAre(app1_id));
in_scope = registrar().FindAppsInScope(app1_scope);
EXPECT_THAT(in_scope, testing::UnorderedElementsAre(app1_id));
auto app2 = CreateWebApp(app2_scope.spec());
app2->SetScope(app2_scope);
RegisterApp(std::move(app2));
in_scope = registrar().FindAppsInScope(origin_scope);
EXPECT_THAT(in_scope, testing::UnorderedElementsAre(app1_id, app2_id));
in_scope = registrar().FindAppsInScope(app1_scope);
EXPECT_THAT(in_scope, testing::UnorderedElementsAre(app1_id, app2_id));
in_scope = registrar().FindAppsInScope(app2_scope);
EXPECT_THAT(in_scope, testing::UnorderedElementsAre(app2_id));
auto app3 = CreateWebApp(app3_scope.spec());
app3->SetScope(app3_scope);
RegisterApp(std::move(app3));
in_scope = registrar().FindAppsInScope(origin_scope);
EXPECT_THAT(in_scope, testing::UnorderedElementsAre(app1_id, app2_id));
in_scope = registrar().FindAppsInScope(app3_scope);
EXPECT_THAT(in_scope, testing::UnorderedElementsAre(app3_id));
}
TEST_F(WebAppRegistrarTest, CanFindAppWithUrlInScope) {
controller().Init();
const GURL origin_scope("https://example.com/");
const GURL app1_scope("https://example.com/app");
const GURL app2_scope("https://example.com/app-two");
const GURL app3_scope("https://not-example.com/app");
const AppId app1_id = GenerateAppIdFromURL(app1_scope);
const AppId app2_id = GenerateAppIdFromURL(app2_scope);
const AppId app3_id = GenerateAppIdFromURL(app3_scope);
auto app1 = CreateWebApp(app1_scope.spec());
app1->SetScope(app1_scope);
RegisterApp(std::move(app1));
base::Optional<AppId> app2_match =
registrar().FindAppWithUrlInScope(app2_scope);
DCHECK(app2_match);
EXPECT_EQ(*app2_match, app1_id);
base::Optional<AppId> app3_match =
registrar().FindAppWithUrlInScope(app3_scope);
EXPECT_FALSE(app3_match);
auto app2 = CreateWebApp(app2_scope.spec());
app2->SetScope(app2_scope);
RegisterApp(std::move(app2));
auto app3 = CreateWebApp(app3_scope.spec());
app3->SetScope(app3_scope);
RegisterApp(std::move(app3));
base::Optional<AppId> origin_match =
registrar().FindAppWithUrlInScope(origin_scope);
EXPECT_FALSE(origin_match);
base::Optional<AppId> app1_match =
registrar().FindAppWithUrlInScope(app1_scope);
DCHECK(app1_match);
EXPECT_EQ(*app1_match, app1_id);
app2_match = registrar().FindAppWithUrlInScope(app2_scope);
DCHECK(app2_match);
EXPECT_EQ(*app2_match, app2_id);
app3_match = registrar().FindAppWithUrlInScope(app3_scope);
DCHECK(app3_match);
EXPECT_EQ(*app3_match, app3_id);
}
TEST_F(WebAppRegistrarTest, CanFindShortcutWithUrlInScope) {
controller().Init();
const GURL app1_page("https://example.com/app/page");
const GURL app2_page("https://example.com/app-two/page");
const GURL app3_page("https://not-example.com/app/page");
const GURL app1_launch("https://example.com/app/launch");
const GURL app2_launch("https://example.com/app-two/launch");
const GURL app3_launch("https://not-example.com/app/launch");
const AppId app1_id = GenerateAppIdFromURL(app1_launch);
const AppId app2_id = GenerateAppIdFromURL(app2_launch);
const AppId app3_id = GenerateAppIdFromURL(app3_launch);
// Implicit scope "https://example.com/app/"
auto app1 = CreateWebApp(app1_launch.spec());
RegisterApp(std::move(app1));
base::Optional<AppId> app2_match =
registrar().FindAppWithUrlInScope(app2_page);
EXPECT_FALSE(app2_match);
base::Optional<AppId> app3_match =
registrar().FindAppWithUrlInScope(app3_page);
EXPECT_FALSE(app3_match);
auto app2 = CreateWebApp(app2_launch.spec());
RegisterApp(std::move(app2));
auto app3 = CreateWebApp(app3_launch.spec());
RegisterApp(std::move(app3));
base::Optional<AppId> app1_match =
registrar().FindAppWithUrlInScope(app1_page);
DCHECK(app1_match);
EXPECT_EQ(app1_match, base::Optional<AppId>(app1_id));
app2_match = registrar().FindAppWithUrlInScope(app2_page);
DCHECK(app2_match);
EXPECT_EQ(app2_match, base::Optional<AppId>(app2_id));
app3_match = registrar().FindAppWithUrlInScope(app3_page);
DCHECK(app3_match);
EXPECT_EQ(app3_match, base::Optional<AppId>(app3_id));
}
TEST_F(WebAppRegistrarTest, FindPwaOverShortcut) {
controller().Init();
const GURL app1_launch("https://example.com/app/specific/launch1");
const GURL app2_scope("https://example.com/app");
const GURL app2_page("https://example.com/app/specific/page2");
const AppId app2_id = GenerateAppIdFromURL(app2_scope);
const GURL app3_launch("https://example.com/app/specific/launch3");
auto app1 = CreateWebApp(app1_launch.spec());
RegisterApp(std::move(app1));
auto app2 = CreateWebApp(app2_scope.spec());
app2->SetScope(app2_scope);
RegisterApp(std::move(app2));
auto app3 = CreateWebApp(app3_launch.spec());
RegisterApp(std::move(app3));
base::Optional<AppId> app2_match =
registrar().FindAppWithUrlInScope(app2_page);
DCHECK(app2_match);
EXPECT_EQ(app2_match, base::Optional<AppId>(app2_id));
}
TEST_F(WebAppRegistrarTest, BeginAndCommitUpdate) {
std::set<AppId> ids = InitRegistrarWithApps("https://example.com/path", 10);
std::unique_ptr<WebAppRegistryUpdate> update = sync_bridge().BeginUpdate();
for (auto& app_id : ids) {
WebApp* app = update->UpdateApp(app_id);
EXPECT_TRUE(app);
app->SetName("New Name");
}
// Acquire each app second time to make sure update requests get merged.
for (auto& app_id : ids) {
WebApp* app = update->UpdateApp(app_id);
EXPECT_TRUE(app);
app->SetDisplayMode(DisplayMode::kStandalone);
}
SyncBridgeCommitUpdate(std::move(update));
// Make sure that all app ids were written to the database.
auto registry_written = database_factory().ReadRegistry();
EXPECT_EQ(ids.size(), registry_written.size());
for (auto& kv : registry_written) {
EXPECT_EQ("New Name", kv.second->name());
ids.erase(kv.second->app_id());
}
EXPECT_TRUE(ids.empty());
}
TEST_F(WebAppRegistrarTest, CommitEmptyUpdate) {
std::set<AppId> ids = InitRegistrarWithApps("https://example.com/path", 10);
const auto initial_registry = database_factory().ReadRegistry();
{
std::unique_ptr<WebAppRegistryUpdate> update = sync_bridge().BeginUpdate();
SyncBridgeCommitUpdate(std::move(update));
auto registry = database_factory().ReadRegistry();
EXPECT_TRUE(IsRegistryEqual(initial_registry, registry));
}
{
std::unique_ptr<WebAppRegistryUpdate> update = sync_bridge().BeginUpdate();
update.reset();
SyncBridgeCommitUpdate(std::move(update));
auto registry = database_factory().ReadRegistry();
EXPECT_TRUE(IsRegistryEqual(initial_registry, registry));
}
{
std::unique_ptr<WebAppRegistryUpdate> update = sync_bridge().BeginUpdate();
WebApp* app = update->UpdateApp("unknown");
EXPECT_FALSE(app);
SyncBridgeCommitUpdate(std::move(update));
auto registry = database_factory().ReadRegistry();
EXPECT_TRUE(IsRegistryEqual(initial_registry, registry));
}
}
TEST_F(WebAppRegistrarTest, ScopedRegistryUpdate) {
std::set<AppId> ids = InitRegistrarWithApps("https://example.com/path", 10);
const auto initial_registry = database_factory().ReadRegistry();
// Test empty update first.
{ ScopedRegistryUpdate update(&sync_bridge()); }
EXPECT_TRUE(
IsRegistryEqual(initial_registry, database_factory().ReadRegistry()));
{
ScopedRegistryUpdate update(&sync_bridge());
for (auto& app_id : ids) {
WebApp* app = update->UpdateApp(app_id);
EXPECT_TRUE(app);
app->SetDescription("New Description");
}
}
// Make sure that all app ids were written to the database.
auto updated_registry = database_factory().ReadRegistry();
EXPECT_EQ(ids.size(), updated_registry.size());
for (auto& kv : updated_registry) {
EXPECT_EQ(kv.second->description(), "New Description");
ids.erase(kv.second->app_id());
}
EXPECT_TRUE(ids.empty());
}
TEST_F(WebAppRegistrarTest, CopyOnWrite) {
controller().Init();
const GURL launch_url("https://example.com");
const AppId app_id = GenerateAppIdFromURL(launch_url);
const WebApp* app = nullptr;
{
auto new_app = CreateWebApp(launch_url.spec());
app = new_app.get();
RegisterApp(std::move(new_app));
}
{
std::unique_ptr<WebAppRegistryUpdate> update = sync_bridge().BeginUpdate();
WebApp* app_copy = update->UpdateApp(app_id);
EXPECT_TRUE(app_copy);
EXPECT_NE(app_copy, app);
app_copy->SetName("New Name");
EXPECT_EQ(app_copy->name(), "New Name");
EXPECT_EQ(app->name(), "Name");
app_copy->AddSource(Source::kPolicy);
app_copy->RemoveSource(Source::kSync);
EXPECT_FALSE(app_copy->IsSynced());
EXPECT_TRUE(app_copy->HasAnySources());
EXPECT_TRUE(app->IsSynced());
EXPECT_TRUE(app->HasAnySources());
SyncBridgeCommitUpdate(std::move(update));
}
// Pointer value stays the same.
EXPECT_EQ(app, registrar().GetAppById(app_id));
EXPECT_EQ(app->name(), "New Name");
EXPECT_FALSE(app->IsSynced());
EXPECT_TRUE(app->HasAnySources());
}
TEST_F(WebAppRegistrarTest, CountUserInstalledApps) {
controller().Init();
const std::string base_url{"https://example.com/path"};
for (int i = Source::kMinValue + 1; i <= Source::kMaxValue; ++i) {
auto source = static_cast<Source::Type>(i);
auto web_app =
CreateWebAppWithSource(base_url + base::NumberToString(i), source);
RegisterApp(std::move(web_app));
}
EXPECT_EQ(2, registrar().CountUserInstalledApps());
}
} // namespace web_app