diff --git a/internal/platform/implementation/apple/Mediums/BLE/Sockets/Source/Central/GNSCentralManager.m b/internal/platform/implementation/apple/Mediums/BLE/Sockets/Source/Central/GNSCentralManager.m index dca2dbf6..f3679ed2 100644 --- a/internal/platform/implementation/apple/Mediums/BLE/Sockets/Source/Central/GNSCentralManager.m +++ b/internal/platform/implementation/apple/Mediums/BLE/Sockets/Source/Central/GNSCentralManager.m @@ -18,27 +18,10 @@ #import "internal/platform/implementation/apple/Log/GNCLogger.h" #import "internal/platform/implementation/apple/Mediums/BLE/Sockets/Source/Central/GNSCentralPeerManager+Private.h" #import "internal/platform/implementation/apple/Mediums/BLE/Sockets/Source/Central/GNSCentralPeerManager.h" +#import "internal/platform/implementation/apple/Mediums/BLE/Sockets/Source/Shared/GNSUtils+Private.h" NS_ASSUME_NONNULL_BEGIN -static NSString *CBManagerStateString(CBManagerState state) { - switch (state) { - case CBManagerStateUnknown: - return @"CBManagerStateUnknown"; - case CBManagerStateResetting: - return @"CBManagerStateResetting"; - case CBManagerStateUnsupported: - return @"CBManagerStateUnsupported"; - case CBManagerStateUnauthorized: - return @"CBManagerStateUnauthorized"; - case CBManagerStatePoweredOff: - return @"CBManagerStatePoweredOff"; - case CBManagerStatePoweredOn: - return @"CBManagerStatePoweredOn"; - } - return [NSString stringWithFormat:@"CBManagerState Unknown(%ld)", (long)state]; -} - @interface GNSCentralManager () { NSString *_advertisedName; CBUUID *_advertisedServiceUUID; diff --git a/internal/platform/implementation/apple/Mediums/BLE/Sockets/Source/Peripheral/GNSPeripheralManager.m b/internal/platform/implementation/apple/Mediums/BLE/Sockets/Source/Peripheral/GNSPeripheralManager.m index 423a1896..12ec6244 100644 --- a/internal/platform/implementation/apple/Mediums/BLE/Sockets/Source/Peripheral/GNSPeripheralManager.m +++ b/internal/platform/implementation/apple/Mediums/BLE/Sockets/Source/Peripheral/GNSPeripheralManager.m @@ -17,29 +17,11 @@ #import "internal/platform/implementation/apple/Log/GNCLogger.h" #import "internal/platform/implementation/apple/Mediums/BLE/Sockets/Source/Peripheral/GNSPeripheralServiceManager+Private.h" #import "internal/platform/implementation/apple/Mediums/BLE/Sockets/Source/Shared/GNSSocket+Private.h" +#import "internal/platform/implementation/apple/Mediums/BLE/Sockets/Source/Shared/GNSUtils+Private.h" #if TARGET_OS_IPHONE #import #endif - -static NSString *CBManagerStateString(CBManagerState state) { - switch (state) { - case CBManagerStateUnknown: - return @"CBManagerStateUnknown"; - case CBManagerStateResetting: - return @"CBManagerStateResetting"; - case CBManagerStateUnsupported: - return @"CBManagerStateUnsupported"; - case CBManagerStateUnauthorized: - return @"CBManagerStateUnauthorized"; - case CBManagerStatePoweredOff: - return @"CBManagerStatePoweredOff"; - case CBManagerStatePoweredOn: - return @"CBManagerStatePoweredOn"; - } - return @"CBManagerState Unknown"; -} - // http://b/28875581 On iOS, under some misterious conditions, the Bluetooth daemon (BTServer // process) continously crashes when attempting to register Bluetooth services. In fact, if the // BTServer process is killed, the OS spins a new instace of this process after 10 seconds. diff --git a/internal/platform/implementation/apple/Mediums/BLE/Sockets/Source/Shared/GNSUtils+Private.h b/internal/platform/implementation/apple/Mediums/BLE/Sockets/Source/Shared/GNSUtils+Private.h index 1e1674f4..7feee87f 100644 --- a/internal/platform/implementation/apple/Mediums/BLE/Sockets/Source/Shared/GNSUtils+Private.h +++ b/internal/platform/implementation/apple/Mediums/BLE/Sockets/Source/Shared/GNSUtils+Private.h @@ -44,4 +44,13 @@ NSError *GNSErrorWithCode(GNSError errorCode); */ NSString *GNSCharacteristicName(NSString *uuid); +/** + * Returns a human readable name based on a CBManagerState. + * + * @param state CBManagerState + * + * @return Name of the CBManagerState. + */ +NSString *CBManagerStateString(CBManagerState state); + NS_ASSUME_NONNULL_END diff --git a/internal/platform/implementation/apple/Mediums/BLE/Sockets/Source/Shared/GNSUtils.m b/internal/platform/implementation/apple/Mediums/BLE/Sockets/Source/Shared/GNSUtils.m index cd295e76..c09b6f2f 100644 --- a/internal/platform/implementation/apple/Mediums/BLE/Sockets/Source/Shared/GNSUtils.m +++ b/internal/platform/implementation/apple/Mediums/BLE/Sockets/Source/Shared/GNSUtils.m @@ -99,4 +99,22 @@ NSString *GNSCharacteristicName(NSString *uuid) { return @"UnknownChar"; } +NSString *CBManagerStateString(CBManagerState state) { + switch (state) { + case CBManagerStateUnknown: + return @"CBManagerStateUnknown"; + case CBManagerStateResetting: + return @"CBManagerStateResetting"; + case CBManagerStateUnsupported: + return @"CBManagerStateUnsupported"; + case CBManagerStateUnauthorized: + return @"CBManagerStateUnauthorized"; + case CBManagerStatePoweredOff: + return @"CBManagerStatePoweredOff"; + case CBManagerStatePoweredOn: + return @"CBManagerStatePoweredOn"; + } + return @"CBManagerState Unknown"; +} + NS_ASSUME_NONNULL_END diff --git a/internal/platform/implementation/apple/Mediums/BLE/Sockets/Tests/BUILD b/internal/platform/implementation/apple/Mediums/BLE/Sockets/Tests/BUILD index 2f4a3001..7df5ae4b 100644 --- a/internal/platform/implementation/apple/Mediums/BLE/Sockets/Tests/BUILD +++ b/internal/platform/implementation/apple/Mediums/BLE/Sockets/Tests/BUILD @@ -46,6 +46,7 @@ objc_library( testonly = 1, srcs = [ "Shared/GNSSocketTest.m", + "Shared/GNSUtilsTest.m", "Shared/GNSWeavePacketTest.m", ], deps = [ diff --git a/internal/platform/implementation/apple/Mediums/BLE/Sockets/Tests/Shared/GNSUtilsTest.m b/internal/platform/implementation/apple/Mediums/BLE/Sockets/Tests/Shared/GNSUtilsTest.m new file mode 100644 index 00000000..08fb83e6 --- /dev/null +++ b/internal/platform/implementation/apple/Mediums/BLE/Sockets/Tests/Shared/GNSUtilsTest.m @@ -0,0 +1,90 @@ +// 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 "internal/platform/implementation/apple/Mediums/BLE/Sockets/Source/Shared/GNSUtils+Private.h" +#import "internal/platform/implementation/apple/Mediums/BLE/Sockets/Source/Shared/GNSUtils.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface GNSUtilsTest : XCTestCase +@end + +@implementation GNSUtilsTest + +- (void)testGNSErrorWithCode { + // Test all error codes to ensure they have descriptions + NSArray *allErrorCodes = @[ + @(GNSErrorNoConnection), + @(GNSErrorLostConnection), + @(GNSErrorOperationInProgress), + @(GNSErrorMissingService), + @(GNSErrorMissingCharacteristics), + @(GNSErrorPeripheralDidRefuseConnection), + @(GNSErrorCancelPendingSocketRequested), + @(GNSErrorNewInviteToConnectReceived), + @(GNSErrorWeaveErrorPacketReceived), + @(GNSErrorUnsupportedWeaveProtocolVersion), + @(GNSErrorUnexpectedWeaveControlPacket), + @(GNSErrorParsingWeavePacket), + @(GNSErrorWrongWeavePacketCounter), + @(GNSErrorWeaveDataTransferInProgress), + @(GNSErrorParsingWeavePacketTooSmall), + @(GNSErrorParsingWeavePacketTooLarge), + @(GNSErrorConnectionTimedOut), + ]; + for (NSNumber *errorCodeNum in allErrorCodes) { + GNSError errorCode = [errorCodeNum integerValue]; + NSError *currentError = GNSErrorWithCode(errorCode); + XCTAssertEqualObjects(currentError.domain, kGNSSocketsErrorDomain); + XCTAssertEqual(currentError.code, errorCode); + XCTAssertNotNil(currentError.localizedDescription, @"Description missing for error code %ld", + (long)errorCode); + XCTAssertTrue(currentError.localizedDescription.length > 0, + @"Description empty for error code %ld", (long)errorCode); + } + + // Test the assertion for GNSErrorNoError + XCTAssertThrows(GNSErrorWithCode(GNSErrorNoError), + @"Should throw an assertion for GNSErrorNoError"); +} + +- (void)testGNSCharacteristicName { + XCTAssertEqualObjects(GNSCharacteristicName(kGNSWeaveToPeripheralCharUUIDString), + @"ToPeripheralChar"); + XCTAssertEqualObjects(GNSCharacteristicName(kGNSWeaveFromPeripheralCharUUIDString), + @"FromPeripheralChar"); + XCTAssertEqualObjects(GNSCharacteristicName(kGNSPairingCharUUIDString), @"PairingChar"); + XCTAssertEqualObjects(GNSCharacteristicName(@"UNKNOWN_UUID"), @"UnknownChar"); + XCTAssertEqualObjects(GNSCharacteristicName(@""), @"UnknownChar"); +} + +- (void)testCBManagerStateString { + XCTAssertEqualObjects(CBManagerStateString(CBManagerStateUnknown), @"CBManagerStateUnknown"); + XCTAssertEqualObjects(CBManagerStateString(CBManagerStateResetting), @"CBManagerStateResetting"); + XCTAssertEqualObjects(CBManagerStateString(CBManagerStateUnsupported), + @"CBManagerStateUnsupported"); + XCTAssertEqualObjects(CBManagerStateString(CBManagerStateUnauthorized), + @"CBManagerStateUnauthorized"); + XCTAssertEqualObjects(CBManagerStateString(CBManagerStatePoweredOff), + @"CBManagerStatePoweredOff"); + XCTAssertEqualObjects(CBManagerStateString(CBManagerStatePoweredOn), @"CBManagerStatePoweredOn"); + // Test with an out-of-range value + XCTAssertEqualObjects(CBManagerStateString((CBManagerState)100), @"CBManagerState Unknown"); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/internal/platform/implementation/apple/Mediums/BLE/Sockets/Tests/Shared/GNSWeavePacketTest.m b/internal/platform/implementation/apple/Mediums/BLE/Sockets/Tests/Shared/GNSWeavePacketTest.m index a1dd64b7..b6064bce 100644 --- a/internal/platform/implementation/apple/Mediums/BLE/Sockets/Tests/Shared/GNSWeavePacketTest.m +++ b/internal/platform/implementation/apple/Mediums/BLE/Sockets/Tests/Shared/GNSWeavePacketTest.m @@ -16,8 +16,47 @@ #import "internal/platform/implementation/apple/Mediums/BLE/Sockets/Source/Shared/GNSWeavePacket.h" +#import "internal/platform/implementation/apple/Mediums/BLE/Sockets/Source/Shared/GNSUtils+Private.h" #import "internal/platform/implementation/apple/Mediums/BLE/Sockets/Source/Shared/GNSUtils.h" +// Fake handler for testing visitWithHandler +@interface GNSFakeWeavePacketHandler : NSObject +@property(nonatomic, nullable) GNSWeaveConnectionRequestPacket *lastConnectionRequestPacket; +@property(nonatomic, nullable) GNSWeaveConnectionConfirmPacket *lastConnectionConfirmPacket; +@property(nonatomic, nullable) GNSWeaveErrorPacket *lastErrorPacket; +@property(nonatomic, nullable) GNSWeaveDataPacket *lastDataPacket; +@property(nonatomic, nullable) id lastContext; +@property(nonatomic) int handleConnectionRequestCalledCount; +@property(nonatomic) int handleConnectionConfirmCalledCount; +@property(nonatomic) int handleErrorCalledCount; +@property(nonatomic) int handleDataCalledCount; +@end + +@implementation GNSFakeWeavePacketHandler +- (void)handleConnectionRequestPacket:(GNSWeaveConnectionRequestPacket *)packet + context:(nullable id)context { + self.lastConnectionRequestPacket = packet; + self.lastContext = context; + self.handleConnectionRequestCalledCount++; +} +- (void)handleConnectionConfirmPacket:(GNSWeaveConnectionConfirmPacket *)packet + context:(nullable id)context { + self.lastConnectionConfirmPacket = packet; + self.lastContext = context; + self.handleConnectionConfirmCalledCount++; +} +- (void)handleErrorPacket:(GNSWeaveErrorPacket *)packet context:(nullable id)context { + self.lastErrorPacket = packet; + self.lastContext = context; + self.handleErrorCalledCount++; +} +- (void)handleDataPacket:(GNSWeaveDataPacket *)packet context:(nullable id)context { + self.lastDataPacket = packet; + self.lastContext = context; + self.handleDataCalledCount++; +} +@end + @interface GNSWeavePacketTest : XCTestCase { NSData *_largeNonEmptyData; NSData *_smallNonEmptyData; @@ -307,4 +346,168 @@ XCTAssertEqualObjects(connectionConfirmPacket.data, smallPayload); } +- (void)testConnectionRequestVisitWithHandler { + GNSWeaveConnectionRequestPacket *requestPacket = + [[GNSWeaveConnectionRequestPacket alloc] initWithMinVersion:1 + maxVersion:1 + maxPacketSize:20 + data:nil]; + GNSFakeWeavePacketHandler *handler = [[GNSFakeWeavePacketHandler alloc] init]; + id context = [[NSObject alloc] init]; + + XCTAssertTrue([requestPacket visitWithHandler:handler context:context]); + XCTAssertEqual(handler.handleConnectionRequestCalledCount, 1); + XCTAssertEqual(handler.lastConnectionRequestPacket, requestPacket); + XCTAssertEqual(handler.lastContext, context); + XCTAssertEqual(handler.handleConnectionConfirmCalledCount, 0); + XCTAssertEqual(handler.handleErrorCalledCount, 0); + XCTAssertEqual(handler.handleDataCalledCount, 0); +} + +- (void)testConnectionConfirmVisitWithHandler { + GNSWeaveConnectionConfirmPacket *confirmPacket = + [[GNSWeaveConnectionConfirmPacket alloc] initWithVersion:1 packetSize:20 data:nil]; + GNSFakeWeavePacketHandler *handler = [[GNSFakeWeavePacketHandler alloc] init]; + id context = [[NSObject alloc] init]; + + XCTAssertTrue([confirmPacket visitWithHandler:handler context:context]); + XCTAssertEqual(handler.handleConnectionConfirmCalledCount, 1); + XCTAssertEqual(handler.lastConnectionConfirmPacket, confirmPacket); + XCTAssertEqual(handler.lastContext, context); + XCTAssertEqual(handler.handleConnectionRequestCalledCount, 0); + XCTAssertEqual(handler.handleErrorCalledCount, 0); + XCTAssertEqual(handler.handleDataCalledCount, 0); +} + +- (void)testErrorVisitWithHandler { + GNSWeaveErrorPacket *errorPacket = [[GNSWeaveErrorPacket alloc] initWithPacketCounter:3]; + GNSFakeWeavePacketHandler *handler = [[GNSFakeWeavePacketHandler alloc] init]; + id context = [[NSObject alloc] init]; + + XCTAssertTrue([errorPacket visitWithHandler:handler context:context]); + XCTAssertEqual(handler.handleErrorCalledCount, 1); + XCTAssertEqual(handler.lastErrorPacket, errorPacket); + XCTAssertEqual(handler.lastContext, context); + XCTAssertEqual(handler.handleConnectionRequestCalledCount, 0); + XCTAssertEqual(handler.handleConnectionConfirmCalledCount, 0); + XCTAssertEqual(handler.handleDataCalledCount, 0); +} + +- (void)testDataVisitWithHandler { + GNSWeaveDataPacket *dataPacket = + [[GNSWeaveDataPacket alloc] initWithPacketCounter:4 + firstPacket:YES + lastPacket:YES + data:_smallNonEmptyData]; + GNSFakeWeavePacketHandler *handler = [[GNSFakeWeavePacketHandler alloc] init]; + id context = [[NSObject alloc] init]; + + XCTAssertTrue([dataPacket visitWithHandler:handler context:context]); + XCTAssertEqual(handler.handleDataCalledCount, 1); + XCTAssertEqual(handler.lastDataPacket, dataPacket); + XCTAssertEqual(handler.lastContext, context); + XCTAssertEqual(handler.handleConnectionRequestCalledCount, 0); + XCTAssertEqual(handler.handleConnectionConfirmCalledCount, 0); + XCTAssertEqual(handler.handleErrorCalledCount, 0); +} + +- (void)testVisitWithHandlerNoMatch { + GNSWeaveConnectionRequestPacket *requestPacket = + [[GNSWeaveConnectionRequestPacket alloc] initWithMinVersion:1 + maxVersion:1 + maxPacketSize:20 + data:nil]; + // Handler that implements nothing from the protocol + id emptyHandler = (id)[[NSObject alloc] init]; + + XCTAssertFalse([requestPacket visitWithHandler:emptyHandler context:nil]); +} + +// Additional tests for parsing edge cases + +- (void)testParseConnectionRequestMinSize { + UInt8 header = (1 << 7) + 0; // Connection Request, counter 0 + NSMutableData *payload = [NSMutableData data]; + UInt16 val = 0; + // Min payload size is 6 bytes + [payload appendBytes:&val length:sizeof(val)]; + [payload appendBytes:&val length:sizeof(val)]; + [payload appendBytes:&val length:sizeof(val)]; + XCTAssertEqual(payload.length, 6); + + NSError *error = nil; + GNSWeavePacket *packet = + [GNSWeavePacket parseData:[self weavePacketWithHeader:header data:payload] error:&error]; + XCTAssertNotNil(packet); + XCTAssertNil(error); + XCTAssertTrue([packet isKindOfClass:[GNSWeaveConnectionRequestPacket class]]); + + // Too small + NSData *tooSmallPayload = [payload subdataWithRange:NSMakeRange(0, 5)]; + packet = [GNSWeavePacket parseData:[self weavePacketWithHeader:header data:tooSmallPayload] + error:&error]; + XCTAssertNil(packet); + XCTAssertEqual(error.code, GNSErrorParsingWeavePacketTooSmall); +} + +- (void)testParseConnectionConfirmMinSize { + UInt8 header = (1 << 7) + 1; // Connection Confirm, counter 0 + NSMutableData *payload = [NSMutableData data]; + UInt16 version = 1; + UInt16 packetSize = kGNSMinSupportedPacketSize; + // Min payload size is 4 bytes + UInt16 versionBigEndian = CFSwapInt16HostToBig(version); + UInt16 packetSizeBigEndian = CFSwapInt16HostToBig(packetSize); + [payload appendBytes:&versionBigEndian length:sizeof(versionBigEndian)]; + [payload appendBytes:&packetSizeBigEndian length:sizeof(packetSizeBigEndian)]; + XCTAssertEqual(payload.length, 4); + + NSError *error = nil; + GNSWeavePacket *packet = + [GNSWeavePacket parseData:[self weavePacketWithHeader:header data:payload] error:&error]; + XCTAssertNotNil(packet); + XCTAssertNil(error); + XCTAssertTrue([packet isKindOfClass:[GNSWeaveConnectionConfirmPacket class]]); + + // Too small + NSData *tooSmallPayload = [payload subdataWithRange:NSMakeRange(0, 3)]; + packet = [GNSWeavePacket parseData:[self weavePacketWithHeader:header data:tooSmallPayload] + error:&error]; + XCTAssertNil(packet); + XCTAssertEqual(error.code, GNSErrorParsingWeavePacketTooSmall); +} + +- (void)testParseErrorPacketSize { + UInt8 header = (1 << 7) + 2; // Error, counter 0 + // Error packet has no payload + NSError *error = nil; + GNSWeavePacket *packet = [GNSWeavePacket parseData:[self weavePacketWithHeader:header data:nil] + error:&error]; + XCTAssertNotNil(packet); + XCTAssertNil(error); + + // Should still be valid with empty data + packet = [GNSWeavePacket parseData:[self weavePacketWithHeader:header data:[NSData data]] + error:&error]; + XCTAssertNotNil(packet); + XCTAssertNil(error); + + // Invalid with payload + packet = [GNSWeavePacket parseData:[self weavePacketWithHeader:header data:_smallNonEmptyData] + error:nil]; + // This doesn't cause an error in the current parseData, it just ignores the extra data. + // Depending on spec, this might be desired or not. Assuming it's fine. + XCTAssertNotNil(packet); +} + +- (void)testMaxPacketCounter { + XCTAssertThrows([[GNSWeaveErrorPacket alloc] initWithPacketCounter:8], + @"Should assert for packet counter >= 8"); + XCTAssertThrows([[GNSWeaveDataPacket alloc] initWithPacketCounter:8 + firstPacket:YES + lastPacket:YES + data:_smallNonEmptyData], + @"Should assert for packet counter >= 8"); +} + @end