chromium/chrome/browser/apps/app_service/publishers/guest_os_apps_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/apps/app_service/publishers/guest_os_apps.h"

#include <memory>
#include <vector>

#include "base/test/simple_test_clock.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/ash/bruschetta/bruschetta_util.h"
#include "chrome/browser/ash/guest_os/guest_os_registry_service_factory.h"
#include "chrome/browser/ash/guest_os/public/types.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_registry_cache.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/services/app_service/public/cpp/app_update.h"
#include "components/services/app_service/public/cpp/intent_util.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace apps {

namespace {

// An app publisher (in the App Service sense) that inherits GuestOSApps and
// implements the necessary virtual functions.
class TestPublisher : public GuestOSApps {
 public:
  explicit TestPublisher(AppServiceProxy* proxy) : GuestOSApps(proxy) {}

 private:
  bool CouldBeAllowed() const override { return true; }

  apps::AppType AppType() const override { return AppType::kBruschetta; }
  guest_os::VmType VmType() const override {
    return guest_os::VmType::BRUSCHETTA;
  }

  // apps::AppPublisher overrides.
  void Launch(const std::string& app_id,
              int32_t event_flags,
              LaunchSource launch_source,
              WindowInfoPtr window_info) override {}

  void CreateAppOverrides(
      const guest_os::GuestOsRegistryService::Registration& registration,
      App* app) override {
    app->name = "override_name";
  }
};

}  // namespace

class GuestOSAppsTest : public testing::Test {
 public:
  GuestOSAppsTest()
      : task_environment_(content::BrowserTaskEnvironment::REAL_IO_THREAD) {}

  TestingProfile* profile() { return profile_.get(); }

  AppServiceProxy* app_service_proxy() { return app_service_proxy_; }

  guest_os::GuestOsRegistryService* registry() { return registry_.get(); }

  void SetUp() override {
    profile_ = std::make_unique<TestingProfile>();
    app_service_proxy_ = AppServiceProxyFactory::GetForProfile(profile_.get());
    web_app::test::AwaitStartWebAppProviderAndSubsystems(profile_.get());
    registry_ =
        guest_os::GuestOsRegistryServiceFactory::GetForProfile(profile_.get());
    registry_->SetClockForTesting(&test_clock_);
    publisher_ = std::make_unique<TestPublisher>(app_service_proxy());
    publisher_->InitializeForTesting();
  }

  void TearDown() override {
    // TestPublisher must be torn down before TestingProfile because the latter
    // destroys the GuestOsRegistryService.
    publisher_.reset();
    profile_.reset();
  }

  // Adds the app to the registry and returns its app_id.
  std::string AddApp(const vm_tools::apps::App& app) {
    // Update the ApplicationList, this calls through GuestOSApps::CreateApp.
    vm_tools::apps::ApplicationList app_list;
    app_list.set_vm_name(bruschetta::kBruschettaVmName);
    app_list.set_container_name("test_container");
    *app_list.add_apps() = app;
    registry()->UpdateApplicationList(app_list);
    return registry()->GenerateAppId(app.desktop_file_id(),
                                     bruschetta::kBruschettaVmName,
                                     app_list.container_name());
  }

  // Adds an app with the specified mime_types and returns its app_id.
  std::string AddAppWithMimeTypes(const std::string& desktop_file_id,
                                  const std::string& app_name,
                                  const std::vector<std::string>& mime_types) {
    vm_tools::apps::App app;
    for (std::string mime_type : mime_types) {
      app.add_mime_types(mime_type);
    }
    app.set_desktop_file_id(desktop_file_id);
    vm_tools::apps::App::LocaleString::Entry* entry =
        app.mutable_name()->add_values();
    entry->set_locale(std::string());
    entry->set_value(app_name);
    return AddApp(app);
  }

  // Get the registered intent filters for the given app_id.
  std::vector<std::unique_ptr<IntentFilter>> GetIntentFiltersForApp(
      const std::string& app_id) {
    std::vector<std::unique_ptr<IntentFilter>> intent_filters;
    app_service_proxy()->AppRegistryCache().ForOneApp(
        app_id, [&intent_filters](const AppUpdate& update) {
          for (auto& intent_filter : update.IntentFilters()) {
            intent_filters.push_back(std::move(intent_filter));
          }
        });
    return intent_filters;
  }

  void UpdateMimeTypes(std::string container_name,
                       std::string extension,
                       std::string mime_type) {
    vm_tools::apps::MimeTypes mime_types_list;
    mime_types_list.set_vm_name(bruschetta::kBruschettaVmName);
    mime_types_list.set_container_name(container_name);
    (*mime_types_list.mutable_mime_type_mappings())[extension] = mime_type;
    auto* mime_types_service =
        guest_os::GuestOsMimeTypesServiceFactory::GetForProfile(profile());
    mime_types_service->UpdateMimeTypes(mime_types_list);
    task_environment_.RunUntilIdle();
  }

  base::SimpleTestClock& test_clock() { return test_clock_; }

 private:
  content::BrowserTaskEnvironment task_environment_;
  std::unique_ptr<TestingProfile> profile_;
  raw_ptr<AppServiceProxy, DanglingUntriaged> app_service_proxy_ = nullptr;
  raw_ptr<guest_os::GuestOsRegistryService, DanglingUntriaged> registry_ =
      nullptr;
  std::unique_ptr<TestPublisher> publisher_;
  base::SimpleTestClock test_clock_;
};

TEST_F(GuestOSAppsTest, CreateApp) {
  // Create a test app.
  vm_tools::apps::App app;
  app.add_mime_types("text/plain");
  app.set_desktop_file_id("desktop_file_id");
  vm_tools::apps::App::LocaleString::Entry* entry =
      app.mutable_name()->add_values();
  entry->set_value("app_name");
  const std::string app_id = AddApp(app);

  // Get the AppUpdate from the registry and check its contents.
  bool seen = false;
  app_service_proxy()->AppRegistryCache().ForOneApp(
      app_id, [&seen](const AppUpdate& update) {
        seen = true;
        EXPECT_EQ(update.AppType(), AppType::kBruschetta);
        EXPECT_EQ(update.Readiness(), Readiness::kReady);
        EXPECT_EQ(update.Name(), "override_name")
            << "CreateAppOverrides should have changed the name.";
        EXPECT_EQ(update.InstallReason(), InstallReason::kUser);
        EXPECT_EQ(update.InstallSource(), InstallSource::kUnknown);
        EXPECT_TRUE(update.IconKey().has_value());
        EXPECT_TRUE(update.ShowInLauncher());
        EXPECT_TRUE(update.ShowInSearch());
        EXPECT_TRUE(update.ShowInShelf());
        EXPECT_EQ(bruschetta::kBruschettaVmName,
                  *update.Extra()->FindString("vm_name"));
        EXPECT_EQ("test_container",
                  *update.Extra()->FindString("container_name"));
        EXPECT_EQ("desktop_file_id",
                  *update.Extra()->FindString("desktop_file_id"));
        EXPECT_EQ("", *update.Extra()->FindString("exec"));
        EXPECT_EQ("", *update.Extra()->FindString("executable_file_name"));
        EXPECT_FALSE(update.Extra()->FindBool("no_display").value());
        EXPECT_FALSE(update.Extra()->FindBool("terminal").value());
        EXPECT_FALSE(update.Extra()->FindBool("scaled").value());
        EXPECT_EQ("", *update.Extra()->FindString("package_id"));
        EXPECT_EQ("", *update.Extra()->FindString("startup_wm_class"));
        EXPECT_FALSE(update.Extra()->FindBool("startup_notify").value());
      });
  EXPECT_TRUE(seen) << "Couldn't find test app in registry.";
}

TEST_F(GuestOSAppsTest, OnAppLastLaunchTimeUpdated) {
  // Create a test app.
  vm_tools::apps::App app;
  app.add_mime_types("text/plain");
  app.set_desktop_file_id("desktop_file_id");
  vm_tools::apps::App::LocaleString::Entry* entry =
      app.mutable_name()->add_values();
  entry->set_value("app_name");
  const std::string app_id = AddApp(app);

  // Get the AppUpdate from the registry and check its contents.
  app_service_proxy()->AppRegistryCache().ForOneApp(
      app_id, [](const AppUpdate& update) {
        EXPECT_EQ(update.LastLaunchTime(), base::Time());
      });

  test_clock().Advance(base::Hours(1));
  registry()->AppLaunched(app_id);

  // Get LastLaunchTime from the registry and check its contents.
  app_service_proxy()->AppRegistryCache().ForOneApp(
      app_id, [](const AppUpdate& update) {
        EXPECT_EQ(update.LastLaunchTime(), base::Time() + base::Hours(1));
      });
}

TEST_F(GuestOSAppsTest, AppServiceHasIntentFilters) {
  const std::vector<std::string> mime_types = {"text/csv", "text/html"};
  const std::string app_id =
      AddAppWithMimeTypes("desktop_file_id", "app_name", mime_types);
  const std::vector<std::unique_ptr<IntentFilter>> intent_filters =
      GetIntentFiltersForApp(app_id);

  ASSERT_EQ(intent_filters.size(), 1U);
  EXPECT_EQ(intent_filters[0]->conditions.size(), 2U);

  // Check that the filter has the correct action type.
  {
    const Condition* condition = intent_filters[0]->conditions[0].get();
    ASSERT_EQ(condition->condition_type, ConditionType::kAction);
    EXPECT_EQ(condition->condition_values.size(), 1U);
    ASSERT_EQ(condition->condition_values[0]->match_type,
              PatternMatchType::kLiteral);
    ASSERT_EQ(condition->condition_values[0]->value,
              apps_util::kIntentActionView);
  }

  // Check that the filter has all our mime types. Realistically, the filter
  // would also have the extension equivalents of the mime types too if there
  // were mime/ extension mappings in prefs.
  {
    const Condition* condition = intent_filters[0]->conditions[1].get();
    ASSERT_EQ(condition->condition_type, ConditionType::kFile);
    EXPECT_EQ(condition->condition_values.size(), 2U);
    ASSERT_EQ(condition->condition_values[0]->value, mime_types[0]);
    ASSERT_EQ(condition->condition_values[1]->value, mime_types[1]);
  }
}

TEST_F(GuestOSAppsTest, IntentFilterWithTextPlainAddsTextWildcardMimeType) {
  {
    const std::string normal_app_id = AddAppWithMimeTypes(
        "normal_desktop_file_id", "normal_app_name", {"text/csv"});
    const std::vector<std::unique_ptr<IntentFilter>> intent_filters =
        GetIntentFiltersForApp(normal_app_id);

    EXPECT_EQ(intent_filters.size(), 1U);
    EXPECT_EQ(intent_filters[0]->conditions.size(), 2U);

    // Check that the filter is unchanged because there isn't a "text/plain"
    // mime type condition.
    const Condition* condition = intent_filters[0]->conditions[1].get();
    ASSERT_EQ(condition->condition_type, ConditionType::kFile);
    EXPECT_EQ(condition->condition_values.size(), 1U);
    ASSERT_EQ(condition->condition_values[0]->value, "text/csv");
  }

  {
    const std::string many_mimes_app_id = AddAppWithMimeTypes(
        "many_mimes_desktop_file_id", "many_mimes_app_name",
        {"text/plain", "text/csv", "text/javascript", "video/mp4"});
    const std::vector<std::unique_ptr<IntentFilter>> intent_filters =
        GetIntentFiltersForApp(many_mimes_app_id);

    EXPECT_EQ(intent_filters.size(), 1U);
    EXPECT_EQ(intent_filters[0]->conditions.size(), 2U);

    // Check that the filter has "text/*" to replace all the text mime types.
    const Condition* condition = intent_filters[0]->conditions[1].get();
    ASSERT_EQ(condition->condition_type, ConditionType::kFile);
    EXPECT_EQ(condition->condition_values.size(), 2U);
    ASSERT_EQ(condition->condition_values[0]->value, "video/mp4");
    ASSERT_EQ(condition->condition_values[1]->value, "text/*");
  }
}

TEST_F(GuestOSAppsTest, IntentFilterHasExtensionsFromPrefs) {
  const std::string mime_type = "test/mime1";
  const std::string extension = "test_extension";
  UpdateMimeTypes("test_container", extension, mime_type);

  // Create app and get its registered intent filters.
  const std::string app_id =
      AddAppWithMimeTypes("desktop_file_id", "app_name", {mime_type});
  std::vector<std::unique_ptr<IntentFilter>> intent_filters =
      GetIntentFiltersForApp(app_id);

  ASSERT_EQ(intent_filters.size(), 1U);
  std::unique_ptr<IntentFilter> intent_filter = std::move(intent_filters[0]);
  ASSERT_EQ(intent_filter->conditions.size(), 2U);

  // Check that the filter has the correct action type.
  {
    const Condition* condition = intent_filter->conditions[0].get();
    EXPECT_EQ(condition->condition_type, ConditionType::kAction);
    ASSERT_EQ(condition->condition_values.size(), 1U);
    EXPECT_EQ(condition->condition_values[0]->match_type,
              PatternMatchType::kLiteral);
    EXPECT_EQ(condition->condition_values[0]->value,
              apps_util::kIntentActionView);
  }

  // Check that the filter has both mime type and its extension equivalent based
  // on what is recorded in prefs for GuestOS mime types.
  {
    const Condition* condition = intent_filter->conditions[1].get();
    EXPECT_EQ(condition->condition_type, ConditionType::kFile);
    ASSERT_EQ(condition->condition_values.size(), 2U);
    EXPECT_EQ(condition->condition_values[0]->value, mime_type);
    EXPECT_EQ(condition->condition_values[0]->match_type,
              PatternMatchType::kMimeType);
    EXPECT_EQ(condition->condition_values[1]->value, extension);
    EXPECT_EQ(condition->condition_values[1]->match_type,
              PatternMatchType::kFileExtension);
  }
}

}  // namespace apps