diff --git a/internal/platform/implementation/apple/Mediums/BLE/Tests/BUILD b/internal/platform/implementation/apple/Mediums/BLE/Tests/BUILD index d7b0e86e..86e1be56 100644 --- a/internal/platform/implementation/apple/Mediums/BLE/Tests/BUILD +++ b/internal/platform/implementation/apple/Mediums/BLE/Tests/BUILD @@ -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", diff --git a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEL2CAPClient+Testing.h b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEL2CAPClient+Testing.h index 4024d2ab..1e1f6784 100644 --- a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEL2CAPClient+Testing.h +++ b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEL2CAPClient+Testing.h @@ -16,11 +16,13 @@ #import +#import "internal/platform/implementation/apple/Mediums/BLE/GNCPeripheral.h" + @protocol GNCPeripheral; NS_ASSUME_NONNULL_BEGIN -@interface GNCBLEL2CAPClient (Testing) +@interface GNCBLEL2CAPClient (Testing) /** * 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 diff --git a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEL2CAPClientTest.m b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEL2CAPClientTest.m index 812265e5..631b76c1 100644 --- a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEL2CAPClientTest.m +++ b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEL2CAPClientTest.m @@ -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 _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 diff --git a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEL2CAPConnectionTest.m b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEL2CAPConnectionTest.m index b0b24e6d..37e5793d 100644 --- a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEL2CAPConnectionTest.m +++ b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEL2CAPConnectionTest.m @@ -18,10 +18,13 @@ #import #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) - (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 diff --git a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEL2CAPServer+Testing.h b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEL2CAPServer+Testing.h index 47ab8951..af590098 100644 --- a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEL2CAPServer+Testing.h +++ b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEL2CAPServer+Testing.h @@ -16,11 +16,13 @@ #import +#import "internal/platform/implementation/apple/Mediums/BLE/GNCPeripheralManager.h" + @protocol GNCPeripheralManager; NS_ASSUME_NONNULL_BEGIN -@interface GNCBLEL2CAPServer (Testing) +@interface GNCBLEL2CAPServer (Testing) /** * Creates a L2CAP server with a provided peripheral manager. @@ -31,6 +33,13 @@ NS_ASSUME_NONNULL_BEGIN */ - (instancetype)initWithPeripheralManager:(id)peripheralManager; +/** + * Closes the L2CAP channel. + * + * This is only exposed for testing. + */ +- (void)closeL2CAPChannel; + @end NS_ASSUME_NONNULL_END diff --git a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEL2CAPServerTest.m b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEL2CAPServerTest.m index d24f0e58..2380d287 100644 --- a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEL2CAPServerTest.m +++ b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEL2CAPServerTest.m @@ -18,8 +18,11 @@ #import #import +#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)l2capServer + peripheralManagerDidUpdateState:(CBPeripheralManager *)fakePeripheralManager]; [l2capServer startListeningChannelWithPSMPublishedCompletionHandler:^(uint16_t PSM, @@ -108,7 +132,38 @@ NSError *_Nullable error){ }]; + fakePeripheralManager.state = CBManagerStatePoweredOff; + [(id)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)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)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)l2capServer + peripheralManager:(CBPeripheralManager *)fakePeripheralManager + didPublishL2CAPChannel:0 + error:publishError]; + [self waitForExpectations:@[ expectation ] timeout:kTestTimeout]; + XCTAssertEqual(l2capServer.PSM, 0); } @end diff --git a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeCBL2CAPChannel.h b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeCBL2CAPChannel.h new file mode 100644 index 00000000..69a1b791 --- /dev/null +++ b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeCBL2CAPChannel.h @@ -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 +#import + +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 diff --git a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeCBL2CAPChannel.m b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeCBL2CAPChannel.m new file mode 100644 index 00000000..fe097a7a --- /dev/null +++ b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeCBL2CAPChannel.m @@ -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 diff --git a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakePeripheral.h b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakePeripheral.h index 3dc4e45a..866968e8 100644 --- a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakePeripheral.h +++ b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakePeripheral.h @@ -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 diff --git a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakePeripheral.m b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakePeripheral.m index f6f5839c..971977ab 100644 --- a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakePeripheral.m +++ b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakePeripheral.m @@ -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]; } }]; } diff --git a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakePeripheralManager.h b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakePeripheralManager.h index 68078e0f..9337a2fc 100644 --- a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakePeripheralManager.h +++ b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakePeripheralManager.h @@ -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; diff --git a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakePeripheralManager.m b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakePeripheralManager.m index d63505a3..912974df 100644 --- a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakePeripheralManager.m +++ b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakePeripheralManager.m @@ -19,6 +19,8 @@ #import #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 *_advertisementData; NSMutableArray *_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]; } }