[NC Apple coverage] Refactor: Wrap Network.framework C functions for testability. Part II.

PiperOrigin-RevId: 831136979
This commit is contained in:
Edwin Wu
2025-11-11 17:47:56 -08:00
committed by Copybara-Service
parent 2695dd4089
commit 50de2233f5
15 changed files with 915 additions and 180 deletions
@@ -19,6 +19,12 @@
NS_ASSUME_NONNULL_BEGIN
@interface GNCNWBrowseResultImpl : NSObject <GNCNWBrowseResult>
/**
* Returns the shared singleton instance of this class.
*/
+ (instancetype)sharedInstance;
@end
NS_ASSUME_NONNULL_END
@@ -20,6 +20,15 @@ NS_ASSUME_NONNULL_BEGIN
@implementation GNCNWBrowseResultImpl
+ (instancetype)sharedInstance {
static GNCNWBrowseResultImpl *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
- (void)enumerateInterfaces:(nw_browse_result_t)browseResult
usingBlock:(nw_browse_result_enumerate_interface_t)block {
nw_browse_result_enumerate_interfaces(browseResult, block);
@@ -22,6 +22,40 @@ NS_ASSUME_NONNULL_BEGIN
*/
@protocol GNCNWConnection <NSObject>
/**
* Creates a connection to an endpoint.
*
* @param endpoint The endpoint to connect to.
* @param parameters The parameters to use for the connection.
* @return The new connection object.
*/
- (nw_connection_t)createConnectionWithEndpoint:(nw_endpoint_t)endpoint
parameters:(nw_parameters_t)parameters;
/**
* Sets the dispatch queue for the connection.
*
* @param queue The dispatch queue to use for the connection.
*/
- (void)setQueue:(dispatch_queue_t)queue;
/**
* Sets the state changed handler for the connection.
*
* @param handler The state changed handler to use for the connection.
*/
- (void)setStateChangedHandler:(nw_connection_state_changed_handler_t)handler;
/**
* Starts the connection.
*/
- (void)start;
/**
* Cancels the connection.
*/
- (void)cancel;
@optional
/**
* Initializes the connection object with a Network.framework connection object.
@@ -34,7 +68,8 @@ NS_ASSUME_NONNULL_BEGIN
/**
* Receives a message from the connection.
*
* @param minIncompleteLength The minimum number of bytes to receive before the completion handler is called.
* @param minIncompleteLength The minimum number of bytes to receive before the completion handler
* is called.
* @param maxLengths The maximum number of bytes to receive.
* @param handler The completion handler to call when the receive is complete or an error occurs.
*/
@@ -57,11 +92,6 @@ NS_ASSUME_NONNULL_BEGIN
isComplete:(BOOL)isComplete
completionHandler:(void (^)(nw_error_t _Nullable error))handler;
/**
* Cancels the connection.
*/
- (void)cancel;
/**
* Returns the underlying nw_connection_t object.
*
@@ -25,8 +25,7 @@ NS_ASSUME_NONNULL_BEGIN
*/
@interface GNCNWConnectionImpl : NSObject <GNCNWConnection>
- (instancetype)initWithNWConnection:(nw_connection_t)connection NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithNWConnection:(nw_connection_t)connection;
@end
@@ -31,6 +31,31 @@ NS_ASSUME_NONNULL_BEGIN
return self;
}
- (nw_connection_t)createConnectionWithEndpoint:(nw_endpoint_t)endpoint
parameters:(nw_parameters_t)parameters {
if (_connection) {
nw_connection_cancel(_connection);
}
_connection = nw_connection_create(endpoint, parameters);
return _connection;
}
- (void)setQueue:(dispatch_queue_t)queue {
nw_connection_set_queue(_connection, queue);
}
- (void)setStateChangedHandler:(nw_connection_state_changed_handler_t)handler {
nw_connection_set_state_changed_handler(_connection, handler);
}
- (void)start {
nw_connection_start(_connection);
}
- (void)cancel {
nw_connection_cancel(_connection);
}
- (void)receiveMessageWithMinLength:(uint32_t)minIncompleteLength
maxLength:(uint32_t)maxLength
completionHandler:(void (^)(dispatch_data_t _Nullable content,
@@ -46,10 +71,6 @@ NS_ASSUME_NONNULL_BEGIN
nw_connection_send(_connection, content, context, isComplete, handler);
}
- (void)cancel {
nw_connection_cancel(_connection);
}
- (nullable nw_connection_t)nwConnection {
return _connection;
}
@@ -19,6 +19,11 @@
#import "internal/platform/implementation/apple/Log/GNCLogger.h"
#import "internal/platform/implementation/apple/Mediums/WiFiCommon/GNCIPv4Address.h"
#import "internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWBrowseResult.h"
#import "internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWBrowseResultImpl.h"
#import "internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWBrowser.h"
#import "internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWBrowserImpl.h"
#import "internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWConnection.h"
#import "internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWConnectionImpl.h"
#import "internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWFrameworkError.h"
#import "internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWFrameworkServerSocket+Internal.h"
@@ -36,31 +41,36 @@ NSTimeInterval const GNCStartDiscoveryTimeoutInSeconds = 0.5;
BOOL GNCHasLoopbackInterface(nw_browse_result_t browseResult) {
__block BOOL isLoopback = NO;
nw_browse_result_enumerate_interfaces(browseResult, ^bool(nw_interface_t interface) {
nw_interface_type_t type = nw_interface_get_type(interface);
if (type == nw_interface_type_loopback) {
isLoopback = YES;
return NO;
}
return YES;
});
id<GNCNWBrowseResult> wrapper = [GNCNWBrowseResultImpl sharedInstance];
[wrapper enumerateInterfaces:browseResult
usingBlock:^bool(nw_interface_t interface) {
nw_interface_type_t type = [wrapper interfaceGetType:interface];
if (type == nw_interface_type_loopback) {
isLoopback = YES;
return NO;
}
return YES;
}];
return isLoopback;
}
NSDictionary<NSString *, NSString *> *GNCTXTRecordForBrowseResult(nw_browse_result_t browseResult) {
nw_txt_record_t txt_record = nw_browse_result_copy_txt_record_object(browseResult);
id<GNCNWBrowseResult> wrapper = [GNCNWBrowseResultImpl sharedInstance];
nw_txt_record_t txt_record = [wrapper copyTXTRecordObject:browseResult];
NSMutableDictionary<NSString *, NSString *> *txtRecords =
[[NSMutableDictionary<NSString *, NSString *> alloc] init];
nw_txt_record_apply(txt_record, ^bool(const char *key, const nw_txt_record_find_key_t found,
const uint8_t *value, const size_t value_len) {
if (found == nw_txt_record_find_key_non_empty_value) {
NSString *valueString = [[NSString alloc] initWithBytes:value
length:value_len
encoding:NSUTF8StringEncoding];
[txtRecords setValue:valueString forKey:@(key)];
}
return YES;
});
[wrapper applyTXTRecord:txt_record
block:^bool(const char *key, const nw_txt_record_find_key_t found,
const uint8_t *value, const size_t value_len) {
if (found == nw_txt_record_find_key_non_empty_value) {
NSString *valueString =
[[NSString alloc] initWithBytes:value
length:value_len
encoding:NSUTF8StringEncoding];
[txtRecords setValue:valueString forKey:@(key)];
}
return YES;
}];
return txtRecords;
}
@@ -189,71 +199,90 @@ NSDictionary<NSString *, NSString *> *GNCTXTRecordForBrowseResult(nw_browse_resu
nw_browse_descriptor_t descriptor =
nw_browse_descriptor_create_bonjour_service([serviceType UTF8String], /*domain=*/nil);
nw_browse_descriptor_set_include_txt_record(descriptor, YES);
nw_browser_t browser = nw_browser_create(descriptor, parameters);
nw_browser_set_queue(browser, _dispatchQueue);
id<GNCNWBrowser> browserWrapper = [[GNCNWBrowserImpl alloc] init];
nw_browser_t browser = [browserWrapper createWithDescriptor:descriptor parameters:parameters];
nw_browser_set_browse_results_changed_handler(browser, ^(nw_browse_result_t old_result,
nw_browse_result_t new_result,
bool batch_complete) {
nw_browse_result_change_t change = nw_browse_result_get_changes(old_result, new_result);
switch (change) {
case nw_browse_result_change_identical:
case nw_browse_result_change_interface_added:
case nw_browse_result_change_interface_removed:
break;
case nw_browse_result_change_result_added: {
// If the service has a loopback interface, we probably discovered our own
// advertisement, so we ignore it.
BOOL hasLoopback = GNCHasLoopbackInterface(new_result);
if (hasLoopback) {
GNCLoggerInfo(@"Ignoring a service discovered with loopback interface.");
break;
}
nw_endpoint_t endpoint = nw_browse_result_copy_endpoint(new_result);
NSString *name = @(nw_endpoint_get_bonjour_service_name(endpoint));
NSDictionary<NSString *, NSString *> *txtRecords = GNCTXTRecordForBrowseResult(new_result);
serviceFoundHandler(name, txtRecords);
break;
}
case nw_browse_result_change_txt_record_changed: {
// If the service has a loopback interface, we probably lost our own advertisement, so
// we ignore it.
BOOL oldHasLoopback = GNCHasLoopbackInterface(old_result);
BOOL newHasLoopback = GNCHasLoopbackInterface(new_result);
if (oldHasLoopback || newHasLoopback) {
GNCLoggerInfo(@"Ignoring a service change with loopback interface.");
break;
}
nw_endpoint_t old_endpoint = nw_browse_result_copy_endpoint(old_result);
NSString *oldName = @(nw_endpoint_get_bonjour_service_name(old_endpoint));
NSDictionary<NSString *, NSString *> *oldTXTRecords =
GNCTXTRecordForBrowseResult(old_result);
serviceLostHandler(oldName, oldTXTRecords);
[browserWrapper setQueue:browser queue:_dispatchQueue];
nw_endpoint_t new_endpoint = nw_browse_result_copy_endpoint(new_result);
NSString *newName = @(nw_endpoint_get_bonjour_service_name(new_endpoint));
NSDictionary<NSString *, NSString *> *newTXTRecords =
GNCTXTRecordForBrowseResult(new_result);
serviceFoundHandler(newName, newTXTRecords);
break;
}
case nw_browse_result_change_result_removed: {
// If the service has a loopback interface, we probably lost our own advertisement, so
// we ignore it.
BOOL hasLoopback = GNCHasLoopbackInterface(old_result);
if (hasLoopback) {
GNCLoggerInfo(@"Ignoring a service lost with loopback interface.");
break;
}
nw_endpoint_t endpoint = nw_browse_result_copy_endpoint(old_result);
NSString *name = @(nw_endpoint_get_bonjour_service_name(endpoint));
NSDictionary<NSString *, NSString *> *txtRecords = GNCTXTRecordForBrowseResult(old_result);
serviceLostHandler(name, txtRecords);
break;
}
}
});
[browserWrapper
setBrowseResultsChangedHandler:browser
handler:^(nw_browse_result_t old_result, nw_browse_result_t new_result,
bool batch_complete) {
id<GNCNWBrowseResult> browseResultWrapper =
[GNCNWBrowseResultImpl sharedInstance];
nw_browse_result_change_t change =
[browseResultWrapper getChangesFrom:old_result to:new_result];
switch (change) {
case nw_browse_result_change_identical:
case nw_browse_result_change_interface_added:
case nw_browse_result_change_interface_removed:
break;
case nw_browse_result_change_result_added: {
// If the service has a loopback interface, we probably
// discovered our own advertisement, so we ignore it.
BOOL hasLoopback = GNCHasLoopbackInterface(new_result);
if (hasLoopback) {
GNCLoggerInfo(
@"Ignoring a service discovered with loopback interface.");
break;
}
nw_endpoint_t endpoint =
[browseResultWrapper copyEndpointFromResult:new_result];
NSString *name = [browseResultWrapper
getBonjourServiceNameFromEndpoint:endpoint];
NSDictionary<NSString *, NSString *> *txtRecords =
GNCTXTRecordForBrowseResult(new_result);
serviceFoundHandler(name, txtRecords);
break;
}
case nw_browse_result_change_txt_record_changed: {
// If the service has a loopback interface, we probably lost our
// own advertisement, so we ignore it.
BOOL oldHasLoopback = GNCHasLoopbackInterface(old_result);
BOOL newHasLoopback = GNCHasLoopbackInterface(new_result);
if (oldHasLoopback || newHasLoopback) {
GNCLoggerInfo(
@"Ignoring a service change with loopback interface.");
break;
}
nw_endpoint_t old_endpoint =
[browseResultWrapper copyEndpointFromResult:old_result];
NSString *oldName = [browseResultWrapper
getBonjourServiceNameFromEndpoint:old_endpoint];
NSDictionary<NSString *, NSString *> *oldTXTRecords =
GNCTXTRecordForBrowseResult(old_result);
serviceLostHandler(oldName, oldTXTRecords);
nw_endpoint_t new_endpoint =
[browseResultWrapper copyEndpointFromResult:new_result];
NSString *newName = [browseResultWrapper
getBonjourServiceNameFromEndpoint:new_endpoint];
NSDictionary<NSString *, NSString *> *newTXTRecords =
GNCTXTRecordForBrowseResult(new_result);
serviceFoundHandler(newName, newTXTRecords);
break;
}
case nw_browse_result_change_result_removed: {
// If the service has a loopback interface, we probably lost our
// own advertisement, so we ignore it.
BOOL hasLoopback = GNCHasLoopbackInterface(old_result);
if (hasLoopback) {
GNCLoggerInfo(
@"Ignoring a service lost with loopback interface.");
break;
}
nw_endpoint_t endpoint =
[browseResultWrapper copyEndpointFromResult:old_result];
NSString *name = [browseResultWrapper
getBonjourServiceNameFromEndpoint:endpoint];
NSDictionary<NSString *, NSString *> *txtRecords =
GNCTXTRecordForBrowseResult(old_result);
serviceLostHandler(name, txtRecords);
break;
}
}
}];
NSCondition *condition = [[NSCondition alloc] init];
[condition lock];
@@ -261,18 +290,20 @@ NSDictionary<NSString *, NSString *> *GNCTXTRecordForBrowseResult(nw_browse_resu
__block nw_browser_state_t blockResult = nw_browser_state_invalid;
__block NSError *blockError = nil;
nw_browser_set_state_changed_handler(browser, ^(nw_browser_state_t state, nw_error_t error) {
[condition lock];
blockResult = state;
if (error != nil) {
blockError = (__bridge_transfer NSError *)nw_error_copy_cf_error(error);
}
[condition signal];
[condition unlock];
});
[browserWrapper setStateChangedHandler:browser
handler:^(nw_browser_state_t state, nw_error_t error) {
[condition lock];
blockResult = state;
if (error != nil) {
blockError =
(__bridge_transfer NSError *)nw_error_copy_cf_error(error);
}
[condition signal];
[condition unlock];
}];
[_serviceBrowsers setObject:browser forKey:serviceType];
nw_browser_start(browser);
[browserWrapper start:browser];
BOOL didSignal = [condition
waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:GNCStartDiscoveryTimeoutInSeconds]];
@@ -306,8 +337,11 @@ NSDictionary<NSString *, NSString *> *GNCTXTRecordForBrowseResult(nw_browse_resu
- (void)stopDiscoveryForServiceType:(nonnull NSString *)serviceType {
nw_browser_t browser = [_serviceBrowsers objectForKey:serviceType];
[_serviceBrowsers removeObjectForKey:serviceType];
nw_browser_cancel(browser);
if (browser) {
[_serviceBrowsers removeObjectForKey:serviceType];
id<GNCNWBrowser> browserWrapper = [[GNCNWBrowserImpl alloc] init];
[browserWrapper cancel:browser];
}
}
- (nullable GNCNWFrameworkSocket *)connectToServiceName:(NSString *)serviceName
@@ -391,10 +425,12 @@ NSDictionary<NSString *, NSString *> *GNCTXTRecordForBrowseResult(nw_browse_resu
GNCLoggerError(@"[GNCNWFramework] Failed to create NW parameters.");
return nil;
}
nw_connection_t connection = nw_connection_create(endpoint, parameters);
nw_connection_set_queue(connection, queue ? queue : dispatch_get_main_queue());
nw_connection_set_state_changed_handler(
connection, ^(nw_connection_state_t state, nw_error_t error) {
id<GNCNWConnection> connectionWrapper = [[GNCNWConnectionImpl alloc] init];
[connectionWrapper createConnectionWithEndpoint:endpoint parameters:parameters];
[connectionWrapper setQueue:queue ?: _dispatchQueue];
[connectionWrapper
setStateChangedHandler:^(nw_connection_state_t state, nw_error_t error) {
[condition lock];
// Ignore the preparing state, because it is not a final state.
if (state != nw_connection_state_preparing) {
@@ -405,24 +441,23 @@ NSDictionary<NSString *, NSString *> *GNCTXTRecordForBrowseResult(nw_browse_resu
[condition signal];
}
[condition unlock];
});
}];
if (cancelSource) {
dispatch_source_set_event_handler(cancelSource, ^{
GNCLoggerInfo(
@"[GNCNWFramework] Connection to endpoint was cancelled before it could be established.");
nw_connection_cancel(connection);
[connectionWrapper cancel];
dispatch_source_cancel(cancelSource);
});
}
nw_connection_start(connection);
[connectionWrapper start];
BOOL didSignal =
[condition waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:GNCConnectionTimeoutInSeconds]];
[condition unlock];
// We timed out waiting for the connection to transition into a state.
if (!didSignal) {
nw_connection_cancel(connection);
[connectionWrapper cancel];
if (error != nil) {
*error = [NSError errorWithDomain:GNCNWFrameworkErrorDomain
code:GNCNWFrameworkErrorTimedOut
@@ -443,9 +478,9 @@ NSDictionary<NSString *, NSString *> *GNCTXTRecordForBrowseResult(nw_browse_resu
case nw_connection_state_failed:
case nw_connection_state_cancelled:
return nil;
case nw_connection_state_ready:
return [[GNCNWFrameworkSocket alloc]
initWithConnection:[[GNCNWConnectionImpl alloc] initWithNWConnection:connection]];
case nw_connection_state_ready: {
return [[GNCNWFrameworkSocket alloc] initWithConnection:connectionWrapper];
}
}
}
@@ -137,7 +137,7 @@ static const NSTimeInterval kConnectionWriteTimeout = 5.0; // 5 seconds timeout
}
- (void)close {
[self.connection cancel];
[_connection cancel];
_connection = nil;
}
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#import "internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWBrowseResult.h"
#import "internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWBrowseResultImpl.h"
#import <Foundation/Foundation.h>
#import <Network/Network.h>
@@ -24,7 +24,7 @@ NS_ASSUME_NONNULL_BEGIN
@property(nonatomic) nw_interface_type_t type;
@end
@interface GNCFakeNWBrowseResult : NSObject <GNCNWBrowseResult>
@interface GNCFakeNWBrowseResult : GNCNWBrowseResultImpl
@property(nonatomic, copy) NSArray<GNCFakeNWInterface *> *interfaces;
@property(nonatomic, nullable) NSDictionary<NSString *, NSString *> *txtRecord;
@@ -14,11 +14,11 @@
#import <Network/Network.h>
#import "internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWBrowser.h"
#import "internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWBrowserImpl.h"
NS_ASSUME_NONNULL_BEGIN
@interface GNCFakeNWBrowser : NSObject <GNCNWBrowser>
@interface GNCFakeNWBrowser : GNCNWBrowserImpl
@property(nonatomic, nullable) nw_browser_t createWithDescriptorResult;
@property(nonatomic) BOOL simulateTimeout;
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#import "internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWConnection.h"
#import "internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWConnectionImpl.h"
#import <Foundation/Foundation.h>
#import <Network/Network.h>
@@ -22,7 +22,7 @@ NS_ASSUME_NONNULL_BEGIN
/**
* A fake implementation of GNCNWConnection for testing purposes.
*/
@interface GNCFakeNWConnection : NSObject <GNCNWConnection>
@interface GNCFakeNWConnection : GNCNWConnectionImpl
/** The data to be received by the fake connection. */
@property(nonatomic, nullable) dispatch_data_t dataToReceive;
@@ -30,16 +30,20 @@ NS_ASSUME_NONNULL_BEGIN
/** Whether cancel has been called on the fake connection. */
@property(nonatomic) BOOL cancelCalled;
/** Property to simulate start failure. */
@property(nonatomic) BOOL simulateStartFailure;
/** Property to simulate send failure. */
@property(nonatomic) BOOL simulateSendFailure;
/** Property to simulate receive failure. */
@property(nonatomic) BOOL simulateReceiveFailure;
- (instancetype)init NS_DESIGNATED_INITIALIZER;
/** The state changed handler for the fake connection. */
@property(nonatomic, nullable) nw_connection_state_changed_handler_t stateChangedHandler;
- (instancetype)initWithNWConnection:(nw_connection_t)connection NS_UNAVAILABLE;
- (instancetype)initWithNWConnection:(nw_connection_t)connection;
@end
NS_ASSUME_NONNULL_END
NS_ASSUME_NONNULL_END
@@ -19,10 +19,50 @@
NS_ASSUME_NONNULL_BEGIN
@implementation GNCFakeNWConnection
@implementation GNCFakeNWConnection {
nw_connection_t _fakeNWConnection;
}
- (instancetype)init {
return [super init];
- (instancetype)initWithNWConnection:(nw_connection_t)connection {
self = [super init];
if (self) {
_fakeNWConnection = connection;
}
return self;
}
- (nw_connection_t)createConnectionWithEndpoint:(nw_endpoint_t)endpoint
parameters:(nw_parameters_t)parameters {
// Return a non-nil, unique-ish value to represent the connection.
_fakeNWConnection = (nw_connection_t)[[NSObject alloc] init];
return _fakeNWConnection;
}
- (void)setQueue:(dispatch_queue_t)queue {
// No-op for fake.
}
- (void)setStateChangedHandler:(nullable nw_connection_state_changed_handler_t)handler {
// Capture the handler passed to the method.
_stateChangedHandler = handler;
}
- (void)start {
// Simulate immediate transition to ready or failed.
if (self.stateChangedHandler) {
nw_connection_state_t state =
self.simulateStartFailure ? nw_connection_state_failed : nw_connection_state_ready;
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
self.stateChangedHandler(state, nil);
});
}
}
- (void)cancel {
self.cancelCalled = YES;
if (self.stateChangedHandler) {
self.stateChangedHandler(nw_connection_state_cancelled, nil);
}
}
- (void)receiveMessageWithMinLength:(uint32_t)minIncompleteLength
@@ -53,12 +93,8 @@ NS_ASSUME_NONNULL_BEGIN
});
}
- (void)cancel {
self.cancelCalled = YES;
}
- (nullable nw_connection_t)nwConnection {
return nil; // Fake doesn't wrap a real connection
return _fakeNWConnection;
}
@end
@@ -31,6 +31,12 @@ NS_ASSUME_NONNULL_BEGIN
*/
@property(nonatomic, nullable) NSError *simulatedError;
/** If set, `start` will not call the state changed handler, simulating a timeout. */
@property(nonatomic) BOOL simulateTimeout;
/** State to report from `start` if `simulatedError` is nil. Defaults to `ready`. */
@property(nonatomic) nw_listener_state_t stateForStart;
/** The port number to return from the `port` property. */
@property(nonatomic) uint16_t port;
@@ -64,6 +70,13 @@ NS_ASSUME_NONNULL_BEGIN
/** The descriptor captured from the last call to `setAdvertiseDescriptor:`. */
@property(nonatomic, nullable) nw_advertise_descriptor_t capturedAdvertiseDescriptor;
/**
* Simulates a new connection arriving, calling newConnectionHandler.
*
* @param connection The connection to pass to handler.
*/
- (void)simulateNewConnection:(nw_connection_t)connection;
@end
NS_ASSUME_NONNULL_END
@@ -22,6 +22,14 @@ NS_ASSUME_NONNULL_BEGIN
@synthesize port = _port;
- (instancetype)init {
self = [super init];
if (self) {
_stateForStart = nw_listener_state_ready;
}
return self;
}
- (void)setNewConnectionHandler:(void (^)(nw_connection_t connection))handler {
self.capturedNewConnectionHandler = handler;
}
@@ -40,10 +48,12 @@ NS_ASSUME_NONNULL_BEGIN
- (void)start {
self.startCalled = YES;
if (self.simulateTimeout) {
return;
}
// Trigger the state change handler asynchronously.
if (self.capturedStateChangedHandler) {
nw_listener_state_t state =
self.simulatedError ? nw_listener_state_failed : nw_listener_state_ready;
nw_listener_state_t state = self.simulatedError ? nw_listener_state_failed : self.stateForStart;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// We cannot create a realistic nw_error_t, so pass nil.
self.capturedStateChangedHandler(state, nil);
@@ -53,6 +63,11 @@ NS_ASSUME_NONNULL_BEGIN
- (void)cancel {
self.cancelCalled = YES;
if (self.capturedStateChangedHandler) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
self.capturedStateChangedHandler(nw_listener_state_cancelled, nil);
});
}
}
- (void)setAdvertiseDescriptor:(nullable nw_advertise_descriptor_t)advertiseDescriptor {
@@ -60,6 +75,14 @@ NS_ASSUME_NONNULL_BEGIN
self.capturedAdvertiseDescriptor = advertiseDescriptor;
}
- (void)simulateNewConnection:(nw_connection_t)connection {
if (self.capturedNewConnectionHandler) {
dispatch_async(self.capturedQueue ?: dispatch_get_main_queue(), ^{
self.capturedNewConnectionHandler(connection);
});
}
}
@end
NS_ASSUME_NONNULL_END
@@ -14,19 +14,53 @@
#import "internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWFramework.h"
#import <Network/Network.h>
#import <XCTest/XCTest.h>
#import "third_party/objective_c/ocmock/v3/Source/OCMock/OCMock.h"
#import "internal/platform/implementation/apple/Mediums/WiFiCommon/GNCIPv4Address.h"
#import "internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWBrowseResult.h"
#import "internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWBrowseResultImpl.h"
#import "internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWBrowser.h"
#import "internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWBrowserImpl.h"
#import "internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWConnection.h"
#import "internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWConnectionImpl.h"
#import "internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWFrameworkError.h"
#import "internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWFrameworkServerSocket+Internal.h"
#import "internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWFrameworkSocket.h"
#import "internal/platform/implementation/apple/Mediums/WiFiCommon/Tests/GNCFakeNWBrowseResult.h"
#import "internal/platform/implementation/apple/Mediums/WiFiCommon/Tests/GNCFakeNWBrowser.h"
#import "internal/platform/implementation/apple/Mediums/WiFiCommon/Tests/GNCFakeNWConnection.h"
#import "internal/platform/implementation/apple/Mediums/WiFiCommon/Tests/GNCFakeNWFrameworkServerSocket.h"
#import "internal/platform/implementation/apple/Mediums/WiFiCommon/Tests/GNCFakeNWListener.h"
#import "third_party/objective_c/ocmock/v3/Source/OCMock/OCMock.h"
// Extern C functions for testing.
extern BOOL GNCHasLoopbackInterface(nw_browse_result_t browseResult);
extern NSDictionary<NSString *, NSString *> *GNCTXTRecordForBrowseResult(
nw_browse_result_t browseResult);
static const NSInteger kPort = 1234;
static NSString *const kServiceName = @"TestService";
static NSString *const kServiceType = @"_test._tcp";
static NSString *const kHostAddress = @"127.0.0.1";
@interface GNCNWFrameworkTest : XCTestCase
@end
@implementation GNCNWFrameworkTest
@implementation GNCNWFrameworkTest {
id _mockBrowseResultImpl;
id _mockBrowserImpl;
id _mockConnectionImpl;
}
- (void)setUp {
[super setUp];
// TODO: b/377543997 - Migrate to dependency injection and remove mocks.
_mockBrowseResultImpl = OCMClassMock([GNCNWBrowseResultImpl class]);
_mockBrowserImpl = OCMClassMock([GNCNWBrowserImpl class]);
_mockConnectionImpl = OCMClassMock([GNCNWConnectionImpl class]);
}
- (void)testGNCNWFrameworkCanBeInstantiated {
GNCNWFramework *framework = [[GNCNWFramework alloc] init];
@@ -43,12 +77,12 @@
id mockServerSocket = OCMClassMock([GNCNWFrameworkServerSocket class]);
id mockServerSocketAlloc = OCMClassMock([GNCNWFrameworkServerSocket class]);
OCMStub([mockServerSocketAlloc alloc]).andReturn(mockServerSocketAlloc);
OCMStub([mockServerSocketAlloc initWithPort:1234]).andReturn(mockServerSocket);
OCMStub([mockServerSocketAlloc initWithPort:kPort]).andReturn(mockServerSocket);
OCMStub([mockServerSocket startListeningWithError:[OCMArg anyObjectRef] includePeerToPeer:NO])
.andReturn(YES);
NSError *error = nil;
GNCNWFrameworkServerSocket *serverSocket = [framework listenForServiceOnPort:1234
GNCNWFrameworkServerSocket *serverSocket = [framework listenForServiceOnPort:kPort
includePeerToPeer:NO
error:&error];
XCTAssertNotNil(serverSocket);
@@ -61,7 +95,7 @@
id mockServerSocket = OCMClassMock([GNCNWFrameworkServerSocket class]);
id mockServerSocketAlloc = OCMClassMock([GNCNWFrameworkServerSocket class]);
OCMStub([mockServerSocketAlloc alloc]).andReturn(mockServerSocketAlloc);
OCMStub([mockServerSocketAlloc initWithPort:1234]).andReturn(mockServerSocket);
OCMStub([mockServerSocketAlloc initWithPort:kPort]).andReturn(mockServerSocket);
OCMStub([mockServerSocket startListeningWithError:[OCMArg anyObjectRef] includePeerToPeer:NO])
.andDo(^(id localSelf, NSError **error, BOOL includePeerToPeer) {
if (error) {
@@ -71,7 +105,7 @@
});
NSError *error = nil;
GNCNWFrameworkServerSocket *serverSocket = [framework listenForServiceOnPort:1234
GNCNWFrameworkServerSocket *serverSocket = [framework listenForServiceOnPort:kPort
includePeerToPeer:NO
error:&error];
XCTAssertNil(serverSocket);
@@ -87,7 +121,7 @@
id mockServerSocket = OCMClassMock([GNCNWFrameworkServerSocket class]);
id mockServerSocketAlloc = OCMClassMock([GNCNWFrameworkServerSocket class]);
OCMStub([mockServerSocketAlloc alloc]).andReturn(mockServerSocketAlloc);
OCMStub([mockServerSocketAlloc initWithPort:1234]).andReturn(mockServerSocket);
OCMStub([mockServerSocketAlloc initWithPort:kPort]).andReturn(mockServerSocket);
OCMStub([mockServerSocket startListeningWithPSKIdentity:pskIdentity
PSKSharedSecret:pskSharedSecret
includePeerToPeer:YES
@@ -98,7 +132,7 @@
GNCNWFrameworkServerSocket *serverSocket =
[framework listenForServiceWithPSKIdentity:pskIdentity
PSKSharedSecret:pskSharedSecret
port:1234
port:kPort
includePeerToPeer:YES
error:&error];
XCTAssertNotNil(serverSocket);
@@ -114,7 +148,7 @@
id mockServerSocket = OCMClassMock([GNCNWFrameworkServerSocket class]);
id mockServerSocketAlloc = OCMClassMock([GNCNWFrameworkServerSocket class]);
OCMStub([mockServerSocketAlloc alloc]).andReturn(mockServerSocketAlloc);
OCMStub([mockServerSocketAlloc initWithPort:1234]).andReturn(mockServerSocket);
OCMStub([mockServerSocketAlloc initWithPort:kPort]).andReturn(mockServerSocket);
OCMStub([mockServerSocket startListeningWithPSKIdentity:pskIdentity
PSKSharedSecret:pskSharedSecret
includePeerToPeer:NO
@@ -131,7 +165,7 @@
GNCNWFrameworkServerSocket *serverSocket =
[framework listenForServiceWithPSKIdentity:pskIdentity
PSKSharedSecret:pskSharedSecret
port:1234
port:kPort
includePeerToPeer:NO
error:&error];
XCTAssertNil(serverSocket);
@@ -140,46 +174,42 @@
}
- (void)testStartAdvertisingPort {
NSInteger port = 1234;
NSString *serviceName = @"TestService";
NSString *serviceType = @"_test._tcp.";
NSDictionary<NSString *, NSString *> *txtRecords = @{@"key" : @"value"};
GNCFakeNWFrameworkServerSocket *fakeServerSocket =
[[GNCFakeNWFrameworkServerSocket alloc] initWithPort:port];
[[GNCFakeNWFrameworkServerSocket alloc] initWithPort:kPort];
GNCNWFramework *framework = [[GNCNWFramework alloc] init];
NSMapTable<NSNumber *, GNCNWFrameworkServerSocket *> *serverSockets =
[framework valueForKey:@"_serverSockets"];
[serverSockets setObject:fakeServerSocket forKey:@(port)];
[serverSockets setObject:fakeServerSocket forKey:@(kPort)];
[framework startAdvertisingPort:port
serviceName:serviceName
serviceType:serviceType
[framework startAdvertisingPort:kPort
serviceName:kServiceName
serviceType:kServiceType
txtRecords:txtRecords];
XCTAssertTrue(fakeServerSocket.startAdvertisingCalled);
XCTAssertEqualObjects(fakeServerSocket.startAdvertisingServiceName, serviceName);
XCTAssertEqualObjects(fakeServerSocket.startAdvertisingServiceType, serviceType);
XCTAssertEqualObjects(fakeServerSocket.startAdvertisingServiceName, kServiceName);
XCTAssertEqualObjects(fakeServerSocket.startAdvertisingServiceType, kServiceType);
XCTAssertEqualObjects(fakeServerSocket.startAdvertisingTXTRecords, txtRecords);
}
- (void)testStopAdvertisingPort {
NSInteger port = 1234;
GNCFakeNWFrameworkServerSocket *fakeServerSocket =
[[GNCFakeNWFrameworkServerSocket alloc] initWithPort:port];
[[GNCFakeNWFrameworkServerSocket alloc] initWithPort:kPort];
GNCNWFramework *framework = [[GNCNWFramework alloc] init];
NSMapTable<NSNumber *, GNCNWFrameworkServerSocket *> *serverSockets =
[framework valueForKey:@"_serverSockets"];
[serverSockets setObject:fakeServerSocket forKey:@(port)];
[serverSockets setObject:fakeServerSocket forKey:@(kPort)];
XCTAssertTrue([[[serverSockets keyEnumerator] allObjects] containsObject:@(port)]);
XCTAssertTrue([[[serverSockets keyEnumerator] allObjects] containsObject:@(kPort)]);
[framework stopAdvertisingPort:port];
[framework stopAdvertisingPort:kPort];
XCTAssertTrue(fakeServerSocket.stopAdvertisingCalled);
XCTAssertNil([serverSockets objectForKey:@(port)]);
XCTAssertNil([serverSockets objectForKey:@(kPort)]);
}
- (void)testIsDiscoveringAnyServiceWhenNoServicesAreDiscovering {
@@ -187,4 +217,537 @@
XCTAssertFalse([framework isDiscoveringAnyService]);
}
- (void)testStartDiscoveryForServiceTypeSuccess API_AVAILABLE(ios(13.0)) {
GNCNWFramework *framework = [[GNCNWFramework alloc] init];
GNCFakeNWBrowser *fakeBrowser = [[GNCFakeNWBrowser alloc] init];
fakeBrowser.createWithDescriptorResult = (nw_browser_t)fakeBrowser;
// TODO: b/377543997 - Migrate to dependency injection and remove mocks.
OCMStub([_mockBrowserImpl alloc]).andReturn(fakeBrowser);
XCTestExpectation *serviceFoundExpectation = [self expectationWithDescription:@"Service found"];
__block NSString *foundServiceName = nil;
__block NSDictionary<NSString *, NSString *> *foundTXTRecords = nil;
NSError *error = nil;
BOOL result = [framework startDiscoveryForServiceType:kServiceType
serviceFoundHandler:^(NSString *serviceName,
NSDictionary<NSString *, NSString *> *txtRecords) {
foundServiceName = serviceName;
foundTXTRecords = txtRecords;
[serviceFoundExpectation fulfill];
}
serviceLostHandler:^(NSString *serviceName,
NSDictionary<NSString *, NSString *> *txtRecords) {
}
includePeerToPeer:NO
error:&error];
XCTAssertTrue(result);
XCTAssertNil(error);
XCTAssertTrue([framework isDiscoveringAnyService]);
XCTAssertNotNil(fakeBrowser.stateChangedHandler);
XCTAssertNotNil(fakeBrowser.browseResultsChangedHandler);
// Simulate a service being found.
GNCFakeNWBrowseResult *fakeBrowseResult = [[GNCFakeNWBrowseResult alloc] init];
fakeBrowseResult.txtRecord = @{@"key" : @"value"};
fakeBrowseResult.getChangesFromResult = nw_browse_result_change_result_added;
nw_endpoint_t fakeEndpoint =
nw_endpoint_create_host("localhost", [[NSString stringWithFormat:@"%ld", kPort] UTF8String]);
fakeBrowseResult.endpointFromResultResult = fakeEndpoint;
fakeBrowseResult.getBonjourServiceNameFromEndpointResult = kServiceName;
// TODO: b/377543997 - Migrate to dependency injection and remove mocks.
OCMStub([_mockBrowseResultImpl sharedInstance]).andReturn(fakeBrowseResult);
if (fakeBrowser.browseResultsChangedHandler) {
GNCFakeNWBrowseResult *oldFakeBrowseResult = [[GNCFakeNWBrowseResult alloc] init];
fakeBrowser.browseResultsChangedHandler((nw_browse_result_t)oldFakeBrowseResult,
(nw_browse_result_t)fakeBrowseResult, true);
}
[self waitForExpectationsWithTimeout:1.0 handler:nil];
XCTAssertEqualObjects(foundServiceName, kServiceName);
XCTAssertEqualObjects(foundTXTRecords, @{@"key" : @"value"});
}
- (void)testStartDiscoveryForServiceTypeDuplicate API_AVAILABLE(ios(13.0)) {
GNCNWFramework *framework = [[GNCNWFramework alloc] init];
GNCFakeNWBrowser *fakeBrowser = [[GNCFakeNWBrowser alloc] init];
fakeBrowser.createWithDescriptorResult = (nw_browser_t)fakeBrowser;
// TODO: b/377543997 - Migrate to dependency injection and remove mocks.
OCMStub([_mockBrowserImpl alloc]).andReturn(fakeBrowser);
// Start discovery for kServiceType.
NSError *error = nil;
[framework startDiscoveryForServiceType:kServiceType
serviceFoundHandler:^(NSString *serviceName,
NSDictionary<NSString *, NSString *> *txtRecords) {
}
serviceLostHandler:^(NSString *serviceName,
NSDictionary<NSString *, NSString *> *txtRecords) {
}
includePeerToPeer:NO
error:&error];
XCTAssertNil(error);
// Try to start discovery again for same service type.
BOOL result = [framework startDiscoveryForServiceType:kServiceType
serviceFoundHandler:^(NSString *serviceName,
NSDictionary<NSString *, NSString *> *txtRecords) {
}
serviceLostHandler:^(NSString *serviceName,
NSDictionary<NSString *, NSString *> *txtRecords) {
}
includePeerToPeer:NO
error:&error];
XCTAssertFalse(result);
XCTAssertNotNil(error);
XCTAssertEqual(error.domain, GNCNWFrameworkErrorDomain);
XCTAssertEqual(error.code, GNCNWFrameworkErrorDuplicateDiscovererForServiceType);
}
- (void)testStartDiscoveryForServiceTypeTimeout API_AVAILABLE(ios(13.0)) {
GNCNWFramework *framework = [[GNCNWFramework alloc] init];
GNCFakeNWBrowser *fakeBrowser = [[GNCFakeNWBrowser alloc] init];
fakeBrowser.createWithDescriptorResult = (nw_browser_t)fakeBrowser;
fakeBrowser.simulateTimeout = YES;
// TODO: b/377543997 - Migrate to dependency injection and remove mocks.
OCMStub([_mockBrowserImpl alloc]).andReturn(fakeBrowser);
NSError *error = nil;
BOOL result = [framework startDiscoveryForServiceType:kServiceType
serviceFoundHandler:^(NSString *serviceName,
NSDictionary<NSString *, NSString *> *txtRecords) {
}
serviceLostHandler:^(NSString *serviceName,
NSDictionary<NSString *, NSString *> *txtRecords) {
}
includePeerToPeer:NO
error:&error];
XCTAssertFalse(result);
XCTAssertNotNil(error);
XCTAssertEqual(error.domain, GNCNWFrameworkErrorDomain);
XCTAssertEqual(error.code, GNCNWFrameworkErrorTimedOut);
XCTAssertFalse([framework isDiscoveringAnyService]); // Should be removed on timeout.
}
- (void)testStartDiscoveryForServiceTypeServiceLost API_AVAILABLE(ios(13.0)) {
GNCNWFramework *framework = [[GNCNWFramework alloc] init];
GNCFakeNWBrowser *fakeBrowser = [[GNCFakeNWBrowser alloc] init];
fakeBrowser.createWithDescriptorResult = (nw_browser_t)fakeBrowser;
// TODO: b/377543997 - Migrate to dependency injection and remove mocks.
OCMStub([_mockBrowserImpl alloc]).andReturn(fakeBrowser);
XCTestExpectation *serviceLostExpectation = [self expectationWithDescription:@"Service lost"];
__block NSString *lostServiceName = nil;
NSError *error = nil;
[framework startDiscoveryForServiceType:kServiceType
serviceFoundHandler:^(NSString *serviceName,
NSDictionary<NSString *, NSString *> *txtRecords) {
}
serviceLostHandler:^(NSString *serviceName,
NSDictionary<NSString *, NSString *> *txtRecords) {
lostServiceName = serviceName;
[serviceLostExpectation fulfill];
}
includePeerToPeer:NO
error:&error];
XCTAssertNil(error);
// Simulate a service being lost.
GNCFakeNWBrowseResult *fakeBrowseResult = [[GNCFakeNWBrowseResult alloc] init];
fakeBrowseResult.txtRecord = @{@"key" : @"value"};
fakeBrowseResult.getChangesFromResult = nw_browse_result_change_result_removed;
nw_endpoint_t fakeEndpoint =
nw_endpoint_create_host("localhost", [[NSString stringWithFormat:@"%ld", kPort] UTF8String]);
fakeBrowseResult.endpointFromResultResult = fakeEndpoint;
fakeBrowseResult.getBonjourServiceNameFromEndpointResult = kServiceName;
// TODO: b/377543997 - Migrate to dependency injection and remove mocks.
OCMStub([_mockBrowseResultImpl sharedInstance]).andReturn(fakeBrowseResult);
if (fakeBrowser.browseResultsChangedHandler) {
GNCFakeNWBrowseResult *newFakeBrowseResult = [[GNCFakeNWBrowseResult alloc] init];
fakeBrowser.browseResultsChangedHandler((nw_browse_result_t)fakeBrowseResult,
(nw_browse_result_t)newFakeBrowseResult, true);
}
[self waitForExpectationsWithTimeout:1.0 handler:nil];
XCTAssertEqualObjects(lostServiceName, kServiceName);
}
- (void)testStartDiscoveryForServiceTypeTxtRecordChanged API_AVAILABLE(ios(13.0)) {
GNCNWFramework *framework = [[GNCNWFramework alloc] init];
GNCFakeNWBrowser *fakeBrowser = [[GNCFakeNWBrowser alloc] init];
fakeBrowser.createWithDescriptorResult = (nw_browser_t)fakeBrowser;
// TODO: b/377543997 - Migrate to dependency injection and remove mocks.
OCMStub([_mockBrowserImpl alloc]).andReturn(fakeBrowser);
XCTestExpectation *serviceLostExpectation =
[self expectationWithDescription:@"Service lost for TXT change"];
XCTestExpectation *serviceFoundExpectation =
[self expectationWithDescription:@"Service found for TXT change"];
__block NSString *lostServiceName = nil;
__block NSString *foundServiceName = nil;
__block NSDictionary<NSString *, NSString *> *foundTxtRecords = nil;
NSError *error = nil;
[framework startDiscoveryForServiceType:kServiceType
serviceFoundHandler:^(NSString *serviceName,
NSDictionary<NSString *, NSString *> *txtRecords) {
foundServiceName = serviceName;
foundTxtRecords = txtRecords;
[serviceFoundExpectation fulfill];
}
serviceLostHandler:^(NSString *serviceName,
NSDictionary<NSString *, NSString *> *txtRecords) {
lostServiceName = serviceName;
[serviceLostExpectation fulfill];
}
includePeerToPeer:NO
error:&error];
XCTAssertNil(error);
// Simulate a TXT record change.
GNCFakeNWBrowseResult *oldResult = [[GNCFakeNWBrowseResult alloc] init];
oldResult.txtRecord = @{@"key" : @"value1"};
oldResult.getChangesFromResult = nw_browse_result_change_txt_record_changed;
nw_endpoint_t fakeEndpoint1 =
nw_endpoint_create_host("localhost", [[NSString stringWithFormat:@"%ld", kPort] UTF8String]);
oldResult.endpointFromResultResult = fakeEndpoint1;
oldResult.getBonjourServiceNameFromEndpointResult = kServiceName;
GNCFakeNWBrowseResult *newResult = [[GNCFakeNWBrowseResult alloc] init];
newResult.txtRecord = @{@"key" : @"value2"};
newResult.getChangesFromResult = nw_browse_result_change_txt_record_changed;
nw_endpoint_t fakeEndpoint2 =
nw_endpoint_create_host("localhost", [[NSString stringWithFormat:@"%ld", kPort] UTF8String]);
newResult.endpointFromResultResult = fakeEndpoint2;
newResult.getBonjourServiceNameFromEndpointResult = kServiceName;
// TODO: b/377543997 - Migrate to dependency injection and remove mocks.
OCMStub([_mockBrowseResultImpl sharedInstance]).andReturn(oldResult);
if (fakeBrowser.browseResultsChangedHandler) {
fakeBrowser.browseResultsChangedHandler((nw_browse_result_t)oldResult,
(nw_browse_result_t)newResult, true);
}
[self waitForExpectationsWithTimeout:1.0 handler:nil];
XCTAssertEqualObjects(lostServiceName, kServiceName);
XCTAssertEqualObjects(foundServiceName, kServiceName);
XCTAssertEqualObjects(foundTxtRecords, @{@"key" : @"value2"});
}
- (void)testStartDiscoveryIgnoresLoopbackAdd API_AVAILABLE(ios(13.0)) {
GNCNWFramework *framework = [[GNCNWFramework alloc] init];
GNCFakeNWBrowser *fakeBrowser = [[GNCFakeNWBrowser alloc] init];
fakeBrowser.createWithDescriptorResult = (nw_browser_t)fakeBrowser;
// TODO: b/377543997 - Migrate to dependency injection and remove mocks.
OCMStub([_mockBrowserImpl alloc]).andReturn(fakeBrowser);
__block BOOL serviceFound = NO;
NSError *error = nil;
[framework startDiscoveryForServiceType:kServiceType
serviceFoundHandler:^(NSString *serviceName,
NSDictionary<NSString *, NSString *> *txtRecords) {
serviceFound = YES;
}
serviceLostHandler:^(NSString *serviceName,
NSDictionary<NSString *, NSString *> *txtRecords) {
}
includePeerToPeer:NO
error:&error];
XCTAssertNil(error);
// Simulate a loopback service being found.
GNCFakeNWBrowseResult *fakeBrowseResult = [[GNCFakeNWBrowseResult alloc] init];
fakeBrowseResult.getChangesFromResult = nw_browse_result_change_result_added;
GNCFakeNWInterface *loopbackInterface = [[GNCFakeNWInterface alloc] init];
loopbackInterface.type = nw_interface_type_loopback;
fakeBrowseResult.interfaces = @[ loopbackInterface ];
// TODO: b/377543997 - Migrate to dependency injection and remove mocks.
OCMStub([_mockBrowseResultImpl sharedInstance]).andReturn(fakeBrowseResult);
if (fakeBrowser.browseResultsChangedHandler) {
GNCFakeNWBrowseResult *oldFakeBrowseResult = [[GNCFakeNWBrowseResult alloc] init];
fakeBrowser.browseResultsChangedHandler((nw_browse_result_t)oldFakeBrowseResult,
(nw_browse_result_t)fakeBrowseResult, true);
}
// Allow async blocks to run.
XCTestExpectation *delay = [[XCTestExpectation alloc] initWithDescription:@"delay"];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[delay fulfill];
});
[self waitForExpectations:@[ delay ] timeout:0.5];
XCTAssertFalse(serviceFound);
}
- (void)testStartDiscoveryIgnoresLoopbackRemove API_AVAILABLE(ios(13.0)) {
GNCNWFramework *framework = [[GNCNWFramework alloc] init];
GNCFakeNWBrowser *fakeBrowser = [[GNCFakeNWBrowser alloc] init];
fakeBrowser.createWithDescriptorResult = (nw_browser_t)fakeBrowser;
// TODO: b/377543997 - Migrate to dependency injection and remove mocks.
OCMStub([_mockBrowserImpl alloc]).andReturn(fakeBrowser);
__block BOOL serviceLost = NO;
NSError *error = nil;
[framework startDiscoveryForServiceType:kServiceType
serviceFoundHandler:^(NSString *serviceName,
NSDictionary<NSString *, NSString *> *txtRecords) {
}
serviceLostHandler:^(NSString *serviceName,
NSDictionary<NSString *, NSString *> *txtRecords) {
serviceLost = YES;
}
includePeerToPeer:NO
error:&error];
XCTAssertNil(error);
// Simulate a loopback service being lost.
GNCFakeNWBrowseResult *fakeBrowseResult = [[GNCFakeNWBrowseResult alloc] init];
fakeBrowseResult.getChangesFromResult = nw_browse_result_change_result_removed;
GNCFakeNWInterface *loopbackInterface = [[GNCFakeNWInterface alloc] init];
loopbackInterface.type = nw_interface_type_loopback;
fakeBrowseResult.interfaces = @[ loopbackInterface ];
// TODO: b/377543997 - Migrate to dependency injection and remove mocks.
OCMStub([_mockBrowseResultImpl sharedInstance]).andReturn(fakeBrowseResult);
if (fakeBrowser.browseResultsChangedHandler) {
GNCFakeNWBrowseResult *newFakeBrowseResult = [[GNCFakeNWBrowseResult alloc] init];
fakeBrowser.browseResultsChangedHandler((nw_browse_result_t)fakeBrowseResult,
(nw_browse_result_t)newFakeBrowseResult, true);
}
// Allow async blocks to run.
XCTestExpectation *delay = [[XCTestExpectation alloc] initWithDescription:@"delay"];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[delay fulfill];
});
[self waitForExpectations:@[ delay ] timeout:0.5];
XCTAssertFalse(serviceLost);
}
- (void)testStopDiscoveryForServiceType API_AVAILABLE(ios(13.0)) {
GNCNWFramework *framework = [[GNCNWFramework alloc] init];
GNCFakeNWBrowser *fakeBrowser = [[GNCFakeNWBrowser alloc] init];
fakeBrowser.createWithDescriptorResult = (nw_browser_t)fakeBrowser;
// TODO: b/377543997 - Migrate to dependency injection and remove mocks.
OCMStub([_mockBrowserImpl alloc]).andReturn(fakeBrowser);
NSError *error = nil;
BOOL result = [framework startDiscoveryForServiceType:kServiceType
serviceFoundHandler:^(NSString *serviceName,
NSDictionary<NSString *, NSString *> *txtRecords) {
}
serviceLostHandler:^(NSString *serviceName,
NSDictionary<NSString *, NSString *> *txtRecords) {
}
includePeerToPeer:NO
error:&error];
XCTAssertTrue(result);
XCTAssertNil(error);
XCTAssertTrue([framework isDiscoveringAnyService]);
nw_browser_t createdBrowser = fakeBrowser.createWithDescriptorResult;
XCTAssertNotNil(createdBrowser);
[framework stopDiscoveryForServiceType:kServiceType];
XCTAssertFalse([framework isDiscoveringAnyService]);
XCTAssertTrue(fakeBrowser.cancelCalled);
XCTAssertEqual(fakeBrowser.cancelledBrowser, createdBrowser);
}
- (void)testGNCHasLoopbackInterface_WithLoopback API_AVAILABLE(ios(13.0)) {
GNCFakeNWBrowseResult *fakeBrowseResult = [[GNCFakeNWBrowseResult alloc] init];
GNCFakeNWInterface *wifiInterface = [[GNCFakeNWInterface alloc] init];
wifiInterface.type = nw_interface_type_wifi;
GNCFakeNWInterface *loopbackInterface = [[GNCFakeNWInterface alloc] init];
loopbackInterface.type = nw_interface_type_loopback;
fakeBrowseResult.interfaces = @[ wifiInterface, loopbackInterface ];
// TODO: b/377543997 - Migrate to dependency injection and remove mocks.
OCMStub([_mockBrowseResultImpl sharedInstance]).andReturn(fakeBrowseResult);
XCTAssertTrue(GNCHasLoopbackInterface((nw_browse_result_t)fakeBrowseResult));
}
- (void)testGNCHasLoopbackInterface_WithoutLoopback API_AVAILABLE(ios(13.0)) {
GNCFakeNWBrowseResult *fakeBrowseResult = [[GNCFakeNWBrowseResult alloc] init];
GNCFakeNWInterface *wifiInterface = [[GNCFakeNWInterface alloc] init];
wifiInterface.type = nw_interface_type_wifi;
GNCFakeNWInterface *otherInterface = [[GNCFakeNWInterface alloc] init];
otherInterface.type = nw_interface_type_other;
fakeBrowseResult.interfaces = @[ wifiInterface, otherInterface ];
// TODO: b/377543997 - Migrate to dependency injection and remove mocks.
OCMStub([_mockBrowseResultImpl sharedInstance]).andReturn(fakeBrowseResult);
XCTAssertFalse(GNCHasLoopbackInterface((nw_browse_result_t)fakeBrowseResult));
}
- (void)testGNCHasLoopbackInterface_NoInterfaces API_AVAILABLE(ios(13.0)) {
GNCFakeNWBrowseResult *fakeBrowseResult = [[GNCFakeNWBrowseResult alloc] init];
fakeBrowseResult.interfaces = @[];
// TODO: b/377543997 - Migrate to dependency injection and remove mocks.
OCMStub([_mockBrowseResultImpl sharedInstance]).andReturn(fakeBrowseResult);
XCTAssertFalse(GNCHasLoopbackInterface((nw_browse_result_t)fakeBrowseResult));
}
- (void)testGNCTXTRecordForBrowseResult API_AVAILABLE(ios(13.0)) {
GNCFakeNWBrowseResult *fakeBrowseResult = [[GNCFakeNWBrowseResult alloc] init];
NSDictionary<NSString *, NSString *> *txtRecord = @{@"key1" : @"value1", @"key2" : @"value2"};
fakeBrowseResult.txtRecord = txtRecord;
// TODO: b/377543997 - Migrate to dependency injection and remove mocks.
OCMStub([_mockBrowseResultImpl sharedInstance]).andReturn(fakeBrowseResult);
NSDictionary<NSString *, NSString *> *result =
GNCTXTRecordForBrowseResult((nw_browse_result_t)fakeBrowseResult);
XCTAssertEqualObjects(result, txtRecord);
}
- (void)testGNCTXTRecordForBrowseResult_Empty API_AVAILABLE(ios(13.0)) {
GNCFakeNWBrowseResult *fakeBrowseResult = [[GNCFakeNWBrowseResult alloc] init];
fakeBrowseResult.txtRecord = @{};
// TODO: b/377543997 - Migrate to dependency injection and remove mocks.
OCMStub([_mockBrowseResultImpl sharedInstance]).andReturn(fakeBrowseResult);
NSDictionary<NSString *, NSString *> *result =
GNCTXTRecordForBrowseResult((nw_browse_result_t)fakeBrowseResult);
XCTAssertEqualObjects(result, @{});
}
- (void)testGNCTXTRecordForBrowseResult_Nil API_AVAILABLE(ios(13.0)) {
GNCFakeNWBrowseResult *fakeBrowseResult = [[GNCFakeNWBrowseResult alloc] init];
// TODO: b/377543997 - Migrate to dependency injection and remove mocks.
OCMStub([_mockBrowseResultImpl sharedInstance]).andReturn(fakeBrowseResult);
NSDictionary<NSString *, NSString *> *result =
GNCTXTRecordForBrowseResult((nw_browse_result_t)fakeBrowseResult);
XCTAssertEqualObjects(result, @{});
}
- (void)testConnectToServiceNameSuccess API_AVAILABLE(ios(13.0)) {
GNCNWFramework *framework = [[GNCNWFramework alloc] init];
GNCFakeNWConnection *fakeConnection = [[GNCFakeNWConnection alloc] init];
// TODO: b/377543997 - Migrate to dependency injection and remove mocks.
OCMStub([_mockConnectionImpl alloc]).andReturn(fakeConnection);
NSError *error = nil;
GNCNWFrameworkSocket *socket = [framework connectToServiceName:kServiceName
serviceType:kServiceType
error:&error];
XCTAssertNotNil(socket);
XCTAssertNil(error);
}
- (void)testConnectToServiceNameFailure API_AVAILABLE(ios(13.0)) {
GNCNWFramework *framework = [[GNCNWFramework alloc] init];
GNCFakeNWConnection *fakeConnection = [[GNCFakeNWConnection alloc] init];
fakeConnection.simulateStartFailure = YES;
// TODO: b/377543997 - Migrate to dependency injection and remove mocks.
OCMStub([_mockConnectionImpl alloc]).andReturn(fakeConnection);
NSError *error = nil;
GNCNWFrameworkSocket *socket = [framework connectToServiceName:kServiceName
serviceType:kServiceType
error:&error];
XCTAssertNil(socket);
XCTAssertNil(error); // As established, framework doesn't set error on non-timeout failure
}
- (void)testConnectToServiceNameWithPSKSuccess API_AVAILABLE(ios(13.0)) {
GNCNWFramework *framework = [[GNCNWFramework alloc] init];
GNCFakeNWConnection *fakeConnection = [[GNCFakeNWConnection alloc] init];
// TODO: b/377543997 - Migrate to dependency injection and remove mocks.
OCMStub([_mockConnectionImpl alloc]).andReturn(fakeConnection);
NSData *pskIdentity = [@"testIdentity" dataUsingEncoding:NSUTF8StringEncoding];
NSData *pskSharedSecret = [@"testSharedSecret" dataUsingEncoding:NSUTF8StringEncoding];
NSError *error = nil;
GNCNWFrameworkSocket *socket = [framework connectToServiceName:kServiceName
serviceType:kServiceType
PSKIdentity:pskIdentity
PSKSharedSecret:pskSharedSecret
error:&error];
XCTAssertNotNil(socket);
XCTAssertNil(error);
}
- (void)testConnectToServiceNameWithPSKFailure API_AVAILABLE(ios(13.0)) {
GNCNWFramework *framework = [[GNCNWFramework alloc] init];
GNCFakeNWConnection *fakeConnection = [[GNCFakeNWConnection alloc] init];
fakeConnection.simulateStartFailure = YES;
// TODO: b/377543997 - Migrate to dependency injection and remove mocks.
OCMStub([_mockConnectionImpl alloc]).andReturn(fakeConnection);
NSData *pskIdentity = [@"testIdentity" dataUsingEncoding:NSUTF8StringEncoding];
NSData *pskSharedSecret = [@"testSharedSecret" dataUsingEncoding:NSUTF8StringEncoding];
NSError *error = nil;
GNCNWFrameworkSocket *socket = [framework connectToServiceName:kServiceName
serviceType:kServiceType
PSKIdentity:pskIdentity
PSKSharedSecret:pskSharedSecret
error:&error];
XCTAssertNil(socket);
XCTAssertNil(error);
}
- (void)testConnectToHostSuccess API_AVAILABLE(ios(13.0)) {
GNCNWFramework *framework = [[GNCNWFramework alloc] init];
GNCFakeNWConnection *fakeConnection = [[GNCFakeNWConnection alloc] init];
// TODO: b/377543997 - Migrate to dependency injection and remove mocks.
OCMStub([_mockConnectionImpl alloc]).andReturn(fakeConnection);
GNCIPv4Address *address = [GNCIPv4Address addressWithDottedRepresentation:kHostAddress];
NSError *error = nil;
GNCNWFrameworkSocket *socket = [framework connectToHost:address
port:kPort
includePeerToPeer:NO
cancelSource:nil
queue:nil
error:&error];
XCTAssertNotNil(socket);
XCTAssertNil(error);
}
- (void)testConnectToHostFailure API_AVAILABLE(ios(13.0)) {
GNCNWFramework *framework = [[GNCNWFramework alloc] init];
GNCFakeNWConnection *fakeConnection = [[GNCFakeNWConnection alloc] init];
fakeConnection.simulateStartFailure = YES;
// TODO: b/377543997 - Migrate to dependency injection and remove mocks.
OCMStub([_mockConnectionImpl alloc]).andReturn(fakeConnection);
GNCIPv4Address *address = [GNCIPv4Address addressWithDottedRepresentation:kHostAddress];
NSError *error = nil;
GNCNWFrameworkSocket *socket = [framework connectToHost:address
port:kPort
includePeerToPeer:NO
cancelSource:nil
queue:nil
error:&error];
XCTAssertNil(socket);
XCTAssertNil(error);
}
@end
@@ -17,28 +17,24 @@
#import <Network/Network.h>
#import <XCTest/XCTest.h>
#import "internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWParameters.h"
@interface GNCNWListenerImplTest : XCTestCase
@end
@implementation GNCNWListenerImplTest {
GNCNWListenerImpl *_listenerImpl;
@implementation GNCNWListenerImplTest
- (void)testInitWithParameters {
nw_parameters_t parameters = nw_parameters_create_secure_tcp(NW_PARAMETERS_DEFAULT_CONFIGURATION,
NW_PARAMETERS_DEFAULT_CONFIGURATION);
GNCNWListenerImpl *listener = [[GNCNWListenerImpl alloc] initWithParameters:parameters];
XCTAssertNotNil(listener);
}
- (void)setUp {
[super setUp];
if (@available(iOS 13.0, *)) {
// Note: GNCBuildNonTLSParameters is not available in tests, so we can't fully initialize.
// This test is limited to checking basic allocation.
}
- (void)testInitWithPortAndParameters {
nw_parameters_t parameters = nw_parameters_create_secure_tcp(NW_PARAMETERS_DEFAULT_CONFIGURATION,
NW_PARAMETERS_DEFAULT_CONFIGURATION);
GNCNWListenerImpl *listener = [[GNCNWListenerImpl alloc] initWithPort:"1234"
parameters:parameters];
XCTAssertNotNil(listener);
}
// No tests here. The functionality is tested through GNCNWFrameworkServerSocketTest.
// NOTE: Testing the interaction with the Network.framework C functions
// is difficult without a way to mock or intercept C function calls.
// The main test coverage for the listener logic is achieved through
// GNCFakeNWListener in GNCNWFrameworkServerSocketTest.m.
@end