| // 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/ash/android_sms/android_sms_app_setup_controller_impl.h" |
| |
| #include <memory> |
| #include <string> |
| #include <tuple> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/containers/contains.h" |
| #include "base/containers/flat_map.h" |
| #include "base/files/file_path.h" |
| #include "base/functional/bind.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/path_service.h" |
| #include "base/run_loop.h" |
| #include "base/test/bind.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "chrome/browser/content_settings/host_content_settings_map_factory.h" |
| #include "chrome/browser/web_applications/external_install_options.h" |
| #include "chrome/browser/web_applications/mojom/user_display_mode.mojom.h" |
| #include "chrome/browser/web_applications/test/fake_externally_managed_app_manager.h" |
| #include "chrome/browser/web_applications/test/fake_web_app_provider.h" |
| #include "chrome/browser/web_applications/test/web_app_install_test_utils.h" |
| #include "chrome/browser/web_applications/web_app_helpers.h" |
| #include "chrome/test/base/testing_profile.h" |
| #include "components/content_settings/core/browser/host_content_settings_map.h" |
| #include "components/webapps/browser/install_result_code.h" |
| #include "content/public/test/browser_task_environment.h" |
| #include "services/network/test/test_cookie_manager.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/mojom/manifest/display_mode.mojom.h" |
| |
| namespace ash { |
| namespace android_sms { |
| |
| namespace { |
| |
| const char kTestUrl1[] = "https://test-url-1.com/"; |
| const char kTestInstallUrl1[] = "https://test-url-1.com/install"; |
| const char kTestUrl2[] = "https://test-url-2.com/"; |
| |
| web_app::ExternalInstallOptions GetInstallOptionsForUrl(const GURL& url) { |
| web_app::ExternalInstallOptions options( |
| url, web_app::mojom::UserDisplayMode::kStandalone, |
| web_app::ExternalInstallSource::kInternalDefault); |
| options.override_previous_user_uninstall = true; |
| options.require_manifest = true; |
| return options; |
| } |
| |
| class FakeCookieManager : public network::TestCookieManager { |
| public: |
| FakeCookieManager() = default; |
| ~FakeCookieManager() override { |
| EXPECT_TRUE(set_canonical_cookie_calls_.empty()); |
| EXPECT_TRUE(delete_cookies_calls_.empty()); |
| } |
| |
| void InvokePendingSetCanonicalCookieCallback( |
| const std::string& expected_cookie_name, |
| const std::string& expected_cookie_value, |
| const GURL& expected_source_url, |
| bool expected_modify_http_only, |
| net::CookieOptions::SameSiteCookieContext expect_same_site_context, |
| bool success) { |
| ASSERT_FALSE(set_canonical_cookie_calls_.empty()); |
| auto params = std::move(set_canonical_cookie_calls_.front()); |
| set_canonical_cookie_calls_.erase(set_canonical_cookie_calls_.begin()); |
| |
| EXPECT_EQ(expected_cookie_name, std::get<0>(params).Name()); |
| EXPECT_EQ(expected_cookie_value, std::get<0>(params).Value()); |
| EXPECT_EQ(expected_source_url, std::get<1>(params)); |
| EXPECT_EQ(expected_modify_http_only, |
| !std::get<2>(params).exclude_httponly()); |
| EXPECT_EQ(expect_same_site_context, |
| std::get<2>(params).same_site_cookie_context()); |
| net::CookieAccessResult access_result; |
| |
| if (!success) { |
| access_result.status.AddExclusionReason( |
| net::CookieInclusionStatus::EXCLUDE_UNKNOWN_ERROR); |
| } |
| |
| std::move(std::get<3>(params)).Run(access_result); |
| } |
| |
| void InvokePendingDeleteCookiesCallback( |
| const GURL& expected_url, |
| const std::string& expected_cookie_name, |
| bool success) { |
| ASSERT_FALSE(delete_cookies_calls_.empty()); |
| auto params = std::move(delete_cookies_calls_.front()); |
| delete_cookies_calls_.erase(delete_cookies_calls_.begin()); |
| |
| EXPECT_EQ(expected_url, params.first->url); |
| EXPECT_EQ(expected_cookie_name, params.first->cookie_name); |
| |
| std::move(params.second).Run(success); |
| } |
| |
| // network::mojom::CookieManager |
| void SetCanonicalCookie(const net::CanonicalCookie& cookie, |
| const GURL& source_url, |
| const net::CookieOptions& options, |
| SetCanonicalCookieCallback callback) override { |
| set_canonical_cookie_calls_.emplace_back(cookie, source_url, options, |
| std::move(callback)); |
| } |
| |
| void DeleteCookies(network::mojom::CookieDeletionFilterPtr filter, |
| DeleteCookiesCallback callback) override { |
| delete_cookies_calls_.emplace_back(std::move(filter), std::move(callback)); |
| } |
| |
| private: |
| std::vector<std::tuple<net::CanonicalCookie, |
| GURL, |
| net::CookieOptions, |
| SetCanonicalCookieCallback>> |
| set_canonical_cookie_calls_; |
| std::vector< |
| std::pair<network::mojom::CookieDeletionFilterPtr, DeleteCookiesCallback>> |
| delete_cookies_calls_; |
| }; |
| |
| } // namespace |
| |
| class AndroidSmsAppSetupControllerImplTest : public testing::Test { |
| public: |
| AndroidSmsAppSetupControllerImplTest( |
| const AndroidSmsAppSetupControllerImplTest&) = delete; |
| AndroidSmsAppSetupControllerImplTest& operator=( |
| const AndroidSmsAppSetupControllerImplTest&) = delete; |
| |
| protected: |
| class TestPwaDelegate : public AndroidSmsAppSetupControllerImpl::PwaDelegate { |
| public: |
| explicit TestPwaDelegate(FakeCookieManager* fake_cookie_manager) |
| : fake_cookie_manager_(fake_cookie_manager) {} |
| ~TestPwaDelegate() override = default; |
| |
| void SetHasPwa(const GURL& url) { |
| // If a PWA already exists for this URL, there is nothing to do. |
| if (base::Contains(url_to_pwa_map_, url)) |
| return; |
| |
| url_to_pwa_map_[url] = |
| web_app::GenerateAppId(/*manifest_id=*/std::nullopt, url); |
| } |
| |
| // AndroidSmsAppSetupControllerImpl::PwaDelegate: |
| std::optional<webapps::AppId> GetPwaForUrl(const GURL& install_url, |
| Profile* profile) override { |
| if (!base::Contains(url_to_pwa_map_, install_url)) |
| return std::nullopt; |
| |
| return url_to_pwa_map_[install_url]; |
| } |
| |
| network::mojom::CookieManager* GetCookieManager(Profile* profile) override { |
| return fake_cookie_manager_; |
| } |
| |
| void RemovePwa( |
| const webapps::AppId& app_id, |
| Profile* profile, |
| AndroidSmsAppSetupController::SuccessCallback callback) override { |
| for (const auto& url_pwa_pair : url_to_pwa_map_) { |
| if (url_pwa_pair.second == app_id) { |
| url_to_pwa_map_.erase(url_pwa_pair.first); |
| std::move(callback).Run(true); |
| return; |
| } |
| } |
| |
| std::move(callback).Run(false); |
| } |
| |
| private: |
| raw_ptr<FakeCookieManager> fake_cookie_manager_; |
| base::flat_map<GURL, webapps::AppId> url_to_pwa_map_; |
| }; |
| |
| AndroidSmsAppSetupControllerImplTest() |
| : task_environment_( |
| content::BrowserTaskEnvironment::TimeSource::MOCK_TIME), |
| host_content_settings_map_( |
| HostContentSettingsMapFactory::GetForProfile(&profile_)) {} |
| |
| ~AndroidSmsAppSetupControllerImplTest() override = default; |
| |
| // testing::Test: |
| void SetUp() override { |
| host_content_settings_map_->ClearSettingsForOneType( |
| ContentSettingsType::NOTIFICATIONS); |
| fake_cookie_manager_ = std::make_unique<FakeCookieManager>(); |
| auto test_pwa_delegate = |
| std::make_unique<TestPwaDelegate>(fake_cookie_manager_.get()); |
| test_pwa_delegate_ = test_pwa_delegate.get(); |
| |
| provider_ = web_app::FakeWebAppProvider::Get(&profile_); |
| web_app::test::AwaitStartWebAppProviderAndSubsystems(&profile_); |
| |
| fake_externally_managed_app_manager().SetHandleInstallRequestCallback( |
| base::BindLambdaForTesting( |
| [this](const web_app::ExternalInstallOptions& install_options) { |
| return web_app::ExternallyManagedAppManager::InstallResult( |
| install_result_code_); |
| })); |
| |
| setup_controller_ = base::WrapUnique(new AndroidSmsAppSetupControllerImpl( |
| &profile_, &fake_externally_managed_app_manager(), |
| host_content_settings_map_)); |
| |
| std::unique_ptr<AndroidSmsAppSetupControllerImpl::PwaDelegate> |
| base_delegate(test_pwa_delegate.release()); |
| |
| static_cast<AndroidSmsAppSetupControllerImpl*>(setup_controller_.get()) |
| ->SetPwaDelegateForTesting(std::move(base_delegate)); |
| } |
| |
| void CallSetUpAppWithRetries(const GURL& app_url, |
| const GURL& install_url, |
| size_t num_failure_tries, |
| bool expected_setup_result) { |
| const auto& install_requests = |
| fake_externally_managed_app_manager().install_requests(); |
| size_t num_install_requests_before_call = install_requests.size(); |
| |
| base::RunLoop run_loop; |
| base::HistogramTester histogram_tester; |
| |
| SetInstallResultCode( |
| webapps::InstallResultCode::kGetWebAppInstallInfoFailed); |
| |
| setup_controller_->SetUpApp( |
| app_url, install_url, |
| base::BindOnce(&AndroidSmsAppSetupControllerImplTest::OnSetUpAppResult, |
| base::Unretained(this), run_loop.QuitClosure())); |
| |
| fake_cookie_manager_->InvokePendingSetCanonicalCookieCallback( |
| "default_to_persist" /* expected_cookie_name */, |
| "true" /* expected_cookie_value */, |
| GURL("https://" + app_url.host()) /* expected_source_url */, |
| false /* expected_modify_http_only */, |
| net::CookieOptions::SameSiteCookieContext::MakeInclusive(), |
| true /* success */); |
| |
| fake_cookie_manager_->InvokePendingDeleteCookiesCallback( |
| app_url, "cros_migrated_to" /* expected_cookie_name */, |
| true /* success */); |
| |
| base::RunLoop().RunUntilIdle(); |
| |
| // Fast forward through remaining attempts. |
| for (size_t retry_count = 0; retry_count < num_failure_tries - 1; |
| retry_count++) { |
| EXPECT_NE(ContentSetting::CONTENT_SETTING_ALLOW, |
| GetNotificationSetting(app_url)); |
| EXPECT_EQ(num_install_requests_before_call + retry_count + 1u, |
| install_requests.size()); |
| EXPECT_EQ(GetInstallOptionsForUrl(install_url), install_requests.back()); |
| |
| task_environment_.FastForwardBy( |
| AndroidSmsAppSetupControllerImpl::kInstallRetryDelay * |
| (1 << retry_count)); |
| } |
| |
| // Send success code for last attempt. |
| SetInstallResultCode(webapps::InstallResultCode::kSuccessNewInstall); |
| task_environment_.FastForwardBy( |
| AndroidSmsAppSetupControllerImpl::kInstallRetryDelay * |
| (1 << (num_failure_tries - 1))); |
| |
| if (last_set_up_app_result_ && *last_set_up_app_result_) { |
| histogram_tester.ExpectBucketCount( |
| "AndroidSms.NumAttemptsForSuccessfulInstallation", |
| num_failure_tries + 1, 1); |
| } |
| histogram_tester.ExpectBucketCount( |
| "AndroidSms.EffectivePWAInstallationSuccess", expected_setup_result, 1); |
| |
| EXPECT_EQ(last_set_up_app_result_, expected_setup_result); |
| last_set_up_app_result_.reset(); |
| } |
| |
| void CallSetUpApp(const GURL& app_url, |
| const GURL& install_url, |
| size_t num_expected_app_installs) { |
| const auto& install_requests = |
| fake_externally_managed_app_manager().install_requests(); |
| size_t num_install_requests_before_call = install_requests.size(); |
| |
| base::RunLoop run_loop; |
| base::HistogramTester histogram_tester; |
| |
| setup_controller_->SetUpApp( |
| app_url, install_url, |
| base::BindOnce(&AndroidSmsAppSetupControllerImplTest::OnSetUpAppResult, |
| base::Unretained(this), run_loop.QuitClosure())); |
| |
| fake_cookie_manager_->InvokePendingSetCanonicalCookieCallback( |
| "default_to_persist" /* expected_cookie_name */, |
| "true" /* expected_cookie_value */, |
| GURL("https://" + app_url.host()) /* expected_source_url */, |
| false /* expected_modify_http_only */, |
| net::CookieOptions::SameSiteCookieContext::MakeInclusive(), |
| true /* success */); |
| |
| fake_cookie_manager_->InvokePendingDeleteCookiesCallback( |
| app_url, "cros_migrated_to" /* expected_cookie_name */, |
| true /* success */); |
| base::RunLoop().RunUntilIdle(); |
| |
| // If the PWA was not already installed at the URL, SetUpApp() should |
| // install it. |
| if (!test_pwa_delegate_->GetPwaForUrl(install_url, &profile_)) { |
| EXPECT_EQ(num_install_requests_before_call + 1u, install_requests.size()); |
| EXPECT_EQ(GetInstallOptionsForUrl(install_url), install_requests.back()); |
| |
| EXPECT_EQ(ContentSetting::CONTENT_SETTING_ALLOW, |
| GetNotificationSetting(app_url)); |
| } |
| |
| if (num_expected_app_installs) { |
| histogram_tester.ExpectBucketCount( |
| "AndroidSms.PWAInstallationResult", |
| webapps::InstallResultCode::kSuccessNewInstall, |
| num_expected_app_installs); |
| histogram_tester.ExpectBucketCount( |
| "AndroidSms.EffectivePWAInstallationSuccess", true, 1); |
| } |
| |
| run_loop.Run(); |
| EXPECT_TRUE(*last_set_up_app_result_); |
| last_set_up_app_result_.reset(); |
| } |
| |
| void CallDeleteRememberDeviceByDefaultCookie(const GURL& app_url) { |
| base::RunLoop run_loop; |
| |
| setup_controller_->DeleteRememberDeviceByDefaultCookie( |
| app_url, |
| base::BindOnce(&AndroidSmsAppSetupControllerImplTest:: |
| OnDeleteRememberDeviceByDefaultCookieResult, |
| base::Unretained(this), run_loop.QuitClosure())); |
| |
| fake_cookie_manager_->InvokePendingDeleteCookiesCallback( |
| app_url, "default_to_persist" /* expected_cookie_name */, |
| true /* success */); |
| |
| run_loop.Run(); |
| EXPECT_TRUE(*last_delete_cookie_result_); |
| last_delete_cookie_result_.reset(); |
| } |
| |
| void CallRemoveApp(const GURL& app_url, |
| const GURL& install_url, |
| const GURL& migrated_to_app_url, |
| size_t num_expected_app_uninstalls) { |
| base::RunLoop run_loop; |
| base::HistogramTester histogram_tester; |
| |
| bool was_installed = |
| test_pwa_delegate_->GetPwaForUrl(install_url, &profile_).has_value(); |
| setup_controller_->RemoveApp( |
| app_url, install_url, migrated_to_app_url, |
| base::BindOnce(&AndroidSmsAppSetupControllerImplTest::OnRemoveAppResult, |
| base::Unretained(this), run_loop.QuitClosure())); |
| base::RunLoop().RunUntilIdle(); |
| |
| // If the PWA was already installed at the URL, RemoveApp() should uninstall |
| // the it. |
| if (was_installed) { |
| EXPECT_FALSE(test_pwa_delegate()->GetPwaForUrl(install_url, &profile_)); |
| |
| fake_cookie_manager_->InvokePendingSetCanonicalCookieCallback( |
| "cros_migrated_to" /* expected_cookie_name */, |
| migrated_to_app_url.GetContent() /* expected_cookie_value */, |
| GURL("https://" + app_url.host()) /* expected_source_url */, |
| false /* expected_modify_http_only */, |
| net::CookieOptions::SameSiteCookieContext::MakeInclusive(), |
| true /* success */); |
| |
| fake_cookie_manager_->InvokePendingDeleteCookiesCallback( |
| app_url, "default_to_persist" /* expected_cookie_name */, |
| true /* success */); |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| if (num_expected_app_uninstalls) { |
| histogram_tester.ExpectBucketCount("AndroidSms.PWAUninstallationResult", |
| true, num_expected_app_uninstalls); |
| } |
| |
| run_loop.Run(); |
| EXPECT_TRUE(*last_remove_app_result_); |
| last_remove_app_result_.reset(); |
| } |
| |
| TestPwaDelegate* test_pwa_delegate() { return test_pwa_delegate_; } |
| |
| void SetInstallResultCode(webapps::InstallResultCode result_code) { |
| install_result_code_ = result_code; |
| } |
| |
| web_app::FakeExternallyManagedAppManager& |
| fake_externally_managed_app_manager() { |
| return static_cast<web_app::FakeExternallyManagedAppManager&>( |
| provider_->externally_managed_app_manager()); |
| } |
| |
| private: |
| ContentSetting GetNotificationSetting(const GURL& url) { |
| return host_content_settings_map_->GetContentSetting( |
| url, GURL() /* top_level_url */, ContentSettingsType::NOTIFICATIONS); |
| } |
| |
| void OnSetUpAppResult(base::OnceClosure quit_closure, bool success) { |
| EXPECT_FALSE(last_set_up_app_result_); |
| last_set_up_app_result_ = success; |
| std::move(quit_closure).Run(); |
| } |
| |
| void OnDeleteRememberDeviceByDefaultCookieResult( |
| base::OnceClosure quit_closure, |
| bool success) { |
| EXPECT_FALSE(last_delete_cookie_result_); |
| last_delete_cookie_result_ = success; |
| std::move(quit_closure).Run(); |
| } |
| |
| void OnRemoveAppResult(base::OnceClosure quit_closure, bool success) { |
| EXPECT_FALSE(last_remove_app_result_); |
| last_remove_app_result_ = success; |
| std::move(quit_closure).Run(); |
| } |
| |
| webapps::InstallResultCode install_result_code_ = |
| webapps::InstallResultCode::kSuccessNewInstall; |
| |
| content::BrowserTaskEnvironment task_environment_; |
| |
| std::optional<bool> last_set_up_app_result_; |
| std::optional<bool> last_delete_cookie_result_; |
| std::optional<bool> last_remove_app_result_; |
| |
| raw_ptr<web_app::FakeWebAppProvider, DanglingUntriaged> provider_; |
| |
| TestingProfile profile_; |
| raw_ptr<HostContentSettingsMap> host_content_settings_map_; |
| std::unique_ptr<FakeCookieManager> fake_cookie_manager_; |
| raw_ptr<TestPwaDelegate, DanglingUntriaged> test_pwa_delegate_; |
| std::unique_ptr<AndroidSmsAppSetupController> setup_controller_; |
| }; |
| |
| TEST_F(AndroidSmsAppSetupControllerImplTest, SetUpApp_NoPreviousApp) { |
| CallSetUpApp(GURL(kTestUrl1), GURL(kTestInstallUrl1), |
| 1u /* num_expected_app_installs */); |
| } |
| |
| TEST_F(AndroidSmsAppSetupControllerImplTest, SetUpApp_AppAlreadyInstalled) { |
| // Start with a PWA already installed at the URL. |
| test_pwa_delegate()->SetHasPwa(GURL(kTestInstallUrl1)); |
| CallSetUpApp(GURL(kTestUrl1), GURL(kTestInstallUrl1), |
| 0u /* num_expected_app_installs */); |
| } |
| |
| TEST_F(AndroidSmsAppSetupControllerImplTest, SetUpApp_OtherPwaInstalled) { |
| // Start with a PWA already installed at a different URL. |
| test_pwa_delegate()->SetHasPwa(GURL(kTestUrl2)); |
| CallSetUpApp(GURL(kTestUrl1), GURL(kTestInstallUrl1), |
| 1u /* num_expected_app_installs */); |
| } |
| |
| TEST_F(AndroidSmsAppSetupControllerImplTest, SetUpAppThenDeleteCookie) { |
| CallSetUpApp(GURL(kTestUrl1), GURL(kTestInstallUrl1), |
| 1u /* num_expected_app_installs */); |
| CallDeleteRememberDeviceByDefaultCookie(GURL(kTestUrl1)); |
| } |
| |
| TEST_F(AndroidSmsAppSetupControllerImplTest, SetUpApp_Retry) { |
| // Setup should fail when all attempts fail. |
| CallSetUpAppWithRetries( |
| GURL(kTestUrl1), GURL(kTestInstallUrl1), |
| AndroidSmsAppSetupControllerImpl::kMaxInstallRetryCount + |
| 1 /* num_failure_tries*/, |
| false /* expected_setup_result */); |
| |
| // Setup should succeed when the last attempt succeeds. |
| CallSetUpAppWithRetries(GURL(kTestUrl1), GURL(kTestInstallUrl1), |
| AndroidSmsAppSetupControllerImpl:: |
| kMaxInstallRetryCount /* num_failure_tries*/, |
| true /* expected_setup_result */); |
| |
| // Setup should succeed when only fewer than max attempts fail. |
| CallSetUpAppWithRetries(GURL(kTestUrl1), GURL(kTestInstallUrl1), |
| 1 /* num_failure_tries*/, |
| true /* expected_setup_result */); |
| } |
| |
| TEST_F(AndroidSmsAppSetupControllerImplTest, SetUpAppThenRemove) { |
| // Install and remove. |
| CallSetUpApp(GURL(kTestUrl1), GURL(kTestInstallUrl1), |
| 1u /* num_expected_app_installs */); |
| test_pwa_delegate()->SetHasPwa(GURL(kTestInstallUrl1)); |
| CallRemoveApp(GURL(kTestUrl1), GURL(kTestInstallUrl1), |
| GURL(kTestUrl2) /* migrated_to_app_url */, |
| 1u /* num_expected_app_uninstalls */); |
| |
| // Repeat once more. |
| CallSetUpApp(GURL(kTestUrl1), GURL(kTestInstallUrl1), |
| 1u /* num_expected_app_installs */); |
| test_pwa_delegate()->SetHasPwa(GURL(kTestInstallUrl1)); |
| CallRemoveApp(GURL(kTestUrl1), GURL(kTestInstallUrl1), |
| GURL(kTestUrl2) /* migrated_to_app_url */, |
| 1u /* num_expected_app_uninstalls */); |
| } |
| |
| TEST_F(AndroidSmsAppSetupControllerImplTest, RemoveApp_NoInstalledApp) { |
| // Do not have an installed app before attempting to remove it. |
| CallRemoveApp(GURL(kTestUrl1), GURL(kTestInstallUrl1), |
| GURL(kTestUrl2) /* migrated_to_app_url */, |
| 0u /* num_expected_app_uninstalls */); |
| } |
| |
| } // namespace android_sms |
| } // namespace ash |