mgmt: add new Dispatcher API for control commands

The new registration API is slightly higher level, while providing
better separation of concerns between Dispatcher and ControlCommand.
The latter is now solely responsible for encoding, decoding, and
validation of the command parameters. This should simplify the
implementation of alternative encoding formats for command parameters
in the future.

Change-Id: I3296e7ddf5db2f2def3ae676f66b7c50b6fba957
diff --git a/ndn-cxx/mgmt/dispatcher.cpp b/ndn-cxx/mgmt/dispatcher.cpp
index 5594757..7956198 100644
--- a/ndn-cxx/mgmt/dispatcher.cpp
+++ b/ndn-cxx/mgmt/dispatcher.cpp
@@ -93,7 +93,7 @@
 Dispatcher::checkPrefix(const PartialName& relPrefix) const
 {
   if (!m_topLevelPrefixes.empty()) {
-    NDN_THROW(std::domain_error("one or more top-level prefix has been added"));
+    NDN_THROW(std::domain_error("one or more top-level prefixes have already been added"));
   }
 
   bool hasOverlap = std::any_of(m_handlers.begin(), m_handlers.end(), [&] (const auto& entry) {
@@ -162,22 +162,18 @@
 
 void
 Dispatcher::processCommand(const Name& prefix,
-                           const Name& relPrefix,
                            const Interest& interest,
-                           const ControlParametersParser& parse,
+                           const ParametersParser& parse,
                            const Authorization& authorize,
                            ValidateParameters validate,
                            ControlCommandHandler handler)
 {
-  // /<prefix>/<relPrefix>/<parameters>
-  size_t parametersLoc = prefix.size() + relPrefix.size();
-  const name::Component& pc = interest.getName().get(parametersLoc);
-
   shared_ptr<ControlParameters> parameters;
   try {
-    parameters = parse(pc);
+    parameters = parse(prefix, interest);
   }
-  catch (const tlv::Error&) {
+  catch (const std::exception& e) {
+    NDN_LOG_DEBUG("malformed command " << interest.getName() << ": " << e.what());
     return;
   }
 
@@ -197,9 +193,18 @@
                                      const ValidateParameters& validate,
                                      const ControlCommandHandler& handler)
 {
-  if (validate(*parameters)) {
-    handler(prefix, interest, *parameters,
-            [=] (const auto& resp) { sendControlResponse(resp, interest); });
+  bool ok = false;
+  try {
+    ok = validate(*parameters);
+  }
+  catch (const std::exception& e) {
+    NDN_LOG_DEBUG("invalid parameters for command " << interest.getName() << ": " << e.what());
+  }
+
+  if (ok) {
+    handler(prefix, interest, *parameters, [this, interest] (const auto& resp) {
+      sendControlResponse(resp, interest);
+    });
   }
   else {
     sendControlResponse(ControlResponse(400, "failed in validating parameters"), interest);
diff --git a/ndn-cxx/mgmt/dispatcher.hpp b/ndn-cxx/mgmt/dispatcher.hpp
index 3be18ae..cfc56b8 100644
--- a/ndn-cxx/mgmt/dispatcher.hpp
+++ b/ndn-cxx/mgmt/dispatcher.hpp
@@ -87,11 +87,12 @@
 
 // ---- CONTROL COMMAND ----
 
-/** \brief A function to validate input ControlParameters.
- *  \param params parsed ControlParameters;
- *                This is guaranteed to have correct type for the command.
+/**
+ * \brief A function to validate and normalize the incoming request parameters.
+ * \param params The parsed ControlParameters; guaranteed to be of the correct (sub-)type
+ *               for the command.
  */
-using ValidateParameters = std::function<bool(const ControlParameters& params)>;
+using ValidateParameters = std::function<bool(ControlParameters& params)>;
 
 /**
  * \brief A function to be called after a ControlCommandHandler completes.
@@ -183,7 +184,7 @@
 
 public: // ControlCommand
   /**
-   * \brief Register a ControlCommand.
+   * \brief Register a ControlCommand (old style).
    * \tparam ParametersType Concrete subclass of ControlParameters used by this command.
    * \param relPrefix The name prefix for this command relative to the top-level prefix,
    *                  e.g., "faces/create". The prefixes across all ControlCommands,
@@ -219,17 +220,69 @@
   {
     checkPrefix(relPrefix);
 
-    ControlParametersParser parse = [] (const name::Component& comp) -> shared_ptr<ControlParameters> {
+    auto relPrefixLen = relPrefix.size();
+    ParametersParser parse = [relPrefixLen] (const Name& prefix,
+                                             const auto& interest) -> shared_ptr<ControlParameters> {
+      const name::Component& comp = interest.getName().get(prefix.size() + relPrefixLen);
       return make_shared<ParametersType>(comp.blockFromValue());
     };
 
-    m_handlers[relPrefix] = [this, relPrefix,
-                             parse = std::move(parse),
-                             authorize = std::move(authorize),
-                             validate = std::move(validate),
-                             handle = std::move(handle)] (const auto& prefix, const auto& interest) {
-      processCommand(prefix, relPrefix, interest, parse, authorize,
-                     std::move(validate), std::move(handle));
+    m_handlers[relPrefix] = [this,
+                             parser = std::move(parse),
+                             authorizer = std::move(authorize),
+                             validator = std::move(validate),
+                             handler = std::move(handle)] (const auto& prefix, const auto& interest) {
+      processCommand(prefix, interest, parser, authorizer, std::move(validator), std::move(handler));
+    };
+  }
+
+  /**
+   * \brief Register a ControlCommand (new style).
+   * \tparam Command The type of ControlCommand to register.
+   * \param authorize Callback to authorize the incoming commands.
+   * \param handle Callback to handle the commands.
+   * \pre No top-level prefix has been added.
+   * \throw std::out_of_range \p relPrefix overlaps with an existing relPrefix.
+   * \throw std::domain_error One or more top-level prefixes have been added.
+   *
+   * Procedure for processing a ControlCommand registered through this function:
+   *  1. Extract the parameters from the request by invoking `Command::parseRequest` on the
+   *     incoming Interest; if parsing fails, abort these steps.
+   *  2. Perform authorization; if the authorization is rejected, perform the RejectReply action
+   *     and abort these steps.
+   *  3. Validate the parameters with `Command::validateRequest`.
+   *  4. Normalize the parameters with `Command::applyDefaultsToRequest`.
+   *  5. If either step 3 or 4 fails, create a ControlResponse with StatusCode 400 and go to step 7.
+   *  6. Invoke the command handler, wait until CommandContinuation is called.
+   *  7. Encode the ControlResponse into one Data packet.
+   *  8. Sign the Data packet.
+   *  9. If the Data packet is too large, log an error and abort these steps.
+   * 10. Send the signed Data packet.
+   */
+  template<typename Command>
+  void
+  addControlCommand(Authorization authorize, ControlCommandHandler handle)
+  {
+    auto relPrefix = Command::getName();
+    checkPrefix(relPrefix);
+
+    ParametersParser parse = [] (const Name& prefix, const auto& interest) {
+      return Command::parseRequest(interest, prefix.size());
+    };
+    ValidateParameters validate = [] (auto& params) {
+      auto& reqParams = static_cast<typename Command::RequestParameters&>(params);
+      Command::validateRequest(reqParams);
+      Command::applyDefaultsToRequest(reqParams);
+      // for compatibility with ValidateParameters signature; consider refactoring in the future
+      return true;
+    };
+
+    m_handlers[relPrefix] = [this,
+                             parser = std::move(parse),
+                             authorizer = std::move(authorize),
+                             validator = std::move(validate),
+                             handler = std::move(handle)] (const auto& prefix, const auto& interest) {
+      processCommand(prefix, interest, parser, authorizer, std::move(validator), std::move(handler));
     };
   }
 
@@ -299,11 +352,11 @@
   using InterestHandler = std::function<void(const Name& prefix, const Interest&)>;
 
   /**
-   * @brief The parser for extracting control parameters from a name component.
+   * @brief The parser for extracting the parameters from a command request.
    * @return A shared pointer to the extracted ControlParameters.
-   * @throw tlv::Error if the name component cannot be parsed as ControlParameters
+   * @throw tlv::Error The request parameters cannot be parsed.
    */
-  using ControlParametersParser = std::function<shared_ptr<ControlParameters>(const name::Component&)>;
+  using ParametersParser = std::function<shared_ptr<ControlParameters>(const Name& prefix, const Interest&)>;
 
   void
   checkPrefix(const PartialName& relPrefix) const;
@@ -364,7 +417,6 @@
    * @brief Process an incoming control command Interest before authorization.
    *
    * @param prefix the top-level prefix
-   * @param relPrefix the relative prefix
    * @param interest the incoming Interest
    * @param parse function to extract the control parameters from the command
    * @param authorize function to determine whether the command is authorized
@@ -373,9 +425,8 @@
    */
   void
   processCommand(const Name& prefix,
-                 const Name& relPrefix,
                  const Interest& interest,
-                 const ControlParametersParser& parse,
+                 const ParametersParser& parse,
                  const Authorization& authorize,
                  ValidateParameters validate,
                  ControlCommandHandler handler);
diff --git a/ndn-cxx/mgmt/nfd/control-command.cpp b/ndn-cxx/mgmt/nfd/control-command.cpp
index 5c17a22..31ec2df 100644
--- a/ndn-cxx/mgmt/nfd/control-command.cpp
+++ b/ndn-cxx/mgmt/nfd/control-command.cpp
@@ -47,8 +47,15 @@
   }
 }
 
+shared_ptr<ControlParameters>
+ControlParametersCommandFormat::decode(const Interest& interest, size_t prefixLen)
+{
+  auto block = interest.getName().at(prefixLen).blockFromValue();
+  return make_shared<ControlParameters>(block);
+}
+
 void
-ControlParametersCommandFormat::encode(Interest& interest, const ControlParameters& params) const
+ControlParametersCommandFormat::encode(Interest& interest, const ControlParameters& params)
 {
   auto name = interest.getName();
   name.append(params.wireEncode());
diff --git a/ndn-cxx/mgmt/nfd/control-command.hpp b/ndn-cxx/mgmt/nfd/control-command.hpp
index 2ba628d..f9b15ca 100644
--- a/ndn-cxx/mgmt/nfd/control-command.hpp
+++ b/ndn-cxx/mgmt/nfd/control-command.hpp
@@ -42,7 +42,12 @@
 
 /**
  * \ingroup management
- * \brief Implements encoding and validation of the ControlParameters format for control commands.
+ * \brief Implements decoding, encoding, and validation of ControlParameters in control commands.
+ *
+ * According to this format, the request parameters are encoded as a single GenericNameComponent
+ * in the Interest name, immediately after the command module and command verb components.
+ *
+ * \sa https://redmine.named-data.net/projects/nfd/wiki/ControlCommand
  */
 class ControlParametersCommandFormat
 {
@@ -78,11 +83,17 @@
   validate(const ControlParameters& params) const;
 
   /**
-   * \brief Serializes the parameters into the request \p interest.
+   * \brief Extract the parameters from the request \p interest.
+   */
+  static shared_ptr<ControlParameters>
+  decode(const Interest& interest, size_t prefixLen);
+
+  /**
+   * \brief Serialize the parameters into the request \p interest.
    * \pre \p params are valid.
    */
-  void
-  encode(Interest& interest, const ControlParameters& params) const;
+  static void
+  encode(Interest& interest, const ControlParameters& params);
 
 private:
   std::bitset<CONTROL_PARAMETER_UBOUND> m_required;
@@ -116,6 +127,15 @@
   ControlCommand() = delete;
 
   /**
+   * \brief Return the command name (module + verb).
+   */
+  static PartialName
+  getName()
+  {
+    return PartialName().append(Derived::s_module).append(Derived::s_verb);
+  }
+
+  /**
    * \brief Construct request Interest.
    * \throw ArgumentError if parameters are invalid
    */
@@ -130,6 +150,16 @@
   }
 
   /**
+   * \brief Extract parameters from request Interest.
+   */
+  static shared_ptr<mgmt::ControlParameters>
+  parseRequest(const Interest& interest, size_t prefixLen)
+  {
+    // /<prefix>/<module>/<verb>
+    return Derived::s_requestFormat.decode(interest, prefixLen + 2);
+  }
+
+  /**
    * \brief Validate request parameters.
    * \throw ArgumentError if parameters are invalid
    */
diff --git a/tests/unit/mgmt/control-response.t.cpp b/tests/unit/mgmt/control-response.t.cpp
index 7e72c26..6bda463 100644
--- a/tests/unit/mgmt/control-response.t.cpp
+++ b/tests/unit/mgmt/control-response.t.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2023 Regents of the University of California.
+ * Copyright (c) 2013-2025 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -36,7 +36,7 @@
 BOOST_AUTO_TEST_SUITE(Mgmt)
 BOOST_AUTO_TEST_SUITE(TestControlResponse)
 
-static const uint8_t WIRE[] = {
+const uint8_t WIRE[] = {
   0x65, 0x17, // ControlResponse
         0x66, 0x02, // StatusCode
               0x01, 0x94,
@@ -46,10 +46,21 @@
 
 BOOST_AUTO_TEST_CASE(Encode)
 {
-  ControlResponse cr(404, "Nothing not found");
-  const Block& wire = cr.wireEncode();
-  BOOST_CHECK_EQUAL_COLLECTIONS(WIRE, WIRE + sizeof(WIRE),
-                                wire.begin(), wire.end());
+  ControlResponse cr1(404, "Nothing not found");
+  BOOST_TEST(cr1.wireEncode() == WIRE, boost::test_tools::per_element());
+
+  ControlResponse cr2;
+  cr2.setCode(404);
+  cr2.setText("Nothing not found");
+  BOOST_TEST(cr2.wireEncode() == WIRE, boost::test_tools::per_element());
+
+  ControlResponse cr3(cr1);
+  cr3.setCode(405);
+  BOOST_TEST(cr3.wireEncode() != Block{WIRE});
+
+  ControlResponse cr4(cr1);
+  cr4.setText("foo");
+  BOOST_TEST(cr4.wireEncode() != Block{WIRE});
 }
 
 BOOST_AUTO_TEST_CASE(Decode)
@@ -57,6 +68,26 @@
   ControlResponse cr(Block{WIRE});
   BOOST_CHECK_EQUAL(cr.getCode(), 404);
   BOOST_CHECK_EQUAL(cr.getText(), "Nothing not found");
+
+  // wrong outer TLV type
+  BOOST_CHECK_EXCEPTION(cr.wireDecode("6406660201946700"_block), tlv::Error, [] (const auto& e) {
+    return e.what() == "Expecting ControlResponse element, but TLV has type 100"sv;
+  });
+
+  // empty TLV
+  BOOST_CHECK_EXCEPTION(cr.wireDecode("6500"_block), tlv::Error, [] (const auto& e) {
+    return e.what() == "missing StatusCode sub-element"sv;
+  });
+
+  // missing StatusCode
+  BOOST_CHECK_EXCEPTION(cr.wireDecode("65026700"_block), tlv::Error, [] (const auto& e) {
+    return e.what() == "missing StatusCode sub-element"sv;
+  });
+
+  // missing StatusText
+  BOOST_CHECK_EXCEPTION(cr.wireDecode("650466020194"_block), tlv::Error, [] (const auto& e) {
+    return e.what() == "missing StatusText sub-element"sv;
+  });
 }
 
 BOOST_AUTO_TEST_SUITE_END() // TestControlResponse
diff --git a/tests/unit/mgmt/dispatcher.t.cpp b/tests/unit/mgmt/dispatcher.t.cpp
index ec6f90f..6a80012 100644
--- a/tests/unit/mgmt/dispatcher.t.cpp
+++ b/tests/unit/mgmt/dispatcher.t.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2024 Regents of the University of California.
+ * Copyright (c) 2013-2025 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -20,7 +20,7 @@
  */
 
 #include "ndn-cxx/mgmt/dispatcher.hpp"
-#include "ndn-cxx/mgmt/nfd/control-parameters.hpp"
+#include "ndn-cxx/mgmt/nfd/control-command.hpp"
 #include "ndn-cxx/util/dummy-client-face.hpp"
 
 #include "tests/test-common.hpp"
@@ -85,46 +85,39 @@
 
 BOOST_AUTO_TEST_CASE(Basic)
 {
-  BOOST_CHECK_NO_THROW(dispatcher
-                         .addControlCommand<VoidParameters>("test/1", makeAcceptAllAuthorization(),
-                                                            std::bind([] { return true; }),
-                                                            std::bind([]{})));
-  BOOST_CHECK_NO_THROW(dispatcher
-                         .addControlCommand<VoidParameters>("test/2", makeAcceptAllAuthorization(),
-                                                            std::bind([] { return true; }),
-                                                            std::bind([]{})));
+  auto nop = [] (auto&&...) {};
+  auto tautology = [] (auto&&...) { return true; };
 
+  BOOST_CHECK_NO_THROW(dispatcher
+                       .addControlCommand<VoidParameters>("test/1", makeAcceptAllAuthorization(),
+                                                          tautology, nop));
+  BOOST_CHECK_NO_THROW(dispatcher
+                       .addControlCommand<VoidParameters>("test/2", makeAcceptAllAuthorization(),
+                                                          tautology, nop));
   BOOST_CHECK_THROW(dispatcher
-                      .addControlCommand<VoidParameters>("test", makeAcceptAllAuthorization(),
-                                                         std::bind([] { return true; }),
-                                                         std::bind([]{})),
+                    .addControlCommand<VoidParameters>("test", makeAcceptAllAuthorization(),
+                                                       tautology, nop),
                     std::out_of_range);
 
-  BOOST_CHECK_NO_THROW(dispatcher.addStatusDataset("status/1",
-                                                   makeAcceptAllAuthorization(), std::bind([]{})));
-  BOOST_CHECK_NO_THROW(dispatcher.addStatusDataset("status/2",
-                                                   makeAcceptAllAuthorization(), std::bind([]{})));
-  BOOST_CHECK_THROW(dispatcher.addStatusDataset("status",
-                                                makeAcceptAllAuthorization(), std::bind([]{})),
+  BOOST_CHECK_NO_THROW(dispatcher.addStatusDataset("status/1", makeAcceptAllAuthorization(), nop));
+  BOOST_CHECK_NO_THROW(dispatcher.addStatusDataset("status/2", makeAcceptAllAuthorization(), nop));
+  BOOST_CHECK_THROW(dispatcher.addStatusDataset("status", makeAcceptAllAuthorization(), nop),
                     std::out_of_range);
 
   BOOST_CHECK_NO_THROW(dispatcher.addNotificationStream("stream/1"));
   BOOST_CHECK_NO_THROW(dispatcher.addNotificationStream("stream/2"));
   BOOST_CHECK_THROW(dispatcher.addNotificationStream("stream"), std::out_of_range);
 
-
   BOOST_CHECK_NO_THROW(dispatcher.addTopPrefix("/root/1"));
   BOOST_CHECK_NO_THROW(dispatcher.addTopPrefix("/root/2"));
   BOOST_CHECK_THROW(dispatcher.addTopPrefix("/root"), std::out_of_range);
 
   BOOST_CHECK_THROW(dispatcher
-                      .addControlCommand<VoidParameters>("test/3", makeAcceptAllAuthorization(),
-                                                         std::bind([] { return true; }),
-                                                         std::bind([]{})),
+                    .addControlCommand<VoidParameters>("test/3", makeAcceptAllAuthorization(),
+                                                       tautology, nop),
                     std::domain_error);
 
-  BOOST_CHECK_THROW(dispatcher.addStatusDataset("status/3",
-                                                makeAcceptAllAuthorization(), std::bind([]{})),
+  BOOST_CHECK_THROW(dispatcher.addStatusDataset("status/3", makeAcceptAllAuthorization(), nop),
                     std::domain_error);
 
   BOOST_CHECK_THROW(dispatcher.addNotificationStream("stream/3"), std::domain_error);
@@ -135,13 +128,13 @@
   std::map<std::string, size_t> nCallbackCalled;
   dispatcher
     .addControlCommand<VoidParameters>("test/1", makeAcceptAllAuthorization(),
-                                       std::bind([] { return true; }),
-                                       std::bind([&nCallbackCalled] { ++nCallbackCalled["test/1"]; }));
+                                       [] (auto&&...) { return true; },
+                                       [&nCallbackCalled] (auto&&...) { ++nCallbackCalled["test/1"]; });
 
   dispatcher
     .addControlCommand<VoidParameters>("test/2", makeAcceptAllAuthorization(),
-                                       std::bind([] { return true; }),
-                                       std::bind([&nCallbackCalled] { ++nCallbackCalled["test/2"]; }));
+                                       [] (auto&&...) { return true; },
+                                       [&nCallbackCalled] (auto&&...) { ++nCallbackCalled["test/2"]; });
 
   face.receive(*makeInterest("/root/1/test/1/%80%00"));
   advanceClocks(1_ms);
@@ -190,18 +183,17 @@
   BOOST_CHECK_EQUAL(nCallbackCalled["test/1"], 4);
 }
 
-BOOST_AUTO_TEST_CASE(ControlCommand)
+BOOST_AUTO_TEST_CASE(ControlCommandOld,
+  * ut::description("test old-style ControlCommand registration"))
 {
   size_t nCallbackCalled = 0;
-  dispatcher
-    .addControlCommand<VoidParameters>("test",
-                                       makeTestAuthorization(),
-                                       std::bind([] { return true; }),
-                                       std::bind([&nCallbackCalled] { ++nCallbackCalled; }));
+  auto handler = [&nCallbackCalled] (auto&&...) { ++nCallbackCalled; };
+  dispatcher.addControlCommand<VoidParameters>("test", makeTestAuthorization(),
+                                               [] (auto&&...) { return true; }, std::move(handler));
 
   dispatcher.addTopPrefix("/root");
   advanceClocks(1_ms);
-  face.sentData.clear();
+  BOOST_REQUIRE_EQUAL(face.sentData.size(), 0);
 
   face.receive(*makeInterest("/root/test/%80%00")); // returns 403
   face.receive(*makeInterest("/root/test/%80%00/invalid")); // returns 403
@@ -210,7 +202,7 @@
   face.receive(*makeInterest("/root/test/.../valid"));  // silently ignored (wrong format)
   advanceClocks(1_ms, 20);
   BOOST_CHECK_EQUAL(nCallbackCalled, 0);
-  BOOST_CHECK_EQUAL(face.sentData.size(), 2);
+  BOOST_REQUIRE_EQUAL(face.sentData.size(), 2);
 
   BOOST_CHECK_EQUAL(face.sentData[0].getContentType(), tlv::ContentType_Blob);
   BOOST_CHECK_EQUAL(ControlResponse(face.sentData[0].getContent().blockFromValue()).getCode(), 403);
@@ -222,6 +214,96 @@
   BOOST_CHECK_EQUAL(nCallbackCalled, 1);
 }
 
+BOOST_AUTO_TEST_CASE(ControlCommandNew,
+  * ut::description("test new-style ControlCommand registration"))
+{
+  size_t nHandlerCalled = 0;
+  auto handler = [&nHandlerCalled] (auto&&...) { ++nHandlerCalled; };
+
+  // test addControlCommand()
+  dispatcher.addControlCommand<nfd::FaceCreateCommand>(makeAcceptAllAuthorization(), handler);
+  dispatcher.addControlCommand<nfd::FaceUpdateCommand>(makeAcceptAllAuthorization(), handler);
+  dispatcher.addControlCommand<nfd::FaceDestroyCommand>(makeAcceptAllAuthorization(), handler);
+  dispatcher.addControlCommand<nfd::FibAddNextHopCommand>(makeAcceptAllAuthorization(), handler);
+  dispatcher.addControlCommand<nfd::FibRemoveNextHopCommand>(makeAcceptAllAuthorization(), handler);
+  dispatcher.addControlCommand<nfd::CsConfigCommand>(makeAcceptAllAuthorization(), handler);
+  dispatcher.addControlCommand<nfd::CsEraseCommand>(makeAcceptAllAuthorization(), handler);
+  dispatcher.addControlCommand<nfd::StrategyChoiceSetCommand>(makeAcceptAllAuthorization(), handler);
+  dispatcher.addControlCommand<nfd::StrategyChoiceUnsetCommand>(makeAcceptAllAuthorization(), handler);
+  dispatcher.addControlCommand<nfd::RibRegisterCommand>(makeAcceptAllAuthorization(), handler);
+  dispatcher.addControlCommand<nfd::RibUnregisterCommand>(makeAcceptAllAuthorization(), handler);
+
+  BOOST_CHECK_THROW(dispatcher.addControlCommand<nfd::CsConfigCommand>(makeAcceptAllAuthorization(),
+                                                                       [] (auto&&...) {}),
+                    std::out_of_range);
+
+  dispatcher.addTopPrefix("/root");
+  advanceClocks(1_ms);
+  BOOST_REQUIRE_EQUAL(face.sentData.size(), 0);
+
+  BOOST_CHECK_THROW(dispatcher.addControlCommand<nfd::CsConfigCommand>(makeAcceptAllAuthorization(),
+                                                                       [] (auto&&...) {}),
+                    std::domain_error);
+
+  // we pick FaceDestroyCommand as an example for the following tests
+
+  // malformed request (missing ControlParameters) => silently ignored
+  auto baseName = Name("/root").append(nfd::FaceDestroyCommand::getName());
+  auto interest = makeInterest(baseName);
+  face.receive(*interest);
+  advanceClocks(1_ms);
+  BOOST_CHECK_EQUAL(nHandlerCalled, 0);
+  BOOST_CHECK_EQUAL(face.sentData.size(), 0);
+
+  // ControlParameters present but invalid (missing required field) => reply with error 400
+  nfd::ControlParameters params;
+  interest->setName(Name(baseName).append(params.wireEncode()));
+  face.receive(*interest);
+  advanceClocks(1_ms);
+  BOOST_CHECK_EQUAL(nHandlerCalled, 0);
+  BOOST_REQUIRE_EQUAL(face.sentData.size(), 1);
+  BOOST_CHECK_EQUAL(face.sentData[0].getContentType(), tlv::ContentType_Blob);
+  BOOST_CHECK_EQUAL(ControlResponse(face.sentData[0].getContent().blockFromValue()).getCode(), 400);
+
+  // valid request
+  params.setFaceId(42);
+  interest->setName(Name(baseName).append(params.wireEncode()));
+  face.receive(*interest);
+  advanceClocks(1_ms);
+  BOOST_CHECK_EQUAL(nHandlerCalled, 1);
+  BOOST_CHECK_EQUAL(face.sentData.size(), 1);
+}
+
+BOOST_AUTO_TEST_CASE(ControlCommandResponse)
+{
+  auto handler = [] (const Name& prefix, const Interest& interest,
+                     const ControlParameters&, const CommandContinuation& done) {
+    BOOST_CHECK_EQUAL(prefix, "/root");
+    BOOST_CHECK_EQUAL(interest.getName().getPrefix(3),
+                      Name("/root").append(nfd::CsConfigCommand::getName()));
+    done(ControlResponse(42, "the answer"));
+  };
+
+  // use CsConfigCommand as an example
+  dispatcher.addControlCommand<nfd::CsConfigCommand>(makeAcceptAllAuthorization(), handler);
+  dispatcher.addTopPrefix("/root");
+  advanceClocks(1_ms);
+  BOOST_REQUIRE_EQUAL(face.sentData.size(), 0);
+
+  nfd::ControlParameters params;
+  auto interest = makeInterest(Name("/root")
+                               .append(nfd::CsConfigCommand::getName())
+                               .append(params.wireEncode()));
+  face.receive(*interest);
+  advanceClocks(1_ms, 10);
+
+  BOOST_REQUIRE_EQUAL(face.sentData.size(), 1);
+  BOOST_CHECK_EQUAL(face.sentData[0].getContentType(), tlv::ContentType_Blob);
+  ControlResponse resp(face.sentData[0].getContent().blockFromValue());
+  BOOST_CHECK_EQUAL(resp.getCode(), 42);
+  BOOST_CHECK_EQUAL(resp.getText(), "the answer");
+}
+
 class StatefulParameters : public mgmt::ControlParameters
 {
 public:
@@ -269,7 +351,7 @@
 
   size_t nCallbackCalled = 0;
   dispatcher.addControlCommand<StatefulParameters>("test", authorization, validateParameters,
-                                                   std::bind([&nCallbackCalled] { ++nCallbackCalled; }));
+                                                   [&nCallbackCalled] (auto&&...) { ++nCallbackCalled; });
 
   dispatcher.addTopPrefix("/root");
   advanceClocks(1_ms);