IBM-Swift/Kitura

View on GitHub
Tests/KituraTests/TestStaticFileServer.swift

Summary

Maintainability
F
5 days
Test Coverage
/**
 * Copyright IBM Corporation 2016,2017
 *
 * 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 Foundation

@testable import Kitura
@testable import KituraNet

#if os(Linux)
import Glibc
#else
import Darwin
#endif

final class TestStaticFileServer: KituraTest, KituraTestSuite {

    static var allTests: [(String, (TestStaticFileServer) -> () throws -> Void)] {
        return [
            ("testFileServer", testFileServer),
            ("testGetWithWhiteSpaces", testGetWithWhiteSpaces),
            ("testGetWithSpecialCharacters", testGetWithSpecialCharacters),
            ("testGetWithSpecialCharactersEncoded", testGetWithSpecialCharactersEncoded),
            ("testWelcomePageCanBeDisabled", testWelcomePageCanBeDisabled),
            ("testGetKituraResource", testGetKituraResource),
            ("testGetDefaultResponse", testGetDefaultResponse),
            ("testGetMissingKituraResource", testGetMissingKituraResource),
            ("testGetTraversedFileKituraResource", testGetTraversedFileKituraResource),
            ("testGetTraversedFile", testGetTraversedFile),
            ("testAbsolutePathFunction", testAbsolutePathFunction),
            ("testAbsoluteRootPath", testAbsoluteRootPath),
            ("testSubRouterStaticFileServer", testSubRouterStaticFileServer),
            ("testSubRouterSubFolderStaticFileServer", testSubRouterSubFolderStaticFileServer),
            ("testSubRouterStaticFileServerRedirect", testSubRouterStaticFileServerRedirect),
            ("testSubRouterSubFolderStaticFileServerRedirect", testSubRouterSubFolderStaticFileServerRedirect),
            ("testParameterizedSubRouterSubFolderStaticFileServer", testParameterizedSubRouterSubFolderStaticFileServer),
            ("testParameterizedSubRouterSubFolderStaticFileServerRedirect", testParameterizedSubRouterSubFolderStaticFileServerRedirect),
            ("testRangeRequests", testRangeRequests),
            ("testRangeRequestsWithLargeLastBytePos", testRangeRequestsWithLargeLastBytePos),
            ("testRangeRequestIsIgnoredOnOptionOff", testRangeRequestIsIgnoredOnOptionOff),
            ("testRangeRequestIsIgnoredOnNonGetMethod", testRangeRequestIsIgnoredOnNonGetMethod),
            ("testDataIsNotCorrupted", testDataIsNotCorrupted),
            ("testRangeRequestsWithMultipleRanges", testRangeRequestsWithMultipleRanges),
            ("testRangeRequestWithNotSatisfiableRange", testRangeRequestWithNotSatisfiableRange),
            ("testRangeRequestWithSintacticallyInvalidRange", testRangeRequestWithSintacticallyInvalidRange),
            ("testRangeRequestWithIfRangeHeaderWithETag", testRangeRequestWithIfRangeHeaderWithETag),
            ("testRangeRequestWithIfRangeHeaderWithOldETag", testRangeRequestWithIfRangeHeaderWithOldETag),
            ("testRangeRequestWithIfRangeHeaderAsLastModified", testRangeRequestWithIfRangeHeaderAsLastModified),
            ("testRangeRequestWithIfRangeHeaderAsOldLastModified", testRangeRequestWithIfRangeHeaderAsOldLastModified),
            ("testStaticFileServerRedirectPreservingQueryParams", testStaticFileServerRedirectPreservingQueryParams),
            ("testFallbackToDefaultIndex", testFallbackToDefaultIndex),
            ("testFallbackToDefaultIndexFailsIfOptionNotSet", testFallbackToDefaultIndexFailsIfOptionNotSet),
            ("testFallbackToDefaultIndexWithSubrouter", testFallbackToDefaultIndexWithSubrouter),
        ]
    }

    let router = TestStaticFileServer.setupRouter()
    let routerWithoutWelcome = TestStaticFileServer.setupRouter(enableWelcomePage: false)

    func testFileServer() {
        performServerTest(router, asyncTasks: { expectation in
            self.performRequest("get", path:"/qwer", callback: {response in
                XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil")
                XCTAssertEqual(response?.statusCode, HTTPStatusCode.OK, "HTTP Status code was \(String(describing: response?.statusCode))")
                do {
                    let body = try response?.readString()
                    XCTAssertEqual(body, "<!DOCTYPE html><html><body><b>Index</b></body></html>\n")
                } catch {
                    XCTFail("No response body")
                }

                XCTAssertEqual(response?.headers["x-custom-header"]?.first, "Kitura")
                XCTAssertNotNil(response?.headers["Last-Modified"])
                XCTAssertNotNil(response?.headers["Etag"])
                XCTAssertEqual(response?.headers["Cache-Control"]?.first, "max-age=2")
                expectation.fulfill()
            })
        }, { expectation in
            self.performRequest("get", path:"/qwer/index.html", callback: {response in
                XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil")
                XCTAssertEqual(response?.statusCode, HTTPStatusCode.OK, "HTTP Status code was \(String(describing: response?.statusCode))")
                do {
                    let body = try response?.readString()
                    XCTAssertEqual(body, "<!DOCTYPE html><html><body><b>Index</b></body></html>\n")
                } catch {
                    XCTFail("No response body")
                }
                expectation.fulfill()
            })
        }, { expectation in
            self.performRequest("get", path:"/qwer/index", callback: {response in
                XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil")
                XCTAssertEqual(response?.statusCode, HTTPStatusCode.OK, "HTTP Status code was \(String(describing: response?.statusCode))")
                do {
                    let body = try response?.readString()
                    XCTAssertEqual(body, "<!DOCTYPE html><html><body><b>Index</b></body></html>\n")
                } catch {
                    XCTFail("No response body")
                }
                expectation.fulfill()
            })
            }, { expectation in
                self.performRequest("get", path:"/zxcv/index.html", callback: {response in
                    XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil")
                    XCTAssertEqual(response?.statusCode, HTTPStatusCode.OK, "HTTP Status code was \(String(describing: response?.statusCode))")
                    do {
                        let body = try response?.readString()
                        XCTAssertEqual(body, "<!DOCTYPE html><html><body><b>Index</b></body></html>\n")
                    } catch {
                        XCTFail("No response body")
                    }
                    XCTAssertNil(response?.headers["x-custom-header"])
                    XCTAssertNil(response?.headers["Last-Modified"])
                    XCTAssertNil(response?.headers["Etag"])
                    XCTAssertEqual(response?.headers["Cache-Control"]?.first, "max-age=0")
                    expectation.fulfill()
                })
            }, { expectation in
                self.performRequest("get", path:"/zxcv", callback: {response in
                    XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil")
                    XCTAssertEqual(response?.statusCode, HTTPStatusCode.notFound, "HTTP Status code was \(String(describing: response?.statusCode))")
                    expectation.fulfill()
                })
            }, { expectation in
                self.performRequest("get", path:"/zxcv/index", callback: {response in
                    XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil")
                    XCTAssertEqual(response?.statusCode, HTTPStatusCode.notFound, "HTTP Status code was \(String(describing: response?.statusCode))")
                    expectation.fulfill()
                })
            }, { expectation in
                self.performRequest("get", path:"/asdf", callback: {response in
                    XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil")
                    XCTAssertEqual(response?.statusCode, HTTPStatusCode.notFound, "HTTP Status code was \(String(describing: response?.statusCode))")
                    expectation.fulfill()
                })
            }, { expectation in
                self.performRequest("put", path:"/asdf", callback: {response in
                    XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil")
                    XCTAssertEqual(response?.statusCode, HTTPStatusCode.notFound, "HTTP Status code was \(String(describing:response?.statusCode))")
                    expectation.fulfill()
                })
            }, { expectation in
                self.performRequest("get", path:"/asdf/", callback: {response in
                    XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil")
                    XCTAssertEqual(response?.statusCode, HTTPStatusCode.OK, "HTTP Status code was \(String(describing: response?.statusCode))")
                    do {
                        let body = try response?.readString()
                        XCTAssertEqual(body, "<!DOCTYPE html><html><body><b>Index</b></body></html>\n")
                    } catch {
                        XCTFail("No response body")
                    }
                    XCTAssertNil(response?.headers["x-custom-header"])
                    XCTAssertNotNil(response?.headers["Last-Modified"])
                    XCTAssertNotNil(response?.headers["Etag"])
                    XCTAssertEqual(response?.headers["Cache-Control"]?.first, "max-age=0")
                    expectation.fulfill()
                })
        })
    }

    static func servingPathPrefix() -> String {
        // this file is at
        // <original repository directory>/Tests/KituraTests/TestStaticFileServer.swift
        // the original repository directory is 3 path components up
        let currentFilePath = #file

        let pathComponents = currentFilePath.split(separator: "/").map(String.init)

        // We need to check whether we have an edited Kitura package, this will be seen from a path containing Packages and Kitura at the relevant indexes
        let expectedKituraIndex = pathComponents.count - 4
        let expectedPackagesIndex = pathComponents.count - 5
        if pathComponents[expectedKituraIndex] == "Kitura"
            && pathComponents[expectedPackagesIndex] == "Packages" {
            return "./Packages/Kitura/"
        } else {
            return "./"
        }
    }

    static func setupRouter(enableWelcomePage: Bool = true) -> Router {
        let router = Router(enableWelcomePage: enableWelcomePage)

        if !enableWelcomePage {
            // Testing the default welcome page can be disabled does not require a `StaticFileServer` to be configured.
            return router
        }

        // The route below ensures that the static file server does not prevent all routes being walked
        router.all("/", middleware: StaticFileServer())

        var cacheOptions = StaticFileServer.CacheOptions(maxAgeCacheControlHeader: 2)
        var options = StaticFileServer.Options(possibleExtensions: ["exe", "html"], cacheOptions: cacheOptions)
        router.all("/qwer", middleware: StaticFileServer(path: servingPathPrefix() + "Tests/KituraTests/TestStaticFileServer/", options:options, customResponseHeadersSetter: HeaderSetter()))

        cacheOptions = StaticFileServer.CacheOptions(addLastModifiedHeader: false, generateETag: false)
        options = StaticFileServer.Options(serveIndexForDirectory: false, cacheOptions: cacheOptions)
        router.all("/zxcv", middleware: StaticFileServer(path: servingPathPrefix() + "Tests/KituraTests/TestStaticFileServer/", options:options))

        options = StaticFileServer.Options(redirect: false)
        let directoryURL = URL(fileURLWithPath: #file + "/../TestStaticFileServer").standardizedFileURL
        router.all("/asdf", middleware: StaticFileServer(path: directoryURL.path, options:options))

        options = StaticFileServer.Options(possibleExtensions: ["exe", "html"], cacheOptions: cacheOptions, acceptRanges: false)
        router.all("/tyui", middleware: StaticFileServer(path: servingPathPrefix() + "Tests/KituraTests/TestStaticFileServer/", options:options, customResponseHeadersSetter: HeaderSetter()))
        
        options = StaticFileServer.Options(serveIndexForDirectory: true, redirect: true, cacheOptions: cacheOptions)
        router.route("/ghjk").all(middleware: StaticFileServer(path: servingPathPrefix() + "Tests/KituraTests/TestStaticFileServer/", options: options))
        
        options = StaticFileServer.Options(serveIndexForDirectory: true, redirect: true, cacheOptions: cacheOptions)
        router.route("/opnm/:parameter").all(middleware: StaticFileServer(path: servingPathPrefix() + "Tests/KituraTests/TestStaticFileServer/subfolder", options: options))

        options = StaticFileServer.Options(serveIndexForDirectory: true, redirect: true)
        router.route("/queryparams").all(middleware: StaticFileServer(path: servingPathPrefix() + "Tests/KituraTests/TestStaticFileServer/", options: options))

        return router
    }

    class HeaderSetter: ResponseHeadersSetter {
        func setCustomResponseHeaders(response: RouterResponse, filePath: String, fileAttributes: [FileAttributeKey : Any]) {
            response.headers["x-custom-header"] = "Kitura"
        }
    }

    private typealias BodyChecker =  (String) -> Void
    private func runGetResponseTest(path: String, expectedResponseText: String? = nil,
                                    expectedStatusCode: HTTPStatusCode = HTTPStatusCode.OK,
                                    bodyChecker: BodyChecker? = nil,
                                    withRouter: Router? = nil) {
        performServerTest(withRouter ?? router) { expectation in
            self.performRequest("get", path: path, callback: { response in
                guard let response = response else {
                    XCTFail("ClientRequest response object was nil")
                    expectation.fulfill()
                    return
                }
                XCTAssertEqual(response.statusCode, expectedStatusCode,
                               "No success status code returned")

                if let body = (try? response.readString()).flatMap({ $0 }) {
                    if let expectedResponseText = expectedResponseText {
                        XCTAssertEqual(body, expectedResponseText, "mismatch in body")
                    }
                    bodyChecker?(body)
                } else {
                    XCTFail("No response body")
                }
                expectation.fulfill()
            })
        }
    }

    func testGetWithWhiteSpaces() {
        runGetResponseTest(path: "/qwer/index%20with%20whitespace.html", expectedResponseText: "<!DOCTYPE html><html><body><b>Index with whitespace</b></body></html>\n")
    }

    func testGetWithSpecialCharacters() {
        runGetResponseTest(path: "/qwer/index+@,.html", expectedResponseText: "<!DOCTYPE html><html><body><b>Index with plus at comma</b></body></html>\n")
    }

    func testGetWithSpecialCharactersEncoded() {
        runGetResponseTest(path: "/qwer/index%2B%40%2C.html", expectedResponseText: "<!DOCTYPE html><html><body><b>Index with plus at comma</b></body></html>\n")
    }

    func testWelcomePageCanBeDisabled() {
        runGetResponseTest(path: "/", expectedStatusCode: HTTPStatusCode.notFound, withRouter: routerWithoutWelcome)
    }

    func testGetKituraResource() {
        runGetResponseTest(path: "/@@Kitura-router@@/")
    }

    func testGetDefaultResponse() {
        runGetResponseTest(path: "/")
    }

    func testGetMissingKituraResource() {
        runGetResponseTest(path: "/@@Kitura-router@@/missing.file", expectedStatusCode: HTTPStatusCode.notFound)
    }

    func testGetTraversedFileKituraResource() {
        runGetResponseTest(path: "/@@Kitura-router@@/../../../Tests/KituraTests/TestStaticFileServer.swift", expectedStatusCode: HTTPStatusCode.notFound)
    }

    func testGetTraversedFile() {
        runGetResponseTest(path: "../Tests/KituraTests/TestStaticFileServer.swift", expectedStatusCode: HTTPStatusCode.notFound)
    }

    func testAbsolutePathFunction() {
        XCTAssertEqual(StaticFileServer.ResourcePathHandler.getAbsolutePath(for: "/"), "/", "Absolute path did not resolve to system root")
    }

    func testAbsoluteRootPath() {
        XCTAssertEqual(StaticFileServer(path: "/").absoluteRootPath, "/", "Absolute root path did not resolve to system root")
    }

    let indexHtmlContents = "<!DOCTYPE html><html><body><b>Index</b></body></html>" // contents of index.html
    let subfolderIndexHtmlContents = "<!DOCTYPE html><html><body><b>Sub Folder Index</b></body></html>" // contents of subfolder/index.html
    let indexHtmlCount = 54 // index.html file data length

    func testSubRouterStaticFileServer() {
        runGetResponseTest(path: "/ghjk/", expectedResponseText: indexHtmlContents + "\n")
    }
    
    func testSubRouterSubFolderStaticFileServer() {
        runGetResponseTest(path: "/ghjk/subfolder/", expectedResponseText: subfolderIndexHtmlContents + "\n")
    }
    
    func testSubRouterStaticFileServerRedirect() {
        runGetResponseTest(path: "/ghjk", expectedResponseText: indexHtmlContents + "\n")
    }
    
    func testSubRouterSubFolderStaticFileServerRedirect() {
        runGetResponseTest(path: "/ghjk/subfolder", expectedResponseText: subfolderIndexHtmlContents + "\n")
    }
    
    func testParameterizedSubRouterSubFolderStaticFileServer() {
        runGetResponseTest(path: "/opnm/xxxx/", expectedResponseText: subfolderIndexHtmlContents + "\n")
    }
    
    func testParameterizedSubRouterSubFolderStaticFileServerRedirect() {
        runGetResponseTest(path: "/opnm/xxxx", expectedResponseText: subfolderIndexHtmlContents + "\n")
    }
    
    func testRangeRequests() {
        let requestingBytes = 10
        performServerTest(router) { expectation in
            self.performRequest("get", path: "/qwer/index.html", callback: { response in
                XCTAssertNotNil(response)
                XCTAssertEqual(response?.statusCode, HTTPStatusCode.partialContent)
                XCTAssertEqual(response?.headers["Content-Range"]?.first, "bytes 0-\(requestingBytes)/\(self.indexHtmlCount)")
                XCTAssertEqual(response?.headers["Accept-Ranges"]?.first, "bytes")
                XCTAssertEqual(response?.headers["Content-Length"]?.first, "11")
                var bodyData = Data()
                _ = try? response?.readAllData(into: &bodyData)
                XCTAssertEqual(bodyData.count, requestingBytes + 1)
                expectation.fulfill()
            }, headers: ["Range": "bytes=0-\(requestingBytes)"])
        }
    }

    func testRangeRequestsWithLargeLastBytePos() {
        performServerTest(router) { expectation in
            self.performRequest("get", path: "/qwer/index.html", callback: { response in
                XCTAssertNotNil(response)
                XCTAssertEqual(response?.statusCode, HTTPStatusCode.partialContent)
                XCTAssertEqual(response?.headers["Content-Range"]?.first, "bytes 2-53/54")
                XCTAssertEqual(response?.headers["Accept-Ranges"]?.first, "bytes")
                XCTAssertEqual(response?.headers["Content-Length"]?.first, "52")
                var bodyData = Data()
                _ = try? response?.readAllData(into: &bodyData)
                XCTAssertEqual(bodyData.count, 52)
                expectation.fulfill()
            }, headers: ["Range": "bytes=2-100"])
        }
    }

    func testRangeRequestIsIgnoredOnOptionOff() {
        performServerTest(router) { expectation in
            // static server for "/tyui" has the range option off
            self.performRequest("get", path: "/tyui/index.html", callback: { response in
                XCTAssertNotNil(response)
                XCTAssertEqual(response?.statusCode, HTTPStatusCode.OK)
                var bodyData = Data()
                _ = try? response?.readAllData(into: &bodyData)
                XCTAssertEqual(bodyData.count, self.indexHtmlCount)
                XCTAssertEqual(response?.headers["Accept-Ranges"]?.first, "none")
                expectation.fulfill()
            }, headers: ["Range": "bytes=0-10"])
        }
    }

    func testRangeRequestIsIgnoredOnNonGetMethod() {
        performServerTest(router) { expectation in
            self.performRequest("head", path: "/qwer/index.html", callback: { response in
                XCTAssertNotNil(response)
                XCTAssertEqual(response?.headers["Accept-Ranges"]?.first, "bytes")
                // Range requests should be ignored on non GET method
                // In this case we expect status code 200, no Content-Range and no body since it is a HEAD request
                XCTAssertEqual(response?.statusCode, HTTPStatusCode.OK)
                XCTAssertNil(response?.headers["Content-Range"])
                let bodyString = (try? response?.readString()).flatMap({ $0 })
                XCTAssertNil(bodyString)
                expectation.fulfill()
            }, headers: ["Range": "bytes=0-10"])
        }
    }

    func testDataIsNotCorrupted() {
        // Corrupted files will have more bytes or less bytes than required
        // So we check the file is intact after reconstructing it (after various range requests)
        performServerTest(router) { expectation in
            self.performRequest("get", path: "/qwer/index.html", callback: { response in
                XCTAssertNotNil(response)
                XCTAssertEqual(response?.statusCode, HTTPStatusCode.OK)
                XCTAssertNil(response?.headers["Content-Range"]?.first)
                // Original file:
                var original = Data()
                _ = try? response?.readAllData(into: &original)

                self.performRequest("get", path: "/qwer/index.html", callback: { response in
                    XCTAssertNotNil(response)
                    XCTAssertEqual(response?.statusCode, HTTPStatusCode.partialContent)
                    XCTAssertEqual(response?.headers["Content-Range"]?.first, "bytes 0-10/\(self.indexHtmlCount)")
                    // First 11 bytes
                    var reconstructed = Data()
                    _ = try? response?.readAllData(into: &reconstructed)

                    self.performRequest("get", path: "/qwer/index.html", callback: { response in
                        XCTAssertNotNil(response)
                        XCTAssertEqual(response?.statusCode, HTTPStatusCode.partialContent)
                        XCTAssertEqual(response?.headers["Content-Range"]?.first, "bytes 11-\(self.indexHtmlCount-1)/\(self.indexHtmlCount)")
                        // 12th bytes and later
                        var part2 = Data()
                        _ = try? response?.readAllData(into: &part2)

                        // Reconstruct data
                        reconstructed.append(part2)

                        // Check both datas are the same
                        XCTAssertEqual(reconstructed.count, original.count)
                        if reconstructed.count == original.count {
                            for i in 0..<original.count {
                                XCTAssertEqual(reconstructed[i], original[i])
                            }
                        }
                    }, headers: ["Range": "bytes=11-"])
                }, headers: ["Range": "bytes=0-10"])
                expectation.fulfill()
            })
        }
    }

    #if os(Linux) && !swift(>=3.2)
    typealias NSTextCheckingResult = TextCheckingResult
    #endif

    /// Helper function to assert a regex pattern and returns matched groups
    func assertMatch(_ target: String?, _ pattern: String, matchedGroups: inout [String], file: StaticString = #file, line: UInt = #line) {
        guard let regex = try? NSRegularExpression(pattern: pattern, options: [])else {
            return XCTFail("invalid pattern: \(pattern)", file: file, line: line)
        }
        guard let target = target else {
            return XCTFail("target string is nil")
        }
        let matches = regex.matches(in: target, options: [], range: NSRange(location: 0, length: target.count))
        if matches.isEmpty {
            XCTFail("target string didn't match", file: file, line: line)
        } else {
            let match = matches.first!
            let nsstring = NSString(string: target)
            for index in 0..<match.numberOfRanges {
                #if !os(Linux) && !swift(>=3.2)
                    let range = match.rangeAt(index)
                #else
                    let range = match.range(at: index)
                #endif
                if  range.location != NSNotFound  &&  range.location != -1 {
                    matchedGroups.append(nsstring.substring(with: range))
                }
            }
        }
    }

    func testRangeRequestsWithMultipleRanges() {
        performServerTest(router) { expectation in
            self.performRequest("get", path: "/qwer/index.html", callback: { response in
                defer {
                    expectation.fulfill()
                }
                XCTAssertEqual(response?.statusCode, HTTPStatusCode.partialContent)

                // Assert required headers in multipart/bytesranges
                var capturedGroups: [String] = []
                XCTAssertEqual(response?.headers["Accept-Ranges"]?.first, "bytes")
                self.assertMatch(response?.headers["Content-Type"]?.first, "multipart\\/byteranges; boundary=(.+)", matchedGroups: &capturedGroups)
                XCTAssertFalse(capturedGroups.isEmpty, "No captured groups, body and boundary tests will fail")
                guard capturedGroups.count > 1 else {
                    XCTFail("No boundary found in Content-Type header")
                    return
                }

                // Assert Content-Range is not present
                XCTAssertNil(response?.headers["Content-Range"]?.first)

                // Assert body
                var bodyData = Data()
                _ = try? response?.readAllData(into: &bodyData)
                XCTAssertTrue(bodyData.count > 0)

                // Assert body structure (Should be same as a regular multipart body)
                let bodyParser = MultiPartBodyParser(boundary: capturedGroups[1])
                guard let parsedBody = bodyParser.parse(bodyData) else {
                    XCTFail("parsedBody must not be nil")
                    return
                }
                switch parsedBody {
                case .multipart(let parts):
                    // Assert each part has the required headers and its data is of the desired length
                    XCTAssertEqual(parts.count, 2)
                    XCTAssertEqual(parts[0].headers[.contentRange], "Content-Range: bytes 0-10/\(self.indexHtmlCount)")
                    XCTAssertEqual(parts[0].headers[.type], "Content-Type: text/html")
                    let data0 = parts[0].body.asText?.data(using: .utf8)
                    XCTAssertEqual(data0?.count, 11)
                    XCTAssertEqual(parts[1].headers[.contentRange], "Content-Range: bytes 20-33/\(self.indexHtmlCount)")
                    XCTAssertEqual(parts[1].headers[.type], "Content-Type: text/html")
                    let data1 = parts[1].body.asText?.data(using: .utf8)
                    XCTAssertEqual(data1?.count, 14)
                default:
                    XCTFail("Multipart body was expected \(parsedBody)")
                }
            }, headers: ["Range": "bytes=0-10,20-33"])
        }
    }

    func testRangeRequestWithNotSatisfiableRange() {
        /// when the first- byte-pos of the range is greater than the current length
        performServerTest(router) { expectation in
            self.performRequest("get", path: "/qwer/index.html", callback: { response in
                XCTAssertNotNil(response)
                XCTAssertEqual(response?.headers["Accept-Ranges"]?.first, "bytes")
                XCTAssertEqual(response?.statusCode, HTTPStatusCode.requestedRangeNotSatisfiable)
                XCTAssertEqual(response?.headers["Content-Range"]?.first, "bytes */54")
                expectation.fulfill()
            }, headers: ["Range": "bytes=54-55"])
        }
    }

    func testRangeRequestWithSintacticallyInvalidRange() {
        performServerTest(router) { expectation in
            self.performRequest("get", path: "/qwer/index.html", callback: { response in
                XCTAssertNotNil(response)
                XCTAssertEqual(response?.headers["Accept-Ranges"]?.first, "bytes")
                XCTAssertEqual(response?.statusCode, HTTPStatusCode.OK)
                XCTAssertNil(response?.headers["Content-Range"]?.first)
                expectation.fulfill()
            }, headers: ["Range": "asdf"])
        }
    }

    func testRangeRequestWithIfRangeHeaderWithETag() {
        performServerTest(router) { expectation in
            self.performRequest("get", path: "/qwer/index.html", callback: { response in
                XCTAssertNotNil(response)
                XCTAssertEqual(response?.statusCode, HTTPStatusCode.OK)
                XCTAssertNotNil(response?.headers["Last-Modified"]?.first)
                guard let eTag = response?.headers["eTag"]?.first else {
                    XCTFail("eTag header was missing")
                    return expectation.fulfill()
                }

                // if ETag is the same then partial content (206) should be served
                self.performRequest("get", path: "/qwer/index.html", callback: { response in
                    XCTAssertNotNil(response)
                    XCTAssertEqual(response?.statusCode, HTTPStatusCode.partialContent)
                    XCTAssertEqual(response?.headers["Content-Range"]?.first, "bytes 0-10/\(self.indexHtmlCount)")
                    var data = Data()
                    _ = try? response?.readAllData(into: &data)
                    XCTAssertEqual(data.count, 11)
                }, headers: ["Range": "bytes=0-10", "If-Range": "\(eTag)"])
                expectation.fulfill()
            })
        }
    }

    func testRangeRequestWithIfRangeHeaderWithOldETag() {
        performServerTest(router) { expectation in
            // if ETag is NOT the same then the entire file (200) should be served
            self.performRequest("get", path: "/qwer/index.html", callback: { response in
                XCTAssertNotNil(response)
                XCTAssertEqual(response?.statusCode, HTTPStatusCode.OK)
                XCTAssertNil(response?.headers["Content-Range"]?.first
                )
            }, headers: ["Range": "bytes=0-10", "If-Range": "\"old-etag\""])
            expectation.fulfill()
        }
    }

    func testRangeRequestWithIfRangeHeaderAsLastModified() {
        performServerTest(router) { expectation in
            self.performRequest("get", path: "/qwer/index.html", callback: { response in
                XCTAssertNotNil(response)
                XCTAssertEqual(response?.statusCode, HTTPStatusCode.OK)
                guard let lastModified = response?.headers["Last-Modified"]?.first else {
                    XCTFail("Last-Modified header was missing")
                    return expectation.fulfill()
                }
                XCTAssertNotNil(response?.headers["eTag"]?.first)

                // if Last-Modified is the same then partial content (206) should be served
                self.performRequest("get", path: "/qwer/index.html", callback: { response in
                    XCTAssertNotNil(response)
                    XCTAssertEqual(response?.statusCode, HTTPStatusCode.partialContent)
                    XCTAssertEqual(response?.headers["Content-Range"]?.first, "bytes 0-10/\(self.indexHtmlCount)")
                    var data = Data()
                    _ = try? response?.readAllData(into: &data)
                    XCTAssertEqual(data.count, 11)
                }, headers: ["Range": "bytes=0-10", "If-Range": "\(lastModified)"])
                expectation.fulfill()
            })
        }
    }

    func testRangeRequestWithIfRangeHeaderAsOldLastModified() {
        // Range request with If-Range with etag
        performServerTest(router) { expectation in
            // if Last-Modified is NOT the same then the entire file (200) should be served
            self.performRequest("get", path: "/qwer/index.html", callback: { response in
                XCTAssertNotNil(response)
                XCTAssertEqual(response?.statusCode, HTTPStatusCode.OK)
                XCTAssertNil(response?.headers["Content-Range"]?.first)
            }, headers: ["Range": "bytes=0-10", "If-Range": "Wed, 01 Jan 2000 00:00:00 GMT"])
            expectation.fulfill()
        }
    }

    func testStaticFileServerRedirectPreservingQueryParams() {
        performServerTest(router) { expectation in
            self.performRequest("get", path: "/queryparams?a=b&c=d", followRedirects: false, callback: { response in
                defer {
                    expectation.fulfill()
                }
                guard let response = response else {
                    return XCTFail()
                }
                // We expect StaticFileServer to redirect us. In order to see what location header
                // has been sent, we have disabled following of redirects, so expect a 3xx response:
                XCTAssertEqual(response.statusCode.class, HTTPStatusCode.Class.redirection)
                guard let location = response.headers["Location"] else {
                    return XCTFail("Location header was missing")
                }
                XCTAssertEqual(location, ["/queryparams/?a=b&c=d"])

            })
        }
    }

    func testFallbackToDefaultIndex() {
        // This test verifies the fallback to the default index.html if the requested path
        // is not found. This feature is expected to be used by single file applications.
        let router = TestStaticFileServer.setupRouter(defaultIndex: "/index.html")
        performServerTest(router, asyncTasks: { expectation in
            self.performRequest("get", path:"/help/contact", callback: { response in
                XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil")
                XCTAssertEqual(response?.statusCode, HTTPStatusCode.OK, "HTTP Status code was \(String(describing: response?.statusCode))")
                do {
                    let body = try response?.readString()
                    XCTAssertEqual(body, "<!DOCTYPE html><html><body><b>Index</b></body></html>\n")
                } catch {
                    XCTFail("No response body")
                }
                expectation.fulfill()
            })
        })
    }

    func testFallbackToDefaultIndexFailsIfOptionNotSet() {
        let router = TestStaticFileServer.setupRouter(defaultIndex: nil)
        performServerTest(router, asyncTasks: { expectation in
            self.performRequest("get", path:"/help/contact", callback: { response in
                XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil")
                XCTAssertEqual(response?.statusCode, HTTPStatusCode.notFound, "HTTP Status code was \(String(describing: response?.statusCode))")
                expectation.fulfill()
            })
        })
    }

    func testFallbackToDefaultIndexWithSubrouter() {
        let router = Router(enableWelcomePage: true)
        let parent = router.route("/help")
        parent.all("/contact", middleware: StaticFileServer(
            path: TestStaticFileServer.servingPathPrefix() + "Tests/KituraTests/TestStaticFileServer/",
            options: StaticFileServer.Options(defaultIndex: "/index.html")))

        performServerTest(router, asyncTasks: { expectation in
            self.performRequest("get", path:"/help/contact/details", callback: { response in
                XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil")
                XCTAssertEqual(response?.statusCode, HTTPStatusCode.OK, "HTTP Status code was \(String(describing: response?.statusCode))")
                do {
                    let body = try response?.readString()
                    XCTAssertEqual(body, "<!DOCTYPE html><html><body><b>Index</b></body></html>\n")
                } catch {
                    XCTFail("No response body")
                }
                expectation.fulfill()
            })
        })
    }

    static func setupRouter(defaultIndex: String?) -> Router {
        let router = Router(enableWelcomePage: true)
        router.all("/help", middleware: StaticFileServer(
            path: servingPathPrefix() + "Tests/KituraTests/TestStaticFileServer/",
            options: StaticFileServer.Options(defaultIndex: defaultIndex))
        )
        return router
    }
}