// 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 "src/device_registration_info.h" #include <base/json/json_reader.h> #include <base/json/json_writer.h> #include <base/values.h> #include <gtest/gtest.h> #include <weave/provider/test/fake_task_runner.h> #include <weave/provider/test/mock_config_store.h> #include <weave/provider/test/mock_http_client.h> #include <weave/test/unittest_utils.h> #include "src/bind_lambda.h" #include "src/component_manager_impl.h" #include "src/http_constants.h" #include "src/privet/auth_manager.h" #include "src/test/mock_clock.h" using testing::_; using testing::AtLeast; using testing::HasSubstr; using testing::Invoke; using testing::InvokeWithoutArgs; using testing::Mock; using testing::Return; using testing::ReturnRef; using testing::ReturnRefOfCopy; using testing::SaveArg; using testing::StrictMock; using testing::WithArgs; namespace weave { using test::CreateDictionaryValue; using test::CreateValue; using provider::test::MockHttpClient; using provider::test::MockHttpClientResponse; using provider::HttpClient; namespace { namespace test_data { const char kXmppEndpoint[] = "xmpp.server.com:1234"; const char kServiceURL[] = "http://gcd.server.com/"; const char kOAuthURL[] = "http://oauth.server.com/"; const char kApiKey[] = "GOadRdTf9FERf0k4w6EFOof56fUJ3kFDdFL3d7f"; const char kClientId[] = "123543821385-sfjkjshdkjhfk234sdfsdfkskd" "fkjh7f.apps.googleusercontent.com"; const char kClientSecret[] = "5sdGdGlfolGlrFKfdFlgP6FG"; const char kCloudId[] = "4a7ea2d1-b331-1e1f-b206-e863c7635196"; const char kDeviceId[] = "f6885e46-b432-42d7-86a5-d759bfb61f62"; const char kClaimTicketId[] = "RTcUE"; const char kAccessToken[] = "ya29.1.AADtN_V-dLUM-sVZ0qVjG9Dxm5NgdS9J" "Mx_JLUqhC9bED_YFjzHZtYt65ZzXCS35NMAeaVZ" "Dei530-w0yE2urpQ"; const char kRefreshToken[] = "1/zQmxR6PKNvhcxf9SjXUrCjcmCrcqRKXctc6cp" "1nI-GQ"; const char kRobotAccountAuthCode[] = "4/Mf_ujEhPejVhOq-OxW9F5cSOnWzx." "YgciVjTYGscRshQV0ieZDAqiTIjMigI"; const char kRobotAccountEmail[] = "6ed0b3f54f9bd619b942f4ad2441c252@" "clouddevices.gserviceaccount.com"; const char kAuthInfo[] = R"({ "localAuthInfo": { "certFingerprint": "FQY6BEINDjw3FgsmYChRWgMzMhc4TC8uG0UUUFhdDz0=", "localId": "f6885e46-b432-42d7-86a5-d759bfb61f62" } })"; } // namespace test_data std::string GetFormField(const std::string& data, const std::string& name) { EXPECT_FALSE(data.empty()); for (const auto& i : WebParamsDecode(data)) { if (i.first == name) return i.second; } return {}; } std::unique_ptr<HttpClient::Response> ReplyWithJson(int status_code, const base::Value& json) { std::string text; base::JSONWriter::WriteWithOptions( json, base::JSONWriter::OPTIONS_PRETTY_PRINT, &text); std::unique_ptr<MockHttpClientResponse> response{ new StrictMock<MockHttpClientResponse>}; EXPECT_CALL(*response, GetStatusCode()) .Times(AtLeast(1)) .WillRepeatedly(Return(status_code)); EXPECT_CALL(*response, GetContentType()) .Times(AtLeast(1)) .WillRepeatedly(Return(http::kJsonUtf8)); EXPECT_CALL(*response, GetData()) .Times(AtLeast(1)) .WillRepeatedly(Return(text)); return std::move(response); } std::pair<std::string, std::string> GetAuthHeader() { return {http::kAuthorization, std::string("Bearer ") + test_data::kAccessToken}; } std::pair<std::string, std::string> GetJsonHeader() { return {http::kContentType, http::kJsonUtf8}; } std::pair<std::string, std::string> GetFormHeader() { return {http::kContentType, http::kWwwFormUrlEncoded}; } } // anonymous namespace class DeviceRegistrationInfoTest : public ::testing::Test { protected: void SetUp() override { EXPECT_CALL(clock_, Now()) .WillRepeatedly(Return(base::Time::FromTimeT(1450000000))); ReloadDefaults(); } void ReloadDefaults() { EXPECT_CALL(config_store_, LoadDefaults(_)) .WillOnce(Invoke([](Settings* settings) { settings->client_id = test_data::kClientId; settings->client_secret = test_data::kClientSecret; settings->api_key = test_data::kApiKey; settings->oem_name = "Coffee Pot Maker"; settings->model_name = "Pot v1"; settings->name = "Coffee Pot"; settings->description = "Easy to clean"; settings->location = "Kitchen"; settings->local_anonymous_access_role = AuthScope::kViewer; settings->model_id = "AAAAA"; settings->oauth_url = test_data::kOAuthURL; settings->service_url = test_data::kServiceURL; settings->xmpp_endpoint = test_data::kXmppEndpoint; return true; })); config_.reset(new Config{&config_store_}); dev_reg_.reset(new DeviceRegistrationInfo{ config_.get(), &component_manager_, &task_runner_, &http_client_, nullptr, &auth_}); dev_reg_->Start(); } void ReloadSettings(bool registered = true) { base::DictionaryValue dict; dict.SetInteger("version", 1); if (registered) { dict.SetString("refresh_token", test_data::kRefreshToken); dict.SetString("cloud_id", test_data::kCloudId); dict.SetString("robot_account", test_data::kRobotAccountEmail); } dict.SetString("device_id", test_data::kDeviceId); std::string json_string; base::JSONWriter::WriteWithOptions( dict, base::JSONWriter::OPTIONS_PRETTY_PRINT, &json_string); EXPECT_CALL(config_store_, LoadSettings()).WillOnce(Return(json_string)); ReloadDefaults(); } void PublishCommands(const base::ListValue& commands) { dev_reg_->PublishCommands(commands, nullptr); } bool RefreshAccessToken(ErrorPtr* error) const { bool succeeded = false; auto callback = [&succeeded, &error](ErrorPtr in_error) { if (error) { *error = std::move(in_error); return; } succeeded = true; }; dev_reg_->RefreshAccessToken(base::Bind(callback)); return succeeded; } void SetAccessToken() { dev_reg_->access_token_ = test_data::kAccessToken; } GcdState GetGcdState() const { return dev_reg_->GetGcdState(); } bool HaveRegistrationCredentials() const { return dev_reg_->HaveRegistrationCredentials(); } provider::test::FakeTaskRunner task_runner_; provider::test::MockConfigStore config_store_; StrictMock<MockHttpClient> http_client_; base::DictionaryValue data_; std::unique_ptr<Config> config_; test::MockClock clock_; privet::AuthManager auth_{ {68, 52, 36, 95, 74, 89, 25, 2, 31, 5, 65, 87, 64, 32, 17, 26, 8, 73, 57, 16, 33, 82, 71, 10, 72, 62, 45, 1, 77, 97, 70, 24}, {21, 6, 58, 4, 66, 13, 14, 60, 55, 22, 11, 38, 96, 40, 81, 90, 3, 51, 50, 23, 56, 76, 47, 46, 27, 69, 20, 80, 88, 93, 15, 61}, {}, &clock_}; std::unique_ptr<DeviceRegistrationInfo> dev_reg_; ComponentManagerImpl component_manager_{&task_runner_}; }; TEST_F(DeviceRegistrationInfoTest, GetServiceURL) { EXPECT_EQ(test_data::kServiceURL, dev_reg_->GetServiceURL()); std::string url = test_data::kServiceURL; url += "registrationTickets"; EXPECT_EQ(url, dev_reg_->GetServiceURL("registrationTickets")); url += "?key="; url += test_data::kApiKey; EXPECT_EQ(url, dev_reg_->GetServiceURL("registrationTickets", {{"key", test_data::kApiKey}})); url += "&restart=true"; EXPECT_EQ(url, dev_reg_->GetServiceURL( "registrationTickets", { {"key", test_data::kApiKey}, {"restart", "true"}, })); } TEST_F(DeviceRegistrationInfoTest, GetOAuthURL) { EXPECT_EQ(test_data::kOAuthURL, dev_reg_->GetOAuthURL()); std::string url = test_data::kOAuthURL; url += "auth?redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&"; url += "response_type=code&"; url += "client_id="; url += test_data::kClientId; EXPECT_EQ(url, dev_reg_->GetOAuthURL( "auth", {{"redirect_uri", "urn:ietf:wg:oauth:2.0:oob"}, {"response_type", "code"}, {"client_id", test_data::kClientId}})); } TEST_F(DeviceRegistrationInfoTest, HaveRegistrationCredentials) { EXPECT_FALSE(HaveRegistrationCredentials()); ReloadSettings(); EXPECT_CALL( http_client_, SendRequest(HttpClient::Method::kPost, dev_reg_->GetOAuthURL("token"), HttpClient::Headers{GetFormHeader()}, _, _)) .WillOnce(WithArgs<3, 4>( Invoke([](const std::string& data, const HttpClient::SendRequestCallback& callback) { EXPECT_EQ("refresh_token", GetFormField(data, "grant_type")); EXPECT_EQ(test_data::kRefreshToken, GetFormField(data, "refresh_token")); EXPECT_EQ(test_data::kClientId, GetFormField(data, "client_id")); EXPECT_EQ(test_data::kClientSecret, GetFormField(data, "client_secret")); base::DictionaryValue json; json.SetString("access_token", test_data::kAccessToken); json.SetInteger("expires_in", 3600); callback.Run(ReplyWithJson(200, json), nullptr); }))); EXPECT_CALL( http_client_, SendRequest(HttpClient::Method::kPost, HasSubstr("upsertLocalAuthInfo"), HttpClient::Headers{GetAuthHeader(), GetJsonHeader()}, _, _)) .WillOnce(WithArgs<3, 4>( Invoke([](const std::string& data, const HttpClient::SendRequestCallback& callback) { auto dict = CreateDictionaryValue(data); EXPECT_TRUE(dict->Remove("localAuthInfo.clientToken", nullptr)); EXPECT_JSON_EQ(test_data::kAuthInfo, *dict); base::DictionaryValue json; callback.Run(ReplyWithJson(200, json), nullptr); }))); EXPECT_TRUE(RefreshAccessToken(nullptr)); EXPECT_TRUE(HaveRegistrationCredentials()); } TEST_F(DeviceRegistrationInfoTest, CheckAuthenticationFailure) { ReloadSettings(); EXPECT_EQ(GcdState::kConnecting, GetGcdState()); EXPECT_CALL( http_client_, SendRequest(HttpClient::Method::kPost, dev_reg_->GetOAuthURL("token"), HttpClient::Headers{GetFormHeader()}, _, _)) .WillOnce(WithArgs<3, 4>( Invoke([](const std::string& data, const HttpClient::SendRequestCallback& callback) { EXPECT_EQ("refresh_token", GetFormField(data, "grant_type")); EXPECT_EQ(test_data::kRefreshToken, GetFormField(data, "refresh_token")); EXPECT_EQ(test_data::kClientId, GetFormField(data, "client_id")); EXPECT_EQ(test_data::kClientSecret, GetFormField(data, "client_secret")); base::DictionaryValue json; json.SetString("error", "unable_to_authenticate"); callback.Run(ReplyWithJson(400, json), nullptr); }))); ErrorPtr error; EXPECT_FALSE(RefreshAccessToken(&error)); EXPECT_TRUE(error->HasError("unable_to_authenticate")); EXPECT_EQ(GcdState::kConnecting, GetGcdState()); } TEST_F(DeviceRegistrationInfoTest, CheckDeregistration) { ReloadSettings(); EXPECT_EQ(GcdState::kConnecting, GetGcdState()); EXPECT_CALL( http_client_, SendRequest(HttpClient::Method::kPost, dev_reg_->GetOAuthURL("token"), HttpClient::Headers{GetFormHeader()}, _, _)) .WillOnce(WithArgs<3, 4>( Invoke([](const std::string& data, const HttpClient::SendRequestCallback& callback) { EXPECT_EQ("refresh_token", GetFormField(data, "grant_type")); EXPECT_EQ(test_data::kRefreshToken, GetFormField(data, "refresh_token")); EXPECT_EQ(test_data::kClientId, GetFormField(data, "client_id")); EXPECT_EQ(test_data::kClientSecret, GetFormField(data, "client_secret")); base::DictionaryValue json; json.SetString("error", "invalid_grant"); callback.Run(ReplyWithJson(400, json), nullptr); }))); ErrorPtr error; EXPECT_FALSE(RefreshAccessToken(&error)); EXPECT_TRUE(error->HasError("invalid_grant")); EXPECT_EQ(GcdState::kInvalidCredentials, GetGcdState()); EXPECT_EQ(test_data::kCloudId, dev_reg_->GetSettings().cloud_id); } TEST_F(DeviceRegistrationInfoTest, GetDeviceInfo) { ReloadSettings(); SetAccessToken(); EXPECT_CALL( http_client_, SendRequest(HttpClient::Method::kGet, dev_reg_->GetDeviceURL(), HttpClient::Headers{GetAuthHeader(), GetJsonHeader()}, _, _)) .WillOnce(WithArgs<3, 4>( Invoke([](const std::string& data, const HttpClient::SendRequestCallback& callback) { base::DictionaryValue json; json.SetString("channel.supportedType", "xmpp"); json.SetString("deviceKind", "vendor"); json.SetString("id", test_data::kCloudId); json.SetString("kind", "weave#device"); callback.Run(ReplyWithJson(200, json), nullptr); }))); bool succeeded = false; auto callback = [&succeeded, this](const base::DictionaryValue& info, ErrorPtr error) { EXPECT_FALSE(error); std::string id; EXPECT_TRUE(info.GetString("id", &id)); EXPECT_EQ(test_data::kCloudId, id); succeeded = true; }; dev_reg_->GetDeviceInfo(base::Bind(callback)); EXPECT_TRUE(succeeded); } TEST_F(DeviceRegistrationInfoTest, RegisterDevice) { ReloadSettings(false); auto json_traits = CreateDictionaryValue(R"({ 'base': { 'commands': { 'reboot': { 'parameters': {'delay': {'minimum': 10, 'type': 'integer'}}, 'minimalRole': 'user' } }, 'state': { 'firmwareVersion': {'type': 'string'} } }, 'robot': { 'commands': { '_jump': { 'parameters': {'_height': {'type': 'integer'}}, 'minimalRole': 'user' } } } })"); EXPECT_TRUE(component_manager_.LoadTraits(*json_traits, nullptr)); EXPECT_TRUE( component_manager_.AddComponent("", "comp", {"base", "robot"}, nullptr)); base::StringValue ver{"1.0"}; EXPECT_TRUE(component_manager_.SetStateProperty( "comp", "base.firmwareVersion", ver, nullptr)); std::string ticket_url = dev_reg_->GetServiceURL("registrationTickets/") + test_data::kClaimTicketId; EXPECT_CALL(http_client_, SendRequest(HttpClient::Method::kPatch, ticket_url + "?key=" + test_data::kApiKey, HttpClient::Headers{GetJsonHeader()}, _, _)) .WillOnce(WithArgs<3, 4>( Invoke([](const std::string& data, const HttpClient::SendRequestCallback& callback) { auto json = test::CreateDictionaryValue(data); EXPECT_NE(nullptr, json.get()); std::string value; EXPECT_TRUE(json->GetString("id", &value)); EXPECT_EQ(test_data::kClaimTicketId, value); EXPECT_TRUE( json->GetString("deviceDraft.channel.supportedType", &value)); EXPECT_EQ("pull", value); EXPECT_TRUE(json->GetString("oauthClientId", &value)); EXPECT_EQ(test_data::kClientId, value); EXPECT_TRUE(json->GetString("deviceDraft.description", &value)); EXPECT_EQ("Easy to clean", value); EXPECT_TRUE(json->GetString("deviceDraft.location", &value)); EXPECT_EQ("Kitchen", value); EXPECT_TRUE(json->GetString("deviceDraft.modelManifestId", &value)); EXPECT_EQ("AAAAA", value); EXPECT_TRUE(json->GetString("deviceDraft.name", &value)); EXPECT_EQ("Coffee Pot", value); base::DictionaryValue* dict = nullptr; EXPECT_FALSE(json->GetDictionary("deviceDraft.commandDefs", &dict)); EXPECT_FALSE(json->GetDictionary("deviceDraft.state", &dict)); EXPECT_TRUE(json->GetDictionary("deviceDraft.traits", &dict)); auto expectedTraits = R"({ 'base': { 'commands': { 'reboot': { 'parameters': {'delay': {'minimum': 10, 'type': 'integer'}}, 'minimalRole': 'user' } }, 'state': { 'firmwareVersion': {'type': 'string'} } }, 'robot': { 'commands': { '_jump': { 'parameters': {'_height': {'type': 'integer'}}, 'minimalRole': 'user' } } } })"; EXPECT_JSON_EQ(expectedTraits, *dict); EXPECT_TRUE(json->GetDictionary("deviceDraft.components", &dict)); auto expectedComponents = R"({ 'comp': { 'traits': ['base', 'robot'], 'state': { 'base': { 'firmwareVersion': '1.0' } } } })"; EXPECT_JSON_EQ(expectedComponents, *dict); base::DictionaryValue json_resp; json_resp.SetString("id", test_data::kClaimTicketId); json_resp.SetString("kind", "weave#registrationTicket"); json_resp.SetString("oauthClientId", test_data::kClientId); base::DictionaryValue* device_draft = nullptr; EXPECT_TRUE(json->GetDictionary("deviceDraft", &device_draft)); device_draft = device_draft->DeepCopy(); device_draft->SetString("id", test_data::kCloudId); device_draft->SetString("kind", "weave#device"); json_resp.Set("deviceDraft", device_draft); callback.Run(ReplyWithJson(200, json_resp), nullptr); }))); EXPECT_CALL(http_client_, SendRequest(HttpClient::Method::kPost, ticket_url + "/finalize?key=" + test_data::kApiKey, HttpClient::Headers{}, _, _)) .WillOnce(WithArgs<4>( Invoke([](const HttpClient::SendRequestCallback& callback) { base::DictionaryValue json; json.SetString("id", test_data::kClaimTicketId); json.SetString("kind", "weave#registrationTicket"); json.SetString("oauthClientId", test_data::kClientId); json.SetString("userEmail", "user@email.com"); json.SetString("deviceDraft.id", test_data::kCloudId); json.SetString("deviceDraft.kind", "weave#device"); json.SetString("deviceDraft.channel.supportedType", "xmpp"); json.SetString("robotAccountEmail", test_data::kRobotAccountEmail); json.SetString("robotAccountAuthorizationCode", test_data::kRobotAccountAuthCode); callback.Run(ReplyWithJson(200, json), nullptr); }))); EXPECT_CALL( http_client_, SendRequest(HttpClient::Method::kPost, dev_reg_->GetOAuthURL("token"), HttpClient::Headers{GetFormHeader()}, _, _)) .WillOnce(WithArgs<3, 4>(Invoke([]( const std::string& data, const HttpClient::SendRequestCallback& callback) { EXPECT_EQ("authorization_code", GetFormField(data, "grant_type")); EXPECT_EQ(test_data::kRobotAccountAuthCode, GetFormField(data, "code")); EXPECT_EQ(test_data::kClientId, GetFormField(data, "client_id")); EXPECT_EQ(test_data::kClientSecret, GetFormField(data, "client_secret")); EXPECT_EQ("oob", GetFormField(data, "redirect_uri")); base::DictionaryValue json; json.SetString("access_token", test_data::kAccessToken); json.SetString("token_type", "Bearer"); json.SetString("refresh_token", test_data::kRefreshToken); json.SetInteger("expires_in", 3600); callback.Run(ReplyWithJson(200, json), nullptr); }))); EXPECT_CALL( http_client_, SendRequest(HttpClient::Method::kPost, HasSubstr("upsertLocalAuthInfo"), HttpClient::Headers{GetAuthHeader(), GetJsonHeader()}, _, _)) .WillOnce(WithArgs<3, 4>( Invoke([](const std::string& data, const HttpClient::SendRequestCallback& callback) { auto dict = CreateDictionaryValue(data); EXPECT_TRUE(dict->Remove("localAuthInfo.clientToken", nullptr)); EXPECT_JSON_EQ(test_data::kAuthInfo, *dict); base::DictionaryValue json; callback.Run(ReplyWithJson(200, json), nullptr); }))); bool done = false; dev_reg_->RegisterDevice( test_data::kClaimTicketId, base::Bind([this, &done](ErrorPtr error) { EXPECT_FALSE(error); done = true; task_runner_.Break(); EXPECT_EQ(GcdState::kConnecting, GetGcdState()); // Validate the device info saved to storage... EXPECT_EQ(test_data::kCloudId, dev_reg_->GetSettings().cloud_id); EXPECT_EQ(test_data::kRefreshToken, dev_reg_->GetSettings().refresh_token); EXPECT_EQ(test_data::kRobotAccountEmail, dev_reg_->GetSettings().robot_account); })); task_runner_.Run(); EXPECT_TRUE(done); } TEST_F(DeviceRegistrationInfoTest, ReRegisterDevice) { ReloadSettings(); bool done = false; dev_reg_->RegisterDevice( test_data::kClaimTicketId, base::Bind([this, &done](ErrorPtr error) { EXPECT_TRUE(error->HasError("already_registered")); done = true; task_runner_.Break(); EXPECT_EQ(GcdState::kConnecting, GetGcdState()); // Validate the device info saved to storage... EXPECT_EQ(test_data::kCloudId, dev_reg_->GetSettings().cloud_id); EXPECT_EQ(test_data::kRefreshToken, dev_reg_->GetSettings().refresh_token); EXPECT_EQ(test_data::kRobotAccountEmail, dev_reg_->GetSettings().robot_account); })); task_runner_.Run(); EXPECT_TRUE(done); } TEST_F(DeviceRegistrationInfoTest, OOBRegistrationStatus) { // After we've been initialized, we should be either offline or // unregistered, depending on whether or not we've found credentials. EXPECT_EQ(GcdState::kUnconfigured, GetGcdState()); // Put some credentials into our state, make sure we call that offline. ReloadSettings(); EXPECT_EQ(GcdState::kConnecting, GetGcdState()); } class DeviceRegistrationInfoUpdateCommandTest : public DeviceRegistrationInfoTest { protected: void SetUp() override { DeviceRegistrationInfoTest::SetUp(); ReloadSettings(); SetAccessToken(); auto json_traits = CreateDictionaryValue(R"({ 'robot': { 'commands': { '_jump': { 'parameters': {'_height': 'integer'}, 'progress': {'progress': 'integer'}, 'results': {'status': 'string'}, 'minimalRole': 'user' } } } })"); EXPECT_TRUE(component_manager_.LoadTraits(*json_traits, nullptr)); EXPECT_TRUE( component_manager_.AddComponent("", "comp", {"robot"}, nullptr)); command_url_ = dev_reg_->GetServiceURL("commands/1234"); auto commands_json = CreateValue(R"([{ 'name':'robot._jump', 'component': 'comp', 'id':'1234', 'parameters': {'_height': 100}, 'minimalRole': 'user' }])"); ASSERT_NE(nullptr, commands_json.get()); const base::ListValue* command_list = nullptr; ASSERT_TRUE(commands_json->GetAsList(&command_list)); PublishCommands(*command_list); command_ = component_manager_.FindCommand("1234"); ASSERT_NE(nullptr, command_); } void TearDown() override { task_runner_.RunOnce(); DeviceRegistrationInfoTest::TearDown(); } Command* command_{nullptr}; std::string command_url_; }; TEST_F(DeviceRegistrationInfoUpdateCommandTest, SetProgress) { EXPECT_CALL( http_client_, SendRequest(HttpClient::Method::kPatch, command_url_, HttpClient::Headers{GetAuthHeader(), GetJsonHeader()}, _, _)) .WillOnce(WithArgs<3, 4>(Invoke([]( const std::string& data, const HttpClient::SendRequestCallback& callback) { EXPECT_JSON_EQ((R"({"state":"inProgress","progress":{"progress":18}})"), *CreateDictionaryValue(data)); base::DictionaryValue json; callback.Run(ReplyWithJson(200, json), nullptr); }))); EXPECT_TRUE(command_->SetProgress(*CreateDictionaryValue("{'progress':18}"), nullptr)); } TEST_F(DeviceRegistrationInfoUpdateCommandTest, Complete) { EXPECT_CALL( http_client_, SendRequest(HttpClient::Method::kPatch, command_url_, HttpClient::Headers{GetAuthHeader(), GetJsonHeader()}, _, _)) .WillOnce(WithArgs<3, 4>( Invoke([](const std::string& data, const HttpClient::SendRequestCallback& callback) { EXPECT_JSON_EQ(R"({"state":"done", "results":{"status":"Ok"}})", *CreateDictionaryValue(data)); base::DictionaryValue json; callback.Run(ReplyWithJson(200, json), nullptr); }))); EXPECT_TRUE( command_->Complete(*CreateDictionaryValue("{'status': 'Ok'}"), nullptr)); } TEST_F(DeviceRegistrationInfoUpdateCommandTest, Cancel) { EXPECT_CALL( http_client_, SendRequest(HttpClient::Method::kPatch, command_url_, HttpClient::Headers{GetAuthHeader(), GetJsonHeader()}, _, _)) .WillOnce(WithArgs<3, 4>( Invoke([](const std::string& data, const HttpClient::SendRequestCallback& callback) { EXPECT_JSON_EQ(R"({"state":"cancelled"})", *CreateDictionaryValue(data)); base::DictionaryValue json; callback.Run(ReplyWithJson(200, json), nullptr); }))); EXPECT_TRUE(command_->Cancel(nullptr)); } } // namespace weave