chromium/chrome/browser/android/httpclient/http_client_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 "chrome/browser/android/httpclient/http_client.h"

#include <iostream>
#include <ostream>
#include <string>
#include <type_traits>

#include "base/functional/bind.h"
#include "base/ranges/algorithm.h"
#include "base/strings/string_number_conversions.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/test/browser_task_environment.h"
#include "net/base/net_errors.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_status_code.h"
#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/url_loader_completion_status.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace httpclient {

using ResponseProduceFlags =
    network::TestURLLoaderFactory::ResponseProduceFlags;

struct TestHttpRequest {
  explicit TestHttpRequest(
      std::string url_input,
      std::string request_type = "POST",
      std::string request_body = "",
      std::map<std::string, std::string> request_headers = {})
      : headers(request_headers),
        url(url_input),
        type(request_type),
        body(request_body) {}
  std::map<std::string, std::string> headers;
  std::string url;
  std::string type;
  std::string body;
};

struct TestHttpResponse {
  explicit TestHttpResponse(
      int32_t http_status_input = net::HTTP_OK,
      int32_t net_error_code_input = 0,
      std::string response_body = "",
      std::map<std::string, std::string> response_headers = {})
      : headers(response_headers),
        http_status(http_status_input),
        net_error_code(net_error_code_input),
        body(response_body) {}

  std::map<std::string, std::string> headers;
  int32_t http_status;
  int32_t net_error_code;
  std::string body;

  bool operator==(const TestHttpResponse& other) const {
    return headers == other.headers && http_status == other.http_status &&
           net_error_code == other.net_error_code && !body.compare(other.body);
  }

  friend std::ostream& operator<<(std::ostream& os,
                                  const TestHttpResponse& response);
};

std::ostream& operator<<(std::ostream& os, const TestHttpResponse& response) {
  os << "http_status=[" << base::NumberToString(response.http_status) << "] ";
  os << "net_error_code=[" << base::NumberToString(response.net_error_code)
     << "] ";
  os << "body=[" << response.body << "] ";
  os << "headers={";
  bool first = true;
  for (auto const& [key, value] : response.headers) {
    if (!first) {
      os << ", ";
    }
    first = false;
    os << key << "=" << value;
  }
  os << "}";
  return os;
}

class MockResponseDoneCallback {
 public:
  MockResponseDoneCallback() = default;

  void Done(int32_t http_status,
            int32_t net_error_code,
            std::vector<uint8_t>&& response_bytes,
            std::map<std::string, std::string>&& input_response_headers) {
    EXPECT_FALSE(has_run);
    has_run = true;
    response = TestHttpResponse(
        http_status, net_error_code,
        std::string(response_bytes.begin(), response_bytes.end()),
        std::move(input_response_headers));
  }

  bool has_run{false};
  TestHttpResponse response;
};

class HttpClientTest : public testing::Test {
 public:
  HttpClientTest(const HttpClientTest&) = delete;
  HttpClientTest& operator=(const HttpClientTest&) = delete;

 protected:
  HttpClientTest() = default;

  ~HttpClientTest() override = default;

  void SetUp() override {
    shared_url_loader_factory_ =
        base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
            &test_url_loader_factory_);

    http_client_ = std::make_unique<HttpClient>(shared_url_loader_factory_);
  }

  void DestroyService() { http_client_.reset(); }

  HttpClient* service() { return http_client_.get(); }

  // Helper to check the next request for the network.
  network::TestURLLoaderFactory::PendingRequest* GetLastPendingRequest() {
    EXPECT_GT(test_url_loader_factory_.pending_requests()->size(), 0U)
        << "No pending request!";
    network::TestURLLoaderFactory::PendingRequest* request =
        &(test_url_loader_factory_.pending_requests()->back());
    return request;
  }

  void Respond(
      const GURL& url,
      TestHttpResponse response,
      network::URLLoaderCompletionStatus status =
          network::URLLoaderCompletionStatus(),
      ResponseProduceFlags flag = ResponseProduceFlags::kResponseDefault) {
    auto head = network::mojom::URLResponseHead::New();
    if (response.http_status >= 0) {
      head->headers = base::MakeRefCounted<net::HttpResponseHeaders>("");
      head->headers->ReplaceStatusLine(
          "HTTP/1.1 " + base::NumberToString(response.http_status));

      for (auto const& [key, value] : response.headers) {
        head->headers->SetHeader(key, value);
      }

      status.decoded_body_length = response.body.length();
    }

    test_url_loader_factory_.AddResponse(
        url, std::move(head), response.body, status,
        network::TestURLLoaderFactory::Redirects(), flag);

    task_environment_.FastForwardUntilNoTasksRemain();
  }

  void SendRequest(TestHttpRequest request,
                   MockResponseDoneCallback* done_callback) {
    GURL req_url(request.url);
    std::vector<uint8_t> request_body_bytes(request.body.begin(),
                                            request.body.end());
    std::map<std::string, std::string> headers(request.headers);
    content::GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE,
        base::BindOnce(
            &HttpClient::Send, base::Unretained(service()), req_url,
            request.type, std::move(request_body_bytes), std::move(headers),
            net::NetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS),
            base::BindOnce(&MockResponseDoneCallback::Done,
                           base::Unretained(done_callback))));

    task_environment_.RunUntilIdle();
  }

  void SendRequestAndRespond(TestHttpRequest request,
                             TestHttpResponse response,
                             network::URLLoaderCompletionStatus status,
                             MockResponseDoneCallback* done_callback) {
    SendRequest(request, done_callback);
    Respond(GURL(request.url), response, status);
  }

  void SendEmptyRequestAndResponseNoHeaders(
      std::string request_url,
      MockResponseDoneCallback* done_callback) {
    SendRequestAndRespond(TestHttpRequest(request_url), TestHttpResponse(),
                          network::URLLoaderCompletionStatus(), done_callback);
  }

  void SendRequestAndValidateResponse(
      const TestHttpRequest& request,
      const TestHttpResponse& response,
      network::URLLoaderCompletionStatus status =
          network::URLLoaderCompletionStatus()) {
    MockResponseDoneCallback done_callback;
    SendRequestAndRespond(request, response, status, &done_callback);

    EXPECT_TRUE(done_callback.has_run);
    EXPECT_EQ(done_callback.response, response);
  }

  network::TestURLLoaderFactory* test_url_loader_factory() {
    return &test_url_loader_factory_;
  }

  content::BrowserTaskEnvironment task_environment_{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};

 private:
  std::unique_ptr<HttpClient> http_client_;
  network::TestURLLoaderFactory test_url_loader_factory_;
  scoped_refptr<network::SharedURLLoaderFactory> shared_url_loader_factory_;
};

TEST_F(HttpClientTest, TestSendEmptyRequest) {
  SendRequestAndValidateResponse(TestHttpRequest("http://foobar.com/survey"),
                                 TestHttpResponse());
}

TEST_F(HttpClientTest, TestSendSimpleRequest) {
  std::map<std::string, std::string> request_headers;
  request_headers["Content-Type"] = "application/x-protobuf";
  TestHttpRequest request("http://foobar.com/survey", "POST", "?bar=baz&foo=1",
                          request_headers);
  TestHttpResponse response(net::HTTP_OK, 0, "{key:'val'}");
  SendRequestAndValidateResponse(request, response);
}

TEST_F(HttpClientTest, TestSendDifferentRequestMethod) {
  std::vector<std::string> request_methods({"POST", "PUT", "PATCH"});
  for (const auto& method : request_methods) {
    MockResponseDoneCallback done_callback;
    std::string content_type = "application/x-protobuf";
    std::map<std::string, std::string> request_headers;
    request_headers["Content-Type"] = content_type;
    request_headers["TestMethod"] = method;
    TestHttpRequest request("http://foobar.com/survey", method, "request_body",
                            request_headers);
    SendRequest(request, &done_callback);

    {
      auto pendingRequest = GetLastPendingRequest()->request;
      EXPECT_EQ(pendingRequest.method, method);

      EXPECT_EQ(pendingRequest.headers.GetHeader("Content-Type"), content_type);
      EXPECT_EQ(pendingRequest.headers.GetHeader("TestMethod"), method);
    }

    Respond(GURL(request.url), TestHttpResponse());

    task_environment_.FastForwardUntilNoTasksRemain();
    EXPECT_TRUE(done_callback.has_run);
    EXPECT_EQ(done_callback.response.http_status, net::HTTP_OK);

    test_url_loader_factory()->ClearResponses();
  }
}

TEST_F(HttpClientTest, TestSendMultipleRequests) {
  MockResponseDoneCallback done_callback1;
  MockResponseDoneCallback done_callback2;
  MockResponseDoneCallback done_callback3;

  SendEmptyRequestAndResponseNoHeaders("http://foobar.com/survey1",
                                       &done_callback1);
  SendEmptyRequestAndResponseNoHeaders("http://foobar.com/survey2",
                                       &done_callback2);
  SendEmptyRequestAndResponseNoHeaders("http://foobar.com/survey3",
                                       &done_callback3);

  task_environment_.FastForwardUntilNoTasksRemain();

  EXPECT_TRUE(done_callback1.has_run);
  EXPECT_TRUE(done_callback2.has_run);
  EXPECT_TRUE(done_callback3.has_run);
}

TEST_F(HttpClientTest, TestResponseHeader) {
  TestHttpRequest request("http://foobar.com/survey");
  std::map<std::string, std::string> response_headers;
  response_headers["Foo"] = "foo_value";
  response_headers["Bar"] = "bar_value";
  TestHttpResponse response(net::HTTP_OK, 0, /*response_body*/ "",
                            response_headers);
  SendRequestAndValidateResponse(request, response);
}

TEST_F(HttpClientTest, TestCancelRequest) {
  MockResponseDoneCallback done_callback;

  GURL url("http://foobar.com/survey");
  service()->Send(
      url, "GET", /*request_body*/ {}, /*headers*/ {},
      net::NetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS),
      base::BindOnce(&MockResponseDoneCallback::Done,
                     base::Unretained(&done_callback)));

  DestroyService();

  Respond(url, TestHttpResponse());

  EXPECT_FALSE(done_callback.has_run);
}

TEST_F(HttpClientTest, TestRequestTimeout) {
  MockResponseDoneCallback done_callback;
  SendRequest(TestHttpRequest("http://foobar.com/survey"), &done_callback);

  task_environment_.FastForwardUntilNoTasksRemain();

  EXPECT_TRUE(done_callback.has_run);
  EXPECT_EQ(done_callback.response.net_error_code, net::ERR_TIMED_OUT);
}

TEST_F(HttpClientTest, TestHttpError) {
  std::vector<net::HttpStatusCode> error_codes(
      {net::HTTP_BAD_REQUEST, net::HTTP_UNAUTHORIZED, net::HTTP_FORBIDDEN,
       net::HTTP_NOT_FOUND, net::HTTP_INTERNAL_SERVER_ERROR,
       net::HTTP_BAD_GATEWAY, net::HTTP_SERVICE_UNAVAILABLE});
  std::map<std::string, std::string> response_headers;
  response_headers["Foo"] = "foo_value";
  response_headers["Bar"] = "bar_value";

  for (const auto& http_status : error_codes) {
    MockResponseDoneCallback done_callback;
    SendRequestAndRespond(
        TestHttpRequest("http://foobar.com/survey"),
        TestHttpResponse(http_status, 0, "error_response_data",
                         response_headers),
        network::URLLoaderCompletionStatus(), &done_callback);

    TestHttpResponse expected_response = TestHttpResponse(
        http_status, 0, "error_response_data", response_headers);

    EXPECT_TRUE(done_callback.has_run);
    EXPECT_EQ(done_callback.response, expected_response);

    test_url_loader_factory()->ClearResponses();
  }
}

TEST_F(HttpClientTest, TestNetworkError) {
  std::vector<int32_t> error_codes(
      {net::ERR_CERT_COMMON_NAME_INVALID, net::ERR_CERT_DATE_INVALID,
       net::ERR_CERT_WEAK_KEY, net::ERR_NAME_RESOLUTION_FAILED});
  std::map<std::string, std::string> response_headers;
  response_headers["Foo"] = "foo_value";
  response_headers["Bar"] = "bar_value";

  for (const auto& code : error_codes) {
    MockResponseDoneCallback done_callback;
    SendRequestAndRespond(
        TestHttpRequest("http://foobar.com/survey"),
        TestHttpResponse(net::HTTP_OK, 0, "success_response_data",
                         response_headers),
        network::URLLoaderCompletionStatus(code), &done_callback);

    // The expected response should have the code override by network error, and
    // empty response body, and empty headers.
    TestHttpResponse expected_response = TestHttpResponse(0, code, "");

    EXPECT_TRUE(done_callback.has_run);
    EXPECT_EQ(done_callback.response, expected_response);

    test_url_loader_factory()->ClearResponses();
  }
}

TEST_F(HttpClientTest, TestNetworkErrorAfterSendHeaders) {
  std::vector<int32_t> error_codes(
      {net::ERR_CERT_COMMON_NAME_INVALID, net::ERR_CERT_DATE_INVALID,
       net::ERR_CERT_WEAK_KEY, net::ERR_NAME_RESOLUTION_FAILED});
  std::map<std::string, std::string> response_headers;
  response_headers["Foo"] = "foo_value";
  response_headers["Bar"] = "bar_value";

  std::string url = "http://foobar.com/survey";
  GURL gurl(url);
  for (const auto& code : error_codes) {
    MockResponseDoneCallback done_callback;
    SendRequest(TestHttpRequest(url), &done_callback);

    Respond(gurl,
            TestHttpResponse(net::HTTP_OK, 0, "success_response_data",
                             response_headers),
            network::URLLoaderCompletionStatus(code),
            ResponseProduceFlags::kSendHeadersOnNetworkError);

    // The expected response should have the code override by network error, and
    // empty response body, and the same headers.
    TestHttpResponse expected_response =
        TestHttpResponse(net::HTTP_OK, code, "", response_headers);

    EXPECT_TRUE(done_callback.has_run);
    EXPECT_EQ(done_callback.response, expected_response);

    test_url_loader_factory()->ClearResponses();
  }
}

}  // namespace httpclient