| // Copyright 2018 The Chromium Authors |
| // 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_database.h" |
| |
| #include <memory> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/base64.h" |
| #include "base/files/file_path.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/run_loop.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/bind.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/test_future.h" |
| #include "base/time/time.h" |
| #include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_integrity_block_data.h" |
| #include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_storage_location.h" |
| #include "chrome/browser/web_applications/mojom/user_display_mode.mojom-shared.h" |
| #include "chrome/browser/web_applications/mojom/user_display_mode.mojom.h" |
| #include "chrome/browser/web_applications/proto/web_app.pb.h" |
| #include "chrome/browser/web_applications/proto/web_app_install_state.pb.h" |
| #include "chrome/browser/web_applications/scope_extension_info.h" |
| #include "chrome/browser/web_applications/test/fake_web_app_database_factory.h" |
| #include "chrome/browser/web_applications/test/fake_web_app_provider.h" |
| #include "chrome/browser/web_applications/test/web_app_test.h" |
| #include "chrome/browser/web_applications/test/web_app_test_utils.h" |
| #include "chrome/browser/web_applications/user_display_mode.h" |
| #include "chrome/browser/web_applications/web_app.h" |
| #include "chrome/browser/web_applications/web_app_command_manager.h" |
| #include "chrome/browser/web_applications/web_app_constants.h" |
| #include "chrome/browser/web_applications/web_app_helpers.h" |
| #include "chrome/browser/web_applications/web_app_install_info.h" |
| #include "chrome/browser/web_applications/web_app_install_manager.h" |
| #include "chrome/browser/web_applications/web_app_proto_utils.h" |
| #include "chrome/browser/web_applications/web_app_registrar.h" |
| #include "chrome/browser/web_applications/web_app_registry_update.h" |
| #include "chrome/browser/web_applications/web_app_sync_bridge.h" |
| #include "chrome/browser/web_applications/web_app_utils.h" |
| #include "components/services/app_service/public/cpp/file_handler.h" |
| #include "components/services/app_service/public/cpp/protocol_handler_info.h" |
| #include "components/services/app_service/public/cpp/share_target.h" |
| #include "components/services/app_service/public/cpp/url_handler_info.h" |
| #include "components/sync/model/data_type_store.h" |
| #include "components/sync/protocol/web_app_specifics.pb.h" |
| #include "components/sync/test/mock_data_type_local_change_processor.h" |
| #include "components/web_package/signed_web_bundles/ed25519_public_key.h" |
| #include "components/web_package/signed_web_bundles/ed25519_signature.h" |
| #include "components/web_package/signed_web_bundles/signed_web_bundle_signature_stack_entry.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/abseil-cpp/absl/types/variant.h" |
| #include "third_party/blink/public/common/permissions_policy/origin_with_possible_wildcards.h" |
| #include "third_party/blink/public/common/permissions_policy/permissions_policy_declaration.h" |
| #include "third_party/blink/public/mojom/permissions_policy/permissions_policy_feature.mojom.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| |
| namespace web_app { |
| |
| using ::testing::Eq; |
| using ::testing::Field; |
| using ::testing::IsNull; |
| using ::testing::NotNull; |
| using ::testing::Optional; |
| using ::testing::Property; |
| using ::testing::VariantWith; |
| |
| class WebAppDatabaseTest : public WebAppTest { |
| public: |
| void SetUp() override { |
| WebAppTest::SetUp(); |
| provider_ = FakeWebAppProvider::Get(profile()); |
| |
| auto sync_bridge = std::make_unique<WebAppSyncBridge>( |
| &provider_->GetRegistrarMutable(), |
| mock_processor_.CreateForwardingProcessor()); |
| sync_bridge_ = sync_bridge.get(); |
| |
| auto database_factory = std::make_unique<FakeWebAppDatabaseFactory>(); |
| database_factory_ = database_factory.get(); |
| |
| provider_->SetDatabaseFactory(std::move(database_factory)); |
| provider_->SetSyncBridge(std::move(sync_bridge)); |
| |
| sync_bridge_->SetSubsystems( |
| database_factory_, &provider_->GetCommandManager(), |
| &provider_->scheduler(), &provider_->GetInstallManager()); |
| |
| provider_->Start(); |
| ON_CALL(mock_processor_, IsTrackingMetadata()) |
| .WillByDefault(testing::Return(true)); |
| } |
| |
| bool IsDatabaseRegistryEqualToRegistrar() { |
| Registry registry = database_factory().ReadRegistry(); |
| return IsRegistryEqual(mutable_registrar().registry(), registry); |
| } |
| |
| void WriteBatch( |
| std::unique_ptr<syncer::DataTypeStore::WriteBatch> write_batch) { |
| base::RunLoop run_loop; |
| |
| database_factory().GetStore()->CommitWriteBatch( |
| std::move(write_batch), |
| base::BindLambdaForTesting( |
| [&](const std::optional<syncer::ModelError>& error) { |
| EXPECT_FALSE(error); |
| run_loop.Quit(); |
| })); |
| |
| run_loop.Run(); |
| } |
| |
| Registry WriteWebApps(uint32_t num_apps, bool ensure_no_migration_needed) { |
| Registry registry; |
| |
| auto write_batch = database_factory().GetStore()->CreateWriteBatch(); |
| |
| for (uint32_t i = 0; i < num_apps; ++i) { |
| std::unique_ptr<WebApp> app = test::CreateRandomWebApp({.seed = i}); |
| if (ensure_no_migration_needed) { |
| EnsureHasUserDisplayModeForCurrentPlatform(*app); |
| } |
| std::unique_ptr<WebAppProto> proto = |
| WebAppDatabase::CreateWebAppProto(*app); |
| const webapps::AppId app_id = app->app_id(); |
| |
| write_batch->WriteData(app_id, proto->SerializeAsString()); |
| |
| registry.emplace(app_id, std::move(app)); |
| } |
| |
| WriteBatch(std::move(write_batch)); |
| |
| return registry; |
| } |
| |
| void EnsureHasUserDisplayModeForCurrentPlatform(WebApp& app) { |
| // Avoid using `WebApp::user_display_mode` because it DCHECKs for a valid |
| // UDM. |
| #if BUILDFLAG(IS_CHROMEOS) |
| if (app.sync_proto().has_user_display_mode_cros()) { |
| return; |
| } |
| #else |
| if (app.sync_proto().has_user_display_mode_default()) { |
| return; |
| } |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| app.SetUserDisplayMode(ToMojomUserDisplayMode( |
| app.sync_proto().has_user_display_mode_default() |
| ? app.sync_proto().user_display_mode_default() |
| : sync_pb::WebAppSpecifics_UserDisplayMode_STANDALONE)); |
| } |
| |
| protected: |
| FakeWebAppDatabaseFactory& database_factory() { return *database_factory_; } |
| |
| WebAppRegistrar& registrar() { return provider_->GetRegistrarMutable(); } |
| |
| WebAppRegistrarMutable& mutable_registrar() { |
| return provider_->GetRegistrarMutable(); |
| } |
| |
| WebAppSyncBridge& sync_bridge() { return *sync_bridge_; } |
| |
| void InitSyncBridge() { |
| base::RunLoop loop; |
| sync_bridge_->Init(loop.QuitClosure()); |
| loop.Run(); |
| } |
| |
| void RegisterApp(std::unique_ptr<WebApp> web_app) { |
| ScopedRegistryUpdate update = sync_bridge().BeginUpdate(); |
| update->CreateApp(std::move(web_app)); |
| } |
| |
| void UnregisterApp(const webapps::AppId& app_id) { |
| ScopedRegistryUpdate update = sync_bridge().BeginUpdate(); |
| update->DeleteApp(app_id); |
| } |
| |
| void UnregisterAll() { |
| ScopedRegistryUpdate update = sync_bridge().BeginUpdate(); |
| for (const webapps::AppId& app_id : registrar().GetAppIds()) { |
| update->DeleteApp(app_id); |
| } |
| } |
| |
| private: |
| raw_ptr<WebAppSyncBridge, DanglingUntriaged> sync_bridge_ = nullptr; |
| raw_ptr<FakeWebAppDatabaseFactory, DanglingUntriaged> database_factory_ = |
| nullptr; |
| raw_ptr<FakeWebAppProvider, DanglingUntriaged> provider_ = nullptr; |
| base::test::ScopedFeatureList feature_list_; |
| |
| testing::NiceMock<syncer::MockDataTypeLocalChangeProcessor> mock_processor_; |
| }; |
| |
| TEST_F(WebAppDatabaseTest, WriteAndReadRegistry) { |
| InitSyncBridge(); |
| EXPECT_TRUE(registrar().is_empty()); |
| |
| const uint32_t num_apps = 100; |
| |
| std::unique_ptr<WebApp> app = test::CreateRandomWebApp({.seed = 0}); |
| webapps::AppId app_id = app->app_id(); |
| RegisterApp(std::move(app)); |
| EXPECT_TRUE(IsDatabaseRegistryEqualToRegistrar()); |
| |
| for (uint32_t i = 1; i <= num_apps; ++i) { |
| std::unique_ptr<WebApp> extra_app = test::CreateRandomWebApp({.seed = i}); |
| RegisterApp(std::move(extra_app)); |
| } |
| EXPECT_TRUE(IsDatabaseRegistryEqualToRegistrar()); |
| |
| UnregisterApp(app_id); |
| EXPECT_TRUE(IsDatabaseRegistryEqualToRegistrar()); |
| |
| UnregisterAll(); |
| EXPECT_TRUE(IsDatabaseRegistryEqualToRegistrar()); |
| } |
| |
| TEST_F(WebAppDatabaseTest, WriteAndDeleteAppsWithCallbacks) { |
| InitSyncBridge(); |
| EXPECT_TRUE(registrar().is_empty()); |
| |
| const uint32_t num_apps = 100; |
| |
| RegistryUpdateData::Apps apps_to_create; |
| std::vector<webapps::AppId> apps_to_delete; |
| Registry expected_registry; |
| |
| #if BUILDFLAG(IS_CHROMEOS_ASH) |
| bool allow_system_source = true; |
| #else |
| bool allow_system_source = false; |
| #endif |
| |
| for (uint32_t i = 0; i < num_apps; ++i) { |
| std::unique_ptr<WebApp> app = test::CreateRandomWebApp( |
| {.seed = i, .allow_system_source = allow_system_source}); |
| apps_to_delete.push_back(app->app_id()); |
| apps_to_create.push_back(std::move(app)); |
| |
| std::unique_ptr<WebApp> expected_app = test::CreateRandomWebApp( |
| {.seed = i, .allow_system_source = allow_system_source}); |
| expected_registry.emplace(expected_app->app_id(), std::move(expected_app)); |
| } |
| |
| { |
| base::test::TestFuture<bool> future; |
| { |
| ScopedRegistryUpdate update = |
| sync_bridge().BeginUpdate(future.GetCallback()); |
| for (std::unique_ptr<WebApp>& web_app : apps_to_create) { |
| update->CreateApp(std::move(web_app)); |
| } |
| } |
| EXPECT_TRUE(future.Take()); |
| |
| Registry registry_written = database_factory().ReadRegistry(); |
| EXPECT_TRUE(IsRegistryEqual(registry_written, expected_registry)); |
| } |
| |
| { |
| base::test::TestFuture<bool> future; |
| { |
| ScopedRegistryUpdate update = |
| sync_bridge().BeginUpdate(future.GetCallback()); |
| for (const webapps::AppId& app_id : apps_to_delete) { |
| update->DeleteApp(app_id); |
| } |
| } |
| EXPECT_TRUE(future.Take()); |
| |
| Registry registry_deleted = database_factory().ReadRegistry(); |
| EXPECT_TRUE(registry_deleted.empty()); |
| } |
| } |
| |
| // Read a database where all apps are already in a valid state, so there should |
| // be no difference between the apps written and read. |
| TEST_F(WebAppDatabaseTest, OpenDatabaseAndReadRegistry) { |
| Registry registry = WriteWebApps(100, /*ensure_no_migration_needed=*/true); |
| |
| InitSyncBridge(); |
| EXPECT_TRUE(IsRegistryEqual(mutable_registrar().registry(), registry)); |
| } |
| |
| // Read a database where some apps will be migrated at read time. |
| TEST_F(WebAppDatabaseTest, OpenDatabaseAndReadRegistryWithMigration) { |
| Registry registry = WriteWebApps(100, /*ensure_no_migration_needed=*/false); |
| |
| InitSyncBridge(); |
| |
| // Some apps should have been migrated from an invalid state (missing |
| // UserDisplayMode setting for the current platform) at read time. |
| EXPECT_FALSE(IsRegistryEqual(mutable_registrar().registry(), registry)); |
| |
| // Update the registry so apps reflect expected migrated state. |
| for (auto& [app_id, app] : registry) { |
| #if BUILDFLAG(IS_CHROMEOS_LACROS) |
| // System Web Apps are ignored by the registry on Lacros. |
| if (app->IsSystemApp()) { |
| continue; |
| } |
| #endif |
| EnsureHasUserDisplayModeForCurrentPlatform(*app); |
| } |
| |
| EXPECT_TRUE(IsRegistryEqual(mutable_registrar().registry(), registry)); |
| } |
| |
| TEST_F(WebAppDatabaseTest, BackwardCompatibility_WebAppWithOnlyRequiredFields) { |
| const GURL start_url{"https://example.com/"}; |
| const webapps::AppId app_id = |
| GenerateAppId(/*manifest_id=*/std::nullopt, start_url); |
| const std::string name = "App Name"; |
| |
| std::vector<std::unique_ptr<WebAppProto>> protos; |
| |
| // Create a proto with |required| only fields. |
| // Do not add new fields in this test: any new fields should be |optional|. |
| auto proto = std::make_unique<WebAppProto>(); |
| { |
| sync_pb::WebAppSpecifics sync_proto; |
| sync_proto.set_start_url(start_url.spec()); |
| sync_proto.set_user_display_mode_default( |
| sync_pb::WebAppSpecifics_UserDisplayMode_BROWSER); |
| *(proto->mutable_sync_data()) = std::move(sync_proto); |
| } |
| |
| proto->set_name(name); |
| proto->set_install_state(proto::INSTALLED_WITH_OS_INTEGRATION); |
| |
| proto->mutable_sources()->set_system(false); |
| proto->mutable_sources()->set_policy(false); |
| proto->mutable_sources()->set_web_app_store(false); |
| proto->mutable_sources()->set_sync(true); |
| proto->mutable_sources()->set_default_(false); |
| |
| if (IsChromeOsDataMandatory()) { |
| proto->mutable_chromeos_data()->set_show_in_launcher(false); |
| proto->mutable_chromeos_data()->set_show_in_search_and_shelf(false); |
| proto->mutable_chromeos_data()->set_show_in_management(false); |
| proto->mutable_chromeos_data()->set_is_disabled(true); |
| } |
| |
| protos.push_back(std::move(proto)); |
| database_factory().WriteProtos(protos); |
| |
| // Read the registry: the proto parsing may fail while reading the proto |
| // above. |
| InitSyncBridge(); |
| |
| const WebApp* app = registrar().GetAppById(app_id); |
| EXPECT_EQ(app_id, app->app_id()); |
| EXPECT_EQ(start_url, app->start_url()); |
| EXPECT_EQ(name, app->untranslated_name()); |
| EXPECT_EQ(mojom::UserDisplayMode::kBrowser, app->user_display_mode()); |
| EXPECT_EQ(proto::INSTALLED_WITHOUT_OS_INTEGRATION, app->install_state()); |
| EXPECT_TRUE(app->IsSynced()); |
| EXPECT_FALSE(app->IsPreinstalledApp()); |
| |
| if (IsChromeOsDataMandatory()) { |
| EXPECT_FALSE(app->chromeos_data()->show_in_launcher); |
| EXPECT_FALSE(app->chromeos_data()->show_in_search_and_shelf); |
| EXPECT_FALSE(app->chromeos_data()->show_in_management); |
| EXPECT_TRUE(app->chromeos_data()->is_disabled); |
| } else { |
| EXPECT_FALSE(app->chromeos_data().has_value()); |
| } |
| } |
| |
| TEST_F(WebAppDatabaseTest, UserDisplayModeCrosOnly_MigratesToCurrentPlatform) { |
| std::unique_ptr<WebApp> base_app = test::CreateRandomWebApp({}); |
| std::unique_ptr<WebAppProto> base_proto = |
| WebAppDatabase::CreateWebAppProto(*base_app); |
| |
| base_proto->mutable_sync_data()->set_user_display_mode_cros( |
| sync_pb::WebAppSpecifics_UserDisplayMode_BROWSER); |
| base_proto->mutable_sync_data()->clear_user_display_mode_default(); |
| |
| std::vector<std::unique_ptr<WebAppProto>> protos; |
| protos.push_back(std::move(base_proto)); |
| database_factory().WriteProtos(protos); |
| |
| InitSyncBridge(); |
| |
| const WebApp* app = registrar().GetAppById(base_app->app_id()); |
| std::unique_ptr<WebAppProto> new_proto = |
| WebAppDatabase::CreateWebAppProto(*app); |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| // On CrOS, the default field should remain absent. |
| EXPECT_EQ(new_proto->sync_data().user_display_mode_cros(), |
| sync_pb::WebAppSpecifics_UserDisplayMode_BROWSER); |
| EXPECT_FALSE(new_proto->sync_data().has_user_display_mode_default()); |
| EXPECT_EQ(app->user_display_mode(), mojom::UserDisplayMode::kBrowser); |
| #else |
| // On non-CrOS, both platform's fields should now be populated. |
| EXPECT_EQ(new_proto->sync_data().user_display_mode_cros(), |
| sync_pb::WebAppSpecifics_UserDisplayMode_BROWSER); |
| // Default value doesn't migrate from CrOS value so should fall back to |
| // standalone. |
| EXPECT_EQ(new_proto->sync_data().user_display_mode_default(), |
| sync_pb::WebAppSpecifics_UserDisplayMode_STANDALONE); |
| EXPECT_EQ(app->user_display_mode(), mojom::UserDisplayMode::kStandalone); |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| } |
| |
| TEST_F(WebAppDatabaseTest, |
| UserDisplayModeDefaultOnly_MigratesToCurrentPlatform) { |
| std::unique_ptr<WebApp> base_app = test::CreateRandomWebApp({}); |
| std::unique_ptr<WebAppProto> base_proto = |
| WebAppDatabase::CreateWebAppProto(*base_app); |
| |
| base_proto->mutable_sync_data()->set_user_display_mode_default( |
| sync_pb::WebAppSpecifics_UserDisplayMode_BROWSER); |
| base_proto->mutable_sync_data()->clear_user_display_mode_cros(); |
| |
| std::vector<std::unique_ptr<WebAppProto>> protos; |
| protos.push_back(std::move(base_proto)); |
| database_factory().WriteProtos(protos); |
| |
| InitSyncBridge(); |
| |
| const WebApp* app = registrar().GetAppById(base_app->app_id()); |
| |
| // Regardless of platform, the current platform's UDM should be set: the |
| // default value should have been migrated in CrOS. |
| EXPECT_EQ(app->user_display_mode(), mojom::UserDisplayMode::kBrowser); |
| |
| std::unique_ptr<WebAppProto> new_proto = |
| WebAppDatabase::CreateWebAppProto(*app); |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| // On CrOS, both platform's fields should now be populated. |
| EXPECT_EQ(new_proto->sync_data().user_display_mode_cros(), |
| sync_pb::WebAppSpecifics_UserDisplayMode_BROWSER); |
| EXPECT_EQ(new_proto->sync_data().user_display_mode_default(), |
| sync_pb::WebAppSpecifics_UserDisplayMode_BROWSER); |
| #else |
| // On non-CrOS, the CrOS field should remain absent. |
| EXPECT_FALSE(new_proto->sync_data().has_user_display_mode_cros()); |
| EXPECT_EQ(new_proto->sync_data().user_display_mode_default(), |
| sync_pb::WebAppSpecifics_UserDisplayMode_BROWSER); |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| } |
| |
| TEST_F(WebAppDatabaseTest, WebAppWithoutOptionalFields) { |
| InitSyncBridge(); |
| |
| const auto start_url = GURL("https://example.com/"); |
| const webapps::AppId app_id = |
| GenerateAppId(/*manifest_id=*/std::nullopt, GURL(start_url)); |
| const std::string name = "Name"; |
| |
| auto app = std::make_unique<WebApp>(app_id); |
| |
| // Required fields: |
| app->SetStartUrl(start_url); |
| app->SetManifestId(GenerateManifestIdFromStartUrlOnly(start_url)); |
| app->SetName(name); |
| app->SetUserDisplayMode(mojom::UserDisplayMode::kBrowser); |
| app->SetInstallState(proto::InstallState::SUGGESTED_FROM_ANOTHER_DEVICE); |
| // chromeos_data should always be set on ChromeOS. |
| if (IsChromeOsDataMandatory()) |
| app->SetWebAppChromeOsData(std::make_optional<WebAppChromeOsData>()); |
| |
| EXPECT_FALSE(app->HasAnySources()); |
| for (WebAppManagement::Type type : WebAppManagementTypes::All()) { |
| app->AddSource(type); |
| EXPECT_TRUE(app->HasAnySources()); |
| } |
| |
| // Let optional fields be empty: |
| EXPECT_EQ(app->display_mode(), DisplayMode::kUndefined); |
| EXPECT_TRUE(app->display_mode_override().empty()); |
| EXPECT_TRUE(app->untranslated_description().empty()); |
| EXPECT_TRUE(app->scope().is_empty()); |
| EXPECT_FALSE(app->theme_color().has_value()); |
| EXPECT_FALSE(app->dark_mode_theme_color().has_value()); |
| EXPECT_FALSE(app->background_color().has_value()); |
| EXPECT_FALSE(app->dark_mode_background_color().has_value()); |
| EXPECT_TRUE(app->manifest_icons().empty()); |
| EXPECT_TRUE(app->downloaded_icon_sizes(IconPurpose::ANY).empty()); |
| EXPECT_TRUE(app->downloaded_icon_sizes(IconPurpose::MASKABLE).empty()); |
| EXPECT_TRUE(app->downloaded_icon_sizes(IconPurpose::MONOCHROME).empty()); |
| EXPECT_FALSE(app->is_generated_icon()); |
| EXPECT_FALSE(app->is_from_sync_and_pending_installation()); |
| EXPECT_FALSE(app->sync_proto().has_name()); |
| EXPECT_FALSE(app->sync_proto().has_theme_color()); |
| EXPECT_FALSE(app->sync_proto().has_scope()); |
| EXPECT_EQ(app->sync_proto().icon_infos_size(), 0); |
| EXPECT_TRUE(app->file_handlers().empty()); |
| EXPECT_FALSE(app->share_target().has_value()); |
| EXPECT_TRUE(app->additional_search_terms().empty()); |
| EXPECT_TRUE(app->protocol_handlers().empty()); |
| EXPECT_TRUE(app->allowed_launch_protocols().empty()); |
| EXPECT_TRUE(app->disallowed_launch_protocols().empty()); |
| EXPECT_TRUE(app->url_handlers().empty()); |
| EXPECT_TRUE(app->scope_extensions().empty()); |
| EXPECT_TRUE(app->validated_scope_extensions().empty()); |
| EXPECT_TRUE(app->last_badging_time().is_null()); |
| EXPECT_TRUE(app->last_launch_time().is_null()); |
| EXPECT_TRUE(app->first_install_time().is_null()); |
| EXPECT_TRUE(app->shortcuts_menu_item_infos().empty()); |
| EXPECT_EQ(app->run_on_os_login_mode(), RunOnOsLoginMode::kNotRun); |
| EXPECT_TRUE(app->manifest_url().is_empty()); |
| EXPECT_TRUE(app->permissions_policy().empty()); |
| EXPECT_FALSE(app->isolation_data().has_value()); |
| EXPECT_TRUE(app->latest_install_time().is_null()); |
| |
| RegisterApp(std::move(app)); |
| |
| Registry registry = database_factory().ReadRegistry(); |
| EXPECT_EQ(1UL, registry.size()); |
| |
| std::unique_ptr<WebApp>& app_copy = registry.at(app_id); |
| |
| // Required fields were serialized: |
| EXPECT_EQ(app_id, app_copy->app_id()); |
| EXPECT_EQ(GenerateManifestIdFromStartUrlOnly(start_url), |
| app_copy->manifest_id()); |
| EXPECT_EQ(start_url, app_copy->start_url()); |
| EXPECT_EQ(name, app_copy->untranslated_name()); |
| EXPECT_EQ(mojom::UserDisplayMode::kBrowser, app_copy->user_display_mode()); |
| EXPECT_EQ(proto::SUGGESTED_FROM_ANOTHER_DEVICE, app_copy->install_state()); |
| |
| auto& chromeos_data = app_copy->chromeos_data(); |
| if (IsChromeOsDataMandatory()) { |
| EXPECT_TRUE(chromeos_data->show_in_launcher); |
| EXPECT_TRUE(chromeos_data->show_in_search_and_shelf); |
| EXPECT_TRUE(chromeos_data->show_in_management); |
| EXPECT_FALSE(chromeos_data->is_disabled); |
| EXPECT_FALSE(chromeos_data->oem_installed); |
| } else { |
| EXPECT_FALSE(chromeos_data.has_value()); |
| } |
| |
| for (WebAppManagement::Type type : WebAppManagementTypes::All()) { |
| EXPECT_TRUE(app_copy->HasAnySources()); |
| app_copy->RemoveSource(type); |
| } |
| EXPECT_FALSE(app_copy->HasAnySources()); |
| |
| // No optional fields. |
| EXPECT_EQ(app_copy->display_mode(), DisplayMode::kUndefined); |
| EXPECT_TRUE(app_copy->display_mode_override().empty()); |
| EXPECT_TRUE(app_copy->untranslated_description().empty()); |
| EXPECT_TRUE(app_copy->scope().is_empty()); |
| EXPECT_FALSE(app_copy->theme_color().has_value()); |
| EXPECT_FALSE(app_copy->dark_mode_theme_color().has_value()); |
| EXPECT_FALSE(app_copy->background_color().has_value()); |
| EXPECT_FALSE(app_copy->dark_mode_background_color().has_value()); |
| EXPECT_TRUE(app_copy->last_badging_time().is_null()); |
| EXPECT_TRUE(app_copy->last_launch_time().is_null()); |
| EXPECT_TRUE(app_copy->first_install_time().is_null()); |
| EXPECT_TRUE(app_copy->manifest_icons().empty()); |
| EXPECT_TRUE(app_copy->downloaded_icon_sizes(IconPurpose::ANY).empty()); |
| EXPECT_TRUE(app_copy->downloaded_icon_sizes(IconPurpose::MASKABLE).empty()); |
| EXPECT_TRUE(app_copy->downloaded_icon_sizes(IconPurpose::MONOCHROME).empty()); |
| EXPECT_FALSE(app_copy->is_generated_icon()); |
| EXPECT_FALSE(app_copy->is_from_sync_and_pending_installation()); |
| EXPECT_FALSE(app_copy->sync_proto().has_name()); |
| EXPECT_FALSE(app_copy->sync_proto().has_theme_color()); |
| EXPECT_FALSE(app_copy->sync_proto().has_scope()); |
| EXPECT_EQ(app_copy->sync_proto().icon_infos_size(), 0); |
| EXPECT_TRUE(app_copy->file_handlers().empty()); |
| EXPECT_FALSE(app_copy->share_target().has_value()); |
| EXPECT_TRUE(app_copy->additional_search_terms().empty()); |
| EXPECT_TRUE(app_copy->allowed_launch_protocols().empty()); |
| EXPECT_TRUE(app_copy->disallowed_launch_protocols().empty()); |
| EXPECT_TRUE(app_copy->url_handlers().empty()); |
| EXPECT_TRUE(app_copy->scope_extensions().empty()); |
| EXPECT_TRUE(app_copy->validated_scope_extensions().empty()); |
| EXPECT_TRUE(app_copy->shortcuts_menu_item_infos().empty()); |
| EXPECT_EQ(app_copy->run_on_os_login_mode(), RunOnOsLoginMode::kNotRun); |
| EXPECT_TRUE(app_copy->manifest_url().is_empty()); |
| EXPECT_TRUE(app_copy->permissions_policy().empty()); |
| EXPECT_FALSE(app_copy->tab_strip()); |
| EXPECT_TRUE(app_copy->latest_install_time().is_null()); |
| } |
| |
| TEST_F(WebAppDatabaseTest, WebAppWithManyIcons) { |
| InitSyncBridge(); |
| |
| const GURL base_url("https://example.com/path"); |
| // A number of icons of each IconPurpose. |
| const int num_icons = 32; |
| |
| std::unique_ptr<WebApp> app = |
| test::CreateRandomWebApp({.base_url = base_url}); |
| webapps::AppId app_id = app->app_id(); |
| |
| std::vector<apps::IconInfo> icons; |
| |
| for (IconPurpose purpose : kIconPurposes) { |
| std::vector<SquareSizePx> sizes; |
| for (int i = 1; i <= num_icons; ++i) { |
| apps::IconInfo icon; |
| icon.url = base_url.Resolve("icon" + base::NumberToString(num_icons)); |
| // Let size equals the icon's number squared. |
| icon.square_size_px = i * i; |
| |
| icon.purpose = ManifestPurposeToIconInfoPurpose(purpose); |
| sizes.push_back(*icon.square_size_px); |
| icons.push_back(std::move(icon)); |
| } |
| |
| app->SetDownloadedIconSizes(purpose, std::move(sizes)); |
| } |
| |
| app->SetManifestIcons(std::move(icons)); |
| app->SetIsGeneratedIcon(false); |
| |
| RegisterApp(std::move(app)); |
| |
| Registry registry = database_factory().ReadRegistry(); |
| EXPECT_EQ(1UL, registry.size()); |
| |
| std::unique_ptr<WebApp>& app_copy = registry.at(app_id); |
| EXPECT_EQ(static_cast<unsigned>(num_icons * kIconPurposes.size()), |
| app_copy->manifest_icons().size()); |
| for (int i = 1; i <= num_icons; ++i) { |
| const int icon_size_in_px = i * i; |
| EXPECT_EQ(icon_size_in_px, |
| app_copy->manifest_icons()[i - 1].square_size_px); |
| } |
| EXPECT_FALSE(app_copy->is_generated_icon()); |
| } |
| |
| TEST_F(WebAppDatabaseTest, MigrateOldLaunchHandlerSyntax) { |
| std::unique_ptr<WebApp> base_app = test::CreateRandomWebApp({}); |
| std::unique_ptr<WebAppProto> base_proto = |
| WebAppDatabase::CreateWebAppProto(*base_app); |
| |
| // "launch_handler": { |
| // "route_to": "existing-client", |
| // "navigate_existing_client": "always" |
| // } |
| // -> |
| // "launch_handler": { |
| // "client_mode": "navigate-existing" |
| // } |
| WebAppProto old_navigate_proto(*base_proto); |
| old_navigate_proto.mutable_launch_handler()->set_route_to( |
| LaunchHandlerProto_DeprecatedRouteTo_EXISTING_CLIENT); |
| old_navigate_proto.mutable_launch_handler()->set_navigate_existing_client( |
| LaunchHandlerProto_DeprecatedNavigateExistingClient_ALWAYS); |
| old_navigate_proto.mutable_launch_handler()->set_client_mode( |
| LaunchHandlerProto_ClientMode_UNSPECIFIED_CLIENT_MODE); |
| |
| std::unique_ptr<WebApp> new_navigate_app = |
| WebAppDatabase::CreateWebApp(old_navigate_proto); |
| EXPECT_EQ(new_navigate_app->launch_handler(), |
| (LaunchHandler{LaunchHandler::ClientMode::kNavigateExisting})) |
| << new_navigate_app->launch_handler()->client_mode; |
| |
| std::unique_ptr<WebAppProto> new_navigate_proto = |
| WebAppDatabase::CreateWebAppProto(*new_navigate_app); |
| EXPECT_EQ(new_navigate_proto->launch_handler().route_to(), |
| LaunchHandlerProto_DeprecatedRouteTo_UNSPECIFIED_ROUTE); |
| EXPECT_EQ( |
| new_navigate_proto->launch_handler().navigate_existing_client(), |
| LaunchHandlerProto_DeprecatedNavigateExistingClient_UNSPECIFIED_NAVIGATE); |
| EXPECT_EQ(new_navigate_proto->launch_handler().client_mode(), |
| LaunchHandlerProto_ClientMode_NAVIGATE_EXISTING); |
| |
| // "launch_handler": { |
| // "route_to": "existing-client", |
| // "navigate_existing_client": "never" |
| // } |
| // -> |
| // "launch_handler": { |
| // "client_mode": "focus-existing" |
| // } |
| WebAppProto old_focus_proto(*base_proto); |
| old_focus_proto.mutable_launch_handler()->set_route_to( |
| LaunchHandlerProto_DeprecatedRouteTo_EXISTING_CLIENT); |
| old_focus_proto.mutable_launch_handler()->set_navigate_existing_client( |
| LaunchHandlerProto_DeprecatedNavigateExistingClient_NEVER); |
| old_focus_proto.mutable_launch_handler()->set_client_mode( |
| LaunchHandlerProto_ClientMode_UNSPECIFIED_CLIENT_MODE); |
| |
| std::unique_ptr<WebApp> new_focus_app = |
| WebAppDatabase::CreateWebApp(old_focus_proto); |
| EXPECT_EQ(new_focus_app->launch_handler(), |
| (LaunchHandler{LaunchHandler::ClientMode::kFocusExisting})); |
| |
| std::unique_ptr<WebAppProto> new_focus_proto = |
| WebAppDatabase::CreateWebAppProto(*new_focus_app); |
| EXPECT_EQ(new_focus_proto->launch_handler().route_to(), |
| LaunchHandlerProto_DeprecatedRouteTo_UNSPECIFIED_ROUTE); |
| EXPECT_EQ( |
| new_focus_proto->launch_handler().navigate_existing_client(), |
| LaunchHandlerProto_DeprecatedNavigateExistingClient_UNSPECIFIED_NAVIGATE); |
| EXPECT_EQ(new_focus_proto->launch_handler().client_mode(), |
| LaunchHandlerProto_ClientMode_FOCUS_EXISTING); |
| } |
| |
| // Tests handling crashes fixed in crbug.com/1417955. |
| TEST_F(WebAppDatabaseTest, MigrateFromMissingShortcutsSizes) { |
| std::unique_ptr<WebApp> base_app = test::CreateRandomWebApp({}); |
| WebAppShortcutsMenuItemInfo shortcut_item_info{}; |
| shortcut_item_info.name = u"shortcut"; |
| shortcut_item_info.url = GURL("http://example.com/shortcut"); |
| shortcut_item_info.downloaded_icon_sizes.any = {42}; |
| shortcut_item_info.downloaded_icon_sizes.maskable = {24}; |
| shortcut_item_info.downloaded_icon_sizes.monochrome = {123}; |
| base_app->SetShortcutsMenuInfo({shortcut_item_info}); |
| |
| std::unique_ptr<WebAppProto> base_proto = |
| WebAppDatabase::CreateWebAppProto(*base_app); |
| |
| WebAppProto proto_without_shortcut_info(*base_proto); |
| proto_without_shortcut_info.clear_shortcuts_menu_item_infos(); |
| // Fail to parse when fewer shortcut infos than downloaded sizes. No evidence |
| // this happens in the wild. |
| EXPECT_EQ(WebAppDatabase::CreateWebApp(proto_without_shortcut_info), nullptr); |
| |
| // If DB is missing downloaded shortcut icon sizes information, expect to pad |
| // the vector with empty IconSizes structs so the vectors in WebApp have equal |
| // length. |
| WebAppProto proto_without_downloaded_sizes(*base_proto); |
| proto_without_downloaded_sizes.clear_downloaded_shortcuts_menu_icons_sizes(); |
| auto roundtrip_app = |
| WebAppDatabase::CreateWebApp(proto_without_downloaded_sizes); |
| |
| auto app_with_empty_downloaded_sizes = std::make_unique<WebApp>(*base_app); |
| shortcut_item_info.downloaded_icon_sizes = {}; |
| app_with_empty_downloaded_sizes->SetShortcutsMenuInfo({shortcut_item_info}); |
| |
| EXPECT_EQ(base::ToString(*roundtrip_app), |
| base::ToString(*app_with_empty_downloaded_sizes)); |
| } |
| |
| // Old versions of Chrome may have stored sync data with a manifest_id_path |
| // containing a fragment part in the URL. It should be stripped out, because the |
| // spec requires that ManifestIds with different fragments are considered |
| // equivalent. |
| TEST_F(WebAppDatabaseTest, RemovesFragmentFromSyncProtoManifestIdPath) { |
| base::HistogramTester histogram_tester; |
| |
| std::unique_ptr<WebApp> app = test::CreateRandomWebApp({}); |
| // Apps must always have a valid manifest ID without a ref. |
| EXPECT_TRUE(app->manifest_id().is_valid()); |
| EXPECT_FALSE(app->manifest_id().has_ref()); |
| std::string relative_manifest_id_path = |
| app->sync_proto().relative_manifest_id(); |
| |
| std::unique_ptr<WebAppProto> proto = WebAppDatabase::CreateWebAppProto(*app); |
| proto->mutable_sync_data()->set_relative_manifest_id( |
| relative_manifest_id_path + "#fragment"); |
| EXPECT_EQ(proto->sync_data().relative_manifest_id(), |
| relative_manifest_id_path + "#fragment"); |
| |
| // Re-parse the app from the proto. |
| auto roundtrip_app = WebAppDatabase::CreateWebApp(*proto); |
| ASSERT_TRUE(roundtrip_app); |
| |
| // Loaded app should have had the fragment stripped. |
| EXPECT_EQ(roundtrip_app->sync_proto().relative_manifest_id(), |
| relative_manifest_id_path); |
| EXPECT_FALSE(roundtrip_app->manifest_id().has_ref()); |
| |
| histogram_tester.ExpectUniqueSample("WebApp.CreateWebApp.ManifestIdMatch", |
| false, 1); |
| } |
| |
| TEST_F(WebAppDatabaseTest, RemovesFragmentAndQueriesFromScopeDuringParsing) { |
| std::unique_ptr<WebApp> app = test::CreateRandomWebApp({}); |
| EXPECT_TRUE(app->scope().is_valid()); |
| EXPECT_FALSE(app->scope().has_ref()); |
| std::string basic_scope_path = app->scope().spec(); |
| std::string scope_path_with_queries_and_fragment = |
| base::StrCat({basic_scope_path, "?query=abc", "fragment"}); |
| |
| // Create a WebAppProto with a scope that has queries and fragments. |
| std::unique_ptr<WebAppProto> proto = WebAppDatabase::CreateWebAppProto(*app); |
| proto->set_scope(scope_path_with_queries_and_fragment); |
| EXPECT_EQ(proto->scope(), scope_path_with_queries_and_fragment); |
| |
| // Re-parse the app from the proto. |
| auto reparsed_app = WebAppDatabase::CreateWebApp(*proto); |
| ASSERT_TRUE(reparsed_app); |
| |
| // Loaded app should have had the fragment and query stripped. |
| EXPECT_EQ(reparsed_app->scope(), basic_scope_path); |
| EXPECT_FALSE(reparsed_app->scope().has_ref()); |
| EXPECT_FALSE(reparsed_app->scope().has_query()); |
| } |
| |
| class WebAppDatabaseProtoDataTest : public ::testing::Test { |
| public: |
| std::unique_ptr<WebApp> CreateMinimalWebApp() { |
| GURL start_url{"https://example.com/"}; |
| webapps::AppId app_id = |
| GenerateAppId(/*manifest_id=*/std::nullopt, start_url); |
| auto web_app = std::make_unique<WebApp>(app_id); |
| web_app->SetStartUrl(start_url); |
| web_app->SetUserDisplayMode(mojom::UserDisplayMode::kBrowser); |
| web_app->AddSource(WebAppManagement::Type::kDefault); |
| return web_app; |
| } |
| |
| std::unique_ptr<WebApp> CreateIsolatedWebApp( |
| const WebApp::IsolationData& isolation_data) { |
| std::unique_ptr<WebApp> web_app = CreateMinimalWebApp(); |
| web_app->SetIsolationData(isolation_data); |
| return web_app; |
| } |
| |
| std::unique_ptr<WebApp> CreateWebAppWithPermissionsPolicy( |
| const blink::ParsedPermissionsPolicy& permissions_policy) { |
| std::unique_ptr<WebApp> web_app = CreateMinimalWebApp(); |
| web_app->SetPermissionsPolicy(permissions_policy); |
| return web_app; |
| } |
| |
| std::unique_ptr<WebApp> ToAndFromProto(const WebApp& web_app) { |
| return WebAppDatabase::CreateWebApp( |
| *WebAppDatabase::CreateWebAppProto(web_app)); |
| } |
| }; |
| |
| TEST_F(WebAppDatabaseProtoDataTest, DoesNotSetIsolationDataIfNotIsolated) { |
| std::unique_ptr<WebApp> web_app = CreateMinimalWebApp(); |
| std::unique_ptr<WebApp> protoed_web_app = ToAndFromProto(*web_app); |
| EXPECT_THAT(*web_app, AllOf(Eq(*protoed_web_app), |
| Property("isolation_data", |
| &WebApp::isolation_data, std::nullopt))); |
| } |
| |
| TEST_F(WebAppDatabaseProtoDataTest, SavesOwnedBundleIsolationData) { |
| std::string dir_name_ascii = "folder_name"; |
| std::unique_ptr<WebApp> web_app = CreateIsolatedWebApp(WebApp::IsolationData( |
| IwaStorageOwnedBundle{dir_name_ascii, /*dev_mode=*/false}, |
| base::Version("1.0.0"))); |
| |
| std::unique_ptr<WebApp> protoed_web_app = ToAndFromProto(*web_app); |
| EXPECT_THAT(*web_app, Eq(*protoed_web_app)); |
| EXPECT_THAT(web_app->isolation_data()->location, |
| IwaStorageOwnedBundle(dir_name_ascii, /*dev_mode=*/false)); |
| EXPECT_THAT(web_app->isolation_data()->version, Eq(base::Version("1.0.0"))); |
| } |
| |
| TEST_F(WebAppDatabaseProtoDataTest, HandlesCorruptedOwnedBundleIsolationData) { |
| std::unique_ptr<WebApp> web_app = CreateIsolatedWebApp(WebApp::IsolationData( |
| IwaStorageOwnedBundle{"folder_name", /*dev_mode=*/false}, |
| base::Version("1.0.0"))); |
| |
| std::unique_ptr<WebAppProto> web_app_proto = |
| WebAppDatabase::CreateWebAppProto(*web_app); |
| ASSERT_THAT(web_app_proto, NotNull()); |
| |
| // Setting non-ASCII characters should break deserialization. |
| web_app_proto->mutable_isolation_data() |
| ->mutable_owned_bundle() |
| ->mutable_dir_name_ascii() |
| ->assign("日本"); |
| |
| std::unique_ptr<WebApp> protoed_web_app = |
| WebAppDatabase::CreateWebApp(*web_app_proto); |
| EXPECT_THAT(protoed_web_app, IsNull()); |
| } |
| |
| TEST_F(WebAppDatabaseProtoDataTest, SavesUnownedBundleIsolationData) { |
| base::FilePath path(FILE_PATH_LITERAL("dev_bundle_path")); |
| std::unique_ptr<WebApp> web_app = CreateIsolatedWebApp(WebApp::IsolationData( |
| IwaStorageUnownedBundle{path}, base::Version("1.0.0"))); |
| |
| std::unique_ptr<WebApp> protoed_web_app = ToAndFromProto(*web_app); |
| EXPECT_THAT(*web_app, Eq(*protoed_web_app)); |
| EXPECT_THAT(web_app->isolation_data()->location, |
| IwaStorageUnownedBundle{path}); |
| EXPECT_THAT(web_app->isolation_data()->version, Eq(base::Version("1.0.0"))); |
| } |
| |
| TEST_F(WebAppDatabaseProtoDataTest, |
| HandlesCorruptedUnownedBundleIsolationData) { |
| base::FilePath path(FILE_PATH_LITERAL("bundle_path")); |
| std::unique_ptr<WebApp> web_app = CreateIsolatedWebApp(WebApp::IsolationData( |
| IwaStorageUnownedBundle{path}, base::Version("1.0.0"))); |
| |
| std::unique_ptr<WebAppProto> web_app_proto = |
| WebAppDatabase::CreateWebAppProto(*web_app); |
| ASSERT_THAT(web_app_proto, NotNull()); |
| |
| // The path is encoded with Pickle, thus setting some non-pickle data here |
| // should break deserialization. |
| web_app_proto->mutable_isolation_data() |
| ->mutable_unowned_bundle() |
| ->mutable_path() |
| ->assign("foo"); |
| |
| std::unique_ptr<WebApp> protoed_web_app = |
| WebAppDatabase::CreateWebApp(*web_app_proto); |
| EXPECT_THAT(protoed_web_app, IsNull()); |
| } |
| |
| TEST_F(WebAppDatabaseProtoDataTest, SavesProxyIsolationData) { |
| std::unique_ptr<WebApp> web_app = CreateIsolatedWebApp(WebApp::IsolationData( |
| IwaStorageProxy{url::Origin::Create(GURL("https://proxy-example.com/"))}, |
| base::Version("1.0.0"))); |
| |
| std::unique_ptr<WebApp> protoed_web_app = ToAndFromProto(*web_app); |
| EXPECT_THAT(*web_app, Eq(*protoed_web_app)); |
| EXPECT_THAT( |
| web_app->isolation_data()->location, |
| IwaStorageProxy{url::Origin::Create(GURL("https://proxy-example.com/"))}); |
| EXPECT_THAT(web_app->isolation_data()->version, Eq(base::Version("1.0.0"))); |
| } |
| |
| TEST_F(WebAppDatabaseProtoDataTest, HandlesCorruptedProxyIsolationData) { |
| std::unique_ptr<WebApp> web_app = CreateIsolatedWebApp(WebApp::IsolationData( |
| IwaStorageProxy{ |
| |
| url::Origin::Create(GURL("https://proxy-example.com/"))}, |
| base::Version("1.0.0"))); |
| |
| std::unique_ptr<WebAppProto> web_app_proto = |
| WebAppDatabase::CreateWebAppProto(*web_app); |
| ASSERT_THAT(web_app_proto, NotNull()); |
| |
| web_app_proto->mutable_isolation_data() |
| ->mutable_proxy() |
| ->mutable_proxy_url() |
| ->assign(""); |
| |
| std::unique_ptr<WebApp> protoed_web_app = |
| WebAppDatabase::CreateWebApp(*web_app_proto); |
| EXPECT_THAT(protoed_web_app, IsNull()); |
| } |
| |
| TEST_F(WebAppDatabaseProtoDataTest, HandlesCorruptedIsolationDataVersion) { |
| std::unique_ptr<WebApp> web_app = CreateIsolatedWebApp(WebApp::IsolationData( |
| IwaStorageOwnedBundle{"folder_name", /*dev_mode=*/false}, |
| base::Version("1.2.3"))); |
| |
| std::unique_ptr<WebAppProto> web_app_proto = |
| WebAppDatabase::CreateWebAppProto(*web_app); |
| ASSERT_THAT(web_app_proto, NotNull()); |
| web_app_proto->mutable_isolation_data()->mutable_version()->assign("abc"); |
| |
| std::unique_ptr<WebApp> protoed_web_app = |
| WebAppDatabase::CreateWebApp(*web_app_proto); |
| EXPECT_THAT(protoed_web_app, IsNull()); |
| } |
| |
| TEST_F(WebAppDatabaseProtoDataTest, |
| HandlesCorruptedIsolationDataPendingUpdateVersion) { |
| std::unique_ptr<WebApp> web_app = CreateIsolatedWebApp(WebApp::IsolationData( |
| IwaStorageOwnedBundle{"folder_name", /*dev_mode=*/false}, |
| base::Version("1.2.3"), {}, |
| WebApp::IsolationData::PendingUpdateInfo( |
| IwaStorageOwnedBundle{"folder_name", /*dev_mode=*/false}, |
| base::Version("1.2.3"), /*integrity_block_data=*/std::nullopt), |
| /*integrity_block_data=*/std::nullopt)); |
| |
| std::unique_ptr<WebAppProto> web_app_proto = |
| WebAppDatabase::CreateWebAppProto(*web_app); |
| ASSERT_THAT(web_app_proto, NotNull()); |
| web_app_proto->mutable_isolation_data() |
| ->mutable_pending_update_info() |
| ->mutable_version() |
| ->assign("abc"); |
| |
| std::unique_ptr<WebApp> protoed_web_app = |
| WebAppDatabase::CreateWebApp(*web_app_proto); |
| EXPECT_THAT(protoed_web_app, IsNull()); |
| } |
| |
| TEST_F(WebAppDatabaseProtoDataTest, |
| HandlesDifferentTypeOfIsolationDataPendingUpdateLocation) { |
| std::unique_ptr<WebApp> web_app = CreateIsolatedWebApp(WebApp::IsolationData( |
| IwaStorageOwnedBundle{"folder_name", /*dev_mode*/ true}, |
| base::Version("1.0.0"), {}, |
| WebApp::IsolationData::PendingUpdateInfo( |
| IwaStorageProxy{url::Origin::Create(GURL("https://example.com"))}, |
| base::Version("2.0.0"), /*integrity_block_data=*/std::nullopt), |
| /*integrity_block_data=*/std::nullopt)); |
| |
| std::unique_ptr<WebAppProto> web_app_proto = |
| WebAppDatabase::CreateWebAppProto(*web_app); |
| std::unique_ptr<WebApp> protoed_web_app = |
| WebAppDatabase::CreateWebApp(*web_app_proto); |
| EXPECT_THAT(protoed_web_app, NotNull()); |
| } |
| |
| TEST_F(WebAppDatabaseProtoDataTest, |
| HandlesMismatchedIsolationDataPendingUpdateLocation) { |
| std::unique_ptr<WebApp> web_app = CreateIsolatedWebApp(WebApp::IsolationData( |
| IwaStorageOwnedBundle{"folder_name", /*dev_mode*/ false}, |
| base::Version("1.0.0"), {}, |
| WebApp::IsolationData::PendingUpdateInfo( |
| IwaStorageOwnedBundle{"folder_name", /*dev_mode*/ false}, |
| base::Version("2.0.0"), /*integrity_block_data=*/std::nullopt), |
| /*integrity_block_data=*/std::nullopt)); |
| |
| // Test what happens if both are owned bundles, but one is dev mode and |
| // the other one is not. |
| { |
| std::unique_ptr<WebAppProto> web_app_proto = |
| WebAppDatabase::CreateWebAppProto(*web_app); |
| ASSERT_THAT(web_app_proto, NotNull()); |
| web_app_proto->mutable_isolation_data() |
| ->mutable_pending_update_info() |
| ->mutable_owned_bundle() |
| ->set_dev_mode(true); |
| web_app_proto->mutable_isolation_data() |
| ->mutable_pending_update_info() |
| ->mutable_proxy(); |
| |
| std::unique_ptr<WebApp> protoed_web_app = |
| WebAppDatabase::CreateWebApp(*web_app_proto); |
| EXPECT_THAT(protoed_web_app, IsNull()); |
| } |
| |
| // Test what happens if one is an owned non-dev-mode bundle, but the other one |
| // is a proxy. |
| { |
| std::unique_ptr<WebAppProto> web_app_proto = |
| WebAppDatabase::CreateWebAppProto(*web_app); |
| ASSERT_THAT(web_app_proto, NotNull()); |
| web_app_proto->mutable_isolation_data() |
| ->mutable_pending_update_info() |
| ->clear_location(); |
| web_app_proto->mutable_isolation_data() |
| ->mutable_pending_update_info() |
| ->mutable_proxy() |
| ->set_proxy_url("https://example.com"); |
| |
| std::unique_ptr<WebApp> protoed_web_app = |
| WebAppDatabase::CreateWebApp(*web_app_proto); |
| EXPECT_THAT(protoed_web_app, IsNull()); |
| } |
| } |
| |
| TEST_F(WebAppDatabaseProtoDataTest, SavesIsolationDataUpdateInfo) { |
| base::FilePath path(FILE_PATH_LITERAL("bundle_path")); |
| base::FilePath update_path(FILE_PATH_LITERAL("update_path")); |
| |
| auto integrity_block_data = |
| IsolatedWebAppIntegrityBlockData(test::CreateSignatures()); |
| std::unique_ptr<WebApp> web_app = CreateIsolatedWebApp(WebApp::IsolationData( |
| IwaStorageUnownedBundle{path}, base::Version("1.0.0"), {}, |
| WebApp::IsolationData::PendingUpdateInfo( |
| IwaStorageUnownedBundle{update_path}, base::Version("2.0.0"), |
| integrity_block_data), |
| integrity_block_data)); |
| |
| std::unique_ptr<WebApp> protoed_web_app = ToAndFromProto(*web_app); |
| EXPECT_THAT(*web_app, Eq(*protoed_web_app)); |
| EXPECT_THAT(web_app->isolation_data()->location, |
| IwaStorageUnownedBundle{path}); |
| EXPECT_THAT(web_app->isolation_data()->version, Eq(base::Version("1.0.0"))); |
| EXPECT_THAT(web_app->isolation_data()->pending_update_info()->location, |
| IwaStorageUnownedBundle{update_path}); |
| EXPECT_THAT(web_app->isolation_data()->pending_update_info()->version, |
| Eq(base::Version("2.0.0"))); |
| EXPECT_THAT( |
| web_app->isolation_data()->pending_update_info()->integrity_block_data, |
| Optional(Eq(integrity_block_data))); |
| EXPECT_THAT(web_app->isolation_data()->integrity_block_data, |
| Optional(Eq(integrity_block_data))); |
| } |
| |
| TEST_F(WebAppDatabaseProtoDataTest, PermissionsPolicyRoundTrip) { |
| const blink::ParsedPermissionsPolicy policy = { |
| {blink::mojom::PermissionsPolicyFeature::kGyroscope, |
| /*allowed_origins=*/{}, |
| /*self_if_matches=*/std::nullopt, |
| /*matches_all_origins=*/false, |
| /*matches_opaque_src=*/true}, |
| {blink::mojom::PermissionsPolicyFeature::kGeolocation, |
| /*allowed_origins=*/{}, |
| /*self_if_matches=*/std::nullopt, |
| /*matches_all_origins=*/true, |
| /*matches_opaque_src=*/false}, |
| {blink::mojom::PermissionsPolicyFeature::kGamepad, |
| {*blink::OriginWithPossibleWildcards::FromOriginAndWildcardsForTest( |
| url::Origin::Create(GURL("https://example.com")), |
| /*has_subdomain_wildcard=*/false), |
| *blink::OriginWithPossibleWildcards::FromOriginAndWildcardsForTest( |
| url::Origin::Create(GURL("https://example.net")), |
| /*has_subdomain_wildcard=*/true)}, |
| /*self_if_matches=*/std::nullopt, |
| /*matches_all_origins=*/false, |
| /*matches_opaque_src=*/false}, |
| }; |
| std::unique_ptr<WebApp> web_app = CreateWebAppWithPermissionsPolicy(policy); |
| |
| std::unique_ptr<WebApp> protoed_web_app = ToAndFromProto(*web_app); |
| EXPECT_THAT(*web_app, Eq(*protoed_web_app)); |
| EXPECT_EQ(policy, protoed_web_app->permissions_policy()); |
| } |
| |
| TEST_F(WebAppDatabaseProtoDataTest, PermissionsPolicyProto) { |
| const blink::ParsedPermissionsPolicy policy = { |
| {blink::mojom::PermissionsPolicyFeature::kGyroscope, |
| /*allowed_origins=*/{}, |
| /*self_if_matches=*/std::nullopt, |
| /*matches_all_origins=*/false, |
| /*matches_opaque_src=*/true}, |
| {blink::mojom::PermissionsPolicyFeature::kGeolocation, |
| /*allowed_origins=*/{}, |
| /*self_if_matches=*/std::nullopt, |
| /*matches_all_origins=*/true, |
| /*matches_opaque_src=*/false}, |
| {blink::mojom::PermissionsPolicyFeature::kGamepad, |
| {*blink::OriginWithPossibleWildcards::FromOriginAndWildcardsForTest( |
| url::Origin::Create(GURL("https://example.com")), |
| /*has_subdomain_wildcard=*/false), |
| *blink::OriginWithPossibleWildcards::FromOriginAndWildcardsForTest( |
| url::Origin::Create(GURL("https://example.net")), |
| /*has_subdomain_wildcard=*/true)}, |
| /*self_if_matches=*/std::nullopt, |
| /*matches_all_origins=*/false, |
| /*matches_opaque_src=*/false}, |
| }; |
| std::unique_ptr<WebApp> web_app = CreateWebAppWithPermissionsPolicy(policy); |
| |
| std::unique_ptr<WebAppProto> proto = |
| WebAppDatabase::CreateWebAppProto(*web_app); |
| ASSERT_EQ(proto->permissions_policy().size(), 3); |
| EXPECT_EQ(proto->permissions_policy().at(0).feature(), "gyroscope"); |
| EXPECT_EQ(proto->permissions_policy().at(0).allowed_origins_size(), 0); |
| EXPECT_EQ(proto->permissions_policy().at(0).matches_all_origins(), false); |
| EXPECT_EQ(proto->permissions_policy().at(0).matches_opaque_src(), true); |
| EXPECT_EQ(proto->permissions_policy().at(1).feature(), "geolocation"); |
| EXPECT_EQ(proto->permissions_policy().at(1).allowed_origins_size(), 0); |
| EXPECT_EQ(proto->permissions_policy().at(1).matches_all_origins(), true); |
| EXPECT_EQ(proto->permissions_policy().at(1).matches_opaque_src(), false); |
| EXPECT_EQ(proto->permissions_policy().at(2).feature(), "gamepad"); |
| ASSERT_EQ(proto->permissions_policy().at(2).allowed_origins_size(), 2); |
| EXPECT_EQ(proto->permissions_policy().at(2).allowed_origins(0), |
| "https://example.com"); |
| EXPECT_EQ(proto->permissions_policy().at(2).allowed_origins(1), |
| "https://*.example.net"); |
| EXPECT_EQ(proto->permissions_policy().at(2).matches_all_origins(), false); |
| EXPECT_EQ(proto->permissions_policy().at(2).matches_opaque_src(), false); |
| } |
| } // namespace web_app |