chromium/chrome/browser/apps/app_service/app_install/app_install_almanac_endpoint_unittest.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/app_install_almanac_endpoint.h"

#include <optional>

#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/scoped_refptr.h"
#include "base/strings/to_string.h"
#include "base/test/bind.h"
#include "base/test/test_future.h"
#include "chrome/browser/apps/almanac_api_client/device_info_manager.h"
#include "chrome/browser/apps/almanac_api_client/proto/client_context.pb.h"
#include "chrome/browser/apps/app_service/app_install/app_install.pb.h"
#include "chrome/browser/apps/app_service/app_install/app_install_types.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "content/public/test/browser_task_environment.h"
#include "net/http/http_request_headers.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/test/test_url_loader_factory.h"
#include "services/network/test/test_utils.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace apps {

namespace {

using ResponseFuture =
    base::test::TestFuture<base::expected<AppInstallData, QueryError>>;

const PackageId kTestPackageId(PackageType::kWeb, "https://example.com/");

}  // namespace

class AppInstallAlmanacEndpointTest : public testing::Test {
 public:
  AppInstallAlmanacEndpointTest() = default;

 protected:
  network::TestURLLoaderFactory test_url_loader_factory_;

 private:
  content::BrowserTaskEnvironment task_environment_;
};

TEST_F(AppInstallAlmanacEndpointTest, GetAppInstallInfoRequest) {
  DeviceInfo device_info;
  device_info.board = "brya";
  device_info.user_type = "unmanaged";

  std::string method;
  std::optional<std::string> method_override_header;
  std::optional<std::string> content_type;
  std::string body;

  test_url_loader_factory_.SetInterceptor(
      base::BindLambdaForTesting([&](const network::ResourceRequest& request) {
        content_type =
            request.headers.GetHeader(net::HttpRequestHeaders::kContentType);
        method_override_header =
            request.headers.GetHeader("X-HTTP-Method-Override");
        method = request.method;
        body = network::GetUploadData(request);
      }));

  app_install_almanac_endpoint::GetAppInstallInfo(
      PackageId(PackageType::kWeb, "https://example.com/"), device_info,
      test_url_loader_factory_, base::DoNothing());

  EXPECT_EQ(method, "POST");
  EXPECT_EQ(method_override_header, "GET");
  EXPECT_EQ(content_type, "application/x-protobuf");

  proto::AppInstallRequest request;
  ASSERT_TRUE(request.ParseFromString(body));

  EXPECT_EQ(request.device_context().board(), "brya");
  EXPECT_EQ(request.user_context().user_type(),
            apps::proto::ClientUserContext::USERTYPE_UNMANAGED);
  EXPECT_EQ(request.package_id(), "web:https://example.com/");
}

TEST_F(AppInstallAlmanacEndpointTest, GetAppInstallInfoSuccessfulResponse) {
  proto::AppInstallResponse response;
  proto::AppInstallResponse_AppInstance& instance =
      *response.mutable_app_instance();
  instance.set_package_id("web:https://example.com/");
  instance.set_name("Example");
  instance.set_description("Description.");
  {
    proto::AppInstallResponse_Icon& icon = *instance.mutable_icon();
    icon.set_url("https://example.com/icon.png");
    icon.set_width_in_pixels(144);
    icon.set_mime_type("image/png");
    icon.set_is_masking_allowed(true);
  }
  {
    proto::AppInstallResponse_Screenshot& screenshot =
        *instance.add_screenshots();
    screenshot.set_url("https://example.com/screenshot1.png");
    screenshot.set_mime_type("image/png");
    screenshot.set_width_in_pixels(400);
    screenshot.set_height_in_pixels(400);
  }
  {
    proto::AppInstallResponse_Screenshot& screenshot =
        *instance.add_screenshots();
    screenshot.set_url("https://example.com/screenshot2.png");
    screenshot.set_mime_type("image/png");
    screenshot.set_width_in_pixels(800);
    screenshot.set_height_in_pixels(800);
  }
  instance.set_install_url("https://example.com/install");
  proto::AppInstallResponse_WebExtras& web_app_extras =
      *instance.mutable_web_extras();
  web_app_extras.set_document_url("https://example.com/start.html");
  web_app_extras.set_original_manifest_url("https://example.com/manifest.json");
  web_app_extras.set_scs_url(
      "https://almanac.chromium.org/example_manifest.json");
  web_app_extras.set_open_as_window(true);

  test_url_loader_factory_.AddResponse(
      app_install_almanac_endpoint::GetEndpointUrlForTesting().spec(),
      response.SerializeAsString());

  ResponseFuture response_future;
  app_install_almanac_endpoint::GetAppInstallInfo(
      kTestPackageId, DeviceInfo(), test_url_loader_factory_,
      response_future.GetCallback());
  EXPECT_TRUE(response_future.Get().has_value());

  AppInstallData expected_data(
      PackageId(PackageType::kWeb, "https://example.com/"));
  expected_data.name = "Example";
  expected_data.description = "Description.";
  expected_data.icon = AppInstallIcon{
      .url = GURL("https://example.com/icon.png"),
      .width_in_pixels = 144,
      .mime_type = "image/png",
      .is_masking_allowed = true,
  };
  expected_data.screenshots = {
      AppInstallScreenshot{
          .url = GURL("https://example.com/screenshot1.png"),
          .mime_type = "image/png",
          .width_in_pixels = 400,
          .height_in_pixels = 400,
      },
      AppInstallScreenshot{
          .url = GURL("https://example.com/screenshot2.png"),
          .mime_type = "image/png",
          .width_in_pixels = 800,
          .height_in_pixels = 800,
      }};
  expected_data.install_url = GURL("https://example.com/install");
  auto& web_app_data = expected_data.app_type_data.emplace<WebAppInstallData>();
  web_app_data.original_manifest_url =
      GURL("https://example.com/manifest.json");
  web_app_data.proxied_manifest_url =
      GURL("https://almanac.chromium.org/example_manifest.json");
  web_app_data.document_url = GURL("https://example.com/start.html");
  web_app_data.open_as_window = true;
  EXPECT_EQ(base::ToString(response_future.Get().value()),
            base::ToString(expected_data));
}

TEST_F(AppInstallAlmanacEndpointTest, GetAppInstallInfoMinimalResponse) {
  proto::AppInstallResponse response;
  proto::AppInstallResponse_AppInstance& instance =
      *response.mutable_app_instance();
  instance.set_package_id("android:com.foo.app");
  instance.set_name("Example");

  test_url_loader_factory_.AddResponse(
      app_install_almanac_endpoint::GetEndpointUrlForTesting().spec(),
      response.SerializeAsString());

  ResponseFuture response_future;
  app_install_almanac_endpoint::GetAppInstallInfo(
      kTestPackageId, DeviceInfo(), test_url_loader_factory_,
      response_future.GetCallback());

  AppInstallData expected_data(PackageId(PackageType::kArc, "com.foo.app"));
  expected_data.name = "Example";

  EXPECT_EQ(base::ToString(response_future.Get().value()),
            base::ToString(expected_data));
}

TEST_F(AppInstallAlmanacEndpointTest, GetAppInstallInfoIncompleteResponse) {
  proto::AppInstallResponse response;
  proto::AppInstallResponse_AppInstance& instance =
      *response.mutable_app_instance();
  instance.set_package_id("web:https://example.com/");

  test_url_loader_factory_.AddResponse(
      app_install_almanac_endpoint::GetEndpointUrlForTesting().spec(),
      response.SerializeAsString());

  ResponseFuture response_future;
  app_install_almanac_endpoint::GetAppInstallInfo(
      kTestPackageId, DeviceInfo(), test_url_loader_factory_,
      response_future.GetCallback());
  EXPECT_EQ(response_future.Get().error().type, QueryError::kBadResponse);
}

TEST_F(AppInstallAlmanacEndpointTest, GetAppInstallInfoMalformedResponse) {
  test_url_loader_factory_.AddResponse(
      app_install_almanac_endpoint::GetEndpointUrlForTesting().spec(),
      "Not a valid proto");

  ResponseFuture response_future;
  app_install_almanac_endpoint::GetAppInstallInfo(
      kTestPackageId, DeviceInfo(), test_url_loader_factory_,
      response_future.GetCallback());
  EXPECT_EQ(response_future.Get().error().type, QueryError::kBadResponse);
}

TEST_F(AppInstallAlmanacEndpointTest, GetAppInstallInfoWrongExtras) {
  proto::AppInstallResponse response;
  proto::AppInstallResponse_AppInstance& instance =
      *response.mutable_app_instance();
  instance.set_package_id("web:https://example.com/");
  instance.set_name("Example");
  instance.set_description("Description.");
  instance.mutable_android_extras();

  test_url_loader_factory_.AddResponse(
      app_install_almanac_endpoint::GetEndpointUrlForTesting().spec(),
      response.SerializeAsString());

  ResponseFuture response_future;
  app_install_almanac_endpoint::GetAppInstallInfo(
      PackageId(PackageType::kWeb, "https://example.com/"), DeviceInfo(),
      test_url_loader_factory_, response_future.GetCallback());
  EXPECT_EQ(response_future.Get().error().type, QueryError::kBadResponse);
}

TEST_F(AppInstallAlmanacEndpointTest, GetAppInstallInfoServerError) {
  test_url_loader_factory_.AddResponse(
      app_install_almanac_endpoint::GetEndpointUrlForTesting().spec(),
      /*content=*/"", net::HTTP_INTERNAL_SERVER_ERROR);

  ResponseFuture response_future;
  app_install_almanac_endpoint::GetAppInstallInfo(
      kTestPackageId, DeviceInfo(), test_url_loader_factory_,
      response_future.GetCallback());
  EXPECT_EQ(response_future.Get().error().type, QueryError::kConnectionError);
}

TEST_F(AppInstallAlmanacEndpointTest, GetAppInstallInfoNetworkError) {
  test_url_loader_factory_.AddResponse(
      app_install_almanac_endpoint::GetEndpointUrlForTesting(),
      network::mojom::URLResponseHead::New(), /*content=*/"",
      network::URLLoaderCompletionStatus(net::ERR_TIMED_OUT));

  ResponseFuture response_future;
  app_install_almanac_endpoint::GetAppInstallInfo(
      kTestPackageId, DeviceInfo(), test_url_loader_factory_,
      response_future.GetCallback());
  EXPECT_EQ(response_future.Get().error().type, QueryError::kConnectionError);
}

TEST_F(AppInstallAlmanacEndpointTest, GetAppInstallInfoNotFound) {
  test_url_loader_factory_.AddResponse(
      app_install_almanac_endpoint::GetEndpointUrlForTesting().spec(),
      /*content=*/"",

      net::HTTP_NOT_FOUND);

  ResponseFuture response_future;
  app_install_almanac_endpoint::GetAppInstallInfo(
      kTestPackageId, DeviceInfo(), test_url_loader_factory_,
      response_future.GetCallback());
  EXPECT_EQ(response_future.Get().error().type, QueryError::kBadRequest);
}

}  // namespace apps