import Foundation import HTTPTypes import Hummingbird import HummingbirdTesting import Testing @testable import TodoApp let origin = "http://127.0.0.1:8080" /// Runs `body` against the app's router on a fresh database. func withApp(_ body: @escaping @Sendable (any TestClientProtocol) async throws -> Void) async throws { try await withStore { store, _ in let options = try Options(arguments: ["--listen", "127.0.0.1:8080", "--db", "unused"], environment: [:]) let app = Application(router: Server.router(store: store, options: options, version: "test-version")) try await app.test(.router, body) } } extension TestResponse { var text: String { body.getString(at: body.readerIndex, length: body.readableBytes) ?? "" } var listCookie: String? { guard let header = headers[.setCookie], header.hasPrefix("todo_list=") else { return nil } return String(header.prefix { $0 != ";" }) } } extension TestClientProtocol { /// Visits `/` and returns the cookie for a new, empty list. func newVisitor() async throws -> String { try await execute(uri: "/", method: .get) { try #require($0.listCookie) } } func post(_ path: String, _ form: String, cookie: String?, headers: HTTPFields = [:]) async throws -> TestResponse { var fields = headers fields[.contentType] = "application/x-www-form-urlencoded" if let cookie { fields[.cookie] = cookie } return try await execute(uri: path, method: .post, headers: fields, body: ByteBuffer(string: form)) } func get(_ path: String, cookie: String) async throws -> TestResponse { try await execute(uri: path, method: .get, headers: [.cookie: cookie]) } } func ids(_ html: String) -> [String] { html.split(separator: "data-id=\"").dropFirst().map { String($0.prefix { $0 != "\"" }) } } @Suite struct RouteTests { @Test func healthAndVersion() async throws { try await withApp { client in try await client.execute(uri: "/healthz", method: .get) { #expect($0.status == .ok) } try await client.execute(uri: "/api/version", method: .get) { #expect($0.status == .ok) #expect($0.text == "test-version") } } } @Test func firstVisitGetsAnHttpOnlyLaxCookie() async throws { try await withApp { client in try await client.execute(uri: "/", method: .get) { response in #expect(response.status == .ok) let header = try #require(response.headers[.setCookie]) let value = header.prefix { $0 != ";" }.dropFirst("todo_list=".count) #expect(Visitors.isWellFormed(String(value))) #expect(header.contains("HttpOnly")) #expect(header.contains("SameSite=Lax")) #expect(header.contains("Path=/")) #expect(!header.contains("Secure")) #expect(!response.text.contains("class=\"main\"")) #expect(response.text.contains("· Swift
")) } } } @Test(arguments: ["todo_list=%%%", "todo_list=0123456789abcdef0123456789abcdef", "todo_list="]) func unknownCookieGetsAFreshList(_ cookie: String) async throws { try await withApp { client in let response = try await client.get("/", cookie: cookie) #expect(response.status == .ok) #expect(response.listCookie != nil) #expect(response.listCookie != cookie) } } @Test func addRedirectsToTheFilterAndTrims() async throws { try await withApp { client in let me = try await client.newVisitor() for (filter, location) in [("all", "/"), ("active", "/active"), ("completed", "/completed"), ("bogus", "/"), ("", "/")] { let response = try await client.post("/todos", "title=++x++&filter=\(filter)", cookie: me) #expect(response.status == .seeOther) #expect(response.headers[.location] == location) } _ = try await client.post("/todos", "title=+++", cookie: me) let page = try await client.get("/", cookie: me).text #expect(page.components(separatedBy: "").count - 1 == 5) } } @Test func titlesAreEscapedAndKeptExactly() async throws { try await withApp { client in let me = try await client.newVisitor() let title = "