rib: provide self-learning helpers

refs #4683

Change-Id: Ib1e586a505d07a5e1f7a4b6d78306ea08b4dcda8
diff --git a/rib/rib-manager.cpp b/rib/rib-manager.cpp
index a1707ef..009b9ad 100644
--- a/rib/rib-manager.cpp
+++ b/rib/rib-manager.cpp
@@ -46,9 +46,11 @@
 static const Name LOCALHOP_TOP_PREFIX = "/localhop/nfd";
 static const time::seconds ACTIVE_FACE_FETCH_INTERVAL = time::seconds(300);
 
-RibManager::RibManager(Rib& rib, ndn::Face& face, ndn::nfd::Controller& nfdController, Dispatcher& dispatcher)
+RibManager::RibManager(Rib& rib, ndn::Face& face, ndn::KeyChain& keyChain,
+                       ndn::nfd::Controller& nfdController, Dispatcher& dispatcher)
   : ManagerBase(dispatcher, MGMT_MODULE_NAME)
   , m_rib(rib)
+  , m_keyChain(keyChain)
   , m_nfdController(nfdController)
   , m_dispatcher(dispatcher)
   , m_faceMonitor(face)
@@ -118,18 +120,15 @@
                           const std::function<void(RibUpdateResult)>& done)
 {
   if (expires) {
-    if (*expires <= 0_ns) {
-      done(RibUpdateResult::EXPIRED);
-      return;
-    }
     route.expires = time::steady_clock::now() + *expires;
   }
   else if (route.expires) {
     expires = *route.expires - time::steady_clock::now();
-    if (*expires <= 0_ns) {
-      done(RibUpdateResult::EXPIRED);
-      return;
-    }
+  }
+
+  if (expires && *expires <= 0_s) {
+    m_rib.onRouteExpiration(name, route);
+    return done(RibUpdateResult::EXPIRED);
   }
 
   NFD_LOG_INFO("Adding route " << name << " nexthop=" << route.faceId <<
@@ -318,6 +317,116 @@
   };
 }
 
+std::ostream&
+operator<<(std::ostream& os, RibManager::SlAnnounceResult res)
+{
+  switch (res) {
+    case RibManager::SlAnnounceResult::OK:
+      return os << "OK";
+    case RibManager::SlAnnounceResult::ERROR:
+      return os << "ERROR";
+    case RibManager::SlAnnounceResult::VALIDATION_FAILURE:
+      return os << "VALIDATION_FAILURE";
+    case RibManager::SlAnnounceResult::EXPIRED:
+      return os << "EXPIRED";
+    case RibManager::SlAnnounceResult::NOT_FOUND:
+      return os << "NOT_FOUND";
+  }
+  BOOST_ASSERT_MSG(false, "bad SlAnnounceResult");
+  return os;
+}
+
+RibManager::SlAnnounceResult
+RibManager::getSlAnnounceResultFromRibUpdateResult(RibUpdateResult r)
+{
+  switch (r) {
+  case RibUpdateResult::OK:
+    return SlAnnounceResult::OK;
+  case RibUpdateResult::ERROR:
+    return SlAnnounceResult::ERROR;
+  case RibUpdateResult::EXPIRED:
+    return SlAnnounceResult::EXPIRED;
+  default:
+    BOOST_ASSERT(false);
+    return SlAnnounceResult::ERROR;
+  }
+}
+
+void
+RibManager::slAnnounce(const ndn::PrefixAnnouncement& pa, uint64_t faceId,
+                       time::milliseconds maxLifetime, const SlAnnounceCallback& cb)
+{
+  BOOST_ASSERT(pa.getData());
+
+  if (!m_isLocalhopEnabled) {
+    NFD_LOG_INFO("slAnnounce " << pa.getAnnouncedName() << ' ' << faceId <<
+                 ": localhop_security unconfigured");
+    cb(SlAnnounceResult::VALIDATION_FAILURE);
+    return;
+  }
+
+  m_localhopValidator.validate(*pa.getData(),
+    [=] (const Data&) {
+      Route route(pa, faceId);
+      route.expires = std::min(route.annExpires, time::steady_clock::now() + maxLifetime);
+      beginAddRoute(pa.getAnnouncedName(), route, nullopt,
+        [=] (RibUpdateResult ribRes) {
+          auto res = getSlAnnounceResultFromRibUpdateResult(ribRes);
+          NFD_LOG_INFO("slAnnounce " << pa.getAnnouncedName() << ' ' << faceId << ": " << res);
+          cb(res);
+        });
+    },
+    [=] (const Data&, ndn::security::v2::ValidationError err) {
+      NFD_LOG_INFO("slAnnounce " << pa.getAnnouncedName() << ' ' << faceId <<
+                   " validation error: " << err);
+      cb(SlAnnounceResult::VALIDATION_FAILURE);
+    }
+  );
+}
+
+void
+RibManager::slRenew(const Name& name, uint64_t faceId, time::milliseconds maxLifetime,
+                    const SlAnnounceCallback& cb)
+{
+  Route routeQuery;
+  routeQuery.faceId = faceId;
+  routeQuery.origin = ndn::nfd::ROUTE_ORIGIN_PREFIXANN;
+  Route* oldRoute = m_rib.find(name, routeQuery);
+  if (oldRoute == nullptr || !oldRoute->announcement) {
+    NFD_LOG_DEBUG("slRenew " << name << ' ' << faceId << ": not found");
+    return cb(SlAnnounceResult::NOT_FOUND);
+  }
+
+  Route route = *oldRoute;
+  route.expires = std::min(route.annExpires, time::steady_clock::now() + maxLifetime);
+  beginAddRoute(name, route, nullopt,
+    [=] (RibUpdateResult ribRes) {
+      auto res = getSlAnnounceResultFromRibUpdateResult(ribRes);
+      NFD_LOG_INFO("slRenew " << name << ' ' << faceId << ": " << res);
+      cb(res);
+    });
+}
+
+void
+RibManager::slFindAnn(const Name& name, const SlFindAnnCallback& cb) const
+{
+  shared_ptr<RibEntry> entry;
+  auto exactMatch = m_rib.find(name);
+  if (exactMatch != m_rib.end()) {
+    entry = exactMatch->second;
+  }
+  else {
+    entry = m_rib.findParent(name);
+  }
+  if (entry == nullptr) {
+    return cb(nullopt);
+  }
+
+  auto pa = entry->getPrefixAnnouncement();
+  pa.toData(m_keyChain);
+  cb(pa);
+}
+
 void
 RibManager::fetchActiveFaces()
 {
diff --git a/rib/rib-manager.hpp b/rib/rib-manager.hpp
index 169c9ec..0ae96a4 100644
--- a/rib/rib-manager.hpp
+++ b/rib/rib-manager.hpp
@@ -54,7 +54,8 @@
     using std::runtime_error::runtime_error;
   };
 
-  RibManager(Rib& rib, ndn::Face& face, ndn::nfd::Controller& nfdController, Dispatcher& dispatcher);
+  RibManager(Rib& rib, ndn::Face& face, ndn::KeyChain& keyChain,
+             ndn::nfd::Controller& nfdController, Dispatcher& dispatcher);
 
   /**
    * @brief Apply localhost_security configuration.
@@ -87,6 +88,75 @@
   void
   enableLocalFields();
 
+public: // self-learning support
+  enum class SlAnnounceResult {
+    OK,                 ///< RIB and FIB have been updated
+    ERROR,              ///< unspecified error
+    VALIDATION_FAILURE, ///< the announcement cannot be verified against the trust schema
+    EXPIRED,            ///< the announcement has expired
+    NOT_FOUND,          ///< route does not exist (slRenew only)
+  };
+
+  using SlAnnounceCallback = std::function<void(SlAnnounceResult res)>;
+  using SlFindAnnCallback = std::function<void(optional<ndn::PrefixAnnouncement>)>;
+
+  /** \brief Insert a route by prefix announcement from self-learning strategy.
+   *  \param pa A prefix announcement. It must contain the Data.
+   *  \param faceId Face on which the announcement arrives.
+   *  \param maxLifetime Maximum route lifetime as imposed by self-learning strategy.
+   *  \param cb Callback to receive the operation result.
+   *
+   *  If \p pa passes validation and is unexpired, inserts or replaces a route for the announced
+   *  name and faceId whose lifetime is set to the earlier of now+maxLifetime or prefix
+   *  announcement expiration time, updates FIB, and invokes \p cb with SlAnnounceResult::OK.
+   *  In case \p pa expires when validation completes, invokes \p cb with SlAnnounceResult::EXPIRED.
+   *  If \p pa cannot be verified by the trust schema given in rib.localhop_security config key,
+   *  or the relevant config has not been loaded via \c enableLocalHop, invokes \p cb with
+   *  SlAnnounceResult::VALIDATION_FAILURE.
+   *
+   *  Self-learning strategy invokes this method after receiving a Data carrying a prefix
+   *  announcement.
+   */
+  void
+  slAnnounce(const ndn::PrefixAnnouncement& pa, uint64_t faceId, time::milliseconds maxLifetime,
+             const SlAnnounceCallback& cb);
+
+  /** \brief Renew a route created by prefix announcement from self-learning strategy.
+   *  \param name Data name, for finding RIB entry by longest-prefix-match.
+   *  \param faceId Nexthop face.
+   *  \param maxLifetime Maximum route lifetime as imposed by self-learning strategy.
+   *  \param cb Callback to receive the operation result.
+   *
+   *  If the specified route exists, prolongs its lifetime to the earlier of now+maxLifetime or
+   *  prefix announcement expiration time, and invokes \p cb with SlAnnounceResult::OK.
+   *  If the prefix announcement has expired, invokes \p cb with SlAnnounceResult::EXPIRED.
+   *  If the route is not found, invokes \p cb with SlAnnounceResult::NOT_FOUND.
+   *
+   *  Self-learning strategy invokes this method after an Interest forwarded via a learned route
+   *  is satisfied.
+   *
+   *  \bug In current implementation, if an slAnnounce operation is in progress to create a Route
+   *       or replace a prefix announcement, slRenew could fail because Route does not exist in
+   *       existing RIB, or overwrite the new prefix announcement with an old one.
+   */
+  void
+  slRenew(const Name& name, uint64_t faceId, time::milliseconds maxLifetime,
+          const SlAnnounceCallback& cb);
+
+  /** \brief Retrieve an outgoing prefix announcement for self-learning strategy.
+   *  \param name Data name.
+   *  \param cb Callback to receive a prefix announcement that announces a prefix of \p name, or
+   *            nullopt if no RIB entry is found by longest-prefix-match of \p name.
+   *
+   *  Self-learning strategy invokes this method before sending a Data in reply to a discovery
+   *  Interest, so as to attach a prefix announcement onto that Data.
+   *
+   *  \bug In current implementation, if an slAnnounce operation is in progress, slFindAnn does not
+   *       wait for that operation to complete and its result reflects the prior RIB state.
+   */
+  void
+  slFindAnn(const Name& name, const SlFindAnnCallback& cb) const;
+
 private: // RIB and FibUpdater actions
   enum class RibUpdateResult
   {
@@ -95,6 +165,9 @@
     EXPIRED,
   };
 
+  static SlAnnounceResult
+  getSlAnnounceResultFromRibUpdateResult(RibUpdateResult r);
+
   /** \brief Start adding a route to RIB and FIB.
    *  \param name route name
    *  \param route route parameters; may contain absolute expiration time
@@ -179,6 +252,7 @@
 
 private:
   Rib& m_rib;
+  ndn::KeyChain& m_keyChain;
   ndn::nfd::Controller& m_nfdController;
   Dispatcher& m_dispatcher;
 
@@ -187,6 +261,8 @@
   ndn::ValidatorConfig m_localhopValidator;
   bool m_isLocalhopEnabled;
 
+  static const std::map<RibManager::RibUpdateResult, RibManager::SlAnnounceResult> RIB_RESULT_TO_SL_ANNOUNCE_RESULT;
+
 private:
   scheduler::ScopedEventId m_activeFaceFetchEvent;
 
@@ -196,6 +272,9 @@
   FaceIdSet m_registeredFaces;
 };
 
+std::ostream&
+operator<<(std::ostream& os, RibManager::SlAnnounceResult res);
+
 } // namespace rib
 } // namespace nfd
 
diff --git a/rib/service.cpp b/rib/service.cpp
index 067806f..c25e977 100644
--- a/rib/service.cpp
+++ b/rib/service.cpp
@@ -108,7 +108,7 @@
   , m_nfdController(m_face, m_keyChain)
   , m_fibUpdater(m_rib, m_nfdController)
   , m_dispatcher(m_face, m_keyChain)
-  , m_ribManager(m_rib, m_face, m_nfdController, m_dispatcher)
+  , m_ribManager(m_rib, m_face, m_keyChain, m_nfdController, m_dispatcher)
 {
   if (s_instance != nullptr) {
     BOOST_THROW_EXCEPTION(std::logic_error("RIB service cannot be instantiated more than once"));
diff --git a/rib/service.hpp b/rib/service.hpp
index a349787..135df58 100644
--- a/rib/service.hpp
+++ b/rib/service.hpp
@@ -85,6 +85,12 @@
   static Service&
   get();
 
+  RibManager&
+  getRibManager()
+  {
+    return m_ribManager;
+  }
+
 private:
   template<typename ConfigParseFunc>
   Service(ndn::KeyChain& keyChain, shared_ptr<ndn::Transport> localNfdTransport,
diff --git a/tests/identity-management-fixture.cpp b/tests/identity-management-fixture.cpp
index a2977f8..ab361fd 100644
--- a/tests/identity-management-fixture.cpp
+++ b/tests/identity-management-fixture.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
-/**
- * Copyright (c) 2014-2017,  Regents of the University of California,
+/*
+ * Copyright (c) 2014-2018,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -27,6 +27,7 @@
 #include <ndn-cxx/security/pib/identity.hpp>
 #include <ndn-cxx/security/pib/key.hpp>
 #include <ndn-cxx/security/pib/pib.hpp>
+#include <ndn-cxx/security/transform.hpp>
 #include <ndn-cxx/security/v2/certificate.hpp>
 #include <ndn-cxx/util/io.hpp>
 #include <boost/filesystem.hpp>
@@ -84,5 +85,27 @@
   }
 }
 
+std::string
+IdentityManagementFixture::getIdentityCertificateBase64(const Name& identity, bool wantAdd)
+{
+  ndn::security::v2::Certificate cert;
+  try {
+    cert = m_keyChain.getPib().getIdentity(identity).getDefaultKey().getDefaultCertificate();
+  }
+  catch (const ndn::security::Pib::Error&) {
+    if (!wantAdd) {
+      BOOST_THROW_EXCEPTION(std::runtime_error("identity does not exist"));
+    }
+    cert = m_keyChain.createIdentity(identity).getDefaultKey().getDefaultCertificate();
+  }
+
+  Block wire = cert.wireEncode();
+
+  std::ostringstream oss;
+  namespace tr = ndn::security::transform;
+  tr::bufferSource(wire.wire(), wire.size()) >> tr::base64Encode(false) >> tr::streamSink(oss);
+  return oss.str();
+}
+
 } // namespace tests
 } // namespace nfd
diff --git a/tests/identity-management-fixture.hpp b/tests/identity-management-fixture.hpp
index c9d054b..0dbf333 100644
--- a/tests/identity-management-fixture.hpp
+++ b/tests/identity-management-fixture.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
-/**
- * Copyright (c) 2014-2017,  Regents of the University of California,
+/*
+ * Copyright (c) 2014-2018,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -59,6 +59,14 @@
   bool
   saveIdentityCertificate(const Name& identity, const std::string& filename, bool wantAdd = false);
 
+  /** \brief retrieve identity certificate as base64 string
+   *  \param identity identity name
+   *  \param wantAdd if true, add new identity when necessary
+   *  \throw std::runtime_error identity does not exist and wantAdd is false
+   */
+  std::string
+  getIdentityCertificateBase64(const Name& identity, bool wantAdd = false);
+
 protected:
   ndn::KeyChain m_keyChain;
 
diff --git a/tests/rib/rib-manager.t.cpp b/tests/rib/rib-manager.t.cpp
index 6966018..8ccde7d 100644
--- a/tests/rib/rib-manager.t.cpp
+++ b/tests/rib/rib-manager.t.cpp
@@ -63,7 +63,7 @@
     , m_status(status)
     , m_nfdController(m_face, m_keyChain)
     , m_fibUpdater(m_rib, m_nfdController)
-    , m_manager(m_rib, m_face, m_nfdController, m_dispatcher)
+    , m_manager(m_rib, m_face, m_keyChain, m_nfdController, m_dispatcher)
   {
     m_rib.mockFibResponse = [] (const RibUpdateBatch& batch) {
       BOOST_CHECK(batch.begin() != batch.end());
diff --git a/tests/rib/sl-announce.t.cpp b/tests/rib/sl-announce.t.cpp
new file mode 100644
index 0000000..c77b91a
--- /dev/null
+++ b/tests/rib/sl-announce.t.cpp
@@ -0,0 +1,331 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2014-2018,  Regents of the University of California,
+ *                           Arizona Board of Regents,
+ *                           Colorado State University,
+ *                           University Pierre & Marie Curie, Sorbonne University,
+ *                           Washington University in St. Louis,
+ *                           Beijing Institute of Technology,
+ *                           The University of Memphis.
+ *
+ * This file is part of NFD (Named Data Networking Forwarding Daemon).
+ * See AUTHORS.md for complete list of NFD authors and contributors.
+ *
+ * NFD is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU General Public License as published by the Free Software Foundation,
+ * either version 3 of the License, or (at your option) any later version.
+ *
+ * NFD 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * NFD, e.g., in COPYING.md file.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "rib/rib-manager.hpp"
+
+#include "tests/identity-management-fixture.hpp"
+#include <ndn-cxx/util/dummy-client-face.hpp>
+
+namespace nfd {
+namespace rib {
+namespace tests {
+
+using namespace nfd::tests;
+
+class RibManagerSlAnnounceFixture : public IdentityManagementTimeFixture
+{
+public:
+  using SlAnnounceResult = RibManager::SlAnnounceResult;
+
+  RibManagerSlAnnounceFixture()
+    : m_face(getGlobalIoService(), m_keyChain)
+    , m_nfdController(m_face, m_keyChain)
+    , m_dispatcher(m_face, m_keyChain)
+    , m_fibUpdater(rib, m_nfdController)
+    , m_trustedSigner(m_keyChain.createIdentity("/trusted", ndn::RsaKeyParams()))
+    , m_untrustedSigner(m_keyChain.createIdentity("/untrusted", ndn::RsaKeyParams()))
+  {
+    rib.mockFibResponse = [] (const RibUpdateBatch& batch) { return true; };
+    rib.wantMockFibResponseOnce = false;
+
+    // Face, Controller, Dispatcher are irrelevant to SlAnnounce functions but required by
+    // RibManager construction, so they are private. RibManager is a pointer to avoid code style
+    // rule 1.4 violation.
+    manager = make_unique<RibManager>(rib, m_face, m_keyChain, m_nfdController, m_dispatcher);
+
+    loadTrustSchema();
+  }
+
+  template<typename ...T>
+  ndn::PrefixAnnouncement
+  makeTrustedAnn(const T&... args)
+  {
+    return signPrefixAnn(makePrefixAnn(args...), m_keyChain, m_trustedSigner);
+  }
+
+  template<typename ...T>
+  ndn::PrefixAnnouncement
+  makeUntrustedAnn(const T&... args)
+  {
+    return signPrefixAnn(makePrefixAnn(args...), m_keyChain, m_untrustedSigner);
+  }
+
+  /** \brief Invoke manager->slAnnounce and wait for result.
+   */
+  SlAnnounceResult
+  slAnnounceSync(const ndn::PrefixAnnouncement& pa, uint64_t faceId, time::milliseconds maxLifetime)
+  {
+    optional<SlAnnounceResult> result;
+    manager->slAnnounce(pa, faceId, maxLifetime,
+      [&] (RibManager::SlAnnounceResult res) {
+        BOOST_CHECK(!result);
+        result = res;
+      });
+
+    getGlobalIoService().poll();
+    BOOST_CHECK(result);
+    return result.value_or(SlAnnounceResult::ERROR);
+  }
+
+  /** \brief Invoke manager->slRenew and wait for result.
+   */
+  SlAnnounceResult
+  slRenewSync(const Name& name, uint64_t faceId, time::milliseconds maxLifetime)
+  {
+    optional<SlAnnounceResult> result;
+    manager->slRenew(name, faceId, maxLifetime,
+      [&] (RibManager::SlAnnounceResult res) {
+        BOOST_CHECK(!result);
+        result = res;
+      });
+
+    getGlobalIoService().poll();
+    BOOST_CHECK(result);
+    return result.value_or(SlAnnounceResult::ERROR);
+  }
+
+  /** \brief Invoke manager->slFindAnn and wait for result.
+   */
+  optional<ndn::PrefixAnnouncement>
+  slFindAnnSync(const Name& name)
+  {
+    optional<optional<ndn::PrefixAnnouncement>> result;
+    manager->slFindAnn(name,
+      [&] (optional<ndn::PrefixAnnouncement> found) {
+        BOOST_CHECK(!result);
+        result = found;
+      });
+
+    getGlobalIoService().poll();
+    BOOST_CHECK(result);
+    return result.value_or(nullopt);
+  }
+
+  /** \brief Lookup a route with PREFIXANN origin.
+   */
+  Route*
+  findAnnRoute(const Name& name, uint64_t faceId)
+  {
+    Route routeQuery;
+    routeQuery.faceId = faceId;
+    routeQuery.origin = ndn::nfd::ROUTE_ORIGIN_PREFIXANN;
+    return rib.find(name, routeQuery);
+  }
+
+private:
+  /** \brief Prepare a trust schema and load as localhop_security.
+   *
+   *  Test case may revert this operation with ribManager->disableLocalhop().
+   */
+  void
+  loadTrustSchema()
+  {
+    ConfigSection section;
+    section.put("rule.id", "PA");
+    section.put("rule.for", "data");
+    section.put("rule.checker.type", "customized");
+    section.put("rule.checker.sig-type", "rsa-sha256");
+    section.put("rule.checker.key-locator.type", "name");
+    section.put("rule.checker.key-locator.name", "/trusted");
+    section.put("rule.checker.key-locator.relation", "is-prefix-of");
+    section.put("trust-anchor.type", "base64");
+    section.put("trust-anchor.base64-string", getIdentityCertificateBase64("/trusted"));
+    manager->enableLocalhop(section, "trust-schema.section");
+  }
+
+public:
+  Rib rib;
+  unique_ptr<RibManager> manager;
+
+private:
+  ndn::util::DummyClientFace m_face;
+  ndn::nfd::Controller m_nfdController;
+  ndn::mgmt::Dispatcher m_dispatcher;
+  FibUpdater m_fibUpdater;
+
+  ndn::security::SigningInfo m_trustedSigner;
+  ndn::security::SigningInfo m_untrustedSigner;
+};
+
+BOOST_FIXTURE_TEST_SUITE(TestSlAnnounce, RibManagerSlAnnounceFixture)
+
+BOOST_AUTO_TEST_CASE(AnnounceUnconfigured)
+{
+  manager->disableLocalhop();
+  auto pa = makeTrustedAnn("/fMXN7UeB", 1_h);
+  BOOST_CHECK_EQUAL(slAnnounceSync(pa, 3275, 1_h), SlAnnounceResult::VALIDATION_FAILURE);
+
+  BOOST_CHECK(findAnnRoute("/fMXN7UeB", 3275) == nullptr);
+}
+
+BOOST_AUTO_TEST_CASE(AnnounceValidationError)
+{
+  auto pa = makeUntrustedAnn("/1nzAe0Y4", 1_h);
+  BOOST_CHECK_EQUAL(slAnnounceSync(pa, 2959, 1_h), SlAnnounceResult::VALIDATION_FAILURE);
+
+  BOOST_CHECK(findAnnRoute("/1nzAe0Y4", 2959) == nullptr);
+}
+
+BOOST_AUTO_TEST_CASE(AnnounceInsert_AnnLifetime)
+{
+  auto pa = makeTrustedAnn("/EHJYmJz9", 1_h);
+  BOOST_CHECK_EQUAL(slAnnounceSync(pa, 1641, 2_h), SlAnnounceResult::OK);
+
+  Route* route = findAnnRoute("/EHJYmJz9", 1641);
+  BOOST_REQUIRE(route != nullptr);
+  BOOST_CHECK_EQUAL(route->annExpires, time::steady_clock::now() + 1_h);
+  BOOST_CHECK_EQUAL(route->expires.value(), time::steady_clock::now() + 1_h);
+}
+
+BOOST_AUTO_TEST_CASE(AnnounceInsert_ArgLifetime)
+{
+  auto pa = makeTrustedAnn("/BU9Fec9E", 2_h);
+  BOOST_CHECK_EQUAL(slAnnounceSync(pa, 1282, 1_h), SlAnnounceResult::OK);
+
+  Route* route = findAnnRoute("/BU9Fec9E", 1282);
+  BOOST_REQUIRE(route != nullptr);
+  BOOST_CHECK_EQUAL(route->annExpires, time::steady_clock::now() + 2_h);
+  BOOST_CHECK_EQUAL(route->expires.value(), time::steady_clock::now() + 1_h);
+}
+
+BOOST_AUTO_TEST_CASE(AnnounceReplace)
+{
+  auto pa = makeTrustedAnn("/HsBFGvL3", 1_h);
+  BOOST_CHECK_EQUAL(slAnnounceSync(pa, 2813, 1_h), SlAnnounceResult::OK);
+
+  pa = makeTrustedAnn("/HsBFGvL3", 2_h);
+  BOOST_CHECK_EQUAL(slAnnounceSync(pa, 2813, 2_h), SlAnnounceResult::OK);
+
+  Route* route = findAnnRoute("/HsBFGvL3", 2813);
+  BOOST_REQUIRE(route != nullptr);
+  BOOST_CHECK_EQUAL(route->annExpires, time::steady_clock::now() + 2_h);
+  BOOST_CHECK_EQUAL(route->expires.value(), time::steady_clock::now() + 2_h);
+}
+
+BOOST_AUTO_TEST_CASE(AnnounceExpired)
+{
+  auto pa = makeTrustedAnn("/awrVv6V7", 1_h, std::make_pair(-3_h, -1_h));
+  BOOST_CHECK_EQUAL(slAnnounceSync(pa, 9087, 1_h), SlAnnounceResult::EXPIRED);
+
+  BOOST_CHECK(findAnnRoute("/awrVv6V7", 9087) == nullptr);
+}
+
+BOOST_AUTO_TEST_CASE(RenewNotFound)
+{
+  BOOST_CHECK_EQUAL(slRenewSync("IAYigN73", 1070, 1_h), SlAnnounceResult::NOT_FOUND);
+
+  BOOST_CHECK(findAnnRoute("/IAYigN73", 1070) == nullptr);
+}
+
+BOOST_AUTO_TEST_CASE(RenewProlong_ArgLifetime)
+{
+  auto pa = makeTrustedAnn("/P2IYFqtr", 4_h);
+  BOOST_CHECK_EQUAL(slAnnounceSync(pa, 4506, 2_h), SlAnnounceResult::OK);
+  advanceClocks(1_h); // Route has 1_h remaining lifetime
+
+  BOOST_CHECK_EQUAL(slRenewSync("/P2IYFqtr", 4506, 2_h), SlAnnounceResult::OK);
+
+  Route* route = findAnnRoute("/P2IYFqtr", 4506);
+  BOOST_REQUIRE(route != nullptr);
+  BOOST_CHECK_EQUAL(route->annExpires, time::steady_clock::now() + 3_h);
+  BOOST_CHECK_EQUAL(route->expires.value(), time::steady_clock::now() + 2_h); // set by slRenew
+}
+
+BOOST_AUTO_TEST_CASE(RenewProlong_AnnLifetime)
+{
+  auto pa = makeTrustedAnn("/be01Yiba", 4_h);
+  BOOST_CHECK_EQUAL(slAnnounceSync(pa, 1589, 2_h), SlAnnounceResult::OK);
+  advanceClocks(1_h); // Route has 1_h remaining lifetime
+
+  BOOST_CHECK_EQUAL(slRenewSync("/be01Yiba", 1589, 5_h), SlAnnounceResult::OK);
+
+  Route* route = findAnnRoute("/be01Yiba", 1589);
+  BOOST_REQUIRE(route != nullptr);
+  BOOST_CHECK_EQUAL(route->annExpires, time::steady_clock::now() + 3_h);
+  BOOST_CHECK_EQUAL(route->expires.value(), time::steady_clock::now() + 3_h); // capped by annExpires
+}
+
+BOOST_AUTO_TEST_CASE(RenewShorten)
+{
+  auto pa = makeTrustedAnn("/5XCHYCAd", 4_h);
+  BOOST_CHECK_EQUAL(slAnnounceSync(pa, 3851, 4_h), SlAnnounceResult::OK);
+  advanceClocks(1_h); // Route has 3_h remaining lifetime
+
+  BOOST_CHECK_EQUAL(slRenewSync("/5XCHYCAd", 3851, 1_h), SlAnnounceResult::OK);
+
+  Route* route = findAnnRoute("/5XCHYCAd", 3851);
+  BOOST_REQUIRE(route != nullptr);
+  BOOST_CHECK_EQUAL(route->annExpires, time::steady_clock::now() + 3_h);
+  BOOST_CHECK_EQUAL(route->expires.value(), time::steady_clock::now() + 1_h); // set by slRenew
+}
+
+BOOST_AUTO_TEST_CASE(RenewShorten_Zero)
+{
+  auto pa = makeTrustedAnn("/cdQ7KPNw", 4_h);
+  BOOST_CHECK_EQUAL(slAnnounceSync(pa, 8031, 4_h), SlAnnounceResult::OK);
+  advanceClocks(1_h); // Route has 3_h remaining lifetime
+
+  BOOST_CHECK_EQUAL(slRenewSync("/cdQ7KPNw", 8031, 0_s), SlAnnounceResult::EXPIRED);
+
+  BOOST_CHECK(findAnnRoute("/cdQ7KPNw", 8031) == nullptr);
+}
+
+BOOST_AUTO_TEST_CASE(FindExisting)
+{
+  auto pa = makeTrustedAnn("/JHugsjjr", 1_h);
+  BOOST_CHECK_EQUAL(slAnnounceSync(pa, 2363, 1_h), SlAnnounceResult::OK);
+
+  auto found = slFindAnnSync("/JHugsjjr");
+  BOOST_REQUIRE(found);
+  BOOST_CHECK_EQUAL(found->getAnnouncedName(), "/JHugsjjr");
+  BOOST_CHECK(found->getData());
+
+  auto found2 = slFindAnnSync("/JHugsjjr/StvXhKR5");
+  BOOST_CHECK(found == found2);
+}
+
+BOOST_AUTO_TEST_CASE(FindNew)
+{
+  Route route;
+  route.faceId = 1367;
+  route.origin = ndn::nfd::ROUTE_ORIGIN_APP;
+  rib.insert("/dLY1pRhR", route);
+
+  auto pa = slFindAnnSync("/dLY1pRhR/3qNK9Ngn");
+  BOOST_REQUIRE(pa);
+  BOOST_CHECK_EQUAL(pa->getAnnouncedName(), "/dLY1pRhR");
+}
+
+BOOST_AUTO_TEST_CASE(FindNone)
+{
+  auto pa = slFindAnnSync("/2YNeYuV2");
+  BOOST_CHECK(!pa);
+}
+
+BOOST_AUTO_TEST_SUITE_END() // TestSlAnnounce
+
+} // namespace tests
+} // namespace rib
+} // namespace nfd