chromium/chrome/updater/app/app_net_worker_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/updater/app/app_net_worker.h"

#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>

#include "base/files/file_util.h"
#include "base/files/scoped_temp_file.h"
#include "base/functional/bind.h"
#include "base/process/launch.h"
#include "base/process/process.h"
#include "base/run_loop.h"
#include "base/task/bind_post_task.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/test/bind.h"
#include "base/test/multiprocess_test.h"
#include "base/test/task_environment.h"
#include "base/test/test_timeouts.h"
#include "base/threading/thread_restrictions.h"
#include "chrome/updater/app/app.h"
#include "chrome/updater/constants.h"
#include "chrome/updater/ipc/ipc_support.h"
#include "chrome/updater/net/fetcher_callback_adapter.h"
#include "chrome/updater/net/mac/mojom/updater_fetcher.mojom.h"
#include "chrome/updater/test/unit_test_util.h"
#include "components/update_client/network.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "mojo/public/cpp/platform/platform_channel.h"
#include "mojo/public/cpp/system/invitation.h"
#include "mojo/public/cpp/system/message_pipe.h"
#include "net/base/net_errors.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_status_code.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_response.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/multiprocess_func_list.h"
#include "url/gurl.h"

namespace updater {

class AppNetWorkerTest : public ::testing::Test {
 protected:
  void SetUp() override {
    mojo::PlatformChannel channel;
    base::LaunchOptions options;
    base::CommandLine command_line =
        base::GetMultiProcessTestChildBaseCommandLine();

    channel.PrepareToPassRemoteEndpoint(&options, &command_line);
    fetcher_process_ = base::SpawnMultiProcessTestChild("NetWorkerChild",
                                                        command_line, options);
    channel.RemoteProcessLaunchAttempted();
    ASSERT_TRUE(fetcher_process_.IsValid());

    mojo::ScopedMessagePipeHandle pipe = mojo::OutgoingInvitation::SendIsolated(
        channel.TakeLocalEndpoint(), {}, fetcher_process_.Handle());
    mojo::PendingRemote<mojom::FetchService> pending_remote(
        std::move(pipe), mojom::FetchService::Version_);
    ASSERT_TRUE(pending_remote);
    remote_ = mojo::Remote<mojom::FetchService>(std::move(pending_remote));
  }

  void TearDown() override {
    test::WaitForProcess(fetcher_process_);
    fetcher_process_.Close();
    remote_.reset();
  }

  base::test::TaskEnvironment environment_;
  ScopedIPCSupportWrapper ipc_support_;
  mojo::Remote<mojom::FetchService> remote_;
  base::Process fetcher_process_;
};

TEST_F(AppNetWorkerTest, PostRequest) {
  net::EmbeddedTestServer test_server;
  test_server.RegisterRequestHandler(base::BindRepeating(
      [](const net::test_server::HttpRequest& request)
          -> std::unique_ptr<net::test_server::HttpResponse> {
        auto http_response =
            std::make_unique<net::test_server::BasicHttpResponse>();
        http_response->set_code(net::HTTP_OK);
        http_response->set_content("hello world!");
        http_response->set_content_type("text/plain");
        http_response->AddCustomHeader(
            update_client::NetworkFetcher::kHeaderEtag, "etag-for-test");
        http_response->AddCustomHeader(
            update_client::NetworkFetcher::kHeaderXCupServerProof,
            "cup-server-proof-xyz");
        http_response->AddCustomHeader("SomeOtherHeader", "foo-bar");
        return http_response;
      }));
  ASSERT_TRUE(test_server.Start());

  base::RunLoop run_loop;
  std::vector<mojom::HttpHeaderPtr> headers;
  headers.push_back(mojom::HttpHeader::New("CustomizedHeader", "abc"));
  remote_->PostRequest(
      test_server.GetURL("/"), "", "text/plain", std::move(headers),
      MakePostRequestObserver(
          base::BindRepeating(
              [](int32_t http_status_code, int64_t content_length) {
                EXPECT_EQ(http_status_code, net::HTTP_OK);
                EXPECT_EQ(content_length, 12);  // Length of `hello world!`
              }),
          base::BindRepeating([](int64_t current) { EXPECT_LE(current, 12); }),
          base::BindLambdaForTesting(
              [&](std::unique_ptr<std::string> response_body, int32_t net_error,
                  const std::string& header_etag,
                  const std::string& header_x_cup_server_proof,
                  int64_t xheader_retry_after_sec) {
                EXPECT_EQ(net_error, 0);
                EXPECT_EQ(*response_body, "hello world!");
                EXPECT_EQ(header_etag, "etag-for-test");
                EXPECT_EQ(header_x_cup_server_proof, "cup-server-proof-xyz");
                run_loop.Quit();
              })));
  run_loop.Run();
}

TEST_F(AppNetWorkerTest, DownloadFile) {
  base::FilePath payload_path = updater::test::GetTestFilePath("signed.exe.gz");
  int64_t payload_size = {};
  ASSERT_TRUE(base::GetFileSize(payload_path, &payload_size));
  std::string payload;
  ASSERT_TRUE(base::ReadFileToString(payload_path, &payload));
  net::EmbeddedTestServer test_server;
  test_server.RegisterRequestHandler(base::BindLambdaForTesting(
      [&](const net::test_server::HttpRequest& request)
          -> std::unique_ptr<net::test_server::HttpResponse> {
        auto http_response =
            std::make_unique<net::test_server::BasicHttpResponse>();
        http_response->set_code(net::HTTP_OK);
        http_response->set_content(payload);
        http_response->set_content_type("application/octet-stream");
        return http_response;
      }));
  ASSERT_TRUE(test_server.Start());

  base::ScopedTempFile output;
  ASSERT_TRUE(output.Create());
  base::File output_file(output.path(),
                         base::File::FLAG_OPEN_ALWAYS | base::File::FLAG_WRITE);
  ASSERT_TRUE(output_file.IsValid());
  base::RunLoop run_loop;
  remote_->DownloadToFile(
      test_server.GetURL("/"), std::move(output_file),
      MakeFileDownloadObserver(
          base::BindLambdaForTesting(
              [&](int32_t http_status_code, int64_t content_length) {
                EXPECT_EQ(http_status_code, net::HTTP_OK);
                if (content_length > 0) {
                  EXPECT_EQ(content_length, payload_size);
                }
              }),
          base::BindLambdaForTesting(
              [&](int64_t current) { EXPECT_LE(current, payload_size); }),
          base::BindLambdaForTesting(
              [&](int32_t net_error, int64_t content_length) {
                EXPECT_EQ(net_error, 0);
                EXPECT_EQ(content_length, payload_size);
                run_loop.Quit();
              })));
  run_loop.Run();
  EXPECT_TRUE(base::ContentsEqual(output.path(), payload_path));
}

TEST_F(AppNetWorkerTest, ServerNotExist) {
  base::RunLoop run_loop;
  remote_->PostRequest(
      GURL("https://host_that.does_not_exist"), "", "text/plain", {},
      MakePostRequestObserver(
          base::BindRepeating(
              [](int32_t http_status_code, int64_t content_length) {}),
          base::BindRepeating([](int64_t current) {}),
          base::BindLambdaForTesting(
              [&](std::unique_ptr<std::string> response_body, int32_t net_error,
                  const std::string& header_etag,
                  const std::string& header_x_cup_server_proof,
                  int64_t xheader_retry_after_sec) {
                EXPECT_NE(net_error, 0);
                run_loop.Quit();
              })));
  run_loop.Run();
}

MULTIPROCESS_TEST_MAIN(NetWorkerChild) {
  base::test::TaskEnvironment task_environment;
  ScopedIPCSupportWrapper ipc_support;

  // This is a net-worker process, mark it as such, so that it doesn't do
  // further fallback.
  base::CommandLine::ForCurrentProcess()->AppendSwitch(kNetWorkerSwitch);
  return !MakeAppNetWorker()->Run();
}

}  // namespace updater