Enable local network access during iOS UI test in iOS14

We are building an iOS app that connects to a device using Bluetooth. To test unhappy flow scenarios for this app, we'd like to power cycle the device we are connecting to by using an IoT power switch that connects to the local network using WiFi (a Shelly Plug-S).

In my test code on iOS13, I was able to do a local HTTP call to the IP address of the power switch and trigger a power cycle using its REST interface. In iOS 14 this is no longer possible, probably due to new restrictions regarding local network usage without permissions (see: https://developer.apple.com/videos/play/wwdc2020/10110 ).

When running the test and trying a local network call to the power switch in iOS14, I get the following error:

Code Block
Task <D206B326-1820-43CA-A54C-5B470B4F1A79>.<2> finished with error [-1009] Error Domain=NSURLErrorDomain Code=-1009 "The internet connection appears to be offline." UserInfo={_kCFStreamErrorCodeKey=50, NSUnderlyingError=0x2833f34b0 {Error Domain=kCFErrorDomainCFNetwork Code=-1009 "(null)" UserInfo={_kCFStreamErrorCodeKey=50, _kCFStreamErrorDomainKey=1}}, _NSURLErrorFailingURLSessionTaskErrorKey=LocalDataTask <D206B326-1820-43CA-A54C-5B470B4F1A79>.<2>, _NSURLErrorRelatedURLSessionTaskErrorKey=("LocalDataTask <D206B326-1820-43CA-A54C-5B470B4F1A79>.<2>"), NSLocalizedDescription=The internet connection appears to be offline., NSErrorFailingURLStringKey=http://192.168.22.57/relay/0?turn=on, NSErrorFailingURLKey=http://192.168.22.57/relay/0?turn=on, _kCFStreamErrorDomainKey=1}

An external network call (to google.com) works just fine in the test.

I have tried fixing this by adding the following entries to the Info.plist of my UI test target:

Code Block
<key>NSLocalNetworkUsageDescription</key>
<string>Local network access is needed for tests</string>
<key>NSBonjourServices</key>
<array>
<string>_http._tcp</string>
</array>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>

However, this has no effect.

I have also tried adding these entries to the Info.plist of my app target to see if that makes a difference, but it doesn't. I'd also rather not add these entries to my app's Info.plist, because the app does not need local network access. Only the test does.

Does anyone know how to enable local network access during an iOS UI test in iOS14?

I have also tried adding these entries to the Info.plist of my app target to see if that makes a difference, but it doesn't.

Right. You cannot override local network privacy with NSAllowsArbitraryLoads being true.

Does anyone know how to enable local network access during an iOS UI test in iOS14?

I just setup a XCTest for UI Testing in a main app that triggers local network privacy and it does prompt the user but there is no way to automatically allow or do not allow this prompt. In this case you will need to manually allow or do not allow this prompt. If you are looking for a way to manually interact with this prompt for use in an XCTest only I would submit an enhancement request. Please respond back with the Feedback ID.

I should also note that just like the normal usage of the local network privacy prompt, that once you allow or do not allow the first time you local network is accessed then this value will be cached. So you could seed you tests with this value if this is an option for your workflow.


Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com
Hi Matt,

Thank you for your response.

I have tried only setting the NSLocalNetworkUsageDescription key in my test target's Info.plist (no more entries for NSBonjourServices or NSAllowsArbitraryLoads). My test code now simply executes a URLSession.dataTask to a local endpoint via http. However, I do not get the local network usage prompt when running the test, and the network call fails with the same error as before. I removed the test app before running to make sure the value was not cached.

Can you tell me what code you wrote in your test that triggered the prompt? And just to be sure: the prompt will be triggered by the test app, not the actual app under test, right? Also, the Info.plist of the app under test can remain untouched?

Kind regards,
Ejnar
Ejnar,

I didn't set the NSLocalNetworkUsageDescription in the testing target. However, it looks like we may have run different tests.

Regarding:

Can you tell me what code you wrote in your test that triggered the prompt? And just to
be sure: the prompt will be triggered by the test app, not the actual app under test, right?

My original test was to find a button in the app being tested, tap that button, and that tap would send the dataTask request via URLSession over the local network. This would trigger the prompt and this was the test I ran yesterday.

This morning I refactored that XCTest and tried to run the dataTask inside the XCTest case and did not receive the local network privacy prompt. Open a bug report on this one just to make sure that not receiving the local network privacy prompt from a XCTest case is the intended behavior. Please please responde with the Feedback ID.


Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com
Hello Matt,

I step across the same issue. I want to make an HTTP call to the server running in my local network. I added the NSLocalNetworkUsageDescription key both to my app target's Info.plist and my UITest target's Info.plist.

When running on physical device connected to the same network as the server:
  • Sending the URLSession.dataTask() from the app brings the local network privacy prompt. If approved, the request is send and the approval is cached so it doesn't appear next time. All works fine - as expected.

  • Sending the URLSession.dataTask() from the UITest runner's XCTestCase does not bring the local network privacy prompt. The request never reaches the local server and URLSession.dataTask() results with the Error Domain=NSURLErrorDomain Code=-1009 "The internet connection appears to be offline." as reported by Ejnar.

I opened a ticket in Feedback Assistant: FB8983382 (Local Network Privacy Prompt is not received from an XCTestCase)

Would love to hear Apple Engineer's reply on this.

Is there any workaround for sending local requests from UITest runner?
I've also encountered this issue, here are my findings:

When firing a local network request from the application:
  • Permission prompt is displayed;

  • After allowing local network access, requests pass successfully;

  • Works on simulator;

  • Works on real device;

When firing a local network request from a Unit Test target:
  • Permission prompt is displayed;

  • After allowing local network access, requests pass successfully;

  • Works on simulator;

  • Works on real device;

When firing a local network request from a UI Test target:
  • Permission prompt is not displayed;

  • Even after allowing local network access (manually from Settings), requests fail;

  • Works on simulator;

  • Fails on real device;

I've tested now with Xcode 12.5 beta 3 and it seems that the issue has been fixed! 🥳

I've tested now with Xcode 12.5 beta 3 and it seems that the issue has been fixed! 🥳

Great news. I did also try invoking a datagram message over the local network from the context of an XCTestCase today in Xcode 13 Beta on iOS 14.5 and I did receive the prompt here as well and did see the message received on the listening side.

Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com

Today I did some testing on iOS 14.8 and iOS 15.2 trying to enable the Local Network Privacy Prompt in my UI XCTestCase and here is what I found:

I setup a test case in the main app like so:

class ViewController: UIViewController {
    
    @IBOutlet weak var testButton: UIButton!
    
    var networkSessionManager: NetworkSessionManager?

    override func viewDidLoad() {
        super.viewDidLoad()
        
        networkSessionManager = NetworkSessionManager()
        networkSessionManager?.delegate = self
        testButton.accessibilityIdentifier = "myButton"
    }

    @IBAction func testRequest() {
        instruction.text = "Sending Network Request"
        // Hits redirect
        networkSessionManager?.dataTask(with: "http://192.168.***.***:8000")
    }
}

protocol NetworkManagerDelegate: AnyObject {
    func networkSessionManager(networkSession: NetworkSessionManager, response: String, error: Error?)
    
}


class NetworkSessionManager: NSObject {
    
    private var urlSession: URLSession?
    private var config: URLSessionConfiguration = URLSessionConfiguration.default
    weak var delegate: NetworkManagerDelegate?
    
    override init() {
        super.init()
        
        config.waitsForConnectivity = true
        urlSession = URLSession(configuration: config, delegate: self, delegateQueue: .main)
    }
    
    func dataTask(with url: String) {
        guard let url = URL(string: url),
            let unwrappedURLSession = urlSession else { return }
        
        var urlRequest = URLRequest(url: url)
        urlRequest.httpMethod = "GET"
        let task = unwrappedURLSession.dataTask(with: urlRequest, completionHandler: { (data, response, error) in
            
            ...
        })
        task.resume()
    }
    
}

extension NetworkSessionManager: URLSessionDelegate {
    func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) {
        ...
    }
}

Then in the XCTestCase I set it up like so:

import XCTest
@testable import URLSessionExample

class URLSessionExampleUITests: XCTestCase {

    var networkSessionManager: NetworkSessionManager?
    let expectation = XCTestExpectation(description: "Download from local network home page")
    
    override func setUpWithError() throws {

        // In UI tests it is usually best to stop immediately when a failure occurs.
        continueAfterFailure = false

        networkSessionManager = NetworkSessionManager()
        networkSessionManager?.delegate = self
    }

    override func tearDownWithError() throws {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
    }

    func testExample() throws {
        // UI tests must launch the application that they test.
        let app = XCUIApplication()
        app.launch()

        let button = app.buttons["myButton"]
        button.tap()
        
        wait(for: [expectation], timeout: 10.0)
    }

    func testLaunchPerformance() throws {
        if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) {
            // This measures how long it takes to launch your application.
            measure(metrics: [XCTApplicationLaunchMetric()]) {
                XCUIApplication().launch()
            }
        }
    }
}

extension URLSessionExampleUITests: NetworkManagerDelegate {

    func networkSessionManager(networkSession: NetworkSessionManager, response: String, error: Error?) {
        XCTAssertNotNil(response, "No data was downloaded.")  
        // Fulfill the expectation to indicate that the background task has finished successfully.
        expectation.fulfill()
    }

}

Now, on Xcode 12.5 with iOS 14.8 I was able to run testExample() and have the Local Network Privacy Prompt show up. On Xcode 13.1 with iOS 15.2 I did not see the Local Network Privacy Prompt. I am still doing some digging to see what is the correct path here, but I've had a few folks ask about the test I ran from this example, so I wanted to post it here.

Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com

The first time I read this I thought the problem was solved but after rereading and it seems the problem isn't fixed. Is that correct, I am trying to do a similar task right now and haven't had any luck. https://developer.apple.com/forums/thread/725734

The solution I've found in the Xcode 15.3 working with iOS 17.4.1 made me SICK! I couldn't find any doc about it, but I'm sure it works. You have to bring your xctrunner app to front, this way OS asks for local network permission in UI test target.

Try this in your UI test target without need to specify NSLocalNetworkUsageDescription and bonjour services, etc.

func testXCTRunnerToAskForLocalNetworkPermission() {
        // without bringing xctrunner to front, permission alert won't be shown.
        let app = XCUIApplication(bundleIdentifier: "{YOUR_APP_BUNDLE_ID}UITests.xctrunner")
        app.activate()

        connectToLocalNetwork()

        waitFor(aSecond: 1)

        let springBoard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
        // Allows local network permission.
        let button = springBoard.alerts.firstMatch.buttons["Allow"]
        if button.exists {
            button.tap()
            // local network is allowed in current running ui test.
            connectToLocalNetwork() // try to connect, as it's allowed now.
        }

        // Keep XCUITest running forever, though sometimes it stops automatically
        wait(for: [expectation(description: "keep running")], timeout: .infinity)
    }

waiter helper function:

    func waitFor(aSecond seconds: TimeInterval) {
        let expectation = expectation(description: "waiting for \(seconds)")
        let timer = Timer(timeInterval: seconds, repeats: false) { _ in
            expectation.fulfill()
        }
        wait(for: [expectation], timeout: seconds)
    }

local network function:

    func connectToLocalNetwork() {
        URLSession.shared.dataTask(with: URLRequest(url: URL(string: "http://192.168.2.101:5055")!)) { data, response, error in
            if let data {
                print("Data: \(String(data: data, encoding: .utf8) ?? "")")
            }
        }.resume()
    }
Enable local network access during iOS UI test in iOS14
 
 
Q