[NC Apple coverage]Add more comprehensive tests for BLE L2CAP components.

PiperOrigin-RevId: 821886071
This commit is contained in:
Edwin Wu
2025-10-20 18:22:03 -07:00
committed by Copybara-Service
parent ed2d8ab7c3
commit 3878ce99c8
12 changed files with 587 additions and 43 deletions
@@ -35,6 +35,7 @@ objc_library(
"GNCBLEMediumTest.m",
"GNCFakeBLEGATTServer.m",
"GNCFakeBLEMedium.m",
"GNCFakeCBL2CAPChannel.m",
"GNCFakeCentralManager.m",
"GNCFakePeripheral.m",
"GNCFakePeripheralManager.m",
@@ -53,9 +54,11 @@ objc_library(
"GNCBLEGATTServer+Testing.h",
"GNCBLEL2CAPClient+Testing.h",
"GNCBLEL2CAPFakeInputOutputStream.h",
"GNCBLEL2CAPServer+Testing.h",
"GNCBLEMedium+Testing.h",
"GNCFakeBLEGATTServer.h",
"GNCFakeBLEMedium.h",
"GNCFakeCBL2CAPChannel.h",
"GNCFakeCentralManager.h",
"GNCFakePeripheral.h",
"GNCFakePeripheralManager.h",
@@ -16,11 +16,13 @@
#import <Foundation/Foundation.h>
#import "internal/platform/implementation/apple/Mediums/BLE/GNCPeripheral.h"
@protocol GNCPeripheral;
NS_ASSUME_NONNULL_BEGIN
@interface GNCBLEL2CAPClient (Testing)
@interface GNCBLEL2CAPClient (Testing) <GNCPeripheralDelegate>
/**
* Initializes the L2CAP client with a provided queue and request disconnection handler.
@@ -32,8 +34,15 @@ NS_ASSUME_NONNULL_BEGIN
* @param requestDisconnectionHandler Called on a private queue with @c peripheral when the
* connection to the peripheral should be cancelled.
*/
-(instancetype)initWithQueue:(nullable dispatch_queue_t)queue
requestDisconnectionHandler:(GNCRequestDisconnectionHandler)requestDisconnectionHandler;
- (instancetype)initWithQueue:(nullable dispatch_queue_t)queue
requestDisconnectionHandler:(GNCRequestDisconnectionHandler)requestDisconnectionHandler;
/**
* Closes the L2CAP channel and disconnects the peripheral.
*
* This is only exposed for testing.
*/
- (void)closeL2CAPChannel;
@end
@@ -20,8 +20,11 @@
#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/GNCBLEL2CAPFakeInputOutputStream.h"
#import "internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakePeripheral.h"
static const NSTimeInterval kTestTimeout = 1.0;
@interface GNCBLEL2CAPClientTest : XCTestCase
@property(nonatomic) GNCFakePeripheral *fakePeripheral;
@property(nonatomic) GNCBLEL2CAPClient *l2capClient;
@@ -58,10 +61,11 @@
peripheral:self.fakePeripheral
completionHandler:^(GNCBLEL2CAPStream *stream, NSError *error) {
XCTAssertNil(error, @"Error should be nil");
XCTAssertNil(stream, @"Stream should be nil due to testing limitation");
[expectation fulfill];
}];
[self waitForExpectations:@[ expectation ] timeout:1.0];
[self waitForExpectations:@[ expectation ] timeout:kTestTimeout];
}
- (void)testOpenL2CAPChannelWithError {
@@ -79,7 +83,7 @@
[expectation fulfill];
}];
[self waitForExpectations:@[ expectation ] timeout:1.0];
[self waitForExpectations:@[ expectation ] timeout:kTestTimeout];
}
- (void)testDisconnect {
@@ -94,7 +98,96 @@
[self.l2capClient disconnect];
[self waitForExpectations:@[ self.requestDisconnectionHandlerExpectation ] timeout:1.0];
[self waitForExpectations:@[ self.requestDisconnectionHandlerExpectation ] timeout:kTestTimeout];
}
- (void)testOpenL2CAPChannelSetsPeripheralDelegate {
uint16_t psm = 123;
[self.l2capClient openL2CAPChannelWithPSM:psm
peripheral:self.fakePeripheral
completionHandler:^(GNCBLEL2CAPStream *stream, NSError *error){
}];
XCTAssertEqualObjects(self.fakePeripheral.peripheralDelegate, self.l2capClient);
}
- (void)testInitWithRequestDisconnectionHandlerAndDisconnect {
XCTestExpectation *expectation =
[self expectationWithDescription:@"Disconnection handler called"];
GNCBLEL2CAPClient *client = [[GNCBLEL2CAPClient alloc]
initWithRequestDisconnectionHandler:^(id<GNCPeripheral> _Nullable peripheral) {
XCTAssertNil(peripheral);
[expectation fulfill];
}];
[client disconnect];
[self waitForExpectations:@[ expectation ] timeout:kTestTimeout];
}
- (void)testCloseL2CAPChannel {
self.requestDisconnectionHandlerExpectation =
[self expectationWithDescription:@"Request disconnection handler should be called"];
uint16_t psm = 123;
[self.l2capClient openL2CAPChannelWithPSM:psm
peripheral:self.fakePeripheral
completionHandler:^(GNCBLEL2CAPStream *stream, NSError *error){
}];
[self.l2capClient closeL2CAPChannel];
[self waitForExpectations:@[ self.requestDisconnectionHandlerExpectation ] timeout:kTestTimeout];
}
- (void)testDidOpenL2CAPChannelWithStreams {
XCTestExpectation *expectation =
[self expectationWithDescription:@"L2CAP channel opened successfully"];
uint16_t psm = 123;
GNCBLEL2CAPFakeInputOutputStream *fakeStreams =
[[GNCBLEL2CAPFakeInputOutputStream alloc] initWithBufferSize:1];
self.fakePeripheral.channelInputStream = fakeStreams.inputStream;
self.fakePeripheral.channelOutputStream = fakeStreams.outputStream;
[self.l2capClient openL2CAPChannelWithPSM:psm
peripheral:self.fakePeripheral
completionHandler:^(GNCBLEL2CAPStream *stream, NSError *error) {
XCTAssertNil(error);
XCTAssertNotNil(stream);
[expectation fulfill];
}];
[self waitForExpectations:@[ expectation ] timeout:kTestTimeout];
}
- (void)testDidOpenL2CAPChannelCalledTwice {
XCTestExpectation *expectation = [self expectationWithDescription:@"L2CAP channel opened twice"];
expectation.expectedFulfillmentCount = 2;
self.requestDisconnectionHandlerExpectation =
[self expectationWithDescription:@"Request disconnection handler should be called"];
uint16_t psm = 123;
GNCBLEL2CAPFakeInputOutputStream *fakeStreams =
[[GNCBLEL2CAPFakeInputOutputStream alloc] initWithBufferSize:1];
self.fakePeripheral.channelInputStream = fakeStreams.inputStream;
self.fakePeripheral.channelOutputStream = fakeStreams.outputStream;
[self.l2capClient openL2CAPChannelWithPSM:psm
peripheral:self.fakePeripheral
completionHandler:^(GNCBLEL2CAPStream *stream, NSError *error) {
XCTAssertNil(error);
XCTAssertNotNil(stream);
[expectation fulfill];
}];
// Calling gnc_peripheral again to simulate channel opened again.
CBL2CAPChannel *channel = [[CBL2CAPChannel alloc] init];
[channel setValue:fakeStreams.inputStream forKey:@"inputStream"];
[channel setValue:fakeStreams.outputStream forKey:@"outputStream"];
[self.l2capClient peripheral:(CBPeripheral *)self.fakePeripheral
didOpenL2CAPChannel:channel
error:nil];
[self waitForExpectations:@[ expectation, self.requestDisconnectionHandlerExpectation ]
timeout:kTestTimeout];
}
@end
@@ -18,10 +18,13 @@
#import <XCTest/XCTest.h>
#import "internal/platform/implementation/apple/Mediums/BLE/GNCBLEL2CAPStream.h"
#import "internal/platform/implementation/apple/Mediums/BLE/GNCMBleUtils.h"
#import "internal/platform/implementation/apple/Mediums/BLE/GNCMConnection.h"
#import "internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEL2CAPFakeInputOutputStream.h"
// TODO: b/399815436 - More tests for GNCBLEL2CAPConnection.
@interface GNCBLEL2CAPConnection (Testing)
static const NSTimeInterval kTestTimeout = 1.0;
@interface GNCBLEL2CAPConnection (Testing) <GNCBLEL2CAPStreamDelegate>
- (NSData *_Nullable)extractRealDataFromData:(NSData *)data;
@property(nonatomic) NSUInteger expectedDataLength;
@property(nonatomic) dispatch_queue_t selfQueue;
@@ -33,13 +36,14 @@
@property(nonatomic) GNCBLEL2CAPConnection *connection;
@property(nonatomic) dispatch_queue_t testCallbackQueue;
@property(nonatomic) NSString *serviceID;
@property(nonatomic) NSData *serviceIDHash;
@end
@implementation GNCBLEL2CAPConnectionTest
- (void)setUp {
[super setUp];
_fakeInputOutputStream = [[GNCBLEL2CAPFakeInputOutputStream alloc] initWithBufferSize:100];
_fakeInputOutputStream = [[GNCBLEL2CAPFakeInputOutputStream alloc] initWithBufferSize:1024];
_stream = [[GNCBLEL2CAPStream alloc]
initWithClosedBlock:^{
}
@@ -47,6 +51,7 @@
outputStream:_fakeInputOutputStream.outputStream];
_testCallbackQueue = dispatch_queue_create("com.google.test.callback", DISPATCH_QUEUE_SERIAL);
_serviceID = @"testServiceID";
_serviceIDHash = GNCMServiceIDHash(_serviceID);
}
- (void)tearDown {
@@ -71,10 +76,11 @@
return connection;
}
- (NSData *)createDataWithLength:(NSUInteger)length prefix:(NSData *)prefix {
NSMutableData *data = [NSMutableData dataWithLength:length];
if (prefix) {
[data appendData:prefix];
- (NSData *)createDataWithLength:(NSUInteger)length {
NSMutableData *data = [NSMutableData data];
for (NSUInteger i = 0; i < length; ++i) {
uint8_t byte = i % 256;
[data appendBytes:&byte length:1];
}
return data;
}
@@ -90,45 +96,200 @@
return packet;
}
- (NSData *)prefixDataWithServiceIDHash:(NSData *)serviceIDHash data:(NSData *)data {
NSMutableData *combinedData = [NSMutableData dataWithCapacity:serviceIDHash.length + data.length];
[combinedData appendData:serviceIDHash];
[combinedData appendData:data];
return combinedData;
}
- (void)waitForCallbackQueue {
XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for callback queue"];
dispatch_async(_testCallbackQueue, ^{
[expectation fulfill];
});
[self waitForExpectations:@[ expectation ] timeout:1];
[self waitForExpectations:@[ expectation ] timeout:kTestTimeout];
}
- (void)waitForSelfQueueWithConnection:(GNCBLEL2CAPConnection *)connection {
XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for self queue"];
dispatch_async(connection.selfQueue, ^{
[expectation fulfill];
});
[self waitForExpectations:@[ expectation ] timeout:kTestTimeout];
}
#pragma mark - Tests
- (void)testExtractRealDataFromData_validData {
_connection = [self createConnectionWithIncoming:YES];
NSData *testData = [self createDataWithLength:10 prefix:nil];
NSData *prefixData = [self prefixLengthData:[self createDataWithLength:testData.length
prefix:nil]];
_connection.expectedDataLength = testData.length;
NSData *realData = [_connection extractRealDataFromData:prefixData];
NSData *testData = [self createDataWithLength:10];
NSData *prefixData = [self prefixLengthData:testData];
NSMutableData *streamData = [NSMutableData dataWithData:prefixData];
[streamData appendData:[self createDataWithLength:5]];
_connection.expectedDataLength = 0;
NSData *realData = [_connection extractRealDataFromData:streamData];
XCTAssertEqualObjects(realData, testData);
XCTAssertEqual(_connection.expectedDataLength, testData.length);
}
- (void)testExtractRealDataFromData_notEnoughData {
_connection = [self createConnectionWithIncoming:YES];
NSData *prefixData = [self createDataWithLength:2 prefix:nil];
NSData *prefixData = [self createDataWithLength:2];
NSData *realData = [_connection extractRealDataFromData:prefixData];
XCTAssertNil(realData);
}
- (void)testExtractRealDataFromData_moreThanExpectedData {
_connection = [self createConnectionWithIncoming:YES];
NSData *testData = [self createDataWithLength:10 prefix:nil];
NSData *moreData = [self createDataWithLength:5 prefix:nil];
NSData *testData = [self createDataWithLength:10];
NSData *moreData = [self createDataWithLength:5];
NSMutableData *combinedData = [NSMutableData dataWithData:testData];
[combinedData appendData:moreData];
NSData *prefixData = [self prefixLengthData:[self createDataWithLength:combinedData.length
prefix:nil]];
NSData *prefixData = [self prefixLengthData:combinedData];
_connection.expectedDataLength = testData.length;
NSData *receivedData = [NSMutableData dataWithData:prefixData];
NSData *realData = [_connection extractRealDataFromData:receivedData];
XCTAssertEqualObjects(realData, testData);
}
- (void)testSendData {
_connection = [self createConnectionWithIncoming:YES];
NSData *payload = [@"test data" dataUsingEncoding:NSUTF8StringEncoding];
XCTestExpectation *expectation = [self expectationWithDescription:@"send data completion"];
[_connection sendData:payload
completion:^(BOOL result) {
XCTAssertTrue(result);
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:kTestTimeout handler:nil];
NSData *expectedPacket = [self prefixLengthData:[self prefixDataWithServiceIDHash:_serviceIDHash
data:payload]];
NSData *writtenData = [_fakeInputOutputStream dataSentToWatchWithMaxBytes:expectedPacket.length];
XCTAssertEqualObjects(writtenData, expectedPacket);
}
- (void)testRequestDataConnectionSuccess {
_connection = [self createConnectionWithIncoming:NO];
XCTestExpectation *expectation =
[self expectationWithDescription:@"request data connection completion"];
__block BOOL success = NO;
[_connection requestDataConnectionWithCompletion:^(BOOL result) {
success = result;
[expectation fulfill];
}];
// Verify RequestDataConnection packet is sent.
NSData *requestPacket = [self
prefixLengthData:GNCMGenerateBLEL2CAPPacket(GNCMBLEL2CAPCommandRequestDataConnection, nil)];
NSData *writtenReqData =
[_fakeInputOutputStream dataSentToWatchWithMaxBytes:requestPacket.length];
XCTAssertEqualObjects(writtenReqData, requestPacket);
// Simulate receiving ResponseDataConnectionReady packet.
NSData *responsePacket =
[self prefixLengthData:GNCMGenerateBLEL2CAPPacket(
GNCMBLEL2CAPCommandResponseDataConnectionReady, nil)];
[_fakeInputOutputStream writeFromDevice:responsePacket];
[self waitForExpectationsWithTimeout:kTestTimeout handler:nil];
XCTAssertTrue(success);
// Verify Introduction packet is sent.
NSData *introPacket =
[self prefixLengthData:GNCMGenerateBLEFramesIntroductionPacket(_serviceIDHash)];
NSData *writtenIntroData =
[_fakeInputOutputStream dataSentToWatchWithMaxBytes:introPacket.length];
XCTAssertEqualObjects(writtenIntroData, introPacket);
}
- (void)testRequestDataConnectionTimeout {
_connection = [self createConnectionWithIncoming:NO];
XCTestExpectation *expectation =
[self expectationWithDescription:@"request data connection completion"];
__block BOOL success = YES;
[_connection requestDataConnectionWithCompletion:^(BOOL result) {
success = result;
[expectation fulfill];
}];
// Verify RequestDataConnection packet is sent.
NSData *requestPacket = [self
prefixLengthData:GNCMGenerateBLEL2CAPPacket(GNCMBLEL2CAPCommandRequestDataConnection, nil)];
NSData *writtenReqData =
[_fakeInputOutputStream dataSentToWatchWithMaxBytes:requestPacket.length];
XCTAssertEqualObjects(writtenReqData, requestPacket);
// Don't send ResponseDataConnectionReady packet to simulate timeout.
[self waitForExpectationsWithTimeout:kTestTimeout * 10 handler:nil];
XCTAssertFalse(success);
}
- (void)testIncomingConnectionReceiveRequestDataConnection {
_connection = [self createConnectionWithIncoming:YES];
// Simulate receiving RequestDataConnection packet.
NSData *requestPacket = [self
prefixLengthData:GNCMGenerateBLEL2CAPPacket(GNCMBLEL2CAPCommandRequestDataConnection, nil)];
[_fakeInputOutputStream writeFromDevice:requestPacket];
// Verify ResponseDataConnectionReady packet is sent.
NSData *responsePacket =
[self prefixLengthData:GNCMGenerateBLEL2CAPPacket(
GNCMBLEL2CAPCommandResponseDataConnectionReady, nil)];
NSData *writtenData = [_fakeInputOutputStream dataSentToWatchWithMaxBytes:responsePacket.length];
XCTAssertEqualObjects(writtenData, responsePacket);
}
- (void)testReceivePayload {
_connection = [self createConnectionWithIncoming:YES];
NSData *payload = [@"test data" dataUsingEncoding:NSUTF8StringEncoding];
XCTestExpectation *payloadExpectation = [self expectationWithDescription:@"payload handler"];
_connection.connectionHandlers = [GNCMConnectionHandlers
payloadHandler:^(NSData *data) {
XCTAssertEqualObjects(data, payload);
[payloadExpectation fulfill];
}
disconnectedHandler:^{
}];
// For incoming connection, need to receive RequestDataConnection and Introduction packet first.
NSData *requestConnectionPacket = [self
prefixLengthData:GNCMGenerateBLEL2CAPPacket(GNCMBLEL2CAPCommandRequestDataConnection, nil)];
[_fakeInputOutputStream writeFromDevice:requestConnectionPacket];
[self waitForSelfQueueWithConnection:_connection];
NSData *introPacket =
[self prefixLengthData:GNCMGenerateBLEFramesIntroductionPacket(_serviceIDHash)];
[_fakeInputOutputStream writeFromDevice:introPacket];
[self waitForSelfQueueWithConnection:_connection];
// Send payload packet.
NSData *payloadPacket = [self prefixLengthData:[self prefixDataWithServiceIDHash:_serviceIDHash
data:payload]];
[_fakeInputOutputStream writeFromDevice:payloadPacket];
[self waitForExpectationsWithTimeout:kTestTimeout handler:nil];
}
- (void)testDidDisconnectWithError {
_connection = [self createConnectionWithIncoming:YES];
XCTestExpectation *disconnectionExpectation =
[self expectationWithDescription:@"disconnection handler"];
_connection.connectionHandlers = [GNCMConnectionHandlers
payloadHandler:^(NSData *data) {
}
disconnectedHandler:^{
[disconnectionExpectation fulfill];
}];
[_connection stream:_stream didDisconnectWithError:nil];
[self waitForExpectationsWithTimeout:kTestTimeout handler:nil];
}
@end
@@ -16,11 +16,13 @@
#import <Foundation/Foundation.h>
#import "internal/platform/implementation/apple/Mediums/BLE/GNCPeripheralManager.h"
@protocol GNCPeripheralManager;
NS_ASSUME_NONNULL_BEGIN
@interface GNCBLEL2CAPServer (Testing)
@interface GNCBLEL2CAPServer (Testing) <GNCPeripheralManagerDelegate>
/**
* Creates a L2CAP server with a provided peripheral manager.
@@ -31,6 +33,13 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (instancetype)initWithPeripheralManager:(id<GNCPeripheralManager>)peripheralManager;
/**
* Closes the L2CAP channel.
*
* This is only exposed for testing.
*/
- (void)closeL2CAPChannel;
@end
NS_ASSUME_NONNULL_END
@@ -18,8 +18,11 @@
#import <Foundation/Foundation.h>
#import <XCTest/XCTest.h>
#import "internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEL2CAPServer+Testing.h"
#import "internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakePeripheralManager.h"
static const NSTimeInterval kTestTimeout = 1.0;
@interface GNCBLEL2CAPServerTest : XCTestCase
@end
@@ -27,11 +30,19 @@
#pragma mark Tests
- (void)testInit {
GNCBLEL2CAPServer *l2capServer = [[GNCBLEL2CAPServer alloc] init];
XCTAssertNotNil(l2capServer);
XCTAssertNil([l2capServer valueForKey:@"_peripheralManager"]);
}
- (void)testPublishL2CAPChannelAndOpenChannelWhenStartListeningChannel {
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
GNCBLEL2CAPServer *l2capServer =
[[GNCBLEL2CAPServer alloc] initWithPeripheralManager:fakePeripheralManager
queue:dispatch_get_main_queue()];
XCTestExpectation *psmPublishedExpectation =
[[XCTestExpectation alloc] initWithDescription:@"PSM published."];
XCTestExpectation *channelOpenedexpectation =
[[XCTestExpectation alloc] initWithDescription:@"Channel opened."];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
@@ -41,12 +52,18 @@
NSError *_Nullable error) {
XCTAssertEqual(error, nil);
XCTAssertEqual(PSM, fakePeripheralManager.PSM);
[psmPublishedExpectation fulfill];
}
channelOpenedCompletionHandler:^(GNCBLEL2CAPStream *_Nullable stream,
NSError *_Nullable error) {
XCTAssertNil(error);
XCTAssertNotNil(stream);
[channelOpenedexpectation fulfill];
}];
[self waitForExpectations:@[ channelOpenedexpectation ] timeout:0.5];
[self waitForExpectations:@[ psmPublishedExpectation, channelOpenedexpectation ]
timeout:kTestTimeout];
XCTAssertNotNil([l2capServer valueForKey:@"l2CAPChannel"]);
XCTAssertNotNil([l2capServer valueForKey:@"l2CAPStream"]);
}
- (void)testFailedToPublishL2CAPChannel {
@@ -57,6 +74,8 @@
GNCBLEL2CAPServer *l2capServer =
[[GNCBLEL2CAPServer alloc] initWithPeripheralManager:fakePeripheralManager
queue:dispatch_get_main_queue()];
XCTestExpectation *psmPublishedExpectation =
[[XCTestExpectation alloc] initWithDescription:@"PSM published with error."];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
[l2capServer
@@ -64,10 +83,12 @@
NSError *_Nullable error) {
XCTAssertEqual(error, fakePeripheralManager.didPublishL2CAPChannelError);
XCTAssertEqual(PSM, 0);
[psmPublishedExpectation fulfill];
}
channelOpenedCompletionHandler:^(GNCBLEL2CAPStream *_Nullable stream,
NSError *_Nullable error){
}];
[self waitForExpectations:@[ psmPublishedExpectation ] timeout:kTestTimeout];
}
- (void)testPoweredOffUnpublishesChannel {
@@ -89,14 +110,17 @@
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOff];
[self waitForExpectations:@[ fakePeripheralManager.unpublishExpectation ] timeout:0.0];
[self waitForExpectations:@[ fakePeripheralManager.unpublishExpectation ] timeout:kTestTimeout];
}
- (void)testStartPeripheralManagerInitiallyOff {
- (void)testPeripheralManagerDidUpdateStatePoweredOffUnpublishesChannel {
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
GNCBLEL2CAPServer *l2capServer =
[[GNCBLEL2CAPServer alloc] initWithPeripheralManager:fakePeripheralManager
queue:dispatch_get_main_queue()];
fakePeripheralManager.state = CBManagerStatePoweredOn;
[(id<CBPeripheralManagerDelegate>)l2capServer
peripheralManagerDidUpdateState:(CBPeripheralManager *)fakePeripheralManager];
[l2capServer
startListeningChannelWithPSMPublishedCompletionHandler:^(uint16_t PSM,
@@ -108,7 +132,38 @@
NSError *_Nullable error){
}];
fakePeripheralManager.state = CBManagerStatePoweredOff;
[(id<CBPeripheralManagerDelegate>)l2capServer
peripheralManagerDidUpdateState:(CBPeripheralManager *)fakePeripheralManager];
[self waitForExpectations:@[ fakePeripheralManager.unpublishExpectation ] timeout:kTestTimeout];
}
- (void)testStartPeripheralManagerInitiallyOff {
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
GNCBLEL2CAPServer *l2capServer =
[[GNCBLEL2CAPServer alloc] initWithPeripheralManager:fakePeripheralManager
queue:dispatch_get_main_queue()];
XCTestExpectation *psmPublishedExpectation =
[[XCTestExpectation alloc] initWithDescription:@"PSM published."];
XCTestExpectation *channelOpenedexpectation =
[[XCTestExpectation alloc] initWithDescription:@"Channel opened."];
[l2capServer
startListeningChannelWithPSMPublishedCompletionHandler:^(uint16_t PSM,
NSError *_Nullable error) {
XCTAssertEqual(error, nil);
XCTAssertEqual(PSM, fakePeripheralManager.PSM);
[psmPublishedExpectation fulfill];
}
channelOpenedCompletionHandler:^(GNCBLEL2CAPStream *_Nullable stream,
NSError *_Nullable error) {
[channelOpenedexpectation fulfill];
}];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
[self waitForExpectations:@[ psmPublishedExpectation, channelOpenedexpectation ]
timeout:kTestTimeout];
}
- (void)testClose {
@@ -142,7 +197,133 @@
[l2capServer close];
[self waitForExpectations:@[ fakePeripheralManager.unpublishExpectation ] timeout:1.0];
[self waitForExpectations:@[ fakePeripheralManager.unpublishExpectation ] timeout:kTestTimeout];
}
- (void)testFailedToOpenL2CAPChannel {
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
fakePeripheralManager.didOpenL2CAPChannelError = [NSError errorWithDomain:@"fake"
code:0
userInfo:nil];
GNCBLEL2CAPServer *l2capServer =
[[GNCBLEL2CAPServer alloc] initWithPeripheralManager:fakePeripheralManager
queue:dispatch_get_main_queue()];
XCTestExpectation *channelOpenedexpectation =
[[XCTestExpectation alloc] initWithDescription:@"Channel opened."];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
[l2capServer
startListeningChannelWithPSMPublishedCompletionHandler:^(uint16_t PSM,
NSError *_Nullable error) {
XCTAssertEqual(error, nil);
XCTAssertEqual(PSM, fakePeripheralManager.PSM);
}
channelOpenedCompletionHandler:^(GNCBLEL2CAPStream *_Nullable stream,
NSError *_Nullable error) {
XCTAssertNil(stream);
XCTAssertEqual(error, fakePeripheralManager.didOpenL2CAPChannelError);
[channelOpenedexpectation fulfill];
}];
[self waitForExpectations:@[ channelOpenedexpectation ] timeout:kTestTimeout];
XCTAssertNil([l2capServer valueForKey:@"l2CAPChannel"]);
XCTAssertNil([l2capServer valueForKey:@"l2CAPStream"]);
}
- (void)testPeripheralManagerDidUnpublishL2CAPChannel {
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
GNCBLEL2CAPServer *l2capServer =
[[GNCBLEL2CAPServer alloc] initWithPeripheralManager:fakePeripheralManager
queue:dispatch_get_main_queue()];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
[l2capServer
startListeningChannelWithPSMPublishedCompletionHandler:^(uint16_t PSM,
NSError *_Nullable error) {
}
channelOpenedCompletionHandler:^(GNCBLEL2CAPStream *_Nullable stream,
NSError *_Nullable error){
}];
[(id<CBPeripheralManagerDelegate>)l2capServer
peripheralManager:(CBPeripheralManager *)fakePeripheralManager
didUnpublishL2CAPChannel:l2capServer.PSM
error:[NSError errorWithDomain:@"fake" code:0 userInfo:nil]];
XCTAssertEqual(l2capServer.PSM, 0);
}
- (void)testCloseL2CAPChannel {
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
GNCBLEL2CAPServer *l2capServer =
[[GNCBLEL2CAPServer alloc] initWithPeripheralManager:fakePeripheralManager
queue:dispatch_get_main_queue()];
XCTestExpectation *channelOpenedexpectation =
[[XCTestExpectation alloc] initWithDescription:@"Channel opened."];
[fakePeripheralManager simulatePeripheralManagerDidUpdateState:CBManagerStatePoweredOn];
[l2capServer
startListeningChannelWithPSMPublishedCompletionHandler:^(uint16_t PSM,
NSError *_Nullable error) {
}
channelOpenedCompletionHandler:^(GNCBLEL2CAPStream *_Nullable stream,
NSError *_Nullable error) {
[channelOpenedexpectation fulfill];
}];
[self waitForExpectations:@[ channelOpenedexpectation ] timeout:kTestTimeout];
[l2capServer closeL2CAPChannel];
XCTAssertNil([l2capServer valueForKey:@"l2CAPChannel"]);
XCTAssertNil([l2capServer valueForKey:@"l2CAPStream"]);
}
- (void)testPeripheralManagerDidPublishL2CAPChannel {
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
GNCBLEL2CAPServer *l2capServer =
[[GNCBLEL2CAPServer alloc] initWithPeripheralManager:fakePeripheralManager
queue:dispatch_get_main_queue()];
XCTestExpectation *expectation =
[[XCTestExpectation alloc] initWithDescription:@"completion called"];
[l2capServer
startListeningChannelWithPSMPublishedCompletionHandler:^(uint16_t PSM,
NSError *_Nullable error) {
XCTAssertEqual(PSM, 1);
XCTAssertNil(error);
[expectation fulfill];
}
channelOpenedCompletionHandler:^(GNCBLEL2CAPStream *_Nullable stream,
NSError *_Nullable error){
}];
[(id<CBPeripheralManagerDelegate>)l2capServer
peripheralManager:(CBPeripheralManager *)fakePeripheralManager
didPublishL2CAPChannel:1
error:nil];
[self waitForExpectations:@[ expectation ] timeout:kTestTimeout];
XCTAssertEqual(l2capServer.PSM, 1);
}
- (void)testPeripheralManagerDidPublishL2CAPChannelWithError {
GNCFakePeripheralManager *fakePeripheralManager = [[GNCFakePeripheralManager alloc] init];
GNCBLEL2CAPServer *l2capServer =
[[GNCBLEL2CAPServer alloc] initWithPeripheralManager:fakePeripheralManager
queue:dispatch_get_main_queue()];
XCTestExpectation *expectation =
[[XCTestExpectation alloc] initWithDescription:@"completion called"];
NSError *publishError = [NSError errorWithDomain:@"test" code:0 userInfo:nil];
[l2capServer
startListeningChannelWithPSMPublishedCompletionHandler:^(uint16_t PSM,
NSError *_Nullable error) {
XCTAssertEqual(PSM, 0);
XCTAssertEqualObjects(error, publishError);
[expectation fulfill];
}
channelOpenedCompletionHandler:^(GNCBLEL2CAPStream *_Nullable stream,
NSError *_Nullable error){
}];
[(id<CBPeripheralManagerDelegate>)l2capServer
peripheralManager:(CBPeripheralManager *)fakePeripheralManager
didPublishL2CAPChannel:0
error:publishError];
[self waitForExpectations:@[ expectation ] timeout:kTestTimeout];
XCTAssertEqual(l2capServer.PSM, 0);
}
@end
@@ -0,0 +1,36 @@
// Copyright 2025 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 <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/**
* A fake CBL2CAPChannel for testing.
*/
@interface GNCFakeCBL2CAPChannel : NSObject
/** The input stream for the L2CAP channel. */
@property(nonatomic, nullable) NSInputStream *inputStream;
/** The output stream for the L2CAP channel. */
@property(nonatomic, nullable) NSOutputStream *outputStream;
/** 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;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,18 @@
// Copyright 2025 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/Tests/GNCFakeCBL2CAPChannel.h"
@implementation GNCFakeCBL2CAPChannel
@end
@@ -67,6 +67,11 @@ NS_ASSUME_NONNULL_BEGIN
*/
@property(nonatomic, nullable, readwrite) NSError *openL2CAPChannelError;
/** Fake input stream to inject into CBL2CAPChannel. */
@property(nonatomic, nullable) NSInputStream *channelInputStream;
/** Fake output stream to inject into CBL2CAPChannel. */
@property(nonatomic, nullable) NSOutputStream *channelOutputStream;
@end
NS_ASSUME_NONNULL_END
@@ -117,12 +117,20 @@ NS_ASSUME_NONNULL_BEGIN
- (void)openL2CAPChannel:(CBL2CAPPSM)PSM {
[self delayDelegateUsingBlock:^() {
CBL2CAPChannel *channel = [[CBL2CAPChannel alloc] init];
if (self.channelInputStream) {
[channel setValue:self.channelInputStream forKey:@"inputStream"];
}
if (self.channelOutputStream) {
[channel setValue:self.channelOutputStream forKey:@"outputStream"];
}
if (_openL2CAPChannelError) {
[self.peripheralDelegate gnc_peripheral:self
didOpenL2CAPChannel:channel
error:_openL2CAPChannelError];
[self.peripheralDelegate peripheral:(CBPeripheral *)self
didOpenL2CAPChannel:channel
error:_openL2CAPChannelError];
} else {
[self.peripheralDelegate gnc_peripheral:self didOpenL2CAPChannel:channel error:nil];
[self.peripheralDelegate peripheral:(CBPeripheral *)self
didOpenL2CAPChannel:channel
error:nil];
}
}];
}
@@ -38,6 +38,9 @@ NS_ASSUME_NONNULL_BEGIN
/** The PSM of the L2CAP channel. */
@property(nonatomic) CBL2CAPPSM PSM;
/** The fake peripheral manager state. */
@property(nonatomic, readwrite) CBManagerState state;
/** Expectation fulfilled when peripheral responds to a request with success. */
@property(nonatomic, readonly) XCTestExpectation *respondToRequestSuccessExpectation;
@@ -83,6 +86,15 @@ NS_ASSUME_NONNULL_BEGIN
*/
@property(nonatomic, nullable, readwrite) NSError *didUnPublishL2CAPChannelError;
/**
* Similates a @c openL2CAPChannel: error.
*
* Setting this error to a value other than @c nil will simulate a failure when calling @c
* openL2CAPChannel: and will call the @c gnc_peripheralManager:didOpenL2CAPChannel:error:
* delegate method with the provided error.
*/
@property(nonatomic, nullable, readwrite) NSError *didOpenL2CAPChannelError;
/**
* Simulates a state update event.
*
@@ -93,6 +105,15 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (void)simulatePeripheralManagerDidUpdateState:(CBManagerState)fakeState;
/**
* Simulates a read request event.
*
* Creates a fake read request for the given service and characteristic UUIDs and calls the
* @c gnc_peripheralManager:didReceiveReadRequest: delegate method.
*
* @param service The service UUID of the characteristic to read from.
* @param characteristic The characteristic UUID to read from.
*/
- (void)simulatePeripheralManagerDidReceiveReadRequestForService:(CBUUID *)service
characteristic:(CBUUID *)characteristic;
@@ -19,6 +19,8 @@
#import <XCTest/XCTest.h>
#import "internal/platform/implementation/apple/Mediums/BLE/GNCPeripheralManager.h"
#import "internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEL2CAPFakeInputOutputStream.h"
#import "internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeCBL2CAPChannel.h"
@interface CBCharacteristic ()
@@ -59,7 +61,6 @@
static const uint16_t kPSM = 192;
@implementation GNCFakePeripheralManager {
CBManagerState _state;
BOOL _isAdvertising;
NSDictionary<NSString *, id> *_advertisementData;
NSMutableArray<CBService *> *_services;
@@ -87,10 +88,6 @@ static const uint16_t kPSM = 192;
return self;
}
- (CBManagerState)state {
return _state;
}
- (BOOL)isAdvertising {
return _isAdvertising;
}
@@ -99,9 +96,7 @@ static const uint16_t kPSM = 192;
if (!_didAddServiceError) {
[_services addObject:service];
}
[_peripheralDelegate gnc_peripheralManager:self
didAddService:service
error:_didAddServiceError];
[_peripheralDelegate gnc_peripheralManager:self didAddService:service error:_didAddServiceError];
}
- (void)removeService:(CBMutableService *)service {
@@ -140,9 +135,14 @@ static const uint16_t kPSM = 192;
didPublishL2CAPChannel:localPSM
error:_didPublishL2CAPChannelError];
if (!_didPublishL2CAPChannelError) {
GNCBLEL2CAPFakeInputOutputStream *fakeStream =
[[GNCBLEL2CAPFakeInputOutputStream alloc] initWithBufferSize:1024];
GNCFakeCBL2CAPChannel *fakeChannel = [[GNCFakeCBL2CAPChannel alloc] init];
fakeChannel.inputStream = fakeStream.inputStream;
fakeChannel.outputStream = fakeStream.outputStream;
[_peripheralDelegate gnc_peripheralManager:self
didOpenL2CAPChannel:[[CBL2CAPChannel alloc] init]
error:nil];
didOpenL2CAPChannel:(CBL2CAPChannel *)fakeChannel
error:_didOpenL2CAPChannelError];
}
}