blob: fd9eb7c390842313e2c0f7bd84ab967a03af6840 [file] [log] [blame]
// 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/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "content/browser/smart_card/mock_smart_card_context_factory.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/content_browser_client.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/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/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 "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/smart_card/smart_card.mojom.h"
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::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::InSequence;
using testing::StrictMock;
namespace content {
namespace {
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(BrowserContext& browser_context) override;
bool SupportsReaderAddedRemovedNotifications() const override { return true; }
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(
content::BrowserContext* browser_context) override;
bool ShouldUrlUseApplicationIsolationLevel(BrowserContext* browser_context,
const GURL& url) override;
absl::optional<blink::ParsedPermissionsPolicy>
GetPermissionsPolicyForIsolatedWebApp(
content::BrowserContext* browser_context,
const url::Origin& app_origin) override;
private:
std::unique_ptr<SmartCardDelegate> delegate_;
};
class SmartCardTest : public ContentBrowserTest {
public:
GURL GetIsolatedContextUrl() {
return https_server_.GetURL(
"a.com",
"/set-header?Cross-Origin-Opener-Policy: same-origin&"
"Cross-Origin-Embedder-Policy: require-corp&"
"Permissions-Policy: smart-card%3D(self)");
}
FakeSmartCardDelegate& GetFakeSmartCardDelegate() {
return *static_cast<FakeSmartCardDelegate*>(
test_client_->GetSmartCardDelegate(nullptr));
}
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;
mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver);
mock_connection.ExpectBeginTransaction(transaction_receiver);
mock_transaction.ExpectEndTransaction(SmartCardDisposition::kLeave);
}
std::string js_snippet = std::format(R"(
(async () => {{
let context = await navigator.smartCard.establishContext();
let connection =
(await context.connect("Fake reader", "shared", ["t1"])).connection;
let transaction = {};
let transactionPromise = connection.startTransaction(transaction);
try {{
await transactionPromise;
}} catch (e) {{
return `startTransaction: ${{e.name}}, ${{e.message}}`;
}}
return "ok";
}})())",
transaction_callback);
EXPECT_EQ(expected_result, EvalJs(shell(), js_snippet));
}
private:
void SetUpCommandLine(base::CommandLine* command_line) override {
ContentBrowserTest::SetUpCommandLine(command_line);
mock_cert_verifier_.SetUpCommandLine(command_line);
}
void SetUpOnMainThread() override {
ContentBrowserTest::SetUpOnMainThread();
test_client_ = std::make_unique<SmartCardTestContentBrowserClient>();
test_client_->SetSmartCardDelegate(
std::make_unique<FakeSmartCardDelegate>());
mock_cert_verifier_.mock_cert_verifier()->set_default_result(net::OK);
// Serve a.com (and any other domain).
host_resolver()->AddRule("*", "127.0.0.1");
// Add a handler for the "/set-header" page (among others)
https_server_.AddDefaultHandlers(GetTestDataFilePath());
ASSERT_TRUE(https_server_.Start());
}
void SetUpInProcessBrowserTestFixture() override {
ContentBrowserTest::SetUpInProcessBrowserTestFixture();
mock_cert_verifier_.SetUpInProcessBrowserTestFixture();
}
void TearDownInProcessBrowserTestFixture() override {
ContentBrowserTest::TearDownInProcessBrowserTestFixture();
mock_cert_verifier_.TearDownInProcessBrowserTestFixture();
}
void TearDown() override {
ASSERT_TRUE(https_server_.ShutdownAndWaitUntilComplete());
ContentBrowserTest::TearDown();
}
std::unique_ptr<SmartCardTestContentBrowserClient> test_client_;
// Need a mock CertVerifier for HTTPS connections to succeed with the test
// server.
ContentMockCertVerifier mock_cert_verifier_;
net::EmbeddedTestServer https_server_{net::EmbeddedTestServer::TYPE_HTTPS};
base::test::ScopedFeatureList scoped_feature_list_{
blink::features::kSmartCard};
};
} // namespace
SmartCardTestContentBrowserClient::SmartCardTestContentBrowserClient() =
default;
SmartCardTestContentBrowserClient::~SmartCardTestContentBrowserClient() =
default;
SmartCardDelegate* SmartCardTestContentBrowserClient::GetSmartCardDelegate(
content::BrowserContext* browser_context) {
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;
}
absl::optional<blink::ParsedPermissionsPolicy>
SmartCardTestContentBrowserClient::GetPermissionsPolicyForIsolatedWebApp(
content::BrowserContext* browser_context,
const url::Origin& app_origin) {
blink::ParsedPermissionsPolicy out;
blink::ParsedPermissionsPolicyDeclaration decl(
blink::mojom::PermissionsPolicyFeature::kSmartCard,
/*allowed_origins=*/{},
/*self_if_matches=*/app_origin, /*matches_all_origins=*/false,
/*matches_opaque_src=*/false);
out.push_back(decl);
return out;
}
mojo::PendingRemote<device::mojom::SmartCardContextFactory>
FakeSmartCardDelegate::GetSmartCardContextFactory(
BrowserContext& browser_context) {
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;
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", ["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`;
})())"));
}
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;
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 in progress.",
EvalJs(shell(), R"(
(async () => {
let context = await navigator.smartCard.establishContext();
let connection =
(await context.connect("Fake reader", "shared", ["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;
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", ["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, 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;
mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver);
EXPECT_CALL(mock_connection, Control(42, _, _))
.WillOnce([](uint32_t control_code, 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", ["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;
mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver);
EXPECT_CALL(mock_connection, GetAttrib(42, _))
.WillOnce(
[](uint32_t tag, 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", ["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;
mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver);
EXPECT_CALL(mock_connection,
SetAttrib(42, std::vector<uint8_t>({3u, 2u, 1u}), _))
.WillOnce([](uint32_t tag, const std::vector<uint8_t>& data,
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", ["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;
mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver);
EXPECT_CALL(mock_connection, Status(_))
.WillOnce([](SmartCardConnection::StatusCallback callback) {
auto result = device::mojom::SmartCardStatusResult::NewStatus(
SmartCardStatus::New(
"Fake reader", 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", ["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;
EXPECT_CALL(mock_context_factory, ListReaders(_))
.WillOnce([](SmartCardContext::ListReadersCallback callback) {
std::vector<std::string> readers{"Foo", "Bar"};
auto result =
device::mojom::SmartCardListReadersResult::NewReaders(readers);
std::move(callback).Run(std::move(result));
});
ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));
auto expected_reader_names =
base::Value(base::Value::List().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([](SmartCardContext::ListReadersCallback callback) {
auto result = device::mojom::SmartCardListReadersResult::NewError(
SmartCardError::kNoReadersAvailable);
std::move(callback).Run(std::move(result));
});
ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));
auto expected_reader_names = base::Value(base::Value::List());
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(
[](base::TimeDelta timeout,
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, {unaware=false, 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 = `unaware=${flags.unaware}`
+ `, 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, 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(
[&get_status_callback](
base::TimeDelta timeout,
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("Fake reader", SmartCardShareMode::kShared, _, _))
.WillOnce([](const std::string& reader,
device::mojom::SmartCardShareMode share_mode,
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)));
});
ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));
auto expected_reader_names =
base::Value(base::Value::List().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",
["t0", "t1"]);
return `${result.connection}, ${result.activeProtocol}`;
})())"));
}
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;
mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver);
mock_connection.ExpectBeginTransaction(transaction_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}));
});
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", ["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, 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;
mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver);
mock_connection.ExpectBeginTransaction(transaction_receiver);
EXPECT_CALL(mock_transaction,
EndTransaction(SmartCardDisposition::kEject, _))
.WillOnce([](SmartCardDisposition disposition,
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", ["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;
mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver);
mock_connection.ExpectBeginTransaction(transaction_receiver);
EXPECT_CALL(mock_connection, Disconnect(SmartCardDisposition::kLeave, _))
.WillOnce([](SmartCardDisposition disposition,
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", ["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;
mock_context_factory.ExpectConnectFakeReaderSharedT1(connection_receiver);
mock_connection.ExpectBeginTransaction(transaction_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}));
});
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", ["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";
})())"));
}
} // namespace content