chromium/chrome/browser/chromeos/extensions/smart_card_provider_private/smart_card_provider_private_apitest.cc

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

#include "base/memory/raw_ptr.h"
#include "base/test/test_future.h"
#include "chrome/browser/chromeos/extensions/smart_card_provider_private/smart_card_provider_private_api.h"
#include "chrome/browser/extensions/extension_apitest.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/extensions/api/smart_card_provider_private.h"
#include "chrome/test/base/ui_test_utils.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "extensions/browser/background_script_executor.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/event_router_factory.h"
#include "extensions/browser/script_executor.h"
#include "extensions/common/switches.h"
#include "extensions/test/result_catcher.h"
#include "extensions/test/test_extension_dir.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace scard_api = extensions::api::smart_card_provider_private;

using device::mojom::SmartCardDisposition;
using device::mojom::SmartCardError;
using device::mojom::SmartCardResult;
using device::mojom::SmartCardResultPtr;
using device::mojom::SmartCardSuccess;
using testing::ElementsAre;

MATCHER_P(IsError, expected_error, "") {
  if (!arg->is_error()) {
    *result_listener << "is not an error";
    return false;
  }
  if (arg->get_error() != expected_error) {
    *result_listener << "expected " << expected_error << ", got "
                     << arg->get_error();
    return false;
  }
  return true;
}

namespace extensions {

class SmartCardProviderPrivateApiTest : public ExtensionApiTest {
 public:
  static constexpr char kEstablishContextJs[] =
      R"(
      chrome.smartCardProviderPrivate.onEstablishContextRequested.addListener(
          establishContext);

      function establishContext(requestId) {
        chrome.smartCardProviderPrivate.reportEstablishContextResult(
            requestId, 123, "SUCCESS");
      }
    )";

  static constexpr char kConnectJs[] =
      R"(
      let validHandle = 0;

      chrome.smartCardProviderPrivate.onConnectRequested.addListener(
          connect);

      function connect(requestId, scardContext, reader,
          shareMode, preferredProtocols) {
        if (scardContext != 123
            || validHandle !== 0) {
          chrome.smartCardProviderPrivate.reportGetStatusChangeResult(requestId,
              readerStates, "INVALID_PARAMETER");
          return;
        }
        validHandle = 987;
        chrome.smartCardProviderPrivate.reportConnectResult(requestId,
            validHandle, "T1", "SUCCESS");
      }
    )";

  static constexpr char kTransactionJs[] =
      R"(
      let transactionActive = false;

      chrome.smartCardProviderPrivate.onBeginTransactionRequested.addListener(
          beginTransaction);

      function beginTransaction(requestId, scardHandle) {
        if (scardHandle !== validHandle) {
          chrome.smartCardProviderPrivate.reportPlainResult(requestId,
            "INVALID_PARAMETER");
          return;
        }

        if (transactionActive === true) {
          chrome.smartCardProviderPrivate.reportPlainResult(requestId,
            "SHARING_VIOLATION");
          return;
        }

        transactionActive = true;

        chrome.smartCardProviderPrivate.reportPlainResult(requestId,
          "SUCCESS");
      }

      chrome.smartCardProviderPrivate.onEndTransactionRequested.addListener(
          endTransaction);

      function endTransaction(requestId, scardHandle, disposition) {
        if (scardHandle !== validHandle) {
          chrome.smartCardProviderPrivate.reportPlainResult(requestId,
            "INVALID_PARAMETER");
          chrome.test.notifyFail(`Got EndTransaction on a dead connection.`);
          return;
        }

        if (transactionActive === false) {
          chrome.smartCardProviderPrivate.reportPlainResult(requestId,
            "NOT_TRANSACTED");
          chrome.test.notifyFail(
            `Got EndTransaction without an active transaction.`);
          return;
        }

        transactionActive = false;

        chrome.smartCardProviderPrivate.reportPlainResult(requestId,
          "SUCCESS");

        if (afterEndTransaction) {
          afterEndTransaction(disposition);
        }
      }
    )";

  static constexpr char kArrayEqualsJs[] =
      R"(
      const arrayEquals = (a, b) =>
        a.length === b.length &&
        a.every((v, i) => v === b[i]);
    )";

  void LoadFakeProviderExtension(const std::string& background_js) {
    TestExtensionDir test_dir;
    constexpr char kManifest[] =
        R"({
             "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2nI64+TVbJNYUfte1hEwrWpjgiH3ucfKZ12NC6IT/Pm2pQdjR/3alrdW+rCkYbs0KmfUymb0GOE7fwZ0Gs+EqfxoKmwJaaZiv86GXEkPJctDvjqrJRUrKvM6aXZEkTQeaFLQVY9NDk3grSZzvC365l3c4zRN3A2i8KMWzB9HRQzKnN49zjgcTTu5DERYTzbJZBd0m9Ln1b3x3UVkVgoTUq7DexGMcOq1KYz0VHrFRo/LN1yJvECFmBb2pdl40g4UHq3UqrWDDInZZJ3sr01EePxYYwimMFsGnvH6sz8wHC09rXZ+w1YFYjsQ3P/3Bih1q/NdZ0aop3MEOCbHb4gipQIDAQAB",
             "name": "Fake Smart Card Provider",
             "version": "0.1",
             "manifest_version": 2,
             "background": { "scripts": ["background.js"] },
             "permissions": [ "smartCardProviderPrivate" ]
           })";
    test_dir.WriteManifest(kManifest);
    test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), background_js);
    extension_ = LoadExtension(test_dir.UnpackedPath());
    ASSERT_TRUE(extension_);
  }

  void LoadFakeProviderExtension(
      std::initializer_list<std::string> js_snippets) {
    std::string background_js;
    for (auto& js_snippet : js_snippets) {
      background_js.append(js_snippet);
    }
    LoadFakeProviderExtension(background_js);
  }

  device::mojom::SmartCardCreateContextResultPtr CreateContext() {
    base::test::TestFuture<device::mojom::SmartCardCreateContextResultPtr>
        result_future;

    ProviderAPI().CreateContext(result_future.GetCallback());

    return result_future.Take();
  }

  // Create a context and call GetStatusChange() on it with valid
  // parameters.
  device::mojom::SmartCardStatusChangeResultPtr GetStatusChange() {
    auto context_result = CreateContext();
    EXPECT_TRUE(context_result->is_context());
    mojo::Remote<device::mojom::SmartCardContext> context(
        std::move(context_result->get_context()));

    base::test::TestFuture<device::mojom::SmartCardStatusChangeResultPtr>
        result_future;

    {
      std::vector<device::mojom::SmartCardReaderStateInPtr> states_in;
      {
        auto state_in = device::mojom::SmartCardReaderStateIn::New();
        state_in->reader = "foo";
        state_in->current_state =
            device::mojom::SmartCardReaderStateFlags::New();
        state_in->current_state->unaware = true;
        state_in->current_state->ignore = false;
        states_in.push_back(std::move(state_in));
      }

      context->GetStatusChange(base::Seconds(1), std::move(states_in),
                               result_future.GetCallback());
    }

    return result_future.Take();
  }

  const Extension* extension() const { return extension_; }

  mojo::Remote<device::mojom::SmartCardConnection> CreateConnection(
      device::mojom::SmartCardContext& context) {
    base::test::TestFuture<device::mojom::SmartCardConnectResultPtr>
        result_future;

    auto preferred_protocols = device::mojom::SmartCardProtocols::New();
    preferred_protocols->t1 = true;

    context.Connect("foo-reader", device::mojom::SmartCardShareMode::kShared,
                    std::move(preferred_protocols),
                    result_future.GetCallback());

    device::mojom::SmartCardConnectResultPtr result = result_future.Take();
    if (result->is_error()) {
      ADD_FAILURE() << "Connect failed: " << result->get_error();
      return mojo::Remote<device::mojom::SmartCardConnection>();
    }

    device::mojom::SmartCardConnectSuccessPtr success =
        std::move(result->get_success());

    mojo::Remote<device::mojom::SmartCardConnection> connection(
        std::move(success->connection));
    EXPECT_TRUE(connection.is_connected());
    return connection;
  }

  SmartCardProviderPrivateAPI& ProviderAPI() {
    return SmartCardProviderPrivateAPI::Get(*profile());
  }

  using ContextAndConnection =
      std::tuple<mojo::Remote<device::mojom::SmartCardContext>,
                 mojo::Remote<device::mojom::SmartCardConnection>>;

  ContextAndConnection CreateContextAndConnection() {
    ContextAndConnection result;
    auto context_result = CreateContext();
    if (!context_result->is_context()) {
      ADD_FAILURE() << "Failed to create a smart card context.";
      return ContextAndConnection();
    }
    mojo::Remote<device::mojom::SmartCardContext> context(
        std::move(context_result->get_context()));

    mojo::Remote<device::mojom::SmartCardConnection> connection =
        CreateConnection(*context.get());
    if (!connection.is_bound()) {
      ADD_FAILURE() << "Failed to create a smart card connection,";
      return ContextAndConnection();
    }

    return ContextAndConnection(std::move(context), std::move(connection));
  }

 protected:
  void SetUpCommandLine(base::CommandLine* command_line) override {
    command_line->AppendSwitchASCII(switches::kAllowlistedExtensionID,
                                    "jofgjdphhceggjecimellaapdjjadibj");
    ExtensionApiTest::SetUpCommandLine(command_line);
  }

 private:
  raw_ptr<const Extension, DanglingUntriaged> extension_;
};

class EventObserver : public EventRouter::TestObserver {
 public:
  size_t GetEventCount(const std::string& name) const {
    return event_count_.contains(name) ? event_count_.at(name) : 0;
  }
  void WaitForEventCount(const std::string& name, size_t count) {
    if (GetEventCount(name) >= count) {
      return;
    }
    expected_event_name_ = name;
    expected_event_count_ = count;
    run_loop_.Run();
  }

 private:
  void OnWillDispatchEvent(const Event& event) override {
    event_count_[event.event_name]++;
    if (expected_event_name_ == event.event_name &&
        GetEventCount(expected_event_name_) >= expected_event_count_) {
      run_loop_.Quit();
    }
  }
  void OnDidDispatchEventToProcess(const Event& event,
                                   int process_id) override {}

  std::map<std::string, size_t> event_count_;
  std::string expected_event_name_;
  size_t expected_event_count_;
  base::RunLoop run_loop_;
};

class DisconnectObserver {
 public:
  base::RepeatingClosure GetClosure() { return run_loop_.QuitClosure(); }
  void Wait() { run_loop_.Run(); }

 private:
  base::RunLoop run_loop_;
};

IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest,
                       EstablishContextNoProvider) {
  EXPECT_THAT(CreateContext(), IsError(SmartCardError::kNoService));
}

IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest,
                       EstablishContextResponseTimeout) {
  ProviderAPI().SetResponseTimeLimitForTesting(base::Seconds(1));

  LoadFakeProviderExtension(R"(
      chrome.smartCardProviderPrivate.onEstablishContextRequested.addListener(
          function(requestId){});
    )");

  EXPECT_THAT(CreateContext(), IsError(SmartCardError::kNoService));
}

IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest,
                       EstablishContextResponseTimeoutTwice) {
  ProviderAPI().SetResponseTimeLimitForTesting(base::Seconds(1));

  LoadFakeProviderExtension(R"(
      chrome.smartCardProviderPrivate.onEstablishContextRequested.addListener(
          function(requestId){});
    )");

  EXPECT_THAT(CreateContext(), IsError(SmartCardError::kNoService));
  EXPECT_THAT(CreateContext(), IsError(SmartCardError::kNoService));
}

IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest, CreateContext) {
  LoadFakeProviderExtension(kEstablishContextJs);
  auto context_result = CreateContext();
  EXPECT_TRUE(context_result->is_context());
}

IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest, CreateContextFails) {
  LoadFakeProviderExtension(R"(
      chrome.smartCardProviderPrivate.onEstablishContextRequested.addListener(
          establishContext);

      function establishContext(requestId) {
        chrome.smartCardProviderPrivate.reportEstablishContextResult(
            requestId, 0, "INTERNAL_ERROR");
      }
    )");

  EXPECT_THAT(CreateContext(), IsError(SmartCardError::kInternalError));
}

// Tests that smartCardProviderPrivate.onReleaseContextRequested is emitted
// when a device::mojom::SmartCardContext is disconnected from its remote
// endpoint.
IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest,
                       ContextMojoDisconnection) {
  LoadFakeProviderExtension(R"(
      let establishedContext = 0;
      let establishContextCalled = false;

      chrome.smartCardProviderPrivate.onEstablishContextRequested.addListener(
          establishContext);

      function establishContext(requestId) {
        if (establishContextCalled) {
          chrome.test.fail("EstablishContext called more than once");
          chrome.smartCardProviderPrivate.reportEstablishContextResult(
              requestId, establishedContext, "INTERNAL_ERROR");
          return;
        }

        establishContextCalled = true;
        establishedContext = 123;
        chrome.smartCardProviderPrivate.reportEstablishContextResult(
            requestId, establishedContext, "SUCCESS");
      }

      chrome.smartCardProviderPrivate.onReleaseContextRequested.addListener(
          releaseContext);

      function releaseContext(requestId, scardContext) {
        if (scardContext != establishedContext || scardContext === 0) {
          chrome.smartCardProviderPrivate.reportReleaseContextResult(requestId,
              "INVALID_PARAMETER");
          return;
        }

        establishedContext = 0;

        chrome.smartCardProviderPrivate.reportReleaseContextResult(
            requestId, "SUCCESS");

        chrome.test.notifyPass();
      }
    )");

  ResultCatcher result_catcher;
  {
    auto context_result = CreateContext();
    ASSERT_TRUE(context_result->is_context());
    mojo::Remote<device::mojom::SmartCardContext> context(
        std::move(context_result->get_context()));
  }
  ASSERT_TRUE(result_catcher.GetNextResult()) << result_catcher.message();
}

// If we receive an scard_context from an unknown request, we should release it
// automatically to avoid "leaking" it in the provider side.
IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest,
                       EstablishContextUnknown) {
  LoadFakeProviderExtension(R"(
      // An scard context that SmartCardProviderPrivateAPI did not ask for.
      let unwantedScardContext = 333;

      chrome.smartCardProviderPrivate.onReleaseContextRequested.addListener(
          releaseContext);

      function releaseContext(requestId, scardContext) {
        if (scardContext !== unwantedScardContext) {
          chrome.smartCardProviderPrivate.reportReleaseContextResult(requestId,
              "INVALID_PARAMETER");
          return;
        }
        chrome.test.notifyPass();
      }

        function reportUnknownEstablishContext() {
          // Some arbitrary request id that SmartCardProviderPrivateAPI did
          // not generate.
          let unknownRequestId = 222;
          chrome.smartCardProviderPrivate.reportEstablishContextResult(
            unknownRequestId, unwantedScardContext, "SUCCESS");
        };
    )");
  ResultCatcher result_catcher;
  BackgroundScriptExecutor::ExecuteScriptAsync(
      profile(), extension()->id(), "reportUnknownEstablishContext();");
  ASSERT_TRUE(result_catcher.GetNextResult()) << result_catcher.message();
}

// In case the provider reports a successful EstablishContext request
// but gives an invalid scard_context value for it, the browser should
// consider the request as failed, ignoring this scard_context.
IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest,
                       EstablishContextInvalid) {
  LoadFakeProviderExtension(R"(
      chrome.smartCardProviderPrivate.onEstablishContextRequested.addListener(
          establishContext);

      function establishContext(requestId) {
        let invalidScardContext = 0;
        chrome.smartCardProviderPrivate.reportEstablishContextResult(
            requestId, invalidScardContext, "SUCCESS");
      }
    )");

  EXPECT_THAT(CreateContext(), IsError(SmartCardError::kInternalError));
}

IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest, ListReaders) {
  LoadFakeProviderExtension({kEstablishContextJs,
                             R"(
      chrome.smartCardProviderPrivate.onListReadersRequested.addListener(
          listReaders);

      function listReaders(requestId, scardContext) {
        if (scardContext != 123) {
          chrome.smartCardProviderPrivate.reportListReadersResult(requestId,
              readerStates, "INVALID_PARAMETER");
          return;
        }

        let readers = ["foo", "bar"];

        chrome.smartCardProviderPrivate.reportListReadersResult(requestId,
            readers, "SUCCESS");
      }
    )"});

  auto context_result = CreateContext();
  ASSERT_TRUE(context_result->is_context());
  mojo::Remote<device::mojom::SmartCardContext> context(
      std::move(context_result->get_context()));

  base::test::TestFuture<device::mojom::SmartCardListReadersResultPtr>
      result_future;

  context->ListReaders(result_future.GetCallback());

  device::mojom::SmartCardListReadersResultPtr result = result_future.Take();
  ASSERT_TRUE(result->is_readers());

  std::vector<std::string>& readers = result->get_readers();

  EXPECT_THAT(readers, ElementsAre("foo", "bar"));
}

IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest, ListReadersNoProvider) {
  LoadFakeProviderExtension(kEstablishContextJs);

  auto context_result = CreateContext();
  ASSERT_TRUE(context_result->is_context());
  mojo::Remote<device::mojom::SmartCardContext> context(
      std::move(context_result->get_context()));

  base::test::TestFuture<device::mojom::SmartCardListReadersResultPtr>
      result_future;

  context->ListReaders(result_future.GetCallback());

  EXPECT_THAT(result_future.Take(), IsError(SmartCardError::kNoService));
}

IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest,
                       ListReadersResponseTimeout) {
  ProviderAPI().SetResponseTimeLimitForTesting(base::Seconds(1));

  LoadFakeProviderExtension({kEstablishContextJs,
                             R"(
      chrome.smartCardProviderPrivate.onListReadersRequested.addListener(
          function(requestId, scardContext){});
    )"});

  auto context_result = CreateContext();
  ASSERT_TRUE(context_result->is_context());
  mojo::Remote<device::mojom::SmartCardContext> context(
      std::move(context_result->get_context()));

  base::test::TestFuture<device::mojom::SmartCardListReadersResultPtr>
      result_future;

  context->ListReaders(result_future.GetCallback());

  EXPECT_THAT(result_future.Take(), IsError(SmartCardError::kNoService));
}

IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest, GetStatusChange) {
  LoadFakeProviderExtension({kEstablishContextJs,
                             R"(
      chrome.smartCardProviderPrivate.onGetStatusChangeRequested.addListener(
          getStatusChange);

      function getStatusChange(requestId, scardContext, timeout,
                               readerStatesIn) {
        let readerStates = [];

        if (scardContext != 123 || timeout.milliseconds !== 1000) {
          chrome.smartCardProviderPrivate.reportGetStatusChangeResult(requestId,
              readerStates, "INVALID_PARAMETER");
          return;
        }

        for (const stateIn of readerStatesIn) {
          let state = {};
          state.reader = stateIn.reader;
          state.eventState = {"present": true};
          // Just so that the test code can also check that
          // currentCount was correctly sent.
          state.eventCount = stateIn.currentCount + 1;
          state.atr = new Uint8Array([1,2,3,4,5]);
          readerStates.push(state);
        }

        chrome.smartCardProviderPrivate.reportGetStatusChangeResult(requestId,
            readerStates, "SUCCESS");
      }
    )"});

  auto context_result = CreateContext();
  ASSERT_TRUE(context_result->is_context());
  mojo::Remote<device::mojom::SmartCardContext> context(
      std::move(context_result->get_context()));

  base::test::TestFuture<device::mojom::SmartCardStatusChangeResultPtr>
      result_future;

  {
    std::vector<device::mojom::SmartCardReaderStateInPtr> states_in;
    {
      auto state_in = device::mojom::SmartCardReaderStateIn::New();
      state_in->reader = "foo";
      state_in->current_state = device::mojom::SmartCardReaderStateFlags::New();
      state_in->current_state->unaware = true;
      state_in->current_state->ignore = false;
      state_in->current_count = 9u;
      states_in.push_back(std::move(state_in));
    }

    context->GetStatusChange(base::Seconds(1), std::move(states_in),
                             result_future.GetCallback());
  }

  device::mojom::SmartCardStatusChangeResultPtr result = result_future.Take();
  ASSERT_TRUE(result->is_reader_states());

  std::vector<device::mojom::SmartCardReaderStateOutPtr>& states_out =
      result->get_reader_states();

  ASSERT_EQ(states_out.size(), size_t(1));
  auto& state_out = states_out.at(0);
  EXPECT_EQ(state_out->reader, "foo");
  EXPECT_FALSE(state_out->event_state->unaware);
  EXPECT_TRUE(state_out->event_state->present);
  EXPECT_EQ(state_out->event_count, 10u);
  EXPECT_EQ(state_out->answer_to_reset, std::vector<uint8_t>({1, 2, 3, 4, 5}));
}

IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest,
                       GetStatusChangeNoProvider) {
  LoadFakeProviderExtension(kEstablishContextJs);

  EXPECT_THAT(GetStatusChange(), IsError(SmartCardError::kNoService));
}

IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest,
                       GetStatusChangeResponseTimeout) {
  ProviderAPI().SetResponseTimeLimitForTesting(base::Seconds(1));

  LoadFakeProviderExtension({kEstablishContextJs,
                             R"(
    chrome.smartCardProviderPrivate.onGetStatusChangeRequested.addListener(
        function (requestId, scardContext, timeout, readerStatesIn) {});
  )"});

  EXPECT_THAT(GetStatusChange(), IsError(SmartCardError::kNoService));
}

IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest, Connect) {
  LoadFakeProviderExtension({kEstablishContextJs,
                             R"(
      chrome.smartCardProviderPrivate.onConnectRequested.addListener(
          connect);

      function connect(requestId, scardContext, reader,
          shareMode, preferredProtocols) {
        if (scardContext != 123
            || reader !== "foo-reader"
            || shareMode !== "SHARED"
            || preferredProtocols.t0 !== false
            || preferredProtocols.t1 !== true
            || preferredProtocols.raw !== false) {
          chrome.smartCardProviderPrivate.reportGetStatusChangeResult(requestId,
              readerStates, "INVALID_PARAMETER");
          return;
        }

        chrome.smartCardProviderPrivate.reportConnectResult(requestId, 987,
            "T1", "SUCCESS");
      }
    )"});

  auto context_result = CreateContext();
  ASSERT_TRUE(context_result->is_context());
  mojo::Remote<device::mojom::SmartCardContext> context(
      std::move(context_result->get_context()));

  base::test::TestFuture<device::mojom::SmartCardConnectResultPtr>
      result_future;

  auto preferred_protocols = device::mojom::SmartCardProtocols::New();
  preferred_protocols->t0 = false;
  preferred_protocols->t1 = true;
  preferred_protocols->raw = false;

  context->Connect("foo-reader", device::mojom::SmartCardShareMode::kShared,
                   std::move(preferred_protocols), result_future.GetCallback());

  device::mojom::SmartCardConnectResultPtr result = result_future.Take();
  ASSERT_TRUE(result->is_success());

  device::mojom::SmartCardConnectSuccessPtr success =
      std::move(result->get_success());
  EXPECT_EQ(success->active_protocol, device::mojom::SmartCardProtocol::kT1);

  mojo::Remote<device::mojom::SmartCardConnection> connection(
      std::move(success->connection));
  EXPECT_TRUE(connection.is_connected());
}

IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest, ConnectNoProvider) {
  LoadFakeProviderExtension(kEstablishContextJs);

  auto context_result = CreateContext();
  ASSERT_TRUE(context_result->is_context());
  mojo::Remote<device::mojom::SmartCardContext> context(
      std::move(context_result->get_context()));

  base::test::TestFuture<device::mojom::SmartCardConnectResultPtr>
      result_future;

  auto preferred_protocols = device::mojom::SmartCardProtocols::New();
  preferred_protocols->t1 = true;

  context->Connect("foo-reader", device::mojom::SmartCardShareMode::kShared,
                   std::move(preferred_protocols), result_future.GetCallback());

  EXPECT_THAT(result_future.Take(), IsError(SmartCardError::kNoService));
}

IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest,
                       ConnectResponseTimeout) {
  ProviderAPI().SetResponseTimeLimitForTesting(base::Seconds(1));

  LoadFakeProviderExtension({kEstablishContextJs,
                             R"(
      chrome.smartCardProviderPrivate.onConnectRequested.addListener(
          function (requestId, scardContext, reader, shareMode,
              preferredProtocols) {});
    )"});

  auto context_result = CreateContext();
  ASSERT_TRUE(context_result->is_context());
  mojo::Remote<device::mojom::SmartCardContext> context(
      std::move(context_result->get_context()));

  base::test::TestFuture<device::mojom::SmartCardConnectResultPtr>
      result_future;

  auto preferred_protocols = device::mojom::SmartCardProtocols::New();
  preferred_protocols->t1 = true;

  context->Connect("foo-reader", device::mojom::SmartCardShareMode::kShared,
                   std::move(preferred_protocols), result_future.GetCallback());

  EXPECT_THAT(result_future.Take(), IsError(SmartCardError::kNoService));
}

IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest, Disconnect) {
  LoadFakeProviderExtension({kEstablishContextJs, kConnectJs,
                             R"(
      chrome.smartCardProviderPrivate.onDisconnectRequested.addListener(
          disconnect);

      function disconnect(requestId, scardHandle, disposition) {
        if (scardHandle !== validHandle || disposition != "UNPOWER_CARD") {
          chrome.smartCardProviderPrivate.reportPlainResult(requestId,
            "INVALID_PARAMETER");
          return;
        }
        validHandle = 0;
        chrome.smartCardProviderPrivate.reportPlainResult(requestId,
          "SUCCESS");
      }
    )"});

  auto [context, connection] = CreateContextAndConnection();
  ASSERT_TRUE(connection.is_bound());

  base::test::TestFuture<SmartCardResultPtr> result_future;

  connection->Disconnect(device::mojom::SmartCardDisposition::kUnpower,
                         result_future.GetCallback());

  SmartCardResultPtr result = result_future.Take();
  EXPECT_TRUE(result->is_success());
}

IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest, DisconnectNoProvider) {
  LoadFakeProviderExtension({kEstablishContextJs, kConnectJs});

  auto [context, connection] = CreateContextAndConnection();
  ASSERT_TRUE(connection.is_bound());

  base::test::TestFuture<SmartCardResultPtr> result_future;

  connection->Disconnect(device::mojom::SmartCardDisposition::kUnpower,
                         result_future.GetCallback());

  EXPECT_THAT(result_future.Take(), IsError(SmartCardError::kNoService));
}

IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest, DisconnectTimeout) {
  ProviderAPI().SetResponseTimeLimitForTesting(base::Seconds(1));

  LoadFakeProviderExtension({kEstablishContextJs, kConnectJs,
                             R"(
    chrome.smartCardProviderPrivate.onDisconnectRequested.addListener(
        function (requestId, scardHandle, disposition) {
          // Do nothing
        });
    )"});

  auto [context, connection] = CreateContextAndConnection();
  ASSERT_TRUE(connection.is_bound());

  base::test::TestFuture<SmartCardResultPtr> result_future;

  connection->Disconnect(device::mojom::SmartCardDisposition::kUnpower,
                         result_future.GetCallback());

  EXPECT_THAT(result_future.Take(), IsError(SmartCardError::kNoService));
}

// Tests that smartCardProviderPrivate.onDisconnectRequested is emitted
// when a device::mojom::SmartCardConnection is disconnected from its remote
// endpoint.
IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest,
                       ConnectionMojoDisconnection) {
  LoadFakeProviderExtension({kEstablishContextJs, kConnectJs,
                             R"(
      chrome.smartCardProviderPrivate.onDisconnectRequested.addListener(
          disconnect);

      function disconnect(requestId, scardHandle, disposition) {
        if (scardHandle !== validHandle) {
          chrome.smartCardProviderPrivate.reportPlainResult(requestId,
            "INVALID_HANDLE");
          return;
        }
        validHandle = 0;
        chrome.smartCardProviderPrivate.reportPlainResult(requestId,
          "SUCCESS");
        chrome.test.notifyPass();
      }
      )"});

  auto context_result = CreateContext();
  ASSERT_TRUE(context_result->is_context());
  mojo::Remote<device::mojom::SmartCardContext> context(
      std::move(context_result->get_context()));

  ResultCatcher result_catcher;
  {
    mojo::Remote<device::mojom::SmartCardConnection> connection =
        CreateConnection(*context.get());
    ASSERT_TRUE(connection.is_bound());
  }
  ASSERT_TRUE(result_catcher.GetNextResult()) << result_catcher.message();
}

// If SmartCardConnection::Disconnect() was previously successfully called,
// do nothing once that SmartCardConnection is disconnected from its remote
// endpoint.
// Reasoning being that the PC/SC handle represented by this SmartCardConnection
// is no longer valid. There's nothing to cleanup at that point.
IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest,
                       ConnectionApiDisconnectAndMojoDisconnection) {
  LoadFakeProviderExtension({kEstablishContextJs, kConnectJs,
                             R"(
      chrome.smartCardProviderPrivate.onDisconnectRequested.addListener(
          disconnect);

      function disconnect(requestId, scardHandle, disposition) {
        if (scardHandle !== validHandle) {
          chrome.smartCardProviderPrivate.reportPlainResult(requestId,
            "INVALID_HANDLE");
          return;
        }
        validHandle = 0;
        chrome.smartCardProviderPrivate.reportPlainResult(requestId,
          "SUCCESS");
      }
      )"});

  auto context_result = CreateContext();
  ASSERT_TRUE(context_result->is_context());
  mojo::Remote<device::mojom::SmartCardContext> context(
      std::move(context_result->get_context()));

  mojo::Remote<device::mojom::SmartCardConnection> connection =
      CreateConnection(*context.get());
  ASSERT_TRUE(connection.is_bound());

  base::test::TestFuture<SmartCardResultPtr> disconnect_result_future;
  connection->Disconnect(SmartCardDisposition::kLeave,
                         disconnect_result_future.GetCallback());

  ASSERT_TRUE(disconnect_result_future.Take()->is_success());

  DisconnectObserver disconnect_observer;
  ProviderAPI().SetDisconnectObserverForTesting(
      disconnect_observer.GetClosure());

  EventObserver event_observer;
  EventRouter* event_router =
      EventRouterFactory::GetForBrowserContext(profile());
  event_router->AddObserverForTesting(&event_observer);

  // Mojo disconnection from the remote endpoint should not cause
  // SmartCardProviderPrivateAPI to dispatch a
  // smartCardProviderPrivate.onDisconnectRequested event to the provider
  // extension since a successful PC/SC disconnection already took place.
  connection.reset();
  disconnect_observer.Wait();
  EXPECT_EQ(event_observer.GetEventCount(
                scard_api::OnDisconnectRequested::kEventName),
            0u);
}

IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest, Cancel) {
  LoadFakeProviderExtension({kEstablishContextJs,
                             R"(
      chrome.smartCardProviderPrivate.onCancelRequested.addListener(
          cancel);

      function cancel(requestId, scardContext) {
        if (scardContext != 123) {
          chrome.smartCardProviderPrivate.reportPlainResult(requestId,
              readerStates, "INVALID_PARAMETER");
          return;
        }

        chrome.smartCardProviderPrivate.reportPlainResult(requestId,
            "SUCCESS");
      }
      )"});

  auto context_result = CreateContext();
  ASSERT_TRUE(context_result->is_context());
  mojo::Remote<device::mojom::SmartCardContext> context(
      std::move(context_result->get_context()));

  base::test::TestFuture<device::mojom::SmartCardResultPtr> result_future;

  context->Cancel(result_future.GetCallback());

  SmartCardResultPtr result = result_future.Take();
  ASSERT_TRUE(result->is_success());
}

IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest, CancelNoProvider) {
  LoadFakeProviderExtension(kEstablishContextJs);

  auto context_result = CreateContext();
  ASSERT_TRUE(context_result->is_context());
  mojo::Remote<device::mojom::SmartCardContext> context(
      std::move(context_result->get_context()));

  base::test::TestFuture<device::mojom::SmartCardResultPtr> result_future;

  context->Cancel(result_future.GetCallback());

  EXPECT_THAT(result_future.Take(), IsError(SmartCardError::kNoService));
}

IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest, CancelResponseTimeout) {
  ProviderAPI().SetResponseTimeLimitForTesting(base::Seconds(1));

  LoadFakeProviderExtension({kEstablishContextJs, R"(
      chrome.smartCardProviderPrivate.onCancelRequested.addListener(
          function(requestId, scardContext){});
    )"});

  auto context_result = CreateContext();
  ASSERT_TRUE(context_result->is_context());
  mojo::Remote<device::mojom::SmartCardContext> context(
      std::move(context_result->get_context()));

  base::test::TestFuture<device::mojom::SmartCardResultPtr> result_future;

  context->Cancel(result_future.GetCallback());

  EXPECT_THAT(result_future.Take(), IsError(SmartCardError::kNoService));
}

// A mojo::SmartCardContext receives a call while there's still another call
// waiting for an answer from the provider.
// The implementation should wait until the provider answers that pending
// request before it forwards him the next one.
//
// In this case, it's a ListReaders() followed by a Connect().
IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest, ContextBusy) {
  LoadFakeProviderExtension(R"(
      let establishedContext = 0;

      let letListReadersProceed;
      let listReadersCanProceed = new Promise(function(resolve) {
        letListReadersProceed = resolve;
      });


      chrome.smartCardProviderPrivate.onEstablishContextRequested.addListener(
          function (requestId) {
        // Ensure we only give one context.
        if (establishedContext !== 0) {
          chrome.smartCardProviderPrivate.reportEstablishContextResult(
              requestId, establishedContext, "NO_MEMORY");
          return;
        }
        establishedContext = 123;
        chrome.smartCardProviderPrivate.reportEstablishContextResult(
            requestId, establishedContext, "SUCCESS");
      });

      chrome.smartCardProviderPrivate.onListReadersRequested.addListener(
          async function(requestId, scardContext){
        // Verify that the context is valid.
        if (establishedContext === 0 || scardContext !== establishedContext) {
          chrome.smartCardProviderPrivate.reportListReadersResult(requestId,
              readerStates, "INVALID_PARAMETER");
          return;
        }

        await listReadersCanProceed;

        let readers = ["foo", "bar"];

        chrome.smartCardProviderPrivate.reportListReadersResult(requestId,
            readers, "SUCCESS");
      });

      chrome.smartCardProviderPrivate.onConnectRequested.addListener(
          function (requestId, scardContext, reader, shareMode,
            preferredProtocols) {

        // Verify that the context is valid
        if (establishedContext === 0 || scardContext !== establishedContext) {
          chrome.smartCardProviderPrivate.reportConnectResult(requestId, 0,
              "", "INVALID_PARAMETER");
          return;
        }

        chrome.smartCardProviderPrivate.reportConnectResult(requestId, 987,
            "T1", "SUCCESS");
      });
    )");

  auto context_result = CreateContext();
  ASSERT_TRUE(context_result->is_context());
  mojo::Remote<device::mojom::SmartCardContext> context(
      std::move(context_result->get_context()));

  EventObserver event_observer;

  EventRouter* event_router =
      EventRouterFactory::GetForBrowserContext(profile());
  event_router->AddObserverForTesting(&event_observer);

  base::test::TestFuture<device::mojom::SmartCardListReadersResultPtr>
      list_readers_future;

  base::test::TestFuture<device::mojom::SmartCardConnectResultPtr>
      connect_future;

  EXPECT_EQ(event_observer.GetEventCount(
                scard_api::OnListReadersRequested::kEventName),
            0u);
  context->ListReaders(list_readers_future.GetCallback());
  context.FlushForTesting();
  // The ListReaders request should go straight away since the context is free.
  EXPECT_EQ(event_observer.GetEventCount(
                scard_api::OnListReadersRequested::kEventName),
            1u);

  context->Connect("foo", device::mojom::SmartCardShareMode::kShared,
                   device::mojom::SmartCardProtocols::New(),
                   connect_future.GetCallback());
  context.FlushForTesting();
  // The Connect request should not have been sent since the context is still
  // busy with the ListReaders that hasn't been answered yet.
  EXPECT_EQ(
      event_observer.GetEventCount(scard_api::OnConnectRequested::kEventName),
      0u);

  // Let the ListReaders call finish.
  {
    static constexpr char kScript[] =
        R"(
           letListReadersProceed();
           chrome.test.sendScriptResult('ok');
         )";
    base::Value result = BackgroundScriptExecutor::ExecuteScript(
        profile(), extension()->id(), kScript,
        BackgroundScriptExecutor::ResultCapture::kSendScriptResult);
    ASSERT_TRUE(result.is_string());
    EXPECT_EQ("ok", result.GetString());
  }

  {
    device::mojom::SmartCardListReadersResultPtr result =
        list_readers_future.Take();
    EXPECT_TRUE(result->is_readers());
  }

  // Now that the ListReaders call has finished the queued Connect request
  // should finally go through.
  event_observer.WaitForEventCount(scard_api::OnConnectRequested::kEventName,
                                   1u);

  {
    device::mojom::SmartCardConnectResultPtr result = connect_future.Take();
    EXPECT_TRUE(result->is_success());
  }

  event_router->RemoveObserverForTesting(&event_observer);
}

IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest,
                       ConnectionSharesContextFate) {
  LoadFakeProviderExtension({kEstablishContextJs, kConnectJs,
                             R"(
      let letListReadersProceed;
      let listReadersCanProceed = new Promise(function(resolve) {
        letListReadersProceed = resolve;
      });

      chrome.smartCardProviderPrivate.onListReadersRequested.addListener(
          async function(requestId, scardContext){
        if (scardContext !== 123) {
          chrome.smartCardProviderPrivate.reportListReadersResult(requestId,
              readerStates, "INVALID_PARAMETER");
          return;
        }

        console.log('listreaders will wait...');
        await listReadersCanProceed;
        console.log('proceeding with listreaders');
        let readers = ["foo-reader"];

        chrome.smartCardProviderPrivate.reportListReadersResult(requestId,
            readers, "SUCCESS");
      });

      chrome.smartCardProviderPrivate.onReleaseContextRequested.addListener(
          function(requestId, scardContext) {
        if (scardContext != 123) {
          chrome.smartCardProviderPrivate.reportReleaseContextResult(requestId,
              "INVALID_PARAMETER");
          return;
        }
        chrome.smartCardProviderPrivate.reportReleaseContextResult(
            requestId, "SUCCESS");
      });
      )"});

  DisconnectObserver disconnect_observer;

  ProviderAPI().SetDisconnectObserverForTesting(
      disconnect_observer.GetClosure());

  EventObserver event_observer;
  EventRouter* event_router =
      EventRouterFactory::GetForBrowserContext(profile());
  event_router->AddObserverForTesting(&event_observer);

  auto [context, connection] = CreateContextAndConnection();
  ASSERT_TRUE(connection.is_bound());

  // ListReaders() won't be answered until told so.
  // Thus its request will remain pending.
  base::test::TestFuture<device::mojom::SmartCardListReadersResultPtr>
      list_readers_future;
  context->ListReaders(list_readers_future.GetCallback());
  context.FlushForTesting();
  EXPECT_EQ(event_observer.GetEventCount(
                scard_api::OnListReadersRequested::kEventName),
            1u);

  EXPECT_TRUE(connection.is_connected());

  // Queues a ReleaseContext() to the provider due to the pending ListReaders()
  // response.
  context.reset();
  disconnect_observer.Wait();
  EXPECT_EQ(event_observer.GetEventCount(
                scard_api::OnReleaseContextRequested::kEventName),
            0u);

  // Lost of the SmartCardContext also causes the SmartCardConnection it created
  // to get disconnected, so it won't be able to send any requests.
  connection.FlushForTesting();
  EXPECT_FALSE(connection.is_connected());

  // Let the ListReaders call finish.
  {
    static constexpr char kScript[] =
        R"(
           letListReadersProceed();
           chrome.test.sendScriptResult('ok');
         )";
    base::Value result = BackgroundScriptExecutor::ExecuteScript(
        profile(), extension()->id(), kScript,
        BackgroundScriptExecutor::ResultCapture::kSendScriptResult);
    ASSERT_TRUE(result.is_string());
    EXPECT_EQ("ok", result.GetString());
  }

  // After ListReaders is done, ReleaseContext should go through.
  event_observer.WaitForEventCount(
      scard_api::OnReleaseContextRequested::kEventName, 1u);

  // The ListReaders callback never had the chance to get called since the
  // mojom::SmartCardContext was reset before that request finished.
  EXPECT_FALSE(list_readers_future.IsReady());

  ProviderAPI().SetDisconnectObserverForTesting(base::RepeatingClosure());
  event_router->RemoveObserverForTesting(&event_observer);
}

IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest, Transmit) {
  LoadFakeProviderExtension({kEstablishContextJs, kConnectJs, kArrayEqualsJs,
                             R"(
      chrome.smartCardProviderPrivate.onTransmitRequested.addListener(
          transmit);

      function transmit(requestId, scardHandle, protocol, data) {

        const inputArray = new Uint8Array(data);
        const expectedInputArray = new Uint8Array([3, 2, 1]);

        if (scardHandle !== validHandle || protocol != "T1"
            || !arrayEquals(inputArray, expectedInputArray)) {
          chrome.smartCardProviderPrivate.reportDataResult(requestId,
            new Uint8Array().buffer,
            "INVALID_PARAMETER");
          return;
        }

        let responseData = new Uint8Array([1, 100, 255]);

        chrome.smartCardProviderPrivate.reportDataResult(requestId,
          responseData.buffer, "SUCCESS");
      }
      )"});

  auto [context, connection] = CreateContextAndConnection();
  ASSERT_TRUE(connection.is_bound());

  base::test::TestFuture<device::mojom::SmartCardDataResultPtr> result_future;

  connection->Transmit(device::mojom::SmartCardProtocol::kT1, {3u, 2u, 1u},
                       result_future.GetCallback());

  auto result = result_future.Take();
  ASSERT_TRUE(result->is_data());

  EXPECT_EQ(result->get_data(), std::vector<uint8_t>({1u, 100u, 255u}));
}

IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest, TransmitTimeout) {
  ProviderAPI().SetResponseTimeLimitForTesting(base::Seconds(1));

  LoadFakeProviderExtension({kEstablishContextJs, kConnectJs,
                             R"(
      chrome.smartCardProviderPrivate.onTransmitRequested.addListener(
          function (requestId, scardHandle, protocol, data) {
            // Do nothing.
          });
      )"});

  auto [context, connection] = CreateContextAndConnection();
  ASSERT_TRUE(connection.is_bound());

  base::test::TestFuture<device::mojom::SmartCardDataResultPtr> result_future;

  connection->Transmit(device::mojom::SmartCardProtocol::kT1,
                       std::vector<uint8_t>({3u, 2u, 1u}),
                       result_future.GetCallback());

  EXPECT_THAT(result_future.Take(), IsError(SmartCardError::kNoService));
}

IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest, Control) {
  LoadFakeProviderExtension({kEstablishContextJs, kConnectJs, kArrayEqualsJs,
                             R"(
      chrome.smartCardProviderPrivate.onControlRequested.addListener(
          control);

      function control(requestId, scardHandle, controlCode, data) {

        const inputArray = new Uint8Array(data);
        const expectedInputArray = new Uint8Array([3, 2, 1]);

        if (scardHandle !== validHandle || controlCode !== 111
            || !arrayEquals(inputArray, expectedInputArray)) {
          chrome.smartCardProviderPrivate.reportDataResult(requestId,
            new Uint8Array().buffer,
            "INVALID_PARAMETER");
          return;
        }

        let responseData = new Uint8Array([1, 100, 255]);

        chrome.smartCardProviderPrivate.reportDataResult(requestId,
          responseData.buffer, "SUCCESS");
      }
      )"});

  auto [context, connection] = CreateContextAndConnection();
  ASSERT_TRUE(connection.is_bound());

  base::test::TestFuture<device::mojom::SmartCardDataResultPtr> result_future;

  connection->Control(111u, {3u, 2u, 1u}, result_future.GetCallback());

  device::mojom::SmartCardDataResultPtr result = result_future.Take();
  ASSERT_TRUE(result->is_data());

  EXPECT_EQ(result->get_data(), std::vector<uint8_t>({1u, 100u, 255u}));
}

IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest, ControlTimeout) {
  ProviderAPI().SetResponseTimeLimitForTesting(base::Seconds(1));

  LoadFakeProviderExtension({kEstablishContextJs, kConnectJs, R"(
      chrome.smartCardProviderPrivate.onControlRequested.addListener(
          function (requestId, scardHandle, controlCode, data) {
            // Do nothing.
          });
      )"});

  auto [context, connection] = CreateContextAndConnection();
  ASSERT_TRUE(connection.is_bound());

  base::test::TestFuture<device::mojom::SmartCardDataResultPtr> result_future;

  connection->Control(111u, std::vector<uint8_t>({3u, 2u, 1u}),
                      result_future.GetCallback());

  EXPECT_THAT(result_future.Take(), IsError(SmartCardError::kNoService));
}

IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest, GetAttrib) {
  LoadFakeProviderExtension({kEstablishContextJs, kConnectJs, R"(
      chrome.smartCardProviderPrivate.onGetAttribRequested.addListener(
          getAttrib);

      function getAttrib(requestId, scardHandle, attribId) {
        if (scardHandle !== validHandle || attribId !== 111) {
          chrome.smartCardProviderPrivate.reportDataResult(requestId,
            new Uint8Array().buffer,
            "INVALID_PARAMETER");
          return;
        }

        let responseData = new Uint8Array([1, 100, 255]);

        chrome.smartCardProviderPrivate.reportDataResult(requestId,
          responseData.buffer, "SUCCESS");
      }
      )"});

  auto [context, connection] = CreateContextAndConnection();
  ASSERT_TRUE(connection.is_bound());

  base::test::TestFuture<device::mojom::SmartCardDataResultPtr> result_future;

  connection->GetAttrib(111u, result_future.GetCallback());

  device::mojom::SmartCardDataResultPtr result = result_future.Take();
  ASSERT_TRUE(result->is_data());

  EXPECT_EQ(result->get_data(), std::vector<uint8_t>({1u, 100u, 255u}));
}

IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest, GetAttribTimeout) {
  ProviderAPI().SetResponseTimeLimitForTesting(base::Seconds(1));

  LoadFakeProviderExtension({kEstablishContextJs, kConnectJs, R"(
      chrome.smartCardProviderPrivate.onGetAttribRequested.addListener(
          function (requestId, scardHandle, attribId) {
            // Do nothing.
          });
      )"});

  auto [context, connection] = CreateContextAndConnection();
  ASSERT_TRUE(connection.is_bound());

  base::test::TestFuture<device::mojom::SmartCardDataResultPtr> result_future;

  connection->GetAttrib(111u, result_future.GetCallback());

  device::mojom::SmartCardDataResultPtr result = result_future.Take();
  ASSERT_TRUE(result->is_error());
  EXPECT_EQ(result->get_error(), SmartCardError::kNoService);
}

IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest, SetAttrib) {
  LoadFakeProviderExtension({kEstablishContextJs, kConnectJs, kArrayEqualsJs,
                             R"(
      chrome.smartCardProviderPrivate.onSetAttribRequested.addListener(
          setAttrib);

      function setAttrib(requestId, scardHandle, attribId, data) {

        const inputArray = new Uint8Array(data);
        const expectedInputArray = new Uint8Array([3, 2, 1]);

        if (scardHandle !== validHandle || attribId != 111
            || !arrayEquals(inputArray, expectedInputArray)) {
          chrome.smartCardProviderPrivate.reportPlainResult(requestId,
            "INVALID_PARAMETER");
          return;
        }

        chrome.smartCardProviderPrivate.reportPlainResult(requestId,
          "SUCCESS");
      }
      )"});

  auto [context, connection] = CreateContextAndConnection();
  ASSERT_TRUE(connection.is_bound());

  base::test::TestFuture<device::mojom::SmartCardResultPtr> result_future;

  connection->SetAttrib(111u, std::vector<uint8_t>({3u, 2u, 1u}),
                        result_future.GetCallback());

  device::mojom::SmartCardResultPtr result = result_future.Take();
  EXPECT_TRUE(result->is_success());
}

IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest, SetAttribTimeout) {
  ProviderAPI().SetResponseTimeLimitForTesting(base::Seconds(1));

  LoadFakeProviderExtension({kEstablishContextJs, kConnectJs, R"(
      chrome.smartCardProviderPrivate.onSetAttribRequested.addListener(
          function (requestId, scardHandle, attribId, data) {
            // Do nothing.
          });
      )"});

  auto [context, connection] = CreateContextAndConnection();
  ASSERT_TRUE(connection.is_bound());

  base::test::TestFuture<device::mojom::SmartCardResultPtr> result_future;

  connection->SetAttrib(111u, std::vector<uint8_t>({3u, 2u, 1u}),
                        result_future.GetCallback());

  device::mojom::SmartCardResultPtr result = result_future.Take();
  ASSERT_TRUE(result->is_error());
  EXPECT_EQ(result->get_error(), SmartCardError::kNoService);
}

IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest, Status) {
  LoadFakeProviderExtension({kEstablishContextJs, kConnectJs,
                             R"(
      chrome.smartCardProviderPrivate.onStatusRequested.addListener(
          status);

      function status(requestId, scardHandle) {
        if (scardHandle !== validHandle) {
          chrome.smartCardProviderPrivate.reportPlainResult(requestId,
            "INVALID_PARAMETER");
          return;
        }

        chrome.smartCardProviderPrivate.reportStatusResult(requestId,
          "FooReader", "SPECIFIC", "T1", new Uint8Array([3, 2, 1]),
          "SUCCESS");
      }
      )"});

  auto [context, connection] = CreateContextAndConnection();
  ASSERT_TRUE(connection.is_bound());

  base::test::TestFuture<device::mojom::SmartCardStatusResultPtr> result_future;

  connection->Status(result_future.GetCallback());

  auto result = result_future.Take();
  ASSERT_TRUE(result->is_status());

  device::mojom::SmartCardStatusPtr& status = result->get_status();
  EXPECT_EQ(status->reader_name, "FooReader");
  EXPECT_EQ(status->state, device::mojom::SmartCardConnectionState::kSpecific);
  EXPECT_EQ(status->protocol, device::mojom::SmartCardProtocol::kT1);
  EXPECT_EQ(status->answer_to_reset, std::vector<uint8_t>({3u, 2u, 1u}));
}

IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest,
                       BeginTransactionAndDropMojoRemote) {
  LoadFakeProviderExtension({kEstablishContextJs, kConnectJs, kTransactionJs,
                             R"(
      function afterEndTransaction(disposition) {
        if (disposition !== "LEAVE_CARD") {
          chrome.test.notifyFail(`Wrong disposition: ${disposition}`);
        }
        chrome.test.notifyPass();
      }
      )"});

  auto [context, connection] = CreateContextAndConnection();
  ASSERT_TRUE(connection.is_bound());

  base::test::TestFuture<device::mojom::SmartCardTransactionResultPtr>
      result_future;

  connection->BeginTransaction(result_future.GetCallback());

  auto result = result_future.Take();
  ASSERT_TRUE(result->is_transaction());

  // mojo disconnection of the SmartCardTransaction should trigger a
  // onEndTransactionRequested event to the provider.
  ResultCatcher result_catcher;
  {
    mojo::AssociatedRemote<device::mojom::SmartCardTransaction> transaction(
        std::move(result->get_transaction()));

    EXPECT_TRUE(transaction.is_connected());
  }
  ASSERT_TRUE(result_catcher.GetNextResult()) << result_catcher.message();
}

IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest,
                       BeginAndEndTransaction) {
  LoadFakeProviderExtension({kEstablishContextJs, kConnectJs, kTransactionJs,
                             R"(
      function afterEndTransaction(disposition) {
        if (disposition !== "UNPOWER_CARD") {
          chrome.test.notifyFail(`Wrong disposition: ${disposition}`);
        }
        chrome.test.notifyPass();
      }
      )"});

  auto [context, connection] = CreateContextAndConnection();
  ASSERT_TRUE(connection.is_bound());

  base::test::TestFuture<device::mojom::SmartCardTransactionResultPtr>
      result_future;

  connection->BeginTransaction(result_future.GetCallback());

  auto result = result_future.Take();
  ASSERT_TRUE(result->is_transaction());

  mojo::AssociatedRemote<device::mojom::SmartCardTransaction> transaction(
      std::move(result->get_transaction()));
  EXPECT_TRUE(transaction.is_connected());

  ResultCatcher result_catcher;
  base::test::TestFuture<device::mojom::SmartCardResultPtr> end_result_future;
  transaction->EndTransaction(SmartCardDisposition::kUnpower,
                              end_result_future.GetCallback());
  EXPECT_TRUE(end_result_future.Take()->is_success());
  EXPECT_TRUE(result_catcher.GetNextResult()) << result_catcher.message();
}

// If a SmartCardConnection remote is dropped, its SmartCardTransaction mojo
// connection (if any) will be dropped as well.
// The interface implementations are expected to clean up appropriately by
// calling first EndTransaction() and then Disconnect().
IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest,
                       TransactionSharesConnectionFate) {
  LoadFakeProviderExtension({kEstablishContextJs, kConnectJs, kTransactionJs,
                             R"(
      function afterEndTransaction(disposition) {
        if (disposition !== "LEAVE_CARD") {
          chrome.test.notifyFail(`Wrong disposition: ${disposition}`);
        }
        chrome.test.notifyPass();
      }

      chrome.smartCardProviderPrivate.onDisconnectRequested.addListener(
          disconnect);

      function disconnect(requestId, scardHandle, disposition) {
        if (scardHandle !== validHandle || disposition != "LEAVE_CARD") {
          chrome.smartCardProviderPrivate.reportPlainResult(requestId,
            "INVALID_PARAMETER");
          return;
        }
        validHandle = 0;
        chrome.smartCardProviderPrivate.reportPlainResult(requestId,
          "SUCCESS");

        if (transactionActive === true) {
          chrome.test.notifyFail(`Disconnected with an active transaction.`);
        }
      }
      )"});

  auto [context, connection] = CreateContextAndConnection();
  ASSERT_TRUE(connection.is_bound());

  base::test::TestFuture<device::mojom::SmartCardTransactionResultPtr>
      result_future;

  connection->BeginTransaction(result_future.GetCallback());

  auto result = result_future.Take();
  ASSERT_TRUE(result->is_transaction());

  mojo::AssociatedRemote<device::mojom::SmartCardTransaction> transaction(
      std::move(result->get_transaction()));
  EXPECT_TRUE(transaction.is_connected());

  ResultCatcher result_catcher;

  // Losing an SmartCardConnection mojo connection should also trigger
  // mojo disconnection of the SmartCardTransaction
  connection.reset();
  transaction.FlushForTesting();
  EXPECT_FALSE(transaction.is_connected());

  EXPECT_TRUE(result_catcher.GetNextResult()) << result_catcher.message();
}

}  // namespace extensions