blob: 19f55a47587aa47a92e710fdf7e142c9da7d96e4 [file] [log] [blame]
// 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/public/pw_bluetooth_sapphire/internal/host/gatt/local_service_manager.h"
#include <gtest/gtest.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/common/assert.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"
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());
fit::result<att::ErrorCode> status = fit::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(fit::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());
fit::result<att::ErrorCode> status =
fit::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(fit::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());
fit::result<att::ErrorCode> status = fit::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(fit::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());
fit::result<att::ErrorCode> status =
fit::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(fit::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(fit::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());
fit::result<att::ErrorCode> status =
fit::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(fit::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(fit::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());
fit::result<att::ErrorCode> status =
fit::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(fit::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,
fit::result<att::ErrorCode>* out_status,
uint16_t* out_value) {
BT_ASSERT(attr);
BT_ASSERT(out_status);
BT_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,
fit::result<att::ErrorCode>* out_status) {
BT_ASSERT(attr);
BT_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.
fit::result<att::ErrorCode> status = fit::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(fit::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.
fit::result<att::ErrorCode> status = fit::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(fit::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));
fit::result<att::ErrorCode> status =
fit::error(att::ErrorCode::kUnlikelyError);
EXPECT_TRUE(WriteCcc(attr, kTestPeerId, kEnableNot, &status));
EXPECT_EQ(fit::ok(), status);
uint16_t ccc_value;
// Notifications should be enabled for kTestPeerId.
EXPECT_TRUE(ReadCcc(attr, kTestPeerId, &status, &ccc_value));
EXPECT_EQ(fit::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(fit::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(fit::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());
fit::result<att::ErrorCode> status =
fit::error(att::ErrorCode::kUnlikelyError);
EXPECT_TRUE(WriteCcc(attr, kTestPeerId, kEnableInd, &status));
EXPECT_EQ(fit::ok(), status);
uint16_t ccc_value;
// Indications should be enabled for kTestPeerId.
EXPECT_TRUE(ReadCcc(attr, kTestPeerId, &status, &ccc_value));
EXPECT_EQ(fit::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(fit::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(fit::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));
fit::result<att::ErrorCode> status =
fit::error(att::ErrorCode::kUnlikelyError);
EXPECT_TRUE(WriteCcc(attr, kTestPeerId, kEnableNot, &status));
EXPECT_EQ(fit::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(fit::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