WKWebViewでの認証ハンドリングをテストする

こんにちは、モバイルチームの@el_metal_です。

弊社のiOSアプリではWKWebViewでも認証が必要なコンテンツを表示しているため、WKWebViewで認証処理を行なっています。

今回はWKWebViewでの認証ハンドリングをテストする方法についてご紹介します。

WKWebViewでの認証について

主な流れはHandling an Authentication Challengeに記載されています。

WKWebViewの認証ハンドリングはWKNavigationDelegatewebView(_:didReceive:completionHandler:)が担っているので、これを呼んだ結果を検証すればテストできます。

developer.apple.com

ここでは、例えばクライアント証明書を使った通信が対象になります。

WKWebViewでのクライアント証明書を使った通信の実装に興味がある方は、こちらも併せてご覧ください。

blog.cybozu.io

前準備

webView(_:didReceive:completionHandler:)を呼ぶにあたって引数の設定が必要です。

WebView

第一引数には通信をしたいWebViewを渡します。

URLAuthenticationChallenge

第二引数はURLAuthenticationChallengeです。

developer.apple.com

このオブジェクトを新たに作るinitは一つだけです。

URLProtectionSpace

同一の認証情報が適用される範囲のことで、一般にrealmと呼ばれます。

let protectionSpace = URLProtectionSpace(host: "cybozu.co.jp",
                                         port: 0,
                                         protocol: "https",
                                         realm: nil,
                                         authenticationMethod: NSURLAuthenticationMethodClientCertificate)

URLCredential

クレデンシャル情報と、利用する場合は永続化に用いるストレージのセットです。

URLAuthenticationChallengeSender

Authentication challengeを開始したオブジェクトです。 テストでは以下のようにURLProtocolを用意して渡すことができます。

fileprivate class URLProtocolMock: URLProtocol, URLAuthenticationChallengeSender {
    // MARK: URLAuthenticationChallengeSender
    func use(_ credential: URLCredential, for challenge: URLAuthenticationChallenge) {}

    func continueWithoutCredential(for challenge: URLAuthenticationChallenge) {}

    func cancel(_ challenge: URLAuthenticationChallenge) {}

    // MARK: URLProtocol
    override class func canInit(with request: URLRequest) -> Bool { true }

    override open class func canInit(with task: URLSessionTask) -> Bool { true }

    override func startLoading() {}

    override func stopLoading() {}

    open override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }

}

completionHandler

レスポンスと共に実行されるクロージャです。 テストコードでは、ここでアサーションを行うと良いでしょう。

サンプルコード

WKWebViewを持つViewControllerWKNavigationDelegateに設定し、テスト対象とします。

final class ViewController: UIViewController {

    @IBOutlet private var stackView: UIStackView!
    private var browser: WKWebView!

    override func viewDidLoad() {
        super.viewDidLoad()

        self.browser = WKWebView(frame: .zero)
        self.stackView.addArrangedSubview(browser)

        self.browser.navigationDelegate = self
        self.browser.uiDelegate = self

        self.browser.load(URLRequest(url: URL(string: "https://cybozu.co.jp")!))
    }
}

extension ViewController: WKNavigationDelegate {
    func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge,
                 completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {

        if challenge.previousFailureCount > 3 {
            completionHandler(.performDefaultHandling, nil)
            return
        }
    }
}

テストでは、以下のようにViewControllerを作って実行します。

final class WKNavigationDelegateTests: XCTestCase {
    func test() throws {
        let sut = UIStoryboard(name: "ViewController", bundle: nil).instantiateInitialViewController() as! ViewController

        sut.view.layoutIfNeeded()
        URLProtocol.registerClass(URLProtocolMock.self)

        let webView = sut.view.subviews
            .compactMap { $0 as? UIStackView }
            .first?
            .subviews
            .compactMap { $0 as? WKWebView }
            .first!

        let protectionSpace = URLProtectionSpace(host: "example.com",
                                                 port: 0,
                                                 protocol: "https",
                                                 realm: nil,
                                                 authenticationMethod: NSURLAuthenticationMethodClientCertificate)

        let credential = URLCredential()
        let challenge = URLAuthenticationChallenge(protectionSpace: protectionSpace,
                                                   proposedCredential: credential,
                                                   previousFailureCount: 1,
                                                   failureResponse: nil,
                                                   error: nil,
                                                   sender: URLProtocolMock())

        sut.webView(webView!, didReceive: challenge, completionHandler: { authChallengeDisposition, _ -> Void in
            XCTAssertEqual(authChallengeDisposition, .performDefaultHandling)
        })
    }
}

以上でWKWebViewの認証ハンドリングをテストすることができました。

おわりに

WKWebViewの認証ハンドリングは複雑になりやすい割にドキュメントに乏しく、テストを書くのに手間取りました。
本記事がお役に立てば幸いです。