diff --git a/internal/platform/implementation/apple/Mediums/BLE/Tests/BUILD b/internal/platform/implementation/apple/Mediums/BLE/Tests/BUILD index 97a19a88..96cbf582 100644 --- a/internal/platform/implementation/apple/Mediums/BLE/Tests/BUILD +++ b/internal/platform/implementation/apple/Mediums/BLE/Tests/BUILD @@ -37,6 +37,7 @@ objc_library( "GNCFakePeripheral.m", "GNCFakePeripheralManager.m", "GNCFakeSocket.m", + "GNCMBleConnectionTest.m", "GNCMBleUtilsTest.m", "GNCMConnectionsTest.m", "GNCMFakeConnection.mm", diff --git a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeSocket.h b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeSocket.h index d94e4aca..27c01ea0 100644 --- a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeSocket.h +++ b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeSocket.h @@ -16,20 +16,40 @@ #import +#import + NS_ASSUME_NONNULL_BEGIN /** A fake implementation of @c GNSSocket to inject for testing. */ @interface GNCFakeSocket : NSObject +/** An expectation to fulfill when -sendData:progressHandler:completion: is called. */ +@property(nonatomic, nullable) XCTestExpectation *sendDataExpectation; + /** The socket's delegate. */ @property(nonatomic, weak) id delegate; +/** The last data that was sent to the socket. */ +@property(nonatomic, copy, nullable) NSData *sentData; + +/** The completion handler for the last sent data. */ +@property(nonatomic, nullable) GNSErrorHandler sentDataCompletion; + /** Simulates a socket connection event. */ - (void)simulateSocketDidConnect; /** Simulates a socket disconnection event. */ - (void)simulateSocketDidDisconnectWithError:(nullable NSError *)error; +/** Simulates a socket receiving data event. */ +- (void)simulateSocketDidReceiveData:(NSData *)data; + +- (void)sendData:(NSData *)data + progressHandler:(GNSProgressHandler)progressHandler + completion:(GNSErrorHandler)completion; + +- (void)disconnect; + @end NS_ASSUME_NONNULL_END diff --git a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeSocket.m b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeSocket.m index c6364d82..53feb68d 100644 --- a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeSocket.m +++ b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeSocket.m @@ -18,7 +18,17 @@ NS_ASSUME_NONNULL_BEGIN @implementation GNCFakeSocket -@synthesize delegate = _delegate; +- (void)sendData:(NSData *)data + progressHandler:(GNSProgressHandler)progressHandler + completion:(GNSErrorHandler)completion { + self.sentData = data; + self.sentDataCompletion = completion; + [self.sendDataExpectation fulfill]; +} + +- (void)disconnect { + // No-op for fake. +} - (void)simulateSocketDidConnect { [self.delegate socketDidConnect:(GNSSocket *)self]; @@ -28,6 +38,10 @@ NS_ASSUME_NONNULL_BEGIN [self.delegate socket:(GNSSocket *)self didDisconnectWithError:error]; } +- (void)simulateSocketDidReceiveData:(NSData *)data { + [self.delegate socket:(GNSSocket *)self didReceiveData:data]; +} + @end NS_ASSUME_NONNULL_END diff --git a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCMBleConnectionTest.m b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCMBleConnectionTest.m new file mode 100644 index 00000000..a382ee08 --- /dev/null +++ b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCMBleConnectionTest.m @@ -0,0 +1,244 @@ +// 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/GNCMBleConnection.h" + +#import +#import +#import + +#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/Sockets/Source/Shared/GNSSocket.h" +#import "internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeSocket.h" + +static NSString *const kServiceID = @"TestServiceID"; +static const NSTimeInterval kTimeout = 1.0; + +@interface GNCMBleConnectionTest : XCTestCase +@end + +@implementation GNCMBleConnectionTest { + GNCFakeSocket *_fakeSocket; + dispatch_queue_t _callbackQueue; + GNCMBleConnection *_connection; +} + +- (void)setUp { + [super setUp]; + _fakeSocket = [[GNCFakeSocket alloc] init]; + _callbackQueue = dispatch_get_main_queue(); +} + +- (void)tearDown { + _connection = nil; + _fakeSocket = nil; + [super tearDown]; +} + +- (void)testCreateConnectionWithServiceID { + _connection = [GNCMBleConnection connectionWithSocket:(GNSSocket *)_fakeSocket + serviceID:kServiceID + expectedIntroPacket:NO + callbackQueue:_callbackQueue]; + + XCTAssertNotNil(_connection); + XCTAssertEqual(_fakeSocket.delegate, (id)_connection); +} + +- (void)testCreateConnectionWithoutServiceID { + _connection = [GNCMBleConnection connectionWithSocket:(GNSSocket *)_fakeSocket + serviceID:nil + expectedIntroPacket:YES + callbackQueue:_callbackQueue]; + + XCTAssertNotNil(_connection); + XCTAssertEqual(_fakeSocket.delegate, (id)_connection); +} + +- (void)testSendData { + _connection = [GNCMBleConnection connectionWithSocket:(GNSSocket *)_fakeSocket + serviceID:kServiceID + expectedIntroPacket:NO + callbackQueue:_callbackQueue]; + + NSData *data = [@"TestData" dataUsingEncoding:NSUTF8StringEncoding]; + NSData *serviceIDHash = GNCMServiceIDHash(kServiceID); + NSMutableData *expectedPacket = [NSMutableData dataWithData:serviceIDHash]; + [expectedPacket appendData:data]; + + XCTestExpectation *sendDataExpectation = [self expectationWithDescription:@"Send data called"]; + _fakeSocket.sendDataExpectation = sendDataExpectation; + + XCTestExpectation *completionExpectation = [self expectationWithDescription:@"Send completion"]; + + [_connection sendData:data + progressHandler:^(size_t progress) { + } + completion:^(GNCMPayloadResult result) { + XCTAssertEqual(result, GNCMPayloadResultSuccess); + [completionExpectation fulfill]; + }]; + + [self waitForExpectations:@[ sendDataExpectation ] timeout:kTimeout]; + + XCTAssertEqualObjects(_fakeSocket.sentData, expectedPacket); + if (_fakeSocket.sentDataCompletion) { + _fakeSocket.sentDataCompletion(nil); + } + + [self waitForExpectations:@[ completionExpectation ] timeout:kTimeout]; +} + +- (void)testSendDataEmpty { + _connection = [GNCMBleConnection connectionWithSocket:(GNSSocket *)_fakeSocket + serviceID:kServiceID + expectedIntroPacket:NO + callbackQueue:_callbackQueue]; + + NSData *data = [NSData data]; + NSData *expectedPacket = GNCMGenerateBLEFramesIntroductionPacket(GNCMServiceIDHash(kServiceID)); + + XCTestExpectation *sendDataExpectation = [self expectationWithDescription:@"Send data called"]; + _fakeSocket.sendDataExpectation = sendDataExpectation; + + XCTestExpectation *completionExpectation = [self expectationWithDescription:@"Send completion"]; + + [_connection sendData:data + progressHandler:^(size_t progress) { + } + completion:^(GNCMPayloadResult result) { + XCTAssertEqual(result, GNCMPayloadResultSuccess); + [completionExpectation fulfill]; + }]; + + [self waitForExpectations:@[ sendDataExpectation ] timeout:kTimeout]; + + XCTAssertEqualObjects(_fakeSocket.sentData, expectedPacket); + if (_fakeSocket.sentDataCompletion) { + _fakeSocket.sentDataCompletion(nil); + } + + [self waitForExpectations:@[ completionExpectation ] timeout:kTimeout]; +} + +- (void)testReceiveData { + _connection = [GNCMBleConnection connectionWithSocket:(GNSSocket *)_fakeSocket + serviceID:kServiceID + expectedIntroPacket:NO + callbackQueue:_callbackQueue]; + + NSData *expectedData = [@"TestData" dataUsingEncoding:NSUTF8StringEncoding]; + NSData *serviceIDHash = GNCMServiceIDHash(kServiceID); + NSMutableData *packet = [NSMutableData dataWithData:serviceIDHash]; + [packet appendData:expectedData]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Payload handler"]; + + GNCMConnectionHandlers *handlers = [[GNCMConnectionHandlers alloc] init]; + handlers.payloadHandler = ^(NSData *data) { + XCTAssertEqualObjects(data, expectedData); + [expectation fulfill]; + }; + _connection.connectionHandlers = handlers; + + [_fakeSocket simulateSocketDidReceiveData:packet]; + + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; +} + +- (void)testReceiveIntroPacketWithServiceID { + _connection = [GNCMBleConnection connectionWithSocket:(GNSSocket *)_fakeSocket + serviceID:kServiceID + expectedIntroPacket:YES + callbackQueue:_callbackQueue]; + + NSData *introPacket = GNCMGenerateBLEFramesIntroductionPacket(GNCMServiceIDHash(kServiceID)); + + XCTestExpectation *expectation = [self expectationWithDescription:@"Intro packet received"]; + expectation.inverted = YES; // Should not trigger payload handler + + GNCMConnectionHandlers *handlers = [[GNCMConnectionHandlers alloc] init]; + handlers.payloadHandler = ^(NSData *data) { + [expectation fulfill]; + }; + _connection.connectionHandlers = handlers; + + [_fakeSocket simulateSocketDidReceiveData:introPacket]; + + // No data should be passed to the payload handler. + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; +} + +- (void)testReceiveIntroPacketWithoutServiceID { + _connection = [GNCMBleConnection connectionWithSocket:(GNSSocket *)_fakeSocket + serviceID:nil + expectedIntroPacket:YES + callbackQueue:_callbackQueue]; + + NSData *introPacket = GNCMGenerateBLEFramesIntroductionPacket(GNCMServiceIDHash(kServiceID)); + + XCTestExpectation *expectation = [self expectationWithDescription:@"Intro packet received"]; + expectation.inverted = YES; // Should not trigger payload handler + + GNCMConnectionHandlers *handlers = [[GNCMConnectionHandlers alloc] init]; + handlers.payloadHandler = ^(NSData *data) { + [expectation fulfill]; + }; + _connection.connectionHandlers = handlers; + + [_fakeSocket simulateSocketDidReceiveData:introPacket]; + + // No data should be passed to the payload handler. + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + + // Now test receiving data after the intro packet. + NSData *expectedData = [@"TestData" dataUsingEncoding:NSUTF8StringEncoding]; + NSData *serviceIDHash = GNCMServiceIDHash(kServiceID); + NSMutableData *packet = [NSMutableData dataWithData:serviceIDHash]; + [packet appendData:expectedData]; + + XCTestExpectation *dataExpectation = [self expectationWithDescription:@"Payload handler"]; + + GNCMConnectionHandlers *dataHandlers = [[GNCMConnectionHandlers alloc] init]; + dataHandlers.payloadHandler = ^(NSData *data) { + XCTAssertEqualObjects(data, expectedData); + [dataExpectation fulfill]; + }; + _connection.connectionHandlers = dataHandlers; + + [_fakeSocket simulateSocketDidReceiveData:packet]; + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; +} + +- (void)testDisconnect { + _connection = [GNCMBleConnection connectionWithSocket:(GNSSocket *)_fakeSocket + serviceID:kServiceID + expectedIntroPacket:NO + callbackQueue:_callbackQueue]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Disconnected handler"]; + + GNCMConnectionHandlers *handlers = [[GNCMConnectionHandlers alloc] init]; + handlers.disconnectedHandler = ^{ + [expectation fulfill]; + }; + _connection.connectionHandlers = handlers; + + [_fakeSocket simulateSocketDidDisconnectWithError:nil]; + + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; +} + +@end