chromium/chrome/browser/apps/app_service/app_install/web_app_installer_browsertest.cc

// Copyright 2024 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_install/web_app_installer.h"

#include "base/functional/bind.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/test_future.h"
#include "chrome/browser/apps/app_service/app_install/app_install_types.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/ui/browser.h"
#include "chrome/browser/web_applications/test/web_app_install_test_utils.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_app_registrar.h"
#include "chrome/test/base/in_process_browser_test.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/webapps/browser/install_result_code.h"
#include "content/public/test/browser_test.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"

namespace apps {

class WebAppInstallerBrowserTest : public InProcessBrowserTest {
 public:
  void SetUpOnMainThread() override {
    InProcessBrowserTest::SetUpOnMainThread();

    https_server_.RegisterRequestHandler(base::BindRepeating(
        &WebAppInstallerBrowserTest::HandleRequest, base::Unretained(this)));
    https_server_.AddDefaultHandlers(GetChromeTestDataDir());

    ASSERT_TRUE(https_server_.Start());

    // Icon URLs should remap to the test server.
    host_resolver()->AddRule("meltingpot.googleusercontent.com", "127.0.0.1");
  }

  std::unique_ptr<net::test_server::HttpResponse> HandleRequest(
      const net::test_server::HttpRequest& request) {
    if ((request.relative_url != "/manifest.json" &&
         request.relative_url != "/manifest2.json") ||
        manifest_.empty()) {
      return nullptr;
    }

    auto response = std::make_unique<net::test_server::BasicHttpResponse>();
    response->set_code(net::HTTP_OK);
    response->set_content_type("application/json");
    if (request.relative_url == "/manifest.json") {
      response->set_content(manifest_);

    } else {
      response->set_content(manifest2_);
    }
    return response;
  }

  std::string AddIconToManifest(const std::string& manifest_template) {
    GURL icon_url = https_server()->GetURL("meltingpot.googleusercontent.com",
                                           "/web_apps/blue-192.png");
    constexpr char kIconsBlock[] = R"([{
        "src": "$1",
        "sizes": "192x192",
        "type": "image/png"
      }])";
    std::string icon_value = base::ReplaceStringPlaceholders(
        kIconsBlock, {icon_url.spec()}, nullptr);
    return base::ReplaceStringPlaceholders(manifest_template, {icon_value},
                                           nullptr);
  }

  AppInstallData CreateInstallData(std::string_view name,
                                   std::string_view package_id,
                                   std::string_view original_manifest_url,
                                   std::string_view test_manifest_url) {
    AppInstallData data(PackageId::FromString(package_id).value());
    data.name = name;
    WebAppInstallData& web_app_data =
        data.app_type_data.emplace<WebAppInstallData>();
    web_app_data.original_manifest_url = GURL(original_manifest_url);
    web_app_data.proxied_manifest_url =
        https_server()->GetURL(test_manifest_url);
    web_app_data.document_url =
        web_app_data.original_manifest_url.GetWithEmptyPath();
    return data;
  }

  void VerifyAppInstalled(webapps::AppId app_id,
                          const std::string& app_name,
                          InstallReason expected_reason) {
    bool found = app_registry_cache().ForOneApp(
        app_id, [app_name, expected_reason](const AppUpdate& update) {
          EXPECT_EQ(update.Name(), app_name);
          EXPECT_EQ(update.InstallReason(), expected_reason);
        });
    ASSERT_TRUE(found);
  }

  void SetManifestResponse(std::string manifest) { manifest_ = manifest; }
  void SetManifest2Response(std::string manifest) { manifest2_ = manifest; }

  Profile* profile() { return browser()->profile(); }

  net::EmbeddedTestServer* https_server() { return &https_server_; }

  AppRegistryCache& app_registry_cache() {
    auto* proxy = AppServiceProxyFactory::GetForProfile(browser()->profile());
    return proxy->AppRegistryCache();
  }

 private:
  net::EmbeddedTestServer https_server_;
  std::string manifest_;
  std::string manifest2_;
};

IN_PROC_BROWSER_TEST_F(WebAppInstallerBrowserTest, InstallOneOemApp) {
  WebAppInstaller installer(profile());

  constexpr char kManifestTemplate[] = R"({
    "name": "Example App",
    "start_url": "/index.html",
    "scope": "/",
    "icons": $1
  })";
  SetManifestResponse(AddIconToManifest(kManifestTemplate));

  base::HistogramTester histograms;
  base::test::TestFuture<bool> result;
  installer.InstallApp(
      AppInstallSurface::kAppPreloadServiceOem,
      CreateInstallData("Example App", "web:https://www.example.com/index.html",
                        "https://www.example.com/manifest.json",
                        "/manifest.json"),
      result.GetCallback());
  ASSERT_TRUE(result.Get());

  auto app_id = web_app::GenerateAppId(
      std::nullopt, GURL("https://www.example.com/index.html"));
  VerifyAppInstalled(app_id, "Example App", InstallReason::kOem);

  histograms.ExpectBucketCount(
      "Apps.AppInstallService.WebAppInstaller.InstallResult",
      WebAppInstallResult::kSuccess, 1);
  histograms.ExpectBucketCount(
      "Apps.AppInstallService.WebAppInstaller.InstallResult."
      "AppPreloadServiceOem",
      WebAppInstallResult::kSuccess, 1);
  histograms.ExpectBucketCount(
      "Apps.AppInstallService.WebAppInstaller.CommandResultCode",
      webapps::InstallResultCode::kSuccessNewInstall, 1);
  histograms.ExpectBucketCount(
      "Apps.AppInstallService.WebAppInstaller.CommandResultCode."
      "AppPreloadServiceOem",
      webapps::InstallResultCode::kSuccessNewInstall, 1);
}

IN_PROC_BROWSER_TEST_F(WebAppInstallerBrowserTest, InstallOneDefaultApp) {
  WebAppInstaller installer(profile());

  constexpr char kManifestTemplate[] = R"({
    "name": "Example App",
    "start_url": "/index.html",
    "scope": "/",
    "icons": $1
  })";
  SetManifestResponse(AddIconToManifest(kManifestTemplate));

  base::HistogramTester histograms;
  base::test::TestFuture<bool> result;
  installer.InstallApp(
      AppInstallSurface::kAppPreloadServiceDefault,
      CreateInstallData("Example App", "web:https://www.example.com/index.html",
                        "https://www.example.com/manifest.json",
                        "/manifest.json"),
      result.GetCallback());
  ASSERT_TRUE(result.Get());

  auto app_id = web_app::GenerateAppId(
      std::nullopt, GURL("https://www.example.com/index.html"));
  VerifyAppInstalled(app_id, "Example App", InstallReason::kDefault);

  histograms.ExpectBucketCount(
      "Apps.AppInstallService.WebAppInstaller.InstallResult",
      WebAppInstallResult::kSuccess, 1);
  histograms.ExpectBucketCount(
      "Apps.AppInstallService.WebAppInstaller.InstallResult."
      "AppPreloadServiceDefault",
      WebAppInstallResult::kSuccess, 1);
  histograms.ExpectBucketCount(
      "Apps.AppInstallService.WebAppInstaller.CommandResultCode",
      webapps::InstallResultCode::kSuccessNewInstall, 1);
  histograms.ExpectBucketCount(
      "Apps.AppInstallService.WebAppInstaller.CommandResultCode."
      "AppPreloadServiceDefault",
      webapps::InstallResultCode::kSuccessNewInstall, 1);
}

IN_PROC_BROWSER_TEST_F(WebAppInstallerBrowserTest, InstallMultipleOemApps) {
  WebAppInstaller installer(profile());

  constexpr char kManifestTemplate[] = R"({
    "name": "Example App",
    "start_url": "/index.html",
    "scope": "/",
    "icons": $1
  })";
  constexpr char kManifestTemplate2[] = R"({
    "name": "Example App2",
    "start_url": "/index.html",
    "scope": "/",
    "icons": $1
  })";
  SetManifestResponse(AddIconToManifest(kManifestTemplate));
  SetManifest2Response(AddIconToManifest(kManifestTemplate2));

  base::HistogramTester histograms;
  base::test::TestFuture<bool> result;
  base::test::TestFuture<bool> result2;
  installer.InstallApp(
      AppInstallSurface::kAppPreloadServiceOem,
      CreateInstallData("Example App", "web:https://www.example.com/index.html",
                        "https://www.example.com/manifest.json",
                        "/manifest.json"),
      result.GetCallback());
  installer.InstallApp(
      AppInstallSurface::kAppPreloadServiceOem,
      CreateInstallData(
          "Example App2", "web:https://www.example2.com/index.html",
          "https://www.example2.com/manifest2.json", "/manifest2.json"),
      result2.GetCallback());
  ASSERT_TRUE(result.Get());
  ASSERT_TRUE(result2.Get());

  auto app_id = web_app::GenerateAppId(
      std::nullopt, GURL("https://www.example.com/index.html"));
  VerifyAppInstalled(app_id, "Example App", InstallReason::kOem);

  auto app_id2 = web_app::GenerateAppId(
      std::nullopt, GURL("https://www.example2.com/index.html"));
  VerifyAppInstalled(app_id2, "Example App2", InstallReason::kOem);

  histograms.ExpectBucketCount(
      "Apps.AppInstallService.WebAppInstaller.InstallResult",
      WebAppInstallResult::kSuccess, 2);
  histograms.ExpectBucketCount(
      "Apps.AppInstallService.WebAppInstaller.InstallResult."
      "AppPreloadServiceOem",
      WebAppInstallResult::kSuccess, 2);
  histograms.ExpectBucketCount(
      "Apps.AppInstallService.WebAppInstaller.CommandResultCode",
      webapps::InstallResultCode::kSuccessNewInstall, 2);
  histograms.ExpectBucketCount(
      "Apps.AppInstallService.WebAppInstaller.CommandResultCode."
      "AppPreloadServiceOem",
      webapps::InstallResultCode::kSuccessNewInstall, 2);
}

IN_PROC_BROWSER_TEST_F(WebAppInstallerBrowserTest, InstallWithManifestId) {
  WebAppInstaller installer(profile());

  SetManifestResponse(AddIconToManifest(R"({
    "id": "manifest_id",
    "name": "Example App",
    "start_url": "/index.html",
    "scope": "/",
    "icons": $1
  })"));

  base::test::TestFuture<bool> result;
  installer.InstallApp(
      AppInstallSurface::kAppPreloadServiceOem,
      CreateInstallData(
          "Example App", "web:https://www.example.com/manifest_id",
          "https://www.example.com/manifest.json", "/manifest.json"),
      result.GetCallback());
  ASSERT_TRUE(result.Get());

  // The generated app ID should take the manifest ID into account.
  auto app_id = web_app::GenerateAppId(
      "manifest_id", GURL("https://www.example.com/index.html"));
  ASSERT_TRUE(
      app_registry_cache().ForOneApp(app_id, [](const AppUpdate& update) {}));
}

// Reinstalling an existing user-installed app should not overwrite manifest
// data, but will add the OEM install reason.
IN_PROC_BROWSER_TEST_F(WebAppInstallerBrowserTest, InstallOverUserApp) {
  constexpr char kStartUrl[] = "https://www.example.com/";
  constexpr char kOriginalManifestUrl[] =
      "https://www.example.com/manifest.json";
  constexpr char kUserAppName[] = "User Installed App";

  WebAppInstaller installer(profile());

  auto app_id = web_app::test::InstallDummyWebApp(profile(), kUserAppName,
                                                  GURL(kStartUrl));

  SetManifestResponse(AddIconToManifest(R"({
    "name": "OEM Installed app",
    "start_url": "/",
    "icons": $1
  })"));

  base::test::TestFuture<bool> result;
  installer.InstallApp(
      AppInstallSurface::kAppPreloadServiceOem,
      CreateInstallData("OEM Installed app", base::StrCat({"web:", kStartUrl}),
                        kOriginalManifestUrl, "/manifest.json"),
      result.GetCallback());
  ASSERT_TRUE(result.Get());

  VerifyAppInstalled(app_id, kUserAppName, InstallReason::kOem);
}

// The manifest id in the proto does not match the calculated manifest id.
IN_PROC_BROWSER_TEST_F(WebAppInstallerBrowserTest,
                       InstallMismatchedDataManifestId) {
  WebAppInstaller installer(profile());

  SetManifestResponse(AddIconToManifest(R"({
    "name": "Example App",
    "start_url": "/index.html",
    "scope": "/",
    "icons": $1
  })"));

  base::HistogramTester histograms;
  base::test::TestFuture<bool> result;
  installer.InstallApp(
      AppInstallSurface::kAppPreloadServiceOem,
      CreateInstallData(
          "Example App", "web:https://www.example.com/manifest_id",
          "https://www.example.com/manifest.json", "/manifest.json"),
      result.GetCallback());
  ASSERT_FALSE(result.Get());

  auto app_id = web_app::GenerateAppId(
      std::nullopt, GURL("https://www.example.com/index.html"));
  bool found =
      app_registry_cache().ForOneApp(app_id, [](const AppUpdate& update) {});
  ASSERT_FALSE(found);

  histograms.ExpectBucketCount(
      "Apps.AppInstallService.WebAppInstaller.InstallResult",
      WebAppInstallResult::kWebAppInstallError, 1);
  histograms.ExpectBucketCount(
      "Apps.AppInstallService.WebAppInstaller.InstallResult."
      "AppPreloadServiceOem",
      WebAppInstallResult::kWebAppInstallError, 1);
  histograms.ExpectBucketCount(
      "Apps.AppInstallService.WebAppInstaller.CommandResultCode",
      webapps::InstallResultCode::kExpectedAppIdCheckFailed, 1);
  histograms.ExpectBucketCount(
      "Apps.AppInstallService.WebAppInstaller.CommandResultCode."
      "AppPreloadServiceOem",
      webapps::InstallResultCode::kExpectedAppIdCheckFailed, 1);
}

IN_PROC_BROWSER_TEST_F(WebAppInstallerBrowserTest, ManifestFileIsNotJSON) {
  WebAppInstaller installer(profile());

  SetManifestResponse("INVALID");

  base::test::TestFuture<bool> result;
  installer.InstallApp(
      AppInstallSurface::kAppPreloadServiceOem,
      CreateInstallData(
          "Example App", "web:https://www.example.com/manifest_id",
          "https://www.example.com/manifest.json", "/manifest.json"),
      result.GetCallback());
  ASSERT_FALSE(result.Get());

  auto app_id = web_app::GenerateAppId(
      std::nullopt, GURL("https://www.example.com/index.html"));
  bool found =
      app_registry_cache().ForOneApp(app_id, [](const AppUpdate& update) {});
  ASSERT_FALSE(found);
}

IN_PROC_BROWSER_TEST_F(WebAppInstallerBrowserTest,
                       ManifestFileIsHasMissingFields) {
  WebAppInstaller installer(profile());

  SetManifestResponse(R"({
    "is_valid": "no."
  })");

  base::HistogramTester histograms;
  base::test::TestFuture<bool> result;
  installer.InstallApp(
      AppInstallSurface::kAppPreloadServiceOem,
      CreateInstallData(
          "Example App", "web:https://www.example.com/manifest_id",
          "https://www.example.com/manifest.json", "/manifest.json"),
      result.GetCallback());
  ASSERT_FALSE(result.Get());

  auto app_id = web_app::GenerateAppId(
      std::nullopt, GURL("https://www.example.com/index.html"));
  bool found =
      app_registry_cache().ForOneApp(app_id, [](const AppUpdate& update) {});
  ASSERT_FALSE(found);

  histograms.ExpectBucketCount(
      "Apps.AppInstallService.WebAppInstaller.CommandResultCode",
      webapps::InstallResultCode::kNotValidManifestForWebApp, 1);
  histograms.ExpectBucketCount(
      "Apps.AppInstallService.WebAppInstaller.CommandResultCode."
      "AppPreloadServiceOem",
      webapps::InstallResultCode::kNotValidManifestForWebApp, 1);
}

IN_PROC_BROWSER_TEST_F(WebAppInstallerBrowserTest, ManifestWithFailingIcons) {
  WebAppInstaller installer(profile());

  constexpr char kManifestTemplate[] = R"({
    "name": "Example App",
    "start_url": "/index.html",
    "scope": "/",
    "icons": [{
      "src": "$1",
      "sizes": "96x96",
      "type": "image/png"
    }]
  })";

  // The image will fail to download, which will cause the installation to fail.
  GURL image_url =
      https_server()->GetURL("meltingpot.googleusercontent.com", "/404");

  SetManifestResponse(base::ReplaceStringPlaceholders(
      kManifestTemplate, {image_url.spec()}, nullptr));

  base::HistogramTester histograms;
  base::test::TestFuture<bool> result;
  installer.InstallApp(
      AppInstallSurface::kAppPreloadServiceOem,
      CreateInstallData(
          "Example App", "web:https://www.example.com/manifest_id",
          "https://www.example.com/manifest.json", "/manifest.json"),
      result.GetCallback());
  ASSERT_FALSE(result.Get());

  histograms.ExpectBucketCount(
      "Apps.AppInstallService.WebAppInstaller.CommandResultCode",
      webapps::InstallResultCode::kIconDownloadingFailed, 1);
  histograms.ExpectBucketCount(
      "Apps.AppInstallService.WebAppInstaller.CommandResultCode."
      "AppPreloadServiceOem",
      webapps::InstallResultCode::kIconDownloadingFailed, 1);
}

IN_PROC_BROWSER_TEST_F(WebAppInstallerBrowserTest, InstallWebsite) {
  WebAppInstaller installer(profile());
  SetManifestResponse(AddIconToManifest(R"({
    "name": "Example App",
    "start_url": "/",
    "icons": $1
  })"));

  AppInstallData data = CreateInstallData(
      "Example App", "website:https://www.example.com/",
      "https://www.example.com/manifest.json", "/manifest.json");
  // Unset user_window_override to request UserDisplayMode::kBrowser.
  absl::get<WebAppInstallData>(data.app_type_data).open_as_window = false;

  base::test::TestFuture<bool> result;
  installer.InstallApp(AppInstallSurface::kAppInstallUriUnknown, data,
                       result.GetCallback());
  ASSERT_TRUE(result.Get());

  // Verify that the app is set to open in a browser in App Service.
  auto app_id =
      web_app::GenerateAppId(std::nullopt, GURL("https://www.example.com/"));
  bool found =
      app_registry_cache().ForOneApp(app_id, [](const AppUpdate& update) {
        EXPECT_EQ(update.WindowMode(), apps::WindowMode::kBrowser);
      });
  ASSERT_TRUE(found);

  EXPECT_TRUE(web_app::WebAppProvider::GetForWebApps(profile())
                  ->registrar_unsafe()
                  .IsDiyApp(app_id));
}

IN_PROC_BROWSER_TEST_F(WebAppInstallerBrowserTest,
                       InstallWebsiteWithOpenInWindowOverride) {
  WebAppInstaller installer(profile());

  constexpr char kManifestTemplate[] = R"({
    "name": "Example App",
    "start_url": "/",
    "scope": "/",
    "icons": $1
  })";
  SetManifestResponse(AddIconToManifest(kManifestTemplate));

  AppInstallData data = CreateInstallData(
      "Example App", "website:https://www.example.com/",
      "https://www.example.com/manifest.json", "/manifest.json");
  // Unset user_window_override to request UserDisplayMode::kStandalone.
  absl::get<WebAppInstallData>(data.app_type_data).open_as_window = true;

  base::test::TestFuture<bool> result;
  installer.InstallApp(AppInstallSurface::kAppInstallUriUnknown, data,
                       result.GetCallback());
  ASSERT_TRUE(result.Get());

  // Verify that the app is set to open in a window in App Service.
  auto app_id =
      web_app::GenerateAppId(std::nullopt, GURL("https://www.example.com/"));
  bool found =
      app_registry_cache().ForOneApp(app_id, [](const AppUpdate& update) {
        EXPECT_EQ(update.WindowMode(), apps::WindowMode::kWindow);
      });
  ASSERT_TRUE(found);

  EXPECT_TRUE(web_app::WebAppProvider::GetForWebApps(profile())
                  ->registrar_unsafe()
                  .IsDiyApp(app_id));
}

}  // namespace apps