blob: 33124f015576fdc501602666ce52cfcf8e1cc6b2 [file] [log] [blame]
// Copyright 2017 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 "device/fido/u2f_request.h"
#include <list>
#include <string>
#include <utility>
#include "base/test/scoped_task_environment.h"
#include "device/fido/fake_fido_discovery.h"
#include "device/fido/fido_transport_protocol.h"
#include "device/fido/mock_fido_device.h"
#include "device/fido/test_callback_receiver.h"
#include "testing/gtest/include/gtest/gtest.h"
using ::testing::_;
namespace device {
namespace {
class FakeU2fRequest : public U2fRequest {
public:
explicit FakeU2fRequest(
const base::flat_set<FidoTransportProtocol>& transports)
: U2fRequest(nullptr /* connector */,
transports,
std::vector<uint8_t>(),
std::vector<uint8_t>(),
std::vector<std::vector<uint8_t>>()) {}
~FakeU2fRequest() override = default;
void TryDevice() override {
// Do nothing.
}
};
using TestVersionCallback =
::device::test::ValueCallbackReceiver<ProtocolVersion>;
} // namespace
class U2fRequestTest : public ::testing::Test {
protected:
base::test::ScopedTaskEnvironment& scoped_task_environment() {
return scoped_task_environment_;
}
test::ScopedFakeFidoDiscoveryFactory& discovery_factory() {
return discovery_factory_;
}
TestVersionCallback& version_callback_receiver() {
return version_callback_receiver_;
}
private:
base::test::ScopedTaskEnvironment scoped_task_environment_{
base::test::ScopedTaskEnvironment::MainThreadType::MOCK_TIME};
TestVersionCallback version_callback_receiver_;
test::ScopedFakeFidoDiscoveryFactory discovery_factory_;
};
TEST_F(U2fRequestTest, TestIterateDevice) {
auto* discovery = discovery_factory().ForgeNextHidDiscovery();
FakeU2fRequest request({FidoTransportProtocol::kUsbHumanInterfaceDevice});
request.Start();
auto device0 = std::make_unique<MockFidoDevice>();
auto device1 = std::make_unique<MockFidoDevice>();
EXPECT_CALL(*device0, GetId()).WillRepeatedly(::testing::Return("device0"));
EXPECT_CALL(*device1, GetId()).WillRepeatedly(::testing::Return("device1"));
// Add two U2F devices
discovery->AddDevice(std::move(device0));
discovery->AddDevice(std::move(device1));
// Move first device to current.
request.IterateDevice();
ASSERT_NE(nullptr, request.current_device_);
EXPECT_EQ(static_cast<size_t>(1), request.devices_.size());
// Move second device to current, first to attempted.
request.IterateDevice();
ASSERT_NE(nullptr, request.current_device_);
EXPECT_EQ(static_cast<size_t>(1), request.attempted_devices_.size());
// Move second device from current to attempted, move attempted to devices as
// all devices have been attempted.
request.IterateDevice();
ASSERT_EQ(nullptr, request.current_device_);
EXPECT_EQ(static_cast<size_t>(2), request.devices_.size());
EXPECT_EQ(static_cast<size_t>(0), request.attempted_devices_.size());
// Moving attempted devices results in a delayed retry, after which the first
// device will be tried again. Check for the expected behavior here.
auto* mock_device = static_cast<MockFidoDevice*>(request.devices_.front());
EXPECT_CALL(*mock_device, TryWinkRef(_));
scoped_task_environment().FastForwardUntilNoTasksRemain();
EXPECT_EQ(mock_device, request.current_device_);
EXPECT_EQ(static_cast<size_t>(1), request.devices_.size());
EXPECT_EQ(static_cast<size_t>(0), request.attempted_devices_.size());
}
TEST_F(U2fRequestTest, TestAbandonCurrentDeviceAndTransition) {
auto* discovery = discovery_factory().ForgeNextHidDiscovery();
FakeU2fRequest request({FidoTransportProtocol::kUsbHumanInterfaceDevice});
request.Start();
auto device = std::make_unique<MockFidoDevice>();
EXPECT_CALL(*device, GetId()).WillRepeatedly(::testing::Return("device"));
discovery->AddDevice(std::move(device));
// Move device to current.
request.IterateDevice();
EXPECT_NE(nullptr, request.current_device_);
// Abandon device.
request.AbandonCurrentDeviceAndTransition();
EXPECT_EQ(nullptr, request.current_device_);
EXPECT_EQ(1u, request.abandoned_devices_.size());
// Iterating through the device list should not change the state.
request.IterateDevice();
EXPECT_EQ(nullptr, request.current_device_);
EXPECT_EQ(1u, request.abandoned_devices_.size());
// Removing the device from the discovery should clear it from the list.
discovery->RemoveDevice("device");
EXPECT_TRUE(request.abandoned_devices_.empty());
}
TEST_F(U2fRequestTest, TestBasicMachine) {
auto* discovery = discovery_factory().ForgeNextHidDiscovery();
FakeU2fRequest request({FidoTransportProtocol::kUsbHumanInterfaceDevice});
request.Start();
ASSERT_NO_FATAL_FAILURE(discovery->WaitForCallToStartAndSimulateSuccess());
// Add one U2F device
auto device = std::make_unique<MockFidoDevice>();
EXPECT_CALL(*device, GetId());
EXPECT_CALL(*device, TryWinkRef(_))
.WillOnce(::testing::Invoke(MockFidoDevice::WinkDoNothing));
discovery->AddDevice(std::move(device));
EXPECT_EQ(U2fRequest::State::BUSY, request.state_);
}
TEST_F(U2fRequestTest, TestAlreadyPresentDevice) {
auto* discovery = discovery_factory().ForgeNextHidDiscovery();
FakeU2fRequest request({FidoTransportProtocol::kUsbHumanInterfaceDevice});
request.Start();
auto device = std::make_unique<MockFidoDevice>();
EXPECT_CALL(*device, GetId()).WillRepeatedly(::testing::Return("device"));
discovery->AddDevice(std::move(device));
ASSERT_NO_FATAL_FAILURE(discovery->WaitForCallToStartAndSimulateSuccess());
EXPECT_NE(nullptr, request.current_device_);
}
TEST_F(U2fRequestTest, TestMultipleDiscoveries) {
auto* discovery_1 = discovery_factory().ForgeNextHidDiscovery();
auto* discovery_2 = discovery_factory().ForgeNextBleDiscovery();
// Create a fake request with two different discoveries that both start up
// successfully.
FakeU2fRequest request({FidoTransportProtocol::kUsbHumanInterfaceDevice,
FidoTransportProtocol::kBluetoothLowEnergy});
request.Start();
ASSERT_NO_FATAL_FAILURE(discovery_1->WaitForCallToStartAndSimulateSuccess());
ASSERT_NO_FATAL_FAILURE(discovery_2->WaitForCallToStartAndSimulateSuccess());
// Let each discovery find a device.
auto device_1 = std::make_unique<MockFidoDevice>();
auto device_2 = std::make_unique<MockFidoDevice>();
EXPECT_CALL(*device_1, GetId()).WillRepeatedly(::testing::Return("device_1"));
EXPECT_CALL(*device_2, GetId()).WillRepeatedly(::testing::Return("device_2"));
auto* device_1_ptr = device_1.get();
auto* device_2_ptr = device_2.get();
discovery_1->AddDevice(std::move(device_1));
discovery_2->AddDevice(std::move(device_2));
// Iterate through the devices and make sure they are considered in the same
// order as they were added.
EXPECT_EQ(device_1_ptr, request.current_device_);
request.IterateDevice();
EXPECT_EQ(device_2_ptr, request.current_device_);
request.IterateDevice();
EXPECT_EQ(nullptr, request.current_device_);
EXPECT_EQ(2u, request.devices_.size());
// Add a third device.
auto device_3 = std::make_unique<MockFidoDevice>();
EXPECT_CALL(*device_3, GetId()).WillRepeatedly(::testing::Return("device_3"));
auto* device_3_ptr = device_3.get();
discovery_1->AddDevice(std::move(device_3));
// Exhaust the timeout and remove the first two devices, making sure the just
// added one is the only device considered.
scoped_task_environment().FastForwardUntilNoTasksRemain();
discovery_2->RemoveDevice("device_2");
discovery_1->RemoveDevice("device_1");
EXPECT_EQ(device_3_ptr, request.current_device_);
// Finally remove the last remaining device.
discovery_1->RemoveDevice("device_3");
EXPECT_EQ(nullptr, request.current_device_);
}
TEST_F(U2fRequestTest, TestSlowDiscovery) {
auto* fast_discovery = discovery_factory().ForgeNextHidDiscovery();
auto* slow_discovery = discovery_factory().ForgeNextBleDiscovery();
// Create a fake request with two different discoveries that start at
// different times.
FakeU2fRequest request({FidoTransportProtocol::kUsbHumanInterfaceDevice,
FidoTransportProtocol::kBluetoothLowEnergy});
auto fast_device = std::make_unique<MockFidoDevice>();
auto slow_device = std::make_unique<MockFidoDevice>();
EXPECT_CALL(*fast_device, GetId())
.WillRepeatedly(::testing::Return("fast_device"));
EXPECT_CALL(*slow_device, GetId())
.WillRepeatedly(::testing::Return("slow_device"));
bool fast_winked = false;
EXPECT_CALL(*fast_device, TryWinkRef(_))
.WillOnce(
::testing::DoAll(::testing::Assign(&fast_winked, true),
::testing::Invoke(MockFidoDevice::WinkDoNothing)))
.WillRepeatedly(::testing::Invoke(MockFidoDevice::WinkDoNothing));
bool slow_winked = false;
EXPECT_CALL(*slow_device, TryWinkRef(_))
.WillOnce(testing::DoAll(testing::Assign(&slow_winked, true),
testing::Invoke(MockFidoDevice::WinkDoNothing)));
auto* fast_device_ptr = fast_device.get();
auto* slow_device_ptr = slow_device.get();
EXPECT_EQ(nullptr, request.current_device_);
request.state_ = U2fRequest::State::INIT;
// The discoveries will be started and |fast_discovery| will succeed
// immediately with a device already found.
request.Start();
EXPECT_FALSE(fast_winked);
ASSERT_NO_FATAL_FAILURE(fast_discovery->WaitForCallToStart());
fast_discovery->AddDevice(std::move(fast_device));
ASSERT_NO_FATAL_FAILURE(fast_discovery->SimulateStarted(true /* success */));
EXPECT_TRUE(fast_winked);
EXPECT_EQ(fast_device_ptr, request.current_device_);
EXPECT_EQ(U2fRequest::State::BUSY, request.state_);
// There are no more devices at this time.
request.state_ = U2fRequest::State::IDLE;
request.Transition();
EXPECT_EQ(nullptr, request.current_device_);
EXPECT_EQ(U2fRequest::State::OFF, request.state_);
// All devices have been tried and have been re-enqueued to try again in the
// future. Now |slow_discovery| starts:
ASSERT_TRUE(slow_discovery->is_start_requested());
ASSERT_FALSE(slow_discovery->is_running());
ASSERT_NO_FATAL_FAILURE(slow_discovery->WaitForCallToStart());
slow_discovery->AddDevice(std::move(slow_device));
ASSERT_NO_FATAL_FAILURE(slow_discovery->SimulateStarted(true /* success */));
// |fast_device| is already enqueued and will be retried immediately.
EXPECT_EQ(fast_device_ptr, request.current_device_);
EXPECT_EQ(U2fRequest::State::BUSY, request.state_);
// Next the newly found |slow_device| will be tried.
request.state_ = U2fRequest::State::IDLE;
EXPECT_FALSE(slow_winked);
request.Transition();
EXPECT_TRUE(slow_winked);
EXPECT_EQ(slow_device_ptr, request.current_device_);
EXPECT_EQ(U2fRequest::State::BUSY, request.state_);
// All discoveries are complete so the request transitions to |OFF|.
request.state_ = U2fRequest::State::IDLE;
request.Transition();
EXPECT_EQ(nullptr, request.current_device_);
EXPECT_EQ(U2fRequest::State::OFF, request.state_);
}
TEST_F(U2fRequestTest, TestMultipleDiscoveriesWithFailures) {
// Create a fake request with two different discoveries that both start up
// unsuccessfully.
{
auto* discovery_1 = discovery_factory().ForgeNextHidDiscovery();
auto* discovery_2 = discovery_factory().ForgeNextBleDiscovery();
FakeU2fRequest request({FidoTransportProtocol::kUsbHumanInterfaceDevice,
FidoTransportProtocol::kBluetoothLowEnergy});
request.Start();
ASSERT_NO_FATAL_FAILURE(discovery_1->WaitForCallToStart());
ASSERT_NO_FATAL_FAILURE(discovery_1->SimulateStarted(false /* success */));
ASSERT_NO_FATAL_FAILURE(discovery_2->WaitForCallToStart());
ASSERT_NO_FATAL_FAILURE(discovery_2->SimulateStarted(false /* success */));
EXPECT_EQ(U2fRequest::State::OFF, request.state_);
}
// Create a fake request with two different discoveries, where only one starts
// up successfully.
{
auto* discovery_1 = discovery_factory().ForgeNextHidDiscovery();
auto* discovery_2 = discovery_factory().ForgeNextBleDiscovery();
FakeU2fRequest request({FidoTransportProtocol::kUsbHumanInterfaceDevice,
FidoTransportProtocol::kBluetoothLowEnergy});
request.Start();
ASSERT_NO_FATAL_FAILURE(discovery_1->WaitForCallToStart());
ASSERT_NO_FATAL_FAILURE(discovery_1->SimulateStarted(false /* success */));
ASSERT_NO_FATAL_FAILURE(discovery_2->WaitForCallToStart());
ASSERT_NO_FATAL_FAILURE(discovery_2->SimulateStarted(true /* success */));
auto device0 = std::make_unique<MockFidoDevice>();
EXPECT_CALL(*device0, GetId())
.WillRepeatedly(::testing::Return("device_0"));
EXPECT_CALL(*device0, TryWinkRef(_))
.WillOnce(::testing::Invoke(MockFidoDevice::WinkDoNothing));
discovery_2->AddDevice(std::move(device0));
EXPECT_EQ(U2fRequest::State::BUSY, request.state_);
// Simulate an action that sets the request state to idle.
// This and the call to Transition() below is necessary to trigger iterating
// and trying the new device.
request.state_ = U2fRequest::State::IDLE;
// Adding another device should trigger examination and a busy state.
auto device1 = std::make_unique<MockFidoDevice>();
EXPECT_CALL(*device1, GetId())
.WillRepeatedly(::testing::Return("device_1"));
EXPECT_CALL(*device1, TryWinkRef(_))
.WillOnce(::testing::Invoke(MockFidoDevice::WinkDoNothing));
discovery_2->AddDevice(std::move(device1));
request.Transition();
EXPECT_EQ(U2fRequest::State::BUSY, request.state_);
}
}
} // namespace device