| // Copyright 2017 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/gatt/local_service_manager.h" |
| |
| #include <zircon/assert.h> |
| |
| #include <gtest/gtest.h> |
| |
| #include "src/connectivity/bluetooth/core/bt-host/att/att.h" |
| #include "src/connectivity/bluetooth/core/bt-host/common/test_helpers.h" |
| #include "src/connectivity/bluetooth/core/bt-host/gatt/gatt_defs.h" |
| |
| namespace bt::gatt { |
| namespace { |
| |
| constexpr PeerId kTestPeerId(1); |
| constexpr PeerId kTestPeerId2(2); |
| constexpr UUID kTestType16(uint16_t{0xdead}); |
| constexpr UUID kTestType32(uint32_t{0xdeadbeef}); |
| |
| // The first characteristic value attribute of the first service has handle |
| // number 3. |
| constexpr att::Handle kFirstChrcValueHandle = 0x0003; |
| |
| // The first descroptor of the first characteristic of the first service has |
| // handle number 4. |
| constexpr att::Handle kFirstDescrHandle = 0x0004; |
| |
| inline att::AccessRequirements AllowedNoSecurity() { |
| return att::AccessRequirements(/*encryption=*/false, /*authentication=*/false, |
| /*authorization=*/false); |
| } |
| |
| // Convenience function that registers |service| with |mgr| using the NOP |
| // handlers above by default. |
| IdType RegisterService(LocalServiceManager* mgr, ServicePtr service, |
| ReadHandler read_handler = NopReadHandler, |
| WriteHandler write_handler = NopWriteHandler, |
| ClientConfigCallback ccc_callback = NopCCCallback) { |
| return mgr->RegisterService(std::move(service), std::move(read_handler), std::move(write_handler), |
| std::move(ccc_callback)); |
| } |
| |
| TEST(LocalServiceManagerTest, EmptyService) { |
| LocalServiceManager mgr; |
| |
| auto service = std::make_unique<Service>(/*primary=*/true, kTestType16); |
| auto id1 = RegisterService(&mgr, std::move(service)); |
| EXPECT_NE(0u, id1); |
| |
| service = std::make_unique<Service>(/*primary=*/false, kTestType32); |
| auto id2 = RegisterService(&mgr, std::move(service)); |
| EXPECT_NE(0u, id2); |
| |
| EXPECT_EQ(2u, mgr.database()->groupings().size()); |
| |
| auto iter = mgr.database()->groupings().begin(); |
| |
| EXPECT_TRUE(iter->complete()); |
| EXPECT_EQ(1u, iter->attributes().size()); |
| EXPECT_TRUE(iter->active()); |
| EXPECT_EQ(0x0001, iter->start_handle()); |
| EXPECT_EQ(0x0001, iter->end_handle()); |
| EXPECT_EQ(types::kPrimaryService, iter->group_type()); |
| EXPECT_TRUE(ContainersEqual(StaticByteBuffer(0xad, 0xde), iter->decl_value())); |
| |
| iter++; |
| |
| EXPECT_TRUE(iter->complete()); |
| EXPECT_EQ(1u, iter->attributes().size()); |
| EXPECT_TRUE(iter->active()); |
| EXPECT_EQ(0x0002, iter->start_handle()); |
| EXPECT_EQ(0x0002, iter->end_handle()); |
| EXPECT_EQ(types::kSecondaryService, iter->group_type()); |
| EXPECT_TRUE(ContainersEqual(StaticByteBuffer(0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, 0x00, 0x80, 0x00, |
| 0x10, 0x00, 0x00, 0xef, 0xbe, 0xad, 0xde), |
| iter->decl_value())); |
| } |
| |
| TEST(LocalServiceManagerTest, UnregisterService) { |
| LocalServiceManager mgr; |
| |
| auto service = std::make_unique<Service>(/*primary=*/true, kTestType16); |
| auto id1 = RegisterService(&mgr, std::move(service)); |
| EXPECT_NE(0u, id1); |
| EXPECT_EQ(1u, mgr.database()->groupings().size()); |
| |
| // Unknown id |
| EXPECT_FALSE(mgr.UnregisterService(id1 + 1)); |
| |
| // Success |
| EXPECT_TRUE(mgr.UnregisterService(id1)); |
| EXPECT_TRUE(mgr.database()->groupings().empty()); |
| |
| // |id1| becomes unknown |
| EXPECT_FALSE(mgr.UnregisterService(id1)); |
| } |
| |
| TEST(LocalServiceManagerTest, RegisterCharacteristic) { |
| LocalServiceManager mgr; |
| |
| constexpr IdType kChrcId = 0; |
| constexpr uint8_t kChrcProps = Property::kRead; |
| constexpr UUID kTestChrcType(uint16_t{0xabcd}); |
| const att::AccessRequirements kReadReqs(/*encryption=*/true, /*authentication=*/true, |
| /*authorization=*/true); |
| const att::AccessRequirements kWriteReqs, kUpdateReqs; |
| |
| auto service = std::make_unique<Service>(/*primary=*/true, kTestType16); |
| service->AddCharacteristic(std::make_unique<Characteristic>(kChrcId, kTestChrcType, kChrcProps, 0, |
| kReadReqs, kWriteReqs, kUpdateReqs)); |
| auto id1 = RegisterService(&mgr, std::move(service)); |
| EXPECT_NE(0u, id1); |
| |
| ASSERT_EQ(1u, mgr.database()->groupings().size()); |
| const auto& grouping = mgr.database()->groupings().front(); |
| EXPECT_TRUE(grouping.complete()); |
| |
| const auto& attrs = grouping.attributes(); |
| ASSERT_EQ(3u, attrs.size()); |
| |
| att::Handle srvc_handle = attrs[0].handle(); |
| EXPECT_EQ(att::kHandleMin, srvc_handle); |
| |
| // Characteristic declaration |
| EXPECT_EQ(srvc_handle + 1, attrs[1].handle()); |
| EXPECT_EQ(types::kCharacteristicDeclaration, attrs[1].type()); |
| EXPECT_EQ(AllowedNoSecurity(), attrs[1].read_reqs()); |
| EXPECT_EQ(att::AccessRequirements(), attrs[1].write_reqs()); |
| EXPECT_TRUE(attrs[1].value()); |
| |
| // clang-format off |
| const StaticByteBuffer kDeclValue( |
| 0x02, // properties |
| 0x03, 0x00, // value handle |
| 0xcd, 0xab // UUID |
| ); |
| // clang-format on |
| EXPECT_TRUE(ContainersEqual(kDeclValue, *attrs[1].value())); |
| |
| // Characteristic value |
| EXPECT_EQ(srvc_handle + 2, attrs[2].handle()); |
| EXPECT_EQ(kTestChrcType, attrs[2].type()); |
| EXPECT_EQ(kReadReqs, attrs[2].read_reqs()); |
| EXPECT_EQ(kWriteReqs, attrs[2].write_reqs()); |
| |
| // This value is dynamic. |
| EXPECT_FALSE(attrs[2].value()); |
| } |
| |
| TEST(LocalServiceManagerTest, RegisterCharacteristic32) { |
| LocalServiceManager mgr; |
| |
| constexpr IdType kChrcId = 0; |
| constexpr uint8_t kChrcProps = Property::kRead; |
| constexpr UUID kTestChrcType(uint32_t{0xdeadbeef}); |
| const att::AccessRequirements kReadReqs(/*encryption=*/true, /*authentication=*/true, |
| /*authorization=*/true); |
| const att::AccessRequirements kWriteReqs, kUpdateReqs; |
| |
| auto service = std::make_unique<Service>(/*primary=*/true, kTestType16); |
| service->AddCharacteristic(std::make_unique<Characteristic>(kChrcId, kTestChrcType, kChrcProps, 0, |
| kReadReqs, kWriteReqs, kUpdateReqs)); |
| auto id1 = RegisterService(&mgr, std::move(service)); |
| EXPECT_NE(0u, id1); |
| |
| ASSERT_EQ(1u, mgr.database()->groupings().size()); |
| const auto& grouping = mgr.database()->groupings().front(); |
| EXPECT_TRUE(grouping.complete()); |
| |
| const auto& attrs = grouping.attributes(); |
| ASSERT_EQ(3u, attrs.size()); |
| |
| att::Handle srvc_handle = attrs[0].handle(); |
| EXPECT_EQ(att::kHandleMin, srvc_handle); |
| |
| // Characteristic declaration |
| EXPECT_EQ(srvc_handle + 1, attrs[1].handle()); |
| EXPECT_EQ(types::kCharacteristicDeclaration, attrs[1].type()); |
| EXPECT_EQ(AllowedNoSecurity(), attrs[1].read_reqs()); |
| EXPECT_EQ(att::AccessRequirements(), attrs[1].write_reqs()); |
| EXPECT_TRUE(attrs[1].value()); |
| |
| const StaticByteBuffer kDeclValue(0x02, // properties |
| 0x03, 0x00, // value handle |
| |
| // The 32-bit UUID will be stored as 128-bit |
| 0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, |
| 0x00, 0x00, 0xef, 0xbe, 0xad, 0xde); |
| EXPECT_TRUE(ContainersEqual(kDeclValue, *attrs[1].value())); |
| |
| // Characteristic value |
| EXPECT_EQ(srvc_handle + 2, attrs[2].handle()); |
| EXPECT_EQ(kTestChrcType, attrs[2].type()); |
| EXPECT_EQ(kReadReqs, attrs[2].read_reqs()); |
| EXPECT_EQ(kWriteReqs, attrs[2].write_reqs()); |
| |
| // This value is dynamic. |
| EXPECT_FALSE(attrs[2].value()); |
| } |
| |
| TEST(LocalServiceManagerTest, RegisterCharacteristic128) { |
| LocalServiceManager mgr; |
| |
| constexpr IdType kChrcId = 0; |
| constexpr uint8_t kChrcProps = Property::kRead; |
| UUID kTestChrcType; |
| EXPECT_TRUE(StringToUuid("00112233-4455-6677-8899-AABBCCDDEEFF", &kTestChrcType)); |
| const att::AccessRequirements kReadReqs(/*encryption=*/true, /*authentication=*/true, |
| /*authorization=*/true); |
| const att::AccessRequirements kWriteReqs, kUpdateReqs; |
| |
| auto service = std::make_unique<Service>(/*primary=*/true, kTestType16); |
| service->AddCharacteristic(std::make_unique<Characteristic>(kChrcId, kTestChrcType, kChrcProps, 0, |
| kReadReqs, kWriteReqs, kUpdateReqs)); |
| auto id1 = RegisterService(&mgr, std::move(service)); |
| EXPECT_NE(0u, id1); |
| |
| ASSERT_EQ(1u, mgr.database()->groupings().size()); |
| const auto& grouping = mgr.database()->groupings().front(); |
| EXPECT_TRUE(grouping.complete()); |
| |
| const auto& attrs = grouping.attributes(); |
| ASSERT_EQ(3u, attrs.size()); |
| |
| att::Handle srvc_handle = attrs[0].handle(); |
| EXPECT_EQ(att::kHandleMin, srvc_handle); |
| |
| // Characteristic declaration |
| EXPECT_EQ(srvc_handle + 1, attrs[1].handle()); |
| EXPECT_EQ(types::kCharacteristicDeclaration, attrs[1].type()); |
| EXPECT_EQ(AllowedNoSecurity(), attrs[1].read_reqs()); |
| EXPECT_EQ(att::AccessRequirements(), attrs[1].write_reqs()); |
| EXPECT_TRUE(attrs[1].value()); |
| |
| const StaticByteBuffer kDeclValue(0x02, // properties |
| 0x03, 0x00, // value handle |
| |
| // 128-bit UUID |
| 0xFF, 0xEE, 0xDD, 0xCC, 0xBB, 0xAA, 0x99, 0x88, 0x77, 0x66, |
| 0x55, 0x44, 0x33, 0x22, 0x11, 0x00); |
| EXPECT_TRUE(ContainersEqual(kDeclValue, *attrs[1].value())); |
| |
| // Characteristic value |
| EXPECT_EQ(srvc_handle + 2, attrs[2].handle()); |
| EXPECT_EQ(kTestChrcType, attrs[2].type()); |
| EXPECT_EQ(kReadReqs, attrs[2].read_reqs()); |
| EXPECT_EQ(kWriteReqs, attrs[2].write_reqs()); |
| |
| // This value is dynamic. |
| EXPECT_FALSE(attrs[2].value()); |
| } |
| |
| TEST(LocalServiceManagerTest, ExtPropSetSuccess) { |
| LocalServiceManager mgr; |
| const att::AccessRequirements kReadReqs, kWriteReqs, kUpdateReqs; |
| constexpr UUID kChrcType16(uint16_t{0x1234}); |
| constexpr IdType kChrcId = 5; |
| |
| auto service = std::make_unique<Service>(/*primary=*/true, kTestType16); |
| |
| constexpr uint8_t kChrcProps = Property::kRead; |
| constexpr uint8_t kExtChrcProps = ExtendedProperty::kReliableWrite; |
| auto chrc = std::make_unique<Characteristic>(kChrcId, kChrcType16, kChrcProps, kExtChrcProps, |
| kReadReqs, kWriteReqs, kUpdateReqs); |
| service->AddCharacteristic(std::move(chrc)); |
| |
| ASSERT_TRUE(RegisterService(&mgr, std::move(service))); |
| ASSERT_NE(0u, mgr.database()->groupings().size()); |
| const auto& grouping = mgr.database()->groupings().front(); |
| const auto& attrs = grouping.attributes(); |
| ASSERT_EQ(4u, attrs.size()); |
| EXPECT_EQ(types::kCharacteristicExtProperties, attrs[3].type()); |
| EXPECT_TRUE(ContainersEqual( // Reliable Write property |
| StaticByteBuffer(0x01, 0x00), *attrs[3].value())); |
| } |
| |
| // tests that the extended properties descriptor cannot be set externally |
| TEST(LocalServiceManagerTest, ExtPropSetFailure) { |
| LocalServiceManager mgr; |
| const att::AccessRequirements kReadReqs, kWriteReqs, kUpdateReqs; |
| |
| constexpr UUID kChrcType16(uint16_t{0x1234}); |
| constexpr UUID kDescType16(uint16_t{0x2900}); // UUID for Ext Prop |
| |
| auto service = std::make_unique<Service>(/*primary=*/true, kTestType16); |
| auto chrc = |
| std::make_unique<Characteristic>(0, kChrcType16, 0, 0, kReadReqs, kWriteReqs, kUpdateReqs); |
| chrc->AddDescriptor(std::make_unique<Descriptor>(1, kDescType16, kReadReqs, kWriteReqs)); |
| service->AddCharacteristic(std::move(chrc)); |
| |
| EXPECT_EQ(false, RegisterService(&mgr, std::move(service))); |
| } |
| |
| TEST(LocalServiceManagerTest, RegisterCharacteristicSorted) { |
| LocalServiceManager mgr; |
| const att::AccessRequirements kReadReqs, kWriteReqs, kUpdateReqs; |
| |
| constexpr UUID kType16(uint16_t{0xbeef}); |
| constexpr UUID kType128(uint32_t{0xdeadbeef}); |
| |
| constexpr IdType kChrcId0 = 0; |
| constexpr uint8_t kChrcProps0 = 0; |
| constexpr IdType kChrcId1 = 1; |
| constexpr uint8_t kChrcProps1 = 1; |
| constexpr IdType kChrcId2 = 2; |
| constexpr uint8_t kChrcProps2 = 2; |
| constexpr IdType kChrcId3 = 3; |
| constexpr uint8_t kChrcProps3 = 3; |
| |
| auto service = std::make_unique<Service>(/*primary=*/true, kTestType16); |
| service->AddCharacteristic(std::make_unique<Characteristic>(kChrcId0, kType128, kChrcProps0, 0, |
| kReadReqs, kWriteReqs, kUpdateReqs)); |
| service->AddCharacteristic(std::make_unique<Characteristic>(kChrcId1, kType16, kChrcProps1, 0, |
| kReadReqs, kWriteReqs, kUpdateReqs)); |
| service->AddCharacteristic(std::make_unique<Characteristic>(kChrcId2, kType128, kChrcProps2, 0, |
| kReadReqs, kWriteReqs, kUpdateReqs)); |
| service->AddCharacteristic(std::make_unique<Characteristic>(kChrcId3, kType16, kChrcProps3, 0, |
| kReadReqs, kWriteReqs, kUpdateReqs)); |
| auto id1 = RegisterService(&mgr, std::move(service)); |
| EXPECT_NE(0u, id1); |
| |
| ASSERT_EQ(1u, mgr.database()->groupings().size()); |
| const auto& grouping = mgr.database()->groupings().front(); |
| EXPECT_TRUE(grouping.complete()); |
| |
| const auto& attrs = grouping.attributes(); |
| ASSERT_EQ(9u, attrs.size()); |
| |
| // The declaration attributes should be sorted by service type (16-bit UUIDs |
| // first). |
| ASSERT_TRUE(attrs[1].value()); |
| EXPECT_EQ(kChrcProps1, (*attrs[1].value())[0]); |
| ASSERT_TRUE(attrs[3].value()); |
| EXPECT_EQ(kChrcProps3, (*attrs[3].value())[0]); |
| ASSERT_TRUE(attrs[5].value()); |
| EXPECT_EQ(kChrcProps0, (*attrs[5].value())[0]); |
| ASSERT_TRUE(attrs[7].value()); |
| EXPECT_EQ(kChrcProps2, (*attrs[7].value())[0]); |
| } |
| |
| TEST(LocalServiceManagerTest, RegisterDescriptor) { |
| LocalServiceManager mgr; |
| const att::AccessRequirements kReadReqs, kWriteReqs, kUpdateReqs; |
| |
| constexpr UUID kChrcType16(uint16_t{0x1234}); |
| constexpr UUID kDescType16(uint16_t{0x5678}); |
| |
| auto service = std::make_unique<Service>(/*primary=*/true, kTestType16); |
| auto chrc = |
| std::make_unique<Characteristic>(0, kChrcType16, 0, 0, kReadReqs, kWriteReqs, kUpdateReqs); |
| chrc->AddDescriptor(std::make_unique<Descriptor>(1, kDescType16, kReadReqs, kWriteReqs)); |
| service->AddCharacteristic(std::move(chrc)); |
| |
| EXPECT_NE(0u, RegisterService(&mgr, std::move(service))); |
| ASSERT_EQ(1u, mgr.database()->groupings().size()); |
| const auto& grouping = mgr.database()->groupings().front(); |
| EXPECT_TRUE(grouping.complete()); |
| |
| const auto& attrs = grouping.attributes(); |
| ASSERT_EQ(4u, attrs.size()); |
| EXPECT_EQ(types::kCharacteristicDeclaration, attrs[1].type()); |
| EXPECT_EQ(kChrcType16, attrs[2].type()); |
| EXPECT_EQ(kDescType16, attrs[3].type()); |
| EXPECT_FALSE(attrs[3].value()); |
| } |
| |
| TEST(LocalServiceManagerTest, DuplicateChrcIds) { |
| LocalServiceManager mgr; |
| const att::AccessRequirements kReadReqs, kWriteReqs, kUpdateReqs; |
| |
| constexpr UUID kChrcType16(uint16_t{0x1234}); |
| |
| auto service = std::make_unique<Service>(/*primary=*/true, kTestType16); |
| |
| // Use same characteristic ID twice. |
| service->AddCharacteristic( |
| std::make_unique<Characteristic>(0, kChrcType16, 0, 0, kReadReqs, kWriteReqs, kUpdateReqs)); |
| service->AddCharacteristic( |
| std::make_unique<Characteristic>(0, kChrcType16, 0, 0, kReadReqs, kWriteReqs, kUpdateReqs)); |
| |
| EXPECT_EQ(0u, RegisterService(&mgr, std::move(service))); |
| } |
| |
| TEST(LocalServiceManagerTest, DuplicateDescIds) { |
| LocalServiceManager mgr; |
| const att::AccessRequirements kReadReqs, kWriteReqs, kUpdateReqs; |
| |
| constexpr UUID kChrcType16(uint16_t{0x1234}); |
| constexpr UUID kDescType16(uint16_t{0x5678}); |
| |
| auto service = std::make_unique<Service>(/*primary=*/true, kTestType16); |
| |
| // Use same descriptor ID twice. |
| auto chrc = |
| std::make_unique<Characteristic>(0, kChrcType16, 0, 0, kReadReqs, kWriteReqs, kUpdateReqs); |
| chrc->AddDescriptor(std::make_unique<Descriptor>(1, kDescType16, kReadReqs, kWriteReqs)); |
| chrc->AddDescriptor(std::make_unique<Descriptor>(1, kDescType16, kReadReqs, kWriteReqs)); |
| service->AddCharacteristic(std::move(chrc)); |
| |
| EXPECT_EQ(0u, RegisterService(&mgr, std::move(service))); |
| } |
| |
| TEST(LocalServiceManagerTest, DuplicateChrcAndDescIds) { |
| LocalServiceManager mgr; |
| const att::AccessRequirements kReadReqs, kWriteReqs, kUpdateReqs; |
| |
| constexpr UUID kChrcType16(uint16_t{0x1234}); |
| constexpr UUID kDescType16(uint16_t{0x5678}); |
| |
| auto service = std::make_unique<Service>(/*primary=*/true, kTestType16); |
| |
| // Use same descriptor ID twice. |
| auto chrc = |
| std::make_unique<Characteristic>(0, kChrcType16, 0, 0, kReadReqs, kWriteReqs, kUpdateReqs); |
| chrc->AddDescriptor(std::make_unique<Descriptor>(0, kDescType16, kReadReqs, kWriteReqs)); |
| service->AddCharacteristic(std::move(chrc)); |
| |
| EXPECT_EQ(0u, RegisterService(&mgr, std::move(service))); |
| } |
| |
| TEST(LocalServiceManagerTest, ReadCharacteristicNoReadPermission) { |
| LocalServiceManager mgr; |
| const att::AccessRequirements kReadReqs, kWriteReqs, kUpdateReqs; |
| constexpr UUID kChrcType16(uint16_t{0x1234}); |
| constexpr IdType kChrcId = 5; |
| |
| auto service = std::make_unique<Service>(/*primary=*/true, kTestType16); |
| service->AddCharacteristic(std::make_unique<Characteristic>( |
| kChrcId, kChrcType16, Property::kRead, 0, kReadReqs, kWriteReqs, kUpdateReqs)); |
| |
| bool called = false; |
| auto read_cb = [&called](auto, auto, auto, auto, auto) { called = true; }; |
| |
| EXPECT_NE(0u, RegisterService(&mgr, std::move(service), std::move(read_cb))); |
| |
| auto* attr = mgr.database()->FindAttribute(kFirstChrcValueHandle); |
| ASSERT_TRUE(attr); |
| EXPECT_EQ(kChrcType16, attr->type()); |
| |
| bool result_called = false; |
| auto result_cb = [&result_called](auto, const auto&) { result_called = true; }; |
| |
| EXPECT_FALSE(attr->ReadAsync(kTestPeerId, 0, std::move(result_cb))); |
| EXPECT_FALSE(called); |
| EXPECT_FALSE(result_called); |
| } |
| |
| TEST(LocalServiceManagerTest, ReadCharacteristicNoReadProperty) { |
| LocalServiceManager mgr; |
| constexpr UUID kChrcType16(uint16_t{0x1234}); |
| constexpr IdType kChrcId = 5; |
| |
| // Characteristic is readable but doesn't have the "read" property. |
| auto kReadReqs = AllowedNoSecurity(); |
| const att::AccessRequirements kWriteReqs, kUpdateReqs; |
| |
| auto service = std::make_unique<Service>(/*primary=*/true, kTestType16); |
| service->AddCharacteristic(std::make_unique<Characteristic>(kChrcId, kChrcType16, 0, 0, kReadReqs, |
| kWriteReqs, kUpdateReqs)); |
| |
| bool called = false; |
| auto read_cb = [&called](auto, auto, auto, auto, auto) { called = true; }; |
| |
| EXPECT_NE(0u, RegisterService(&mgr, std::move(service), std::move(read_cb))); |
| |
| auto* attr = mgr.database()->FindAttribute(kFirstChrcValueHandle); |
| ASSERT_TRUE(attr); |
| EXPECT_EQ(kChrcType16, attr->type()); |
| |
| fitx::result<att::ErrorCode> status = fitx::ok(); |
| auto result_cb = [&status](auto cb_status, const auto&) { status = cb_status; }; |
| |
| EXPECT_TRUE(attr->ReadAsync(kTestPeerId, 0, std::move(result_cb))); |
| |
| // The error should be handled internally and not reach |read_cb|. |
| EXPECT_FALSE(called); |
| ASSERT_TRUE(status.is_error()); |
| EXPECT_EQ(att::ErrorCode::kReadNotPermitted, status.error_value()); |
| } |
| |
| TEST(LocalServiceManagerTest, ReadCharacteristic) { |
| LocalServiceManager mgr; |
| constexpr UUID kChrcType16(uint16_t{0x1234}); |
| constexpr IdType kChrcId = 5; |
| constexpr uint16_t kOffset = 10; |
| |
| const StaticByteBuffer kTestValue('f', 'o', 'o'); |
| |
| auto kReadReqs = AllowedNoSecurity(); |
| const att::AccessRequirements kWriteReqs, kUpdateReqs; |
| |
| auto service = std::make_unique<Service>(/*primary=*/true, kTestType16); |
| service->AddCharacteristic(std::make_unique<Characteristic>( |
| kChrcId, kChrcType16, Property::kRead, 0, kReadReqs, kWriteReqs, kUpdateReqs)); |
| |
| bool called = false; |
| IdType svc_id; |
| auto read_cb = [&](PeerId peer_id, auto cb_svc_id, auto id, auto offset, auto responder) { |
| called = true; |
| EXPECT_EQ(kTestPeerId, peer_id); |
| EXPECT_EQ(svc_id, cb_svc_id); |
| EXPECT_EQ(kChrcId, id); |
| EXPECT_EQ(kOffset, offset); |
| responder(fitx::ok(), kTestValue); |
| }; |
| |
| svc_id = RegisterService(&mgr, std::move(service), std::move(read_cb)); |
| ASSERT_NE(0u, svc_id); |
| |
| auto* attr = mgr.database()->FindAttribute(kFirstChrcValueHandle); |
| ASSERT_TRUE(attr); |
| EXPECT_EQ(kChrcType16, attr->type()); |
| |
| fitx::result<att::ErrorCode> status = fitx::error(att::ErrorCode::kUnlikelyError); |
| auto result_cb = [&status, &kTestValue](auto cb_status, const auto& value) { |
| status = cb_status; |
| EXPECT_TRUE(ContainersEqual(kTestValue, value)); |
| }; |
| |
| EXPECT_TRUE(attr->ReadAsync(kTestPeerId, kOffset, std::move(result_cb))); |
| |
| EXPECT_TRUE(called); |
| EXPECT_EQ(fitx::ok(), status); |
| } |
| |
| TEST(LocalServiceManagerTest, WriteCharacteristicNoWritePermission) { |
| LocalServiceManager mgr; |
| const att::AccessRequirements kReadReqs, kWriteReqs, kUpdateReqs; |
| constexpr UUID kChrcType16(uint16_t{0x1234}); |
| constexpr IdType kChrcId = 5; |
| const BufferView kTestValue; |
| |
| auto service = std::make_unique<Service>(/*primary=*/true, kTestType16); |
| service->AddCharacteristic(std::make_unique<Characteristic>( |
| kChrcId, kChrcType16, Property::kWrite, 0, kReadReqs, kWriteReqs, kUpdateReqs)); |
| |
| bool called = false; |
| auto write_cb = [&called](auto, auto, auto, auto, auto&, auto) { called = true; }; |
| |
| EXPECT_NE(0u, RegisterService(&mgr, std::move(service), NopReadHandler, std::move(write_cb))); |
| |
| auto* attr = mgr.database()->FindAttribute(kFirstChrcValueHandle); |
| ASSERT_TRUE(attr); |
| EXPECT_EQ(kChrcType16, attr->type()); |
| |
| bool result_called = false; |
| auto result_cb = [&result_called](auto) { result_called = true; }; |
| |
| EXPECT_FALSE(attr->WriteAsync(kTestPeerId, 0, kTestValue, std::move(result_cb))); |
| EXPECT_FALSE(called); |
| EXPECT_FALSE(result_called); |
| } |
| |
| TEST(LocalServiceManagerTest, WriteCharacteristicNoWriteProperty) { |
| LocalServiceManager mgr; |
| constexpr UUID kChrcType16(uint16_t{0x1234}); |
| constexpr IdType kChrcId = 5; |
| const BufferView kTestValue; |
| |
| const att::AccessRequirements kReadReqs, kUpdateReqs; |
| auto kWriteReqs = AllowedNoSecurity(); |
| |
| auto service = std::make_unique<Service>(/*primary=*/true, kTestType16); |
| service->AddCharacteristic(std::make_unique<Characteristic>(kChrcId, kChrcType16, 0, 0, kReadReqs, |
| kWriteReqs, kUpdateReqs)); |
| |
| bool called = false; |
| auto write_cb = [&called](auto, auto, auto, auto, auto&, auto) { called = true; }; |
| |
| EXPECT_NE(0u, RegisterService(&mgr, std::move(service), NopReadHandler, std::move(write_cb))); |
| |
| auto* attr = mgr.database()->FindAttribute(kFirstChrcValueHandle); |
| ASSERT_TRUE(attr); |
| EXPECT_EQ(kChrcType16, attr->type()); |
| |
| fitx::result<att::ErrorCode> status = fitx::ok(); |
| auto result_cb = [&status](auto cb_status) { status = cb_status; }; |
| |
| EXPECT_TRUE(attr->WriteAsync(kTestPeerId, 0, kTestValue, std::move(result_cb))); |
| |
| // The error should be handled internally and not reach |write_cb|. |
| EXPECT_FALSE(called); |
| ASSERT_TRUE(status.is_error()); |
| EXPECT_EQ(att::ErrorCode::kWriteNotPermitted, status.error_value()); |
| } |
| |
| TEST(LocalServiceManagerTest, WriteCharacteristic) { |
| LocalServiceManager mgr; |
| constexpr UUID kChrcType16(uint16_t{0x1234}); |
| constexpr IdType kChrcId = 5; |
| constexpr uint16_t kOffset = 10; |
| |
| const StaticByteBuffer kTestValue('f', 'o', 'o'); |
| |
| const att::AccessRequirements kReadReqs, kUpdateReqs; |
| auto kWriteReqs = AllowedNoSecurity(); |
| |
| auto service = std::make_unique<Service>(/*primary=*/true, kTestType16); |
| service->AddCharacteristic(std::make_unique<Characteristic>( |
| kChrcId, kChrcType16, Property::kWrite, 0, kReadReqs, kWriteReqs, kUpdateReqs)); |
| |
| bool called = false; |
| IdType svc_id; |
| auto write_cb = [&](PeerId peer_id, auto cb_svc_id, auto id, auto offset, const auto& value, |
| auto responder) { |
| called = true; |
| EXPECT_EQ(kTestPeerId, peer_id); |
| EXPECT_EQ(svc_id, cb_svc_id); |
| EXPECT_EQ(kChrcId, id); |
| EXPECT_EQ(kOffset, offset); |
| EXPECT_TRUE(ContainersEqual(kTestValue, value)); |
| responder(fitx::ok()); |
| }; |
| |
| svc_id = RegisterService(&mgr, std::move(service), NopReadHandler, std::move(write_cb)); |
| ASSERT_NE(0u, svc_id); |
| |
| auto* attr = mgr.database()->FindAttribute(kFirstChrcValueHandle); |
| ASSERT_TRUE(attr); |
| EXPECT_EQ(kChrcType16, attr->type()); |
| |
| fitx::result<att::ErrorCode> status = fitx::error(att::ErrorCode::kUnlikelyError); |
| auto result_cb = [&status](auto cb_status) { status = cb_status; }; |
| |
| EXPECT_TRUE(attr->WriteAsync(kTestPeerId, kOffset, kTestValue, std::move(result_cb))); |
| |
| EXPECT_TRUE(called); |
| EXPECT_EQ(fitx::ok(), status); |
| } |
| |
| TEST(LocalServiceManagerTest, ReadDescriptorNoReadPermission) { |
| LocalServiceManager mgr; |
| const att::AccessRequirements kReadReqs, kWriteReqs, kUpdateReqs; |
| constexpr UUID kChrcType16(uint16_t{0x1234}); |
| constexpr UUID kDescType16(uint16_t{0x5678}); |
| constexpr IdType kChrcId = 0; |
| constexpr IdType kDescId = 1; |
| |
| auto service = std::make_unique<Service>(/*primary=*/true, kTestType16); |
| auto chrc = std::make_unique<Characteristic>(kChrcId, kChrcType16, 0, 0, kReadReqs, kWriteReqs, |
| kUpdateReqs); |
| chrc->AddDescriptor(std::make_unique<Descriptor>(kDescId, kDescType16, kReadReqs, kWriteReqs)); |
| service->AddCharacteristic(std::move(chrc)); |
| |
| bool called = false; |
| auto read_cb = [&called](auto, auto, auto, auto, auto) { called = true; }; |
| |
| EXPECT_NE(0u, RegisterService(&mgr, std::move(service), std::move(read_cb))); |
| |
| auto* attr = mgr.database()->FindAttribute(kFirstDescrHandle); |
| ASSERT_TRUE(attr); |
| EXPECT_EQ(kDescType16, attr->type()); |
| |
| bool result_called = false; |
| auto result_cb = [&result_called](auto, const auto&) { result_called = true; }; |
| |
| EXPECT_FALSE(attr->ReadAsync(kTestPeerId, 0, std::move(result_cb))); |
| EXPECT_FALSE(called); |
| EXPECT_FALSE(result_called); |
| } |
| |
| TEST(LocalServiceManagerTest, ReadDescriptor) { |
| LocalServiceManager mgr; |
| constexpr UUID kChrcType16(uint16_t{0x1234}); |
| constexpr UUID kDescType16(uint16_t{0x5678}); |
| constexpr IdType kChrcId = 0; |
| constexpr IdType kDescId = 1; |
| constexpr uint16_t kOffset = 10; |
| |
| const StaticByteBuffer kTestValue('f', 'o', 'o'); |
| |
| auto kReadReqs = AllowedNoSecurity(); |
| const att::AccessRequirements kWriteReqs, kUpdateReqs; |
| |
| auto service = std::make_unique<Service>(/*primary=*/true, kTestType16); |
| auto chrc = std::make_unique<Characteristic>(kChrcId, kChrcType16, 0, 0, kReadReqs, kWriteReqs, |
| kUpdateReqs); |
| chrc->AddDescriptor(std::make_unique<Descriptor>(kDescId, kDescType16, kReadReqs, kReadReqs)); |
| service->AddCharacteristic(std::move(chrc)); |
| |
| bool called = false; |
| IdType svc_id; |
| auto read_cb = [&](PeerId peer_id, auto cb_svc_id, auto id, auto offset, auto responder) { |
| called = true; |
| EXPECT_EQ(kTestPeerId, peer_id); |
| EXPECT_EQ(svc_id, cb_svc_id); |
| EXPECT_EQ(kDescId, id); |
| EXPECT_EQ(kOffset, offset); |
| responder(fitx::ok(), kTestValue); |
| }; |
| |
| svc_id = RegisterService(&mgr, std::move(service), std::move(read_cb)); |
| ASSERT_NE(0u, svc_id); |
| |
| auto* attr = mgr.database()->FindAttribute(kFirstDescrHandle); |
| ASSERT_TRUE(attr); |
| EXPECT_EQ(kDescType16, attr->type()); |
| |
| fitx::result<att::ErrorCode> status = fitx::error(att::ErrorCode::kUnlikelyError); |
| auto result_cb = [&status, &kTestValue](auto cb_status, const auto& value) { |
| status = cb_status; |
| EXPECT_TRUE(ContainersEqual(kTestValue, value)); |
| }; |
| |
| EXPECT_TRUE(attr->ReadAsync(kTestPeerId, kOffset, std::move(result_cb))); |
| |
| EXPECT_TRUE(called); |
| EXPECT_EQ(fitx::ok(), status); |
| } |
| |
| TEST(LocalServiceManagerTest, WriteDescriptorNoWritePermission) { |
| LocalServiceManager mgr; |
| const att::AccessRequirements kReadReqs, kWriteReqs, kUpdateReqs; |
| constexpr UUID kChrcType16(uint16_t{0x1234}); |
| constexpr UUID kDescType16(uint16_t{0x5678}); |
| constexpr IdType kChrcId = 0; |
| constexpr IdType kDescId = 1; |
| const BufferView kTestValue; |
| |
| auto service = std::make_unique<Service>(/*primary=*/true, kTestType16); |
| auto chrc = std::make_unique<Characteristic>(kChrcId, kChrcType16, 0, 0, kReadReqs, kWriteReqs, |
| kUpdateReqs); |
| chrc->AddDescriptor(std::make_unique<Descriptor>(kDescId, kDescType16, kReadReqs, kWriteReqs)); |
| service->AddCharacteristic(std::move(chrc)); |
| |
| bool called = false; |
| auto write_cb = [&called](auto, auto, auto, auto, auto&, auto) { called = true; }; |
| |
| EXPECT_NE(0u, RegisterService(&mgr, std::move(service), NopReadHandler, write_cb)); |
| |
| auto* attr = mgr.database()->FindAttribute(kFirstDescrHandle); |
| ASSERT_TRUE(attr); |
| EXPECT_EQ(kDescType16, attr->type()); |
| |
| bool result_called = false; |
| auto result_cb = [&result_called](auto) { result_called = true; }; |
| |
| EXPECT_FALSE(attr->WriteAsync(kTestPeerId, 0, kTestValue, result_cb)); |
| EXPECT_FALSE(called); |
| EXPECT_FALSE(result_called); |
| } |
| |
| TEST(LocalServiceManagerTest, WriteDescriptor) { |
| LocalServiceManager mgr; |
| constexpr UUID kChrcType16(uint16_t{0x1234}); |
| constexpr UUID kDescType16(uint16_t{0x5678}); |
| constexpr IdType kChrcId = 0; |
| constexpr IdType kDescId = 1; |
| constexpr uint16_t kOffset = 10; |
| |
| const StaticByteBuffer kTestValue('f', 'o', 'o'); |
| |
| const att::AccessRequirements kReadReqs, kUpdateReqs; |
| auto kWriteReqs = AllowedNoSecurity(); |
| |
| auto service = std::make_unique<Service>(/*primary=*/true, kTestType16); |
| auto chrc = std::make_unique<Characteristic>(kChrcId, kChrcType16, 0, 0, kReadReqs, kWriteReqs, |
| kUpdateReqs); |
| chrc->AddDescriptor(std::make_unique<Descriptor>(kDescId, kDescType16, kReadReqs, kWriteReqs)); |
| service->AddCharacteristic(std::move(chrc)); |
| |
| bool called = false; |
| IdType svc_id; |
| auto write_cb = [&](PeerId peer_id, auto cb_svc_id, auto id, auto offset, const auto& value, |
| auto responder) { |
| called = true; |
| EXPECT_EQ(kTestPeerId, peer_id); |
| EXPECT_EQ(svc_id, cb_svc_id); |
| EXPECT_EQ(kDescId, id); |
| EXPECT_EQ(kOffset, offset); |
| EXPECT_TRUE(ContainersEqual(kTestValue, value)); |
| responder(fitx::ok()); |
| }; |
| |
| svc_id = RegisterService(&mgr, std::move(service), NopReadHandler, write_cb); |
| ASSERT_NE(0u, svc_id); |
| |
| auto* attr = mgr.database()->FindAttribute(kFirstDescrHandle); |
| ASSERT_TRUE(attr); |
| EXPECT_EQ(kDescType16, attr->type()); |
| |
| fitx::result<att::ErrorCode> status = fitx::error(att::ErrorCode::kUnlikelyError); |
| auto result_cb = [&status](auto cb_status) { status = cb_status; }; |
| |
| EXPECT_TRUE(attr->WriteAsync(kTestPeerId, kOffset, kTestValue, result_cb)); |
| |
| EXPECT_TRUE(called); |
| EXPECT_EQ(fitx::ok(), status); |
| } |
| |
| TEST(LocalServiceManagerTest, ServiceChanged) { |
| IdType expected_id; |
| att::Handle expected_start, expected_end; |
| int callback_count = 0; |
| ServiceChangedCallback service_changed_callback = [&](IdType id, att::Handle start, |
| att::Handle end) { |
| callback_count++; |
| EXPECT_EQ(expected_id, id); |
| EXPECT_EQ(expected_start, start); |
| EXPECT_EQ(expected_end, end); |
| }; |
| |
| LocalServiceManager mgr; |
| |
| // Other tests add and remove services without the callback so there is |
| // already the expectation that they work. |
| mgr.set_service_changed_callback(std::move(service_changed_callback)); |
| |
| expected_id = 1u; |
| expected_start = 1u; |
| expected_end = 1u; |
| auto service = std::make_unique<Service>(/*primary=*/true, kTestType16); |
| auto id1 = RegisterService(&mgr, std::move(service)); |
| EXPECT_NE(0u, id1); |
| EXPECT_EQ(1, callback_count); |
| |
| expected_start = 2u; |
| expected_end = 2u; |
| service = std::make_unique<Service>(/*primary=*/false, kTestType32); |
| |
| constexpr IdType kChrcId = 0; |
| constexpr uint8_t kChrcProps = Property::kRead; |
| constexpr UUID kTestChrcType(uint32_t{0xdeadbeef}); |
| const att::AccessRequirements kReadReqs(/*encryption=*/true, /*authentication=*/true, |
| /*authorization=*/true); |
| const att::AccessRequirements kWriteReqs, kUpdateReqs; |
| service->AddCharacteristic(std::make_unique<Characteristic>(kChrcId, kTestChrcType, kChrcProps, 0, |
| kReadReqs, kWriteReqs, kUpdateReqs)); |
| expected_id = 2u; |
| expected_start = 2u; |
| expected_end = 4u; |
| auto id2 = RegisterService(&mgr, std::move(service)); |
| EXPECT_NE(0u, id2); |
| EXPECT_EQ(2, callback_count); |
| |
| expected_id = id1; |
| expected_start = 1u; |
| expected_end = 1u; |
| EXPECT_TRUE(mgr.UnregisterService(id1)); |
| EXPECT_EQ(3, callback_count); |
| |
| expected_id = id2; |
| expected_start = 2u; |
| expected_end = 4u; |
| EXPECT_TRUE(mgr.UnregisterService(id2)); |
| EXPECT_EQ(4, callback_count); |
| } |
| |
| // TODO(armansito): Some functional groupings of tests above (such as |
| // ReadCharacteristic, WriteCharacteristic, etc) should each use a common test |
| // harness to reduce code duplication. |
| class LocalClientCharacteristicConfigurationTest : public ::testing::Test { |
| protected: |
| LocalServiceManager mgr; |
| |
| // The CCC descriptor is set up as the first descriptor of the first |
| // characteristic of the first service: |
| // 0x0001: service decl. |
| // 0x0002: characteristic decl. |
| // 0x0003: characteristic value |
| // 0x0004: CCC descriptor |
| const att::Handle kChrcHandle = 0x0003; |
| const att::Handle kCCCHandle = 0x0004; |
| |
| const IdType kChrcId = 0; |
| |
| const uint16_t kEnableNot = 0x0001; |
| const uint16_t kEnableInd = 0x0002; |
| |
| int ccc_callback_count = 0; |
| IdType last_service_id = 0u; |
| PeerId last_peer_id = PeerId(kInvalidId); |
| bool last_notify = false; |
| bool last_indicate = false; |
| |
| void BuildService(uint8_t props, const att::AccessRequirements& update_reqs) { |
| const att::AccessRequirements kReqs; |
| auto service = std::make_unique<Service>(/*is_primary=*/true, kTestType16); |
| service->AddCharacteristic(std::make_unique<Characteristic>(kChrcId, kTestType32, props, 0, |
| kReqs, kReqs, update_reqs)); |
| auto ccc_callback = [this](IdType cb_svc_id, IdType id, PeerId peer_id, bool notify, |
| bool indicate) { |
| ccc_callback_count++; |
| EXPECT_EQ(last_service_id, cb_svc_id); |
| EXPECT_EQ(kChrcId, id); |
| last_peer_id = peer_id; |
| last_notify = notify; |
| last_indicate = indicate; |
| }; |
| |
| const size_t last_service_count = mgr.database()->groupings().size(); |
| last_service_id = |
| mgr.RegisterService(std::move(service), NopReadHandler, NopWriteHandler, ccc_callback); |
| EXPECT_NE(0u, last_service_id); |
| EXPECT_EQ(last_service_count + 1u, mgr.database()->groupings().size()); |
| } |
| |
| bool ReadCcc(const att::Attribute* attr, PeerId peer_id, fitx::result<att::ErrorCode>* out_status, |
| uint16_t* out_value) { |
| ZX_ASSERT(attr); |
| ZX_ASSERT(out_status); |
| ZX_ASSERT(out_value); |
| |
| auto result_cb = [&out_status, &out_value](auto cb_status, const auto& value) { |
| *out_status = cb_status; |
| EXPECT_EQ(2u, value.size()); |
| |
| if (value.size() == 2u) { |
| *out_value = le16toh(value.template To<uint16_t>()); |
| } |
| }; |
| |
| return attr->ReadAsync(peer_id, 0u, result_cb); |
| } |
| |
| bool WriteCcc(const att::Attribute* attr, PeerId peer_id, uint16_t ccc_value, |
| fitx::result<att::ErrorCode>* out_status) { |
| ZX_ASSERT(attr); |
| ZX_ASSERT(out_status); |
| |
| auto result_cb = [&out_status](auto cb_status) { *out_status = cb_status; }; |
| uint16_t value = htole16(ccc_value); |
| return attr->WriteAsync(peer_id, 0u, BufferView(&value, sizeof(value)), result_cb); |
| } |
| }; |
| |
| TEST_F(LocalClientCharacteristicConfigurationTest, UpdatePermissions) { |
| // Require authentication. This should have no bearing on reads but it should |
| // prevent writes. |
| const att::AccessRequirements kUpdateReqs(/*encryption=*/false, /*authentication=*/true, |
| /*authorization=*/false); |
| constexpr uint8_t kProps = Property::kNotify; |
| BuildService(kProps, kUpdateReqs); |
| |
| auto* attr = mgr.database()->FindAttribute(kCCCHandle); |
| ASSERT_TRUE(attr); |
| EXPECT_EQ(types::kClientCharacteristicConfig, attr->type()); |
| |
| EXPECT_FALSE(attr->read_reqs().authentication_required()); |
| EXPECT_TRUE(attr->write_reqs().authentication_required()); |
| } |
| |
| TEST_F(LocalClientCharacteristicConfigurationTest, IndicationNotSupported) { |
| // No security required to enable notifications. |
| auto kUpdateReqs = AllowedNoSecurity(); |
| constexpr uint8_t kProps = Property::kNotify; |
| BuildService(kProps, kUpdateReqs); |
| |
| auto* attr = mgr.database()->FindAttribute(kCCCHandle); |
| ASSERT_TRUE(attr); |
| EXPECT_EQ(types::kClientCharacteristicConfig, attr->type()); |
| |
| // Enabling indications should fail as the characteristic only supports notifications. |
| fitx::result<att::ErrorCode> status = fitx::ok(); |
| EXPECT_TRUE(WriteCcc(attr, kTestPeerId, kEnableInd, &status)); |
| ASSERT_TRUE(status.is_error()); |
| EXPECT_EQ(att::ErrorCode::kWriteNotPermitted, status.error_value()); |
| |
| uint16_t ccc_value; |
| |
| // Notifications and indications for this device should remain disabled. |
| EXPECT_TRUE(ReadCcc(attr, kTestPeerId, &status, &ccc_value)); |
| EXPECT_EQ(fitx::ok(), status); |
| EXPECT_EQ(0x0000, ccc_value); |
| } |
| |
| TEST_F(LocalClientCharacteristicConfigurationTest, NotificationNotSupported) { |
| // No security. |
| auto kUpdateReqs = AllowedNoSecurity(); |
| constexpr uint8_t kProps = Property::kIndicate; |
| BuildService(kProps, kUpdateReqs); |
| |
| auto* attr = mgr.database()->FindAttribute(kCCCHandle); |
| ASSERT_TRUE(attr); |
| EXPECT_EQ(types::kClientCharacteristicConfig, attr->type()); |
| |
| // Enabling notifications should fail as the characteristic only supports indications. |
| fitx::result<att::ErrorCode> status = fitx::ok(); |
| EXPECT_TRUE(WriteCcc(attr, kTestPeerId, kEnableNot, &status)); |
| ASSERT_TRUE(status.is_error()); |
| EXPECT_EQ(att::ErrorCode::kWriteNotPermitted, status.error_value()); |
| |
| uint16_t ccc_value; |
| |
| // Notifications and indications for this device should remain disabled. |
| EXPECT_TRUE(ReadCcc(attr, kTestPeerId, &status, &ccc_value)); |
| EXPECT_EQ(fitx::ok(), status); |
| EXPECT_EQ(0x0000, ccc_value); |
| } |
| |
| TEST_F(LocalClientCharacteristicConfigurationTest, EnableNotify) { |
| // No security. |
| auto kUpdateReqs = AllowedNoSecurity(); |
| constexpr uint8_t kProps = Property::kNotify; |
| BuildService(kProps, kUpdateReqs); |
| |
| auto* attr = mgr.database()->FindAttribute(kCCCHandle); |
| ASSERT_TRUE(attr); |
| EXPECT_EQ(types::kClientCharacteristicConfig, attr->type()); |
| |
| LocalServiceManager::ClientCharacteristicConfig config; |
| EXPECT_FALSE(mgr.GetCharacteristicConfig(last_service_id, kChrcId, kTestPeerId, &config)); |
| |
| fitx::result<att::ErrorCode> status = fitx::error(att::ErrorCode::kUnlikelyError); |
| EXPECT_TRUE(WriteCcc(attr, kTestPeerId, kEnableNot, &status)); |
| EXPECT_EQ(fitx::ok(), status); |
| |
| uint16_t ccc_value; |
| |
| // Notifications should be enabled for kTestPeerId. |
| EXPECT_TRUE(ReadCcc(attr, kTestPeerId, &status, &ccc_value)); |
| EXPECT_EQ(fitx::ok(), status); |
| EXPECT_EQ(kEnableNot, ccc_value); |
| |
| EXPECT_TRUE(mgr.GetCharacteristicConfig(last_service_id, kChrcId, kTestPeerId, &config)); |
| EXPECT_EQ(kChrcHandle, config.handle); |
| EXPECT_TRUE(config.notify); |
| EXPECT_FALSE(config.indicate); |
| |
| // ..but not for kTestPeerId2. |
| EXPECT_TRUE(ReadCcc(attr, kTestPeerId2, &status, &ccc_value)); |
| EXPECT_EQ(fitx::ok(), status); |
| EXPECT_EQ(0x0000, ccc_value); |
| |
| // A set configurations now exists for |kChrcId| but kTestPeerId2 should |
| // appear as unsubscribed. |
| EXPECT_TRUE(mgr.GetCharacteristicConfig(last_service_id, kChrcId, kTestPeerId2, &config)); |
| EXPECT_EQ(kChrcHandle, config.handle); |
| EXPECT_FALSE(config.notify); |
| EXPECT_FALSE(config.indicate); |
| |
| // The callback should have been notified. |
| EXPECT_EQ(1, ccc_callback_count); |
| EXPECT_EQ(kTestPeerId, last_peer_id); |
| EXPECT_TRUE(last_notify); |
| EXPECT_FALSE(last_indicate); |
| |
| // Enable notifications again. The write should succeed but the callback |
| // should not get called as the value will remain unchanged. |
| EXPECT_TRUE(WriteCcc(attr, kTestPeerId, kEnableNot, &status)); |
| EXPECT_EQ(fitx::ok(), status); |
| EXPECT_EQ(1, ccc_callback_count); |
| } |
| |
| TEST_F(LocalClientCharacteristicConfigurationTest, EnableIndicate) { |
| // No security. |
| auto kUpdateReqs = AllowedNoSecurity(); |
| constexpr uint8_t kProps = Property::kIndicate; |
| BuildService(kProps, kUpdateReqs); |
| |
| auto* attr = mgr.database()->FindAttribute(kCCCHandle); |
| ASSERT_TRUE(attr); |
| EXPECT_EQ(types::kClientCharacteristicConfig, attr->type()); |
| |
| fitx::result<att::ErrorCode> status = fitx::error(att::ErrorCode::kUnlikelyError); |
| EXPECT_TRUE(WriteCcc(attr, kTestPeerId, kEnableInd, &status)); |
| EXPECT_EQ(fitx::ok(), status); |
| |
| uint16_t ccc_value; |
| |
| // Indications should be enabled for kTestPeerId. |
| EXPECT_TRUE(ReadCcc(attr, kTestPeerId, &status, &ccc_value)); |
| EXPECT_EQ(fitx::ok(), status); |
| EXPECT_EQ(kEnableInd, ccc_value); |
| |
| LocalServiceManager::ClientCharacteristicConfig config; |
| EXPECT_TRUE(mgr.GetCharacteristicConfig(last_service_id, kChrcId, kTestPeerId, &config)); |
| EXPECT_EQ(kChrcHandle, config.handle); |
| EXPECT_FALSE(config.notify); |
| EXPECT_TRUE(config.indicate); |
| |
| // ..but not for kTestPeerId2. |
| EXPECT_TRUE(ReadCcc(attr, kTestPeerId2, &status, &ccc_value)); |
| EXPECT_EQ(fitx::ok(), status); |
| EXPECT_EQ(0x0000, ccc_value); |
| |
| // A set configurations now exists for |kChrcId| but kTestPeerId2 should |
| // appear as unsubscribed. |
| EXPECT_TRUE(mgr.GetCharacteristicConfig(last_service_id, kChrcId, kTestPeerId2, &config)); |
| EXPECT_EQ(kChrcHandle, config.handle); |
| EXPECT_FALSE(config.notify); |
| EXPECT_FALSE(config.indicate); |
| |
| // The callback should have been notified. |
| EXPECT_EQ(1, ccc_callback_count); |
| EXPECT_EQ(kTestPeerId, last_peer_id); |
| EXPECT_FALSE(last_notify); |
| EXPECT_TRUE(last_indicate); |
| |
| // Enable indications again. The write should succeed but the callback |
| // should not get called as the value will remain unchanged. |
| EXPECT_TRUE(WriteCcc(attr, kTestPeerId, kEnableInd, &status)); |
| EXPECT_EQ(fitx::ok(), status); |
| EXPECT_EQ(1, ccc_callback_count); |
| } |
| |
| TEST_F(LocalClientCharacteristicConfigurationTest, DisconnectCleanup) { |
| // No security. |
| auto kUpdateReqs = AllowedNoSecurity(); |
| constexpr uint8_t kProps = Property::kNotify; |
| BuildService(kProps, kUpdateReqs); |
| |
| auto* attr = mgr.database()->FindAttribute(kCCCHandle); |
| ASSERT_TRUE(attr); |
| EXPECT_EQ(types::kClientCharacteristicConfig, attr->type()); |
| |
| LocalServiceManager::ClientCharacteristicConfig config; |
| EXPECT_FALSE(mgr.GetCharacteristicConfig(last_service_id, kChrcId, kTestPeerId, &config)); |
| |
| fitx::result<att::ErrorCode> status = fitx::error(att::ErrorCode::kUnlikelyError); |
| EXPECT_TRUE(WriteCcc(attr, kTestPeerId, kEnableNot, &status)); |
| EXPECT_EQ(fitx::ok(), status); |
| |
| // The callback should have been notified. |
| EXPECT_EQ(1, ccc_callback_count); |
| EXPECT_EQ(kTestPeerId, last_peer_id); |
| EXPECT_TRUE(last_notify); |
| EXPECT_FALSE(last_indicate); |
| |
| mgr.DisconnectClient(kTestPeerId); |
| |
| uint16_t ccc_value; |
| // Reads should succeed but notifications should be disabled. |
| EXPECT_TRUE(ReadCcc(attr, kTestPeerId, &status, &ccc_value)); |
| EXPECT_EQ(fitx::ok(), status); |
| EXPECT_EQ(0x0000, ccc_value); |
| |
| // The callback should not have been called on client disconnect. |
| EXPECT_EQ(1, ccc_callback_count); |
| EXPECT_EQ(kTestPeerId, last_peer_id); |
| EXPECT_TRUE(last_notify); |
| EXPECT_FALSE(last_indicate); |
| |
| mgr.DisconnectClient(kTestPeerId2); |
| |
| // The callback should not be called on client disconnect. |
| EXPECT_EQ(1, ccc_callback_count); |
| EXPECT_EQ(kTestPeerId, last_peer_id); |
| } |
| |
| } // namespace |
| } // namespace bt::gatt |