diff --git a/src/mdns/mdns.cpp b/src/mdns/mdns.cpp index 83ccaeccee7..49fa0a335f9 100644 --- a/src/mdns/mdns.cpp +++ b/src/mdns/mdns.cpp @@ -197,6 +197,21 @@ void Publisher::OnServiceResolved(std::string aType, DiscoveredInstanceInfo aIns aInstanceInfo.mRemoved ? "remove" : "add", aInstanceInfo.mName.c_str(), aInstanceInfo.mHostName.c_str(), aInstanceInfo.mAddresses.size()); + if (!aInstanceInfo.mRemoved) + { + std::string addressesString; + + for (const auto &address : aInstanceInfo.mAddresses) + { + addressesString += address.ToString() + ","; + } + if (addressesString.size()) + { + addressesString.pop_back(); + } + otbrLogInfo("addresses: [ %s ]", addressesString.c_str()); + } + DnsUtils::CheckServiceNameSanity(aType); assert(aInstanceInfo.mNetifIndex > 0); @@ -619,5 +634,20 @@ void Publisher::UpdateHostResolutionEmaLatency(const std::string &aHostName, otb } } +void Publisher::AddAddress(AddressList &aAddressList, const Ip6Address &aAddress) +{ + aAddressList.push_back(aAddress); +} + +void Publisher::RemoveAddress(AddressList &aAddressList, const Ip6Address &aAddress) +{ + auto it = std::find(aAddressList.begin(), aAddressList.end(), aAddress); + + if (it != aAddressList.end()) + { + aAddressList.erase(it); + } +} + } // namespace Mdns } // namespace otbr diff --git a/src/mdns/mdns.hpp b/src/mdns/mdns.hpp index 2cb410533cb..03f780b3aa5 100644 --- a/src/mdns/mdns.hpp +++ b/src/mdns/mdns.hpp @@ -136,6 +136,9 @@ class Publisher : private NonCopyable uint16_t mWeight = 0; ///< Service weight. TxtData mTxtData; ///< TXT RDATA bytes. uint32_t mTtl = 0; ///< Service TTL. + + void AddAddress(const Ip6Address &aAddress) { Publisher::AddAddress(mAddresses, aAddress); } + void RemoveAddress(const Ip6Address &aAddress) { Publisher::RemoveAddress(mAddresses, aAddress); } }; /** @@ -148,6 +151,9 @@ class Publisher : private NonCopyable AddressList mAddresses; ///< IP6 addresses. uint32_t mNetifIndex = 0; ///< Network interface. uint32_t mTtl = 0; ///< Host TTL. + + void AddAddress(const Ip6Address &aAddress) { Publisher::AddAddress(mAddresses, aAddress); } + void RemoveAddress(const Ip6Address &aAddress) { Publisher::RemoveAddress(mAddresses, aAddress); } }; /** @@ -574,6 +580,9 @@ class Publisher : private NonCopyable otbrError aError); void UpdateHostResolutionEmaLatency(const std::string &aHostName, otbrError aError); + static void AddAddress(AddressList &aAddressList, const Ip6Address &aAddress); + static void RemoveAddress(AddressList &aAddressList, const Ip6Address &aAddress); + ServiceRegistrationMap mServiceRegistrations; HostRegistrationMap mHostRegistrations; diff --git a/src/mdns/mdns_avahi.cpp b/src/mdns/mdns_avahi.cpp index c0d8762d9d5..2d109c36ca8 100644 --- a/src/mdns/mdns_avahi.cpp +++ b/src/mdns/mdns_avahi.cpp @@ -1238,6 +1238,7 @@ void PublisherAvahi::ServiceResolver::HandleResolveServiceResult(AvahiServiceRes { avahi_record_browser_free(mRecordBrowser); mRecordBrowser = nullptr; + mInstanceInfo.mAddresses.clear(); } // NOTE: This `ServiceResolver` object may be freed in `OnServiceResolved`. mRecordBrowser = avahi_record_browser_new(mPublisherAvahi->mClient, aInterfaceIndex, AVAHI_PROTO_UNSPEC, @@ -1299,7 +1300,7 @@ void PublisherAvahi::ServiceResolver::HandleResolveHostResult(AvahiRecordBrowser aName, aInterfaceIndex, aProtocol, aClazz, aType, aSize, static_cast(aFlags), static_cast(aEvent)); - VerifyOrExit(aEvent == AVAHI_BROWSER_NEW); + VerifyOrExit(aEvent == AVAHI_BROWSER_NEW || aEvent == AVAHI_BROWSER_REMOVE); VerifyOrExit(aSize == OTBR_IP6_ADDRESS_SIZE || aSize == OTBR_IP4_ADDRESS_SIZE, otbrLogErr("Unexpected address data length: %zu", aSize), avahiError = AVAHI_ERR_INVALID_ADDRESS); VerifyOrExit(aSize == OTBR_IP6_ADDRESS_SIZE, otbrLogInfo("IPv4 address ignored"), @@ -1308,9 +1309,16 @@ void PublisherAvahi::ServiceResolver::HandleResolveHostResult(AvahiRecordBrowser VerifyOrExit(!address.IsLinkLocal() && !address.IsMulticast() && !address.IsLoopback() && !address.IsUnspecified(), avahiError = AVAHI_ERR_INVALID_ADDRESS); - otbrLogInfo("Resolved host address: %s", address.ToString().c_str()); - - mInstanceInfo.mAddresses.push_back(std::move(address)); + otbrLogInfo("Resolved host address: %s %s", aEvent == AVAHI_BROWSER_NEW ? "add" : "remove", + address.ToString().c_str()); + if (aEvent == AVAHI_BROWSER_NEW) + { + mInstanceInfo.AddAddress(address); + } + else + { + mInstanceInfo.RemoveAddress(address); + } resolved = true; exit: @@ -1423,7 +1431,7 @@ void PublisherAvahi::HostSubscription::HandleResolveResult(AvahiRecordBrowser aName, aInterfaceIndex, aProtocol, aClazz, aType, aSize, static_cast(aFlags), static_cast(aEvent)); - VerifyOrExit(aEvent == AVAHI_BROWSER_NEW); + VerifyOrExit(aEvent == AVAHI_BROWSER_NEW || aEvent == AVAHI_BROWSER_REMOVE); VerifyOrExit(aSize == OTBR_IP6_ADDRESS_SIZE || aSize == OTBR_IP4_ADDRESS_SIZE, otbrLogErr("Unexpected address data length: %zu", aSize), avahiError = AVAHI_ERR_INVALID_ADDRESS); VerifyOrExit(aSize == OTBR_IP6_ADDRESS_SIZE, otbrLogInfo("IPv4 address ignored"), @@ -1432,10 +1440,18 @@ void PublisherAvahi::HostSubscription::HandleResolveResult(AvahiRecordBrowser VerifyOrExit(!address.IsLinkLocal() && !address.IsMulticast() && !address.IsLoopback() && !address.IsUnspecified(), avahiError = AVAHI_ERR_INVALID_ADDRESS); - otbrLogInfo("Resolved host address: %s", address.ToString().c_str()); + otbrLogInfo("Resolved host address: %s %s", aEvent == AVAHI_BROWSER_NEW ? "add" : "remove", + address.ToString().c_str()); mHostInfo.mHostName = std::string(aName) + "."; - mHostInfo.mAddresses.push_back(std::move(address)); + if (aEvent == AVAHI_BROWSER_NEW) + { + mHostInfo.AddAddress(address); + } + else + { + mHostInfo.RemoveAddress(address); + } mHostInfo.mNetifIndex = static_cast(aInterfaceIndex); // TODO: Use a more proper TTL mHostInfo.mTtl = kDefaultTtl; diff --git a/src/mdns/mdns_mdnssd.cpp b/src/mdns/mdns_mdnssd.cpp index 2dc4c27a572..a7cd456dc76 100644 --- a/src/mdns/mdns_mdnssd.cpp +++ b/src/mdns/mdns_mdnssd.cpp @@ -941,19 +941,6 @@ void PublisherMDnsSd::ServiceSubscription::Resolve(uint32_t aInterface mResolvingInstances.back()->Resolve(); } -void PublisherMDnsSd::ServiceSubscription::RemoveInstanceResolution( - PublisherMDnsSd::ServiceInstanceResolution &aInstanceResolution) -{ - auto it = std::find_if(mResolvingInstances.begin(), mResolvingInstances.end(), - [&aInstanceResolution](const std::unique_ptr &aElem) { - return &aInstanceResolution == aElem.get(); - }); - - assert(it != mResolvingInstances.end()); - - mResolvingInstances.erase(it); -} - void PublisherMDnsSd::ServiceSubscription::UpdateAll(MainloopContext &aMainloop) const { Update(aMainloop); @@ -1056,7 +1043,7 @@ otbrError PublisherMDnsSd::ServiceInstanceResolution::GetAddrInfo(uint32_t aInte otbrLogInfo("DNSServiceGetAddrInfo %s inf %d", mInstanceInfo.mHostName.c_str(), aInterfaceIndex); - dnsError = DNSServiceGetAddrInfo(&mServiceRef, kDNSServiceFlagsTimeout, aInterfaceIndex, + dnsError = DNSServiceGetAddrInfo(&mServiceRef, /* flags */ 0, aInterfaceIndex, kDNSServiceProtocol_IPv6 | kDNSServiceProtocol_IPv4, mInstanceInfo.mHostName.c_str(), HandleGetAddrInfoResult, this); @@ -1093,22 +1080,31 @@ void PublisherMDnsSd::ServiceInstanceResolution::HandleGetAddrInfoResult(DNSServ OTBR_UNUSED_VARIABLE(aInterfaceIndex); Ip6Address address; + bool isAdd = (aFlags & kDNSServiceFlagsAdd) != 0; otbrLog(aErrorCode == kDNSServiceErr_NoError ? OTBR_LOG_INFO : OTBR_LOG_WARNING, OTBR_LOG_TAG, "DNSServiceGetAddrInfo reply: flags=%" PRIu32 ", host=%s, sa_family=%u, error=%" PRId32, aFlags, aHostName, static_cast(aAddress->sa_family), aErrorCode); VerifyOrExit(aErrorCode == kDNSServiceErr_NoError); - VerifyOrExit((aFlags & kDNSServiceFlagsAdd) && aAddress->sa_family == AF_INET6); + VerifyOrExit(aAddress->sa_family == AF_INET6); address.CopyFrom(*reinterpret_cast(aAddress)); VerifyOrExit(!address.IsUnspecified() && !address.IsLinkLocal() && !address.IsMulticast() && !address.IsLoopback(), otbrLogDebug("DNSServiceGetAddrInfo ignores address %s", address.ToString().c_str())); - mInstanceInfo.mAddresses.push_back(address); - mInstanceInfo.mTtl = aTtl; + otbrLogInfo("DNSServiceGetAddrInfo reply: %s address=%s, ttl=%" PRIu32, isAdd ? "add" : "remove", + address.ToString().c_str(), aTtl); - otbrLogInfo("DNSServiceGetAddrInfo reply: address=%s, ttl=%" PRIu32, address.ToString().c_str(), aTtl); + if (isAdd) + { + mInstanceInfo.AddAddress(address); + } + else + { + mInstanceInfo.RemoveAddress(address); + } + mInstanceInfo.mTtl = aTtl; exit: if (!mInstanceInfo.mAddresses.empty() || aErrorCode != kDNSServiceErr_NoError) @@ -1123,10 +1119,6 @@ void PublisherMDnsSd::ServiceInstanceResolution::FinishResolution(void) std::string serviceName = mSubscription->mType; DiscoveredInstanceInfo instanceInfo = mInstanceInfo; - // NOTE: `RemoveInstanceResolution` will free this `ServiceInstanceResolution` object. - // So, We can't access `mSubscription` after `RemoveInstanceResolution`. - subscription->RemoveInstanceResolution(*this); - // NOTE: The `ServiceSubscription` object may be freed in `OnServiceResolved`. subscription->mPublisher.OnServiceResolved(serviceName, instanceInfo); } @@ -1170,25 +1162,34 @@ void PublisherMDnsSd::HostSubscription::HandleResolveResult(DNSServiceRef OTBR_UNUSED_VARIABLE(aServiceRef); Ip6Address address; + bool isAdd = (aFlags & kDNSServiceFlagsAdd) != 0; otbrLog(aErrorCode == kDNSServiceErr_NoError ? OTBR_LOG_INFO : OTBR_LOG_WARNING, OTBR_LOG_TAG, "DNSServiceGetAddrInfo reply: flags=%" PRIu32 ", host=%s, sa_family=%u, error=%" PRId32, aFlags, aHostName, static_cast(aAddress->sa_family), aErrorCode); VerifyOrExit(aErrorCode == kDNSServiceErr_NoError); - VerifyOrExit((aFlags & kDNSServiceFlagsAdd) && aAddress->sa_family == AF_INET6); + VerifyOrExit(aAddress->sa_family == AF_INET6); address.CopyFrom(*reinterpret_cast(aAddress)); VerifyOrExit(!address.IsLinkLocal(), otbrLogDebug("DNSServiceGetAddrInfo ignore link-local address %s", address.ToString().c_str())); - mHostInfo.mHostName = aHostName; - mHostInfo.mAddresses.push_back(address); + otbrLogInfo("DNSServiceGetAddrInfo reply: %s address=%s, ttl=%" PRIu32, isAdd ? "add" : "remove", + address.ToString().c_str(), aTtl); + + if (isAdd) + { + mHostInfo.AddAddress(address); + } + else + { + mHostInfo.RemoveAddress(address); + } + mHostInfo.mHostName = aHostName; mHostInfo.mNetifIndex = aInterfaceIndex; mHostInfo.mTtl = aTtl; - otbrLogInfo("DNSServiceGetAddrInfo reply: address=%s, ttl=%" PRIu32, address.ToString().c_str(), aTtl); - // NOTE: This `HostSubscription` object may be freed in `OnHostResolved`. mPublisher.OnHostResolved(mHostName, mHostInfo); diff --git a/src/mdns/mdns_mdnssd.hpp b/src/mdns/mdns_mdnssd.hpp index fa4bf4d1a73..46f7a338d7f 100644 --- a/src/mdns/mdns_mdnssd.hpp +++ b/src/mdns/mdns_mdnssd.hpp @@ -256,7 +256,6 @@ class PublisherMDnsSd : public MainloopProcessor, public Publisher const std::string &aInstanceName, const std::string &aType, const std::string &aDomain); - void RemoveInstanceResolution(ServiceInstanceResolution &aInstanceResolution); void UpdateAll(MainloopContext &aMainloop) const; void ProcessAll(const MainloopContext &aMainloop, std::vector &aReadyServices) const; diff --git a/tests/mdns/CMakeLists.txt b/tests/mdns/CMakeLists.txt index f4778feece3..861a79f17d0 100644 --- a/tests/mdns/CMakeLists.txt +++ b/tests/mdns/CMakeLists.txt @@ -87,3 +87,20 @@ set_tests_properties( PROPERTIES ENVIRONMENT "OTBR_MDNS=${OTBR_MDNS};OTBR_TEST_MDNS=$" ) + +add_executable(otbr-test-mdns-subscribe + test_subscribe.cpp +) + +target_link_libraries(otbr-test-mdns-subscribe PRIVATE + otbr-config + otbr-mdns + $<$:-L$> + ${CPPUTEST_LIBRARIES} +) + + +add_test( + NAME mdns-subscribe + COMMAND otbr-test-mdns-subscribe +) diff --git a/tests/mdns/test_subscribe.cpp b/tests/mdns/test_subscribe.cpp new file mode 100644 index 00000000000..554a33f092d --- /dev/null +++ b/tests/mdns/test_subscribe.cpp @@ -0,0 +1,347 @@ +/* + * Copyright (c) 2023, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include + +#include +#include + +#include "common/mainloop.hpp" +#include "common/mainloop_manager.hpp" +#include "mdns/mdns.hpp" + +#include +#include + +using namespace otbr; +using namespace otbr::Mdns; + +TEST_GROUP(Mdns){}; + +static constexpr int kTimeoutSeconds = 3; + +SimpleString StringFrom(const std::set &aAddresses) +{ + std::string result = "["; + + for (const auto &address : aAddresses) + { + result += address.ToString() + ","; + } + result.back() = ']'; + + return SimpleString(result.c_str()); +} + +int RunMainloopUntilTimeout(int aSeconds) +{ + using namespace otbr; + + int rval = 0; + auto beginTime = Clock::now(); + + while (true) + { + MainloopContext mainloop; + + mainloop.mMaxFd = -1; + mainloop.mTimeout = {1, 0}; + FD_ZERO(&mainloop.mReadFdSet); + FD_ZERO(&mainloop.mWriteFdSet); + FD_ZERO(&mainloop.mErrorFdSet); + + MainloopManager::GetInstance().Update(mainloop); + rval = select(mainloop.mMaxFd + 1, &mainloop.mReadFdSet, &mainloop.mWriteFdSet, &mainloop.mErrorFdSet, + (mainloop.mTimeout.tv_sec == INT_MAX ? nullptr : &mainloop.mTimeout)); + + if (rval < 0) + { + perror("select"); + break; + } + + MainloopManager::GetInstance().Process(mainloop); + + if (Clock::now() - beginTime >= std::chrono::seconds(aSeconds)) + { + break; + } + } + + return rval; +} + +template std::set AsSet(const Container &aContainer) +{ + return std::set(aContainer.begin(), aContainer.end()); +} + +Publisher::ResultCallback NoOpCallback(void) +{ + return [](otbrError aError) { OTBR_UNUSED_VARIABLE(aError); }; +} + +std::map> AsTxtMap(const Publisher::TxtData &aTxtData) +{ + Publisher::TxtList txtList; + std::map> map; + + Publisher::DecodeTxtData(txtList, aTxtData.data(), aTxtData.size()); + for (const auto &entry : txtList) + { + map[entry.mKey] = entry.mValue; + } + + return map; +} + +Publisher::TxtList sTxtList1{{"a", "1"}, {"b", "2"}}; +Publisher::TxtData sTxtData1; +Ip6Address sAddr1; +Ip6Address sAddr2; +Ip6Address sAddr3; +Ip6Address sAddr4; + +void SetUp(void) +{ + otbrLogInit("test-mdns-subscriber", OTBR_LOG_INFO, true); + SuccessOrDie(Ip6Address::FromString("2002::1", sAddr1), ""); + SuccessOrDie(Ip6Address::FromString("2002::2", sAddr2), ""); + SuccessOrDie(Ip6Address::FromString("2002::3", sAddr3), ""); + SuccessOrDie(Ip6Address::FromString("2002::4", sAddr4), ""); + SuccessOrDie(Publisher::EncodeTxtData(sTxtList1, sTxtData1), ""); +} + +std::unique_ptr CreatePublisher(void) +{ + bool ready = false; + std::unique_ptr publisher{Publisher::Create([&publisher, &ready](Mdns::Publisher::State aState) { + if (aState == Publisher::State::kReady) + { + ready = true; + } + })}; + + publisher->Start(); + RunMainloopUntilTimeout(kTimeoutSeconds); + CHECK_TRUE(ready); + + return publisher; +} + +void CheckServiceInstance(const Publisher::DiscoveredInstanceInfo aInstanceInfo, + bool aRemoved, + const std::string &aHostName, + const std::vector &aAddresses, + const std::string &aServiceName, + uint16_t aPort, + const Publisher::TxtData aTxtData) +{ + CHECK_EQUAL(aRemoved, aInstanceInfo.mRemoved); + CHECK_EQUAL(aServiceName, aInstanceInfo.mName); + if (!aRemoved) + { + CHECK_EQUAL(aHostName, aInstanceInfo.mHostName); + CHECK_EQUAL(AsSet(aAddresses), AsSet(aInstanceInfo.mAddresses)); + CHECK_EQUAL(aPort, aInstanceInfo.mPort); + CHECK(AsTxtMap(aTxtData) == AsTxtMap(aInstanceInfo.mTxtData)); + } +} + +void CheckServiceInstanceAdded(const Publisher::DiscoveredInstanceInfo aInstanceInfo, + const std::string &aHostName, + const std::vector &aAddresses, + const std::string &aServiceName, + uint16_t aPort, + const Publisher::TxtData aTxtData) +{ + CheckServiceInstance(aInstanceInfo, false, aHostName, aAddresses, aServiceName, aPort, aTxtData); +} + +void CheckServiceInstanceRemoved(const Publisher::DiscoveredInstanceInfo aInstanceInfo, const std::string &aServiceName) +{ + CheckServiceInstance(aInstanceInfo, true, "", {}, aServiceName, 0, {}); +} + +void CheckHostAdded(const Publisher::DiscoveredHostInfo &aHostInfo, + const std::string &aHostName, + const std::vector &aAddresses) +{ + CHECK_EQUAL(aHostName, aHostInfo.mHostName); + CHECK_EQUAL(AsSet(aAddresses), AsSet(aHostInfo.mAddresses)); +} + +TEST(Mdns, SubscribeHost) +{ + std::unique_ptr pub = CreatePublisher(); + std::string lastHostName; + Publisher::DiscoveredHostInfo lastHostInfo{}; + + auto clearLastHost = [&lastHostName, &lastHostInfo] { + lastHostName = ""; + lastHostInfo = {}; + }; + + pub->AddSubscriptionCallbacks( + nullptr, + [&lastHostName, &lastHostInfo](const std::string &aHostName, const Publisher::DiscoveredHostInfo &aHostInfo) { + lastHostName = aHostName; + lastHostInfo = aHostInfo; + }); + pub->SubscribeHost("host1"); + + pub->PublishHost("host1", Publisher::AddressList{sAddr1, sAddr2}, NoOpCallback()); + pub->PublishService("host1", "service1", "_test._tcp", Publisher::SubTypeList{"_sub1", "_sub2"}, 11111, sTxtData1, + NoOpCallback()); + RunMainloopUntilTimeout(kTimeoutSeconds); + CHECK_EQUAL("host1", lastHostName); + CheckHostAdded(lastHostInfo, "host1.local.", {sAddr1, sAddr2}); + clearLastHost(); + + pub->PublishService("host1", "service2", "_test._tcp", {}, 22222, {}, NoOpCallback()); + RunMainloopUntilTimeout(kTimeoutSeconds); + CHECK_EQUAL("", lastHostName); + clearLastHost(); + + pub->PublishHost("host2", Publisher::AddressList{sAddr3}, NoOpCallback()); + pub->PublishService("host2", "service3", "_test._tcp", {}, 33333, {}, NoOpCallback()); + RunMainloopUntilTimeout(kTimeoutSeconds); + CHECK_EQUAL("", lastHostName); + clearLastHost(); +} + +TEST(Mdns, SubscribeServiceInstance) +{ + std::unique_ptr pub = CreatePublisher(); + std::string lastServiceType; + Publisher::DiscoveredInstanceInfo lastInstanceInfo{}; + + auto clearLastInstance = [&lastServiceType, &lastInstanceInfo] { + lastServiceType = ""; + lastInstanceInfo = {}; + }; + + pub->AddSubscriptionCallbacks( + [&lastServiceType, &lastInstanceInfo](const std::string &aType, + Publisher::DiscoveredInstanceInfo aInstanceInfo) { + lastServiceType = aType; + lastInstanceInfo = aInstanceInfo; + }, + nullptr); + pub->SubscribeService("_test._tcp", "service1"); + + pub->PublishHost("host1", Publisher::AddressList{sAddr1, sAddr2}, NoOpCallback()); + pub->PublishService("host1", "service1", "_test._tcp", Publisher::SubTypeList{"_sub1", "_sub2"}, 11111, sTxtData1, + NoOpCallback()); + RunMainloopUntilTimeout(kTimeoutSeconds); + CHECK_EQUAL("_test._tcp", lastServiceType); + CheckServiceInstanceAdded(lastInstanceInfo, "host1.local.", {sAddr1, sAddr2}, "service1", 11111, sTxtData1); + clearLastInstance(); + + pub->PublishService("host1", "service2", "_test._tcp", {}, 22222, {}, NoOpCallback()); + RunMainloopUntilTimeout(kTimeoutSeconds); + CHECK_EQUAL("", lastServiceType); + clearLastInstance(); + + pub->PublishHost("host2", Publisher::AddressList{sAddr3}, NoOpCallback()); + pub->PublishService("host2", "service3", "_test._tcp", {}, 33333, {}, NoOpCallback()); + RunMainloopUntilTimeout(kTimeoutSeconds); + CHECK_EQUAL("", lastServiceType); + clearLastInstance(); +} + +TEST(Mdns, SubscribeServiceType) +{ + std::unique_ptr pub = CreatePublisher(); + std::string lastServiceType; + Publisher::DiscoveredInstanceInfo lastInstanceInfo{}; + + auto clearLastInstance = [&lastServiceType, &lastInstanceInfo] { + lastServiceType = ""; + lastInstanceInfo = {}; + }; + + pub->AddSubscriptionCallbacks( + [&lastServiceType, &lastInstanceInfo](const std::string &aType, + Publisher::DiscoveredInstanceInfo aInstanceInfo) { + lastServiceType = aType; + lastInstanceInfo = aInstanceInfo; + }, + nullptr); + pub->SubscribeService("_test._tcp", ""); + + pub->PublishHost("host1", Publisher::AddressList{sAddr1, sAddr2}, NoOpCallback()); + pub->PublishService("host1", "service1", "_test._tcp", Publisher::SubTypeList{"_sub1", "_sub2"}, 11111, sTxtData1, + NoOpCallback()); + RunMainloopUntilTimeout(kTimeoutSeconds); + CHECK_EQUAL("_test._tcp", lastServiceType); + CheckServiceInstanceAdded(lastInstanceInfo, "host1.local.", {sAddr1, sAddr2}, "service1", 11111, sTxtData1); + clearLastInstance(); + + pub->PublishService("host1", "service2", "_test._tcp", {}, 22222, {}, NoOpCallback()); + RunMainloopUntilTimeout(kTimeoutSeconds); + CHECK_EQUAL("_test._tcp", lastServiceType); + CheckServiceInstanceAdded(lastInstanceInfo, "host1.local.", {sAddr1, sAddr2}, "service2", 22222, {}); + clearLastInstance(); + + pub->PublishHost("host2", Publisher::AddressList{sAddr3}, NoOpCallback()); + pub->PublishService("host2", "service3", "_test._tcp", {}, 33333, {}, NoOpCallback()); + RunMainloopUntilTimeout(kTimeoutSeconds); + CHECK_EQUAL("_test._tcp", lastServiceType); + CheckServiceInstanceAdded(lastInstanceInfo, "host2.local.", {sAddr3}, "service3", 33333, {}); + clearLastInstance(); + + pub->UnpublishHost("host2", NoOpCallback()); + pub->UnpublishService("service3", "_test._tcp", NoOpCallback()); + RunMainloopUntilTimeout(kTimeoutSeconds); + CHECK_EQUAL("_test._tcp", lastServiceType); + CheckServiceInstanceRemoved(lastInstanceInfo, "service3"); + clearLastInstance(); + + pub->PublishHost("host2", {sAddr3}, NoOpCallback()); + pub->PublishService("host2", "service3", "_test._tcp", {}, 44444, {}, NoOpCallback()); + pub->PublishHost("host2", {sAddr3, sAddr4}, NoOpCallback()); + RunMainloopUntilTimeout(kTimeoutSeconds); + CHECK_EQUAL("_test._tcp", lastServiceType); + CheckServiceInstanceAdded(lastInstanceInfo, "host2.local.", {sAddr3, sAddr4}, "service3", 44444, {}); + clearLastInstance(); + + pub->PublishHost("host2", {sAddr4}, NoOpCallback()); + RunMainloopUntilTimeout(kTimeoutSeconds); + CHECK_EQUAL("_test._tcp", lastServiceType); + CheckServiceInstanceAdded(lastInstanceInfo, "host2.local.", {sAddr4}, "service3", 44444, {}); + clearLastInstance(); +} + +int main(int argc, const char *argv[]) +{ + SetUp(); + + return RUN_ALL_TESTS(argc, argv); +}