blob: b28c46a1a20539194d0ccbec6baec5c9017f58eb [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <optional>
#include "base/base64.h"
#include "base/memory/raw_ptr.h"
#include "base/test/gmock_expected_support.h"
#include "base/values.h"
#include "chrome/browser/ui/web_applications/test/isolated_web_app_test_utils.h"
#include "chrome/browser/web_applications/isolated_web_apps/test/isolated_web_app_builder.h"
#include "components/permissions/permission_request_manager.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_devtools_protocol_client.h"
#include "content/public/test/test_navigation_observer.h"
#include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/blink/public/common/features.h"
namespace web_app {
namespace {
// Verifies that the command result does not contain an "error" field.
testing::Matcher<const base::DictValue*> IsSuccess() {
return testing::Pointee(testing::ResultOf(
"FindDict('error')",
[](const base::DictValue& dict) { return dict.FindDict("error"); },
testing::IsNull()));
}
// A simplified struct to make test assertions readable.
// Note: This does not cover all Mojo reader state flags,
// only the specific flags relevant to these tests.
struct TestReaderStateOut {
std::string name;
int event_count = 0;
std::vector<uint8_t> atr;
// Selected flags
bool changed = false;
bool present = false;
bool empty = false;
};
} // namespace
class SmartCardEmulationBrowserTest : public IsolatedWebAppBrowserTestHarness {
public:
SmartCardEmulationBrowserTest() {
feature_list_.InitAndEnableFeature(blink::features::kSmartCard);
}
~SmartCardEmulationBrowserTest() override = default;
void SetUpOnMainThread() override {
IsolatedWebAppBrowserTestHarness::SetUpOnMainThread();
InstallAndLaunchIWA();
AttachDevTools();
SetAutoAcceptPermissions();
}
void TearDownOnMainThread() override {
if (devtools_client_) {
devtools_client_->DetachProtocolClient();
devtools_client_.reset();
}
web_contents_ = nullptr;
IsolatedWebAppBrowserTestHarness::TearDownOnMainThread();
}
const base::DictValue* SendCommand(const std::string& method,
base::DictValue params = {}) {
return devtools_client_->SendCommandSync(method, std::move(params));
}
base::DictValue WaitForNotificationParams(const std::string& event_name) {
return devtools_client_->WaitForNotification(event_name, true);
}
void SendResponse(const std::string& response_method,
const base::DictValue& event_params,
base::DictValue result_fields = {}) {
const std::string* req_id = event_params.FindString("requestId");
ASSERT_TRUE(req_id) << "Event missing 'requestId'";
result_fields.Set("requestId", *req_id);
ASSERT_THAT(SendCommand(response_method, std::move(result_fields)),
IsSuccess());
}
content::TestDevToolsProtocolClient* client() {
return devtools_client_.get();
}
content::RenderFrameHost* app_frame() {
return web_contents_->GetPrimaryMainFrame();
}
content::WebContents* web_contents() { return web_contents_; }
// Helper: Waits for establishContext and reports success.
void HandleEstablishContext(
int context_id = 123,
std::optional<std::string> error_code = std::nullopt) {
auto params = WaitForNotificationParams(
"SmartCardEmulation.establishContextRequested");
if (error_code.has_value()) {
SendResponse("SmartCardEmulation.reportError", params,
base::DictValue().Set("resultCode", *error_code));
return;
}
SendResponse("SmartCardEmulation.reportEstablishContextResult", params,
base::DictValue().Set("contextId", context_id));
}
// Helper: Waits for listReaders and reports either success or error.
void HandleListReaders(const std::vector<std::string>& readers = {"Reader A"},
int context_id = 123,
std::optional<std::string> error_code = std::nullopt) {
auto params =
WaitForNotificationParams("SmartCardEmulation.listReadersRequested");
EXPECT_EQ(params.FindInt("contextId"), context_id);
if (error_code.has_value()) {
SendResponse("SmartCardEmulation.reportError", params,
base::DictValue().Set("resultCode", *error_code));
return;
}
base::ListValue readers_list;
for (const auto& reader : readers) {
readers_list.Append(reader);
}
SendResponse("SmartCardEmulation.reportListReadersResult", params,
base::DictValue().Set("readers", std::move(readers_list)));
}
// Helper: Waits for connect and reports either success or error.
void HandleConnect(const std::string& expected_reader = "Reader A",
const int handle = 123,
std::optional<std::string> active_protocol = std::nullopt,
std::optional<std::string> error_code = std::nullopt) {
auto params =
WaitForNotificationParams("SmartCardEmulation.connectRequested");
// Verify the request is for the correct reader.
const std::string* reader = params.FindString("reader");
ASSERT_THAT(reader, testing::Pointee(testing::StrEq(expected_reader)))
<< "Connect request was for the wrong reader!";
if (error_code.has_value()) {
SendResponse("SmartCardEmulation.reportError", params,
base::DictValue().Set("resultCode", *error_code));
return;
}
base::DictValue response;
response.Set("handle", handle);
if (active_protocol.has_value()) {
response.Set("activeProtocol", *active_protocol);
}
SendResponse("SmartCardEmulation.reportConnectResult", params,
std::move(response));
}
// Helper: Waits for getStatusChangeRequested and sends a simulated response.
void HandleGetStatusChange(
const std::vector<TestReaderStateOut>& states,
int context_id = 123,
std::optional<std::string> error_code = std::nullopt) {
auto params = WaitForNotificationParams(
"SmartCardEmulation.getStatusChangeRequested");
EXPECT_EQ(params.FindInt("contextId"), context_id);
if (error_code.has_value()) {
SendResponse("SmartCardEmulation.reportError", params,
base::DictValue().Set("resultCode", *error_code));
return;
}
base::ListValue response_list;
for (const auto& state : states) {
base::DictValue dict;
dict.Set("reader", state.name);
dict.Set("eventCount", state.event_count);
dict.Set("atr", base::Base64Encode(state.atr));
base::DictValue flags;
flags.Set("changed", state.changed);
flags.Set("present", state.present);
flags.Set("empty", state.empty);
dict.Set("eventState", std::move(flags));
response_list.Append(std::move(dict));
}
SendResponse(
"SmartCardEmulation.reportGetStatusChangeResult", params,
base::DictValue().Set("readerStates", std::move(response_list)));
}
// Helper: Waits for statusRequested and sends a response.
void HandleStatus(const std::string& reader_name,
const std::string& state,
const std::string& protocol,
const std::vector<uint8_t>& atr,
int handle = 123,
std::optional<std::string> error_code = std::nullopt) {
auto params =
WaitForNotificationParams("SmartCardEmulation.statusRequested");
EXPECT_EQ(params.FindInt("handle"), handle);
if (error_code.has_value()) {
SendResponse("SmartCardEmulation.reportError", params,
base::DictValue().Set("resultCode", *error_code));
return;
}
base::DictValue response;
response.Set("readerName", reader_name);
response.Set("state", state);
response.Set("atr", base::Base64Encode(atr));
if (!protocol.empty()) {
response.Set("protocol", protocol);
}
SendResponse("SmartCardEmulation.reportStatusResult", params,
std::move(response));
}
// Helper: Waits for transmitRequested and sends a response.
void HandleTransmit(
const std::vector<uint8_t>& expected_command,
const base::expected<std::vector<uint8_t>, std::string>& result,
int handle = 123) {
auto params =
WaitForNotificationParams("SmartCardEmulation.transmitRequested");
EXPECT_EQ(params.FindInt("handle"), handle);
const std::string* command_b64 = params.FindString("data");
ASSERT_TRUE(command_b64) << "Transmit event missing 'data' field";
std::string command_str;
base::Base64Decode(*command_b64, &command_str);
std::vector<uint8_t> actual_command(command_str.begin(), command_str.end());
EXPECT_EQ(actual_command, expected_command)
<< "JS sent wrong command bytes";
if (result.has_value()) {
SendResponse(
"SmartCardEmulation.reportDataResult", params,
base::DictValue().Set("data", base::Base64Encode(result.value())));
} else {
SendResponse("SmartCardEmulation.reportError", params,
base::DictValue().Set("resultCode", result.error()));
}
}
// Helper: Waits for controlRequested and sends a response.
void HandleControl(
int expected_control_code,
const std::vector<uint8_t>& expected_data,
const base::expected<std::vector<uint8_t>, std::string>& result,
int handle = 123) {
auto params =
WaitForNotificationParams("SmartCardEmulation.controlRequested");
EXPECT_EQ(params.FindInt("handle"), handle);
EXPECT_EQ(params.FindInt("controlCode"), expected_control_code);
const std::string* data_b64 = params.FindString("data");
ASSERT_TRUE(data_b64) << "Control event missing 'data' field";
std::string data_str;
base::Base64Decode(*data_b64, &data_str);
std::vector<uint8_t> actual_data(data_str.begin(), data_str.end());
EXPECT_EQ(actual_data, expected_data) << "JS sent wrong control data bytes";
if (result.has_value()) {
SendResponse(
"SmartCardEmulation.reportDataResult", params,
base::DictValue().Set("data", base::Base64Encode(result.value())));
} else {
SendResponse("SmartCardEmulation.reportError", params,
base::DictValue().Set("resultCode", result.error()));
}
}
// Helper: Waits for getAttributeRequested and sends a response.
void HandleGetAttribute(
int attrib_id,
const base::expected<std::vector<uint8_t>, std::string>& result,
int handle = 123) {
auto params =
WaitForNotificationParams("SmartCardEmulation.getAttribRequested");
EXPECT_EQ(params.FindInt("handle"), handle);
EXPECT_EQ(params.FindInt("attribId"), attrib_id);
if (result.has_value()) {
SendResponse(
"SmartCardEmulation.reportDataResult", params,
base::DictValue().Set("data", base::Base64Encode(result.value())));
} else {
SendResponse("SmartCardEmulation.reportError", params,
base::DictValue().Set("resultCode", result.error()));
}
}
// Helper: Waits for disconnectRequested and sends a response.
void HandleDisconnect(const std::string& expected_disposition = "leave-card",
int handle = 123,
std::optional<std::string> error_code = std::nullopt) {
auto params =
WaitForNotificationParams("SmartCardEmulation.disconnectRequested");
EXPECT_EQ(params.FindInt("handle"), handle);
EXPECT_EQ(*params.FindString("disposition"), expected_disposition);
if (error_code.has_value()) {
SendResponse("SmartCardEmulation.reportError", params,
base::DictValue().Set("resultCode", *error_code));
} else {
SendResponse("SmartCardEmulation.reportPlainResult", params, {});
}
}
// Helper: Waits for beginTransactionRequested and sends a response.
void HandleBeginTransaction(
int handle = 123,
std::optional<std::string> error_code = std::nullopt) {
auto params = WaitForNotificationParams(
"SmartCardEmulation.beginTransactionRequested");
EXPECT_EQ(params.FindInt("handle"), handle);
if (error_code.has_value()) {
SendResponse("SmartCardEmulation.reportError", params,
base::DictValue().Set("resultCode", *error_code));
} else {
SendResponse("SmartCardEmulation.reportBeginTransactionResult", params,
base::DictValue().Set("handle", handle));
}
}
// Helper: Waits for endTransactionRequested and sends a response.
void HandleEndTransaction(
const std::string& expected_disposition = "leave-card",
int handle = 123,
std::optional<std::string> error_code = std::nullopt) {
auto params =
WaitForNotificationParams("SmartCardEmulation.endTransactionRequested");
EXPECT_EQ(params.FindInt("handle"), handle);
EXPECT_EQ(*params.FindString("disposition"), expected_disposition);
if (error_code.has_value()) {
SendResponse("SmartCardEmulation.reportError", params,
base::DictValue().Set("resultCode", *error_code));
} else {
SendResponse("SmartCardEmulation.reportPlainResult", params, {});
}
}
// Helper: Waits for cancelRequested and sends a response.
void HandleCancel(int context_id = 123,
std::optional<std::string> error_code = std::nullopt) {
auto params =
WaitForNotificationParams("SmartCardEmulation.cancelRequested");
EXPECT_EQ(params.FindInt("contextId"), context_id);
if (error_code.has_value()) {
SendResponse("SmartCardEmulation.reportError", params,
base::DictValue().Set("resultCode", *error_code));
} else {
SendResponse("SmartCardEmulation.reportPlainResult", params, {});
}
}
// Helper: Waits for setAttribRequested (SCardSetAttrib)
void HandleSetAttribute(
int attrib_id,
const std::vector<uint8_t>& expected_data,
int handle = 123,
std::optional<std::string> error_code = std::nullopt) {
auto params =
WaitForNotificationParams("SmartCardEmulation.setAttribRequested");
EXPECT_EQ(params.FindInt("handle"), handle);
EXPECT_EQ(params.FindInt("attribId"), attrib_id);
const std::string* data_b64 = params.FindString("data");
ASSERT_TRUE(data_b64) << "Event is missing the 'data' field";
std::string data_str;
base::Base64Decode(*data_b64, &data_str);
std::vector<uint8_t> actual_data(data_str.begin(), data_str.end());
EXPECT_EQ(actual_data, expected_data);
if (error_code.has_value()) {
SendResponse("SmartCardEmulation.reportError", params,
base::DictValue().Set("resultCode", *error_code));
} else {
SendResponse("SmartCardEmulation.reportPlainResult", params, {});
}
}
void HandleReleaseContext(
int context_id = 123,
std::optional<std::string> error_code = std::nullopt) {
auto params =
WaitForNotificationParams("SmartCardEmulation.releaseContextRequested");
EXPECT_EQ(params.FindInt("contextId"), context_id);
if (error_code.has_value()) {
SendResponse("SmartCardEmulation.reportError", params,
base::DictValue().Set("resultCode", *error_code));
} else {
SendResponse("SmartCardEmulation.reportReleaseContextResult", params, {});
}
}
void ReloadPage() {
content::TestNavigationObserver observer(web_contents_);
web_contents_->GetController().Reload(content::ReloadType::NORMAL, false);
observer.Wait();
}
void NavigateToSubPage() {
GURL current_url = web_contents_->GetLastCommittedURL();
GURL new_url = current_url.Resolve("/target.html");
ASSERT_TRUE(content::NavigateToURL(web_contents_, new_url));
}
private:
void InstallAndLaunchIWA() {
auto app =
IsolatedWebAppBuilder(
ManifestBuilder().AddPermissionsPolicyWildcard(
network::mojom::PermissionsPolicyFeature::kSmartCard))
// Add a valid destination for navigation.
.AddHtml("/target.html", "<html><body>Target Page</body></html>")
.BuildBundle();
app->TrustSigningKey();
auto install_result = app->Install(profile());
ASSERT_TRUE(install_result.has_value());
auto url_info = install_result.value();
raw_ptr<content::RenderFrameHost> app_frame = OpenApp(url_info.app_id());
ASSERT_TRUE(app_frame);
web_contents_ = content::WebContents::FromRenderFrameHost(app_frame);
ASSERT_TRUE(web_contents_);
}
void AttachDevTools() {
devtools_client_ = std::make_unique<content::TestDevToolsProtocolClient>();
devtools_client_->AttachToWebContents(web_contents_);
}
void SetAutoAcceptPermissions() {
permissions::PermissionRequestManager::FromWebContents(
content::WebContents::FromRenderFrameHost(app_frame()))
->set_auto_response_for_test(
permissions::PermissionRequestManager::ACCEPT_ALL);
}
base::test::ScopedFeatureList feature_list_;
std::unique_ptr<content::TestDevToolsProtocolClient> devtools_client_;
raw_ptr<content::WebContents> web_contents_ = nullptr;
};
IN_PROC_BROWSER_TEST_F(SmartCardEmulationBrowserTest, EnableDisableEmulation) {
// Disable (Should be successful even if already disabled - Idempotency).
EXPECT_THAT(SendCommand("SmartCardEmulation.disable"), IsSuccess());
// Enable (Should activate the override).
EXPECT_THAT(SendCommand("SmartCardEmulation.enable"), IsSuccess());
// Re-Enable (Should be successful - Idempotency).
EXPECT_THAT(SendCommand("SmartCardEmulation.enable"), IsSuccess());
// Disable (Should clean up).
EXPECT_THAT(SendCommand("SmartCardEmulation.disable"), IsSuccess());
}
IN_PROC_BROWSER_TEST_F(SmartCardEmulationBrowserTest,
ListReadersAndReportSuccess) {
ASSERT_THAT(SendCommand("SmartCardEmulation.enable"), IsSuccess());
content::DOMMessageQueue message_queue(app_frame());
const std::string kScript = R"(
(async () => {
try {
const context = await navigator.smartCard.establishContext();
const readers = await context.listReaders();
if (readers.length === 2 &&
readers.includes("Reader A") &&
readers.includes("Reader B")) {
window.domAutomationController.send("Success");
} else {
window.domAutomationController.send(
"Failure: Unexpected list: " + JSON.stringify(readers));
}
} catch (e) {
window.domAutomationController.send(
"Error: " + e.name + " - " + e.message);
}
})();
)";
content::ExecuteScriptAsync(app_frame(), kScript);
HandleEstablishContext();
HandleListReaders({"Reader A", "Reader B"});
std::string message;
ASSERT_TRUE(message_queue.WaitForMessage(&message));
EXPECT_EQ("\"Success\"", message);
}
IN_PROC_BROWSER_TEST_F(SmartCardEmulationBrowserTest,
ListReadersAndReportError) {
ASSERT_THAT(SendCommand("SmartCardEmulation.enable"), IsSuccess());
content::DOMMessageQueue message_queue(app_frame());
const std::string kScript = R"(
(async () => {
try {
const context = await navigator.smartCard.establishContext();
// This is expected to fail
await context.listReaders();
window.domAutomationController.send(
"Failure: Should have thrown error");
} catch (e) {
window.domAutomationController.send("Error: " + e.name);
}
})();
)";
content::ExecuteScriptAsync(app_frame(), kScript);
HandleEstablishContext();
HandleListReaders({}, 123, "no-service");
std::string message;
ASSERT_TRUE(message_queue.WaitForMessage(&message));
EXPECT_EQ("\"Error: SmartCardError\"", message);
}
IN_PROC_BROWSER_TEST_F(SmartCardEmulationBrowserTest, ConnectAndReportSuccess) {
ASSERT_THAT(SendCommand("SmartCardEmulation.enable"), IsSuccess());
content::DOMMessageQueue message_queue(app_frame());
const std::string kScript = R"(
(async () => {
try {
const context = await navigator.smartCard.establishContext();
const readers = await context.listReaders();
const readerName = readers[0];
const result = await context.connect(readerName, "shared",
{preferredProtocols: ["t1"]});
window.domAutomationController.send("Success");
} catch (e) {
window.domAutomationController.send(
"Error: " + e.name + " - " + e.message);
}
})();
)";
content::ExecuteScriptAsync(app_frame(), kScript);
HandleEstablishContext();
HandleListReaders();
HandleConnect();
std::string message;
ASSERT_TRUE(message_queue.WaitForMessage(&message));
EXPECT_EQ("\"Success\"", message);
}
IN_PROC_BROWSER_TEST_F(SmartCardEmulationBrowserTest, ConnectAndReportError) {
ASSERT_THAT(SendCommand("SmartCardEmulation.enable"), IsSuccess());
content::DOMMessageQueue message_queue(app_frame());
const std::string kScript = R"(
(async () => {
try {
const context = await navigator.smartCard.establishContext();
const readers = await context.listReaders();
const readerName = readers[0];
// This connect attempt is expected to fail.
await context.connect(readerName, "shared",
{preferredProtocols: ["t1"]});
if (result.activeProtocol === "t1") {
window.domAutomationController.send("Success");
} else {
window.domAutomationController.send("Failure: Wrong protocol");
}
window.domAutomationController.send(
"Failure: Should have thrown error");
} catch (e) {
window.domAutomationController.send("Error: " + e.name);
}
})();
)";
content::ExecuteScriptAsync(app_frame(), kScript);
HandleEstablishContext();
HandleListReaders();
HandleConnect("Reader A", 123, "t1", "reader-unavailable");
std::string message;
ASSERT_TRUE(message_queue.WaitForMessage(&message));
EXPECT_EQ("\"Error: SmartCardError\"", message);
}
IN_PROC_BROWSER_TEST_F(SmartCardEmulationBrowserTest,
EmulationPersistsAcrossReload) {
EXPECT_THAT(SendCommand("SmartCardEmulation.enable"), IsSuccess());
content::DOMMessageQueue message_queue(web_contents());
ReloadPage();
const std::string kScript = R"(
(async () => {
try {
const context = await navigator.smartCard.establishContext();
window.domAutomationController.send("Success");
} catch (e) {
window.domAutomationController.send(
"Error: " + e.name + " - " + e.message);
}
})();
)";
content::ExecuteScriptAsync(app_frame(), kScript);
HandleEstablishContext();
std::string message;
ASSERT_TRUE(message_queue.WaitForMessage(&message));
EXPECT_EQ("\"Success\"", message);
}
IN_PROC_BROWSER_TEST_F(SmartCardEmulationBrowserTest,
EmulationPersistsAcrossNavigation) {
EXPECT_THAT(SendCommand("SmartCardEmulation.enable"), IsSuccess());
content::DOMMessageQueue message_queue(web_contents());
NavigateToSubPage();
// Execute JS on the new page.
const std::string kScript = R"(
(async () => {
try {
const context = await navigator.smartCard.establishContext();
window.domAutomationController.send("Success");
} catch (e) {
window.domAutomationController.send("Error: " + e.name);
}
})();
)";
content::ExecuteScriptAsync(app_frame(), kScript);
HandleEstablishContext();
std::string message;
ASSERT_TRUE(message_queue.WaitForMessage(&message));
EXPECT_EQ("\"Success\"", message);
}
IN_PROC_BROWSER_TEST_F(SmartCardEmulationBrowserTest,
IframeCanAccessEmulatedReaders) {
ASSERT_THAT(SendCommand("SmartCardEmulation.enable"), IsSuccess());
content::DOMMessageQueue message_queue(app_frame());
// Inject Iframe
const std::string kCreateIframeScript = R"(
const iframe = document.createElement('iframe');
iframe.src = 'about:blank';
iframe.allow = 'smart-card';
document.body.appendChild(iframe);
)";
EXPECT_TRUE(content::ExecJs(app_frame(), kCreateIframeScript));
content::RenderFrameHost* iframe_host = content::ChildFrameAt(app_frame(), 0);
ASSERT_TRUE(iframe_host);
const std::string kScript = R"(
(async () => {
try {
const context = await navigator.smartCard.establishContext();
window.domAutomationController.send("Success");
} catch (e) {
window.domAutomationController.send("Error: " + e.name);
}
})();
)";
content::ExecuteScriptAsync(app_frame(), kScript);
HandleEstablishContext();
std::string message;
ASSERT_TRUE(message_queue.WaitForMessage(&message));
EXPECT_EQ("\"Success\"", message);
}
IN_PROC_BROWSER_TEST_F(SmartCardEmulationBrowserTest, GetStatusChangePnP) {
ASSERT_THAT(SendCommand("SmartCardEmulation.enable"), IsSuccess());
content::DOMMessageQueue message_queue(app_frame());
const std::string kScript = R"(
(async () => {
try {
const context = await navigator.smartCard.establishContext();
const oldReaders = await context.listReaders();
let allReaders = await context.getStatusChange(
[{readerName: "\\\\?PnP?\\Notification", currentState: {}}],
{timeout: 10000})
.then((statesOut) => {
return context.listReaders();
});
let newReaders = allReaders.filter(x => !oldReaders.includes(x));
if (newReaders.length === 1 && newReaders[0] === "New Reader") {
window.domAutomationController.send("Success");
} else {
window.domAutomationController.send(
"Failure: " + JSON.stringify(newReaders));
}
} catch (e) {
window.domAutomationController.send(
"Error: " + e.name + " - " + e.message);
}
})();
)";
content::ExecuteScriptAsync(app_frame(), kScript);
HandleEstablishContext();
HandleListReaders({});
HandleGetStatusChange(
{{.name = "\\\\?PnP?\\Notification", .event_count = 1, .changed = true}});
HandleListReaders({"New Reader"});
std::string message;
ASSERT_TRUE(message_queue.WaitForMessage(&message));
EXPECT_EQ("\"Success\"", message);
}
IN_PROC_BROWSER_TEST_F(SmartCardEmulationBrowserTest,
GetStatusChangeReturnsError) {
ASSERT_THAT(SendCommand("SmartCardEmulation.enable"), IsSuccess());
content::DOMMessageQueue message_queue(app_frame());
const std::string kScript = R"(
(async () => {
try {
const context = await navigator.smartCard.establishContext();
const readers = await context.listReaders();
await context.getStatusChange(
[{readerName: "Simulated Reader", currentState: {}}],
{timeout: 10000}
);
window.domAutomationController.send(
"Failure: Promise resolved but should have rejected.");
} catch (e) {
if (e.name === "SmartCardError") {
window.domAutomationController.send("Success");
} else {
window.domAutomationController.send(
"Failure: Wrong error type - " + e.name + ": " + e.message);
}
}
})();
)";
content::ExecuteScriptAsync(app_frame(), kScript);
HandleEstablishContext();
HandleListReaders({"Simulated Reader"});
HandleGetStatusChange({}, 123, "no-service");
std::string message;
ASSERT_TRUE(message_queue.WaitForMessage(&message));
EXPECT_EQ("\"Success\"", message);
}
IN_PROC_BROWSER_TEST_F(SmartCardEmulationBrowserTest,
GetStatusChangeMultiReader) {
ASSERT_THAT(SendCommand("SmartCardEmulation.enable"), IsSuccess());
content::DOMMessageQueue message_queue(app_frame());
const std::string kScript = R"(
(async () => {
try {
const context = await navigator.smartCard.establishContext();
const readers = await context.listReaders();
if (readers.length !== 2 || !readers.includes("Reader A") ||
!readers.includes("Reader B")) {
window.domAutomationController.send(
"Failure: Readers missing. Got: " + readers);
return;
}
const states = await context.getStatusChange([
{readerName: "Reader A", currentState: {empty: true}},
{readerName: "Reader B", currentState: {empty: true}}
], {timeout: 10000});
const stateA = states.find(s => s.readerName === "Reader A");
const stateB = states.find(s => s.readerName === "Reader B");
if (!stateA || !stateB) {
throw new Error("Missing state for one or more readers.");
}
if (!stateA.eventState.changed || !stateA.eventState.present) {
throw new Error(
"Reader A failed: Expected changed/present. Got: " +
JSON.stringify(stateA));
}
if (stateB.eventState.changed || !stateB.eventState.empty) {
throw new Error(
"Reader B failed: Expected !changed/empty. Got: " +
JSON.stringify(stateB));
}
window.domAutomationController.send("Success");
} catch (e) {
window.domAutomationController.send(
"Error: " + e.name + " - " + e.message);
}
})();
)";
content::ExecuteScriptAsync(app_frame(), kScript);
HandleEstablishContext();
HandleListReaders({"Reader A", "Reader B"});
HandleGetStatusChange({{.name = "Reader A",
.event_count = 1,
.atr = {0x3B, 0x11},
.changed = true,
.present = true},
{.name = "Reader B",
.event_count = 0,
.atr = {},
.changed = false,
.empty = true}});
std::string message;
ASSERT_TRUE(message_queue.WaitForMessage(&message));
EXPECT_EQ("\"Success\"", message);
}
IN_PROC_BROWSER_TEST_F(SmartCardEmulationBrowserTest, ConnectionStatus) {
ASSERT_THAT(SendCommand("SmartCardEmulation.enable"), IsSuccess());
content::DOMMessageQueue message_queue(app_frame());
const std::string kScript = R"(
(async () => {
try {
const context = await navigator.smartCard.establishContext();
const readers = await context.listReaders();
const connectResult = await context.connect(readers[0], "shared",
{preferredProtocols: ["t1"]});
const status = await connectResult.connection.status();
if (status.readerName !== "Reader A") {
throw new Error("Wrong readerName: " + status.readerName);
}
if (status.state !== "t1") {
throw new Error("Wrong state: " + status.state);
}
const atrBytes = new Uint8Array(status.answerToReset);
if (atrBytes.length !== 4 || atrBytes[0] !== 0x3B) {
throw new Error("Wrong ATR bytes: " + atrBytes);
}
window.domAutomationController.send("Success");
} catch (e) {
window.domAutomationController.send("Error: " +
e.name + " - " + e.message);
}
})();
)";
content::ExecuteScriptAsync(app_frame(), kScript);
HandleEstablishContext();
HandleListReaders();
HandleConnect();
HandleStatus("Reader A", "specific", "t1", {0x3B, 0x01, 0x02, 0x03});
std::string message;
ASSERT_TRUE(message_queue.WaitForMessage(&message));
EXPECT_EQ("\"Success\"", message);
}
IN_PROC_BROWSER_TEST_F(SmartCardEmulationBrowserTest, ConnectionStatusError) {
ASSERT_THAT(SendCommand("SmartCardEmulation.enable"), IsSuccess());
content::DOMMessageQueue message_queue(app_frame());
const std::string kScript = R"(
(async () => {
try {
const context = await navigator.smartCard.establishContext();
const readers = await context.listReaders();
const connectResult = await context.connect(readers[0], "shared",
{preferredProtocols: ["t1"]});
await connectResult.connection.status();
window.domAutomationController.send(
"Failure: Promise resolved but should have rejected.");
} catch (e) {
if (e.name === "SmartCardError") {
window.domAutomationController.send("Success");
} else {
window.domAutomationController.send(
"Failure: Wrong error type. Got: " +
e.name + ", " + e.message);
}
}
})();
)";
content::ExecuteScriptAsync(app_frame(), kScript);
HandleEstablishContext();
HandleListReaders();
HandleConnect();
HandleStatus("", "", "", {}, 123, "reader-unavailable");
std::string message;
ASSERT_TRUE(message_queue.WaitForMessage(&message));
EXPECT_EQ("\"Success\"", message);
}
IN_PROC_BROWSER_TEST_F(SmartCardEmulationBrowserTest, DataOperationsSuccess) {
ASSERT_THAT(SendCommand("SmartCardEmulation.enable"), IsSuccess());
content::DOMMessageQueue message_queue(app_frame());
const std::string kScript = R"(
(async () => {
try {
const context = await navigator.smartCard.establishContext();
const readers = await context.listReaders();
const connectResult = await context.connect(readers[0],
"shared", {preferredProtocols: ["t1"]});
const connection = connectResult.connection;
// --- Transmit ---
const txResponse = await connection.transmit(
new Uint8Array([0x00, 0xA4, 0x04, 0x00]));
const txBytes = new Uint8Array(txResponse);
if (txBytes.length !== 2 || txBytes[0] !== 0x90 ||
txBytes[1] !== 0x00) {
throw new Error("Transmit failed. Got: " + txBytes.join(','));
}
// --- Control ---
const ctrlResponse = await connection.control(42,
new Uint8Array([1, 2, 3]));
const ctrlBytes = new Uint8Array(ctrlResponse);
if (ctrlBytes.length !== 2 || ctrlBytes[0] !== 10 ||
ctrlBytes[1] !== 20) {
throw new Error("Control failed. Got: " + ctrlBytes.join(','));
}
// --- GetAttribute ---
// Tag 0x00090101 (SCARD_ATTR_VENDOR_NAME)
const attrResponse = await connection.getAttribute(0x00090101);
const vendorName = new TextDecoder().decode(attrResponse);
if (vendorName !== "Chrome Reader") {
throw new Error("GetAttribute failed. Got: " + vendorName);
}
window.domAutomationController.send("Success");
} catch (e) {
window.domAutomationController.send("Error: " + e.message);
}
})();
)";
content::ExecuteScriptAsync(app_frame(), kScript);
HandleEstablishContext();
HandleListReaders();
HandleConnect("Reader A", 123, "t1");
HandleTransmit({0x00, 0xA4, 0x04, 0x00}, std::vector<uint8_t>{0x90, 0x00});
HandleControl(42, {1, 2, 3}, std::vector<uint8_t>{10, 20});
std::string kName = "Chrome Reader";
HandleGetAttribute(0x00090101,
std::vector<uint8_t>(kName.begin(), kName.end()));
std::string message;
ASSERT_TRUE(message_queue.WaitForMessage(&message));
EXPECT_EQ("\"Success\"", message);
}
IN_PROC_BROWSER_TEST_F(SmartCardEmulationBrowserTest, DataOperationsErrors) {
ASSERT_THAT(SendCommand("SmartCardEmulation.enable"), IsSuccess());
content::DOMMessageQueue message_queue(app_frame());
const std::string kScript = R"(
(async () => {
try {
const context = await navigator.smartCard.establishContext();
const readers = await context.listReaders();
const connectResult = await context.connect(
readers[0], "shared", {preferredProtocols: ["t1"]});
const connection = connectResult.connection;
// --- Transmit Error ---
try {
await connection.transmit(new Uint8Array([0x00, 0x00]));
throw new Error("Transmit should have failed but succeeded.");
} catch (e) {
if (e.name !== "SmartCardError") {
throw new Error("Transmit wrong error: " + e.name);
}
}
// --- Control Error ---
try {
await connection.control(42, new Uint8Array([1]));
throw new Error("Control should have failed but succeeded.");
} catch (e) {
if (e.name !== "SmartCardError") {
throw new Error("Control wrong error: " + e.name);
}
}
// --- GetAttribute Error ---
try {
await connection.getAttribute(1234);
throw new Error("GetAttribute should have failed but succeeded.");
} catch (e) {
if (e.name !== "SmartCardError") {
throw new Error("GetAttrib wrong error: " + e.name);
}
}
window.domAutomationController.send("Success");
} catch (e) {
window.domAutomationController.send("Failure: " + e.message);
}
})();
)";
content::ExecuteScriptAsync(app_frame(), kScript);
HandleEstablishContext();
HandleListReaders();
HandleConnect("Reader A", 123, "t1");
HandleTransmit({0x00, 0x00}, base::unexpected("sharing-violation"));
HandleControl(42, {1}, base::unexpected("unsupported-feature"));
HandleGetAttribute(1234, base::unexpected("reader-unavailable"));
std::string message;
ASSERT_TRUE(message_queue.WaitForMessage(&message));
EXPECT_EQ("\"Success\"", message);
}
IN_PROC_BROWSER_TEST_F(SmartCardEmulationBrowserTest, TransactionSuccess) {
ASSERT_THAT(SendCommand("SmartCardEmulation.enable"), IsSuccess());
content::DOMMessageQueue message_queue(app_frame());
const std::string kScript = R"(
(async () => {
try {
const context = await navigator.smartCard.establishContext();
const readers = await context.listReaders();
const connectResult = await context.connect(readers[0], "shared",
{preferredProtocols: ["t1"]});
const connection = connectResult.connection;
await connection.startTransaction(async function() {
return "leave";
});
await connection.disconnect();
window.domAutomationController.send("Success");
} catch (e) {
window.domAutomationController.send("Error: " + e.message);
}
})();
)";
content::ExecuteScriptAsync(app_frame(), kScript);
HandleEstablishContext();
HandleListReaders();
HandleConnect("Reader A", 123, "t1");
HandleBeginTransaction();
HandleEndTransaction();
HandleDisconnect();
std::string message;
ASSERT_TRUE(message_queue.WaitForMessage(&message));
EXPECT_EQ("\"Success\"", message);
}
IN_PROC_BROWSER_TEST_F(SmartCardEmulationBrowserTest, BeginTransactionFailure) {
ASSERT_THAT(SendCommand("SmartCardEmulation.enable"), IsSuccess());
content::DOMMessageQueue message_queue(app_frame());
const std::string kScript = R"(
(async () => {
try {
const context = await navigator.smartCard.establishContext();
const readers = await context.listReaders();
const connectResult = await context.connect(readers[0], "shared",
{preferredProtocols: ["t1"]});
const connection = connectResult.connection;
try {
// Attempt to start a transaction - this should fail immediately.
await connection.startTransaction(async function() {
throw new Error(
"Transaction callback should not run on failure!");
});
throw new Error("Transaction succeeded but was expected to fail.");
} catch (txError) {
if (txError.name === "SmartCardError") {
window.domAutomationController.send("Success");
} else {
throw new Error("Caught unexpected error type: " +
txError.name + " - " + txError.message);
}
}
} catch (e) {
window.domAutomationController.send("Error: " + e.message);
}
})();
)";
content::ExecuteScriptAsync(app_frame(), kScript);
HandleEstablishContext();
HandleListReaders();
HandleConnect("Reader A", 123, "t1");
HandleBeginTransaction(123, "sharing-violation");
std::string message;
ASSERT_TRUE(message_queue.WaitForMessage(&message));
EXPECT_EQ("\"Success\"", message);
}
IN_PROC_BROWSER_TEST_F(SmartCardEmulationBrowserTest, EndTransactionFailure) {
ASSERT_THAT(SendCommand("SmartCardEmulation.enable"), IsSuccess());
content::DOMMessageQueue message_queue(app_frame());
const std::string kScript = R"(
(async () => {
try {
const context = await navigator.smartCard.establishContext();
const readers = await context.listReaders();
const connectResult = await context.connect(readers[0], "shared",
{preferredProtocols: ["t1"]});
const connection = connectResult.connection;
try {
await connection.startTransaction(async function() {
return "leave";
});
throw new Error("Transaction succeeded but was expected to fail.");
} catch (e) {
if (e.message.includes("communications error")) {
window.domAutomationController.send("Success");
} else {
throw new Error("Unexpected error: " +
e.name + " - " + e.message);
}
}
} catch (e) {
window.domAutomationController.send("Error: " + e.message);
}
})();
)";
content::ExecuteScriptAsync(app_frame(), kScript);
HandleEstablishContext();
HandleListReaders();
HandleConnect("Reader A", 123, "t1");
HandleBeginTransaction();
HandleEndTransaction("leave-card", 123, "comm-error");
std::string message;
ASSERT_TRUE(message_queue.WaitForMessage(&message));
EXPECT_EQ("\"Success\"", message);
}
IN_PROC_BROWSER_TEST_F(SmartCardEmulationBrowserTest, SetAttributeSuccess) {
ASSERT_THAT(SendCommand("SmartCardEmulation.enable"), IsSuccess());
content::DOMMessageQueue message_queue(app_frame());
const std::string kScript = R"(
(async () => {
try {
const context = await navigator.smartCard.establishContext();
const readers = await context.listReaders();
const connectResult = await context.connect(readers[0],
"shared", {preferredProtocols: ["t1"]});
const connection = connectResult.connection;
await connection.setAttribute(1234, new Uint8Array([0x96, 0x00]));
window.domAutomationController.send("Success");
} catch (e) {
window.domAutomationController.send("Error: " + e.message);
}
})();
)";
content::ExecuteScriptAsync(app_frame(), kScript);
HandleEstablishContext();
HandleListReaders();
HandleConnect("Reader A", 123, "t1");
HandleSetAttribute(1234, {0x96, 0x00});
std::string message;
ASSERT_TRUE(message_queue.WaitForMessage(&message));
EXPECT_EQ("\"Success\"", message);
}
IN_PROC_BROWSER_TEST_F(SmartCardEmulationBrowserTest, SetAttributeFailure) {
ASSERT_THAT(SendCommand("SmartCardEmulation.enable"), IsSuccess());
content::DOMMessageQueue message_queue(app_frame());
const std::string kScript = R"(
(async () => {
try {
const context = await navigator.smartCard.establishContext();
const readers = await context.listReaders();
const connectResult = await context.connect(readers[0],
"shared", {preferredProtocols: ["t1"]});
const connection = connectResult.connection;
try {
await connection.setAttribute(1234, new Uint8Array([0x96, 0x00]));
throw new Error("setAttribute succeeded but was expected to fail.");
} catch (e) {
if (e.name === "SmartCardError" ) {
window.domAutomationController.send("Success");
} else {
throw new Error(
"Caught unexpected error: " + e.name + " - " + e.message);
}
}
} catch (e) {
window.domAutomationController.send("Error: " + e.message);
}
})();
)";
content::ExecuteScriptAsync(app_frame(), kScript);
HandleEstablishContext();
HandleListReaders();
HandleConnect("Reader A", 123, "t1");
HandleSetAttribute(1234, {0x96, 0x00}, 123, "sharing-violation");
std::string message;
ASSERT_TRUE(message_queue.WaitForMessage(&message));
EXPECT_EQ("\"Success\"", message);
}
IN_PROC_BROWSER_TEST_F(SmartCardEmulationBrowserTest, CancelGetStatusChange) {
ASSERT_THAT(SendCommand("SmartCardEmulation.enable"), IsSuccess());
content::DOMMessageQueue message_queue(app_frame());
const std::string kScript = R"(
(async () => {
try {
const context = await navigator.smartCard.establishContext();
const controller = new AbortController();
const signal = controller.signal;
const statusPromise = context.getStatusChange(
[{
readerName: "Simulated Reader",
currentState: { empty: true }
}],
{signal: signal}
);
controller.abort();
try {
await statusPromise;
throw new Error(
"GetStatusChange should have been aborted but succeeded.");
} catch (e) {
if (e.name === 'AbortError') {
window.domAutomationController.send("Success");
} else {
throw e;
}
}
} catch (e) {
window.domAutomationController.send("Error: " + e.message);
}
})();
)";
content::ExecuteScriptAsync(app_frame(), kScript);
HandleEstablishContext();
// Intercept GetStatusChange but do not answer yet.
auto status_params =
WaitForNotificationParams("SmartCardEmulation.getStatusChangeRequested");
HandleCancel();
base::DictValue response;
response.Set("resultCode", "cancelled");
SendResponse("SmartCardEmulation.reportError", status_params,
std::move(response));
std::string message;
ASSERT_TRUE(message_queue.WaitForMessage(&message));
EXPECT_EQ("\"Success\"", message);
}
IN_PROC_BROWSER_TEST_F(SmartCardEmulationBrowserTest,
CancelFailureOperationContinues) {
ASSERT_THAT(SendCommand("SmartCardEmulation.enable"), IsSuccess());
content::DOMMessageQueue message_queue(app_frame());
const std::string kScript = R"(
(async () => {
try {
const context = await navigator.smartCard.establishContext();
const controller = new AbortController();
const statusPromise = context.getStatusChange(
[{readerName: "Simulated Reader", currentState: {empty: true}}],
{signal: controller.signal}
);
controller.abort();
const result = await statusPromise;
console.log(result);
if (result.length === 1 && result[0].readerName === "Reader A") {
window.domAutomationController.send("Success");
} else {
throw new Error("Operation finished but returned wrong data.");
}
} catch (e) {
window.domAutomationController.send(
"Error: " + e.name + " - " + e.message);
}
})();
)";
content::ExecuteScriptAsync(app_frame(), kScript);
HandleEstablishContext();
auto status_params =
WaitForNotificationParams("SmartCardEmulation.getStatusChangeRequested");
HandleCancel(123, "comm-error");
base::DictValue response;
base::DictValue state;
base::ListValue response_list;
state.Set("reader", "Reader A");
state.Set("eventCount", 1);
state.Set("atr", base::Base64Encode({0x3B, 0x11}));
base::DictValue flags;
flags.Set("changed", true);
state.Set("eventState", std::move(flags));
response_list.Append(std::move(state));
response.Set("readerStates", std::move(response_list));
SendResponse("SmartCardEmulation.reportGetStatusChangeResult", status_params,
std::move(response));
std::string message;
ASSERT_TRUE(message_queue.WaitForMessage(&message));
EXPECT_EQ("\"Success\"", message);
}
IN_PROC_BROWSER_TEST_F(SmartCardEmulationBrowserTest, ReleaseContextOnGC) {
ASSERT_THAT(SendCommand("SmartCardEmulation.enable"), IsSuccess());
ASSERT_THAT(SendCommand("HeapProfiler.enable"), IsSuccess());
content::DOMMessageQueue message_queue(app_frame());
const std::string kScript = R"(
(async () => {
try {
const context = await navigator.smartCard.establishContext();
window.domAutomationController.send("Success");
} catch (e) {
window.domAutomationController.send("Error: " + e.message);
}
})();
)";
content::ExecuteScriptAsync(app_frame(), kScript);
HandleEstablishContext();
std::string message;
ASSERT_TRUE(message_queue.WaitForMessage(&message));
EXPECT_EQ("\"Success\"", message);
ASSERT_THAT(SendCommand("HeapProfiler.collectGarbage"), IsSuccess());
HandleReleaseContext();
}
IN_PROC_BROWSER_TEST_F(SmartCardEmulationBrowserTest,
ReleaseContextOnNavigation) {
ASSERT_THAT(SendCommand("SmartCardEmulation.enable"), IsSuccess());
content::DOMMessageQueue message_queue(app_frame());
const std::string kSetupScript = R"(
(async () => {
window.smartCardContext = await navigator.smartCard.establishContext();
window.domAutomationController.send("Success");
})();
)";
content::ExecuteScriptAsync(app_frame(), kSetupScript);
HandleEstablishContext(123);
std::string message;
ASSERT_TRUE(message_queue.WaitForMessage(&message));
EXPECT_EQ("\"Success\"", message);
content::ExecuteScriptAsync(app_frame(), "window.location.reload();");
HandleReleaseContext();
}
IN_PROC_BROWSER_TEST_F(SmartCardEmulationBrowserTest, ReleaseContextFailure) {
ASSERT_THAT(SendCommand("SmartCardEmulation.enable"), IsSuccess());
content::DOMMessageQueue message_queue(app_frame());
const std::string kSetupScript = R"(
(async () => {
window.smartCardContext = await navigator.smartCard.establishContext();
window.domAutomationController.send("Success");
})();
)";
content::ExecuteScriptAsync(app_frame(), kSetupScript);
HandleEstablishContext(123);
std::string message;
ASSERT_TRUE(message_queue.WaitForMessage(&message));
EXPECT_EQ("\"Success\"", message);
content::ExecuteScriptAsync(app_frame(), "window.location.reload();");
HandleReleaseContext(123, "internal-error");
}
} // namespace web_app