chromium/chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller_webauthn_delegate_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/touch_to_fill/password_manager/touch_to_fill_controller_webauthn_delegate.h"

#include <memory>
#include <string>

#include "base/types/pass_key.h"
#include "chrome/browser/password_manager/android/password_manager_launcher_android.h"
#include "chrome/browser/touch_to_fill/password_manager/no_passkeys/android/no_passkeys_bottom_sheet_bridge.h"
#include "chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller.h"
#include "chrome/browser/webauthn/android/webauthn_request_delegate_android.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "components/password_manager/content/browser/mock_keyboard_replacing_surface_visibility_controller.h"
#include "components/password_manager/core/browser/origin_credential_store.h"
#include "components/password_manager/core/browser/passkey_credential.h"
#include "components/webauthn/android/cred_man_support.h"
#include "components/webauthn/android/webauthn_cred_man_delegate.h"
#include "content/public/browser/site_instance.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/web_contents_tester.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/android/window_android.h"
#include "url/gurl.h"

namespace {

using password_manager::PasskeyCredential;
using password_manager::UiCredential;
using IsOriginSecure = TouchToFillView::IsOriginSecure;
using ::testing::_;
using ::testing::ElementsAreArray;
using ::testing::Eq;

constexpr char kRpId[] = "example.com";
constexpr char kExampleCom[] = "https://example.com/";
const std::vector<uint8_t> kCredentialId1 = {'a', 'b', 'c', 'd'};
const std::vector<uint8_t> kCredentialId2 = {'e', 'f', 'g', 'h'};
const std::vector<uint8_t> kUserId1 = {'1', '2', '3', '4'};
constexpr char kUserName1[] = "[email protected]";

PasskeyCredential CreatePasskey(
    std::vector<uint8_t> credential_id = kCredentialId1,
    std::vector<uint8_t> user_id = kUserId1,
    std::string username = kUserName1) {
  return PasskeyCredential(
      PasskeyCredential::Source::kAndroidPhone, PasskeyCredential::RpId(kRpId),
      PasskeyCredential::CredentialId(std::move(credential_id)),
      PasskeyCredential::UserId(std::move(user_id)),
      PasskeyCredential::Username(std::move(username)));
}

class MockWebAuthnRequestDelegateAndroid
    : public WebAuthnRequestDelegateAndroid {
 public:
  explicit MockWebAuthnRequestDelegateAndroid(
      content::WebContents* web_contents)
      : WebAuthnRequestDelegateAndroid(web_contents) {}
  ~MockWebAuthnRequestDelegateAndroid() override = default;

  MOCK_METHOD(void,
              OnWebAuthnAccountSelected,
              (const std::vector<uint8_t>& id),
              (override));
  MOCK_METHOD(void, ShowHybridSignIn, (), (override));
};

struct MockTouchToFillView : public TouchToFillView {
  MOCK_METHOD(bool,
              Show,
              (const GURL&,
               IsOriginSecure,
               base::span<const UiCredential>,
               base::span<const PasskeyCredential>,
               int),
              (override));
  MOCK_METHOD(void, OnCredentialSelected, (const UiCredential&));
  MOCK_METHOD(void, OnDismiss, ());
};

struct MockJniDelegate : public NoPasskeysBottomSheetBridge::JniDelegate {
  ~MockJniDelegate() override = default;
  MOCK_METHOD(void, Create, (ui::WindowAndroid*), (override));
  MOCK_METHOD(void, Show, (const std::string&), (override));
  MOCK_METHOD(void, Dismiss, (), (override));
};

}  // namespace

class TouchToFillControllerWebAuthnTest
    : public ChromeRenderViewHostTestHarness {
 protected:
  TouchToFillControllerWebAuthnTest() = default;
  ~TouchToFillControllerWebAuthnTest() override = default;

  void SetUp() override {
    ChromeRenderViewHostTestHarness::SetUp();

    password_manager_launcher::
        OverrideManagePasswordWhenPasskeysPresentForTesting(false);
    webauthn::WebAuthnCredManDelegate::override_cred_man_support_for_testing(
        webauthn::CredManSupport::DISABLED);

    visibility_controller_ = std::make_unique<
        password_manager::MockKeyboardReplacingSurfaceVisibilityController>();
    touch_to_fill_controller_ = std::make_unique<TouchToFillController>(
        profile(), visibility_controller_->AsWeakPtr());
    auto mock_view = std::make_unique<MockTouchToFillView>();
    mock_view_ = mock_view.get();
    touch_to_fill_controller().set_view(std::move(mock_view));

    auto jni_delegate = std::make_unique<MockJniDelegate>();
    jni_delegate_ = jni_delegate.get();
    auto no_passkeys_bridge = std::make_unique<NoPasskeysBottomSheetBridge>(
        base::PassKey<class TouchToFillControllerWebAuthnTest>(),
        std::move(jni_delegate));
    touch_to_fill_controller().set_no_passkeys_bridge(
        std::move(no_passkeys_bridge));

    web_contents_ = content::WebContentsTester::CreateTestWebContents(
        profile(), content::SiteInstance::Create(profile()));
    window_ = ui::WindowAndroid::CreateForTesting();
    window_.get()->get()->AddChild(web_contents_->GetNativeView());
    web_contents_tester()->NavigateAndCommit(GURL(kExampleCom));

    request_delegate_ = std::make_unique<MockWebAuthnRequestDelegateAndroid>(
        web_contents_.get());
  }

  void TearDown() override {
    request_delegate_.reset();
    web_contents_.reset();
    ChromeRenderViewHostTestHarness::TearDown();
  }

  MockWebAuthnRequestDelegateAndroid& request_delegate() {
    return *request_delegate_;
  }

  MockTouchToFillView& view() { return *mock_view_; }

  MockJniDelegate& jni_delegate() { return *jni_delegate_; }

  TouchToFillController& touch_to_fill_controller() {
    return *touch_to_fill_controller_;
  }

  content::WebContentsTester* web_contents_tester() {
    return content::WebContentsTester::For(web_contents_.get());
  }

  std::unique_ptr<TouchToFillControllerWebAuthnDelegate>
  MakeTouchToFillControllerDelegate(bool should_show_hybrid_option) {
    return std::make_unique<TouchToFillControllerWebAuthnDelegate>(
        request_delegate_.get(), should_show_hybrid_option);
  }

 private:
  std::unique_ptr<content::WebContents> web_contents_;
  std::unique_ptr<ui::WindowAndroid::ScopedWindowAndroidForTesting> window_;
  std::unique_ptr<MockWebAuthnRequestDelegateAndroid> request_delegate_;
  std::unique_ptr<
      password_manager::MockKeyboardReplacingSurfaceVisibilityController>
      visibility_controller_;
  std::unique_ptr<TouchToFillController> touch_to_fill_controller_;
  raw_ptr<MockTouchToFillView> mock_view_ = nullptr;
  raw_ptr<MockJniDelegate> jni_delegate_ = nullptr;
};

TEST_F(TouchToFillControllerWebAuthnTest, ShowAndSelectCredential) {
  std::vector<PasskeyCredential> credentials{CreatePasskey()};

  EXPECT_CALL(view(),
              Show(Eq(GURL(kExampleCom)), IsOriginSecure(true),
                   ElementsAreArray(std::vector<UiCredential>()),
                   ElementsAreArray(credentials), TouchToFillView::kNone));
  touch_to_fill_controller().Show(
      {}, credentials,
      MakeTouchToFillControllerDelegate(/*should_show_hybrid_option=*/false),
      /*cred_man_delegate=*/nullptr,
      /*frame_driver=*/nullptr);

  EXPECT_CALL(request_delegate(), OnWebAuthnAccountSelected(kCredentialId1));
  touch_to_fill_controller().OnPasskeyCredentialSelected(credentials[0]);
}

TEST_F(TouchToFillControllerWebAuthnTest, ShowAndSelectWithMultipleCredential) {
  std::vector<PasskeyCredential> credentials(
      {CreatePasskey(kCredentialId1), CreatePasskey(kCredentialId2)});

  EXPECT_CALL(view(),
              Show(Eq(GURL(kExampleCom)), IsOriginSecure(true),
                   ElementsAreArray(std::vector<UiCredential>()),
                   ElementsAreArray(credentials), TouchToFillView::kNone));
  touch_to_fill_controller().Show(
      {}, credentials,
      MakeTouchToFillControllerDelegate(/*should_show_hybrid_option=*/false),
      /*cred_man_delegate=*/nullptr,
      /*frame_driver=*/nullptr);

  EXPECT_CALL(request_delegate(), OnWebAuthnAccountSelected(kCredentialId2));
  touch_to_fill_controller().OnPasskeyCredentialSelected(credentials[1]);
}

TEST_F(TouchToFillControllerWebAuthnTest, ShowAndCancel) {
  std::vector<PasskeyCredential> credentials({CreatePasskey()});

  EXPECT_CALL(view(),
              Show(Eq(GURL(kExampleCom)), IsOriginSecure(true),
                   ElementsAreArray(std::vector<UiCredential>()),
                   ElementsAreArray(credentials), TouchToFillView::kNone));
  touch_to_fill_controller().Show(
      {}, credentials,
      MakeTouchToFillControllerDelegate(/*should_show_hybrid_option=*/false),
      /*cred_man_delegate=*/nullptr,
      /*frame_driver=*/nullptr);

  EXPECT_CALL(request_delegate(),
              OnWebAuthnAccountSelected(std::vector<uint8_t>()));
  touch_to_fill_controller().Close();
}

TEST_F(TouchToFillControllerWebAuthnTest, ShowAndSelectHybrid) {
  std::vector<PasskeyCredential> credentials({CreatePasskey()});

  EXPECT_CALL(view(), Show(Eq(GURL(kExampleCom)), IsOriginSecure(true),
                           ElementsAreArray(std::vector<UiCredential>()),
                           ElementsAreArray(credentials),
                           TouchToFillView::kShouldShowHybridOption));
  touch_to_fill_controller().Show(
      {}, credentials,
      MakeTouchToFillControllerDelegate(/*should_show_hybrid_option=*/true),
      /*cred_man_delegate=*/nullptr,
      /*frame_driver=*/nullptr);
  EXPECT_CALL(request_delegate(), ShowHybridSignIn());
  touch_to_fill_controller().OnHybridSignInSelected();
}

TEST_F(TouchToFillControllerWebAuthnTest,
       ShowNoPasskeysSheetIfGpmNotInCredMan) {
  webauthn::WebAuthnCredManDelegate::override_cred_man_support_for_testing(
      webauthn::CredManSupport::PARALLEL_WITH_FIDO_2);

  EXPECT_CALL(view(), Show).Times(0);
  EXPECT_CALL(jni_delegate(), Show).Times(1);
  touch_to_fill_controller().Show(
      {}, {},
      MakeTouchToFillControllerDelegate(/*should_show_hybrid_option=*/false),
      /*cred_man_delegate=*/nullptr,
      /*frame_driver=*/nullptr);
}

TEST_F(TouchToFillControllerWebAuthnTest, ShowNothingIfGpmInCredMan) {
  webauthn::WebAuthnCredManDelegate::override_cred_man_support_for_testing(
      webauthn::CredManSupport::FULL_UNLESS_INAPPLICABLE);

  EXPECT_CALL(view(), Show).Times(0);
  EXPECT_CALL(jni_delegate(), Show).Times(0);
  touch_to_fill_controller().Show(
      {}, {},
      MakeTouchToFillControllerDelegate(/*should_show_hybrid_option=*/false),
      /*cred_man_delegate=*/nullptr,
      /*frame_driver=*/nullptr);
}