| // Copyright 2015 The Chromium OS 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/test/mock_bluetooth.h> |
| #include <weave/test/mock_config_store.h> |
| #include <weave/test/mock_http_client.h> |
| #include <weave/test/mock_http_server.h> |
| #include <weave/test/mock_mdns.h> |
| #include <weave/test/mock_network.h> |
| #include <weave/test/mock_task_runner.h> |
| #include <weave/test/mock_wifi_provider.h> |
| #include <weave/test/unittest_utils.h> |
| |
| #include "libweave/src/bind_lambda.h" |
| |
| using testing::_; |
| using testing::AtMost; |
| using testing::HasSubstr; |
| using testing::Invoke; |
| using testing::InvokeWithoutArgs; |
| using testing::MatchesRegex; |
| using testing::Mock; |
| using testing::AtLeast; |
| using testing::Return; |
| using testing::ReturnRefOfCopy; |
| using testing::StartsWith; |
| using testing::StrictMock; |
| using testing::WithArgs; |
| |
| namespace weave { |
| |
| using test::CreateDictionaryValue; |
| using test::ValueToString; |
| |
| const char kCategory[] = "powerd"; |
| |
| const char kBaseCommandDefs[] = R"({ |
| "base": { |
| "reboot": { |
| "parameters": {"delay": "integer"}, |
| "results": {} |
| }, |
| "shutdown": { |
| "parameters": {}, |
| "results": {} |
| } |
| } |
| })"; |
| |
| const char kCommandDefs[] = R"({ |
| "base": { |
| "reboot": {}, |
| "shutdown": {} |
| } |
| })"; |
| |
| const char kBaseStateDefs[] = R"({ |
| "base": { |
| "firmwareVersion": "string", |
| "localDiscoveryEnabled": "boolean", |
| "localAnonymousAccessMaxRole": [ "none", "viewer", "user" ], |
| "localPairingEnabled": "boolean", |
| "network": { |
| "properties": { |
| "name": "string" |
| } |
| } |
| } |
| })"; |
| |
| const char kBaseStateDefaults[] = R"({ |
| "base": { |
| "firmwareVersion": "", |
| "localDiscoveryEnabled": false, |
| "localAnonymousAccessMaxRole": "none", |
| "localPairingEnabled": false |
| } |
| })"; |
| |
| const char kDeviceResource[] = R"({ |
| "kind": "clouddevices#device", |
| "id": "DEVICE_ID", |
| "channel": { |
| "supportedType": "pull" |
| }, |
| "deviceKind": "vendor", |
| "modelManifestId": "ABCDE", |
| "systemName": "", |
| "name": "DEVICE_NAME", |
| "displayName": "", |
| "description": "Developer device", |
| "stateValidationEnabled": true, |
| "commandDefs":{ |
| "base": { |
| "reboot": { |
| "minimalRole": "user", |
| "parameters": {"delay": "integer"}, |
| "results": {} |
| }, |
| "shutdown": { |
| "minimalRole": "user", |
| "parameters": {}, |
| "results": {} |
| } |
| } |
| }, |
| "state":{ |
| "base":{ |
| "firmwareVersion":"FIRMWARE_VERSION", |
| "localAnonymousAccessMaxRole":"viewer", |
| "localDiscoveryEnabled":true, |
| "localPairingEnabled":true, |
| "network":{ |
| } |
| }, |
| "power": {"battery_level":44} |
| } |
| })"; |
| |
| const char kRegistrationResponse[] = R"({ |
| "kind": "clouddevices#registrationTicket", |
| "id": "TEST_ID", |
| "deviceId": "DEVICE_ID", |
| "oauthClientId": "CLIENT_ID", |
| "userEmail": "USER@gmail.com", |
| "creationTimeMs": "1440087183738", |
| "expirationTimeMs": "1440087423738" |
| })"; |
| |
| const char kRegistrationFinalResponse[] = R"({ |
| "kind": "clouddevices#registrationTicket", |
| "id": "TEST_ID", |
| "deviceId": "DEVICE_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" |
| })"; |
| |
| const char kStateDefs[] = R"({"power": {"battery_level":"integer"}})"; |
| |
| const char kStateDefaults[] = R"({"power": {"battery_level":44}})"; |
| |
| 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); |
| } |
| |
| class WeaveTest : public ::testing::Test { |
| protected: |
| void SetUp() override { device_ = weave::Device::Create(); } |
| |
| void ExpectRequest(const std::string& method, |
| const std::string& url, |
| const std::string& json_response) { |
| EXPECT_CALL(http_client_, MockSendRequest(method, url, _, _, _)) |
| .WillOnce(InvokeWithoutArgs([json_response]() { |
| test::MockHttpClientResponse* response = |
| new StrictMock<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()) |
| .Times(AtLeast(1)) |
| .WillRepeatedly(ReturnRefOfCopy(json_response)); |
| return response; |
| })); |
| } |
| |
| void InitConfigStore() { |
| EXPECT_CALL(config_store_, LoadDefaults(_)) |
| .WillOnce(Invoke([](weave::Settings* settings) { |
| settings->api_key = "API_KEY"; |
| settings->client_secret = "CLIENT_SECRET"; |
| settings->client_id = "CLIENT_ID"; |
| settings->firmware_version = "FIRMWARE_VERSION"; |
| settings->name = "DEVICE_NAME"; |
| settings->model_id = "ABCDE"; |
| return true; |
| })); |
| EXPECT_CALL(config_store_, SaveSettings("")).WillRepeatedly(Return()); |
| |
| EXPECT_CALL(config_store_, LoadBaseCommandDefs()) |
| .WillOnce(Return(kBaseCommandDefs)); |
| |
| EXPECT_CALL(config_store_, LoadCommandDefs()) |
| .WillOnce(Return( |
| std::map<std::string, std::string>{{kCategory, kCommandDefs}})); |
| |
| EXPECT_CALL(config_store_, LoadBaseStateDefs()) |
| .WillOnce(Return(kBaseStateDefs)); |
| |
| EXPECT_CALL(config_store_, LoadStateDefs()) |
| .WillOnce(Return( |
| std::map<std::string, std::string>{{kCategory, kStateDefs}})); |
| |
| EXPECT_CALL(config_store_, LoadBaseStateDefaults()) |
| .WillOnce(Return(kBaseStateDefaults)); |
| |
| EXPECT_CALL(config_store_, LoadStateDefaults()) |
| .WillOnce(Return(std::vector<std::string>{kStateDefaults})); |
| } |
| |
| void InitNetwork() { |
| EXPECT_CALL(network_, AddOnConnectionChangedCallback(_)) |
| .WillRepeatedly( |
| Invoke([this](const Network::OnConnectionChangedCallback& cb) { |
| network_callbacks_.push_back(cb); |
| })); |
| EXPECT_CALL(network_, GetConnectionState()) |
| .WillRepeatedly(Return(NetworkState::kOffline)); |
| } |
| |
| void IgnoreMdns() { |
| EXPECT_CALL(mdns_, GetId()).WillRepeatedly(Return("TEST_ID")); |
| EXPECT_CALL(mdns_, PublishService(_, _, _)).WillRepeatedly(Return()); |
| EXPECT_CALL(mdns_, StopPublishing("_privet._tcp")).WillOnce(Return()); |
| } |
| |
| void InitMdns() { |
| EXPECT_CALL(mdns_, GetId()).WillRepeatedly(Return("TEST_ID")); |
| EXPECT_CALL(mdns_, StopPublishing("_privet._tcp")).WillOnce(Return()); |
| } |
| |
| void InitMdnsPublishing(bool registered, const std::string& flags) { |
| std::vector<std::string> txt{{"id=TEST_ID"}, {"flags=" + flags}, |
| {"mmid=ABCDE"}, {"services=_base"}, |
| {"txtvers=3"}, {"ty=DEVICE_NAME"}}; |
| if (registered) { |
| txt.push_back("gcd_id=DEVICE_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(mdns_, PublishService("_privet._tcp", 11, MatchTxt(txt))) |
| .Times(AtMost(1)) |
| .WillOnce(Return()); |
| |
| txt[1] = "flags=BB"; |
| } |
| |
| EXPECT_CALL(mdns_, PublishService("_privet._tcp", 11, MatchTxt(txt))) |
| .WillOnce(Return()); |
| } |
| |
| void InitHttpServer() { |
| EXPECT_CALL(http_server_, GetHttpPort()).WillRepeatedly(Return(11)); |
| EXPECT_CALL(http_server_, GetHttpsPort()).WillRepeatedly(Return(12)); |
| EXPECT_CALL(http_server_, GetHttpsCertificateFingerprint()) |
| .WillRepeatedly(ReturnRefOfCopy(std::vector<uint8_t>{1, 2, 3})); |
| EXPECT_CALL(http_server_, AddRequestHandler(_, _)) |
| .WillRepeatedly(Invoke([this](const std::string& path_prefix, |
| const HttpServer::OnRequestCallback& cb) { |
| http_server_request_cb_.push_back(cb); |
| })); |
| EXPECT_CALL(http_server_, AddOnStateChangedCallback(_)) |
| .WillRepeatedly( |
| Invoke([this](const HttpServer::OnStateChangedCallback& cb) { |
| http_server_changed_cb_.push_back(cb); |
| })); |
| } |
| |
| void InitDefaultExpectations() { |
| InitConfigStore(); |
| InitNetwork(); |
| EXPECT_CALL(wifi_, StartAccessPoint(MatchesRegex("DEVICE_NAME.*prv"))) |
| .WillOnce(Return()); |
| InitHttpServer(); |
| InitMdns(); |
| } |
| |
| void StartDevice() { |
| weave::Device::Options options; |
| options.xmpp_enabled = false; |
| |
| device_->Start(options, &config_store_, &task_runner_, &http_client_, |
| &network_, &mdns_, &http_server_, &wifi_, &bluetooth_); |
| |
| cloud_ = device_->GetCloud(); |
| ASSERT_TRUE(cloud_); |
| |
| cloud_->GetDeviceInfo( |
| base::Bind( |
| [](const base::DictionaryValue& response) { ADD_FAILURE(); }), |
| base::Bind([](const Error* error) { |
| EXPECT_TRUE(error->HasError("gcd", "device_not_registered")); |
| })); |
| |
| for (const auto& cb : http_server_changed_cb_) |
| cb.Run(http_server_); |
| |
| task_runner_.Run(); |
| } |
| |
| void NotifyNetworkChanged(NetworkState state, base::TimeDelta delay) { |
| EXPECT_CALL(network_, GetConnectionState()).WillRepeatedly(Return(state)); |
| for (const auto& cb : network_callbacks_) { |
| task_runner_.PostDelayedTask(FROM_HERE, cb, delay); |
| } |
| } |
| |
| std::vector<HttpServer::OnStateChangedCallback> http_server_changed_cb_; |
| std::vector<HttpServer::OnRequestCallback> http_server_request_cb_; |
| |
| StrictMock<test::MockConfigStore> config_store_; |
| StrictMock<test::MockTaskRunner> task_runner_; |
| StrictMock<test::MockHttpClient> http_client_; |
| StrictMock<test::MockNetwork> network_; |
| StrictMock<test::MockMdns> mdns_; |
| StrictMock<test::MockHttpServer> http_server_; |
| StrictMock<test::MockWifiProvider> wifi_; |
| StrictMock<test::MockBluetooth> bluetooth_; |
| |
| std::vector<Network::OnConnectionChangedCallback> network_callbacks_; |
| |
| weave::Cloud* cloud_{nullptr}; |
| |
| std::unique_ptr<weave::Device> device_; |
| }; |
| |
| TEST_F(WeaveTest, Create) { |
| ASSERT_TRUE(device_.get()); |
| } |
| |
| TEST_F(WeaveTest, StartMinimal) { |
| weave::Device::Options options; |
| options.xmpp_enabled = false; |
| options.disable_privet = true; |
| options.disable_security = true; |
| |
| InitConfigStore(); |
| device_->Start(options, &config_store_, &task_runner_, &http_client_, |
| &network_, nullptr, nullptr, &wifi_, nullptr); |
| } |
| |
| TEST_F(WeaveTest, StartNoWifi) { |
| InitConfigStore(); |
| InitNetwork(); |
| InitHttpServer(); |
| InitMdns(); |
| InitMdnsPublishing(false, "CB"); |
| |
| weave::Device::Options options; |
| device_->Start(options, &config_store_, &task_runner_, &http_client_, |
| &network_, &mdns_, &http_server_, nullptr, &bluetooth_); |
| |
| for (const auto& cb : http_server_changed_cb_) |
| cb.Run(http_server_); |
| |
| task_runner_.Run(); |
| } |
| |
| class WeaveBasicTest : public WeaveTest { |
| public: |
| void SetUp() override { |
| WeaveTest::SetUp(); |
| |
| InitDefaultExpectations(); |
| InitMdnsPublishing(false, "DB"); |
| } |
| }; |
| |
| TEST_F(WeaveBasicTest, Start) { |
| StartDevice(); |
| } |
| |
| TEST_F(WeaveBasicTest, Register) { |
| StartDevice(); |
| |
| auto draft = CreateDictionaryValue(kDeviceResource); |
| auto response = CreateDictionaryValue(kRegistrationResponse); |
| response->Set("deviceDraft", draft->DeepCopy()); |
| ExpectRequest( |
| "PATCH", |
| "https://www.googleapis.com/clouddevices/v1/registrationTickets/" |
| "TEST_ID?key=API_KEY", |
| ValueToString(*response)); |
| |
| response = CreateDictionaryValue(kRegistrationFinalResponse); |
| response->Set("deviceDraft", draft->DeepCopy()); |
| ExpectRequest( |
| "POST", |
| "https://www.googleapis.com/clouddevices/v1/registrationTickets/" |
| "TEST_ID/finalize?key=API_KEY", |
| ValueToString(*response)); |
| |
| ExpectRequest("POST", "https://accounts.google.com/o/oauth2/token", |
| kAuthTokenResponse); |
| |
| InitMdnsPublishing(true, "DB"); |
| |
| EXPECT_EQ("DEVICE_ID", cloud_->RegisterDevice("TEST_ID", nullptr)); |
| } |
| |
| class WeaveWiFiSetupTest : public WeaveTest { |
| public: |
| void SetUp() override { |
| WeaveTest::SetUp(); |
| |
| InitConfigStore(); |
| InitHttpServer(); |
| InitNetwork(); |
| IgnoreMdns(); |
| |
| EXPECT_CALL(network_, GetConnectionState()) |
| .WillRepeatedly(Return(NetworkState::kConnected)); |
| } |
| }; |
| |
| TEST_F(WeaveWiFiSetupTest, StartOnlineNoPrevSsid) { |
| StartDevice(); |
| |
| // Short disconnect. |
| NotifyNetworkChanged(NetworkState::kOffline, {}); |
| NotifyNetworkChanged(NetworkState::kConnected, |
| base::TimeDelta::FromSeconds(10)); |
| task_runner_.Run(); |
| |
| // Long disconnect. |
| NotifyNetworkChanged(NetworkState::kOffline, {}); |
| auto offline_from = task_runner_.GetClock()->Now(); |
| EXPECT_CALL(wifi_, StartAccessPoint(MatchesRegex("DEVICE_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(NetworkState::kOffline, {}); |
| |
| for (int i = 0; i < 5; ++i) { |
| auto offline_from = task_runner_.GetClock()->Now(); |
| // Temporarily offline mode. |
| EXPECT_CALL(wifi_, StartAccessPoint(MatchesRegex("DEVICE_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(NetworkState::kConnected, {}); |
| 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(NetworkState::kOffline)); |
| |
| auto offline_from = task_runner_.GetClock()->Now(); |
| EXPECT_CALL(wifi_, StartAccessPoint(MatchesRegex("DEVICE_NAME.*prv"))) |
| .WillOnce(InvokeWithoutArgs([this, &offline_from]() { |
| EXPECT_GT(task_runner_.GetClock()->Now() - offline_from, |
| base::TimeDelta::FromMinutes(1)); |
| task_runner_.Break(); |
| })); |
| |
| StartDevice(); |
| } |
| |
| } // namespace weave |