blob: 1625db7a8edab552a5fce35452da8f74fd1e6422 [file] [log] [blame]
// Copyright 2019 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/sdp/client.h"
#include <chrono>
#include <ratio>
#include <gtest/gtest.h>
#include <pw_async/dispatcher.h>
#include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/l2cap/fake_channel.h"
#include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/l2cap/fake_channel_test.h"
#include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/sdp/service_record.h"
#include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/testing/test_helpers.h"
namespace bt::sdp {
namespace {
using TestingBase = bt::l2cap::testing::FakeChannelTest;
constexpr l2cap::ChannelId kTestChannelId = 0x0041;
constexpr uint16_t kResponseMaxSize = 672;
class ClientTest : public TestingBase {
public:
ClientTest() = default;
protected:
void SetUp() override {
ChannelOptions options(kTestChannelId);
options.link_type = bt::LinkType::kACL;
channel_ = CreateFakeChannel(options);
}
void TearDown() override { channel_ = nullptr; }
private:
std::unique_ptr<l2cap::testing::FakeChannel> channel_;
};
// Flower Path Test:
// - sends correctly formatted request
// - receives response in the callback
// - receives kNotFound at the end of the callbacks
// - closes SDP channel when client is deallocated
TEST_F(ClientTest, ConnectAndQuery) {
{
auto client = Client::Create(fake_chan()->GetWeakPtr(), dispatcher());
EXPECT_TRUE(fake_chan()->activated());
size_t cb_count = 0;
auto result_cb =
[&](fit::result<
Error<>,
std::reference_wrapper<const std::map<AttributeId, DataElement>>>
attrs_result) {
cb_count++;
if (cb_count == 3) {
EXPECT_EQ(Error(HostError::kNotFound), attrs_result);
return true;
}
const std::map<AttributeId, DataElement>& attrs =
attrs_result.value();
// All results should have the ServiceClassIdList.
EXPECT_EQ(1u, attrs.count(kServiceClassIdList));
// The first result has a kProtocolDescriptorList and the second has a
// kBluetoothProfileDescriptorList
if (cb_count == 1) {
EXPECT_EQ(1u, attrs.count(kProtocolDescriptorList));
EXPECT_EQ(0u, attrs.count(kBluetoothProfileDescriptorList));
} else if (cb_count == 2) {
EXPECT_EQ(0u, attrs.count(kProtocolDescriptorList));
EXPECT_EQ(1u, attrs.count(kBluetoothProfileDescriptorList));
}
return true;
};
const StaticByteBuffer kSearchExpectedParams(
// ServiceSearchPattern
0x35,
0x03, // Sequence uint8 3 bytes
0x19,
0x11,
0x0B, // UUID (kAudioSink)
0xFF,
0xFF, // MaxAttributeByteCount (no max)
// Attribute ID list
0x35,
0x09, // Sequence uint8 9 bytes
0x09,
0x00,
0x01, // uint16_t (kServiceClassIdList)
0x09,
0x00,
0x04, // uint16_t (kProtocolDescriptorList)
0x09,
0x00,
0x09, // uint16_t (kBluetoothProfileDescriptorList)
0x00 // No continuation state
);
uint32_t request_tid;
bool success = false;
fake_chan()->SetSendCallback(
[&request_tid, &success, &kSearchExpectedParams](auto packet) {
// First byte should be type.
ASSERT_LE(3u, packet->size());
ASSERT_EQ(kServiceSearchAttributeRequest, (*packet)[0]);
ASSERT_EQ(kSearchExpectedParams, packet->view(5));
request_tid = ((*packet)[1] << 8) != 0 || (*packet)[2];
success = true;
},
dispatcher());
// Search for all A2DP sinks, get the:
// - Service Class ID list
// - Descriptor List
// - Bluetooth Profile Descriptor List
client->ServiceSearchAttributes({profile::kAudioSink},
{kServiceClassIdList,
kProtocolDescriptorList,
kBluetoothProfileDescriptorList},
result_cb);
RunUntilIdle();
EXPECT_TRUE(success);
// Receive the response
// Record makes building the response easier.
ServiceRecord rec;
rec.AddProtocolDescriptor(ServiceRecord::kPrimaryProtocolList,
protocol::kL2CAP,
DataElement(l2cap::kAVDTP));
// The second element here indicates version 1.3 (specified in A2DP spec)
rec.AddProtocolDescriptor(ServiceRecord::kPrimaryProtocolList,
protocol::kAVDTP,
DataElement(uint16_t{0x0103}));
rec.AddProfile(profile::kAudioSink, 1, 3);
ServiceSearchAttributeResponse rsp;
rsp.SetAttribute(0,
kServiceClassIdList,
DataElement({DataElement(profile::kAudioSink)}));
rsp.SetAttribute(0,
kProtocolDescriptorList,
rec.GetAttribute(kProtocolDescriptorList).Clone());
rsp.SetAttribute(1,
kServiceClassIdList,
DataElement({DataElement(profile::kAudioSink)}));
rsp.SetAttribute(1,
kBluetoothProfileDescriptorList,
rec.GetAttribute(kBluetoothProfileDescriptorList).Clone());
auto rsp_ptr = rsp.GetPDU(0xFFFF /* Max attribute bytes */,
request_tid,
kResponseMaxSize,
BufferView());
fake_chan()->Receive(*rsp_ptr);
RunUntilIdle();
EXPECT_EQ(3u, cb_count);
}
EXPECT_FALSE(fake_chan()->activated());
}
TEST_F(ClientTest, TwoQueriesSubsequent) {
{
auto client = Client::Create(fake_chan()->GetWeakPtr(), dispatcher());
EXPECT_TRUE(fake_chan()->activated());
size_t cb_count = 0;
auto result_cb =
[&](fit::result<
Error<>,
std::reference_wrapper<const std::map<AttributeId, DataElement>>>
attrs_result) {
cb_count++;
// We return no results for both queries.
EXPECT_EQ(Error(HostError::kNotFound), attrs_result);
return true;
};
const StaticByteBuffer kSearchExpectedParams(
// ServiceSearchPattern
0x35,
0x03, // Sequence uint8 3 bytes
0x19,
0x11,
0x0B, // UUID (kAudioSink)
0xFF,
0xFF, // MaxAttributeByteCount (no max)
// Attribute ID list
0x35,
0x03, // Sequence uint8 3 bytes
0x09,
0x00,
0x01, // uint16_t (kServiceClassIdList)
0x00 // No continuation state
);
uint32_t request_tid;
bool success = false;
fake_chan()->SetSendCallback(
[&request_tid, &success, &kSearchExpectedParams](auto packet) {
// First byte should be type.
ASSERT_LE(3u, packet->size());
ASSERT_EQ(kServiceSearchAttributeRequest, (*packet)[0]);
ASSERT_EQ(kSearchExpectedParams, packet->view(5));
request_tid = ((*packet)[1] << 8) != 0 || (*packet)[2];
success = true;
},
dispatcher());
// Search for all A2DP sinks, get the:
// - Service Class ID list
client->ServiceSearchAttributes(
{profile::kAudioSink}, {kServiceClassIdList}, result_cb);
RunUntilIdle();
EXPECT_TRUE(success);
// Receive the response (empty response)
// Record makes building the response easier.
ServiceSearchAttributeResponse rsp;
auto rsp_ptr = rsp.GetPDU(0xFFFF /* Max attribute bytes */,
request_tid,
kResponseMaxSize,
BufferView());
fake_chan()->Receive(*rsp_ptr);
RunUntilIdle();
EXPECT_EQ(1u, cb_count);
// Twice
success = false;
client->ServiceSearchAttributes(
{profile::kAudioSink}, {kServiceClassIdList}, result_cb);
RunUntilIdle();
EXPECT_TRUE(success);
rsp_ptr = rsp.GetPDU(0xFFFF /* Max attribute bytes */,
request_tid,
kResponseMaxSize,
BufferView());
fake_chan()->Receive(*rsp_ptr);
RunUntilIdle();
EXPECT_EQ(2u, cb_count);
}
EXPECT_FALSE(fake_chan()->activated());
}
TEST_F(ClientTest, TwoQueriesQueued) {
{
auto client = Client::Create(fake_chan()->GetWeakPtr(), dispatcher());
EXPECT_TRUE(fake_chan()->activated());
size_t cb_count = 0;
auto result_cb =
[&](fit::result<
Error<>,
std::reference_wrapper<const std::map<AttributeId, DataElement>>>
attrs_result) {
cb_count++;
// We return no results for both queries.
EXPECT_EQ(Error(HostError::kNotFound), attrs_result);
return true;
};
const StaticByteBuffer kSearchExpectedParams(
// ServiceSearchPattern
0x35,
0x03, // Sequence uint8 3 bytes
0x19,
0x11,
0x0B, // UUID (kAudioSink)
0xFF,
0xFF, // MaxAttributeByteCount (no max)
// Attribute ID list
0x35,
0x03, // Sequence uint8 3 bytes
0x09,
0x00,
0x01, // uint16_t (kServiceClassIdList)
0x00 // No continuation state
);
uint32_t request_tid;
size_t sent_packets = 0;
fake_chan()->SetSendCallback(
[&request_tid, &sent_packets, &kSearchExpectedParams](auto packet) {
// First byte should be type.
ASSERT_LE(3u, packet->size());
ASSERT_EQ(kServiceSearchAttributeRequest, (*packet)[0]);
ASSERT_EQ(kSearchExpectedParams, packet->view(5));
request_tid = ((*packet)[1] << 8) != 0 || (*packet)[2];
sent_packets++;
},
dispatcher());
// Search for all A2DP sinks, get the:
// - Service Class ID list
client->ServiceSearchAttributes(
{profile::kAudioSink}, {kServiceClassIdList}, result_cb);
// Twice (without waiting)
client->ServiceSearchAttributes(
{profile::kAudioSink}, {kServiceClassIdList}, result_cb);
RunUntilIdle();
// Only one request should have been sent.
EXPECT_EQ(1u, sent_packets);
// Receive the response (empty response)
// Record makes building the response easier.
ServiceSearchAttributeResponse rsp;
auto rsp_ptr = rsp.GetPDU(0xFFFF /* Max attribute bytes */,
request_tid,
kResponseMaxSize,
BufferView());
fake_chan()->Receive(*rsp_ptr);
RunUntilIdle();
EXPECT_EQ(1u, cb_count);
// The second request should have been sent when the first completed.
EXPECT_EQ(2u, sent_packets);
// Respond to the second request.
rsp_ptr = rsp.GetPDU(0xFFFF /* Max attribute bytes */,
request_tid,
kResponseMaxSize,
BufferView());
fake_chan()->Receive(*rsp_ptr);
RunUntilIdle();
EXPECT_EQ(2u, cb_count);
EXPECT_EQ(2u, sent_packets);
}
EXPECT_FALSE(fake_chan()->activated());
}
// Continuing response test:
// - send correctly formatted request
// - receives a response with a continuing response
// - sends a second request to get the rest of the response
// - receives the continued response
// - responds with the results
// - gives up when callback returns false
TEST_F(ClientTest, ContinuingResponseRequested) {
auto client = Client::Create(fake_chan()->GetWeakPtr(), dispatcher());
size_t cb_count = 0;
auto result_cb =
[&](fit::result<
Error<>,
std::reference_wrapper<const std::map<AttributeId, DataElement>>>
attrs_result) {
cb_count++;
if (cb_count == 3) {
EXPECT_EQ(Error(HostError::kNotFound), attrs_result);
return true;
}
const std::map<AttributeId, DataElement>& attrs = attrs_result.value();
// All results should have the ServiceClassIdList.
EXPECT_EQ(1u, attrs.count(kServiceClassIdList));
EXPECT_EQ(1u, attrs.count(kProtocolDescriptorList));
return true;
};
const StaticByteBuffer kSearchExpectedParams(
// ServiceSearchPattern
0x35,
0x03, // Sequence uint8 3 bytes
0x19,
0x11,
0x0B, // UUID (kAudioSink)
0xFF,
0xFF, // MaxAttributeByteCount (no max)
// Attribute ID list
0x35,
0x06, // Sequence uint8 6 bytes
0x09,
0x00,
0x01, // uint16_t (0x0001 = kServiceClassIdList)
0x09,
0x00,
0x04 // uint16_t (0x0004 = kProtocolDescriptorList)
);
size_t requests_made = 0;
// Record makes building the response easier.
ServiceRecord rec;
rec.AddProtocolDescriptor(ServiceRecord::kPrimaryProtocolList,
protocol::kL2CAP,
DataElement(l2cap::kAVDTP));
// The second element here indicates version 1.3 (specified in A2DP spec)
rec.AddProtocolDescriptor(ServiceRecord::kPrimaryProtocolList,
protocol::kAVDTP,
DataElement(uint16_t{0x0103}));
rec.AddProfile(profile::kAudioSink, 1, 3);
ServiceSearchAttributeResponse rsp;
rsp.SetAttribute(
0, kServiceClassIdList, DataElement({DataElement(profile::kAudioSink)}));
rsp.SetAttribute(0,
kProtocolDescriptorList,
rec.GetAttribute(kProtocolDescriptorList).Clone());
rsp.SetAttribute(
1, kServiceClassIdList, DataElement({DataElement(profile::kAudioSink)}));
rsp.SetAttribute(1,
kProtocolDescriptorList,
rec.GetAttribute(kProtocolDescriptorList).Clone());
fake_chan()->SetSendCallback(
[&](auto packet) {
requests_made++;
// First byte should be type.
ASSERT_LE(5u, packet->size());
ASSERT_EQ(kServiceSearchAttributeRequest, (*packet)[0]);
uint16_t request_tid = ((*packet)[1] << 8) != 0 || (*packet)[2];
ASSERT_EQ(kSearchExpectedParams,
packet->view(5, kSearchExpectedParams.size()));
// The stuff after the params is the continuation state.
auto rsp_ptr =
rsp.GetPDU(16 /* Max attribute bytes */,
request_tid,
kResponseMaxSize,
packet->view(5 + kSearchExpectedParams.size() + 1));
fake_chan()->Receive(*rsp_ptr);
},
dispatcher());
// Search for all A2DP sinks, get the:
// - Service Class ID list
// - Descriptor List
// - Bluetooth Profile Descriptor List
client->ServiceSearchAttributes(
{profile::kAudioSink},
{kServiceClassIdList, kProtocolDescriptorList},
result_cb);
RunUntilIdle();
EXPECT_EQ(3u, cb_count);
EXPECT_EQ(4u, requests_made);
}
// No results test:
// - send correctly formatted request
// - receives response with no results
// - callback with no results (kNotFound right away)
TEST_F(ClientTest, NoResults) {
auto client = Client::Create(fake_chan()->GetWeakPtr(), dispatcher());
size_t cb_count = 0;
auto result_cb =
[&](fit::result<
Error<>,
std::reference_wrapper<const std::map<AttributeId, DataElement>>>
attrs_result) {
cb_count++;
EXPECT_EQ(Error(HostError::kNotFound), attrs_result);
return true;
};
const StaticByteBuffer kSearchExpectedParams(
// ServiceSearchPattern
0x35,
0x03, // Sequence uint8 3 bytes
0x19,
0x11,
0x0B, // UUID (kAudioSink)
0xFF,
0xFF, // MaxAttributeByteCount (no max)
// Attribute ID list
0x35,
0x06, // Sequence uint8 6 bytes
0x09,
0x00,
0x01, // uint16_t (0x0001 = kServiceClassIdList)
0x09,
0x00,
0x04, // uint16_t (0x0004 = kProtocolDescriptorList)
0x00 // No continuation state
);
uint32_t request_tid;
bool success = false;
fake_chan()->SetSendCallback(
[&request_tid, &success, &kSearchExpectedParams](auto packet) {
// First byte should be type.
ASSERT_LE(3u, packet->size());
ASSERT_EQ(kServiceSearchAttributeRequest, (*packet)[0]);
ASSERT_EQ(kSearchExpectedParams, packet->view(5));
request_tid = ((*packet)[1] << 8) != 0 || (*packet)[2];
success = true;
},
dispatcher());
// Search for all A2DP sinks, get the:
// - Service Class ID list
// - Descriptor List
// - Bluetooth Profile Descriptor List
client->ServiceSearchAttributes(
{profile::kAudioSink},
{kServiceClassIdList, kProtocolDescriptorList},
result_cb);
RunUntilIdle();
EXPECT_TRUE(success);
// Receive an empty response
ServiceSearchAttributeResponse rsp;
auto rsp_ptr = rsp.GetPDU(0xFFFF /* Max attribute bytes */,
request_tid,
kResponseMaxSize,
BufferView());
fake_chan()->Receive(*rsp_ptr);
RunUntilIdle();
EXPECT_EQ(1u, cb_count);
}
// Disconnect early test:
// - send request
// - remote end disconnects
// - result should be called with kLinkDisconnected
TEST_F(ClientTest, Disconnected) {
auto client = Client::Create(fake_chan()->GetWeakPtr(), dispatcher());
size_t cb_count = 0;
auto result_cb =
[&](fit::result<
Error<>,
std::reference_wrapper<const std::map<AttributeId, DataElement>>>
attrs_result) {
cb_count++;
EXPECT_EQ(Error(HostError::kLinkDisconnected), attrs_result);
return true;
};
const StaticByteBuffer kSearchExpectedParams(
// ServiceSearchPattern
0x35,
0x03, // Sequence uint8 3 bytes
0x19,
0x11,
0x0B, // UUID (kAudioSink)
0xFF,
0xFF, // MaxAttributeByteCount (no max)
// Attribute ID list
0x35,
0x06, // Sequence uint8 6 bytes
0x09,
0x00,
0x01, // uint16_t (0x0001 = kServiceClassIdList)
0x09,
0x00,
0x04, // uint16_t (0x0004 = kProtocolDescriptorList)
0x00 // No continuation state
);
bool requested = false;
fake_chan()->SetSendCallback(
[&](auto packet) {
// First byte should be type.
ASSERT_LE(3u, packet->size());
ASSERT_EQ(kServiceSearchAttributeRequest, (*packet)[0]);
ASSERT_EQ(kSearchExpectedParams, packet->view(5));
requested = true;
},
dispatcher());
// Search for all A2DP sinks, get the:
// - Service Class ID list
// - Descriptor List
// - Bluetooth Profile Descriptor List
client->ServiceSearchAttributes(
{profile::kAudioSink},
{kServiceClassIdList, kProtocolDescriptorList},
result_cb);
RunUntilIdle();
EXPECT_TRUE(requested);
EXPECT_EQ(0u, cb_count);
// Remote end closes the channel.
fake_chan()->Close();
RunUntilIdle();
EXPECT_EQ(1u, cb_count);
}
// Malformed reply test:
// - remote end sends wrong packet type in response (dropped)
// - remote end sends invalid response
// - callback receives no response with a malformed packet error
TEST_F(ClientTest, InvalidResponse) {
auto client = Client::Create(fake_chan()->GetWeakPtr(), dispatcher());
size_t cb_count = 0;
auto result_cb =
[&](fit::result<
Error<>,
std::reference_wrapper<const std::map<AttributeId, DataElement>>>
attrs_result) {
cb_count++;
EXPECT_EQ(Error(HostError::kPacketMalformed), attrs_result);
return true;
};
const StaticByteBuffer kSearchExpectedParams(
// ServiceSearchPattern
0x35,
0x03, // Sequence uint8 3 bytes
0x19,
0x11,
0x0B, // UUID (kAudioSink)
0xFF,
0xFF, // MaxAttributeByteCount (no max)
// Attribute ID list
0x35,
0x06, // Sequence uint8 6 bytes
0x09,
0x00,
0x01, // uint16_t (0x0001 = kServiceClassIdList)
0x09,
0x00,
0x04, // uint16_t (0x0004 = kProtocolDescriptorList)
0x00 // No continuation state
);
uint32_t request_tid;
bool requested = false;
fake_chan()->SetSendCallback(
[&](auto packet) {
// First byte should be type.
ASSERT_LE(3u, packet->size());
ASSERT_EQ(kServiceSearchAttributeRequest, (*packet)[0]);
ASSERT_EQ(kSearchExpectedParams, packet->view(5));
request_tid = ((*packet)[1] << 8) != 0 || (*packet)[2];
requested = true;
},
dispatcher());
// Search for all A2DP sinks, get the:
// - Service Class ID list
// - Descriptor List
// - Bluetooth Profile Descriptor List
client->ServiceSearchAttributes(
{profile::kAudioSink},
{kServiceClassIdList, kProtocolDescriptorList},
result_cb);
RunUntilIdle();
EXPECT_TRUE(requested);
EXPECT_EQ(0u, cb_count);
// Remote end sends some unparsable stuff for the packet.
fake_chan()->Receive(StaticByteBuffer(0x07,
UpperBits(request_tid),
LowerBits(request_tid),
0x00,
0x03,
0x05,
0x06,
0x07));
RunUntilIdle();
EXPECT_EQ(1u, cb_count);
}
// Time out (or possibly dropped packets that were malformed)
TEST_F(ClientTest, Timeout) {
constexpr uint32_t kTimeoutMs = 10000;
auto client = Client::Create(fake_chan()->GetWeakPtr(), dispatcher());
size_t cb_count = 0;
auto result_cb =
[&](fit::result<
Error<>,
std::reference_wrapper<const std::map<AttributeId, DataElement>>>
attrs_result) {
cb_count++;
EXPECT_EQ(Error(HostError::kTimedOut), attrs_result);
return true;
};
const StaticByteBuffer kSearchExpectedParams(
// ServiceSearchPattern
0x35,
0x03, // Sequence uint8 3 bytes
0x19,
0x11,
0x0B, // UUID (kAudioSink)
0xFF,
0xFF, // MaxAttributeByteCount (no max)
// Attribute ID list
0x35,
0x06, // Sequence uint8 6 bytes
0x09,
0x00,
0x01, // uint16_t (0x0001 = kServiceClassIdList)
0x09,
0x00,
0x04, // uint16_t (0x0004 = kProtocolDescriptorList)
0x00 // No continuation state
);
bool requested = false;
fake_chan()->SetSendCallback(
[&](auto packet) {
// First byte should be type.
ASSERT_LE(3u, packet->size());
ASSERT_EQ(kServiceSearchAttributeRequest, (*packet)[0]);
ASSERT_EQ(kSearchExpectedParams, packet->view(5));
requested = true;
},
dispatcher());
// Search for all A2DP sinks, get the:
// - Service Class ID list
// - Descriptor List
// - Bluetooth Profile Descriptor List
client->ServiceSearchAttributes(
{profile::kAudioSink},
{kServiceClassIdList, kProtocolDescriptorList},
result_cb);
RunUntilIdle();
EXPECT_TRUE(requested);
EXPECT_EQ(0u, cb_count);
// Wait until the timeout happens
RunFor(std::chrono::milliseconds(kTimeoutMs + 1));
EXPECT_EQ(1u, cb_count);
}
TEST_F(ClientTest, DestroyClientInErrorResultCallbackDoesNotCrash) {
constexpr uint32_t kTimeoutMs = 10000;
auto client = Client::Create(fake_chan()->GetWeakPtr(), dispatcher());
size_t cb_count = 0;
auto result_cb =
[&](fit::result<
Error<>,
std::reference_wrapper<const std::map<AttributeId, DataElement>>>
attrs_result) {
cb_count++;
EXPECT_TRUE(attrs_result.is_error());
client.reset();
return true;
};
bool requested = false;
fake_chan()->SetSendCallback([&](auto packet) { requested = true; },
dispatcher());
client->ServiceSearchAttributes(
{profile::kAudioSink},
{kServiceClassIdList, kProtocolDescriptorList},
result_cb);
RunUntilIdle();
EXPECT_TRUE(requested);
EXPECT_EQ(0u, cb_count);
// Wait until the timeout happens
RunFor(std::chrono::milliseconds(kTimeoutMs + 1));
EXPECT_EQ(1u, cb_count);
}
TEST_F(ClientTest, DestroyClientInDisconnectedResultCallback) {
auto client = Client::Create(fake_chan()->GetWeakPtr(), dispatcher());
size_t cb_count = 0;
auto result_cb =
[&](fit::result<
Error<>,
std::reference_wrapper<const std::map<AttributeId, DataElement>>>
attrs_result) {
cb_count++;
EXPECT_EQ(Error(HostError::kLinkDisconnected), attrs_result);
client.reset();
return true;
};
bool requested = false;
fake_chan()->SetSendCallback([&](auto packet) { requested = true; },
dispatcher());
client->ServiceSearchAttributes(
{profile::kAudioSink},
{kServiceClassIdList, kProtocolDescriptorList},
result_cb);
RunUntilIdle();
EXPECT_TRUE(requested);
EXPECT_EQ(0u, cb_count);
// Remote end closes the channel.
fake_chan()->Close();
RunUntilIdle();
EXPECT_EQ(1u, cb_count);
}
} // namespace
} // namespace bt::sdp