// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

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

#include "base/containers/flat_set.h"
#include "base/containers/to_vector.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "components/cbor/reader.h"
#include "components/cbor/values.h"
#include "device/bluetooth/bluetooth_adapter_factory.h"
#include "device/bluetooth/test/mock_bluetooth_adapter.h"
#include "device/fido/authenticator_get_assertion_response.h"
#include "device/fido/ctap_get_assertion_request.h"
#include "device/fido/ctap_make_credential_request.h"
#include "device/fido/device_response_converter.h"
#include "device/fido/fake_fido_discovery.h"
#include "device/fido/fido_device_authenticator.h"
#include "device/fido/fido_discovery_base.h"
#include "device/fido/fido_request_handler_base.h"
#include "device/fido/fido_test_data.h"
#include "device/fido/get_assertion_request_handler.h"
#include "device/fido/make_credential_task.h"
#include "device/fido/mock_fido_device.h"
#include "device/fido/public/fido_constants.h"
#include "device/fido/public/fido_transport_protocol.h"
#include "device/fido/public/fido_types.h"
#include "device/fido/public/public_key_credential_descriptor.h"
#include "device/fido/u2f_command_constructor.h"
#include "device/fido/virtual_ctap2_device.h"
#include "device/fido/virtual_fido_device.h"
#include "device/fido/virtual_fido_device_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

#if BUILDFLAG(IS_WIN)
#include "device/fido/hid/fake_hid_impl_for_testing.h"
#include "device/fido/win/fake_webauthn_api.h"
#endif  // BUILDFLAG(IS_WIN)

#if BUILDFLAG(IS_CHROMEOS)
#include "chromeos/dbus/u2f/u2f_client.h"
#endif

namespace device {

namespace {

constexpr char kRequestTransportHistogram[] =
    "WebAuthentication.GetAssertionRequestTransport";
constexpr char kResponseTransportHistogram[] =
    "WebAuthentication.GetAssertionResponseTransport";

using TestGetAssertionRequestFuture = base::test::TestFuture<
    GetAssertionStatus,
    std::optional<std::vector<AuthenticatorGetAssertionResponse>>,
    FidoAuthenticator*>;

MATCHER_P(CborRequestHasCrossDeviceFallbackUrl, expected, "") {
  if (arg.empty()) {
    return false;
  }
  if (arg[0] !=
      static_cast<uint8_t>(CtapRequestCommand::kAuthenticatorGetAssertion)) {
    return false;
  }
  base::span<const uint8_t> cbor_data =
      base::span<const uint8_t>(arg).subspan(1u);
  std::optional<cbor::Value> val = cbor::Reader::Read(cbor_data);
  if (!val || !val->is_map()) {
    return false;
  }
  const auto& map = val->GetMap();

  std::optional<std::string> expected_opt(expected);

  auto ext_it = map.find(cbor::Value(4));
  if (ext_it == map.end()) {
    return !expected_opt.has_value();
  }
  if (!ext_it->second.is_map()) {
    return false;
  }
  const auto& ext_map = ext_it->second.GetMap();

  auto url_it = ext_map.find(cbor::Value("crossDeviceFallbackUrl"));
  if (url_it == ext_map.end()) {
    return !expected_opt.has_value();
  }
  if (!url_it->second.is_string()) {
    return false;
  }

  return expected_opt.has_value() &&
         url_it->second.GetString() == *expected_opt;
}

}  // namespace

using testing::_;

// FidoGetAssertionHandlerTest allows testing GetAssertionRequestHandler against
// MockFidoDevices injected via a FakeFidoDiscoveryFactory.
class FidoGetAssertionHandlerTest : public ::testing::Test {
 public:
  void SetUp() override {
    bluetooth_config_->SetLESupported(true);
    BluetoothAdapterFactory::SetAdapterForTesting(mock_adapter_);

#if BUILDFLAG(IS_CHROMEOS)
    chromeos::U2FClient::InitializeFake();
#endif
  }

  void TearDown() override {
#if BUILDFLAG(IS_CHROMEOS)
    task_environment_.RunUntilIdle();
    chromeos::U2FClient::Shutdown();
#endif
  }

  void ForgeDiscoveries() {
    discovery_ = fake_discovery_factory_->ForgeNextHidDiscovery()->GetWeakPtr();
    cable_discovery_ =
        fake_discovery_factory_->ForgeNextCableDiscovery()->GetWeakPtr();
    nfc_discovery_ =
        fake_discovery_factory_->ForgeNextNfcDiscovery()->GetWeakPtr();
    platform_discovery_ =
        fake_discovery_factory_->ForgeNextPlatformDiscovery()->GetWeakPtr();
  }

  std::unique_ptr<GetAssertionRequestHandler> CreateGetAssertionHandlerU2f() {
    CtapGetAssertionRequest request(test_data::kRelyingPartyId,
                                    test_data::kClientDataJson);
    request.allow_list = {PublicKeyCredentialDescriptor(
        CredentialType::kPublicKey,
        base::ToVector(test_data::kU2fSignKeyHandle))};
    return CreateGetAssertionHandlerWithRequest(std::move(request));
  }

  std::unique_ptr<GetAssertionRequestHandler> CreateGetAssertionHandlerCtap() {
    CtapGetAssertionRequest request(test_data::kRelyingPartyId,
                                    test_data::kClientDataJson);
    request.allow_list = {PublicKeyCredentialDescriptor(
        CredentialType::kPublicKey,
        base::ToVector(test_data::kTestGetAssertionCredentialId))};
    return CreateGetAssertionHandlerWithRequest(std::move(request));
  }

  std::unique_ptr<GetAssertionRequestHandler>
  CreateGetAssertionHandlerWithRequest(CtapGetAssertionRequest request) {
    ForgeDiscoveries();

    auto handler = std::make_unique<GetAssertionRequestHandler>(
        fake_discovery_factory_.get(),
        std::vector<std::unique_ptr<FidoDiscoveryBase>>(),
        supported_transports_, std::move(request), CtapGetAssertionOptions(),
        /*allow_skipping_pin_touch=*/true, get_assertion_future_.GetCallback());
    return handler;
  }

  std::unique_ptr<GetAssertionRequestHandler>
  CreateGetAssertionHandlerWithRequestedTransports(
      std::vector<std::vector<FidoTransportProtocol>> transports) {
    CtapGetAssertionRequest request(test_data::kRelyingPartyId,
                                    test_data::kClientDataJson);
    for (uint8_t i = 0; i < transports.size(); ++i) {
      request.allow_list.emplace_back(CredentialType::kPublicKey,
                                      std::vector<uint8_t>{i});
      request.allow_list.back().transports = transports[i];
    }
    return CreateGetAssertionHandlerWithRequest(std::move(request));
  }

  void ExpectAllowedTransportsForRequestAre(
      GetAssertionRequestHandler* request_handler,
      base::flat_set<FidoTransportProtocol> transports) {
    using Transport = FidoTransportProtocol;
    if (transports.contains(Transport::kUsbHumanInterfaceDevice))
      discovery()->WaitForCallToStartAndSimulateSuccess();
    if (transports.contains(Transport::kHybrid))
      cable_discovery()->WaitForCallToStartAndSimulateSuccess();
    if (transports.contains(Transport::kNearFieldCommunication))
      nfc_discovery()->WaitForCallToStartAndSimulateSuccess();
    if (transports.contains(Transport::kInternal))
      platform_discovery()->WaitForCallToStartAndSimulateSuccess();

    task_environment_.FastForwardUntilNoTasksRemain();
    EXPECT_FALSE(get_assertion_future().IsReady());

    if (!transports.contains(Transport::kUsbHumanInterfaceDevice))
      EXPECT_FALSE(discovery()->is_start_requested());
    if (!transports.contains(Transport::kHybrid))
      EXPECT_FALSE(cable_discovery()->is_start_requested());
    if (!transports.contains(Transport::kNearFieldCommunication))
      EXPECT_FALSE(nfc_discovery()->is_start_requested());
    if (!transports.contains(Transport::kInternal))
      EXPECT_FALSE(platform_discovery()->is_start_requested());

    EXPECT_THAT(
        request_handler->transport_availability_info().available_transports,
        ::testing::UnorderedElementsAreArray(transports));
  }

  test::FakeFidoDiscovery* discovery() const { return discovery_.get(); }
  test::FakeFidoDiscovery* cable_discovery() const {
    return cable_discovery_.get();
  }
  test::FakeFidoDiscovery* nfc_discovery() const {
    return nfc_discovery_.get();
  }
  test::FakeFidoDiscovery* platform_discovery() const {
    return platform_discovery_.get();
  }
  TestGetAssertionRequestFuture& get_assertion_future() {
    return get_assertion_future_;
  }

  void set_supported_transports(
      base::flat_set<FidoTransportProtocol> transports) {
    supported_transports_ = std::move(transports);
  }

 protected:
  base::test::TaskEnvironment task_environment_{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
  std::unique_ptr<test::FakeFidoDiscoveryFactory> fake_discovery_factory_ =
      std::make_unique<test::FakeFidoDiscoveryFactory>();
  base::WeakPtr<test::FakeFidoDiscovery> discovery_;
  base::WeakPtr<test::FakeFidoDiscovery> cable_discovery_;
  base::WeakPtr<test::FakeFidoDiscovery> nfc_discovery_;
  base::WeakPtr<test::FakeFidoDiscovery> platform_discovery_;
  scoped_refptr<::testing::NiceMock<MockBluetoothAdapter>> mock_adapter_ =
      base::MakeRefCounted<::testing::NiceMock<MockBluetoothAdapter>>();
  TestGetAssertionRequestFuture get_assertion_future_;
  base::flat_set<FidoTransportProtocol> supported_transports_ = {
      FidoTransportProtocol::kUsbHumanInterfaceDevice,
      FidoTransportProtocol::kInternal,
      FidoTransportProtocol::kNearFieldCommunication,
      FidoTransportProtocol::kHybrid};
  std::unique_ptr<BluetoothAdapterFactory::GlobalOverrideValues>
      bluetooth_config_ =
          BluetoothAdapterFactory::Get()->InitGlobalOverrideValues();
  FidoRequestHandlerBase::ScopedAlwaysAllowBLECalls always_allow_ble_calls_;
};

TEST_F(FidoGetAssertionHandlerTest, TransportAvailabilityInfo) {
  {
    // Empty allow list.
    auto request_handler = CreateGetAssertionHandlerWithRequestedTransports({});
    EXPECT_EQ(FidoRequestType::kGetAssertion,
              request_handler->transport_availability_info().request_type);
    EXPECT_FALSE(request_handler->transport_availability_info()
                     .transport_list_did_include_internal);
    EXPECT_FALSE(request_handler->transport_availability_info()
                     .transport_list_did_include_hybrid);
    EXPECT_FALSE(request_handler->transport_availability_info()
                     .transport_list_did_include_security_key);
    EXPECT_TRUE(
        request_handler->transport_availability_info().has_empty_allow_list);
    EXPECT_FALSE(request_handler->transport_availability_info()
                     .request_is_internal_only);
  }
  {
    // Internal and a phone.
    auto request_handler = CreateGetAssertionHandlerWithRequestedTransports(
        {{FidoTransportProtocol::kInternal},
         {FidoTransportProtocol::kInternal, FidoTransportProtocol::kHybrid}});
    EXPECT_EQ(FidoRequestType::kGetAssertion,
              request_handler->transport_availability_info().request_type);
    EXPECT_TRUE(request_handler->transport_availability_info()
                    .transport_list_did_include_internal);
    EXPECT_TRUE(request_handler->transport_availability_info()
                    .transport_list_did_include_hybrid);
    EXPECT_FALSE(request_handler->transport_availability_info()
                     .transport_list_did_include_security_key);
    EXPECT_FALSE(
        request_handler->transport_availability_info().has_empty_allow_list);
    EXPECT_FALSE(request_handler->transport_availability_info()
                     .request_is_internal_only);
  }
  {
    // Internal, a phone, and USB.
    auto request_handler = CreateGetAssertionHandlerWithRequestedTransports(
        {{FidoTransportProtocol::kUsbHumanInterfaceDevice},
         {FidoTransportProtocol::kInternal},
         {FidoTransportProtocol::kInternal, FidoTransportProtocol::kHybrid}});
    EXPECT_EQ(FidoRequestType::kGetAssertion,
              request_handler->transport_availability_info().request_type);
    EXPECT_TRUE(request_handler->transport_availability_info()
                    .transport_list_did_include_internal);
    EXPECT_TRUE(request_handler->transport_availability_info()
                    .transport_list_did_include_hybrid);
    EXPECT_TRUE(request_handler->transport_availability_info()
                    .transport_list_did_include_security_key);
    EXPECT_FALSE(
        request_handler->transport_availability_info().has_empty_allow_list);
    EXPECT_FALSE(request_handler->transport_availability_info()
                     .request_is_internal_only);
  }
  {
    // Only USB.
    auto request_handler = CreateGetAssertionHandlerWithRequestedTransports(
        {{FidoTransportProtocol::kUsbHumanInterfaceDevice}});
    EXPECT_EQ(FidoRequestType::kGetAssertion,
              request_handler->transport_availability_info().request_type);
    EXPECT_FALSE(request_handler->transport_availability_info()
                     .transport_list_did_include_internal);
    EXPECT_FALSE(request_handler->transport_availability_info()
                     .transport_list_did_include_hybrid);
    EXPECT_TRUE(request_handler->transport_availability_info()
                    .transport_list_did_include_security_key);
    EXPECT_FALSE(
        request_handler->transport_availability_info().has_empty_allow_list);
    EXPECT_FALSE(request_handler->transport_availability_info()
                     .request_is_internal_only);
  }
  {
    // A phone and an unknown (empty) transport credential.
    auto request_handler = CreateGetAssertionHandlerWithRequestedTransports(
        {{}, {FidoTransportProtocol::kHybrid}});
    EXPECT_EQ(FidoRequestType::kGetAssertion,
              request_handler->transport_availability_info().request_type);
    EXPECT_TRUE(request_handler->transport_availability_info()
                    .transport_list_did_include_internal);
    EXPECT_TRUE(request_handler->transport_availability_info()
                    .transport_list_did_include_hybrid);
    EXPECT_TRUE(request_handler->transport_availability_info()
                    .transport_list_did_include_security_key);
    EXPECT_FALSE(
        request_handler->transport_availability_info().has_empty_allow_list);
    EXPECT_FALSE(request_handler->transport_availability_info()
                     .request_is_internal_only);
  }
  {
    // Internal only.
    auto request_handler = CreateGetAssertionHandlerWithRequestedTransports(
        {{FidoTransportProtocol::kInternal},
         {FidoTransportProtocol::kInternal}});
    EXPECT_EQ(FidoRequestType::kGetAssertion,
              request_handler->transport_availability_info().request_type);
    EXPECT_TRUE(request_handler->transport_availability_info()
                    .transport_list_did_include_internal);
    EXPECT_FALSE(request_handler->transport_availability_info()
                     .transport_list_did_include_hybrid);
    EXPECT_FALSE(request_handler->transport_availability_info()
                     .transport_list_did_include_security_key);
    EXPECT_FALSE(
        request_handler->transport_availability_info().has_empty_allow_list);
    EXPECT_TRUE(request_handler->transport_availability_info()
                    .request_is_internal_only);
  }
}

TEST_F(FidoGetAssertionHandlerTest, CtapRequestOnSingleDevice) {
  auto request_handler = CreateGetAssertionHandlerCtap();
  discovery()->WaitForCallToStartAndSimulateSuccess();
  auto device = MockFidoDevice::MakeCtapWithGetInfoExpectation();
  device->ExpectCtap2CommandAndRespondWith(
      CtapRequestCommand::kAuthenticatorGetAssertion,
      test_data::kTestGetAssertionResponse);

  discovery()->AddDevice(std::move(device));
  EXPECT_TRUE(get_assertion_future().Wait());

  EXPECT_EQ(GetAssertionStatus::kSuccess,
            std::get<0>(get_assertion_future().Get()));
  EXPECT_TRUE(std::get<1>(get_assertion_future().Get()));
}

// Test a scenario where the connected authenticator is a U2F device.
TEST_F(FidoGetAssertionHandlerTest, TestU2fSign) {
  auto request_handler = CreateGetAssertionHandlerU2f();
  discovery()->WaitForCallToStartAndSimulateSuccess();

  auto device = MockFidoDevice::MakeU2fWithGetInfoExpectation();
  device->ExpectRequestAndRespondWith(
      test_data::kU2fSignCommandApdu,
      test_data::kApduEncodedNoErrorSignResponse);

  discovery()->AddDevice(std::move(device));
  task_environment_.FastForwardUntilNoTasksRemain();
  EXPECT_EQ(GetAssertionStatus::kSuccess,
            std::get<0>(get_assertion_future().Get()));
  EXPECT_TRUE(std::get<1>(get_assertion_future().Get()));
}

TEST_F(FidoGetAssertionHandlerTest, TestIncompatibleUserVerificationSetting) {
  auto request = CtapGetAssertionRequest(test_data::kRelyingPartyId,
                                         test_data::kClientDataJson);
  request.user_verification = UserVerificationRequirement::kRequired;
  auto request_handler =
      CreateGetAssertionHandlerWithRequest(std::move(request));
  discovery()->WaitForCallToStartAndSimulateSuccess();

  auto device = MockFidoDevice::MakeCtapWithGetInfoExpectation(
      test_data::kTestGetInfoResponseWithoutUvSupport);
  device->ExpectRequestAndRespondWith(
      MockFidoDevice::EncodeCBORRequest(AsCTAPRequestValuePair(
          MakeCredentialTask::GetTouchRequest(device.get()))),
      test_data::kTestMakeCredentialResponse);

  discovery()->AddDevice(std::move(device));

  task_environment_.FastForwardUntilNoTasksRemain();
  EXPECT_EQ(GetAssertionStatus::kAuthenticatorMissingUserVerification,
            std::get<0>(get_assertion_future().Get()));
}

TEST_F(FidoGetAssertionHandlerTest,
       TestU2fSignRequestWithUserVerificationRequired) {
  auto request = CtapGetAssertionRequest(test_data::kRelyingPartyId,
                                         test_data::kClientDataJson);
  request.allow_list = {PublicKeyCredentialDescriptor(
      CredentialType::kPublicKey,
      base::ToVector(test_data::kU2fSignKeyHandle))};
  request.user_verification = UserVerificationRequirement::kRequired;
  auto request_handler =
      CreateGetAssertionHandlerWithRequest(std::move(request));
  discovery()->WaitForCallToStartAndSimulateSuccess();

  auto device = MockFidoDevice::MakeU2fWithGetInfoExpectation();
  device->ExpectRequestAndRespondWith(
      ConstructBogusU2fRegistrationCommand(),
      test_data::kApduEncodedNoErrorRegisterResponse);
  discovery()->AddDevice(std::move(device));

  task_environment_.FastForwardUntilNoTasksRemain();
  EXPECT_EQ(GetAssertionStatus::kAuthenticatorMissingUserVerification,
            std::get<0>(get_assertion_future().Get()));
}

TEST_F(FidoGetAssertionHandlerTest, IncorrectRpIdHash) {
  auto request_handler =
      CreateGetAssertionHandlerWithRequest(CtapGetAssertionRequest(
          test_data::kRelyingPartyId, test_data::kClientDataJson));
  discovery()->WaitForCallToStartAndSimulateSuccess();
  auto device = MockFidoDevice::MakeCtapWithGetInfoExpectation();
  device->ExpectCtap2CommandAndRespondWith(
      CtapRequestCommand::kAuthenticatorGetAssertion,
      test_data::kTestGetAssertionResponseWithIncorrectRpIdHash);

  discovery()->AddDevice(std::move(device));

  EXPECT_TRUE(get_assertion_future().Wait());
  EXPECT_EQ(GetAssertionStatus::kAuthenticatorResponseInvalid,
            std::get<0>(get_assertion_future().Get()));
}

// Tests a scenario where the authenticator responds with credential ID that
// is not included in the allowed list.
TEST_F(FidoGetAssertionHandlerTest, InvalidCredential) {
  CtapGetAssertionRequest request(test_data::kRelyingPartyId,
                                  test_data::kClientDataJson);
  request.allow_list = {PublicKeyCredentialDescriptor(
      CredentialType::kPublicKey, base::ToVector(test_data::kKeyHandleAlpha))};
  auto request_handler =
      CreateGetAssertionHandlerWithRequest(std::move(request));
  discovery()->WaitForCallToStartAndSimulateSuccess();
  // Resident Keys must be disabled, otherwise allow list check is skipped.
  auto device = MockFidoDevice::MakeCtapWithGetInfoExpectation(
      test_data::kTestGetInfoResponseWithoutResidentKeySupport);
  device->ExpectCtap2CommandAndRespondWith(
      CtapRequestCommand::kAuthenticatorGetAssertion,
      test_data::kTestGetAssertionResponse);

  discovery()->AddDevice(std::move(device));

  // The response with the invalid credential ID is considered to be an error at
  // the task level and the request handler will drop the authenticator.
  task_environment_.FastForwardUntilNoTasksRemain();
  EXPECT_FALSE(get_assertion_future().IsReady());
}

// Tests a scenario where the authenticator responds with an empty credential.
// When GetAssertion request only has a single credential in the allow list,
// this is a valid response. Check that credential is set by the client before
// the response is returned to the relying party.
TEST_F(FidoGetAssertionHandlerTest, ValidEmptyCredential) {
  auto request_handler = CreateGetAssertionHandlerCtap();
  discovery()->WaitForCallToStartAndSimulateSuccess();
  // Resident Keys must be disabled, otherwise allow list check is skipped.
  auto device = MockFidoDevice::MakeCtapWithGetInfoExpectation(
      test_data::kTestGetInfoResponseWithoutResidentKeySupport);
  device->ExpectCtap2CommandAndRespondWith(
      CtapRequestCommand::kAuthenticatorGetAssertion,
      test_data::kTestGetAssertionResponseWithEmptyCredential);
  discovery()->AddDevice(std::move(device));

  EXPECT_TRUE(get_assertion_future().Wait());
  const auto& response = std::get<1>(get_assertion_future().Get());
  EXPECT_EQ(GetAssertionStatus::kSuccess,
            std::get<0>(get_assertion_future().Get()));
  ASSERT_TRUE(response);
  ASSERT_EQ(1u, response->size());
  EXPECT_TRUE(response.value()[0].credential);
  EXPECT_THAT(
      response.value()[0].credential->id,
      ::testing::ElementsAreArray(test_data::kTestGetAssertionCredentialId));
}

TEST_F(FidoGetAssertionHandlerTest, TruncatedUTF8) {
  // Webauthn says[1] that authenticators may truncate strings in user entities.
  // Since authenticators aren't going to do UTF-8 processing, that means that
  // they may truncate a multi-byte code point and thus produce an invalid
  // string in the CBOR. This test exercises that case.
  //
  // [1] https://www.w3.org/TR/webauthn/#sctn-user-credential-params
  auto request_handler = CreateGetAssertionHandlerCtap();
  discovery()->WaitForCallToStartAndSimulateSuccess();
  auto device = MockFidoDevice::MakeCtapWithGetInfoExpectation(
      test_data::kTestCtap2OnlyAuthenticatorGetInfoResponse);
  device->ExpectCtap2CommandAndRespondWith(
      CtapRequestCommand::kAuthenticatorGetAssertion,
      test_data::kTestGetAssertionResponseWithTruncatedUTF8);
  discovery()->AddDevice(std::move(device));

  EXPECT_TRUE(get_assertion_future().Wait());
  const auto& response = std::get<1>(get_assertion_future().Get());
  EXPECT_EQ(GetAssertionStatus::kSuccess,
            std::get<0>(get_assertion_future().Get()));
  ASSERT_TRUE(response);
  ASSERT_EQ(1u, response->size());
  ASSERT_TRUE(response.value()[0].user_entity);
  EXPECT_EQ(63u, response.value()[0].user_entity->name->size());
}

TEST_F(FidoGetAssertionHandlerTest, TruncatedAndInvalidUTF8) {
  // This test exercises the case where a UTF-8 string is truncated in a
  // response, and the UTF-8 string contains invalid code-points that
  // |base::IsStringUTF8| will be unhappy with.
  auto request_handler = CreateGetAssertionHandlerCtap();
  discovery()->WaitForCallToStartAndSimulateSuccess();
  auto device = MockFidoDevice::MakeCtapWithGetInfoExpectation(
      test_data::kTestCtap2OnlyAuthenticatorGetInfoResponse);
  device->ExpectCtap2CommandAndRespondWith(
      CtapRequestCommand::kAuthenticatorGetAssertion,
      test_data::kTestGetAssertionResponseWithTruncatedAndInvalidUTF8);
  discovery()->AddDevice(std::move(device));

  task_environment_.FastForwardUntilNoTasksRemain();
  EXPECT_FALSE(get_assertion_future().IsReady());
}

// Tests a scenario where authenticator responds without user entity in its
// response but client is expecting a resident key credential.
TEST_F(FidoGetAssertionHandlerTest, IncorrectUserEntity) {
  // Use a GetAssertion request with an empty allow list.
  auto request_handler =
      CreateGetAssertionHandlerWithRequest(CtapGetAssertionRequest(
          test_data::kRelyingPartyId, test_data::kClientDataJson));
  discovery()->WaitForCallToStartAndSimulateSuccess();
  auto device = MockFidoDevice::MakeCtapWithGetInfoExpectation();
  device->ExpectCtap2CommandAndRespondWith(
      CtapRequestCommand::kAuthenticatorGetAssertion,
      test_data::kTestGetAssertionResponse);

  discovery()->AddDevice(std::move(device));

  EXPECT_TRUE(get_assertion_future().Wait());
  EXPECT_EQ(GetAssertionStatus::kAuthenticatorResponseInvalid,
            std::get<0>(get_assertion_future().Get()));
}

// If a device with transport type kInternal returns a
// CTAP2_ERR_OPERATION_DENIED error, the request should complete with
// GetAssertionStatus::kUserConsentDenied. Pending authenticators should be
// cancelled.
TEST_F(FidoGetAssertionHandlerTest,
       TestRequestWithOperationDeniedErrorPlatform) {
  auto request_handler = CreateGetAssertionHandlerCtap();

  auto platform_device = MockFidoDevice::MakeCtapWithGetInfoExpectation(
      test_data::kTestGetInfoResponsePlatformDevice);
  platform_device->SetDeviceTransport(FidoTransportProtocol::kInternal);
  platform_device->ExpectCtap2CommandAndRespondWithError(
      CtapRequestCommand::kAuthenticatorGetAssertion,
      CtapDeviceResponseCode::kCtap2ErrOperationDenied, base::Microseconds(10));
  platform_discovery()->WaitForCallToStartAndSimulateSuccess();
  platform_discovery()->AddDevice(std::move(platform_device));

  auto other_device = MockFidoDevice::MakeCtapWithGetInfoExpectation();
  other_device->ExpectCtap2CommandAndDoNotRespond(
      CtapRequestCommand::kAuthenticatorGetAssertion);
  EXPECT_CALL(*other_device, Cancel);

  discovery()->WaitForCallToStartAndSimulateSuccess();
  discovery()->AddDevice(std::move(other_device));

  task_environment_.FastForwardUntilNoTasksRemain();
  EXPECT_TRUE(get_assertion_future().IsReady());
  EXPECT_EQ(GetAssertionStatus::kUserConsentDenied,
            std::get<0>(get_assertion_future().Get()));
}

// Like |TestRequestWithOperationDeniedErrorPlatform|, but with a
// cross-platform device.
TEST_F(FidoGetAssertionHandlerTest,
       TestRequestWithOperationDeniedErrorCrossPlatform) {
  auto device = MockFidoDevice::MakeCtapWithGetInfoExpectation();
  device->ExpectCtap2CommandAndRespondWithError(
      CtapRequestCommand::kAuthenticatorGetAssertion,
      CtapDeviceResponseCode::kCtap2ErrOperationDenied);

  auto request_handler = CreateGetAssertionHandlerCtap();
  discovery()->WaitForCallToStartAndSimulateSuccess();
  discovery()->AddDevice(std::move(device));

  task_environment_.FastForwardUntilNoTasksRemain();
  EXPECT_TRUE(get_assertion_future().IsReady());
  EXPECT_EQ(GetAssertionStatus::kUserConsentDenied,
            std::get<0>(get_assertion_future().Get()));
}

// If a device returns CTAP2_ERR_PIN_AUTH_INVALID, the request should complete
// with GetAssertionStatus::kUserConsentDenied.
TEST_F(FidoGetAssertionHandlerTest, TestRequestWithPinAuthInvalid) {
  auto device = MockFidoDevice::MakeCtapWithGetInfoExpectation();
  device->ExpectCtap2CommandAndRespondWithError(
      CtapRequestCommand::kAuthenticatorGetAssertion,
      CtapDeviceResponseCode::kCtap2ErrPinAuthInvalid);

  auto request_handler = CreateGetAssertionHandlerCtap();
  discovery()->WaitForCallToStartAndSimulateSuccess();
  discovery()->AddDevice(std::move(device));

  task_environment_.FastForwardUntilNoTasksRemain();
  EXPECT_TRUE(get_assertion_future().IsReady());
  EXPECT_EQ(GetAssertionStatus::kUserConsentDenied,
            std::get<0>(get_assertion_future().Get()));
}

MATCHER_P(IsCtap2Command, expected_command, "") {
  return !arg.empty() && arg[0] == base::strict_cast<uint8_t>(expected_command);
}

TEST_F(FidoGetAssertionHandlerTest, DeviceFailsImmediately) {
  // Test that, when a device immediately returns an unexpected error, the
  // request continues and waits for another device.

  auto broken_device = MockFidoDevice::MakeCtapWithGetInfoExpectation();
  EXPECT_CALL(
      *broken_device,
      DeviceTransactPtr(
          IsCtap2Command(CtapRequestCommand::kAuthenticatorGetAssertion), _))
      .WillOnce(::testing::DoAll(
          ::testing::WithArg<1>(
              [this](FidoDevice::DeviceCallback& callback) {
                std::vector<uint8_t> response = {static_cast<uint8_t>(
                    CtapDeviceResponseCode::kCtap2ErrInvalidCBOR)};
                base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
                    FROM_HERE,
                    base::BindOnce(std::move(callback), std::move(response)));

                auto working_device =
                    MockFidoDevice::MakeCtapWithGetInfoExpectation();
                working_device->ExpectCtap2CommandAndRespondWith(
                    CtapRequestCommand::kAuthenticatorGetAssertion,
                    test_data::kTestGetAssertionResponse);
                discovery()->AddDevice(std::move(working_device));
              }),
          ::testing::Return(0)));

  auto request_handler = CreateGetAssertionHandlerCtap();
  discovery()->WaitForCallToStartAndSimulateSuccess();
  discovery()->AddDevice(std::move(broken_device));

  EXPECT_TRUE(get_assertion_future().Wait());
  EXPECT_EQ(GetAssertionStatus::kSuccess,
            std::get<0>(get_assertion_future().Get()));
}

TEST_F(FidoGetAssertionHandlerTest, PinUvAuthTokenPreTouchFailure) {
  VirtualCtap2Device::Config config;
  config.ctap2_versions = {Ctap2Version::kCtap2_1};
  config.pin_uv_auth_token_support = true;
  config.internal_uv_support = true;
  config.override_response_map[CtapRequestCommand::kAuthenticatorClientPin] =
      std::make_pair(device::CtapDeviceResponseCode::kCtap2ErrOther,
                     std::nullopt);
  auto state = base::MakeRefCounted<VirtualFidoDevice::State>();
  state->fingerprints_enrolled = true;

  CtapGetAssertionRequest request(test_data::kRelyingPartyId,
                                  test_data::kClientDataJson);
  request.allow_list = {PublicKeyCredentialDescriptor(
      CredentialType::kPublicKey,
      base::ToVector(test_data::kTestGetAssertionCredentialId))};
  request.user_verification = UserVerificationRequirement::kRequired;
  auto request_handler =
      CreateGetAssertionHandlerWithRequest(std::move(request));
  discovery()->WaitForCallToStartAndSimulateSuccess();
  discovery()->AddDevice(std::make_unique<VirtualCtap2Device>(
      std::move(state), std::move(config)));

  task_environment_.FastForwardUntilNoTasksRemain();
  EXPECT_FALSE(get_assertion_future().IsReady());
}

TEST_F(FidoGetAssertionHandlerTest, ReportTransportMetric) {
  base::HistogramTester histograms;
  auto request_handler = CreateGetAssertionHandlerCtap();

  auto device = MockFidoDevice::MakeCtapWithGetInfoExpectation();
  device->ExpectCtap2CommandAndRespondWith(
      CtapRequestCommand::kAuthenticatorGetAssertion,
      test_data::kTestGetAssertionResponse);
  discovery()->AddDevice(std::move(device));

  auto nfc_device = MockFidoDevice::MakeCtapWithGetInfoExpectation();
  nfc_device->SetDeviceTransport(
      FidoTransportProtocol::kNearFieldCommunication);
  nfc_device->ExpectCtap2CommandAndDoNotRespond(
      CtapRequestCommand::kAuthenticatorGetAssertion);
  EXPECT_CALL(*nfc_device, Cancel(_));
  nfc_discovery()->AddDevice(std::move(nfc_device));

  nfc_discovery()->WaitForCallToStartAndSimulateSuccess();
  discovery()->WaitForCallToStartAndSimulateSuccess();
  EXPECT_TRUE(get_assertion_future().Wait());

  EXPECT_EQ(GetAssertionStatus::kSuccess,
            std::get<0>(get_assertion_future().Get()));
  histograms.ExpectBucketCount(kRequestTransportHistogram,
                               FidoTransportProtocol::kUsbHumanInterfaceDevice,
                               1);
  histograms.ExpectBucketCount(kRequestTransportHistogram,
                               FidoTransportProtocol::kNearFieldCommunication,
                               1);
  histograms.ExpectUniqueSample(kResponseTransportHistogram,
                                FidoTransportProtocol::kUsbHumanInterfaceDevice,
                                1);
}

MATCHER_P(GetAssertionRequestWithAllowlist, empty_list, "") {
  if (arg.empty() ||
      arg[0] != base::strict_cast<uint8_t>(
                    CtapRequestCommand::kAuthenticatorGetAssertion)) {
    *result_listener << "not get assertion";
    return false;
  }

  base::span<const uint8_t> param_bytes(arg);
  param_bytes = param_bytes.subspan<1>();
  const auto maybe_map = cbor::Reader::Read(param_bytes);
  if (!maybe_map || !maybe_map->is_map()) {
    *result_listener << "not a map";
    return false;
  }
  const auto& map = maybe_map->GetMap();

  const auto options_it = map.find(cbor::Value(3));
  if (options_it == map.end() || !options_it->second.is_array()) {
    return empty_list;
  }

  return empty_list == options_it->second.GetArray().empty();
}

TEST_F(FidoGetAssertionHandlerTest, CtapRequestUsesPreselectedAccount) {
  auto request_handler =
      CreateGetAssertionHandlerWithRequest(CtapGetAssertionRequest(
          test_data::kRelyingPartyId, test_data::kClientDataJson));

  discovery()->WaitForCallToStartAndSimulateSuccess();
  auto device = MockFidoDevice::MakeCtapWithGetInfoExpectation();
  device->ExpectCtap2CommandAndRespondWith(
      CtapRequestCommand::kAuthenticatorGetAssertion,
      test_data::kTestGetAssertionResponseWithUserEntity, base::TimeDelta(),
      GetAssertionRequestWithAllowlist(/*empty_list=*/false));

  PublicKeyCredentialUserEntity user_entity(base::ToVector(test_data::kUserId),
                                            test_data::kUsername,
                                            test_data::kUserDisplayName);
  DiscoverableCredentialMetadata preselected_account(
      AuthenticatorType::kOther, test_data::kRelyingPartyId,
      base::ToVector(test_data::kTestGetAssertionCredentialId),
      std::move(user_entity),
      /*provider_name=*/std::nullopt);
  request_handler->PreselectAccount(std::move(preselected_account));

  discovery()->AddDevice(std::move(device));
  EXPECT_TRUE(get_assertion_future().Wait());

  EXPECT_EQ(GetAssertionStatus::kSuccess,
            std::get<0>(get_assertion_future().Get()));
  EXPECT_TRUE(std::get<1>(get_assertion_future().Get()));
}

// See https://crbug.com/400761095 for context.
TEST_F(FidoGetAssertionHandlerTest,
       CtapRequestIgnoresPreselectedAccountFromOtherAuthenticator) {
  auto request_handler =
      CreateGetAssertionHandlerWithRequest(CtapGetAssertionRequest(
          test_data::kRelyingPartyId, test_data::kClientDataJson));

  discovery()->WaitForCallToStartAndSimulateSuccess();
  auto device = MockFidoDevice::MakeCtapWithGetInfoExpectation();
  device->ExpectCtap2CommandAndRespondWith(
      CtapRequestCommand::kAuthenticatorGetAssertion,
      test_data::kTestGetAssertionResponseWithUserEntity, base::TimeDelta(),
      GetAssertionRequestWithAllowlist(/*empty_list=*/true));

  PublicKeyCredentialUserEntity user_entity(base::ToVector(test_data::kUserId),
                                            test_data::kUsername,
                                            test_data::kUserDisplayName);
  DiscoverableCredentialMetadata preselected_account(
      AuthenticatorType::kEnclave, test_data::kRelyingPartyId,
      base::ToVector(test_data::kTestGetAssertionCredentialId),
      std::move(user_entity),
      /*provider_name=*/std::nullopt);
  request_handler->PreselectAccount(std::move(preselected_account));

  discovery()->AddDevice(std::move(device));
  EXPECT_TRUE(get_assertion_future().Wait());

  EXPECT_EQ(GetAssertionStatus::kSuccess,
            std::get<0>(get_assertion_future().Get()));
  EXPECT_TRUE(std::get<1>(get_assertion_future().Get()));
}

#if BUILDFLAG(IS_WIN)

// Verify that the request handler instantiates a HID device backed
// FidoDeviceAuthenticator or a WinNativeCrossPlatformAuthenticator, depending
// on API availability.
TEST(GetAssertionRequestHandlerWinTest, TestWinUsbDiscovery) {
  base::test::TaskEnvironment task_environment;
  for (const bool enable_api : {false, true}) {
    SCOPED_TRACE(::testing::Message() << "enable_api=" << enable_api);
    FakeWinWebAuthnApi api;
    api.set_available(enable_api);
    api.InjectNonDiscoverableCredential(
        test_data::kTestGetAssertionCredentialId, test_data::kRelyingPartyId);
    WinWebAuthnApi::ScopedOverride win_webauthn_api_override(&api);

    // Simulate a connected HID device.
    ScopedFakeFidoHidManager fake_hid_manager;
    fake_hid_manager.AddFidoHidDevice("guid");

    TestGetAssertionRequestFuture future;
    FidoDiscoveryFactory fido_discovery_factory;
    CtapGetAssertionRequest request(test_data::kRelyingPartyId,
                                    test_data::kClientDataJson);
    request.allow_list = {PublicKeyCredentialDescriptor(
        CredentialType::kPublicKey,
        base::ToVector(test_data::kTestGetAssertionCredentialId))};
    auto handler = std::make_unique<GetAssertionRequestHandler>(
        &fido_discovery_factory,
        std::vector<std::unique_ptr<FidoDiscoveryBase>>(),
        base::flat_set<FidoTransportProtocol>(
            {FidoTransportProtocol::kUsbHumanInterfaceDevice}),
        std::move(request), CtapGetAssertionOptions(),
        /*allow_skipping_pin_touch=*/true, future.GetCallback());
    task_environment.RunUntilIdle();

    EXPECT_EQ(handler->AuthenticatorsForTesting().size(), 1u);
    EXPECT_EQ(handler->AuthenticatorsForTesting().begin()->second->GetType() ==
                  AuthenticatorType::kWinNative,
              enable_api);
  }
}

#endif  // BUILDFLAG(IS_WIN)

TEST_F(FidoGetAssertionHandlerTest, CrossDeviceFallbackUrl_Usb) {
  set_supported_transports({FidoTransportProtocol::kUsbHumanInterfaceDevice});

  auto request = CtapGetAssertionRequest(test_data::kRelyingPartyId,
                                         test_data::kClientDataJson);
  request.allow_list = {PublicKeyCredentialDescriptor(
      CredentialType::kPublicKey,
      base::ToVector(test_data::kTestGetAssertionCredentialId))};
  request.cross_device_fallback_url = "https://example.com/fallback";

  auto request_handler =
      CreateGetAssertionHandlerWithRequest(std::move(request));
  discovery()->WaitForCallToStartAndSimulateSuccess();

  auto device = MockFidoDevice::MakeCtapWithGetInfoExpectation();
  device->SetDeviceTransport(FidoTransportProtocol::kUsbHumanInterfaceDevice);
  device->ExpectCtap2CommandAndRespondWith(
      CtapRequestCommand::kAuthenticatorGetAssertion,
      test_data::kTestGetAssertionResponse, base::TimeDelta(),
      CborRequestHasCrossDeviceFallbackUrl(std::nullopt));

  discovery()->AddDevice(std::move(device));
  EXPECT_TRUE(get_assertion_future().Wait());
}

TEST_F(FidoGetAssertionHandlerTest, CrossDeviceFallbackUrl_Hybrid) {
  set_supported_transports({FidoTransportProtocol::kHybrid});

  auto request = CtapGetAssertionRequest(test_data::kRelyingPartyId,
                                         test_data::kClientDataJson);
  request.allow_list = {PublicKeyCredentialDescriptor(
      CredentialType::kPublicKey,
      base::ToVector(test_data::kTestGetAssertionCredentialId))};
  request.cross_device_fallback_url = "https://example.com/fallback";

  auto request_handler =
      CreateGetAssertionHandlerWithRequest(std::move(request));
  cable_discovery()->WaitForCallToStartAndSimulateSuccess();

  auto device = MockFidoDevice::MakeCtapWithGetInfoExpectation();
  device->SetDeviceTransport(FidoTransportProtocol::kHybrid);
  device->ExpectCtap2CommandAndRespondWith(
      CtapRequestCommand::kAuthenticatorGetAssertion,
      test_data::kTestGetAssertionResponse, base::TimeDelta(),
      CborRequestHasCrossDeviceFallbackUrl(
          std::make_optional<std::string>("https://example.com/fallback")));

  cable_discovery()->AddDevice(std::move(device));
  EXPECT_TRUE(get_assertion_future().Wait());
}

}  // namespace device
