buffet: Added unit tests for DeviceRegistrationInfo class Added unit tests for GCD registration workflow in Buffet. BUG=chromium:367381 TEST=Unit tests pass (old and new). Change-Id: Ia3ad5f028ae6fc7f3d2acdf4648ceb88cc4e00ef Reviewed-on: https://chromium-review.googlesource.com/197568 Tested-by: Alex Vakulenko <avakulenko@chromium.org> Reviewed-by: Christopher Wiley <wiley@chromium.org> Commit-Queue: Alex Vakulenko <avakulenko@chromium.org>
diff --git a/buffet/buffet.gyp b/buffet/buffet.gyp index 2288162..c4c5a1b 100644 --- a/buffet/buffet.gyp +++ b/buffet/buffet.gyp
@@ -76,6 +76,7 @@ 'async_event_sequencer_unittest.cc', 'buffet_testrunner.cc', 'data_encoding_unittest.cc', + 'device_registration_info_unittest.cc', 'exported_property_set_unittest.cc', 'http_connection_fake.cc', 'http_transport_fake.cc',
diff --git a/buffet/device_registration_info.cc b/buffet/device_registration_info.cc index de89aff..c9aea9f 100644 --- a/buffet/device_registration_info.cc +++ b/buffet/device_registration_info.cc
@@ -4,13 +4,15 @@ #include "buffet/device_registration_info.h" +#include <base/file_util.h> +#include <base/files/important_file_writer.h> #include <base/json/json_reader.h> #include <base/json/json_writer.h> #include <base/values.h> -#include <base/file_util.h> #include <memory> #include "buffet/data_encoding.h" +#include "buffet/device_registration_storage_keys.h" #include "buffet/http_transport_curl.h" #include "buffet/http_utils.h" #include "buffet/mime_utils.h" @@ -20,7 +22,9 @@ using namespace chromeos; using namespace chromeos::data_encoding; -namespace { +namespace buffet { +namespace storage_keys { + // Persistent keys const char kClientId[] = "client_id"; const char kClientSecret[] = "client_secret"; @@ -35,6 +39,11 @@ const char kSystemName[] = "system_name"; const char kDisplayName[] = "display_name"; +} // namespace storage_keys +} // namespace buffet + +namespace { + const base::FilePath::CharType kDeviceInfoFilePath[] = FILE_PATH_LITERAL("/var/lib/buffet/device_reg_info"); @@ -85,21 +94,52 @@ return url::AppendQueryParams(result, params); } +class FileStorage : public buffet::DeviceRegistrationInfo::StorageInterface { + public: + virtual std::unique_ptr<base::Value> Load() override { + // TODO(avakulenko): Figure out security implications of storing + // this data unencrypted. + std::string json; + if (!base::ReadFileToString(GetFilePath(), &json)) + return std::unique_ptr<base::Value>(); -} // anonymous namespace + return std::unique_ptr<base::Value>(base::JSONReader::Read(json)); + } + + virtual bool Save(const base::Value* config) { + // TODO(avakulenko): Figure out security implications of storing + // this data unencrypted. + std::string json; + base::JSONWriter::WriteWithOptions(config, + base::JSONWriter::OPTIONS_PRETTY_PRINT, + &json); + return base::ImportantFileWriter::WriteFileAtomically(GetFilePath(), json); + } + + private: + base::FilePath GetFilePath() const{ + return base::FilePath(kDeviceInfoFilePath); + } +}; + +} // anonymous namespace namespace buffet { + DeviceRegistrationInfo::DeviceRegistrationInfo() - : transport_(new http::curl::Transport()){ + : transport_(new http::curl::Transport()), + storage_(new FileStorage()) { } DeviceRegistrationInfo::DeviceRegistrationInfo( - std::shared_ptr<http::Transport> transport) : transport_(transport) { + std::shared_ptr<http::Transport> transport, + std::shared_ptr<StorageInterface> storage) : transport_(transport), + storage_(storage) { } std::pair<std::string, std::string> DeviceRegistrationInfo::GetAuthorizationHeader() const { - return BuildAuthHeader(/*"Bearer"*/"OAuth", access_token_); + return BuildAuthHeader("Bearer", access_token_); } std::string DeviceRegistrationInfo::GetServiceURL( @@ -123,13 +163,7 @@ } bool DeviceRegistrationInfo::Load() { - // TODO(avakulenko): Figure out security implications of storing - // this data unencrypted. - std::string json; - if (!base::ReadFileToString(base::FilePath(kDeviceInfoFilePath), &json)) - return false; - - auto value = std::unique_ptr<base::Value>(base::JSONReader::Read(json)); + auto value = storage_->Load(); const base::DictionaryValue* dict = nullptr; if (!value || !value->GetAsDictionary(&dict)) return false; @@ -137,28 +171,28 @@ // Get the values into temp variables first to make sure we can get // all the data correctly before changing the state of this object. std::string client_id; - if (!dict->GetString(kClientId, &client_id)) + if (!dict->GetString(storage_keys::kClientId, &client_id)) return false; std::string client_secret; - if (!dict->GetString(kClientSecret, &client_secret)) + if (!dict->GetString(storage_keys::kClientSecret, &client_secret)) return false; std::string api_key; - if (!dict->GetString(kApiKey, &api_key)) + if (!dict->GetString(storage_keys::kApiKey, &api_key)) return false; std::string refresh_token; - if (!dict->GetString(kRefreshToken, &refresh_token)) + if (!dict->GetString(storage_keys::kRefreshToken, &refresh_token)) return false; std::string device_id; - if (!dict->GetString(kDeviceId, &device_id)) + if (!dict->GetString(storage_keys::kDeviceId, &device_id)) return false; std::string oauth_url; - if (!dict->GetString(kOAuthURL, &oauth_url)) + if (!dict->GetString(storage_keys::kOAuthURL, &oauth_url)) return false; std::string service_url; - if (!dict->GetString(kServiceURL, &service_url)) + if (!dict->GetString(storage_keys::kServiceURL, &service_url)) return false; std::string device_robot_account; - if (!dict->GetString(kRobotAccount, &device_robot_account)) + if (!dict->GetString(storage_keys::kRobotAccount, &device_robot_account)) return false; client_id_ = client_id; @@ -173,26 +207,16 @@ } bool DeviceRegistrationInfo::Save() const { - // TODO(avakulenko): Figure out security implications of storing - // this data unencrypted. base::DictionaryValue dict; - dict.SetString(kClientId, client_id_); - dict.SetString(kClientSecret, client_secret_); - dict.SetString(kApiKey, api_key_); - dict.SetString(kRefreshToken, refresh_token_); - dict.SetString(kDeviceId, device_id_); - dict.SetString(kOAuthURL, oauth_url_); - dict.SetString(kServiceURL, service_url_); - dict.SetString(kRobotAccount, device_robot_account_); - - std::string json; - base::JSONWriter::WriteWithOptions(&dict, - base::JSONWriter::OPTIONS_PRETTY_PRINT, - &json); - int count = file_util::WriteFile(base::FilePath(kDeviceInfoFilePath), - json.data(), static_cast<int>(json.size())); - - return (count > 0); + dict.SetString(storage_keys::kClientId, client_id_); + dict.SetString(storage_keys::kClientSecret, client_secret_); + dict.SetString(storage_keys::kApiKey, api_key_); + dict.SetString(storage_keys::kRefreshToken, refresh_token_); + dict.SetString(storage_keys::kDeviceId, device_id_); + dict.SetString(storage_keys::kOAuthURL, oauth_url_); + dict.SetString(storage_keys::kServiceURL, service_url_); + dict.SetString(storage_keys::kRobotAccount, device_robot_account_); + return storage_->Save(&dict); } bool DeviceRegistrationInfo::CheckRegistration() { @@ -209,7 +233,7 @@ } bool DeviceRegistrationInfo::ValidateAndRefreshAccessToken() { - LOG(INFO) << " Checking access token expiration."; + LOG(INFO) << "Checking access token expiration."; if (!access_token_.empty() && !access_token_expiration_.is_null() && access_token_expiration_ > base::Time::Now()) { @@ -284,29 +308,29 @@ std::string DeviceRegistrationInfo::StartRegistration( const std::map<std::string, std::shared_ptr<base::Value>>& params, std::string* error_msg) { - GetParamValue(params, kClientId, &client_id_); - GetParamValue(params, kClientSecret, &client_secret_); - GetParamValue(params, kApiKey, &api_key_); - GetParamValue(params, kDeviceId, &device_id_); - GetParamValue(params, kDeviceKind, &device_kind_); - GetParamValue(params, kSystemName, &system_name_); - GetParamValue(params, kDisplayName, &display_name_); - GetParamValue(params, kOAuthURL, &oauth_url_); - GetParamValue(params, kServiceURL, &service_url_); + GetParamValue(params, storage_keys::kClientId, &client_id_); + GetParamValue(params, storage_keys::kClientSecret, &client_secret_); + GetParamValue(params, storage_keys::kApiKey, &api_key_); + GetParamValue(params, storage_keys::kDeviceId, &device_id_); + GetParamValue(params, storage_keys::kDeviceKind, &device_kind_); + GetParamValue(params, storage_keys::kSystemName, &system_name_); + GetParamValue(params, storage_keys::kDisplayName, &display_name_); + GetParamValue(params, storage_keys::kOAuthURL, &oauth_url_); + GetParamValue(params, storage_keys::kServiceURL, &service_url_); - if (!CheckParam(kClientId, client_id_, error_msg)) + if (!CheckParam(storage_keys::kClientId, client_id_, error_msg)) return std::string(); - if (!CheckParam(kClientSecret, client_secret_, error_msg)) + if (!CheckParam(storage_keys::kClientSecret, client_secret_, error_msg)) return std::string(); - if (!CheckParam(kApiKey, api_key_, error_msg)) + if (!CheckParam(storage_keys::kApiKey, api_key_, error_msg)) return std::string(); - if (!CheckParam(kDeviceKind, device_kind_, error_msg)) + if (!CheckParam(storage_keys::kDeviceKind, device_kind_, error_msg)) return std::string(); - if (!CheckParam(kSystemName, system_name_, error_msg)) + if (!CheckParam(storage_keys::kSystemName, system_name_, error_msg)) return std::string(); - if (!CheckParam(kOAuthURL, oauth_url_, error_msg)) + if (!CheckParam(storage_keys::kOAuthURL, oauth_url_, error_msg)) return std::string(); - if (!CheckParam(kServiceURL, service_url_, error_msg)) + if (!CheckParam(storage_keys::kServiceURL, service_url_, error_msg)) return std::string(); std::vector<std::pair<std::string, std::vector<std::string>>> commands = { @@ -416,18 +440,9 @@ std::string auth_code; url += "/finalize?key=" + api_key_; - do { - LOG(INFO) << "Sending request to: " << url; - response = http::PostBinary(url, nullptr, 0, transport_); - if (response) { - if (response->GetStatusCode() == http::status_code::BadRequest) - sleep(1); - } - } - while (response && - response->GetStatusCode() == http::status_code::BadRequest); - if (response && - response->GetStatusCode() == http::status_code::Ok) { + LOG(INFO) << "Sending request to: " << url; + response = http::PostBinary(url, nullptr, 0, transport_); + if (response && response->IsSuccessful()) { auto json_resp = http::ParseJsonResponse(response.get(), nullptr, nullptr); if (json_resp && json_resp->GetString("robotAccountEmail", &device_robot_account_) && @@ -463,8 +478,9 @@ Save(); } + return true; } - return true; + return false; } } // namespace buffet
diff --git a/buffet/device_registration_info.h b/buffet/device_registration_info.h index 6eea8eb..c60cf69 100644 --- a/buffet/device_registration_info.h +++ b/buffet/device_registration_info.h
@@ -22,17 +22,33 @@ namespace buffet { // The DeviceRegistrationInfo class represents device registration information. - class DeviceRegistrationInfo { +class DeviceRegistrationInfo { public: - // Default-constructed uses CURL HTTP transport. - DeviceRegistrationInfo(); - // This constructor allows to pass in a custom HTTP transport - // (mainly for testing). - DeviceRegistrationInfo(std::shared_ptr<chromeos::http::Transport> transport); + // The device registration configuration storage interface. + struct StorageInterface { + virtual ~StorageInterface() = default; + // Load the device registration configuration from storage. + // If it fails (e.g. the storage container [file?] doesn't exist), then + // it returns empty unique_ptr (aka nullptr). + virtual std::unique_ptr<base::Value> Load() = 0; + // Save the device registration configuration to storage. + // If saved successfully, returns true. Could fail when writing to + // physical storage like file system for various reasons (out of disk space, + // access permissions, etc). + virtual bool Save(const base::Value* config) = 0; + }; + // This is a helper class for unit testing. + class TestHelper; + // Default-constructed uses CURL HTTP transport. + DeviceRegistrationInfo(); + // This constructor allows to pass in a custom HTTP transport + // (mainly for testing). + DeviceRegistrationInfo(std::shared_ptr<chromeos::http::Transport> transport, + std::shared_ptr<StorageInterface> storage); // Returns the authorization HTTP header that can be used to talk // to GCD server for authenticated device communication. - // Make sure CheckRegistration() is called before this call. + // Make sure ValidateAndRefreshAccessToken() is called before this call. std::pair<std::string, std::string> GetAuthorizationHeader() const; // Returns the GCD service request URL. If |subpath| is specified, it is @@ -122,7 +138,10 @@ // HTTP transport used for communications. std::shared_ptr<chromeos::http::Transport> transport_; + // Serialization interface to save and load device registration info. + std::shared_ptr<StorageInterface> storage_; + friend class TestHelper; DISALLOW_COPY_AND_ASSIGN(DeviceRegistrationInfo); };
diff --git a/buffet/device_registration_info_unittest.cc b/buffet/device_registration_info_unittest.cc new file mode 100644 index 0000000..2357904 --- /dev/null +++ b/buffet/device_registration_info_unittest.cc
@@ -0,0 +1,409 @@ +// Copyright 2014 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 <base/json/json_reader.h> +#include <base/values.h> +#include <gtest/gtest.h> + +#include "buffet/bind_lambda.h" +#include "buffet/device_registration_info.h" +#include "buffet/device_registration_storage_keys.h" +#include "buffet/http_request.h" +#include "buffet/http_transport_fake.h" +#include "buffet/mime_utils.h" + +using namespace buffet; +using namespace chromeos; +using namespace chromeos::http; + +namespace { +// StorageInterface for testing. Just stores the values in memory. +class MemStorage : public DeviceRegistrationInfo::StorageInterface { + public: + virtual std::unique_ptr<base::Value> Load() override { + return std::unique_ptr<base::Value>(cache_->DeepCopy()); + } + + virtual bool Save(const base::Value* config) { + cache_.reset(config->DeepCopy()); + ++save_count_; + return true; + } + + int save_count_ = 0; + +private: + std::unique_ptr<base::Value> cache_; +}; + +namespace test_data { + +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 kDeviceId[] = "4a7ea2d1-b331-1e1f-b206-e863c7635196"; +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 kUserAccountAuthCode[] = "2/sd_GD1TGFKpJOLJ34-0g5fK0fflp.GlT" + "I0F5g7hNtFgj5HFGOf8FlGK9eflO"; +const char kUserAccessToken[] = "sd56.4.FGDjG_F-gFGF-dFG6gGOG9Dxm5NgdS9" + "JMx_JLUqhC9bED_YFjLKjlkjLKJlkjLKjlKJea" + "VZDei530-w0yE2urpQ"; +const char kUserRefreshToken[] = "1/zQLKjlKJlkLkLKjLkjLKjLkjLjLkjl0ftc6" + "cp1nI-GQ"; + +} // namespace test_data + +// Fill in the storage with default environment information (URLs, etc). +void InitDefaultStorage(base::DictionaryValue* data) { + data->SetString(storage_keys::kClientId, test_data::kClientId); + data->SetString(storage_keys::kClientSecret, test_data::kClientSecret); + data->SetString(storage_keys::kApiKey, test_data::kApiKey); + data->SetString(storage_keys::kRefreshToken, ""); + data->SetString(storage_keys::kDeviceId, ""); + data->SetString(storage_keys::kOAuthURL, test_data::kOAuthURL); + data->SetString(storage_keys::kServiceURL, test_data::kServiceURL); + data->SetString(storage_keys::kRobotAccount, ""); +} + +// Add the test device registration information. +void SetDefaultDeviceRegistration(base::DictionaryValue* data) { + data->SetString(storage_keys::kRefreshToken, test_data::kRefreshToken); + data->SetString(storage_keys::kDeviceId, test_data::kDeviceId); + data->SetString(storage_keys::kRobotAccount, test_data::kRobotAccountEmail); +} + +void OAuth2Handler(const fake::ServerRequest& request, + fake::ServerResponse* response) { + base::DictionaryValue json; + if (request.GetFormField("grant_type") == "refresh_token") { + // Refresh device access token. + EXPECT_EQ(test_data::kRefreshToken, request.GetFormField("refresh_token")); + EXPECT_EQ(test_data::kClientId, request.GetFormField("client_id")); + EXPECT_EQ(test_data::kClientSecret, request.GetFormField("client_secret")); + json.SetString("access_token", test_data::kAccessToken); + } else if (request.GetFormField("grant_type") == "authorization_code") { + // Obtain access token. + std::string code = request.GetFormField("code"); + if (code == test_data::kUserAccountAuthCode) { + // Get user access token. + EXPECT_EQ(test_data::kClientId, request.GetFormField("client_id")); + EXPECT_EQ(test_data::kClientSecret, + request.GetFormField("client_secret")); + EXPECT_EQ("urn:ietf:wg:oauth:2.0:oob", + request.GetFormField("redirect_uri")); + json.SetString("access_token", test_data::kUserAccessToken); + json.SetString("token_type", "Bearer"); + json.SetString("refresh_token", test_data::kUserRefreshToken); + } else if (code == test_data::kRobotAccountAuthCode) { + // Get device access token. + EXPECT_EQ(test_data::kClientId, request.GetFormField("client_id")); + EXPECT_EQ(test_data::kClientSecret, + request.GetFormField("client_secret")); + EXPECT_EQ("oob", request.GetFormField("redirect_uri")); + EXPECT_EQ("https://www.googleapis.com/auth/clouddevices", + request.GetFormField("scope")); + json.SetString("access_token", test_data::kAccessToken); + json.SetString("token_type", "Bearer"); + json.SetString("refresh_token", test_data::kRefreshToken); + } else { + ASSERT_TRUE(false); // Unexpected authorization code. + } + } else { + ASSERT_TRUE(false); // Unexpected grant type. + } + json.SetInteger("expires_in", 3600); + response->ReplyJson(status_code::Ok, &json); +} + +void DeviceInfoHandler(const fake::ServerRequest& request, + fake::ServerResponse* response) { + std::string auth = "Bearer "; + auth += test_data::kAccessToken; + EXPECT_EQ(auth, request.GetHeader(http::request_header::kAuthorization)); + response->ReplyJson(status_code::Ok, { + {"channel.supportedType", "xmpp"}, + {"deviceKind", "vendor"}, + {"id", test_data::kDeviceId}, + {"kind", "clouddevices#device"}, + }); +} + +void FinalizeTicketHandler(const fake::ServerRequest& request, + fake::ServerResponse* response) { + EXPECT_EQ(test_data::kApiKey, request.GetFormField("key")); + EXPECT_TRUE(request.GetData().empty()); + + response->ReplyJson(status_code::Ok, { + {"id", test_data::kClaimTicketId}, + {"kind", "clouddevices#registrationTicket"}, + {"oauthClientId", test_data::kClientId}, + {"userEmail", "user@email.com"}, + {"deviceDraft.id", test_data::kDeviceId}, + {"deviceDraft.kind", "clouddevices#device"}, + {"deviceDraft.channel.supportedType", "xmpp"}, + {"robotAccountEmail", test_data::kRobotAccountEmail}, + {"robotAccountAuthorizationCode", test_data::kRobotAccountAuthCode}, + }); +} + +} // anonymous namespace + +// This is a helper class that allows the unit tests to set the private +// member DeviceRegistrationInfo::ticket_id_, since TestHelper is declared +// as a friend to DeviceRegistrationInfo. +class DeviceRegistrationInfo::TestHelper { + public: + static void SetTestTicketId(DeviceRegistrationInfo* info) { + info->ticket_id_ = test_data::kClaimTicketId; + } +}; + +class DeviceRegistrationInfoTest : public ::testing::Test { + protected: + virtual void SetUp() override { + InitDefaultStorage(&data); + storage = std::make_shared<MemStorage>(); + storage->Save(&data); + transport = std::make_shared<fake::Transport>(); + dev_reg = std::unique_ptr<DeviceRegistrationInfo>( + new DeviceRegistrationInfo(transport, storage)); + } + + base::DictionaryValue data; + std::shared_ptr<MemStorage> storage; + std::shared_ptr<fake::Transport> transport; + std::unique_ptr<DeviceRegistrationInfo> dev_reg; +}; + +//////////////////////////////////////////////////////////////////////////////// +TEST_F(DeviceRegistrationInfoTest, GetServiceURL) { + EXPECT_TRUE(dev_reg->Load()); + 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_TRUE(dev_reg->Load()); + EXPECT_EQ(test_data::kOAuthURL, dev_reg->GetOAuthURL()); + std::string url = test_data::kOAuthURL; + url += "auth?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fclouddevices&"; + url += "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", { + {"scope", "https://www.googleapis.com/auth/clouddevices"}, + {"redirect_uri", "urn:ietf:wg:oauth:2.0:oob"}, + {"response_type", "code"}, + {"client_id", test_data::kClientId} + })); +} + +TEST_F(DeviceRegistrationInfoTest, CheckRegistration) { + EXPECT_TRUE(dev_reg->Load()); + EXPECT_FALSE(dev_reg->CheckRegistration()); + EXPECT_EQ(0, transport->GetRequestCount()); + + SetDefaultDeviceRegistration(&data); + storage->Save(&data); + EXPECT_TRUE(dev_reg->Load()); + + transport->AddHandler(dev_reg->GetOAuthURL("token"), request_type::kPost, + base::Bind(OAuth2Handler)); + transport->ResetRequestCount(); + EXPECT_TRUE(dev_reg->CheckRegistration()); + EXPECT_EQ(1, transport->GetRequestCount()); +} + +TEST_F(DeviceRegistrationInfoTest, GetDeviceInfo) { + SetDefaultDeviceRegistration(&data); + storage->Save(&data); + EXPECT_TRUE(dev_reg->Load()); + + transport->AddHandler(dev_reg->GetOAuthURL("token"), request_type::kPost, + base::Bind(OAuth2Handler)); + transport->AddHandler(dev_reg->GetDeviceURL(), request_type::kGet, + base::Bind(DeviceInfoHandler)); + transport->ResetRequestCount(); + auto device_info = dev_reg->GetDeviceInfo(); + EXPECT_EQ(2, transport->GetRequestCount()); + EXPECT_NE(nullptr, device_info.get()); + base::DictionaryValue* dict = nullptr; + EXPECT_TRUE(device_info->GetAsDictionary(&dict)); + std::string id; + EXPECT_TRUE(dict->GetString("id", &id)); + EXPECT_EQ(test_data::kDeviceId, id); +} + +TEST_F(DeviceRegistrationInfoTest, GetDeviceId) { + SetDefaultDeviceRegistration(&data); + storage->Save(&data); + EXPECT_TRUE(dev_reg->Load()); + + transport->AddHandler(dev_reg->GetOAuthURL("token"), request_type::kPost, + base::Bind(OAuth2Handler)); + transport->AddHandler(dev_reg->GetDeviceURL(), request_type::kGet, + base::Bind(DeviceInfoHandler)); + std::string id = dev_reg->GetDeviceId(); + EXPECT_EQ(test_data::kDeviceId, id); +} + +TEST_F(DeviceRegistrationInfoTest, StartRegistration) { + EXPECT_TRUE(dev_reg->Load()); + + auto create_ticket = [](const fake::ServerRequest& request, + fake::ServerResponse* response) { + EXPECT_EQ(test_data::kApiKey, request.GetFormField("key")); + auto json = request.GetDataAsJson(); + EXPECT_NE(nullptr, json.get()); + std::string value; + EXPECT_TRUE(json->GetString("deviceDraft.channel.supportedType", &value)); + EXPECT_EQ("xmpp", value); + EXPECT_TRUE(json->GetString("oauthClientId", &value)); + EXPECT_EQ(test_data::kClientId, value); + EXPECT_TRUE(json->GetString("deviceDraft.deviceKind", &value)); + EXPECT_EQ("vendor", value); + + base::DictionaryValue json_resp; + json_resp.SetString("id", test_data::kClaimTicketId); + json_resp.SetString("kind", "clouddevices#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::kDeviceId); + device_draft->SetString("kind", "clouddevices#device"); + json_resp.Set("deviceDraft", device_draft); + + response->ReplyJson(status_code::Ok, &json_resp); + }; + + transport->AddHandler(dev_reg->GetServiceURL("registrationTickets"), + request_type::kPost, + base::Bind(create_ticket)); + std::map<std::string, std::shared_ptr<base::Value>> params; + std::string json_resp = dev_reg->StartRegistration(params, nullptr); + auto json = std::unique_ptr<base::Value>(base::JSONReader::Read(json_resp)); + EXPECT_NE(nullptr, json.get()); + base::DictionaryValue* dict = nullptr; + EXPECT_TRUE(json->GetAsDictionary(&dict)); + std::string value; + EXPECT_TRUE(dict->GetString("ticket_id", &value)); + EXPECT_EQ(test_data::kClaimTicketId, value); +} + +TEST_F(DeviceRegistrationInfoTest, FinishRegistration_NoAuth) { + // Test finalizing ticket with no user authorization token. + // This assumes that a client would patch in their email separately. + EXPECT_TRUE(dev_reg->Load()); + + // General ticket finalization handler. + std::string ticket_url = + dev_reg->GetServiceURL("registrationTickets/" + + std::string(test_data::kClaimTicketId)); + transport->AddHandler(ticket_url + "/finalize", request_type::kPost, + base::Bind(FinalizeTicketHandler)); + + transport->AddHandler(dev_reg->GetOAuthURL("token"), request_type::kPost, + base::Bind(OAuth2Handler)); + + storage->save_count_ = 0; + DeviceRegistrationInfo::TestHelper::SetTestTicketId(dev_reg.get()); + EXPECT_TRUE(dev_reg->FinishRegistration("")); + EXPECT_EQ(1, storage->save_count_); // The device info must have been saved. + EXPECT_EQ(2, transport->GetRequestCount()); + + // Validate the device info saved to storage... + auto storage_data = storage->Load(); + base::DictionaryValue* dict = nullptr; + EXPECT_TRUE(storage_data->GetAsDictionary(&dict)); + std::string value; + EXPECT_TRUE(dict->GetString(storage_keys::kApiKey, &value)); + EXPECT_EQ(test_data::kApiKey, value); + EXPECT_TRUE(dict->GetString(storage_keys::kClientId, &value)); + EXPECT_EQ(test_data::kClientId, value); + EXPECT_TRUE(dict->GetString(storage_keys::kClientSecret, &value)); + EXPECT_EQ(test_data::kClientSecret, value); + EXPECT_TRUE(dict->GetString(storage_keys::kDeviceId, &value)); + EXPECT_EQ(test_data::kDeviceId, value); + EXPECT_TRUE(dict->GetString(storage_keys::kOAuthURL, &value)); + EXPECT_EQ(test_data::kOAuthURL, value); + EXPECT_TRUE(dict->GetString(storage_keys::kRefreshToken, &value)); + EXPECT_EQ(test_data::kRefreshToken, value); + EXPECT_TRUE(dict->GetString(storage_keys::kRobotAccount, &value)); + EXPECT_EQ(test_data::kRobotAccountEmail, value); + EXPECT_TRUE(dict->GetString(storage_keys::kServiceURL, &value)); + EXPECT_EQ(test_data::kServiceURL, value); +} + +TEST_F(DeviceRegistrationInfoTest, FinishRegistration_Auth) { + // Test finalizing ticket with user authorization token. + EXPECT_TRUE(dev_reg->Load()); + + // General ticket finalization handler. + std::string ticket_url = + dev_reg->GetServiceURL("registrationTickets/" + + std::string(test_data::kClaimTicketId)); + transport->AddHandler(ticket_url + "/finalize", request_type::kPost, + base::Bind(FinalizeTicketHandler)); + + transport->AddHandler(dev_reg->GetOAuthURL("token"), request_type::kPost, + base::Bind(OAuth2Handler)); + + // Handle patching in the user email onto the device record. + auto email_patch_handler = [](const fake::ServerRequest& request, + fake::ServerResponse* response) { + std::string auth_header = "Bearer "; + auth_header += test_data::kUserAccessToken; + EXPECT_EQ(auth_header, + request.GetHeader(http::request_header::kAuthorization)); + auto json = request.GetDataAsJson(); + EXPECT_NE(nullptr, json.get()); + std::string value; + EXPECT_TRUE(json->GetString("userEmail", &value)); + EXPECT_EQ("me", value); + + response->ReplyJson(status_code::Ok, { + {"id", test_data::kClaimTicketId}, + {"kind", "clouddevices#registrationTicket"}, + {"oauthClientId", test_data::kClientId}, + {"userEmail", "user@email.com"}, + {"deviceDraft.id", test_data::kDeviceId}, + {"deviceDraft.kind", "clouddevices#device"}, + {"deviceDraft.channel.supportedType", "xmpp"}, + }); + }; + transport->AddHandler(ticket_url, request_type::kPatch, + base::Bind(email_patch_handler)); + + storage->save_count_ = 0; + DeviceRegistrationInfo::TestHelper::SetTestTicketId(dev_reg.get()); + EXPECT_TRUE(dev_reg->FinishRegistration(test_data::kUserAccountAuthCode)); + EXPECT_EQ(1, storage->save_count_); // The device info must have been saved. + EXPECT_EQ(4, transport->GetRequestCount()); +}
diff --git a/buffet/device_registration_storage_keys.h b/buffet/device_registration_storage_keys.h new file mode 100644 index 0000000..a6c9239 --- /dev/null +++ b/buffet/device_registration_storage_keys.h
@@ -0,0 +1,31 @@ +// Copyright 2014 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. + +#ifndef BUFFET_DEVICE_REGISTRATION_STORAGE_KEYS_H_ +#define BUFFET_DEVICE_REGISTRATION_STORAGE_KEYS_H_ + +// These are the keys used to identify specific device registration information +// being saved to a storage. Used mostly internally by DeviceRegistrationInfo +// but also exposed so that tests can access them. +namespace buffet { +namespace storage_keys { + +// Persistent keys +extern const char kClientId[]; +extern const char kClientSecret[]; +extern const char kApiKey[]; +extern const char kRefreshToken[]; +extern const char kDeviceId[]; +extern const char kOAuthURL[]; +extern const char kServiceURL[]; +extern const char kRobotAccount[]; +// Transient keys +extern const char kDeviceKind[]; +extern const char kSystemName[]; +extern const char kDisplayName[]; + +} // namespace storage_keys +} // namespace buffet + +#endif // BUFFET_DEVICE_REGISTRATION_STORAGE_KEYS_H_
diff --git a/buffet/http_connection_fake.cc b/buffet/http_connection_fake.cc index 0507c0b..6a1c3b9 100644 --- a/buffet/http_connection_fake.cc +++ b/buffet/http_connection_fake.cc
@@ -40,6 +40,8 @@ CHECK(transport) << "Expecting a fake transport"; auto handler = transport->GetHandler(request_.GetURL(), request_.GetMethod()); if (handler.is_null()) { + LOG(ERROR) << "Received unexpected " << request_.GetMethod() + << " request at " << request_.GetURL(); response_.ReplyText(status_code::NotFound, "<html><body>Not found</body></html>", mime::text::kHtml);
diff --git a/buffet/http_transport_fake.cc b/buffet/http_transport_fake.cc index 84d70cb..4a3a781 100644 --- a/buffet/http_transport_fake.cc +++ b/buffet/http_transport_fake.cc
@@ -4,9 +4,11 @@ #include "buffet/http_transport_fake.h" +#include <base/json/json_reader.h> #include <base/json/json_writer.h> #include <base/logging.h> +#include "buffet/bind_lambda.h" #include "buffet/http_connection_fake.h" #include "buffet/http_request.h" #include "buffet/mime_utils.h" @@ -48,6 +50,7 @@ if (error_msg) *error_msg = "Failed to send request headers"; } + request_count_++; return connection; } @@ -61,6 +64,18 @@ handlers_.insert(std::make_pair(GetHandlerMapKey(url, method), handler)); } +void Transport::AddSimpleReplyHandler(const std::string& url, + const std::string& method, + int status_code, + const std::string& reply_text, + const std::string& mime_type) { + auto handler = [status_code, reply_text, mime_type]( + const ServerRequest& request, ServerResponse* response) { + response->ReplyText(status_code, reply_text, mime_type.c_str()); + }; + AddHandler(url, method, base::Bind(handler)); +} + Transport::HandlerCallback Transport::GetHandler( const std::string& url, const std::string& method) const { // First try the exact combination of URL/Method @@ -92,6 +107,23 @@ return std::string(chars, data_.size()); } +std::unique_ptr<base::DictionaryValue> + ServerRequestResponseBase::GetDataAsJson() const { + if (mime::RemoveParameters(GetHeader(request_header::kContentType)) == + mime::application::kJson) { + auto value = base::JSONReader::Read(GetDataAsString()); + if (value) { + base::DictionaryValue* dict = nullptr; + if (value->GetAsDictionary(&dict)) { + return std::unique_ptr<base::DictionaryValue>(dict); + } else { + delete value; + } + } + } + return std::unique_ptr<base::DictionaryValue>(); +} + void ServerRequestResponseBase::AddHeaders(const HeaderList& headers) { for (auto&& pair : headers) { if (pair.second.empty()) @@ -150,7 +182,19 @@ base::JSONWriter::WriteWithOptions(json, base::JSONWriter::OPTIONS_PRETTY_PRINT, &text); - ReplyText(status_code, text, mime::application::kJson); + std::string mime_type = mime::AppendParameter(mime::application::kJson, + mime::parameters::kCharset, + "utf-8"); + ReplyText(status_code, text, mime_type.c_str()); +} + +void ServerResponse::ReplyJson(int status_code, + const http::FormFieldList& fields) { + base::DictionaryValue json; + for (auto&& pair : fields) { + json.SetString(pair.first, pair.second); + } + ReplyJson(status_code, &json); } std::string ServerResponse::GetStatusText() const {
diff --git a/buffet/http_transport_fake.h b/buffet/http_transport_fake.h index b95599f..a2fb04d 100644 --- a/buffet/http_transport_fake.h +++ b/buffet/http_transport_fake.h
@@ -11,6 +11,7 @@ #include <base/values.h> #include "buffet/http_transport.h" +#include "buffet/http_utils.h" namespace chromeos { namespace http { @@ -44,10 +45,22 @@ // The lookup starts with the most specific data pair to the catch-all (*,*). void AddHandler(const std::string& url, const std::string& method, const HandlerCallback& handler); + // Simple version of AddHandler. AddSimpleReplyHandler just returns the + // specified text response of given MIME type. + void AddSimpleReplyHandler(const std::string& url, + const std::string& method, + int status_code, + const std::string& reply_text, + const std::string& mime_type); // Retrieve a handler for specific |url| and request |method|. HandlerCallback GetHandler(const std::string& url, const std::string& method) const; + // For tests that want to assert on the number of HTTP requests sent, + // these methods can be used to do just that. + int GetRequestCount() const { return request_count_; } + void ResetRequestCount() { request_count_ = 0; } + // Overload from http::Transport virtual std::unique_ptr<http::Connection> CreateConnection( std::shared_ptr<http::Transport> transport, @@ -63,6 +76,8 @@ // A list of user-supplied request handlers. std::map<std::string, HandlerCallback> handlers_; + // Counter incremented each time a request is made. + int request_count_ = 0; }; /////////////////////////////////////////////////////////////////////////////// @@ -77,6 +92,7 @@ void AddData(const void* data, size_t data_size); const std::vector<unsigned char>& GetData() const { return data_; } std::string GetDataAsString() const; + std::unique_ptr<base::DictionaryValue> GetDataAsJson() const; // Add/retrieve request/response HTTP headers. void AddHeaders(const HeaderList& headers); @@ -148,6 +164,9 @@ const char* mime_type); // Reply with JSON object. The content type will be "application/json". void ReplyJson(int status_code, const base::Value* json); + // Special form for JSON response for simple objects that have a flat + // list of key-value pairs of string type. + void ReplyJson(int status_code, const FormFieldList& fields); // Specialized overload to send the binary data as an array of simple // data elements. Only trivial data types (scalars, POD structures, etc)
diff --git a/buffet/http_utils.cc b/buffet/http_utils.cc index 2cd8b80..d711cf8 100644 --- a/buffet/http_utils.cc +++ b/buffet/http_utils.cc
@@ -93,8 +93,11 @@ std::string data; if (json) base::JSONWriter::Write(json, &data); + std::string mime_type = mime::AppendParameter(mime::application::kJson, + mime::parameters::kCharset, + "utf-8"); return PostBinary(url, data.c_str(), data.size(), - mime::application::kJson, headers, transport); + mime_type.c_str(), headers, transport); } std::unique_ptr<Response> PatchJson(const std::string& url, @@ -104,8 +107,11 @@ std::string data; if (json) base::JSONWriter::Write(json, &data); + std::string mime_type = mime::AppendParameter(mime::application::kJson, + mime::parameters::kCharset, + "utf-8"); return SendRequest(request_type::kPatch, url, data.c_str(), data.size(), - mime::application::kJson, headers, transport); + mime_type.c_str(), headers, transport); } std::unique_ptr<base::DictionaryValue> ParseJsonResponse(
diff --git a/buffet/http_utils_unittest.cc b/buffet/http_utils_unittest.cc index 1453b86..6438660 100644 --- a/buffet/http_utils_unittest.cc +++ b/buffet/http_utils_unittest.cc
@@ -125,7 +125,8 @@ {request_header::kIfMatch, "*"}, }, transport); EXPECT_TRUE(response->IsSuccessful()); - EXPECT_EQ(mime::application::kJson, response->GetContentType()); + EXPECT_EQ(mime::application::kJson, + mime::RemoveParameters(response->GetContentType())); auto json = ParseJsonResponse(response.get(), nullptr, nullptr); std::string value; EXPECT_TRUE(json->GetString("method", &value)); @@ -261,12 +262,13 @@ TEST(HttpUtils, PostPatchJson) { auto JsonHandler = [](const fake::ServerRequest& request, fake::ServerResponse* response) { - EXPECT_EQ(mime::application::kJson, - request.GetHeader(request_header::kContentType)); - base::DictionaryValue json; - json.SetString("method", request.GetMethod()); - json.SetString("data", request.GetDataAsString()); - response->ReplyJson(status_code::Ok, &json); + auto mime_type = mime::RemoveParameters( + request.GetHeader(request_header::kContentType)); + EXPECT_EQ(mime::application::kJson, mime_type); + response->ReplyJson(status_code::Ok, { + {"method", request.GetMethod()}, + {"data", request.GetDataAsString()}, + }); }; std::shared_ptr<fake::Transport> transport(new fake::Transport); transport->AddHandler(kFakeUrl, "*", base::Bind(JsonHandler)); @@ -298,10 +300,8 @@ TEST(HttpUtils, ParseJsonResponse) { auto JsonHandler = [](const fake::ServerRequest& request, fake::ServerResponse* response) { - base::DictionaryValue json; - json.SetString("data", request.GetFormField("value")); int status_code = std::stoi(request.GetFormField("code")); - response->ReplyJson(status_code, &json); + response->ReplyJson(status_code, {{"data", request.GetFormField("value")}}); }; std::shared_ptr<fake::Transport> transport(new fake::Transport); transport->AddHandler(kFakeUrl, request_type::kPost, base::Bind(JsonHandler));