// Copyright 2020 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/trusted_vault/trusted_vault_request.h"

#include <memory>
#include <string>
#include <utility>

#include "base/containers/flat_map.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/scoped_refptr.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/mock_callback.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "components/signin/public/identity_manager/access_token_info.h"
#include "components/trusted_vault/test/fake_trusted_vault_access_token_fetcher.h"
#include "components/trusted_vault/trusted_vault_access_token_fetcher.h"
#include "components/trusted_vault/trusted_vault_histograms.h"
#include "components/trusted_vault/trusted_vault_server_constants.h"
#include "google_apis/gaia/core_account_id.h"
#include "services/network/public/cpp/shared_url_loader_factory.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 "services/network/test/test_utils.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace trusted_vault {

namespace {

using testing::_;
using testing::Eq;
using testing::IsEmpty;
using testing::Ne;
using testing::NotNull;
using testing::Pointee;

const char kAccessToken[] = "access_token";
const char kRequestUrl[] = "https://test.com/test";
const char kRequestUrlWithAlternateOutputProto[] =
    "https://test.com/test?alt=proto";
const char kResponseBody[] = "response_body";

MATCHER(HasValidAccessToken, "") {
  const network::TestURLLoaderFactory::PendingRequest& pending_request = arg;
  std::string access_token_header =
      pending_request.request.headers.GetHeader("Authorization")
          .value_or(std::string());
  return access_token_header == base::StringPrintf("Bearer %s", kAccessToken);
}

signin::AccessTokenInfo MakeAccessTokenInfo(const std::string& access_token) {
  return signin::AccessTokenInfo(
      access_token,
      /*expiration_time_param=*/base::Time::Now() + base::Hours(1),
      /*id_token=*/std::string());
}

class TrustedVaultRequestTest : public testing::Test {
 public:
  TrustedVaultRequestTest()
      : shared_url_loader_factory_(
            base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
                &test_url_loader_factory_)) {}

  std::unique_ptr<TrustedVaultRequest>
  StartNewRequestWithAccessTokenAndRetriesDuration(
      const std::string& access_token,
      TrustedVaultRequest::HttpMethod http_method,
      const std::optional<std::string>& request_body,
      base::TimeDelta max_retry_duration,
      TrustedVaultRequest::CompletionCallback completion_callback) {
    const CoreAccountId account_id =
        CoreAccountId::FromGaiaId(GaiaId("user_id"));
    FakeTrustedVaultAccessTokenFetcher access_token_fetcher(
        MakeAccessTokenInfo(access_token));

    auto request = std::make_unique<TrustedVaultRequest>(
        GetSecurityDomainId(), account_id, http_method, GURL(kRequestUrl),
        request_body, max_retry_duration, shared_url_loader_factory_,
        std::make_unique<FakeTrustedVaultAccessTokenFetcher>(
            MakeAccessTokenInfo(access_token)),
        /*record_fetch_status_callback=*/base::DoNothing());
    request->FetchAccessTokenAndSendRequest(std::move(completion_callback));
    return request;
  }

  std::unique_ptr<TrustedVaultRequest> StartNewRequestWithAccessToken(
      const std::string& access_token,
      TrustedVaultRequest::HttpMethod http_method,
      const std::optional<std::string>& request_body,
      TrustedVaultRequest::CompletionCallback completion_callback) {
    return StartNewRequestWithAccessTokenAndRetriesDuration(
        access_token, http_method, request_body,
        /*max_retry_duration=*/base::Seconds(0),
        std::move(completion_callback));
  }

  std::unique_ptr<TrustedVaultRequest> StartNewRequestWithAccessTokenError(
      TrustedVaultAccessTokenFetcher::FetchingError error,
      TrustedVaultRequest::CompletionCallback completion_callback) {
    const CoreAccountId account_id =
        CoreAccountId::FromGaiaId(GaiaId("user_id"));

    auto request = std::make_unique<TrustedVaultRequest>(
        GetSecurityDomainId(), account_id,
        TrustedVaultRequest::HttpMethod::kGet, GURL(kRequestUrl),
        /*serialized_request_proto=*/std::nullopt,
        /*max_retry_duration=*/base::Seconds(0), shared_url_loader_factory_,
        std::make_unique<FakeTrustedVaultAccessTokenFetcher>(
            base::unexpected{error}),
        /*record_fetch_status_callback=*/base::DoNothing());
    request->FetchAccessTokenAndSendRequest(std::move(completion_callback));
    return request;
  }

  bool RespondToHttpRequest(
      net::Error error,
      std::optional<net::HttpStatusCode> response_http_code,
      const std::string& response_body) {
    network::mojom::URLResponseHeadPtr response_head;
    if (response_http_code.has_value()) {
      response_head = network::CreateURLResponseHead(*response_http_code);
    } else {
      response_head = network::mojom::URLResponseHead::New();
    }
    return test_url_loader_factory_.SimulateResponseForPendingRequest(
        GURL(kRequestUrlWithAlternateOutputProto),
        network::URLLoaderCompletionStatus(error), std::move(response_head),
        response_body);
  }

  SecurityDomainId GetSecurityDomainId() {
    return SecurityDomainId::kChromeSync;
  }

  network::TestURLLoaderFactory::PendingRequest* GetPendingRequest() {
    return test_url_loader_factory_.GetPendingRequest(/*index=*/0);
  }

  base::test::SingleThreadTaskEnvironment& task_environment() {
    return task_environment_;
  }

 private:
  base::test::SingleThreadTaskEnvironment task_environment_{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};

  network::TestURLLoaderFactory test_url_loader_factory_;
  scoped_refptr<network::SharedURLLoaderFactory> shared_url_loader_factory_;
};

}  // namespace

TEST_F(TrustedVaultRequestTest, ShouldSendGetRequestAndHandleSuccess) {
  base::MockCallback<TrustedVaultRequest::CompletionCallback>
      completion_callback;
  base::HistogramTester histogram_tester;
  std::unique_ptr<TrustedVaultRequest> request = StartNewRequestWithAccessToken(
      kAccessToken, TrustedVaultRequest::HttpMethod::kGet,
      /*request_body=*/std::nullopt, completion_callback.Get());

  histogram_tester.ExpectUniqueSample(
      /*name=*/"TrustedVault.AccessTokenFetchSuccess." +
          GetSecurityDomainNameForUma(GetSecurityDomainId()),
      /*sample=*/true,
      /*expected_bucket_count=*/1);

  network::TestURLLoaderFactory::PendingRequest* pending_request =
      GetPendingRequest();
  EXPECT_THAT(pending_request, Pointee(HasValidAccessToken()));

  const network::ResourceRequest& resource_request = pending_request->request;
  EXPECT_THAT(resource_request.method, Eq("GET"));
  EXPECT_THAT(resource_request.url,
              Eq(GURL(kRequestUrlWithAlternateOutputProto)));
  EXPECT_THAT(network::GetUploadData(resource_request), IsEmpty());

  // |completion_callback| should be called after receiving response.
  EXPECT_CALL(
      completion_callback,
      Run(TrustedVaultRequest::HttpStatus::kSuccess, Eq(kResponseBody)));
  EXPECT_TRUE(RespondToHttpRequest(net::OK, net::HTTP_OK, kResponseBody));
}

TEST_F(TrustedVaultRequestTest,
       ShouldSendPostRequestWithoutPayloadAndHandleSuccess) {
  base::MockCallback<TrustedVaultRequest::CompletionCallback>
      completion_callback;
  std::unique_ptr<TrustedVaultRequest> request = StartNewRequestWithAccessToken(
      kAccessToken, TrustedVaultRequest::HttpMethod::kPost,
      /*request_body=*/std::nullopt, completion_callback.Get());

  network::TestURLLoaderFactory::PendingRequest* pending_request =
      GetPendingRequest();
  EXPECT_THAT(pending_request, Pointee(HasValidAccessToken()));

  const network::ResourceRequest& resource_request = pending_request->request;
  EXPECT_THAT(resource_request.method, Eq("POST"));
  EXPECT_THAT(resource_request.url,
              Eq(GURL(kRequestUrlWithAlternateOutputProto)));
  EXPECT_THAT(network::GetUploadData(resource_request), IsEmpty());

  // |completion_callback| should be called after receiving response.
  EXPECT_CALL(
      completion_callback,
      Run(TrustedVaultRequest::HttpStatus::kSuccess, Eq(kResponseBody)));
  EXPECT_TRUE(RespondToHttpRequest(net::OK, net::HTTP_OK, kResponseBody));
}

TEST_F(TrustedVaultRequestTest,
       ShouldSendPatchRequestWithoutPayloadAndHandleSuccess) {
  base::MockCallback<TrustedVaultRequest::CompletionCallback>
      completion_callback;
  std::unique_ptr<TrustedVaultRequest> request = StartNewRequestWithAccessToken(
      kAccessToken, TrustedVaultRequest::HttpMethod::kPatch,
      /*request_body=*/std::nullopt, completion_callback.Get());

  network::TestURLLoaderFactory::PendingRequest* pending_request =
      GetPendingRequest();
  EXPECT_THAT(pending_request, Pointee(HasValidAccessToken()));

  const network::ResourceRequest& resource_request = pending_request->request;
  EXPECT_THAT(resource_request.method, Eq("PATCH"));
  EXPECT_THAT(resource_request.url,
              Eq(GURL(kRequestUrlWithAlternateOutputProto)));
  EXPECT_THAT(network::GetUploadData(resource_request), IsEmpty());

  // |completion_callback| should be called after receiving response.
  EXPECT_CALL(
      completion_callback,
      Run(TrustedVaultRequest::HttpStatus::kSuccess, Eq(kResponseBody)));
  EXPECT_TRUE(RespondToHttpRequest(net::OK, net::HTTP_OK, kResponseBody));
}

TEST_F(TrustedVaultRequestTest,
       ShouldSendPostRequestWithPayloadAndHandleSuccess) {
  base::MockCallback<TrustedVaultRequest::CompletionCallback>
      completion_callback;
  const std::string kRequestBody = "Request body";
  std::unique_ptr<TrustedVaultRequest> request = StartNewRequestWithAccessToken(
      kAccessToken, TrustedVaultRequest::HttpMethod::kPost, kRequestBody,
      completion_callback.Get());

  network::TestURLLoaderFactory::PendingRequest* pending_request =
      GetPendingRequest();
  EXPECT_THAT(pending_request, Pointee(HasValidAccessToken()));

  const network::ResourceRequest& resource_request = pending_request->request;
  EXPECT_THAT(resource_request.method, Eq("POST"));
  EXPECT_THAT(resource_request.url,
              Eq(GURL(kRequestUrlWithAlternateOutputProto)));
  EXPECT_THAT(network::GetUploadData(resource_request), Eq(kRequestBody));

  // |completion_callback| should be called after receiving response.
  EXPECT_CALL(
      completion_callback,
      Run(TrustedVaultRequest::HttpStatus::kSuccess, Eq(kResponseBody)));
  EXPECT_TRUE(RespondToHttpRequest(net::OK, net::HTTP_OK, kResponseBody));
}

TEST_F(TrustedVaultRequestTest, ShouldHandleNetworkFailures) {
  base::MockCallback<TrustedVaultRequest::CompletionCallback>
      completion_callback;
  std::unique_ptr<TrustedVaultRequest> request = StartNewRequestWithAccessToken(
      kAccessToken, TrustedVaultRequest::HttpMethod::kGet,
      /*request_body=*/std::nullopt, completion_callback.Get());

  // |completion_callback| should be called after receiving response.
  EXPECT_CALL(completion_callback,
              Run(TrustedVaultRequest::HttpStatus::kNetworkError, _));
  EXPECT_TRUE(RespondToHttpRequest(net::ERR_FAILED, std::nullopt,
                                   /*response_body=*/std::string()));
}

TEST_F(TrustedVaultRequestTest, ShouldHandleHttpErrors) {
  base::MockCallback<TrustedVaultRequest::CompletionCallback>
      completion_callback;
  std::unique_ptr<TrustedVaultRequest> request = StartNewRequestWithAccessToken(
      kAccessToken, TrustedVaultRequest::HttpMethod::kGet,
      /*request_body=*/std::nullopt, completion_callback.Get());

  // |completion_callback| should be called after receiving response.
  EXPECT_CALL(completion_callback,
              Run(TrustedVaultRequest::HttpStatus::kOtherError, _));
  EXPECT_TRUE(RespondToHttpRequest(net::OK, net::HTTP_INTERNAL_SERVER_ERROR,
                                   /*response_body=*/""));
}

TEST_F(TrustedVaultRequestTest, ShouldHandleBadRequestStatus) {
  base::MockCallback<TrustedVaultRequest::CompletionCallback>
      completion_callback;
  std::unique_ptr<TrustedVaultRequest> request = StartNewRequestWithAccessToken(
      kAccessToken, TrustedVaultRequest::HttpMethod::kGet,
      /*request_body=*/std::nullopt, completion_callback.Get());

  // |completion_callback| should be called after receiving response.
  EXPECT_CALL(completion_callback,
              Run(TrustedVaultRequest::HttpStatus::kBadRequest, _));
  EXPECT_TRUE(RespondToHttpRequest(net::OK, net::HTTP_BAD_REQUEST,
                                   /*response_body=*/""));
}

TEST_F(TrustedVaultRequestTest,
       ShouldHandleConflictStatusAndPopulateResponseBody) {
  base::MockCallback<TrustedVaultRequest::CompletionCallback>
      completion_callback;
  std::unique_ptr<TrustedVaultRequest> request = StartNewRequestWithAccessToken(
      kAccessToken, TrustedVaultRequest::HttpMethod::kGet,
      /*request_body=*/std::nullopt, completion_callback.Get());

  // |completion_callback| should be called after receiving response.
  EXPECT_CALL(completion_callback,
              Run(TrustedVaultRequest::HttpStatus::kConflict, kResponseBody));
  EXPECT_TRUE(RespondToHttpRequest(net::OK, net::HTTP_CONFLICT, kResponseBody));
}

TEST_F(TrustedVaultRequestTest, ShouldHandleNotFoundStatus) {
  base::MockCallback<TrustedVaultRequest::CompletionCallback>
      completion_callback;
  std::unique_ptr<TrustedVaultRequest> request = StartNewRequestWithAccessToken(
      kAccessToken, TrustedVaultRequest::HttpMethod::kGet,
      /*request_body=*/std::nullopt, completion_callback.Get());

  // |completion_callback| should be called after receiving response.
  EXPECT_CALL(completion_callback,
              Run(TrustedVaultRequest::HttpStatus::kNotFound, _));
  EXPECT_TRUE(RespondToHttpRequest(net::OK, net::HTTP_NOT_FOUND,
                                   /*response_body=*/""));
}

TEST_F(TrustedVaultRequestTest, ShouldRetryUponNetworkChange) {
  base::MockCallback<TrustedVaultRequest::CompletionCallback>
      completion_callback;
  std::unique_ptr<TrustedVaultRequest> request = StartNewRequestWithAccessToken(
      kAccessToken, TrustedVaultRequest::HttpMethod::kGet,
      /*request_body=*/std::nullopt, completion_callback.Get());

  // Mimic network change error for the first request.
  EXPECT_CALL(completion_callback, Run).Times(0);
  EXPECT_TRUE(RespondToHttpRequest(net::ERR_NETWORK_CHANGED, net::HTTP_OK,
                                   /*response_body=*/""));
  testing::Mock::VerifyAndClearExpectations(&completion_callback);

  // Second request should be sent, mimic its success.
  network::TestURLLoaderFactory::PendingRequest* pending_request =
      GetPendingRequest();
  EXPECT_THAT(pending_request, NotNull());

  EXPECT_CALL(completion_callback,
              Run(TrustedVaultRequest::HttpStatus::kSuccess, kResponseBody));
  EXPECT_TRUE(RespondToHttpRequest(net::OK, net::HTTP_OK, kResponseBody));
}

TEST_F(TrustedVaultRequestTest, ShouldRetryUponTransientErrorAndHandleSuccess) {
  base::MockCallback<TrustedVaultRequest::CompletionCallback>
      completion_callback;
  std::unique_ptr<TrustedVaultRequest> request =
      StartNewRequestWithAccessTokenAndRetriesDuration(
          kAccessToken, TrustedVaultRequest::HttpMethod::kGet,
          /*request_body=*/std::nullopt,
          /*max_retry_duration=*/base::Minutes(1), completion_callback.Get());

  // Mimic network error for the first request.
  EXPECT_CALL(completion_callback, Run).Times(0);
  EXPECT_TRUE(RespondToHttpRequest(net::ERR_DNS_REQUEST_CANCELLED, net::HTTP_OK,
                                   /*response_body=*/""));
  testing::Mock::VerifyAndClearExpectations(&completion_callback);

  // Forward time and expect the second attempt.
  task_environment().FastForwardBy(base::Seconds(15));
  network::TestURLLoaderFactory::PendingRequest* pending_request =
      GetPendingRequest();
  EXPECT_THAT(pending_request, NotNull());

  EXPECT_CALL(completion_callback,
              Run(TrustedVaultRequest::HttpStatus::kSuccess, kResponseBody));
  EXPECT_TRUE(RespondToHttpRequest(net::OK, net::HTTP_OK, kResponseBody));
}

TEST_F(TrustedVaultRequestTest, ShouldStopRetryingAndReportTransientError) {
  base::MockCallback<TrustedVaultRequest::CompletionCallback>
      completion_callback;
  std::unique_ptr<TrustedVaultRequest> request =
      StartNewRequestWithAccessTokenAndRetriesDuration(
          kAccessToken, TrustedVaultRequest::HttpMethod::kGet,
          /*request_body=*/std::nullopt,
          /*max_retry_duration=*/base::Minutes(1), completion_callback.Get());

  // Mimic network error for the first request with significant delay,
  // sufficient to stop retries.
  task_environment().FastForwardBy(base::Minutes(2));
  EXPECT_CALL(completion_callback,
              Run(TrustedVaultRequest::HttpStatus::kNetworkError, _));
  EXPECT_TRUE(RespondToHttpRequest(net::ERR_DNS_REQUEST_CANCELLED, net::HTTP_OK,
                                   /*response_body=*/""));
  testing::Mock::VerifyAndClearExpectations(&completion_callback);
}

TEST_F(TrustedVaultRequestTest, ShouldHandleAccessTokenFetchingFailures) {
  base::flat_map<TrustedVaultAccessTokenFetcher::FetchingError,
                 TrustedVaultRequest::HttpStatus>
      fetching_error_to_http_status = {
          {TrustedVaultAccessTokenFetcher::FetchingError::kTransientAuthError,
           TrustedVaultRequest::HttpStatus::kTransientAccessTokenFetchError},
          {TrustedVaultAccessTokenFetcher::FetchingError::kPersistentAuthError,
           TrustedVaultRequest::HttpStatus::kPersistentAccessTokenFetchError},
          {TrustedVaultAccessTokenFetcher::FetchingError::kNotPrimaryAccount,
           TrustedVaultRequest::HttpStatus::
               kPrimaryAccountChangeAccessTokenFetchError}};

  for (const auto& [fetching_error, expected_http_status] :
       fetching_error_to_http_status) {
    base::HistogramTester histogram_tester;

    base::MockCallback<TrustedVaultRequest::CompletionCallback>
        completion_callback;
    // Access token fetching failure propagated immediately in this test, so
    // |completion_callback| should be called immediately as well.
    EXPECT_CALL(completion_callback, Run(expected_http_status, _));
    std::unique_ptr<TrustedVaultRequest> request =
        StartNewRequestWithAccessTokenError(fetching_error,
                                            completion_callback.Get());
    histogram_tester.ExpectUniqueSample(
        /*name=*/"TrustedVault.AccessTokenFetchSuccess." +
            GetSecurityDomainNameForUma(GetSecurityDomainId()),
        /*sample=*/false,
        /*expected_bucket_count=*/1);
  }
}

}  // namespace trusted_vault
