import Foundation import Testing @testable import ClientTLS // B4 · The URLSession seam. `ClientTLSSessionDelegate` is the object every HTTP // call to an mTLS host hangs off, so the one behaviour that matters is that the // session-level callback ALWAYS invokes the completion handler exactly once with // the responder's decision — a delegate that forgets to call back hangs the // request forever (no timeout maps to a user-visible error). private func makeChallenge(method: String) -> URLAuthenticationChallenge { let space = URLProtectionSpace( host: "t1.terminal.yaojia.wang", port: 443, protocol: "https", realm: nil, authenticationMethod: method ) return URLAuthenticationChallenge( protectionSpace: space, proposedCredential: nil, previousFailureCount: 0, failureResponse: nil, error: nil, sender: DelegateTestSender() ) } /// The responder never calls back into the sender; it only reads the protection /// space. Present because the designated initializer demands a non-optional one. private final class DelegateTestSender: NSObject, URLAuthenticationChallengeSender { func use(_ credential: URLCredential, for challenge: URLAuthenticationChallenge) {} func continueWithoutCredential(for challenge: URLAuthenticationChallenge) {} func cancel(_ challenge: URLAuthenticationChallenge) {} } /// Drive the delegate and capture what it handed back. private func resolve( identity: ClientIdentity?, method: String ) -> (calls: Int, disposition: URLSession.AuthChallengeDisposition?, credential: URLCredential?) { let delegate = ClientTLSSessionDelegate(identity: identity) var calls = 0 var disposition: URLSession.AuthChallengeDisposition? var credential: URLCredential? delegate.urlSession( URLSession.shared, didReceive: makeChallenge(method: method) ) { receivedDisposition, receivedCredential in calls += 1 disposition = receivedDisposition credential = receivedCredential } return (calls, disposition, credential) } @Test("a ClientCertificate challenge answers once with the installed identity") func delegateAnswersClientCertificateWithIdentity() throws { // Arrange let identity = try PKCS12Importer.importIdentity( data: ClientTLSFixtures.deviceP12Data, passphrase: ClientTLSFixtures.passphrase ) // Act let result = resolve( identity: identity, method: NSURLAuthenticationMethodClientCertificate ) // Assert #expect(result.calls == 1) // exactly once — never zero (hang) or twice (crash) #expect(result.disposition == .useCredential) #expect(result.credential?.identity != nil) } @Test("a ClientCertificate challenge with no identity cancels instead of hanging") func delegateCancelsWithoutIdentity() { // Act let result = resolve(identity: nil, method: NSURLAuthenticationMethodClientCertificate) // Assert #expect(result.calls == 1) #expect(result.disposition == .cancelAuthenticationChallenge) #expect(result.credential == nil) } @Test("server-trust challenges fall through to the system evaluation") func delegateDefersServerTrust() throws { // Arrange let identity = try PKCS12Importer.importIdentity( data: ClientTLSFixtures.deviceP12Data, passphrase: ClientTLSFixtures.passphrase ) // Act / Assert — the client cert must never be offered as a server-trust // answer, and pinning is NOT this package's job (plan §5: default handling). for candidate in [identity, nil] as [ClientIdentity?] { let result = resolve(identity: candidate, method: NSURLAuthenticationMethodServerTrust) #expect(result.calls == 1) #expect(result.disposition == .performDefaultHandling) #expect(result.credential == nil) } }