chromium/chrome/browser/ash/guest_os/guest_os_registry_service_icon_browsertest.cc

// 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 "base/memory/raw_ptr.h"
#include "chrome/browser/ash/guest_os/guest_os_registry_service.h"

#include <stddef.h>

#include "base/files/file_path_watcher.h"
#include "base/files/file_util.h"
#include "base/task/thread_pool.h"
#include "base/test/bind.h"
#include "base/threading/thread_restrictions.h"
#include "chrome/browser/ash/crostini/crostini_test_helper.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/testing_profile.h"
#include "chromeos/ash/components/dbus/cicerone/cicerone_service.pb.h"
#include "chromeos/ash/components/dbus/cicerone/fake_cicerone_client.h"
#include "chromeos/ash/components/dbus/concierge/fake_concierge_client.h"
#include "chromeos/ash/components/dbus/dbus_thread_manager.h"
#include "chromeos/ash/components/dbus/seneschal/seneschal_client.h"
#include "content/public/test/browser_test.h"

using vm_tools::apps::ApplicationList;

namespace guest_os {

class GuestOsRegistryServiceIconTest : public InProcessBrowserTest {
 public:
  void SetUpInProcessBrowserTestFixture() override {
    InProcessBrowserTest::SetUpInProcessBrowserTestFixture();
    ash::CiceroneClient::InitializeFake();
    ash::ConciergeClient::InitializeFake();
    ash::SeneschalClient::InitializeFake();
    fake_cicerone_client_ = ash::FakeCiceroneClient::Get();
  }

  void TearDownInProcessBrowserTestFixture() override {
    service_.reset();
    ash::SeneschalClient::Shutdown();
    ash::ConciergeClient::Shutdown();
    ash::CiceroneClient::Shutdown();
    InProcessBrowserTest::TearDownInProcessBrowserTestFixture();
  }

  guest_os::GuestOsRegistryService* service() {
    if (!service_) {
      service_ = std::make_unique<GuestOsRegistryService>(browser()->profile());
    }
    return service_.get();
  }

  void LoadIconAndValidateNoWait(const std::string& app_id,
                                 bool expect_loaded,
                                 base::OnceClosure done_closure) {
    service()->LoadIcon(
        app_id, apps::IconKey(), apps::IconType::kCompressed,
        /*size_hint_in_dip=*/1, /*allow_placeholder_icon=*/false,
        /*fallback_resource_id=*/0,
        base::BindOnce(
            [](bool expect_loaded, base::OnceClosure done_closure,
               apps::IconValuePtr icon) {
              ASSERT_NE(nullptr, icon.get());
              if (expect_loaded) {
                EXPECT_FALSE(icon->is_placeholder_icon);
                EXPECT_EQ(apps::IconType::kCompressed, icon->icon_type);
                EXPECT_GT(icon->compressed.size(), 0u);
              } else {
                EXPECT_EQ(apps::IconType::kUnknown, icon->icon_type);
              }
              std::move(done_closure).Run();
            },
            expect_loaded, std::move(done_closure)));
  }

  void LoadIconAndValidate(const std::string& app_id, bool expect_loaded) {
    base::RunLoop run_loop;
    LoadIconAndValidateNoWait(app_id, expect_loaded, run_loop.QuitClosure());
    run_loop.Run();
    if (expect_loaded) {
      ExpectIconFiles(app_id);
    }
  }

  void ExpectIconFiles(const std::string& app_id) {
    base::ScopedAllowBlockingForTesting allow_blocking;
    base::FilePath icon_dir =
        browser()->profile()->GetPath().Append("crostini.icons").Append(app_id);

    EXPECT_TRUE(base::PathExists(icon_dir.Append("icon.svg")));

    std::string icon_svg_data;

    EXPECT_TRUE(
        base::ReadFileToString(icon_dir.Append("icon.svg"), &icon_svg_data));
    EXPECT_EQ(kSvgData, icon_svg_data);

    // There should also be a transcoded .png file.
    EXPECT_TRUE(base::PathExists(icon_dir.Append("icon_100p.png")));
  }

  std::string AddApp() {
    ApplicationList crostini_list;
    crostini_list.set_vm_type(VmType::TERMINA);
    crostini_list.set_vm_name("termina");
    crostini_list.set_container_name("penguin");
    *crostini_list.add_apps() =
        crostini::CrostiniTestHelper::BasicApp(kSvgAppName);
    std::string app_id = crostini::CrostiniTestHelper::GenerateAppId(
        kSvgAppName, "termina", "penguin");
    service()->UpdateApplicationList(crostini_list);

    vm_tools::cicerone::ContainerAppIconResponse response;
    auto* icon = response.add_icons();
    icon->set_desktop_file_id(app_id);
    icon->set_format(vm_tools::cicerone::DesktopIcon::SVG);
    icon->set_icon(kSvgData);
    fake_cicerone_client_->set_container_app_icon_response(response);

    return app_id;
  }

  void RemoveApps() {
    ApplicationList crostini_list;
    crostini_list.set_vm_type(VmType::TERMINA);
    crostini_list.set_vm_name("termina");
    crostini_list.set_container_name("penguin");
    service()->UpdateApplicationList(crostini_list);
  }

 protected:
  raw_ptr<ash::FakeCiceroneClient, DanglingUntriaged> fake_cicerone_client_;
  static constexpr char kSvgData[] =
      "<svg width='20px' height='20px' viewBox='0 0 24 24' "
      "fill='rgb(95,99,104)' "
      "xmlns='http://www.w3.org/2000/svg'><path d='M0 0h24v24H0V0z' "
      "fill='none'/><path d='M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 "
      "2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 "
      "6.41V10h2V3h-7z'/></svg>";

  static constexpr char kSvgAppName[] = "app_with_svg_icon";

 private:
  std::unique_ptr<GuestOsRegistryService> service_;
};

IN_PROC_BROWSER_TEST_F(GuestOsRegistryServiceIconTest, LoadIconOnce) {
  LoadIconAndValidate(AddApp(), true);
}

IN_PROC_BROWSER_TEST_F(GuestOsRegistryServiceIconTest, LoadIconTwice) {
  std::string app_id = AddApp();
  base::RunLoop run_loop1, run_loop2;
  LoadIconAndValidateNoWait(app_id, true, run_loop1.QuitClosure());
  LoadIconAndValidateNoWait(app_id, true, run_loop2.QuitClosure());
  run_loop1.Run();
  run_loop2.Run();
  ExpectIconFiles(app_id);
}

IN_PROC_BROWSER_TEST_F(GuestOsRegistryServiceIconTest, AddRemoveAddAppIcon) {
  base::ScopedAllowBlockingForTesting allow_blocking;
  std::string app_id = crostini::CrostiniTestHelper::GenerateAppId(
      kSvgAppName, "termina", "penguin");
  base::FilePath icon_dir =
      browser()->profile()->GetPath().Append("crostini.icons").Append(app_id);

  // Initially, there's no app icon.
  EXPECT_FALSE(base::PathExists(icon_dir));
  LoadIconAndValidate(app_id, false);

  // Add the app. There should be an icon.
  AddApp();
  LoadIconAndValidate(app_id, true);

  // RemoveApps, and the icon should go away. We need a FilePathWatcher to know
  // when RemoveApps has finished deleting icons since it is done async.
  base::RunLoop remove_apps_loop;
  base::FilePathWatcher watcher;
  ASSERT_TRUE(watcher.Watch(
      icon_dir, base::FilePathWatcher::Type::kNonRecursive,
      base::BindLambdaForTesting([&](const base::FilePath& path, bool error) {
        if (!base::PathExists(icon_dir)) {
          remove_apps_loop.Quit();
        }
      })));
  RemoveApps();
  remove_apps_loop.Run();

  LoadIconAndValidate(app_id, false);

  // Add the app. Ensure 2nd fetch of icon from VM succeeds.
  AddApp();
  LoadIconAndValidate(app_id, true);
}

}  // namespace guest_os