// Copyright 2023 The Pigweed Authors // // Licensed under the Apache License, Version 2.0 (the "License"); you may not // use this file except in compliance with the License. You may obtain a copy of // the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations under // the License. #include "pw_bluetooth_sapphire/internal/host/sdp/client.h" #include #include #include #include "pw_bluetooth_sapphire/internal/host/l2cap/fake_channel.h" #include "pw_bluetooth_sapphire/internal/host/l2cap/fake_channel_test.h" #include "pw_bluetooth_sapphire/internal/host/sdp/service_record.h" #include "pw_bluetooth_sapphire/internal/host/testing/test_helpers.h" #include "pw_unit_test/framework.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 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>> attrs_result) { cb_count++; if (cb_count == 3) { EXPECT_EQ(Error(HostError::kNotFound), attrs_result); return true; } const std::map& 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 ); uint16_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>> 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 ); uint16_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>> 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 ); uint16_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>> attrs_result) { cb_count++; if (cb_count == 3) { EXPECT_EQ(Error(HostError::kNotFound), attrs_result); return true; } const std::map& 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>> 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 ); uint16_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>> 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>> 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 ); uint16_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>> 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>> 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>> 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