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