1 // Copyright 2023 The Pigweed Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License"); you may not
4 // use this file except in compliance with the License. You may obtain a copy of
5 // the License at
6 //
7 // https://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 // License for the specific language governing permissions and limitations under
13 // the License.
14
15 #include "pw_bluetooth_sapphire/internal/host/sdp/service_discoverer.h"
16
17 #include "pw_bluetooth_sapphire/internal/host/l2cap/fake_channel.h"
18 #include "pw_bluetooth_sapphire/internal/host/l2cap/fake_channel_test.h"
19 #include "pw_bluetooth_sapphire/internal/host/testing/test_helpers.h"
20 #include "pw_unit_test/framework.h"
21
22 namespace bt::sdp {
23 namespace {
24
25 constexpr PeerId kDeviceOne(1), kDeviceTwo(2), kDeviceThree(3);
26
27 class FakeClient : public Client {
28 public:
29 // |destroyed_cb| will be called when this client is destroyed, with true if
30 // there are outstanding expected requests.
FakeClient(fit::closure destroyed_cb)31 FakeClient(fit::closure destroyed_cb)
32 : destroyed_cb_(std::move(destroyed_cb)) {}
33
~FakeClient()34 virtual ~FakeClient() override { destroyed_cb_(); }
35
ServiceSearchAttributes(std::unordered_set<UUID> search_pattern,const std::unordered_set<AttributeId> & req_attributes,SearchResultFunction result_cb)36 virtual void ServiceSearchAttributes(
37 std::unordered_set<UUID> search_pattern,
38 const std::unordered_set<AttributeId>& req_attributes,
39 SearchResultFunction result_cb) override {
40 if (!service_search_attributes_cb_) {
41 FAIL() << "ServiceSearchAttributes with no callback set";
42 }
43
44 service_search_attributes_cb_(std::move(search_pattern),
45 std::move(req_attributes),
46 std::move(result_cb));
47 }
48
49 using ServiceSearchAttributesCallback =
50 fit::function<void(std::unordered_set<UUID>,
51 std::unordered_set<AttributeId>,
52 SearchResultFunction)>;
SetServiceSearchAttributesCallback(ServiceSearchAttributesCallback callback)53 void SetServiceSearchAttributesCallback(
54 ServiceSearchAttributesCallback callback) {
55 service_search_attributes_cb_ = std::move(callback);
56 }
57
58 private:
59 ServiceSearchAttributesCallback service_search_attributes_cb_;
60 fit::closure destroyed_cb_;
61 };
62
63 class ServiceDiscovererTest : public pw::async::test::FakeDispatcherFixture {
64 public:
65 ServiceDiscovererTest() = default;
66 ~ServiceDiscovererTest() = default;
67
heap_dispatcher()68 pw::async::HeapDispatcher& heap_dispatcher() { return heap_dispatcher_; }
69
70 protected:
SetUp()71 void SetUp() override {
72 clients_created_ = 0;
73 clients_destroyed_ = 0;
74 }
75
TearDown()76 void TearDown() override {}
77
78 // Connect an SDP client to a fake channel, which is available in channel_
GetFakeClient()79 std::unique_ptr<FakeClient> GetFakeClient() {
80 SCOPED_TRACE("Connect Client");
81 clients_created_++;
82 return std::make_unique<FakeClient>([this]() { clients_destroyed_++; });
83 }
84
clients_created() const85 size_t clients_created() const { return clients_created_; }
clients_destroyed() const86 size_t clients_destroyed() const { return clients_destroyed_; }
87
88 private:
89 size_t clients_created_, clients_destroyed_;
90 pw::async::HeapDispatcher heap_dispatcher_{dispatcher()};
91 };
92
93 // When there are no searches registered, it just disconnects the client.
TEST_F(ServiceDiscovererTest,NoSearches)94 TEST_F(ServiceDiscovererTest, NoSearches) {
95 ServiceDiscoverer discoverer;
96 EXPECT_EQ(0u, discoverer.search_count());
97
98 discoverer.StartServiceDiscovery(kDeviceOne, GetFakeClient());
99
100 RETURN_IF_FATAL(RunUntilIdle());
101
102 EXPECT_EQ(1u, clients_destroyed());
103 }
104
105 // Happy path test with one registered service and no results.
TEST_F(ServiceDiscovererTest,NoResults)106 TEST_F(ServiceDiscovererTest, NoResults) {
107 ServiceDiscoverer discoverer;
108
109 size_t cb_count = 0;
110
111 auto result_cb = [&cb_count](auto, const auto&) { cb_count++; };
112
113 ServiceDiscoverer::SearchId id = discoverer.AddSearch(
114 profile::kSerialPort,
115 {kServiceId, kProtocolDescriptorList, kBluetoothProfileDescriptorList},
116 std::move(result_cb));
117 ASSERT_NE(ServiceDiscoverer::kInvalidSearchId, id);
118 EXPECT_EQ(1u, discoverer.search_count());
119
120 auto client = GetFakeClient();
121
122 std::vector<std::unordered_set<UUID>> searches;
123
124 client->SetServiceSearchAttributesCallback(
125 [dispatcher = heap_dispatcher(), &searches](
126 auto pattern, auto attributes, auto callback) mutable {
127 searches.emplace_back(std::move(pattern));
128 (void)dispatcher.Post(
129 [cb = std::move(callback)](pw::async::Context /*ctx*/,
130 pw::Status status) {
131 if (status.ok()) {
132 cb(fit::error(Error(HostError::kNotFound)));
133 }
134 });
135 });
136
137 discoverer.StartServiceDiscovery(kDeviceOne, std::move(client));
138
139 RETURN_IF_FATAL(RunUntilIdle());
140
141 EXPECT_EQ(1u, searches.size());
142 ASSERT_EQ(0u, cb_count);
143 ASSERT_EQ(1u, clients_destroyed());
144 }
145
TEST_F(ServiceDiscovererTest,SynchronousErrorResult)146 TEST_F(ServiceDiscovererTest, SynchronousErrorResult) {
147 ServiceDiscoverer discoverer;
148
149 size_t cb_count = 0;
150 auto result_cb = [&cb_count](auto, const auto&) { cb_count++; };
151 ServiceDiscoverer::SearchId id = discoverer.AddSearch(
152 profile::kSerialPort,
153 {kServiceId, kProtocolDescriptorList, kBluetoothProfileDescriptorList},
154 std::move(result_cb));
155 ASSERT_NE(ServiceDiscoverer::kInvalidSearchId, id);
156 EXPECT_EQ(1u, discoverer.search_count());
157
158 auto client = GetFakeClient();
159 std::vector<std::unordered_set<UUID>> searches;
160 client->SetServiceSearchAttributesCallback(
161 [&searches](auto pattern, auto attributes, auto callback) {
162 searches.emplace_back(std::move(pattern));
163 callback(fit::error(Error(HostError::kLinkDisconnected)));
164 });
165
166 discoverer.StartServiceDiscovery(kDeviceOne, std::move(client));
167 RETURN_IF_FATAL(RunUntilIdle());
168 EXPECT_EQ(1u, searches.size());
169 ASSERT_EQ(0u, cb_count);
170 ASSERT_EQ(1u, clients_destroyed());
171 }
172
173 // Happy path test with two registered searches.
174 // No results, then two results.
175 // Unregister one search.
176 // Then one result are searched for and two returned.
TEST_F(ServiceDiscovererTest,SomeResults)177 TEST_F(ServiceDiscovererTest, SomeResults) {
178 ServiceDiscoverer discoverer;
179
180 std::vector<std::pair<PeerId, std::map<AttributeId, DataElement>>> results;
181
182 ServiceDiscoverer::ResultCallback result_cb =
183 [&results](PeerId id, const auto& attributes) {
184 std::map<AttributeId, DataElement> attributes_clone;
185 for (const auto& it : attributes) {
186 auto [inserted_it, added] =
187 attributes_clone.try_emplace(it.first, it.second.Clone());
188 ASSERT_TRUE(added);
189 }
190 results.emplace_back(id, std::move(attributes_clone));
191 };
192
193 ServiceDiscoverer::SearchId one = discoverer.AddSearch(
194 profile::kSerialPort,
195 {kServiceId, kProtocolDescriptorList, kBluetoothProfileDescriptorList},
196 result_cb.share());
197 ASSERT_NE(ServiceDiscoverer::kInvalidSearchId, one);
198 EXPECT_EQ(1u, discoverer.search_count());
199 ServiceDiscoverer::SearchId two = discoverer.AddSearch(
200 profile::kAudioSink,
201 {kProtocolDescriptorList, kBluetoothProfileDescriptorList},
202 result_cb.share());
203 ASSERT_NE(ServiceDiscoverer::kInvalidSearchId, two);
204 EXPECT_EQ(2u, discoverer.search_count());
205
206 auto client = GetFakeClient();
207
208 std::vector<std::unordered_set<UUID>> searches;
209
210 client->SetServiceSearchAttributesCallback(
211 [dispatcher = heap_dispatcher(), &searches](
212 auto pattern, auto attributes, auto callback) mutable {
213 searches.emplace_back(std::move(pattern));
214 (void)dispatcher.Post(
215 [cb = std::move(callback)](pw::async::Context /*ctx*/,
216 pw::Status status) {
217 if (status.ok()) {
218 cb(fit::error(Error(HostError::kNotFound)));
219 }
220 });
221 });
222
223 discoverer.StartServiceDiscovery(kDeviceOne, std::move(client));
224
225 RETURN_IF_FATAL(RunUntilIdle());
226
227 EXPECT_EQ(2u, searches.size());
228 ASSERT_EQ(0u, results.size());
229 ASSERT_EQ(1u, clients_destroyed());
230
231 client = GetFakeClient();
232
233 searches.clear();
234
235 client->SetServiceSearchAttributesCallback(
236 [cb_dispatcher = heap_dispatcher(), &searches](
237 auto pattern, auto attributes, auto callback) mutable {
238 searches.emplace_back(pattern);
239 if (pattern.count(profile::kSerialPort)) {
240 (void)cb_dispatcher.Post([cb = std::move(callback)](
241 pw::async::Context /*ctx*/,
242 pw::Status status) {
243 if (!status.ok()) {
244 return;
245 }
246 ServiceSearchAttributeResponse rsp;
247 rsp.SetAttribute(0, kServiceId, DataElement(UUID(uint16_t{1})));
248 // This would normally be a element list. uint32_t for Testing.
249 rsp.SetAttribute(
250 0, kBluetoothProfileDescriptorList, DataElement(uint32_t{1}));
251
252 if (!cb(fit::ok(std::cref(rsp.attributes(0))))) {
253 return;
254 }
255 cb(fit::error(Error(HostError::kNotFound)));
256 });
257 } else if (pattern.count(profile::kAudioSink)) {
258 (void)cb_dispatcher.Post([cb = std::move(callback)](
259 pw::async::Context /*ctx*/,
260 pw::Status status) {
261 if (!status.ok()) {
262 return;
263 }
264 ServiceSearchAttributeResponse rsp;
265 // This would normally be a element list. uint32_t for Testing.
266 rsp.SetAttribute(
267 0, kBluetoothProfileDescriptorList, DataElement(uint32_t{1}));
268
269 if (!cb(fit::ok(std::cref(rsp.attributes(0))))) {
270 return;
271 }
272 cb(fit::error(Error(HostError::kNotFound)));
273 });
274 } else {
275 std::cerr << "Searched for " << pattern.size() << std::endl;
276 for (auto it : pattern) {
277 std::cerr << it.ToString() << std::endl;
278 }
279 FAIL() << "Unexpected search called";
280 }
281 });
282
283 discoverer.StartServiceDiscovery(kDeviceTwo, std::move(client));
284
285 RETURN_IF_FATAL(RunUntilIdle());
286
287 EXPECT_EQ(2u, searches.size());
288 ASSERT_EQ(2u, results.size());
289 ASSERT_EQ(2u, clients_destroyed());
290
291 results.clear();
292 searches.clear();
293
294 ASSERT_TRUE(discoverer.RemoveSearch(one));
295 ASSERT_FALSE(discoverer.RemoveSearch(one));
296 EXPECT_EQ(1u, discoverer.search_count());
297
298 client = GetFakeClient();
299
300 client->SetServiceSearchAttributesCallback(
301 [cb_dispatcher = heap_dispatcher(), &searches](
302 auto pattern, auto attributes, auto callback) mutable {
303 searches.emplace_back(pattern);
304 if (pattern.count(profile::kAudioSink)) {
305 (void)cb_dispatcher.Post([cb = std::move(callback)](
306 pw::async::Context /*ctx*/,
307 pw::Status status) {
308 if (!status.ok()) {
309 return;
310 }
311 ServiceSearchAttributeResponse rsp;
312 // This would normally be a element list. uint32_t for Testing.
313 rsp.SetAttribute(
314 0, kBluetoothProfileDescriptorList, DataElement(uint32_t{1}));
315 rsp.SetAttribute(
316 1, kProtocolDescriptorList, DataElement(uint32_t{2}));
317
318 if (!cb(fit::ok(std::cref(rsp.attributes(0))))) {
319 return;
320 }
321 if (!cb(fit::ok(std::cref(rsp.attributes(1))))) {
322 return;
323 }
324 cb(fit::error(Error(HostError::kNotFound)));
325 });
326 } else {
327 std::cerr << "Searched for " << pattern.size() << std::endl;
328 for (auto it : pattern) {
329 std::cerr << it.ToString() << std::endl;
330 }
331 FAIL() << "Unexpected search called";
332 }
333 });
334
335 discoverer.StartServiceDiscovery(kDeviceThree, std::move(client));
336
337 RETURN_IF_FATAL(RunUntilIdle());
338
339 EXPECT_EQ(1u, searches.size());
340 ASSERT_EQ(2u, results.size());
341 ASSERT_EQ(3u, clients_destroyed());
342 }
343
344 // Single search behavior when there is a discovery not running (2 clients
345 // simultaneously)
TEST_F(ServiceDiscovererTest,SingleSearchDifferentPeers)346 TEST_F(ServiceDiscovererTest, SingleSearchDifferentPeers) {
347 ServiceDiscoverer discoverer;
348
349 size_t cb_count = 0;
350
351 auto result_cb = [&cb_count](auto, const auto&) { cb_count++; };
352
353 ServiceDiscoverer::SearchId search_id = discoverer.AddSearch(
354 profile::kSerialPort, {kServiceId}, std::move(result_cb));
355 ASSERT_NE(ServiceDiscoverer::kInvalidSearchId, search_id);
356 EXPECT_EQ(1u, discoverer.search_count());
357
358 auto client = GetFakeClient();
359 auto client2 = GetFakeClient();
360
361 std::vector<std::unordered_set<UUID>> searches;
362
363 auto search_attributes_cb =
364 [cb_dispatcher = heap_dispatcher(), &searches](
365 auto pattern, auto attributes, auto callback) mutable {
366 searches.emplace_back(pattern);
367 if (pattern.count(profile::kSerialPort)) {
368 (void)cb_dispatcher.Post(
369 [cb = std::move(callback)](pw::async::Context /*ctx*/,
370 pw::Status status) {
371 if (!status.ok()) {
372 return;
373 }
374 ServiceSearchAttributeResponse rsp;
375 rsp.SetAttribute(0, kServiceId, DataElement(UUID(uint16_t{1})));
376 if (!cb(fit::ok(std::cref(rsp.attributes(0))))) {
377 return;
378 }
379 cb(fit::error(Error(HostError::kNotFound)));
380 });
381 } else {
382 std::cerr << "Searched for " << pattern.size() << std::endl;
383 for (auto it : pattern) {
384 std::cerr << it.ToString() << std::endl;
385 }
386 FAIL() << "Unexpected search called";
387 }
388 };
389
390 client->SetServiceSearchAttributesCallback(search_attributes_cb);
391 client2->SetServiceSearchAttributesCallback(search_attributes_cb);
392
393 discoverer.SingleSearch(search_id, PeerId(1), std::move(client));
394 discoverer.SingleSearch(search_id, PeerId(2), std::move(client2));
395
396 RETURN_IF_FATAL(RunUntilIdle());
397
398 EXPECT_EQ(2u, searches.size());
399 ASSERT_EQ(2u, cb_count);
400 ASSERT_EQ(2u, clients_destroyed());
401 }
402
403 // Single search behavior when there is a discovery running (2 searches
404 // simultaneously)
TEST_F(ServiceDiscovererTest,SingleSearchSamePeer)405 TEST_F(ServiceDiscovererTest, SingleSearchSamePeer) {
406 ServiceDiscoverer discoverer;
407
408 size_t cb_count = 0;
409
410 auto result_cb = [&cb_count](auto, const auto&) { cb_count++; };
411
412 ServiceDiscoverer::SearchId search_id = discoverer.AddSearch(
413 profile::kSerialPort, {kServiceId}, std::move(result_cb));
414 ASSERT_NE(ServiceDiscoverer::kInvalidSearchId, search_id);
415 EXPECT_EQ(1u, discoverer.search_count());
416
417 auto client = GetFakeClient();
418 std::vector<std::unordered_set<UUID>> searches;
419
420 auto search_attributes_cb =
421 [cb_dispatcher = heap_dispatcher(), &searches](
422 auto pattern, auto attributes, auto callback) mutable {
423 searches.emplace_back(pattern);
424 if (pattern.count(profile::kSerialPort)) {
425 (void)cb_dispatcher.Post(
426 [cb = std::move(callback)](pw::async::Context /*ctx*/,
427 pw::Status status) {
428 if (!status.ok()) {
429 return;
430 }
431 ServiceSearchAttributeResponse rsp;
432 rsp.SetAttribute(0, kServiceId, DataElement(UUID(uint16_t{1})));
433 if (!cb(fit::ok(std::cref(rsp.attributes(0))))) {
434 return;
435 }
436 cb(fit::error(Error(HostError::kNotFound)));
437 });
438 } else {
439 std::cerr << "Searched for " << pattern.size() << std::endl;
440 for (auto it : pattern) {
441 std::cerr << it.ToString() << std::endl;
442 }
443 FAIL() << "Unexpected search called";
444 }
445 };
446
447 client->SetServiceSearchAttributesCallback(search_attributes_cb);
448
449 discoverer.SingleSearch(search_id, PeerId(1), std::move(client));
450 discoverer.SingleSearch(search_id, PeerId(1), nullptr);
451
452 RETURN_IF_FATAL(RunUntilIdle());
453
454 EXPECT_EQ(2u, searches.size());
455 ASSERT_EQ(2u, cb_count);
456 ASSERT_EQ(1u, clients_destroyed());
457 }
458
459 // Disconnected on the other end before the discovery completes
TEST_F(ServiceDiscovererTest,Disconnected)460 TEST_F(ServiceDiscovererTest, Disconnected) {
461 ServiceDiscoverer discoverer;
462
463 size_t cb_count = 0;
464
465 auto result_cb = [&cb_count](auto, const auto&) { cb_count++; };
466
467 ServiceDiscoverer::SearchId id = discoverer.AddSearch(
468 profile::kSerialPort,
469 {kServiceId, kProtocolDescriptorList, kBluetoothProfileDescriptorList},
470 std::move(result_cb));
471 ASSERT_NE(ServiceDiscoverer::kInvalidSearchId, id);
472 EXPECT_EQ(1u, discoverer.search_count());
473
474 auto client = GetFakeClient();
475
476 std::vector<std::unordered_set<UUID>> searches;
477
478 client->SetServiceSearchAttributesCallback(
479 [cb_dispatcher = heap_dispatcher(), &searches](
480 auto pattern, auto attributes, auto callback) mutable {
481 searches.emplace_back(pattern);
482 if (pattern.count(profile::kSerialPort)) {
483 (void)cb_dispatcher.Post(
484 [cb = std::move(callback)](pw::async::Context /*ctx*/,
485 pw::Status status) {
486 if (status.ok()) {
487 cb(fit::error(Error(HostError::kLinkDisconnected)));
488 }
489 });
490 } else {
491 std::cerr << "Searched for " << pattern.size() << std::endl;
492 for (auto it : pattern) {
493 std::cerr << it.ToString() << std::endl;
494 }
495 FAIL() << "Unexpected search called";
496 }
497 });
498
499 discoverer.StartServiceDiscovery(kDeviceOne, std::move(client));
500
501 RETURN_IF_FATAL(RunUntilIdle());
502
503 EXPECT_EQ(1u, searches.size());
504 ASSERT_EQ(0u, cb_count);
505 ASSERT_EQ(1u, clients_destroyed());
506 }
507
508 // Unregistered Search when partway through the discovery
TEST_F(ServiceDiscovererTest,UnregisterInProgress)509 TEST_F(ServiceDiscovererTest, UnregisterInProgress) {
510 ServiceDiscoverer discoverer;
511
512 std::optional<std::pair<PeerId, std::map<AttributeId, DataElement>>> result;
513
514 ServiceDiscoverer::SearchId id = ServiceDiscoverer::kInvalidSearchId;
515
516 ServiceDiscoverer::ResultCallback one_result_cb =
517 [&discoverer, &result, &id](auto peer_id, const auto& attributes) {
518 // We should only be called once
519 ASSERT_TRUE(!result.has_value());
520 std::map<AttributeId, DataElement> attributes_clone;
521 for (const auto& it : attributes) {
522 auto [inserted_it, added] =
523 attributes_clone.try_emplace(it.first, it.second.Clone());
524 ASSERT_TRUE(added);
525 }
526 result.emplace(peer_id, std::move(attributes_clone));
527 discoverer.RemoveSearch(id);
528 };
529
530 id = discoverer.AddSearch(
531 profile::kAudioSink,
532 {kProtocolDescriptorList, kBluetoothProfileDescriptorList},
533 one_result_cb.share());
534 ASSERT_NE(ServiceDiscoverer::kInvalidSearchId, id);
535 EXPECT_EQ(1u, discoverer.search_count());
536
537 auto client = GetFakeClient();
538
539 std::vector<std::unordered_set<UUID>> searches;
540
541 client->SetServiceSearchAttributesCallback(
542 [cb_dispatcher = heap_dispatcher(), &searches](
543 auto pattern, auto attributes, auto callback) mutable {
544 searches.emplace_back(pattern);
545 if (pattern.count(profile::kAudioSink)) {
546 (void)cb_dispatcher.Post([cb = std::move(callback)](
547 pw::async::Context /*ctx*/,
548 pw::Status status) {
549 if (!status.ok()) {
550 return;
551 }
552 ServiceSearchAttributeResponse rsp;
553 // This would normally be a element list. uint32_t for Testing.
554 rsp.SetAttribute(
555 0, kBluetoothProfileDescriptorList, DataElement(uint32_t{1}));
556 rsp.SetAttribute(
557 1, kProtocolDescriptorList, DataElement(uint32_t{2}));
558
559 if (!cb(fit::ok(std::cref(rsp.attributes(0))))) {
560 return;
561 }
562 if (!cb(fit::ok(std::cref(rsp.attributes(1))))) {
563 return;
564 }
565 cb(fit::error(Error(HostError::kNotFound)));
566 });
567 } else {
568 std::cerr << "Searched for " << pattern.size() << std::endl;
569 for (auto it : pattern) {
570 std::cerr << it.ToString() << std::endl;
571 }
572 FAIL() << "Unexpected search called";
573 }
574 });
575
576 discoverer.StartServiceDiscovery(kDeviceOne, std::move(client));
577
578 RETURN_IF_FATAL(RunUntilIdle());
579
580 EXPECT_EQ(1u, searches.size());
581
582 ASSERT_TRUE(result.has_value());
583 ASSERT_EQ(kDeviceOne, result->first);
584 auto value = result->second[kBluetoothProfileDescriptorList].Get<uint32_t>();
585 ASSERT_TRUE(value);
586 ASSERT_EQ(1u, *value);
587
588 ASSERT_EQ(1u, clients_destroyed());
589 EXPECT_EQ(0u, discoverer.search_count());
590 }
591
592 } // namespace
593 } // namespace bt::sdp
594