libweave: Copy buffet_config* into config*

I am going to split that into two different classes.
No changes. This will help follow future changes.

BUG=brillo:1257,brillo:1253
TEST=`FEATURES=test emerge-gizmo libweave buffet`

Change-Id: Ie2e7b882186c959e35ff224c211325e1e6c14c94
Reviewed-on: https://chromium-review.googlesource.com/293897
Reviewed-by: Vitaly Buka <vitalybuka@chromium.org>
Tested-by: Vitaly Buka <vitalybuka@chromium.org>
Reviewed-by: Alex Vakulenko <avakulenko@chromium.org>
Commit-Queue: Vitaly Buka <vitalybuka@chromium.org>
diff --git a/libweave/src/config.cc b/libweave/src/config.cc
new file mode 100644
index 0000000..85c75fe
--- /dev/null
+++ b/libweave/src/config.cc
@@ -0,0 +1,295 @@
+// Copyright 2015 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 "libweave/src/config.h"
+
+#include <set>
+
+#include <base/files/file_util.h>
+#include <base/logging.h>
+#include <base/strings/string_number_conversions.h>
+#include <weave/enum_to_string.h>
+
+#include "libweave/src/storage_impls.h"
+#include "libweave/src/storage_interface.h"
+#include "libweave/src/string_utils.h"
+
+namespace {
+
+bool IsValidAccessRole(const std::string& role) {
+  return role == "none" || role == "viewer" || role == "user";
+}
+
+bool StringToTimeDelta(const std::string& value, base::TimeDelta* delta) {
+  uint64_t ms{0};
+  if (!base::StringToUint64(value, &ms))
+    return false;
+  *delta = base::TimeDelta::FromMilliseconds(ms);
+  return true;
+}
+
+}  // namespace
+
+namespace weave {
+
+namespace config_keys {
+
+const char kClientId[] = "client_id";
+const char kClientSecret[] = "client_secret";
+const char kApiKey[] = "api_key";
+const char kOAuthURL[] = "oauth_url";
+const char kServiceURL[] = "service_url";
+const char kName[] = "name";
+const char kDescription[] = "description";
+const char kLocation[] = "location";
+const char kLocalAnonymousAccessRole[] = "local_anonymous_access_role";
+const char kLocalDiscoveryEnabled[] = "local_discovery_enabled";
+const char kLocalPairingEnabled[] = "local_pairing_enabled";
+const char kOemName[] = "oem_name";
+const char kModelName[] = "model_name";
+const char kModelId[] = "model_id";
+const char kPollingPeriodMs[] = "polling_period_ms";
+const char kBackupPollingPeriodMs[] = "backup_polling_period_ms";
+const char kRefreshToken[] = "refresh_token";
+const char kDeviceId[] = "device_id";
+const char kRobotAccount[] = "robot_account";
+const char kWifiAutoSetupEnabled[] = "wifi_auto_setup_enabled";
+const char kBleSetupEnabled[] = "ble_setup_enabled";
+const char kEmbeddedCode[] = "embedded_code";
+const char kPairingModes[] = "pairing_modes";
+const char kLastConfiguredSsid[] = "last_configured_ssid";
+
+}  // namespace config_keys
+
+Settings Config::CreateDefaultSettings() {
+  Settings result;
+  result.client_id = "58855907228.apps.googleusercontent.com";
+  result.client_secret = "eHSAREAHrIqPsHBxCE9zPPBi";
+  result.api_key = "AIzaSyDSq46gG-AxUnC3zoqD9COIPrjolFsMfMA";
+  result.oauth_url = "https://accounts.google.com/o/oauth2/";
+  result.service_url = "https://www.googleapis.com/clouddevices/v1/";
+  result.name = "Developer device";
+  result.local_anonymous_access_role = "viewer";
+  result.local_discovery_enabled = true;
+  result.local_pairing_enabled = true;
+  result.oem_name = "Chromium";
+  result.model_name = "Brillo";
+  result.model_id = "AAAAA";
+  result.polling_period = base::TimeDelta::FromSeconds(7);
+  result.backup_polling_period = base::TimeDelta::FromMinutes(30);
+  result.wifi_auto_setup_enabled = true;
+  result.ble_setup_enabled = false;
+  result.pairing_modes.emplace(PairingType::kPinCode);
+  return result;
+}
+
+Config::Config(std::unique_ptr<StorageInterface> storage)
+    : storage_{std::move(storage)} {}
+
+Config::Config(const base::FilePath& state_path)
+    : Config{std::unique_ptr<StorageInterface>{new FileStorage{state_path}}} {}
+
+void Config::AddOnChangedCallback(const OnChangedCallback& callback) {
+  on_changed_.push_back(callback);
+  // Force to read current state.
+  callback.Run(settings_);
+}
+
+const Settings& Config::GetSettings() const {
+  return settings_;
+}
+
+void Config::Load(const base::FilePath& config_path) {
+  chromeos::KeyValueStore store;
+  if (base::PathExists(config_path)) {
+    CHECK(store.Load(config_path)) << "Unable to read or parse config file at"
+                                   << config_path.value();
+  }
+  Load(store);
+}
+
+void Config::Load(const chromeos::KeyValueStore& store) {
+  Transaction change{this};
+  change.save_ = false;
+
+  store.GetString(config_keys::kClientId, &settings_.client_id);
+  CHECK(!settings_.client_id.empty());
+
+  store.GetString(config_keys::kClientSecret, &settings_.client_secret);
+  CHECK(!settings_.client_secret.empty());
+
+  store.GetString(config_keys::kApiKey, &settings_.api_key);
+  CHECK(!settings_.api_key.empty());
+
+  store.GetString(config_keys::kOAuthURL, &settings_.oauth_url);
+  CHECK(!settings_.oauth_url.empty());
+
+  store.GetString(config_keys::kServiceURL, &settings_.service_url);
+  CHECK(!settings_.service_url.empty());
+
+  store.GetString(config_keys::kOemName, &settings_.oem_name);
+  CHECK(!settings_.oem_name.empty());
+
+  store.GetString(config_keys::kModelName, &settings_.model_name);
+  CHECK(!settings_.model_name.empty());
+
+  store.GetString(config_keys::kModelId, &settings_.model_id);
+  CHECK(!settings_.model_id.empty());
+
+  std::string polling_period_str;
+  if (store.GetString(config_keys::kPollingPeriodMs, &polling_period_str))
+    CHECK(StringToTimeDelta(polling_period_str, &settings_.polling_period));
+
+  if (store.GetString(config_keys::kBackupPollingPeriodMs, &polling_period_str))
+    CHECK(StringToTimeDelta(polling_period_str,
+                            &settings_.backup_polling_period));
+
+  store.GetBoolean(config_keys::kWifiAutoSetupEnabled,
+                   &settings_.wifi_auto_setup_enabled);
+
+  store.GetBoolean(config_keys::kBleSetupEnabled, &settings_.ble_setup_enabled);
+
+  store.GetString(config_keys::kEmbeddedCode, &settings_.embedded_code);
+
+  std::string modes_str;
+  if (store.GetString(config_keys::kPairingModes, &modes_str)) {
+    std::set<PairingType> pairing_modes;
+    for (const std::string& mode : Split(modes_str, ",", true, true)) {
+      PairingType pairing_mode;
+      CHECK(StringToEnum(mode, &pairing_mode));
+      pairing_modes.insert(pairing_mode);
+    }
+    settings_.pairing_modes = std::move(pairing_modes);
+  }
+
+  // Empty name set by user or server is allowed, still we expect some
+  // meaningfull config value.
+  store.GetString(config_keys::kName, &settings_.name);
+  CHECK(!settings_.name.empty());
+
+  store.GetString(config_keys::kDescription, &settings_.description);
+  store.GetString(config_keys::kLocation, &settings_.location);
+
+  store.GetString(config_keys::kLocalAnonymousAccessRole,
+                  &settings_.local_anonymous_access_role);
+  CHECK(IsValidAccessRole(settings_.local_anonymous_access_role))
+      << "Invalid role: " << settings_.local_anonymous_access_role;
+
+  store.GetBoolean(config_keys::kLocalDiscoveryEnabled,
+                   &settings_.local_discovery_enabled);
+  store.GetBoolean(config_keys::kLocalPairingEnabled,
+                   &settings_.local_pairing_enabled);
+
+  change.LoadState();
+}
+
+void Config::Transaction::LoadState() {
+  if (!config_->storage_)
+    return;
+  auto value = config_->storage_->Load();
+  const base::DictionaryValue* dict = nullptr;
+  if (!value || !value->GetAsDictionary(&dict))
+    return;
+
+  std::string tmp;
+  bool tmp_bool{false};
+
+  if (dict->GetString(config_keys::kClientId, &tmp))
+    set_client_id(tmp);
+
+  if (dict->GetString(config_keys::kClientSecret, &tmp))
+    set_client_secret(tmp);
+
+  if (dict->GetString(config_keys::kApiKey, &tmp))
+    set_api_key(tmp);
+
+  if (dict->GetString(config_keys::kOAuthURL, &tmp))
+    set_oauth_url(tmp);
+
+  if (dict->GetString(config_keys::kServiceURL, &tmp))
+    set_service_url(tmp);
+
+  if (dict->GetString(config_keys::kName, &tmp))
+    set_name(tmp);
+
+  if (dict->GetString(config_keys::kDescription, &tmp))
+    set_description(tmp);
+
+  if (dict->GetString(config_keys::kLocation, &tmp))
+    set_location(tmp);
+
+  if (dict->GetString(config_keys::kLocalAnonymousAccessRole, &tmp))
+    set_local_anonymous_access_role(tmp);
+
+  if (dict->GetBoolean(config_keys::kLocalDiscoveryEnabled, &tmp_bool))
+    set_local_discovery_enabled(tmp_bool);
+
+  if (dict->GetBoolean(config_keys::kLocalPairingEnabled, &tmp_bool))
+    set_local_pairing_enabled(tmp_bool);
+
+  if (dict->GetString(config_keys::kRefreshToken, &tmp))
+    set_refresh_token(tmp);
+
+  if (dict->GetString(config_keys::kRobotAccount, &tmp))
+    set_robot_account(tmp);
+
+  if (dict->GetString(config_keys::kLastConfiguredSsid, &tmp))
+    set_last_configured_ssid(tmp);
+
+  if (dict->GetString(config_keys::kDeviceId, &tmp))
+    set_device_id(tmp);
+}
+
+bool Config::Save() {
+  if (!storage_)
+    return false;
+  base::DictionaryValue dict;
+  dict.SetString(config_keys::kClientId, settings_.client_id);
+  dict.SetString(config_keys::kClientSecret, settings_.client_secret);
+  dict.SetString(config_keys::kApiKey, settings_.api_key);
+  dict.SetString(config_keys::kOAuthURL, settings_.oauth_url);
+  dict.SetString(config_keys::kServiceURL, settings_.service_url);
+  dict.SetString(config_keys::kRefreshToken, settings_.refresh_token);
+  dict.SetString(config_keys::kDeviceId, settings_.device_id);
+  dict.SetString(config_keys::kRobotAccount, settings_.robot_account);
+  dict.SetString(config_keys::kLastConfiguredSsid,
+                 settings_.last_configured_ssid);
+  dict.SetString(config_keys::kName, settings_.name);
+  dict.SetString(config_keys::kDescription, settings_.description);
+  dict.SetString(config_keys::kLocation, settings_.location);
+  dict.SetString(config_keys::kLocalAnonymousAccessRole,
+                 settings_.local_anonymous_access_role);
+  dict.SetBoolean(config_keys::kLocalDiscoveryEnabled,
+                  settings_.local_discovery_enabled);
+  dict.SetBoolean(config_keys::kLocalPairingEnabled,
+                  settings_.local_pairing_enabled);
+
+  return storage_->Save(dict);
+}
+
+Config::Transaction::~Transaction() {
+  Commit();
+}
+
+bool Config::Transaction::set_local_anonymous_access_role(
+    const std::string& role) {
+  if (!IsValidAccessRole(role)) {
+    LOG(ERROR) << "Invalid role: " << role;
+    return false;
+  }
+  settings_->local_anonymous_access_role = role;
+  return true;
+}
+
+void Config::Transaction::Commit() {
+  if (!config_)
+    return;
+  if (save_)
+    config_->Save();
+  for (const auto& cb : config_->on_changed_)
+    cb.Run(*settings_);
+  config_ = nullptr;
+}
+
+}  // namespace weave
diff --git a/libweave/src/config.h b/libweave/src/config.h
new file mode 100644
index 0000000..5871629
--- /dev/null
+++ b/libweave/src/config.h
@@ -0,0 +1,154 @@
+// Copyright 2015 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 LIBWEAVE_SRC_CONFIG_H_
+#define LIBWEAVE_SRC_CONFIG_H_
+
+#include <set>
+#include <string>
+#include <vector>
+
+#include <base/callback.h>
+#include <base/files/file_path.h>
+#include <weave/error.h>
+#include <chromeos/key_value_store.h>
+#include <weave/config.h>
+
+#include "libweave/src/privet/security_delegate.h"
+
+namespace weave {
+
+class StorageInterface;
+
+// Handles reading buffet config and state files.
+class Config final : public Config {
+ public:
+  using OnChangedCallback = base::Callback<void(const Settings&)>;
+  ~Config() override = default;
+
+  explicit Config(std::unique_ptr<StorageInterface> storage);
+
+  explicit Config(const base::FilePath& state_path);
+
+  // Config overrides.
+  void AddOnChangedCallback(const OnChangedCallback& callback) override;
+  const Settings& GetSettings() const override;
+
+  void Load(const base::FilePath& config_path);
+  void Load(const chromeos::KeyValueStore& store);
+
+  // Allows editing of config. Makes sure that callbacks were called and changes
+  // were saved.
+  // User can commit changes by calling Commit method or by destroying the
+  // object.
+  class Transaction final {
+   public:
+    explicit Transaction(Config* config)
+        : config_(config), settings_(&config->settings_) {
+      CHECK(config_);
+    }
+
+    ~Transaction();
+
+    void set_client_id(const std::string& id) { settings_->client_id = id; }
+    void set_client_secret(const std::string& secret) {
+      settings_->client_secret = secret;
+    }
+    void set_api_key(const std::string& key) { settings_->api_key = key; }
+    void set_oauth_url(const std::string& url) { settings_->oauth_url = url; }
+    void set_service_url(const std::string& url) {
+      settings_->service_url = url;
+    }
+    void set_name(const std::string& name) { settings_->name = name; }
+    void set_description(const std::string& description) {
+      settings_->description = description;
+    }
+    void set_location(const std::string& location) {
+      settings_->location = location;
+    }
+    bool set_local_anonymous_access_role(const std::string& role);
+    void set_local_discovery_enabled(bool enabled) {
+      settings_->local_discovery_enabled = enabled;
+    }
+    void set_local_pairing_enabled(bool enabled) {
+      settings_->local_pairing_enabled = enabled;
+    }
+    void set_device_id(const std::string& id) { settings_->device_id = id; }
+    void set_refresh_token(const std::string& token) {
+      settings_->refresh_token = token;
+    }
+    void set_robot_account(const std::string& account) {
+      settings_->robot_account = account;
+    }
+    void set_last_configured_ssid(const std::string& ssid) {
+      settings_->last_configured_ssid = ssid;
+    }
+
+    void Commit();
+
+   private:
+    friend class Config;
+    void LoadState();
+    Config* config_;
+    Settings* settings_;
+    bool save_{true};
+  };
+
+  const std::string& client_id() const { return settings_.client_id; }
+  const std::string& client_secret() const { return settings_.client_secret; }
+  const std::string& api_key() const { return settings_.api_key; }
+  const std::string& oauth_url() const { return settings_.oauth_url; }
+  const std::string& service_url() const { return settings_.service_url; }
+  const std::string& oem_name() const { return settings_.oem_name; }
+  const std::string& model_name() const { return settings_.model_name; }
+  const std::string& model_id() const { return settings_.model_id; }
+  base::TimeDelta polling_period() const { return settings_.polling_period; }
+  base::TimeDelta backup_polling_period() const {
+    return settings_.backup_polling_period;
+  }
+
+  bool wifi_auto_setup_enabled() const {
+    return settings_.wifi_auto_setup_enabled;
+  }
+  bool ble_setup_enabled() const { return settings_.ble_setup_enabled; }
+  const std::set<PairingType>& pairing_modes() const {
+    return settings_.pairing_modes;
+  }
+  const std::string& embedded_code() const { return settings_.embedded_code; }
+
+  const std::string& name() const { return settings_.name; }
+  const std::string& description() const { return settings_.description; }
+  const std::string& location() const { return settings_.location; }
+  const std::string& local_anonymous_access_role() const {
+    return settings_.local_anonymous_access_role;
+  }
+  bool local_pairing_enabled() const { return settings_.local_pairing_enabled; }
+  bool local_discovery_enabled() const {
+    return settings_.local_discovery_enabled;
+  }
+
+  const std::string& device_id() const { return settings_.device_id; }
+  const std::string& refresh_token() const { return settings_.refresh_token; }
+  const std::string& robot_account() const { return settings_.robot_account; }
+  const std::string& last_configured_ssid() const {
+    return settings_.last_configured_ssid;
+  }
+
+ private:
+  bool Save();
+  static Settings CreateDefaultSettings();
+
+  Settings settings_ = CreateDefaultSettings();
+
+  // Serialization interface to save and load buffet state.
+  std::unique_ptr<StorageInterface> storage_;
+
+  std::vector<OnChangedCallback> on_changed_;
+
+  DISALLOW_COPY_AND_ASSIGN(Config);
+};
+
+}  // namespace weave
+
+#endif  // LIBWEAVE_SRC_CONFIG_H_
diff --git a/libweave/src/config_unittest.cc b/libweave/src/config_unittest.cc
new file mode 100644
index 0000000..0ffad43
--- /dev/null
+++ b/libweave/src/config_unittest.cc
@@ -0,0 +1,314 @@
+// 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 "libweave/src/config.h"
+
+#include <set>
+
+#include <base/bind.h>
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include "libweave/src/commands/unittest_utils.h"
+#include "libweave/src/storage_impls.h"
+
+using testing::_;
+
+namespace weave {
+
+class ConfigTest : public ::testing::Test {
+ protected:
+  void SetUp() override {
+    EXPECT_CALL(*this, OnConfigChanged(_))
+        .Times(1);  // Called from AddOnChangedCallback
+
+    std::unique_ptr<StorageInterface> storage{new MemStorage};
+    storage_ = storage.get();
+    config_.reset(new Config{std::move(storage)});
+
+    config_->AddOnChangedCallback(
+        base::Bind(&ConfigTest::OnConfigChanged, base::Unretained(this)));
+  }
+
+  MOCK_METHOD1(OnConfigChanged, void(const Settings&));
+
+  StorageInterface* storage_{nullptr};
+  std::unique_ptr<Config> config_;
+  const Config default_{nullptr};
+};
+
+TEST_F(ConfigTest, NoStorage) {
+  Config config{nullptr};
+  Config::Transaction change{&config};
+  change.Commit();
+}
+
+TEST_F(ConfigTest, Defaults) {
+  EXPECT_EQ("58855907228.apps.googleusercontent.com", config_->client_id());
+  EXPECT_EQ("eHSAREAHrIqPsHBxCE9zPPBi", config_->client_secret());
+  EXPECT_EQ("AIzaSyDSq46gG-AxUnC3zoqD9COIPrjolFsMfMA", config_->api_key());
+  EXPECT_EQ("https://accounts.google.com/o/oauth2/", config_->oauth_url());
+  EXPECT_EQ("https://www.googleapis.com/clouddevices/v1/",
+            config_->service_url());
+  EXPECT_EQ("Chromium", config_->oem_name());
+  EXPECT_EQ("Brillo", config_->model_name());
+  EXPECT_EQ("AAAAA", config_->model_id());
+  EXPECT_EQ(base::TimeDelta::FromSeconds(7), config_->polling_period());
+  EXPECT_EQ(base::TimeDelta::FromMinutes(30), config_->backup_polling_period());
+  EXPECT_TRUE(config_->wifi_auto_setup_enabled());
+  EXPECT_FALSE(config_->ble_setup_enabled());
+  EXPECT_EQ(std::set<PairingType>{PairingType::kPinCode},
+            config_->pairing_modes());
+  EXPECT_EQ("", config_->embedded_code());
+  EXPECT_EQ("Developer device", config_->name());
+  EXPECT_EQ("", config_->description());
+  EXPECT_EQ("", config_->location());
+  EXPECT_EQ("viewer", config_->local_anonymous_access_role());
+  EXPECT_TRUE(config_->local_pairing_enabled());
+  EXPECT_TRUE(config_->local_discovery_enabled());
+  EXPECT_EQ("", config_->device_id());
+  EXPECT_EQ("", config_->refresh_token());
+  EXPECT_EQ("", config_->robot_account());
+  EXPECT_EQ("", config_->last_configured_ssid());
+}
+
+TEST_F(ConfigTest, LoadConfig) {
+  chromeos::KeyValueStore config_store;
+  config_store.SetString("client_id", "conf_client_id");
+  config_store.SetString("client_secret", "conf_client_secret");
+  config_store.SetString("api_key", "conf_api_key");
+  config_store.SetString("oauth_url", "conf_oauth_url");
+  config_store.SetString("service_url", "conf_service_url");
+  config_store.SetString("oem_name", "conf_oem_name");
+  config_store.SetString("model_name", "conf_model_name");
+  config_store.SetString("model_id", "ABCDE");
+  config_store.SetString("polling_period_ms", "12345");
+  config_store.SetString("backup_polling_period_ms", "6589");
+  config_store.SetBoolean("wifi_auto_setup_enabled", false);
+  config_store.SetBoolean("ble_setup_enabled", true);
+  config_store.SetString("pairing_modes",
+                         "pinCode,embeddedCode,ultrasound32,audible32");
+  config_store.SetString("embedded_code", "1234");
+  config_store.SetString("name", "conf_name");
+  config_store.SetString("description", "conf_description");
+  config_store.SetString("location", "conf_location");
+  config_store.SetString("local_anonymous_access_role", "user");
+  config_store.SetBoolean("local_pairing_enabled", false);
+  config_store.SetBoolean("local_discovery_enabled", false);
+
+  // Following will be ignored.
+  config_store.SetString("device_kind", "conf_device_kind");
+  config_store.SetString("device_id", "conf_device_id");
+  config_store.SetString("refresh_token", "conf_refresh_token");
+  config_store.SetString("robot_account", "conf_robot_account");
+  config_store.SetString("last_configured_ssid", "conf_last_configured_ssid");
+
+  EXPECT_CALL(*this, OnConfigChanged(_)).Times(1);
+  config_->Load(config_store);
+
+  EXPECT_EQ("conf_client_id", config_->client_id());
+  EXPECT_EQ("conf_client_secret", config_->client_secret());
+  EXPECT_EQ("conf_api_key", config_->api_key());
+  EXPECT_EQ("conf_oauth_url", config_->oauth_url());
+  EXPECT_EQ("conf_service_url", config_->service_url());
+  EXPECT_EQ("conf_oem_name", config_->oem_name());
+  EXPECT_EQ("conf_model_name", config_->model_name());
+  EXPECT_EQ("ABCDE", config_->model_id());
+  EXPECT_EQ(base::TimeDelta::FromMilliseconds(12345),
+            config_->polling_period());
+  EXPECT_EQ(base::TimeDelta::FromMilliseconds(6589),
+            config_->backup_polling_period());
+  EXPECT_FALSE(config_->wifi_auto_setup_enabled());
+  EXPECT_TRUE(config_->ble_setup_enabled());
+  std::set<PairingType> pairing_types{
+      PairingType::kPinCode, PairingType::kEmbeddedCode,
+      PairingType::kUltrasound32, PairingType::kAudible32};
+  EXPECT_EQ(pairing_types, config_->pairing_modes());
+  EXPECT_EQ("1234", config_->embedded_code());
+  EXPECT_EQ("conf_name", config_->name());
+  EXPECT_EQ("conf_description", config_->description());
+  EXPECT_EQ("conf_location", config_->location());
+  EXPECT_EQ("user", config_->local_anonymous_access_role());
+  EXPECT_FALSE(config_->local_pairing_enabled());
+  EXPECT_FALSE(config_->local_discovery_enabled());
+
+  // Not from config.
+  EXPECT_EQ(default_.device_id(), config_->device_id());
+  EXPECT_EQ(default_.refresh_token(), config_->refresh_token());
+  EXPECT_EQ(default_.robot_account(), config_->robot_account());
+  EXPECT_EQ(default_.last_configured_ssid(), config_->last_configured_ssid());
+
+  // Nothing should be saved yet.
+  EXPECT_JSON_EQ("{}", *storage_->Load());
+
+  Config::Transaction change{config_.get()};
+  EXPECT_CALL(*this, OnConfigChanged(_)).Times(1);
+  change.Commit();
+
+  auto expected = R"({
+    'api_key': 'conf_api_key',
+    'client_id': 'conf_client_id',
+    'client_secret': 'conf_client_secret',
+    'description': 'conf_description',
+    'device_id': '',
+    'local_anonymous_access_role': 'user',
+    'local_discovery_enabled': false,
+    'local_pairing_enabled': false,
+    'location': 'conf_location',
+    'name': 'conf_name',
+    'oauth_url': 'conf_oauth_url',
+    'refresh_token': '',
+    'robot_account': '',
+    'last_configured_ssid': '',
+    'service_url': 'conf_service_url'
+  })";
+  EXPECT_JSON_EQ(expected, *storage_->Load());
+}
+
+TEST_F(ConfigTest, LoadState) {
+  auto state = R"({
+    'api_key': 'state_api_key',
+    'client_id': 'state_client_id',
+    'client_secret': 'state_client_secret',
+    'description': 'state_description',
+    'device_id': 'state_device_id',
+    'local_anonymous_access_role': 'user',
+    'local_discovery_enabled': false,
+    'local_pairing_enabled': false,
+    'location': 'state_location',
+    'name': 'state_name',
+    'oauth_url': 'state_oauth_url',
+    'refresh_token': 'state_refresh_token',
+    'robot_account': 'state_robot_account',
+    'last_configured_ssid': 'state_last_configured_ssid',
+    'service_url': 'state_service_url'
+  })";
+  storage_->Save(*unittests::CreateDictionaryValue(state));
+
+  chromeos::KeyValueStore config_store;
+  EXPECT_CALL(*this, OnConfigChanged(_)).Times(1);
+  config_->Load(config_store);
+
+  // Clear storage.
+  storage_->Save(base::DictionaryValue());
+
+  EXPECT_EQ("state_client_id", config_->client_id());
+  EXPECT_EQ("state_client_secret", config_->client_secret());
+  EXPECT_EQ("state_api_key", config_->api_key());
+  EXPECT_EQ("state_oauth_url", config_->oauth_url());
+  EXPECT_EQ("state_service_url", config_->service_url());
+  EXPECT_EQ(default_.oem_name(), config_->oem_name());
+  EXPECT_EQ(default_.model_name(), config_->model_name());
+  EXPECT_EQ(default_.model_id(), config_->model_id());
+  EXPECT_EQ(default_.polling_period(), config_->polling_period());
+  EXPECT_EQ(default_.backup_polling_period(), config_->backup_polling_period());
+  EXPECT_EQ(default_.wifi_auto_setup_enabled(),
+            config_->wifi_auto_setup_enabled());
+  EXPECT_EQ(default_.ble_setup_enabled(), config_->ble_setup_enabled());
+  EXPECT_EQ(default_.pairing_modes(), config_->pairing_modes());
+  EXPECT_EQ(default_.embedded_code(), config_->embedded_code());
+  EXPECT_EQ("state_name", config_->name());
+  EXPECT_EQ("state_description", config_->description());
+  EXPECT_EQ("state_location", config_->location());
+  EXPECT_EQ("user", config_->local_anonymous_access_role());
+  EXPECT_FALSE(config_->local_pairing_enabled());
+  EXPECT_FALSE(config_->local_discovery_enabled());
+  EXPECT_EQ("state_device_id", config_->device_id());
+  EXPECT_EQ("state_refresh_token", config_->refresh_token());
+  EXPECT_EQ("state_robot_account", config_->robot_account());
+  EXPECT_EQ("state_last_configured_ssid", config_->last_configured_ssid());
+
+  // Nothing should be saved yet.
+  EXPECT_JSON_EQ("{}", *storage_->Load());
+
+  Config::Transaction change{config_.get()};
+  EXPECT_CALL(*this, OnConfigChanged(_)).Times(1);
+  change.Commit();
+
+  EXPECT_JSON_EQ(state, *storage_->Load());
+}
+
+TEST_F(ConfigTest, Setters) {
+  Config::Transaction change{config_.get()};
+
+  change.set_client_id("set_client_id");
+  EXPECT_EQ("set_client_id", config_->client_id());
+
+  change.set_client_secret("set_client_secret");
+  EXPECT_EQ("set_client_secret", config_->client_secret());
+
+  change.set_api_key("set_api_key");
+  EXPECT_EQ("set_api_key", config_->api_key());
+
+  change.set_oauth_url("set_oauth_url");
+  EXPECT_EQ("set_oauth_url", config_->oauth_url());
+
+  change.set_service_url("set_service_url");
+  EXPECT_EQ("set_service_url", config_->service_url());
+
+  change.set_name("set_name");
+  EXPECT_EQ("set_name", config_->name());
+
+  change.set_description("set_description");
+  EXPECT_EQ("set_description", config_->description());
+
+  change.set_location("set_location");
+  EXPECT_EQ("set_location", config_->location());
+
+  change.set_local_anonymous_access_role("viewer");
+  EXPECT_EQ("viewer", config_->local_anonymous_access_role());
+
+  change.set_local_anonymous_access_role("none");
+  EXPECT_EQ("none", config_->local_anonymous_access_role());
+
+  change.set_local_anonymous_access_role("user");
+  EXPECT_EQ("user", config_->local_anonymous_access_role());
+
+  change.set_local_discovery_enabled(false);
+  EXPECT_FALSE(config_->local_discovery_enabled());
+
+  change.set_local_pairing_enabled(false);
+  EXPECT_FALSE(config_->local_pairing_enabled());
+
+  change.set_local_discovery_enabled(true);
+  EXPECT_TRUE(config_->local_discovery_enabled());
+
+  change.set_local_pairing_enabled(true);
+  EXPECT_TRUE(config_->local_pairing_enabled());
+
+  change.set_device_id("set_id");
+  EXPECT_EQ("set_id", config_->device_id());
+
+  change.set_refresh_token("set_token");
+  EXPECT_EQ("set_token", config_->refresh_token());
+
+  change.set_robot_account("set_account");
+  EXPECT_EQ("set_account", config_->robot_account());
+
+  change.set_last_configured_ssid("set_last_configured_ssid");
+  EXPECT_EQ("set_last_configured_ssid", config_->last_configured_ssid());
+
+  EXPECT_CALL(*this, OnConfigChanged(_)).Times(1);
+  change.Commit();
+
+  auto expected = R"({
+    'api_key': 'set_api_key',
+    'client_id': 'set_client_id',
+    'client_secret': 'set_client_secret',
+    'description': 'set_description',
+    'device_id': 'set_id',
+    'local_anonymous_access_role': 'user',
+    'local_discovery_enabled': true,
+    'local_pairing_enabled': true,
+    'location': 'set_location',
+    'name': 'set_name',
+    'oauth_url': 'set_oauth_url',
+    'refresh_token': 'set_token',
+    'robot_account': 'set_account',
+    'last_configured_ssid': 'set_last_configured_ssid',
+    'service_url': 'set_service_url'
+  })";
+  EXPECT_JSON_EQ(expected, *storage_->Load());
+}
+}  // namespace weave