// TLSEvaluationTests.swift
//
// Copyright (c) 2014–2015 Alamofire Software Foundation (http://alamofire.org/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

import Alamofire
import Foundation
import XCTest

private struct TestCertificates {
    static let RootCA = TestCertificates.certificateWithFileName("root-ca-disig")
    static let IntermediateCA = TestCertificates.certificateWithFileName("intermediate-ca-disig")
    static let Leaf = TestCertificates.certificateWithFileName("testssl-expire.disig.sk")

    static func certificateWithFileName(fileName: String) -> SecCertificate {
        class Bundle {}
        let filePath = NSBundle(forClass: Bundle.self).pathForResource(fileName, ofType: "cer")!
        let data = NSData(contentsOfFile: filePath)!
        let certificate = SecCertificateCreateWithData(nil, data)!

        return certificate
    }
}

// MARK: -

private struct TestPublicKeys {
    static let RootCA = TestPublicKeys.publicKeyForCertificate(TestCertificates.RootCA)
    static let IntermediateCA = TestPublicKeys.publicKeyForCertificate(TestCertificates.IntermediateCA)
    static let Leaf = TestPublicKeys.publicKeyForCertificate(TestCertificates.Leaf)

    static func publicKeyForCertificate(certificate: SecCertificate) -> SecKey {
        let policy = SecPolicyCreateBasicX509()
        var trust: SecTrust?
        SecTrustCreateWithCertificates(certificate, policy, &trust)

        let publicKey = SecTrustCopyPublicKey(trust!)!

        return publicKey
    }
}

// MARK: -

class TLSEvaluationExpiredLeafCertificateTestCase: BaseTestCase {
    let URL = "https://testssl-expire.disig.sk/"
    let host = "testssl-expire.disig.sk"
    var configuration: NSURLSessionConfiguration!

    // MARK: Setup and Teardown

    override func setUp() {
        super.setUp()
        configuration = NSURLSessionConfiguration.ephemeralSessionConfiguration()
    }

    // MARK: Default Behavior Tests

    func testThatExpiredCertificateRequestFailsWithNoServerTrustPolicy() {
        // Given
        let expectation = expectationWithDescription("\(URL)")
        let manager = Manager(configuration: configuration)
        var error: ErrorType?

        // When
        manager.request(.GET, URL)
            .response { _, _, _, responseError in
                error = responseError
                expectation.fulfill()
            }

        waitForExpectationsWithTimeout(defaultTimeout, handler: nil)

        // Then
        XCTAssertNotNil(error, "error should not be nil")

        if let code = (error as? NSError)?.code {
            XCTAssertEqual(code, NSURLErrorServerCertificateUntrusted, "code should be untrusted server certficate")
        } else {
            XCTFail("error should be an NSError")
        }
    }

    // MARK: Server Trust Policy - Perform Default Tests

    func testThatExpiredCertificateRequestFailsWithDefaultServerTrustPolicy() {
        // Given
        let policies = [host: ServerTrustPolicy.PerformDefaultEvaluation(validateHost: true)]
        let manager = Manager(
            configuration: configuration,
            serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies)
        )

        let expectation = expectationWithDescription("\(URL)")
        var error: ErrorType?

        // When
        manager.request(.GET, URL)
            .response { _, _, _, responseError in
                error = responseError
                expectation.fulfill()
            }

        waitForExpectationsWithTimeout(defaultTimeout, handler: nil)

        // Then
        XCTAssertNotNil(error, "error should not be nil")

        if let code = (error as? NSError)?.code {
            XCTAssertEqual(code, NSURLErrorCancelled, "code should be cancelled")
        } else {
            XCTFail("error should be an NSError")
        }
    }

    // MARK: Server Trust Policy - Certificate Pinning Tests

    func testThatExpiredCertificateRequestFailsWhenPinningLeafCertificateWithCertificateChainValidation() {
        // Given
        let certificates = [TestCertificates.Leaf]
        let policies: [String: ServerTrustPolicy] = [
            host: .PinCertificates(certificates: certificates, validateCertificateChain: true, validateHost: true)
        ]

        let manager = Manager(
            configuration: configuration,
            serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies)
        )

        let expectation = expectationWithDescription("\(URL)")
        var error: ErrorType?

        // When
        manager.request(.GET, URL)
            .response { _, _, _, responseError in
                error = responseError
                expectation.fulfill()
            }

        waitForExpectationsWithTimeout(defaultTimeout, handler: nil)

        // Then
        XCTAssertNotNil(error, "error should not be nil")

        if let code = (error as? NSError)?.code {
            XCTAssertEqual(code, NSURLErrorCancelled, "code should be cancelled")
        } else {
            XCTFail("error should be an NSError")
        }
    }

    func testThatExpiredCertificateRequestFailsWhenPinningAllCertificatesWithCertificateChainValidation() {
        // Given
        let certificates = [TestCertificates.Leaf, TestCertificates.IntermediateCA, TestCertificates.RootCA]
        let policies: [String: ServerTrustPolicy] = [
            host: .PinCertificates(certificates: certificates, validateCertificateChain: true, validateHost: true)
        ]

        let manager = Manager(
            configuration: configuration,
            serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies)
        )

        let expectation = expectationWithDescription("\(URL)")
        var error: ErrorType?

        // When
        manager.request(.GET, URL)
            .response { _, _, _, responseError in
                error = responseError
                expectation.fulfill()
            }

        waitForExpectationsWithTimeout(defaultTimeout, handler: nil)

        // Then
        XCTAssertNotNil(error, "error should not be nil")

        if let code = (error as? NSError)?.code {
            XCTAssertEqual(code, NSURLErrorCancelled, "code should be cancelled")
        } else {
            XCTFail("error should be an NSError")
        }
    }

    func testThatExpiredCertificateRequestSucceedsWhenPinningLeafCertificateWithoutCertificateChainValidation() {
        // Given
        let certificates = [TestCertificates.Leaf]
        let policies: [String: ServerTrustPolicy] = [
            host: .PinCertificates(certificates: certificates, validateCertificateChain: false, validateHost: true)
        ]

        let manager = Manager(
            configuration: configuration,
            serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies)
        )

        let expectation = expectationWithDescription("\(URL)")
        var error: ErrorType?

        // When
        manager.request(.GET, URL)
            .response { _, _, _, responseError in
                error = responseError
                expectation.fulfill()
            }

        waitForExpectationsWithTimeout(defaultTimeout, handler: nil)

        // Then
        XCTAssertNil(error, "error should be nil")
    }

    func testThatExpiredCertificateRequestSucceedsWhenPinningIntermediateCACertificateWithoutCertificateChainValidation() {
        // Given
        let certificates = [TestCertificates.IntermediateCA]
        let policies: [String: ServerTrustPolicy] = [
            host: .PinCertificates(certificates: certificates, validateCertificateChain: false, validateHost: true)
        ]

        let manager = Manager(
            configuration: configuration,
            serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies)
        )

        let expectation = expectationWithDescription("\(URL)")
        var error: ErrorType?

        // When
        manager.request(.GET, URL)
            .response { _, _, _, responseError in
                error = responseError
                expectation.fulfill()
            }

        waitForExpectationsWithTimeout(defaultTimeout, handler: nil)

        // Then
        XCTAssertNil(error, "error should be nil")
    }

    func testThatExpiredCertificateRequestSucceedsWhenPinningRootCACertificateWithoutCertificateChainValidation() {
        // Given
        let certificates = [TestCertificates.RootCA]
        let policies: [String: ServerTrustPolicy] = [
            host: .PinCertificates(certificates: certificates, validateCertificateChain: false, validateHost: true)
        ]

        let manager = Manager(
            configuration: configuration,
            serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies)
        )

        let expectation = expectationWithDescription("\(URL)")
        var error: ErrorType?

        // When
        manager.request(.GET, URL)
            .response { _, _, _, responseError in
                error = responseError
                expectation.fulfill()
            }

        waitForExpectationsWithTimeout(defaultTimeout, handler: nil)

        // Then
        XCTAssertNil(error, "error should be nil")
    }

    // MARK: Server Trust Policy - Public Key Pinning Tests

    func testThatExpiredCertificateRequestFailsWhenPinningLeafPublicKeyWithCertificateChainValidation() {
        // Given
        let publicKeys = [TestPublicKeys.Leaf]
        let policies: [String: ServerTrustPolicy] = [
            host: .PinPublicKeys(publicKeys: publicKeys, validateCertificateChain: true, validateHost: true)
        ]

        let manager = Manager(
            configuration: configuration,
            serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies)
        )

        let expectation = expectationWithDescription("\(URL)")
        var error: ErrorType?

        // When
        manager.request(.GET, URL)
            .response { _, _, _, responseError in
                error = responseError
                expectation.fulfill()
            }

        waitForExpectationsWithTimeout(defaultTimeout, handler: nil)

        // Then
        XCTAssertNotNil(error, "error should not be nil")

        if let code = (error as? NSError)?.code {
            XCTAssertEqual(code, NSURLErrorCancelled, "code should be cancelled")
        } else {
            XCTFail("error should be an NSError")
        }
    }

    func testThatExpiredCertificateRequestSucceedsWhenPinningLeafPublicKeyWithoutCertificateChainValidation() {
        // Given
        let publicKeys = [TestPublicKeys.Leaf]
        let policies: [String: ServerTrustPolicy] = [
            host: .PinPublicKeys(publicKeys: publicKeys, validateCertificateChain: false, validateHost: true)
        ]

        let manager = Manager(
            configuration: configuration,
            serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies)
        )

        let expectation = expectationWithDescription("\(URL)")
        var error: ErrorType?

        // When
        manager.request(.GET, URL)
            .response { _, _, _, responseError in
                error = responseError
                expectation.fulfill()
            }

        waitForExpectationsWithTimeout(defaultTimeout, handler: nil)

        // Then
        XCTAssertNil(error, "error should be nil")
    }

    func testThatExpiredCertificateRequestSucceedsWhenPinningIntermediateCAPublicKeyWithoutCertificateChainValidation() {
        // Given
        let publicKeys = [TestPublicKeys.IntermediateCA]
        let policies: [String: ServerTrustPolicy] = [
            host: .PinPublicKeys(publicKeys: publicKeys, validateCertificateChain: false, validateHost: true)
        ]

        let manager = Manager(
            configuration: configuration,
            serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies)
        )

        let expectation = expectationWithDescription("\(URL)")
        var error: ErrorType?

        // When
        manager.request(.GET, URL)
            .response { _, _, _, responseError in
                error = responseError
                expectation.fulfill()
            }

        waitForExpectationsWithTimeout(defaultTimeout, handler: nil)

        // Then
        XCTAssertNil(error, "error should be nil")
    }

    func testThatExpiredCertificateRequestSucceedsWhenPinningRootCAPublicKeyWithoutCertificateChainValidation() {
        // Given
        let publicKeys = [TestPublicKeys.RootCA]
        let policies: [String: ServerTrustPolicy] = [
            host: .PinPublicKeys(publicKeys: publicKeys, validateCertificateChain: false, validateHost: true)
        ]

        let manager = Manager(
            configuration: configuration,
            serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies)
        )

        let expectation = expectationWithDescription("\(URL)")
        var error: ErrorType?

        // When
        manager.request(.GET, URL)
            .response { _, _, _, responseError in
                error = responseError
                expectation.fulfill()
            }

        waitForExpectationsWithTimeout(defaultTimeout, handler: nil)

        // Then
        XCTAssertNil(error, "error should be nil")
    }

    // MARK: Server Trust Policy - Disabling Evaluation Tests

    func testThatExpiredCertificateRequestSucceedsWhenDisablingEvaluation() {
        // Given
        let policies = [host: ServerTrustPolicy.DisableEvaluation]
        let manager = Manager(
            configuration: configuration,
            serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies)
        )

        let expectation = expectationWithDescription("\(URL)")
        var error: ErrorType?

        // When
        manager.request(.GET, URL)
            .response { _, _, _, responseError in
                error = responseError
                expectation.fulfill()
            }

        waitForExpectationsWithTimeout(defaultTimeout, handler: nil)

        // Then
        XCTAssertNil(error, "error should be nil")
    }

    // MARK: Server Trust Policy - Custom Evaluation Tests

    func testThatExpiredCertificateRequestSucceedsWhenCustomEvaluationReturnsTrue() {
        // Given
        let policies = [
            host: ServerTrustPolicy.CustomEvaluation { _, _ in
                // Implement a custom evaluation routine here...
                return true
            }
        ]

        let manager = Manager(
            configuration: configuration,
            serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies)
        )

        let expectation = expectationWithDescription("\(URL)")
        var error: ErrorType?

        // When
        manager.request(.GET, URL)
            .response { _, _, _, responseError in
                error = responseError
                expectation.fulfill()
            }

        waitForExpectationsWithTimeout(defaultTimeout, handler: nil)

        // Then
        XCTAssertNil(error, "error should be nil")
    }

    func testThatExpiredCertificateRequestFailsWhenCustomEvaluationReturnsFalse() {
        // Given
        let policies = [
            host: ServerTrustPolicy.CustomEvaluation { _, _ in
                // Implement a custom evaluation routine here...
                return false
            }
        ]

        let manager = Manager(
            configuration: configuration,
            serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies)
        )

        let expectation = expectationWithDescription("\(URL)")
        var error: ErrorType?

        // When
        manager.request(.GET, URL)
            .response { _, _, _, responseError in
                error = responseError
                expectation.fulfill()
            }

        waitForExpectationsWithTimeout(defaultTimeout, handler: nil)

        // Then
        XCTAssertNotNil(error, "error should not be nil")

        if let code = (error as? NSError)?.code {
            XCTAssertEqual(code, NSURLErrorCancelled, "code should be cancelled")
        } else {
            XCTFail("error should be an NSError")
        }
    }
}
