chromium/components/webapps/browser/android/webapk/webapk_proto_builder_unittest.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 "components/webapps/browser/android/webapk/webapk_proto_builder.h"

#include "base/files/file_path.h"
#include "base/run_loop.h"
#include "base/test/scoped_feature_list.h"
#include "components/webapk/webapk.pb.h"
#include "components/webapps/browser/android/shortcut_info.h"
#include "components/webapps/browser/android/webapp_icon.h"
#include "components/webapps/browser/features.h"
#include "content/public/test/browser_task_environment.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/skia/include/core/SkColor.h"

namespace {

const base::FilePath::CharType kTestDataDir[] =
    FILE_PATH_LITERAL("components/test/data/webapps");

// The URLs of best icons from Web Manifest. We use a random file in the test
// data directory. Since WebApkInstaller does not try to decode the file as an
// image it is OK that the file is not an image.
const char* kBestPrimaryIconUrl = "/simple.html";
const char* kBestSplashIconUrl = "/nostore.html";
const char* kBestShortcutIconUrl = "/title1.html";

const char* kUnusedIconPath = "https://example.com/unused_icon.png";

std::unique_ptr<webapps::WebappIcon> BuildTestWebApkIcon(
    GURL icon_url,
    std::string data,
    std::string hash,
    std::set<webapk::Image::Usage> usages) {
  auto icon = std::make_unique<webapps::WebappIcon>(icon_url);
  icon->SetData(std::move(data));
  icon->set_hash(std::move(hash));
  for (const auto& usage : usages) {
    icon->AddUsage(usage);
  }
  return icon;
}

}  // namespace

// Builds WebApk proto and blocks till done.
class BuildProtoRunner {
 public:
  BuildProtoRunner() {}

  BuildProtoRunner(const BuildProtoRunner&) = delete;
  BuildProtoRunner& operator=(const BuildProtoRunner&) = delete;

  ~BuildProtoRunner() {}

  void BuildSync(
      const GURL& best_primary_icon_url,
      const GURL& splash_image_url,
      std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons,
      std::unique_ptr<webapps::WebappIcon> primary_icon,
      std::unique_ptr<webapps::WebappIcon> splash_icon,
      const GURL& manifest_id,
      const GURL& app_key,
      const std::optional<SkColor>& dark_theme_color,
      const std::optional<SkColor>& dark_background_color,
      bool is_manifest_stale,
      bool is_app_identity_update_supported,
      const std::vector<GURL>& best_shortcut_icon_urls) {
    webapps::ShortcutInfo info{GURL()};
    info.manifest_id = manifest_id;
    info.dark_theme_color = dark_theme_color;
    info.dark_background_color = dark_background_color;
    info.best_primary_icon_url = best_primary_icon_url;
    info.splash_image_url = splash_image_url;
    if (!best_primary_icon_url.is_empty())
      info.icon_urls.push_back(best_primary_icon_url.spec());
    if (!splash_image_url.is_empty())
      info.icon_urls.push_back(splash_image_url.spec());
    info.icon_urls.push_back(kUnusedIconPath);

    for (const GURL& shortcut_url : best_shortcut_icon_urls) {
      info.best_shortcut_icon_urls.push_back(shortcut_url);
      info.shortcut_items.emplace_back();
      info.shortcut_items.back().icons.emplace_back();
      info.shortcut_items.back().icons.back().src = shortcut_url;
    }

    webapps::BuildProto(info, app_key, std::move(primary_icon),
                        std::move(splash_icon), "" /* package_name */,
                        "" /* version */, std::move(webapk_icons),
                        is_manifest_stale, is_app_identity_update_supported,
                        base::BindOnce(&BuildProtoRunner::OnBuiltWebApkProto,
                                       base::Unretained(this)));

    run_loop_.Run();
  }

  // Helper function to build proto without |primary_icon|, similar to install
  // new WebAPKs. All icon information should come from |webapk_icons|
  void BuildForIconInstallTestSync(
      const GURL& icon_url,
      std::vector<std::string> icon_urls,
      std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons) {
    webapps::ShortcutInfo info{GURL()};
    info.best_primary_icon_url = icon_url;
    info.icon_urls = std::move(icon_urls);

    webapps::BuildProto(info, GURL() /* app_key */, nullptr /* primary_icon */,
                        nullptr /* splash_icon */, "" /* package_name */,
                        "" /* version */, std::move(webapk_icons),
                        false /* is_manifest_stale */,
                        false /* is_app_identity_update_supported */,
                        base::BindOnce(&BuildProtoRunner::OnBuiltWebApkProto,
                                       base::Unretained(this)));

    run_loop_.Run();
  }

  // Helper function to build proto with |primary_icon|, similar to update
  // existing WebAPKs.
  void BuildForIconUpdateTestSync(
      const GURL& icon_url,
      std::vector<std::string> icon_urls,
      std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons,
      const std::string& icon_data,
      const std::string& icon_hash) {
    webapps::ShortcutInfo info{GURL()};
    info.best_primary_icon_url = icon_url;
    info.icon_urls = std::move(icon_urls);

    auto primary_icon = BuildTestWebApkIcon(icon_url, icon_data, icon_hash,
                                            {webapk::Image::PRIMARY_ICON});

    webapps::BuildProto(info, GURL() /* app_key */, std::move(primary_icon),
                        nullptr /* splash_icon */, "" /* package_name */,
                        "" /* version */, std::move(webapk_icons),
                        false /* is_manifest_stale */,
                        false /* is_app_identity_update_supported */,
                        base::BindOnce(&BuildProtoRunner::OnBuiltWebApkProto,
                                       base::Unretained(this)));
    run_loop_.Run();
  }

  webapk::WebApk* GetWebApkRequest() { return webapk_request_.get(); }

 private:
  // Called when the |webapk_request_| is populated.
  void OnBuiltWebApkProto(std::unique_ptr<std::string> serialized_proto) {
    webapk_request_ = std::make_unique<webapk::WebApk>();
    webapk_request_->ParseFromString(*serialized_proto);
    run_loop_.QuitClosure().Run();
  }

  // The populated webapk::WebApk.
  std::unique_ptr<webapk::WebApk> webapk_request_;

  // Called after the |webapk_request_| is built.
  base::RunLoop run_loop_;
};

class WebApkProtoBuilderTest : public ::testing::Test {
 public:
  std::unique_ptr<BuildProtoRunner> CreateBuildProtoRunner() {
    return std::make_unique<BuildProtoRunner>();
  }

  WebApkProtoBuilderTest()
      : task_environment_(content::BrowserTaskEnvironment::IO_MAINLOOP) {}

  WebApkProtoBuilderTest(const WebApkProtoBuilderTest&) = delete;
  WebApkProtoBuilderTest& operator=(const WebApkProtoBuilderTest&) = delete;

  ~WebApkProtoBuilderTest() override {}

  void SetUp() override {
    test_server_.AddDefaultHandlers(base::FilePath(kTestDataDir));
    ASSERT_TRUE(test_server_.Start());
  }

  void TearDown() override { base::RunLoop().RunUntilIdle(); }

  net::test_server::EmbeddedTestServer* test_server() { return &test_server_; }

 private:
  net::EmbeddedTestServer test_server_;

  content::BrowserTaskEnvironment task_environment_;
};

// When there is no Web Manifest available for a site, an empty
// |best_primary_icon_url| and an empty |splash_image_url| is used to build a
// WebApk update request. Tests the request can be built properly.
TEST_F(WebApkProtoBuilderTest, BuildWebApkProtoWhenManifestIsObsolete) {
  GURL icon_url_1 = test_server()->GetURL("/icon1.png");
  GURL icon_url_2 = test_server()->GetURL("/icon2.png");
  std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
  webapk_icons.emplace(icon_url_1,
                       BuildTestWebApkIcon(icon_url_1, "data1", "1",
                                           {webapk::Image::PRIMARY_ICON}));
  webapk_icons.emplace(icon_url_2,
                       BuildTestWebApkIcon(icon_url_2, "data2", "2",
                                           {webapk::Image::SPLASH_ICON}));

  auto primary_icon = BuildTestWebApkIcon(GURL(), "data3", std::string(),
                                          {webapk::Image::PRIMARY_ICON});
  auto splash_icon = BuildTestWebApkIcon(GURL(), "data4", std::string(),
                                         {webapk::Image::SPLASH_ICON});

  std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
  runner->BuildSync(
      GURL(), GURL(), std::move(webapk_icons), std::move(primary_icon),
      std::move(splash_icon), GURL() /*manifest_id*/, GURL() /*app_key*/,
      0x000000 /*dark_theme_color*/, 0x000000 /*dark_background_color*/,
      true /* is_manifest_stale */, true /* is_app_identity_update_supported */,
      {});
  webapk::WebApk* webapk_request = runner->GetWebApkRequest();
  ASSERT_NE(nullptr, webapk_request);

  webapk::WebAppManifest manifest = webapk_request->manifest();
  ASSERT_EQ(3, manifest.icons_size());

  EXPECT_EQ("", manifest.icons(0).src());
  EXPECT_FALSE(manifest.icons(0).has_hash());
  EXPECT_EQ(manifest.icons(0).image_data(), "data3");

  EXPECT_EQ("", manifest.icons(1).src());
  EXPECT_FALSE(manifest.icons(1).has_hash());
  EXPECT_EQ(manifest.icons(1).image_data(), "data4");

  EXPECT_EQ(kUnusedIconPath, manifest.icons(2).src());
  EXPECT_FALSE(manifest.icons(2).has_hash());
  EXPECT_FALSE(manifest.icons(2).has_image_data());
}

// Tests a WebApk install or update request is built properly when the Chrome
// knows the best icon URL of a site after fetching its Web Manifest.
TEST_F(WebApkProtoBuilderTest, BuildWebApkProtoWhenManifestIsAvailable) {
  GURL icon_url_1 = test_server()->GetURL("/icon.png");
  GURL best_primary_icon_url = test_server()->GetURL(kBestPrimaryIconUrl);
  GURL best_splash_icon_url = test_server()->GetURL(kBestSplashIconUrl);
  GURL best_shortcut_icon_url = test_server()->GetURL(kBestShortcutIconUrl);

  std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
  webapk_icons.emplace(icon_url_1,
                       BuildTestWebApkIcon(icon_url_1, "data0", "0", {}));
  webapk_icons.emplace(best_primary_icon_url,
                       BuildTestWebApkIcon(best_primary_icon_url, "data1", "1",
                                           {webapk::Image::PRIMARY_ICON}));
  webapk_icons.emplace(best_splash_icon_url,
                       BuildTestWebApkIcon(best_splash_icon_url, "data2", "2",
                                           {webapk::Image::SPLASH_ICON}));
  webapk_icons.emplace(best_shortcut_icon_url,
                       BuildTestWebApkIcon(best_shortcut_icon_url, "data3", "3",
                                           {webapk::Image::SHORTCUT_ICON}));

  std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
  runner->BuildSync(
      best_primary_icon_url, best_splash_icon_url, std::move(webapk_icons),
      nullptr /* primary_icon */, nullptr /*splash_icon*/,
      GURL() /*manifest_id*/, GURL() /*app_key*/, 0x000000 /*dark_theme_color*/,
      0x000000 /*dark_background_color*/, false /* is_manifest_stale*/,
      false /* is_app_identity_update_supported */, {best_shortcut_icon_url});
  webapk::WebApk* webapk_request = runner->GetWebApkRequest();
  ASSERT_NE(nullptr, webapk_request);

  webapk::WebAppManifest manifest = webapk_request->manifest();
  ASSERT_EQ(3, manifest.icons_size());

  // Check protobuf fields for kBestPrimaryIconUrl.
  EXPECT_EQ(best_primary_icon_url.spec(), manifest.icons(0).src());
  EXPECT_EQ(manifest.icons(0).hash(), "1");
  EXPECT_EQ(manifest.icons(0).image_data(), "data1");
  EXPECT_THAT(manifest.icons(0).usages(),
              testing::ElementsAre(webapk::Image::PRIMARY_ICON));

  // Check protobuf fields for kBestSplashIconUrl.
  EXPECT_EQ(best_splash_icon_url.spec(), manifest.icons(1).src());
  EXPECT_EQ(manifest.icons(1).hash(), "2");
  EXPECT_EQ(manifest.icons(1).image_data(), "data2");
  EXPECT_THAT(manifest.icons(1).usages(),
              testing::ElementsAre(webapk::Image::SPLASH_ICON));

  // Check protobuf fields for unused icon.
  EXPECT_EQ(kUnusedIconPath, manifest.icons(2).src());
  EXPECT_FALSE(manifest.icons(2).has_hash());
  EXPECT_FALSE(manifest.icons(2).has_image_data());

  // Check shortcut fields.
  ASSERT_EQ(manifest.shortcuts_size(), 1);
  ASSERT_EQ(manifest.shortcuts(0).icons_size(), 1);
  EXPECT_EQ(manifest.shortcuts(0).icons(0).src(),
            best_shortcut_icon_url.spec());
  EXPECT_EQ(manifest.shortcuts(0).icons(0).hash(), "3");
  EXPECT_EQ(manifest.shortcuts(0).icons(0).image_data(), "data3");
}

// Tests a WebApk install or update request is built properly when the Chrome
// knows the best icon URL of a site after fetching its Web Manifest, and
// primary icon and splash icon share the same URL.
TEST_F(WebApkProtoBuilderTest,
       BuildWebApkProtoPrimaryIconAndSplashIconSameUrl) {
  GURL icon_url_1 = test_server()->GetURL("/icon.png");
  GURL best_icon_url = test_server()->GetURL(kBestPrimaryIconUrl);
  std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
  webapk_icons.emplace(icon_url_1,
                       BuildTestWebApkIcon(icon_url_1, "data1", "1", {}));
  webapk_icons.emplace(best_icon_url,
                       BuildTestWebApkIcon(best_icon_url, "data0", "0",
                                           {webapk::Image::PRIMARY_ICON,
                                            webapk::Image::SPLASH_ICON,
                                            webapk::Image::SHORTCUT_ICON}));

  std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
  runner->BuildSync(
      GURL(best_icon_url), GURL(best_icon_url), std::move(webapk_icons),
      nullptr /* primary_icon */, nullptr /*splash_icon*/,
      GURL() /*manifest_id*/, GURL() /*app_key*/, 0x000000 /*dark_theme_color*/,
      0x000000 /*dark_background_color*/, false /* is_manifest_stale*/,
      false /* is_app_identity_update_supported */, {GURL(best_icon_url)});
  webapk::WebApk* webapk_request = runner->GetWebApkRequest();
  ASSERT_NE(nullptr, webapk_request);

  webapk::WebAppManifest manifest = webapk_request->manifest();
  ASSERT_EQ(2, manifest.icons_size());

  // Check protobuf fields for icons.
  EXPECT_EQ(best_icon_url, manifest.icons(0).src());
  EXPECT_EQ(manifest.icons(0).hash(), "0");
  EXPECT_EQ(manifest.icons(0).image_data(), "data0");
  EXPECT_THAT(manifest.icons(0).usages(),
              testing::UnorderedElementsAre(webapk::Image::PRIMARY_ICON,
                                            webapk::Image::SPLASH_ICON,
                                            webapk::Image::SHORTCUT_ICON));

  // Check protobuf fields for unused icon.
  EXPECT_EQ(kUnusedIconPath, manifest.icons(1).src());
  EXPECT_FALSE(manifest.icons(1).has_hash());
  EXPECT_FALSE(manifest.icons(1).has_image_data());

  // Check shortcut fields.
  ASSERT_EQ(manifest.shortcuts_size(), 1);
  ASSERT_EQ(manifest.shortcuts(0).icons_size(), 1);
  EXPECT_EQ(manifest.shortcuts(0).icons(0).src(), best_icon_url);
  EXPECT_EQ(manifest.shortcuts(0).icons(0).hash(), "0");
  EXPECT_EQ(manifest.shortcuts(0).icons(0).image_data(), "data0");
}

TEST_F(WebApkProtoBuilderTest, BuildWebApkProtoWhenWithMultipleShortcuts) {
  GURL best_shortcut_icon_url1 = test_server()->GetURL(kBestShortcutIconUrl);
  GURL best_shortcut_icon_url2 = test_server()->GetURL(kBestPrimaryIconUrl);
  std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
  webapk_icons.emplace(
      best_shortcut_icon_url1,
      BuildTestWebApkIcon(best_shortcut_icon_url1, "data1", "1",
                          {webapk::Image::SHORTCUT_ICON}));
  webapk_icons.emplace(
      best_shortcut_icon_url2,
      BuildTestWebApkIcon(best_shortcut_icon_url2, "data2", "2",
                          {webapk::Image::SHORTCUT_ICON}));

  std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
  runner->BuildSync(
      GURL(), GURL(), std::move(webapk_icons), nullptr /* primary_icon */,
      nullptr /*splash_icon*/, GURL() /*manifest_id*/, GURL() /*app_key*/,
      0x000000 /*dark_theme_color*/, 0x000000 /*dark_background_color*/,
      false /* is_manifest_stale*/,
      false /* is_app_identity_update_supported */,
      {GURL(best_shortcut_icon_url1), GURL(best_shortcut_icon_url2)});
  webapk::WebApk* webapk_request = runner->GetWebApkRequest();
  ASSERT_NE(nullptr, webapk_request);

  webapk::WebAppManifest manifest = webapk_request->manifest();
  ASSERT_EQ(manifest.shortcuts_size(), 2);

  // Check shortcut fields.
  ASSERT_EQ(manifest.shortcuts(0).icons_size(), 1);
  EXPECT_EQ(manifest.shortcuts(0).icons(0).src(), best_shortcut_icon_url1);
  EXPECT_EQ(manifest.shortcuts(0).icons(0).hash(), "1");
  EXPECT_EQ(manifest.shortcuts(0).icons(0).image_data(), "data1");

  ASSERT_EQ(manifest.shortcuts(1).icons_size(), 1);
  EXPECT_EQ(manifest.shortcuts(1).icons(0).src(), best_shortcut_icon_url2);
  EXPECT_EQ(manifest.shortcuts(1).icons(0).hash(), "2");
  EXPECT_EQ(manifest.shortcuts(1).icons(0).image_data(), "data2");
}

TEST_F(WebApkProtoBuilderTest,
       BuildWebApkProtoWhenWithMultipleShortcutsAndSameIcons) {
  GURL best_shortcut_icon_url = test_server()->GetURL(kBestShortcutIconUrl);
  std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
  webapk_icons.emplace(best_shortcut_icon_url,
                       BuildTestWebApkIcon(best_shortcut_icon_url, "data1", "1",
                                           {webapk::Image::SHORTCUT_ICON}));

  std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
  runner->BuildSync(
      GURL(), GURL(), std::move(webapk_icons), nullptr /* primary_icon */,
      nullptr /*splash_icon*/, GURL() /*manifest_id*/, GURL() /*app_key*/,
      0x000000 /*dark_theme_color*/, 0x000000 /*dark_background_color*/,
      false /* is_manifest_stale*/,
      false /* is_app_identity_update_supported */,
      {GURL(best_shortcut_icon_url), GURL(best_shortcut_icon_url)});
  webapk::WebApk* webapk_request = runner->GetWebApkRequest();
  ASSERT_NE(nullptr, webapk_request);

  webapk::WebAppManifest manifest = webapk_request->manifest();
  ASSERT_EQ(manifest.shortcuts_size(), 2);

  // Check shortcut fields.
  ASSERT_EQ(manifest.shortcuts(0).icons_size(), 1);
  EXPECT_EQ(manifest.shortcuts(0).icons(0).src(), best_shortcut_icon_url);
  EXPECT_EQ(manifest.shortcuts(0).icons(0).hash(), "1");
  EXPECT_EQ(manifest.shortcuts(0).icons(0).image_data(), "data1");

  ASSERT_EQ(manifest.shortcuts(1).icons_size(), 1);
  EXPECT_EQ(manifest.shortcuts(1).icons(0).src(), best_shortcut_icon_url);
  EXPECT_EQ(manifest.shortcuts(1).icons(0).hash(), "1");
  // This is a duplicate icon, so the data won't be included again.
  EXPECT_EQ(manifest.shortcuts(1).icons(0).image_data(), "");
}

TEST_F(WebApkProtoBuilderTest,
       BuildWebApkProtoSplashIconAndShortcutIconSameUrl) {
  GURL icon_url_1 = test_server()->GetURL("/icon.png");
  GURL best_icon_url = test_server()->GetURL(kBestPrimaryIconUrl);
  std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
  webapk_icons.emplace(icon_url_1,
                       BuildTestWebApkIcon(icon_url_1, "data1", "1",
                                           {webapk::Image::PRIMARY_ICON}));
  webapk_icons.emplace(best_icon_url,
                       BuildTestWebApkIcon(best_icon_url, "data0", "0",
                                           {webapk::Image::SPLASH_ICON}));

  std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
  runner->BuildSync(
      GURL(icon_url_1), GURL(best_icon_url), std::move(webapk_icons),
      nullptr /* primary_icon */, nullptr /*splash_icon*/,
      GURL() /*manifest_id*/, GURL() /*app_key*/, 0x000000 /*dark_theme_color*/,
      0x000000 /*dark_background_color*/, false /* is_manifest_stale*/,
      true /* is_app_identity_update_supported */, {GURL(best_icon_url)});
  webapk::WebApk* webapk_request = runner->GetWebApkRequest();
  ASSERT_NE(nullptr, webapk_request);

  webapk::WebAppManifest manifest = webapk_request->manifest();
  ASSERT_EQ(3, manifest.icons_size());
  ASSERT_EQ(manifest.shortcuts_size(), 1);

  // Check primary icon fields.
  EXPECT_EQ(icon_url_1, manifest.icons(0).src());
  EXPECT_EQ(manifest.icons(0).hash(), "1");
  EXPECT_EQ(manifest.icons(0).image_data(), "data1");
  EXPECT_THAT(manifest.icons(0).usages(),
              testing::ElementsAre(webapk::Image::PRIMARY_ICON));

  // Check splash icon fields
  EXPECT_EQ(best_icon_url, manifest.icons(1).src());
  EXPECT_EQ(manifest.icons(1).hash(), "0");
  EXPECT_EQ(manifest.icons(1).image_data(), "data0");
  EXPECT_THAT(manifest.icons(1).usages(),
              testing::ElementsAre(webapk::Image::SPLASH_ICON));

  // Check protobuf fields for unused icon.
  EXPECT_EQ(kUnusedIconPath, manifest.icons(2).src());
  EXPECT_FALSE(manifest.icons(2).has_hash());
  EXPECT_FALSE(manifest.icons(2).has_image_data());

  // Check shortcut fields.
  ASSERT_EQ(manifest.shortcuts_size(), 1);
  ASSERT_EQ(manifest.shortcuts(0).icons_size(), 1);
  EXPECT_EQ(manifest.shortcuts(0).icons(0).src(), best_icon_url);
  EXPECT_EQ(manifest.shortcuts(0).icons(0).hash(), "0");
  EXPECT_TRUE(manifest.shortcuts(0).icons(0).has_image_data());
  EXPECT_EQ(manifest.shortcuts(0).icons(0).image_data(), "data0");
}

TEST_F(WebApkProtoBuilderTest, BuildWebApkProtoManifestIdAndKey) {
  GURL manifest_id_1 = test_server()->GetURL("/test_id");
  GURL app_key_1 = test_server()->GetURL("/test_key");

  std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
  std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
  runner->BuildSync(GURL(), GURL(), std::move(webapk_icons),
                    nullptr /* primary_icon */, nullptr /*splash_icon*/,
                    manifest_id_1, app_key_1, 0x000000 /*dark_theme_color*/,
                    0x000000 /*dark_background_color*/,
                    false /* is_manifest_stale*/,
                    false /* is_app_identity_update_supported */, {});
  webapk::WebApk* webapk_request = runner->GetWebApkRequest();
  ASSERT_NE(nullptr, webapk_request);

  EXPECT_EQ(webapk_request->app_key(), app_key_1.spec());
  EXPECT_EQ(webapk_request->manifest().id(), manifest_id_1.spec());
}

TEST_F(WebApkProtoBuilderTest, MapContainsEmptyIconUrl) {
  std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
  webapk_icons.emplace(GURL(""),
                       BuildTestWebApkIcon(GURL(""), "data", "0", {}));

  auto primary_icon =
      BuildTestWebApkIcon(GURL(), "primary_icon_data", std::string(),
                          {webapk::Image::PRIMARY_ICON});
  auto splash_icon = BuildTestWebApkIcon(
      GURL(), "splash_icon_data", std::string(), {webapk::Image::SPLASH_ICON});

  std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
  runner->BuildSync(
      GURL() /* primary_icon_url */, GURL() /*splash_icon_url*/,
      std::move(webapk_icons), std::move(primary_icon), std::move(splash_icon),
      GURL() /*manifest_id*/, GURL() /*app_key*/, 0x000000 /*dark_theme_color*/,
      0x000000 /*dark_background_color*/, false /* is_manifest_stale*/,
      false /* is_app_identity_update_supported */, {});
  webapk::WebApk* webapk_request = runner->GetWebApkRequest();
  ASSERT_NE(nullptr, webapk_request);

  webapk::WebAppManifest manifest = webapk_request->manifest();
  ASSERT_EQ(3, manifest.icons_size());

  // Checks the request contains primary icon and splash icon with no url and
  // hash.
  EXPECT_EQ(manifest.icons(0).src(), "");
  EXPECT_FALSE(manifest.icons(0).has_hash());
  EXPECT_EQ(manifest.icons(0).image_data(), "primary_icon_data");
  EXPECT_THAT(manifest.icons(0).usages(),
              testing::ElementsAre(webapk::Image::PRIMARY_ICON));

  EXPECT_EQ(manifest.icons(1).src(), "");
  EXPECT_FALSE(manifest.icons(0).has_hash());
  EXPECT_EQ(manifest.icons(1).image_data(), "splash_icon_data");
  EXPECT_THAT(manifest.icons(1).usages(),
              testing::ElementsAre(webapk::Image::SPLASH_ICON));
}

TEST_F(WebApkProtoBuilderTest, EmptyPrimaryIconUrlValidSplashIcon) {
  GURL splash_icon_url = test_server()->GetURL(kBestSplashIconUrl);
  std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
  webapk_icons.emplace(splash_icon_url,
                       BuildTestWebApkIcon(splash_icon_url, "data2", "2",
                                           {webapk::Image::SPLASH_ICON}));

  auto primary_icon =
      BuildTestWebApkIcon(GURL(), "primary_icon_data", std::string(),
                          {webapk::Image::PRIMARY_ICON});
  auto splash_icon =
      BuildTestWebApkIcon(splash_icon_url, "splash_icon_data",
                          "splash_icon_hash", {webapk::Image::SPLASH_ICON});

  std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
  runner->BuildSync(
      GURL(), GURL(splash_icon_url), std::move(webapk_icons),
      std::move(primary_icon), std::move(splash_icon), GURL() /*manifest_id*/,
      GURL() /*app_key*/, 0x000000 /*dark_theme_color*/,
      0x000000 /*dark_background_color*/, false /* is_manifest_stale*/,
      false /* is_app_identity_update_supported */, {});
  webapk::WebApk* webapk_request = runner->GetWebApkRequest();
  ASSERT_NE(nullptr, webapk_request);

  webapk::WebAppManifest manifest = webapk_request->manifest();
  ASSERT_EQ(3, manifest.icons_size());

  // Check protobuf fields for icons.
  EXPECT_EQ(manifest.icons(0).src(), "");
  EXPECT_FALSE(manifest.icons(0).has_hash());
  EXPECT_EQ(manifest.icons(0).image_data(), "primary_icon_data");
  EXPECT_THAT(manifest.icons(0).usages(),
              testing::ElementsAre(webapk::Image::PRIMARY_ICON));

  EXPECT_EQ(manifest.icons(1).src(), splash_icon_url);
  EXPECT_EQ(manifest.icons(1).hash(), "splash_icon_hash");
  EXPECT_EQ(manifest.icons(1).image_data(), "splash_icon_data");
  EXPECT_THAT(manifest.icons(1).usages(),
              testing::ElementsAre(webapk::Image::SPLASH_ICON));
}

TEST_F(WebApkProtoBuilderTest, IconWithoutUrl_Install) {
  // Test primary icon with empty URL.
  std::vector<std::string> icon_urls = {kUnusedIconPath};
  std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
  webapk_icons.emplace(GURL(), BuildTestWebApkIcon(GURL(), "data", "hash", {}));
  webapk_icons.emplace(
      GURL(kUnusedIconPath),
      BuildTestWebApkIcon(GURL(kUnusedIconPath), "data2", "2", {}));

  std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
  runner->BuildForIconInstallTestSync(GURL(), icon_urls,
                                      std::move(webapk_icons));

  webapk::WebApk* webapk_request = runner->GetWebApkRequest();
  ASSERT_NE(nullptr, webapk_request);
  webapk::WebAppManifest manifest = webapk_request->manifest();
  ASSERT_EQ(1, manifest.icons_size());
  // The dummy unused icon has src and hash but no data
  EXPECT_TRUE(manifest.icons(0).has_src());
  EXPECT_TRUE(manifest.icons(0).has_hash());
}

TEST_F(WebApkProtoBuilderTest, IconWithoutUrl_Update) {
  // Test primary icon with empty URL.
  std::vector<std::string> icon_urls = {kUnusedIconPath};
  std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
  webapk_icons.emplace(GURL(), BuildTestWebApkIcon(GURL(), "data", "hash", {}));
  webapk_icons.emplace(
      GURL(kUnusedIconPath),
      BuildTestWebApkIcon(GURL(kUnusedIconPath), "data2", "2", {}));

  std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
  runner->BuildForIconUpdateTestSync(GURL(), icon_urls, std::move(webapk_icons),
                                     "icon_data", "icon_hash");

  webapk::WebApk* webapk_request = runner->GetWebApkRequest();
  ASSERT_NE(nullptr, webapk_request);
  webapk::WebAppManifest manifest = webapk_request->manifest();
  ASSERT_EQ(2, manifest.icons_size());
  // Icon has data and hash but no src.
  EXPECT_FALSE(manifest.icons(0).has_src());
  EXPECT_EQ(manifest.icons(0).hash(), "icon_hash");
  EXPECT_EQ(manifest.icons(0).image_data(), "icon_data");
  EXPECT_THAT(manifest.icons(0).usages(),
              testing::ElementsAre(webapk::Image::PRIMARY_ICON));
  // The dummy unused icon has src and hash but no data
  EXPECT_TRUE(manifest.icons(1).has_src());
  EXPECT_TRUE(manifest.icons(1).has_hash());
}

TEST_F(WebApkProtoBuilderTest, IconUrlNotInListAndNotInHash_Install) {
  // Test primary icon URL is NOT in list and NOT in hashmap
  GURL test_icon_url = test_server()->GetURL(kBestPrimaryIconUrl);
  std::vector<std::string> icon_urls = {kUnusedIconPath};
  std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
  webapk_icons.emplace(
      GURL(kUnusedIconPath),
      BuildTestWebApkIcon(GURL(kUnusedIconPath), "data1", "1", {}));

  std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
  runner->BuildForIconInstallTestSync(test_icon_url, icon_urls,
                                      std::move(webapk_icons));

  webapk::WebApk* webapk_request = runner->GetWebApkRequest();
  ASSERT_NE(nullptr, webapk_request);
  webapk::WebAppManifest manifest = webapk_request->manifest();
  ASSERT_EQ(1, manifest.icons_size());
  // The dummy unused icon has src and hash but no data
  EXPECT_TRUE(manifest.icons(0).has_src());
  EXPECT_TRUE(manifest.icons(0).has_hash());
}

TEST_F(WebApkProtoBuilderTest, IconUrlNotInListAndNotInHash_Update) {
  // Test primary icon has URL but NOT in list and NOT in hashmap
  GURL test_icon_url = test_server()->GetURL(kBestPrimaryIconUrl);
  std::vector<std::string> icon_urls = {kUnusedIconPath};
  std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
  webapk_icons.emplace(
      GURL(kUnusedIconPath),
      BuildTestWebApkIcon(GURL(kUnusedIconPath), "data1", "1", {}));

  std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
  runner->BuildForIconUpdateTestSync(test_icon_url, icon_urls,
                                     std::move(webapk_icons), "icon_data",
                                     "icon_hash");

  webapk::WebApk* webapk_request = runner->GetWebApkRequest();
  ASSERT_NE(nullptr, webapk_request);
  webapk::WebAppManifest manifest = webapk_request->manifest();
  ASSERT_EQ(2, manifest.icons_size());
  // Icon has data, src and hash
  EXPECT_EQ(manifest.icons(0).src(), test_icon_url.spec());
  EXPECT_EQ(manifest.icons(0).hash(), "icon_hash");
  EXPECT_EQ(manifest.icons(0).image_data(), "icon_data");
  EXPECT_THAT(manifest.icons(0).usages(),
              testing::ElementsAre(webapk::Image::PRIMARY_ICON));
  // The dummy unused icon has src and hash but no data
  EXPECT_TRUE(manifest.icons(1).has_src());
  EXPECT_TRUE(manifest.icons(1).has_hash());
}

TEST_F(WebApkProtoBuilderTest, IconUrlInListNotInHash_Install) {
  // Test primary icon has URL in the urls list but NOT in hashmap
  GURL test_icon_url = test_server()->GetURL(kBestPrimaryIconUrl);
  std::vector<std::string> icon_urls = {test_icon_url.spec()};
  std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
  webapk_icons.emplace(
      GURL(kUnusedIconPath),
      BuildTestWebApkIcon(GURL(kUnusedIconPath), "data2", "hash2", {}));

  std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
  runner->BuildForIconInstallTestSync(test_icon_url, icon_urls,
                                      std::move(webapk_icons));

  webapk::WebApk* webapk_request = runner->GetWebApkRequest();
  ASSERT_NE(nullptr, webapk_request);
  webapk::WebAppManifest manifest = webapk_request->manifest();
  // No icon added because either primary icon or list icons in hash.
  ASSERT_EQ(0, manifest.icons_size());
}

TEST_F(WebApkProtoBuilderTest, IconUrlInListNotInHash_Update) {
  // Test primary icon has URL in the urls list but NOT in hashmap
  GURL test_icon_url = test_server()->GetURL(kBestPrimaryIconUrl);
  std::vector<std::string> icon_urls = {test_icon_url.spec()};
  std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
  webapk_icons.emplace(
      GURL(kUnusedIconPath),
      BuildTestWebApkIcon(GURL(kUnusedIconPath), "data2", "hash2", {}));

  std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
  runner->BuildForIconUpdateTestSync(test_icon_url, icon_urls,
                                     std::move(webapk_icons), "icon_data",
                                     "icon_hash");

  webapk::WebApk* webapk_request = runner->GetWebApkRequest();
  ASSERT_NE(nullptr, webapk_request);
  webapk::WebAppManifest manifest = webapk_request->manifest();
  ASSERT_EQ(1, manifest.icons_size());
  // Icon has data, src and hash
  EXPECT_EQ(manifest.icons(0).src(), test_icon_url.spec());
  EXPECT_EQ(manifest.icons(0).hash(), "icon_hash");
  EXPECT_EQ(manifest.icons(0).image_data(), "icon_data");
  EXPECT_THAT(manifest.icons(0).usages(),
              testing::ElementsAre(webapk::Image::PRIMARY_ICON));
}

TEST_F(WebApkProtoBuilderTest, IconUrlNotInListInHash_Install) {
  // Test primary icon has URL NOT in the urls list but in the hashmap
  GURL test_icon_url = test_server()->GetURL(kBestPrimaryIconUrl);
  std::vector<std::string> icon_urls = {};
  std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
  webapk_icons.emplace(test_icon_url,
                       BuildTestWebApkIcon(test_icon_url, "data3", "hash3",
                                           {webapk::Image::PRIMARY_ICON}));
  webapk_icons.emplace(
      GURL(kUnusedIconPath),
      BuildTestWebApkIcon(GURL(kUnusedIconPath), "data2", "hash2", {}));

  std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
  runner->BuildForIconInstallTestSync(test_icon_url, icon_urls,
                                      std::move(webapk_icons));

  webapk::WebApk* webapk_request = runner->GetWebApkRequest();
  ASSERT_NE(nullptr, webapk_request);
  webapk::WebAppManifest manifest = webapk_request->manifest();
  ASSERT_EQ(1, manifest.icons_size());
  // Primary icon has data, src and hash
  EXPECT_EQ(manifest.icons(0).src(), test_icon_url.spec());
  EXPECT_EQ(manifest.icons(0).hash(), "hash3");
  EXPECT_EQ(manifest.icons(0).image_data(), "data3");
  EXPECT_THAT(manifest.icons(0).usages(),
              testing::ElementsAre(webapk::Image::PRIMARY_ICON));
}

TEST_F(WebApkProtoBuilderTest, IconUrlNotInListInHash_Update) {
  // Test primary icon has URL NOT in the urls list but in the hashmap
  GURL test_icon_url = test_server()->GetURL(kBestPrimaryIconUrl);
  std::vector<std::string> icon_urls = {};
  std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
  webapk_icons.emplace(test_icon_url,
                       BuildTestWebApkIcon(test_icon_url, "data3", "hash3",
                                           {webapk::Image::PRIMARY_ICON}));

  std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
  runner->BuildForIconUpdateTestSync(test_icon_url, icon_urls,
                                     std::move(webapk_icons), "icon_data",
                                     "icon_hash");

  webapk::WebApk* webapk_request = runner->GetWebApkRequest();
  ASSERT_NE(nullptr, webapk_request);
  webapk::WebAppManifest manifest = webapk_request->manifest();
  ASSERT_EQ(1, manifest.icons_size());
  // Icon has data, src and hash
  EXPECT_EQ(manifest.icons(0).src(), test_icon_url.spec());
  EXPECT_EQ(manifest.icons(0).hash(), "icon_hash");
  EXPECT_EQ(manifest.icons(0).image_data(), "icon_data");
  EXPECT_THAT(manifest.icons(0).usages(),
              testing::ElementsAre(webapk::Image::PRIMARY_ICON));
}

TEST_F(WebApkProtoBuilderTest, IconUrlInListAndHash_Install) {
  // Test primary icon has URL in list and hashmap
  GURL test_icon_url = test_server()->GetURL(kBestPrimaryIconUrl);
  std::vector<std::string> icon_urls = {test_icon_url.spec()};
  std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
  webapk_icons.emplace(test_icon_url,
                       BuildTestWebApkIcon(test_icon_url, "data4", "hash4",
                                           {webapk::Image::PRIMARY_ICON}));

  std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
  runner->BuildForIconInstallTestSync(test_icon_url, icon_urls,
                                      std::move(webapk_icons));

  webapk::WebApk* webapk_request = runner->GetWebApkRequest();
  ASSERT_NE(nullptr, webapk_request);
  webapk::WebAppManifest manifest = webapk_request->manifest();
  ASSERT_EQ(1, manifest.icons_size());
  // Icon has data, src and hash
  EXPECT_EQ(manifest.icons(0).src(), test_icon_url.spec());
  EXPECT_EQ(manifest.icons(0).hash(), "hash4");
  EXPECT_EQ(manifest.icons(0).image_data(), "data4");
  EXPECT_THAT(manifest.icons(0).usages(),
              testing::ElementsAre(webapk::Image::PRIMARY_ICON));
}

TEST_F(WebApkProtoBuilderTest, IconUrlInListAndHash_Update) {
  // Test primary icon has URL in list and hashmap
  GURL test_icon_url = test_server()->GetURL(kBestPrimaryIconUrl);
  std::vector<std::string> icon_urls = {test_icon_url.spec()};
  std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
  webapk_icons.emplace(test_icon_url,
                       BuildTestWebApkIcon(test_icon_url, "data4", "hash4",
                                           {webapk::Image::PRIMARY_ICON}));

  std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
  runner->BuildForIconUpdateTestSync(test_icon_url, icon_urls,
                                     std::move(webapk_icons), "icon_data",
                                     "icon_hash");

  webapk::WebApk* webapk_request = runner->GetWebApkRequest();
  ASSERT_NE(nullptr, webapk_request);
  webapk::WebAppManifest manifest = webapk_request->manifest();
  ASSERT_EQ(1, manifest.icons_size());
  // Icon has data, src and hash
  EXPECT_EQ(manifest.icons(0).src(), test_icon_url.spec());
  EXPECT_EQ(manifest.icons(0).hash(), "icon_hash");
  EXPECT_EQ(manifest.icons(0).image_data(), "icon_data");
  EXPECT_THAT(manifest.icons(0).usages(),
              testing::ElementsAre(webapk::Image::PRIMARY_ICON));
}

TEST_F(WebApkProtoBuilderTest, BuildWebApkProtoDarkThemeAndBackground) {
  std::optional<SkColor> dark_theme_color = 0x000000;
  std::optional<SkColor> dark_background_color = 0x888888;
  std::string dark_theme_color_expected = "rgba(0,0,0,0)";
  std::string dark_background_color_expected = "rgba(136,136,136,0)";

  std::map<GURL, std::unique_ptr<webapps::WebappIcon>> webapk_icons;
  std::unique_ptr<BuildProtoRunner> runner = CreateBuildProtoRunner();
  runner->BuildSync(
      GURL(), GURL(), std::move(webapk_icons), nullptr /* primary_icon */,
      nullptr /*splash_icon*/, GURL() /*manifest_id*/, GURL() /*app_key*/,
      dark_theme_color, dark_background_color, false /* is_manifest_stale*/,
      false /* is_app_identity_update_supported */, {});
  webapk::WebApk* webapk_request = runner->GetWebApkRequest();
  ASSERT_NE(nullptr, webapk_request);

  webapk::WebAppManifest manifest = webapk_request->manifest();
  EXPECT_EQ(manifest.dark_theme_color(), dark_theme_color_expected);
  EXPECT_EQ(manifest.dark_background_color(), dark_background_color_expected);
}