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