| // 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/sdp/client.h" |
| |
| #include <gtest/gtest.h> |
| |
| #include "src/connectivity/bluetooth/core/bt-host/common/test_helpers.h" |
| #include "src/connectivity/bluetooth/core/bt-host/l2cap/fake_channel.h" |
| #include "src/connectivity/bluetooth/core/bt-host/l2cap/fake_channel_test.h" |
| #include "src/connectivity/bluetooth/core/bt-host/sdp/service_record.h" |
| |
| namespace bt::sdp { |
| namespace { |
| |
| using TestingBase = bt::l2cap::testing::FakeChannelTest; |
| constexpr l2cap::ChannelId kTestChannelId = 0x0041; |
| constexpr uint16_t kResponseMaxSize = 672; |
| |
| class SDP_ClientTest : public TestingBase { |
| public: |
| SDP_ClientTest() = default; |
| ~SDP_ClientTest() = default; |
| |
| protected: |
| void SetUp() override { |
| ChannelOptions options(kTestChannelId); |
| options.link_type = hci::Connection::LinkType::kACL; |
| channel_ = CreateFakeChannel(options); |
| } |
| |
| void TearDown() override { channel_ = nullptr; } |
| |
| fbl::RefPtr<l2cap::testing::FakeChannel> channel() { return channel_; } |
| |
| private: |
| fbl::RefPtr<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(SDP_ClientTest, ConnectAndQuery) { |
| { |
| auto client = Client::Create(channel()); |
| |
| EXPECT_TRUE(fake_chan()->activated()); |
| |
| size_t cb_count = 0; |
| auto result_cb = [&](auto status, const std::map<AttributeId, DataElement> &attrs) { |
| cb_count++; |
| if (cb_count == 3) { |
| EXPECT_FALSE(status); |
| EXPECT_EQ(HostError::kNotFound, status.error()); |
| return true; |
| } |
| // 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 auto kSearchExpectedParams = CreateStaticByteBuffer( |
| // 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 || (*packet)[2]; |
| success = true; |
| }, |
| dispatcher()); |
| |
| // Seartch 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, |
| dispatcher()); |
| RunLoopUntilIdle(); |
| 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); |
| |
| RunLoopUntilIdle(); |
| |
| EXPECT_EQ(3u, cb_count); |
| } |
| EXPECT_FALSE(fake_chan()->activated()); |
| } |
| |
| TEST_F(SDP_ClientTest, TwoQueriesSubsequent) { |
| { |
| auto client = Client::Create(channel()); |
| |
| EXPECT_TRUE(fake_chan()->activated()); |
| |
| size_t cb_count = 0; |
| auto result_cb = [&](auto status, const std::map<AttributeId, DataElement> &attrs) { |
| cb_count++; |
| // We return no results for both queries. |
| EXPECT_FALSE(status); |
| EXPECT_EQ(HostError::kNotFound, status.error()); |
| return true; |
| }; |
| |
| const auto kSearchExpectedParams = CreateStaticByteBuffer( |
| // 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 || (*packet)[2]; |
| success = true; |
| }, |
| dispatcher()); |
| |
| // Search for all A2DP sinks, get the: |
| // - Service Class ID list |
| client->ServiceSearchAttributes({profile::kAudioSink}, {kServiceClassIdList}, result_cb, |
| dispatcher()); |
| RunLoopUntilIdle(); |
| 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); |
| |
| RunLoopUntilIdle(); |
| |
| EXPECT_EQ(1u, cb_count); |
| |
| // Twice |
| success = false; |
| client->ServiceSearchAttributes({profile::kAudioSink}, {kServiceClassIdList}, result_cb, |
| dispatcher()); |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(success); |
| |
| rsp_ptr = |
| rsp.GetPDU(0xFFFF /* Max attribute bytes */, request_tid, kResponseMaxSize, BufferView()); |
| fake_chan()->Receive(*rsp_ptr); |
| |
| RunLoopUntilIdle(); |
| |
| EXPECT_EQ(2u, cb_count); |
| } |
| EXPECT_FALSE(fake_chan()->activated()); |
| } |
| |
| TEST_F(SDP_ClientTest, TwoQueriesQueued) { |
| { |
| auto client = Client::Create(channel()); |
| |
| EXPECT_TRUE(fake_chan()->activated()); |
| |
| size_t cb_count = 0; |
| auto result_cb = [&](auto status, const std::map<AttributeId, DataElement> &attrs) { |
| cb_count++; |
| // We return no results for both queries. |
| EXPECT_FALSE(status); |
| EXPECT_EQ(HostError::kNotFound, status.error()); |
| return true; |
| }; |
| |
| const auto kSearchExpectedParams = CreateStaticByteBuffer( |
| // 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 || (*packet)[2]; |
| sent_packets++; |
| }, |
| dispatcher()); |
| |
| // Search for all A2DP sinks, get the: |
| // - Service Class ID list |
| client->ServiceSearchAttributes({profile::kAudioSink}, {kServiceClassIdList}, result_cb, |
| dispatcher()); |
| // Twice (without waiting) |
| client->ServiceSearchAttributes({profile::kAudioSink}, {kServiceClassIdList}, result_cb, |
| dispatcher()); |
| RunLoopUntilIdle(); |
| // 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); |
| |
| RunLoopUntilIdle(); |
| |
| 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); |
| |
| RunLoopUntilIdle(); |
| |
| 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(SDP_ClientTest, ContinuingResponseRequested) { |
| auto client = Client::Create(channel()); |
| |
| size_t cb_count = 0; |
| auto result_cb = [&](auto status, const std::map<AttributeId, DataElement> &attrs) { |
| cb_count++; |
| if (cb_count == 3) { |
| EXPECT_FALSE(status); |
| EXPECT_EQ(HostError::kNotFound, status.error()); |
| return true; |
| } |
| // All results should have the ServiceClassIdList. |
| EXPECT_EQ(1u, attrs.count(kServiceClassIdList)); |
| EXPECT_EQ(1u, attrs.count(kProtocolDescriptorList)); |
| return true; |
| }; |
| |
| const auto kSearchExpectedParams = CreateStaticByteBuffer( |
| // 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 || (*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()); |
| |
| // Seartch for all A2DP sinks, get the: |
| // - Service Class ID list |
| // - Descriptor List |
| // - Bluetooth Profile Descriptor List |
| client->ServiceSearchAttributes({profile::kAudioSink}, |
| {kServiceClassIdList, kProtocolDescriptorList}, result_cb, |
| dispatcher()); |
| RunLoopUntilIdle(); |
| 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(SDP_ClientTest, NoResults) { |
| auto client = Client::Create(channel()); |
| |
| size_t cb_count = 0; |
| auto result_cb = [&](auto status, const std::map<AttributeId, DataElement> &attrs) { |
| cb_count++; |
| EXPECT_FALSE(status); |
| EXPECT_EQ(HostError::kNotFound, status.error()); |
| return true; |
| }; |
| |
| const auto kSearchExpectedParams = CreateStaticByteBuffer( |
| // 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 || (*packet)[2]; |
| success = true; |
| }, |
| dispatcher()); |
| |
| // Seartch for all A2DP sinks, get the: |
| // - Service Class ID list |
| // - Descriptor List |
| // - Bluetooth Profile Descriptor List |
| client->ServiceSearchAttributes({profile::kAudioSink}, |
| {kServiceClassIdList, kProtocolDescriptorList}, result_cb, |
| dispatcher()); |
| RunLoopUntilIdle(); |
| 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); |
| |
| RunLoopUntilIdle(); |
| |
| EXPECT_EQ(1u, cb_count); |
| } |
| |
| // Disconnect early test: |
| // - send request |
| // - remote end disconnects |
| // - result should be called with kLinkDisconnected |
| TEST_F(SDP_ClientTest, Disconnected) { |
| auto client = Client::Create(channel()); |
| |
| size_t cb_count = 0; |
| auto result_cb = [&](auto status, const std::map<AttributeId, DataElement> &attrs) { |
| cb_count++; |
| EXPECT_FALSE(status); |
| EXPECT_EQ(HostError::kLinkDisconnected, status.error()); |
| return true; |
| }; |
| |
| const auto kSearchExpectedParams = CreateStaticByteBuffer( |
| // 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()); |
| |
| // Seartch for all A2DP sinks, get the: |
| // - Service Class ID list |
| // - Descriptor List |
| // - Bluetooth Profile Descriptor List |
| client->ServiceSearchAttributes({profile::kAudioSink}, |
| {kServiceClassIdList, kProtocolDescriptorList}, result_cb, |
| dispatcher()); |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(requested); |
| EXPECT_EQ(0u, cb_count); |
| |
| // Remote end closes the channel. |
| fake_chan()->Close(); |
| |
| RunLoopUntilIdle(); |
| |
| 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(SDP_ClientTest, InvalidResponse) { |
| auto client = Client::Create(channel()); |
| |
| size_t cb_count = 0; |
| auto result_cb = [&](auto status, const std::map<AttributeId, DataElement> &attrs) { |
| cb_count++; |
| EXPECT_FALSE(status); |
| EXPECT_EQ(HostError::kPacketMalformed, status.error()); |
| return true; |
| }; |
| |
| const auto kSearchExpectedParams = CreateStaticByteBuffer( |
| // 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 || (*packet)[2]; |
| requested = true; |
| }, |
| dispatcher()); |
| |
| // Seartch for all A2DP sinks, get the: |
| // - Service Class ID list |
| // - Descriptor List |
| // - Bluetooth Profile Descriptor List |
| client->ServiceSearchAttributes({profile::kAudioSink}, |
| {kServiceClassIdList, kProtocolDescriptorList}, result_cb, |
| dispatcher()); |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(requested); |
| EXPECT_EQ(0u, cb_count); |
| |
| // Remote end sends some unparseable stuff for the packet. |
| fake_chan()->Receive(CreateStaticByteBuffer(0x07, UpperBits(request_tid), LowerBits(request_tid), |
| 0x00, 0x03, 0x05, 0x06, 0x07)); |
| |
| RunLoopUntilIdle(); |
| |
| EXPECT_EQ(1u, cb_count); |
| } |
| |
| // Time out (or possibly dropped packets that were malformed) |
| TEST_F(SDP_ClientTest, Timeout) { |
| constexpr uint32_t kTimeoutMs = 10000; |
| auto client = Client::Create(channel()); |
| |
| size_t cb_count = 0; |
| auto result_cb = [&](auto status, const std::map<AttributeId, DataElement> &attrs) { |
| cb_count++; |
| EXPECT_FALSE(status); |
| EXPECT_EQ(HostError::kTimedOut, status.error()); |
| return true; |
| }; |
| |
| const auto kSearchExpectedParams = CreateStaticByteBuffer( |
| // 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()); |
| |
| // Seartch for all A2DP sinks, get the: |
| // - Service Class ID list |
| // - Descriptor List |
| // - Bluetooth Profile Descriptor List |
| client->ServiceSearchAttributes({profile::kAudioSink}, |
| {kServiceClassIdList, kProtocolDescriptorList}, result_cb, |
| dispatcher()); |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(requested); |
| EXPECT_EQ(0u, cb_count); |
| |
| // Wait until the timeout happens |
| RunLoopFor(zx::msec(kTimeoutMs + 1)); |
| |
| EXPECT_EQ(1u, cb_count); |
| } |
| |
| } // namespace |
| } // namespace bt::sdp |