Refactor GNCBLEMedium to support shared PeripheralManager injection.

PiperOrigin-RevId: 882032712
This commit is contained in:
Edwin Wu
2026-03-11 08:43:30 -07:00
committed by Copybara-Service
parent dcb6918889
commit 54ef43eb83
10 changed files with 719 additions and 456 deletions
@@ -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<GNCCentralManager>)centralManager
queue:(nullable dispatch_queue_t)queue;
/** The hardware supports BOTH advertising extensions and extended scans. */
@property(nonatomic, readonly) BOOL supportsExtendedAdvertisements;
@@ -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 () <GNCCentralManagerDelegate, CBCentralManagerDelegate>
- (instancetype)initWithCentralManager:(id<GNCCentralManager>)centralManager
peripheralManager:(nullable id<GNCPeripheralManager>)peripheralManager
queue:(dispatch_queue_t)queue;
@end
@implementation GNCBLEMedium {
dispatch_queue_t _queue;
id<GNCCentralManager> _centralManager;
id<GNCPeripheralManager> _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<NSUUID *> *_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<GNCCentralManager>)centralManager
queue:(nullable dispatch_queue_t)queue {
peripheralManager:(nullable id<GNCPeripheralManager>)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<CBUUID *, NSData *> *)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<CBUUID *, NSData *> *)decodeAdvertisementData:
@@ -479,7 +518,7 @@ static NSError *AlreadyScanningError() {
return;
}
dispatch_assert_queue(_queue);
[self internalStartScanningIfPoweredOn];
[self updateScanningState];
}
- (void)gnc_centralManager:(id<GNCCentralManager>)central
@@ -496,6 +535,9 @@ static NSError *AlreadyScanningError() {
didConnectPeripheral:(id<GNCPeripheral>)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<GNCPeripheral>)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) {
@@ -22,8 +22,10 @@ NS_ASSUME_NONNULL_BEGIN
@implementation CBCentralManager (GNCCentralManagerAdditions)
- (void)setCentralDelegate:(nullable id<GNCCentralManagerDelegate>)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<CBCentralManagerDelegate>)centralDelegate;
}
@@ -32,7 +32,7 @@ objc_library(
"GNCBLEL2CAPFakeInputOutputStream.m",
"GNCBLEL2CAPServerTest.mm",
"GNCBLEL2CAPStreamTest.m",
"GNCBLEMediumTest.m",
"GNCBLEMediumTest.mm",
"GNCFakeBLEGATTServer.m",
"GNCFakeBLEMedium.m",
"GNCFakeCBL2CAPChannel.m",
@@ -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<GNCCentralManager>)centralManager
queue:(nullable dispatch_queue_t)queue;
peripheralManager:(nullable id<GNCPeripheralManager>)peripheralManager
queue:(dispatch_queue_t)queue;
- (NSDictionary<CBUUID *, NSData *> *)decodeAdvertisementData:
(NSDictionary<NSString *, id> *)advertisementData;
@@ -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 <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
#import <XCTest/XCTest.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/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<GNCPeripheral> peripheral,
NSDictionary<CBUUID *, NSData *> *data) {
NSDictionary<CBUUID *, NSData *> *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<GNCPeripheral> peripheral,
NSDictionary<CBUUID *, NSData *> *data) {
}
completionHandler:nil];
[medium startScanningForService:[CBUUID UUIDWithString:kServiceUUID]
advertisementFoundHandler:^(id<GNCPeripheral> peripheral,
NSDictionary<CBUUID *, NSData *> *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<GNCPeripheral> peripheral,
NSDictionary<CBUUID *, NSData *> *data) {
}
completionHandler:^(NSError *error) {
XCTAssertNil(error);
[medium stopScanningWithCompletionHandler:^(NSError *error) {
XCTAssertNil(error);
[medium startScanningForService:serviceUUID
advertisementFoundHandler:^(id<GNCPeripheral> peripheral,
NSDictionary<CBUUID *, NSData *> *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<CBUUID *, NSData *> *expected = @{
serviceUUID : [@"test" dataUsingEncoding:NSUTF8StringEncoding],
};
NSDictionary<NSString *, id> *data = @{
CBAdvertisementDataServiceDataKey : @{
serviceUUID : [@"test" dataUsingEncoding:NSUTF8StringEncoding],
},
};
NSDictionary<CBUUID *, NSData *> *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<CBUUID *, NSData *> *expected = @{
serviceUUID : [@"test" dataUsingEncoding:NSUTF8StringEncoding],
};
NSDictionary<NSString *, id> *data = @{
CBAdvertisementDataLocalNameKey : @"Nearby", // Just happens to be base64 decodable.
CBAdvertisementDataServiceUUIDsKey : @[ serviceUUID ],
CBAdvertisementDataServiceDataKey : @{
serviceUUID : [@"test" dataUsingEncoding:NSUTF8StringEncoding],
},
};
NSDictionary<CBUUID *, NSData *> *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<CBUUID *, NSData *> *expected = @{
serviceUUID : [@"test" dataUsingEncoding:NSUTF8StringEncoding],
};
NSDictionary<NSString *, id> *data = @{
CBAdvertisementDataLocalNameKey : @"dGVzdA",
CBAdvertisementDataServiceUUIDsKey : @[ serviceUUID ],
};
NSDictionary<CBUUID *, NSData *> *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<NSString *, id> *data = @{
CBAdvertisementDataLocalNameKey : @"!@#$",
CBAdvertisementDataServiceUUIDsKey : @[ serviceUUID ],
};
NSDictionary<CBUUID *, NSData *> *actual = [medium decodeAdvertisementData:data];
XCTAssertEqualObjects(@{}, actual);
}
- (void)testDecodeEmptyAdvertisementData {
GNCFakeCentralManager *fakeCentralManager = [[GNCFakeCentralManager alloc] init];
GNCBLEMedium *medium = [[GNCBLEMedium alloc] initWithCentralManager:fakeCentralManager queue:nil];
NSDictionary<CBUUID *, NSData *> *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<GNCPeripheral> _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
@@ -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 <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
#import <XCTest/XCTest.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/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<GNCPeripheral> peripheral,
NSDictionary<CBUUID *, NSData *> *data) {
NSDictionary<CBUUID *, NSData *> *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<GNCPeripheral> peripheral,
NSDictionary<CBUUID *, NSData *> *data) {
}
completionHandler:nil];
[medium startScanningForService:[CBUUID UUIDWithString:kServiceUUID]
advertisementFoundHandler:^(id<GNCPeripheral> peripheral,
NSDictionary<CBUUID *, NSData *> *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<GNCPeripheral> peripheral,
NSDictionary<CBUUID *, NSData *> *data) {
}
completionHandler:^(NSError *error) {
XCTAssertNil(error);
[medium stopScanningWithCompletionHandler:^(NSError *error) {
XCTAssertNil(error);
[medium startScanningForService:serviceUUID
advertisementFoundHandler:^(id<GNCPeripheral> peripheral,
NSDictionary<CBUUID *, NSData *> *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<CBUUID *, NSData *> *expected = @{
serviceUUID : [@"test" dataUsingEncoding:NSUTF8StringEncoding],
};
NSDictionary<NSString *, id> *data = @{
CBAdvertisementDataServiceDataKey : @{
serviceUUID : [@"test" dataUsingEncoding:NSUTF8StringEncoding],
},
};
NSDictionary<CBUUID *, NSData *> *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<CBUUID *, NSData *> *expected = @{
serviceUUID : [@"test" dataUsingEncoding:NSUTF8StringEncoding],
};
NSDictionary<NSString *, id> *data = @{
CBAdvertisementDataLocalNameKey : @"Nearby", // Just happens to be base64 decodable.
CBAdvertisementDataServiceUUIDsKey : @[ serviceUUID ],
CBAdvertisementDataServiceDataKey : @{
serviceUUID : [@"test" dataUsingEncoding:NSUTF8StringEncoding],
},
};
NSDictionary<CBUUID *, NSData *> *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<CBUUID *, NSData *> *expected = @{
serviceUUID : [@"test" dataUsingEncoding:NSUTF8StringEncoding],
};
NSDictionary<NSString *, id> *data = @{
CBAdvertisementDataLocalNameKey : @"dGVzdA",
CBAdvertisementDataServiceUUIDsKey : @[ serviceUUID ],
};
NSDictionary<CBUUID *, NSData *> *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<NSString *, id> *data = @{
CBAdvertisementDataLocalNameKey : @"!@#$",
CBAdvertisementDataServiceUUIDsKey : @[ serviceUUID ],
};
NSDictionary<CBUUID *, NSData *> *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<CBUUID *, NSData *> *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<GNCPeripheral> _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
@@ -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
@@ -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];
@@ -23,9 +23,6 @@
#include <utility>
#include <vector>
#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<nearby::apple::BleMedium>((GNCBLEMedium *)_fakeGNCBLEMedium);
}