From 54ef43eb83977a1f10d417c8a0a5d5dcbdb920eb Mon Sep 17 00:00:00 2001 From: Edwin Wu Date: Wed, 11 Mar 2026 08:41:45 -0700 Subject: [PATCH] Refactor GNCBLEMedium to support shared PeripheralManager injection. PiperOrigin-RevId: 882032712 --- .../apple/Mediums/BLE/GNCBLEMedium.h | 9 - .../apple/Mediums/BLE/GNCBLEMedium.m | 107 ++- .../apple/Mediums/BLE/GNCCentralManager.m | 6 +- .../apple/Mediums/BLE/Tests/BUILD | 2 +- .../Mediums/BLE/Tests/GNCBLEMedium+Testing.h | 11 +- .../Mediums/BLE/Tests/GNCBLEMediumTest.m | 404 ------------ .../Mediums/BLE/Tests/GNCBLEMediumTest.mm | 621 ++++++++++++++++++ .../Mediums/BLE/Tests/GNCFakeCBL2CAPChannel.h | 2 +- .../BLE/Tests/GNCFakePeripheralManager.m | 1 + .../apple/Tests/ble_medium_test.mm | 12 +- 10 files changed, 719 insertions(+), 456 deletions(-) delete mode 100644 internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEMediumTest.m create mode 100644 internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEMediumTest.mm diff --git a/internal/platform/implementation/apple/Mediums/BLE/GNCBLEMedium.h b/internal/platform/implementation/apple/Mediums/BLE/GNCBLEMedium.h index 893fd32f..8e0df1fe 100644 --- a/internal/platform/implementation/apple/Mediums/BLE/GNCBLEMedium.h +++ b/internal/platform/implementation/apple/Mediums/BLE/GNCBLEMedium.h @@ -106,15 +106,6 @@ typedef void (^GNCGATTConnectionCompletionHandler)(GNCBLEGATTClient *_Nullable c */ - (instancetype)init; -/** - * Initializes the BLE medium with a custom central manager. - * - * @param centralManager The central manager to use for BLE operations. - * @param queue The queue to use for all internal operations. - */ -- (instancetype)initWithCentralManager:(id)centralManager - queue:(nullable dispatch_queue_t)queue; - /** The hardware supports BOTH advertising extensions and extended scans. */ @property(nonatomic, readonly) BOOL supportsExtendedAdvertisements; diff --git a/internal/platform/implementation/apple/Mediums/BLE/GNCBLEMedium.m b/internal/platform/implementation/apple/Mediums/BLE/GNCBLEMedium.m index b4ab5364..e2d3defc 100644 --- a/internal/platform/implementation/apple/Mediums/BLE/GNCBLEMedium.m +++ b/internal/platform/implementation/apple/Mediums/BLE/GNCBLEMedium.m @@ -22,10 +22,10 @@ #import "internal/platform/implementation/apple/Mediums/BLE/GNCBLEError.h" #import "internal/platform/implementation/apple/Mediums/BLE/GNCBLEGATTClient.h" #import "internal/platform/implementation/apple/Mediums/BLE/GNCBLEGATTServer.h" -#import "internal/platform/implementation/apple/Mediums/BLE/GNCBLEL2CAPClient.h" #import "internal/platform/implementation/apple/Mediums/BLE/GNCBLEL2CAPServer.h" #import "internal/platform/implementation/apple/Mediums/BLE/GNCCentralManager.h" #import "internal/platform/implementation/apple/Mediums/BLE/GNCPeripheral.h" +#import "internal/platform/implementation/apple/Mediums/BLE/GNCPeripheralManagerMultiplexer.h" #import "internal/platform/implementation/apple/Mediums/BLE/NSData+GNCBase85.h" #import "internal/platform/implementation/apple/Mediums/BLE/NSData+GNCWebSafeBase64.h" @@ -41,11 +41,16 @@ static NSError *AlreadyScanningError() { } @interface GNCBLEMedium () +- (instancetype)initWithCentralManager:(id)centralManager + peripheralManager:(nullable id)peripheralManager + queue:(dispatch_queue_t)queue; @end @implementation GNCBLEMedium { dispatch_queue_t _queue; id _centralManager; + id _peripheralManager; + GNCPeripheralManagerMultiplexer *_multiplexer; // The active GATT server, or @nil if one hasn't been started yet. GNCBLEGATTServer *_server; @@ -84,31 +89,45 @@ static NSError *AlreadyScanningError() { // The block to call when the BLE connection times out. dispatch_block_t _connectionTimeoutBlock; + + // The set of connected peripherals. + NSMutableSet *_connectedPeripherals; } - (instancetype)init { dispatch_queue_t queue = dispatch_queue_create(kBLEMediumQueueLabel, DISPATCH_QUEUE_SERIAL); CBCentralManager *centralManager = - [[CBCentralManager alloc] initWithDelegate:self + [[CBCentralManager alloc] initWithDelegate:nil queue:queue options:@{CBCentralManagerOptionShowPowerAlertKey : @NO}]; - return [self initWithCentralManager:centralManager queue:queue]; + CBPeripheralManager *peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:nil + queue:queue]; + return [self initWithCentralManager:centralManager + peripheralManager:peripheralManager + queue:queue]; } // This is private and should only be used for tests. The provided central manager must call // delegate methods on the main queue. - (instancetype)initWithCentralManager:(id)centralManager - queue:(nullable dispatch_queue_t)queue { + peripheralManager:(nullable id)peripheralManager + queue:(dispatch_queue_t)queue { self = [super init]; if (self) { - _queue = queue ?: dispatch_get_main_queue(); + _queue = queue; _centralManager = centralManager; _centralManager.centralDelegate = self; + _peripheralManager = peripheralManager; + if (GNCFeatureFlags.sharedPeripheralManagerEnabled && _peripheralManager) { + _multiplexer = [[GNCPeripheralManagerMultiplexer alloc] initWithCallbackQueue:_queue]; + _peripheralManager.peripheralDelegate = _multiplexer; + } _gattConnectionCompletionHandlers = [NSMutableDictionary dictionary]; _gattDisconnectionHandlers = [NSMutableDictionary dictionary]; _scanningServiceUUIDs = [NSMutableArray array]; _l2capStreamCompletionHandlers = [NSMutableDictionary dictionary]; _l2capPSM = 0; + _connectedPeripherals = [NSMutableSet set]; } return self; } @@ -150,15 +169,22 @@ static NSError *AlreadyScanningError() { return NO; } +- (void)dealloc { + [_centralManager stopScan]; + _centralManager.centralDelegate = nil; + + [_peripheralManager stopAdvertising]; + _peripheralManager.peripheralDelegate = nil; +} + - (void)startAdvertisingData:(NSDictionary *)serviceData completionHandler:(nullable GNCStartAdvertisingCompletionHandler)completionHandler { dispatch_async(_queue, ^{ if (!_server) { if (GNCFeatureFlags.sharedPeripheralManagerEnabled) { - // TODO (edwinwu): Implement shared peripheral manager. - // For now, raise an exception. - [NSException raise:NSInvalidArgumentException - format:@"Not implemented for shared manager is enabled."]; + _server = [[GNCBLEGATTServer alloc] initWithPeripheralManager:_peripheralManager + queue:_queue]; + [_multiplexer addListener:_server]; } else { // In legacy mode, we pass nil (or a separate manager) and do NOT add to multiplexer. // GNCBLEGATTServer will create its own internal manager. @@ -205,7 +231,7 @@ static NSError *AlreadyScanningError() { [_scanningServiceUUIDs addObjectsFromArray:serviceUUIDs]; _advertisementFoundHandler = advertisementFoundHandler; - [self internalStartScanningIfPoweredOn]; + [self updateScanningState]; if (completionHandler) { completionHandler(nil); } @@ -226,7 +252,7 @@ static NSError *AlreadyScanningError() { - (void)resumeMediumScanning:(nullable GNCStartScanningCompletionHandler)completionHandler { dispatch_async(_queue, ^{ - [self internalStartScanningIfPoweredOn]; + [self updateScanningState]; if (completionHandler) { completionHandler(nil); } @@ -238,10 +264,9 @@ static NSError *AlreadyScanningError() { dispatch_async(_queue, ^{ if (!_server) { if (GNCFeatureFlags.sharedPeripheralManagerEnabled) { - // TODO (edwinwu): Implement shared peripheral manager. - // For now, raise an exception. - [NSException raise:NSInvalidArgumentException - format:@"Not implemented for shared manager is enabled."]; + _server = [[GNCBLEGATTServer alloc] initWithPeripheralManager:_peripheralManager + queue:_queue]; + [_multiplexer addListener:_server]; } else { _server = [[GNCBLEGATTServer alloc] initWithPeripheralManager:nil queue:nil]; } @@ -291,10 +316,15 @@ static NSError *AlreadyScanningError() { dispatch_async(_queue, ^{ if (!_l2capServer) { if (GNCFeatureFlags.sharedPeripheralManagerEnabled) { - // TODO (edwinwu): Implement shared peripheral manager. - // For now, raise an exception. - [NSException raise:NSInvalidArgumentException - format:@"Not implemented for shared manager is enabled."]; + _l2capServer = [[GNCBLEL2CAPServer alloc] + initWithPeripheralManager:peripheralManager ?: _peripheralManager + queue:peripheralManager ? dispatch_get_main_queue() : _queue]; + // Only add to multiplexer if we are using the internal shared manager. + // If a specific manager was passed in (e.g. for testing?), we might still need logic here. + // But typically `peripheralManager` is nil in prod. + if (peripheralManager == nil || peripheralManager == _peripheralManager) { + [_multiplexer addListener:_l2capServer]; + } } else { // Legacy mode _l2capServer = [[GNCBLEL2CAPServer alloc] @@ -351,23 +381,32 @@ static NSError *AlreadyScanningError() { #pragma mark - Internal -- (void)internalStartScanningIfPoweredOn { +- (void)updateScanningState { dispatch_assert_queue(_queue); // Scanning can only be done when powered on and must be restarted if bluetooth is turned off // then back on. This will be called anytime the central manager's state changes, so // @c scanForPeripheralsWithServices:options: will be called anytime state transitions back to // powered on. - if (_centralManager.state == CBManagerStatePoweredOn && _scanningServiceUUIDs.count > 0) { - // Stop scanning just in case something outside of this class is already scanning. - [_centralManager stopScan]; - [_centralManager - scanForPeripheralsWithServices:_scanningServiceUUIDs - // Nearby relies on the existence of an advertisement for endpoint - // discovery/lost events, so we must set this key to keep the stream - // of duplicate delegate events flowing. This has adverse effect on - // battery life, but currently necessary. - options:@{CBCentralManagerScanOptionAllowDuplicatesKey : @YES}]; + if (_centralManager.state != CBManagerStatePoweredOn) { + return; } + + // If there are any connected peripherals, stop scanning to avoid high interrupt load on the + // Bluetooth controller, which can cause system-level crashes (XPC connection invalid). + if (_connectedPeripherals.count > 0 || _scanningServiceUUIDs.count == 0) { + [_centralManager stopScan]; + return; + } + + // Stop scanning just in case something outside of this class is already scanning. + [_centralManager stopScan]; + [_centralManager + scanForPeripheralsWithServices:_scanningServiceUUIDs + // Nearby relies on the existence of an advertisement for endpoint + // discovery/lost events, so we must set this key to keep the stream + // of duplicate delegate events flowing. This has adverse effect on + // battery life, but currently necessary. + options:@{CBCentralManagerScanOptionAllowDuplicatesKey : @YES}]; } - (NSDictionary *)decodeAdvertisementData: @@ -479,7 +518,7 @@ static NSError *AlreadyScanningError() { return; } dispatch_assert_queue(_queue); - [self internalStartScanningIfPoweredOn]; + [self updateScanningState]; } - (void)gnc_centralManager:(id)central @@ -496,6 +535,9 @@ static NSError *AlreadyScanningError() { didConnectPeripheral:(id)peripheral { dispatch_assert_queue(_queue); [self cancelConnectionTimeout]; + [_connectedPeripherals addObject:peripheral.identifier]; + [self updateScanningState]; + if (_l2capPSM > 0) { [self internalOpenL2CAPChannel:peripheral]; return; @@ -541,6 +583,9 @@ static NSError *AlreadyScanningError() { didDisconnectPeripheral:(id)peripheral error:(nullable NSError *)error { dispatch_assert_queue(_queue); + [_connectedPeripherals removeObject:peripheral.identifier]; + [self updateScanningState]; + GNCGATTDisconnectionHandler handler = _gattDisconnectionHandlers[peripheral.identifier]; _gattDisconnectionHandlers[peripheral.identifier] = nil; if (handler) { diff --git a/internal/platform/implementation/apple/Mediums/BLE/GNCCentralManager.m b/internal/platform/implementation/apple/Mediums/BLE/GNCCentralManager.m index a86bfee6..7b642fce 100644 --- a/internal/platform/implementation/apple/Mediums/BLE/GNCCentralManager.m +++ b/internal/platform/implementation/apple/Mediums/BLE/GNCCentralManager.m @@ -22,8 +22,10 @@ NS_ASSUME_NONNULL_BEGIN @implementation CBCentralManager (GNCCentralManagerAdditions) - (void)setCentralDelegate:(nullable id)centralDelegate { - NSAssert([centralDelegate conformsToProtocol:@protocol(CBCentralManagerDelegate)], - @"centralDelegate must conform to protocol CBCentralManagerDelegate"); + if (centralDelegate) { + NSAssert([centralDelegate conformsToProtocol:@protocol(CBCentralManagerDelegate)], + @"centralDelegate must conform to protocol CBCentralManagerDelegate"); + } self.delegate = (id)centralDelegate; } diff --git a/internal/platform/implementation/apple/Mediums/BLE/Tests/BUILD b/internal/platform/implementation/apple/Mediums/BLE/Tests/BUILD index 2f6066a3..520b9014 100644 --- a/internal/platform/implementation/apple/Mediums/BLE/Tests/BUILD +++ b/internal/platform/implementation/apple/Mediums/BLE/Tests/BUILD @@ -32,7 +32,7 @@ objc_library( "GNCBLEL2CAPFakeInputOutputStream.m", "GNCBLEL2CAPServerTest.mm", "GNCBLEL2CAPStreamTest.m", - "GNCBLEMediumTest.m", + "GNCBLEMediumTest.mm", "GNCFakeBLEGATTServer.m", "GNCFakeBLEMedium.m", "GNCFakeCBL2CAPChannel.m", diff --git a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEMedium+Testing.h b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEMedium+Testing.h index 5da28b57..9761a796 100644 --- a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEMedium+Testing.h +++ b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEMedium+Testing.h @@ -24,16 +24,19 @@ NS_ASSUME_NONNULL_BEGIN @interface GNCBLEMedium (Testing) /** - * Creates a BLE Medium with a provided central manager. + * Creates a BLE Medium with a provided central manager and peripheral manager. * - * This is only exposed for testing and can be used to inject a fake central manager. + * This is only exposed for tests and can be used to inject a fake central manager and + * peripheral manager. * * @param centralManager The central manager instance. + * @param peripheralManager The peripheral manager instance. * @param queue The queue to run on, this must match the queue that the central manager's delegate - * is running on. Defaults to the main queue when @c nil. + * is running on. */ - (instancetype)initWithCentralManager:(id)centralManager - queue:(nullable dispatch_queue_t)queue; + peripheralManager:(nullable id)peripheralManager + queue:(dispatch_queue_t)queue; - (NSDictionary *)decodeAdvertisementData: (NSDictionary *)advertisementData; diff --git a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEMediumTest.m b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEMediumTest.m deleted file mode 100644 index 10b623d9..00000000 --- a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEMediumTest.m +++ /dev/null @@ -1,404 +0,0 @@ -// Copyright 2023 Google LLC -// -// 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. - -#import "internal/platform/implementation/apple/Mediums/BLE/GNCBLEMedium.h" - -#import -#import -#import - -#import "internal/platform/implementation/apple/Mediums/BLE/GNCBLEGATTClient.h" -#import "internal/platform/implementation/apple/Mediums/BLE/GNCBLEGATTServer.h" -#import "internal/platform/implementation/apple/Mediums/BLE/GNCPeripheral.h" -#import "internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEL2CAPClient+Testing.h" -#import "internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEMedium+Testing.h" -#import "internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeCentralManager.h" -#import "internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakePeripheral.h" -#import "internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakePeripheralManager.h" - -static NSString *const kServiceUUID = @"0000FEF3-0000-1000-8000-00805F9B34FB"; - -@interface GNCBLEMediumTest : XCTestCase -@end - -@implementation GNCBLEMediumTest - -#pragma mark - Supports Extended Advertisements - -- (void)testSupportsExtendedAdvertisements { - GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; - - GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil]; - - XCTAssertFalse([medium supportsExtendedAdvertisements]); -} - -#pragma mark - Start Scanning - -- (void)testStartScanning { - GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; - GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil]; - XCTestExpectation *startScanningExpectation = - [[XCTestExpectation alloc] initWithDescription:@"Start scanning."]; - XCTestExpectation *advertisementFoundExpectation = - [[XCTestExpectation alloc] initWithDescription:@"Advertisement found."]; - CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID]; - - [fakeCentralManager simulateCentralManagerDidUpdateState:CBManagerStatePoweredOn]; - - [medium startScanningForService:serviceUUID - advertisementFoundHandler:^(id peripheral, - NSDictionary *data) { - NSDictionary *expected = @{ - serviceUUID : [@"test" dataUsingEncoding:NSUTF8StringEncoding], - }; - XCTAssertEqualObjects(expected, data); - [advertisementFoundExpectation fulfill]; - } - completionHandler:^(NSError *error) { - XCTAssertNil(error); - [startScanningExpectation fulfill]; - }]; - - [self waitForExpectations:@[ startScanningExpectation ] timeout:3]; - - XCTAssertEqualObjects(@[ serviceUUID ], fakeCentralManager.serviceUUIDs); - - [fakeCentralManager - simulateCentralManagerDidDiscoverPeripheral:[[GNCFakePeripheral alloc] init] - advertisementData:@{ - CBAdvertisementDataLocalNameKey : @"dGVzdA", - CBAdvertisementDataServiceUUIDsKey : @[ serviceUUID ], - }]; - - [self waitForExpectations:@[ advertisementFoundExpectation ] timeout:3]; -} - -- (void)testAlreadyScanning { - GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; - GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil]; - XCTestExpectation *expectation = - [[XCTestExpectation alloc] initWithDescription:@"Start scanning."]; - - [medium startScanningForService:[CBUUID UUIDWithString:kServiceUUID] - advertisementFoundHandler:^(id peripheral, - NSDictionary *data) { - } - completionHandler:nil]; - - [medium startScanningForService:[CBUUID UUIDWithString:kServiceUUID] - advertisementFoundHandler:^(id peripheral, - NSDictionary *data) { - } - completionHandler:^(NSError *error) { - XCTAssertNotNil(error); - [expectation fulfill]; - }]; - - [self waitForExpectations:@[ expectation ] timeout:3]; -} - -- (void)testStartStopStartScanning { - GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; - GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil]; - XCTestExpectation *expectation = - [[XCTestExpectation alloc] initWithDescription:@"Start scanning."]; - - CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID]; - - [medium startScanningForService:serviceUUID - advertisementFoundHandler:^(id peripheral, - NSDictionary *data) { - } - completionHandler:^(NSError *error) { - XCTAssertNil(error); - [medium stopScanningWithCompletionHandler:^(NSError *error) { - XCTAssertNil(error); - [medium startScanningForService:serviceUUID - advertisementFoundHandler:^(id peripheral, - NSDictionary *data) { - } - completionHandler:^(NSError *error) { - XCTAssertNil(error); - [expectation fulfill]; - }]; - }]; - }]; - - [self waitForExpectations:@[ expectation ] timeout:3]; -} - -#pragma mark - Decode Advertisement Data - -- (void)testDecodeAndroidStyleAdvertisementData { - GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; - GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil]; - CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID]; - - NSDictionary *expected = @{ - serviceUUID : [@"test" dataUsingEncoding:NSUTF8StringEncoding], - }; - - NSDictionary *data = @{ - CBAdvertisementDataServiceDataKey : @{ - serviceUUID : [@"test" dataUsingEncoding:NSUTF8StringEncoding], - }, - }; - NSDictionary *actual = [medium decodeAdvertisementData:data]; - - XCTAssertEqualObjects(expected, actual); -} - -- (void)testDecodeAndroidStyleAdvertisementDataWithLocalName { - GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; - GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil]; - CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID]; - - NSDictionary *expected = @{ - serviceUUID : [@"test" dataUsingEncoding:NSUTF8StringEncoding], - }; - - NSDictionary *data = @{ - CBAdvertisementDataLocalNameKey : @"Nearby", // Just happens to be base64 decodable. - CBAdvertisementDataServiceUUIDsKey : @[ serviceUUID ], - CBAdvertisementDataServiceDataKey : @{ - serviceUUID : [@"test" dataUsingEncoding:NSUTF8StringEncoding], - }, - }; - NSDictionary *actual = [medium decodeAdvertisementData:data]; - - XCTAssertEqualObjects(expected, actual); -} - -- (void)testDecodeAppleStyleAdvertisementData { - GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; - GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil]; - CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID]; - - NSDictionary *expected = @{ - serviceUUID : [@"test" dataUsingEncoding:NSUTF8StringEncoding], - }; - - NSDictionary *data = @{ - CBAdvertisementDataLocalNameKey : @"dGVzdA", - CBAdvertisementDataServiceUUIDsKey : @[ serviceUUID ], - }; - NSDictionary *actual = [medium decodeAdvertisementData:data]; - - XCTAssertEqualObjects(expected, actual); -} - -- (void)testDecodeInvalidAdvertisementData { - GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; - GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil]; - CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID]; - - NSDictionary *data = @{ - CBAdvertisementDataLocalNameKey : @"!@#$", - CBAdvertisementDataServiceUUIDsKey : @[ serviceUUID ], - }; - NSDictionary *actual = [medium decodeAdvertisementData:data]; - - XCTAssertEqualObjects(@{}, actual); -} - -- (void)testDecodeEmptyAdvertisementData { - GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; - GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil]; - - NSDictionary *actual = [medium decodeAdvertisementData:@{}]; - - XCTAssertEqualObjects(@{}, actual); -} - -#pragma mark - Start GATT Server - -- (void)testStartGATTServer { - GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; - GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil]; - XCTestExpectation *expectation = - [[XCTestExpectation alloc] initWithDescription:@"Start GATT server."]; - - [medium startGATTServerWithCompletionHandler:^(GNCBLEGATTServer *server, NSError *error) { - XCTAssertNotNil(server); - XCTAssertNil(error); - [expectation fulfill]; - }]; - - [self waitForExpectations:@[ expectation ] timeout:3]; -} - -#pragma mark - Start Advertising - -- (void)testStartAdvertising { - GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; - GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil]; - XCTestExpectation *expectation = - [[XCTestExpectation alloc] initWithDescription:@"Start advertising."]; - - // Start advertising is fully covered with @c GNCBLEGATTServer tests. We are passing invalid - // advertising data here so we can test code paths relevant to @c GNCBLEMedium, but bail early - // enough to avoid making actual CoreBluetooth calls. - [medium startAdvertisingData:@{} - completionHandler:^(NSError *error) { - XCTAssertNotNil(error); - [expectation fulfill]; - }]; - - [self waitForExpectations:@[ expectation ] timeout:3]; -} - -- (void)testStopAdvertising { - GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; - GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil]; - XCTestExpectation *expectation = - [[XCTestExpectation alloc] initWithDescription:@"Stop advertising."]; - - // Stop advertising is fully covered with @c GNCBLEGATTServer tests. We are only testing stopping - // without having started which tests the code paths relevant to @c GNCBLEMedium. - [medium stopAdvertisingWithCompletionHandler:^(NSError *error) { - XCTAssertNil(error); - [expectation fulfill]; - }]; - - [self waitForExpectations:@[ expectation ] timeout:3]; -} - -#pragma mark - Open L2CAP Channel - -- (void)testOpenL2CAPServerSocket { - GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; - GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init]; - GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil]; - XCTestExpectation *psmPublishedexpectation = - [[XCTestExpectation alloc] initWithDescription:@"PSM published."]; - XCTestExpectation *channelOpenedexpectation = - [[XCTestExpectation alloc] initWithDescription:@"Channel opened."]; - - [fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn]; - - // Open L2CAP server is fully covered with @c GNCBLEL2CAPServer tests. - [medium - openL2CAPServerWithPSMPublishedCompletionHandler:^(uint16_t PSM, NSError *error) { - XCTAssertEqual(PSM, fakePeripheralManager.PSM); - XCTAssertNil(error); - [psmPublishedexpectation fulfill]; - } - channelOpenedCompletionHandler:^(GNCBLEL2CAPStream *stream, NSError *error) { - XCTAssertNil(error); - [channelOpenedexpectation fulfill]; - } - peripheralManager:fakePeripheralManager]; - - [self waitForExpectations:@[ psmPublishedexpectation ] timeout:0.1]; - [self waitForExpectations:@[ channelOpenedexpectation ] timeout:0.5]; -} - -- (void)testSuccessfulOpenL2CAPChannel { - GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; - GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil]; - XCTestExpectation *expectation = - [[XCTestExpectation alloc] initWithDescription:@"Open L2CAP channel."]; - - GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init]; - GNCBLEL2CAPClient *l2capClient = - [[GNCBLEL2CAPClient alloc] initWithQueue:nil - requestDisconnectionHandler:^(id _Nonnull peripheral){ - }]; - [medium setL2CAPClient:l2capClient]; - [medium openL2CAPChannelWithPSM:123 - peripheral:fakePeripheral - completionHandler:^(GNCBLEL2CAPStream *_Nullable stream, NSError *_Nullable error) { - [expectation fulfill]; - }]; - - [self waitForExpectations:@[ expectation ] timeout:3]; -} - -#pragma mark - Connect - -- (void)testSuccessfulConnect { - GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; - GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil]; - XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"Connect."]; - - [medium connectToGATTServerForPeripheral:[[GNCFakePeripheral alloc] init] - disconnectionHandler:nil - completionHandler:^(GNCBLEGATTClient *client, NSError *error) { - XCTAssertNotNil(client); - XCTAssertNil(error); - [expectation fulfill]; - }]; - - [self waitForExpectations:@[ expectation ] timeout:3]; -} - -- (void)testFailedConnect { - GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; - GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil]; - XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"Connect."]; - - fakeCentralManager.didFailToConnectPeripheralError = [NSError errorWithDomain:@"fake" - code:0 - userInfo:nil]; - - [medium connectToGATTServerForPeripheral:[[GNCFakePeripheral alloc] init] - disconnectionHandler:nil - completionHandler:^(GNCBLEGATTClient *client, NSError *error) { - XCTAssertNil(client); - XCTAssertNotNil(error); - [expectation fulfill]; - }]; - - [self waitForExpectations:@[ expectation ] timeout:3]; -} - -- (void)testDisconnect { - GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; - GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil]; - XCTestExpectation *disconnectExpectation = - [[XCTestExpectation alloc] initWithDescription:@"Disconnect."]; - - GNCFakePeripheral *peripheral = [[GNCFakePeripheral alloc] init]; - - [medium connectToGATTServerForPeripheral:peripheral - disconnectionHandler:^() { - [disconnectExpectation fulfill]; - } - completionHandler:^(GNCBLEGATTClient *client, NSError *error) { - XCTAssertNotNil(client); - XCTAssertNil(error); - [client disconnect]; - }]; - - [self waitForExpectations:@[ disconnectExpectation ] timeout:3]; -} - -- (void)testRetrievePeripheralWithIdentifier_exists { - GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; - GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil]; - XCTAssertNotNil( - [medium retrievePeripheralWithIdentifier: - [[NSUUID alloc] initWithUUIDString:@"11111111-1111-1111-1111-111111111111"]]); -} - -- (void)testRetrievePeripheralWithIdentifier_doesNotExist { - GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; - GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil]; - XCTAssertNil( - [medium retrievePeripheralWithIdentifier: - [[NSUUID alloc] initWithUUIDString:@"11111111-1111-1111-1111-111111111112"]]); -} - -@end diff --git a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEMediumTest.mm b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEMediumTest.mm new file mode 100644 index 00000000..49e0a462 --- /dev/null +++ b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEMediumTest.mm @@ -0,0 +1,621 @@ +// Copyright 2023 Google LLC +// +// 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. + +#import "internal/platform/implementation/apple/Mediums/BLE/GNCBLEMedium.h" + +#import +#import +#import + +#import "internal/platform/implementation/apple/Mediums/BLE/GNCBLEGATTClient.h" +#import "internal/platform/implementation/apple/Mediums/BLE/GNCBLEGATTServer.h" +#import "internal/platform/implementation/apple/Mediums/BLE/GNCPeripheral.h" +#import "internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEL2CAPClient+Testing.h" +#import "internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEMedium+Testing.h" +#import "internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeCentralManager.h" +#import "internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakePeripheral.h" +#import "internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakePeripheralManager.h" + +#include "connections/implementation/flags/nearby_connections_feature_flags.h" +#include "internal/flags/nearby_flags.h" + +static NSString *const kServiceUUID = @"0000FEF3-0000-1000-8000-00805F9B34FB"; + +@interface GNCBLEMediumTest : XCTestCase +@end + +@implementation GNCBLEMediumTest + +- (void)tearDown { + nearby::NearbyFlags::GetInstance().ResetOverridedValues(); + [super tearDown]; +} + +- (void)testInit_allocatesMultiplexerWhenFlagEnabled { + for (NSNumber *enabled in @[ @NO, @YES ]) { + nearby::NearbyFlags::GetInstance().OverrideBoolFlagValue( + nearby::connections::config_package_nearby::nearby_connections_feature:: + kEnableSharedPeripheralManager, + enabled.boolValue); + GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; + GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init]; + + GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager + peripheralManager:fakePeripheralManager + queue:dispatch_get_main_queue()]; + + id multiplexer = [medium valueForKey:@"_multiplexer"]; + if (enabled.boolValue) { + XCTAssertNotNil(multiplexer); + XCTAssertEqual(fakePeripheralManager.peripheralDelegate, multiplexer); + } else { + XCTAssertNil(multiplexer); + XCTAssertNil(fakePeripheralManager.peripheralDelegate); + } + } +} + +#pragma mark - Supports Extended Advertisements + +- (void)testSupportsExtendedAdvertisements { + for (NSNumber *enabled in @[ @NO, @YES ]) { + nearby::NearbyFlags::GetInstance().OverrideBoolFlagValue( + nearby::connections::config_package_nearby::nearby_connections_feature:: + kEnableSharedPeripheralManager, + enabled.boolValue); + GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; + GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init]; + + GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager + peripheralManager:fakePeripheralManager + queue:dispatch_get_main_queue()]; + + XCTAssertFalse([medium supportsExtendedAdvertisements]); + } +} + +#pragma mark - Start Scanning + +- (void)testStartScanning { + for (NSNumber *enabled in @[ @NO, @YES ]) { + nearby::NearbyFlags::GetInstance().OverrideBoolFlagValue( + nearby::connections::config_package_nearby::nearby_connections_feature:: + kEnableSharedPeripheralManager, + enabled.boolValue); + GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; + GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init]; + GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager + peripheralManager:fakePeripheralManager + queue:dispatch_get_main_queue()]; + XCTestExpectation *startScanningExpectation = + [[XCTestExpectation alloc] initWithDescription:@"Start scanning."]; + XCTestExpectation *advertisementFoundExpectation = + [[XCTestExpectation alloc] initWithDescription:@"Advertisement found."]; + CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID]; + + [fakeCentralManager simulateCentralManagerDidUpdateState:CBManagerStatePoweredOn]; + + [medium startScanningForService:serviceUUID + advertisementFoundHandler:^(id peripheral, + NSDictionary *data) { + NSDictionary *expected = @{ + serviceUUID : [@"test" dataUsingEncoding:NSUTF8StringEncoding], + }; + XCTAssertEqualObjects(expected, data); + [advertisementFoundExpectation fulfill]; + } + completionHandler:^(NSError *error) { + XCTAssertNil(error); + [startScanningExpectation fulfill]; + }]; + + [self waitForExpectations:@[ startScanningExpectation ] timeout:3]; + + XCTAssertEqualObjects(@[ serviceUUID ], fakeCentralManager.serviceUUIDs); + + [fakeCentralManager + simulateCentralManagerDidDiscoverPeripheral:[[GNCFakePeripheral alloc] init] + advertisementData:@{ + CBAdvertisementDataLocalNameKey : @"dGVzdA", + CBAdvertisementDataServiceUUIDsKey : @[ serviceUUID ], + }]; + + [self waitForExpectations:@[ advertisementFoundExpectation ] timeout:3]; + } +} + +- (void)testAlreadyScanning { + for (NSNumber *enabled in @[ @NO, @YES ]) { + nearby::NearbyFlags::GetInstance().OverrideBoolFlagValue( + nearby::connections::config_package_nearby::nearby_connections_feature:: + kEnableSharedPeripheralManager, + enabled.boolValue); + GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; + GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init]; + GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager + peripheralManager:fakePeripheralManager + queue:dispatch_get_main_queue()]; + XCTestExpectation *expectation = + [[XCTestExpectation alloc] initWithDescription:@"Start scanning."]; + + [medium startScanningForService:[CBUUID UUIDWithString:kServiceUUID] + advertisementFoundHandler:^(id peripheral, + NSDictionary *data) { + } + completionHandler:nil]; + + [medium startScanningForService:[CBUUID UUIDWithString:kServiceUUID] + advertisementFoundHandler:^(id peripheral, + NSDictionary *data) { + } + completionHandler:^(NSError *error) { + XCTAssertNotNil(error); + [expectation fulfill]; + }]; + + [self waitForExpectations:@[ expectation ] timeout:3]; + } +} + +- (void)testStartStopStartScanning { + for (NSNumber *enabled in @[ @NO, @YES ]) { + nearby::NearbyFlags::GetInstance().OverrideBoolFlagValue( + nearby::connections::config_package_nearby::nearby_connections_feature:: + kEnableSharedPeripheralManager, + enabled.boolValue); + GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; + GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init]; + GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager + peripheralManager:fakePeripheralManager + queue:dispatch_get_main_queue()]; + XCTestExpectation *expectation = + [[XCTestExpectation alloc] initWithDescription:@"Start scanning."]; + + CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID]; + + [medium startScanningForService:serviceUUID + advertisementFoundHandler:^(id peripheral, + NSDictionary *data) { + } + completionHandler:^(NSError *error) { + XCTAssertNil(error); + [medium stopScanningWithCompletionHandler:^(NSError *error) { + XCTAssertNil(error); + [medium startScanningForService:serviceUUID + advertisementFoundHandler:^(id peripheral, + NSDictionary *data) { + } + completionHandler:^(NSError *error) { + XCTAssertNil(error); + [expectation fulfill]; + }]; + }]; + }]; + + [self waitForExpectations:@[ expectation ] timeout:3]; + } +} + +#pragma mark - Decode Advertisement Data + +- (void)testDecodeAndroidStyleAdvertisementData { + for (NSNumber *enabled in @[ @NO, @YES ]) { + nearby::NearbyFlags::GetInstance().OverrideBoolFlagValue( + nearby::connections::config_package_nearby::nearby_connections_feature:: + kEnableSharedPeripheralManager, + enabled.boolValue); + GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; + GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init]; + GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager + peripheralManager:fakePeripheralManager + queue:dispatch_get_main_queue()]; + CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID]; + + NSDictionary *expected = @{ + serviceUUID : [@"test" dataUsingEncoding:NSUTF8StringEncoding], + }; + + NSDictionary *data = @{ + CBAdvertisementDataServiceDataKey : @{ + serviceUUID : [@"test" dataUsingEncoding:NSUTF8StringEncoding], + }, + }; + NSDictionary *actual = [medium decodeAdvertisementData:data]; + + XCTAssertEqualObjects(expected, actual); + } +} + +- (void)testDecodeAndroidStyleAdvertisementDataWithLocalName { + for (NSNumber *enabled in @[ @NO, @YES ]) { + nearby::NearbyFlags::GetInstance().OverrideBoolFlagValue( + nearby::connections::config_package_nearby::nearby_connections_feature:: + kEnableSharedPeripheralManager, + enabled.boolValue); + GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; + GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init]; + GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager + peripheralManager:fakePeripheralManager + queue:dispatch_get_main_queue()]; + CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID]; + + NSDictionary *expected = @{ + serviceUUID : [@"test" dataUsingEncoding:NSUTF8StringEncoding], + }; + + NSDictionary *data = @{ + CBAdvertisementDataLocalNameKey : @"Nearby", // Just happens to be base64 decodable. + CBAdvertisementDataServiceUUIDsKey : @[ serviceUUID ], + CBAdvertisementDataServiceDataKey : @{ + serviceUUID : [@"test" dataUsingEncoding:NSUTF8StringEncoding], + }, + }; + NSDictionary *actual = [medium decodeAdvertisementData:data]; + + XCTAssertEqualObjects(expected, actual); + } +} + +- (void)testDecodeAppleStyleAdvertisementData { + for (NSNumber *enabled in @[ @NO, @YES ]) { + nearby::NearbyFlags::GetInstance().OverrideBoolFlagValue( + nearby::connections::config_package_nearby::nearby_connections_feature:: + kEnableSharedPeripheralManager, + enabled.boolValue); + GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; + GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init]; + GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager + peripheralManager:fakePeripheralManager + queue:dispatch_get_main_queue()]; + CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID]; + + NSDictionary *expected = @{ + serviceUUID : [@"test" dataUsingEncoding:NSUTF8StringEncoding], + }; + + NSDictionary *data = @{ + CBAdvertisementDataLocalNameKey : @"dGVzdA", + CBAdvertisementDataServiceUUIDsKey : @[ serviceUUID ], + }; + NSDictionary *actual = [medium decodeAdvertisementData:data]; + + XCTAssertEqualObjects(expected, actual); + } +} + +- (void)testDecodeInvalidAdvertisementData { + for (NSNumber *enabled in @[ @NO, @YES ]) { + nearby::NearbyFlags::GetInstance().OverrideBoolFlagValue( + nearby::connections::config_package_nearby::nearby_connections_feature:: + kEnableSharedPeripheralManager, + enabled.boolValue); + GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; + GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init]; + GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager + peripheralManager:fakePeripheralManager + queue:dispatch_get_main_queue()]; + CBUUID *serviceUUID = [CBUUID UUIDWithString:kServiceUUID]; + + NSDictionary *data = @{ + CBAdvertisementDataLocalNameKey : @"!@#$", + CBAdvertisementDataServiceUUIDsKey : @[ serviceUUID ], + }; + NSDictionary *actual = [medium decodeAdvertisementData:data]; + + XCTAssertEqualObjects(@{}, actual); + } +} + +- (void)testDecodeEmptyAdvertisementData { + for (NSNumber *enabled in @[ @NO, @YES ]) { + nearby::NearbyFlags::GetInstance().OverrideBoolFlagValue( + nearby::connections::config_package_nearby::nearby_connections_feature:: + kEnableSharedPeripheralManager, + enabled.boolValue); + GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; + GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init]; + GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager + peripheralManager:fakePeripheralManager + queue:dispatch_get_main_queue()]; + + NSDictionary *actual = [medium decodeAdvertisementData:@{}]; + + XCTAssertEqualObjects(@{}, actual); + } +} + +#pragma mark - Start GATT Server + +- (void)testStartGATTServer { + for (NSNumber *enabled in @[ @NO, @YES ]) { + nearby::NearbyFlags::GetInstance().OverrideBoolFlagValue( + nearby::connections::config_package_nearby::nearby_connections_feature:: + kEnableSharedPeripheralManager, + enabled.boolValue); + GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; + GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init]; + GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager + peripheralManager:fakePeripheralManager + queue:dispatch_get_main_queue()]; + XCTestExpectation *expectation = + [[XCTestExpectation alloc] initWithDescription:@"Start GATT server."]; + + [medium startGATTServerWithCompletionHandler:^(GNCBLEGATTServer *server, NSError *error) { + XCTAssertNotNil(server); + XCTAssertNil(error); + + // Verify internal structure based on flag + id mediumManager = [medium valueForKey:@"_peripheralManager"]; + id serverManager = [server valueForKey:@"_peripheralManager"]; + + if (enabled.boolValue) { + XCTAssertEqual(mediumManager, serverManager); + id multiplexer = [medium valueForKey:@"_multiplexer"]; + XCTAssertEqual([mediumManager peripheralDelegate], multiplexer); + } else { + XCTAssertNotEqual(mediumManager, serverManager); + id manager = [server valueForKey:@"_peripheralManager"]; + XCTAssertEqual([manager peripheralDelegate], server); + } + + [expectation fulfill]; + }]; + + [self waitForExpectations:@[ expectation ] timeout:3]; + } +} + +#pragma mark - Start Advertising + +- (void)testStartAdvertising { + for (NSNumber *enabled in @[ @NO, @YES ]) { + nearby::NearbyFlags::GetInstance().OverrideBoolFlagValue( + nearby::connections::config_package_nearby::nearby_connections_feature:: + kEnableSharedPeripheralManager, + enabled.boolValue); + GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; + GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init]; + GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager + peripheralManager:fakePeripheralManager + queue:dispatch_get_main_queue()]; + XCTestExpectation *expectation = + [[XCTestExpectation alloc] initWithDescription:@"Start advertising."]; + + // Start advertising is fully covered with @c GNCBLEGATTServer tests. We are passing invalid + // advertising data here so we can test code paths relevant to @c GNCBLEMedium, but bail early + // enough to avoid making actual CoreBluetooth calls. + [medium startAdvertisingData:@{} + completionHandler:^(NSError *error) { + XCTAssertNotNil(error); + [expectation fulfill]; + }]; + + [self waitForExpectations:@[ expectation ] timeout:3]; + } +} + +- (void)testStopAdvertising { + for (NSNumber *enabled in @[ @NO, @YES ]) { + nearby::NearbyFlags::GetInstance().OverrideBoolFlagValue( + nearby::connections::config_package_nearby::nearby_connections_feature:: + kEnableSharedPeripheralManager, + enabled.boolValue); + GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; + GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init]; + GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager + peripheralManager:fakePeripheralManager + queue:dispatch_get_main_queue()]; + XCTestExpectation *expectation = + [[XCTestExpectation alloc] initWithDescription:@"Stop advertising."]; + + // Stop advertising is fully covered with @c GNCBLEGATTServer tests. We are only testing stopping + // without having started which tests the code paths relevant to @c GNCBLEMedium. + [medium stopAdvertisingWithCompletionHandler:^(NSError *error) { + XCTAssertNil(error); + [expectation fulfill]; + }]; + + [self waitForExpectations:@[ expectation ] timeout:3]; + } +} + +#pragma mark - Open L2CAP Channel + +- (void)testOpenL2CAPServerSocket { + for (NSNumber *enabled in @[ @NO, @YES ]) { + nearby::NearbyFlags::GetInstance().OverrideBoolFlagValue( + nearby::connections::config_package_nearby::nearby_connections_feature:: + kEnableSharedPeripheralManager, + enabled.boolValue); + GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; + GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init]; + GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager + peripheralManager:fakePeripheralManager + queue:dispatch_get_main_queue()]; + XCTestExpectation *psmPublishedexpectation = + [[XCTestExpectation alloc] initWithDescription:@"PSM published."]; + XCTestExpectation *channelOpenedexpectation = + [[XCTestExpectation alloc] initWithDescription:@"Channel opened."]; + + [fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn]; + + // Open L2CAP server is fully covered with @c GNCBLEL2CAPServer tests. + [medium + openL2CAPServerWithPSMPublishedCompletionHandler:^(uint16_t PSM, NSError *error) { + XCTAssertEqual(PSM, fakePeripheralManager.PSM); + XCTAssertNil(error); + [psmPublishedexpectation fulfill]; + } + channelOpenedCompletionHandler:^(GNCBLEL2CAPStream *stream, NSError *error) { + XCTAssertNil(error); + [channelOpenedexpectation fulfill]; + } + peripheralManager:fakePeripheralManager]; + + [self waitForExpectations:@[ psmPublishedexpectation ] timeout:0.1]; + [self waitForExpectations:@[ channelOpenedexpectation ] timeout:0.5]; + } +} + +- (void)testSuccessfulOpenL2CAPChannel { + for (NSNumber *enabled in @[ @NO, @YES ]) { + nearby::NearbyFlags::GetInstance().OverrideBoolFlagValue( + nearby::connections::config_package_nearby::nearby_connections_feature:: + kEnableSharedPeripheralManager, + enabled.boolValue); + GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; + GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init]; + GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager + peripheralManager:fakePeripheralManager + queue:dispatch_get_main_queue()]; + XCTestExpectation *expectation = + [[XCTestExpectation alloc] initWithDescription:@"Open L2CAP channel."]; + + GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init]; + GNCBLEL2CAPClient *l2capClient = + [[GNCBLEL2CAPClient alloc] initWithQueue:nil + requestDisconnectionHandler:^(id _Nonnull peripheral){ + }]; + [medium setL2CAPClient:l2capClient]; + [medium openL2CAPChannelWithPSM:123 + peripheral:fakePeripheral + completionHandler:^(GNCBLEL2CAPStream *_Nullable stream, NSError *_Nullable error) { + [expectation fulfill]; + }]; + + [self waitForExpectations:@[ expectation ] timeout:3]; + } +} + +#pragma mark - Connect + +- (void)testSuccessfulConnect { + for (NSNumber *enabled in @[ @NO, @YES ]) { + nearby::NearbyFlags::GetInstance().OverrideBoolFlagValue( + nearby::connections::config_package_nearby::nearby_connections_feature:: + kEnableSharedPeripheralManager, + enabled.boolValue); + GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; + GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init]; + GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager + peripheralManager:fakePeripheralManager + queue:dispatch_get_main_queue()]; + XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"Connect."]; + + [medium connectToGATTServerForPeripheral:[[GNCFakePeripheral alloc] init] + disconnectionHandler:nil + completionHandler:^(GNCBLEGATTClient *client, NSError *error) { + XCTAssertNotNil(client); + XCTAssertNil(error); + [expectation fulfill]; + }]; + + [self waitForExpectations:@[ expectation ] timeout:3]; + } +} + +- (void)testFailedConnect { + for (NSNumber *enabled in @[ @NO, @YES ]) { + nearby::NearbyFlags::GetInstance().OverrideBoolFlagValue( + nearby::connections::config_package_nearby::nearby_connections_feature:: + kEnableSharedPeripheralManager, + enabled.boolValue); + GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; + GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init]; + GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager + peripheralManager:fakePeripheralManager + queue:dispatch_get_main_queue()]; + XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"Connect."]; + + fakeCentralManager.didFailToConnectPeripheralError = [NSError errorWithDomain:@"fake" + code:0 + userInfo:nil]; + + [medium connectToGATTServerForPeripheral:[[GNCFakePeripheral alloc] init] + disconnectionHandler:nil + completionHandler:^(GNCBLEGATTClient *client, NSError *error) { + XCTAssertNil(client); + XCTAssertNotNil(error); + [expectation fulfill]; + }]; + + [self waitForExpectations:@[ expectation ] timeout:3]; + } +} + +- (void)testDisconnect { + for (NSNumber *enabled in @[ @NO, @YES ]) { + nearby::NearbyFlags::GetInstance().OverrideBoolFlagValue( + nearby::connections::config_package_nearby::nearby_connections_feature:: + kEnableSharedPeripheralManager, + enabled.boolValue); + GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; + GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init]; + GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager + peripheralManager:fakePeripheralManager + queue:dispatch_get_main_queue()]; + XCTestExpectation *disconnectExpectation = + [[XCTestExpectation alloc] initWithDescription:@"Disconnect."]; + + GNCFakePeripheral *peripheral = [[GNCFakePeripheral alloc] init]; + + [medium connectToGATTServerForPeripheral:peripheral + disconnectionHandler:^() { + [disconnectExpectation fulfill]; + } + completionHandler:^(GNCBLEGATTClient *client, NSError *error) { + XCTAssertNotNil(client); + XCTAssertNil(error); + [client disconnect]; + }]; + + [self waitForExpectations:@[ disconnectExpectation ] timeout:3]; + } +} + +- (void)testRetrievePeripheralWithIdentifier_exists { + for (NSNumber *enabled in @[ @NO, @YES ]) { + nearby::NearbyFlags::GetInstance().OverrideBoolFlagValue( + nearby::connections::config_package_nearby::nearby_connections_feature:: + kEnableSharedPeripheralManager, + enabled.boolValue); + GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; + GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init]; + GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager + peripheralManager:fakePeripheralManager + queue:dispatch_get_main_queue()]; + XCTAssertNotNil( + [medium retrievePeripheralWithIdentifier: + [[NSUUID alloc] initWithUUIDString:@"11111111-1111-1111-1111-111111111111"]]); + } +} + +- (void)testRetrievePeripheralWithIdentifier_doesNotExist { + for (NSNumber *enabled in @[ @NO, @YES ]) { + nearby::NearbyFlags::GetInstance().OverrideBoolFlagValue( + nearby::connections::config_package_nearby::nearby_connections_feature:: + kEnableSharedPeripheralManager, + enabled.boolValue); + GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; + GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init]; + GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager + peripheralManager:fakePeripheralManager + queue:dispatch_get_main_queue()]; + XCTAssertNil( + [medium retrievePeripheralWithIdentifier: + [[NSUUID alloc] initWithUUIDString:@"11111111-1111-1111-1111-111111111112"]]); + } +} + +@end diff --git a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeCBL2CAPChannel.h b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeCBL2CAPChannel.h index 69a1b791..465b3120 100644 --- a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeCBL2CAPChannel.h +++ b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeCBL2CAPChannel.h @@ -29,7 +29,7 @@ NS_ASSUME_NONNULL_BEGIN /** The socket file descriptor for the L2CAP channel. */ @property(nonatomic, readonly) int socketFD; /** The PSM (Protocol/Service Multiplexer) of the L2CAP channel. */ -@property(nonatomic, readonly) CBL2CAPPSM PSM; +@property(nonatomic, readwrite) CBL2CAPPSM PSM; @end diff --git a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakePeripheralManager.m b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakePeripheralManager.m index 912974df..3f18bf6a 100644 --- a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakePeripheralManager.m +++ b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakePeripheralManager.m @@ -140,6 +140,7 @@ static const uint16_t kPSM = 192; GNCFakeCBL2CAPChannel *fakeChannel = [[GNCFakeCBL2CAPChannel alloc] init]; fakeChannel.inputStream = fakeStream.inputStream; fakeChannel.outputStream = fakeStream.outputStream; + fakeChannel.PSM = _PSM; [_peripheralDelegate gnc_peripheralManager:self didOpenL2CAPChannel:(CBL2CAPChannel *)fakeChannel error:_didOpenL2CAPChannelError]; diff --git a/internal/platform/implementation/apple/Tests/ble_medium_test.mm b/internal/platform/implementation/apple/Tests/ble_medium_test.mm index 23a79dd9..ff4f688a 100644 --- a/internal/platform/implementation/apple/Tests/ble_medium_test.mm +++ b/internal/platform/implementation/apple/Tests/ble_medium_test.mm @@ -23,9 +23,6 @@ #include #include -#import "internal/platform/implementation/apple/Mediums/BLE/GNCBLEGATTClient.h" -#import "internal/platform/implementation/apple/Mediums/BLE/GNCBLEGATTServer.h" -#import "internal/platform/implementation/apple/Mediums/BLE/GNCBLEL2CAPClient.h" #import "internal/platform/implementation/apple/Mediums/BLE/GNCBLEMedium.h" #import "internal/platform/implementation/apple/Mediums/BLE/GNCPeripheral.h" #import "internal/platform/implementation/apple/Mediums/BLE/Sockets/Source/Central/GNSCentralManager.h" @@ -33,7 +30,10 @@ #import "internal/platform/implementation/apple/Mediums/BLE/Sockets/Source/Peripheral/GNSPeripheralManager.h" #import "internal/platform/implementation/apple/Mediums/BLE/Sockets/Source/Peripheral/GNSPeripheralServiceManager.h" #import "internal/platform/implementation/apple/Mediums/BLE/Sockets/Source/Shared/GNSSocket.h" +#import "internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEMedium+Testing.h" #import "internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeBLEGATTServer.h" +#import "internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeCentralManager.h" +#import "internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakePeripheralManager.h" #import "internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeBLEMedium.h" #import "internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakePeripheral.h" #include "internal/platform/implementation/apple/ble_utils.h" @@ -71,7 +71,11 @@ static const char *const kTestServiceID = "TestServiceID"; - (void)setUp { [super setUp]; - _fakeGNCBLEMedium = [[GNCFakeBLEMedium alloc] init]; + GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init]; + GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init]; + _fakeGNCBLEMedium = [[GNCFakeBLEMedium alloc] initWithCentralManager:fakeCentralManager + peripheralManager:fakePeripheralManager + queue:dispatch_get_main_queue()]; _medium = std::make_unique((GNCBLEMedium *)_fakeGNCBLEMedium); }