chromium/chrome/browser/web_applications/isolated_web_apps/policy/isolated_web_app_policy_manager_ash_browsertest.cc

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <stddef.h>

#include <string>
#include <string_view>
#include <vector>

#include "ash/constants/ash_switches.h"
#include "base/check_deref.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/callback_helpers.h"
#include "base/json/json_writer.h"
#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/task/thread_pool.h"
#include "base/test/bind.h"
#include "base/test/run_until.h"
#include "base/test/test_future.h"
#include "base/threading/thread_restrictions.h"
#include "base/types/cxx23_to_underlying.h"
#include "base/types/expected.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/ash/login/existing_user_controller.h"
#include "chrome/browser/ash/login/login_manager_test.h"
#include "chrome/browser/ash/login/session/user_session_manager.h"
#include "chrome/browser/ash/login/test/login_manager_mixin.h"
#include "chrome/browser/ash/login/test/session_manager_state_waiter.h"
#include "chrome/browser/ash/login/ui/login_display_host.h"
#include "chrome/browser/ash/login/wizard_controller.h"
#include "chrome/browser/ash/policy/core/browser_policy_connector_ash.h"
#include "chrome/browser/ash/policy/core/device_local_account_policy_service.h"
#include "chrome/browser/ash/policy/core/device_policy_cros_browser_test.h"
#include "chrome/browser/ash/policy/test_support/embedded_policy_test_server_mixin.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/browser_process_platform_part_ash.h"
#include "chrome/browser/devtools/devtools_window_testing.h"
#include "chrome/browser/policy/developer_tools_policy_handler.h"
#include "chrome/browser/prefs/session_startup_pref.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/web_applications/test/isolated_web_app_test_utils.h"
#include "chrome/browser/ui/web_applications/test/web_app_browsertest_util.h"
#include "chrome/browser/web_applications/isolated_web_apps/test/policy_generator.h"
#include "chrome/browser/web_applications/isolated_web_apps/test/test_iwa_installer_factory.h"
#include "chrome/browser/web_applications/isolated_web_apps/test/test_signed_web_bundle_builder.h"
#include "chrome/browser/web_applications/policy/web_app_policy_manager.h"
#include "chrome/browser/web_applications/test/web_app_test_observers.h"
#include "chrome/browser/web_applications/web_app_command_manager.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/pref_names.h"
#include "chromeos/ash/components/dbus/session_manager/fake_session_manager_client.h"
#include "components/policy/core/common/cloud/test/policy_builder.h"
#include "components/policy/core/common/device_local_account_type.h"
#include "components/policy/core/common/mock_configuration_policy_provider.h"
#include "components/policy/core/common/policy_namespace.h"
#include "components/policy/policy_constants.h"
#include "components/policy/proto/chrome_device_policy.pb.h"
#include "components/webapps/common/web_app_id.h"
#include "content/public/browser/browsing_data_remover.h"
#include "content/public/common/content_features.h"
#include "content/public/test/browser_test.h"

namespace web_app {

namespace {

constexpr char kUpdateManifestFileName1[] = "update_manifest_1.json";
constexpr char kUpdateManifestFileName2[] = "update_manifest_2.json";
constexpr char kIwaBundleFileName1[] = "iwa_bundle_1.swbn";
constexpr char kIwaBundleFileName2[] = "iwa_bundle_2.swbn";
constexpr char kUpdateManifestTemplate1[] = R"(
    {"versions":[
      {"version": "1.0.0", "src": "https://example.com/not-used.swbn"},
      {"version": "7.0.6", "src": "$1"}]
    })";
constexpr char kUpdateManifestTemplate2[] = R"(
    {"versions":[
      {"version": "2.0.0", "src": "$1"}]
    })";

constexpr char kUserMail[] = "[email protected]";
constexpr char kDisplayName[] = "display name";

constexpr char kOrphanedBundleDirectory[] = "6zsr4hjoudsu6ihf";

using policy::DeveloperToolsPolicyHandler;

}  // namespace

class IsolatedWebAppPolicyManagerAshBrowserTestBase
    : public ash::LoginManagerTest {
 public:
  IsolatedWebAppPolicyManagerAshBrowserTestBase(
      const IsolatedWebAppPolicyManagerAshBrowserTestBase&) = delete;
  IsolatedWebAppPolicyManagerAshBrowserTestBase& operator=(
      const IsolatedWebAppPolicyManagerAshBrowserTestBase&) = delete;

 protected:
  explicit IsolatedWebAppPolicyManagerAshBrowserTestBase(bool is_user_session)
      : is_user_session_(is_user_session) {
    std::vector<base::test::FeatureRef> enabled_features = {
        features::kIsolatedWebApps};
    if (is_user_session_) {
      login_manager_mixin_.AppendRegularUsers(1);
    } else {
      enabled_features.push_back(
          features::kIsolatedWebAppManagedGuestSessionInstall);
    }
    scoped_feature_list_.InitWithFeatures(enabled_features,
                                          /*disabled_features=*/{});
  }

  ~IsolatedWebAppPolicyManagerAshBrowserTestBase() override = default;

  void SetUpCommandLine(base::CommandLine* command_line) override {
    ash::LoginManagerTest::SetUpCommandLine(command_line);
    command_line->AppendSwitch(ash::switches::kLoginManager);
    command_line->AppendSwitch(ash::switches::kForceLoginManagerInTests);
  }

  void SetUpInProcessBrowserTestFixture() override {
    ash::LoginManagerTest::SetUpInProcessBrowserTestFixture();

    if (is_user_session_) {
      policy_provider_.SetDefaultReturns(
          /*is_initialization_complete_return=*/true,
          /*is_first_policy_load_complete_return=*/true);
      policy::BrowserPolicyConnector::SetPolicyProviderForTesting(
          &policy_provider_);
    } else {
      // Turning on device local account.
      device_policy()->policy_data().set_public_key_version(1);
      policy::DeviceLocalAccountTestHelper::SetupDeviceLocalAccount(
          &device_local_account_policy_, kUserMail, kDisplayName);
    }
  }

  void UploadAndInstallDeviceLocalAccountPolicy() {
    // Build device local account policy.
    device_local_account_policy_.SetDefaultSigningKey();
    device_local_account_policy_.Build();

    policy_test_server_mixin_.UpdatePolicy(
        policy::dm_protocol::kChromePublicAccountPolicyType, kUserMail,
        device_local_account_policy_.payload().SerializeAsString());

    session_manager_client()->set_device_local_account_policy(
        kUserMail, device_local_account_policy_.GetBlob());
  }

  void AddUser(bool set_iwa_policy_on_login = false) {
    if (is_user_session_) {
      // No user needs to be created: for user sessions the user was already
      // added in the constructor (technical constraint).
      if (set_iwa_policy_on_login) {
        SetIWAForceInstallPolicy(
            {{iwa_bundle_1_.id, kUpdateManifestFileName1}});
      }
    } else {
      AddManagedGuestSessionToDevicePolicy();
      if (set_iwa_policy_on_login) {
        AddDeviceLocalAccountIwaPolicy();
      }
      UploadAndInstallDeviceLocalAccountPolicy();
      WaitForPolicy();
    }
  }

  void AddManagedGuestSessionToDevicePolicy() {
    em::ChromeDeviceSettingsProto& proto(device_policy()->payload());
    policy::DeviceLocalAccountTestHelper::AddPublicSession(&proto, kUserMail);
    RefreshDevicePolicy();
    policy_test_server_mixin_.UpdateDevicePolicy(proto);
  }

  // This policy is active at the moment of login.
  void AddDeviceLocalAccountIwaPolicy() {
    PolicyGenerator policy_generator;
    policy_generator.AddForceInstalledIwa(
        iwa_bundle_1_.id,
        iwa_server_.GetURL(base::StrCat({"/", kUpdateManifestFileName1})));

    em::StringPolicyProto* const isolated_web_apps_proto =
        device_local_account_policy_.payload()
            .mutable_isolatedwebappinstallforcelist();
    isolated_web_apps_proto->set_value(
        WriteJson(policy_generator.Generate()).value());
  }

  void SetIWAForceInstallPolicy(
      const std::vector<std::pair<web_package::SignedWebBundleId, std::string>>&
          update_manifest_entries) {
    PolicyGenerator policy_generator;
    for (const auto& [bundle_id, manifest_entry] : update_manifest_entries) {
      policy_generator.AddForceInstalledIwa(
          bundle_id, iwa_server_.GetURL(base::StrCat({"/", manifest_entry})));
    }

    if (is_user_session_) {
      policy::PolicyMap policies;
      policies.Set(policy::key::kIsolatedWebAppInstallForceList,
                   policy::POLICY_LEVEL_MANDATORY, policy::POLICY_SCOPE_USER,
                   policy::POLICY_SOURCE_CLOUD, policy_generator.Generate(),
                   nullptr);
      policy_provider_.UpdateChromePolicy(policies);
    } else {
      GetProfileForTest()->GetPrefs()->Set(
          prefs::kIsolatedWebAppInstallForceList, policy_generator.Generate());
    }
  }

  void SetPolicyWithOneApp() {
    SetIWAForceInstallPolicy({{iwa_bundle_1_.id, kUpdateManifestFileName1}});
  }

  void SetPolicyWithTwoApps() {
    SetIWAForceInstallPolicy({{iwa_bundle_1_.id, kUpdateManifestFileName1},
                              {iwa_bundle_2_.id, kUpdateManifestFileName2}});
  }

  // Returns a profile which can be used for testing.
  Profile* GetProfileForTest() {
    // Any profile can be used here since this test does not test multi profile.
    return ProfileManager::GetActiveUserProfile();
  }

  void WaitForPolicy() {
    // Wait for the display name becoming available as that indicates
    // device-local account policy is fully loaded, which is a prerequisite for
    // successful login.
    policy::DictionaryLocalStateValueWaiter("UserDisplayName", kDisplayName,
                                            account_id_.GetUserEmail())
        .Wait();
  }

  void StartLogin() {
    if (is_user_session_) {
      LoginUser(login_manager_mixin_.users()[0].account_id);
    } else {
      // Start login into the device-local account.
      auto* host = ash::LoginDisplayHost::default_host();
      ASSERT_TRUE(host);
      host->StartSignInScreen();
      auto* controller = ash::ExistingUserController::current_controller();
      ASSERT_TRUE(controller);

      ash::UserContext user_context(user_manager::UserType::kPublicAccount,
                                    account_id_);
      controller->Login(user_context, ash::SigninSpecifics());
    }
  }

  void WaitForSessionStart() {
    if (session_manager::SessionManager::Get()->IsSessionStarted()) {
      return;
    }
    if (ash::WizardController::default_controller()) {
      ash::WizardController::default_controller()
          ->SkipPostLoginScreensForTesting();
    }
    ash::test::WaitForPrimaryUserSessionStart();
  }

  void WriteFile(const base::FilePath::StringType& filename,
                 std::string_view contents) {
    base::ScopedAllowBlockingForTesting allow_blocking;
    EXPECT_TRUE(
        base::WriteFile(temp_dir_.GetPath().Append(filename), contents));
  }

  void SetupServer() {
    // Set up server that will serve update manifest and the Web Bundle
    // of the IWA.
    base::ScopedAllowBlockingForTesting allow_blocking;
    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
    iwa_server_.ServeFilesFromDirectory(temp_dir_.GetPath());
    ASSERT_TRUE(iwa_server_.Start());
    {
      const std::vector<std::string> replacements1 = {
          iwa_server_.GetURL(std::string("/") + kIwaBundleFileName1).spec(),
          iwa_bundle_1_.id.id()};
      const std::string update_manifest_value = base::ReplaceStringPlaceholders(
          kUpdateManifestTemplate1, replacements1, nullptr);

      WriteFile(kUpdateManifestFileName1, update_manifest_value);
      WriteFile(kIwaBundleFileName1, std::string(iwa_bundle_1_.data.begin(),
                                                 iwa_bundle_1_.data.end()));
    }

    {
      const std::vector<std::string> replacements2 = {
          iwa_server_.GetURL(std::string("/") + kIwaBundleFileName2).spec(),
          iwa_bundle_2_.id.id()};
      const std::string update_manifest_value_app2 =
          base::ReplaceStringPlaceholders(kUpdateManifestTemplate2,
                                          replacements2, nullptr);
      WriteFile(kUpdateManifestFileName2, update_manifest_value_app2);
      WriteFile(kIwaBundleFileName2, std::string(iwa_bundle_2_.data.begin(),
                                                 iwa_bundle_2_.data.end()));
    }
  }

  void RefreshDevicePolicy() { policy_helper_.RefreshDevicePolicy(); }

  policy::DevicePolicyBuilder* device_policy() {
    return policy_helper_.device_policy();
  }

  ash::FakeSessionManagerClient* session_manager_client() {
    return ash::FakeSessionManagerClient::Get();
  }

  const AccountId account_id_ =
      AccountId::FromUserEmail(GenerateDeviceLocalAccountUserId(
          kUserMail,
          policy::DeviceLocalAccountType::kPublicSession));
  policy::UserPolicyBuilder device_local_account_policy_;
  const web_app::TestSignedWebBundle iwa_bundle_1_ =
      web_app::TestSignedWebBundleBuilder::BuildDefault(
          TestSignedWebBundleBuilder::BuildOptions()
              .SetVersion(base::Version("7.0.6"))
              .AddKeyPair(web_package::test::Ed25519KeyPair::CreateRandom()));
  const web_app::TestSignedWebBundle iwa_bundle_2_ =
      web_app::TestSignedWebBundleBuilder::BuildDefault(
          TestSignedWebBundleBuilder::BuildOptions().SetVersion(
              base::Version("2.0.0")));
  const bool is_user_session_;

 private:
  ash::EmbeddedPolicyTestServerMixin policy_test_server_mixin_{&mixin_host_};
  ash::DeviceStateMixin device_state_{
      &mixin_host_,
      ash::DeviceStateMixin::State::OOBE_COMPLETED_CLOUD_ENROLLED};
  ash::LoginManagerMixin login_manager_mixin_{&mixin_host_};
  base::ScopedTempDir temp_dir_;
  net::EmbeddedTestServer iwa_server_;
  base::test::ScopedFeatureList scoped_feature_list_;
  testing::NiceMock<policy::MockConfigurationPolicyProvider> policy_provider_;
  policy::DevicePolicyCrosTestHelper policy_helper_;
};

class IsolatedWebAppPolicyManagerAshBrowserTest
    : public IsolatedWebAppPolicyManagerAshBrowserTestBase,
      public testing::WithParamInterface<bool> {
 public:
  IsolatedWebAppPolicyManagerAshBrowserTest()
      : IsolatedWebAppPolicyManagerAshBrowserTestBase(GetParam()) {}
  IsolatedWebAppPolicyManagerAshBrowserTest(
      const IsolatedWebAppPolicyManagerAshBrowserTest&) = delete;
  IsolatedWebAppPolicyManagerAshBrowserTest& operator=(
      const IsolatedWebAppPolicyManagerAshBrowserTest&) = delete;
};

IN_PROC_BROWSER_TEST_P(IsolatedWebAppPolicyManagerAshBrowserTest,
                       InstallIsolatedWebAppOnLogin) {
  SetupServer();

  AddUser(/*set_iwa_policy_on_login=*/true);

  // Log in in the managed guest session.
  ASSERT_NO_FATAL_FAILURE(StartLogin());
  WaitForSessionStart();

  Profile* profile = GetProfileForTest();

  // Wait for the IWA to be installed.
  WebAppTestInstallObserver observer(profile);
  const webapps::AppId id = observer.BeginListeningAndWait();
  EXPECT_EQ(id,
            IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(iwa_bundle_1_.id)
                .app_id());
  const WebAppProvider* provider = WebAppProvider::GetForTest(profile);
  EXPECT_TRUE(provider->registrar_unsafe().IsInstalled(id));
}

IN_PROC_BROWSER_TEST_P(IsolatedWebAppPolicyManagerAshBrowserTest,
                       PolicyUpdate) {
  SetupServer();

  AddUser();

  SetPolicyWithOneApp();

  // Log in in the managed guest session.
  // There no IWA policy set at the moment of login.
  ASSERT_NO_FATAL_FAILURE(StartLogin());
  WaitForSessionStart();

  // Set the policy with 1 IWA and wait for the IWA to be installed.
  WebAppTestInstallObserver observer(GetProfileForTest());
  SetPolicyWithOneApp();
  const webapps::AppId id = observer.BeginListeningAndWait();
  ASSERT_EQ(id,
            IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(iwa_bundle_1_.id)
                .app_id());
  const WebAppProvider* provider =
      WebAppProvider::GetForTest(GetProfileForTest());
  EXPECT_TRUE(provider->registrar_unsafe().IsInstalled(id));

  // Set the policy with 2 IWAs and wait for the IWA to be installed.
  WebAppTestInstallObserver observer2(GetProfileForTest());
  SetPolicyWithTwoApps();
  const webapps::AppId id2 = observer2.BeginListeningAndWait();
  EXPECT_EQ(id2,
            IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(iwa_bundle_2_.id)
                .app_id());
}

IN_PROC_BROWSER_TEST_P(IsolatedWebAppPolicyManagerAshBrowserTest,
                       PolicyDeleteAndReinstall) {
  SetupServer();

  AddUser();

  // Log in to the managed guest session. There is no IWA policy set at the
  // moment of login.
  ASSERT_NO_FATAL_FAILURE(StartLogin());
  WaitForSessionStart();

  const webapps::AppId id1 =
      IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(iwa_bundle_1_.id)
          .app_id();
  const webapps::AppId id2 =
      IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(iwa_bundle_2_.id)
          .app_id();

  const WebAppProvider* provider =
      WebAppProvider::GetForTest(GetProfileForTest());

  // Set the policy with 2 IWAs and wait for the IWAs to be installed.
  {
    WebAppTestInstallObserver install_observer(GetProfileForTest());
    install_observer.BeginListening({id1, id2});

    SetPolicyWithTwoApps();
    install_observer.Wait();

    EXPECT_TRUE(provider->registrar_unsafe().IsInstalled(id1));
    EXPECT_TRUE(provider->registrar_unsafe().IsInstalled(id2));
  }

  // Set the policy with 1 IWA and wait for the unnecessary IWA to be
  // uninstalled.
  {
    // Prepare testing environment for uninstalling.
    base::test::TestFuture<void> uninstall_browsing_data_future;
    auto* browsing_data_remover = GetProfileForTest()->GetBrowsingDataRemover();
    browsing_data_remover->SetWouldCompleteCallbackForTesting(
        base::BindLambdaForTesting([&](base::OnceClosure callback) {
          if (browsing_data_remover->GetPendingTaskCountForTesting() == 1u) {
            uninstall_browsing_data_future.SetValue();
          }
          std::move(callback).Run();
        }));

    WebAppTestUninstallObserver uninstall_observer(GetProfileForTest());
    uninstall_observer.BeginListening({id2});
    SetPolicyWithOneApp();

    EXPECT_TRUE(uninstall_browsing_data_future.Wait());
    EXPECT_EQ(uninstall_observer.Wait(), id2);

    EXPECT_TRUE(provider->registrar_unsafe().IsInstalled(id1));
    EXPECT_FALSE(provider->registrar_unsafe().IsInstalled(id2));
  }

  // Set the policy with 2 IWAs and wait for the second IWA to be re-installed.
  {
    WebAppTestInstallObserver install_observer(GetProfileForTest());
    install_observer.BeginListening({id2});

    SetPolicyWithTwoApps();
    install_observer.Wait();

    EXPECT_TRUE(provider->registrar_unsafe().IsInstalled(id1));
    EXPECT_TRUE(provider->registrar_unsafe().IsInstalled(id2));
  }
}

INSTANTIATE_TEST_SUITE_P(
    /***/,
    IsolatedWebAppPolicyManagerAshBrowserTest,
    // Controls whether or not to test in a user session (true) or in a managed
    // guest session (false).
    testing::Bool());

class IsolatedWebAppDevToolsTestWithPolicy
    : public IsolatedWebAppPolicyManagerAshBrowserTestBase,
      public testing::WithParamInterface<
          std::tuple<bool, DeveloperToolsPolicyHandler::Availability>> {
 public:
  IsolatedWebAppDevToolsTestWithPolicy()
      : IsolatedWebAppPolicyManagerAshBrowserTestBase(
            std::get<bool>(GetParam())) {}

  void SetDevToolsAvailability() {
    GetProfileForTest()->GetPrefs()->SetInteger(
        prefs::kDevToolsAvailability,
        base::to_underlying(
            std::get<DeveloperToolsPolicyHandler::Availability>(GetParam())));
  }
  bool AreDevToolsWindowsAllowedByCurrentPolicy() const {
    return std::get<DeveloperToolsPolicyHandler::Availability>(GetParam()) ==
           DeveloperToolsPolicyHandler::Availability::kAllowed;
  }
};

IN_PROC_BROWSER_TEST_P(IsolatedWebAppDevToolsTestWithPolicy,
                       DisabledForForceInstalledIwas) {
  SetupServer();

  AddUser();

  // Log in to the managed guest session. There is no IWA policy set at the
  // moment of login.
  ASSERT_NO_FATAL_FAILURE(StartLogin());
  WaitForSessionStart();

  const webapps::AppId id1 =
      IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(iwa_bundle_1_.id)
          .app_id();
  {
    WebAppTestInstallObserver install_observer(GetProfileForTest());
    install_observer.BeginListening({id1});

    SetPolicyWithOneApp();
    install_observer.Wait();

    EXPECT_TRUE(WebAppProvider::GetForTest(GetProfileForTest())
                    ->registrar_unsafe()
                    .IsInstalled(id1));
  }

  SetDevToolsAvailability();

  auto* browser = web_app::LaunchWebAppBrowserAndWait(GetProfileForTest(), id1);
  content::WebContents* web_contents =
      browser->tab_strip_model()->GetActiveWebContents();
  ASSERT_EQ(!!DevToolsWindowTesting::OpenDevToolsWindowSync(web_contents,
                                                            /*is_docked=*/true),
            AreDevToolsWindowsAllowedByCurrentPolicy());
}

INSTANTIATE_TEST_SUITE_P(
    /***/,
    IsolatedWebAppDevToolsTestWithPolicy,
    testing::Combine(
        testing::Bool(),
        testing::Values(
            DeveloperToolsPolicyHandler::Availability::kAllowed,
            DeveloperToolsPolicyHandler::Availability::
                kDisallowedForForceInstalledExtensions,
            DeveloperToolsPolicyHandler::Availability::kDisallowed)));

class CleanupOrphanedBundlesTest
    : public IsolatedWebAppPolicyManagerAshBrowserTestBase,
      public ProfileManagerObserver,
      public testing::WithParamInterface<bool> {
 public:
  CleanupOrphanedBundlesTest()
      : IsolatedWebAppPolicyManagerAshBrowserTestBase(
            /*is_user_session=*/GetParam()) {}

  void SetUpOnMainThread() override {
    iwa_installer_factory_.SetUp(GetProfileForTest());
    IsolatedWebAppPolicyManagerAshBrowserTestBase::SetUpOnMainThread();
    profile_manager_observation_.Observe(g_browser_process->profile_manager());
  }

  void TearDownOnMainThread() override {
    last_simulate_orphaned_bundle_profile_ = nullptr;
  }

  void SimulateOrphanedBundle(Profile* profile,
                              const std::string& bundle_directory) {
    base::ScopedAllowBlockingForTesting allow_blocking;
    auto base_path = CHECK_DEREF(profile)
                         .GetPath()
                         .Append(kIwaDirName)
                         .Append(bundle_directory);
    ASSERT_TRUE(base::CreateDirectory(base_path));
    ASSERT_TRUE(
        base::WriteFile(base_path.Append("main.swbn"), "Sample content"));
  }

  bool CheckBundleDirectoryExists(Profile* profile,
                                  const std::string& bundle_directory) {
    base::ScopedAllowBlockingForTesting allow_blocking;
    return base::DirectoryExists(CHECK_DEREF(profile)
                                     .GetPath()
                                     .Append(kIwaDirName)
                                     .Append(bundle_directory));
  }

  // ProfileManagerObserver:
  void OnProfileAdded(Profile* profile) override {
    last_simulate_orphaned_bundle_profile_ = profile;
    SimulateOrphanedBundle(profile, kOrphanedBundleDirectory);
    ASSERT_TRUE(CheckBundleDirectoryExists(profile, kOrphanedBundleDirectory));
  }

  void OnProfileManagerDestroying() override {
    profile_manager_observation_.Reset();
  }

 protected:
  TestIwaInstallerFactory iwa_installer_factory_;
  raw_ptr<Profile> last_simulate_orphaned_bundle_profile_ = nullptr;
  base::ScopedObservation<ProfileManager, ProfileManagerObserver>
      profile_manager_observation_{this};
  base::test::ScopedFeatureList scoped_feature_list_;
};

IN_PROC_BROWSER_TEST_P(CleanupOrphanedBundlesTest,
                       CleanUpSuccessfulOnSessionStart) {
  SetupServer();

  AddUser(/*set_iwa_policy_on_login=*/false);

  // Login to the session.
  ASSERT_NO_FATAL_FAILURE(StartLogin());
  WaitForSessionStart();

  Profile* const profile = GetProfileForTest();
  WebAppProvider::GetForTest(profile)
      ->command_manager()
      .AwaitAllCommandsCompleteForTesting();

  // Make sure we simulated the orphaned bundle for the profile we run the
  // cleanup command on.
  EXPECT_EQ(last_simulate_orphaned_bundle_profile_, profile);
  EXPECT_FALSE(CheckBundleDirectoryExists(profile, kOrphanedBundleDirectory));
}

IN_PROC_BROWSER_TEST_P(CleanupOrphanedBundlesTest,
                       CleanUpSuccessfulOnFailedInstall) {
  SetupServer();

  base::test::TestFuture<void> future;
  iwa_installer_factory_.SetInstallCompletedClosure(
      future.GetRepeatingCallback());

  AddUser(/*set_iwa_policy_on_login=*/false);

  // Login to the session.
  ASSERT_NO_FATAL_FAILURE(StartLogin());
  WaitForSessionStart();

  Profile* const profile = GetProfileForTest();
  WebAppProvider* provider = WebAppProvider::GetForTest(profile);
  WebAppCommandManager& command_manager = provider->command_manager();
  command_manager.AwaitAllCommandsCompleteForTesting();

  SimulateOrphanedBundle(profile, kOrphanedBundleDirectory);
  ASSERT_TRUE(CheckBundleDirectoryExists(profile, kOrphanedBundleDirectory));

  // Try to install an isolated web app, which should fail. This should trigger
  // a cleanup.
  iwa_installer_factory_.SetCommandBehavior(
      iwa_bundle_1_.id.id(), /*execution_mode=*/
      MockIsolatedWebAppInstallCommandWrapper::ExecutionMode::kSimulateFailure,
      /*execute_immediately=*/true);
  SetPolicyWithOneApp();
  ASSERT_TRUE(future.Wait());
  webapps::AppId id =
      IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(iwa_bundle_1_.id)
          .app_id();
  EXPECT_FALSE(provider->registrar_unsafe().IsInstalled(id));

  // Wait until the cleanup is done.
  command_manager.AwaitAllCommandsCompleteForTesting();
  EXPECT_EQ(0u, command_manager.GetCommandCountForTesting());
  EXPECT_FALSE(CheckBundleDirectoryExists(profile, kOrphanedBundleDirectory));
}

INSTANTIATE_TEST_SUITE_P(
    /***/,
    CleanupOrphanedBundlesTest,
    // Is a user session (true) or a managed guest session (false).
    testing::Bool());

}  // namespace web_app