| // Copyright 2018 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/gatt/remote_service_manager.h" |
| |
| #include <vector> |
| |
| #include <gmock/gmock.h> |
| |
| #include "pw_async/fake_dispatcher_fixture.h" |
| #include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/att/att.h" |
| #include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/att/error.h" |
| #include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/common/macros.h" |
| #include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/gatt/fake_client.h" |
| #include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/gatt/gatt_defs.h" |
| #include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/testing/test_helpers.h" |
| |
| #pragma clang diagnostic ignored "-Wshadow" |
| |
| namespace bt::gatt::internal { |
| namespace { |
| |
| using namespace ::testing; |
| |
| constexpr UUID kTestServiceUuid1(uint16_t{0xbeef}); |
| constexpr UUID kTestServiceUuid2(uint16_t{0xcafe}); |
| constexpr UUID kTestServiceUuid3(uint16_t{0xface}); |
| constexpr UUID kTestUuid3(uint16_t{0xfefe}); |
| constexpr UUID kTestUuid4(uint16_t{0xefef}); |
| |
| // Buffers for descriptor responses. |
| // ExtendedProperty::kReliableWrite enabled. |
| const StaticByteBuffer kExtendedPropValue(0x01, 0x00); |
| const StaticByteBuffer kCCCNotifyValue(0x01, 0x00); |
| const StaticByteBuffer kCCCIndicateValue(0x02, 0x00); |
| |
| // Constants used for initializing fake characteristic data. |
| constexpr att::Handle kStart = 1; |
| constexpr att::Handle kCharDecl = 2; |
| constexpr att::Handle kCharValue = 3; |
| constexpr att::Handle kDesc1 = 4; |
| constexpr att::Handle kDesc2 = 5; |
| constexpr att::Handle kEnd = 5; |
| |
| void NopStatusCallback(att::Result<>) {} |
| void NopMtuCallback(uint16_t) {} |
| void NopValueCallback(const ByteBuffer& /*value*/, bool /*maybe_truncated*/) {} |
| |
| class RemoteServiceManagerTest : public pw::async::test::FakeDispatcherFixture { |
| public: |
| RemoteServiceManagerTest() = default; |
| ~RemoteServiceManagerTest() override = default; |
| |
| protected: |
| void SetUp() override { |
| auto client = std::make_unique<testing::FakeClient>(dispatcher()); |
| fake_client_ = client.get(); |
| |
| mgr_ = std::make_unique<RemoteServiceManager>(std::move(client)); |
| } |
| |
| void TearDown() override { |
| // Clear any previous expectations that are based on the ATT Write Request, |
| // so that write requests sent during RemoteService::ShutDown() are ignored. |
| if (fake_client_) { |
| fake_client()->set_write_request_callback({}); |
| } |
| mgr_ = nullptr; |
| } |
| |
| // Initializes a RemoteService based on |data|. |
| RemoteService::WeakPtr SetUpFakeService(const ServiceData& data) { |
| std::vector<ServiceData> fake_services{{data}}; |
| fake_client()->set_services(std::move(fake_services)); |
| |
| mgr()->Initialize(NopStatusCallback, NopMtuCallback); |
| |
| ServiceList services; |
| mgr()->ListServices(std::vector<UUID>(), |
| [&services](auto status, ServiceList cb_services) { |
| services = std::move(cb_services); |
| }); |
| |
| RunUntilIdle(); |
| |
| BT_DEBUG_ASSERT(services.size() == 1u); |
| return services[0]; |
| } |
| |
| void SetCharacteristicsAndDescriptors( |
| std::vector<CharacteristicData> fake_chrs, |
| std::vector<DescriptorData> fake_descrs = std::vector<DescriptorData>()) { |
| fake_client()->set_characteristics(std::move(fake_chrs)); |
| fake_client()->set_descriptors(std::move(fake_descrs)); |
| fake_client()->set_characteristic_discovery_status(fit::ok()); |
| } |
| |
| // Discover the characteristics of |service| based on the given |fake_data|. |
| void SetupCharacteristics( |
| RemoteService::WeakPtr service, |
| std::vector<CharacteristicData> fake_chrs, |
| std::vector<DescriptorData> fake_descrs = std::vector<DescriptorData>()) { |
| BT_DEBUG_ASSERT(service.is_alive()); |
| |
| SetCharacteristicsAndDescriptors(std::move(fake_chrs), |
| std::move(fake_descrs)); |
| |
| service->DiscoverCharacteristics([](auto, const auto&) {}); |
| RunUntilIdle(); |
| } |
| |
| RemoteService::WeakPtr SetupServiceWithChrcs( |
| const ServiceData& data, |
| std::vector<CharacteristicData> fake_chrs, |
| std::vector<DescriptorData> fake_descrs = std::vector<DescriptorData>()) { |
| auto service = SetUpFakeService(data); |
| SetupCharacteristics(service, fake_chrs, fake_descrs); |
| return service; |
| } |
| |
| // Create a fake service with one notifiable characteristic. |
| RemoteService::WeakPtr SetupNotifiableService() { |
| ServiceData data(ServiceKind::PRIMARY, 1, 4, kTestServiceUuid1); |
| auto service = SetUpFakeService(data); |
| |
| CharacteristicData chr(Property::kNotify, std::nullopt, 2, 3, kTestUuid3); |
| DescriptorData desc(4, types::kClientCharacteristicConfig); |
| SetupCharacteristics(service, {{chr}}, {{desc}}); |
| |
| fake_client()->set_write_request_callback( |
| [&](att::Handle, const auto&, auto status_callback) { |
| status_callback(fit::ok()); |
| }); |
| |
| RunUntilIdle(); |
| |
| return service; |
| } |
| |
| void EnableNotifications( |
| RemoteService::WeakPtr service, |
| CharacteristicHandle chr_id, |
| att::Result<>* out_status, |
| IdType* out_id, |
| RemoteService::ValueCallback callback = NopValueCallback) { |
| BT_DEBUG_ASSERT(out_status); |
| BT_DEBUG_ASSERT(out_id); |
| service->EnableNotifications(chr_id, |
| std::move(callback), |
| [&](att::Result<> cb_status, IdType cb_id) { |
| *out_status = cb_status; |
| *out_id = cb_id; |
| }); |
| RunUntilIdle(); |
| } |
| |
| void DestroyServiceManager() { |
| mgr_.reset(); |
| fake_client_ = nullptr; |
| } |
| |
| RemoteServiceManager* mgr() const { return mgr_.get(); } |
| testing::FakeClient* fake_client() const { return fake_client_; } |
| |
| private: |
| std::unique_ptr<RemoteServiceManager> mgr_; |
| |
| // The memory is owned by |mgr_|. |
| testing::FakeClient* fake_client_; |
| |
| BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(RemoteServiceManagerTest); |
| }; |
| |
| TEST_F(RemoteServiceManagerTest, InitializeNoServices) { |
| ServiceList services; |
| mgr()->set_service_watcher( |
| [&services](auto /*removed*/, ServiceList added, auto /*modified*/) { |
| services.insert(services.end(), added.begin(), added.end()); |
| }); |
| |
| att::Result<> status = ToResult(HostError::kFailed); |
| mgr()->Initialize([&status](att::Result<> val) { status = val; }, |
| NopMtuCallback); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(fit::ok(), status); |
| EXPECT_TRUE(services.empty()); |
| |
| mgr()->ListServices(std::vector<UUID>(), |
| [&services](auto status, ServiceList cb_services) { |
| services = std::move(cb_services); |
| }); |
| EXPECT_TRUE(services.empty()); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, Initialize) { |
| ServiceData svc1(ServiceKind::PRIMARY, 1, 1, kTestServiceUuid1); |
| ServiceData svc2(ServiceKind::PRIMARY, 2, 2, kTestServiceUuid2); |
| std::vector<ServiceData> fake_services{{svc1, svc2}}; |
| fake_client()->set_services(std::move(fake_services)); |
| |
| ServiceList services; |
| mgr()->set_service_watcher( |
| [&services](auto /*removed*/, ServiceList added, auto /*modified*/) { |
| services.insert(services.end(), added.begin(), added.end()); |
| }); |
| |
| const uint16_t kArbitraryMtu = att::kLEMinMTU + 10; |
| fake_client()->set_server_mtu(kArbitraryMtu); |
| |
| uint16_t new_mtu = 0; |
| auto mtu_cb = [&](uint16_t cb_mtu) { new_mtu = cb_mtu; }; |
| |
| att::Result<> status = ToResult(HostError::kFailed); |
| mgr()->Initialize([&status](att::Result<> val) { status = val; }, |
| std::move(mtu_cb)); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(kArbitraryMtu, new_mtu); |
| |
| EXPECT_EQ(fit::ok(), status); |
| EXPECT_EQ(2u, services.size()); |
| EXPECT_EQ(svc1.range_start, services[0]->handle()); |
| EXPECT_EQ(svc2.range_start, services[1]->handle()); |
| EXPECT_EQ(svc1.type, services[0]->uuid()); |
| EXPECT_EQ(svc2.type, services[1]->uuid()); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, InitializeFailure) { |
| fake_client()->set_discover_services_callback([](ServiceKind kind) { |
| if (kind == ServiceKind::PRIMARY) { |
| return ToResult(att::ErrorCode::kRequestNotSupported); |
| } |
| return att::Result<>(fit::ok()); |
| }); |
| |
| ServiceList watcher_services; |
| mgr()->set_service_watcher([&watcher_services](auto /*removed*/, |
| ServiceList added, |
| auto /*modified*/) { |
| watcher_services.insert(watcher_services.end(), added.begin(), added.end()); |
| }); |
| |
| ServiceList services; |
| mgr()->ListServices(std::vector<UUID>(), |
| [&services](auto status, ServiceList cb_services) { |
| services = std::move(cb_services); |
| }); |
| ASSERT_TRUE(services.empty()); |
| |
| att::Result<> status = ToResult(HostError::kFailed); |
| mgr()->Initialize([&status](att::Result<> val) { status = val; }, |
| NopMtuCallback); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(ToResult(att::ErrorCode::kRequestNotSupported), status); |
| EXPECT_TRUE(services.empty()); |
| EXPECT_TRUE(watcher_services.empty()); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, InitializeMtuExchangeNotSupportedSucceeds) { |
| ServiceData svc(ServiceKind::PRIMARY, 1, 1, kTestServiceUuid1); |
| fake_client()->set_services({svc}); |
| // The MTU exchange is an optional procedure, so if the peer tells us that |
| // they do not support it, we should continue with initialization. |
| fake_client()->set_exchange_mtu_status( |
| ToResult(att::ErrorCode::kRequestNotSupported)); |
| |
| ServiceList services; |
| mgr()->set_service_watcher( |
| [&services](auto /*removed*/, ServiceList added, auto /*modified*/) { |
| services.insert(services.end(), added.begin(), added.end()); |
| }); |
| |
| std::optional<uint16_t> mtu; |
| auto mtu_cb = [&mtu](uint16_t cb_mtu) { mtu = cb_mtu; }; |
| |
| att::Result<> status = ToResult(HostError::kFailed); |
| mgr()->Initialize([&status](att::Result<> val) { status = val; }, |
| std::move(mtu_cb)); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(fit::ok(), status); |
| ASSERT_EQ(1u, services.size()); |
| EXPECT_EQ(svc.range_start, services[0]->handle()); |
| EXPECT_EQ(svc.type, services[0]->uuid()); |
| |
| // If the MTU exchange isn't supported, "the default MTU shall be used" (v5.3 |
| // Vol. 3 Part G 4.3.1) |
| ASSERT_TRUE(mtu.has_value()); |
| EXPECT_EQ(att::kLEMinMTU, *mtu); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, InitializeMtuExchangeFailure) { |
| fake_client()->set_exchange_mtu_status( |
| ToResult(att::ErrorCode::kUnlikelyError)); |
| |
| bool mtu_updated = false; |
| auto mtu_cb = [&](uint16_t /*ignore*/) { mtu_updated = true; }; |
| |
| att::Result<> status = fit::ok(); |
| mgr()->Initialize([&status](att::Result<> val) { status = val; }, |
| std::move(mtu_cb)); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(ToResult(att::ErrorCode::kUnlikelyError), status); |
| EXPECT_FALSE(mtu_updated); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, InitializeByUUIDNoServices) { |
| ServiceList services; |
| mgr()->set_service_watcher( |
| [&services](auto /*removed*/, ServiceList added, auto /*modified*/) { |
| services.insert(services.end(), added.begin(), added.end()); |
| }); |
| |
| att::Result<> status = ToResult(HostError::kFailed); |
| mgr()->Initialize([&status](att::Result<> val) { status = val; }, |
| NopMtuCallback, |
| {kTestServiceUuid1}); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(fit::ok(), status); |
| EXPECT_TRUE(services.empty()); |
| |
| mgr()->ListServices(std::vector<UUID>(), |
| [&services](auto status, ServiceList cb_services) { |
| services = std::move(cb_services); |
| }); |
| EXPECT_TRUE(services.empty()); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, InitializeWithUuids) { |
| ServiceData svc1(ServiceKind::PRIMARY, 1, 1, kTestServiceUuid1); |
| ServiceData svc2(ServiceKind::PRIMARY, 2, 2, kTestServiceUuid2); |
| ServiceData svc3(ServiceKind::PRIMARY, 3, 3, kTestServiceUuid3); |
| std::vector<ServiceData> fake_services{{svc1, svc2, svc3}}; |
| fake_client()->set_services(std::move(fake_services)); |
| |
| ServiceList services; |
| mgr()->set_service_watcher( |
| [&services](auto /*removed*/, ServiceList added, auto /*modified*/) { |
| services.insert(services.end(), added.begin(), added.end()); |
| }); |
| |
| att::Result<> status = ToResult(HostError::kFailed); |
| mgr()->Initialize([&status](att::Result<> val) { status = val; }, |
| NopMtuCallback, |
| {kTestServiceUuid1, kTestServiceUuid3}); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(fit::ok(), status); |
| std::vector<ServiceData> found_services; |
| for (const auto& ptr : services) { |
| found_services.push_back(ptr->info()); |
| } |
| EXPECT_THAT(found_services, UnorderedElementsAre(Eq(svc1), Eq(svc3))); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, InitializeByUUIDFailure) { |
| fake_client()->set_discover_services_callback([](ServiceKind kind) { |
| if (kind == ServiceKind::PRIMARY) { |
| return ToResult(att::ErrorCode::kRequestNotSupported); |
| } |
| return att::Result<>(fit::ok()); |
| }); |
| |
| ServiceList watcher_services; |
| mgr()->set_service_watcher([&watcher_services](auto /*removed*/, |
| ServiceList added, |
| auto /*modified*/) { |
| watcher_services.insert(watcher_services.end(), added.begin(), added.end()); |
| }); |
| |
| ServiceList services; |
| mgr()->ListServices(std::vector<UUID>(), |
| [&services](auto status, ServiceList cb_services) { |
| services = std::move(cb_services); |
| }); |
| ASSERT_TRUE(services.empty()); |
| |
| att::Result<> status = ToResult(HostError::kFailed); |
| mgr()->Initialize([&status](att::Result<> val) { status = val; }, |
| NopMtuCallback, |
| {kTestServiceUuid1}); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(ToResult(att::ErrorCode::kRequestNotSupported), status); |
| EXPECT_TRUE(services.empty()); |
| EXPECT_TRUE(watcher_services.empty()); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, InitializeSecondaryServices) { |
| ServiceData svc(ServiceKind::SECONDARY, 1, 1, kTestServiceUuid1); |
| std::vector<ServiceData> fake_services{{svc}}; |
| fake_client()->set_services(std::move(fake_services)); |
| |
| ServiceList services; |
| mgr()->set_service_watcher( |
| [&services](auto /*removed*/, ServiceList added, auto /*modified*/) { |
| services.insert(services.end(), added.begin(), added.end()); |
| }); |
| |
| att::Result<> status = ToResult(HostError::kFailed); |
| mgr()->Initialize([&status](att::Result<> val) { status = val; }, |
| NopMtuCallback); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(fit::ok(), status); |
| ASSERT_EQ(1u, services.size()); |
| EXPECT_EQ(svc.range_start, services[0]->handle()); |
| EXPECT_EQ(svc.type, services[0]->uuid()); |
| EXPECT_EQ(ServiceKind::SECONDARY, services[0]->info().kind); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, InitializePrimaryAndSecondaryServices) { |
| ServiceData svc1(ServiceKind::PRIMARY, 1, 1, kTestServiceUuid1); |
| ServiceData svc2(ServiceKind::SECONDARY, 2, 2, kTestServiceUuid2); |
| std::vector<ServiceData> fake_services{{svc1, svc2}}; |
| fake_client()->set_services(std::move(fake_services)); |
| |
| ServiceList services; |
| mgr()->set_service_watcher( |
| [&services](auto /*removed*/, ServiceList added, auto /*modified*/) { |
| services.insert(services.end(), added.begin(), added.end()); |
| }); |
| |
| att::Result<> status = ToResult(HostError::kFailed); |
| mgr()->Initialize([&status](att::Result<> val) { status = val; }, |
| NopMtuCallback); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(fit::ok(), status); |
| EXPECT_EQ(2u, services.size()); |
| EXPECT_EQ(ServiceKind::PRIMARY, services[0]->info().kind); |
| EXPECT_EQ(ServiceKind::SECONDARY, services[1]->info().kind); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, |
| InitializePrimaryAndSecondaryServicesOutOfOrder) { |
| // RemoteServiceManager discovers primary services first, followed by |
| // secondary services. Test that the results are stored and represented in the |
| // correct order when a secondary service precedes a primary service. |
| ServiceData svc1(ServiceKind::SECONDARY, 1, 1, kTestServiceUuid1); |
| ServiceData svc2(ServiceKind::PRIMARY, 2, 2, kTestServiceUuid2); |
| fake_client()->set_services({{svc1, svc2}}); |
| |
| ServiceList services; |
| mgr()->set_service_watcher( |
| [&services](auto /*removed*/, ServiceList added, auto /*modified*/) { |
| services.insert(services.end(), added.begin(), added.end()); |
| }); |
| |
| att::Result<> status = ToResult(HostError::kFailed); |
| mgr()->Initialize([&status](att::Result<> val) { status = val; }, |
| NopMtuCallback); |
| RunUntilIdle(); |
| |
| EXPECT_EQ(fit::ok(), status); |
| EXPECT_EQ(2u, services.size()); |
| EXPECT_EQ(ServiceKind::SECONDARY, services[0]->info().kind); |
| EXPECT_EQ(ServiceKind::PRIMARY, services[1]->info().kind); |
| } |
| |
| // Tests that an ATT error that occurs during secondary service aborts |
| // initialization. |
| TEST_F(RemoteServiceManagerTest, InitializeSecondaryServicesFailure) { |
| fake_client()->set_discover_services_callback([](ServiceKind kind) { |
| if (kind == ServiceKind::SECONDARY) { |
| return ToResult(att::ErrorCode::kRequestNotSupported); |
| } |
| return att::Result<>(fit::ok()); |
| }); |
| |
| ServiceList services; |
| mgr()->set_service_watcher( |
| [&services](auto /*removed*/, ServiceList added, auto /*modified*/) { |
| services.insert(services.end(), added.begin(), added.end()); |
| }); |
| |
| att::Result<> status = fit::ok(); |
| mgr()->Initialize([&status](att::Result<> val) { status = val; }, |
| NopMtuCallback); |
| RunUntilIdle(); |
| |
| EXPECT_EQ(ToResult(att::ErrorCode::kRequestNotSupported), status); |
| EXPECT_TRUE(services.empty()); |
| } |
| |
| // Tests that the "unsupported group type" error is treated as a failure for |
| // primary services. |
| TEST_F(RemoteServiceManagerTest, |
| InitializePrimaryServicesErrorUnsupportedGroupType) { |
| fake_client()->set_discover_services_callback([](ServiceKind kind) { |
| if (kind == ServiceKind::PRIMARY) { |
| return ToResult(att::ErrorCode::kUnsupportedGroupType); |
| } |
| return att::Result<>(fit::ok()); |
| }); |
| |
| ServiceList services; |
| mgr()->set_service_watcher( |
| [&services](auto /*removed*/, ServiceList added, auto /*modified*/) { |
| services.insert(services.end(), added.begin(), added.end()); |
| }); |
| |
| att::Result<> status = fit::ok(); |
| mgr()->Initialize([&status](att::Result<> val) { status = val; }, |
| NopMtuCallback); |
| RunUntilIdle(); |
| |
| EXPECT_EQ(ToResult(att::ErrorCode::kUnsupportedGroupType), status); |
| EXPECT_TRUE(services.empty()); |
| } |
| |
| // Tests that the "unsupported group type" error is NOT treated as a failure for |
| // secondary services. |
| TEST_F(RemoteServiceManagerTest, |
| InitializeSecondaryServicesErrorUnsupportedGroupTypeIsIgnored) { |
| ServiceData svc1(ServiceKind::PRIMARY, 1, 1, kTestServiceUuid1); |
| fake_client()->set_services({{svc1}}); |
| fake_client()->set_discover_services_callback([](ServiceKind kind) { |
| if (kind == ServiceKind::SECONDARY) { |
| return ToResult(att::ErrorCode::kUnsupportedGroupType); |
| } |
| return att::Result<>(fit::ok()); |
| }); |
| |
| ServiceList services; |
| mgr()->set_service_watcher( |
| [&services](auto /*removed*/, ServiceList added, auto /*modified*/) { |
| services.insert(services.end(), added.begin(), added.end()); |
| }); |
| |
| att::Result<> status = fit::ok(); |
| mgr()->Initialize([&status](att::Result<> val) { status = val; }, |
| NopMtuCallback); |
| RunUntilIdle(); |
| |
| EXPECT_EQ(fit::ok(), status); |
| ASSERT_EQ(1u, services.size()); |
| EXPECT_EQ(svc1, services[0]->info()); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, ListServicesBeforeInit) { |
| ServiceData svc(ServiceKind::PRIMARY, 1, 1, kTestServiceUuid1); |
| std::vector<ServiceData> fake_services{{svc}}; |
| fake_client()->set_services(std::move(fake_services)); |
| |
| ServiceList services; |
| mgr()->ListServices(std::vector<UUID>(), |
| [&services](auto status, ServiceList cb_services) { |
| services = std::move(cb_services); |
| }); |
| EXPECT_TRUE(services.empty()); |
| |
| att::Result<> status = ToResult(HostError::kFailed); |
| mgr()->Initialize([&status](att::Result<> val) { status = val; }, |
| NopMtuCallback); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(fit::ok(), status); |
| ASSERT_EQ(1u, services.size()); |
| EXPECT_EQ(svc.range_start, services[0]->handle()); |
| EXPECT_EQ(svc.type, services[0]->uuid()); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, ListServicesAfterInit) { |
| ServiceData svc(ServiceKind::PRIMARY, 1, 1, kTestServiceUuid1); |
| std::vector<ServiceData> fake_services{{svc}}; |
| fake_client()->set_services(std::move(fake_services)); |
| |
| att::Result<> status = ToResult(HostError::kFailed); |
| mgr()->Initialize([&status](att::Result<> val) { status = val; }, |
| NopMtuCallback); |
| |
| RunUntilIdle(); |
| |
| ASSERT_EQ(fit::ok(), status); |
| |
| ServiceList services; |
| mgr()->ListServices(std::vector<UUID>(), |
| [&services](auto status, ServiceList cb_services) { |
| services = std::move(cb_services); |
| }); |
| ASSERT_EQ(1u, services.size()); |
| EXPECT_EQ(svc.range_start, services[0]->handle()); |
| EXPECT_EQ(svc.type, services[0]->uuid()); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, ListServicesByUuid) { |
| std::vector<UUID> uuids{kTestServiceUuid1}; |
| |
| ServiceData svc1(ServiceKind::PRIMARY, 1, 1, kTestServiceUuid1); |
| ServiceData svc2(ServiceKind::PRIMARY, 2, 2, kTestServiceUuid2); |
| std::vector<ServiceData> fake_services{{svc1, svc2}}; |
| fake_client()->set_services(std::move(fake_services)); |
| |
| ServiceList service_watcher_services; |
| mgr()->set_service_watcher([&service_watcher_services](auto /*removed*/, |
| ServiceList added, |
| auto /*modified*/) { |
| service_watcher_services.insert( |
| service_watcher_services.end(), added.begin(), added.end()); |
| }); |
| |
| att::Result<> list_services_status = fit::ok(); |
| ServiceList list_services; |
| mgr()->ListServices(std::move(uuids), |
| [&](att::Result<> cb_status, ServiceList cb_services) { |
| list_services_status = cb_status; |
| list_services = std::move(cb_services); |
| }); |
| ASSERT_TRUE(service_watcher_services.empty()); |
| |
| att::Result<> status = ToResult(HostError::kFailed); |
| mgr()->Initialize([&status](att::Result<> val) { status = val; }, |
| NopMtuCallback); |
| |
| RunUntilIdle(); |
| EXPECT_EQ(fit::ok(), status); |
| EXPECT_EQ(fit::ok(), list_services_status); |
| // Only svc1 has a type in |uuids|. |
| EXPECT_EQ(1u, list_services.size()); |
| EXPECT_EQ(svc1.range_start, list_services[0]->handle()); |
| EXPECT_EQ(svc1.type, list_services[0]->uuid()); |
| // All services should be discovered and returned to service watcher because |
| // Initialize() was not called with a list of uuids to discover. |
| EXPECT_EQ(2u, service_watcher_services.size()); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, DiscoverCharacteristicsSuccess) { |
| auto data = ServiceData(ServiceKind::PRIMARY, 1, 5, kTestServiceUuid1); |
| auto service = SetUpFakeService(data); |
| |
| CharacteristicData fake_chrc1(0, std::nullopt, 2, 3, kTestUuid3); |
| CharacteristicData fake_chrc2(0, std::nullopt, 4, 5, kTestUuid4); |
| std::vector<CharacteristicData> fake_chrcs{{fake_chrc1, fake_chrc2}}; |
| fake_client()->set_characteristics(std::move(fake_chrcs)); |
| |
| std::map< |
| CharacteristicHandle, |
| std::pair<CharacteristicData, std::map<DescriptorHandle, DescriptorData>>> |
| expected = {{CharacteristicHandle(3), {fake_chrc1, {}}}, |
| {CharacteristicHandle(5), {fake_chrc2, {}}}}; |
| |
| att::Result<> status1 = ToResult(HostError::kFailed); |
| |
| auto cb = [expected](att::Result<>* status) { |
| return [status, expected](att::Result<> cb_status, const auto& chrcs) { |
| *status = cb_status; |
| EXPECT_EQ(expected, chrcs); |
| }; |
| }; |
| |
| service->DiscoverCharacteristics( |
| [&](att::Result<> cb_status, const auto& chrcs) { |
| status1 = cb_status; |
| EXPECT_EQ(expected, chrcs); |
| }); |
| |
| // Queue a second request. |
| att::Result<> status2 = ToResult(HostError::kFailed); |
| service->DiscoverCharacteristics( |
| [&](att::Result<> cb_status, const auto& chrcs) { |
| status2 = cb_status; |
| EXPECT_EQ(expected, chrcs); |
| }); |
| |
| RunUntilIdle(); |
| // Only one ATT request should have been made. |
| EXPECT_EQ(1u, fake_client()->chrc_discovery_count()); |
| EXPECT_TRUE(service->IsDiscovered()); |
| EXPECT_EQ(fit::ok(), status1); |
| EXPECT_EQ(fit::ok(), status2); |
| EXPECT_EQ(data.range_start, |
| fake_client()->last_chrc_discovery_start_handle()); |
| EXPECT_EQ(data.range_end, fake_client()->last_chrc_discovery_end_handle()); |
| |
| // Request discovery again. This should succeed without an ATT request. |
| status1 = ToResult(HostError::kFailed); |
| service->DiscoverCharacteristics( |
| [&status1](att::Result<> cb_status, const auto&) { |
| status1 = cb_status; |
| }); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(fit::ok(), status1); |
| EXPECT_EQ(1u, fake_client()->chrc_discovery_count()); |
| EXPECT_TRUE(service->IsDiscovered()); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, DiscoverCharacteristicsError) { |
| auto service = SetUpFakeService( |
| ServiceData(ServiceKind::PRIMARY, 1, 5, kTestServiceUuid1)); |
| |
| CharacteristicData chrc1(0, std::nullopt, 2, 3, kTestUuid3); |
| CharacteristicData chrc2(0, std::nullopt, 4, 5, kTestUuid4); |
| std::vector<CharacteristicData> fake_chrcs{{chrc1, chrc2}}; |
| fake_client()->set_characteristics(std::move(fake_chrcs)); |
| |
| fake_client()->set_characteristic_discovery_status( |
| ToResult(HostError::kNotSupported)); |
| |
| att::Result<> status1 = fit::ok(); |
| service->DiscoverCharacteristics( |
| [&](att::Result<> cb_status, const auto& chrcs) { |
| status1 = cb_status; |
| EXPECT_TRUE(chrcs.empty()); |
| }); |
| |
| // Queue a second request. |
| att::Result<> status2 = fit::ok(); |
| service->DiscoverCharacteristics( |
| [&](att::Result<> cb_status, const auto& chrcs) { |
| status2 = cb_status; |
| EXPECT_TRUE(chrcs.empty()); |
| }); |
| |
| RunUntilIdle(); |
| // Only one request should have been made. |
| EXPECT_EQ(1u, fake_client()->chrc_discovery_count()); |
| EXPECT_FALSE(service->IsDiscovered()); |
| EXPECT_EQ(ToResult(HostError::kNotSupported), status1); |
| EXPECT_EQ(ToResult(HostError::kNotSupported), status2); |
| } |
| |
| // Discover descriptors of a service with one characteristic. |
| TEST_F(RemoteServiceManagerTest, DiscoverDescriptorsOfOneSuccess) { |
| ServiceData data(ServiceKind::PRIMARY, kStart, kEnd, kTestServiceUuid1); |
| auto service = SetUpFakeService(data); |
| |
| CharacteristicData fake_chrc( |
| 0, std::nullopt, kCharDecl, kCharValue, kTestUuid3); |
| fake_client()->set_characteristics({{fake_chrc}}); |
| |
| DescriptorData fake_desc1(kDesc1, kTestUuid3); |
| DescriptorData fake_desc2(kDesc2, kTestUuid4); |
| fake_client()->set_descriptors({{fake_desc1, fake_desc2}}); |
| |
| att::Result<> status = ToResult(HostError::kFailed); |
| service->DiscoverCharacteristics( |
| [&](att::Result<> cb_status, const auto chrcs) { |
| status = cb_status; |
| EXPECT_EQ(1u, chrcs.size()); |
| |
| std::map<CharacteristicHandle, |
| std::pair<CharacteristicData, |
| std::map<DescriptorHandle, DescriptorData>>> |
| expected = { |
| {CharacteristicHandle(kCharValue), |
| {fake_chrc, {{kDesc1, fake_desc1}, {kDesc2, fake_desc2}}}}}; |
| |
| EXPECT_EQ(expected, chrcs); |
| }); |
| |
| RunUntilIdle(); |
| EXPECT_EQ(1u, fake_client()->chrc_discovery_count()); |
| EXPECT_EQ(1u, fake_client()->desc_discovery_count()); |
| EXPECT_TRUE(service->IsDiscovered()); |
| EXPECT_EQ(fit::ok(), status); |
| EXPECT_EQ(kDesc1, fake_client()->last_desc_discovery_start_handle()); |
| EXPECT_EQ(kEnd, fake_client()->last_desc_discovery_end_handle()); |
| } |
| |
| // Discover descriptors of a service with one characteristic. |
| TEST_F(RemoteServiceManagerTest, DiscoverDescriptorsOfOneError) { |
| ServiceData data(ServiceKind::PRIMARY, kStart, kEnd, kTestServiceUuid1); |
| auto service = SetUpFakeService(data); |
| |
| CharacteristicData fake_chrc( |
| 0, std::nullopt, kCharDecl, kCharValue, kTestUuid3); |
| fake_client()->set_characteristics({{fake_chrc}}); |
| |
| DescriptorData fake_desc1(kDesc1, kTestUuid3); |
| DescriptorData fake_desc2(kDesc2, kTestUuid4); |
| fake_client()->set_descriptors({{fake_desc1, fake_desc2}}); |
| fake_client()->set_descriptor_discovery_status( |
| ToResult(HostError::kNotSupported)); |
| |
| att::Result<> status = fit::ok(); |
| service->DiscoverCharacteristics( |
| [&](att::Result<> cb_status, const auto& chrcs) { |
| status = cb_status; |
| EXPECT_TRUE(chrcs.empty()); |
| }); |
| |
| RunUntilIdle(); |
| EXPECT_EQ(1u, fake_client()->chrc_discovery_count()); |
| EXPECT_EQ(1u, fake_client()->desc_discovery_count()); |
| EXPECT_FALSE(service->IsDiscovered()); |
| EXPECT_EQ(ToResult(HostError::kNotSupported), status); |
| } |
| |
| // Discover descriptors of a service with multiple characteristics |
| TEST_F(RemoteServiceManagerTest, DiscoverDescriptorsOfMultipleSuccess) { |
| // Has one descriptor |
| CharacteristicData fake_char1(0, std::nullopt, 2, 3, kTestUuid3); |
| DescriptorData fake_desc1(4, kTestUuid4); |
| |
| // Has no descriptors |
| CharacteristicData fake_char2(0, std::nullopt, 5, 6, kTestUuid3); |
| |
| // Has two descriptors |
| CharacteristicData fake_char3(0, std::nullopt, 7, 8, kTestUuid3); |
| DescriptorData fake_desc2(9, kTestUuid4); |
| DescriptorData fake_desc3(10, kTestUuid4); |
| |
| ServiceData data( |
| ServiceKind::PRIMARY, 1, fake_desc3.handle, kTestServiceUuid1); |
| auto service = SetUpFakeService(data); |
| fake_client()->set_characteristics({{fake_char1, fake_char2, fake_char3}}); |
| fake_client()->set_descriptors({{fake_desc1, fake_desc2, fake_desc3}}); |
| |
| att::Result<> status = ToResult(HostError::kFailed); |
| service->DiscoverCharacteristics([&](att::Result<> cb_status, |
| const auto& chrcs) { |
| status = cb_status; |
| |
| std::map<CharacteristicHandle, |
| std::pair<CharacteristicData, |
| std::map<DescriptorHandle, DescriptorData>>> |
| expected = {{CharacteristicHandle(3), {fake_char1, {{4, fake_desc1}}}}, |
| {CharacteristicHandle(6), {fake_char2, {}}}, |
| {CharacteristicHandle(8), |
| {fake_char3, {{9, fake_desc2}, {10, fake_desc3}}}}}; |
| |
| EXPECT_EQ(expected, chrcs); |
| }); |
| |
| RunUntilIdle(); |
| EXPECT_EQ(1u, fake_client()->chrc_discovery_count()); |
| // There should have been two descriptor discovery requests as discovery |
| // should have been skipped for characteristic #2 due to its handles. |
| EXPECT_EQ(2u, fake_client()->desc_discovery_count()); |
| EXPECT_TRUE(service->IsDiscovered()); |
| EXPECT_EQ(fit::ok(), status); |
| } |
| |
| // Discover descriptors of a service with multiple characteristics. The first |
| // request results in an error though others succeed. |
| TEST_F(RemoteServiceManagerTest, DiscoverDescriptorsOfMultipleEarlyFail) { |
| // Has one descriptor |
| CharacteristicData fake_char1(0, std::nullopt, 2, 3, kTestUuid3); |
| DescriptorData fake_desc1(4, kTestUuid4); |
| |
| // Has no descriptors |
| CharacteristicData fake_char2(0, std::nullopt, 5, 6, kTestUuid3); |
| |
| // Has two descriptors |
| CharacteristicData fake_char3(0, std::nullopt, 7, 8, kTestUuid3); |
| DescriptorData fake_desc2(9, kTestUuid4); |
| DescriptorData fake_desc3(10, kTestUuid4); |
| |
| ServiceData data( |
| ServiceKind::PRIMARY, 1, fake_desc3.handle, kTestServiceUuid1); |
| auto service = SetUpFakeService(data); |
| fake_client()->set_characteristics({{fake_char1, fake_char2, fake_char3}}); |
| fake_client()->set_descriptors({{fake_desc1, fake_desc2, fake_desc3}}); |
| |
| // The first request will fail |
| fake_client()->set_descriptor_discovery_status( |
| ToResult(HostError::kNotSupported), 1); |
| |
| att::Result<> status = ToResult(HostError::kFailed); |
| service->DiscoverCharacteristics( |
| [&](att::Result<> cb_status, const auto& chrcs) { |
| status = cb_status; |
| EXPECT_TRUE(chrcs.empty()); |
| }); |
| |
| RunUntilIdle(); |
| EXPECT_EQ(1u, fake_client()->chrc_discovery_count()); |
| // There should have been two descriptor discovery requests as discovery |
| // should have been skipped for characteristic #2 due to its handles. |
| EXPECT_EQ(2u, fake_client()->desc_discovery_count()); |
| EXPECT_FALSE(service->IsDiscovered()); |
| EXPECT_EQ(ToResult(HostError::kNotSupported), status); |
| } |
| |
| // Discover descriptors of a service with multiple characteristics. The last |
| // request results in an error while the preceding ones succeed. |
| TEST_F(RemoteServiceManagerTest, DiscoverDescriptorsOfMultipleLateFail) { |
| // Has one descriptor |
| CharacteristicData fake_char1(0, std::nullopt, 2, 3, kTestUuid3); |
| DescriptorData fake_desc1(4, kTestUuid4); |
| |
| // Has no descriptors |
| CharacteristicData fake_char2(0, std::nullopt, 5, 6, kTestUuid3); |
| |
| // Has two descriptors |
| CharacteristicData fake_char3(0, std::nullopt, 7, 8, kTestUuid3); |
| DescriptorData fake_desc2(9, kTestUuid4); |
| DescriptorData fake_desc3(10, kTestUuid4); |
| |
| ServiceData data( |
| ServiceKind::PRIMARY, 1, fake_desc3.handle, kTestServiceUuid1); |
| auto service = SetUpFakeService(data); |
| fake_client()->set_characteristics({{fake_char1, fake_char2, fake_char3}}); |
| fake_client()->set_descriptors({{fake_desc1, fake_desc2, fake_desc3}}); |
| |
| // The last request will fail |
| fake_client()->set_descriptor_discovery_status( |
| ToResult(HostError::kNotSupported), 2); |
| |
| att::Result<> status = ToResult(HostError::kFailed); |
| service->DiscoverCharacteristics( |
| [&](att::Result<> cb_status, const auto& chrcs) { |
| status = cb_status; |
| EXPECT_TRUE(chrcs.empty()); |
| }); |
| |
| RunUntilIdle(); |
| EXPECT_EQ(1u, fake_client()->chrc_discovery_count()); |
| // There should have been two descriptor discovery requests as discovery |
| // should have been skipped for characteristic #2 due to its handles. |
| EXPECT_EQ(2u, fake_client()->desc_discovery_count()); |
| EXPECT_FALSE(service->IsDiscovered()); |
| EXPECT_EQ(ToResult(HostError::kNotSupported), status); |
| } |
| |
| // Discover descriptors of a service with extended properties set. |
| TEST_F(RemoteServiceManagerTest, |
| DiscoverDescriptorsWithExtendedPropertiesSuccess) { |
| ServiceData data(ServiceKind::PRIMARY, kStart, kEnd, kTestServiceUuid1); |
| auto service = SetUpFakeService(data); |
| |
| // The ExtendedProperties of the characteristic is set. |
| const Properties props = Property::kExtendedProperties; |
| CharacteristicData fake_chrc( |
| props, std::nullopt, kCharDecl, kCharValue, kTestUuid3); |
| |
| DescriptorData fake_desc1(kDesc1, types::kCharacteristicExtProperties); |
| DescriptorData fake_desc2(kDesc2, kTestUuid4); |
| |
| SetCharacteristicsAndDescriptors({fake_chrc}, {fake_desc1, fake_desc2}); |
| |
| // The callback should be triggered once to read the value of the descriptor |
| // containing the ExtendedProperties bitfield. |
| size_t read_cb_count = 0; |
| auto extended_prop_read_cb = [&](att::Handle handle, auto callback) { |
| EXPECT_EQ(kDesc1, handle); |
| callback(fit::ok(), kExtendedPropValue, /*maybe_truncated=*/false); |
| read_cb_count++; |
| }; |
| fake_client()->set_read_request_callback(std::move(extended_prop_read_cb)); |
| |
| att::Result<> status = ToResult(HostError::kFailed); |
| service->DiscoverCharacteristics([&](att::Result<> cb_status, |
| const auto chrcs) { |
| status = cb_status; |
| EXPECT_EQ(1u, chrcs.size()); |
| |
| CharacteristicData expected_chrc( |
| props, kReliableWrite, kCharDecl, kCharValue, kTestUuid3); |
| std::map<CharacteristicHandle, |
| std::pair<CharacteristicData, |
| std::map<DescriptorHandle, DescriptorData>>> |
| expected = { |
| {CharacteristicHandle(kCharValue), |
| {expected_chrc, {{kDesc1, fake_desc1}, {kDesc2, fake_desc2}}}}}; |
| EXPECT_EQ(expected, chrcs); |
| |
| // Validate that the ExtendedProperties have been written to the |chrcs| |
| // returned in the callback. |
| CharacteristicData chrc_data = |
| chrcs.at(CharacteristicHandle(kCharValue)).first; |
| EXPECT_TRUE(chrc_data.extended_properties.has_value()); |
| EXPECT_EQ(ExtendedProperty::kReliableWrite, |
| chrc_data.extended_properties.value()); |
| }); |
| |
| RunUntilIdle(); |
| EXPECT_EQ(1u, fake_client()->chrc_discovery_count()); |
| EXPECT_EQ(1u, fake_client()->desc_discovery_count()); |
| EXPECT_EQ(1u, read_cb_count); |
| EXPECT_TRUE(service->IsDiscovered()); |
| EXPECT_EQ(fit::ok(), status); |
| EXPECT_EQ(kDesc1, fake_client()->last_desc_discovery_start_handle()); |
| EXPECT_EQ(kEnd, fake_client()->last_desc_discovery_end_handle()); |
| } |
| |
| // Discover descriptors of a service that doesn't contain the ExtendedProperties |
| // bit set, but with a descriptor containing an ExtendedProperty value. This is |
| // not invalid, as per the spec, and so discovery shouldn't fail. |
| TEST_F(RemoteServiceManagerTest, DiscoverDescriptorsExtendedPropertiesNotSet) { |
| ServiceData data(ServiceKind::PRIMARY, kStart, kEnd, kTestServiceUuid1); |
| auto service = SetUpFakeService(data); |
| |
| // The ExtendedProperties of the characteristic is not set. |
| CharacteristicData fake_chrc( |
| 0, std::nullopt, kCharDecl, kCharValue, kTestUuid3); |
| DescriptorData fake_desc1(kDesc1, types::kCharacteristicExtProperties); |
| SetCharacteristicsAndDescriptors({fake_chrc}, {fake_desc1}); |
| |
| // Callback should not be executed. |
| size_t read_cb_count = 0; |
| auto extended_prop_read_cb = [&](att::Handle handle, auto callback) { |
| callback(fit::ok(), kExtendedPropValue, /*maybe_truncated=*/false); |
| read_cb_count++; |
| }; |
| fake_client()->set_read_request_callback(std::move(extended_prop_read_cb)); |
| |
| att::Result<> status = ToResult(HostError::kFailed); |
| service->DiscoverCharacteristics( |
| [&](att::Result<> cb_status, const auto chrcs) { |
| status = cb_status; |
| EXPECT_EQ(1u, chrcs.size()); |
| |
| std::map<CharacteristicHandle, |
| std::pair<CharacteristicData, |
| std::map<DescriptorHandle, DescriptorData>>> |
| expected = {{CharacteristicHandle(kCharValue), |
| {fake_chrc, {{kDesc1, fake_desc1}}}}}; |
| |
| EXPECT_EQ(expected, chrcs); |
| |
| // Validate that the ExtendedProperties has not been updated. |
| CharacteristicData chrc_data = |
| chrcs.at(CharacteristicHandle(kCharValue)).first; |
| EXPECT_FALSE(chrc_data.extended_properties.has_value()); |
| }); |
| |
| RunUntilIdle(); |
| EXPECT_EQ(1u, fake_client()->chrc_discovery_count()); |
| EXPECT_EQ(1u, fake_client()->desc_discovery_count()); |
| EXPECT_EQ(0u, read_cb_count); |
| EXPECT_TRUE(service->IsDiscovered()); |
| EXPECT_EQ(fit::ok(), status); |
| } |
| |
| // Discover descriptors of a service with two descriptors containing |
| // ExtendedProperties. This is invalid, and discovery should fail. |
| TEST_F(RemoteServiceManagerTest, |
| DiscoverDescriptorsMultipleExtendedPropertiesError) { |
| ServiceData data(ServiceKind::PRIMARY, kStart, kEnd, kTestServiceUuid1); |
| auto service = SetUpFakeService(data); |
| |
| // The ExtendedProperties of the characteristic is set. |
| const Properties props = Property::kExtendedProperties; |
| CharacteristicData fake_chrc( |
| props, std::nullopt, kCharDecl, kCharValue, kTestUuid3); |
| // Two descriptors with ExtProperties. |
| DescriptorData fake_desc1(kDesc1, types::kCharacteristicExtProperties); |
| DescriptorData fake_desc2(kDesc2, types::kCharacteristicExtProperties); |
| SetCharacteristicsAndDescriptors({fake_chrc}, {fake_desc1, fake_desc2}); |
| |
| size_t read_cb_count = 0; |
| auto extended_prop_read_cb = [&](att::Handle handle, auto callback) { |
| callback(fit::ok(), kExtendedPropValue, /*maybe_truncated=*/false); |
| read_cb_count++; |
| }; |
| fake_client()->set_read_request_callback(std::move(extended_prop_read_cb)); |
| |
| att::Result<> status = ToResult(HostError::kFailed); |
| service->DiscoverCharacteristics( |
| [&](att::Result<> cb_status, const auto chrcs) { |
| status = cb_status; |
| EXPECT_TRUE(chrcs.empty()); |
| }); |
| |
| RunUntilIdle(); |
| EXPECT_EQ(1u, fake_client()->chrc_discovery_count()); |
| EXPECT_EQ(1u, fake_client()->desc_discovery_count()); |
| EXPECT_EQ(0u, read_cb_count); |
| EXPECT_FALSE(service->IsDiscovered()); |
| EXPECT_EQ(ToResult(HostError::kFailed), status); |
| } |
| |
| // Discover descriptors of a service with ExtendedProperties set, but with |
| // an error when reading the descriptor value. Discovery should fail. |
| TEST_F(RemoteServiceManagerTest, |
| DiscoverDescriptorsExtendedPropertiesReadDescValueError) { |
| ServiceData data(ServiceKind::PRIMARY, kStart, kEnd, kTestServiceUuid1); |
| auto service = SetUpFakeService(data); |
| |
| // The ExtendedProperties of the characteristic is set. |
| const Properties props = Property::kExtendedProperties; |
| CharacteristicData fake_chrc( |
| props, std::nullopt, kCharDecl, kCharValue, kTestUuid3); |
| DescriptorData fake_desc1(kDesc1, types::kCharacteristicExtProperties); |
| DescriptorData fake_desc2(kDesc2, kTestUuid4); |
| SetCharacteristicsAndDescriptors({fake_chrc}, {fake_desc1, fake_desc2}); |
| |
| // The callback should be triggered once to read the value of the descriptor |
| // containing the ExtendedProperties bitfield. |
| size_t read_cb_count = 0; |
| auto extended_prop_read_cb = [&](att::Handle handle, auto callback) { |
| EXPECT_EQ(kDesc1, handle); |
| callback(ToResult(att::ErrorCode::kReadNotPermitted), |
| BufferView(), |
| /*maybe_truncated=*/false); |
| read_cb_count++; |
| }; |
| fake_client()->set_read_request_callback(std::move(extended_prop_read_cb)); |
| |
| att::Result<> status = ToResult(HostError::kFailed); |
| service->DiscoverCharacteristics( |
| [&](att::Result<> cb_status, const auto chrcs) { |
| status = cb_status; |
| EXPECT_TRUE(chrcs.empty()); |
| }); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(1u, read_cb_count); |
| EXPECT_FALSE(service->IsDiscovered()); |
| ASSERT_TRUE(status.is_error()); |
| EXPECT_TRUE(status.error_value().is_protocol_error()); |
| } |
| |
| // Discover descriptors of a service with ExtendedProperties set, but with |
| // a malformed response when reading the descriptor value. Discovery should |
| // fail. |
| TEST_F(RemoteServiceManagerTest, |
| DiscoverDescriptorsExtendedPropertiesReadDescInvalidValue) { |
| ServiceData data(ServiceKind::PRIMARY, kStart, kEnd, kTestServiceUuid1); |
| auto service = SetUpFakeService(data); |
| |
| // The ExtendedProperties of the characteristic is set. |
| const Properties props = Property::kExtendedProperties; |
| CharacteristicData fake_chrc( |
| props, std::nullopt, kCharDecl, kCharValue, kTestUuid3); |
| DescriptorData fake_desc1(kDesc1, types::kCharacteristicExtProperties); |
| DescriptorData fake_desc2(kDesc2, kTestUuid4); |
| SetCharacteristicsAndDescriptors({fake_chrc}, {fake_desc1, fake_desc2}); |
| |
| // The callback should be triggered once to read the value of the descriptor |
| // containing the ExtendedProperties bitfield. |
| size_t read_cb_count = 0; |
| auto extended_prop_read_cb = [&](att::Handle handle, auto callback) { |
| EXPECT_EQ(kDesc1, handle); |
| callback(fit::ok(), |
| BufferView(), |
| /*maybe_truncated=*/false); // Invalid return buf |
| read_cb_count++; |
| }; |
| fake_client()->set_read_request_callback(std::move(extended_prop_read_cb)); |
| |
| att::Result<> status = ToResult(HostError::kFailed); |
| service->DiscoverCharacteristics( |
| [&](att::Result<> cb_status, const auto chrcs) { |
| status = cb_status; |
| EXPECT_TRUE(chrcs.empty()); |
| }); |
| |
| RunUntilIdle(); |
| EXPECT_EQ(1u, fake_client()->chrc_discovery_count()); |
| EXPECT_EQ(1u, fake_client()->desc_discovery_count()); |
| EXPECT_EQ(1u, read_cb_count); |
| EXPECT_FALSE(service->IsDiscovered()); |
| EXPECT_EQ(ToResult(HostError::kPacketMalformed), status); |
| } |
| |
| constexpr CharacteristicHandle kDefaultCharacteristic(3); |
| constexpr CharacteristicHandle kSecondCharacteristic(6); |
| constexpr CharacteristicHandle kInvalidCharacteristic(1); |
| |
| constexpr att::Handle kDefaultChrcValueHandle = 3; |
| |
| CharacteristicData UnreadableChrc() { |
| return CharacteristicData( |
| 0, std::nullopt, 2, kDefaultChrcValueHandle, kTestUuid3); |
| } |
| CharacteristicData ReadableChrc() { |
| return CharacteristicData( |
| Property::kRead, std::nullopt, 2, kDefaultChrcValueHandle, kTestUuid3); |
| } |
| CharacteristicData WritableChrc() { |
| return CharacteristicData( |
| Property::kWrite, std::nullopt, 2, kDefaultChrcValueHandle, kTestUuid3); |
| } |
| CharacteristicData WriteableExtendedPropChrc() { |
| auto props = Property::kWrite | Property::kExtendedProperties; |
| return CharacteristicData( |
| props, std::nullopt, 2, kDefaultChrcValueHandle, kTestUuid3); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, ReadCharWhileNotReady) { |
| auto service = SetUpFakeService( |
| ServiceData(ServiceKind::PRIMARY, 1, 2, kTestServiceUuid1)); |
| |
| att::Result<> status = fit::ok(); |
| service->ReadCharacteristic( |
| kDefaultCharacteristic, |
| [&](att::Result<> cb_status, const auto&, auto) { status = cb_status; }); |
| |
| RunUntilIdle(); |
| EXPECT_EQ(ToResult(HostError::kNotReady), status); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, ReadCharNotFound) { |
| auto service = SetupServiceWithChrcs( |
| ServiceData(ServiceKind::PRIMARY, 1, 2, kTestServiceUuid1), {}); |
| att::Result<> status = fit::ok(); |
| service->ReadCharacteristic( |
| kDefaultCharacteristic, |
| [&](att::Result<> cb_status, const auto&, auto) { status = cb_status; }); |
| |
| RunUntilIdle(); |
| EXPECT_EQ(ToResult(HostError::kNotFound), status); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, ReadCharNotSupported) { |
| auto service = SetupServiceWithChrcs( |
| ServiceData(ServiceKind::PRIMARY, 1, 3, kTestServiceUuid1), |
| {UnreadableChrc()}); |
| att::Result<> status = fit::ok(); |
| service->ReadCharacteristic( |
| kDefaultCharacteristic, |
| [&](att::Result<> cb_status, const auto&, auto) { status = cb_status; }); |
| |
| RunUntilIdle(); |
| EXPECT_EQ(ToResult(HostError::kNotSupported), status); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, ReadCharSendsReadRequest) { |
| auto service = SetupServiceWithChrcs( |
| ServiceData( |
| ServiceKind::PRIMARY, 1, kDefaultChrcValueHandle, kTestServiceUuid1), |
| {ReadableChrc()}); |
| |
| const StaticByteBuffer kValue('t', 'e', 's', 't'); |
| |
| fake_client()->set_read_request_callback( |
| [&](att::Handle handle, auto callback) { |
| EXPECT_EQ(kDefaultChrcValueHandle, handle); |
| callback(fit::ok(), kValue, /*maybe_truncated=*/false); |
| }); |
| |
| att::Result<> status = ToResult(HostError::kFailed); |
| service->ReadCharacteristic( |
| kDefaultCharacteristic, |
| [&](att::Result<> cb_status, const auto& value, bool maybe_truncated) { |
| status = cb_status; |
| EXPECT_TRUE(ContainersEqual(kValue, value)); |
| EXPECT_FALSE(maybe_truncated); |
| }); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(fit::ok(), status); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, ReadLongWhileNotReady) { |
| auto service = SetUpFakeService( |
| ServiceData(ServiceKind::PRIMARY, 1, 2, kTestServiceUuid1)); |
| |
| att::Result<> status = fit::ok(); |
| service->ReadLongCharacteristic( |
| CharacteristicHandle(0), |
| 0, |
| 512, |
| [&](att::Result<> cb_status, const auto&, auto) { status = cb_status; }); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(ToResult(HostError::kNotReady), status); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, ReadLongNotFound) { |
| auto service = SetupServiceWithChrcs( |
| ServiceData(ServiceKind::PRIMARY, 1, 2, kTestServiceUuid1), {}); |
| |
| att::Result<> status = fit::ok(); |
| service->ReadLongCharacteristic( |
| CharacteristicHandle(0), |
| 0, |
| 512, |
| [&](att::Result<> cb_status, const auto&, auto) { status = cb_status; }); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(ToResult(HostError::kNotFound), status); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, ReadLongNotSupported) { |
| auto service = SetupServiceWithChrcs( |
| ServiceData(ServiceKind::PRIMARY, 1, 3, kTestServiceUuid1), |
| {UnreadableChrc()}); |
| |
| att::Result<> status = fit::ok(); |
| service->ReadLongCharacteristic( |
| kDefaultCharacteristic, |
| 0, |
| 512, |
| [&](att::Result<> cb_status, const auto&, auto) { status = cb_status; }); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(ToResult(HostError::kNotSupported), status); |
| } |
| |
| // 0 is not a valid parameter for the |max_size| field of ReadLongCharacteristic |
| TEST_F(RemoteServiceManagerTest, ReadLongMaxSizeZero) { |
| auto service = SetupServiceWithChrcs( |
| ServiceData(ServiceKind::PRIMARY, 1, 3, kTestServiceUuid1), |
| {ReadableChrc()}); |
| |
| att::Result<> status = fit::ok(); |
| service->ReadLongCharacteristic( |
| kDefaultCharacteristic, |
| 0, |
| 0, |
| [&](att::Result<> cb_status, const auto&, auto) { status = cb_status; }); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(ToResult(HostError::kInvalidParameters), status); |
| } |
| |
| // The complete attribute value is read in a single request. |
| TEST_F(RemoteServiceManagerTest, ReadLongSingleBlob) { |
| constexpr uint16_t kOffset = 0; |
| constexpr size_t kMaxBytes = 1000; |
| |
| auto service = SetupServiceWithChrcs( |
| ServiceData( |
| ServiceKind::PRIMARY, 1, kDefaultChrcValueHandle, kTestServiceUuid1), |
| {ReadableChrc()}); |
| |
| const StaticByteBuffer kValue('t', 'e', 's', 't'); |
| |
| int request_count = 0; |
| fake_client()->set_read_request_callback( |
| [&](att::Handle handle, auto callback) { |
| request_count++; |
| EXPECT_EQ(request_count, 1); |
| EXPECT_EQ(kDefaultChrcValueHandle, handle); |
| callback(fit::ok(), kValue, /*maybe_truncated=*/false); |
| }); |
| |
| att::Result<> status = ToResult(HostError::kFailed); |
| service->ReadLongCharacteristic( |
| kDefaultCharacteristic, |
| kOffset, |
| kMaxBytes, |
| [&](att::Result<> cb_status, const auto& value, bool maybe_truncated) { |
| status = cb_status; |
| EXPECT_TRUE(ContainersEqual(kValue, value)); |
| EXPECT_FALSE(maybe_truncated); |
| }); |
| |
| RunUntilIdle(); |
| EXPECT_EQ(fit::ok(), status); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, ReadLongMultipleBlobs) { |
| constexpr uint16_t kOffset = 0; |
| constexpr size_t kMaxBytes = 1000; |
| constexpr int kExpectedBlobCount = 4; |
| |
| auto service = SetupServiceWithChrcs( |
| ServiceData( |
| ServiceKind::PRIMARY, 1, kDefaultChrcValueHandle, kTestServiceUuid1), |
| {ReadableChrc()}); |
| |
| // Create a buffer that will take 4 requests to read. Since the default MTU is |
| // 23: |
| // a. The size of |expected_value| is 69. |
| // b. We should read 22 + 22 + 22 + 3 bytes across 4 requests. |
| StaticByteBuffer<att::kLEMinMTU * 3> expected_value; |
| |
| // Initialize the contents. |
| for (size_t i = 0; i < expected_value.size(); ++i) { |
| expected_value[i] = i; |
| } |
| |
| int read_count = 0; |
| fake_client()->set_read_request_callback( |
| [&](att::Handle handle, auto callback) { |
| read_count++; |
| EXPECT_EQ(read_count, 1); |
| EXPECT_EQ(kDefaultChrcValueHandle, handle); |
| auto blob = expected_value.view(0, att::kLEMinMTU - 1); |
| callback(fit::ok(), blob, /*maybe_truncated=*/true); |
| }); |
| |
| fake_client()->set_read_blob_request_callback( |
| [&](att::Handle handle, uint16_t offset, auto callback) { |
| read_count++; |
| EXPECT_GT(read_count, 1); |
| EXPECT_EQ(kDefaultChrcValueHandle, handle); |
| bool maybe_truncated = true; |
| |
| // Return a blob at the given offset with at most MTU - 1 bytes. |
| auto blob = expected_value.view(offset, att::kLEMinMTU - 1); |
| if (read_count == kExpectedBlobCount) { |
| // The final blob should contain 3 bytes. |
| EXPECT_EQ(3u, blob.size()); |
| maybe_truncated = false; |
| } |
| |
| callback(fit::ok(), blob, maybe_truncated); |
| }); |
| |
| att::Result<> status = ToResult(HostError::kFailed); |
| service->ReadLongCharacteristic( |
| kDefaultCharacteristic, |
| kOffset, |
| kMaxBytes, |
| [&](att::Result<> cb_status, const auto& value, bool maybe_truncated) { |
| status = cb_status; |
| EXPECT_TRUE(ContainersEqual(expected_value, value)); |
| EXPECT_FALSE(maybe_truncated); |
| }); |
| |
| RunUntilIdle(); |
| EXPECT_EQ(fit::ok(), status); |
| EXPECT_EQ(kExpectedBlobCount, read_count); |
| } |
| |
| // Simulates a peer that rejects a read blob request with a kAttributeNotLong |
| // error. The initial read request completes successfully and contains the |
| // entire value. The specification implies that the peer can either respond with |
| // an empty buffer or a kAttributeNotLong error for the second request. |
| TEST_F(RemoteServiceManagerTest, |
| ReadLongCharacteristicAttributeNotLongErrorIgnored) { |
| constexpr uint16_t kOffset = 0; |
| constexpr size_t kMaxBytes = 1000; |
| constexpr int kExpectedBlobCount = 2; |
| |
| auto service = SetupServiceWithChrcs( |
| ServiceData( |
| ServiceKind::PRIMARY, 1, kDefaultChrcValueHandle, kTestServiceUuid1), |
| {ReadableChrc()}); |
| |
| StaticByteBuffer<att::kLEMinMTU - 1> expected_value; |
| for (size_t i = 0; i < expected_value.size(); ++i) { |
| expected_value[i] = i; |
| } |
| |
| int read_count = 0; |
| fake_client()->set_read_request_callback( |
| [&](att::Handle handle, auto callback) { |
| read_count++; |
| EXPECT_EQ(read_count, 1); |
| callback(fit::ok(), expected_value.view(), /*maybe_truncated=*/true); |
| }); |
| |
| fake_client()->set_read_blob_request_callback( |
| [&](att::Handle handle, uint16_t offset, auto callback) { |
| read_count++; |
| EXPECT_EQ(read_count, 2); |
| callback(ToResult(att::ErrorCode::kAttributeNotLong), |
| BufferView(), |
| /*maybe_truncated=*/false); |
| }); |
| |
| att::Result<> status = ToResult(HostError::kFailed); |
| service->ReadLongCharacteristic( |
| kDefaultCharacteristic, |
| kOffset, |
| kMaxBytes, |
| [&](att::Result<> cb_status, const auto& value, bool maybe_truncated) { |
| status = cb_status; |
| EXPECT_TRUE(ContainersEqual(expected_value, value)); |
| EXPECT_FALSE(maybe_truncated); |
| }); |
| |
| RunUntilIdle(); |
| EXPECT_EQ(fit::ok(), status); |
| EXPECT_EQ(kExpectedBlobCount, read_count); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, |
| ReadLongCharacteristicAttributeNotLongErrorOnFirstReadRequest) { |
| constexpr uint16_t kOffset = 0; |
| constexpr size_t kMaxBytes = 1000; |
| constexpr int kExpectedBlobCount = 1; |
| |
| auto service = SetupServiceWithChrcs( |
| ServiceData( |
| ServiceKind::PRIMARY, 1, kDefaultChrcValueHandle, kTestServiceUuid1), |
| {ReadableChrc()}); |
| |
| int read_count = 0; |
| fake_client()->set_read_request_callback( |
| [&](att::Handle handle, auto callback) { |
| read_count++; |
| EXPECT_EQ(read_count, 1); |
| callback(ToResult(att::ErrorCode::kAttributeNotLong), |
| BufferView(), |
| /*maybe_truncated=*/false); |
| }); |
| fake_client()->set_read_blob_request_callback( |
| [&](auto, auto, auto) { FAIL(); }); |
| |
| att::Result<> status = fit::ok(); |
| service->ReadLongCharacteristic( |
| kDefaultCharacteristic, |
| kOffset, |
| kMaxBytes, |
| [&](att::Result<> cb_status, const auto& value, bool maybe_truncated) { |
| status = cb_status; |
| EXPECT_FALSE(maybe_truncated); |
| }); |
| |
| RunUntilIdle(); |
| EXPECT_EQ(ToResult(att::ErrorCode::kAttributeNotLong), status); |
| EXPECT_EQ(kExpectedBlobCount, read_count); |
| } |
| |
| // Same as ReadLongMultipleBlobs except the characteristic value has a size that |
| // is a multiple of (ATT_MTU - 1), so that the last read blob request returns 0 |
| // bytes. |
| TEST_F(RemoteServiceManagerTest, ReadLongValueExactMultipleOfMTU) { |
| constexpr uint16_t kOffset = 0; |
| constexpr size_t kMaxBytes = 1000; |
| constexpr int kExpectedBlobCount = 4; |
| |
| auto service = SetupServiceWithChrcs( |
| ServiceData( |
| ServiceKind::PRIMARY, 1, kDefaultChrcValueHandle, kTestServiceUuid1), |
| {ReadableChrc()}); |
| |
| // Create a buffer that will take 4 requests to read. Since the default MTU is |
| // 23: |
| // a. The size of |expected_value| is 66. |
| // b. We should read 22 + 22 + 22 + 0 bytes across 4 requests. |
| StaticByteBuffer<(att::kLEMinMTU - 1) * 3> expected_value; |
| |
| // Initialize the contents. |
| for (size_t i = 0; i < expected_value.size(); ++i) { |
| expected_value[i] = i; |
| } |
| |
| int read_count = 0; |
| fake_client()->set_read_request_callback( |
| [&](att::Handle handle, auto callback) { |
| read_count++; |
| EXPECT_EQ(read_count, 1); |
| EXPECT_EQ(kDefaultChrcValueHandle, handle); |
| auto blob = expected_value.view(0, att::kLEMinMTU - 1); |
| callback(fit::ok(), blob, /*maybe_truncated=*/true); |
| }); |
| |
| fake_client()->set_read_blob_request_callback( |
| [&](att::Handle handle, uint16_t offset, auto callback) { |
| read_count++; |
| EXPECT_GT(read_count, 1); |
| EXPECT_EQ(kDefaultChrcValueHandle, handle); |
| bool maybe_truncated = true; |
| |
| // Return a blob at the given offset with at most MTU - 1 bytes. |
| auto blob = expected_value.view(offset, att::kLEMinMTU - 1); |
| if (read_count == kExpectedBlobCount) { |
| // The final blob should be empty. |
| EXPECT_EQ(0u, blob.size()); |
| maybe_truncated = false; |
| } |
| |
| callback(fit::ok(), blob, maybe_truncated); |
| }); |
| |
| att::Result<> status = ToResult(HostError::kFailed); |
| service->ReadLongCharacteristic( |
| kDefaultCharacteristic, |
| kOffset, |
| kMaxBytes, |
| [&](att::Result<> cb_status, const auto& value, bool maybe_truncated) { |
| status = cb_status; |
| EXPECT_TRUE(ContainersEqual(expected_value, value)); |
| EXPECT_FALSE(maybe_truncated); |
| }); |
| |
| RunUntilIdle(); |
| EXPECT_EQ(fit::ok(), status); |
| EXPECT_EQ(kExpectedBlobCount, read_count); |
| } |
| |
| // Same as ReadLongMultipleBlobs but a maximum size is given that is smaller |
| // than the size of the attribute value. |
| TEST_F(RemoteServiceManagerTest, ReadLongMultipleBlobsWithMaxSize) { |
| constexpr uint16_t kOffset = 0; |
| constexpr size_t kMaxBytes = 40; |
| constexpr int kExpectedBlobCount = 2; |
| |
| auto service = SetupServiceWithChrcs( |
| ServiceData( |
| ServiceKind::PRIMARY, 1, kDefaultChrcValueHandle, kTestServiceUuid1), |
| {ReadableChrc()}); |
| |
| // Reads will return 22 + 22 bytes across 2 requests, but only 18 bytes of the |
| // second read will be reported to ReadLongCharacteristic (the value will be |
| // truncated). |
| StaticByteBuffer<att::kLEMinMTU * 3> expected_value; |
| |
| // Initialize the contents. |
| for (size_t i = 0; i < expected_value.size(); ++i) { |
| expected_value[i] = i; |
| } |
| |
| int read_count = 0; |
| fake_client()->set_read_request_callback( |
| [&](att::Handle handle, auto callback) { |
| read_count++; |
| EXPECT_EQ(read_count, 1); |
| EXPECT_EQ(kDefaultChrcValueHandle, handle); |
| BufferView blob = expected_value.view(0, att::kLEMinMTU - 1); |
| callback(fit::ok(), blob, /*maybe_truncated=*/true); |
| }); |
| |
| fake_client()->set_read_blob_request_callback( |
| [&](att::Handle handle, uint16_t offset, auto callback) { |
| read_count++; |
| EXPECT_GT(read_count, 1); |
| EXPECT_EQ(kDefaultChrcValueHandle, handle); |
| BufferView blob = expected_value.view(offset, att::kLEMinMTU - 1); |
| callback(fit::ok(), blob, /*maybe_truncated=*/true); |
| }); |
| |
| att::Result<> status = ToResult(HostError::kFailed); |
| service->ReadLongCharacteristic( |
| kDefaultCharacteristic, |
| kOffset, |
| kMaxBytes, |
| [&](att::Result<> cb_status, const auto& value, bool maybe_truncated) { |
| status = cb_status; |
| EXPECT_TRUE(ContainersEqual(expected_value.view(0, kMaxBytes), value)); |
| EXPECT_TRUE(maybe_truncated); |
| }); |
| |
| RunUntilIdle(); |
| EXPECT_EQ(fit::ok(), status); |
| EXPECT_EQ(kExpectedBlobCount, read_count); |
| } |
| |
| // Same as ReadLongMultipleBlobs but a non-zero offset is given. |
| TEST_F(RemoteServiceManagerTest, ReadLongAtOffset) { |
| constexpr uint16_t kOffset = 30; |
| constexpr size_t kMaxBytes = 1000; |
| constexpr int kExpectedBlobCount = 2; |
| |
| auto service = SetupServiceWithChrcs( |
| ServiceData( |
| ServiceKind::PRIMARY, 1, kDefaultChrcValueHandle, kTestServiceUuid1), |
| {ReadableChrc()}); |
| |
| // Size: 69. |
| // Reads starting at offset 30 will return 22 + 17 bytes across 2 requests. |
| StaticByteBuffer<att::kLEMinMTU * 3> expected_value; |
| |
| // Initialize the contents. |
| for (size_t i = 0; i < expected_value.size(); ++i) { |
| expected_value[i] = i; |
| } |
| |
| int read_blob_count = 0; |
| fake_client()->set_read_blob_request_callback( |
| [&](att::Handle handle, uint16_t offset, auto callback) { |
| EXPECT_EQ(kDefaultChrcValueHandle, handle); |
| read_blob_count++; |
| BufferView blob = expected_value.view(offset, att::kLEMinMTU - 1); |
| bool maybe_truncated = (read_blob_count != kExpectedBlobCount); |
| callback(fit::ok(), blob, maybe_truncated); |
| }); |
| |
| att::Result<> status = ToResult(HostError::kFailed); |
| service->ReadLongCharacteristic( |
| kDefaultCharacteristic, |
| kOffset, |
| kMaxBytes, |
| [&](att::Result<> cb_status, const auto& value, bool maybe_truncated) { |
| status = cb_status; |
| EXPECT_TRUE( |
| ContainersEqual(expected_value.view(kOffset, kMaxBytes), value)); |
| EXPECT_FALSE(maybe_truncated); |
| }); |
| |
| RunUntilIdle(); |
| EXPECT_EQ(fit::ok(), status); |
| EXPECT_EQ(kExpectedBlobCount, read_blob_count); |
| } |
| |
| // Same as ReadLongAtOffset but a very small max size is given. |
| TEST_F(RemoteServiceManagerTest, ReadLongAtOffsetWithMaxBytes) { |
| constexpr uint16_t kOffset = 10; |
| constexpr size_t kMaxBytes = 34; |
| constexpr int kExpectedBlobCount = 2; |
| |
| auto service = SetupServiceWithChrcs( |
| ServiceData( |
| ServiceKind::PRIMARY, 1, kDefaultChrcValueHandle, kTestServiceUuid1), |
| {ReadableChrc()}); |
| |
| // Size: 69. |
| // Reads starting at offset 10 will return 22 + 22 bytes across 2 requests, |
| // but the second read value will be truncated to 12 bytes by RemoteService |
| // due to |kMaxBytes|. A third read blob should not be sent since this should |
| // satisfy |kMaxBytes|. |
| StaticByteBuffer<att::kLEMinMTU * 3> expected_value; |
| |
| // Initialize the contents. |
| for (size_t i = 0; i < expected_value.size(); ++i) { |
| expected_value[i] = static_cast<uint8_t>(i); |
| } |
| |
| int read_blob_count = 0; |
| fake_client()->set_read_blob_request_callback( |
| [&](att::Handle handle, uint16_t offset, auto callback) { |
| EXPECT_EQ(kDefaultChrcValueHandle, handle); |
| read_blob_count++; |
| BufferView blob = expected_value.view(offset, att::kLEMinMTU - 1); |
| callback(fit::ok(), blob, /*maybe_truncated=*/true); |
| }); |
| |
| att::Result<> status = ToResult(HostError::kFailed); |
| service->ReadLongCharacteristic( |
| kDefaultCharacteristic, |
| kOffset, |
| kMaxBytes, |
| [&](att::Result<> cb_status, const auto& value, bool maybe_truncated) { |
| status = cb_status; |
| EXPECT_TRUE( |
| ContainersEqual(expected_value.view(kOffset, kMaxBytes), value)); |
| EXPECT_TRUE(maybe_truncated); |
| }); |
| |
| RunUntilIdle(); |
| EXPECT_EQ(fit::ok(), status); |
| EXPECT_EQ(kExpectedBlobCount, read_blob_count); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, ReadLongError) { |
| constexpr uint16_t kOffset = 0; |
| constexpr size_t kMaxBytes = 1000; |
| constexpr int kExpectedBlobCount = 2; // The second request will fail. |
| |
| auto service = SetupServiceWithChrcs( |
| ServiceData( |
| ServiceKind::PRIMARY, 1, kDefaultChrcValueHandle, kTestServiceUuid1), |
| {ReadableChrc()}); |
| |
| // Make the first blob large enough that it will cause a second read blob |
| // request. |
| StaticByteBuffer<att::kLEMinMTU - 1> first_blob; |
| |
| int read_count = 0; |
| fake_client()->set_read_request_callback( |
| [&](att::Handle handle, auto callback) { |
| read_count++; |
| EXPECT_EQ(read_count, 1); |
| EXPECT_EQ(kDefaultChrcValueHandle, handle); |
| callback(fit::ok(), first_blob, /*maybe_truncated=*/true); |
| }); |
| |
| fake_client()->set_read_blob_request_callback( |
| [&](att::Handle handle, uint16_t offset, auto callback) { |
| read_count++; |
| EXPECT_EQ(read_count, 2); |
| EXPECT_EQ(kDefaultChrcValueHandle, handle); |
| callback(ToResult(att::ErrorCode::kInvalidOffset), |
| BufferView(), |
| /*maybe_truncated=*/false); |
| }); |
| |
| att::Result<> status = fit::ok(); |
| service->ReadLongCharacteristic( |
| kDefaultCharacteristic, |
| kOffset, |
| kMaxBytes, |
| [&](att::Result<> cb_status, const auto& value, bool maybe_truncated) { |
| status = cb_status; |
| EXPECT_EQ(0u, value.size()); // No value should be returned on error. |
| EXPECT_FALSE(maybe_truncated); |
| }); |
| |
| RunUntilIdle(); |
| EXPECT_EQ(ToResult(att::ErrorCode::kInvalidOffset), status); |
| EXPECT_EQ(kExpectedBlobCount, read_count); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, |
| ReadByTypeSendsReadRequestsUntilAttributeNotFound) { |
| constexpr att::Handle kStartHandle = 1; |
| constexpr att::Handle kEndHandle = 5; |
| auto service = SetUpFakeService(ServiceData( |
| ServiceKind::PRIMARY, kStartHandle, kEndHandle, kTestServiceUuid1)); |
| |
| constexpr UUID kCharUuid(uint16_t{0xfefe}); |
| |
| constexpr att::Handle kHandle0 = 2; |
| const StaticByteBuffer kValue0(0x00, 0x01, 0x02); |
| const std::vector<Client::ReadByTypeValue> kValues0 = { |
| {kHandle0, kValue0.view(), /*maybe_truncated=*/false}}; |
| |
| constexpr att::Handle kHandle1 = 3; |
| const StaticByteBuffer kValue1(0x03, 0x04, 0x05); |
| const std::vector<Client::ReadByTypeValue> kValues1 = { |
| {kHandle1, kValue1.view(), /*maybe_truncated=*/true}}; |
| |
| size_t read_count = 0; |
| fake_client()->set_read_by_type_request_callback( |
| [&](const UUID& type, att::Handle start, att::Handle end, auto callback) { |
| switch (read_count++) { |
| case 0: |
| EXPECT_EQ(kStartHandle, start); |
| callback(fit::ok(kValues0)); |
| break; |
| case 1: |
| EXPECT_EQ(kHandle0 + 1, start); |
| callback(fit::ok(kValues1)); |
| break; |
| case 2: |
| EXPECT_EQ(kHandle1 + 1, start); |
| callback(fit::error(Client::ReadByTypeError{ |
| att::Error(att::ErrorCode::kAttributeNotFound), start})); |
| break; |
| default: |
| FAIL(); |
| } |
| }); |
| |
| std::optional<att::Result<>> status; |
| service->ReadByType( |
| kCharUuid, |
| [&](att::Result<> cb_status, |
| std::vector<RemoteService::ReadByTypeResult> values) { |
| status = cb_status; |
| EXPECT_EQ(fit::ok(), *status); |
| ASSERT_EQ(2u, values.size()); |
| EXPECT_EQ(CharacteristicHandle(kHandle0), values[0].handle); |
| ASSERT_EQ(fit::ok(), values[0].result); |
| EXPECT_TRUE(ContainersEqual(kValue0, *values[0].result.value())); |
| EXPECT_FALSE(values[0].maybe_truncated); |
| EXPECT_EQ(CharacteristicHandle(kHandle1), values[1].handle); |
| ASSERT_EQ(fit::ok(), values[1].result); |
| EXPECT_TRUE(ContainersEqual(kValue1, *values[1].result.value())); |
| EXPECT_TRUE(values[1].maybe_truncated); |
| }); |
| |
| RunUntilIdle(); |
| ASSERT_TRUE(status.has_value()); |
| // kAttributeNotFound error should be treated as success. |
| EXPECT_EQ(fit::ok(), *status); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, |
| ReadByTypeSendsReadRequestsUntilServiceEndHandle) { |
| constexpr att::Handle kStartHandle = 1; |
| constexpr att::Handle kEndHandle = 2; |
| auto service = SetUpFakeService(ServiceData( |
| ServiceKind::PRIMARY, kStartHandle, kEndHandle, kTestServiceUuid1)); |
| |
| constexpr UUID kCharUuid(uint16_t{0xfefe}); |
| |
| constexpr att::Handle kHandle = kEndHandle; |
| const StaticByteBuffer kValue(0x00, 0x01, 0x02); |
| const std::vector<Client::ReadByTypeValue> kValues = { |
| {kHandle, kValue.view(), /*maybe_truncated=*/false}}; |
| |
| size_t read_count = 0; |
| fake_client()->set_read_by_type_request_callback( |
| [&](const UUID& type, att::Handle start, att::Handle end, auto callback) { |
| EXPECT_EQ(kStartHandle, start); |
| EXPECT_EQ(0u, read_count++); |
| callback(fit::ok(kValues)); |
| }); |
| |
| std::optional<att::Result<>> status; |
| service->ReadByType(kCharUuid, [&](att::Result<> cb_status, auto values) { |
| status = cb_status; |
| ASSERT_EQ(1u, values.size()); |
| EXPECT_EQ(CharacteristicHandle(kHandle), values[0].handle); |
| ASSERT_EQ(fit::ok(), values[0].result); |
| EXPECT_TRUE(ContainersEqual(kValue, *values[0].result.value())); |
| }); |
| |
| RunUntilIdle(); |
| ASSERT_TRUE(status.has_value()); |
| EXPECT_EQ(fit::ok(), *status); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, ReadByTypeReturnsReadErrorsWithResults) { |
| constexpr att::Handle kStartHandle = 1; |
| constexpr att::Handle kEndHandle = 5; |
| auto service = SetUpFakeService(ServiceData( |
| ServiceKind::PRIMARY, kStartHandle, kEndHandle, kTestServiceUuid1)); |
| |
| constexpr UUID kCharUuid(uint16_t{0xfefe}); |
| |
| const std::array<att::ErrorCode, 5> errors = { |
| att::ErrorCode::kInsufficientAuthorization, |
| att::ErrorCode::kInsufficientAuthentication, |
| att::ErrorCode::kInsufficientEncryptionKeySize, |
| att::ErrorCode::kInsufficientEncryption, |
| att::ErrorCode::kReadNotPermitted}; |
| |
| size_t read_count = 0; |
| fake_client()->set_read_by_type_request_callback( |
| [&](const UUID& type, att::Handle start, att::Handle end, auto callback) { |
| if (read_count < errors.size()) { |
| EXPECT_EQ(kStartHandle + read_count, start); |
| callback(fit::error(Client::ReadByTypeError{ |
| att::Error(errors[read_count++]), start})); |
| } else { |
| FAIL(); |
| } |
| }); |
| |
| std::optional<att::Result<>> status; |
| service->ReadByType(kCharUuid, [&](att::Result<> cb_status, auto values) { |
| status = cb_status; |
| ASSERT_EQ(errors.size(), values.size()); |
| for (size_t i = 0; i < values.size(); i++) { |
| SCOPED_TRACE(bt_lib_cpp_string::StringPrintf("i: %zu", i)); |
| EXPECT_EQ(CharacteristicHandle(kStartHandle + i), values[i].handle); |
| ASSERT_TRUE(values[i].result.is_error()); |
| EXPECT_EQ(errors[i], values[i].result.error_value()); |
| EXPECT_FALSE(values[i].maybe_truncated); |
| } |
| }); |
| |
| RunUntilIdle(); |
| ASSERT_TRUE(status.has_value()); |
| ASSERT_EQ(fit::ok(), *status); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, ReadByTypeReturnsProtocolErrorAfterRead) { |
| constexpr att::Handle kStartHandle = 1; |
| constexpr att::Handle kEndHandle = 5; |
| auto service = SetUpFakeService(ServiceData( |
| ServiceKind::PRIMARY, kStartHandle, kEndHandle, kTestServiceUuid1)); |
| |
| constexpr UUID kCharUuid(uint16_t{0xfefe}); |
| |
| constexpr att::Handle kHandle = kEndHandle; |
| const auto kValue = StaticByteBuffer(0x00, 0x01, 0x02); |
| const std::vector<Client::ReadByTypeValue> kValues = { |
| {kHandle, kValue.view(), /*maybe_truncated=*/false}}; |
| |
| const std::vector<std::pair<const char*, att::ErrorCode>> |
| general_protocol_errors = { |
| {"kRequestNotSupported", att::ErrorCode::kRequestNotSupported}, |
| {"kInsufficientResources", att::ErrorCode::kInsufficientResources}, |
| {"kInvalidPDU", att::ErrorCode::kInvalidPDU}}; |
| |
| for (const auto& [name, code] : general_protocol_errors) { |
| SCOPED_TRACE(bt_lib_cpp_string::StringPrintf("Error Code: %s", name)); |
| size_t read_count = 0; |
| fake_client()->set_read_by_type_request_callback( |
| [&, code = code](const UUID& type, |
| att::Handle start, |
| att::Handle end, |
| auto callback) { |
| ASSERT_EQ(0u, read_count++); |
| switch (read_count++) { |
| case 0: |
| callback(fit::ok(kValues)); |
| break; |
| case 1: |
| callback(fit::error( |
| Client::ReadByTypeError{att::Error(code), std::nullopt})); |
| break; |
| default: |
| FAIL(); |
| } |
| }); |
| |
| std::optional<att::Result<>> status; |
| service->ReadByType(kCharUuid, [&](att::Result<> cb_status, auto values) { |
| status = cb_status; |
| EXPECT_EQ(0u, values.size()); |
| }); |
| |
| RunUntilIdle(); |
| ASSERT_TRUE(status.has_value()); |
| EXPECT_EQ(ToResult(code), status); |
| } |
| } |
| |
| TEST_F(RemoteServiceManagerTest, ReadByTypeHandlesReadErrorWithMissingHandle) { |
| constexpr att::Handle kStartHandle = 1; |
| constexpr att::Handle kEndHandle = 5; |
| auto service = SetUpFakeService(ServiceData( |
| ServiceKind::PRIMARY, kStartHandle, kEndHandle, kTestServiceUuid1)); |
| |
| constexpr UUID kCharUuid(uint16_t{0xfefe}); |
| |
| size_t read_count = 0; |
| fake_client()->set_read_by_type_request_callback( |
| [&](const UUID& type, att::Handle start, att::Handle end, auto callback) { |
| ASSERT_EQ(0u, read_count++); |
| callback(fit::error(Client::ReadByTypeError{ |
| att::Error(att::ErrorCode::kReadNotPermitted), std::nullopt})); |
| }); |
| |
| std::optional<att::Result<>> status; |
| service->ReadByType(kCharUuid, [&](att::Result<> cb_status, auto values) { |
| status = cb_status; |
| }); |
| RunUntilIdle(); |
| ASSERT_TRUE(status.has_value()); |
| EXPECT_EQ(ToResult(att::ErrorCode::kReadNotPermitted), *status); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, |
| ReadByTypeHandlesReadErrorWithOutOfRangeHandle) { |
| constexpr att::Handle kStartHandle = 1; |
| constexpr att::Handle kEndHandle = 5; |
| auto service = SetUpFakeService(ServiceData( |
| ServiceKind::PRIMARY, kStartHandle, kEndHandle, kTestServiceUuid1)); |
| |
| constexpr UUID kCharUuid(uint16_t{0xfefe}); |
| |
| size_t read_count = 0; |
| fake_client()->set_read_by_type_request_callback( |
| [&](const UUID& type, att::Handle start, att::Handle end, auto callback) { |
| ASSERT_EQ(0u, read_count++); |
| callback(fit::error(Client::ReadByTypeError{ |
| att::Error(att::ErrorCode::kReadNotPermitted), kEndHandle + 1})); |
| }); |
| |
| std::optional<att::Result<>> status; |
| service->ReadByType(kCharUuid, [&](att::Result<> cb_status, auto values) { |
| status = cb_status; |
| }); |
| RunUntilIdle(); |
| ASSERT_TRUE(status.has_value()); |
| EXPECT_EQ(ToResult(HostError::kPacketMalformed), *status); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, ReadByTypeReturnsErrorIfUuidIsInternal) { |
| const std::array<UUID, 10> kInternalUuids = { |
| types::kPrimaryService, |
| types::kSecondaryService, |
| types::kIncludeDeclaration, |
| types::kCharacteristicDeclaration, |
| types::kCharacteristicExtProperties, |
| types::kCharacteristicUserDescription, |
| types::kClientCharacteristicConfig, |
| types::kServerCharacteristicConfig, |
| types::kCharacteristicFormat, |
| types::kCharacteristicAggregateFormat}; |
| |
| auto service = SetUpFakeService(ServiceData( |
| ServiceKind::PRIMARY, 1, kDefaultChrcValueHandle, kTestServiceUuid1)); |
| |
| fake_client()->set_read_by_type_request_callback( |
| [&](auto, auto, auto, auto) { ADD_FAILURE(); }); |
| |
| for (const UUID& uuid : kInternalUuids) { |
| std::optional<att::Result<>> status; |
| service->ReadByType(uuid, [&](att::Result<> cb_status, auto values) { |
| status = cb_status; |
| EXPECT_EQ(0u, values.size()); |
| }); |
| |
| RunUntilIdle(); |
| ASSERT_TRUE(status.has_value()) << "UUID: " << uuid; |
| EXPECT_EQ(ToResult(HostError::kInvalidParameters), *status); |
| } |
| } |
| |
| TEST_F(RemoteServiceManagerTest, WriteCharWhileNotReady) { |
| auto service = SetUpFakeService( |
| ServiceData(ServiceKind::PRIMARY, 1, 2, kTestServiceUuid1)); |
| |
| att::Result<> status = fit::ok(); |
| service->WriteCharacteristic( |
| kDefaultCharacteristic, |
| std::vector<uint8_t>(), |
| [&](att::Result<> cb_status) { status = cb_status; }); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(ToResult(HostError::kNotReady), status); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, WriteCharNotFound) { |
| auto service = SetupServiceWithChrcs( |
| ServiceData(ServiceKind::PRIMARY, 1, 2, kTestServiceUuid1), {}); |
| |
| att::Result<> status = fit::ok(); |
| service->WriteCharacteristic( |
| kDefaultCharacteristic, |
| std::vector<uint8_t>(), |
| [&](att::Result<> cb_status) { status = cb_status; }); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(ToResult(HostError::kNotFound), status); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, WriteCharNotSupported) { |
| // No "write" property set. |
| auto service = SetupServiceWithChrcs( |
| ServiceData(ServiceKind::PRIMARY, 1, 3, kTestServiceUuid1), |
| {ReadableChrc()}); |
| |
| att::Result<> status = fit::ok(); |
| service->WriteCharacteristic( |
| kDefaultCharacteristic, |
| std::vector<uint8_t>(), |
| [&](att::Result<> cb_status) { status = cb_status; }); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(ToResult(HostError::kNotSupported), status); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, WriteCharSendsWriteRequest) { |
| const std::vector<uint8_t> kValue{{'t', 'e', 's', 't'}}; |
| constexpr att::Result<> kStatus = |
| ToResult(att::ErrorCode::kWriteNotPermitted); |
| |
| auto service = SetupServiceWithChrcs( |
| ServiceData( |
| ServiceKind::PRIMARY, 1, kDefaultChrcValueHandle, kTestServiceUuid1), |
| {WritableChrc()}); |
| |
| fake_client()->set_write_request_callback( |
| [&](att::Handle handle, const auto& value, auto status_callback) { |
| EXPECT_EQ(kDefaultChrcValueHandle, handle); |
| EXPECT_TRUE(std::equal( |
| kValue.begin(), kValue.end(), value.begin(), value.end())); |
| status_callback(kStatus); |
| }); |
| |
| att::Result<> status = fit::ok(); |
| service->WriteCharacteristic( |
| kDefaultCharacteristic, kValue, [&](att::Result<> cb_status) { |
| status = cb_status; |
| }); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(kStatus, status); |
| } |
| |
| // Tests that a long write is chunked up properly into a series of QueuedWrites |
| // that will be processed by the client. This tests a non-zero offset. |
| TEST_F(RemoteServiceManagerTest, WriteCharLongOffsetSuccess) { |
| constexpr uint16_t kOffset = 5; |
| constexpr uint16_t kExpectedQueueSize = 4; |
| constexpr uint16_t kExpectedFullWriteSize = 18; |
| constexpr uint16_t kExpectedFinalWriteSize = 15; |
| |
| auto service = SetupServiceWithChrcs( |
| ServiceData( |
| ServiceKind::PRIMARY, 1, kDefaultChrcValueHandle, kTestServiceUuid1), |
| {WritableChrc()}); |
| |
| // Create a vector that will take 4 requests to write. Since the default MTU |
| // is 23: |
| // a. The size of |full_write_value| is 69. |
| // b. att:Handle, |kOffset|, and att::OpCode size is 5 bytes total. |
| // c. We should write 18 + 18 + 18 + 15 bytes across 4 requests. |
| // d. These bytes will be written with offset 5, from (5) to (5+69) |
| std::vector<uint8_t> full_write_value(att::kLEMinMTU * 3); |
| |
| // Initialize the contents. |
| for (size_t i = 0; i < full_write_value.size(); ++i) { |
| full_write_value[i] = i; |
| } |
| |
| uint8_t process_long_write_count = 0; |
| fake_client()->set_execute_prepare_writes_callback( |
| [&](att::PrepareWriteQueue write_queue, |
| auto /*reliable_mode*/, |
| auto callback) { |
| EXPECT_EQ(write_queue.size(), kExpectedQueueSize); |
| |
| for (int i = 0; i < kExpectedQueueSize; i++) { |
| auto write = std::move(write_queue.front()); |
| write_queue.pop(); |
| |
| EXPECT_EQ(write.handle(), kDefaultChrcValueHandle); |
| EXPECT_EQ(write.offset(), kOffset + (i * kExpectedFullWriteSize)); |
| |
| // All writes expect the final should be full, the final should be |
| // the remainder. |
| if (i < kExpectedQueueSize - 1) { |
| EXPECT_EQ(write.value().size(), kExpectedFullWriteSize); |
| } else { |
| EXPECT_EQ(write.value().size(), kExpectedFinalWriteSize); |
| } |
| } |
| |
| process_long_write_count++; |
| |
| callback(fit::ok()); |
| }); |
| |
| ReliableMode mode = ReliableMode::kDisabled; |
| att::Result<> status = ToResult(HostError::kFailed); |
| service->WriteLongCharacteristic( |
| kDefaultCharacteristic, |
| kOffset, |
| full_write_value, |
| mode, |
| [&](att::Result<> cb_status) { status = cb_status; }); |
| |
| RunUntilIdle(); |
| EXPECT_EQ(fit::ok(), status); |
| EXPECT_EQ(1u, process_long_write_count); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, WriteCharLongAtExactMultipleOfMtu) { |
| constexpr uint16_t kOffset = 0; |
| constexpr uint16_t kExpectedQueueSize = 4; |
| constexpr uint16_t kExpectedFullWriteSize = 18; |
| |
| auto service = SetupServiceWithChrcs( |
| ServiceData( |
| ServiceKind::PRIMARY, 1, kDefaultChrcValueHandle, kTestServiceUuid1), |
| {WritableChrc()}); |
| |
| // Create a vector that will take 4 requests to write. Since the default MTU |
| // is 23: |
| // a. The size of |full_write_value| is 72. |
| // b. att:Handle, |kOffset|, and att::OpCode size is 5 bytes total. |
| // c. We should write 18 + 18 + 18 + 18 bytes across 4 requests. |
| // d. These bytes will be written with offset 0, from (0) to (72) |
| std::vector<uint8_t> full_write_value((att::kLEMinMTU - 5) * 4); |
| |
| // Initialize the contents. |
| for (size_t i = 0; i < full_write_value.size(); ++i) { |
| full_write_value[i] = i; |
| } |
| |
| uint8_t process_long_write_count = 0; |
| fake_client()->set_execute_prepare_writes_callback( |
| [&](att::PrepareWriteQueue write_queue, |
| auto /*reliable_mode*/, |
| auto callback) { |
| EXPECT_EQ(write_queue.size(), kExpectedQueueSize); |
| |
| for (int i = 0; i < kExpectedQueueSize; i++) { |
| auto write = std::move(write_queue.front()); |
| write_queue.pop(); |
| |
| EXPECT_EQ(write.handle(), kDefaultChrcValueHandle); |
| EXPECT_EQ(write.offset(), kOffset + (i * kExpectedFullWriteSize)); |
| |
| // All writes should be full |
| EXPECT_EQ(write.value().size(), kExpectedFullWriteSize); |
| } |
| |
| process_long_write_count++; |
| |
| callback(fit::ok()); |
| }); |
| |
| ReliableMode mode = ReliableMode::kDisabled; |
| att::Result<> status = ToResult(HostError::kFailed); |
| service->WriteLongCharacteristic( |
| kDefaultCharacteristic, |
| kOffset, |
| full_write_value, |
| mode, |
| [&](att::Result<> cb_status) { status = cb_status; }); |
| |
| RunUntilIdle(); |
| EXPECT_EQ(fit::ok(), status); |
| EXPECT_EQ(1u, process_long_write_count); |
| } |
| |
| // Writing a long characteristic with ReliableMode::Enabled should succeed. |
| TEST_F(RemoteServiceManagerTest, WriteCharLongReliableWrite) { |
| constexpr uint16_t kOffset = 0; |
| constexpr uint16_t kExpectedQueueSize = 1; |
| |
| DescriptorData fake_desc1(kDesc1, types::kCharacteristicExtProperties); |
| DescriptorData fake_desc2(kDesc2, kTestUuid4); |
| |
| // The callback should be triggered once to read the value of the descriptor |
| // containing the ExtendedProperties bitfield. |
| auto extended_prop_read_cb = [&](att::Handle handle, auto callback) { |
| callback(fit::ok(), kExtendedPropValue, /*maybe_truncated=*/false); |
| }; |
| fake_client()->set_read_request_callback(std::move(extended_prop_read_cb)); |
| |
| auto service = SetupServiceWithChrcs( |
| ServiceData(ServiceKind::PRIMARY, kStart, kEnd, kTestServiceUuid1), |
| {WriteableExtendedPropChrc()}, |
| {fake_desc1, fake_desc2}); |
| |
| // Create a vector that will take 1 request to write. Since the default MTU |
| // is 23: |
| // a. The size of |full_write_value| is 18. |
| // b. att:Handle, |kOffset|, and att::OpCode size is 5 bytes total. |
| // c. We should write 18 bytes. |
| std::vector<uint8_t> full_write_value((att::kLEMinMTU - 5)); |
| |
| // Initialize the contents. |
| for (size_t i = 0; i < full_write_value.size(); ++i) { |
| full_write_value[i] = i; |
| } |
| |
| uint8_t process_long_write_count = 0; |
| fake_client()->set_execute_prepare_writes_callback( |
| [&](att::PrepareWriteQueue write_queue, |
| auto /*reliable_mode*/, |
| auto callback) { |
| EXPECT_EQ(write_queue.size(), kExpectedQueueSize); |
| process_long_write_count++; |
| callback(fit::ok()); |
| }); |
| |
| ReliableMode mode = ReliableMode::kEnabled; |
| att::Result<> status = ToResult(HostError::kFailed); |
| service->WriteLongCharacteristic( |
| kDefaultCharacteristic, |
| kOffset, |
| full_write_value, |
| mode, |
| [&](att::Result<> cb_status) { status = cb_status; }); |
| |
| RunUntilIdle(); |
| EXPECT_EQ(fit::ok(), status); |
| EXPECT_EQ(1u, process_long_write_count); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, WriteWithoutResponseNotSupported) { |
| ServiceData data(ServiceKind::PRIMARY, 1, 3, kTestServiceUuid1); |
| auto service = SetUpFakeService(data); |
| |
| // No "write" or "write without response" property. |
| CharacteristicData chr(0, std::nullopt, 2, 3, kTestUuid3); |
| SetupCharacteristics(service, {{chr}}); |
| |
| bool called = false; |
| fake_client()->set_write_without_rsp_callback( |
| [&](auto, const auto&, auto) { called = true; }); |
| |
| std::optional<att::Result<>> status; |
| service->WriteCharacteristicWithoutResponse( |
| kDefaultCharacteristic, |
| std::vector<uint8_t>(), |
| [&](att::Result<> cb_status) { status = cb_status; }); |
| RunUntilIdle(); |
| EXPECT_FALSE(called); |
| ASSERT_TRUE(status.has_value()); |
| EXPECT_EQ(ToResult(HostError::kNotSupported), *status); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, |
| WriteWithoutResponseBeforeCharacteristicDiscovery) { |
| ServiceData data(ServiceKind::PRIMARY, 1, 3, kTestServiceUuid1); |
| auto service = SetUpFakeService(data); |
| |
| bool called = false; |
| fake_client()->set_write_without_rsp_callback( |
| [&](auto, const auto&, auto) { called = true; }); |
| |
| std::optional<att::Result<>> status; |
| service->WriteCharacteristicWithoutResponse( |
| kDefaultCharacteristic, |
| std::vector<uint8_t>(), |
| [&](att::Result<> cb_status) { status = cb_status; }); |
| RunUntilIdle(); |
| EXPECT_FALSE(called); |
| ASSERT_TRUE(status.has_value()); |
| EXPECT_EQ(ToResult(HostError::kNotReady), *status); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, |
| WriteWithoutResponseSuccessWithWriteWithoutResponseProperty) { |
| const std::vector<uint8_t> kValue{{'t', 'e', 's', 't'}}; |
| |
| CharacteristicData chr(Property::kWriteWithoutResponse, |
| std::nullopt, |
| 2, |
| kDefaultChrcValueHandle, |
| kTestUuid3); |
| auto service = SetupServiceWithChrcs( |
| ServiceData( |
| ServiceKind::PRIMARY, 1, kDefaultChrcValueHandle, kTestServiceUuid1), |
| {chr}); |
| |
| bool called = false; |
| fake_client()->set_write_without_rsp_callback( |
| [&](att::Handle handle, const auto& value, att::ResultFunction<> cb) { |
| EXPECT_EQ(kDefaultChrcValueHandle, handle); |
| EXPECT_TRUE(std::equal( |
| kValue.begin(), kValue.end(), value.begin(), value.end())); |
| called = true; |
| cb(fit::ok()); |
| }); |
| |
| std::optional<att::Result<>> status; |
| service->WriteCharacteristicWithoutResponse( |
| kDefaultCharacteristic, kValue, [&](att::Result<> cb_status) { |
| status = cb_status; |
| }); |
| RunUntilIdle(); |
| EXPECT_TRUE(called); |
| ASSERT_TRUE(status.has_value()); |
| EXPECT_EQ(fit::ok(), *status); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, WriteWithoutResponseSuccessWithWriteProperty) { |
| const std::vector<uint8_t> kValue{{'t', 'e', 's', 't'}}; |
| |
| CharacteristicData chr( |
| Property::kWrite, std::nullopt, 2, kDefaultChrcValueHandle, kTestUuid3); |
| auto service = SetupServiceWithChrcs( |
| ServiceData( |
| ServiceKind::PRIMARY, 1, kDefaultChrcValueHandle, kTestServiceUuid1), |
| {chr}); |
| |
| bool called = false; |
| fake_client()->set_write_without_rsp_callback( |
| [&](att::Handle handle, const auto& value, att::ResultFunction<> cb) { |
| EXPECT_EQ(kDefaultChrcValueHandle, handle); |
| EXPECT_TRUE(std::equal( |
| kValue.begin(), kValue.end(), value.begin(), value.end())); |
| called = true; |
| cb(fit::ok()); |
| }); |
| |
| std::optional<att::Result<>> status; |
| service->WriteCharacteristicWithoutResponse( |
| kDefaultCharacteristic, kValue, [&](att::Result<> cb_status) { |
| status = cb_status; |
| }); |
| RunUntilIdle(); |
| |
| EXPECT_TRUE(called); |
| ASSERT_TRUE(status.has_value()); |
| EXPECT_EQ(fit::ok(), *status); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, ReadDescWhileNotReady) { |
| auto service = SetUpFakeService( |
| ServiceData(ServiceKind::PRIMARY, 1, 2, kTestServiceUuid1)); |
| |
| att::Result<> status = fit::ok(); |
| service->ReadDescriptor(0, [&](att::Result<> cb_status, const auto&, auto) { |
| status = cb_status; |
| }); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(ToResult(HostError::kNotReady), status); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, ReadDescriptorNotFound) { |
| auto service = SetupServiceWithChrcs( |
| ServiceData(ServiceKind::PRIMARY, 1, 2, kTestServiceUuid1), {}); |
| |
| att::Result<> status = fit::ok(); |
| service->ReadDescriptor(0, [&](att::Result<> cb_status, const auto&, auto) { |
| status = cb_status; |
| }); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(ToResult(HostError::kNotFound), status); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, ReadDescSendsReadRequest) { |
| // TODO(armansito): Some of the service set up and |status| verification |
| // boilerplate could be reduced by factoring them out into helpers on the test |
| // harness (also see code review comment in |
| // https://fuchsia-review.googlesource.com/c/garnet/+/213794/6/drivers/bluetooth/lib/gatt/remote_service_manager_test.cc). |
| constexpr att::Handle kValueHandle1 = 3; |
| constexpr att::Handle kValueHandle2 = 5; |
| constexpr att::Handle kDescrHandle = 6; |
| |
| ServiceData data(ServiceKind::PRIMARY, 1, kDescrHandle, kTestServiceUuid1); |
| auto service = SetUpFakeService(data); |
| |
| CharacteristicData chr1( |
| Property::kRead, std::nullopt, 2, kValueHandle1, kTestUuid3); |
| CharacteristicData chr2( |
| Property::kRead, std::nullopt, 4, kValueHandle2, kTestUuid3); |
| DescriptorData desc(kDescrHandle, kTestUuid4); |
| SetupCharacteristics(service, {{chr1, chr2}}, {{desc}}); |
| |
| const StaticByteBuffer kValue('t', 'e', 's', 't'); |
| |
| fake_client()->set_read_request_callback( |
| [&](att::Handle handle, auto callback) { |
| EXPECT_EQ(kDescrHandle, handle); |
| callback(fit::ok(), kValue, /*maybe_truncated=*/false); |
| }); |
| |
| att::Result<> status = ToResult(HostError::kFailed); |
| service->ReadDescriptor( |
| DescriptorHandle(kDescrHandle), |
| [&](att::Result<> cb_status, const auto& value, auto) { |
| status = cb_status; |
| EXPECT_TRUE(ContainersEqual(kValue, value)); |
| }); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(fit::ok(), status); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, ReadLongDescWhileNotReady) { |
| auto service = SetUpFakeService( |
| ServiceData(ServiceKind::PRIMARY, 1, 2, kTestServiceUuid1)); |
| |
| att::Result<> status = fit::ok(); |
| service->ReadLongDescriptor( |
| 0, 0, 512, [&](att::Result<> cb_status, const auto&, auto) { |
| status = cb_status; |
| }); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(ToResult(HostError::kNotReady), status); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, ReadLongDescNotFound) { |
| auto service = SetupServiceWithChrcs( |
| ServiceData(ServiceKind::PRIMARY, 1, 2, kTestServiceUuid1), {}); |
| |
| att::Result<> status = fit::ok(); |
| service->ReadLongDescriptor( |
| 0, 0, 512, [&](att::Result<> cb_status, const auto&, auto) { |
| status = cb_status; |
| }); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(ToResult(HostError::kNotFound), status); |
| } |
| |
| // Tests that ReadLongDescriptor sends Read Blob requests. Other conditions |
| // around the long read procedure are already covered by the tests for |
| // ReadLongCharacteristic as the implementations are shared. |
| TEST_F(RemoteServiceManagerTest, ReadLongDescriptor) { |
| constexpr att::Handle kValueHandle = 3; |
| constexpr att::Handle kDescrHandle = 4; |
| constexpr uint16_t kOffset = 0; |
| constexpr size_t kMaxBytes = 1000; |
| constexpr int kExpectedBlobCount = 4; |
| |
| ServiceData data(ServiceKind::PRIMARY, 1, kDescrHandle, kTestServiceUuid1); |
| auto service = SetUpFakeService(data); |
| |
| CharacteristicData chr( |
| Property::kRead, std::nullopt, 2, kValueHandle, kTestUuid3); |
| DescriptorData desc(kDescrHandle, kTestUuid4); |
| SetupCharacteristics(service, {{chr}}, {{desc}}); |
| |
| // Create a buffer that will take 4 requests to read. Since the default MTU is |
| // 23: |
| // a. The size of |expected_value| is 69. |
| // b. We should read 22 + 22 + 22 + 3 bytes across 4 requests. |
| StaticByteBuffer<att::kLEMinMTU * 3> expected_value; |
| |
| // Initialize the contents. |
| for (size_t i = 0; i < expected_value.size(); ++i) { |
| expected_value[i] = i; |
| } |
| |
| int read_count = 0; |
| fake_client()->set_read_request_callback( |
| [&](att::Handle handle, auto callback) { |
| read_count++; |
| EXPECT_EQ(read_count, 1); |
| EXPECT_EQ(kDescrHandle, handle); |
| auto blob = expected_value.view(0, att::kLEMinMTU - 1); |
| callback(fit::ok(), blob, /*maybe_truncated=*/true); |
| }); |
| |
| fake_client()->set_read_blob_request_callback( |
| [&](att::Handle handle, uint16_t offset, auto callback) { |
| read_count++; |
| EXPECT_GT(read_count, 1); |
| EXPECT_EQ(kDescrHandle, handle); |
| bool maybe_truncated = true; |
| |
| // Return a blob at the given offset with at most MTU - 1 bytes. |
| auto blob = expected_value.view(offset, att::kLEMinMTU - 1); |
| if (read_count == kExpectedBlobCount) { |
| // The final blob should contain 3 bytes. |
| EXPECT_EQ(3u, blob.size()); |
| maybe_truncated = false; |
| } |
| |
| callback(fit::ok(), blob, maybe_truncated); |
| }); |
| |
| att::Result<> status = ToResult(HostError::kFailed); |
| service->ReadLongDescriptor( |
| DescriptorHandle(kDescrHandle), |
| kOffset, |
| kMaxBytes, |
| [&](att::Result<> cb_status, const auto& value, bool maybe_truncated) { |
| status = cb_status; |
| EXPECT_TRUE(ContainersEqual(expected_value, value)); |
| EXPECT_FALSE(maybe_truncated); |
| }); |
| |
| RunUntilIdle(); |
| EXPECT_EQ(fit::ok(), status); |
| EXPECT_EQ(kExpectedBlobCount, read_count); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, WriteDescWhileNotReady) { |
| auto service = SetUpFakeService( |
| ServiceData(ServiceKind::PRIMARY, 1, 2, kTestServiceUuid1)); |
| |
| att::Result<> status = fit::ok(); |
| service->WriteDescriptor( |
| 0, std::vector<uint8_t>(), [&](att::Result<> cb_status) { |
| status = cb_status; |
| }); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(ToResult(HostError::kNotReady), status); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, WriteDescNotFound) { |
| auto service = SetUpFakeService( |
| ServiceData(ServiceKind::PRIMARY, 1, 2, kTestServiceUuid1)); |
| SetupCharacteristics(service, std::vector<CharacteristicData>()); |
| |
| att::Result<> status = fit::ok(); |
| service->WriteDescriptor( |
| 0, std::vector<uint8_t>(), [&](att::Result<> cb_status) { |
| status = cb_status; |
| }); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(ToResult(HostError::kNotFound), status); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, WriteDescNotAllowed) { |
| auto service = SetUpFakeService( |
| ServiceData(ServiceKind::PRIMARY, 1, 4, kTestServiceUuid1)); |
| |
| // "CCC" characteristic cannot be written to. |
| CharacteristicData chr(0, std::nullopt, 2, 3, kTestUuid3); |
| DescriptorData desc(4, types::kClientCharacteristicConfig); |
| SetupCharacteristics(service, {{chr}}, {{desc}}); |
| |
| att::Result<> status = fit::ok(); |
| service->WriteDescriptor( |
| 4, std::vector<uint8_t>(), [&](att::Result<> cb_status) { |
| status = cb_status; |
| }); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(ToResult(HostError::kNotSupported), status); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, WriteDescSendsWriteRequest) { |
| constexpr att::Handle kValueHandle = 3; |
| constexpr att::Handle kDescrHandle = 4; |
| const std::vector<uint8_t> kValue{{'t', 'e', 's', 't'}}; |
| const att::Result<> kStatus = ToResult(HostError::kNotSupported); |
| |
| ServiceData data(ServiceKind::PRIMARY, 1, kDescrHandle, kTestServiceUuid1); |
| auto service = SetUpFakeService(data); |
| |
| CharacteristicData chr( |
| Property::kWrite, std::nullopt, 2, kValueHandle, kTestUuid3); |
| DescriptorData desc(kDescrHandle, kTestUuid4); |
| SetupCharacteristics(service, {{chr}}, {{desc}}); |
| |
| fake_client()->set_write_request_callback( |
| [&](att::Handle handle, const auto& value, auto status_callback) { |
| EXPECT_EQ(kDescrHandle, handle); |
| EXPECT_TRUE(std::equal( |
| kValue.begin(), kValue.end(), value.begin(), value.end())); |
| status_callback(kStatus); |
| }); |
| |
| att::Result<> status = fit::ok(); |
| service->WriteDescriptor(kDescrHandle, kValue, [&](att::Result<> cb_status) { |
| status = cb_status; |
| }); |
| |
| RunUntilIdle(); |
| EXPECT_EQ(kStatus, status); |
| } |
| |
| // Tests that WriteDescriptor with a long vector is prepared correctly. |
| // Other conditions around the long write procedure are already covered by the |
| // tests for WriteCharacteristic as the implementations are shared. |
| TEST_F(RemoteServiceManagerTest, WriteDescLongSuccess) { |
| constexpr att::Handle kValueHandle = 3; |
| constexpr att::Handle kDescrHandle = 4; |
| constexpr uint16_t kOffset = 0; |
| constexpr uint16_t kExpectedQueueSize = 4; |
| constexpr uint16_t kExpectedFullWriteSize = 18; |
| constexpr uint16_t kExpectedFinalWriteSize = 15; |
| |
| ServiceData data(ServiceKind::PRIMARY, 1, kDescrHandle, kTestServiceUuid1); |
| auto service = SetUpFakeService(data); |
| |
| CharacteristicData chr( |
| Property::kWrite, std::nullopt, 2, kValueHandle, kTestUuid3); |
| DescriptorData desc(kDescrHandle, kTestUuid4); |
| SetupCharacteristics(service, {{chr}}, {{desc}}); |
| |
| // Create a vector that will take 4 requests to write. Since the default MTU |
| // is 23: |
| // a. The size of |full_write_value| is 69. |
| // b. att:Handle, |kOffset|, and att::OpCode size is 5 bytes total. |
| // c. We should write 18 + 18 + 18 + 15 bytes across 4 requests. |
| // d. These bytes will be written with offset 5, from (5) to (5+69) |
| std::vector<uint8_t> full_write_value(att::kLEMinMTU * 3); |
| |
| // Initialize the contents. |
| for (size_t i = 0; i < full_write_value.size(); ++i) { |
| full_write_value[i] = i; |
| } |
| |
| uint8_t process_long_write_count = 0; |
| fake_client()->set_execute_prepare_writes_callback( |
| [&](att::PrepareWriteQueue write_queue, |
| auto /*reliable_mode*/, |
| auto callback) { |
| EXPECT_EQ(write_queue.size(), kExpectedQueueSize); |
| |
| att::QueuedWrite prepare_write; |
| for (int i = 0; i < kExpectedQueueSize; i++) { |
| auto write = std::move(write_queue.front()); |
| write_queue.pop(); |
| |
| EXPECT_EQ(write.handle(), kDescrHandle); |
| EXPECT_EQ(write.offset(), kOffset + (i * kExpectedFullWriteSize)); |
| |
| // All writes expect the final should be full, the final should be |
| // the remainder. |
| if (i < kExpectedQueueSize - 1) { |
| EXPECT_EQ(write.value().size(), kExpectedFullWriteSize); |
| } else { |
| EXPECT_EQ(write.value().size(), kExpectedFinalWriteSize); |
| } |
| } |
| |
| process_long_write_count++; |
| |
| callback(fit::ok()); |
| }); |
| |
| att::Result<> status = ToResult(HostError::kFailed); |
| service->WriteLongDescriptor( |
| DescriptorHandle(kDescrHandle), |
| kOffset, |
| full_write_value, |
| [&](att::Result<> cb_status) { status = cb_status; }); |
| |
| RunUntilIdle(); |
| EXPECT_EQ(fit::ok(), status); |
| EXPECT_EQ(1u, process_long_write_count); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, EnableNotificationsWhileNotReady) { |
| auto service = SetUpFakeService( |
| ServiceData(ServiceKind::PRIMARY, 1, 2, kTestServiceUuid1)); |
| |
| att::Result<> status = fit::ok(); |
| service->EnableNotifications( |
| kDefaultCharacteristic, |
| NopValueCallback, |
| [&](att::Result<> cb_status, IdType) { status = cb_status; }); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(ToResult(HostError::kNotReady), status); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, EnableNotificationsCharNotFound) { |
| auto service = SetupServiceWithChrcs( |
| ServiceData(ServiceKind::PRIMARY, 1, 2, kTestServiceUuid1), {}); |
| |
| att::Result<> status = fit::ok(); |
| service->EnableNotifications( |
| kDefaultCharacteristic, |
| NopValueCallback, |
| [&](att::Result<> cb_status, IdType) { status = cb_status; }); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(ToResult(HostError::kNotFound), status); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, EnableNotificationsNoProperties) { |
| auto service = SetUpFakeService( |
| ServiceData(ServiceKind::PRIMARY, 1, 4, kTestServiceUuid1)); |
| |
| // Has neither the "notify" nor "indicate" property but has a CCC descriptor. |
| CharacteristicData chr(Property::kRead, std::nullopt, 2, 3, kTestUuid3); |
| DescriptorData desc(4, types::kClientCharacteristicConfig); |
| SetupCharacteristics(service, {{chr}}, {{desc}}); |
| |
| att::Result<> status = fit::ok(); |
| service->EnableNotifications( |
| kDefaultCharacteristic, |
| NopValueCallback, |
| [&](att::Result<> cb_status, IdType) { status = cb_status; }); |
| RunUntilIdle(); |
| |
| EXPECT_EQ(ToResult(HostError::kNotSupported), status); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, EnableNotificationsSuccess) { |
| constexpr att::Handle kCCCHandle = 4; |
| auto service = SetUpFakeService( |
| ServiceData(ServiceKind::PRIMARY, 1, kCCCHandle, kTestServiceUuid1)); |
| |
| CharacteristicData chr(Property::kNotify, std::nullopt, 2, 3, kTestUuid3); |
| DescriptorData desc(kCCCHandle, types::kClientCharacteristicConfig); |
| SetupCharacteristics(service, {{chr}}, {{desc}}); |
| |
| fake_client()->set_write_request_callback( |
| [&](att::Handle handle, const auto& value, auto status_callback) { |
| EXPECT_EQ(kCCCHandle, handle); |
| EXPECT_TRUE(ContainersEqual(kCCCNotifyValue, value)); |
| status_callback(fit::ok()); |
| }); |
| |
| IdType id = kInvalidId; |
| att::Result<> status = ToResult(HostError::kFailed); |
| service->EnableNotifications(kDefaultCharacteristic, |
| NopValueCallback, |
| [&](att::Result<> cb_status, IdType cb_id) { |
| status = cb_status; |
| id = cb_id; |
| }); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(fit::ok(), status); |
| EXPECT_NE(kInvalidId, id); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, EnableIndications) { |
| constexpr att::Handle kCCCHandle = 4; |
| auto service = SetUpFakeService( |
| ServiceData(ServiceKind::PRIMARY, 1, kCCCHandle, kTestServiceUuid1)); |
| |
| CharacteristicData chr(Property::kIndicate, std::nullopt, 2, 3, kTestUuid3); |
| DescriptorData desc(kCCCHandle, types::kClientCharacteristicConfig); |
| SetupCharacteristics(service, {{chr}}, {{desc}}); |
| |
| fake_client()->set_write_request_callback( |
| [&](att::Handle handle, const auto& value, auto status_callback) { |
| EXPECT_EQ(kCCCHandle, handle); |
| EXPECT_TRUE(ContainersEqual(kCCCIndicateValue, value)); |
| status_callback(fit::ok()); |
| }); |
| |
| IdType id = kInvalidId; |
| att::Result<> status = ToResult(HostError::kFailed); |
| service->EnableNotifications(kDefaultCharacteristic, |
| NopValueCallback, |
| [&](att::Result<> cb_status, IdType cb_id) { |
| status = cb_status; |
| id = cb_id; |
| }); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(fit::ok(), status); |
| EXPECT_NE(kInvalidId, id); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, EnableNotificationsError) { |
| constexpr att::Handle kCCCHandle = 4; |
| |
| auto service = SetUpFakeService( |
| ServiceData(ServiceKind::PRIMARY, 1, 4, kTestServiceUuid1)); |
| |
| CharacteristicData chr(Property::kNotify, std::nullopt, 2, 3, kTestUuid3); |
| DescriptorData desc(kCCCHandle, types::kClientCharacteristicConfig); |
| SetupCharacteristics(service, {{chr}}, {{desc}}); |
| |
| // Should enable notifications |
| const StaticByteBuffer kExpectedValue(0x01, 0x00); |
| |
| fake_client()->set_write_request_callback( |
| [&](att::Handle handle, const auto& value, auto status_callback) { |
| EXPECT_EQ(kCCCHandle, handle); |
| EXPECT_TRUE(ContainersEqual(kExpectedValue, value)); |
| status_callback(ToResult(att::ErrorCode::kUnlikelyError)); |
| }); |
| |
| IdType id = kInvalidId; |
| att::Result<> status = fit::ok(); |
| service->EnableNotifications(kDefaultCharacteristic, |
| NopValueCallback, |
| [&](att::Result<> cb_status, IdType cb_id) { |
| status = cb_status; |
| id = cb_id; |
| }); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(ToResult(att::ErrorCode::kUnlikelyError), status); |
| EXPECT_EQ(kInvalidId, id); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, EnableNotificationsRequestMany) { |
| constexpr att::Handle kCCCHandle1 = 4; |
| constexpr att::Handle kCCCHandle2 = 7; |
| |
| auto service = SetUpFakeService( |
| ServiceData(ServiceKind::PRIMARY, 1, 7, kTestServiceUuid1)); |
| |
| // Set up two characteristics |
| CharacteristicData chr1(Property::kNotify, std::nullopt, 2, 3, kTestUuid3); |
| DescriptorData desc1(kCCCHandle1, types::kClientCharacteristicConfig); |
| |
| CharacteristicData chr2(Property::kIndicate, std::nullopt, 5, 6, kTestUuid3); |
| DescriptorData desc2(kCCCHandle2, types::kClientCharacteristicConfig); |
| |
| SetupCharacteristics(service, {{chr1, chr2}}, {{desc1, desc2}}); |
| |
| int ccc_write_count = 0; |
| att::ResultFunction<> status_callback1, status_callback2; |
| fake_client()->set_write_request_callback( |
| [&](att::Handle handle, const auto& value, auto status_cb) { |
| if (handle == kCCCHandle1) { |
| EXPECT_TRUE(ContainersEqual(kCCCNotifyValue, value)); |
| status_callback1 = std::move(status_cb); |
| } else if (handle == kCCCHandle2) { |
| EXPECT_TRUE(ContainersEqual(kCCCIndicateValue, value)); |
| status_callback2 = std::move(status_cb); |
| } else { |
| ADD_FAILURE() << "Unexpected handle: " << handle; |
| } |
| ccc_write_count++; |
| }); |
| |
| size_t cb_count = 0u; |
| |
| service->EnableNotifications(kDefaultCharacteristic, |
| NopValueCallback, |
| [&](att::Result<> status, IdType id) { |
| cb_count++; |
| EXPECT_EQ(1u, id); |
| EXPECT_EQ(fit::ok(), status); |
| }); |
| service->EnableNotifications(kDefaultCharacteristic, |
| NopValueCallback, |
| [&](att::Result<> status, IdType id) { |
| cb_count++; |
| EXPECT_EQ(2u, id); |
| EXPECT_EQ(fit::ok(), status); |
| }); |
| service->EnableNotifications(kSecondCharacteristic, |
| NopValueCallback, |
| [&](att::Result<> status, IdType id) { |
| cb_count++; |
| EXPECT_EQ(1u, id); |
| EXPECT_EQ(fit::ok(), status); |
| }); |
| service->EnableNotifications(kSecondCharacteristic, |
| NopValueCallback, |
| [&](att::Result<> status, IdType id) { |
| cb_count++; |
| EXPECT_EQ(2u, id); |
| EXPECT_EQ(fit::ok(), status); |
| }); |
| service->EnableNotifications(kSecondCharacteristic, |
| NopValueCallback, |
| [&](att::Result<> status, IdType id) { |
| cb_count++; |
| EXPECT_EQ(3u, id); |
| EXPECT_EQ(fit::ok(), status); |
| }); |
| |
| RunUntilIdle(); |
| |
| // ATT write requests should be sent but none of the notification requests |
| // should be resolved. |
| EXPECT_EQ(2, ccc_write_count); |
| EXPECT_EQ(0u, cb_count); |
| |
| // An ATT response should resolve all pending requests for the right |
| // characteristic. |
| status_callback1(fit::ok()); |
| EXPECT_EQ(2u, cb_count); |
| status_callback2(fit::ok()); |
| EXPECT_EQ(5u, cb_count); |
| |
| // An extra request should succeed without sending any PDUs. |
| service->EnableNotifications(kDefaultCharacteristic, |
| NopValueCallback, |
| [&](att::Result<> status, IdType) { |
| cb_count++; |
| EXPECT_EQ(fit::ok(), status); |
| }); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(2, ccc_write_count); |
| EXPECT_EQ(6u, cb_count); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, EnableNotificationsRequestManyError) { |
| constexpr att::Handle kCCCHandle = 4; |
| |
| auto service = SetUpFakeService( |
| ServiceData(ServiceKind::PRIMARY, 1, 4, kTestServiceUuid1)); |
| |
| // Set up two characteristics |
| CharacteristicData chr(Property::kNotify, std::nullopt, 2, 3, kTestUuid3); |
| DescriptorData desc(kCCCHandle, types::kClientCharacteristicConfig); |
| |
| SetupCharacteristics(service, {{chr}}, {{desc}}); |
| |
| int ccc_write_count = 0; |
| att::ResultFunction<> status_callback; |
| fake_client()->set_write_request_callback( |
| [&](att::Handle handle, const auto& value, auto status_cb) { |
| EXPECT_EQ(kCCCHandle, handle); |
| EXPECT_TRUE(ContainersEqual(kCCCNotifyValue, value)); |
| |
| ccc_write_count++; |
| status_callback = std::move(status_cb); |
| }); |
| |
| int cb_count = 0; |
| att::Result<> status = fit::ok(); |
| auto cb = [&](att::Result<> cb_status, IdType id) { |
| status = cb_status; |
| cb_count++; |
| }; |
| |
| service->EnableNotifications( |
| kDefaultCharacteristic, NopValueCallback, std::move(cb)); |
| service->EnableNotifications( |
| kDefaultCharacteristic, NopValueCallback, std::move(cb)); |
| service->EnableNotifications( |
| kDefaultCharacteristic, NopValueCallback, std::move(cb)); |
| |
| RunUntilIdle(); |
| |
| // Requests should be buffered and only one ATT request should have been sent |
| // out. |
| EXPECT_EQ(1, ccc_write_count); |
| EXPECT_EQ(0, cb_count); |
| |
| status_callback(ToResult(HostError::kNotSupported)); |
| EXPECT_EQ(3, cb_count); |
| EXPECT_EQ(ToResult(HostError::kNotSupported), status); |
| |
| // A new request should write to the descriptor again. |
| service->EnableNotifications( |
| kDefaultCharacteristic, NopValueCallback, std::move(cb)); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(2, ccc_write_count); |
| EXPECT_EQ(3, cb_count); |
| |
| status_callback(fit::ok()); |
| EXPECT_EQ(2, ccc_write_count); |
| EXPECT_EQ(4, cb_count); |
| EXPECT_EQ(fit::ok(), status); |
| } |
| |
| // Enabling notifications should succeed without a descriptor write. |
| TEST_F(RemoteServiceManagerTest, EnableNotificationsWithoutCCC) { |
| // Has the "notify" property but no CCC descriptor. |
| CharacteristicData chr(Property::kNotify, std::nullopt, 2, 3, kTestUuid3); |
| auto service = SetupServiceWithChrcs( |
| ServiceData(ServiceKind::PRIMARY, 1, 3, kTestServiceUuid1), {chr}); |
| |
| bool write_requested = false; |
| fake_client()->set_write_request_callback( |
| [&](auto, auto&, auto) { write_requested = true; }); |
| |
| int notify_count = 0; |
| auto notify_cb = [&](const auto& value, bool /*maybe_truncated*/) { |
| notify_count++; |
| }; |
| |
| att::Result<> status = fit::ok(); |
| IdType id; |
| service->EnableNotifications(kDefaultCharacteristic, |
| std::move(notify_cb), |
| [&](att::Result<> _status, IdType _id) { |
| status = _status; |
| id = _id; |
| }); |
| RunUntilIdle(); |
| |
| EXPECT_EQ(fit::ok(), status); |
| EXPECT_FALSE(write_requested); |
| |
| fake_client()->SendNotification(/*indicate=*/false, |
| 3, |
| StaticByteBuffer('y', 'e'), |
| /*maybe_truncated=*/false); |
| EXPECT_EQ(1, notify_count); |
| |
| // Disabling notifications should not result in a write request. |
| service->DisableNotifications( |
| kDefaultCharacteristic, id, [&](auto _status) { status = _status; }); |
| RunUntilIdle(); |
| EXPECT_EQ(fit::ok(), status); |
| EXPECT_FALSE(write_requested); |
| |
| // The handler should no longer receive notifications. |
| fake_client()->SendNotification(/*indicate=*/false, |
| 3, |
| StaticByteBuffer('o', 'y', 'e'), |
| /*maybe_truncated=*/false); |
| EXPECT_EQ(1, notify_count); |
| } |
| |
| // Notifications received when the remote service database is empty should be |
| // dropped and not cause a crash. |
| TEST_F(RemoteServiceManagerTest, NotificationWithoutServices) { |
| for (att::Handle i = 0; i < 10; ++i) { |
| fake_client()->SendNotification( |
| /*indicate=*/false, |
| i, |
| StaticByteBuffer('n', 'o', 't', 'i', 'f', 'y'), |
| /*maybe_truncated=*/false); |
| } |
| RunUntilIdle(); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, NotificationCallback) { |
| auto service = SetUpFakeService( |
| ServiceData(ServiceKind::PRIMARY, 1, 7, kTestServiceUuid1)); |
| |
| // Set up two characteristics |
| CharacteristicData chr1(Property::kNotify, std::nullopt, 2, 3, kTestUuid3); |
| DescriptorData desc1(4, types::kClientCharacteristicConfig); |
| |
| CharacteristicData chr2(Property::kIndicate, std::nullopt, 5, 6, kTestUuid3); |
| DescriptorData desc2(7, types::kClientCharacteristicConfig); |
| |
| SetupCharacteristics(service, {{chr1, chr2}}, {{desc1, desc2}}); |
| |
| fake_client()->set_write_request_callback( |
| [&](att::Handle, const auto&, auto status_callback) { |
| status_callback(fit::ok()); |
| }); |
| |
| IdType handler_id = kInvalidId; |
| att::Result<> status = ToResult(HostError::kFailed); |
| |
| int chr1_count = 0; |
| auto chr1_cb = [&](const ByteBuffer& value, bool maybe_truncated) { |
| chr1_count++; |
| EXPECT_EQ("notify", value.AsString()); |
| EXPECT_FALSE(maybe_truncated); |
| }; |
| |
| int chr2_count = 0; |
| auto chr2_cb = [&](const ByteBuffer& value, bool maybe_truncated) { |
| chr2_count++; |
| EXPECT_EQ("indicate", value.AsString()); |
| EXPECT_TRUE(maybe_truncated); |
| }; |
| |
| // Notify both characteristics which should get dropped. |
| fake_client()->SendNotification( |
| /*indicate=*/false, |
| 3, |
| StaticByteBuffer('n', 'o', 't', 'i', 'f', 'y'), |
| /*maybe_truncated=*/false); |
| fake_client()->SendNotification( |
| /*indicate=*/true, |
| 6, |
| StaticByteBuffer('i', 'n', 'd', 'i', 'c', 'a', 't', 'e'), |
| /*maybe_truncated=*/true); |
| |
| EnableNotifications(service, |
| kDefaultCharacteristic, |
| &status, |
| &handler_id, |
| std::move(chr1_cb)); |
| ASSERT_EQ(fit::ok(), status); |
| EnableNotifications( |
| service, kSecondCharacteristic, &status, &handler_id, std::move(chr2_cb)); |
| ASSERT_EQ(fit::ok(), status); |
| |
| // Notify characteristic 1. |
| fake_client()->SendNotification( |
| /*indicate=*/false, |
| 3, |
| StaticByteBuffer('n', 'o', 't', 'i', 'f', 'y'), |
| /*maybe_truncated=*/false); |
| EXPECT_EQ(1, chr1_count); |
| EXPECT_EQ(0, chr2_count); |
| |
| // Notify characteristic 2. |
| fake_client()->SendNotification( |
| /*indicate=*/true, |
| 6, |
| StaticByteBuffer('i', 'n', 'd', 'i', 'c', 'a', 't', 'e'), |
| /*maybe_truncated=*/true); |
| EXPECT_EQ(1, chr1_count); |
| EXPECT_EQ(1, chr2_count); |
| |
| // Disable notifications from characteristic 1. |
| status = ToResult(HostError::kFailed); |
| service->DisableNotifications( |
| kDefaultCharacteristic, handler_id, [&](att::Result<> cb_status) { |
| status = cb_status; |
| }); |
| |
| RunUntilIdle(); |
| EXPECT_EQ(fit::ok(), status); |
| |
| // Notifications for characteristic 1 should get dropped. |
| fake_client()->SendNotification( |
| /*indicate=*/false, |
| 3, |
| StaticByteBuffer('n', 'o', 't', 'i', 'f', 'y'), |
| /*maybe_truncated=*/false); |
| fake_client()->SendNotification( |
| /*indicate=*/true, |
| 6, |
| StaticByteBuffer('i', 'n', 'd', 'i', 'c', 'a', 't', 'e'), |
| /*maybe_truncated=*/true); |
| EXPECT_EQ(1, chr1_count); |
| EXPECT_EQ(2, chr2_count); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, DisableNotificationsWhileNotReady) { |
| ServiceData data(ServiceKind::PRIMARY, 1, 4, kTestServiceUuid1); |
| auto service = SetUpFakeService(data); |
| |
| att::Result<> status = fit::ok(); |
| service->DisableNotifications( |
| kDefaultCharacteristic, 1, [&](att::Result<> cb_status) { |
| status = cb_status; |
| }); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(ToResult(HostError::kNotReady), status); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, DisableNotificationsCharNotFound) { |
| auto service = SetupNotifiableService(); |
| |
| IdType id = kInvalidId; |
| att::Result<> status = ToResult(HostError::kFailed); |
| EnableNotifications(service, kDefaultCharacteristic, &status, &id); |
| |
| // "1" is an invalid characteristic ID. |
| service->DisableNotifications( |
| kInvalidCharacteristic, id, [&](att::Result<> cb_status) { |
| status = cb_status; |
| }); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(ToResult(HostError::kNotFound), status); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, DisableNotificationsIdNotFound) { |
| auto service = SetupNotifiableService(); |
| |
| IdType id = kInvalidId; |
| att::Result<> status = ToResult(HostError::kFailed); |
| EnableNotifications(service, kDefaultCharacteristic, &status, &id); |
| |
| // Valid characteristic ID but invalid notification handler ID. |
| service->DisableNotifications( |
| kDefaultCharacteristic, id + 1, [&](att::Result<> cb_status) { |
| status = cb_status; |
| }); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(ToResult(HostError::kNotFound), status); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, DisableNotificationsSingleHandler) { |
| constexpr att::Handle kCCCHandle = 4; |
| auto service = SetupNotifiableService(); |
| |
| IdType id = kInvalidId; |
| att::Result<> status = ToResult(HostError::kFailed); |
| EnableNotifications(service, kDefaultCharacteristic, &status, &id); |
| |
| // Should disable notifications |
| const StaticByteBuffer kExpectedValue(0x00, 0x00); |
| |
| int ccc_write_count = 0; |
| fake_client()->set_write_request_callback( |
| [&](att::Handle handle, const auto& value, auto status_callback) { |
| EXPECT_EQ(kCCCHandle, handle); |
| EXPECT_TRUE(ContainersEqual(kExpectedValue, value)); |
| ccc_write_count++; |
| status_callback(fit::ok()); |
| }); |
| |
| status = ToResult(HostError::kFailed); |
| service->DisableNotifications( |
| kDefaultCharacteristic, id, [&](att::Result<> cb_status) { |
| status = cb_status; |
| }); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(fit::ok(), status); |
| EXPECT_EQ(1, ccc_write_count); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, DisableNotificationsOnDestruction) { |
| constexpr att::Handle kCCCHandle = 4; |
| auto service = SetupNotifiableService(); |
| |
| IdType id = kInvalidId; |
| att::Result<> status = ToResult(HostError::kFailed); |
| EnableNotifications(service, kDefaultCharacteristic, &status, &id); |
| ASSERT_EQ(fit::ok(), status); |
| |
| // Should disable notifications |
| const StaticByteBuffer kExpectedValue(0x00, 0x00); |
| |
| int ccc_write_count = 0; |
| fake_client()->set_write_request_callback( |
| [&](att::Handle handle, const auto& value, auto status_callback) { |
| EXPECT_EQ(kCCCHandle, handle); |
| EXPECT_TRUE(ContainersEqual(kExpectedValue, value)); |
| ccc_write_count++; |
| status_callback(fit::ok()); |
| }); |
| |
| // Destroying the service manager (which destroys the service characteristics) |
| // should clear the CCC. |
| DestroyServiceManager(); |
| RunUntilIdle(); |
| EXPECT_EQ(1, ccc_write_count); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, DisableNotificationsManyHandlers) { |
| auto service = SetupNotifiableService(); |
| |
| IdType id = kInvalidId; |
| std::vector<IdType> handler_ids; |
| |
| for (int i = 0; i < 2; i++) { |
| att::Result<> status = ToResult(HostError::kFailed); |
| EnableNotifications(service, kDefaultCharacteristic, &status, &id); |
| ASSERT_EQ(fit::ok(), status); |
| handler_ids.push_back(id); |
| } |
| |
| int ccc_write_count = 0; |
| fake_client()->set_write_request_callback( |
| [&](att::Handle handle, const auto& value, auto status_callback) { |
| ccc_write_count++; |
| status_callback(fit::ok()); |
| }); |
| |
| // Disabling should succeed without an ATT transaction. |
| att::Result<> status = ToResult(HostError::kFailed); |
| service->DisableNotifications( |
| kDefaultCharacteristic, handler_ids.back(), [&](att::Result<> cb_status) { |
| status = cb_status; |
| }); |
| handler_ids.pop_back(); |
| RunUntilIdle(); |
| EXPECT_EQ(fit::ok(), status); |
| EXPECT_EQ(0, ccc_write_count); |
| |
| // Enabling should succeed without an ATT transaction. |
| status = ToResult(HostError::kFailed); |
| EnableNotifications(service, kDefaultCharacteristic, &status, &id); |
| EXPECT_EQ(fit::ok(), status); |
| EXPECT_EQ(0, ccc_write_count); |
| handler_ids.push_back(id); |
| |
| // Disabling all should send out an ATT transaction. |
| while (!handler_ids.empty()) { |
| att::Result<> status = ToResult(HostError::kFailed); |
| service->DisableNotifications( |
| kDefaultCharacteristic, |
| handler_ids.back(), |
| [&](att::Result<> cb_status) { status = cb_status; }); |
| handler_ids.pop_back(); |
| RunUntilIdle(); |
| EXPECT_EQ(fit::ok(), status); |
| } |
| |
| EXPECT_EQ(1, ccc_write_count); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, |
| ReadByTypeErrorOnLastHandleDoesNotOverflowHandle) { |
| constexpr att::Handle kStartHandle = 0xFFFE; |
| constexpr att::Handle kEndHandle = 0xFFFF; |
| auto service = SetUpFakeService(ServiceData( |
| ServiceKind::PRIMARY, kStartHandle, kEndHandle, kTestServiceUuid1)); |
| constexpr UUID kCharUuid(uint16_t{0xfefe}); |
| |
| size_t read_count = 0; |
| fake_client()->set_read_by_type_request_callback( |
| [&](const UUID& type, att::Handle start, att::Handle end, auto callback) { |
| ASSERT_EQ(0u, read_count++); |
| EXPECT_EQ(kStartHandle, start); |
| callback(fit::error(Client::ReadByTypeError{ |
| att::Error(att::ErrorCode::kReadNotPermitted), kEndHandle})); |
| }); |
| |
| std::optional<att::Result<>> status; |
| std::vector<RemoteService::ReadByTypeResult> results; |
| service->ReadByType(kCharUuid, [&](att::Result<> cb_status, auto cb_results) { |
| status = cb_status; |
| results = std::move(cb_results); |
| }); |
| RunUntilIdle(); |
| ASSERT_TRUE(status.has_value()); |
| EXPECT_EQ(fit::ok(), *status); |
| ASSERT_EQ(1u, results.size()); |
| EXPECT_EQ(CharacteristicHandle(kEndHandle), results[0].handle); |
| EXPECT_EQ(att::ErrorCode::kReadNotPermitted, results[0].result.error_value()); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, |
| ReadByTypeResultOnLastHandleDoesNotOverflowHandle) { |
| constexpr att::Handle kStartHandle = 0xFFFE; |
| constexpr att::Handle kEndHandle = 0xFFFF; |
| auto service = SetUpFakeService(ServiceData( |
| ServiceKind::PRIMARY, kStartHandle, kEndHandle, kTestServiceUuid1)); |
| |
| constexpr UUID kCharUuid(uint16_t{0xfefe}); |
| |
| constexpr att::Handle kHandle = kEndHandle; |
| const auto kValue = StaticByteBuffer(0x00, 0x01, 0x02); |
| const std::vector<Client::ReadByTypeValue> kValues = { |
| {kHandle, kValue.view(), /*maybe_truncated=*/false}}; |
| |
| size_t read_count = 0; |
| fake_client()->set_read_by_type_request_callback( |
| [&](const UUID& type, att::Handle start, att::Handle end, auto callback) { |
| ASSERT_EQ(0u, read_count++); |
| EXPECT_EQ(kStartHandle, start); |
| callback(fit::ok(kValues)); |
| }); |
| |
| std::optional<att::Result<>> status; |
| service->ReadByType(kCharUuid, [&](att::Result<> cb_status, auto values) { |
| status = cb_status; |
| ASSERT_EQ(1u, values.size()); |
| EXPECT_EQ(CharacteristicHandle(kHandle), values[0].handle); |
| ASSERT_EQ(fit::ok(), values[0].result); |
| EXPECT_TRUE(ContainersEqual(kValue, *values[0].result.value())); |
| }); |
| |
| RunUntilIdle(); |
| ASSERT_TRUE(status.has_value()); |
| EXPECT_EQ(fit::ok(), *status); |
| } |
| |
| class RemoteServiceManagerServiceChangedTest : public RemoteServiceManagerTest { |
| public: |
| RemoteServiceManagerServiceChangedTest() = default; |
| ~RemoteServiceManagerServiceChangedTest() override = default; |
| |
| protected: |
| struct ServiceWatcherData { |
| std::vector<att::Handle> removed; |
| ServiceList added; |
| ServiceList modified; |
| }; |
| |
| void SetUp() override { |
| RemoteServiceManagerTest::SetUp(); |
| fake_client()->set_services({gatt_service()}); |
| fake_client()->set_characteristics({service_changed_characteristic()}); |
| fake_client()->set_descriptors({ccc_descriptor()}); |
| |
| mgr()->set_service_watcher( |
| [this](auto removed, ServiceList added, ServiceList modified) { |
| svc_watcher_data_.push_back({removed, added, modified}); |
| }); |
| |
| // Expect a Service Changed Client Characteristic Config descriptor write |
| // that enables indications. |
| fake_client()->set_write_request_callback( |
| [this](att::Handle handle, const auto& value, auto status_callback) { |
| write_request_count_++; |
| EXPECT_EQ(ccc_descriptor_handle_, handle); |
| EXPECT_TRUE(ContainersEqual(kCCCIndicateValue, value)); |
| status_callback(fit::ok()); |
| }); |
| |
| att::Result<> status = ToResult(HostError::kFailed); |
| mgr()->Initialize([&status](att::Result<> val) { status = val; }, |
| NopMtuCallback); |
| RunUntilIdle(); |
| EXPECT_EQ(fit::ok(), status); |
| EXPECT_EQ(write_request_count_, 1); |
| ASSERT_EQ(1u, svc_watcher_data_.size()); |
| ASSERT_EQ(1u, svc_watcher_data_[0].added.size()); |
| EXPECT_EQ(gatt_svc_start_handle_, svc_watcher_data_[0].added[0]->handle()); |
| EXPECT_EQ(types::kGenericAttributeService, |
| svc_watcher_data_[0].added[0]->uuid()); |
| // Clear data so that tests start with index 0 |
| svc_watcher_data_.clear(); |
| } |
| |
| void TearDown() override { RemoteServiceManagerTest::TearDown(); } |
| |
| ServiceData gatt_service() const { |
| return ServiceData(ServiceKind::PRIMARY, |
| gatt_svc_start_handle_, |
| gatt_svc_end_handle_, |
| types::kGenericAttributeService); |
| } |
| |
| CharacteristicData service_changed_characteristic() const { |
| return CharacteristicData(Property::kIndicate, |
| std::nullopt, |
| svc_changed_char_handle_, |
| svc_changed_char_value_handle_, |
| types::kServiceChangedCharacteristic); |
| } |
| |
| DescriptorData ccc_descriptor() const { |
| return DescriptorData(ccc_descriptor_handle_, |
| types::kClientCharacteristicConfig); |
| } |
| |
| const std::vector<ServiceWatcherData>& svc_watcher_data() { |
| return svc_watcher_data_; |
| } |
| |
| int service_changed_ccc_write_count() const { return write_request_count_; } |
| |
| private: |
| std::vector<ServiceWatcherData> svc_watcher_data_; |
| int write_request_count_ = 0; |
| const att::Handle gatt_svc_start_handle_ = 1; |
| const att::Handle svc_changed_char_handle_ = 2; |
| const att::Handle svc_changed_char_value_handle_ = 3; |
| const att::Handle ccc_descriptor_handle_ = 4; |
| const att::Handle gatt_svc_end_handle_ = 4; |
| }; |
| |
| TEST_F(RemoteServiceManagerServiceChangedTest, |
| ServiceChangedNotificationWrongSizeBuffer) { |
| const att::Handle kSvc1StartHandle(5); |
| const att::Handle kSvc1EndHandle(kSvc1StartHandle); |
| ServiceData svc1(ServiceKind::PRIMARY, |
| kSvc1StartHandle, |
| kSvc1EndHandle, |
| kTestServiceUuid1); |
| fake_client()->set_services({gatt_service(), svc1}); |
| |
| // Send a too small notification. |
| auto svc_changed_range_buffer_too_small = StaticByteBuffer(0x01); |
| fake_client()->SendNotification(/*indicate=*/true, |
| service_changed_characteristic().value_handle, |
| svc_changed_range_buffer_too_small, |
| /*maybe_truncated=*/false); |
| RunUntilIdle(); |
| // The notification should have been safely ignored. |
| ASSERT_EQ(0u, svc_watcher_data().size()); |
| |
| // Send a too large notification. |
| StaticByteBuffer<sizeof(ServiceChangedCharacteristicValue) + 1> |
| svc_changed_range_buffer_too_large = |
| StaticByteBuffer(0x01, 0x02, 0x03, 0x04, 0x05); |
| fake_client()->SendNotification(/*indicate=*/true, |
| service_changed_characteristic().value_handle, |
| svc_changed_range_buffer_too_large, |
| /*maybe_truncated=*/false); |
| RunUntilIdle(); |
| // The notification should have been safely ignored. |
| ASSERT_EQ(0u, svc_watcher_data().size()); |
| } |
| |
| TEST_F(RemoteServiceManagerServiceChangedTest, |
| ServiceChangedNotificationRangeStartGreaterThanRangeEnd) { |
| const att::Handle kSvc1StartHandle(6); |
| const att::Handle kSvc1EndHandle(7); |
| ServiceData svc1(ServiceKind::PRIMARY, |
| kSvc1StartHandle, |
| kSvc1EndHandle, |
| kTestServiceUuid1); |
| fake_client()->set_services({gatt_service(), svc1}); |
| |
| // Send notification with start/end handles swapped. |
| auto svc_changed_range_buffer = StaticByteBuffer( |
| LowerBits(kSvc1EndHandle), |
| UpperBits(kSvc1EndHandle), // start handle of affected range |
| LowerBits(kSvc1StartHandle), |
| UpperBits(kSvc1StartHandle) // end handle of affected range |
| ); |
| fake_client()->SendNotification(/*indicate=*/true, |
| service_changed_characteristic().value_handle, |
| svc_changed_range_buffer, |
| /*maybe_truncated=*/false); |
| RunUntilIdle(); |
| // The notification should have been safely ignored. |
| ASSERT_EQ(0u, svc_watcher_data().size()); |
| } |
| |
| TEST_F(RemoteServiceManagerServiceChangedTest, AddModifyAndRemoveService) { |
| // Add a test service to ensure that service discovery occurs after the |
| // Service Changed characteristic is configured. The test service has a |
| // characteristic that supports indications in order to test that |
| // notifications aren't disabled when the service is modified or removed. |
| const att::Handle kSvc1StartHandle(5); |
| const att::Handle kSvc1ChrcHandle(6); |
| const att::Handle kSvc1ChrcValueHandle(7); |
| const att::Handle kSvc1CCCHandle(8); |
| const UUID kSvc1ChrcUuid(kTestUuid3); |
| const att::Handle kSvc1EndHandle(kSvc1CCCHandle); |
| |
| ServiceData svc1(ServiceKind::PRIMARY, |
| kSvc1StartHandle, |
| kSvc1EndHandle, |
| kTestServiceUuid1); |
| CharacteristicData svc1_characteristic(Property::kIndicate, |
| std::nullopt, |
| kSvc1ChrcHandle, |
| kSvc1ChrcValueHandle, |
| kSvc1ChrcUuid); |
| DescriptorData svc1_descriptor(kSvc1CCCHandle, |
| types::kClientCharacteristicConfig); |
| fake_client()->set_services({gatt_service(), svc1}); |
| fake_client()->set_characteristics( |
| {service_changed_characteristic(), svc1_characteristic}); |
| fake_client()->set_descriptors({ccc_descriptor(), svc1_descriptor}); |
| |
| // Send a notification that svc1 has been added. |
| auto svc_changed_range_buffer = StaticByteBuffer( |
| LowerBits(kSvc1StartHandle), |
| UpperBits(kSvc1StartHandle), // start handle of affected range |
| LowerBits(kSvc1EndHandle), |
| UpperBits(kSvc1EndHandle) // end handle of affected range |
| ); |
| fake_client()->SendNotification(/*indicate=*/true, |
| service_changed_characteristic().value_handle, |
| svc_changed_range_buffer, |
| /*maybe_truncated=*/false); |
| RunUntilIdle(); |
| ASSERT_EQ(1u, svc_watcher_data().size()); |
| ASSERT_EQ(1u, svc_watcher_data()[0].added.size()); |
| EXPECT_EQ(0u, svc_watcher_data()[0].removed.size()); |
| EXPECT_EQ(0u, svc_watcher_data()[0].modified.size()); |
| EXPECT_EQ(kSvc1StartHandle, svc_watcher_data()[0].added[0]->handle()); |
| bool original_service_removed = false; |
| svc_watcher_data()[0].added[0]->AddRemovedHandler( |
| [&]() { original_service_removed = true; }); |
| |
| // Discover the characteristic with the CCC descriptor before enabling |
| // characteristic value notifications. |
| svc_watcher_data()[0].added[0]->DiscoverCharacteristics( |
| [&](att::Result<> status, const CharacteristicMap& characteristics) { |
| EXPECT_EQ(fit::ok(), status); |
| EXPECT_EQ(characteristics.size(), 1u); |
| }); |
| RunUntilIdle(); |
| |
| // Expect writes to the service's CCC descriptor when notifications are |
| // enabled. |
| int svc1_ccc_write_request_count = 0; |
| fake_client()->set_write_request_callback( |
| [&](att::Handle handle, const auto& value, auto status_callback) { |
| svc1_ccc_write_request_count++; |
| EXPECT_EQ(kSvc1CCCHandle, handle); |
| EXPECT_TRUE(ContainersEqual(kCCCIndicateValue, value)); |
| status_callback(fit::ok()); |
| }); |
| |
| std::optional<att::Result<>> original_notification_status; |
| svc_watcher_data()[0].added[0]->EnableNotifications( |
| bt::gatt::CharacteristicHandle(kSvc1ChrcValueHandle), |
| NopValueCallback, |
| [&](att::Result<> cb_status, IdType cb_id) { |
| original_notification_status = cb_status; |
| }); |
| RunUntilIdle(); |
| ASSERT_TRUE(original_notification_status); |
| EXPECT_EQ(fit::ok(), *original_notification_status); |
| EXPECT_EQ(svc1_ccc_write_request_count, 1); |
| |
| // Send a notification that svc1 has been modified. Service Changed |
| // notifications guarantee that all services within their range have been |
| // modified if they are still present after a fresh service discovery, so we |
| // can just send the same range again. (Core Spec v5.3, Vol 3, Part G, |
| // Sec 7.1) |
| fake_client()->SendNotification(/*indicate=*/true, |
| service_changed_characteristic().value_handle, |
| svc_changed_range_buffer, |
| /*maybe_truncated=*/false); |
| RunUntilIdle(); |
| EXPECT_TRUE(original_service_removed); |
| // CCC should not be written to when the service is modified. |
| EXPECT_EQ(svc1_ccc_write_request_count, 1); |
| ASSERT_EQ(2u, svc_watcher_data().size()); |
| EXPECT_EQ(0u, svc_watcher_data()[1].added.size()); |
| EXPECT_EQ(0u, svc_watcher_data()[1].removed.size()); |
| ASSERT_EQ(1u, svc_watcher_data()[1].modified.size()); |
| EXPECT_EQ(kSvc1StartHandle, svc_watcher_data()[1].modified[0]->handle()); |
| bool modified_service_removed = false; |
| svc_watcher_data()[1].modified[0]->AddRemovedHandler( |
| [&]() { modified_service_removed = true; }); |
| |
| svc_watcher_data()[1].modified[0]->DiscoverCharacteristics( |
| [&](att::Result<> status, const CharacteristicMap& characteristics) { |
| EXPECT_EQ(fit::ok(), status); |
| EXPECT_EQ(characteristics.size(), 1u); |
| }); |
| RunUntilIdle(); |
| |
| std::optional<att::Result<>> modified_notification_status; |
| svc_watcher_data()[1].modified[0]->EnableNotifications( |
| bt::gatt::CharacteristicHandle(kSvc1ChrcValueHandle), |
| NopValueCallback, |
| [&](att::Result<> cb_status, IdType cb_id) { |
| modified_notification_status = cb_status; |
| }); |
| RunUntilIdle(); |
| ASSERT_TRUE(modified_notification_status); |
| EXPECT_EQ(fit::ok(), *modified_notification_status); |
| EXPECT_EQ(svc1_ccc_write_request_count, 2); |
| |
| // Remove svc1. |
| fake_client()->set_services({gatt_service()}); |
| |
| // Send a notification that svc1 has been removed. |
| fake_client()->SendNotification(/*indicate=*/true, |
| service_changed_characteristic().value_handle, |
| svc_changed_range_buffer, |
| /*maybe_truncated=*/false); |
| RunUntilIdle(); |
| EXPECT_TRUE(modified_service_removed); |
| // CCC should not be written to when the service is removed. |
| EXPECT_EQ(svc1_ccc_write_request_count, 2); |
| ASSERT_EQ(3u, svc_watcher_data().size()); |
| EXPECT_EQ(0u, svc_watcher_data()[2].added.size()); |
| ASSERT_EQ(1u, svc_watcher_data()[2].removed.size()); |
| EXPECT_EQ(0u, svc_watcher_data()[2].modified.size()); |
| EXPECT_EQ(kSvc1StartHandle, svc_watcher_data()[2].removed[0]); |
| EXPECT_EQ(svc1_ccc_write_request_count, 2); |
| } |
| |
| // A Service Changed notification received during initialization (service |
| // discovery) should be queued and processed after service discovery completes |
| // (as the last step of initialization). The service watcher should only be |
| // called once. |
| TEST_F(RemoteServiceManagerTest, ServiceChangedDuringInitialization) { |
| const att::Handle kGattSvcStartHandle(1); |
| const att::Handle kSvcChangedChrcHandle(2); |
| const att::Handle kSvcChangedChrcValueHandle(3); |
| const att::Handle kCCCDescriptorHandle(4); |
| const att::Handle kGattSvcEndHandle(kCCCDescriptorHandle); |
| const att::Handle kSvc1StartHandle(5); |
| const att::Handle kSvc1EndHandle(kSvc1StartHandle); |
| |
| ServiceData gatt_svc(ServiceKind::PRIMARY, |
| kGattSvcStartHandle, |
| kGattSvcEndHandle, |
| types::kGenericAttributeService); |
| CharacteristicData service_changed_chrc(Property::kIndicate, |
| std::nullopt, |
| kSvcChangedChrcHandle, |
| kSvcChangedChrcValueHandle, |
| types::kServiceChangedCharacteristic); |
| DescriptorData ccc_descriptor(kCCCDescriptorHandle, |
| types::kClientCharacteristicConfig); |
| ServiceData svc1(ServiceKind::PRIMARY, |
| kSvc1StartHandle, |
| kSvc1EndHandle, |
| kTestServiceUuid1); |
| fake_client()->set_services({gatt_svc, svc1}); |
| fake_client()->set_characteristics({service_changed_chrc}); |
| fake_client()->set_descriptors({ccc_descriptor}); |
| |
| int svc_watcher_count = 0; |
| mgr()->set_service_watcher([&](std::vector<att::Handle> removed, |
| ServiceList added, |
| ServiceList modified) { |
| EXPECT_EQ(0u, removed.size()); |
| EXPECT_EQ(2u, added.size()); |
| EXPECT_EQ(0u, modified.size()); |
| svc_watcher_count++; |
| }); |
| |
| // Send a notification during primary service discovery (i.e. the second |
| // discovery, as the first is for discovering the GATT Service). |
| int discover_services_count = 0; |
| fake_client()->set_discover_services_callback([&](ServiceKind /*kind*/) { |
| if (discover_services_count == 1) { |
| auto svc_changed_range_buffer = StaticByteBuffer( |
| LowerBits(kSvc1StartHandle), |
| UpperBits(kSvc1StartHandle), // start handle of affected range |
| LowerBits(kSvc1EndHandle), |
| UpperBits(kSvc1EndHandle) // end handle of affected range |
| ); |
| fake_client()->SendNotification(/*indicate=*/true, |
| kSvcChangedChrcValueHandle, |
| svc_changed_range_buffer, |
| /*maybe_truncated=*/false); |
| } |
| discover_services_count++; |
| return fit::ok(); |
| }); |
| |
| // Expect a Service Changed Client Characteristic Config descriptor write that |
| // enables indications. |
| int write_request_count = 0; |
| fake_client()->set_write_request_callback( |
| [&](att::Handle handle, const auto& value, auto status_callback) { |
| write_request_count++; |
| EXPECT_EQ(kCCCDescriptorHandle, handle); |
| EXPECT_TRUE(ContainersEqual(kCCCIndicateValue, value)); |
| status_callback(fit::ok()); |
| }); |
| |
| att::Result<> status = ToResult(HostError::kFailed); |
| mgr()->Initialize( |
| [&](att::Result<> val) { |
| status = val; |
| EXPECT_EQ(1, svc_watcher_count); |
| }, |
| NopMtuCallback); |
| RunUntilIdle(); |
| EXPECT_EQ(fit::ok(), status); |
| EXPECT_EQ(1, write_request_count); |
| EXPECT_EQ(1, svc_watcher_count); |
| } |
| |
| TEST_F(RemoteServiceManagerServiceChangedTest, |
| SecondServiceChangedNotificationIsQueued) { |
| const att::Handle kSvc1StartHandle(5); |
| const att::Handle kSvc1EndHandle(kSvc1StartHandle); |
| |
| // Add a test service to ensure that service discovery occurs after the |
| // Service Changed characteristic is configured. |
| ServiceData svc1(ServiceKind::PRIMARY, |
| kSvc1StartHandle, |
| kSvc1EndHandle, |
| kTestServiceUuid1); |
| fake_client()->set_services({gatt_service(), svc1}); |
| |
| // Send a notification that svc1 has been added. |
| auto svc_changed_range_buffer = StaticByteBuffer( |
| LowerBits(kSvc1StartHandle), |
| UpperBits(kSvc1StartHandle), // start handle of affected range |
| LowerBits(kSvc1EndHandle), |
| UpperBits(kSvc1EndHandle) // end handle of affected range |
| ); |
| fake_client()->SendNotification(/*indicate=*/true, |
| service_changed_characteristic().value_handle, |
| svc_changed_range_buffer, |
| /*maybe_truncated=*/false); |
| // Send a notification that svc1 has been modified. |
| fake_client()->SendNotification(/*indicate=*/true, |
| service_changed_characteristic().value_handle, |
| svc_changed_range_buffer, |
| /*maybe_truncated=*/false); |
| |
| RunUntilIdle(); |
| ASSERT_EQ(2u, svc_watcher_data().size()); |
| |
| ASSERT_EQ(1u, svc_watcher_data()[0].added.size()); |
| EXPECT_EQ(0u, svc_watcher_data()[0].removed.size()); |
| EXPECT_EQ(0u, svc_watcher_data()[0].modified.size()); |
| EXPECT_FALSE(svc_watcher_data()[0] |
| .added[0] |
| .is_alive()); // WeakPtr should already be invalidated |
| |
| EXPECT_EQ(0u, svc_watcher_data()[1].added.size()); |
| EXPECT_EQ(0u, svc_watcher_data()[1].removed.size()); |
| ASSERT_EQ(1u, svc_watcher_data()[1].modified.size()); |
| EXPECT_EQ(kSvc1StartHandle, svc_watcher_data()[1].modified[0]->handle()); |
| } |
| |
| TEST_F(RemoteServiceManagerServiceChangedTest, ServiceUuidChanged) { |
| const att::Handle kSvcStartHandle(5); |
| const att::Handle kSvcEndHandle(kSvcStartHandle); |
| |
| ServiceData svc1( |
| ServiceKind::PRIMARY, kSvcStartHandle, kSvcEndHandle, kTestServiceUuid1); |
| fake_client()->set_services({gatt_service(), svc1}); |
| |
| // Send a notification that svc1 has been added. |
| auto svc_changed_range_buffer = StaticByteBuffer( |
| LowerBits(kSvcStartHandle), |
| UpperBits(kSvcStartHandle), // start handle of affected range |
| LowerBits(kSvcEndHandle), |
| UpperBits(kSvcEndHandle) // end handle of affected range |
| ); |
| fake_client()->SendNotification(/*indicate=*/true, |
| service_changed_characteristic().value_handle, |
| svc_changed_range_buffer, |
| /*maybe_truncated=*/false); |
| |
| RunUntilIdle(); |
| ASSERT_EQ(1u, svc_watcher_data().size()); |
| |
| ASSERT_EQ(1u, svc_watcher_data()[0].added.size()); |
| EXPECT_EQ(0u, svc_watcher_data()[0].removed.size()); |
| EXPECT_EQ(0u, svc_watcher_data()[0].modified.size()); |
| EXPECT_EQ(kSvcStartHandle, svc_watcher_data()[0].added[0]->handle()); |
| EXPECT_EQ(kTestServiceUuid1, svc_watcher_data()[0].added[0]->uuid()); |
| |
| ServiceData svc2( |
| ServiceKind::PRIMARY, kSvcStartHandle, kSvcEndHandle, kTestServiceUuid2); |
| fake_client()->set_services({gatt_service(), svc2}); |
| |
| // Send a notification that svc2 has replaced svc1. |
| fake_client()->SendNotification(/*indicate=*/true, |
| service_changed_characteristic().value_handle, |
| svc_changed_range_buffer, |
| /*maybe_truncated=*/false); |
| RunUntilIdle(); |
| ASSERT_EQ(2u, svc_watcher_data().size()); |
| |
| ASSERT_EQ(1u, svc_watcher_data()[1].added.size()); |
| ASSERT_EQ(1u, svc_watcher_data()[1].removed.size()); |
| EXPECT_EQ(0u, svc_watcher_data()[1].modified.size()); |
| EXPECT_EQ(kSvcStartHandle, svc_watcher_data()[1].removed[0]); |
| EXPECT_EQ(kSvcStartHandle, svc_watcher_data()[1].added[0]->handle()); |
| EXPECT_EQ(kTestServiceUuid2, svc_watcher_data()[1].added[0]->uuid()); |
| } |
| |
| TEST_F( |
| RemoteServiceManagerServiceChangedTest, |
| AddServiceThenRemoveServiceAndAddTwoMoreServicesBeforeAndAfterRemovedServiceWithSameNotification) { |
| const att::Handle kSvc1StartHandle(7); |
| const att::Handle kSvc1EndHandle(kSvc1StartHandle); |
| const att::Handle kSvc2StartHandle(6); |
| const att::Handle kSvc2EndHandle(kSvc2StartHandle); |
| const att::Handle kSvc3StartHandle(8); |
| const att::Handle kSvc3EndHandle(kSvc3StartHandle); |
| |
| ServiceData svc1(ServiceKind::PRIMARY, |
| kSvc1StartHandle, |
| kSvc1EndHandle, |
| kTestServiceUuid1); |
| fake_client()->set_services({gatt_service(), svc1}); |
| |
| // Send a notification that svc1 has been added. |
| auto svc_changed_range_buffer_0 = StaticByteBuffer( |
| LowerBits(kSvc1StartHandle), |
| UpperBits(kSvc1StartHandle), // start handle of affected range |
| LowerBits(kSvc1EndHandle), |
| UpperBits(kSvc1EndHandle) // end handle of affected range |
| ); |
| fake_client()->SendNotification(/*indicate=*/true, |
| service_changed_characteristic().value_handle, |
| svc_changed_range_buffer_0, |
| /*maybe_truncated=*/false); |
| |
| RunUntilIdle(); |
| ASSERT_EQ(1u, svc_watcher_data().size()); |
| ASSERT_EQ(1u, svc_watcher_data()[0].added.size()); |
| EXPECT_EQ(0u, svc_watcher_data()[0].removed.size()); |
| EXPECT_EQ(0u, svc_watcher_data()[0].modified.size()); |
| EXPECT_EQ(kSvc1StartHandle, svc_watcher_data()[0].added[0]->handle()); |
| |
| ServiceData svc2(ServiceKind::PRIMARY, |
| kSvc2StartHandle, |
| kSvc2EndHandle, |
| kTestServiceUuid2); |
| ServiceData svc3(ServiceKind::PRIMARY, |
| kSvc3StartHandle, |
| kSvc3EndHandle, |
| kTestServiceUuid3); |
| fake_client()->set_services({gatt_service(), svc2, svc3}); |
| |
| // Range includes all 3 services. |
| auto svc_changed_range_buffer_1 = StaticByteBuffer( |
| LowerBits(kSvc2StartHandle), |
| UpperBits(kSvc2StartHandle), // start handle of affected range |
| LowerBits(kSvc3EndHandle), |
| UpperBits(kSvc3EndHandle) // end handle of affected range |
| ); |
| fake_client()->SendNotification(/*indicate=*/true, |
| service_changed_characteristic().value_handle, |
| svc_changed_range_buffer_1, |
| /*maybe_truncated=*/false); |
| |
| RunUntilIdle(); |
| ASSERT_EQ(2u, svc_watcher_data().size()); |
| ASSERT_EQ(2u, svc_watcher_data()[1].added.size()); |
| ASSERT_EQ(1u, svc_watcher_data()[1].removed.size()); |
| EXPECT_EQ(0u, svc_watcher_data()[1].modified.size()); |
| EXPECT_EQ(kSvc1StartHandle, svc_watcher_data()[1].removed[0]); |
| EXPECT_EQ(kSvc2StartHandle, svc_watcher_data()[1].added[0]->handle()); |
| EXPECT_EQ(kSvc3StartHandle, svc_watcher_data()[1].added[1]->handle()); |
| } |
| |
| class RemoteServiceManagerServiceChangedTestWithServiceKindParam |
| : public RemoteServiceManagerServiceChangedTest, |
| public ::testing::WithParamInterface<ServiceKind> {}; |
| |
| TEST_P(RemoteServiceManagerServiceChangedTestWithServiceKindParam, |
| ServiceChangedServiceDiscoveryFailureThenSuccess) { |
| const ServiceKind kKind = GetParam(); |
| |
| const att::Handle kSvc1StartHandle(5); |
| const att::Handle kSvc1EndHandle(kSvc1StartHandle); |
| ServiceData svc1(kKind, kSvc1StartHandle, kSvc1EndHandle, kTestServiceUuid1); |
| const att::Handle kSvc2StartHandle(7); |
| const att::Handle kSvc2EndHandle(kSvc2StartHandle); |
| ServiceData svc2(kKind, kSvc2StartHandle, kSvc2EndHandle, kTestServiceUuid2); |
| fake_client()->set_services({gatt_service(), svc1, svc2}); |
| |
| // Cause only the first service discovery to fail. |
| int discover_services_count = 0; |
| fake_client()->set_discover_services_callback( |
| [&](ServiceKind kind) -> att::Result<> { |
| if (kind == kKind) { |
| discover_services_count++; |
| if (discover_services_count == 1) { |
| return ToResult(HostError::kFailed); |
| } |
| } |
| return fit::ok(); |
| }); |
| |
| auto svc1_changed_range_buffer = StaticByteBuffer( |
| LowerBits(kSvc1StartHandle), |
| UpperBits(kSvc1StartHandle), // start handle of affected range |
| LowerBits(kSvc1EndHandle), |
| UpperBits(kSvc1EndHandle) // end handle of affected range |
| ); |
| fake_client()->SendNotification(/*indicate=*/true, |
| service_changed_characteristic().value_handle, |
| svc1_changed_range_buffer, |
| /*maybe_truncated=*/false); |
| |
| // This second notification should be queued and processed after the first |
| // service discovery fails. |
| auto svc2_changed_range_buffer = StaticByteBuffer( |
| LowerBits(kSvc2StartHandle), |
| UpperBits(kSvc2StartHandle), // start handle of affected range |
| LowerBits(kSvc2EndHandle), |
| UpperBits(kSvc2EndHandle) // end handle of affected range |
| ); |
| fake_client()->SendNotification(/*indicate=*/true, |
| service_changed_characteristic().value_handle, |
| svc2_changed_range_buffer, |
| /*maybe_truncated=*/false); |
| |
| RunUntilIdle(); |
| ASSERT_EQ(1u, svc_watcher_data().size()); |
| ASSERT_EQ(1u, svc_watcher_data()[0].added.size()); |
| EXPECT_EQ(0u, svc_watcher_data()[0].removed.size()); |
| EXPECT_EQ(0u, svc_watcher_data()[0].modified.size()); |
| EXPECT_EQ(kSvc2StartHandle, svc_watcher_data()[0].added[0]->handle()); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| ServiceKind, |
| RemoteServiceManagerServiceChangedTestWithServiceKindParam, |
| ::testing::Values(ServiceKind::PRIMARY, ServiceKind::SECONDARY)); |
| |
| TEST_F(RemoteServiceManagerServiceChangedTest, |
| SecondaryServiceDiscoveryIgnoresUnsupportedGroupTypeError) { |
| const att::Handle kSvc1StartHandle(5); |
| const att::Handle kSvc1EndHandle(kSvc1StartHandle); |
| |
| ServiceData svc1(ServiceKind::PRIMARY, |
| kSvc1StartHandle, |
| kSvc1EndHandle, |
| kTestServiceUuid1); |
| fake_client()->set_services({gatt_service(), svc1}); |
| |
| fake_client()->set_discover_services_callback([](ServiceKind kind) { |
| if (kind == ServiceKind::SECONDARY) { |
| return ToResult(att::ErrorCode::kUnsupportedGroupType); |
| } |
| return att::Result<>(fit::ok()); |
| }); |
| |
| // Send a notification that svc1 has been added. |
| auto svc_changed_range_buffer = StaticByteBuffer( |
| LowerBits(kSvc1StartHandle), |
| UpperBits(kSvc1StartHandle), // start handle of affected range |
| LowerBits(kSvc1EndHandle), |
| UpperBits(kSvc1EndHandle) // end handle of affected range |
| ); |
| fake_client()->SendNotification(/*indicate=*/true, |
| service_changed_characteristic().value_handle, |
| svc_changed_range_buffer, |
| /*maybe_truncated=*/false); |
| RunUntilIdle(); |
| ASSERT_EQ(1u, svc_watcher_data().size()); |
| ASSERT_EQ(1u, svc_watcher_data()[0].added.size()); |
| EXPECT_EQ(0u, svc_watcher_data()[0].removed.size()); |
| EXPECT_EQ(0u, svc_watcher_data()[0].modified.size()); |
| EXPECT_EQ(kSvc1StartHandle, svc_watcher_data()[0].added[0]->handle()); |
| } |
| |
| TEST_F(RemoteServiceManagerServiceChangedTest, GattProfileServiceChanged) { |
| EXPECT_EQ(1, service_changed_ccc_write_count()); |
| |
| StaticByteBuffer svc_changed_range_buffer( |
| LowerBits(gatt_service().range_start), |
| UpperBits(gatt_service().range_start), // start handle of affected range |
| LowerBits(gatt_service().range_end), |
| UpperBits(gatt_service().range_end) // end handle of affected range |
| ); |
| fake_client()->SendNotification(/*indicate=*/true, |
| service_changed_characteristic().value_handle, |
| svc_changed_range_buffer, |
| /*maybe_truncated=*/false); |
| RunUntilIdle(); |
| ASSERT_EQ(1u, svc_watcher_data().size()); |
| EXPECT_EQ(0u, svc_watcher_data()[0].added.size()); |
| EXPECT_EQ(0u, svc_watcher_data()[0].removed.size()); |
| ASSERT_EQ(1u, svc_watcher_data()[0].modified.size()); |
| EXPECT_EQ(gatt_service().range_start, |
| svc_watcher_data()[0].modified[0]->handle()); |
| EXPECT_EQ(1, service_changed_ccc_write_count()); |
| |
| // The handler for notifications should remain configured. |
| fake_client()->SendNotification(/*indicate=*/true, |
| service_changed_characteristic().value_handle, |
| svc_changed_range_buffer, |
| /*maybe_truncated=*/false); |
| RunUntilIdle(); |
| ASSERT_EQ(2u, svc_watcher_data().size()); |
| EXPECT_EQ(0u, svc_watcher_data()[1].added.size()); |
| EXPECT_EQ(0u, svc_watcher_data()[1].removed.size()); |
| ASSERT_EQ(1u, svc_watcher_data()[1].modified.size()); |
| EXPECT_EQ(gatt_service().range_start, |
| svc_watcher_data()[1].modified[0]->handle()); |
| EXPECT_EQ(1, service_changed_ccc_write_count()); |
| // A new service should not have been created for the modified GATT Profile |
| // service. |
| EXPECT_EQ(&svc_watcher_data()[0].modified[0].get(), |
| &svc_watcher_data()[1].modified[0].get()); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, ErrorDiscoveringGattProfileService) { |
| ServiceData gatt_svc( |
| ServiceKind::PRIMARY, 1, 1, types::kGenericAttributeService); |
| ServiceData svc1(ServiceKind::PRIMARY, 2, 2, kTestServiceUuid1); |
| std::vector<ServiceData> fake_services{{gatt_svc, svc1}}; |
| fake_client()->set_services(std::move(fake_services)); |
| |
| fake_client()->set_discover_services_callback([](ServiceKind kind) { |
| if (kind == ServiceKind::PRIMARY) { |
| return ToResult(att::ErrorCode::kRequestNotSupported); |
| } |
| return att::Result<>(fit::ok()); |
| }); |
| |
| std::optional<att::Result<>> status; |
| mgr()->Initialize([&status](att::Result<> val) { status = val; }, |
| NopMtuCallback); |
| RunUntilIdle(); |
| EXPECT_EQ(ToResult(att::ErrorCode::kRequestNotSupported), *status); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, |
| MultipleGattProfileServicesFailsInitialization) { |
| ServiceData gatt_svc0( |
| ServiceKind::PRIMARY, 1, 1, types::kGenericAttributeService); |
| ServiceData gatt_svc1( |
| ServiceKind::PRIMARY, 2, 2, types::kGenericAttributeService); |
| std::vector<ServiceData> fake_services{{gatt_svc0, gatt_svc1}}; |
| fake_client()->set_services(std::move(fake_services)); |
| |
| std::optional<att::Result<>> status; |
| mgr()->Initialize([&status](att::Result<> val) { status = val; }, |
| NopMtuCallback); |
| RunUntilIdle(); |
| EXPECT_EQ(ToResult(HostError::kFailed), *status); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, InitializeEmptyGattProfileService) { |
| ServiceData gatt_svc( |
| ServiceKind::PRIMARY, 1, 1, types::kGenericAttributeService); |
| ServiceData svc1(ServiceKind::PRIMARY, 2, 2, kTestServiceUuid1); |
| std::vector<ServiceData> fake_services{{gatt_svc, svc1}}; |
| fake_client()->set_services(std::move(fake_services)); |
| |
| ServiceList services; |
| mgr()->set_service_watcher( |
| [&services](auto /*removed*/, ServiceList added, auto /*modified*/) { |
| services.insert(services.end(), added.begin(), added.end()); |
| }); |
| |
| att::Result<> status = ToResult(HostError::kFailed); |
| mgr()->Initialize([&status](att::Result<> val) { status = val; }, |
| NopMtuCallback); |
| RunUntilIdle(); |
| EXPECT_EQ(fit::ok(), status); |
| ASSERT_EQ(2u, services.size()); |
| EXPECT_EQ(gatt_svc.range_start, services[0]->handle()); |
| EXPECT_EQ(gatt_svc.type, services[0]->uuid()); |
| EXPECT_EQ(svc1.range_start, services[1]->handle()); |
| EXPECT_EQ(svc1.type, services[1]->uuid()); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, |
| EnableServiceChangedNotificationsReturnsError) { |
| const att::Handle kGattSvcStartHandle(1); |
| const att::Handle kSvcChangedChrcHandle(2); |
| const att::Handle kSvcChangedChrcValueHandle(3); |
| const att::Handle kCCCDescriptorHandle(4); |
| const att::Handle kGattSvcEndHandle(kCCCDescriptorHandle); |
| |
| ServiceData gatt_svc(ServiceKind::PRIMARY, |
| kGattSvcStartHandle, |
| kGattSvcEndHandle, |
| types::kGenericAttributeService); |
| CharacteristicData service_changed_chrc(Property::kIndicate, |
| std::nullopt, |
| kSvcChangedChrcHandle, |
| kSvcChangedChrcValueHandle, |
| types::kServiceChangedCharacteristic); |
| DescriptorData ccc_descriptor(kCCCDescriptorHandle, |
| types::kClientCharacteristicConfig); |
| fake_client()->set_services({gatt_svc}); |
| fake_client()->set_characteristics({service_changed_chrc}); |
| fake_client()->set_descriptors({ccc_descriptor}); |
| |
| ServiceList services; |
| mgr()->set_service_watcher( |
| [&services](auto /*removed*/, ServiceList added, auto /*modified*/) { |
| services.insert(services.end(), added.begin(), added.end()); |
| }); |
| |
| // Return an error when a Service Changed Client Characteristic Config |
| // descriptor write is performed. |
| int write_request_count = 0; |
| fake_client()->set_write_request_callback( |
| [&](att::Handle handle, const auto& value, auto status_callback) { |
| write_request_count++; |
| EXPECT_EQ(kCCCDescriptorHandle, handle); |
| status_callback(ToResult(att::ErrorCode::kWriteNotPermitted)); |
| }); |
| |
| std::optional<att::Result<>> status; |
| mgr()->Initialize([&status](att::Result<> val) { status = val; }, |
| NopMtuCallback); |
| RunUntilIdle(); |
| EXPECT_EQ(ToResult(att::ErrorCode::kWriteNotPermitted), *status); |
| EXPECT_EQ(write_request_count, 1); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, |
| ErrorDiscoveringGattProfileServiceCharacteristics) { |
| ServiceData gatt_svc( |
| ServiceKind::PRIMARY, 1, 3, types::kGenericAttributeService); |
| ServiceData svc1(ServiceKind::PRIMARY, 4, 4, kTestServiceUuid1); |
| std::vector<ServiceData> fake_services{{gatt_svc, svc1}}; |
| fake_client()->set_services(std::move(fake_services)); |
| |
| fake_client()->set_characteristic_discovery_status( |
| ToResult(att::ErrorCode::kRequestNotSupported)); |
| |
| std::optional<att::Result<>> status; |
| mgr()->Initialize([&status](att::Result<> val) { status = val; }, |
| NopMtuCallback); |
| RunUntilIdle(); |
| EXPECT_EQ(ToResult(att::ErrorCode::kRequestNotSupported), *status); |
| } |
| |
| TEST_F(RemoteServiceManagerTest, DisableNotificationInHandlerCallback) { |
| const CharacteristicHandle kChrcValueHandle(3); |
| RemoteService::WeakPtr svc = SetupNotifiableService(); |
| std::optional<IdType> handler_id; |
| RemoteCharacteristic::NotifyStatusCallback status_cb = |
| [&](att::Result<> status, IdType cb_handler_id) { |
| EXPECT_EQ(fit::ok(), status); |
| handler_id = cb_handler_id; |
| }; |
| |
| int value_cb_count = 0; |
| RemoteCharacteristic::ValueCallback value_cb = [&](auto&, auto) { |
| value_cb_count++; |
| ASSERT_TRUE(handler_id); |
| // Disabling notifications in handler should not crash. |
| svc->DisableNotifications( |
| kChrcValueHandle, handler_id.value(), [](auto) {}); |
| }; |
| svc->EnableNotifications( |
| kChrcValueHandle, std::move(value_cb), std::move(status_cb)); |
| |
| fake_client()->SendNotification(/*indicate=*/false, |
| kChrcValueHandle.value, |
| StaticByteBuffer('y', 'e'), |
| /*maybe_truncated=*/false); |
| RunUntilIdle(); |
| EXPECT_EQ(value_cb_count, 1); |
| |
| // Second notification should not notify disabled handler. |
| fake_client()->SendNotification(/*indicate=*/false, |
| kChrcValueHandle.value, |
| StaticByteBuffer('y', 'e'), |
| /*maybe_truncated=*/false); |
| RunUntilIdle(); |
| EXPECT_EQ(value_cb_count, 1); |
| } |
| |
| TEST_F(RemoteServiceManagerServiceChangedTest, |
| ServiceRemovedDuringReadLongCharacteristic) { |
| const att::Handle kSvc1StartHandle(5); |
| const att::Handle kSvc1ChrcHandle(6); |
| const att::Handle kSvc1ChrcValueHandle(7); |
| const att::Handle kSvc1EndHandle(kSvc1ChrcValueHandle); |
| ServiceData svc1(ServiceKind::PRIMARY, |
| kSvc1StartHandle, |
| kSvc1EndHandle, |
| kTestServiceUuid1); |
| const UUID kSvc1ChrcUuid(kTestUuid3); |
| CharacteristicData svc1_characteristic(Property::kRead, |
| std::nullopt, |
| kSvc1ChrcHandle, |
| kSvc1ChrcValueHandle, |
| kSvc1ChrcUuid); |
| fake_client()->set_services({gatt_service(), svc1}); |
| fake_client()->set_characteristics( |
| {service_changed_characteristic(), svc1_characteristic}); |
| fake_client()->set_descriptors({ccc_descriptor()}); |
| |
| // Send a notification that svc1 has been added. |
| auto svc_changed_range_buffer = StaticByteBuffer( |
| LowerBits(kSvc1StartHandle), |
| UpperBits(kSvc1StartHandle), // start handle of affected range |
| LowerBits(kSvc1EndHandle), |
| UpperBits(kSvc1EndHandle) // end handle of affected range |
| ); |
| fake_client()->SendNotification(/*indicate=*/true, |
| service_changed_characteristic().value_handle, |
| svc_changed_range_buffer, |
| /*maybe_truncated=*/false); |
| RunUntilIdle(); |
| ASSERT_EQ(1u, svc_watcher_data().size()); |
| ASSERT_EQ(1u, svc_watcher_data()[0].added.size()); |
| EXPECT_EQ(kSvc1StartHandle, svc_watcher_data()[0].added[0]->handle()); |
| |
| RemoteService::WeakPtr service = svc_watcher_data()[0].added[0]; |
| service->DiscoverCharacteristics([](auto, const auto&) {}); |
| RunUntilIdle(); |
| |
| int read_req_count = 0; |
| RemoteService::ReadValueCallback read_callback = nullptr; |
| fake_client()->set_read_request_callback( |
| [&](auto, RemoteService::ReadValueCallback callback) { |
| EXPECT_EQ(read_req_count, 0); |
| read_req_count++; |
| read_callback = std::move(callback); |
| }); |
| fake_client()->set_read_blob_request_callback( |
| [](auto, auto, auto) { FAIL(); }); |
| |
| int read_long_cb_count = 0; |
| service->ReadLongCharacteristic( |
| CharacteristicHandle(kSvc1ChrcValueHandle), |
| /*offset=*/0, |
| att::kMaxAttributeValueLength, |
| [&](auto, auto&, auto) { read_long_cb_count++; }); |
| |
| // Remove svc1. |
| fake_client()->set_services({gatt_service()}); |
| fake_client()->SendNotification(/*indicate=*/true, |
| service_changed_characteristic().value_handle, |
| svc_changed_range_buffer, |
| /*maybe_truncated=*/false); |
| RunUntilIdle(); |
| EXPECT_FALSE(service.is_alive()); |
| ASSERT_EQ(2u, svc_watcher_data().size()); |
| ASSERT_EQ(1u, svc_watcher_data()[1].removed.size()); |
| EXPECT_EQ(read_req_count, 1); |
| ASSERT_TRUE(read_callback); |
| |
| // Now that the service has been destroyed, the read long procedure should be |
| // aborted when the first read request callback is called. |
| StaticByteBuffer<att::kLEMinMTU - 1> expected_value; |
| expected_value.Fill(0x02); |
| read_callback(fit::ok(), expected_value.view(), /*maybe_truncated=*/true); |
| RunUntilIdle(); |
| EXPECT_EQ(read_long_cb_count, 0); |
| } |
| |
| } // namespace |
| } // namespace bt::gatt::internal |