├── Tests ├── .gitkeep ├── LinuxMain.swift └── VaporTestToolsTests │ ├── Helpers │ └── Application+Testing.swift │ └── AppTests.swift ├── Other └── logo.png ├── scripts ├── update.sh └── upgrade.sh ├── Sources ├── AppVaporTestTools │ ├── Model │ │ └── MyObject.swift │ ├── boot.swift │ ├── routes.swift │ └── configure.swift ├── VaporTestTools │ ├── Properties │ │ └── TestableProperty.swift │ ├── Extensions │ │ ├── Testable │ │ │ ├── String+Testable.swift │ │ │ ├── Encodable+Testable.swift │ │ │ ├── Decodable+Testable.swift │ │ │ └── Application+Testable.swift │ │ ├── Requests │ │ │ ├── Request+Make.swift │ │ │ ├── HTTPRequest+Response.swift │ │ │ ├── HTTPHeaders+Tools.swift │ │ │ └── HTTPRequest+Make.swift │ │ ├── Response tools │ │ │ ├── Response+Decoding.swift │ │ │ ├── Response+Debug.swift │ │ │ ├── Response+Getters.swift │ │ │ └── Response+Checks.swift │ │ └── Tests │ │ │ └── XCTestCase+Tests.swift │ └── TestTools.swift └── RunVaporTestTools │ └── main.swift ├── .gitignore ├── LICENSE ├── Package.swift └── README.md /Tests/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Other/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiveUI/VaporTestTools/HEAD/Other/logo.png -------------------------------------------------------------------------------- /scripts/update.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | rm -rf .build 4 | vapor clean -y --verbose 5 | vapor xcode -n --verbose 6 | -------------------------------------------------------------------------------- /scripts/upgrade.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | rm -rf .build 4 | vapor clean -y --verbose 5 | rm Package.resolved 6 | vapor xcode -n --verbose 7 | -------------------------------------------------------------------------------- /Sources/AppVaporTestTools/Model/MyObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyObject.swift 3 | // App 4 | // 5 | // Created by Ondrej Rafaj on 27/02/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | 11 | struct MyObject: Content { 12 | let code: String 13 | } 14 | -------------------------------------------------------------------------------- /Sources/AppVaporTestTools/boot.swift: -------------------------------------------------------------------------------- 1 | import Routing 2 | import Vapor 3 | 4 | /// Called after your application has initialized. 5 | /// 6 | /// [Learn More →](https://docs.vapor.codes/3.0/getting-started/structure/#bootswift) 7 | public func boot(_ app: Application) throws { 8 | // your code here 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/vapor 3 | 4 | ### Vapor ### 5 | Config/secrets 6 | 7 | ### Vapor Patch ### 8 | Packages 9 | .build 10 | xcuserdata 11 | *.xcodeproj 12 | DerivedData/ 13 | .DS_Store 14 | 15 | # End of https://www.gitignore.io/api/vapor 16 | /Package.resolved 17 | /.swiftpm 18 | -------------------------------------------------------------------------------- /Sources/AppVaporTestTools/routes.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Routing 3 | import Vapor 4 | 5 | /// Register your application's routes here. 6 | /// 7 | /// [Learn More →](https://docs.vapor.codes/3.0/getting-started/structure/#routesswift) 8 | public func routes(_ router: Router) throws { 9 | // Basic "Hello, world!" example 10 | router.get("hello") { req in 11 | return "Hello, world!" 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /Sources/VaporTestTools/Properties/TestableProperty.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestableProperty.swift 3 | // VaporTestTools 4 | // 5 | // Created by Ondrej Rafaj on 27/02/2018. 6 | // 7 | 8 | @_exported import Foundation 9 | 10 | 11 | public struct TestableProperty { 12 | 13 | /// Testable element accessor 14 | public var element: TestableType 15 | 16 | init(_ obj: TestableType) { 17 | element = obj 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /Sources/VaporTestTools/Extensions/Testable/String+Testable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Testable.swift 3 | // VaporTestTools 4 | // 5 | // Created by Ondrej Rafaj on 27/02/2018. 6 | // 7 | 8 | @_exported import Foundation 9 | 10 | 11 | extension TestableProperty where TestableType == String { 12 | 13 | /// Return data in utf8 encoding 14 | public func asData() -> Data? { 15 | let data = element.data(using: .utf8) 16 | return data 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /Sources/VaporTestTools/Extensions/Requests/Request+Make.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Request+Make.swift 3 | // VaporTestTools 4 | // 5 | // Created by Ondrej Rafaj on 27/02/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | 11 | 12 | extension TestableProperty where TestableType: Request { 13 | 14 | /// HTTPRequest access from request 15 | public static var http: TestableProperty.Type { 16 | return TestableProperty.self 17 | } 18 | 19 | } 20 | 21 | -------------------------------------------------------------------------------- /Sources/VaporTestTools/Extensions/Testable/Encodable+Testable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Encodable+Testable.swift 3 | // VaporTestTools 4 | // 5 | // Created by Ondrej Rafaj on 27/02/2018. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | extension TestableProperty where TestableType: Encodable { 12 | 13 | /// Convert to Data 14 | public func asData() -> Data? { 15 | let encoder = JSONEncoder() 16 | let jsonData = try? encoder.encode(element) 17 | return jsonData 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /Sources/VaporTestTools/Extensions/Response tools/Response+Decoding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Response+Decoding.swift 3 | // VaporTestTools 4 | // 5 | // Created by Ondrej Rafaj on 27/02/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | 11 | 12 | extension TestableProperty where TestableType: Response { 13 | 14 | /// Decode returned content 15 | public func content(as type: T.Type) -> T? where T: Decodable { 16 | let object = try? element.content.decode(type).wait() 17 | return object 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /Sources/VaporTestTools/Extensions/Requests/HTTPRequest+Response.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPRequest+Response.swift 3 | // VaporTestTools 4 | // 5 | // Created by Ondrej Rafaj on 27/02/2018. 6 | // 7 | 8 | @_exported import Foundation 9 | @_exported import Vapor 10 | @_exported import NIO 11 | 12 | 13 | extension TestableProperty where TestableType == HTTPRequest { 14 | 15 | /// Make response 16 | func response(using app: Application) -> Response { 17 | let responder = try! app.make(Responder.self) 18 | let wrappedRequest = Request(http: element, using: app) 19 | return try! responder.respond(to: wrappedRequest).wait() 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /Tests/VaporTestToolsTests/Helpers/Application+Testing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Application+Testing.swift 3 | // AppTests 4 | // 5 | // Created by Ondrej Rafaj on 27/02/2018. 6 | // 7 | 8 | import Foundation 9 | import AppVaporTestTools 10 | import Vapor 11 | import VaporTestTools 12 | 13 | 14 | extension TestableProperty where TestableType: Application { 15 | 16 | public static func newTestApp() -> Application { 17 | let app = new({ (config, env, services) in 18 | try! AppVaporTestTools.configure(&config, &env, &services) 19 | }) { (router) in 20 | 21 | } 22 | try! AppVaporTestTools.boot(app) 23 | return app 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /Sources/VaporTestTools/Extensions/Requests/HTTPHeaders+Tools.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPHeaders+Tools.swift 3 | // VaporTestTools 4 | // 5 | // Created by Ondrej Rafaj on 28/02/2018. 6 | // 7 | 8 | @_exported import Foundation 9 | @_exported import Vapor 10 | 11 | 12 | extension TestableProperty where TestableType == Dictionary { 13 | 14 | /// Converts dictionary into HTTPHeaders 15 | func asHTTPHeaders() -> HTTPHeaders { 16 | var headersObject = HTTPHeaders() 17 | for key in element.keys { 18 | let value = element[key]! 19 | headersObject.replaceOrAdd(name: key, value: value) 20 | } 21 | return headersObject 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /Sources/RunVaporTestTools/main.swift: -------------------------------------------------------------------------------- 1 | import AppVaporTestTools 2 | import Service 3 | import Vapor 4 | import Foundation 5 | 6 | // The contents of main are wrapped in a do/catch block because any errors that get raised to the top level will crash Xcode 7 | do { 8 | var config = Config.default() 9 | var env = try Environment.detect() 10 | var services = Services.default() 11 | 12 | try AppVaporTestTools.configure(&config, &env, &services) 13 | 14 | let app = try Application( 15 | config: config, 16 | environment: env, 17 | services: services 18 | ) 19 | 20 | try AppVaporTestTools.boot(app) 21 | 22 | try app.run() 23 | } catch { 24 | print(error) 25 | exit(1) 26 | } 27 | -------------------------------------------------------------------------------- /Sources/VaporTestTools/Extensions/Tests/XCTestCase+Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCTestCase+Tests.swift 3 | // BoostPackageDescription 4 | // 5 | // Created by Ondrej Rafaj on 27/02/2018. 6 | // 7 | 8 | import Foundation 9 | import XCTest 10 | 11 | 12 | extension XCTestCase { 13 | 14 | // func testLinuxTestSuiteIncludesAllTests() { 15 | // #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) 16 | // let thisClass = type(of: self) 17 | // let linuxCount = thisClass.allTests.count 18 | // let darwinCount = Int(thisClass.defaultTestSuite.testCaseCount) 19 | // XCTAssertEqual(linuxCount, darwinCount, "\(darwinCount - linuxCount) tests are missing from allTests") 20 | // #endif 21 | // } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /Sources/AppVaporTestTools/configure.swift: -------------------------------------------------------------------------------- 1 | 2 | import Vapor 3 | 4 | /// Called before your application initializes. 5 | /// 6 | /// [Learn More →](https://docs.vapor.codes/3.0/getting-started/structure/#configureswift) 7 | public func configure( 8 | _ config: inout Config, 9 | _ env: inout Environment, 10 | _ services: inout Services 11 | ) throws { 12 | // Register routes to the router 13 | let router = EngineRouter.default() 14 | try routes(router) 15 | services.register(router, as: Router.self) 16 | 17 | // Register middleware 18 | var middlewares = MiddlewareConfig() // Create _empty_ middleware config 19 | middlewares.use(ErrorMiddleware.self) // Catches errors and converts to HTTP response 20 | services.register(middlewares) 21 | 22 | // Configure the rest of your application here 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 LiveUI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Sources/VaporTestTools/Extensions/Response tools/Response+Debug.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Response+Debug.swift 3 | // VaporTestTools 4 | // 5 | // Created by Ondrej Rafaj on 27/02/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | 11 | 12 | extension TestableProperty where TestableType: Response { 13 | 14 | /// Prints out useful information out the response 15 | public func debug() { 16 | print("Debugging response:") 17 | print("HTTP [\(element.http.version.major).\(element.http.version.minor)] with status code [\(element.http.status.code)]") 18 | print("Headers:") 19 | for header in element.http.headers { 20 | print("\t\(header.name.description) = \(header.value)") 21 | } 22 | print("Content:") 23 | if let size = element.testable.contentSize { 24 | print("\tSize: \(String(size))") 25 | } 26 | if let mediaType = element.http.contentType { 27 | print("\tMedia type: \(mediaType.description)") 28 | } 29 | if let stringContent = element.testable.contentString { 30 | print("\tContent:\n\(stringContent)") 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.0 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "VaporTestTools", 6 | products: [ 7 | .library( 8 | name: "VaporTestTools", 9 | targets: ["VaporTestTools"] 10 | ), 11 | ], 12 | dependencies: [ 13 | .package(url: "https://github.com/apple/swift-nio.git", from: "1.8.0"), 14 | .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0") 15 | ], 16 | targets: [ 17 | .target( 18 | name: "VaporTestTools", 19 | dependencies: [ 20 | "Vapor" 21 | ] 22 | ), 23 | .target( 24 | name: "AppVaporTestTools", 25 | dependencies: [ 26 | "Vapor" 27 | ] 28 | ), 29 | .target( 30 | name: "RunVaporTestTools", 31 | dependencies: [ 32 | "AppVaporTestTools" 33 | ] 34 | ), 35 | .testTarget( 36 | name: "VaporTestToolsTests", 37 | dependencies: [ 38 | "NIO", 39 | "Vapor", 40 | "AppVaporTestTools", 41 | "VaporTestTools" 42 | ] 43 | ) 44 | ] 45 | ) 46 | 47 | -------------------------------------------------------------------------------- /Sources/VaporTestTools/TestTools.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestTools.swift 3 | // VaporTestTools 4 | // 5 | // Created by Ondrej Rafaj on 27/02/2018. 6 | // 7 | 8 | @_exported import Foundation 9 | @_exported import Vapor 10 | 11 | 12 | // MARK: Testable 13 | 14 | /// Testable protocol 15 | public protocol Testable { 16 | /// Supported testable type 17 | associatedtype ObjectType 18 | 19 | /// Main property to access VaporTestTools functionality on supported projects 20 | var testable: TestableProperty { get } 21 | 22 | /// Main static property to access VaporTestTools functionality on supported projects 23 | static var testable: TestableProperty.Type { get } 24 | } 25 | 26 | 27 | extension Testable { 28 | 29 | /// Main property to access VaporTestTools functionality on supported projects 30 | public var testable: TestableProperty { 31 | return TestableProperty(self) 32 | } 33 | 34 | /// Main static property to access VaporTestTools functionality on supported projects 35 | public static var testable: TestableProperty.Type { 36 | return TestableProperty.self 37 | } 38 | 39 | } 40 | 41 | // MARK: Supported types 42 | 43 | extension Request: Testable { } 44 | extension HTTPRequest: Testable { } 45 | extension Response: Testable { } 46 | extension Application: Testable { } 47 | 48 | /// Testable for dictionaries 49 | extension Dictionary: Testable where Key == String, Value == String { 50 | public typealias ObjectType = [String : String] 51 | } 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /Sources/VaporTestTools/Extensions/Response tools/Response+Getters.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Response+Getters.swift 3 | // VaporTestTools 4 | // 5 | // Created by Ondrej Rafaj on 27/02/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | import NIO 11 | 12 | 13 | extension TestableProperty where TestableType: Response { 14 | 15 | /// Make a fake request 16 | public func fakeRequest() -> Request { 17 | let http = HTTPRequest(method: .GET, url: URL(string: "/")!) 18 | let req = Request(http: http, using: element) 19 | return req 20 | } 21 | 22 | /// Get header by it's name 23 | public func header(name: String) -> String? { 24 | let headerName = HTTPHeaderName(name) 25 | return header(name: headerName) 26 | } 27 | 28 | /// Get header by it's HTTPHeaderName representation 29 | public func header(name: HTTPHeaderName) -> String? { 30 | return element.http.headers[name].first 31 | } 32 | 33 | /// Size of the content 34 | public var contentSize: Int? { 35 | return contentString?.count 36 | } 37 | 38 | /// Get content string. Maximum of 0.5Mb of text will be returned 39 | public var contentString: String? { 40 | return contentString(encoding: .utf8) 41 | } 42 | 43 | /// Get content string with encoding. Maximum of 0.5Mb of text will be returned 44 | public func contentString(encoding: String.Encoding) -> String? { 45 | guard let data = try? element.http.body.consumeData(on: element).wait() else { 46 | return nil 47 | } 48 | let string = String(data: data, encoding: encoding) 49 | return string 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /Sources/VaporTestTools/Extensions/Testable/Decodable+Testable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Decodable+Testable.swift 3 | // VaporTestTools 4 | // 5 | // Created by Ondrej Rafaj on 27/02/2018. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | extension TestableProperty where TestableType: Decodable { 12 | 13 | /// Make accessor 14 | public static var make: TestableProperty.Type { 15 | return TestableProperty.self 16 | } 17 | 18 | } 19 | 20 | 21 | extension TestableProperty where TestableType: Decodable { 22 | 23 | /// Decode data from JSON source 24 | public static func fromJSON(fileNamed fileName: String, ofType type: String? = nil, inBundle bundle: Bundle? = nil) -> TestableType { 25 | var bundle = bundle 26 | if bundle == nil { 27 | bundle = Bundle.main 28 | } 29 | guard let filePath = bundle!.path(forResource: fileName, ofType: type) else { 30 | fatalError("Invalid filename") 31 | } 32 | let url = URL(fileURLWithPath: filePath) 33 | return fromJSON(file: url) 34 | } 35 | 36 | /// Decode data from JSON source 37 | public static func fromJSON(file fileUrl: URL) -> TestableType { 38 | let data = try! Data(contentsOf: fileUrl) 39 | return fromJSON(data: data) 40 | } 41 | 42 | /// Decode data from JSON source 43 | public static func fromJSON(string: String) -> TestableType { 44 | guard let data = string.data(using: .utf8) else { 45 | fatalError("Invalid string") 46 | } 47 | return fromJSON(data: data) 48 | } 49 | 50 | /// Decode data from JSON source 51 | public static func fromJSON(data: Data) -> TestableType { 52 | let decoder = JSONDecoder() 53 | let object = try! decoder.decode(TestableType.self, from: data) 54 | return object 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /Tests/VaporTestToolsTests/AppTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GenericControllerTests.swift 3 | // AppTests 4 | // 5 | // Created by Ondrej Rafaj on 27/02/2018. 6 | // 7 | 8 | import XCTest 9 | import Vapor 10 | import VaporTestTools 11 | 12 | 13 | class GenericControllerTests: XCTestCase { 14 | 15 | var app: Application! 16 | 17 | // MARK: Linux 18 | 19 | static let allTests = [ 20 | ("testHello", testHello), 21 | ("testNotFound", testNotFound) 22 | ] 23 | 24 | 25 | // MARK: Setup 26 | 27 | override func setUp() { 28 | super.setUp() 29 | 30 | app = Application.testable.newTestApp() 31 | } 32 | 33 | // MARK: Tests 34 | 35 | func testHello() { 36 | let req = HTTPRequest.testable.get(uri: "/hello") 37 | let r = app.testable.response(to: req) 38 | let res = r.response 39 | 40 | res.testable.debug() 41 | 42 | XCTAssertTrue(res.testable.has(statusCode: .ok), "Wrong status code") 43 | XCTAssertTrue(res.testable.has(contentType: "text/plain; charset=utf-8"), "Missing content type") 44 | XCTAssertTrue(res.testable.has(contentLength: 13), "Wrong content length") 45 | XCTAssertTrue(res.testable.has(content: "Hello, world!"), "Incorrect content") 46 | } 47 | 48 | func testNotFound() { 49 | let req = HTTPRequest.testable.get(uri: "/not-found") 50 | let r = app.testable.response(to: req) 51 | let res = r.response 52 | 53 | res.testable.debug() 54 | 55 | XCTAssertTrue(res.testable.has(statusCode: .notFound), "Wrong status code") 56 | XCTAssertTrue(res.testable.has(header: "Content-Type"), "Should not content type") 57 | XCTAssertTrue(res.testable.has(contentLength: 35), "Wrong content length") 58 | XCTAssertTrue(res.testable.has(content: #"{"error":true,"reason":"Not Found"}"#), "Incorrect content") 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /Sources/VaporTestTools/Extensions/Response tools/Response+Checks.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Response+Checks.swift 3 | // VaporTestTools 4 | // 5 | // Created by Ondrej Rafaj on 27/02/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | import NIO 11 | 12 | 13 | extension TestableProperty where TestableType: Response { 14 | 15 | /// Test header value 16 | public func has(header name: HTTPHeaderName, value: String? = nil) -> Bool { 17 | guard let header = header(name: name) else { 18 | return false 19 | } 20 | 21 | if let value = value { 22 | // QUESTION: Do we want to be specific and use ==? 23 | return header.contains(value) 24 | } 25 | else { 26 | return true 27 | } 28 | } 29 | 30 | /// Test header value 31 | public func has(header name: String, value: String? = nil) -> Bool { 32 | let headerName = HTTPHeaderName(name) 33 | return has(header: headerName, value: value) 34 | } 35 | 36 | /// Test header Content-Type 37 | public func has(contentType value: String) -> Bool { 38 | let headerName = HTTPHeaderName("Content-Type") 39 | return has(header: headerName, value: value) 40 | } 41 | 42 | /// Test header Content-Length 43 | public func has(contentLength value: Int) -> Bool { 44 | let headerName = HTTPHeaderName("Content-Length") 45 | return has(header: headerName, value: String(value)) 46 | } 47 | 48 | /// Test response status code 49 | public func has(statusCode value: HTTPStatus) -> Bool { 50 | return element.http.status.code == value.code 51 | } 52 | 53 | /// Test response status code and message 54 | public func has(statusCode value: HTTPStatus, message: String) -> Bool { 55 | return element.http.status.code == value.code && element.http.status.reasonPhrase == message 56 | } 57 | 58 | /// Test response content 59 | public func has(content value: String) -> Bool { 60 | return contentString == value 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /Sources/VaporTestTools/Extensions/Testable/Application+Testable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Application+Testable.swift 3 | // VaporTestTools 4 | // 5 | // Created by Ondrej Rafaj on 27/02/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | import Routing 11 | 12 | /// Response tuple containing response as well as the request 13 | public typealias TestResponse = (response: Response, request: Request) 14 | 15 | 16 | extension TestableProperty where TestableType: Application { 17 | 18 | public typealias AppConfigClosure = ((_ config: inout Config, _ env: inout Vapor.Environment, _ services: inout Services) -> Void) 19 | public typealias AppRouterClosure = ((_ router: Router) -> Void) 20 | 21 | /// Configure a new test app (in test setup) 22 | public static func new(config: Config = Config.default(), env: Environment? = nil, services: Services = Services.default(), _ configClosure: AppConfigClosure? = nil, _ routerClosure: AppRouterClosure) -> Application { 23 | var config = config 24 | var env = try! env ?? Environment.detect() 25 | var services = services 26 | 27 | configClosure?(&config, &env, &services) 28 | let app = try! Application(config: config, environment: env, services: services) 29 | 30 | let router = try! app.make(Router.self) 31 | routerClosure(router) 32 | 33 | return app 34 | } 35 | 36 | /// Respond to HTTPRequest 37 | public func response(to request: HTTPRequest) -> TestResponse { 38 | let responder = try! element.make(Responder.self) 39 | let wrappedRequest = Request(http: request, using: element) 40 | return try! (response: responder.respond(to: wrappedRequest).wait(), request: wrappedRequest) 41 | } 42 | 43 | /// Respond to HTTPRequest (throwing) 44 | public func response(throwingTo request: HTTPRequest) throws -> TestResponse { 45 | let responder = try element.make(Responder.self) 46 | let wrappedRequest = Request(http: request, using: element) 47 | return try (response: responder.respond(to: wrappedRequest).wait(), request: wrappedRequest) 48 | } 49 | 50 | /// Create fake request 51 | public func fakeRequest() -> Request { 52 | let http = HTTPRequest(method: .GET, url: URL(string: "/")!) 53 | let req = Request(http: http, using: element) 54 | return req 55 | } 56 | 57 | } 58 | 59 | -------------------------------------------------------------------------------- /Sources/VaporTestTools/Extensions/Requests/HTTPRequest+Make.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPRequest+Make.swift 3 | // VaporTestTools 4 | // 5 | // Created by Ondrej Rafaj on 27/02/2018. 6 | // 7 | 8 | @_exported import Foundation 9 | @_exported import Vapor 10 | @_exported import Routing 11 | 12 | 13 | public typealias URI = String 14 | 15 | 16 | extension TestableProperty where TestableType == HTTPRequest { 17 | 18 | /// MAke HTTPRequest 19 | public static func request(method: HTTPMethod, uri: URI, data: Data? = nil, headers: [String: String]? = nil) -> HTTPRequest { 20 | var req = HTTPRequest(method: method, url: URL(string: uri)!) 21 | if let headers = headers { 22 | req.headers = headers.testable.asHTTPHeaders() 23 | } 24 | if let data = data { 25 | req.body = HTTPBody(data: data) 26 | } 27 | return req 28 | } 29 | 30 | /// GET HTTPRequest 31 | public static func get(uri: URI, headers: [String: String]? = nil) -> HTTPRequest { 32 | let req = request(method: .GET, uri: uri, headers: headers) 33 | return req 34 | } 35 | 36 | /// PUT HTTPRequest 37 | public static func put(uri: URI, data: Data? = nil, headers: [String: String]? = nil) -> HTTPRequest { 38 | let req = request(method: .PUT, uri: uri, data: data, headers: headers) 39 | return req 40 | } 41 | 42 | /// POST HTTPRequest 43 | public static func post(uri: URI, data: Data? = nil, headers: [String: String]? = nil) -> HTTPRequest { 44 | let req = request(method: .POST, uri: uri, data: data, headers: headers) 45 | return req 46 | } 47 | 48 | /// PATVH HTTPRequest 49 | public static func patch(uri: URI, data: Data? = nil, headers: [String: String]? = nil) -> HTTPRequest { 50 | let req = request(method: .PATCH, uri: uri, data: data, headers: headers) 51 | return req 52 | } 53 | 54 | /// DELETE HTTPRequest 55 | public static func delete(uri: URI, headers: [String: String]? = nil) -> HTTPRequest { 56 | let req = request(method: .DELETE, uri: uri, headers: headers) 57 | return req 58 | } 59 | 60 | /// Return request and response for GET url string & data 61 | public static func response(get uri: URI, headers: [String: String]? = nil, with app: Application) -> TestResponse { 62 | let req = request(method: .GET, uri: uri, headers: headers) 63 | return app.testable.response(to: req) 64 | } 65 | 66 | /// Return request and response for PUT url string & data 67 | public static func response(put uri: URI, data: Data? = nil, headers: [String: String]? = nil, with app: Application) -> TestResponse { 68 | let req = request(method: .PUT, uri: uri, data: data, headers: headers) 69 | return app.testable.response(to: req) 70 | } 71 | 72 | /// Return request and response for POST url string & data 73 | public static func response(post uri: URI, data: Data? = nil, headers: [String: String]? = nil, with app: Application) -> TestResponse { 74 | let req = request(method: .POST, uri: uri, data: data, headers: headers) 75 | return app.testable.response(to: req) 76 | } 77 | 78 | /// Return request and response for PATCH url string & data 79 | public static func response(patch uri: URI, data: Data? = nil, headers: [String: String]? = nil, with app: Application) -> TestResponse { 80 | let req = request(method: .PATCH, uri: uri, data: data, headers: headers) 81 | return app.testable.response(to: req) 82 | } 83 | 84 | /// Return request and response for DELETE url string & data 85 | public static func response(delete uri: URI, headers: [String: String]? = nil, with app: Application) -> TestResponse { 86 | let req = request(method: .DELETE, uri: uri, headers: headers) 87 | return app.testable.response(to: req) 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Vapor 3 test tools](https://github.com/LiveUI/VaporTestTools/raw/master/Other/logo.png) 2 | 3 | ## 4 | 5 | [![Slack](http://vapor.team/badge.svg?style=flat)](http://vapor.team) 6 | [![Jenkins](https://ci.liveui.io/job/LiveUI/job/VaporTestTools/job/master/badge/icon)](https://ci.liveui.io/job/LiveUI/job/VaporTestTools/) 7 | [![Platforms](https://img.shields.io/badge/platforms-macOS%2010.13%20|%20Ubuntu%2016.04%20LTS-ff0000.svg?style=flat)](https://github.com/LiveUI/VaporTestTools) 8 | [![Swift Package Manager](https://img.shields.io/badge/SPM-compatible-4BC51D.svg?style=flat)](https://swift.org/package-manager/) 9 | [![License](https://img.shields.io/badge/license-MIT-CCCCCC.svg?style=flat)](http://cocoapods.org/pods/Presentables) 10 | [![Platform](https://img.shields.io/badge/framework-Vapor3-FF0000.svg?style=flat)](http://cocoapods.org/pods/Presentables) 11 | 12 | ## 13 | 14 | Vaper test tools is (pretty much vhat it says on the tin) a set of methods designed to make testing your endpoints in Vapor 3 a bit more pain-free ... 15 | 16 | ## Slack 17 | 18 | Get help using and/or installing this library on the Vapor [Slack](http://vapor.team), and look for @rafiki270 19 | 20 | ## Example 21 | 22 | To run the example project on a mac, clone the repo, and run `vapor xcode` to generate xcode project and run `Run` target. 23 | 24 | ## Installation 25 | 26 | #### SPM - Swift Package Manager 27 | 28 | Import 29 | 30 | ```swift 31 | .package(url: "https://github.com/LiveUI/VaporTestTools.git", from: "0.1.1") 32 | 33 | // or to always get the latest changes 34 | 35 | .package(url: "https://github.com/LiveUI/VaporTestTools.git", .branch("master")) 36 | ``` 37 | 38 | ## Usage 39 | 40 | To write tests like this ... 41 | 42 | ```Swift 43 | func testHello() { 44 | let req = HTTPRequest.testable.get(uri: "/hello") 45 | let res = app.testable.response(to: req).response 46 | 47 | res.testable.debug() // Debug response into the console 48 | 49 | let hello = res.testable.content(as: Hello.self)! 50 | XCTAssertEqual(hello.message, "hello world", "Message is incorrect") 51 | 52 | XCTAssertTrue(res.testable.has(statusCode: .ok), "Wrong status code") 53 | XCTAssertTrue(res.testable.has(contentType: "text/plain; charset=utf-8"), "Missing content type") 54 | XCTAssertTrue(res.testable.has(contentLength: 13), "Wrong content length") 55 | XCTAssertTrue(res.testable.has(content: "Hello, world!"), "Incorrect content") 56 | } 57 | 58 | ``` 59 | 60 | ... you first you need to configure your `Application` object in a test environment. To do that I would recommend creating some form of a helper method that would allow you to access the functionality from any file. 61 | 62 | ```swift 63 | let app = Application.testable.new({ (config, env, services) in 64 | try! App.configure(&config, &env, &services) 65 | }) { (router) in 66 | 67 | } 68 | ``` 69 | I would recommend to put the above initialization in a convenience method as described [here](#custom-application-convenience-method) 70 | 71 | And finally create your test file ... the whole thing could look like this: 72 | 73 | ```Swift 74 | import XCTest 75 | import Vapor 76 | import VaporTestTools 77 | 78 | 79 | class GenericControllerTests: XCTestCase { 80 | 81 | var app: Application! 82 | 83 | // MARK: Linux 84 | 85 | static let allTests = [ 86 | ("testHello", testHello), 87 | ("testPing", testPing), 88 | ("testNotFound", testNotFound), 89 | ("testHash", testHash) 90 | ] 91 | 92 | 93 | // MARK: Setup 94 | 95 | override func setUp() { 96 | super.setUp() 97 | 98 | app = Application.testable.newTestApp() 99 | } 100 | 101 | // MARK: Tests 102 | 103 | func testHello() { 104 | let req = HTTPRequest.testable.get(uri: "/hello") 105 | let r = app.testable.response(to: req) 106 | let res = r.response 107 | 108 | res.testable.debug() 109 | 110 | XCTAssertTrue(res.testable.has(statusCode: .ok), "Wrong status code") 111 | XCTAssertTrue(res.testable.has(contentType: "text/plain; charset=utf-8"), "Missing content type") 112 | XCTAssertTrue(res.testable.has(contentLength: 13), "Wrong content length") 113 | XCTAssertTrue(res.testable.has(content: "Hello, world!"), "Incorrect content") 114 | } 115 | 116 | func testPing() { 117 | let req = HTTPRequest.testable.get(uri: "/ping") 118 | let r = app.testable.response(to: req) 119 | let res = r.response 120 | 121 | // Print out info about the Response 122 | res.testable.debug() 123 | /* 124 | Debugging response: 125 | HTTP [1.1] with status code [200] 126 | Headers: 127 | Content-Type = application/json; charset=utf-8 128 | Content-Length = 15 129 | Date = Wed, 28 Feb 2018 00:52:02 GMT 130 | Content: 131 | Size: 15 132 | Media type: application/json; charset=utf-8 133 | Content: 134 | {"code":"pong"} 135 | */ 136 | 137 | XCTAssertTrue(res.testable.has(statusCode: .ok), "Wrong status code") 138 | XCTAssertTrue(res.testable.has(contentType: "application/json; charset=utf-8"), "Missing content type") 139 | XCTAssertTrue(res.testable.has(contentLength: 15), "Wrong content length") 140 | XCTAssertTrue(res.testable.has(content: "{\"code\":\"pong\"}"), "Incorrect content") 141 | } 142 | 143 | func testNotFound() { 144 | let req = HTTPRequest.testable.get(uri: "/not-found") 145 | let r = app.testable.response(to: req) 146 | let res = r.response 147 | 148 | res.testable.debug() 149 | 150 | XCTAssertTrue(res.testable.has(statusCode: 404), "Wrong status code") 151 | XCTAssertFalse(res.testable.has(header: "Content-Type"), "Should not content type") 152 | XCTAssertTrue(res.testable.has(contentLength: 9), "Wrong content length") 153 | XCTAssertTrue(res.testable.has(content: "Not found"), "Incorrect content") 154 | } 155 | 156 | func testHash() { 157 | let req = HTTPRequest.testable.get(uri: "/hash/something") 158 | let r = app.testable.response(to: req) 159 | let res = r.response 160 | 161 | res.testable.debug() 162 | 163 | XCTAssertTrue(res.testable.has(statusCode: .ok), "Wrong status code") 164 | XCTAssertTrue(res.testable.has(contentType: "text/plain; charset=utf-8"), "Missing content type") 165 | XCTAssertTrue(res.testable.has(contentLength: 60), "Wrong content length") 166 | } 167 | 168 | } 169 | 170 | ``` 171 | 172 | To see more examples in action, please see VaporTestTools in action: 173 | * [Boost - Enterprise AppStore for mobile apps](https://github.com/LiveUI/Boost/) 174 | * [API core tests](https://github.com/LiveUI/Boost/tree/master/Tests/ApiCoreTests/Controllers) 175 | 176 | #### Custom `Application` convenience method 177 | 178 | In the following example (`Application+Testing.swift`) you can see an extension on a testable property which holds all the convenience methods. This will be available through `Application.testable.newTestApp()` 179 | 180 | ```Swift 181 | import Foundation 182 | import App 183 | import Vapor 184 | import VaporTestTools 185 | 186 | 187 | extension TestableProperty where TestableType: Application { 188 | 189 | public static func newTestApp() -> Application { 190 | let app = new({ (config, env, services) in 191 | try! App.configure(&config, &env, &services) 192 | }) { (router) in 193 | 194 | } 195 | return app 196 | } 197 | 198 | } 199 | ``` 200 | 201 | ## Example Package.swift for testing 202 | 203 | Your whole `Package.swift` file could look something like this: 204 | ```swift 205 | // swift-tools-version:4.0 206 | import PackageDescription 207 | 208 | let package = Package( 209 | name: "MyApp", 210 | dependencies: [ 211 | .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0-beta.3.1.3"), 212 | .package(url: "https://github.com/LiveUI/VaporTestTools.git", from: "0.0.1") 213 | ], 214 | targets: [ 215 | .target( 216 | name: "MyApp", 217 | dependencies: [ 218 | "Vapor" 219 | ] 220 | ), 221 | .target(name: "Run", dependencies: [ 222 | "MyApp" 223 | ]), 224 | .testTarget(name: "AppTests", dependencies: ["TestApp", "VaporTestTools"]) 225 | ] 226 | ) 227 | ``` 228 | 229 | Notice the line `.testTarget(name: "AppTests", dependencies: ["TestApp", "VaporTestTools"])` where you create a test target and include `VaporTestTools`. 230 | 231 | 232 | Don't forget to star the repo if you think it deserves it! :) 233 | 234 | Have fun testing! 235 | 236 | ## Boost - Open source enterprise AppStore 237 | 238 | **VaporTestTools** has been released as a part of a Boost mobile app distribution platform. 239 | 240 | More info on http://www.boostappstore.com 241 | 242 | Other components in the bundle are: 243 | 244 | * [BoostCore](https://github.com/LiveUI/BoostCore/) - AppStore core module 245 | * [ApiCore](https://github.com/LiveUI/ApiCore/) - Base user & team management including forgotten passwords, etc ... 246 | * [MailCore](https://github.com/LiveUI/MailCore/) - Mailing wrapper for multiple mailing services like MailGun, SendGrig or SMTP (coming) 247 | * [DBCore](https://github.com/LiveUI/DbCore/) - Set of tools for work with PostgreSQL database 248 | 249 | 250 | ## Author 251 | 252 | Ondrej Rafaj (@rafiki270 on [Github](https://github.com/rafiki270), [Twitter](https://twitter.com/rafiki270), [LiveUI Slack](http://bit.ly/2B0dEyt) and [Vapor Slack](https://vapor.team/)) 253 | 254 | ## License 255 | 256 | VaporTestTools are available under an MIT license. See the LICENSE file for more info. 257 | --------------------------------------------------------------------------------