// 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #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 txt_copy = txt; std::sort(txt_copy.begin(), txt_copy.end()); std::vector arg_copy = arg; std::sort(arg_copy.begin(), arg_copy.end()); return (arg_copy == txt_copy); } template std::set GetKeys(const Map& map) { std::set 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 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 response{ new StrictMock}; 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 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{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{ // 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{ // 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 http_handlers_; std::map https_handlers_; StrictMock config_store_; StrictMock task_runner_; StrictMock http_client_; StrictMock network_; StrictMock dns_sd_; StrictMock http_server_; StrictMock wifi_; StrictMock bluetooth_; std::vector network_callbacks_; std::unique_ptr 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