blob: ddc6648a340186ad4c038543711dc619691cdaac [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/webauthn/authenticator_request_dialog_model.h"
#include <algorithm>
#include <utility>
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/containers/contains.h"
#include "base/containers/flat_set.h"
#include "base/macros.h"
#include "base/optional.h"
#include "base/strings/string_util.h"
#include "base/test/bind.h"
#include "base/test/task_environment.h"
#include "build/build_config.h"
#include "chrome/browser/webauthn/authenticator_reference.h"
#include "chrome/browser/webauthn/authenticator_transport.h"
#include "device/fido/authenticator_data.h"
#include "device/fido/authenticator_get_assertion_response.h"
#include "device/fido/fido_constants.h"
#include "device/fido/fido_transport_protocol.h"
#include "device/fido/public_key_credential_user_entity.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace {
using testing::ElementsAre;
using RequestType = device::FidoRequestType;
const base::flat_set<AuthenticatorTransport> kAllTransports = {
AuthenticatorTransport::kUsbHumanInterfaceDevice,
AuthenticatorTransport::kNearFieldCommunication,
AuthenticatorTransport::kInternal,
AuthenticatorTransport::kCloudAssistedBluetoothLowEnergy,
};
const base::flat_set<AuthenticatorTransport> kAllTransportsWithoutCable = {
AuthenticatorTransport::kUsbHumanInterfaceDevice,
AuthenticatorTransport::kNearFieldCommunication,
AuthenticatorTransport::kInternal,
};
using TransportAvailabilityInfo =
::device::FidoRequestHandlerBase::TransportAvailabilityInfo;
class MockDialogModelObserver
: public AuthenticatorRequestDialogModel::Observer {
public:
MockDialogModelObserver() = default;
MOCK_METHOD1(OnModelDestroyed, void(AuthenticatorRequestDialogModel*));
MOCK_METHOD0(OnStepTransition, void());
MOCK_METHOD0(OnCancelRequest, void());
MOCK_METHOD0(OnBluetoothPoweredStateChanged, void());
private:
DISALLOW_COPY_AND_ASSIGN(MockDialogModelObserver);
};
class BluetoothAdapterPowerOnCallbackReceiver {
public:
BluetoothAdapterPowerOnCallbackReceiver() = default;
base::RepeatingClosure GetCallback() {
return base::BindRepeating(
&BluetoothAdapterPowerOnCallbackReceiver::CallbackTarget,
base::Unretained(this));
}
bool was_called() const { return was_called_; }
private:
void CallbackTarget() {
ASSERT_FALSE(was_called_);
was_called_ = true;
}
bool was_called_ = false;
DISALLOW_COPY_AND_ASSIGN(BluetoothAdapterPowerOnCallbackReceiver);
};
base::StringPiece RequestTypeToString(RequestType req_type) {
switch (req_type) {
case RequestType::kGetAssertion:
return "GetAssertion";
case RequestType::kMakeCredential:
return "MakeCredential";
}
}
enum class TransportAvailabilityParam {
kHasPlatformCredential,
kHasWinNativeAuthenticator,
kHasCableV1Extension,
kHasCableV2Extension,
};
base::StringPiece TransportAvailabilityParamToString(
TransportAvailabilityParam param) {
switch (param) {
case TransportAvailabilityParam::kHasPlatformCredential:
return "kHasPlatformCredential";
case TransportAvailabilityParam::kHasWinNativeAuthenticator:
return "kHasWinNativeAuthenticator";
case TransportAvailabilityParam::kHasCableV1Extension:
return "kHasCableV1Extension";
case TransportAvailabilityParam::kHasCableV2Extension:
return "kHasCableV2Extension";
}
}
template <typename T, base::StringPiece (*F)(T)>
std::string SetToString(base::flat_set<T> s) {
std::vector<base::StringPiece> names;
std::transform(s.begin(), s.end(), std::back_inserter(names), F);
return base::JoinString(names, ", ");
}
} // namespace
class AuthenticatorRequestDialogModelTest : public ::testing::Test {
public:
using Step = AuthenticatorRequestDialogModel::Step;
AuthenticatorRequestDialogModelTest() = default;
protected:
base::test::TaskEnvironment task_environment_{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
private:
DISALLOW_COPY_AND_ASSIGN(AuthenticatorRequestDialogModelTest);
};
TEST_F(AuthenticatorRequestDialogModelTest, Mechanisms) {
const auto mc = RequestType::kMakeCredential;
const auto ga = RequestType::kGetAssertion;
const auto usb = AuthenticatorTransport::kUsbHumanInterfaceDevice;
const auto internal = AuthenticatorTransport::kInternal;
const auto cable = AuthenticatorTransport::kCloudAssistedBluetoothLowEnergy;
const auto aoa = AuthenticatorTransport::kAndroidAccessory;
const auto v1 = TransportAvailabilityParam::kHasCableV1Extension;
const auto v2 = TransportAvailabilityParam::kHasCableV2Extension;
const auto has_winapi =
TransportAvailabilityParam::kHasWinNativeAuthenticator;
const auto has_plat = TransportAvailabilityParam::kHasPlatformCredential;
using t = AuthenticatorRequestDialogModel::Mechanism::Transport;
using p = AuthenticatorRequestDialogModel::Mechanism::Phone;
const auto winapi =
AuthenticatorRequestDialogModel::Mechanism::WindowsAPI(true);
const auto usb_ui = Step::kUsbInsertAndActivate;
const auto tss = Step::kTransportSelection;
const auto plat_ui = Step::kNotStarted;
const auto cable_ui = Step::kCableActivate;
const struct {
RequestType request_type;
base::flat_set<AuthenticatorTransport> transports;
base::flat_set<TransportAvailabilityParam> params;
std::vector<std::string> phone_names;
std::vector<AuthenticatorRequestDialogModel::Mechanism::Type>
expected_mechanisms;
Step expected_first_step;
} kTests[] = {
// If there's only a single mechanism, it should activate.
{mc, {usb}, {}, {}, {t(usb)}, usb_ui},
{ga, {usb}, {}, {}, {t(usb)}, usb_ui},
// ... otherwise should the selection sheet.
{mc, {usb, internal}, {}, {}, {t(usb), t(internal)}, tss},
{ga, {usb, internal}, {}, {}, {t(usb), t(internal)}, tss},
// If the platform authenticator has a credential it should activate.
{ga, {usb, internal}, {has_plat}, {}, {t(usb), t(internal)}, plat_ui},
// If the Windows API is available without caBLE, it should activate.
{mc, {}, {has_winapi}, {}, {winapi}, plat_ui},
{ga, {}, {has_winapi}, {}, {winapi}, plat_ui},
// ... even if, somehow, there's another transport.
{mc, {usb}, {has_winapi}, {}, {t(usb), winapi}, plat_ui},
{ga, {usb}, {has_winapi}, {}, {t(usb), winapi}, plat_ui},
// A caBLEv1 extension should cause us to go directly to caBLE.
{ga, {usb, cable}, {v1}, {}, {t(usb), t(cable)}, cable_ui},
// A caBLEv2 extension should cause us to go directly to caBLE, but also
// show the AOA option.
{ga, {usb, aoa, cable}, {v2}, {}, {t(usb), t(aoa), t(cable)}, cable_ui},
// If there are linked phones then AOA doesn't show up, but the phones do,
// and sorted. The selection sheet should show.
{mc, {usb, aoa, cable}, {}, {"b", "a"}, {t(usb), p("a"), p("b")}, tss},
{ga, {usb, aoa, cable}, {}, {"b", "a"}, {t(usb), p("a"), p("b")}, tss},
// On Windows, if there are linked phones we'll show a selection sheet for
// makeCredential.
{mc, {cable}, {has_winapi}, {"a"}, {winapi, p("a")}, tss},
// ... but not for getAssertion (currently).
{ga, {cable}, {has_winapi}, {"a"}, {winapi, p("a")}, plat_ui},
};
unsigned test_num = 0;
for (const auto& test : kTests) {
SCOPED_TRACE(static_cast<int>(test.expected_first_step));
SCOPED_TRACE(
(SetToString<TransportAvailabilityParam,
TransportAvailabilityParamToString>(test.params)));
SCOPED_TRACE((SetToString<device::FidoTransportProtocol, device::ToString>(
test.transports)));
SCOPED_TRACE(RequestTypeToString(test.request_type));
SCOPED_TRACE(test_num++);
TransportAvailabilityInfo transports_info;
transports_info.is_ble_powered = true;
transports_info.request_type = test.request_type;
transports_info.available_transports = test.transports;
transports_info.has_recognized_platform_authenticator_credential =
base::Contains(test.params,
TransportAvailabilityParam::kHasPlatformCredential);
if (base::Contains(
test.params,
TransportAvailabilityParam::kHasWinNativeAuthenticator)) {
transports_info.has_win_native_api_authenticator = true;
transports_info.win_native_api_authenticator_id = "some_authenticator_id";
}
AuthenticatorRequestDialogModel model(/*relying_party_id=*/"example.com");
base::Optional<bool> has_v2_cable_extension;
if (base::Contains(test.params,
TransportAvailabilityParam::kHasCableV1Extension)) {
has_v2_cable_extension = false;
}
if (base::Contains(test.params,
TransportAvailabilityParam::kHasCableV2Extension)) {
CHECK(!has_v2_cable_extension.has_value());
has_v2_cable_extension = true;
}
if (has_v2_cable_extension.has_value() || !test.phone_names.empty()) {
std::vector<AuthenticatorRequestDialogModel::PairedPhone> phones;
for (const auto& name : test.phone_names) {
std::array<uint8_t, device::kP256X962Length> public_key = {0};
public_key[0] = base::checked_cast<uint8_t>(phones.size());
phones.emplace_back(name, /*contact_id=*/0, public_key);
}
model.set_cable_transport_info(has_v2_cable_extension, std::move(phones),
base::DoNothing(), base::nullopt);
}
model.StartFlow(std::move(transports_info),
/*use_location_bar_bubble=*/false);
EXPECT_EQ(test.expected_first_step, model.current_step());
std::vector<AuthenticatorRequestDialogModel::Mechanism::Type>
mechanism_types;
for (const auto& mech : model.mechanisms()) {
mechanism_types.push_back(mech.type);
}
EXPECT_EQ(test.expected_mechanisms, mechanism_types);
if (!model.offer_try_again_in_ui()) {
continue;
}
model.StartOver();
EXPECT_EQ(Step::kTransportSelection, model.current_step());
}
}
TEST_F(AuthenticatorRequestDialogModelTest, NoAvailableTransports) {
testing::StrictMock<MockDialogModelObserver> mock_observer;
AuthenticatorRequestDialogModel model(/*relying_party_id=*/"example.com");
model.AddObserver(&mock_observer);
EXPECT_CALL(mock_observer, OnStepTransition());
model.StartFlow(TransportAvailabilityInfo(),
/*use_location_bar_bubble=*/false);
EXPECT_EQ(Step::kErrorNoAvailableTransports, model.current_step());
testing::Mock::VerifyAndClearExpectations(&mock_observer);
EXPECT_CALL(mock_observer, OnCancelRequest());
model.Cancel();
testing::Mock::VerifyAndClearExpectations(&mock_observer);
EXPECT_CALL(mock_observer, OnStepTransition());
model.OnRequestComplete();
EXPECT_EQ(Step::kClosed, model.current_step());
testing::Mock::VerifyAndClearExpectations(&mock_observer);
EXPECT_CALL(mock_observer, OnModelDestroyed(&model));
}
TEST_F(AuthenticatorRequestDialogModelTest, AwaitingAcknowledgement) {
const struct {
void (AuthenticatorRequestDialogModel::*event)();
Step expected_sheet;
} kTestCases[] = {
{&AuthenticatorRequestDialogModel::OnRequestTimeout, Step::kTimedOut},
{&AuthenticatorRequestDialogModel::OnActivatedKeyNotRegistered,
Step::kKeyNotRegistered},
{&AuthenticatorRequestDialogModel::OnActivatedKeyAlreadyRegistered,
Step::kKeyAlreadyRegistered},
};
for (const auto& test_case : kTestCases) {
testing::StrictMock<MockDialogModelObserver> mock_observer;
AuthenticatorRequestDialogModel model(/*relying_party_id=*/"example.com");
model.AddObserver(&mock_observer);
TransportAvailabilityInfo transports_info;
transports_info.available_transports = kAllTransportsWithoutCable;
EXPECT_CALL(mock_observer, OnStepTransition());
model.StartFlow(std::move(transports_info),
/*use_location_bar_bubble=*/false);
EXPECT_EQ(Step::kTransportSelection, model.current_step());
testing::Mock::VerifyAndClearExpectations(&mock_observer);
EXPECT_CALL(mock_observer, OnStepTransition());
(model.*test_case.event)();
EXPECT_EQ(test_case.expected_sheet, model.current_step());
testing::Mock::VerifyAndClearExpectations(&mock_observer);
EXPECT_CALL(mock_observer, OnStepTransition());
EXPECT_CALL(mock_observer, OnCancelRequest());
model.Cancel();
EXPECT_EQ(Step::kClosed, model.current_step());
testing::Mock::VerifyAndClearExpectations(&mock_observer);
EXPECT_CALL(mock_observer, OnModelDestroyed(&model));
}
}
TEST_F(AuthenticatorRequestDialogModelTest, BleAdapterAlreadyPowered) {
const struct {
AuthenticatorTransport transport;
Step expected_final_step;
} kTestCases[] = {
{AuthenticatorTransport::kCloudAssistedBluetoothLowEnergy,
Step::kCableActivate},
};
for (const auto test_case : kTestCases) {
TransportAvailabilityInfo transports_info;
transports_info.request_type = RequestType::kGetAssertion;
transports_info.available_transports = {test_case.transport};
transports_info.can_power_on_ble_adapter = true;
transports_info.is_ble_powered = true;
BluetoothAdapterPowerOnCallbackReceiver power_receiver;
AuthenticatorRequestDialogModel model(/*relying_party_id=*/"example.com");
model.SetBluetoothAdapterPowerOnCallback(power_receiver.GetCallback());
model.set_cable_transport_info(true, {}, base::DoNothing(), base::nullopt);
model.StartFlow(std::move(transports_info),
/*use_location_bar_bubble=*/false);
EXPECT_EQ(test_case.expected_final_step, model.current_step());
EXPECT_TRUE(model.ble_adapter_is_powered());
EXPECT_FALSE(power_receiver.was_called());
}
}
TEST_F(AuthenticatorRequestDialogModelTest, BleAdapterNeedToBeManuallyPowered) {
const struct {
AuthenticatorTransport transport;
Step expected_final_step;
} kTestCases[] = {
{AuthenticatorTransport::kCloudAssistedBluetoothLowEnergy,
Step::kCableActivate},
};
for (const auto test_case : kTestCases) {
TransportAvailabilityInfo transports_info;
transports_info.request_type = RequestType::kGetAssertion;
transports_info.available_transports = {test_case.transport};
transports_info.can_power_on_ble_adapter = false;
transports_info.is_ble_powered = false;
testing::NiceMock<MockDialogModelObserver> mock_observer;
BluetoothAdapterPowerOnCallbackReceiver power_receiver;
AuthenticatorRequestDialogModel model(/*relying_party_id=*/"example.com");
model.AddObserver(&mock_observer);
model.SetBluetoothAdapterPowerOnCallback(power_receiver.GetCallback());
model.set_cable_transport_info(true, {}, base::DoNothing(), base::nullopt);
model.StartFlow(std::move(transports_info),
/*use_location_bar_bubble=*/false);
EXPECT_EQ(Step::kBlePowerOnManual, model.current_step());
EXPECT_FALSE(model.ble_adapter_is_powered());
EXPECT_CALL(mock_observer, OnBluetoothPoweredStateChanged());
model.OnBluetoothPoweredStateChanged(true /* powered */);
EXPECT_EQ(Step::kBlePowerOnManual, model.current_step());
EXPECT_TRUE(model.ble_adapter_is_powered());
testing::Mock::VerifyAndClearExpectations(&mock_observer);
model.ContinueWithFlowAfterBleAdapterPowered();
EXPECT_EQ(test_case.expected_final_step, model.current_step());
EXPECT_FALSE(power_receiver.was_called());
}
}
TEST_F(AuthenticatorRequestDialogModelTest,
BleAdapterCanBeAutomaticallyPowered) {
const struct {
AuthenticatorTransport transport;
Step expected_final_step;
} kTestCases[] = {
{AuthenticatorTransport::kCloudAssistedBluetoothLowEnergy,
Step::kCableActivate},
};
for (const auto test_case : kTestCases) {
TransportAvailabilityInfo transports_info;
transports_info.request_type = RequestType::kGetAssertion;
transports_info.available_transports = {test_case.transport};
transports_info.can_power_on_ble_adapter = true;
transports_info.is_ble_powered = false;
BluetoothAdapterPowerOnCallbackReceiver power_receiver;
AuthenticatorRequestDialogModel model(/*relying_party_id=*/"example.com");
model.SetBluetoothAdapterPowerOnCallback(power_receiver.GetCallback());
model.set_cable_transport_info(true, {}, base::DoNothing(), base::nullopt);
model.StartFlow(std::move(transports_info),
/*use_location_bar_bubble=*/false);
EXPECT_EQ(Step::kBlePowerOnAutomatic, model.current_step());
model.PowerOnBleAdapter();
EXPECT_EQ(Step::kBlePowerOnAutomatic, model.current_step());
EXPECT_TRUE(power_receiver.was_called());
EXPECT_FALSE(model.ble_adapter_is_powered());
model.OnBluetoothPoweredStateChanged(true /* powered */);
EXPECT_EQ(test_case.expected_final_step, model.current_step());
EXPECT_TRUE(model.ble_adapter_is_powered());
}
}
TEST_F(AuthenticatorRequestDialogModelTest,
RequestCallbackOnlyCalledOncePerAuthenticator) {
::device::FidoRequestHandlerBase::TransportAvailabilityInfo transports_info;
transports_info.request_type = RequestType::kMakeCredential;
transports_info.available_transports = {
AuthenticatorTransport::kInternal,
AuthenticatorTransport::kUsbHumanInterfaceDevice};
int num_called = 0;
AuthenticatorRequestDialogModel model(/*relying_party_id=*/"example.com");
model.SetRequestCallback(base::BindRepeating(
[](int* i, const std::string& authenticator_id) { ++(*i); },
&num_called));
model.saved_authenticators().AddAuthenticator(AuthenticatorReference(
/*device_id=*/"authenticator", AuthenticatorTransport::kInternal));
model.StartFlow(std::move(transports_info),
/*use_location_bar_bubble=*/false);
EXPECT_EQ(AuthenticatorRequestDialogModel::Step::kTransportSelection,
model.current_step());
EXPECT_EQ(0, num_called);
// Simulate switching back and forth between transports. The request callback
// should only be invoked once (USB is not dispatched through the UI).
model.StartTransportFlowForTesting(AuthenticatorTransport::kInternal);
EXPECT_TRUE(model.should_dialog_be_hidden());
task_environment_.FastForwardUntilNoTasksRemain();
EXPECT_EQ(1, num_called);
model.StartTransportFlowForTesting(
AuthenticatorTransport::kUsbHumanInterfaceDevice);
EXPECT_EQ(AuthenticatorRequestDialogModel::Step::kUsbInsertAndActivate,
model.current_step());
task_environment_.FastForwardUntilNoTasksRemain();
EXPECT_EQ(1, num_called);
model.StartTransportFlowForTesting(AuthenticatorTransport::kInternal);
EXPECT_TRUE(model.should_dialog_be_hidden());
task_environment_.FastForwardUntilNoTasksRemain();
EXPECT_EQ(1, num_called);
}
TEST_F(AuthenticatorRequestDialogModelTest,
RequestCallbackForWindowsAuthenticatorIsInvokedAutomatically) {
constexpr char kWinAuthenticatorId[] = "some_authenticator_id";
::device::FidoRequestHandlerBase::TransportAvailabilityInfo transports_info;
transports_info.request_type = RequestType::kMakeCredential;
transports_info.available_transports = {};
transports_info.has_win_native_api_authenticator = true;
transports_info.win_native_api_authenticator_id = kWinAuthenticatorId;
std::vector<std::string> dispatched_authenticator_ids;
AuthenticatorRequestDialogModel model(/*relying_party_id=*/"example.com");
model.SetRequestCallback(base::BindRepeating(
[](std::vector<std::string>* ids, const std::string& authenticator_id) {
ids->push_back(authenticator_id);
},
&dispatched_authenticator_ids));
model.StartFlow(std::move(transports_info),
/*use_location_bar_bubble=*/false);
EXPECT_TRUE(model.should_dialog_be_hidden());
task_environment_.FastForwardUntilNoTasksRemain();
EXPECT_THAT(dispatched_authenticator_ids, ElementsAre(kWinAuthenticatorId));
}
TEST_F(AuthenticatorRequestDialogModelTest,
ConditionalUINoRecognizedCredential) {
AuthenticatorRequestDialogModel model(/*relying_party_id=*/"example.com");
int num_called = 0;
model.SetRequestCallback(base::BindRepeating(
[](int* i, const std::string& authenticator_id) { ++(*i); },
&num_called));
model.saved_authenticators().AddAuthenticator(
AuthenticatorReference(/*device_id=*/"authenticator",
AuthenticatorTransport::kUsbHumanInterfaceDevice));
model.saved_authenticators().AddAuthenticator(AuthenticatorReference(
/*device_id=*/"authenticator", AuthenticatorTransport::kInternal));
TransportAvailabilityInfo transports_info;
transports_info.available_transports = kAllTransports;
transports_info.has_recognized_platform_authenticator_credential = true;
model.StartFlow(std::move(transports_info),
/*use_location_bar_bubble=*/true);
EXPECT_EQ(model.current_step(), Step::kLocationBarBubble);
EXPECT_TRUE(model.should_dialog_be_hidden());
EXPECT_EQ(num_called, 0);
}
TEST_F(AuthenticatorRequestDialogModelTest, ConditionalUIRecognizedCredential) {
AuthenticatorRequestDialogModel model(/*relying_party_id=*/"example.com");
int num_called = 0;
model.SetRequestCallback(base::BindRepeating(
[](int* i, const std::string& authenticator_id) {
EXPECT_EQ(authenticator_id, "internal");
++(*i);
},
&num_called));
model.saved_authenticators().AddAuthenticator(AuthenticatorReference(
/*device_id=*/"usb", AuthenticatorTransport::kUsbHumanInterfaceDevice));
model.saved_authenticators().AddAuthenticator(AuthenticatorReference(
/*device_id=*/"internal", AuthenticatorTransport::kInternal));
TransportAvailabilityInfo transports_info;
transports_info.available_transports = kAllTransports;
transports_info.has_recognized_platform_authenticator_credential = true;
device::PublicKeyCredentialUserEntity user_1({1, 2, 3, 4});
device::PublicKeyCredentialUserEntity user_2({5, 6, 7, 8});
transports_info.recognized_platform_authenticator_credentials = {user_1,
user_2};
model.StartFlow(std::move(transports_info),
/*is_location_bar_bubble_ui==*/true);
EXPECT_EQ(model.current_step(), Step::kLocationBarBubble);
EXPECT_TRUE(model.should_dialog_be_hidden());
EXPECT_EQ(num_called, 0);
// After selecting an account, the request should be dispatched to the
// platform authenticator.
model.OnAccountSelected(0);
task_environment_.FastForwardUntilNoTasksRemain();
EXPECT_EQ(num_called, 1);
static const uint8_t kAppParam[32] = {0};
static const uint8_t kSignatureCounter[4] = {0};
device::AuthenticatorData auth_data(kAppParam, /*flags=*/0, kSignatureCounter,
base::nullopt);
device::AuthenticatorGetAssertionResponse response_1(
device::AuthenticatorData(kAppParam, /*flags=*/0, kSignatureCounter,
base::nullopt),
/*signature=*/{1});
response_1.user_entity = user_1;
device::AuthenticatorGetAssertionResponse response_2(
device::AuthenticatorData(kAppParam, /*flags=*/0, kSignatureCounter,
base::nullopt),
/*signature=*/{2});
response_2.user_entity = user_2;
uint8_t selected_id = -1;
std::vector<device::AuthenticatorGetAssertionResponse> responses;
responses.push_back(std::move(response_1));
responses.push_back(std::move(response_2));
model.SelectAccount(
std::move(responses),
base::BindLambdaForTesting(
[&](device::AuthenticatorGetAssertionResponse selected) {
selected_id = selected.signature[0];
}));
EXPECT_EQ(selected_id, 1);
}