IBM-Swift/Kitura

View on GitHub
Tests/KituraTests/KituraTest.swift

Summary

Maintainability
D
2 days
Test Coverage
/**
 * Copyright IBM Corporation 2016-2019
 *
 * 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
 *
 * http://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 XCTest
import Kitura

@testable import KituraNet

import Foundation
import Dispatch

// The type of transport to use when communicating with the server.
enum SSLOption {
    // Tests will be performed over both HTTP and HTTPS.
    case both
    // Only HTTP will be tested.
    case httpOnly
    // Only HTTPS will be tested.
    case httpsOnly
}

// The type of socket to use when connecting to the server.
enum SocketTypeOption {
    // Both inet and unix sockets will be tested, in turn
    case both
    // Only inet sockets will be tested
    case inet
    // Only unix sockets will be tested
    case unix
}

// Kitura test suites should conform to this so that TestLinuxSafeguard can
// access the `allTests` array generically
protocol KituraTestSuite {
    static var allTests: [(String, (Self) -> () throws -> Void)] { get }
    #if os(macOS)
    // Expose defaultTestSuite from XCTestSuite
    static var defaultTestSuite: XCTestSuite { get }
    #endif
}

class KituraTest: XCTestCase {
    // A singleton Kitura server listening on HTTP on an INET socket
    static private(set) var httpInetServer: HTTPServer?
    // A singleton Kitura server listening on HTTPS on an INET socket
    static private(set) var httpsInetServer: HTTPServer?
    // A singleton Kitura server listening on HTTP on a Unix socket
    static private(set) var httpUnixServer: HTTPServer?
    // A singleton Kitura server listening on HTTPS on a Unix socket
    static private(set) var httpsUnixServer: HTTPServer?

    // A short-lived Kitura server created for a specific test
    private var serverWithOptions: HTTPServer?

    // The port of the server returned by startServer().
    private(set) var port = -1
    // Whether the server used by doPerformServerTest should use SSL.
    private(set) var useSSL = false
    // The socket file path of the server returned by startUnixSocketServer().
    private(set) var socketFilePath: String? = nil
    // Whether the server used by doPerformServerTest should use a Unix domain socket.
    private(set) var useUnixSocket = false

    /// The types of listeners we currently support.
    private enum ListenerType {
        case inet(Int)
        case unix(String)
    }

    static let sslConfig: SSLConfig = {
        let sslConfigDir = URL(fileURLWithPath: #file).appendingPathComponent("../SSLConfig")

        #if os(Linux)
            let certificatePath = sslConfigDir.appendingPathComponent("certificate.pem").standardized.path
            let keyPath = sslConfigDir.appendingPathComponent("key.pem").standardized.path
            return SSLConfig(withCACertificateDirectory: nil, usingCertificateFile: certificatePath,
                             withKeyFile: keyPath, usingSelfSignedCerts: true)
        #else
            let chainFilePath = sslConfigDir.appendingPathComponent("certificateChain.pfx").standardized.path
            return SSLConfig(withChainFilePath: chainFilePath, withPassword: "kitura",
                             usingSelfSignedCerts: true)
        #endif
    }()

    private static let initOnce: () = {
        PrintLogger.use(colored: true)
    }()

    override func setUp() {
        super.setUp()
        KituraTest.initOnce
    }

    func buildServerTest(_ router: ServerDelegate, sslOption: SSLOption = SSLOption.both, socketTypeOption: SocketTypeOption = SocketTypeOption.both, timeout: TimeInterval = 10,
                           line: Int = #line) -> RequestTestBuilder {
        return ServerTestBuilder(test: self, router: router, sslOption: sslOption, socketTypeOption: socketTypeOption, timeout: timeout, line: line)
    }

    func performServerTest(_ router: ServerDelegate, options: ServerOptions? = nil, sslOption: SSLOption = SSLOption.both, socketTypeOption: SocketTypeOption = SocketTypeOption.both, timeout: TimeInterval = 10,
                           line: Int = #line, asyncTasks: (XCTestExpectation) -> Void...) {
        performServerTest(router, options: options, sslOption: sslOption, socketTypeOption: socketTypeOption, timeout: timeout, line: line, asyncTasks: asyncTasks)
    }

    func performServerTest(_ router: ServerDelegate, options: ServerOptions? = nil, sslOption: SSLOption = SSLOption.both, socketTypeOption: SocketTypeOption = SocketTypeOption.both, timeout: TimeInterval = 10,
                           line: Int = #line, asyncTasks: [(XCTestExpectation) -> Void]) {
        if sslOption != SSLOption.httpsOnly {
            self.useSSL = false
            if socketTypeOption != SocketTypeOption.unix {
                self.useUnixSocket = false
                doPerformServerTest(router: router, options: options, timeout: timeout, line: line, asyncTasks: asyncTasks)
            }
#if !SKIP_UNIX_SOCKETS
            setUp()
            if socketTypeOption != SocketTypeOption.inet {
                self.useUnixSocket = true
                doPerformServerTest(router: router, options: options, timeout: timeout, line: line, asyncTasks: asyncTasks)
            }
#endif
        }
        
        // Call setUp to start at a known state (ideally, this should have been written as a separate test)
        setUp()

        if sslOption != SSLOption.httpOnly {
            self.useSSL = true
            if socketTypeOption != SocketTypeOption.unix {
                self.useUnixSocket = false
                doPerformServerTest(router: router, options: options, timeout: timeout, line: line, asyncTasks: asyncTasks)
            }
#if !SKIP_UNIX_SOCKETS
            setUp()
            if socketTypeOption != SocketTypeOption.inet {
                self.useUnixSocket = true
                doPerformServerTest(router: router, options: options, timeout: timeout, line: line, asyncTasks: asyncTasks)
            }
#endif
        }
    }

    func doPerformServerTest(router: ServerDelegate, options: ServerOptions?, timeout: TimeInterval, line: Int, asyncTasks: [(XCTestExpectation) -> Void]) {

        if self.useUnixSocket {
            guard let socketPath = startUnixSocketServer(router: router, options: options) else {
                return XCTFail("Error starting server. useSSL:\(self.useSSL), useUnixSocket:\(self.useUnixSocket)")
            }
            XCTAssertEqual(socketPath, self.socketFilePath, "Server is listening on the wrong path")
        } else {
            guard let port = startServer(router: router, options: options) else {
                return XCTFail("Error starting server. useSSL:\(self.useSSL), useUnixSocket:\(self.useUnixSocket)")
            }
            self.port = port
        }
        let requestQueue = DispatchQueue(label: "Request queue")
        for (index, asyncTask) in asyncTasks.enumerated() {
            let expectation = self.expectation(line: line, index: index)
            requestQueue.async {
                asyncTask(expectation)
            }
        }

        // wait for timeout or for all created expectations to be fulfilled
        waitForExpectations(timeout: timeout) { error in
            XCTAssertNil(error)
        }

        // If we created a short-lived server for specific ServerOptions, shut it down now
        serverWithOptions?.stop()
    }

    // Start a server. If a non-nil `options` is provided, then the server is stored
    // as the `serverWithOptions` property, and will be shut down at the end of the test.
    private func doStartServer(router: ServerDelegate, options: ServerOptions?) -> HTTPServer? {
        let server = HTTP.createServer()
        server.delegate = router

        if useSSL {
            server.sslConfig = KituraTest.sslConfig.config
        }

        if let options = options {
            server.options = options
            serverWithOptions = server
        }

        do {
            try server.listen(on: 0, address: "localhost")
            return server
        } catch {
            XCTFail("Error starting server: \(error)")
            return nil
        }
    }

    // Start a server on an inet socket. If nil `options` are specified, this is
    // a generic server that can be reused between tests, and will be stored as a
    // static property.
    private func startServer(router: ServerDelegate, options: ServerOptions?) -> Int? {
        // Servers with options (live for duration of one test)
        if options != nil {
            let server = doStartServer(router: router, options: options)
            return server?.port
        }
        // Generic servers that can be long-lived (for a whole test class)
        if useSSL {
            if let server = KituraTest.httpsInetServer {
                server.delegate = router
                return server.port
            }
        } else {
            if let server = KituraTest.httpInetServer {
                server.delegate = router
                return server.port
            }
        }

        let server = doStartServer(router: router, options: nil)

        if useSSL {
            KituraTest.httpsInetServer = server
        } else {
            KituraTest.httpInetServer = server
        }
        return server?.port
    }

    // Start a server. If a non-nil `options` is provided, then the server is stored
    // as the `serverWithOptions` property, and will be shut down at the end of the test.
    private func doStartUnixSocketServer(router: ServerDelegate, options: ServerOptions?) -> HTTPServer? {
        let server = HTTP.createServer()
        server.delegate = router

        if useSSL {
            server.sslConfig = KituraTest.sslConfig.config
        }

        if let options = options {
            server.options = options
            serverWithOptions = server
        }

        // Create a temporary path for Unix domain socket
        let socketPath = uniqueTemporaryFilePath()
        self.socketFilePath = socketPath

        do {
            try server.listen(unixDomainSocketPath: socketPath)
            return server
        } catch {
            XCTFail("Error starting server: \(error)")
            return nil
        }
    }

    // Start a server on a unix domain socket. If nil `options` are specified, this is
    // a generic server that can be reused between tests, and will be stored as a
    // static property.
    private func startUnixSocketServer(router: ServerDelegate, options: ServerOptions?) -> String? {
        // Servers with options (live for duration of one test)
        if (options != nil) {
            let server = doStartUnixSocketServer(router: router, options: options)
            return server?.unixDomainSocketPath
        }
        // Generic servers that can be long-lived (for a whole test class)
        if useSSL {
            if let server = KituraTest.httpsUnixServer {
                server.delegate = router
                guard let listenPath = server.unixDomainSocketPath else {
                    XCTFail("Unix socket path missing")
                    return nil
                }
                self.socketFilePath = listenPath
                return server.unixDomainSocketPath
            }
        } else {
            if let server = KituraTest.httpUnixServer {
                server.delegate = router
                guard let listenPath = server.unixDomainSocketPath else {
                    XCTFail("Unix socket path missing")
                    return nil
                }
                self.socketFilePath = listenPath
                return server.unixDomainSocketPath
            }
        }

        let server = doStartUnixSocketServer(router: router, options: nil)

        if useSSL {
            KituraTest.httpsUnixServer = server
        } else {
            KituraTest.httpUnixServer = server
        }
        return server?.unixDomainSocketPath
    }

    func stopServer() {
        KituraTest.httpInetServer?.stop()
        KituraTest.httpInetServer = nil

        KituraTest.httpsInetServer?.stop()
        KituraTest.httpsInetServer = nil

        KituraTest.httpUnixServer?.stop()
        KituraTest.httpUnixServer = nil

        KituraTest.httpsUnixServer?.stop()
        KituraTest.httpsUnixServer = nil

        // Clean up temporary file for Unix domain socket
        if let socketFilePath = socketFilePath {
            removeTemporaryFilePath(socketFilePath)
        }
    }

    func performRequest(_ method: String, path: String, port: Int? = nil, socketPath: String? = nil, useSSL: Bool? = nil, useUnixSocket: Bool? = nil, followRedirects: Bool = true,
                        callback: @escaping ClientRequest.Callback, headers: [String: String]? = nil,
                        requestModifier: ((ClientRequest) -> Void)? = nil) {

        let port = port ?? self.port
        let socketPath = socketPath ?? self.socketFilePath
        let useSSL = useSSL ?? self.useSSL
        let useUnixSocket = useUnixSocket ?? self.useUnixSocket

        var allHeaders = [String: String]()
        if  let headers = headers {
            for  (headerName, headerValue) in headers {
                allHeaders[headerName] = headerValue
            }
        }
        if allHeaders["Content-Type"] == nil {
            allHeaders["Content-Type"] = "text/plain"
        }

        let schema = useSSL ? "https" : "http"
        var options: [ClientRequest.Options] =
            [.method(method), .schema(schema), .hostname("localhost"), .path(path), .headers(allHeaders)]
        if useSSL {
            options.append(.disableSSLVerification)
        }
        if !followRedirects {
            options.append(.maxRedirects(0))
        }
        let req: ClientRequest
        if useUnixSocket {
            req = HTTP.request(options, unixDomainSocketPath: socketPath, callback: callback)
        } else {
            options.append(.port(UInt16(port).toInt16()))
            req = HTTP.request(options, callback: callback)
        }
        if let requestModifier = requestModifier {
            requestModifier(req)
        }
        req.end(close: true)
    }

    func expectation(line: Int, index: Int) -> XCTestExpectation {
        return self.expectation(description: "\(type(of: self)):\(line)[\(index)](ssl:\(useSSL))")
    }

    // Generates a unique temporary file path suitable for use as a Unix domain socket.
    // On Linux, a path is returned within /tmp
    // On MacOS, a path is returned within /var/folders
    func uniqueTemporaryFilePath() -> String {
        #if os(Linux)
        let temporaryDirectory = "/tmp"
        #else
        var temporaryDirectory: String
        if #available(OSX 10.12, *) {
            temporaryDirectory = FileManager.default.temporaryDirectory.path
        } else {
            temporaryDirectory = "/tmp"
        }
        #endif
        return temporaryDirectory + "/KituraTest." + String(ProcessInfo.processInfo.globallyUniqueString.prefix(20))
    }

    // Delete a temporary file path.
    func removeTemporaryFilePath(_ path: String) {
        let fileURL = URL(fileURLWithPath: path)
        let fm = FileManager.default
        guard fm.fileExists(atPath: fileURL.path) else {
            // Kitura-NIO automatically cleans itself.  So currently the item will be removed by the time this function is called.
            return
        }

        do {
            try fm.removeItem(at: fileURL)
        } catch {
            XCTFail("Unable to remove \(path): \(error.localizedDescription)")
        }
    }
}

fileprivate extension UInt16 {
    func toInt16() -> Int16 {
        return Int16(bitPattern: self)
    }
}