chromium/chrome/browser/apps/app_service/app_service_proxy_unittest.cc

// Copyright 2019 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/apps/app_service/app_service_proxy.h"

#include <memory>
#include <optional>
#include <utility>
#include <vector>

#include "base/containers/contains.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/apps/app_service/publishers/app_publisher.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/test/base/testing_profile.h"
#include "components/services/app_service/public/cpp/app_launch_util.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/services/app_service/public/cpp/features.h"
#include "components/services/app_service/public/cpp/icon_types.h"
#include "components/services/app_service/public/cpp/intent.h"
#include "components/services/app_service/public/cpp/intent_filter.h"
#include "components/services/app_service/public/cpp/intent_filter_util.h"
#include "components/services/app_service/public/cpp/intent_test_util.h"
#include "components/services/app_service/public/cpp/intent_util.h"
#include "components/services/app_service/public/cpp/preferred_app.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/image/image_skia_rep.h"

#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "chrome/browser/apps/app_service/subscriber_crosapi.h"
#include "components/services/app_service/public/cpp/types_util.h"
#endif

namespace apps {

#if !BUILDFLAG(IS_CHROMEOS_LACROS)
class FakePublisherForProxyTest : public AppPublisher {};
#endif

#if BUILDFLAG(IS_CHROMEOS_ASH)
// FakeAppRegistryCacheObserver is used to test OnAppUpdate.
class FakeAppRegistryCacheObserver : public apps::AppRegistryCache::Observer {
 public:
  explicit FakeAppRegistryCacheObserver(apps::AppRegistryCache* cache) {
    app_registry_cache_observer_.Observe(cache);
  }

  ~FakeAppRegistryCacheObserver() override = default;

  // apps::AppRegistryCache::Observer overrides.
  void OnAppUpdate(const apps::AppUpdate& update) override {
    if (base::Contains(app_ids_, update.AppId())) {
      app_ids_.erase(update.AppId());
    }
    if (app_ids_.empty() && !result_.IsReady()) {
      result_.SetValue();
    }
  }

  void OnAppRegistryCacheWillBeDestroyed(
      apps::AppRegistryCache* cache) override {
    app_registry_cache_observer_.Reset();
  }

  void WaitForOnAppUpdate(const std::set<std::string>& app_ids) {
    app_ids_ = app_ids;
    EXPECT_TRUE(result_.Wait());
  }

 private:
  base::test::TestFuture<void> result_;
  base::ScopedObservation<apps::AppRegistryCache,
                          apps::AppRegistryCache::Observer>
      app_registry_cache_observer_{this};

  std::set<std::string> app_ids_;
};

class FakeSubscriberForProxyTest : public SubscriberCrosapi {
 public:
  explicit FakeSubscriberForProxyTest(Profile* profile)
      : SubscriberCrosapi(profile) {
    apps::AppServiceProxyFactory::GetForProfile(profile)
        ->RegisterCrosApiSubScriber(this);
  }

  PreferredAppsList& preferred_apps_list() { return preferred_apps_list_; }

  void OnPreferredAppsChanged(PreferredAppChangesPtr changes) override {
    preferred_apps_list_.ApplyBulkUpdate(std::move(changes));
  }

  void InitializePreferredApps(apps::PreferredApps preferred_apps) override {
    preferred_apps_list_.Init(std::move(preferred_apps));
  }

 private:
  apps::PreferredAppsList preferred_apps_list_;
};
#endif

class AppServiceProxyTest : public testing::Test {};

class AppServiceProxyIconTest : public AppServiceProxyTest {};

TEST_F(AppServiceProxyIconTest, IconCache) {}

TEST_F(AppServiceProxyIconTest, IconCoalescer) {}

TEST_F(AppServiceProxyTest, ProxyAccessPerProfile) {}

TEST_F(AppServiceProxyTest, ReinitializeClearsCache) {}

#if !BUILDFLAG(IS_CHROMEOS_LACROS)
class AppServiceProxyPreferredAppsTest : public AppServiceProxyTest {};

TEST_F(AppServiceProxyPreferredAppsTest, UpdatedOnUninstall) {}

TEST_F(AppServiceProxyPreferredAppsTest, SetPreferredApp) {}

// Tests that writing a preferred app value before the PreferredAppsList is
// initialized queues the write for after initialization.
TEST_F(AppServiceProxyPreferredAppsTest, PreferredAppsWriteBeforeInit) {}

TEST_F(AppServiceProxyPreferredAppsTest, PreferredAppsPersistency) {}

TEST_F(AppServiceProxyPreferredAppsTest,
       PreferredAppsSetSupportedLinksPublisher) {}

// Test that app with overlapped supported links works properly.
TEST_F(AppServiceProxyPreferredAppsTest, PreferredAppsOverlapSupportedLink) {}

// Test that duplicated entry will not be added for supported links.
TEST_F(AppServiceProxyPreferredAppsTest, PreferredAppsDuplicatedSupportedLink) {}
#endif  // !BUILDFLAG(IS_CHROMEOS_LACROS)

#if BUILDFLAG(IS_CHROMEOS_ASH)
TEST_F(AppServiceProxyPreferredAppsTest, PreferredAppsSetSupportedLinks) {
  GetPreferredAppsList().Init();

  const char kAppId1[] = "abcdefg";
  const char kAppId2[] = "hijklmn";
  const char kAppId3[] = "opqrstu";

  auto intent_filter_a =
      apps_util::MakeIntentFilterForUrlScope(GURL("https://www.a.com/"));
  auto intent_filter_b =
      apps_util::MakeIntentFilterForUrlScope(GURL("https://www.b.com/"));
  auto intent_filter_c =
      apps_util::MakeIntentFilterForUrlScope(GURL("https://www.c.com/"));

  FakeSubscriberForProxyTest sub(proxy()->profile());

  FakePublisherForProxyTest pub(
      proxy(), AppType::kArc,
      std::vector<std::string>{kAppId1, kAppId2, kAppId3});

  IntentFilters app_1_filters;
  app_1_filters.push_back(intent_filter_a->Clone());
  app_1_filters.push_back(intent_filter_b->Clone());
  proxy()->SetSupportedLinksPreference(kAppId1, std::move(app_1_filters));

  IntentFilters app_2_filters;
  app_2_filters.push_back(intent_filter_c->Clone());
  proxy()->SetSupportedLinksPreference(kAppId2, std::move(app_2_filters));

  EXPECT_TRUE(pub.AppHasSupportedLinksPreference(kAppId1));
  EXPECT_TRUE(pub.AppHasSupportedLinksPreference(kAppId2));
  EXPECT_FALSE(pub.AppHasSupportedLinksPreference(kAppId3));

  EXPECT_EQ(kAppId1, sub.preferred_apps_list().FindPreferredAppForUrl(
                         GURL("https://www.a.com/")));
  EXPECT_EQ(kAppId1, sub.preferred_apps_list().FindPreferredAppForUrl(
                         GURL("https://www.b.com/")));
  EXPECT_EQ(kAppId2, sub.preferred_apps_list().FindPreferredAppForUrl(
                         GURL("https://www.c.com/")));

  // App 3 overlaps with both App 1 and 2. Both previous apps should have all
  // their supported link filters removed.
  IntentFilters app_3_filters;
  app_3_filters.push_back(intent_filter_b->Clone());
  app_3_filters.push_back(intent_filter_c->Clone());
  proxy()->SetSupportedLinksPreference(kAppId3, std::move(app_3_filters));

  EXPECT_FALSE(pub.AppHasSupportedLinksPreference(kAppId1));
  EXPECT_FALSE(pub.AppHasSupportedLinksPreference(kAppId2));
  EXPECT_TRUE(pub.AppHasSupportedLinksPreference(kAppId3));

  EXPECT_EQ(std::nullopt, sub.preferred_apps_list().FindPreferredAppForUrl(
                              GURL("https://www.a.com/")));
  EXPECT_EQ(kAppId3, sub.preferred_apps_list().FindPreferredAppForUrl(
                         GURL("https://www.b.com/")));
  EXPECT_EQ(kAppId3, sub.preferred_apps_list().FindPreferredAppForUrl(
                         GURL("https://www.c.com/")));

  // Setting App 3 as preferred again should not change anything.
  app_3_filters = std::vector<IntentFilterPtr>();
  app_3_filters.push_back(intent_filter_b->Clone());
  app_3_filters.push_back(intent_filter_c->Clone());
  proxy()->SetSupportedLinksPreference(kAppId3, std::move(app_3_filters));

  EXPECT_TRUE(pub.AppHasSupportedLinksPreference(kAppId3));
  EXPECT_EQ(kAppId3, sub.preferred_apps_list().FindPreferredAppForUrl(
                         GURL("https://www.c.com/")));

  proxy()->RemoveSupportedLinksPreference(kAppId3);

  EXPECT_FALSE(pub.AppHasSupportedLinksPreference(kAppId3));
  EXPECT_EQ(std::nullopt, sub.preferred_apps_list().FindPreferredAppForUrl(
                              GURL("https://www.c.com/")));
}

TEST_F(AppServiceProxyTest, LaunchCallback) {
  bool called_1 = false;
  bool called_2 = false;
  auto instance_id_1 = base::UnguessableToken::Create();
  auto instance_id_2 = base::UnguessableToken::Create();

  // If the instance is not created yet, the callback will be stored.
  {
    LaunchResult result_1;
    result_1.instance_ids.push_back(instance_id_1);
    proxy()->OnLaunched(
        base::BindOnce(
            [](bool* called, apps::LaunchResult&& launch_result) {
              *called = true;
            },
            &called_1),
        std::move(result_1));
  }
  EXPECT_EQ(proxy()->callback_list_.size(), 1U);
  EXPECT_FALSE(called_1);

  {
    LaunchResult result_2;
    result_2.instance_ids.push_back(instance_id_2);
    proxy()->OnLaunched(
        base::BindOnce(
            [](bool* called, apps::LaunchResult&& launch_result) {
              *called = true;
            },
            &called_2),
        std::move(result_2));
  }
  EXPECT_EQ(proxy()->callback_list_.size(), 2U);
  EXPECT_FALSE(called_2);

  // Once the instance is created, the callback will be called.
  {
    auto delta =
        std::make_unique<apps::Instance>("abc", instance_id_1, nullptr);
    proxy()->InstanceRegistry().OnInstance(std::move(delta));
  }
  EXPECT_EQ(proxy()->callback_list_.size(), 1U);
  EXPECT_TRUE(called_1);
  EXPECT_FALSE(called_2);

  // New callback with existing instance will be called immediately.
  called_1 = false;
  {
    LaunchResult result_3;
    proxy()->OnLaunched(
        base::BindOnce(
            [](bool* called, apps::LaunchResult&& launch_result) {
              *called = true;
            },
            &called_1),
        std::move(result_3));
  }
  EXPECT_EQ(proxy()->callback_list_.size(), 1U);
  EXPECT_TRUE(called_1);
  EXPECT_FALSE(called_2);

  // A launch that results in multiple instances.
  auto instance_id_3 = base::UnguessableToken::Create();
  auto instance_id_4 = base::UnguessableToken::Create();
  bool called_multi = false;
  {
    LaunchResult result_multi;
    result_multi.instance_ids.push_back(instance_id_3);
    result_multi.instance_ids.push_back(instance_id_4);
    proxy()->OnLaunched(
        base::BindOnce(
            [](bool* called, apps::LaunchResult&& launch_result) {
              *called = true;
            },
            &called_multi),
        std::move(result_multi));
  }
  EXPECT_EQ(proxy()->callback_list_.size(), 2U);
  EXPECT_FALSE(called_multi);
  proxy()->InstanceRegistry().OnInstance(
      std::make_unique<apps::Instance>("foo", instance_id_3, nullptr));
  proxy()->InstanceRegistry().OnInstance(
      std::make_unique<apps::Instance>("bar", instance_id_4, nullptr));
  EXPECT_EQ(proxy()->callback_list_.size(), 1U);

  EXPECT_TRUE(called_multi);
}

TEST_F(AppServiceProxyTest, GetAppsForIntentBestHandler) {
  const char kAppId1[] = "abcdefg";
  const GURL kTestUrl = GURL("https://www.example.com/");

  std::vector<AppPtr> apps;
  // A scheme-only filter that will be excluded by the |exclude_browsers|
  // parameter.
  AppPtr app = std::make_unique<App>(AppType::kWeb, kAppId1);
  app->readiness = Readiness::kReady;
  app->handles_intents = true;
  auto intent_filter = std::make_unique<apps::IntentFilter>();
  intent_filter->AddSingleValueCondition(apps::ConditionType::kScheme,
                                         kTestUrl.scheme(),
                                         apps::PatternMatchType::kLiteral);
  intent_filter->activity_name = "name 1";
  intent_filter->activity_label = "same label";
  app->intent_filters.push_back(std::move(intent_filter));

  // A regular mime type file filter which we expect to match.
  auto intent_filter2 = std::make_unique<apps::IntentFilter>();
  intent_filter2->AddSingleValueCondition(apps::ConditionType::kAction,
                                          apps_util::kIntentActionView,
                                          apps::PatternMatchType::kLiteral);
  intent_filter2->AddSingleValueCondition(apps::ConditionType::kFile,
                                          "text/plain",
                                          apps::PatternMatchType::kMimeType);
  intent_filter2->activity_name = "name 2";
  intent_filter2->activity_label = "same label";
  app->intent_filters.push_back(std::move(intent_filter2));

  apps.push_back(std::move(app));
  proxy()->OnApps(std::move(apps), AppType::kWeb, false);

  std::vector<apps::IntentFilePtr> files;
  auto file = std::make_unique<apps::IntentFile>(GURL("abc.txt"));
  file->mime_type = "text/plain";
  file->is_directory = false;
  files.push_back(std::move(file));
  apps::IntentPtr intent = std::make_unique<apps::Intent>(
      apps_util::kIntentActionView, std::move(files));

  std::vector<apps::IntentLaunchInfo> intent_launch_info =
      proxy()->GetAppsForIntent(intent, /*exclude_browsers=*/true);

  // Check that we actually get back the 2nd filter, and not the excluded
  // scheme-only filter which should have been discarded.
  EXPECT_EQ(1U, intent_launch_info.size());
  EXPECT_EQ("name 2", intent_launch_info[0].activity_name);
}

#endif  // BUILDFLAG(IS_CHROMEOS_ASH)
}  // namespace apps