// 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 <vector>

#include "base/base_switches.h"
#include "base/check_deref.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/stringprintf.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "content/browser/smart_card/mock_smart_card_context_factory.h"
#include "content/browser/smart_card/smart_card_histograms.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/smart_card_delegate.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_content_browser_client.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/content_mock_cert_verifier.h"
#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/default_handlers.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "services/device/public/mojom/smart_card.mojom.h"
#include "services/network/public/cpp/permissions_policy/permissions_policy_declaration.h"
#include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom-shared.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features_generated.h"
#include "third_party/blink/public/mojom/navigation/navigation_params.mojom.h"
#include "third_party/blink/public/mojom/smart_card/smart_card.mojom.h"
#include "url/gurl.h"
#include "url/origin.h"

using base::test::RunOnceCallback;
using base::test::TestFuture;
using device::mojom::SmartCardConnection;
using device::mojom::SmartCardConnectionState;
using device::mojom::SmartCardContext;
using device::mojom::SmartCardDisposition;
using device::mojom::SmartCardError;
using device::mojom::SmartCardListReadersResult;
using device::mojom::SmartCardProtocol;
using device::mojom::SmartCardReaderStateFlags;
using device::mojom::SmartCardReaderStateOut;
using device::mojom::SmartCardReaderStateOutPtr;
using device::mojom::SmartCardResult;
using device::mojom::SmartCardShareMode;
using device::mojom::SmartCardStatus;
using device::mojom::SmartCardSuccess;
using device::mojom::SmartCardTransaction;
using ::testing::_;
using testing::Exactly;
using testing::HasSubstr;
using testing::InSequence;
using testing::MatchesRegex;
using testing::Return;
using testing::SaveArg;
using testing::StrictMock;
using testing::WithArg;
using testing::WithArgs;
using testing::WithoutArgs;

namespace content {

namespace {

constexpr char kFakeReader[] = "Fake reader";

class MockSmartCardConnection : public device::mojom::SmartCardConnection {
 public:
  MOCK_METHOD(void,
              Disconnect,
              (SmartCardDisposition disposition, DisconnectCallback callback),
              (override));

  MOCK_METHOD(void,
              Transmit,
              (device::mojom::SmartCardProtocol protocol,
               const std::vector<uint8_t>& data,
               TransmitCallback callback),
              (override));

  MOCK_METHOD(void,
              Control,
              (uint32_t control_code,
               const std::vector<uint8_t>& data,
               ControlCallback callback),
              (override));

  MOCK_METHOD(void,
              GetAttrib,
              (uint32_t id, GetAttribCallback callback),
              (override));

  MOCK_METHOD(void,
              SetAttrib,
              (uint32_t id,
               const std::vector<uint8_t>& data,
               SetAttribCallback callback),
              (override));

  MOCK_METHOD(void, Status, (StatusCallback callback), (override));

  MOCK_METHOD(void,
              BeginTransaction,
              (BeginTransactionCallback callback),
              (override));

  void ExpectBeginTransaction(
      mojo::AssociatedReceiver<SmartCardTransaction>& transaction_receiver) {
    EXPECT_CALL(*this, BeginTransaction(_))
        .WillOnce([&transaction_receiver](
                      SmartCardConnection::BeginTransactionCallback callback) {
          std::move(callback).Run(
              device::mojom::SmartCardTransactionResult::NewTransaction(
                  transaction_receiver.BindNewEndpointAndPassRemote()));
        });
  }
};

class MockSmartCardTransaction : public SmartCardTransaction {
 public:
  MOCK_METHOD(void,
              EndTransaction,
              (SmartCardDisposition disposition,
               EndTransactionCallback callback),
              (override));

  void ExpectEndTransaction(SmartCardDisposition disposition) {
    EXPECT_CALL(*this, EndTransaction(disposition, _))
        .WillOnce([](SmartCardDisposition disposition,
                     SmartCardTransaction::EndTransactionCallback callback) {
          std::move(callback).Run(
              SmartCardResult::NewSuccess(SmartCardSuccess::kOk));
        });
  }
};

class FakeSmartCardDelegate : public SmartCardDelegate {
 public:
  FakeSmartCardDelegate() = default;
  // SmartCardDelegate overrides:
  mojo::PendingRemote<device::mojom::SmartCardContextFactory>
  GetSmartCardContextFactory(RenderFrameHost& render_frame_host) override;

  void SetEmulationFactory(
      content::GlobalRenderFrameHostId frame_id,
      base::RepeatingCallback<
          mojo::PendingRemote<device::mojom::SmartCardContextFactory>()>
          factory_getter) override {}  // Do nothing.

  void ClearEmulationFactory(
      content::GlobalRenderFrameHostId frame_id) override {}  // Do nothing.

  MOCK_METHOD(bool,
              IsPermissionBlocked,
              (RenderFrameHost & render_frame_host),
              (override));

  MOCK_METHOD(bool,
              HasReaderPermission,
              (content::RenderFrameHost & render_frame_host,
               const std::string& reader_name),
              (override));

  MOCK_METHOD(void,
              RequestReaderPermission,
              (content::RenderFrameHost & render_frame_host,
               const std::string& reader_name,
               RequestReaderPermissionCallback callback),
              (override));

  MOCK_METHOD(void,
              NotifyConnectionUsed,
              (content::RenderFrameHost & render_frame_host),
              (override));

  MOCK_METHOD(void,
              NotifyLastConnectionLost,
              (content::RenderFrameHost & render_frame_host),
              (override));

  MOCK_METHOD(void,
              AddObserver,
              (content::RenderFrameHost & render_frame_host,
               PermissionObserver* observer),
              (override));

  MOCK_METHOD(void,
              RemoveObserver,
              (content::RenderFrameHost & render_frame_host,
               PermissionObserver* observer),
              (override));

  void ExpectHasReaderPermission(const std::string& reader_name) {
    EXPECT_CALL(*this, HasReaderPermission(_, reader_name))
        .WillOnce(Return(true));
  }

  void ExpectAddObserver() { EXPECT_CALL(*this, AddObserver); }

  void ExpectRemoveObserver() { EXPECT_CALL(*this, RemoveObserver); }

  MockSmartCardContextFactory mock_context_factory;
};

class SmartCardTestContentBrowserClient
    : public ContentBrowserTestContentBrowserClient {
 public:
  SmartCardTestContentBrowserClient();
  SmartCardTestContentBrowserClient(SmartCardTestContentBrowserClient&) =
      delete;
  SmartCardTestContentBrowserClient& operator=(
      SmartCardTestContentBrowserClient&) = delete;
  ~SmartCardTestContentBrowserClient() override;

  void SetSmartCardDelegate(std::unique_ptr<SmartCardDelegate>);

  // ContentBrowserClient:
  SmartCardDelegate* GetSmartCardDelegate() override;
  bool ShouldUrlUseApplicationIsolationLevel(BrowserContext* browser_context,
                                             const GURL& url) override;

 private:
  std::unique_ptr<SmartCardDelegate> delegate_;
};

class SmartCardTest : public ContentBrowserTest {
 public:
  GURL GetIsolatedContextUrl(bool include_coi_pp = true) {
    if (include_coi_pp) {
      return embedded_https_test_server().GetURL(
          "a.com",
          "/set-header?Cross-Origin-Opener-Policy: same-origin&"
          "Cross-Origin-Embedder-Policy: require-corp&"
          "Permissions-Policy: smart-card%3D(self), "
          "cross-origin-isolated%3D(self)");
    } else {
      return embedded_https_test_server().GetURL(
          "a.com",
          "/set-header?Cross-Origin-Opener-Policy: same-origin&"
          "Cross-Origin-Embedder-Policy: require-corp&"
          "Permissions-Policy: smart-card%3D(self), "
          "cross-origin-isolated%3D()");
    }
  }

  FakeSmartCardDelegate& GetFakeSmartCardDelegate() {
    return *static_cast<FakeSmartCardDelegate*>(
        test_client_->GetSmartCardDelegate());
  }

  void TestEmptyTransaction(std::string expected_result,
                            std::string transaction_callback) {
    ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

    MockSmartCardContextFactory& mock_context_factory =
        GetFakeSmartCardDelegate().mock_context_factory;
    MockSmartCardConnection mock_connection;
    mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection);

    MockSmartCardTransaction mock_transaction;
    mojo::AssociatedReceiver<SmartCardTransaction> transaction_receiver(
        &mock_transaction);

    {
      InSequence s;

      auto& delegate = GetFakeSmartCardDelegate();
      delegate.ExpectAddObserver();
      delegate.ExpectHasReaderPermission(kFakeReader);
      mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver);
      mock_connection.ExpectBeginTransaction(transaction_receiver);
      mock_transaction.ExpectEndTransaction(SmartCardDisposition::kReset);
      delegate.ExpectRemoveObserver();
    }

    std::string js_snippet = base::StringPrintf(R"(
      (async () => {
        let context = await navigator.smartCard.establishContext();

        let connection =
          (await context.connect("Fake reader", "shared",
            {preferredProtocols: ["t1"]})).connection;

        let transaction = %s;

        let transactionPromise = connection.startTransaction(transaction);
        try {
          await transactionPromise;
        } catch (e) {
          return `startTransaction: ${e.name}, ${e.message}`;
        }

        return "ok";
      })())",
                                                transaction_callback.c_str());

    EXPECT_EQ(expected_result, EvalJs(shell(), js_snippet));
  }

 private:
  void SetUpOnMainThread() override {
    ContentBrowserTest::SetUpOnMainThread();

    test_client_ = std::make_unique<SmartCardTestContentBrowserClient>();
    test_client_->SetSmartCardDelegate(
        std::make_unique<FakeSmartCardDelegate>());

    // Serve a.com (and any other domain).
    host_resolver()->AddRule("*", "127.0.0.1");

    // Add a handler for the "/set-header" page (among others)
    embedded_https_test_server().AddDefaultHandlers(GetTestDataFilePath());

    ASSERT_TRUE(embedded_https_test_server().Start());
  }

  void SetUpInProcessBrowserTestFixture() override {
    ContentBrowserTest::SetUpInProcessBrowserTestFixture();
  }

  void TearDownInProcessBrowserTestFixture() override {
    ContentBrowserTest::TearDownInProcessBrowserTestFixture();
  }

  void TearDown() override {
    ASSERT_TRUE(embedded_https_test_server().ShutdownAndWaitUntilComplete());
    ContentBrowserTest::TearDown();
  }

  std::unique_ptr<SmartCardTestContentBrowserClient> test_client_;

  base::test::ScopedFeatureList scoped_feature_list_{
      blink::features::kSmartCard};

 protected:
  base::HistogramTester histogram_tester_;
};
}  // namespace

SmartCardTestContentBrowserClient::SmartCardTestContentBrowserClient() =
    default;

SmartCardTestContentBrowserClient::~SmartCardTestContentBrowserClient() =
    default;

SmartCardDelegate* SmartCardTestContentBrowserClient::GetSmartCardDelegate() {
  return delegate_.get();
}

void SmartCardTestContentBrowserClient::SetSmartCardDelegate(
    std::unique_ptr<SmartCardDelegate> delegate) {
  delegate_ = std::move(delegate);
}

bool SmartCardTestContentBrowserClient::ShouldUrlUseApplicationIsolationLevel(
    BrowserContext* browser_context,
    const GURL& url) {
  return true;
}

mojo::PendingRemote<device::mojom::SmartCardContextFactory>
FakeSmartCardDelegate::GetSmartCardContextFactory(
    RenderFrameHost& render_frame_host) {
  return mock_context_factory.GetRemote();
}

IN_PROC_BROWSER_TEST_F(SmartCardTest, Disconnect) {
  ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

  MockSmartCardContextFactory& mock_context_factory =
      GetFakeSmartCardDelegate().mock_context_factory;
  MockSmartCardConnection mock_connection;
  mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection);

  {
    InSequence s;

    GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader);

    mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver);

    EXPECT_CALL(mock_connection, Disconnect(SmartCardDisposition::kEject, _))
        .WillOnce([](SmartCardDisposition disposition,
                     SmartCardConnection::DisconnectCallback callback) {
          std::move(callback).Run(
              SmartCardResult::NewSuccess(SmartCardSuccess::kOk));
        });
  }

  EXPECT_EQ(
      "second disconnect: InvalidStateError, Failed to execute 'disconnect' on "
      "'SmartCardConnection': Is disconnected.",
      EvalJs(shell(), R"(
    (async () => {
      let context = await navigator.smartCard.establishContext();

      let connection =
        (await context.connect("Fake reader", "shared",
          {preferredProtocols: ["t1"]})).connection;

      await connection.disconnect("eject");

      // A second attempt should fail.
      try {
        await connection.disconnect("unpower");
      } catch (e) {
        return `second disconnect: ${e.name}, ${e.message}`;
      }

      return `second disconnect did not throw`;
    })())"));

  content::FetchHistogramsFromChildProcesses();
  histogram_tester_.ExpectUniqueSample(
      "SmartCard.ConnectionClosedReason",
      SmartCardConnectionClosedReason::kSmartCardConnectionClosedDisconnect, 1);
}

IN_PROC_BROWSER_TEST_F(SmartCardTest, LastConnectionClosed) {
  ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

  MockSmartCardContextFactory& mock_context_factory =
      GetFakeSmartCardDelegate().mock_context_factory;
  MockSmartCardConnection mock_connection1;
  MockSmartCardConnection mock_connection2;
  mojo::Receiver<SmartCardConnection> connection_receiver1(&mock_connection1);
  mojo::Receiver<SmartCardConnection> connection_receiver2(&mock_connection2);
  mojo::Remote<device::mojom::SmartCardConnectionWatcher> watcher1;
  mojo::Remote<device::mojom::SmartCardConnectionWatcher> watcher2;

  {
    InSequence s;

    auto& delegate = GetFakeSmartCardDelegate();

    delegate.ExpectHasReaderPermission(kFakeReader);
    EXPECT_CALL(mock_context_factory,
                Connect("Fake reader", SmartCardShareMode::kShared, _, _, _))
        .WillOnce(WithArgs<3, 4>(
            [&connection_receiver1, &watcher1](
                mojo::PendingRemote<device::mojom::SmartCardConnectionWatcher>
                    connection_watcher,
                SmartCardContext::ConnectCallback callback) {
              watcher1.Bind(std::move(connection_watcher));

              auto success = device::mojom::SmartCardConnectSuccess::New(
                  connection_receiver1.BindNewPipeAndPassRemote(),
                  SmartCardProtocol::kT1);

              std::move(callback).Run(
                  device::mojom::SmartCardConnectResult::NewSuccess(
                      std::move(success)));
            }));

    delegate.ExpectHasReaderPermission(kFakeReader);
    EXPECT_CALL(mock_context_factory,
                Connect("Fake reader", SmartCardShareMode::kShared, _, _, _))
        .WillOnce(WithArgs<3, 4>(
            [&connection_receiver2, &watcher2](
                mojo::PendingRemote<device::mojom::SmartCardConnectionWatcher>
                    connection_watcher,
                SmartCardContext::ConnectCallback callback) {
              watcher2.Bind(std::move(connection_watcher));

              auto success = device::mojom::SmartCardConnectSuccess::New(
                  connection_receiver2.BindNewPipeAndPassRemote(),
                  SmartCardProtocol::kT1);

              std::move(callback).Run(
                  device::mojom::SmartCardConnectResult::NewSuccess(
                      std::move(success)));
            }));

    EXPECT_CALL(mock_connection1, Disconnect(SmartCardDisposition::kEject, _))
        .WillOnce(WithArg<1>(
            [&watcher1](SmartCardConnection::DisconnectCallback callback) {
              watcher1.reset();
              std::move(callback).Run(
                  SmartCardResult::NewSuccess(SmartCardSuccess::kOk));
            }));

    EXPECT_CALL(mock_connection2, Disconnect(SmartCardDisposition::kEject, _))
        .WillOnce(WithArg<1>(
            [&watcher2](SmartCardConnection::DisconnectCallback callback) {
              watcher2.reset();
              std::move(callback).Run(
                  SmartCardResult::NewSuccess(SmartCardSuccess::kOk));
            }));

    EXPECT_CALL(delegate, NotifyLastConnectionLost).Times(1);
  }

  EXPECT_TRUE(ExecJs(shell(), R"(
    (async () => {
      let context = await navigator.smartCard.establishContext();

      let connection1 =
        (await context.connect("Fake reader", "shared",
          {preferredProtocols: ["t1"]})).connection;

      let connection2 =
        (await context.connect("Fake reader", "shared",
          {preferredProtocols: ["t1"]})).connection;

      await connection1.disconnect("eject");
      await connection2.disconnect("eject");
    })())"));
  content::FetchHistogramsFromChildProcesses();
  histogram_tester_.ExpectUniqueSample(
      "SmartCard.ConnectionClosedReason",
      SmartCardConnectionClosedReason::kSmartCardConnectionClosedDisconnect, 2);
}

IN_PROC_BROWSER_TEST_F(SmartCardTest, NotifyConnectionUsed) {
  ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

  MockSmartCardContextFactory& mock_context_factory =
      GetFakeSmartCardDelegate().mock_context_factory;
  MockSmartCardConnection mock_connection;
  mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection);
  mojo::Remote<device::mojom::SmartCardConnectionWatcher> watcher;

  {
    InSequence s;

    auto& delegate = GetFakeSmartCardDelegate();

    delegate.ExpectHasReaderPermission(kFakeReader);
    EXPECT_CALL(mock_context_factory,
                Connect("Fake reader", SmartCardShareMode::kShared, _, _, _))
        .WillOnce(WithArgs<3, 4>(
            [&connection_receiver, &watcher](
                mojo::PendingRemote<device::mojom::SmartCardConnectionWatcher>
                    connection_watcher,
                SmartCardContext::ConnectCallback callback) {
              watcher.Bind(std::move(connection_watcher));

              auto success = device::mojom::SmartCardConnectSuccess::New(
                  connection_receiver.BindNewPipeAndPassRemote(),
                  SmartCardProtocol::kT1);

              std::move(callback).Run(
                  device::mojom::SmartCardConnectResult::NewSuccess(
                      std::move(success)));
            }));

    EXPECT_CALL(mock_connection, Transmit)
        .WillOnce([&watcher](SmartCardProtocol protocol,
                             const std::vector<uint8_t>& data,
                             SmartCardConnection::TransmitCallback callback) {
          ASSERT_TRUE(watcher.is_bound());
          watcher->NotifyConnectionUsed();
          std::move(callback).Run(
              device::mojom::SmartCardDataResult::NewData({12u, 34u}));
        });

    EXPECT_CALL(delegate, NotifyConnectionUsed).Times(1);

    EXPECT_CALL(mock_connection, Disconnect(SmartCardDisposition::kEject, _))
        .WillOnce(WithArg<1>(
            [&watcher](SmartCardConnection::DisconnectCallback callback) {
              watcher.reset();
              std::move(callback).Run(
                  SmartCardResult::NewSuccess(SmartCardSuccess::kOk));
            }));

    EXPECT_CALL(delegate, NotifyLastConnectionLost).Times(1);
  }

  EXPECT_TRUE(ExecJs(shell(), R"(
    (async () => {
      let context = await navigator.smartCard.establishContext();

      let connection =
        (await context.connect("Fake reader", "shared",
          {preferredProtocols: ["t1"]})).connection;

      let apdu = new Uint8Array([0x03, 0x02, 0x01]);
      await connection.transmit(apdu);

      await connection.disconnect("eject");
    })())"));
}

IN_PROC_BROWSER_TEST_F(SmartCardTest, ConcurrentDisconnect) {
  ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

  MockSmartCardContextFactory& mock_context_factory =
      GetFakeSmartCardDelegate().mock_context_factory;
  MockSmartCardConnection mock_connection;
  mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection);

  TestFuture<SmartCardConnection::DisconnectCallback> disconnect_future;

  {
    InSequence s;

    GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader);

    mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver);

    EXPECT_CALL(mock_connection, Disconnect(SmartCardDisposition::kEject, _))
        .WillOnce([&disconnect_future](
                      SmartCardDisposition disposition,
                      SmartCardConnection::DisconnectCallback callback) {
          // Ensure this disconnect() call doesn't finish before the second
          // one is issued.
          disconnect_future.SetValue(std::move(callback));
        });
  }

  EXPECT_EQ(
      "second disconnect: InvalidStateError, Failed to execute 'disconnect' on "
      "'SmartCardConnection': An operation is already in progress in this "
      "smart card context.",
      EvalJs(shell(), R"(
    (async () => {
      let context = await navigator.smartCard.establishContext();

      let connection =
        (await context.connect("Fake reader", "shared",
          {preferredProtocols: ["t1"]})).connection;

      // This first disconnect() call will go through but won't be finished
      // before the end of this script.
      connection.disconnect("eject");

      // A second attempt should fail since the first one is still ongoing.
      try {
        await connection.disconnect("unpower");
      } catch (e) {
        return `second disconnect: ${e.name}, ${e.message}`;
      }

      return `second disconnect did not throw`;
    })())"));

  // Let the first disconnect() finish.
  disconnect_future.Take().Run(
      SmartCardResult::NewSuccess(SmartCardSuccess::kOk));
}

IN_PROC_BROWSER_TEST_F(SmartCardTest, Transmit) {
  ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

  MockSmartCardContextFactory& mock_context_factory =
      GetFakeSmartCardDelegate().mock_context_factory;
  MockSmartCardConnection mock_connection;
  mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection);

  {
    InSequence s;

    GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader);

    mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver);

    EXPECT_CALL(mock_connection, Transmit(SmartCardProtocol::kT1, _, _))
        .WillOnce([](SmartCardProtocol protocol,
                     const std::vector<uint8_t>& data,
                     SmartCardConnection::TransmitCallback callback) {
          EXPECT_EQ(data, std::vector<uint8_t>({3u, 2u, 1u}));
          std::move(callback).Run(
              device::mojom::SmartCardDataResult::NewData({12u, 34u}));
        });
  }

  EXPECT_EQ("response: 12,34", EvalJs(shell(), R"(
    (async () => {
      let context = await navigator.smartCard.establishContext();

      let connection =
        (await context.connect("Fake reader", "shared",
          {preferredProtocols: ["t1"]})).connection;

      let apdu = new Uint8Array([0x03, 0x02, 0x01]);
      let response = await connection.transmit(apdu);

      let responseString = new Uint8Array(response).toString();
      return `response: ${responseString}`;
    })())"));
}

IN_PROC_BROWSER_TEST_F(SmartCardTest, TransmitWithOptions) {
  ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

  MockSmartCardContextFactory& mock_context_factory =
      GetFakeSmartCardDelegate().mock_context_factory;
  MockSmartCardConnection mock_connection;
  mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection);

  {
    InSequence s;

    GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader);

    EXPECT_CALL(mock_context_factory,
                Connect(kFakeReader, SmartCardShareMode::kDirect, _, _, _))
        .WillOnce(
            [&connection_receiver](
                const std::string& reader,
                device::mojom::SmartCardShareMode share_mode,
                device::mojom::SmartCardProtocolsPtr preferred_protocols,
                mojo::PendingRemote<device::mojom::SmartCardConnectionWatcher>
                    connection_watcher,
                SmartCardContext::ConnectCallback callback) {
              EXPECT_FALSE(preferred_protocols->t0);
              EXPECT_FALSE(preferred_protocols->t1);
              EXPECT_FALSE(preferred_protocols->raw);

              auto success = device::mojom::SmartCardConnectSuccess::New(
                  connection_receiver.BindNewPipeAndPassRemote(),
                  SmartCardProtocol::kUndefined);

              std::move(callback).Run(
                  device::mojom::SmartCardConnectResult::NewSuccess(
                      std::move(success)));
            });

    EXPECT_CALL(mock_connection, Transmit(SmartCardProtocol::kT0, _, _))
        .WillOnce([](SmartCardProtocol protocol,
                     const std::vector<uint8_t>& data,
                     SmartCardConnection::TransmitCallback callback) {
          EXPECT_EQ(data, std::vector<uint8_t>({3u, 2u, 1u}));
          std::move(callback).Run(
              device::mojom::SmartCardDataResult::NewData({12u, 34u}));
        });
  }

  EXPECT_EQ("response: 12,34", EvalJs(shell(), R"(
    (async () => {
      let context = await navigator.smartCard.establishContext();

      let connection =
        (await context.connect("Fake reader", "direct")).connection;

      // In real usage you would have in between:
      // let IOCTL_SMARTCARD_SET_PROTOCOL = ...;
      // connection.control(IOCTL_SMARTCARD_SET_PROTOCOL, ...);

      let apdu = new Uint8Array([0x03, 0x02, 0x01]);
      let response = await connection.transmit(apdu, {protocol: "t0"});

      let responseString = new Uint8Array(response).toString();
      return `response: ${responseString}`;
    })())"));
}

IN_PROC_BROWSER_TEST_F(SmartCardTest, TransmitNoProtocol) {
  ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

  MockSmartCardContextFactory& mock_context_factory =
      GetFakeSmartCardDelegate().mock_context_factory;
  StrictMock<MockSmartCardConnection> mock_connection;
  mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection);

  {
    InSequence s;

    GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader);

    EXPECT_CALL(mock_context_factory,
                Connect(kFakeReader, SmartCardShareMode::kDirect, _, _, _))
        .WillOnce(WithArgs<2, 4>(
            [&connection_receiver](
                device::mojom::SmartCardProtocolsPtr preferred_protocols,
                SmartCardContext::ConnectCallback callback) {
              EXPECT_FALSE(preferred_protocols->t0);
              EXPECT_FALSE(preferred_protocols->t1);
              EXPECT_FALSE(preferred_protocols->raw);

              auto success = device::mojom::SmartCardConnectSuccess::New(
                  connection_receiver.BindNewPipeAndPassRemote(),
                  SmartCardProtocol::kUndefined);

              std::move(callback).Run(
                  device::mojom::SmartCardConnectResult::NewSuccess(
                      std::move(success)));
            }));
  }

  EXPECT_THAT(
      EvalJs(shell(), R"(
    (async () => {
      let context = await navigator.smartCard.establishContext();

      let connection =
        (await context.connect("Fake reader", "direct")).connection;

      let apdu = new Uint8Array([0x03, 0x02, 0x01]);
      try {
        await connection.transmit(apdu);
      } catch(e) {
        return `transmit: ${e.name}, ${e.message}`;
      }

      return "ok";
    })())")
          .ExtractString(),
      MatchesRegex("transmit: InvalidStateError, .*No active protocol\\."));
}

IN_PROC_BROWSER_TEST_F(SmartCardTest, Control) {
  ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

  MockSmartCardContextFactory& mock_context_factory =
      GetFakeSmartCardDelegate().mock_context_factory;
  MockSmartCardConnection mock_connection;
  mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection);

  {
    InSequence s;

    GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader);

    mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver);

    EXPECT_CALL(mock_connection, Control(42, _, _))
        .WillOnce(
            WithArgs<1, 2>([](const std::vector<uint8_t>& data,
                              SmartCardConnection::ControlCallback callback) {
              EXPECT_EQ(data, std::vector<uint8_t>({3u, 2u, 1u}));
              std::move(callback).Run(
                  device::mojom::SmartCardDataResult::NewData({12u, 34u}));
            }));
  }

  EXPECT_EQ("response: 12,34", EvalJs(shell(), R"(
    (async () => {
      let context = await navigator.smartCard.establishContext();

      let connection =
        (await context.connect("Fake reader", "shared",
          {preferredProtocols: ["t1"]})).connection;

      let data = new Uint8Array([0x03, 0x02, 0x01]);
      let response = await connection.control(42, data);

      let responseString = new Uint8Array(response).toString();
      return `response: ${responseString}`;
    })())"));
}

IN_PROC_BROWSER_TEST_F(SmartCardTest, GetAttribute) {
  ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

  MockSmartCardContextFactory& mock_context_factory =
      GetFakeSmartCardDelegate().mock_context_factory;
  MockSmartCardConnection mock_connection;
  mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection);

  {
    InSequence s;

    GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader);

    mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver);

    EXPECT_CALL(mock_connection, GetAttrib(42, _))
        .WillOnce(
            WithArg<1>([](SmartCardConnection::GetAttribCallback callback) {
              std::move(callback).Run(
                  device::mojom::SmartCardDataResult::NewData({12u, 34u}));
            }));
  }

  EXPECT_EQ("response: 12,34", EvalJs(shell(), R"(
    (async () => {
      let context = await navigator.smartCard.establishContext();

      let connection =
        (await context.connect("Fake reader", "shared",
          {preferredProtocols: ["t1"]})).connection;

      let response = await connection.getAttribute(42);

      let responseString = new Uint8Array(response).toString();
      return `response: ${responseString}`;
    })())"));
}

IN_PROC_BROWSER_TEST_F(SmartCardTest, SetAttribute) {
  ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

  MockSmartCardContextFactory& mock_context_factory =
      GetFakeSmartCardDelegate().mock_context_factory;
  MockSmartCardConnection mock_connection;
  mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection);

  {
    InSequence s;

    GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader);

    mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver);

    EXPECT_CALL(mock_connection,
                SetAttrib(42, std::vector<uint8_t>({3u, 2u, 1u}), _))
        .WillOnce(
            WithArg<2>([](SmartCardConnection::SetAttribCallback callback) {
              std::move(callback).Run(
                  device::mojom::SmartCardResult::NewSuccess(
                      SmartCardSuccess::kOk));
            }));
  }

  EXPECT_EQ("success", EvalJs(shell(), R"(
    (async () => {
      let context = await navigator.smartCard.establishContext();

      let connection =
        (await context.connect("Fake reader", "shared",
          {preferredProtocols: ["t1"]})).connection;

      let data = new Uint8Array([0x03, 0x02, 0x01]);
      await connection.setAttribute(42, data);

      return 'success';
    })())"));
}

IN_PROC_BROWSER_TEST_F(SmartCardTest, Status) {
  ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

  MockSmartCardContextFactory& mock_context_factory =
      GetFakeSmartCardDelegate().mock_context_factory;
  MockSmartCardConnection mock_connection;
  mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection);

  {
    InSequence s;

    GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader);

    mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver);

    EXPECT_CALL(mock_connection, Status)
        .WillOnce([](SmartCardConnection::StatusCallback callback) {
          auto result = device::mojom::SmartCardStatusResult::NewStatus(
              SmartCardStatus::New(
                  kFakeReader, SmartCardConnectionState::kSpecific,
                  SmartCardProtocol::kT1, std::vector<uint8_t>({3u, 2u, 1u})));
          std::move(callback).Run(std::move(result));
        });
  }

  EXPECT_EQ("Fake reader, t1, {3,2,1}", EvalJs(shell(), R"(
    (async () => {
      let context = await navigator.smartCard.establishContext();

      let connection =
        (await context.connect("Fake reader", "shared",
          {preferredProtocols: ["t1"]})).connection;

      let status = await connection.status();

      let atr = new Uint8Array(status.answerToReset).toString();
      return `${status.readerName}, ${status.state}, {${atr}}`;
    })())"));
}

IN_PROC_BROWSER_TEST_F(SmartCardTest, ListReaders) {
  MockSmartCardContextFactory& mock_context_factory =
      GetFakeSmartCardDelegate().mock_context_factory;

  mock_context_factory.ExpectListReaders({"Foo", "Bar"});

  ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

  auto expected_reader_names =
      base::Value(base::ListValue().Append("Foo").Append("Bar"));

  EXPECT_EQ(expected_reader_names, EvalJs(shell(), R"((async () => {
       let context = await navigator.smartCard.establishContext();
       return await context.listReaders();
     })())"));
}

/*
This test checks that in case there are no readers available, listReaders() call
will return an empty list of readers with no errors.

Note that internally we will receive a kNoReadersAvailable error from
SmartCardDelegate. However, we should not forward this error to Javascript.
*/
IN_PROC_BROWSER_TEST_F(SmartCardTest, ListReadersEmpty) {
  MockSmartCardContextFactory& mock_context_factory =
      GetFakeSmartCardDelegate().mock_context_factory;

  EXPECT_CALL(mock_context_factory, ListReaders(_))
      .WillOnce(RunOnceCallback<0>(SmartCardListReadersResult::NewError(
          SmartCardError::kNoReadersAvailable)));

  ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

  auto expected_reader_names = base::Value(base::ListValue());

  EXPECT_EQ(expected_reader_names, EvalJs(shell(), R"((async () => {
       let context = await navigator.smartCard.establishContext();
       return await context.listReaders();
     })())"));
}

IN_PROC_BROWSER_TEST_F(SmartCardTest, GetStatusChange) {
  MockSmartCardContextFactory& mock_context_factory =
      GetFakeSmartCardDelegate().mock_context_factory;

  EXPECT_CALL(mock_context_factory,
              GetStatusChange(base::Milliseconds(4321), _, _))
      .WillOnce(WithArgs<1, 2>(
          [](std::vector<device::mojom::SmartCardReaderStateInPtr> states_in,
             SmartCardContext::GetStatusChangeCallback callback) {
            ASSERT_EQ(states_in.size(), 1u);
            ASSERT_EQ(states_in[0]->reader, "Fake Reader");
            EXPECT_FALSE(states_in[0]->current_state->unaware);
            EXPECT_FALSE(states_in[0]->current_state->ignore);
            EXPECT_FALSE(states_in[0]->current_state->changed);
            EXPECT_FALSE(states_in[0]->current_state->unknown);
            EXPECT_FALSE(states_in[0]->current_state->unavailable);
            EXPECT_TRUE(states_in[0]->current_state->empty);
            EXPECT_FALSE(states_in[0]->current_state->present);
            EXPECT_FALSE(states_in[0]->current_state->exclusive);
            EXPECT_FALSE(states_in[0]->current_state->inuse);
            EXPECT_FALSE(states_in[0]->current_state->mute);
            EXPECT_FALSE(states_in[0]->current_state->unpowered);
            EXPECT_EQ(states_in[0]->current_count, 6u);

            auto state_flags = SmartCardReaderStateFlags::New();
            state_flags->unaware = false;
            state_flags->ignore = false;
            state_flags->changed = false;
            state_flags->unknown = false;
            state_flags->unavailable = false;
            state_flags->empty = false;
            state_flags->present = true;
            state_flags->exclusive = false;
            state_flags->inuse = true;
            state_flags->mute = false;
            state_flags->unpowered = false;

            std::vector<SmartCardReaderStateOutPtr> states_out;
            states_out.push_back(SmartCardReaderStateOut::New(
                "Fake Reader", std::move(state_flags), 7,
                std::vector<uint8_t>({1u, 2u, 3u, 4u})));
            auto result =
                device::mojom::SmartCardStatusChangeResult::NewReaderStates(
                    std::move(states_out));
            std::move(callback).Run(std::move(result));
          }));

  ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

  EXPECT_EQ(
      "Fake Reader, {ignore=false, changed=false, "
      "unknown=false, unavailable=false, empty=false, present=true, "
      "exclusive=false, inuse=true, mute=false, unpowered=false}, 7, {1,2,3,4}",
      EvalJs(shell(), R"((async () => {
       let context = await navigator.smartCard.establishContext();

       let readerStates = [{readerName: "Fake Reader",
                            currentState: {empty: true},
                            currentCount: 6 }];
       let statesOut = await context.getStatusChange(
           readerStates,
           {timeout: 4321});

       if (statesOut.length !== 1) {
         return `states array has size ${statesOut.length}`;
       }
       let atrString = new Uint8Array(statesOut[0].answerToReset).toString();

       let flags = statesOut[0].eventState;
       let eventStateString = `ignore=${flags.ignore}`
           + `, changed=${flags.changed}`
           + `, unknown=${flags.unknown}`
           + `, unavailable=${flags.unavailable}`
           + `, empty=${flags.empty}`
           + `, present=${flags.present}`
           + `, exclusive=${flags.exclusive}`
           + `, inuse=${flags.inuse}`
           + `, mute=${flags.mute}`
           + `, unpowered=${flags.unpowered}`;

       return `${statesOut[0].readerName}, {${eventStateString}}` +
         `, ${statesOut[0].eventCount}, {${atrString}}`;
     })())"));
}

IN_PROC_BROWSER_TEST_F(SmartCardTest, GetStatusChangeInterruptedByClose) {
  std::optional<SmartCardContext::GetStatusChangeCallback> callback_holder;
  MockSmartCardContextFactory& mock_context_factory =
      GetFakeSmartCardDelegate().mock_context_factory;

  EXPECT_CALL(mock_context_factory, GetStatusChange)
      .WillOnce(
          WithArg<2>([&callback_holder](
                         SmartCardContext::GetStatusChangeCallback callback) {
            callback_holder.emplace(std::move(callback));
          }));

  ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

  ASSERT_TRUE(ExecJs(shell(), R"((async () => {
       let context = await navigator.smartCard.establishContext();

       let readerStates = [{readerName: "Fake Reader",
                            currentState: {empty: true},
                            currentCount: 6 }];
       let promise = context.getStatusChange(
           readerStates,
           {timeout: 4321});
           })())"));
  ASSERT_NO_FATAL_FAILURE(shell()->Reload());
  content::WaitForLoadStop(shell()->web_contents());
}

IN_PROC_BROWSER_TEST_F(SmartCardTest, GetStatusChangeAborted) {
  MockSmartCardContextFactory& mock_context_factory =
      GetFakeSmartCardDelegate().mock_context_factory;

  base::test::TestFuture<SmartCardContext::GetStatusChangeCallback>
      get_status_callback;

  {
    InSequence s;

    EXPECT_CALL(mock_context_factory,
                GetStatusChange(base::TimeDelta::Max(), _, _))
        .WillOnce(WithArgs<1, 2>(
            [&get_status_callback](
                std::vector<device::mojom::SmartCardReaderStateInPtr> states_in,
                SmartCardContext::GetStatusChangeCallback callback) {
              ASSERT_EQ(states_in.size(), size_t(1));
              ASSERT_EQ(states_in[0]->reader, "Fake Reader");
              EXPECT_FALSE(states_in[0]->current_state->unaware);
              EXPECT_FALSE(states_in[0]->current_state->ignore);
              EXPECT_FALSE(states_in[0]->current_state->changed);
              EXPECT_FALSE(states_in[0]->current_state->unknown);
              EXPECT_FALSE(states_in[0]->current_state->unavailable);
              EXPECT_TRUE(states_in[0]->current_state->empty);
              EXPECT_FALSE(states_in[0]->current_state->present);
              EXPECT_FALSE(states_in[0]->current_state->exclusive);
              EXPECT_FALSE(states_in[0]->current_state->inuse);
              EXPECT_FALSE(states_in[0]->current_state->mute);
              EXPECT_FALSE(states_in[0]->current_state->unpowered);

              // Don't respond immediately.
              get_status_callback.SetValue(std::move(callback));
            }));

    // Aborting a blink context.getStatusChange() call means sending a Cancel()
    // request down to device.mojom.
    EXPECT_CALL(mock_context_factory, Cancel(_))
        .WillOnce(
            [&get_status_callback](SmartCardContext::CancelCallback callback) {
              std::move(get_status_callback)
                  .Take()
                  .Run(device::mojom::SmartCardStatusChangeResult::NewError(
                      SmartCardError::kCancelled));

              std::move(callback).Run(
                  SmartCardResult::NewSuccess(SmartCardSuccess::kOk));
            });
  }

  ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

  EXPECT_EQ("Exception: Error, Something", EvalJs(shell(), R"((async () => {
       let context = await navigator.smartCard.establishContext();

       let abortController = new AbortController();

       let getStatusPromise = context.getStatusChange(
           [{readerName: "Fake Reader", currentState: {empty: true}}],
           {signal: abortController.signal});

       abortController.abort(Error("Something"));

       try {
         let result = await getStatusPromise;
         return "Success";
       } catch (e) {
         return `Exception: ${e.name}, ${e.message}`;
       }
     })())"));
}

// Tests passing an AbortSignal to getStatusChange() that is already aborted.
IN_PROC_BROWSER_TEST_F(SmartCardTest, GetStatusChangeAlreadyAborted) {
  ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

  EXPECT_EQ("Exception: Error, Something", EvalJs(shell(), R"((async () => {
       let context = await navigator.smartCard.establishContext();

       let getStatusPromise = context.getStatusChange(
           [{readerName: "Fake Reader", currentState: {empty: true}}],
           {signal: AbortSignal.abort(Error("Something"))});

       try {
         let result = await getStatusPromise;
         return "Success";
       } catch (e) {
         return `Exception: ${e.name}, ${e.message}`;
       }
     })())"));
}

IN_PROC_BROWSER_TEST_F(SmartCardTest, Connect) {
  MockSmartCardContextFactory& mock_context_factory =
      GetFakeSmartCardDelegate().mock_context_factory;

  EXPECT_CALL(mock_context_factory,
              Connect(kFakeReader, SmartCardShareMode::kShared, _, _, _))
      .WillOnce(WithArgs<2, 4>([](device::mojom::SmartCardProtocolsPtr
                                      preferred_protocols,
                                  SmartCardContext::ConnectCallback callback) {
        mojo::PendingRemote<device::mojom::SmartCardConnection> pending_remote;

        EXPECT_TRUE(preferred_protocols->t0);
        EXPECT_TRUE(preferred_protocols->t1);
        EXPECT_FALSE(preferred_protocols->raw);

        mojo::MakeSelfOwnedReceiver(
            std::make_unique<MockSmartCardConnection>(),
            pending_remote.InitWithNewPipeAndPassReceiver());

        auto success = device::mojom::SmartCardConnectSuccess::New(
            std::move(pending_remote), SmartCardProtocol::kT1);

        std::move(callback).Run(
            device::mojom::SmartCardConnectResult::NewSuccess(
                std::move(success)));
      }));

  GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader);

  ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

  auto expected_reader_names =
      base::Value(base::ListValue().Append("Foo").Append("Bar"));

  EXPECT_EQ("[object SmartCardConnection], t1", EvalJs(shell(), R"(
    (async () => {
      let context = await navigator.smartCard.establishContext();
      let result = await context.connect("Fake reader", "shared",
          {preferredProtocols: ["t0", "t1"]});
      return `${result.connection}, ${result.activeProtocol}`;
    })())"));
}

IN_PROC_BROWSER_TEST_F(SmartCardTest, ConnectDenied) {
  MockSmartCardContextFactory& mock_context_factory =
      GetFakeSmartCardDelegate().mock_context_factory;

  EXPECT_CALL(mock_context_factory, Connect).Times(0);

  {
    InSequence s;

    mock_context_factory.ExpectListReaders({kFakeReader});

    // No permission yet. So renderer will have to request it.
    EXPECT_CALL(GetFakeSmartCardDelegate(), HasReaderPermission(_, kFakeReader))
        .WillOnce(Return(false));

    // Permission was requested and it got denied.
    EXPECT_CALL(GetFakeSmartCardDelegate(),
                RequestReaderPermission(_, kFakeReader, _))
        .WillOnce(RunOnceCallback<2>(false));
  }

  ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

  EXPECT_EQ(
      "NotAllowedError, Failed to execute 'connect' on 'SmartCardContext': The "
      "user has denied permission or the user attention requirement was not "
      "fulfilled.",
      EvalJs(shell(), R"(
    (async () => {
      let context = await navigator.smartCard.establishContext();
      let readers = await context.listReaders();
      try {
        let result = await context.connect(readers[0], "shared",
            {preferredProtocols: ["t0", "t1"]});
      } catch (e) {
        return `${e.name}, ${e.message}`;
      }
      return "ok";
    })())"));
}

// Tests that a connection request is immediately denied if the application
// passes a reader name string that is not known to have come from the smart
// card API.
// This is to avoid presenting unfiltered strings to the user in a permission
// prompt.
IN_PROC_BROWSER_TEST_F(SmartCardTest, ConnectDeniedUnknownString) {
  constexpr char kMyDisturbingString[] = "my disturbing string";

  MockSmartCardContextFactory& mock_context_factory =
      GetFakeSmartCardDelegate().mock_context_factory;

  // The connection request shall not go through.
  EXPECT_CALL(mock_context_factory, Connect(_, _, _, _, _)).Times(0);

  // We tell that there's no permission yet.
  EXPECT_CALL(GetFakeSmartCardDelegate(),
              HasReaderPermission(_, kMyDisturbingString))
      .WillOnce(Return(false));

  // But the permission should not be requested as the reader name string is
  // unknown.
  EXPECT_CALL(GetFakeSmartCardDelegate(),
              RequestReaderPermission(_, kMyDisturbingString, _))
      .Times(0);

  ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

  EXPECT_EQ("NotAllowedError", EvalJs(shell(), JsReplace(R"(
    (async () => {
      let context = await navigator.smartCard.establishContext();
      try {
        let result = await context.connect($1, "shared",
            {preferredProtocols: ["t0", "t1"]});
      } catch (e) {
        return e.name;
      }
      return "ok";
    })())",
                                                         kMyDisturbingString)));
}

IN_PROC_BROWSER_TEST_F(SmartCardTest, ConnectPermissionGranted) {
  MockSmartCardContextFactory& mock_context_factory =
      GetFakeSmartCardDelegate().mock_context_factory;
  MockSmartCardConnection mock_connection;
  mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection);

  {
    InSequence s;

    mock_context_factory.ExpectListReaders({kFakeReader});

    // No permission yet. So renderer will have to request it.
    EXPECT_CALL(GetFakeSmartCardDelegate(), HasReaderPermission(_, kFakeReader))
        .WillOnce(Return(false));

    // Permission was requested and granted.
    EXPECT_CALL(GetFakeSmartCardDelegate(),
                RequestReaderPermission(_, kFakeReader, _))
        .WillOnce(RunOnceCallback<2>(true));

    // The Connect request will then finally go through.
    mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver);
  }

  ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

  EXPECT_EQ("ok", EvalJs(shell(), R"(
    (async () => {
      let context = await navigator.smartCard.establishContext();
      let readers = await context.listReaders();
      try {
        let result = await context.connect(readers[0], "shared",
            {preferredProtocols: ["t1"]});
      } catch (e) {
        return `${e.name}, ${e.message}`;
      }
      return "ok";
    })())"));
}

IN_PROC_BROWSER_TEST_F(SmartCardTest, StartTransaction) {
  ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

  MockSmartCardContextFactory& mock_context_factory =
      GetFakeSmartCardDelegate().mock_context_factory;
  MockSmartCardConnection mock_connection;
  mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection);

  MockSmartCardTransaction mock_transaction;
  mojo::AssociatedReceiver<SmartCardTransaction> transaction_receiver(
      &mock_transaction);

  {
    InSequence s;

    GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader);

    mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver);

    mock_connection.ExpectBeginTransaction(transaction_receiver);

    EXPECT_CALL(mock_connection, Transmit(SmartCardProtocol::kT1, _, _))
        .WillOnce(
            WithArgs<1, 2>([](const std::vector<uint8_t>& data,
                              SmartCardConnection::TransmitCallback callback) {
              EXPECT_EQ(data, std::vector<uint8_t>({3u, 2u, 1u}));
              std::move(callback).Run(
                  device::mojom::SmartCardDataResult::NewData({12u, 34u}));
            }));

    mock_transaction.ExpectEndTransaction(SmartCardDisposition::kReset);
  }

  EXPECT_EQ("ok", EvalJs(shell(), R"(
    (async () => {
      let context = await navigator.smartCard.establishContext();

      let connection =
        (await context.connect("Fake reader", "shared",
          {preferredProtocols: ["t1"]})).connection;

      let transaction = async () => {
        let apdu = new Uint8Array([0x03, 0x02, 0x01]);
        await connection.transmit(apdu);
        return "reset";
      }

      await connection.startTransaction(transaction);

      return "ok";
    })())"));
}

IN_PROC_BROWSER_TEST_F(SmartCardTest, StartTransactionAborted) {
  ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

  MockSmartCardContextFactory& mock_context_factory =
      GetFakeSmartCardDelegate().mock_context_factory;
  StrictMock<MockSmartCardConnection> mock_connection;
  mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection);

  base::test::TestFuture<SmartCardConnection::BeginTransactionCallback>
      begin_transaction_callback;

  {
    InSequence s;

    GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader);

    mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver);

    EXPECT_CALL(mock_connection, BeginTransaction(_))
        .WillOnce([&begin_transaction_callback](
                      SmartCardConnection::BeginTransactionCallback callback) {
          // Don't respond immediately.
          begin_transaction_callback.SetValue(std::move(callback));
        });

    // Aborting a blink connection.startTransaction() call means sending a
    // Cancel() request down to device.mojom.SmartCardContext
    EXPECT_CALL(mock_context_factory, Cancel(_))
        .WillOnce([&begin_transaction_callback](
                      SmartCardContext::CancelCallback callback) {
          begin_transaction_callback.Take().Run(
              device::mojom::SmartCardTransactionResult::NewError(
                  SmartCardError::kCancelled));

          std::move(callback).Run(
              SmartCardResult::NewSuccess(SmartCardSuccess::kOk));
        });
  }

  EXPECT_EQ("Exception: Error, Something", EvalJs(shell(), R"(
    (async () => {
      let context = await navigator.smartCard.establishContext();

      let connection =
        (await context.connect("Fake reader", "shared",
          {preferredProtocols: ["t1"]})).connection;

      let transaction = async () => {
        let apdu = new Uint8Array([0x03, 0x02, 0x01]);
        await connection.transmit(apdu);
        return "reset";
      }

      let abortController = new AbortController();

      let promise =
          connection.startTransaction(transaction,
              {signal: abortController.signal});

      abortController.abort(Error("Something"));

      try {
        await promise;
        return "Success";
      } catch (e) {
        return `Exception: ${e.name}, ${e.message}`;
      }
    })())"));
}

IN_PROC_BROWSER_TEST_F(SmartCardTest, TransactionCallbackRejects) {
  TestEmptyTransaction("startTransaction: Error, Oops!", R"(
      async () => { throw new Error('Oops!'); }
    )");
}

IN_PROC_BROWSER_TEST_F(SmartCardTest, TransactionCallbackThrows) {
  TestEmptyTransaction("startTransaction: Error, Oops!", R"(
      () => { throw new Error('Oops!'); }
    )");
}

IN_PROC_BROWSER_TEST_F(SmartCardTest, TransactionCallbackReturnsEmptyPromise) {
  TestEmptyTransaction("ok", R"( async () => {} )");
}

IN_PROC_BROWSER_TEST_F(SmartCardTest, TransactionCallbackReturnsNothing) {
  TestEmptyTransaction("ok", R"( () => {} )");
}

// A transaction callback must return a SmartCardDisposition.
// Check that if the callback returns something else, startTransaction() returns
// an appropriate error.
IN_PROC_BROWSER_TEST_F(SmartCardTest, TransactionCallbackReturnsInvalidValue) {
  TestEmptyTransaction(
      "startTransaction: TypeError, Failed to execute 'startTransaction' on "
      "'SmartCardConnection': The provided value '[object Object]' is not a "
      "valid enum value of type SmartCardDisposition.",
      R"(
      async () => {
        // Return some random object instead of a SmartCardDisposition
        return {foo: 'bar', hello: 42};
      }
      )");
}

IN_PROC_BROWSER_TEST_F(SmartCardTest, EndTransactionFails) {
  ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

  MockSmartCardContextFactory& mock_context_factory =
      GetFakeSmartCardDelegate().mock_context_factory;
  MockSmartCardConnection mock_connection;
  mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection);

  MockSmartCardTransaction mock_transaction;
  mojo::AssociatedReceiver<SmartCardTransaction> transaction_receiver(
      &mock_transaction);

  {
    InSequence s;

    GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader);
    mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver);
    mock_connection.ExpectBeginTransaction(transaction_receiver);

    EXPECT_CALL(mock_transaction,
                EndTransaction(SmartCardDisposition::kEject, _))
        .WillOnce(WithArg<1>(
            [](SmartCardTransaction::EndTransactionCallback callback) {
              std::move(callback).Run(
                  SmartCardResult::NewError(SmartCardError::kResetCard));
            }));
  }

  EXPECT_EQ(
      "startTransaction: SmartCardError, The smart card has been reset, so any "
      "shared state information is invalid.",
      EvalJs(shell(), R"(
    (async () => {
      let context = await navigator.smartCard.establishContext();

      let connection =
        (await context.connect("Fake reader", "shared",
          {preferredProtocols: ["t1"]})).connection;

      let transaction = async () => {
        return "eject";
      }

      transactionPromise = connection.startTransaction(transaction);
      try {
        await transactionPromise;
      } catch (e) {
        return `startTransaction: ${e.name}, ${e.message}`;
      }

      return "startTransaction did not throw";
    })())"));
}

IN_PROC_BROWSER_TEST_F(SmartCardTest, DisconnectedOnTransactionReturn) {
  ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

  MockSmartCardContextFactory& mock_context_factory =
      GetFakeSmartCardDelegate().mock_context_factory;
  MockSmartCardConnection mock_connection;
  mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection);

  StrictMock<MockSmartCardTransaction> mock_transaction;
  mojo::AssociatedReceiver<SmartCardTransaction> transaction_receiver(
      &mock_transaction);

  {
    InSequence s;

    GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader);
    mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver);
    mock_connection.ExpectBeginTransaction(transaction_receiver);

    EXPECT_CALL(mock_connection, Disconnect(SmartCardDisposition::kLeave, _))
        .WillOnce(
            WithArg<1>([](SmartCardConnection::DisconnectCallback callback) {
              std::move(callback).Run(
                  SmartCardResult::NewSuccess(SmartCardSuccess::kOk));
            }));
  }

  EXPECT_EQ(
      "startTransaction: InvalidStateError, Failed to execute "
      "'startTransaction' on 'SmartCardConnection': Cannot end transaction "
      "with an invalid connection.",
      EvalJs(shell(), R"(
    (async () => {
      let context = await navigator.smartCard.establishContext();

      let connection =
        (await context.connect("Fake reader", "shared",
          {preferredProtocols: ["t1"]})).connection;

      let transaction = async () => {
        await connection.disconnect();
        return "eject";
      }

      transactionPromise = connection.startTransaction(transaction);
      try {
        await transactionPromise;
      } catch (e) {
        return `startTransaction: ${e.name}, ${e.message}`;
      }

      return "startTransaction did not throw";
    })())"));
}

IN_PROC_BROWSER_TEST_F(SmartCardTest, OngoingTransmitOnTransactionReturn) {
  ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

  MockSmartCardContextFactory& mock_context_factory =
      GetFakeSmartCardDelegate().mock_context_factory;
  MockSmartCardConnection mock_connection;
  mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection);

  StrictMock<MockSmartCardTransaction> mock_transaction;
  mojo::AssociatedReceiver<SmartCardTransaction> transaction_receiver(
      &mock_transaction);

  {
    InSequence s;

    GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader);
    mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver);
    mock_connection.ExpectBeginTransaction(transaction_receiver);

    EXPECT_CALL(mock_connection, Transmit(SmartCardProtocol::kT1, _, _))
        .WillOnce(
            WithArgs<1, 2>([](const std::vector<uint8_t>& data,
                              SmartCardConnection::TransmitCallback callback) {
              EXPECT_EQ(data, std::vector<uint8_t>({3u, 2u, 1u}));
              std::move(callback).Run(
                  device::mojom::SmartCardDataResult::NewData({12u, 34u}));
            }));

    mock_transaction.ExpectEndTransaction(SmartCardDisposition::kEject);
  }

  EXPECT_EQ(
      "startTransaction: InvalidStateError, Transaction callback returned "
      "while an operation was still in progress.",
      EvalJs(shell(), R"(
    (async () => {
      let context = await navigator.smartCard.establishContext();

      let connection =
        (await context.connect("Fake reader", "shared",
          {preferredProtocols: ["t1"]})).connection;

      let transaction = async () => {
        // Return before the transmit() completes.
        let apdu = new Uint8Array([0x03, 0x02, 0x01]);
        connection.transmit(apdu);
        return "eject";
      }

      transactionPromise = connection.startTransaction(transaction);
      try {
        await transactionPromise;
      } catch (e) {
        return `startTransaction: ${e.name}, ${e.message}`;
      }
      return "ok";
    })())"));
}

IN_PROC_BROWSER_TEST_F(SmartCardTest,
                       ContextOperationBlocksConnectionOperation) {
  ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

  MockSmartCardContextFactory& mock_context_factory =
      GetFakeSmartCardDelegate().mock_context_factory;
  StrictMock<MockSmartCardConnection> mock_connection;
  mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection);

  TestFuture<SmartCardContext::ListReadersCallback> list_readers_callback;

  {
    InSequence s;

    GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader);

    mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver);

    EXPECT_CALL(mock_context_factory, ListReaders(_))
        .WillOnce([&list_readers_callback](
                      SmartCardContext::ListReadersCallback callback) {
          // Don't respond immediately.
          list_readers_callback.SetValue(std::move(callback));
        });
  }

  EXPECT_EQ(
      "control: InvalidStateError, Failed to execute 'control' on "
      "'SmartCardConnection': An operation is already in progress in this "
      "smart card context.",
      EvalJs(shell(), R"(
    (async () => {
      let context = await navigator.smartCard.establishContext();

      let connection =
        (await context.connect("Fake reader", "shared",
          {preferredProtocols: ["t1"]})).connection;

      let listReadersPromise = context.listReaders();

      try {
        let data = new Uint8Array([0x03, 0x02, 0x01]);
        await connection.control(42, data);
      } catch (e) {
        return `control: ${e.name}, ${e.message}`;
      }

      await listReadersPromise;

      return `ok`;
    })())"));

  // Let context.listReaders() conclude
  list_readers_callback.Take().Run(
      SmartCardListReadersResult::NewReaders({kFakeReader}));
}

IN_PROC_BROWSER_TEST_F(SmartCardTest, ConnectionDiesWithOperationInProgress) {
  ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

  MockSmartCardContextFactory& mock_context_factory =
      GetFakeSmartCardDelegate().mock_context_factory;
  StrictMock<MockSmartCardConnection> mock_connection;
  mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection);

  {
    InSequence s;

    GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader);

    mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver);

    EXPECT_CALL(mock_connection, Control(42, _, _))
        .WillOnce(WithoutArgs(
            [&connection_receiver]() { connection_receiver.reset(); }));
  }

  EXPECT_EQ(
      "control: InvalidStateError, Failed to execute 'control' on "
      "'SmartCardConnection': Is disconnected.",
      EvalJs(shell(), R"(
    (async () => {
      let context = await navigator.smartCard.establishContext();

      let connection =
        (await context.connect("Fake reader", "shared",
          {preferredProtocols: ["t1"]})).connection;

      try {
        let data = new Uint8Array([0x03, 0x02, 0x01]);
        await connection.control(42, data);
      } catch (e) {
        return `control: ${e.name}, ${e.message}`;
      }
    })())"));
}

IN_PROC_BROWSER_TEST_F(SmartCardTest, ContextDiesConnectionStays) {
  ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

  MockSmartCardContextFactory& mock_context_factory =
      GetFakeSmartCardDelegate().mock_context_factory;
  StrictMock<MockSmartCardConnection> mock_connection;
  mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection);

  {
    InSequence s;

    GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader);

    mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver);

    EXPECT_CALL(mock_connection, Control(42, _, _))
        .WillOnce(
            WithArg<2>([&mock_context_factory](
                           SmartCardConnection::ControlCallback callback) {
              mock_context_factory.ClearContextReceivers();
              std::move(callback).Run(
                  device::mojom::SmartCardDataResult::NewData({12u, 34u}));
            }));
  }

  EXPECT_EQ("response: 12,34", EvalJs(shell(), R"(
    (async () => {
      let context = await navigator.smartCard.establishContext();

      let connection =
        (await context.connect("Fake reader", "shared",
          {preferredProtocols: ["t1"]})).connection;

      let data = new Uint8Array([0x03, 0x02, 0x01]);
      let response = await connection.control(42, data);

      let responseString = new Uint8Array(response).toString();
      return `response: ${responseString}`;
    })())"));
}

IN_PROC_BROWSER_TEST_F(SmartCardTest, NoCoiPermission) {
  SmartCardTestContentBrowserClient client;
  client.SetSmartCardDelegate(std::make_unique<FakeSmartCardDelegate>());

  ASSERT_TRUE(
      NavigateToURL(shell(), GetIsolatedContextUrl(/*include_coi_pp=*/false)));

  EXPECT_EQ(false, EvalJs(shell(), "self.crossOriginIsolated"));
  EXPECT_THAT(EvalJs(shell(), "navigator.smartCard.establishContext()"),
              EvalJsResult::ErrorIs(HasSubstr(
                  "Frame is not sufficiently isolated to use smart cards.")));
}

/* Tests the situation where a transaction callback erroneously returns while an
 * operation in this connection is ongoing. If that operation fails at PC/SC
 * level the Web API implementation should still cleanup after itself by ending
 * the PC/SC transaction once that operation completes.
 */
IN_PROC_BROWSER_TEST_F(SmartCardTest, EndTransactionAfterFailedOperation) {
  ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

  MockSmartCardContextFactory& mock_context_factory =
      GetFakeSmartCardDelegate().mock_context_factory;
  MockSmartCardConnection mock_connection;
  mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection);

  MockSmartCardTransaction mock_transaction;
  mojo::AssociatedReceiver<SmartCardTransaction> transaction_receiver(
      &mock_transaction);

  {
    InSequence s;

    GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader);

    mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver);

    mock_connection.ExpectBeginTransaction(transaction_receiver);

    EXPECT_CALL(mock_connection, Status(_))
        .WillOnce([](SmartCardConnection::StatusCallback callback) {
          // Simulate a PC/SC failure.
          auto result = device::mojom::SmartCardStatusResult::NewError(
              SmartCardError::kReaderUnavailable);
          std::move(callback).Run(std::move(result));
        });

    mock_transaction.ExpectEndTransaction(SmartCardDisposition::kReset);
  }

  EXPECT_EQ(
      "startTransaction: InvalidStateError, Transaction callback returned "
      "while an operation was still in progress.",
      EvalJs(shell(), R"(
    (async () => {
      let context = await navigator.smartCard.establishContext();

      let connection =
        (await context.connect("Fake reader", "shared",
          {preferredProtocols: ["t1"]})).connection;

      let transaction = () => {
        connection.status();
      };

      try {
        await connection.startTransaction(transaction);
      } catch (e) {
        return `startTransaction: ${e.name}, ${e.message}`;
      }

      return "ok";
    })())"));
}

/* Tests the situation where a transaction callback erroneously returns while a
 * SmartCardContext operation is ongoing. The Web API implementation should
 * cleanup after itself by ending the PC/SC transaction once that operation
 * completes.
 */
IN_PROC_BROWSER_TEST_F(SmartCardTest, EndTransactionAfterContextOperation) {
  ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

  MockSmartCardContextFactory& mock_context_factory =
      GetFakeSmartCardDelegate().mock_context_factory;
  MockSmartCardConnection mock_connection;
  mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection);

  MockSmartCardTransaction mock_transaction;
  mojo::AssociatedReceiver<SmartCardTransaction> transaction_receiver(
      &mock_transaction);

  TestFuture<SmartCardContext::ListReadersCallback> list_readers_callback;

  {
    InSequence s;

    GetFakeSmartCardDelegate().ExpectHasReaderPermission(kFakeReader);

    mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver);

    mock_connection.ExpectBeginTransaction(transaction_receiver);

    mock_transaction.ExpectEndTransaction(SmartCardDisposition::kEject);
  }

  // Provide a mock expectation for ListReaders so it doesn't crash, but don't
  // resolve it immediately so it remains pending when the transaction callback
  // returns. Kept outside InSequence because Mojo messages on different pipes
  // may arrive in any order.
  EXPECT_CALL(mock_context_factory, ListReaders(_))
      .WillOnce([&list_readers_callback](
                    SmartCardContext::ListReadersCallback callback) {
        list_readers_callback.SetValue(std::move(callback));
      });

  EXPECT_EQ("ok", EvalJs(shell(), R"(
    (async () => {
      let context = await navigator.smartCard.establishContext();

      let connection =
        (await context.connect("Fake reader", "shared",
          {preferredProtocols: ["t1"]})).connection;

      let transaction = () => {
        // Start an operation but don't await it so it's pending when returning
        context.listReaders();
        return "eject";
      };

      window.transactionPromise = connection.startTransaction(transaction);

      return "ok";
    })())"));

  // Let context.listReaders() conclude. This doesn't trigger the error (the
  // InvalidStateError state is already locked in the moment the JS transaction
  // callback returned), but we must ensure it concludes so the test doesn't
  // time out or crash on teardown.
  list_readers_callback.Take().Run(
      SmartCardListReadersResult::NewReaders({kFakeReader}));

  EXPECT_EQ(
      "startTransaction: InvalidStateError, Transaction callback returned "
      "while an operation was still in progress.",
      EvalJs(shell(), R"(
    (async () => {
      try {
        await window.transactionPromise;
      } catch (e) {
        return `startTransaction: ${e.name}, ${e.message}`;
      }
      return "startTransaction succeeded unexpectedly";
    })())"));
}

IN_PROC_BROWSER_TEST_F(SmartCardTest, EstablishContextDenied) {
  ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

  EXPECT_CALL(GetFakeSmartCardDelegate(), IsPermissionBlocked(_))
      .WillOnce(Return(true));

  EXPECT_EQ("NotAllowedError", EvalJs(shell(), R"((async () => {
      try {
         let context = await navigator.smartCard.establishContext();
      } catch (e) {
         return e.name;
      }
      return "ok";
     })())"));
}

IN_PROC_BROWSER_TEST_F(SmartCardTest, WatcherClosedWhenPermissionExpired) {
  ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

  MockSmartCardContextFactory& mock_context_factory =
      GetFakeSmartCardDelegate().mock_context_factory;
  MockSmartCardConnection mock_connection;
  mojo::Receiver<SmartCardConnection> connection_receiver(&mock_connection);
  mojo::Remote<device::mojom::SmartCardConnectionWatcher> watcher;
  raw_ptr<content::SmartCardDelegate::PermissionObserver> observer_store =
      nullptr;

  {
    InSequence s;

    auto& delegate = GetFakeSmartCardDelegate();

    EXPECT_CALL(delegate, AddObserver).WillOnce(SaveArg<1>(&observer_store));
    delegate.ExpectHasReaderPermission(kFakeReader);
    EXPECT_CALL(mock_context_factory,
                Connect("Fake reader", SmartCardShareMode::kShared, _, _, _))
        .WillOnce(WithArgs<3, 4>(
            [&connection_receiver, &watcher](
                mojo::PendingRemote<device::mojom::SmartCardConnectionWatcher>
                    connection_watcher,
                SmartCardContext::ConnectCallback callback) {
              watcher.Bind(std::move(connection_watcher));

              auto success = device::mojom::SmartCardConnectSuccess::New(
                  connection_receiver.BindNewPipeAndPassRemote(),
                  SmartCardProtocol::kT1);

              std::move(callback).Run(
                  device::mojom::SmartCardConnectResult::NewSuccess(
                      std::move(success)));
            }));
    EXPECT_CALL(delegate, HasReaderPermission(_, "Fake reader"))
        .WillOnce(Return(false));
    EXPECT_CALL(delegate, NotifyLastConnectionLost(_));
  }

  EXPECT_TRUE(ExecJs(shell(), R"(
    (async () => {
      let context = await navigator.smartCard.establishContext();

      let connection =
        (await context.connect("Fake reader", "shared",
          {preferredProtocols: ["t1"]})).connection;
    })())"));

  TestFuture<void> watcher_closed;
  ASSERT_TRUE(watcher.is_connected());
  watcher.set_disconnect_handler(base::BindOnce(
      [](TestFuture<void>* watcher_closed) { watcher_closed->SetValue(); },
      &watcher_closed));
  ASSERT_TRUE(observer_store);
  auto& observer = CHECK_DEREF(observer_store.get());

  observer.OnPermissionRevoked(url::Origin::Create(GetIsolatedContextUrl()));
  EXPECT_TRUE(watcher_closed.Wait());
  ASSERT_FALSE(watcher.is_connected());

  content::FetchHistogramsFromChildProcesses();
  histogram_tester_.ExpectUniqueSample(
      "SmartCard.ConnectionClosedReason",
      SmartCardConnectionClosedReason::
          kSmartCardConnectionClosedPermissionRevoked,
      1);
}

IN_PROC_BROWSER_TEST_F(SmartCardTest, TransactionBlocksOtherConnection) {
  ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

  MockSmartCardContextFactory& mock_context_factory =
      GetFakeSmartCardDelegate().mock_context_factory;

  // Connection 1.
  MockSmartCardConnection mock_connection1;
  mojo::Receiver<SmartCardConnection> connection_receiver1(&mock_connection1);

  // Connection 2.
  MockSmartCardConnection mock_connection2;
  mojo::Receiver<SmartCardConnection> connection_receiver2(&mock_connection2);

  MockSmartCardTransaction mock_transaction1;
  mojo::AssociatedReceiver<SmartCardTransaction> transaction_receiver1(
      &mock_transaction1);

  {
    InSequence s;

    auto& delegate = GetFakeSmartCardDelegate();
    delegate.ExpectHasReaderPermission(kFakeReader);

    // Connect 1.
    mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver1);

    // Connect 2.
    delegate.ExpectHasReaderPermission(kFakeReader);
    mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver2);

    // Connection 1 starts transaction.
    mock_connection1.ExpectBeginTransaction(transaction_receiver1);

    // Connection 1 can transmit while holding the transaction.
    EXPECT_CALL(mock_connection1, Transmit(SmartCardProtocol::kT1, _, _))
        .WillOnce(RunOnceCallback<2>(
            device::mojom::SmartCardDataResult::NewData({1u, 2u, 3u})));

    // Connection 1 ends transaction.
    mock_transaction1.ExpectEndTransaction(SmartCardDisposition::kReset);

    // Connection 2 can now transmit.
    EXPECT_CALL(mock_connection2, Transmit(SmartCardProtocol::kT1, _, _))
        .WillOnce(RunOnceCallback<2>(
            device::mojom::SmartCardDataResult::NewData({1u, 2u, 3u})));
  }

  EXPECT_EQ("ok", EvalJs(shell(), R"(
    (async () => {
      const context = await navigator.smartCard.establishContext();

      const c1 = (await context.connect("Fake reader", "shared",
          {preferredProtocols: ["t1"]})).connection;

      const c2 = (await context.connect("Fake reader", "shared",
          {preferredProtocols: ["t1"]})).connection;

      const anotherConnectionHoldsTransaction =
          'Another connection holds an active transaction on this reader.';
      const connectionAlreadyHoldsTransaction =
          'A connection already holds an active transaction on this reader.';

      const check = async (operation, name, msg, callStr) => {
        try {
          await operation();
          return `${callStr} should have failed`;
        } catch (e) {
          if (e.name !== name || !e.message.endsWith(msg)) {
            return `${callStr} failed with error: ${e.name}, ${e.message}`;
          }
        }
      };

      await c1.startTransaction(async () => {
        const apdu1 = new Uint8Array([0x00, 0x01, 0x02]);
        await c1.transmit(apdu1);

        const blockedOperations = [
          {
            call: () => c2.transmit(apdu1),
            name: 'c2.transmit()',
            msg: anotherConnectionHoldsTransaction
          },
          {
            call: () => c2.startTransaction(async () => {}),
            name: 'c2.startTransaction()',
            msg: connectionAlreadyHoldsTransaction
          },
          {
            call: () => c2.control(42, new Uint8Array([0x03, 0x02, 0x01])),
            name: 'c2.control()',
            msg: anotherConnectionHoldsTransaction
          },
          {
            call: () => c2.status(),
            name: 'c2.status()',
            msg: anotherConnectionHoldsTransaction
          },
          {
            call: () => c2.getAttribute(42),
            name: 'c2.getAttribute()',
            msg: anotherConnectionHoldsTransaction
          },
          {
            call: () => c2.setAttribute(42, new Uint8Array([0x03, 0x02, 0x01])),
            name: 'c2.setAttribute()',
            msg: anotherConnectionHoldsTransaction
          }
        ];

        for (const op of blockedOperations) {
          const err = await check(
              op.call, 'InvalidStateError', op.msg, op.name);
          if (err) {
            return err;
          }
        }

        return "reset";
      });

      // Now that the transaction is over, c2.transmit() should work.
      const apdu = new Uint8Array([0x00, 0x01, 0x02]);
      await c2.transmit(apdu);

      return "ok";
    })())"));
}

IN_PROC_BROWSER_TEST_F(SmartCardTest, TransactionReleasedOnDisconnect) {
  ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

  MockSmartCardContextFactory& mock_context_factory =
      GetFakeSmartCardDelegate().mock_context_factory;

  // Connection 1.
  MockSmartCardConnection mock_connection1;
  mojo::Receiver<SmartCardConnection> connection_receiver1(&mock_connection1);

  // Connection 2.
  MockSmartCardConnection mock_connection2;
  mojo::Receiver<SmartCardConnection> connection_receiver2(&mock_connection2);

  MockSmartCardTransaction mock_transaction1;
  mojo::AssociatedReceiver<SmartCardTransaction> transaction_receiver1(
      &mock_transaction1);

  {
    InSequence s;

    auto& delegate = GetFakeSmartCardDelegate();
    delegate.ExpectHasReaderPermission(kFakeReader);

    // Connect 1.
    mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver1);

    // Connect 2.
    delegate.ExpectHasReaderPermission(kFakeReader);
    mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver2);

    // Connection 1 starts transaction.
    mock_connection1.ExpectBeginTransaction(transaction_receiver1);

    // Connection 1 is disconnected.
    EXPECT_CALL(mock_connection1, Disconnect(SmartCardDisposition::kLeave, _))
        .WillOnce(
            WithArg<1>([](SmartCardConnection::DisconnectCallback callback) {
              std::move(callback).Run(
                  SmartCardResult::NewSuccess(SmartCardSuccess::kOk));
            }));

    // Connection 2 can now transmit.
    EXPECT_CALL(mock_connection2, Transmit(SmartCardProtocol::kT1, _, _))
        .WillOnce(RunOnceCallback<2>(
            device::mojom::SmartCardDataResult::NewData({1u, 2u, 3u})));
  }

  EXPECT_EQ("ok", EvalJs(shell(), R"(
    (async () => {
      const context = await navigator.smartCard.establishContext();

      const c1 = (await context.connect("Fake reader", "shared",
          {preferredProtocols: ["t1"]})).connection;

      const c2 = (await context.connect("Fake reader", "shared",
          {preferredProtocols: ["t1"]})).connection;

      // c1 starts a transaction and never finishes it.
      // It disconnects instead.
      const txPromise = c1.startTransaction(async () => {
        await c1.disconnect();
        await new Promise(r => {}); // Never returns
      });

      try {
        await txPromise;
      } catch (e) {
        // Expected to reject with InvalidStateError
      }

      // Connection 2 should now be able to transmit.
      const apdu = new Uint8Array([0x00, 0x01, 0x02]);
      await c2.transmit(apdu);

      return "ok";
    })())"));
}

IN_PROC_BROWSER_TEST_F(SmartCardTest, TransactionIsolation) {
  ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

  MockSmartCardContextFactory& mock_context_factory =
      GetFakeSmartCardDelegate().mock_context_factory;

  MockSmartCardConnection mock_connection1;
  mojo::Receiver<SmartCardConnection> connection_receiver1(&mock_connection1);

  MockSmartCardConnection mock_connection2;
  mojo::Receiver<SmartCardConnection> connection_receiver2(&mock_connection2);

  MockSmartCardTransaction mock_transaction1;
  mojo::AssociatedReceiver<SmartCardTransaction> transaction_receiver1(
      &mock_transaction1);

  MockSmartCardTransaction mock_transaction2;
  mojo::AssociatedReceiver<SmartCardTransaction> transaction_receiver2(
      &mock_transaction2);

  {
    InSequence s;

    auto& delegate = GetFakeSmartCardDelegate();
    delegate.ExpectHasReaderPermission(kFakeReader);

    mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver1);

    delegate.ExpectHasReaderPermission("Another reader");
    EXPECT_CALL(mock_context_factory,
                Connect("Another reader",
                        device::mojom::SmartCardShareMode::kShared, _, _, _))
        .WillOnce(
            [&connection_receiver2](
                const std::string& reader,
                device::mojom::SmartCardShareMode share_mode,
                device::mojom::SmartCardProtocolsPtr preferred_protocols,
                mojo::PendingRemote<device::mojom::SmartCardConnectionWatcher>
                    watcher,
                SmartCardContext::ConnectCallback callback) {
              auto success = device::mojom::SmartCardConnectSuccess::New(
                  connection_receiver2.BindNewPipeAndPassRemote(),
                  device::mojom::SmartCardProtocol::kT1);
              std::move(callback).Run(
                  device::mojom::SmartCardConnectResult::NewSuccess(
                      std::move(success)));
            });

    mock_connection1.ExpectBeginTransaction(transaction_receiver1);

    mock_connection2.ExpectBeginTransaction(transaction_receiver2);

    mock_transaction2.ExpectEndTransaction(SmartCardDisposition::kLeave);

    EXPECT_CALL(mock_connection2, Transmit(SmartCardProtocol::kT1, _, _))
        .WillOnce(RunOnceCallback<2>(
            device::mojom::SmartCardDataResult::NewData({1u, 2u, 3u})));

    mock_transaction1.ExpectEndTransaction(SmartCardDisposition::kReset);
  }

  EXPECT_EQ("ok", EvalJs(shell(), R"(
    (async () => {
      const context = await navigator.smartCard.establishContext();

      const c1 = (await context.connect("Fake reader", "shared",
          {preferredProtocols: ["t1"]})).connection;

      const c2 = (await context.connect("Another reader", "shared",
          {preferredProtocols: ["t1"]})).connection;

      await c1.startTransaction(async () => {
        await c2.startTransaction(async () => {
          return "leave";
        });

        const apdu = new Uint8Array([0x00, 0x01, 0x02]);
        await c2.transmit(apdu);
        return "reset";
      });

      return "ok";
    })())"));
}

IN_PROC_BROWSER_TEST_F(SmartCardTest, TransactionBlocksConnect) {
  ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));

  MockSmartCardContextFactory& mock_context_factory =
      GetFakeSmartCardDelegate().mock_context_factory;

  MockSmartCardConnection mock_connection1;
  mojo::Receiver<SmartCardConnection> connection_receiver1(&mock_connection1);

  MockSmartCardConnection mock_connection2;
  mojo::Receiver<SmartCardConnection> connection_receiver2(&mock_connection2);

  MockSmartCardTransaction mock_transaction1;
  mojo::AssociatedReceiver<SmartCardTransaction> transaction_receiver1(
      &mock_transaction1);

  {
    InSequence s;

    auto& delegate = GetFakeSmartCardDelegate();
    delegate.ExpectHasReaderPermission(kFakeReader);

    mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver1);

    mock_connection1.ExpectBeginTransaction(transaction_receiver1);

    delegate.ExpectHasReaderPermission("Another reader");
    EXPECT_CALL(mock_context_factory,
                Connect("Another reader",
                        device::mojom::SmartCardShareMode::kShared, _, _, _))
        .WillOnce(
            [&connection_receiver2](
                const std::string& reader,
                device::mojom::SmartCardShareMode share_mode,
                device::mojom::SmartCardProtocolsPtr preferred_protocols,
                mojo::PendingRemote<device::mojom::SmartCardConnectionWatcher>
                    watcher,
                SmartCardContext::ConnectCallback callback) {
              auto success = device::mojom::SmartCardConnectSuccess::New(
                  connection_receiver2.BindNewPipeAndPassRemote(),
                  device::mojom::SmartCardProtocol::kT1);
              std::move(callback).Run(
                  device::mojom::SmartCardConnectResult::NewSuccess(
                      std::move(success)));
            });

    mock_transaction1.ExpectEndTransaction(SmartCardDisposition::kReset);
  }

  EXPECT_EQ("ok", EvalJs(shell(), R"(
    (async () => {
      const context = await navigator.smartCard.establishContext();

      const c1 = (await context.connect("Fake reader", "shared",
          {preferredProtocols: ["t1"]})).connection;

      let connectRejected = false;
      await c1.startTransaction(async () => {
        try {
          await context.connect("Fake reader", "shared",
              {preferredProtocols: ["t1"]});
        } catch (e) {
          if (e.name === 'InvalidStateError') {
            connectRejected = true;
          }
        }

        // Should succeed
        await context.connect("Another reader", "shared",
              {preferredProtocols: ["t1"]});

        return "reset";
      });

      return connectRejected ? "ok" : "context.connect() should have failed";
    })())"));
}

}  // namespace content
