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));