face: scoped prefix registration

refs #3919

Change-Id: I858bf38000014a295cf853635b9c2a4275267d32
diff --git a/ndn-cxx/detail/cancel-handle.cpp b/ndn-cxx/detail/cancel-handle.cpp
new file mode 100644
index 0000000..604522b
--- /dev/null
+++ b/ndn-cxx/detail/cancel-handle.cpp
@@ -0,0 +1,79 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2019 Regents of the University of California.
+ *
+ * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
+ *
+ * ndn-cxx library is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
+ *
+ * You should have received copies of the GNU General Public License and GNU Lesser
+ * General Public License along with ndn-cxx, e.g., in COPYING.md file.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * See AUTHORS.md for complete list of ndn-cxx authors and contributors.
+ */
+
+#include "ndn-cxx/detail/cancel-handle.hpp"
+
+namespace ndn {
+namespace detail {
+
+CancelHandle::CancelHandle(function<void()> cancel)
+  : doCancel(std::move(cancel))
+{
+}
+
+void
+CancelHandle::cancel()
+{
+  if (doCancel != nullptr) {
+    doCancel();
+    doCancel = nullptr;
+  }
+}
+
+ScopedCancelHandle::ScopedCancelHandle(CancelHandle hdl)
+  : m_hdl(std::move(hdl))
+{
+}
+
+ScopedCancelHandle::ScopedCancelHandle(ScopedCancelHandle&& other)
+  : m_hdl(other.release())
+{
+}
+
+ScopedCancelHandle&
+ScopedCancelHandle::operator=(ScopedCancelHandle&& other)
+{
+  cancel();
+  m_hdl = other.release();
+  return *this;
+}
+
+ScopedCancelHandle::~ScopedCancelHandle()
+{
+  m_hdl.cancel();
+}
+
+void
+ScopedCancelHandle::cancel()
+{
+  release().cancel();
+}
+
+CancelHandle
+ScopedCancelHandle::release()
+{
+  CancelHandle hdl;
+  std::swap(hdl, m_hdl);
+  return hdl;
+}
+
+} // namespace detail
+} // namespace ndn
diff --git a/ndn-cxx/detail/cancel-handle.hpp b/ndn-cxx/detail/cancel-handle.hpp
new file mode 100644
index 0000000..11af4e4
--- /dev/null
+++ b/ndn-cxx/detail/cancel-handle.hpp
@@ -0,0 +1,101 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2019 Regents of the University of California.
+ *
+ * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
+ *
+ * ndn-cxx library is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
+ *
+ * You should have received copies of the GNU General Public License and GNU Lesser
+ * General Public License along with ndn-cxx, e.g., in COPYING.md file.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * See AUTHORS.md for complete list of ndn-cxx authors and contributors.
+ */
+
+#ifndef NDN_DETAIL_CANCEL_HANDLE_HPP
+#define NDN_DETAIL_CANCEL_HANDLE_HPP
+
+#include "ndn-cxx/detail/common.hpp"
+
+namespace ndn {
+namespace detail {
+
+/** \brief Handle to cancel an operation.
+ */
+class CancelHandle
+{
+public:
+  explicit
+  CancelHandle(function<void()> cancel = nullptr);
+
+  /** \brief Cancel the operation.
+   *  \warning Cancelling the same operation more than once, using same or different CancelHandle or
+   *           ScopedCancelHandle, may trigger undefined behavior.
+   */
+  void
+  cancel();
+
+protected:
+  function<void()> doCancel;
+};
+
+/** \brief Cancels an operation automatically upon destruction.
+ */
+class ScopedCancelHandle
+{
+public:
+  ScopedCancelHandle() = default;
+
+  /** \brief Implicit constructor from CancelHandle.
+   */
+  ScopedCancelHandle(CancelHandle hdl);
+
+  /** \brief Copy construction is disallowed.
+   */
+  ScopedCancelHandle(const ScopedCancelHandle&) = delete;
+
+  /** \brief Move constructor.
+   */
+  ScopedCancelHandle(ScopedCancelHandle&& other);
+
+  /** \brief Copy assignment is disallowed.
+   */
+  ScopedCancelHandle&
+  operator=(const ScopedCancelHandle&) = delete;
+
+  /** \brief Move assignment operator.
+   */
+  ScopedCancelHandle&
+  operator=(ScopedCancelHandle&& other);
+
+  /** \brief Cancel the operation.
+   */
+  ~ScopedCancelHandle();
+
+  /** \brief Cancel the operation.
+   */
+  void
+  cancel();
+
+  /** \brief Release the operation so that it won't be cancelled when this ScopedCancelHandle is
+   *         destructed.
+   *  \return the CancelHandle.
+   */
+  CancelHandle
+  release();
+
+private:
+  CancelHandle m_hdl;
+};
+
+} // namespace detail
+} // namespace ndn
+
+#endif // NDN_DETAIL_CANCEL_HANDLE_HPP
diff --git a/ndn-cxx/face.cpp b/ndn-cxx/face.cpp
index c4dc023..4e09421 100644
--- a/ndn-cxx/face.cpp
+++ b/ndn-cxx/face.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2018 Regents of the University of California.
+ * Copyright (c) 2013-2019 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -267,7 +267,7 @@
   return reinterpret_cast<const InterestFilterId*>(filter.get());
 }
 
-const RegisteredPrefixId*
+RegisteredPrefixHandle
 Face::registerPrefix(const Name& prefix,
                      const RegisterPrefixSuccessCallback& onSuccess,
                      const RegisterPrefixFailureCallback& onFailure,
@@ -277,7 +277,8 @@
   nfd::CommandOptions options;
   options.setSigningInfo(signingInfo);
 
-  return m_impl->registerPrefix(prefix, nullptr, onSuccess, onFailure, flags, options);
+  auto id = m_impl->registerPrefix(prefix, nullptr, onSuccess, onFailure, flags, options);
+  return RegisteredPrefixHandle(*this, id);
 }
 
 void
@@ -410,4 +411,29 @@
   }
 }
 
+RegisteredPrefixHandle::RegisteredPrefixHandle(Face& face, const RegisteredPrefixId* id)
+  : CancelHandle([&face, id] { face.unregisterPrefix(id, nullptr, nullptr); })
+  , m_face(&face)
+  , m_id(id)
+{
+  // The lambda passed to CancelHandle constructor cannot call this->unregister,
+  // because base class destructor cannot access the member fields of this subclass.
+}
+
+void
+RegisteredPrefixHandle::unregister(const UnregisterPrefixSuccessCallback& onSuccess,
+                                   const UnregisterPrefixFailureCallback& onFailure)
+{
+  if (m_id == nullptr) {
+    if (onFailure != nullptr) {
+      onFailure("RegisteredPrefixHandle is empty");
+    }
+    return;
+  }
+
+  m_face->unregisterPrefix(m_id, onSuccess, onFailure);
+  m_face = nullptr;
+  m_id = nullptr;
+}
+
 } // namespace ndn
diff --git a/ndn-cxx/face.hpp b/ndn-cxx/face.hpp
index 434e947..b33abd4 100644
--- a/ndn-cxx/face.hpp
+++ b/ndn-cxx/face.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2018 Regents of the University of California.
+ * Copyright (c) 2013-2019 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -26,6 +26,7 @@
 #include "ndn-cxx/interest.hpp"
 #include "ndn-cxx/interest-filter.hpp"
 #include "ndn-cxx/detail/asio-fwd.hpp"
+#include "ndn-cxx/detail/cancel-handle.hpp"
 #include "ndn-cxx/encoding/nfd-constants.hpp"
 #include "ndn-cxx/lp/nack.hpp"
 #include "ndn-cxx/security/key-chain.hpp"
@@ -37,6 +38,7 @@
 
 class PendingInterestId;
 class RegisteredPrefixId;
+class RegisteredPrefixHandle;
 class InterestFilterId;
 
 namespace nfd {
@@ -345,7 +347,7 @@
    * @return The registered prefix ID which can be used with unregisterPrefix
    * @see nfd::RouteFlags
    */
-  const RegisteredPrefixId*
+  RegisteredPrefixHandle
   registerPrefix(const Name& prefix,
                  const RegisterPrefixSuccessCallback& onSuccess,
                  const RegisterPrefixFailureCallback& onFailure,
@@ -533,6 +535,57 @@
   shared_ptr<Impl> m_impl;
 };
 
+/** \brief A handle of registered prefix.
+ */
+class RegisteredPrefixHandle : public detail::CancelHandle
+{
+public:
+  RegisteredPrefixHandle() = default;
+
+  RegisteredPrefixHandle(Face& face, const RegisteredPrefixId* id);
+
+  operator const RegisteredPrefixId*() const
+  {
+    return m_id;
+  }
+
+  /** \brief Unregister the prefix.
+   *  \warning Unregistering the same prefix more than once, using same or different
+   *           RegisteredPrefixHandle or ScopedRegisteredPrefixHandle, may trigger undefined
+   *           behavior.
+   *  \warning Unregistering a prefix after the face has been destructed may trigger undefined
+   *           behavior.
+   */
+  void
+  unregister(const UnregisterPrefixSuccessCallback& onSuccess = nullptr,
+             const UnregisterPrefixFailureCallback& onFailure = nullptr);
+
+private:
+  Face* m_face = nullptr;
+  const RegisteredPrefixId* m_id = nullptr;
+};
+
+/** \brief A scoped handle of registered prefix.
+ *
+ *  Upon destruction of this handle, the prefix is unregistered automatically.
+ *  Most commonly, the application keeps a ScopedRegisteredPrefixHandle as a class member field,
+ *  so that it can cleanup its prefix registration when the class instance is destructed.
+ *  The application will not be notified whether the unregistration was successful.
+ *
+ *  \code
+ *  {
+ *    ScopedRegisteredPrefixHandle hdl = face.registerPrefix(prefix, onSuccess, onFailure);
+ *  } // hdl goes out of scope, unregistering the prefix
+ *  \endcode
+ *
+ *  \warning Unregistering the same prefix more than once, using same or different
+ *           RegisteredPrefixHandle or ScopedRegisteredPrefixHandle, may trigger undefined
+ *           behavior.
+ *  \warning Unregistering a prefix after the face has been destructed may trigger undefined
+ *           behavior.
+ */
+using ScopedRegisteredPrefixHandle = detail::ScopedCancelHandle;
+
 } // namespace ndn
 
 #endif // NDN_FACE_HPP
diff --git a/ndn-cxx/mgmt/dispatcher.cpp b/ndn-cxx/mgmt/dispatcher.cpp
index d17735f..2c33ef9 100644
--- a/ndn-cxx/mgmt/dispatcher.cpp
+++ b/ndn-cxx/mgmt/dispatcher.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2018 Regents of the University of California.
+ * Copyright (c) 2013-2019 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -103,8 +103,8 @@
   }
 
   const TopPrefixEntry& topPrefixEntry = it->second;
-  if (topPrefixEntry.registeredPrefixId) {
-    m_face.unregisterPrefix(*topPrefixEntry.registeredPrefixId, nullptr, nullptr);
+  if (topPrefixEntry.registeredPrefixId != nullptr) {
+    m_face.unregisterPrefix(topPrefixEntry.registeredPrefixId, nullptr, nullptr);
   }
   for (const auto& filter : topPrefixEntry.interestFilters) {
     m_face.unsetInterestFilter(filter);
diff --git a/ndn-cxx/mgmt/dispatcher.hpp b/ndn-cxx/mgmt/dispatcher.hpp
index 2ec308b..ab96c00 100644
--- a/ndn-cxx/mgmt/dispatcher.hpp
+++ b/ndn-cxx/mgmt/dispatcher.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2018 Regents of the University of California.
+ * Copyright (c) 2013-2019 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -440,7 +440,7 @@
   struct TopPrefixEntry
   {
     Name topPrefix;
-    optional<const RegisteredPrefixId*> registeredPrefixId;
+    const RegisteredPrefixId* registeredPrefixId = nullptr;
     std::vector<const InterestFilterId*> interestFilters;
   };
   std::unordered_map<Name, TopPrefixEntry> m_topLevelPrefixes;
diff --git a/tests/unit/detail/cancel-handle.t.cpp b/tests/unit/detail/cancel-handle.t.cpp
new file mode 100644
index 0000000..b87f9cd
--- /dev/null
+++ b/tests/unit/detail/cancel-handle.t.cpp
@@ -0,0 +1,137 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2019 Regents of the University of California.
+ *
+ * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
+ *
+ * ndn-cxx library is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
+ *
+ * You should have received copies of the GNU General Public License and GNU Lesser
+ * General Public License along with ndn-cxx, e.g., in COPYING.md file.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * See AUTHORS.md for complete list of ndn-cxx authors and contributors.
+ */
+
+#include "ndn-cxx/detail/cancel-handle.hpp"
+
+#include "tests/boost-test.hpp"
+
+namespace ndn {
+namespace detail {
+namespace tests {
+
+BOOST_AUTO_TEST_SUITE(Detail)
+BOOST_AUTO_TEST_SUITE(TestCancelHandle)
+
+static CancelHandle
+makeDummyCancelHandle(int& nCancels)
+{
+  return CancelHandle([&] { ++nCancels; });
+}
+
+BOOST_AUTO_TEST_SUITE(PlainHandle)
+
+BOOST_AUTO_TEST_CASE(ManualCancel)
+{
+  int nCancels = 0;
+  auto hdl = makeDummyCancelHandle(nCancels);
+  BOOST_CHECK_EQUAL(nCancels, 0);
+
+  hdl.cancel();
+  BOOST_CHECK_EQUAL(nCancels, 1);
+
+  hdl = CancelHandle();
+  BOOST_CHECK_EQUAL(nCancels, 1);
+}
+
+BOOST_AUTO_TEST_SUITE_END() // PlainHandle
+
+BOOST_AUTO_TEST_SUITE(ScopedHandle)
+
+BOOST_AUTO_TEST_CASE(ManualCancel)
+{
+  int nCancels = 0;
+  {
+    ScopedCancelHandle hdl = makeDummyCancelHandle(nCancels);
+    BOOST_CHECK_EQUAL(nCancels, 0);
+
+    hdl.cancel();
+    BOOST_CHECK_EQUAL(nCancels, 1);
+  } // hdl goes out of scope
+  BOOST_CHECK_EQUAL(nCancels, 1);
+}
+
+BOOST_AUTO_TEST_CASE(Destruct)
+{
+  int nCancels = 0;
+  {
+    ScopedCancelHandle hdl = makeDummyCancelHandle(nCancels);
+    BOOST_CHECK_EQUAL(nCancels, 0);
+  } // hdl goes out of scope
+  BOOST_CHECK_EQUAL(nCancels, 1);
+}
+
+BOOST_AUTO_TEST_CASE(Assign)
+{
+  int nCancels1 = 0, nCancels2 = 0;
+  {
+    ScopedCancelHandle hdl = makeDummyCancelHandle(nCancels1);
+    hdl = makeDummyCancelHandle(nCancels2);
+    BOOST_CHECK_EQUAL(nCancels1, 1);
+    BOOST_CHECK_EQUAL(nCancels2, 0);
+  } // hdl goes out of scope
+  BOOST_CHECK_EQUAL(nCancels2, 1);
+}
+
+BOOST_AUTO_TEST_CASE(Release)
+{
+  int nCancels = 0;
+  {
+    ScopedCancelHandle hdl = makeDummyCancelHandle(nCancels);
+    hdl.release();
+    hdl.cancel(); // no effect
+  } // hdl goes out of scope
+  BOOST_CHECK_EQUAL(nCancels, 0);
+}
+
+BOOST_AUTO_TEST_CASE(MoveConstruct)
+{
+  int nCancels = 0;
+  unique_ptr<ScopedCancelHandle> hdl1;
+  {
+    ScopedCancelHandle hdl2 = makeDummyCancelHandle(nCancels);
+    hdl1 = make_unique<ScopedCancelHandle>(std::move(hdl2));
+  } // hdl2 goes out of scope
+  BOOST_CHECK_EQUAL(nCancels, 0);
+  hdl1.reset();
+  BOOST_CHECK_EQUAL(nCancels, 1);
+}
+
+BOOST_AUTO_TEST_CASE(MoveAssign)
+{
+  int nCancels = 0;
+  {
+    ScopedCancelHandle hdl1;
+    {
+      ScopedCancelHandle hdl2 = makeDummyCancelHandle(nCancels);
+      hdl1 = std::move(hdl2);
+    } // hdl2 goes out of scope
+    BOOST_CHECK_EQUAL(nCancels, 0);
+  } // hdl1 goes out of scope
+  BOOST_CHECK_EQUAL(nCancels, 1);
+}
+
+BOOST_AUTO_TEST_SUITE_END() // ScopedHandle
+BOOST_AUTO_TEST_SUITE_END() // TestCancelHandle
+BOOST_AUTO_TEST_SUITE_END() // Detail
+
+} // namespace tests
+} // namespace detail
+} // namespace ndn
diff --git a/tests/unit/face.t.cpp b/tests/unit/face.t.cpp
index 6f7bb17..c9b8205 100644
--- a/tests/unit/face.t.cpp
+++ b/tests/unit/face.t.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2018 Regents of the University of California.
+ * Copyright (c) 2013-2019 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -30,34 +30,63 @@
 #include "tests/make-interest-data.hpp"
 #include "tests/unit/identity-management-time-fixture.hpp"
 
+#include <boost/logic/tribool.hpp>
+
 namespace ndn {
 namespace tests {
 
 using ndn::util::DummyClientFace;
 
+struct WantPrefixRegReply;
+struct NoPrefixRegReply;
+
+template<typename PrefixRegReply = WantPrefixRegReply>
 class FaceFixture : public IdentityManagementTimeFixture
 {
 public:
-  explicit
-  FaceFixture(bool enableRegistrationReply = true)
-    : face(io, m_keyChain, {true, enableRegistrationReply})
+  FaceFixture()
+    : face(io, m_keyChain, {true, !std::is_same<PrefixRegReply, NoPrefixRegReply>::value})
   {
+    static_assert(std::is_same<PrefixRegReply, WantPrefixRegReply>::value ||
+                  std::is_same<PrefixRegReply, NoPrefixRegReply>::value, "");
+  }
+
+  /** \brief Execute a prefix registration, and optionally check the name in callback.
+   *  \return whether the prefix registration succeeded.
+   */
+  bool
+  runPrefixReg(function<void(const RegisterPrefixSuccessCallback& success,
+                             const RegisterPrefixFailureCallback& failure)> f)
+  {
+    boost::logic::tribool result = boost::logic::indeterminate;
+    f([&] (const Name&) { result = true; },
+      [&] (const Name&, const std::string&) { result = false; });
+
+    advanceClocks(1_ms);
+    BOOST_REQUIRE(!boost::logic::indeterminate(result));
+    return static_cast<bool>(result);
+  }
+
+  /** \brief Execute a prefix unregistration, and optionally check the name in callback.
+   *  \return whether the prefix unregistration succeeded.
+   */
+  bool
+  runPrefixUnreg(function<void(const UnregisterPrefixSuccessCallback& success,
+                               const UnregisterPrefixFailureCallback& failure)> f)
+  {
+    boost::logic::tribool result = boost::logic::indeterminate;
+    f([&] { result = true; }, [&] (const std::string&) { result = false; });
+
+    advanceClocks(1_ms);
+    BOOST_REQUIRE(!boost::logic::indeterminate(result));
+    return static_cast<bool>(result);
   }
 
 public:
   DummyClientFace face;
 };
 
-class FacesNoRegistrationReplyFixture : public FaceFixture
-{
-public:
-  FacesNoRegistrationReplyFixture()
-    : FaceFixture(false)
-  {
-  }
-};
-
-BOOST_FIXTURE_TEST_SUITE(TestFace, FaceFixture)
+BOOST_FIXTURE_TEST_SUITE(TestFace, FaceFixture<>)
 
 BOOST_AUTO_TEST_SUITE(Consumer)
 
@@ -572,7 +601,7 @@
   advanceClocks(25_ms, 4);
 }
 
-BOOST_FIXTURE_TEST_CASE(SetInterestFilterFail, FacesNoRegistrationReplyFixture)
+BOOST_FIXTURE_TEST_CASE(SetInterestFilterFail, FaceFixture<NoPrefixRegReply>)
 {
   // don't enable registration reply
   size_t nRegFailed = 0;
@@ -588,7 +617,7 @@
   BOOST_CHECK_EQUAL(nRegFailed, 1);
 }
 
-BOOST_FIXTURE_TEST_CASE(SetInterestFilterFailWithoutSuccessCallback, FacesNoRegistrationReplyFixture)
+BOOST_FIXTURE_TEST_CASE(SetInterestFilterFailWithoutSuccessCallback, FaceFixture<NoPrefixRegReply>)
 {
   // don't enable registration reply
   size_t nRegFailed = 0;
@@ -603,36 +632,40 @@
   BOOST_CHECK_EQUAL(nRegFailed, 1);
 }
 
-BOOST_AUTO_TEST_CASE(RegisterUnregisterPrefix)
+BOOST_AUTO_TEST_CASE(RegisterUnregisterPrefixFunc)
 {
-  size_t nRegSuccesses = 0;
-  const RegisteredPrefixId* regPrefixId =
-    face.registerPrefix("/Hello/World",
-                        bind([&nRegSuccesses] { ++nRegSuccesses; }),
-                        bind([] { BOOST_FAIL("Unexpected registerPrefix failure"); }));
+  const RegisteredPrefixId* regPrefixId = nullptr;
+  BOOST_CHECK(runPrefixReg([&] (const auto& success, const auto& failure) {
+    regPrefixId = face.registerPrefix("/Hello/World", success, failure);
+  }));
 
-  advanceClocks(25_ms, 4);
-  BOOST_CHECK_EQUAL(nRegSuccesses, 1);
-
-  size_t nUnregSuccesses = 0;
-  face.unregisterPrefix(regPrefixId,
-                        bind([&nUnregSuccesses] { ++nUnregSuccesses; }),
-                        bind([] { BOOST_FAIL("Unexpected unregisterPrefix failure"); }));
-
-  advanceClocks(25_ms, 4);
-  BOOST_CHECK_EQUAL(nUnregSuccesses, 1);
+  BOOST_CHECK(runPrefixUnreg([&] (const auto& success, const auto& failure) {
+    face.unregisterPrefix(regPrefixId, success, failure);
+  }));
 }
 
-BOOST_FIXTURE_TEST_CASE(RegisterUnregisterPrefixFail, FacesNoRegistrationReplyFixture)
+BOOST_FIXTURE_TEST_CASE(RegisterUnregisterPrefixFail, FaceFixture<NoPrefixRegReply>)
 {
-  // don't enable registration reply
-  size_t nRegFailures = 0;
-  face.registerPrefix("/Hello/World",
-                      bind([] { BOOST_FAIL("Unexpected registerPrefix success"); }),
-                      bind([&nRegFailures] { ++nRegFailures; }));
+  BOOST_CHECK(!runPrefixReg([&] (const auto& success, const auto& failure) {
+    face.registerPrefix("/Hello/World", success, failure);
+    this->advanceClocks(5_s, 20); // wait for command timeout
+  }));
+}
+BOOST_AUTO_TEST_CASE(RegisterUnregisterPrefixHandle)
+{
+  RegisteredPrefixHandle hdl;
+  BOOST_CHECK(!runPrefixUnreg([&] (const auto& success, const auto& failure) {
+    // despite the "undefined behavior" warning, we try not to crash, but no API guarantee for this
+    hdl.unregister(success, failure);
+  }));
 
-  advanceClocks(5000_ms, 20);
-  BOOST_CHECK_EQUAL(nRegFailures, 1);
+  BOOST_CHECK(runPrefixReg([&] (const auto& success, const auto& failure) {
+    hdl = face.registerPrefix("/Hello/World", success, failure);
+  }));
+
+  BOOST_CHECK(runPrefixUnreg([&] (const auto& success, const auto& failure) {
+    hdl.unregister(success, failure);
+  }));
 }
 
 BOOST_AUTO_TEST_CASE(SimilarFilters)
@@ -729,7 +762,7 @@
   BOOST_CHECK_EQUAL(nInInterests, 2);
 }
 
-BOOST_FIXTURE_TEST_CASE(SetInterestFilterNoReg, FacesNoRegistrationReplyFixture) // Bug 2318
+BOOST_FIXTURE_TEST_CASE(SetInterestFilterNoReg, FaceFixture<NoPrefixRegReply>) // Bug 2318
 {
   // This behavior is specific to DummyClientFace.
   // Regular Face won't accept incoming packets until something is sent.