From cee0b3815e205229120559823ae3bee7bd03cffa Mon Sep 17 00:00:00 2001 From: Edwin Wu Date: Tue, 4 Nov 2025 23:05:47 -0800 Subject: [PATCH] [NC Apple coverage] Refactor: Wrap Network.framework C functions for testability. Part I. PiperOrigin-RevId: 828308553 --- .../apple/Mediums/WiFiCommon/BUILD | 6 ++ .../Mediums/WiFiCommon/GNCNWBrowseResult.h | 87 +++++++++++++++++++ .../WiFiCommon/GNCNWBrowseResultImpl.h | 24 +++++ .../WiFiCommon/GNCNWBrowseResultImpl.m | 56 ++++++++++++ .../apple/Mediums/WiFiCommon/GNCNWBrowser.h | 77 ++++++++++++++++ .../Mediums/WiFiCommon/GNCNWBrowserImpl.h | 24 +++++ .../Mediums/WiFiCommon/GNCNWBrowserImpl.m | 52 +++++++++++ .../apple/Mediums/WiFiCommon/Tests/BUILD | 6 ++ .../WiFiCommon/Tests/GNCFakeNWBrowseResult.h | 37 ++++++++ .../WiFiCommon/Tests/GNCFakeNWBrowseResult.m | 75 ++++++++++++++++ .../WiFiCommon/Tests/GNCFakeNWBrowser.h | 35 ++++++++ .../WiFiCommon/Tests/GNCFakeNWBrowser.m | 72 +++++++++++++++ .../Tests/GNCNWBrowseResultImplTest.m | 70 +++++++++++++++ .../WiFiCommon/Tests/GNCNWBrowserImplTest.m | 36 ++++++++ 14 files changed, 657 insertions(+) create mode 100644 internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWBrowseResult.h create mode 100644 internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWBrowseResultImpl.h create mode 100644 internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWBrowseResultImpl.m create mode 100644 internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWBrowser.h create mode 100644 internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWBrowserImpl.h create mode 100644 internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWBrowserImpl.m create mode 100644 internal/platform/implementation/apple/Mediums/WiFiCommon/Tests/GNCFakeNWBrowseResult.h create mode 100644 internal/platform/implementation/apple/Mediums/WiFiCommon/Tests/GNCFakeNWBrowseResult.m create mode 100644 internal/platform/implementation/apple/Mediums/WiFiCommon/Tests/GNCFakeNWBrowser.h create mode 100644 internal/platform/implementation/apple/Mediums/WiFiCommon/Tests/GNCFakeNWBrowser.m create mode 100644 internal/platform/implementation/apple/Mediums/WiFiCommon/Tests/GNCNWBrowseResultImplTest.m create mode 100644 internal/platform/implementation/apple/Mediums/WiFiCommon/Tests/GNCNWBrowserImplTest.m diff --git a/internal/platform/implementation/apple/Mediums/WiFiCommon/BUILD b/internal/platform/implementation/apple/Mediums/WiFiCommon/BUILD index 7959f2aa..43313086 100644 --- a/internal/platform/implementation/apple/Mediums/WiFiCommon/BUILD +++ b/internal/platform/implementation/apple/Mediums/WiFiCommon/BUILD @@ -26,6 +26,8 @@ objc_library( name = "WiFiCommon", srcs = [ "GNCIPv4Address.m", + "GNCNWBrowseResultImpl.m", + "GNCNWBrowserImpl.m", "GNCNWConnectionImpl.m", "GNCNWFramework.m", "GNCNWFrameworkError.m", @@ -36,6 +38,10 @@ objc_library( ], hdrs = [ "GNCIPv4Address.h", + "GNCNWBrowseResult.h", + "GNCNWBrowseResultImpl.h", + "GNCNWBrowser.h", + "GNCNWBrowserImpl.h", "GNCNWConnection.h", "GNCNWConnectionImpl.h", "GNCNWFramework.h", diff --git a/internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWBrowseResult.h b/internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWBrowseResult.h new file mode 100644 index 00000000..298db50b --- /dev/null +++ b/internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWBrowseResult.h @@ -0,0 +1,87 @@ +// 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 + +NS_ASSUME_NONNULL_BEGIN + +/** + * A protocol to wrap Network.framework C functions to allow for faking in tests. + */ +@protocol GNCNWBrowseResult + +/** + * Enumerates the interfaces associated with a browse result. + * + * @param browseResult The browse result to enumerate. + * @param block The block to apply to each interface. + */ +- (void)enumerateInterfaces:(nw_browse_result_t)browseResult + usingBlock:(nw_browse_result_enumerate_interface_t)block; + +/** + * Returns the type of an interface, such as Wi-Fi or cellular. + * + * @param interface The interface to get the type of. + * @return The type of the interface. + */ +- (nw_interface_type_t)interfaceGetType:(nw_interface_t)interface; + +/** + * Copies the TXT record associated with a browse result. + * + * @param browseResult The browse result to copy the TXT record from. + * @return The TXT record object, or nil if one doesn't exist. + */ +- (nullable nw_txt_record_t)copyTXTRecordObject:(nw_browse_result_t)browseResult; + +/** + * Allows you to inspect the keys and values in a TXT record. + * + * @param txtRecord The TXT record to apply the block to. + * @param block The block to apply to each key-value pair in the TXT record. + */ +- (void)applyTXTRecord:(nw_txt_record_t)txtRecord block:(nw_txt_record_applier_t)block; + +/** + * Compares two browse results to determine if they refer to the same endpoint and if any specific + * attributes of the endpoint have changed. + * + * @param oldResult The older browse result. + * @param newResult The newer browse result. + * @return The changes between the two browse results. + */ +- (nw_browse_result_change_t)getChangesFrom:(nw_browse_result_t)oldResult + to:(nw_browse_result_t)newResult; + +/** + * Gets the endpoint associated with a browse result. + * + * @param result The browse result to get the endpoint from. + * @return The endpoint associated with the browse result, or nil if one doesn't exist. + */ +- (nullable nw_endpoint_t)copyEndpointFromResult:(nw_browse_result_t)result; + +/** + * Returns the service name if the endpoint is a Bonjour service endpoint. + * + * @param endpoint The endpoint to get the Bonjour service name from. + * @return The Bonjour service name, or nil if the endpoint is not a Bonjour service endpoint. + */ +- (nullable NSString *)getBonjourServiceNameFromEndpoint:(nw_endpoint_t)endpoint; + +@end + +NS_ASSUME_NONNULL_END diff --git a/internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWBrowseResultImpl.h b/internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWBrowseResultImpl.h new file mode 100644 index 00000000..e42b614b --- /dev/null +++ b/internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWBrowseResultImpl.h @@ -0,0 +1,24 @@ +// 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/WiFiCommon/GNCNWBrowseResult.h" + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface GNCNWBrowseResultImpl : NSObject +@end + +NS_ASSUME_NONNULL_END diff --git a/internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWBrowseResultImpl.m b/internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWBrowseResultImpl.m new file mode 100644 index 00000000..3cb547c3 --- /dev/null +++ b/internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWBrowseResultImpl.m @@ -0,0 +1,56 @@ +// 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/WiFiCommon/GNCNWBrowseResultImpl.h" + +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation GNCNWBrowseResultImpl + +- (void)enumerateInterfaces:(nw_browse_result_t)browseResult + usingBlock:(nw_browse_result_enumerate_interface_t)block { + nw_browse_result_enumerate_interfaces(browseResult, block); +} + +- (nw_interface_type_t)interfaceGetType:(nw_interface_t)interface { + return nw_interface_get_type(interface); +} + +- (nullable nw_txt_record_t)copyTXTRecordObject:(nw_browse_result_t)browseResult { + return nw_browse_result_copy_txt_record_object(browseResult); +} + +- (void)applyTXTRecord:(nw_txt_record_t)txtRecord block:(nw_txt_record_applier_t)block { + nw_txt_record_apply(txtRecord, block); +} + +- (nw_browse_result_change_t)getChangesFrom:(nw_browse_result_t)oldResult + to:(nw_browse_result_t)newResult { + return nw_browse_result_get_changes(oldResult, newResult); +} + +- (nullable nw_endpoint_t)copyEndpointFromResult:(nw_browse_result_t)result { + return nw_browse_result_copy_endpoint(result); +} + +- (nullable NSString *)getBonjourServiceNameFromEndpoint:(nw_endpoint_t)endpoint { + const char *name = nw_endpoint_get_bonjour_service_name(endpoint); + return name ? @(name) : nil; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWBrowser.h b/internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWBrowser.h new file mode 100644 index 00000000..4fb63f8c --- /dev/null +++ b/internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWBrowser.h @@ -0,0 +1,77 @@ +// 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 + +NS_ASSUME_NONNULL_BEGIN + +/** + * A protocol that wraps nw_browser_t functions for testability. + */ +@protocol GNCNWBrowser + +/** + * Creates a new browser object with a browse descriptor and parameters. + * + * @param descriptor The descriptor that defines the service to browse for. + * @param parameters The parameters to use for browsing + * @return An initialized browser object. + */ +- (nw_browser_t)createWithDescriptor:(nw_browse_descriptor_t)descriptor + parameters:(nw_parameters_t)parameters; + +/** + * Sets the dispatch queue on which to deliver browser events. + * + * @param browser The browser object to modify. + * @param queue The dispatch queue to use for browser events. + */ +- (void)setQueue:(nw_browser_t)browser queue:(dispatch_queue_t)queue; + +/** + * Sets a handler that is called when the set of discovered browse results changes. + * + * @param browser The browser object to modify. + * @param handler The handler to call when browse results change. + */ +- (void)setBrowseResultsChangedHandler:(nw_browser_t)browser + handler:(nw_browser_browse_results_changed_handler_t)handler; + +/** + * Sets a handler that is called when the browser state changes. + * + * @param browser The browser object to modify. + * @param handler The handler to call when the browser state changes. + */ +- (void)setStateChangedHandler:(nw_browser_t)browser + handler:(nw_browser_state_changed_handler_t)handler; + +/** + * Starts the browser, which will begin browsing for services. + * + * @param browser The browser object to start. + */ +- (void)start:(nw_browser_t)browser; + +/** + * Cancels the browser, which will stop browsing for services and invalidate the object. + * + * @param browser The browser object to cancel. + */ +- (void)cancel:(nw_browser_t)browser; + +@end + +NS_ASSUME_NONNULL_END diff --git a/internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWBrowserImpl.h b/internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWBrowserImpl.h new file mode 100644 index 00000000..e6f9abaa --- /dev/null +++ b/internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWBrowserImpl.h @@ -0,0 +1,24 @@ +// 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/WiFiCommon/GNCNWBrowser.h" + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface GNCNWBrowserImpl : NSObject +@end + +NS_ASSUME_NONNULL_END diff --git a/internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWBrowserImpl.m b/internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWBrowserImpl.m new file mode 100644 index 00000000..0692ef0d --- /dev/null +++ b/internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWBrowserImpl.m @@ -0,0 +1,52 @@ +// 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/WiFiCommon/GNCNWBrowserImpl.h" + +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation GNCNWBrowserImpl + +- (nw_browser_t)createWithDescriptor:(nw_browse_descriptor_t)descriptor + parameters:(nw_parameters_t)parameters { + return nw_browser_create(descriptor, parameters); +} + +- (void)setQueue:(nw_browser_t)browser queue:(dispatch_queue_t)queue { + nw_browser_set_queue(browser, queue); +} + +- (void)setBrowseResultsChangedHandler:(nw_browser_t)browser + handler:(nw_browser_browse_results_changed_handler_t)handler { + nw_browser_set_browse_results_changed_handler(browser, handler); +} + +- (void)setStateChangedHandler:(nw_browser_t)browser + handler:(nw_browser_state_changed_handler_t)handler { + nw_browser_set_state_changed_handler(browser, handler); +} + +- (void)start:(nw_browser_t)browser { + nw_browser_start(browser); +} + +- (void)cancel:(nw_browser_t)browser { + nw_browser_cancel(browser); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/internal/platform/implementation/apple/Mediums/WiFiCommon/Tests/BUILD b/internal/platform/implementation/apple/Mediums/WiFiCommon/Tests/BUILD index b7c4e6e4..57afaee9 100644 --- a/internal/platform/implementation/apple/Mediums/WiFiCommon/Tests/BUILD +++ b/internal/platform/implementation/apple/Mediums/WiFiCommon/Tests/BUILD @@ -24,6 +24,8 @@ objc_library( name = "FakeNWFramework", testonly = True, srcs = [ + "GNCFakeNWBrowseResult.m", + "GNCFakeNWBrowser.m", "GNCFakeNWConnection.m", "GNCFakeNWFramework.m", "GNCFakeNWFrameworkServerSocket.m", @@ -31,6 +33,8 @@ objc_library( "GNCFakeNWListener.m", ], hdrs = [ + "GNCFakeNWBrowseResult.h", + "GNCFakeNWBrowser.h", "GNCFakeNWConnection.h", "GNCFakeNWFramework.h", "GNCFakeNWFrameworkServerSocket.h", @@ -49,6 +53,8 @@ objc_library( testonly = True, srcs = [ "GNCIPAddressTest.mm", + "GNCNWBrowseResultImplTest.m", + "GNCNWBrowserImplTest.m", "GNCNWConnectionImplTest.m", "GNCNWFrameworkServerSocketTest.m", "GNCNWFrameworkSocketTest.m", diff --git a/internal/platform/implementation/apple/Mediums/WiFiCommon/Tests/GNCFakeNWBrowseResult.h b/internal/platform/implementation/apple/Mediums/WiFiCommon/Tests/GNCFakeNWBrowseResult.h new file mode 100644 index 00000000..b8d10913 --- /dev/null +++ b/internal/platform/implementation/apple/Mediums/WiFiCommon/Tests/GNCFakeNWBrowseResult.h @@ -0,0 +1,37 @@ +// 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/WiFiCommon/GNCNWBrowseResult.h" + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +// Fake interface for testing +@interface GNCFakeNWInterface : NSObject +@property(nonatomic) nw_interface_type_t type; +@end + +@interface GNCFakeNWBrowseResult : NSObject +@property(nonatomic, copy) NSArray *interfaces; +@property(nonatomic, nullable) NSDictionary *txtRecord; + +@property(nonatomic) nw_browse_result_change_t getChangesFromResult; +@property(nonatomic, nullable) nw_endpoint_t endpointFromResultResult; +@property(nonatomic, nullable) NSString *getBonjourServiceNameFromEndpointResult; + +@end + +NS_ASSUME_NONNULL_END diff --git a/internal/platform/implementation/apple/Mediums/WiFiCommon/Tests/GNCFakeNWBrowseResult.m b/internal/platform/implementation/apple/Mediums/WiFiCommon/Tests/GNCFakeNWBrowseResult.m new file mode 100644 index 00000000..2355b6c2 --- /dev/null +++ b/internal/platform/implementation/apple/Mediums/WiFiCommon/Tests/GNCFakeNWBrowseResult.m @@ -0,0 +1,75 @@ +// 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/WiFiCommon/Tests/GNCFakeNWBrowseResult.h" + +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation GNCFakeNWInterface +@end + +@implementation GNCFakeNWBrowseResult + +- (void)enumerateInterfaces:(nw_browse_result_t)browseResult + usingBlock:(nw_browse_result_enumerate_interface_t)block { + for (GNCFakeNWInterface *interface in self.interfaces) { + if (!block((nw_interface_t)interface)) { + break; + } + } +} + +- (nw_interface_type_t)interfaceGetType:(nw_interface_t)interface { + return ((GNCFakeNWInterface *)interface).type; +} + +- (nullable nw_txt_record_t)copyTXTRecordObject:(nw_browse_result_t)browseResult { + return (nw_txt_record_t)browseResult; +} + +- (void)applyTXTRecord:(nw_txt_record_t)txtRecord block:(nw_txt_record_applier_t)block { + GNCFakeNWBrowseResult *result = (GNCFakeNWBrowseResult *)txtRecord; + if (!result.txtRecord) { + return; + } + [result.txtRecord enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *value, + BOOL *stop) { + const char *keyUTF8 = key.UTF8String; + const uint8_t *valueUTF8 = (const uint8_t *)value.UTF8String; + size_t valueLen = value.length; + block(keyUTF8, nw_txt_record_find_key_non_empty_value, valueUTF8, valueLen); + }]; +} + +- (nw_browse_result_change_t)getChangesFrom:(nw_browse_result_t)oldResult + to:(nw_browse_result_t)newResult { + return self.getChangesFromResult; +} + +- (nullable nw_endpoint_t)copyEndpointFromResult:(nw_browse_result_t)result { + if (self.endpointFromResultResult) { + return self.endpointFromResultResult; + } + return nw_endpoint_create_host("fakehost", "1234"); +} + +- (nullable NSString *)getBonjourServiceNameFromEndpoint:(nw_endpoint_t)endpoint { + return self.getBonjourServiceNameFromEndpointResult ?: @"FakeService"; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/internal/platform/implementation/apple/Mediums/WiFiCommon/Tests/GNCFakeNWBrowser.h b/internal/platform/implementation/apple/Mediums/WiFiCommon/Tests/GNCFakeNWBrowser.h new file mode 100644 index 00000000..6a44cb43 --- /dev/null +++ b/internal/platform/implementation/apple/Mediums/WiFiCommon/Tests/GNCFakeNWBrowser.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 + +#import "internal/platform/implementation/apple/Mediums/WiFiCommon/GNCNWBrowser.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface GNCFakeNWBrowser : NSObject + +@property(nonatomic, nullable) nw_browser_t createWithDescriptorResult; +@property(nonatomic) BOOL simulateTimeout; +@property(nonatomic) nw_browser_state_t stateForStart; +@property(nonatomic, nullable) dispatch_queue_t setQueueQueue; +@property(nonatomic, nullable) + nw_browser_browse_results_changed_handler_t browseResultsChangedHandler; +@property(nonatomic, nullable) nw_browser_state_changed_handler_t stateChangedHandler; +@property(nonatomic, readonly) BOOL cancelCalled; +@property(nonatomic, nullable, readonly) nw_browser_t cancelledBrowser; + +@end + +NS_ASSUME_NONNULL_END diff --git a/internal/platform/implementation/apple/Mediums/WiFiCommon/Tests/GNCFakeNWBrowser.m b/internal/platform/implementation/apple/Mediums/WiFiCommon/Tests/GNCFakeNWBrowser.m new file mode 100644 index 00000000..28fba85a --- /dev/null +++ b/internal/platform/implementation/apple/Mediums/WiFiCommon/Tests/GNCFakeNWBrowser.m @@ -0,0 +1,72 @@ +// 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/WiFiCommon/Tests/GNCFakeNWBrowser.h" + +@implementation GNCFakeNWBrowser + +@synthesize createWithDescriptorResult = _createWithDescriptorResult; +@synthesize setQueueQueue = _setQueueQueue; +@synthesize browseResultsChangedHandler = _browseResultsChangedHandler; +@synthesize stateChangedHandler = _stateChangedHandler; +@synthesize cancelCalled = _cancelCalled; +@synthesize cancelledBrowser = _cancelledBrowser; + +- (instancetype)init { + self = [super init]; + if (self) { + _stateForStart = nw_browser_state_ready; + } + return self; +} + +- (nw_browser_t)createWithDescriptor:(nw_browse_descriptor_t)descriptor + parameters:(nw_parameters_t)parameters { + // Return a unique object to represent the browser + _createWithDescriptorResult = (nw_browser_t)[[NSObject alloc] init]; + return _createWithDescriptorResult; +} + +- (void)setQueue:(nw_browser_t)browser queue:(dispatch_queue_t)queue { + self.setQueueQueue = queue; +} + +- (void)setBrowseResultsChangedHandler:(nw_browser_t)browser + handler:(nw_browser_browse_results_changed_handler_t)handler { + self.browseResultsChangedHandler = handler; +} + +- (void)setStateChangedHandler:(nw_browser_t)browser + handler:(nw_browser_state_changed_handler_t)handler { + self.stateChangedHandler = handler; +} + +- (void)start:(nw_browser_t)browser { + if (_simulateTimeout) { + return; + } + // Simulate state change + if (self.stateChangedHandler) { + dispatch_async(self.setQueueQueue ?: dispatch_get_main_queue(), ^{ + self.stateChangedHandler(self.stateForStart, nil); + }); + } +} + +- (void)cancel:(nw_browser_t)browser { + _cancelCalled = YES; + _cancelledBrowser = browser; +} + +@end diff --git a/internal/platform/implementation/apple/Mediums/WiFiCommon/Tests/GNCNWBrowseResultImplTest.m b/internal/platform/implementation/apple/Mediums/WiFiCommon/Tests/GNCNWBrowseResultImplTest.m new file mode 100644 index 00000000..b415c876 --- /dev/null +++ b/internal/platform/implementation/apple/Mediums/WiFiCommon/Tests/GNCNWBrowseResultImplTest.m @@ -0,0 +1,70 @@ +// 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/WiFiCommon/GNCNWBrowseResultImpl.h" + +#import +#import + +@interface GNCNWBrowseResultImplTest : XCTestCase +@end + +@implementation GNCNWBrowseResultImplTest + +- (void)testInit { + GNCNWBrowseResultImpl *browseResult = [[GNCNWBrowseResultImpl alloc] init]; + XCTAssertNotNil(browseResult); +} + +- (void)testGetBonjourServiceName { + GNCNWBrowseResultImpl *browseResult = [[GNCNWBrowseResultImpl alloc] init]; + nw_endpoint_t endpoint = + nw_endpoint_create_bonjour_service("MyService", "_servicetype._tcp", "local."); + NSString *serviceName = [browseResult getBonjourServiceNameFromEndpoint:endpoint]; + XCTAssertEqualObjects(serviceName, @"MyService"); +} + +- (void)testGetBonjourServiceName_NonBonjour { + GNCNWBrowseResultImpl *browseResult = [[GNCNWBrowseResultImpl alloc] init]; + nw_endpoint_t endpoint = nw_endpoint_create_host("google.com", "443"); + NSString *serviceName = [browseResult getBonjourServiceNameFromEndpoint:endpoint]; + XCTAssertNil(serviceName); +} + +- (void)testApplyTXTRecord { + GNCNWBrowseResultImpl *browseResult = [[GNCNWBrowseResultImpl alloc] init]; + nw_txt_record_t txtRecord = nw_txt_record_create_dictionary(); + NSData *value1Data = [@"value1" dataUsingEncoding:NSUTF8StringEncoding]; + NSData *value2Data = [@"value2" dataUsingEncoding:NSUTF8StringEncoding]; + nw_txt_record_set_key(txtRecord, "key1", value1Data.bytes, value1Data.length); + nw_txt_record_set_key(txtRecord, "key2", value2Data.bytes, value2Data.length); + NSMutableDictionary *results = [[NSMutableDictionary alloc] init]; + [browseResult applyTXTRecord:txtRecord + block:^bool(const char *key, nw_txt_record_find_key_t found, + const uint8_t *value, 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]; + [results setObject:valueString forKey:@(key)]; + } + return YES; + }]; + XCTAssertEqual(results.count, 2); + XCTAssertEqualObjects(results[@"key1"], @"value1"); + XCTAssertEqualObjects(results[@"key2"], @"value2"); +} + +@end diff --git a/internal/platform/implementation/apple/Mediums/WiFiCommon/Tests/GNCNWBrowserImplTest.m b/internal/platform/implementation/apple/Mediums/WiFiCommon/Tests/GNCNWBrowserImplTest.m new file mode 100644 index 00000000..b661100e --- /dev/null +++ b/internal/platform/implementation/apple/Mediums/WiFiCommon/Tests/GNCNWBrowserImplTest.m @@ -0,0 +1,36 @@ +// 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/WiFiCommon/GNCNWBrowserImpl.h" + +#import +#import + +@interface GNCNWBrowserImplTest : XCTestCase +@end + +@implementation GNCNWBrowserImplTest + +- (void)testInit { + GNCNWBrowserImpl *browser = [[GNCNWBrowserImpl alloc] init]; + XCTAssertNotNil(browser); +} + +// NOTE: The methods in GNCNWBrowserImpl call C functions from the Network.framework +// (e.g., nw_browser_create, nw_browser_start, nw_browser_cancel). +// OCMock cannot directly mock these C functions. To test these interactions, +// a lower-level interception method or a different testing strategy would be required. +// We can only test the basic initialization here. + +@end