mgmt: handle rib/announce command

This commit adds logic to NFD so it can handle the new Prefix
Announcement protocol, which are commands sent to rib/announce
with Prefix Announcement object in the Application Parameters.

Refs: #4650
Change-Id: I2a306eb2c3eeb638cc789329d998bfa278880ca6
diff --git a/tests/daemon/mgmt/manager-common-fixture.cpp b/tests/daemon/mgmt/manager-common-fixture.cpp
index 3b98dd2..e918cdb 100644
--- a/tests/daemon/mgmt/manager-common-fixture.cpp
+++ b/tests/daemon/mgmt/manager-common-fixture.cpp
@@ -52,6 +52,20 @@
   NDN_CXX_UNREACHABLE;
 }
 
+Interest
+InterestSignerFixture::makeControlCommandRequest(Name commandName,
+                                                 const ndn::PrefixAnnouncement& prefixAnnouncement,
+                                                 const Name& identity)
+{
+  const Block& paBlock = prefixAnnouncement.getData().value().wireEncode();
+
+  Interest interest(commandName);
+  interest.setApplicationParameters(paBlock);
+  m_signer.makeSignedInterest(interest, ndn::security::signingByIdentity(identity));
+
+  return interest;
+}
+
 void
 ManagerCommonFixture::setTopPrefix()
 {
diff --git a/tests/daemon/mgmt/manager-common-fixture.hpp b/tests/daemon/mgmt/manager-common-fixture.hpp
index c8f5498..22d9120 100644
--- a/tests/daemon/mgmt/manager-common-fixture.hpp
+++ b/tests/daemon/mgmt/manager-common-fixture.hpp
@@ -59,6 +59,19 @@
                             ndn::security::SignedInterestFormat format = ndn::security::SignedInterestFormat::V03,
                             const Name& identity = DEFAULT_COMMAND_SIGNER_IDENTITY);
 
+  /**
+   * \brief Create a ControlCommand request for a Prefix Announcement.
+   * \param commandName Command name including prefix, such as `/localhost/nfd/rib/announce`
+   * \param prefixAnnouncement Prefix Announcement object
+   * \param identity Signing identity
+   *
+   * Per specification, Prefix Announcements use Signed Interest v0.3 only.
+   */
+  Interest
+  makeControlCommandRequest(Name commandName,
+                            const ndn::PrefixAnnouncement& prefixAnnouncement,
+                            const Name& identity = DEFAULT_COMMAND_SIGNER_IDENTITY);
+
 protected:
   static inline const Name DEFAULT_COMMAND_SIGNER_IDENTITY{"/InterestSignerFixture-identity"};
 
diff --git a/tests/daemon/mgmt/rib-manager.t.cpp b/tests/daemon/mgmt/rib-manager.t.cpp
index 43c69df..679f40c 100644
--- a/tests/daemon/mgmt/rib-manager.t.cpp
+++ b/tests/daemon/mgmt/rib-manager.t.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2014-2024,  Regents of the University of California,
+ * Copyright (c) 2014-2025,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -117,6 +117,14 @@
   return makeSection(config);
 }
 
+static ConfigSection
+getPaValidatorConfigSection()
+{
+  ConfigSection section;
+  section.put("trust-anchor.type", "any");
+  return section;
+}
+
 class RibManagerFixture : public ManagerCommonFixture
 {
 public:
@@ -151,6 +159,8 @@
       m_manager.disableLocalhop();
     }
 
+    m_manager.applyPaConfig(getPaValidatorConfigSection(), "testPa");
+
     registerWithNfd();
 
     if (shouldClearRib) {
@@ -442,6 +452,7 @@
   }
   auto params = makeRegisterParameters(prefix, 2899);
   auto command = makeControlCommandRequest("/localhost/nfd/rib/register", params);
+
   receiveInterest(command);
 
   BOOST_REQUIRE_EQUAL(m_responses.size(), 1);
@@ -455,6 +466,107 @@
 
 BOOST_AUTO_TEST_SUITE_END() // RegisterUnregister
 
+BOOST_FIXTURE_TEST_SUITE(PrefixAnnounce, LocalhostAuthorizedRibManagerFixture)
+
+BOOST_AUTO_TEST_CASE(Basic)
+{
+  const uint64_t announceFaceId = 1234;
+
+  ndn::PrefixAnnouncement pa = signPrefixAnn(makePrefixAnn("/test-prefix-announce", 10_s), m_keyChain);
+  auto commandAnnounce = makeControlCommandRequest("/localhost/nfd/rib/announce", pa);
+  commandAnnounce.setTag(make_shared<lp::IncomingFaceIdTag>(announceFaceId));
+
+  auto paramsUnregister = makeUnregisterParameters("/test-prefix-announce");
+  paramsUnregister.setOrigin(ndn::nfd::ROUTE_ORIGIN_PREFIXANN);
+  BOOST_CHECK_EQUAL(paramsUnregister.getFaceId(), 0);
+  auto commandUnregister = makeControlCommandRequest("/localhost/nfd/rib/unregister", paramsUnregister);
+  commandUnregister.setTag(make_shared<lp::IncomingFaceIdTag>(announceFaceId)); // same incoming face
+
+  receiveInterest(commandAnnounce);
+  receiveInterest(commandUnregister);
+
+  ControlParameters paramsAnnounceResponse;
+  paramsAnnounceResponse.setName("/test-prefix-announce")
+    .setFaceId(announceFaceId)
+    .setOrigin(ndn::nfd::ROUTE_ORIGIN_PREFIXANN)
+    .setCost(rib::Route::PA_ROUTE_COST)
+    .setFlags(ndn::nfd::ROUTE_FLAG_CHILD_INHERIT)
+    .setExpirationPeriod(10_s);
+  paramsUnregister.setFaceId(announceFaceId);
+
+  BOOST_REQUIRE_EQUAL(m_responses.size(), 2);
+  BOOST_CHECK_EQUAL(checkResponse(0, commandAnnounce.getName(), makeResponse(200, "Success", paramsAnnounceResponse)),
+                    CheckResponseResult::OK);
+  BOOST_CHECK_EQUAL(checkResponse(1, commandUnregister.getName(), makeResponse(200, "Success", paramsUnregister)),
+                    CheckResponseResult::OK);
+
+  BOOST_REQUIRE_EQUAL(m_fibUpdater.updates.size(), 2);
+  BOOST_CHECK_EQUAL(m_fibUpdater.updates.front(),
+                    rib::FibUpdate::createAddUpdate("/test-prefix-announce", announceFaceId, rib::Route::PA_ROUTE_COST));
+  BOOST_CHECK_EQUAL(m_fibUpdater.updates.back(),
+                    rib::FibUpdate::createRemoveUpdate("/test-prefix-announce", announceFaceId));
+}
+
+BOOST_AUTO_TEST_CASE(UnregisterFromDifferentFace)
+{
+  const uint64_t announceFaceId = 1234;
+
+  ndn::PrefixAnnouncement pa = signPrefixAnn(makePrefixAnn("/test-prefix-announce", 10_s), m_keyChain);
+  auto commandAnnounce = makeControlCommandRequest("/localhost/nfd/rib/announce", pa);
+  commandAnnounce.setTag(make_shared<lp::IncomingFaceIdTag>(announceFaceId));
+
+  auto paramsUnregister = makeUnregisterParameters("/test-prefix-announce", announceFaceId);
+  paramsUnregister.setOrigin(ndn::nfd::ROUTE_ORIGIN_PREFIXANN);
+  auto commandUnregister = makeControlCommandRequest("/localhost/nfd/rib/unregister", paramsUnregister);
+  commandUnregister.setTag(make_shared<lp::IncomingFaceIdTag>(999)); // unregister from different face
+
+  receiveInterest(commandAnnounce);
+  receiveInterest(commandUnregister);
+
+  ControlParameters paramsAnnounceResponse;
+  paramsAnnounceResponse.setName("/test-prefix-announce")
+    .setFaceId(announceFaceId)
+    .setOrigin(ndn::nfd::ROUTE_ORIGIN_PREFIXANN)
+    .setCost(rib::Route::PA_ROUTE_COST)
+    .setFlags(ndn::nfd::ROUTE_FLAG_CHILD_INHERIT)
+    .setExpirationPeriod(10_s);
+
+  BOOST_REQUIRE_EQUAL(m_responses.size(), 2);
+  BOOST_CHECK_EQUAL(checkResponse(0, commandAnnounce.getName(), makeResponse(200, "Success", paramsAnnounceResponse)),
+                    CheckResponseResult::OK);
+  BOOST_CHECK_EQUAL(checkResponse(1, commandUnregister.getName(), makeResponse(200, "Success", paramsUnregister)),
+                    CheckResponseResult::OK);
+
+  BOOST_REQUIRE_EQUAL(m_fibUpdater.updates.size(), 2);
+  BOOST_CHECK_EQUAL(m_fibUpdater.updates.front(),
+                    rib::FibUpdate::createAddUpdate("/test-prefix-announce", announceFaceId, rib::Route::PA_ROUTE_COST));
+  BOOST_CHECK_EQUAL(m_fibUpdater.updates.back(),
+                    rib::FibUpdate::createRemoveUpdate("/test-prefix-announce", announceFaceId));
+}
+
+BOOST_AUTO_TEST_CASE(NameTooLong)
+{
+  Name prefix;
+  while (prefix.size() <= Fib::getMaxDepth()) {
+    prefix.append("A");
+  }
+  ndn::PrefixAnnouncement pa = signPrefixAnn(makePrefixAnn(prefix, 10_s), m_keyChain);
+  auto command = makeControlCommandRequest("/localhost/nfd/rib/announce", pa);
+  command.setTag(make_shared<lp::IncomingFaceIdTag>(333));
+
+  receiveInterest(command);
+
+  BOOST_REQUIRE_EQUAL(m_responses.size(), 1);
+  BOOST_CHECK_EQUAL(checkResponse(0, command.getName(),
+                                  ControlResponse(414, "Route prefix cannot exceed " +
+                                                  std::to_string(Fib::getMaxDepth()) + " components")),
+                    CheckResponseResult::OK);
+
+  BOOST_CHECK_EQUAL(m_fibUpdater.updates.size(), 0);
+}
+
+BOOST_AUTO_TEST_SUITE_END() // PrefixAnnounce
+
 BOOST_FIXTURE_TEST_CASE(RibDataset, UnauthorizedRibManagerFixture)
 {
   uint64_t faceId = 0;