Rename 'tests/unit-tests' directory to 'tests/unit'

Change-Id: I78ea29938259fac288781bed12fb2399ac7eba26
diff --git a/tests/unit/net/collect-netifs.cpp b/tests/unit/net/collect-netifs.cpp
new file mode 100644
index 0000000..ca8e6ef
--- /dev/null
+++ b/tests/unit/net/collect-netifs.cpp
@@ -0,0 +1,64 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-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 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 "collect-netifs.hpp"
+#include "net/network-monitor.hpp"
+
+#include <boost/asio/io_service.hpp>
+
+namespace ndn {
+namespace net {
+namespace tests {
+
+std::vector<shared_ptr<const NetworkInterface>>
+collectNetworkInterfaces(bool allowCached)
+{
+  static std::vector<shared_ptr<const NetworkInterface>> cached;
+  // cached.empty() indicates there's no cached list of netifs.
+  // Although it could also mean a system without any network interface, this situation is rare
+  // because the loopback interface is present on almost all systems.
+
+  if (!allowCached || cached.empty()) {
+    boost::asio::io_service io;
+    NetworkMonitor netmon(io);
+    if ((netmon.getCapabilities() & NetworkMonitor::CAP_ENUM) == 0) {
+      BOOST_THROW_EXCEPTION(NetworkMonitor::Error("NetworkMonitor::CAP_ENUM is unavailable"));
+    }
+
+    netmon.onEnumerationCompleted.connect([&io] { io.stop(); });
+    io.run();
+    io.reset();
+
+    cached = netmon.listNetworkInterfaces();
+  }
+
+  return cached;
+}
+
+} // namespace tests
+} // namespace net
+} // namespace ndn
diff --git a/tests/unit/net/collect-netifs.hpp b/tests/unit/net/collect-netifs.hpp
new file mode 100644
index 0000000..322e323
--- /dev/null
+++ b/tests/unit/net/collect-netifs.hpp
@@ -0,0 +1,52 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-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 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 NFD_TESTS_UNIT_TESTS_NET_COLLECT_NETIFS_HPP
+#define NFD_TESTS_UNIT_TESTS_NET_COLLECT_NETIFS_HPP
+
+#include "common.hpp"
+#include "net/network-interface.hpp"
+
+#include <vector>
+
+namespace ndn {
+namespace net {
+namespace tests {
+
+/** \brief Collect information about network interfaces
+ *  \param allowCached if true, previously collected information can be returned
+ *  \note This function is blocking if \p allowCached is false or no previous information exists
+ *  \throw ndn::net::NetworkMonitor::Error NetworkMonitor::CAP_ENUM is unavailable
+ */
+std::vector<shared_ptr<const NetworkInterface>>
+collectNetworkInterfaces(bool allowCached = true);
+
+} // namespace tests
+} // namespace net
+} // namespace ndn
+
+#endif // NFD_TESTS_UNIT_TESTS_NET_COLLECT_NETIFS_HPP
diff --git a/tests/unit/net/dns.t.cpp b/tests/unit/net/dns.t.cpp
new file mode 100644
index 0000000..e4afa8e
--- /dev/null
+++ b/tests/unit/net/dns.t.cpp
@@ -0,0 +1,185 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2018 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 "net/dns.hpp"
+
+#include "boost-test.hpp"
+#include "network-configuration-detector.hpp"
+
+#include <boost/asio/io_service.hpp>
+
+namespace ndn {
+namespace dns {
+namespace tests {
+
+using boost::asio::ip::address_v4;
+using boost::asio::ip::address_v6;
+
+class DnsFixture
+{
+public:
+  DnsFixture()
+    : m_nFailures(0)
+    , m_nSuccesses(0)
+  {
+  }
+
+  void
+  onSuccess(const IpAddress& resolvedAddress,
+            const IpAddress& expectedAddress,
+            bool isValid,
+            bool shouldCheckAddress = false)
+  {
+    ++m_nSuccesses;
+
+    if (!isValid) {
+      BOOST_FAIL("Resolved to " + resolvedAddress.to_string() + ", but should have failed");
+    }
+
+    BOOST_CHECK_EQUAL(resolvedAddress.is_v4(), expectedAddress.is_v4());
+
+    // checking address is not deterministic and should be enabled only
+    // if only one IP address will be returned by resolution
+    if (shouldCheckAddress) {
+      BOOST_CHECK_EQUAL(resolvedAddress, expectedAddress);
+    }
+  }
+
+  void
+  onFailure(bool isValid)
+  {
+    ++m_nFailures;
+
+    if (!isValid) {
+      BOOST_FAIL("Resolution should not have failed");
+    }
+
+    BOOST_CHECK_MESSAGE(true, "Resolution failed as expected");
+  }
+
+protected:
+  uint32_t m_nFailures;
+  uint32_t m_nSuccesses;
+  boost::asio::io_service m_ioService;
+};
+
+BOOST_AUTO_TEST_SUITE(Net)
+BOOST_FIXTURE_TEST_SUITE(TestDns, DnsFixture)
+
+BOOST_AUTO_TEST_CASE(Asynchronous)
+{
+  SKIP_IF_IP_UNAVAILABLE();
+
+  asyncResolve("nothost.nothost.nothost.arpa",
+               bind(&DnsFixture::onSuccess, this, _1, IpAddress(address_v4()), false, false),
+               bind(&DnsFixture::onFailure, this, true),
+               m_ioService); // should fail
+
+  m_ioService.run();
+  BOOST_CHECK_EQUAL(m_nFailures, 1);
+  BOOST_CHECK_EQUAL(m_nSuccesses, 0);
+}
+
+BOOST_AUTO_TEST_CASE(AsynchronousV4)
+{
+  SKIP_IF_IPV4_UNAVAILABLE();
+
+  asyncResolve("192.0.2.1",
+               bind(&DnsFixture::onSuccess, this, _1,
+                    IpAddress(address_v4::from_string("192.0.2.1")), true, true),
+               bind(&DnsFixture::onFailure, this, false),
+               m_ioService);
+
+  m_ioService.run();
+  BOOST_CHECK_EQUAL(m_nFailures, 0);
+  BOOST_CHECK_EQUAL(m_nSuccesses, 1);
+}
+
+BOOST_AUTO_TEST_CASE(AsynchronousV6)
+{
+  SKIP_IF_IPV6_UNAVAILABLE();
+
+  asyncResolve("ipv6.google.com", // only IPv6 address should be available
+               bind(&DnsFixture::onSuccess, this, _1, IpAddress(address_v6()), true, false),
+               bind(&DnsFixture::onFailure, this, false),
+               m_ioService);
+
+  asyncResolve("2001:db8:3f9:0:3025:ccc5:eeeb:86d3",
+               bind(&DnsFixture::onSuccess, this, _1,
+                    IpAddress(address_v6::from_string("2001:db8:3f9:0:3025:ccc5:eeeb:86d3")),
+                    true, true),
+               bind(&DnsFixture::onFailure, this, false),
+               m_ioService);
+
+  m_ioService.run();
+  BOOST_CHECK_EQUAL(m_nFailures, 0);
+  BOOST_CHECK_EQUAL(m_nSuccesses, 2);
+}
+
+BOOST_AUTO_TEST_CASE(AsynchronousV4AndV6)
+{
+  SKIP_IF_IPV4_UNAVAILABLE();
+  SKIP_IF_IPV6_UNAVAILABLE();
+
+  asyncResolve("www.named-data.net",
+               bind(&DnsFixture::onSuccess, this, _1, IpAddress(address_v4()), true, false),
+               bind(&DnsFixture::onFailure, this, false),
+               m_ioService, Ipv4Only());
+
+  asyncResolve("a.root-servers.net",
+               bind(&DnsFixture::onSuccess, this, _1, IpAddress(address_v4()), true, false),
+               bind(&DnsFixture::onFailure, this, false),
+               m_ioService, Ipv4Only()); // request IPv4 address
+
+  asyncResolve("a.root-servers.net",
+               bind(&DnsFixture::onSuccess, this, _1, IpAddress(address_v6()), true, false),
+               bind(&DnsFixture::onFailure, this, false),
+               m_ioService, Ipv6Only()); // request IPv6 address
+
+  asyncResolve("ipv6.google.com", // only IPv6 address should be available
+               bind(&DnsFixture::onSuccess, this, _1, IpAddress(address_v6()), true, false),
+               bind(&DnsFixture::onFailure, this, false),
+               m_ioService, Ipv6Only());
+
+  asyncResolve("ipv6.google.com", // only IPv6 address should be available
+               bind(&DnsFixture::onSuccess, this, _1, IpAddress(address_v6()), false, false),
+               bind(&DnsFixture::onFailure, this, true),
+               m_ioService, Ipv4Only()); // should fail
+
+  m_ioService.run();
+  BOOST_CHECK_EQUAL(m_nFailures, 1);
+  BOOST_CHECK_EQUAL(m_nSuccesses, 4);
+}
+
+BOOST_AUTO_TEST_CASE(Synchronous)
+{
+  SKIP_IF_IP_UNAVAILABLE();
+
+  IpAddress address = syncResolve("www.named-data.net", m_ioService);
+  BOOST_CHECK(address.is_v4() || address.is_v6());
+}
+
+BOOST_AUTO_TEST_SUITE_END() // TestDns
+BOOST_AUTO_TEST_SUITE_END() // Net
+
+} // namespace tests
+} // namespace dns
+} // namespace ndn
diff --git a/tests/unit/net/ethernet.t.cpp b/tests/unit/net/ethernet.t.cpp
new file mode 100644
index 0000000..4f76dbb
--- /dev/null
+++ b/tests/unit/net/ethernet.t.cpp
@@ -0,0 +1,117 @@
+/* -*- 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 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 "net/ethernet.hpp"
+
+#include "boost-test.hpp"
+
+namespace ndn {
+namespace tests {
+
+BOOST_AUTO_TEST_SUITE(Net)
+BOOST_AUTO_TEST_SUITE(TestEthernet)
+
+BOOST_AUTO_TEST_CASE(Basic)
+{
+  ethernet::Address a;
+  BOOST_CHECK(a.isNull());
+
+  a = {0x01, 0x23, 0x45, 0x67, 0x89, 0xAB};
+  ethernet::Address b(0x01, 0x23, 0x45, 0x67, 0x89, 0xAB);
+  const uint8_t bytes[] = {0x01, 0x23, 0x45, 0x67, 0x89, 0xAB};
+  ethernet::Address c(bytes);
+  ethernet::Address d(a);
+  ethernet::Address e;
+  e = a;
+
+  BOOST_CHECK_EQUAL(a, b);
+  BOOST_CHECK_EQUAL(a, c);
+  BOOST_CHECK_EQUAL(a, d);
+  BOOST_CHECK_EQUAL(a, e);
+
+  BOOST_CHECK(ethernet::getBroadcastAddress().isBroadcast());
+  BOOST_CHECK(ethernet::getDefaultMulticastAddress().isMulticast());
+}
+
+BOOST_AUTO_TEST_CASE(ToString)
+{
+  BOOST_CHECK_EQUAL(ethernet::Address().toString('-'),
+                    "00-00-00-00-00-00");
+  BOOST_CHECK_EQUAL(ethernet::getBroadcastAddress().toString(),
+                    "ff:ff:ff:ff:ff:ff");
+  BOOST_CHECK_EQUAL(ethernet::Address(0x01, 0x23, 0x45, 0x67, 0x89, 0xAB).toString('-'),
+                    "01-23-45-67-89-ab");
+  BOOST_CHECK_EQUAL(ethernet::Address(0x01, 0x23, 0x45, 0x67, 0x89, 0xAB).toString(),
+                    "01:23:45:67:89:ab");
+}
+
+BOOST_AUTO_TEST_CASE(FromString)
+{
+  BOOST_CHECK_EQUAL(ethernet::Address::fromString("0:0:0:0:0:0"),
+                    ethernet::Address());
+  BOOST_CHECK_EQUAL(ethernet::Address::fromString("ff-ff-ff-ff-ff-ff"),
+                    ethernet::getBroadcastAddress());
+  BOOST_CHECK_EQUAL(ethernet::Address::fromString("de:ad:be:ef:1:2"),
+                    ethernet::Address(0xde, 0xad, 0xbe, 0xef, 0x01, 0x02));
+  BOOST_CHECK_EQUAL(ethernet::Address::fromString("DE:AD:BE:EF:1:2"),
+                    ethernet::Address(0xde, 0xad, 0xbe, 0xef, 0x01, 0x02));
+
+  // malformed inputs
+  BOOST_CHECK_EQUAL(ethernet::Address::fromString("01.23.45.67.89.ab"),
+                    ethernet::Address());
+  BOOST_CHECK_EQUAL(ethernet::Address::fromString("01:23:45 :67:89:ab"),
+                    ethernet::Address());
+  BOOST_CHECK_EQUAL(ethernet::Address::fromString("01:23:45:67:89::1"),
+                    ethernet::Address());
+  BOOST_CHECK_EQUAL(ethernet::Address::fromString("01-23-45-67-89"),
+                    ethernet::Address());
+  BOOST_CHECK_EQUAL(ethernet::Address::fromString("01:23:45:67:89:ab:cd"),
+                    ethernet::Address());
+  BOOST_CHECK_EQUAL(ethernet::Address::fromString("01:23:45:67-89-ab"),
+                    ethernet::Address());
+  BOOST_CHECK_EQUAL(ethernet::Address::fromString("qw-er-ty-12-34-56"),
+                    ethernet::Address());
+  BOOST_CHECK_EQUAL(ethernet::Address::fromString("this-is-not-an-ethernet-address"),
+                    ethernet::Address());
+  BOOST_CHECK_EQUAL(ethernet::Address::fromString("foobar"),
+                    ethernet::Address());
+  BOOST_CHECK_EQUAL(ethernet::Address::fromString(""),
+                    ethernet::Address());
+}
+
+BOOST_AUTO_TEST_CASE(StdHash)
+{
+  // make sure we can use ethernet::Address as key type in std::unordered_map
+  std::hash<ethernet::Address> h;
+  BOOST_CHECK_NO_THROW(h(ethernet::getDefaultMulticastAddress()));
+}
+
+BOOST_AUTO_TEST_SUITE_END() // TestEthernet
+BOOST_AUTO_TEST_SUITE_END() // Net
+
+} // namespace tests
+} // namespace ndn
diff --git a/tests/unit/net/face-uri.t.cpp b/tests/unit/net/face-uri.t.cpp
new file mode 100644
index 0000000..71d76b1
--- /dev/null
+++ b/tests/unit/net/face-uri.t.cpp
@@ -0,0 +1,647 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-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 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 "net/face-uri.hpp"
+
+#include "boost-test.hpp"
+#include "collect-netifs.hpp"
+#include "network-configuration-detector.hpp"
+
+namespace ndn {
+namespace tests {
+
+BOOST_AUTO_TEST_SUITE(Net)
+BOOST_AUTO_TEST_SUITE(TestFaceUri)
+
+class CanonizeFixture : noncopyable
+{
+protected:
+  void
+  addTest(const std::string& request, bool shouldSucceed, const std::string& expectedUri)
+  {
+    ++m_nPending;
+    auto tc = make_shared<CanonizeTestCase>(request, shouldSucceed, expectedUri);
+
+    FaceUri uri(request);
+    uri.canonize(bind(&CanonizeFixture::onCanonizeSuccess, this, tc, _1),
+                 bind(&CanonizeFixture::onCanonizeFailure, this, tc, _1),
+                 m_io, 10_s);
+  }
+
+  void
+  runTests()
+  {
+    m_io.run();
+    BOOST_CHECK_EQUAL(m_nPending, 0);
+  }
+
+private:
+  class CanonizeTestCase
+  {
+  public:
+    CanonizeTestCase(const std::string& request, bool shouldSucceed, const std::string& expectedUri)
+      : m_expectedUri(expectedUri)
+      , m_message(request + " should " + (shouldSucceed ? "succeed" : "fail"))
+      , m_shouldSucceed(shouldSucceed)
+      , m_isCompleted(false)
+    {
+    }
+
+  public:
+    std::string m_expectedUri;
+    std::string m_message;
+    bool m_shouldSucceed;
+    bool m_isCompleted;
+  };
+
+  void
+  onCanonizeSuccess(const shared_ptr<CanonizeTestCase>& tc, const FaceUri& canonicalUri)
+  {
+    BOOST_CHECK_EQUAL(tc->m_isCompleted, false);
+    tc->m_isCompleted = true;
+    --m_nPending;
+
+    BOOST_CHECK_MESSAGE(tc->m_shouldSucceed, tc->m_message);
+    if (tc->m_shouldSucceed) {
+      BOOST_CHECK_EQUAL(canonicalUri.toString(), tc->m_expectedUri);
+    }
+  }
+
+  void
+  onCanonizeFailure(const shared_ptr<CanonizeTestCase>& tc, const std::string& reason)
+  {
+    BOOST_CHECK_EQUAL(tc->m_isCompleted, false);
+    tc->m_isCompleted = true;
+    --m_nPending;
+
+    BOOST_CHECK_MESSAGE(!tc->m_shouldSucceed, tc->m_message);
+  }
+
+private:
+  boost::asio::io_service m_io;
+  ssize_t m_nPending = 0;
+};
+
+BOOST_AUTO_TEST_CASE(ParseInternal)
+{
+  FaceUri uri;
+
+  BOOST_CHECK(uri.parse("internal://"));
+  BOOST_CHECK_EQUAL(uri.getScheme(), "internal");
+  BOOST_CHECK_EQUAL(uri.getHost(), "");
+  BOOST_CHECK_EQUAL(uri.getPort(), "");
+  BOOST_CHECK_EQUAL(uri.getPath(), "");
+
+  BOOST_CHECK_EQUAL(uri.parse("internal:"), false);
+  BOOST_CHECK_EQUAL(uri.parse("internal:/"), false);
+}
+
+BOOST_AUTO_TEST_CASE(ParseUdp)
+{
+  FaceUri uri("udp://hostname:6363");
+  BOOST_CHECK_THROW(FaceUri("udp//hostname:6363"), FaceUri::Error);
+  BOOST_CHECK_THROW(FaceUri("udp://hostname:port"), FaceUri::Error);
+
+  BOOST_CHECK_EQUAL(uri.parse("udp//hostname:6363"), false);
+
+  BOOST_CHECK(uri.parse("udp://hostname:80"));
+  BOOST_CHECK_EQUAL(uri.getScheme(), "udp");
+  BOOST_CHECK_EQUAL(uri.getHost(), "hostname");
+  BOOST_CHECK_EQUAL(uri.getPort(), "80");
+  BOOST_CHECK_EQUAL(uri.getPath(), "");
+
+  BOOST_CHECK(uri.parse("udp4://192.0.2.1:20"));
+  BOOST_CHECK_EQUAL(uri.getScheme(), "udp4");
+  BOOST_CHECK_EQUAL(uri.getHost(), "192.0.2.1");
+  BOOST_CHECK_EQUAL(uri.getPort(), "20");
+  BOOST_CHECK_EQUAL(uri.getPath(), "");
+
+  BOOST_CHECK(uri.parse("udp6://[2001:db8:3f9:0::1]:6363"));
+  BOOST_CHECK_EQUAL(uri.getScheme(), "udp6");
+  BOOST_CHECK_EQUAL(uri.getHost(), "2001:db8:3f9:0::1");
+  BOOST_CHECK_EQUAL(uri.getPort(), "6363");
+  BOOST_CHECK_EQUAL(uri.getPath(), "");
+
+  BOOST_CHECK(uri.parse("udp6://[2001:db8:3f9:0:3025:ccc5:eeeb:86d3]:6363"));
+  BOOST_CHECK_EQUAL(uri.getScheme(), "udp6");
+  BOOST_CHECK_EQUAL(uri.getHost(), "2001:db8:3f9:0:3025:ccc5:eeeb:86d3");
+  BOOST_CHECK_EQUAL(uri.getPort(), "6363");
+  BOOST_CHECK_EQUAL(uri.getPath(), "");
+
+  BOOST_CHECK_EQUAL(uri.parse("udp6://[2001:db8:3f9:0:3025:ccc5:eeeb:86dg]:6363"), false);
+
+  namespace ip = boost::asio::ip;
+
+  ip::udp::endpoint endpoint4(ip::address_v4::from_string("192.0.2.1"), 7777);
+  uri = FaceUri(endpoint4);
+  BOOST_CHECK_EQUAL(uri.toString(), "udp4://192.0.2.1:7777");
+
+  ip::udp::endpoint endpoint6(ip::address_v6::from_string("2001:DB8::1"), 7777);
+  uri = FaceUri(endpoint6);
+  BOOST_CHECK_EQUAL(uri.toString(), "udp6://[2001:db8::1]:7777");
+
+  BOOST_CHECK(uri.parse("udp6://[fe80::1%25eth1]:6363"));
+  BOOST_CHECK_EQUAL(uri.getHost(), "fe80::1%25eth1");
+
+  BOOST_CHECK(uri.parse("udp6://[fe80::1%eth1]:6363"));
+  BOOST_CHECK_EQUAL(uri.getHost(), "fe80::1%eth1");
+
+  BOOST_CHECK(uri.parse("udp6://[fe80::1%1]:6363"));
+  BOOST_CHECK(uri.parse("udp6://[fe80::1%eth1]"));
+
+  BOOST_CHECK(uri.parse("udp6://[ff01::114%eth#1]"));
+  BOOST_CHECK(uri.parse("udp6://[ff01::114%eth.1,2]"));
+  BOOST_CHECK(uri.parse("udp6://[ff01::114%a+b-c=0]"));
+  BOOST_CHECK(uri.parse("udp6://[ff01::114%[foo]]"));
+  BOOST_CHECK(uri.parse("udp6://[ff01::114%]]"));
+  BOOST_CHECK(uri.parse("udp6://[ff01::114%%]"));
+  BOOST_CHECK(!uri.parse("udp6://[ff01::114%]"));
+  BOOST_CHECK(!uri.parse("udp6://[ff01::114%foo bar]"));
+  BOOST_CHECK(!uri.parse("udp6://[ff01::114%foo/bar]"));
+  BOOST_CHECK(!uri.parse("udp6://[ff01::114%eth0:1]"));
+}
+
+BOOST_FIXTURE_TEST_CASE(IsCanonicalUdp, CanonizeFixture)
+{
+  BOOST_CHECK_EQUAL(FaceUri::canCanonize("udp"), true);
+  BOOST_CHECK_EQUAL(FaceUri::canCanonize("udp4"), true);
+  BOOST_CHECK_EQUAL(FaceUri::canCanonize("udp6"), true);
+
+  BOOST_CHECK_EQUAL(FaceUri("udp4://192.0.2.1:6363").isCanonical(), true);
+  BOOST_CHECK_EQUAL(FaceUri("udp://192.0.2.1:6363").isCanonical(), false);
+  BOOST_CHECK_EQUAL(FaceUri("udp4://192.0.2.1").isCanonical(), false);
+  BOOST_CHECK_EQUAL(FaceUri("udp4://192.0.2.1:6363/").isCanonical(), false);
+  BOOST_CHECK_EQUAL(FaceUri("udp6://[2001:db8::1]:6363").isCanonical(), true);
+  BOOST_CHECK_EQUAL(FaceUri("udp6://[2001:db8::01]:6363").isCanonical(), false);
+  BOOST_CHECK_EQUAL(FaceUri("udp://[2001:db8::1]:6363").isCanonical(), false);
+  BOOST_CHECK_EQUAL(FaceUri("udp://example.net:6363").isCanonical(), false);
+  BOOST_CHECK_EQUAL(FaceUri("udp4://example.net:6363").isCanonical(), false);
+  BOOST_CHECK_EQUAL(FaceUri("udp6://example.net:6363").isCanonical(), false);
+  BOOST_CHECK_EQUAL(FaceUri("udp4://224.0.23.170:56363").isCanonical(), true);
+  BOOST_CHECK_EQUAL(FaceUri("udp4://[2001:db8::1]:6363").isCanonical(), false);
+  BOOST_CHECK_EQUAL(FaceUri("udp6://192.0.2.1:6363").isCanonical(), false);
+
+  const auto& networkInterfaces = ndn::net::tests::collectNetworkInterfaces();
+  if (!networkInterfaces.empty()) {
+    const auto& netif = networkInterfaces.front();
+    auto name = netif->getName();
+    auto index = to_string(netif->getIndex());
+
+    BOOST_CHECK_EQUAL(FaceUri("udp6://[fe80::1%" + name + "]:6363").isCanonical(), true);
+    BOOST_CHECK_EQUAL(FaceUri("udp6://[fe80::1%" + index + "]:6363").isCanonical(), false);
+    BOOST_CHECK_EQUAL(FaceUri("udp6://[fe80::1%" + name + "]").isCanonical(), false);
+    BOOST_CHECK_EQUAL(FaceUri("udp6://[fe80::1068:dddb:fe26:fe3f%25en0]:6363").isCanonical(), false);
+  }
+}
+
+BOOST_AUTO_TEST_CASE_EXPECTED_FAILURES(CanonizeUdpV4, 1)
+BOOST_FIXTURE_TEST_CASE(CanonizeUdpV4, CanonizeFixture)
+{
+  SKIP_IF_IPV4_UNAVAILABLE();
+
+  // IPv4 unicast
+  addTest("udp4://192.0.2.1:6363", true, "udp4://192.0.2.1:6363");
+  addTest("udp://192.0.2.2:6363", true, "udp4://192.0.2.2:6363");
+  addTest("udp4://192.0.2.3", true, "udp4://192.0.2.3:6363");
+  addTest("udp4://192.0.2.4:6363/", true, "udp4://192.0.2.4:6363");
+  addTest("udp4://192.0.2.5:9695", true, "udp4://192.0.2.5:9695");
+  addTest("udp4://192.0.2.666:6363", false, "");
+  addTest("udp4://192.0.2.7:99999", false, ""); // Bug #3897
+  addTest("udp4://google-public-dns-a.google.com", true, "udp4://8.8.8.8:6363");
+  addTest("udp4://google-public-dns-a.google.com:70000", false, "");
+  addTest("udp4://invalid.invalid", false, "");
+
+  // IPv4 multicast
+  addTest("udp4://224.0.23.170:56363", true, "udp4://224.0.23.170:56363");
+  addTest("udp4://224.0.23.170", true, "udp4://224.0.23.170:56363");
+  addTest("udp4://all-routers.mcast.net:56363", true, "udp4://224.0.0.2:56363");
+
+  // IPv6 used with udp4 protocol - not canonical
+  addTest("udp4://[2001:db8::1]:6363", false, "");
+
+  runTests();
+}
+
+BOOST_AUTO_TEST_CASE_EXPECTED_FAILURES(CanonizeUdpV6, 1)
+BOOST_FIXTURE_TEST_CASE(CanonizeUdpV6, CanonizeFixture)
+{
+  SKIP_IF_IPV6_UNAVAILABLE();
+
+  // IPv6 unicast
+  addTest("udp6://[2001:db8::1]:6363", true, "udp6://[2001:db8::1]:6363");
+  addTest("udp6://[2001:db8::1]", true, "udp6://[2001:db8::1]:6363");
+  addTest("udp://[2001:db8::1]:6363", true, "udp6://[2001:db8::1]:6363");
+  addTest("udp6://[2001:db8::01]:6363", true, "udp6://[2001:db8::1]:6363");
+  addTest("udp6://[2001::db8::1]:6363", false, "");
+  addTest("udp6://[2001:db8::1]:99999", false, ""); // Bug #3897
+  addTest("udp6://google-public-dns-a.google.com", true, "udp6://[2001:4860:4860::8888]:6363");
+  addTest("udp6://google-public-dns-a.google.com:70000", false, "");
+  addTest("udp6://invalid.invalid", false, "");
+  addTest("udp://invalid.invalid", false, "");
+
+  // IPv6 multicast
+  addTest("udp6://[ff02::2]:56363", true, "udp6://[ff02::2]:56363");
+  addTest("udp6://[ff02::2]", true, "udp6://[ff02::2]:56363");
+
+  // IPv4 used with udp6 protocol - not canonical
+  addTest("udp6://192.0.2.1:6363", false, "");
+
+  const auto& networkInterfaces = ndn::net::tests::collectNetworkInterfaces();
+  if (!networkInterfaces.empty()) {
+    const auto& netif = networkInterfaces.front();
+    auto name = netif->getName();
+    auto index = to_string(netif->getIndex());
+
+    addTest("udp6://[fe80::1068:dddb:fe26:fe3f%25" + name + "]:6363", true,
+            "udp6://[fe80::1068:dddb:fe26:fe3f%" + name + "]:6363");
+
+    addTest("udp6://[fe80::1068:dddb:fe26:fe3f%" + index + "]:6363", true,
+            "udp6://[fe80::1068:dddb:fe26:fe3f%" + name + "]:6363");
+  }
+
+  runTests();
+}
+
+BOOST_AUTO_TEST_CASE(ParseTcp)
+{
+  FaceUri uri;
+
+  BOOST_CHECK(uri.parse("tcp://random.host.name"));
+  BOOST_CHECK_EQUAL(uri.getScheme(), "tcp");
+  BOOST_CHECK_EQUAL(uri.getHost(), "random.host.name");
+  BOOST_CHECK_EQUAL(uri.getPort(), "");
+  BOOST_CHECK_EQUAL(uri.getPath(), "");
+
+  BOOST_CHECK_EQUAL(uri.parse("tcp://192.0.2.1:"), false);
+  BOOST_CHECK_EQUAL(uri.parse("tcp://[::zzzz]"), false);
+
+  namespace ip = boost::asio::ip;
+
+  ip::tcp::endpoint endpoint4(ip::address_v4::from_string("192.0.2.1"), 7777);
+  uri = FaceUri(endpoint4);
+  BOOST_CHECK_EQUAL(uri.toString(), "tcp4://192.0.2.1:7777");
+
+  uri = FaceUri(endpoint4, "wsclient");
+  BOOST_CHECK_EQUAL(uri.toString(), "wsclient://192.0.2.1:7777");
+
+  ip::tcp::endpoint endpoint6(ip::address_v6::from_string("2001:DB8::1"), 7777);
+  uri = FaceUri(endpoint6);
+  BOOST_CHECK_EQUAL(uri.toString(), "tcp6://[2001:db8::1]:7777");
+
+  BOOST_CHECK(uri.parse("tcp6://[fe80::1%25eth1]:6363"));
+  BOOST_CHECK_EQUAL(uri.getHost(), "fe80::1%25eth1");
+
+  BOOST_CHECK(uri.parse("tcp6://[fe80::1%eth1]:6363"));
+  BOOST_CHECK_EQUAL(uri.getHost(), "fe80::1%eth1");
+
+  BOOST_CHECK(uri.parse("tcp6://[fe80::1%1]:6363"));
+  BOOST_CHECK(uri.parse("tcp6://[fe80::1%eth1]"));
+}
+
+BOOST_FIXTURE_TEST_CASE(IsCanonicalTcp, CanonizeFixture)
+{
+  BOOST_CHECK_EQUAL(FaceUri::canCanonize("tcp"), true);
+  BOOST_CHECK_EQUAL(FaceUri::canCanonize("tcp4"), true);
+  BOOST_CHECK_EQUAL(FaceUri::canCanonize("tcp6"), true);
+
+  BOOST_CHECK_EQUAL(FaceUri("tcp4://192.0.2.1:6363").isCanonical(), true);
+  BOOST_CHECK_EQUAL(FaceUri("tcp://192.0.2.1:6363").isCanonical(), false);
+  BOOST_CHECK_EQUAL(FaceUri("tcp4://192.0.2.1").isCanonical(), false);
+  BOOST_CHECK_EQUAL(FaceUri("tcp4://192.0.2.1:6363/").isCanonical(), false);
+  BOOST_CHECK_EQUAL(FaceUri("tcp6://[2001:db8::1]:6363").isCanonical(), true);
+  BOOST_CHECK_EQUAL(FaceUri("tcp6://[2001:db8::01]:6363").isCanonical(), false);
+  BOOST_CHECK_EQUAL(FaceUri("tcp://[2001:db8::1]:6363").isCanonical(), false);
+  BOOST_CHECK_EQUAL(FaceUri("tcp://example.net:6363").isCanonical(), false);
+  BOOST_CHECK_EQUAL(FaceUri("tcp4://example.net:6363").isCanonical(), false);
+  BOOST_CHECK_EQUAL(FaceUri("tcp6://example.net:6363").isCanonical(), false);
+  BOOST_CHECK_EQUAL(FaceUri("tcp4://224.0.23.170:56363").isCanonical(), false);
+  BOOST_CHECK_EQUAL(FaceUri("tcp4://[2001:db8::1]:6363").isCanonical(), false);
+  BOOST_CHECK_EQUAL(FaceUri("tcp6://192.0.2.1:6363").isCanonical(), false);
+
+  const auto& networkInterfaces = ndn::net::tests::collectNetworkInterfaces();
+  if (!networkInterfaces.empty()) {
+    const auto& netif = networkInterfaces.front();
+    auto name = netif->getName();
+    auto index = to_string(netif->getIndex());
+
+    BOOST_CHECK_EQUAL(FaceUri("tcp6://[fe80::1%" + name + "]:6363").isCanonical(), true);
+    BOOST_CHECK_EQUAL(FaceUri("tcp6://[fe80::1%" + index + "]:6363").isCanonical(), false);
+    BOOST_CHECK_EQUAL(FaceUri("tcp6://[fe80::1%" + name + "]").isCanonical(), false);
+    BOOST_CHECK_EQUAL(FaceUri("tcp6://[fe80::1068:dddb:fe26:fe3f%25en0]:6363").isCanonical(), false);
+  }
+}
+
+BOOST_AUTO_TEST_CASE_EXPECTED_FAILURES(CanonizeTcpV4, 1)
+BOOST_FIXTURE_TEST_CASE(CanonizeTcpV4, CanonizeFixture)
+{
+  SKIP_IF_IPV4_UNAVAILABLE();
+
+  // IPv4 unicast
+  addTest("tcp4://192.0.2.1:6363", true, "tcp4://192.0.2.1:6363");
+  addTest("tcp://192.0.2.2:6363", true, "tcp4://192.0.2.2:6363");
+  addTest("tcp4://192.0.2.3", true, "tcp4://192.0.2.3:6363");
+  addTest("tcp4://192.0.2.4:6363/", true, "tcp4://192.0.2.4:6363");
+  addTest("tcp4://192.0.2.5:9695", true, "tcp4://192.0.2.5:9695");
+  addTest("tcp4://192.0.2.666:6363", false, "");
+  addTest("tcp4://192.0.2.7:99999", false, ""); // Bug #3897
+  addTest("tcp4://google-public-dns-a.google.com", true, "tcp4://8.8.8.8:6363");
+  addTest("tcp4://google-public-dns-a.google.com:70000", false, "");
+  addTest("tcp4://invalid.invalid", false, "");
+
+  // IPv4 multicast
+  addTest("tcp4://224.0.23.170:56363", false, "");
+  addTest("tcp4://224.0.23.170", false, "");
+  addTest("tcp4://all-routers.mcast.net:56363", false, "");
+
+  // IPv6 used with tcp4 protocol - not canonical
+  addTest("tcp4://[2001:db8::1]:6363", false, "");
+
+  const auto& networkInterfaces = ndn::net::tests::collectNetworkInterfaces();
+  if (!networkInterfaces.empty()) {
+    const auto& netif = networkInterfaces.front();
+    auto name = netif->getName();
+    auto index = to_string(netif->getIndex());
+
+    addTest("tcp6://[fe80::1068:dddb:fe26:fe3f%25" + name + "]:6363", true,
+            "tcp6://[fe80::1068:dddb:fe26:fe3f%" + name + "]:6363");
+
+    addTest("tcp6://[fe80::1068:dddb:fe26:fe3f%" + index + "]:6363", true,
+            "tcp6://[fe80::1068:dddb:fe26:fe3f%" + name + "]:6363");
+  }
+
+  runTests();
+}
+
+BOOST_AUTO_TEST_CASE_EXPECTED_FAILURES(CanonizeTcpV6, 1)
+BOOST_FIXTURE_TEST_CASE(CanonizeTcpV6, CanonizeFixture)
+{
+  SKIP_IF_IPV6_UNAVAILABLE();
+
+  // IPv6 unicast
+  addTest("tcp6://[2001:db8::1]:6363", true, "tcp6://[2001:db8::1]:6363");
+  addTest("tcp6://[2001:db8::1]", true, "tcp6://[2001:db8::1]:6363");
+  addTest("tcp://[2001:db8::1]:6363", true, "tcp6://[2001:db8::1]:6363");
+  addTest("tcp6://[2001:db8::01]:6363", true, "tcp6://[2001:db8::1]:6363");
+  addTest("tcp6://[2001::db8::1]:6363", false, "");
+  addTest("tcp6://[2001:db8::1]:99999", false, ""); // Bug #3897
+  addTest("tcp6://google-public-dns-a.google.com", true, "tcp6://[2001:4860:4860::8888]:6363");
+  addTest("tcp6://google-public-dns-a.google.com:70000", false, "");
+  addTest("tcp6://invalid.invalid", false, "");
+  addTest("tcp://invalid.invalid", false, "");
+
+  // IPv6 multicast
+  addTest("tcp6://[ff02::2]:56363", false, "");
+  addTest("tcp6://[ff02::2]", false, "");
+
+  // IPv4 used with tcp6 protocol - not canonical
+  addTest("tcp6://192.0.2.1:6363", false, "");
+
+  runTests();
+}
+
+BOOST_AUTO_TEST_CASE_EXPECTED_FAILURES(ParseUnix, 1)
+BOOST_AUTO_TEST_CASE(ParseUnix)
+{
+  FaceUri uri;
+
+  BOOST_CHECK(uri.parse("unix:///var/run/example.sock"));
+  BOOST_CHECK_EQUAL(uri.getScheme(), "unix");
+  BOOST_CHECK_EQUAL(uri.getHost(), "");
+  BOOST_CHECK_EQUAL(uri.getPort(), "");
+  BOOST_CHECK_EQUAL(uri.getPath(), "/var/run/example.sock");
+
+  // This is not a valid unix:// URI, but the parse would treat "var" as host
+  BOOST_CHECK_EQUAL(uri.parse("unix://var/run/example.sock"), false); // Bug #3896
+
+#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS
+  using boost::asio::local::stream_protocol;
+  stream_protocol::endpoint endpoint("/var/run/example.sock");
+  uri = FaceUri(endpoint);
+  BOOST_CHECK_EQUAL(uri.toString(), "unix:///var/run/example.sock");
+#endif // BOOST_ASIO_HAS_LOCAL_SOCKETS
+}
+
+BOOST_AUTO_TEST_CASE(ParseFd)
+{
+  FaceUri uri;
+
+  BOOST_CHECK(uri.parse("fd://6"));
+  BOOST_CHECK_EQUAL(uri.getScheme(), "fd");
+  BOOST_CHECK_EQUAL(uri.getHost(), "6");
+  BOOST_CHECK_EQUAL(uri.getPort(), "");
+  BOOST_CHECK_EQUAL(uri.getPath(), "");
+
+  int fd = 21;
+  uri = FaceUri::fromFd(fd);
+  BOOST_CHECK_EQUAL(uri.toString(), "fd://21");
+}
+
+BOOST_AUTO_TEST_CASE(ParseEther)
+{
+  FaceUri uri;
+
+  BOOST_CHECK(uri.parse("ether://[08:00:27:01:dd:01]"));
+  BOOST_CHECK_EQUAL(uri.getScheme(), "ether");
+  BOOST_CHECK_EQUAL(uri.getHost(), "08:00:27:01:dd:01");
+  BOOST_CHECK_EQUAL(uri.getPort(), "");
+  BOOST_CHECK_EQUAL(uri.getPath(), "");
+
+  BOOST_CHECK_EQUAL(uri.parse("ether://[08:00:27:zz:dd:01]"), false);
+
+  auto address = ethernet::Address::fromString("33:33:01:01:01:01");
+  uri = FaceUri(address);
+  BOOST_CHECK_EQUAL(uri.toString(), "ether://[33:33:01:01:01:01]");
+}
+
+BOOST_FIXTURE_TEST_CASE(CanonizeEther, CanonizeFixture)
+{
+  BOOST_CHECK_EQUAL(FaceUri::canCanonize("ether"), true);
+
+  BOOST_CHECK_EQUAL(FaceUri("ether://[08:00:27:01:01:01]").isCanonical(), true);
+  BOOST_CHECK_EQUAL(FaceUri("ether://[08:00:27:1:1:1]").isCanonical(), false);
+  BOOST_CHECK_EQUAL(FaceUri("ether://[08:00:27:01:01:01]/").isCanonical(), false);
+  BOOST_CHECK_EQUAL(FaceUri("ether://[33:33:01:01:01:01]").isCanonical(), true);
+
+  addTest("ether://[08:00:27:01:01:01]", true, "ether://[08:00:27:01:01:01]");
+  addTest("ether://[08:00:27:1:1:1]", true, "ether://[08:00:27:01:01:01]");
+  addTest("ether://[08:00:27:01:01:01]/", true, "ether://[08:00:27:01:01:01]");
+  addTest("ether://[33:33:01:01:01:01]", true, "ether://[33:33:01:01:01:01]");
+
+  runTests();
+}
+
+BOOST_AUTO_TEST_CASE_EXPECTED_FAILURES(ParseDev, 1)
+BOOST_AUTO_TEST_CASE(ParseDev)
+{
+  FaceUri uri;
+
+  BOOST_CHECK(uri.parse("dev://eth0"));
+  BOOST_CHECK_EQUAL(uri.getScheme(), "dev");
+  BOOST_CHECK_EQUAL(uri.getHost(), "eth0");
+  BOOST_CHECK_EQUAL(uri.getPort(), "");
+  BOOST_CHECK_EQUAL(uri.getPath(), "");
+
+  BOOST_CHECK_EQUAL(uri.parse("dev://eth0:8888"), false); // Bug #3896
+
+  std::string ifname = "en1";
+  uri = FaceUri::fromDev(ifname);
+  BOOST_CHECK_EQUAL(uri.toString(), "dev://en1");
+}
+
+BOOST_AUTO_TEST_CASE(IsCanonicalDev)
+{
+  BOOST_CHECK_EQUAL(FaceUri::canCanonize("dev"), true);
+
+  BOOST_CHECK_EQUAL(FaceUri("dev://eth0").isCanonical(), true);
+  BOOST_CHECK_EQUAL(FaceUri("dev://").isCanonical(), false);
+  BOOST_CHECK_EQUAL(FaceUri("dev://eth0:8888").isCanonical(), false);
+  BOOST_CHECK_EQUAL(FaceUri("dev://eth0/").isCanonical(), false);
+  BOOST_CHECK_EQUAL(FaceUri("dev://eth0/A").isCanonical(), false);
+}
+
+BOOST_FIXTURE_TEST_CASE(CanonizeDev, CanonizeFixture)
+{
+  addTest("dev://eth0", true, "dev://eth0");
+  addTest("dev://", false, "");
+  addTest("dev://eth0:8888", false, "");
+  addTest("dev://eth0/", true, "dev://eth0");
+  addTest("dev://eth0/A", false, "");
+
+  runTests();
+}
+
+BOOST_AUTO_TEST_CASE(ParseUdpDev)
+{
+  FaceUri uri;
+
+  BOOST_CHECK(uri.parse("udp4+dev://eth0:7777"));
+  BOOST_CHECK_EQUAL(uri.getScheme(), "udp4+dev");
+  BOOST_CHECK_EQUAL(uri.getHost(), "eth0");
+  BOOST_CHECK_EQUAL(uri.getPort(), "7777");
+  BOOST_CHECK_EQUAL(uri.getPath(), "");
+
+  BOOST_CHECK(uri.parse("udp6+dev://eth1:7777"));
+  BOOST_CHECK_EQUAL(uri.getScheme(), "udp6+dev");
+  BOOST_CHECK_EQUAL(uri.getHost(), "eth1");
+  BOOST_CHECK_EQUAL(uri.getPort(), "7777");
+  BOOST_CHECK_EQUAL(uri.getPath(), "");
+
+  BOOST_CHECK(uri.parse("abc+efg://eth0"));
+  BOOST_CHECK(!uri.parse("abc+://eth0"));
+  BOOST_CHECK(!uri.parse("+abc://eth0"));
+
+  namespace ip = boost::asio::ip;
+
+  ip::udp::endpoint endpoint4(ip::udp::v4(), 7777);
+  uri = FaceUri::fromUdpDev(endpoint4, "en1");
+  BOOST_CHECK_EQUAL(uri.toString(), "udp4+dev://en1:7777");
+
+  ip::udp::endpoint endpoint6(ip::udp::v6(), 7777);
+  uri = FaceUri::fromUdpDev(endpoint6, "en2");
+  BOOST_CHECK_EQUAL(uri.toString(), "udp6+dev://en2:7777");
+}
+
+BOOST_FIXTURE_TEST_CASE(CanonizeUdpDev, CanonizeFixture)
+{
+  BOOST_CHECK_EQUAL(FaceUri("udp4+dev://eth0:7777").isCanonical(), true);
+  BOOST_CHECK_EQUAL(FaceUri("udp6+dev://eth1:7777").isCanonical(), true);
+  BOOST_CHECK_EQUAL(FaceUri("udp+dev://eth1:7777").isCanonical(), false);
+  BOOST_CHECK_EQUAL(FaceUri("udp6+dev://eth1").isCanonical(), false);
+
+  addTest("udp4+dev://en0:7777", true, "udp4+dev://en0:7777");
+  addTest("udp6+dev://en0:7777", true, "udp6+dev://en0:7777");
+  addTest("udp+dev://en1:7777", false, "");
+  addTest("udp6+dev://en2", false, "");
+}
+
+BOOST_AUTO_TEST_CASE(CanonizeEmptyCallback)
+{
+  boost::asio::io_service io;
+
+  // unsupported scheme
+  FaceUri("null://").canonize(FaceUri::CanonizeSuccessCallback(),
+                              FaceUri::CanonizeFailureCallback(),
+                              io, 1_ms);
+
+  // cannot resolve
+  FaceUri("udp://192.0.2.333").canonize(FaceUri::CanonizeSuccessCallback(),
+                                        FaceUri::CanonizeFailureCallback(),
+                                        io, 1_ms);
+
+  // already canonical
+  FaceUri("udp4://192.0.2.1:6363").canonize(FaceUri::CanonizeSuccessCallback(),
+                                            FaceUri::CanonizeFailureCallback(),
+                                            io, 1_ms);
+
+  // need DNS resolution
+  FaceUri("udp://192.0.2.1:6363").canonize(FaceUri::CanonizeSuccessCallback(),
+                                           FaceUri::CanonizeFailureCallback(),
+                                           io, 1_ms);
+
+  io.run(); // should not crash
+
+  // avoid "test case [...] did not check any assertions" message from Boost.Test
+  BOOST_CHECK(true);
+}
+
+BOOST_FIXTURE_TEST_CASE(CanonizeUnsupported, CanonizeFixture)
+{
+  BOOST_CHECK_EQUAL(FaceUri::canCanonize("internal"), false);
+  BOOST_CHECK_EQUAL(FaceUri::canCanonize("null"), false);
+  BOOST_CHECK_EQUAL(FaceUri::canCanonize("unix"), false);
+  BOOST_CHECK_EQUAL(FaceUri::canCanonize("fd"), false);
+
+  BOOST_CHECK_EQUAL(FaceUri("internal://").isCanonical(), false);
+  BOOST_CHECK_EQUAL(FaceUri("null://").isCanonical(), false);
+  BOOST_CHECK_EQUAL(FaceUri("unix:///var/run/nfd.sock").isCanonical(), false);
+  BOOST_CHECK_EQUAL(FaceUri("fd://0").isCanonical(), false);
+
+  addTest("internal://", false, "");
+  addTest("null://", false, "");
+  addTest("unix:///var/run/nfd.sock", false, "");
+  addTest("fd://0", false, "");
+
+  runTests();
+}
+
+BOOST_AUTO_TEST_CASE(Bug1635)
+{
+  FaceUri uri;
+
+  BOOST_CHECK(uri.parse("wsclient://[::ffff:76.90.11.239]:56366"));
+  BOOST_CHECK_EQUAL(uri.getScheme(), "wsclient");
+  BOOST_CHECK_EQUAL(uri.getHost(), "76.90.11.239");
+  BOOST_CHECK_EQUAL(uri.getPort(), "56366");
+  BOOST_CHECK_EQUAL(uri.getPath(), "");
+  BOOST_CHECK_EQUAL(uri.toString(), "wsclient://76.90.11.239:56366");
+}
+
+BOOST_AUTO_TEST_SUITE_END() // TestFaceUri
+BOOST_AUTO_TEST_SUITE_END() // Net
+
+} // namespace tests
+} // namespace ndn
diff --git a/tests/unit/net/network-configuration-detector.cpp b/tests/unit/net/network-configuration-detector.cpp
new file mode 100644
index 0000000..3e4f128
--- /dev/null
+++ b/tests/unit/net/network-configuration-detector.cpp
@@ -0,0 +1,87 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2018 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 "network-configuration-detector.hpp"
+
+#include <boost/asio/ip/address.hpp>
+#include <boost/asio/ip/udp.hpp>
+#include <boost/asio/ip/basic_resolver.hpp>
+#include <boost/asio/io_service.hpp>
+#include <boost/range/iterator_range_core.hpp>
+
+namespace ndn {
+namespace tests {
+
+bool NetworkConfigurationDetector::m_isInitialized = false;
+bool NetworkConfigurationDetector::m_hasIpv4 = false;
+bool NetworkConfigurationDetector::m_hasIpv6 = false;
+
+bool
+NetworkConfigurationDetector::hasIpv4()
+{
+  if (!m_isInitialized) {
+    detect();
+  }
+  return m_hasIpv4;
+}
+
+bool
+NetworkConfigurationDetector::hasIpv6()
+{
+  if (!m_isInitialized) {
+    detect();
+  }
+  return m_hasIpv6;
+}
+
+void
+NetworkConfigurationDetector::detect()
+{
+  typedef boost::asio::ip::basic_resolver<boost::asio::ip::udp> BoostResolver;
+
+  boost::asio::io_service ioService;
+  BoostResolver resolver(ioService);
+
+  // The specified hostname must contain both A and AAAA records
+  BoostResolver::query query("a.root-servers.net", "");
+
+  boost::system::error_code errorCode;
+  BoostResolver::iterator begin = resolver.resolve(query, errorCode);
+  if (errorCode) {
+    m_isInitialized = true;
+    return;
+  }
+  BoostResolver::iterator end;
+
+  for (const auto& i : boost::make_iterator_range(begin, end)) {
+    if (i.endpoint().address().is_v4()) {
+      m_hasIpv4 = true;
+    }
+    else if (i.endpoint().address().is_v6()) {
+      m_hasIpv6 = true;
+    }
+  }
+
+  m_isInitialized = true;
+}
+
+} // namespace tests
+} // namespace ndn
diff --git a/tests/unit/net/network-configuration-detector.hpp b/tests/unit/net/network-configuration-detector.hpp
new file mode 100644
index 0000000..58715c8
--- /dev/null
+++ b/tests/unit/net/network-configuration-detector.hpp
@@ -0,0 +1,75 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2018 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_TESTS_NET_NETWORK_CONFIGURATION_DETECTOR_HPP
+#define NDN_TESTS_NET_NETWORK_CONFIGURATION_DETECTOR_HPP
+
+#define SKIP_IF_IPV4_UNAVAILABLE() \
+  do { \
+    if (!::ndn::tests::NetworkConfigurationDetector::hasIpv4()) { \
+      BOOST_WARN_MESSAGE(false, "skipping assertions that require IPv4 support"); \
+      return; \
+    } \
+  } while (false)
+
+#define SKIP_IF_IPV6_UNAVAILABLE() \
+  do { \
+    if (!::ndn::tests::NetworkConfigurationDetector::hasIpv6()) { \
+      BOOST_WARN_MESSAGE(false, "skipping assertions that require IPv6 support"); \
+      return; \
+    } \
+  } while (false)
+
+#define SKIP_IF_IP_UNAVAILABLE() \
+  do { \
+    if (!::ndn::tests::NetworkConfigurationDetector::hasIpv4() && \
+        !::ndn::tests::NetworkConfigurationDetector::hasIpv6()) { \
+      BOOST_WARN_MESSAGE(false, "skipping assertions that require either IPv4 or IPv6 support"); \
+      return; \
+    } \
+  } while (false)
+
+namespace ndn {
+namespace tests {
+
+class NetworkConfigurationDetector
+{
+public:
+  static bool
+  hasIpv4();
+
+  static bool
+  hasIpv6();
+
+private:
+  static void
+  detect();
+
+private:
+  static bool m_isInitialized;
+  static bool m_hasIpv4;
+  static bool m_hasIpv6;
+};
+
+} // namespace tests
+} // namespace ndn
+
+#endif // NDN_TESTS_NET_NETWORK_CONFIGURATION_DETECTOR_HPP
diff --git a/tests/unit/net/network-monitor-stub.t.cpp b/tests/unit/net/network-monitor-stub.t.cpp
new file mode 100644
index 0000000..8fbf3eb
--- /dev/null
+++ b/tests/unit/net/network-monitor-stub.t.cpp
@@ -0,0 +1,125 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2018 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 "net/network-monitor-stub.hpp"
+
+#include "boost-test.hpp"
+
+namespace ndn {
+namespace net {
+namespace tests {
+
+BOOST_AUTO_TEST_SUITE(Net)
+BOOST_AUTO_TEST_SUITE(TestNetworkMonitorStub)
+
+BOOST_AUTO_TEST_CASE(Capabilities)
+{
+  NetworkMonitorStub stub(NetworkMonitor::CAP_ENUM | NetworkMonitor::CAP_IF_ADD_REMOVE);
+  BOOST_CHECK_EQUAL(stub.getCapabilities(),
+                    NetworkMonitor::CAP_ENUM | NetworkMonitor::CAP_IF_ADD_REMOVE);
+}
+
+class StubFixture
+{
+public:
+  StubFixture()
+    : stub(~0)
+  {
+    stub.onEnumerationCompleted.connect([this] { signals.push_back("EnumerationCompleted"); });
+    stub.onInterfaceAdded.connect([this] (shared_ptr<const NetworkInterface> netif) {
+      signals.push_back("InterfaceAdded " + netif->getName()); });
+    stub.onInterfaceRemoved.connect([this] (shared_ptr<const NetworkInterface> netif) {
+      signals.push_back("InterfaceRemoved " + netif->getName()); });
+  }
+
+public:
+  NetworkMonitorStub stub;
+  std::vector<std::string> signals;
+};
+
+BOOST_FIXTURE_TEST_CASE(AddInterface, StubFixture)
+{
+  BOOST_CHECK_EQUAL(stub.listNetworkInterfaces().size(), 0);
+  BOOST_CHECK(stub.getNetworkInterface("eth1") == nullptr);
+
+  shared_ptr<NetworkInterface> if1 = stub.makeNetworkInterface();
+  if1->setIndex(13697);
+  if1->setName("eth1");
+  stub.addInterface(if1);
+  if1.reset();
+  BOOST_REQUIRE(stub.getNetworkInterface("eth1") != nullptr);
+  BOOST_CHECK_EQUAL(stub.getNetworkInterface("eth1")->getIndex(), 13697);
+  BOOST_CHECK_EQUAL(stub.listNetworkInterfaces().at(0)->getIndex(), 13697);
+  BOOST_REQUIRE_EQUAL(signals.size(), 1);
+  BOOST_CHECK_EQUAL(signals.back(), "InterfaceAdded eth1");
+
+  shared_ptr<NetworkInterface> if1b = stub.makeNetworkInterface();
+  if1b->setIndex(3280);
+  if1b->setName("eth1");
+  BOOST_CHECK_THROW(stub.addInterface(if1b), std::invalid_argument);
+  BOOST_CHECK(stub.getNetworkInterface("eth1") != nullptr);
+  BOOST_CHECK_EQUAL(stub.getNetworkInterface("eth1")->getIndex(), 13697);
+  BOOST_CHECK_EQUAL(stub.listNetworkInterfaces().at(0)->getIndex(), 13697);
+  BOOST_CHECK_EQUAL(signals.size(), 1);
+
+  stub.emitEnumerationCompleted();
+  BOOST_REQUIRE_EQUAL(signals.size(), 2);
+  BOOST_CHECK_EQUAL(signals.back(), "EnumerationCompleted");
+
+  shared_ptr<NetworkInterface> if2 = stub.makeNetworkInterface();
+  if2->setIndex(19243);
+  if2->setName("eth2");
+  stub.addInterface(if2);
+  if2.reset();
+  BOOST_REQUIRE(stub.getNetworkInterface("eth2") != nullptr);
+  BOOST_CHECK_EQUAL(stub.getNetworkInterface("eth2")->getIndex(), 19243);
+  BOOST_CHECK_EQUAL(stub.listNetworkInterfaces().size(), 2);
+  BOOST_REQUIRE_EQUAL(signals.size(), 3);
+  BOOST_CHECK_EQUAL(signals.back(), "InterfaceAdded eth2");
+}
+
+BOOST_FIXTURE_TEST_CASE(RemoveInterface, StubFixture)
+{
+  shared_ptr<NetworkInterface> if1 = stub.makeNetworkInterface();
+  if1->setIndex(13697);
+  if1->setName("eth1");
+  stub.addInterface(if1);
+
+  stub.emitEnumerationCompleted();
+  BOOST_REQUIRE_EQUAL(signals.size(), 2);
+  BOOST_CHECK_EQUAL(signals.back(), "EnumerationCompleted");
+
+  stub.removeInterface("eth1");
+  BOOST_CHECK(stub.getNetworkInterface("eth1") == nullptr);
+  BOOST_CHECK_EQUAL(stub.listNetworkInterfaces().size(), 0);
+  BOOST_REQUIRE_EQUAL(signals.size(), 3);
+  BOOST_CHECK_EQUAL(signals.back(), "InterfaceRemoved eth1");
+
+  stub.removeInterface("eth2"); // non-existent
+  BOOST_CHECK_EQUAL(signals.size(), 3);
+}
+
+BOOST_AUTO_TEST_SUITE_END() // TestNetworkMonitorStub
+BOOST_AUTO_TEST_SUITE_END() // Net
+
+} // namespace tests
+} // namespace net
+} // namespace ndn
diff --git a/tests/unit/net/network-monitor.t.cpp b/tests/unit/net/network-monitor.t.cpp
new file mode 100644
index 0000000..b522ee6
--- /dev/null
+++ b/tests/unit/net/network-monitor.t.cpp
@@ -0,0 +1,73 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2018 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 "net/network-monitor.hpp"
+
+#include "boost-test.hpp"
+#include <boost/asio/io_service.hpp>
+
+namespace ndn {
+namespace net {
+namespace tests {
+
+BOOST_AUTO_TEST_SUITE(Net)
+BOOST_AUTO_TEST_SUITE(TestNetworkMonitor)
+
+#define NM_REQUIRE_CAP(capability) \
+  do { \
+    if ((nm->getCapabilities() & NetworkMonitor::CAP_ ## capability) == 0) { \
+      BOOST_WARN_MESSAGE(false, "skipping assertions that require " #capability " capability"); \
+      return; \
+    } \
+  } while (false)
+
+BOOST_AUTO_TEST_CASE(DestructWithoutRun)
+{
+  boost::asio::io_service io;
+  auto nm = make_unique<NetworkMonitor>(io);
+  nm.reset();
+  BOOST_CHECK(true); // if we got this far, the test passed
+}
+
+BOOST_AUTO_TEST_CASE(DestructWhileEnumerating)
+{
+  boost::asio::io_service io;
+  auto nm = make_unique<NetworkMonitor>(io);
+  NM_REQUIRE_CAP(ENUM);
+
+  nm->onInterfaceAdded.connect([&] (const shared_ptr<const NetworkInterface>&) {
+    io.post([&] { nm.reset(); });
+  });
+  nm->onEnumerationCompleted.connect([&] {
+    // make sure the test case terminates even if we have zero interfaces
+    io.post([&] { nm.reset(); });
+  });
+
+  io.run();
+  BOOST_CHECK(true); // if we got this far, the test passed
+}
+
+BOOST_AUTO_TEST_SUITE_END() // TestNetworkMonitor
+BOOST_AUTO_TEST_SUITE_END() // Net
+
+} // namespace tests
+} // namespace net
+} // namespace ndn