chromium/chrome/browser/fast_checkout/fast_checkout_capabilities_fetcher_impl_unittest.cc

// Copyright 2023 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/fast_checkout/fast_checkout_capabilities_fetcher_impl.h"

#include "base/test/metrics/histogram_tester.h"
#include "base/test/task_environment.h"
#include "chrome/browser/fast_checkout/fast_checkout_funnels.pb.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gmock/include/gmock/gmock.h"

namespace {
constexpr char kFastCheckoutFunnelsUrl[] =
    "https://www.gstatic.com/autofill/fast_checkout/funnels.binarypb";
constexpr char kInvalidResponseBody[] = "invalid response body";
constexpr char kDomain[] = "https://www.example.com";
constexpr char kDomainWithoutTriggerForm[] = "https://www.example3.com";
constexpr char kUnsupportedDomain[] = "https://www.example2.com";
constexpr char kInvalidDomain[] = "invaliddomain";
constexpr char kNonHttpSDomain[] = "file://path/to/a/file";
const auto kSupportedDomains =
    std::vector<std::string>({kDomain, kInvalidDomain, kNonHttpSDomain});
constexpr autofill::FormSignature kTriggerFormSignature =
    autofill::FormSignature(1234567890UL);
constexpr autofill::FormSignature kFillFormSignature =
    autofill::FormSignature(9876543210UL);
constexpr char kUmaKeyCacheStateIsTriggerFormSupported[] =
    "Autofill.FastCheckout.CapabilitiesFetcher."
    "CacheStateForIsTriggerFormSupported";
constexpr char kUmaKeyParsingResult[] =
    "Autofill.FastCheckout.CapabilitiesFetcher.ParsingResult";
constexpr char kUmaKeyResponseAndNetErrorCode[] =
    "Autofill.FastCheckout.CapabilitiesFetcher.HttpResponseAndNetErrorCode";
constexpr char kUmaKeyResponseTime[] =
    "Autofill.FastCheckout.CapabilitiesFetcher.ResponseTime";

std::string CreateBinaryProtoResponse() {
  ::fast_checkout::FastCheckoutFunnels funnels;
  for (const std::string& domain : kSupportedDomains) {
    ::fast_checkout::FastCheckoutFunnels_FastCheckoutFunnel* funnel =
        funnels.add_funnels();
    funnel->add_domains(domain);
    funnel->add_trigger(kTriggerFormSignature.value());
    funnel->add_fill(kFillFormSignature.value());
  }
  // Add one additional funnel with empty `trigger` field.
  ::fast_checkout::FastCheckoutFunnels_FastCheckoutFunnel* funnel =
      funnels.add_funnels();
  funnel->add_domains(kDomainWithoutTriggerForm);
  funnel->add_fill(kFillFormSignature.value());
  return funnels.SerializeAsString();
}
}  // namespace

class FastCheckoutCapabilitiesFetcherImplTest
    : public ChromeRenderViewHostTestHarness {
 public:
  FastCheckoutCapabilitiesFetcherImplTest()
      : ChromeRenderViewHostTestHarness(
            base::test::TaskEnvironment::TimeSource::MOCK_TIME) {
    origin_ = url::Origin::Create(GURL(kDomain));
    unsupported_origin_ = url::Origin::Create(GURL(kUnsupportedDomain));
    invalid_origin_ = url::Origin::Create(GURL(kInvalidDomain));
    non_http_s_origin_ = url::Origin::Create(GURL(kNonHttpSDomain));
    origin_without_trigger_form_ =
        url::Origin::Create(GURL(kDomainWithoutTriggerForm));
  }

 protected:
  void SetUp() override {
    content::RenderViewHostTestHarness::SetUp();

    test_url_loader_factory_ =
        std::make_unique<network::TestURLLoaderFactory>();
    test_shared_loader_factory_ =
        base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
            test_url_loader_factory_.get());
    fetcher_ = std::make_unique<FastCheckoutCapabilitiesFetcherImpl>(
        test_shared_loader_factory_);

    binary_proto_response_ = CreateBinaryProtoResponse();
  }

  network::TestURLLoaderFactory* url_loader_factory() {
    return test_url_loader_factory_.get();
  }
  FastCheckoutCapabilitiesFetcherImpl* fetcher() { return fetcher_.get(); }

  void FastForwardBy(base::TimeDelta delta) {
    task_environment()->FastForwardBy(delta);
  }

  base::HistogramTester& histogram_tester() { return histogram_tester_; }
  const std::string& GetBinaryProtoResponse() { return binary_proto_response_; }
  const url::Origin& GetOrigin() { return origin_; }
  const url::Origin& GetUnsupportedOrigin() { return unsupported_origin_; }
  const url::Origin& GetInvalidOrigin() { return invalid_origin_; }
  const url::Origin& GetNonHttpSOrigin() { return non_http_s_origin_; }
  const url::Origin& GetOriginWithoutTriggerForm() {
    return origin_without_trigger_form_;
  }

  bool FetchCapabilitiesAndSimulateResponse(
      net::HttpStatusCode status = net::HTTP_OK) {
    fetcher()->FetchCapabilities();
    return url_loader_factory()->SimulateResponseForPendingRequest(
        kFastCheckoutFunnelsUrl, GetBinaryProtoResponse(), status);
  }

  bool IsTriggerFormSupportedOnSupportedDomain() {
    return fetcher()->IsTriggerFormSupported(GetOrigin(),
                                             kTriggerFormSignature);
  }

 private:
  std::unique_ptr<network::TestURLLoaderFactory> test_url_loader_factory_;
  scoped_refptr<network::SharedURLLoaderFactory> test_shared_loader_factory_;
  std::unique_ptr<FastCheckoutCapabilitiesFetcherImpl> fetcher_;
  base::HistogramTester histogram_tester_;
  std::string binary_proto_response_;
  url::Origin origin_;
  url::Origin unsupported_origin_;
  url::Origin invalid_origin_;
  url::Origin non_http_s_origin_;
  url::Origin origin_without_trigger_form_;
};

TEST_F(FastCheckoutCapabilitiesFetcherImplTest,
       FetchCapabilities_AllowsOnlyOnePendingRequest) {
  fetcher()->FetchCapabilities();
  fetcher()->FetchCapabilities();
  EXPECT_EQ(url_loader_factory()->NumPending(), 1);
}

TEST_F(FastCheckoutCapabilitiesFetcherImplTest,
       FetchCapabilities_OnlyFetchesIfCacheTimeouted) {
  // Fetch funnels successfully.
  EXPECT_TRUE(FetchCapabilitiesAndSimulateResponse());

  // Not able to perform another fetch because cache is still valid.
  fetcher()->FetchCapabilities();
  EXPECT_EQ(url_loader_factory()->NumPending(), 0);

  // Travel through time beyond cache timeout.
  FastForwardBy(base::TimeDelta(base::Minutes(11)));

  // `FetchCapabilities()` is possible again because cache got stale.
  fetcher()->FetchCapabilities();
  EXPECT_EQ(url_loader_factory()->NumPending(), 1);
}

TEST_F(FastCheckoutCapabilitiesFetcherImplTest,
       IsTriggerFormSupported_ReturnsTrueOnlyForTriggerFormOnSupportedDomain) {
  EXPECT_TRUE(FetchCapabilitiesAndSimulateResponse());
  EXPECT_TRUE(IsTriggerFormSupportedOnSupportedDomain());
  EXPECT_FALSE(
      fetcher()->IsTriggerFormSupported(GetOrigin(), kFillFormSignature));
}

TEST_F(FastCheckoutCapabilitiesFetcherImplTest,
       IsTriggerFormSupported_ReturnsFalseOnUnsupportedDomain) {
  EXPECT_TRUE(FetchCapabilitiesAndSimulateResponse());
  EXPECT_FALSE(fetcher()->IsTriggerFormSupported(GetUnsupportedOrigin(),
                                                 kTriggerFormSignature));
  // `IsTriggerFormSupported()` on the supported domain should still return
  // `true` (for `kTriggerFormSignature).
  EXPECT_TRUE(IsTriggerFormSupportedOnSupportedDomain());
}

TEST_F(FastCheckoutCapabilitiesFetcherImplTest,
       IsTriggerFormSupported_ReturnsFalseOnInvalidDomain) {
  EXPECT_TRUE(FetchCapabilitiesAndSimulateResponse());
  EXPECT_FALSE(fetcher()->IsTriggerFormSupported(GetInvalidOrigin(),
                                                 kTriggerFormSignature));
  // `IsTriggerFormSupported()` on the supported domain should still return
  // `true` (for `kTriggerFormSignature).
  EXPECT_TRUE(IsTriggerFormSupportedOnSupportedDomain());
}

TEST_F(FastCheckoutCapabilitiesFetcherImplTest,
       IsTriggerFormSupported_ReturnsFalseOnNonHttpSDomain) {
  EXPECT_TRUE(FetchCapabilitiesAndSimulateResponse());
  EXPECT_FALSE(fetcher()->IsTriggerFormSupported(GetNonHttpSOrigin(),
                                                 kTriggerFormSignature));
  // `IsTriggerFormSupported()` on the supported domain should still return
  // `true` (for `kTriggerFormSignature).
  EXPECT_TRUE(IsTriggerFormSupportedOnSupportedDomain());
}

TEST_F(FastCheckoutCapabilitiesFetcherImplTest,
       IsTriggerFormSupported_ReturnsFalseIfRequestFailed) {
  net::HttpStatusCode http_status = net::HTTP_BAD_REQUEST;

  EXPECT_TRUE(FetchCapabilitiesAndSimulateResponse(http_status));
  EXPECT_FALSE(IsTriggerFormSupportedOnSupportedDomain());

  histogram_tester().ExpectUniqueSample(kUmaKeyResponseAndNetErrorCode,
                                        http_status, 1u);
}

TEST_F(FastCheckoutCapabilitiesFetcherImplTest,
       OnFetchComplete_RecordsResponseTime) {
  EXPECT_TRUE(FetchCapabilitiesAndSimulateResponse());

  histogram_tester().ExpectTotalCount(kUmaKeyResponseTime, 1u);
}

TEST_F(FastCheckoutCapabilitiesFetcherImplTest,
       OnFetchComplete_SuccessfulRequest_RecordsHttpOkCode) {
  EXPECT_TRUE(FetchCapabilitiesAndSimulateResponse());

  histogram_tester().ExpectUniqueSample(kUmaKeyResponseAndNetErrorCode,
                                        net::HTTP_OK, 1u);
}

TEST_F(FastCheckoutCapabilitiesFetcherImplTest,
       OnFetchComplete_ValidResponseBody_RecordsSucessfulParsing) {
  EXPECT_TRUE(FetchCapabilitiesAndSimulateResponse());

  histogram_tester().ExpectUniqueSample(
      kUmaKeyParsingResult,
      FastCheckoutCapabilitiesFetcherImpl::ParsingResult::kSuccess, 1u);
}

TEST_F(FastCheckoutCapabilitiesFetcherImplTest,
       OnFetchComplete_InvalidResponseBody_RecordsParsingError) {
  fetcher()->FetchCapabilities();
  EXPECT_TRUE(url_loader_factory()->SimulateResponseForPendingRequest(
      kFastCheckoutFunnelsUrl, kInvalidResponseBody));

  histogram_tester().ExpectUniqueSample(
      kUmaKeyParsingResult,
      FastCheckoutCapabilitiesFetcherImpl::ParsingResult::kParsingError, 1u);
}

TEST_F(
    FastCheckoutCapabilitiesFetcherImplTest,
    IsTriggerFormSupported_TriggerFormSignature_RecordsTriggerFormSupported) {
  EXPECT_TRUE(FetchCapabilitiesAndSimulateResponse());
  EXPECT_TRUE(IsTriggerFormSupportedOnSupportedDomain());

  histogram_tester().ExpectUniqueSample(
      kUmaKeyCacheStateIsTriggerFormSupported,
      FastCheckoutCapabilitiesFetcherImpl::CacheStateForIsTriggerFormSupported::
          kEntryAvailableAndFormSupported,
      1u);
}

TEST_F(
    FastCheckoutCapabilitiesFetcherImplTest,
    IsTriggerFormSupported_FillFormSignature_RecordsTriggerFormNotSupported) {
  EXPECT_TRUE(FetchCapabilitiesAndSimulateResponse());
  EXPECT_FALSE(
      fetcher()->IsTriggerFormSupported(GetOrigin(), kFillFormSignature));

  histogram_tester().ExpectUniqueSample(
      kUmaKeyCacheStateIsTriggerFormSupported,
      FastCheckoutCapabilitiesFetcherImpl::CacheStateForIsTriggerFormSupported::
          kEntryAvailableAndFormNotSupported,
      1u);
}

TEST_F(FastCheckoutCapabilitiesFetcherImplTest,
       IsTriggerFormSupported_FetchOngoing_RecordsFetchOngoing) {
  fetcher()->FetchCapabilities();
  EXPECT_FALSE(IsTriggerFormSupportedOnSupportedDomain());

  histogram_tester().ExpectUniqueSample(
      kUmaKeyCacheStateIsTriggerFormSupported,
      FastCheckoutCapabilitiesFetcherImpl::CacheStateForIsTriggerFormSupported::
          kFetchOngoing,
      1u);
}

TEST_F(FastCheckoutCapabilitiesFetcherImplTest,
       IsTriggerFormSupported_InvalidDomain_RecordsNotAvailable) {
  EXPECT_TRUE(FetchCapabilitiesAndSimulateResponse());
  fetcher()->IsTriggerFormSupported(GetInvalidOrigin(), kTriggerFormSignature);

  histogram_tester().ExpectUniqueSample(
      kUmaKeyCacheStateIsTriggerFormSupported,
      FastCheckoutCapabilitiesFetcherImpl::CacheStateForIsTriggerFormSupported::
          kEntryNotAvailable,
      1u);
}

TEST_F(FastCheckoutCapabilitiesFetcherImplTest,
       GetFormsToFill_InvalidDomain_ReturnsEmptySet) {
  EXPECT_TRUE(FetchCapabilitiesAndSimulateResponse());
  EXPECT_THAT(fetcher()->GetFormsToFill(GetInvalidOrigin()),
              testing::IsEmpty());
}

TEST_F(FastCheckoutCapabilitiesFetcherImplTest,
       GetFormsToFill_ValidDomain_ReturnsTriggerAndFillForms) {
  EXPECT_TRUE(FetchCapabilitiesAndSimulateResponse());

  base::flat_set<autofill::FormSignature> forms_to_fill =
      fetcher()->GetFormsToFill(GetOrigin());
  EXPECT_THAT(forms_to_fill, testing::UnorderedElementsAre(
                                 kTriggerFormSignature, kFillFormSignature));
}

TEST_F(FastCheckoutCapabilitiesFetcherImplTest, NoTriggerForm) {
  EXPECT_TRUE(FetchCapabilitiesAndSimulateResponse());

  EXPECT_FALSE(fetcher()->IsTriggerFormSupported(GetOriginWithoutTriggerForm(),
                                                 kTriggerFormSignature));
  EXPECT_THAT(fetcher()->GetFormsToFill(GetOriginWithoutTriggerForm()),
              testing::IsEmpty());
  histogram_tester().ExpectUniqueSample(
      kUmaKeyCacheStateIsTriggerFormSupported,
      FastCheckoutCapabilitiesFetcherImpl::CacheStateForIsTriggerFormSupported::
          kEntryNotAvailable,
      1u);
}