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/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());
+}