diff --git a/internal/platform/implementation/apple/Mediums/BLE/GNCMBleUtils.h b/internal/platform/implementation/apple/Mediums/BLE/GNCMBleUtils.h index db7231e2..d5bc59d6 100644 --- a/internal/platform/implementation/apple/Mediums/BLE/GNCMBleUtils.h +++ b/internal/platform/implementation/apple/Mediums/BLE/GNCMBleUtils.h @@ -74,7 +74,7 @@ NSData *GNCMGenerateBLEFramesIntroductionPacket(NSData *serviceIDHash); * Parses the packet for Ble SocketControlFrame introduction packet and returns * serviceIdHash if succeed. */ -NSData *GNCMParseBLEFramesIntroductionPacket(NSData *data); +NSData *_Nullable GNCMParseBLEFramesIntroductionPacket(NSData *_Nullable data); /** Creates the disconnection packet for Ble SocketControlFrame. */ NSData *GNCMGenerateBLEFramesDisconnectionPacket(NSData *serviceIDHash); @@ -83,7 +83,7 @@ NSData *GNCMGenerateBLEFramesDisconnectionPacket(NSData *serviceIDHash); NSData *GNCMGenerateBLEFramesPacketAcknowledgementPacket(NSData *serviceIDHash, int receivedSize); /** Parses the BLE L2CAP packet from the data. */ -GNCMBLEL2CAPPacket *_Nullable GNCMParseBLEL2CAPPacket(NSData *data); +GNCMBLEL2CAPPacket *_Nullable GNCMParseBLEL2CAPPacket(NSData *_Nullable data); /** Creates the BLE L2CAP packet from the command and data. */ NSData *_Nullable GNCMGenerateBLEL2CAPPacket(GNCMBLEL2CAPCommand command, NSData *_Nullable data); diff --git a/internal/platform/implementation/apple/Mediums/BLE/GNCMBleUtils.mm b/internal/platform/implementation/apple/Mediums/BLE/GNCMBleUtils.mm index f7028b8a..6c1c518d 100644 --- a/internal/platform/implementation/apple/Mediums/BLE/GNCMBleUtils.mm +++ b/internal/platform/implementation/apple/Mediums/BLE/GNCMBleUtils.mm @@ -63,9 +63,11 @@ NSData *GNCMGenerateBLEFramesIntroductionPacket(NSData *serviceIDHash) { return packet; } -NSData *_Nullable GNCMParseBLEFramesIntroductionPacket(NSData *data) { +NSData *_Nullable GNCMParseBLEFramesIntroductionPacket(NSData *_Nullable data) { + if (!data) return nil; ::location::nearby::mediums::SocketControlFrame socket_control_frame; NSUInteger prefixLength = sizeof(kGNCMControlPacketServiceIDHash); + if (data.length <= prefixLength) return nil; NSData *packet = [data subdataWithRange:NSMakeRange(prefixLength, data.length - prefixLength)]; if (socket_control_frame.ParseFromArray(packet.bytes, (int)packet.length)) { if (socket_control_frame.type() == @@ -132,9 +134,12 @@ NSData *GNCMGenerateBLEFramesPacketAcknowledgementPacket(NSData *serviceIDHash, @end -// TODO: b/399815436 - Add unit tests for this function. -GNCMBLEL2CAPPacket *_Nullable GNCMParseBLEL2CAPPacket(NSData *data) { - if (data.length < 1) { +GNCMBLEL2CAPPacket *_Nullable GNCMParseBLEL2CAPPacket(NSData *_Nullable data) { + const NSUInteger kCommandLength = 1; + const NSUInteger kLengthFieldLength = 2; + const NSUInteger kHeaderLength = kCommandLength + kLengthFieldLength; + + if (!data || data.length < kCommandLength) { return nil; } @@ -148,26 +153,29 @@ GNCMBLEL2CAPPacket *_Nullable GNCMParseBLEL2CAPPacket(NSData *data) { // Extract data NSData *packetData = nil; - if (receivedDataLength > 3) { + if (receivedDataLength > kHeaderLength) { // Extract data length (2 bytes, big endian) int dataLength = (bytes[1] << 8) | bytes[2]; // Validate data length - if (dataLength != (int)(receivedDataLength - 3)) { + if (dataLength != (int)(receivedDataLength - kHeaderLength)) { GNCLoggerError(@"[NEARBY] Data length mismatch. Expected: %d, Actual: %lu", dataLength, - receivedDataLength - 3); + receivedDataLength - kHeaderLength); return nil; } if (dataLength > 0) { - packetData = [NSData dataWithBytes:&bytes[3] length:dataLength]; + packetData = [NSData dataWithBytes:&bytes[kHeaderLength] length:dataLength]; } else { packetData = nil; } + } else if (receivedDataLength > kCommandLength) { + // Header is incomplete + GNCLoggerError(@"[NEARBY] Incomplete L2CAP packet header."); + return nil; } return [[GNCMBLEL2CAPPacket alloc] initWithCommand:command data:packetData]; } -// TODO: b/399815436 - Add unit tests for this function. NSData *_Nullable GNCMGenerateBLEL2CAPPacket(GNCMBLEL2CAPCommand command, NSData *_Nullable data) { if (!IsSupportedCommand(command)) { GNCLoggerError(@"[NEARBY] Invalid command to generate packet: %lu", command); diff --git a/internal/platform/implementation/apple/Mediums/BLE/Tests/BUILD b/internal/platform/implementation/apple/Mediums/BLE/Tests/BUILD index a79b9042..fe9585b8 100644 --- a/internal/platform/implementation/apple/Mediums/BLE/Tests/BUILD +++ b/internal/platform/implementation/apple/Mediums/BLE/Tests/BUILD @@ -33,10 +33,11 @@ objc_library( "GNCBLEL2CAPServerTest.m", "GNCBLEL2CAPStreamTest.m", "GNCBLEMediumTest.m", - "GNCBLEUtilsTest.mm", "GNCFakeCentralManager.m", "GNCFakePeripheral.m", "GNCFakePeripheralManager.m", + "GNCFakeSocket.m", + "GNCMBleUtilsTest.m", "GNCMFakeConnection.mm", "NSData+GNCBase85Test.m", "NSData+GNCWebSafeBase64Test.m", @@ -50,6 +51,7 @@ objc_library( "GNCFakeCentralManager.h", "GNCFakePeripheral.h", "GNCFakePeripheralManager.h", + "GNCFakeSocket.h", "GNCMFakeConnection.h", ], deps = [ @@ -58,9 +60,11 @@ objc_library( "//internal/platform/implementation/apple", # buildcleaner: keep "//internal/platform/implementation/apple:ble_v2", "//internal/platform/implementation/apple/Mediums/BLE", + "//internal/platform/implementation/apple/Mediums/BLE/Sockets:Shared", "//third_party/apple_frameworks:CoreBluetooth", "//third_party/apple_frameworks:Foundation", "//third_party/apple_frameworks:XCTest", + "//third_party/objective_c/ocmock/v3:OCMock", ], ) diff --git a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeSocket.h b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeSocket.h new file mode 100644 index 00000000..d94e4aca --- /dev/null +++ b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeSocket.h @@ -0,0 +1,35 @@ +// 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/Sockets/Source/Shared/GNSSocket.h" + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** A fake implementation of @c GNSSocket to inject for testing. */ +@interface GNCFakeSocket : NSObject + +/** The socket's delegate. */ +@property(nonatomic, weak) id delegate; + +/** Simulates a socket connection event. */ +- (void)simulateSocketDidConnect; + +/** Simulates a socket disconnection event. */ +- (void)simulateSocketDidDisconnectWithError:(nullable NSError *)error; + +@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 new file mode 100644 index 00000000..c6364d82 --- /dev/null +++ b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeSocket.m @@ -0,0 +1,33 @@ +// 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/GNCFakeSocket.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation GNCFakeSocket + +@synthesize delegate = _delegate; + +- (void)simulateSocketDidConnect { + [self.delegate socketDidConnect:(GNSSocket *)self]; +} + +- (void)simulateSocketDidDisconnectWithError:(nullable NSError *)error { + [self.delegate socket:(GNSSocket *)self didDisconnectWithError:error]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCMBleUtilsTest.m b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCMBleUtilsTest.m new file mode 100644 index 00000000..b1c08d95 --- /dev/null +++ b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCMBleUtilsTest.m @@ -0,0 +1,175 @@ +// 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/GNCMBleUtils.h" + +#import + +#import "internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeSocket.h" + +static const NSTimeInterval kTimeout = 1.0; +static const NSTimeInterval kWaitForConnectionTimeout = 6.0; // Allow for the 5s internal timeout + +@interface GNCMBleUtilsTest : XCTestCase +@end + +@implementation GNCMBleUtilsTest + +- (void)testServiceIDHash { + NSString *serviceID = @"TestServiceID"; + NSData *hash = GNCMServiceIDHash(serviceID); + XCTAssertNotNil(hash); + XCTAssertEqual(hash.length, GNCMBleAdvertisementLengthServiceIDHash); + // Check for consistency + NSData *hash2 = GNCMServiceIDHash(serviceID); + XCTAssertEqualObjects(hash, hash2); +} + +- (void)testGenerateAndParseBLEFramesIntroductionPacket { + NSData *serviceIDHash = [@"123" dataUsingEncoding:NSUTF8StringEncoding]; + NSData *packet = GNCMGenerateBLEFramesIntroductionPacket(serviceIDHash); + XCTAssertNotNil(packet); + + NSData *parsedHash = GNCMParseBLEFramesIntroductionPacket(packet); + XCTAssertEqualObjects(parsedHash, serviceIDHash); +} + +- (void)testParseBLEFramesIntroductionPacketFailure_NilData { + NSData *parsedHash = GNCMParseBLEFramesIntroductionPacket(nil); + XCTAssertNil(parsedHash); +} + +- (void)testParseBLEFramesIntroductionPacketFailure_ShortData { + NSData *invalidPacket = [@"12" dataUsingEncoding:NSUTF8StringEncoding]; + NSData *parsedHash = GNCMParseBLEFramesIntroductionPacket(invalidPacket); + XCTAssertNil(parsedHash); +} + +- (void)testGenerateBLEFramesDisconnectionPacket { + NSData *serviceIDHash = [@"ABC" dataUsingEncoding:NSUTF8StringEncoding]; + NSData *packet = GNCMGenerateBLEFramesDisconnectionPacket(serviceIDHash); + XCTAssertNotNil(packet); + XCTAssertTrue(packet.length > serviceIDHash.length); +} + +- (void)testGenerateBLEFramesPacketAcknowledgementPacket { + NSData *serviceIDHash = [@"DEF" dataUsingEncoding:NSUTF8StringEncoding]; + int receivedSize = 1024; + NSData *packet = GNCMGenerateBLEFramesPacketAcknowledgementPacket(serviceIDHash, receivedSize); + XCTAssertNotNil(packet); + XCTAssertTrue(packet.length > serviceIDHash.length); +} + +- (void)testGenerateAndParseBLEL2CAPPacketWithData { + GNCMBLEL2CAPCommand command = GNCMBLEL2CAPCommandRequestAdvertisement; + NSData *data = [@"test_data" dataUsingEncoding:NSUTF8StringEncoding]; + NSData *packetData = GNCMGenerateBLEL2CAPPacket(command, data); + XCTAssertNotNil(packetData); + + GNCMBLEL2CAPPacket *packet = GNCMParseBLEL2CAPPacket(packetData); + XCTAssertNotNil(packet); + XCTAssertEqual(packet.command, command); + XCTAssertEqualObjects(packet.data, data); +} + +- (void)testGenerateAndParseBLEL2CAPPacketWithoutData { + GNCMBLEL2CAPCommand command = GNCMBLEL2CAPCommandRequestAdvertisementFinish; + NSData *packetData = GNCMGenerateBLEL2CAPPacket(command, nil); + XCTAssertNotNil(packetData); + + GNCMBLEL2CAPPacket *packet = GNCMParseBLEL2CAPPacket(packetData); + XCTAssertNotNil(packet); + XCTAssertEqual(packet.command, command); + XCTAssertNil(packet.data); +} + +- (void)testParseBLEL2CAPPacketInvalidData_Nil { + GNCMBLEL2CAPPacket *packet = GNCMParseBLEL2CAPPacket(nil); + XCTAssertNil(packet); +} + +- (void)testParseBLEL2CAPPacketInvalidData_Empty { + GNCMBLEL2CAPPacket *packet = GNCMParseBLEL2CAPPacket([[NSData alloc] init]); + XCTAssertNil(packet); +} + +- (void)testParseBLEL2CAPPacketShortData_NoLength { + uint8_t commandByte = (uint8_t)GNCMBLEL2CAPCommandRequestAdvertisement; + NSData *shortPacketData = [NSData dataWithBytes:&commandByte length:1]; + GNCMBLEL2CAPPacket *packet = GNCMParseBLEL2CAPPacket(shortPacketData); + XCTAssertNotNil(packet); + XCTAssertEqual(packet.command, GNCMBLEL2CAPCommandRequestAdvertisement); + XCTAssertNil(packet.data); +} + +- (void)testParseBLEL2CAPPacketShortData_IncompleteLength { + uint8_t bytes[] = {GNCMBLEL2CAPCommandRequestAdvertisement, 0x01}; + NSData *shortPacketData = [NSData dataWithBytes:bytes length:sizeof(bytes)]; + GNCMBLEL2CAPPacket *packet = GNCMParseBLEL2CAPPacket(shortPacketData); + XCTAssertNil(packet); +} + +- (void)testParseBLEL2CAPPacketShortData_IncompleteData { + uint8_t bytes[] = {GNCMBLEL2CAPCommandRequestAdvertisement, 0x00, 0x05, 0x01, 0x02, 0x03, 0x04}; + NSData *shortPacketData = [NSData dataWithBytes:bytes length:sizeof(bytes)]; + GNCMBLEL2CAPPacket *packet = GNCMParseBLEL2CAPPacket(shortPacketData); + XCTAssertNil(packet); +} + +- (void)testWaitForConnection_Success { + GNCFakeSocket *fakeSocket = [[GNCFakeSocket alloc] init]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Connection success"]; + GNCMWaitForConnection((GNSSocket *)fakeSocket, ^(BOOL flag) { + XCTAssertTrue(flag); + [expectation fulfill]; + }); + + // Simulate the connection callback + [fakeSocket simulateSocketDidConnect]; + + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; +} + +- (void)testWaitForConnection_Failure_Disconnect { + GNCFakeSocket *fakeSocket = [[GNCFakeSocket alloc] init]; + + XCTestExpectation *expectation = + [self expectationWithDescription:@"Connection failed on disconnect"]; + GNCMWaitForConnection((GNSSocket *)fakeSocket, ^(BOOL flag) { + XCTAssertFalse(flag); + [expectation fulfill]; + }); + + // Simulate the disconnection callback + [fakeSocket simulateSocketDidDisconnectWithError:nil]; + + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; +} + +- (void)testWaitForConnection_Failure_Timeout { + GNCFakeSocket *fakeSocket = [[GNCFakeSocket alloc] init]; + // No delegate call - let it time out + + XCTestExpectation *expectation = + [self expectationWithDescription:@"Connection failed on timeout"]; + GNCMWaitForConnection((GNSSocket *)fakeSocket, ^(BOOL flag) { + XCTAssertFalse(flag); + [expectation fulfill]; + }); + + [self waitForExpectationsWithTimeout:kWaitForConnectionTimeout handler:nil]; +} + +@end diff --git a/internal/platform/implementation/apple/Tests/BUILD b/internal/platform/implementation/apple/Tests/BUILD index 8c680b98..0beba109 100644 --- a/internal/platform/implementation/apple/Tests/BUILD +++ b/internal/platform/implementation/apple/Tests/BUILD @@ -25,6 +25,7 @@ objc_library( testonly = True, srcs = [ "GNCAwdlMediumTest.mm", + "GNCBLEUtilsTest.mm", "GNCCryptoTest.mm", "GNCDeviceInfoTest.mm", "GNCMultiThreadExecutorTest.mm", @@ -41,6 +42,7 @@ objc_library( ], deps = [ "//internal/platform:base", + "//internal/platform/implementation:comm", "//internal/platform/implementation:platform", "//internal/platform/implementation:types", "//internal/platform/implementation/apple", # buildcleaner: keep diff --git a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEUtilsTest.mm b/internal/platform/implementation/apple/Tests/GNCBLEUtilsTest.mm similarity index 98% rename from internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEUtilsTest.mm rename to internal/platform/implementation/apple/Tests/GNCBLEUtilsTest.mm index 48ca7f27..eff61163 100644 --- a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCBLEUtilsTest.mm +++ b/internal/platform/implementation/apple/Tests/GNCBLEUtilsTest.mm @@ -92,8 +92,7 @@ using ByteArray = ::nearby::ByteArray; - (void)testCBAttributePermissionsFromCPPSinglePermission { CBAttributePermissions expected = CBAttributePermissionsReadable; - CBAttributePermissions actual = - ::nearby::apple::CBAttributePermissionsFromCPP(Permission::kRead); + CBAttributePermissions actual = ::nearby::apple::CBAttributePermissionsFromCPP(Permission::kRead); XCTAssertEqual(actual, expected); }