diff --git a/internal/platform/implementation/apple/Tests/BUILD b/internal/platform/implementation/apple/Tests/BUILD index a996bee0..c62951bf 100644 --- a/internal/platform/implementation/apple/Tests/BUILD +++ b/internal/platform/implementation/apple/Tests/BUILD @@ -38,6 +38,7 @@ objc_library( "GNCWifiHotspotMediumTest.mm", "GNCWifiLanMediumTest.mm", "UtilsTest.mm", + "ble_gatt_client_test.mm", "ble_gatt_server_test.mm", "ble_l2cap_server_socket_test.mm", "ble_l2cap_socket_test.mm", @@ -48,6 +49,8 @@ objc_library( "network_utils_test.mm", ], deps = [ + "//connections/implementation/flags:connections_flags", + "//internal/flags:nearby_flags", "//internal/platform:base", "//internal/platform:cancellation_flag", "//internal/platform/implementation:comm", @@ -57,6 +60,7 @@ objc_library( "//internal/platform/implementation/apple:Shared", "//internal/platform/implementation/apple:ble_v2", "//internal/platform/implementation/apple:network_utils", + "//internal/platform/implementation/apple/Flags", "//internal/platform/implementation/apple/Mediums/BLE", "//internal/platform/implementation/apple/Mediums/BLE/Tests:BLETestsLib", "//internal/platform/implementation/apple/Mediums/CoreLocation/CLLocationManager/Fake", diff --git a/internal/platform/implementation/apple/Tests/ble_gatt_client_test.mm b/internal/platform/implementation/apple/Tests/ble_gatt_client_test.mm new file mode 100644 index 00000000..c5969c5f --- /dev/null +++ b/internal/platform/implementation/apple/Tests/ble_gatt_client_test.mm @@ -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. + +#include "internal/platform/implementation/apple/ble_gatt_client.h" + +#import +#import + +#include +#include +#include + +#include "connections/implementation/flags/nearby_connections_feature_flags.h" +#include "internal/flags/nearby_flags.h" +#include "internal/platform/implementation/ble.h" +#import "internal/platform/implementation/apple/Flags/GNCFeatureFlags.h" +#import "internal/platform/implementation/apple/Mediums/BLE/GNCBLEGATTCharacteristic.h" +#import "internal/platform/implementation/apple/Mediums/BLE/GNCBLEGATTClient.h" +#import "internal/platform/implementation/apple/ble_utils.h" + +/** + * Fake implementation for GNCBLEGATTClient. + * TODO: b/434870979 - Move GNCFakeGNCBLEGATTClient to a separate file so it can be used by other + * tests. + */ +@interface GNCFakeGNCBLEGATTClient : NSObject + +/** + * The characteristic to return when `characteristicWithUUID:serviceUUID:completionHandler:` is + * called. + */ + +@property(nonatomic) GNCBLEGATTCharacteristic *characteristicToReturn; +/** + * The value to return when `readValueForCharacteristic:completionHandler:` is called. + */ + +@property(nonatomic) NSData *valueToReturn; +/** + * The error to return through completion handlers if not nil. + */ + +@property(nonatomic) NSError *errorToReturn; + +/** Tracks if disconnect has been called. */ +@property(nonatomic) BOOL disconnectCalled; + +@end + +@implementation GNCFakeGNCBLEGATTClient + +- (void)discoverCharacteristicsWithUUIDs:(NSArray *)characteristicUUIDs + serviceUUID:(CBUUID *)serviceUUID + completionHandler: + (nullable GNCDiscoverCharacteristicsCompletionHandler)completionHandler { + if (completionHandler) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + completionHandler(_errorToReturn); + }); + } +} + +- (void)characteristicWithUUID:(CBUUID *)characteristicUUID + serviceUUID:(CBUUID *)serviceUUID + completionHandler:(nullable GNCGetCharacteristicCompletionHandler)completionHandler { + if (completionHandler) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + completionHandler(_errorToReturn ? nil : _characteristicToReturn, _errorToReturn); + }); + } +} + +- (void)readValueForCharacteristic:(GNCBLEGATTCharacteristic *)characteristic + completionHandler: + (nullable GNCReadCharacteristicValueCompletionHandler)completionHandler { + if (completionHandler) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + completionHandler(_errorToReturn ? nil : _valueToReturn, _errorToReturn); + }); + } +} + +- (void)disconnect { + _disconnectCalled = YES; +} + +@end + +@interface GNCGattClientTest : XCTestCase +@end + +@implementation GNCGattClientTest { + std::unique_ptr _gattClient; + GNCFakeGNCBLEGATTClient *_fakeGNCBLEGATTClient; +} + +- (void)setUp { + [super setUp]; + _fakeGNCBLEGATTClient = [[GNCFakeGNCBLEGATTClient alloc] init]; + _gattClient = + std::make_unique((GNCBLEGATTClient *)_fakeGNCBLEGATTClient); +} + +- (void)tearDown { + nearby::NearbyFlags::GetInstance().OverrideBoolFlagValue( + nearby::connections::config_package_nearby::nearby_connections_feature:: + kEnableGattClientDisconnection, + false); + [super tearDown]; +} + +- (void)testDiscoverServiceAndCharacteristicsSuccess { + nearby::Uuid serviceUUID = nearby::Uuid([[CBUUID UUIDWithString:@"FEF3"] UUIDString].UTF8String); + nearby::Uuid characteristicUUID = + nearby::Uuid([[CBUUID UUIDWithString:@"B2B4"] UUIDString].UTF8String); + std::vector characteristicUUIDs = {characteristicUUID}; + + BOOL result = _gattClient->DiscoverServiceAndCharacteristics(serviceUUID, characteristicUUIDs); + XCTAssertTrue(result); +} + +- (void)testDiscoverServiceAndCharacteristicsFailure { + nearby::Uuid serviceUUID = nearby::Uuid([[CBUUID UUIDWithString:@"FEF3"] UUIDString].UTF8String); + nearby::Uuid characteristicUUID = + nearby::Uuid([[CBUUID UUIDWithString:@"B2B4"] UUIDString].UTF8String); + std::vector characteristicUUIDs = {characteristicUUID}; + _fakeGNCBLEGATTClient.errorToReturn = [NSError errorWithDomain:@"test" code:1 userInfo:nil]; + + BOOL result = _gattClient->DiscoverServiceAndCharacteristics(serviceUUID, characteristicUUIDs); + XCTAssertFalse(result); +} + +- (void)testGetCharacteristicSuccess { + nearby::Uuid serviceUUID = nearby::Uuid([[CBUUID UUIDWithString:@"FEF3"] UUIDString].UTF8String); + nearby::Uuid characteristicUUID = + nearby::Uuid([[CBUUID UUIDWithString:@"B2B4"] UUIDString].UTF8String); + GNCBLEGATTCharacteristic *characteristic = + [[GNCBLEGATTCharacteristic alloc] initWithUUID:[CBUUID UUIDWithString:@"B2B4"] + serviceUUID:[CBUUID UUIDWithString:@"FEF3"] + permissions:CBAttributePermissionsReadable + properties:CBCharacteristicPropertyRead]; + _fakeGNCBLEGATTClient.characteristicToReturn = characteristic; + + std::optional result = + _gattClient->GetCharacteristic(serviceUUID, characteristicUUID); + XCTAssertTrue(result.has_value()); +} + +- (void)testGetCharacteristicFailure { + nearby::Uuid serviceUUID = nearby::Uuid([[CBUUID UUIDWithString:@"FEF3"] UUIDString].UTF8String); + nearby::Uuid characteristicUUID = + nearby::Uuid([[CBUUID UUIDWithString:@"B2B4"] UUIDString].UTF8String); + _fakeGNCBLEGATTClient.errorToReturn = [NSError errorWithDomain:@"test" code:1 userInfo:nil]; + + std::optional result = + _gattClient->GetCharacteristic(serviceUUID, characteristicUUID); + XCTAssertFalse(result.has_value()); +} + +- (void)testReadCharacteristicSuccess { + _fakeGNCBLEGATTClient.valueToReturn = [@"test" dataUsingEncoding:NSUTF8StringEncoding]; + GNCBLEGATTCharacteristic *characteristic = + [[GNCBLEGATTCharacteristic alloc] initWithUUID:[CBUUID UUIDWithString:@"B2B4"] + serviceUUID:[CBUUID UUIDWithString:@"FEF3"] + permissions:CBAttributePermissionsReadable + properties:CBCharacteristicPropertyRead]; + nearby::api::ble::GattCharacteristic cppCharacteristic = + nearby::apple::CPPGATTCharacteristicFromObjC(characteristic); + + std::optional result = _gattClient->ReadCharacteristic(cppCharacteristic); + XCTAssertTrue(result.has_value()); + XCTAssertEqual(result.value(), "test"); +} + +- (void)testReadCharacteristicFailure { + _fakeGNCBLEGATTClient.errorToReturn = [NSError errorWithDomain:@"test" code:1 userInfo:nil]; + GNCBLEGATTCharacteristic *characteristic = + [[GNCBLEGATTCharacteristic alloc] initWithUUID:[CBUUID UUIDWithString:@"B2B4"] + serviceUUID:[CBUUID UUIDWithString:@"FEF3"] + permissions:CBAttributePermissionsReadable + properties:CBCharacteristicPropertyRead]; + nearby::api::ble::GattCharacteristic cppCharacteristic = + nearby::apple::CPPGATTCharacteristicFromObjC(characteristic); + + std::optional result = _gattClient->ReadCharacteristic(cppCharacteristic); + XCTAssertFalse(result.has_value()); +} + +- (void)testWriteCharacteristicReturnsFalse { + GNCBLEGATTCharacteristic *characteristic = + [[GNCBLEGATTCharacteristic alloc] initWithUUID:[CBUUID UUIDWithString:@"B2B4"] + serviceUUID:[CBUUID UUIDWithString:@"FEF3"] + permissions:CBAttributePermissionsWriteable + properties:CBCharacteristicPropertyWrite]; + nearby::api::ble::GattCharacteristic cppCharacteristic = + nearby::apple::CPPGATTCharacteristicFromObjC(characteristic); + BOOL result = _gattClient->WriteCharacteristic( + cppCharacteristic, "test", nearby::api::ble::GattClient::WriteType::kWithResponse); + XCTAssertFalse(result); +} + +- (void)testSetCharacteristicSubscriptionReturnsFalse { + GNCBLEGATTCharacteristic *characteristic = + [[GNCBLEGATTCharacteristic alloc] initWithUUID:[CBUUID UUIDWithString:@"B2B4"] + serviceUUID:[CBUUID UUIDWithString:@"FEF3"] + permissions:CBAttributePermissionsReadable + properties:CBCharacteristicPropertyNotify]; + nearby::api::ble::GattCharacteristic cppCharacteristic = + nearby::apple::CPPGATTCharacteristicFromObjC(characteristic); + BOOL result = _gattClient->SetCharacteristicSubscription(cppCharacteristic, true, + [](absl::string_view value) {}); + XCTAssertFalse(result); +} + +- (void)testDisconnectWhenFlagEnabled { + nearby::NearbyFlags::GetInstance().OverrideBoolFlagValue( + nearby::connections::config_package_nearby::nearby_connections_feature:: + kEnableGattClientDisconnection, + YES); + _gattClient->Disconnect(); + XCTAssertTrue(_fakeGNCBLEGATTClient.disconnectCalled); +} + +- (void)testDisconnectWhenFlagDisabled { + nearby::NearbyFlags::GetInstance().OverrideBoolFlagValue( + nearby::connections::config_package_nearby::nearby_connections_feature:: + kEnableGattClientDisconnection, + NO); + _gattClient->Disconnect(); + XCTAssertFalse(_fakeGNCBLEGATTClient.disconnectCalled); +} + +@end