chromium/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_update_manager_unittest.cc

// Copyright 2023 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/isolated_web_apps/isolated_web_app_update_manager.h"

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

#include "base/containers/to_vector.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/json/values_util.h"
#include "base/memory/scoped_refptr.h"
#include "base/ranges/algorithm.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/gmock_expected_support.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/test/test_timeouts.h"
#include "base/test/values_test_util.h"
#include "base/time/time.h"
#include "base/types/expected.h"
#include "base/version.h"
#include "chrome/browser/browsing_data/chrome_browsing_data_remover_delegate.h"
#include "chrome/browser/browsing_data/chrome_browsing_data_remover_delegate_factory.h"
#include "chrome/browser/ui/web_applications/test/isolated_web_app_test_utils.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_apply_update_command.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_source.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_storage_location.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_trust_checker.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_update_discovery_task.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_info.h"
#include "chrome/browser/web_applications/isolated_web_apps/iwa_identity_validator.h"
#include "chrome/browser/web_applications/isolated_web_apps/policy/isolated_web_app_policy_constants.h"
#include "chrome/browser/web_applications/isolated_web_apps/test/test_signed_web_bundle_builder.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/fake_web_app_ui_manager.h"
#include "chrome/browser/web_applications/test/fake_web_contents_manager.h"
#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
#include "chrome/browser/web_applications/test/web_app_test.h"
#include "chrome/browser/web_applications/test/web_app_test_observers.h"
#include "chrome/browser/web_applications/web_app_command_scheduler.h"
#include "chrome/browser/web_applications/web_app_helpers.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_contents/web_contents_manager.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/url_constants.h"
#include "components/nacl/common/buildflags.h"
#include "components/web_package/signed_web_bundles/ed25519_public_key.h"
#include "components/web_package/signed_web_bundles/signed_web_bundle_id.h"
#include "components/webapps/browser/installable/installable_metrics.h"
#include "components/webapps/browser/uninstall_result_code.h"
#include "components/webapps/common/web_app_id.h"
#include "content/public/common/content_features.h"
#include "net/http/http_status_code.h"
#include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h"
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/origin.h"

#if BUILDFLAG(ENABLE_NACL)
#include "chrome/browser/nacl_host/nacl_browser_delegate_impl.h"
#include "components/nacl/browser/nacl_browser.h"
#endif  // BUILDFLAG(ENABLE_NACL)

namespace web_app {
namespace {

ToVector;
DictionaryHasValue;
ValueIs;
_;
AllOf;
Each;
ElementsAre;
Eq;
ExplainMatchResult;
Field;
Ge;
Invoke;
IsEmpty;
IsFalse;
IsTrue;
Le;
Ne;
NiceMock;
NotNull;
Optional;
Property;
SizeIs;
VariantWith;
WithArg;

blink::mojom::ManifestPtr CreateDefaultManifest(const GURL& application_url,
                                                std::u16string_view short_name,
                                                const base::Version& version) {}

MATCHER_P(IsDict, dict_matcher, "") {}

class MockCommandScheduler : public WebAppCommandScheduler {};

#if BUILDFLAG(ENABLE_NACL)
class ScopedNaClBrowserDelegate {
 public:
  ~ScopedNaClBrowserDelegate() { nacl::NaClBrowser::ClearAndDeleteDelegate(); }

  void Init(ProfileManager* profile_manager) {
    nacl::NaClBrowser::SetDelegate(
        std::make_unique<NaClBrowserDelegateImpl>(profile_manager));
  }
};
#endif  // BUILDFLAG(ENABLE_NACL)

// TODO(b/304691179): Rely less on `RunUntilIdle` and more on concrete events in
// all of these tests.
class IsolatedWebAppUpdateManagerTest : public WebAppTest {};

class IsolatedWebAppUpdateManagerDevModeUpdateTest
    : public IsolatedWebAppUpdateManagerTest {};

TEST_F(IsolatedWebAppUpdateManagerDevModeUpdateTest,
       DiscoversLocalDevModeUpdate) {}

class IsolatedWebAppUpdateManagerUpdateTest
    : public IsolatedWebAppUpdateManagerTest {};

class IsolatedWebAppUpdateManagerUpdateMockTimeTest
    : public IsolatedWebAppUpdateManagerUpdateTest {};

#if BUILDFLAG(IS_CHROMEOS)
TEST_F(IsolatedWebAppUpdateManagerUpdateMockTimeTest,
       DiscoversAndPreparesUpdateOfPolicyInstalledApps) {
  IsolatedWebAppUrlInfo non_installed_url_info =
      IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(
          *web_package::SignedWebBundleId::Create(
              "5tkrnsmftl4ggvvdkfth3piainqragus2qbhf7rlz2a3wo3rh4wqaaic"));
  IsolatedWebAppUrlInfo dev_bundle_url_info =
      IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(
          *web_package::SignedWebBundleId::Create(
              "aerugqztij5biqquuk3mfwpsaibuegaqcitgfchwuosuofdjabzqaaic"));
  IsolatedWebAppUrlInfo dev_proxy_url_info =
      IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(
          web_package::SignedWebBundleId::CreateRandomForProxyMode());

  test::InstallDummyWebApp(profile(), "non-iwa", GURL("https://a"));
  AddDummyIsolatedAppToRegistry(
      profile(), iwa_info1_->url_info.origin().GetURL(), "installed iwa 1",
      WebApp::IsolationData(iwa_info1_->installed_location,
                            iwa_info1_->installed_version),
      webapps::WebappInstallSource::IWA_EXTERNAL_POLICY);
  AddDummyIsolatedAppToRegistry(
      profile(), dev_proxy_url_info.origin().GetURL(),
      "installed iwa 2 (dev mode proxy)",
      WebApp::IsolationData(IwaStorageProxy{dev_proxy_url_info.origin()},
                            base::Version("1.0.0")),
      webapps::WebappInstallSource::IWA_EXTERNAL_POLICY);
  AddDummyIsolatedAppToRegistry(
      profile(), dev_bundle_url_info.origin().GetURL(),
      "installed iwa 3 (unowned bundle)",
      WebApp::IsolationData(IwaStorageUnownedBundle{base::FilePath()},
                            base::Version("1.0.0")),
      webapps::WebappInstallSource::IWA_EXTERNAL_POLICY);
  AddDummyIsolatedAppToRegistry(
      profile(), GURL("isolated-app://b"), "installed iwa 4",
      WebApp::IsolationData(
          IwaStorageOwnedBundle{/*dir_name_ascii=*/"", /*dev_mode=*/false},
          base::Version("1.0.0")),
      webapps::WebappInstallSource::IWA_EXTERNAL_POLICY);

  fake_ui_manager().SetNumWindowsForApp(iwa_info1_->url_info.app_id(), 1);

  SetIwaForceInstallPolicy(
      {{iwa_info1_->url_info, iwa_info1_->update_manifest_url.spec()},
       {non_installed_url_info, "https://example.com/update_manifest.json"},
       {dev_bundle_url_info, "https://example.com/update_manifest.json"},
       {dev_proxy_url_info, "https://example.com/update_manifest.json"}});

  task_environment()->FastForwardBy(
      *update_manager().GetNextUpdateDiscoveryTimeForTesting() -
      base::TimeTicks::Now());
  task_environment()->RunUntilIdle();

  EXPECT_THAT(
      fake_provider().registrar_unsafe().GetAppById(
          iwa_info1_->url_info.app_id()),
      test::IwaIs(Eq("installed iwa 1"),
                  test::IsolationDataIs(Eq(iwa_info1_->installed_location),
                                        Eq(iwa_info1_->installed_version),
                                        /*controlled_frame_partitions=*/_,
                                        test::PendingUpdateInfoIs(
                                            UpdateLocationMatcher(profile()),
                                            Eq(base::Version("2.0.0")),
                                            /*integrity_block_data=*/_),
                                        /*integrity_block_data=*/_)));

  EXPECT_THAT(
      UpdateDiscoveryLog(),
      UnorderedElementsAre(IsDict(DictionaryHasValue(
          "result", base::Value("Success::kUpdateFoundAndDryRunSuccessful")))));
  EXPECT_THAT(UpdateApplyLog(), IsEmpty());

  // TODO(crbug.com/40277668): As a temporary fix to avoid race conditions with
  // `ScopedProfileKeepAlive`s, manually shutdown `KeyedService`s holding them.
  fake_provider().Shutdown();
  ChromeBrowsingDataRemoverDelegateFactory::GetForProfile(profile())
      ->Shutdown();
}
#endif  // BUILDFLAG(IS_CHROMEOS)

TEST_F(IsolatedWebAppUpdateManagerUpdateMockTimeTest,
       MaybeDiscoverUpdatesForApp) {}

#if BUILDFLAG(IS_CHROMEOS)
TEST_F(IsolatedWebAppUpdateManagerUpdateMockTimeTest, DiscoverUpdatesNow) {
  AddDummyIsolatedAppToRegistry(
      profile(), iwa_info1_->url_info.origin().GetURL(), "installed iwa 1",
      WebApp::IsolationData(iwa_info1_->installed_location,
                            iwa_info1_->installed_version),
      webapps::WebappInstallSource::IWA_EXTERNAL_POLICY);

  fake_ui_manager().SetNumWindowsForApp(iwa_info1_->url_info.app_id(), 1);

  SetIwaForceInstallPolicy(
      {{iwa_info1_->url_info, iwa_info1_->update_manifest_url.spec()}});

  // After one hour, the update should not yet have run, but still be scheduled
  // (i.e. containing a value in the `std::optional`).
  task_environment()->FastForwardBy(base::Hours(1));
  auto old_update_discovery_time =
      update_manager().GetNextUpdateDiscoveryTimeForTesting();
  EXPECT_THAT(old_update_discovery_time.has_value(), IsTrue());

  // Once we manually trigger update discovery, the update discovery timer
  // should reset to a different time in the future.
  EXPECT_THAT(update_manager().DiscoverUpdatesNow(), Eq(1ul));
  EXPECT_THAT(update_manager().GetNextUpdateDiscoveryTimeForTesting(),
              AllOf(Ne(old_update_discovery_time), Ge(base::TimeTicks::Now())));

  task_environment()->RunUntilIdle();

  EXPECT_THAT(
      fake_provider().registrar_unsafe().GetAppById(
          iwa_info1_->url_info.app_id()),
      test::IwaIs(Eq("installed iwa 1"),
                  test::IsolationDataIs(Eq(iwa_info1_->installed_location),
                                        Eq(iwa_info1_->installed_version),
                                        /*controlled_frame_partitions=*/_,
                                        test::PendingUpdateInfoIs(
                                            UpdateLocationMatcher(profile()),
                                            Eq(base::Version("2.0.0")),
                                            /*integrity_block_data=*/_),
                                        /*integrity_block_data=*/_)));

  EXPECT_THAT(
      UpdateDiscoveryLog(),
      UnorderedElementsAre(IsDict(DictionaryHasValue(
          "result", base::Value("Success::kUpdateFoundAndDryRunSuccessful")))));
  EXPECT_THAT(UpdateApplyLog(), IsEmpty());

  // TODO(crbug.com/40277668): As a temporary fix to avoid race conditions with
  // `ScopedProfileKeepAlive`s, manually shutdown `KeyedService`s holding them.
  fake_provider().Shutdown();
  ChromeBrowsingDataRemoverDelegateFactory::GetForProfile(profile())
      ->Shutdown();
}

TEST_F(IsolatedWebAppUpdateManagerUpdateTest,
       ApplysUpdatesAfterWindowIsClosed) {
  AddDummyIsolatedAppToRegistry(
      profile(), iwa_info1_->url_info.origin().GetURL(), "installed app",
      WebApp::IsolationData(iwa_info1_->installed_location,
                            iwa_info1_->installed_version),
      webapps::WebappInstallSource::IWA_EXTERNAL_POLICY);

  fake_ui_manager().SetNumWindowsForApp(iwa_info1_->url_info.app_id(), 1);

  SetIwaForceInstallPolicy(
      {{iwa_info1_->url_info, iwa_info1_->update_manifest_url.spec()}});
  update_manager().DiscoverUpdatesNow();
  task_environment()->RunUntilIdle();

  EXPECT_THAT(
      fake_provider().registrar_unsafe().GetAppById(
          iwa_info1_->url_info.app_id()),
      test::IwaIs(Eq("installed app"),
                  test::IsolationDataIs(Eq(iwa_info1_->installed_location),
                                        Eq(iwa_info1_->installed_version),
                                        /*controlled_frame_partitions=*/_,
                                        test::PendingUpdateInfoIs(
                                            UpdateLocationMatcher(profile()),
                                            Eq(iwa_info1_->update_version),
                                            /*integrity_block_data=*/_),
                                        /*integrity_block_data=*/_)));

  EXPECT_THAT(
      UpdateDiscoveryLog(),
      UnorderedElementsAre(IsDict(DictionaryHasValue(
          "result", base::Value("Success::kUpdateFoundAndDryRunSuccessful")))));
  EXPECT_THAT(UpdateApplyLog(), IsEmpty());

  fake_ui_manager().SetNumWindowsForApp(iwa_info1_->url_info.app_id(), 0);
  task_environment()->RunUntilIdle();

  EXPECT_THAT(UpdateApplyLog(), UnorderedElementsAre(IsDict(DictionaryHasValue(
                                    "result", base::Value("Success")))));

  EXPECT_THAT(fake_provider().registrar_unsafe().GetAppById(
                  iwa_info1_->url_info.app_id()),
              test::IwaIs(iwa_info1_->update_app_name,
                          test::IsolationDataIs(
                              UpdateLocationMatcher(profile()),
                              Eq(iwa_info1_->update_version),
                              /*controlled_frame_partitions=*/_,
                              /*pending_update_info=*/Eq(std::nullopt),
                              /*integrity_block_data=*/_)));
}

TEST_F(IsolatedWebAppUpdateManagerUpdateTest,
       ApplysUpdatesWithHigherPriorityThanUpdateDiscovery) {
  AddDummyIsolatedAppToRegistry(
      profile(), iwa_info1_->url_info.origin().GetURL(), "installed app 1",
      WebApp::IsolationData(iwa_info1_->installed_location,
                            iwa_info1_->installed_version),
      webapps::WebappInstallSource::IWA_EXTERNAL_POLICY);
  AddDummyIsolatedAppToRegistry(
      profile(), iwa_info2_->url_info.origin().GetURL(), "installed app 2",
      WebApp::IsolationData(iwa_info2_->installed_location,
                            iwa_info2_->installed_version),
      webapps::WebappInstallSource::IWA_EXTERNAL_POLICY);

  SetIwaForceInstallPolicy(
      {{iwa_info1_->url_info, iwa_info1_->update_manifest_url.spec()},
       {iwa_info2_->url_info, iwa_info2_->update_manifest_url.spec()}});
  update_manager().DiscoverUpdatesNow();
  task_environment()->RunUntilIdle();

  {
    auto update_discovery_log = UpdateDiscoveryLog();
    auto update_apply_log = UpdateApplyLog();

    EXPECT_THAT(
        update_discovery_log,
        UnorderedElementsAre(
            IsDict(DictionaryHasValue(
                "result",
                base::Value("Success::kUpdateFoundAndDryRunSuccessful"))),
            IsDict(DictionaryHasValue(
                "result",
                base::Value("Success::kUpdateFoundAndDryRunSuccessful")))));

    EXPECT_THAT(
        update_apply_log,
        UnorderedElementsAre(
            IsDict(DictionaryHasValue("result", base::Value("Success"))),
            IsDict(DictionaryHasValue("result", base::Value("Success")))));

    std::vector<base::Value*> times(
        {update_discovery_log[0].GetDict().Find("start_time"),
         update_discovery_log[0].GetDict().Find("end_time"),
         update_apply_log[0].GetDict().Find("start_time"),
         update_apply_log[0].GetDict().Find("end_time"),

         update_discovery_log[1].GetDict().Find("start_time"),
         update_discovery_log[1].GetDict().Find("end_time"),
         update_apply_log[1].GetDict().Find("start_time"),
         update_apply_log[1].GetDict().Find("end_time")});
    EXPECT_THAT(base::ranges::is_sorted(times, {},
                                        [](base::Value* value) {
                                          return *base::ValueToTime(value);
                                        }),
                IsTrue())
        << base::JoinString(ToVector(times, &base::Value::DebugString), "");
  }

  EXPECT_THAT(fake_provider().registrar_unsafe().GetAppById(
                  iwa_info1_->url_info.app_id()),
              test::IwaIs(iwa_info1_->update_app_name,
                          test::IsolationDataIs(
                              UpdateLocationMatcher(profile()),
                              Eq(iwa_info1_->update_version),
                              /*controlled_frame_partitions=*/_,
                              /*pending_update_info=*/Eq(std::nullopt),
                              /*integrity_block_data=*/_)));
  EXPECT_THAT(fake_provider().registrar_unsafe().GetAppById(
                  iwa_info2_->url_info.app_id()),
              test::IwaIs(iwa_info2_->update_app_name,
                          test::IsolationDataIs(
                              UpdateLocationMatcher(profile()),
                              Eq(iwa_info2_->update_version),
                              /*controlled_frame_partitions=*/_,
                              /*pending_update_info=*/Eq(std::nullopt),
                              /*integrity_block_data=*/_)));
}

TEST_F(IsolatedWebAppUpdateManagerUpdateTest,
       StopsNonStartedUpdateDiscoveryTasksIfIwaIsUninstalled) {
  profile_url_loader_factory().ClearResponses();

  AddDummyIsolatedAppToRegistry(
      profile(), iwa_info1_->url_info.origin().GetURL(), "installed app 1",
      WebApp::IsolationData(iwa_info1_->installed_location,
                            iwa_info1_->installed_version),
      webapps::WebappInstallSource::IWA_EXTERNAL_POLICY);
  AddDummyIsolatedAppToRegistry(
      profile(), iwa_info2_->url_info.origin().GetURL(), "installed app 2",
      WebApp::IsolationData(iwa_info2_->installed_location,
                            iwa_info2_->installed_version),
      webapps::WebappInstallSource::IWA_EXTERNAL_POLICY);

  SetIwaForceInstallPolicy(
      {{iwa_info1_->url_info, iwa_info1_->update_manifest_url.spec()},
       {iwa_info2_->url_info, iwa_info2_->update_manifest_url.spec()}});
  update_manager().DiscoverUpdatesNow();
  task_environment()->RunUntilIdle();

  // Wait for the update discovery task of either app 1 or app 2 to request the
  // update manifest (which task starts first is undefined).
  ASSERT_THAT(profile_url_loader_factory().NumPending(), Eq(1));
  EXPECT_THAT(UpdateDiscoveryTasks(), SizeIs(2));  // two tasks should be queued
  EXPECT_THAT(UpdateDiscoveryLog(), IsEmpty());  // no task should have finished

  // Uninstall the other IWA whose update discovery task has not yet started.
  GURL pending_url =
      profile_url_loader_factory().GetPendingRequest(0)->request.url;
  IwaInfo* iwa_to_keep;
  IwaInfo* iwa_to_uninstall;
  if (pending_url == iwa_info1_->update_manifest_url) {
    iwa_to_keep = &*iwa_info1_;
    iwa_to_uninstall = &*iwa_info2_;
  } else if (pending_url == iwa_info2_->update_manifest_url) {
    iwa_to_keep = &*iwa_info2_;
    iwa_to_uninstall = &*iwa_info1_;
  } else {
    FAIL() << "Unexpected pending request for: " << pending_url;
  }

  EXPECT_THAT(UninstallPolicyInstalledIwa(iwa_to_uninstall->url_info.app_id()),
              Eq(webapps::UninstallResultCode::kAppRemoved));

  EXPECT_THAT(UpdateDiscoveryTasks(),
              UnorderedElementsAre(IsDict(DictionaryHasValue(
                  "app_id", base::Value(iwa_to_keep->url_info.app_id())))));
  EXPECT_THAT(UpdateDiscoveryLog(), IsEmpty());

  // TODO(crbug.com/40277668): As a temporary fix to avoid race conditions with
  // `ScopedProfileKeepAlive`s, manually shutdown `KeyedService`s holding them.
  fake_provider().Shutdown();
  ChromeBrowsingDataRemoverDelegateFactory::GetForProfile(profile())
      ->Shutdown();
}

// TODO(b/338380813): The test is flaky on asan ChromeOS builder.
TEST_F(IsolatedWebAppUpdateManagerUpdateTest,
       DISABLED_StopsWaitingIfIwaIsUninstalled) {
  AddDummyIsolatedAppToRegistry(
      profile(), iwa_info1_->url_info.origin().GetURL(), "installed app",
      WebApp::IsolationData(iwa_info1_->installed_location,
                            iwa_info1_->installed_version),
      webapps::WebappInstallSource::IWA_EXTERNAL_POLICY);

  fake_ui_manager().SetNumWindowsForApp(iwa_info1_->url_info.app_id(), 1);

  SetIwaForceInstallPolicy(
      {{iwa_info1_->url_info, iwa_info1_->update_manifest_url.spec()}});
  update_manager().DiscoverUpdatesNow();
  task_environment()->RunUntilIdle();

  EXPECT_THAT(
      UpdateDiscoveryLog(),
      UnorderedElementsAre(IsDict(DictionaryHasValue(
          "result", base::Value("Success::kUpdateFoundAndDryRunSuccessful")))));
  EXPECT_THAT(UpdateApplyWaiters(),
              UnorderedElementsAre(IsDict(DictionaryHasValue(
                  "app_id", base::Value(iwa_info1_->url_info.app_id())))));

  EXPECT_THAT(UninstallPolicyInstalledIwa(iwa_info1_->url_info.app_id()),
              Eq(webapps::UninstallResultCode::kAppRemoved));

  EXPECT_THAT(UpdateApplyWaiters(), IsEmpty());
  EXPECT_THAT(UpdateApplyTasks(), IsEmpty());
  EXPECT_THAT(UpdateApplyLog(), IsEmpty());

  // TODO(crbug.com/40277668): As a temporary fix to avoid race conditions with
  // `ScopedProfileKeepAlive`s, manually shutdown `KeyedService`s holding them.
  fake_provider().Shutdown();
  ChromeBrowsingDataRemoverDelegateFactory::GetForProfile(profile())
      ->Shutdown();
}

// TODO(b/326527744): This test is flaky on asan ChromeOS builder.
TEST_F(IsolatedWebAppUpdateManagerUpdateTest,
       DISABLED_StopsNonStartedUpdateApplyTasksIfIwaIsUninstalled) {
  AddDummyIsolatedAppToRegistry(
      profile(), iwa_info1_->url_info.origin().GetURL(), "installed app 1",
      WebApp::IsolationData(iwa_info1_->installed_location,
                            iwa_info1_->installed_version),
      webapps::WebappInstallSource::IWA_EXTERNAL_POLICY);
  AddDummyIsolatedAppToRegistry(
      profile(), iwa_info2_->url_info.origin().GetURL(), "installed app 2",
      WebApp::IsolationData(iwa_info2_->installed_location,
                            iwa_info2_->installed_version),
      webapps::WebappInstallSource::IWA_EXTERNAL_POLICY);

  fake_ui_manager().SetNumWindowsForApp(iwa_info1_->url_info.app_id(), 1);
  fake_ui_manager().SetNumWindowsForApp(iwa_info2_->url_info.app_id(), 1);

  SetIwaForceInstallPolicy(
      {{iwa_info1_->url_info, iwa_info1_->update_manifest_url.spec()},
       {iwa_info2_->url_info, iwa_info2_->update_manifest_url.spec()}});
  update_manager().DiscoverUpdatesNow();
  task_environment()->RunUntilIdle();

  EXPECT_THAT(
      UpdateDiscoveryLog(),
      UnorderedElementsAre(
          IsDict(DictionaryHasValue(
              "result",
              base::Value("Success::kUpdateFoundAndDryRunSuccessful"))),
          IsDict(DictionaryHasValue(
              "result",
              base::Value("Success::kUpdateFoundAndDryRunSuccessful")))));
  EXPECT_THAT(UpdateApplyWaiters(),
              UnorderedElementsAre(
                  IsDict(DictionaryHasValue(
                      "app_id", base::Value(iwa_info1_->url_info.app_id()))),
                  IsDict(DictionaryHasValue(
                      "app_id", base::Value(iwa_info2_->url_info.app_id())))));

  // Wait for the update apply task of either app 1 or app 2 to start.
  base::test::TestFuture<IsolatedWebAppUrlInfo> future;
  EXPECT_CALL(mock_command_scheduler(),
              ApplyPendingIsolatedWebAppUpdate(_, _, _, _, _))
      .WillOnce(WithArg<0>(Invoke(
          &future, &base::test::TestFuture<IsolatedWebAppUrlInfo>::SetValue)));
  fake_ui_manager().SetNumWindowsForApp(iwa_info1_->url_info.app_id(), 0);
  fake_ui_manager().SetNumWindowsForApp(iwa_info2_->url_info.app_id(), 0);
  webapps::AppId iwa_to_keep = future.Take().app_id();

  EXPECT_THAT(UpdateApplyTasks(), SizeIs(2));  // two tasks should be queued
  EXPECT_THAT(UpdateApplyLog(), IsEmpty());    // no task should have finished

  // Uninstall the other IWA whose update apply task has not yet started.
  IwaInfo* iwa_to_uninstall;
  if (iwa_to_keep == iwa_info1_->url_info.app_id()) {
    iwa_to_uninstall = &*iwa_info2_;
  } else if (iwa_to_keep == iwa_info2_->url_info.app_id()) {
    iwa_to_uninstall = &*iwa_info1_;
  } else {
    FAIL() << "Unexpected IWA app id: " << iwa_to_keep;
  }

  EXPECT_THAT(UninstallPolicyInstalledIwa(iwa_to_uninstall->url_info.app_id()),
              Eq(webapps::UninstallResultCode::kAppRemoved));

  EXPECT_THAT(UpdateApplyTasks(),
              UnorderedElementsAre(IsDict(
                  DictionaryHasValue("app_id", base::Value(iwa_to_keep)))));
  EXPECT_THAT(UpdateApplyLog(), IsEmpty());

  // TODO(crbug.com/40277668): As a temporary fix to avoid race conditions with
  // `ScopedProfileKeepAlive`s, manually shutdown `KeyedService`s holding them.
  fake_provider().Shutdown();
  ChromeBrowsingDataRemoverDelegateFactory::GetForProfile(profile())
      ->Shutdown();
}
#endif  // BUILDFLAG(IS_CHROMEOS)

class IsolatedWebAppUpdateManagerUpdateApplyOnStartupTest
    : public IsolatedWebAppUpdateManagerUpdateTest {};

TEST_F(IsolatedWebAppUpdateManagerUpdateApplyOnStartupTest,
       SchedulesPendingUpdateApplyTasks) {}

class IsolatedWebAppUpdateManagerDiscoveryTimerTest
    : public IsolatedWebAppUpdateManagerTest {};

TEST_F(IsolatedWebAppUpdateManagerDiscoveryTimerTest,
       DoesNotStartUpdateDiscoveryIfNoIwaIsInstalled) {}

TEST_F(IsolatedWebAppUpdateManagerDiscoveryTimerTest,
       StartsUpdateDiscoveryTimerWithJitter) {}

TEST_F(IsolatedWebAppUpdateManagerDiscoveryTimerTest,
       RunsUpdateDiscoveryWhileIwaIsInstalled) {}

struct FeatureFlagParam {};

class IsolatedWebAppUpdateManagerFeatureFlagTest
    : public IsolatedWebAppUpdateManagerTest,
      public ::testing::WithParamInterface<FeatureFlagParam> {};

TEST_P(IsolatedWebAppUpdateManagerFeatureFlagTest,
       DoesUpdateDiscoveryIfFeatureFlagsAreEnabled) {}

INSTANTIATE_TEST_SUITE_P();

}  // namespace

TEST_F(IsolatedWebAppUpdateManagerUpdateTest, UpdateDiscoveryTaskSuccess) {}

TEST_F(IsolatedWebAppUpdateManagerUpdateTest, UpdateDiscoveryTaskFails) {}

TEST_F(IsolatedWebAppUpdateManagerUpdateTest, UpdateApplyTaskSuccess) {}

TEST_F(IsolatedWebAppUpdateManagerUpdateTest, UpdateApplyTaskFails) {}

}  // namespace web_app