Skip to content

Latest commit

 

History

History
448 lines (341 loc) · 9.58 KB

demo.md

File metadata and controls

448 lines (341 loc) · 9.58 KB

Start from scratch by using vapor tool

vapor new tasks-api

Explore hierarchy / demo SPM and launch

vapor update -y

Change SQLite storage

.file(path: "db.sqlite")

Add RouteCollection conformance to TodoController

: RouteCollection

and then the stub method

func boot(router: Router) throws {

}

to copy / paste routes in it

router.get("todos", use: index)
router.post("todos", use: create)
router.delete("todos", Todo.parameter, use: delete)

simplify routes using groups

let route = router.grouped("v1", "todos")

route.get(use: index)
route.post(use: create)
route.delete(Todo.parameter, use: delete)

return no content for DELETE todos

transform(to: .noContent)

add single GET handler

func get(_ req: Request) throws -> Future<Todo> {
  return try req.parameters.next(Todo.self)
}

and the required route

route.get(Todo.parameter, use: get)

finaly add an update route

route.put(Todo.parameter, use: update)

and it's implementation

func update(_ req: Request) throws -> Future<Todo> {
    return try flatMap(
      to: Todo.self,
      req.parameters.next(Todo.self),
      req.content.decode(Todo.self)) { todo, updatedTodo in

        todo.title = updatedTodo.title

        return todo.save(on: req)
    }
  }

And then demo unit testing : Create Application+Testable.swift

//
//  Application+Testable.swift
//  AppTests
//
//  Created by David Bonnet on 08/11/2018.
//

@testable import App
import Vapor

extension Application {

  static func testable(envArgs: [String]? = nil) throws -> Application {

    var config = Config.default()
    var services = Services.default()
    var env = Environment.testing

    if let args = envArgs {
      env.arguments = args
    }

    try App.configure(&config, &env, &services)
    let app = try Application(config: config, environment: env, services: services)

    try App.boot(app)
    return app
  }

  // MARK: - Reset

  static func reset() throws {
    let revertEnvironment = ["vapor", "revert", "--all", "-y"]
    try Application.testable(envArgs: revertEnvironment)
      .asyncRun()
      .wait()

    let migrateEnvironment = ["vapor", "migrate", "-y"]
    try Application.testable(envArgs: migrateEnvironment)
      .asyncRun()
      .wait()
  }

  // MARK: - Requests

  @discardableResult
  func sendRequest<T>(to path: String,
                      method: HTTPMethod,
                      headers: HTTPHeaders = .init(),
                      body: T? = nil) throws -> Response where T: Content {
    let responder = try self.make(Responder.self)

    let request = HTTPRequest(method: method,
                              url: URL(string: path)!,
                              headers: headers)
    let wrappedRequest = Request(http: request, using: self)

    if let body = body {
      try wrappedRequest.content.encode(body)
    }

    return try responder.respond(to: wrappedRequest).wait()
  }

  func sendRequest(to path: String,
                   method: HTTPMethod,
                   headers: HTTPHeaders = .init()) throws -> Response {
    let emptyContent: EmptyContent? = nil
    return try sendRequest(to: path,
                           method: method,
                           headers: headers,
                           body: emptyContent)
  }

  func getResponse<C, T>(to path: String,
                         method: HTTPMethod = .GET,
                         headers: HTTPHeaders = .init(),
                         data: C? = nil,
                         decodeTo type: T.Type) throws -> T where C: Content, T: Decodable {

    let response = try self.sendRequest(to: path,
                                        method: method,
                                        headers: headers,
                                        body: data)

    return try response.content.decode(type).wait()
  }

  func getResponse<T>(to path: String,
                      method: HTTPMethod = .GET,
                      headers: HTTPHeaders = .init(),
                      decodeTo type: T.Type) throws -> T where T: Decodable {

    let emptyContent: EmptyContent? = nil
    return try self.getResponse(to: path,
                                method: method,
                                headers: headers,
                                data: emptyContent,
                                decodeTo: type)
  }

}

struct EmptyContent: Content {}

Adds commands to configure.swift

/// Allows revert and migration
var commandConfig = CommandConfig.default()
commandConfig.useFluentCommands()
services.register(commandConfig)

Create second helper Models+Testable.swift

@testable import App
import FluentSQLite

// MARK: - Todo
extension Todo {

  static func create(title: String = "My \(UUID().uuidString) todo",
                     on connection: SQLiteConnection) throws -> Todo {

    let task = Todo(title: title)
    return try task.save(on: connection).wait()
  }
}

Create TodosTests.swift

@testable import App
import Vapor
import XCTest
import FluentSQLite

//swiftlint:disable force_try
final class TodosTests: XCTestCase {

  // MARK: - Properties

  let URI = "/v1/todos/"

  var _app: Application? = nil
  var app: Application { return self._app! }
  var conn: SQLiteConnection!

  let expectedTitle = "CocoaHeads talk"

  // MARK: - Init

  override func setUp() {
    try! Application.reset()
    _app = try! Application.testable()
    conn = try! app.newConnection(to: .sqlite).wait()
  }

  override func tearDown() {
    conn.close()
    _app = nil
  }

  // MARK: - Tests

  
}

First listing test

func test_todosListingFromAPI() throws {

    let todo = try Todo.create(title: expectedTitle, on: conn)
    _ = try Todo.create(on: conn)

    let todoId = try todo.requireID()
    let response = try app.getResponse(to: URI, decodeTo: [Todo].self)

    XCTAssertNotNil(response)
    XCTAssertTrue(response.count == 2)
    XCTAssertEqual(response[0].id, todoId)
    XCTAssertEqual(response[0].title, todo.title)
    XCTAssertEqual(response[0].title, expectedTitle)
}

Add all for linux testing

// MARK: - All

  static let allTests = [
    ("test_todosListingFromAPI", test_todosListingFromAPI)
  ]

Complete the LinuxMain.swift file

import XCTest

@testable import AppTests

XCTMain([
  testCase(TodosTests.allTests)
])

Add more tests

func test_todoRetreivingFromAPI() throws {

    let todo = try Todo.create(on: conn)

    let todoId = try todo.requireID()
    let response = try app.getResponse(to: URI+"\(todoId)", decodeTo: Todo.self)

    XCTAssertNotNil(response)
    XCTAssertEqual(response.id, todoId)
    XCTAssertEqual(response.title, todo.title)
  }

  func test_todoCreationFromAPI() throws {

    let todo = Todo(title: expectedTitle)
    let receivedTodo = try app.getResponse(to: URI,
                                             method: .POST,
                                             headers: ["Content-Type": "application/json"],
                                             data: todo,
                                             decodeTo: Todo.self)

    XCTAssertEqual(receivedTodo.title, todo.title)
    XCTAssertNotNil(receivedTodo.id)

    let todoId = try receivedTodo.requireID()
    let todoResponse = try app.getResponse(to: URI+"\(todoId)", decodeTo: Todo.self)

    XCTAssertNotNil(todoResponse)
    XCTAssertEqual(todoResponse.id, todoId)
    XCTAssertEqual(todoResponse.title, expectedTitle)
  }

  func test_todoUpdateFromAPI() throws {

    let initialTodo = try Todo.create(on: conn)

    let todo = Todo(title: expectedTitle)

    let todoId = try initialTodo.requireID()
    let receivedTodo = try app.getResponse(to: URI+"\(todoId)",
      method: .PUT,
      headers: ["Content-Type": "application/json"],
      data: todo,
      decodeTo: Todo.self)

    XCTAssertEqual(receivedTodo.title, todo.title)
    XCTAssertEqual(receivedTodo.id, initialTodo.id)
    XCTAssertEqual(receivedTodo.title, expectedTitle)
  }

  func test_todoDeleteFromAPI() throws {

    let todo = try Todo.create(on: conn)

    let todoId = try todo.requireID()
    let response = try app.sendRequest(
      to: URI+"\(todoId)",
      method: .DELETE)

    XCTAssertNotNil(response)
    XCTAssertTrue(response.http.status == .noContent)
  }
static let allTests = [
    ("test_todosListingFromAPI", test_todosListingFromAPI),
    ("test_todoRetreivingFromAPI", test_todoRetreivingFromAPI),
    ("test_todoCreationFromAPI", test_todoCreationFromAPI),
    ("test_todoUpdateFromAPI", test_todoUpdateFromAPI),
    ("test_todoDeleteFromAPI", test_todoDeleteFromAPI)
  ]

Add Dockerfile

FROM swift:4.2

WORKDIR /package

COPY . ./

RUN swift package resolve
RUN swift package clean

CMD ["swift", "test"]

add docker-compose.yml

version: '3'

services:
  tasks-api:
    build: .
docker-compose build
docker-compose up --abort-on-container-exit

Add auth

.package(url: "https://github.com/vapor/auth.git", from: "2.0.0")
vapor update

and its user

import Foundation
import Vapor
import FluentSQLite
import Authentication

final class User: Codable {

  var id: UUID?
  var username: String
  var password: String

  init(username: String, password: String) {
    self.username = username
    self.password = password
  }

}

extension User: SQLiteUUIDModel {}
extension User: Migration {}
extension User: Content {}
extension User: Parameter {}

Add user in migrations

migrations.add(model: User.self, database: .sqlite)

Add it's conformance to BasicAuthenticatable

extension User: BasicAuthenticatable {
  static let usernameKey: UsernameKey = \User.username
  static let passwordKey: PasswordKey = \User.password
}