blob: eb651497dcee4e2eb98cdf363467eab3cd7773fb [file] [log] [blame]
// Copyright 2015 The Weave 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 <weave/device.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <weave/provider/test/fake_task_runner.h>
#include <weave/provider/test/mock_bluetooth.h>
#include <weave/provider/test/mock_config_store.h>
#include <weave/provider/test/mock_dns_service_discovery.h>
#include <weave/provider/test/mock_http_client.h>
#include <weave/provider/test/mock_http_server.h>
#include <weave/provider/test/mock_network.h>
#include <weave/provider/test/mock_wifi.h>
#include <weave/test/mock_command.h>
#include <weave/test/mock_device.h>
#include <weave/test/unittest_utils.h>
#include "src/bind_lambda.h"
using testing::_;
using testing::AtLeast;
using testing::AtMost;
using testing::HasSubstr;
using testing::InSequence;
using testing::Invoke;
using testing::InvokeWithoutArgs;
using testing::MatchesRegex;
using testing::Mock;
using testing::Return;
using testing::ReturnRefOfCopy;
using testing::StartsWith;
using testing::StrictMock;
using testing::WithArgs;
namespace weave {
namespace {
using provider::HttpClient;
using provider::Network;
using provider::test::MockHttpClientResponse;
using test::CreateDictionaryValue;
using test::ValueToString;
const char kTraitDefs[] = R"({
"trait1": {
"commands": {
"reboot": {
"minimalRole": "user"
},
"shutdown": {
"minimalRole": "user",
"parameters": {},
"results": {}
}
},
"state": {
"firmwareVersion": {"type": "string"}
}
},
"trait2": {
"state": {
"battery_level": {"type": "integer"}
}
}
})";
const char kDeviceResource[] = R"({
"kind": "weave#device",
"id": "CLOUD_ID",
"channel": {
"supportedType": "pull"
},
"deviceKind": "vendor",
"modelManifestId": "ABCDE",
"systemName": "",
"name": "TEST_NAME",
"displayName": "",
"description": "Developer device",
"stateValidationEnabled": true,
"commandDefs":{
"trait1": {
"reboot": {
"minimalRole": "user",
"parameters": {"delay": {"type": "integer"}},
"results": {}
},
"shutdown": {
"minimalRole": "user",
"parameters": {},
"results": {}
}
}
},
"state":{
"trait1": {"firmwareVersion":"FIRMWARE_VERSION"},
"trait2": {"battery_level":44}
},
"traits": {
"trait1": {
"commands": {
"reboot": {
"minimalRole": "user"
},
"shutdown": {
"minimalRole": "user",
"parameters": {},
"results": {}
}
},
"state": {
"firmwareVersion": {"type": "string"}
}
},
"trait2": {
"state": {
"battery_level": {"type": "integer"}
}
}
},
"components": {
"myComponent": {
"traits": ["trait1", "trait2"],
"state": {
"trait1": {"firmwareVersion":"FIRMWARE_VERSION"},
"trait2": {"battery_level":44}
}
}
}
})";
const char kRegistrationResponse[] = R"({
"kind": "weave#registrationTicket",
"id": "TICKET_ID",
"deviceId": "CLOUD_ID",
"oauthClientId": "CLIENT_ID",
"userEmail": "USER@gmail.com",
"creationTimeMs": "1440087183738",
"expirationTimeMs": "1440087423738"
})";
const char kRegistrationFinalResponse[] = R"({
"kind": "weave#registrationTicket",
"id": "TICKET_ID",
"deviceId": "CLOUD_ID",
"oauthClientId": "CLIENT_ID",
"userEmail": "USER@gmail.com",
"robotAccountEmail": "ROBO@gmail.com",
"robotAccountAuthorizationCode": "AUTH_CODE",
"creationTimeMs": "1440087183738",
"expirationTimeMs": "1440087423738"
})";
const char kAuthTokenResponse[] = R"({
"access_token" : "ACCESS_TOKEN",
"token_type" : "Bearer",
"expires_in" : 3599,
"refresh_token" : "REFRESH_TOKEN"
})";
MATCHER_P(MatchTxt, txt, "") {
std::vector<std::string> txt_copy = txt;
std::sort(txt_copy.begin(), txt_copy.end());
std::vector<std::string> arg_copy = arg;
std::sort(arg_copy.begin(), arg_copy.end());
return (arg_copy == txt_copy);
}
template <class Map>
std::set<typename Map::key_type> GetKeys(const Map& map) {
std::set<typename Map::key_type> result;
for (const auto& pair : map)
result.insert(pair.first);
return result;
}
} // namespace
class WeaveTest : public ::testing::Test {
protected:
void SetUp() override {
EXPECT_CALL(wifi_, IsWifi24Supported()).WillRepeatedly(Return(true));
EXPECT_CALL(wifi_, IsWifi50Supported()).WillRepeatedly(Return(false));
}
template <class UrlMatcher>
void ExpectRequest(HttpClient::Method method,
const UrlMatcher& url_matcher,
const std::string& json_response) {
EXPECT_CALL(http_client_, SendRequest(method, url_matcher, _, _, _))
.WillOnce(WithArgs<4>(Invoke(
[json_response](const HttpClient::SendRequestCallback& callback) {
std::unique_ptr<provider::test::MockHttpClientResponse> response{
new StrictMock<provider::test::MockHttpClientResponse>};
EXPECT_CALL(*response, GetStatusCode())
.Times(AtLeast(1))
.WillRepeatedly(Return(200));
EXPECT_CALL(*response, GetContentType())
.Times(AtLeast(1))
.WillRepeatedly(Return("application/json; charset=utf-8"));
EXPECT_CALL(*response, GetData())
.WillRepeatedly(Return(json_response));
callback.Run(std::move(response), nullptr);
})));
}
void InitNetwork() {
EXPECT_CALL(network_, AddConnectionChangedCallback(_))
.WillRepeatedly(Invoke(
[this](const provider::Network::ConnectionChangedCallback& cb) {
network_callbacks_.push_back(cb);
}));
EXPECT_CALL(network_, GetConnectionState())
.WillRepeatedly(Return(Network::State::kOffline));
}
void InitDnsSd() {
EXPECT_CALL(dns_sd_, PublishService(_, _, _)).WillRepeatedly(Return());
EXPECT_CALL(dns_sd_, StopPublishing("_privet._tcp")).WillOnce(Return());
}
void InitDnsSdPublishing(bool registered, const std::string& flags) {
std::vector<std::string> txt{
{"id=TEST_DEVICE_ID"}, {"flags=" + flags}, {"mmid=ABCDE"},
{"services=developmentBoard"}, {"txtvers=3"}, {"ty=TEST_NAME"}};
if (registered) {
txt.push_back("gcd_id=CLOUD_ID");
// During registration device may announce itself twice:
// 1. with GCD ID but not connected (DB)
// 2. with GCD ID and connected (BB)
EXPECT_CALL(dns_sd_, PublishService("_privet._tcp", 11, MatchTxt(txt)))
.Times(AtMost(1))
.WillOnce(Return());
txt[1] = "flags=BB";
}
EXPECT_CALL(dns_sd_, PublishService("_privet._tcp", 11, MatchTxt(txt)))
.Times(AtMost(1))
.WillOnce(Return());
}
void InitHttpServer() {
EXPECT_CALL(http_server_, GetHttpPort()).WillRepeatedly(Return(11));
EXPECT_CALL(http_server_, GetHttpsPort()).WillRepeatedly(Return(12));
EXPECT_CALL(http_server_, GetRequestTimeout())
.WillRepeatedly(Return(base::TimeDelta::Max()));
EXPECT_CALL(http_server_, GetHttpsCertificateFingerprint())
.WillRepeatedly(Return(std::vector<uint8_t>{1, 2, 3}));
EXPECT_CALL(http_server_, AddHttpRequestHandler(_, _))
.WillRepeatedly(Invoke(
[this](const std::string& path_prefix,
const provider::HttpServer::RequestHandlerCallback& cb) {
http_handlers_[path_prefix] = cb;
}));
EXPECT_CALL(http_server_, AddHttpsRequestHandler(_, _))
.WillRepeatedly(Invoke(
[this](const std::string& path_prefix,
const provider::HttpServer::RequestHandlerCallback& cb) {
https_handlers_[path_prefix] = cb;
}));
EXPECT_CALL(http_server_, RemoveHttpRequestHandler(_))
.WillRepeatedly(Invoke([this](const std::string& path_prefix) {
http_handlers_.erase(path_prefix);
}));
EXPECT_CALL(http_server_, RemoveHttpsRequestHandler(_))
.WillRepeatedly(Invoke([this](const std::string& path_prefix) {
https_handlers_.erase(path_prefix);
}));
}
void InitDefaultExpectations() {
InitNetwork();
EXPECT_CALL(wifi_, StartAccessPoint(MatchesRegex("TEST_NAME.*prv")))
.WillOnce(Return());
InitHttpServer();
InitDnsSd();
}
void StartDevice() {
device_ = weave::Device::Create(&config_store_, &task_runner_,
&http_client_, &network_, &dns_sd_,
&http_server_, &wifi_, &bluetooth_);
EXPECT_EQ((std::set<std::string>{
// clang-format off
"/privet/info",
"/privet/v3/pairing/cancel",
"/privet/v3/pairing/confirm",
"/privet/v3/pairing/start",
// clang-format on
}),
GetKeys(http_handlers_));
EXPECT_EQ((std::set<std::string>{
// clang-format off
"/privet/info",
"/privet/v3/accessControl/claim",
"/privet/v3/accessControl/confirm",
"/privet/v3/auth",
"/privet/v3/checkForUpdates",
"/privet/v3/commands/cancel",
"/privet/v3/commands/execute",
"/privet/v3/commands/list",
"/privet/v3/commands/status",
"/privet/v3/components",
"/privet/v3/pairing/cancel",
"/privet/v3/pairing/confirm",
"/privet/v3/pairing/start",
"/privet/v3/setup/start",
"/privet/v3/setup/status",
"/privet/v3/traits",
// clang-format on
}),
GetKeys(https_handlers_));
device_->AddTraitDefinitionsFromJson(kTraitDefs);
EXPECT_TRUE(
device_->AddComponent("myComponent", {"trait1", "trait2"}, nullptr));
EXPECT_TRUE(device_->SetStatePropertiesFromJson(
"myComponent", R"({"trait2": {"battery_level":44}})", nullptr));
task_runner_.Run();
}
void NotifyNetworkChanged(provider::Network::State state,
base::TimeDelta delay) {
auto task = [this, state] {
EXPECT_CALL(network_, GetConnectionState()).WillRepeatedly(Return(state));
for (const auto& cb : network_callbacks_)
cb.Run();
};
task_runner_.PostDelayedTask(FROM_HERE, base::Bind(task), delay);
}
std::map<std::string, provider::HttpServer::RequestHandlerCallback>
http_handlers_;
std::map<std::string, provider::HttpServer::RequestHandlerCallback>
https_handlers_;
StrictMock<provider::test::MockConfigStore> config_store_;
StrictMock<provider::test::FakeTaskRunner> task_runner_;
StrictMock<provider::test::MockHttpClient> http_client_;
StrictMock<provider::test::MockNetwork> network_;
StrictMock<provider::test::MockDnsServiceDiscovery> dns_sd_;
StrictMock<provider::test::MockHttpServer> http_server_;
StrictMock<provider::test::MockWifi> wifi_;
StrictMock<provider::test::MockBluetooth> bluetooth_;
std::vector<provider::Network::ConnectionChangedCallback> network_callbacks_;
std::unique_ptr<weave::Device> device_;
};
TEST_F(WeaveTest, Mocks) {
// Test checks if mock implements entire interface and mock can be
// instantiated.
test::MockDevice device;
test::MockCommand command;
}
TEST_F(WeaveTest, StartMinimal) {
device_ = weave::Device::Create(&config_store_, &task_runner_, &http_client_,
&network_, nullptr, nullptr, &wifi_, nullptr);
}
TEST_F(WeaveTest, StartNoWifi) {
InitNetwork();
InitHttpServer();
InitDnsSd();
InitDnsSdPublishing(false, "CB");
device_ = weave::Device::Create(&config_store_, &task_runner_, &http_client_,
&network_, &dns_sd_, &http_server_, nullptr,
&bluetooth_);
device_->AddTraitDefinitionsFromJson(kTraitDefs);
EXPECT_TRUE(
device_->AddComponent("myComponent", {"trait1", "trait2"}, nullptr));
task_runner_.Run();
}
class WeaveBasicTest : public WeaveTest {
public:
void SetUp() override {
WeaveTest::SetUp();
InitDefaultExpectations();
InitDnsSdPublishing(false, "DB");
}
};
TEST_F(WeaveBasicTest, Start) {
StartDevice();
}
TEST_F(WeaveBasicTest, Register) {
EXPECT_CALL(network_, OpenSslSocket(_, _, _)).WillRepeatedly(Return());
StartDevice();
auto draft = CreateDictionaryValue(kDeviceResource);
auto response = CreateDictionaryValue(kRegistrationResponse);
response->Set("deviceDraft", draft->CreateDeepCopy());
ExpectRequest(HttpClient::Method::kPatch,
"https://www.googleapis.com/weave/v1/registrationTickets/"
"TICKET_ID?key=TEST_API_KEY",
ValueToString(*response));
response = CreateDictionaryValue(kRegistrationFinalResponse);
response->Set("deviceDraft", draft->CreateDeepCopy());
ExpectRequest(HttpClient::Method::kPost,
"https://www.googleapis.com/weave/v1/registrationTickets/"
"TICKET_ID/finalize?key=TEST_API_KEY",
ValueToString(*response));
ExpectRequest(HttpClient::Method::kPost,
"https://accounts.google.com/o/oauth2/token",
kAuthTokenResponse);
ExpectRequest(HttpClient::Method::kPost, HasSubstr("upsertLocalAuthInfo"),
{});
InitDnsSdPublishing(true, "DB");
bool done = false;
device_->Register(RegistrationData{"TICKET_ID"},
base::Bind([this, &done](ErrorPtr error) {
EXPECT_FALSE(error);
done = true;
task_runner_.Break();
EXPECT_EQ("CLOUD_ID", device_->GetSettings().cloud_id);
}));
task_runner_.Run();
EXPECT_TRUE(done);
done = false;
device_->Register(RegistrationData{"TICKET_ID2"},
base::Bind([this, &done](ErrorPtr error) {
EXPECT_TRUE(error->HasError("already_registered"));
done = true;
task_runner_.Break();
EXPECT_EQ("CLOUD_ID", device_->GetSettings().cloud_id);
}));
task_runner_.Run();
EXPECT_TRUE(done);
}
class WeaveWiFiSetupTest : public WeaveTest {
public:
void SetUp() override {
WeaveTest::SetUp();
InitHttpServer();
InitNetwork();
InitDnsSd();
EXPECT_CALL(network_, GetConnectionState())
.WillRepeatedly(Return(provider::Network::State::kOnline));
}
};
TEST_F(WeaveWiFiSetupTest, StartOnlineNoPrevSsid) {
StartDevice();
// Short disconnect.
NotifyNetworkChanged(provider::Network::State::kOffline, {});
NotifyNetworkChanged(provider::Network::State::kOnline,
base::TimeDelta::FromSeconds(10));
task_runner_.Run();
// Long disconnect.
NotifyNetworkChanged(Network::State::kOffline, {});
auto offline_from = task_runner_.GetClock()->Now();
EXPECT_CALL(wifi_, StartAccessPoint(MatchesRegex("TEST_NAME.*prv")))
.WillOnce(InvokeWithoutArgs([this, offline_from]() {
EXPECT_GT(task_runner_.GetClock()->Now() - offline_from,
base::TimeDelta::FromMinutes(1));
task_runner_.Break();
}));
task_runner_.Run();
}
// If device has previously configured WiFi it will run AP for limited time
// after which it will try to re-connect.
TEST_F(WeaveWiFiSetupTest, StartOnlineWithPrevSsid) {
EXPECT_CALL(config_store_, LoadSettings())
.WillRepeatedly(Return(R"({"last_configured_ssid": "TEST_ssid"})"));
StartDevice();
// Long disconnect.
NotifyNetworkChanged(Network::State::kOffline, {});
for (int i = 0; i < 5; ++i) {
auto offline_from = task_runner_.GetClock()->Now();
// Temporarily offline mode.
EXPECT_CALL(wifi_, StartAccessPoint(MatchesRegex("TEST_NAME.*prv")))
.WillOnce(InvokeWithoutArgs([this, &offline_from]() {
EXPECT_GT(task_runner_.GetClock()->Now() - offline_from,
base::TimeDelta::FromMinutes(1));
task_runner_.Break();
}));
task_runner_.Run();
// Try to reconnect again.
offline_from = task_runner_.GetClock()->Now();
EXPECT_CALL(wifi_, StopAccessPoint())
.WillOnce(InvokeWithoutArgs([this, offline_from]() {
EXPECT_GT(task_runner_.GetClock()->Now() - offline_from,
base::TimeDelta::FromMinutes(5));
task_runner_.Break();
}));
task_runner_.Run();
}
NotifyNetworkChanged(Network::State::kOnline, {});
task_runner_.Run();
}
TEST_F(WeaveWiFiSetupTest, StartOfflineWithSsid) {
EXPECT_CALL(config_store_, LoadSettings())
.WillRepeatedly(Return(R"({"last_configured_ssid": "TEST_ssid"})"));
EXPECT_CALL(network_, GetConnectionState())
.WillRepeatedly(Return(Network::State::kOffline));
auto offline_from = task_runner_.GetClock()->Now();
EXPECT_CALL(wifi_, StartAccessPoint(MatchesRegex("TEST_NAME.*prv")))
.WillOnce(InvokeWithoutArgs([this, &offline_from]() {
EXPECT_GT(task_runner_.GetClock()->Now() - offline_from,
base::TimeDelta::FromMinutes(1));
task_runner_.Break();
}));
StartDevice();
}
TEST_F(WeaveWiFiSetupTest, OfflineLongTimeWithNoSsid) {
EXPECT_CALL(network_, GetConnectionState())
.WillRepeatedly(Return(Network::State::kOffline));
NotifyNetworkChanged(provider::Network::State::kOnline,
base::TimeDelta::FromHours(15));
{
InSequence s;
auto time_stamp = task_runner_.GetClock()->Now();
EXPECT_CALL(wifi_, StartAccessPoint(MatchesRegex("TEST_NAME.*prv")))
.WillOnce(InvokeWithoutArgs([this, &time_stamp]() {
EXPECT_LE(task_runner_.GetClock()->Now() - time_stamp,
base::TimeDelta::FromMinutes(1));
time_stamp = task_runner_.GetClock()->Now();
}));
EXPECT_CALL(wifi_, StopAccessPoint())
.WillOnce(InvokeWithoutArgs([this, &time_stamp]() {
EXPECT_GT(task_runner_.GetClock()->Now() - time_stamp,
base::TimeDelta::FromMinutes(5));
time_stamp = task_runner_.GetClock()->Now();
task_runner_.Break();
}));
}
StartDevice();
}
TEST_F(WeaveWiFiSetupTest, OfflineLongTimeWithSsid) {
EXPECT_CALL(config_store_, LoadSettings())
.WillRepeatedly(Return(R"({"last_configured_ssid": "TEST_ssid"})"));
EXPECT_CALL(network_, GetConnectionState())
.WillRepeatedly(Return(Network::State::kOffline));
NotifyNetworkChanged(provider::Network::State::kOnline,
base::TimeDelta::FromHours(15));
{
InSequence s;
auto time_stamp = task_runner_.GetClock()->Now();
for (size_t i = 0; i < 10; ++i) {
EXPECT_CALL(wifi_, StartAccessPoint(MatchesRegex("TEST_NAME.*prv")))
.WillOnce(InvokeWithoutArgs([this, &time_stamp]() {
EXPECT_GT(task_runner_.GetClock()->Now() - time_stamp,
base::TimeDelta::FromMinutes(1));
time_stamp = task_runner_.GetClock()->Now();
}));
EXPECT_CALL(wifi_, StopAccessPoint())
.WillOnce(InvokeWithoutArgs([this, &time_stamp]() {
EXPECT_GT(task_runner_.GetClock()->Now() - time_stamp,
base::TimeDelta::FromMinutes(5));
time_stamp = task_runner_.GetClock()->Now();
}));
}
EXPECT_CALL(wifi_, StartAccessPoint(MatchesRegex("TEST_NAME.*prv")))
.WillOnce(InvokeWithoutArgs([this]() { task_runner_.Break(); }));
}
StartDevice();
}
} // namespace weave