buffet: Add ExportedObjectManager delegate

This makes it easy to export an object manager.  We'll use this very
soon to implement the ObjectManager interface on the root Buffet
object.

BUG=chromium:359190
TEST=Unittests

Change-Id: I19d2da33b81557431c5787937c49a18e7d7bacb2
Reviewed-on: https://chromium-review.googlesource.com/196387
Reviewed-by: Alex Vakulenko <avakulenko@chromium.org>
Commit-Queue: Christopher Wiley <wiley@chromium.org>
Tested-by: Christopher Wiley <wiley@chromium.org>
diff --git a/buffet/buffet.gyp b/buffet/buffet.gyp
index c4c5a1b..b94677c 100644
--- a/buffet/buffet.gyp
+++ b/buffet/buffet.gyp
@@ -32,6 +32,7 @@
         'dbus_manager.cc',
         'dbus_utils.cc',
         'device_registration_info.cc',
+        'exported_object_manager.cc',
         'exported_property_set.cc',
         'http_request.cc',
         'http_connection_curl.cc',
@@ -77,6 +78,7 @@
         'buffet_testrunner.cc',
         'data_encoding_unittest.cc',
         'device_registration_info_unittest.cc',
+        'exported_object_manager_unittest.cc',
         'exported_property_set_unittest.cc',
         'http_connection_fake.cc',
         'http_transport_fake.cc',
diff --git a/buffet/exported_object_manager.cc b/buffet/exported_object_manager.cc
new file mode 100644
index 0000000..6458128
--- /dev/null
+++ b/buffet/exported_object_manager.cc
@@ -0,0 +1,133 @@
+// 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 "buffet/exported_object_manager.h"
+
+#include <dbus/object_manager.h>
+
+#include "buffet/async_event_sequencer.h"
+
+namespace buffet {
+
+namespace dbus_utils {
+
+ExportedObjectManager::ExportedObjectManager(dbus::Bus* bus,
+                                             const dbus::ObjectPath& path)
+    : bus_(bus), exported_object_(bus->GetExportedObject(path)),
+      weak_ptr_factory_(this) {}
+
+void ExportedObjectManager::Init(const OnInitFinish& cb) {
+  bus_->AssertOnOriginThread();
+  scoped_refptr<dbus_utils::AsyncEventSequencer> sequencer(
+      new dbus_utils::AsyncEventSequencer());
+  exported_object_->ExportMethod(
+      dbus::kObjectManagerInterface,
+      dbus::kObjectManagerGetManagedObjects,
+      base::Bind(&ExportedObjectManager::HandleGetManagedObjects,
+                 weak_ptr_factory_.GetWeakPtr()),
+      sequencer->GetExportHandler(
+          dbus::kObjectManagerInterface,
+          dbus::kObjectManagerGetManagedObjects,
+          "Failed exporting GetManagedObjects method of ObjectManager",
+          false));
+}
+
+void ExportedObjectManager::ClaimInterface(
+    const dbus::ObjectPath& path,
+    const std::string& interface_name,
+    const PropertyWriter& property_writer) {
+  bus_->AssertOnOriginThread();
+  // We're sending signals that look like:
+  //   org.freedesktop.DBus.ObjectManager.InterfacesAdded (
+  //       OBJPATH object_path,
+  //       DICT<STRING,DICT<STRING,VARIANT>> interfaces_and_properties);
+  dbus::Signal signal(dbus::kObjectManagerInterface,
+                      dbus::kObjectManagerInterfacesAdded);
+  dbus::MessageWriter signal_writer(&signal);
+  dbus::MessageWriter all_interfaces(&signal);
+  dbus::MessageWriter each_interface(&signal);
+  signal_writer.AppendObjectPath(path);
+  signal_writer.OpenArray("{sa{sv}}", &all_interfaces);
+  all_interfaces.OpenDictEntry(&each_interface);
+  each_interface.AppendString(interface_name);
+  property_writer.Run(&each_interface);
+  all_interfaces.CloseContainer(&each_interface);
+  signal_writer.CloseContainer(&all_interfaces);
+  exported_object_->SendSignal(&signal);
+  registered_objects_[path][interface_name] = property_writer;
+}
+
+void ExportedObjectManager::ReleaseInterface(
+    const dbus::ObjectPath& path, const std::string& interface_name) {
+  bus_->AssertOnOriginThread();
+  auto interfaces_for_path_itr = registered_objects_.find(path);
+  CHECK(interfaces_for_path_itr != registered_objects_.end())
+      << "Attempting to signal interface removal for path " << path.value()
+      << " which was never registered.";
+  auto interfaces_for_path = interfaces_for_path_itr->second;
+  auto property_for_interface_itr = interfaces_for_path.find(interface_name);
+  CHECK(property_for_interface_itr != interfaces_for_path.end())
+      << "Attempted to remove interface " << interface_name << " from "
+      << path.value() << ", but this interface was never registered.";
+  interfaces_for_path.erase(interface_name);
+  if (interfaces_for_path.size() < 1) {
+    registered_objects_.erase(path);
+  }
+  // We're sending signals that look like:
+  //   org.freedesktop.DBus.ObjectManager.InterfacesRemoved (
+  //       OBJPATH object_path, ARRAY<STRING> interfaces);
+  dbus::Signal signal(dbus::kObjectManagerInterface,
+                      dbus::kObjectManagerInterfacesRemoved);
+  dbus::MessageWriter signal_writer(&signal);
+  signal_writer.AppendObjectPath(path);
+  dbus::MessageWriter interface_writer(nullptr);
+  signal_writer.OpenArray("s", &interface_writer);
+  interface_writer.AppendString(interface_name);
+  signal_writer.CloseContainer(&interface_writer);
+  exported_object_->SendSignal(&signal);
+}
+
+void ExportedObjectManager::HandleGetManagedObjects(
+    dbus::MethodCall* method_call,
+    dbus::ExportedObject::ResponseSender response_sender) const {
+  // Implements the GetManagedObjects method:
+  //
+  // org.freedesktop.DBus.ObjectManager.GetManagedObjects (
+  //     out DICT<OBJPATH,
+  //              DICT<STRING,
+  //                   DICT<STRING,VARIANT>>> )
+  bus_->AssertOnOriginThread();
+  scoped_ptr<dbus::Response> response(
+      dbus::Response::FromMethodCall(method_call));
+  dbus::MessageWriter response_writer(response.get());
+  dbus::MessageWriter all_object_paths(nullptr);
+  dbus::MessageWriter each_object_path(nullptr);
+  dbus::MessageWriter all_interfaces(nullptr);
+  dbus::MessageWriter each_interface(nullptr);
+
+  response_writer.OpenArray("{oa{sa{sv}}}", &all_object_paths);
+  for (const auto path_pair : registered_objects_) {
+    const dbus::ObjectPath& path = path_pair.first;
+    const InterfaceProperties& interface2properties = path_pair.second;
+    all_object_paths.OpenDictEntry(&each_object_path);
+    each_object_path.AppendObjectPath(path);
+    each_object_path.OpenArray("{sa{sv}}", &all_interfaces);
+    for (const auto interface : interface2properties) {
+      const std::string& interface_name = interface.first;
+      const PropertyWriter& property_writer = interface.second;
+      all_interfaces.OpenDictEntry(&each_interface);
+      each_interface.AppendString(interface_name);
+      property_writer.Run(&each_interface);
+      all_interfaces.CloseContainer(&each_interface);
+    }
+    each_object_path.CloseContainer(&all_interfaces);
+    all_object_paths.CloseContainer(&each_object_path);
+  }
+  response_writer.CloseContainer(&all_object_paths);
+  response_sender.Run(response.Pass());
+}
+
+}  //  namespace dbus_utils
+
+}  //  namespace buffet
diff --git a/buffet/exported_object_manager.h b/buffet/exported_object_manager.h
new file mode 100644
index 0000000..14a0b1a
--- /dev/null
+++ b/buffet/exported_object_manager.h
@@ -0,0 +1,118 @@
+// 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 EXPORTED_OBJECT_MANAGER_H_
+#define EXPORTED_OBJECT_MANAGER_H_
+
+#include <map>
+#include <string>
+
+#include <base/memory/weak_ptr.h>
+#include <dbus/bus.h>
+#include <dbus/exported_object.h>
+#include <dbus/object_path.h>
+
+#include "buffet/exported_property_set.h"
+
+namespace buffet {
+
+namespace dbus_utils {
+
+// ExportedObjectManager is a delegate that implements the
+// org.freedesktop.DBus.ObjectManager interface on behalf of another
+// object. It handles sending signals when new interfaces are added.
+//
+// This class is very similar to the ExportedPropertySet class, except that
+// it allows objects to expose an object manager interface rather than the
+// properties interface.
+//
+//  Example usage:
+//
+//   class ExampleObjectManager {
+//    public:
+//     ExampleObjectManager(dbus::Bus* bus)
+//         : object_manager_(bus, "/my/objects/path") { }
+//
+//     void Init(const OnInitFinish& cb) { object_manager_.Init(cb); }
+//     void ClaimInterface(const dbus::ObjectPath& path,
+//                         const std::string& interface_name,
+//                         const PropertyWriter& writer) {
+//       object_manager_->ClaimInterface(...);
+//     }
+//     void ReleaseInterface(const dbus::ObjectPath& path,
+//                           const std::string& interface_name) {
+//       object_manager_->ReleaseInterface(...);
+//     }
+//
+//    private:
+//     ExportedObjectManager object_manager_;
+//   };
+//
+//   class MyObjectClaimingAnInterface {
+//    public:
+//     MyObjectClaimingAnInterface(ExampleObjectManager* object_manager)
+//       : object_manager_(object_manager) {}
+//
+//     void OnInitFinish(bool success) {
+//       if (!success) { /* handle that */ }
+//       object_manager_->ClaimInterface(
+//           my_path_, my_interface_, my_properties_.GetWriter());
+//     }
+//
+//    private:
+//     struct Properties : public ExportedPropertySet {
+//      public:
+//       /* Lots of interesting properties. */
+//     };
+//
+//     Properties my_properties_;
+//     ExampleObjectManager* object_manager_;
+//   };
+class ExportedObjectManager {
+ public:
+  // Writes a dictionary of property name to property value variants to writer.
+  typedef base::Callback<void(dbus::MessageWriter* writer)> PropertyWriter;
+  typedef base::Callback<void(bool success)> OnInitFinish;
+  typedef std::map<std::string, PropertyWriter> InterfaceProperties;
+
+  ExportedObjectManager(dbus::Bus* bus, const dbus::ObjectPath& path);
+
+  // Registers methods implementing the ObjectManager interface on the object
+  // exported on the path given in the constructor. Must be called on the
+  // origin thread.
+  void Init(const OnInitFinish& cb);
+
+  // Trigger a signal that |path| has added an interface |interface_name|
+  // with properties as given by |writer|.
+  void ClaimInterface(const dbus::ObjectPath& path,
+                      const std::string& interface_name,
+                      const PropertyWriter& writer);
+
+  // Trigger a signal that |path| has removed an interface |interface_name|.
+  void ReleaseInterface(const dbus::ObjectPath& path,
+                        const std::string& interface_name);
+
+ private:
+  void HandleGetManagedObjects(
+      dbus::MethodCall* method_call,
+      dbus::ExportedObject::ResponseSender response_sender) const;
+
+  // Both |bus_| and |exported_object_| outlive *this.
+  dbus::Bus* const bus_;
+  dbus::ExportedObject* const exported_object_;
+  // Tracks all objects currently known to the ExportedObjectManager.
+  std::map<dbus::ObjectPath, InterfaceProperties> registered_objects_;
+
+  // We're going to register DBus callbacks that will outlive ourselves.
+  // These callbacks get scheduled on the origin thread.
+  base::WeakPtrFactory<ExportedObjectManager> weak_ptr_factory_;
+  friend class ExportedObjectManagerTest;
+  DISALLOW_COPY_AND_ASSIGN(ExportedObjectManager);
+};
+
+}  //  namespace dbus_utils
+
+}  //  namespace buffet
+
+#endif  // EXPORTED_OBJECT_MANAGER_H_
diff --git a/buffet/exported_object_manager_unittest.cc b/buffet/exported_object_manager_unittest.cc
new file mode 100644
index 0000000..696b76f
--- /dev/null
+++ b/buffet/exported_object_manager_unittest.cc
@@ -0,0 +1,205 @@
+// 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 "buffet/exported_object_manager.h"
+
+#include <base/bind.h>
+#include <dbus/mock_bus.h>
+#include <dbus/mock_exported_object.h>
+#include <dbus/object_manager.h>
+#include <dbus/object_path.h>
+#include <gtest/gtest.h>
+
+using ::testing::AnyNumber;
+using ::testing::InSequence;
+using ::testing::Invoke;
+using ::testing::Return;
+using ::testing::_;
+
+namespace buffet {
+
+namespace dbus_utils {
+
+namespace {
+
+const dbus::ObjectPath kTestPath(std::string("/test/om_path"));
+const dbus::ObjectPath kClaimedTestPath(std::string("/test/claimed_path"));
+const std::string kClaimedInterface("claimed.interface");
+const std::string kTestPropertyName("PropertyName");
+const std::string kTestPropertyValue("PropertyValue");
+
+void WriteTestPropertyDict(dbus::MessageWriter* writer) {
+  dbus::MessageWriter all_properties(nullptr);
+  dbus::MessageWriter each_property(nullptr);
+  writer->OpenArray("{sv}", &all_properties);
+  all_properties.OpenDictEntry(&each_property);
+  each_property.AppendString(kTestPropertyName);
+  each_property.AppendVariantOfString(kTestPropertyValue);
+  all_properties.CloseContainer(&each_property);
+  writer->CloseContainer(&all_properties);
+}
+
+void ReadTestPropertyDict(dbus::MessageReader* reader) {
+  dbus::MessageReader all_properties(nullptr);
+  dbus::MessageReader each_property(nullptr);
+  ASSERT_TRUE(reader->PopArray(&all_properties));
+  ASSERT_TRUE(all_properties.PopDictEntry(&each_property));
+  std::string property_name;
+  std::string property_value;
+  ASSERT_TRUE(each_property.PopString(&property_name));
+  ASSERT_TRUE(each_property.PopVariantOfString(&property_value));
+  EXPECT_FALSE(each_property.HasMoreData());
+  EXPECT_FALSE(all_properties.HasMoreData());
+  EXPECT_EQ(property_name, kTestPropertyName);
+  EXPECT_EQ(property_value, kTestPropertyValue);
+}
+
+void VerifyInterfaceClaimSignal(dbus::Signal* signal) {
+  EXPECT_EQ(signal->GetInterface(),
+            std::string(dbus::kObjectManagerInterface));
+  EXPECT_EQ(signal->GetMember(),
+            std::string(dbus::kObjectManagerInterfacesAdded));
+  //   org.freedesktop.DBus.ObjectManager.InterfacesAdded (
+  //       OBJPATH object_path,
+  //       DICT<STRING,DICT<STRING,VARIANT>> interfaces_and_properties);
+  dbus::MessageReader reader(signal);
+  dbus::MessageReader all_interfaces(nullptr);
+  dbus::MessageReader each_interface(nullptr);
+  dbus::ObjectPath path;
+  ASSERT_TRUE(reader.PopObjectPath(&path));
+  ASSERT_TRUE(reader.PopArray(&all_interfaces));
+  ASSERT_TRUE(all_interfaces.PopDictEntry(&each_interface));
+  std::string interface_name;
+  ASSERT_TRUE(each_interface.PopString(&interface_name));
+  ReadTestPropertyDict(&each_interface);
+  EXPECT_FALSE(each_interface.HasMoreData());
+  EXPECT_FALSE(all_interfaces.HasMoreData());
+  EXPECT_FALSE(reader.HasMoreData());
+  EXPECT_EQ(interface_name, kClaimedInterface);
+  EXPECT_EQ(path, kClaimedTestPath);
+}
+
+void VerifyInterfaceDropSignal(dbus::Signal* signal) {
+  EXPECT_EQ(signal->GetInterface(),
+            std::string(dbus::kObjectManagerInterface));
+  EXPECT_EQ(signal->GetMember(),
+            std::string(dbus::kObjectManagerInterfacesRemoved));
+  //   org.freedesktop.DBus.ObjectManager.InterfacesRemoved (
+  //       OBJPATH object_path, ARRAY<STRING> interfaces);
+  dbus::MessageReader reader(signal);
+  dbus::MessageReader each_interface(nullptr);
+  dbus::ObjectPath path;
+  ASSERT_TRUE(reader.PopObjectPath(&path));
+  ASSERT_TRUE(reader.PopArray(&each_interface));
+  std::string interface_name;
+  ASSERT_TRUE(each_interface.PopString(&interface_name));
+  EXPECT_FALSE(each_interface.HasMoreData());
+  EXPECT_FALSE(reader.HasMoreData());
+  EXPECT_EQ(interface_name, kClaimedInterface);
+  EXPECT_EQ(path, kClaimedTestPath);
+}
+
+}  // namespace
+
+class ExportedObjectManagerTest: public ::testing::Test {
+ public:
+  virtual void SetUp() {
+    dbus::Bus::Options options;
+    options.bus_type = dbus::Bus::SYSTEM;
+    bus_ = new dbus::MockBus(options);
+    // By default, don't worry about threading assertions.
+    EXPECT_CALL(*bus_, AssertOnOriginThread()).Times(AnyNumber());
+    EXPECT_CALL(*bus_, AssertOnDBusThread()).Times(AnyNumber());
+    // Use a mock exported object.
+    mock_exported_object_ = new dbus::MockExportedObject(
+        bus_.get(), kTestPath);
+    EXPECT_CALL(*bus_, GetExportedObject(kTestPath))
+        .Times(1).WillOnce(Return(mock_exported_object_.get()));
+    om_.reset(new ExportedObjectManager(bus_.get(), kTestPath));
+    property_writer_ = base::Bind(&WriteTestPropertyDict);
+    response_storer_ = base::Bind(&ExportedObjectManagerTest::StoreResponse,
+                                  base::Unretained(this));
+  }
+
+  void StoreResponse(scoped_ptr<dbus::Response> method_response) {
+    last_response_.reset(method_response.release());
+  }
+
+  void CallHandleGetManagedObjects(
+      dbus::MethodCall* method_call,
+      dbus::ExportedObject::ResponseSender sender) {
+    om_->HandleGetManagedObjects(method_call, response_storer_);
+  }
+
+  scoped_refptr<dbus::MockBus> bus_;
+  scoped_refptr<dbus::MockExportedObject> mock_exported_object_;
+  scoped_ptr<ExportedObjectManager> om_;
+  ExportedObjectManager::PropertyWriter property_writer_;
+  dbus::ExportedObject::ResponseSender response_storer_;
+  scoped_ptr<dbus::Response> last_response_;
+};
+
+TEST_F(ExportedObjectManagerTest, ClaimInterfaceSendsSignals) {
+  EXPECT_CALL(*mock_exported_object_, SendSignal(_))
+      .Times(1).WillOnce(Invoke(&VerifyInterfaceClaimSignal));
+  om_->ClaimInterface(kClaimedTestPath, kClaimedInterface, property_writer_);
+}
+
+TEST_F(ExportedObjectManagerTest, ReleaseInterfaceSendsSignals) {
+  InSequence dummy;
+  EXPECT_CALL(*mock_exported_object_, SendSignal(_)).Times(1);
+  EXPECT_CALL(*mock_exported_object_, SendSignal(_))
+      .Times(1).WillOnce(Invoke(&VerifyInterfaceDropSignal));
+  om_->ClaimInterface(kClaimedTestPath, kClaimedInterface, property_writer_);
+  om_->ReleaseInterface(kClaimedTestPath, kClaimedInterface);
+}
+
+TEST_F(ExportedObjectManagerTest, GetManagedObjectsResponseEmptyCorrectness) {
+  dbus::MethodCall method_call(dbus::kObjectManagerInterface,
+                               dbus::kObjectManagerGetManagedObjects);
+  method_call.SetSerial(123);
+  CallHandleGetManagedObjects(&method_call, response_storer_);
+  dbus::MessageReader reader(last_response_.get());
+  dbus::MessageReader all_paths(nullptr);
+  ASSERT_TRUE(reader.PopArray(&all_paths));
+  EXPECT_FALSE(reader.HasMoreData());
+}
+
+TEST_F(ExportedObjectManagerTest, GetManagedObjectsResponseCorrectness) {
+  // org.freedesktop.DBus.ObjectManager.GetManagedObjects (
+  //     out DICT<OBJPATH,
+  //              DICT<STRING,
+  //                   DICT<STRING,VARIANT>>> )
+  dbus::MethodCall method_call(dbus::kObjectManagerInterface,
+                               dbus::kObjectManagerGetManagedObjects);
+  method_call.SetSerial(123);
+  EXPECT_CALL(*mock_exported_object_, SendSignal(_)).Times(1);
+  om_->ClaimInterface(kClaimedTestPath, kClaimedInterface, property_writer_);
+  CallHandleGetManagedObjects(&method_call, response_storer_);
+  dbus::MessageReader reader(last_response_.get());
+  dbus::MessageReader all_paths(nullptr);
+  dbus::MessageReader each_path(nullptr);
+  dbus::MessageReader all_interfaces(nullptr);
+  dbus::MessageReader each_interface(nullptr);
+  ASSERT_TRUE(reader.PopArray(&all_paths));
+  ASSERT_TRUE(all_paths.PopDictEntry(&each_path));
+  dbus::ObjectPath path;
+  ASSERT_TRUE(each_path.PopObjectPath(&path));
+  ASSERT_TRUE(each_path.PopArray(&all_interfaces));
+  ASSERT_TRUE(all_interfaces.PopDictEntry(&each_interface));
+  std::string interface_name;
+  ASSERT_TRUE(each_interface.PopString(&interface_name));
+  ReadTestPropertyDict(&each_interface);
+  EXPECT_FALSE(each_interface.HasMoreData());
+  EXPECT_FALSE(all_interfaces.HasMoreData());
+  EXPECT_FALSE(each_path.HasMoreData());
+  EXPECT_FALSE(all_paths.HasMoreData());
+  EXPECT_FALSE(reader.HasMoreData());
+  EXPECT_EQ(path, kClaimedTestPath);
+  EXPECT_EQ(interface_name, kClaimedInterface);
+}
+
+}  // namespace dbus_utils
+
+}  // namespace buffet
diff --git a/buffet/exported_property_set.cc b/buffet/exported_property_set.cc
index f6f7037..ba6b2c7 100644
--- a/buffet/exported_property_set.cc
+++ b/buffet/exported_property_set.cc
@@ -84,28 +84,33 @@
         GetBadArgsError(method_call, "Too many arguments to GetAll."));
     return;
   }
-  auto property_map_itr = properties_.find(interface_name);
-  if (property_map_itr == properties_.end()) {
-    response_sender.Run(
-        GetBadArgsError(method_call, "No such interface on object."));
-    return;
-  }
   scoped_ptr<dbus::Response> response(
       dbus::Response::FromMethodCall(method_call));
   dbus::MessageWriter resp_writer(response.get());
-  dbus::MessageWriter dict_writer(nullptr);
-  resp_writer.OpenArray("{sv}", &dict_writer);
-  for (const auto& kv : property_map_itr->second) {
-    dbus::MessageWriter entry_writer(nullptr);
-    dict_writer.OpenDictEntry(&entry_writer);
-    entry_writer.AppendString(kv.first);
-    kv.second->AppendValueToWriter(&entry_writer);
-    dict_writer.CloseContainer(&entry_writer);
-  }
-  resp_writer.CloseContainer(&dict_writer);
+  WritePropertiesDictToMessage(interface_name, &resp_writer);
   response_sender.Run(response.Pass());
 }
 
+void ExportedPropertySet::WritePropertiesDictToMessage(
+    const std::string& interface_name,
+    dbus::MessageWriter* writer) {
+  dbus::MessageWriter dict_writer(nullptr);
+  writer->OpenArray("{sv}", &dict_writer);
+  auto property_map_itr = properties_.find(interface_name);
+  if (property_map_itr != properties_.end()) {
+    for (const auto& kv : property_map_itr->second) {
+      dbus::MessageWriter entry_writer(nullptr);
+      dict_writer.OpenDictEntry(&entry_writer);
+      entry_writer.AppendString(kv.first);
+      kv.second->AppendValueToWriter(&entry_writer);
+      dict_writer.CloseContainer(&entry_writer);
+    }
+  } else {
+    LOG(WARNING) << "No properties found for interface interface_name";
+  }
+  writer->CloseContainer(&dict_writer);
+}
+
 void ExportedPropertySet::HandleGet(
     dbus::MethodCall* method_call,
     dbus::ExportedObject::ResponseSender response_sender) {
diff --git a/buffet/exported_property_set.h b/buffet/exported_property_set.h
index c464a8b..8e535b6 100644
--- a/buffet/exported_property_set.h
+++ b/buffet/exported_property_set.h
@@ -11,7 +11,6 @@
 #include <base/memory/weak_ptr.h>
 #include <dbus/exported_object.h>
 #include <dbus/message.h>
-#include <gtest/gtest_prod.h>
 
 namespace buffet {
 
@@ -105,6 +104,8 @@
   // are exported to the DBus object.  |cb| will be called on the origin
   // thread.
   void Init(const OnInitFinish& cb);
+  base::Callback<void(dbus::MessageWriter* writer)> GetPropertyWriter(
+      const std::string& interface);
 
  protected:
   void RegisterProperty(const std::string& interface_name,
@@ -112,6 +113,11 @@
                         ExportedPropertyBase* exported_property);
 
  private:
+  // Used to write the dictionary of string->variant to a message.
+  // This dictionary represents the property name/value pairs for the
+  // given interface.
+  void WritePropertiesDictToMessage(const std::string& interface_name,
+                                    dbus::MessageWriter* writer);
   void HandleGetAll(dbus::MethodCall* method_call,
                     dbus::ExportedObject::ResponseSender response_sender);
   void HandleGet(dbus::MethodCall* method_call,
diff --git a/buffet/exported_property_set_unittest.cc b/buffet/exported_property_set_unittest.cc
index 959a46a..7687371 100644
--- a/buffet/exported_property_set_unittest.cc
+++ b/buffet/exported_property_set_unittest.cc
@@ -201,11 +201,22 @@
 }
 
 TEST_F(ExportedPropertySetTest, GetAllInvalidInterface) {
-  dbus::MethodCall method_call(dbus::kPropertiesInterface,
-                               dbus::kPropertiesGetAll);
+  dbus::MethodCall method_call(
+      dbus::kPropertiesInterface, dbus::kPropertiesGetAll);
+  method_call.SetSerial(123);
   dbus::MessageWriter writer(&method_call);
   writer.AppendString("org.chromium.BadInterface");
-  AssertGetAllReturnsError(&method_call);
+  auto response_sender = base::Bind(&ExportedPropertySetTest::StoreResponse,
+                                    base::Unretained(this));
+  p_->CallHandleGetAll(&method_call, response_sender);
+  dbus::MessageReader response_reader(last_response_.get());
+  dbus::MessageReader dict_reader(nullptr);
+  ASSERT_TRUE(response_reader.PopArray(&dict_reader));
+  // The response should just be a an empty array, since there are no properties
+  // on this interface.  The spec doesn't say much about error conditions here,
+  // so I'm going to assume this is a valid implementation.
+  ASSERT_FALSE(dict_reader.HasMoreData());
+  ASSERT_FALSE(response_reader.HasMoreData());
 }
 
 TEST_F(ExportedPropertySetTest, GetAllExtraArgs) {