diff --git a/Package.swift b/Package.swift index b9633a6..d9ed4a6 100644 --- a/Package.swift +++ b/Package.swift @@ -25,12 +25,15 @@ let package = Package( .product(name: "ExtrasBase64", package: "swift-extras-base64"), .product(name: "Hummingbird", package: "hummingbird"), ]), + .target(name: "HummingbirdLambdaTesting", dependencies: [ + .byName(name: "HummingbirdLambda"), + ]), .executableTarget(name: "HBLambdaTest", dependencies: [ .byName(name: "HummingbirdLambda"), ]), .testTarget(name: "HummingbirdLambdaTests", dependencies: [ .byName(name: "HummingbirdLambda"), - .product(name: "HummingbirdXCT", package: "hummingbird"), + .byName(name: "HummingbirdLambdaTesting"), .product(name: "NIOPosix", package: "swift-nio"), ]), ] diff --git a/Sources/HummingbirdLambda/Lambda.swift b/Sources/HummingbirdLambda/Lambda.swift index e8ea481..dc15d1f 100644 --- a/Sources/HummingbirdLambda/Lambda.swift +++ b/Sources/HummingbirdLambda/Lambda.swift @@ -83,7 +83,4 @@ extension HBLambda { } public func shutdown() async throws {} - - /// default configuration - public var configuration: HBApplicationConfiguration { .init() } } diff --git a/Sources/HummingbirdLambda/Response+APIGateway.swift b/Sources/HummingbirdLambda/Response+APIGateway.swift index dfa6017..a289670 100644 --- a/Sources/HummingbirdLambda/Response+APIGateway.swift +++ b/Sources/HummingbirdLambda/Response+APIGateway.swift @@ -57,9 +57,9 @@ extension HBResponse { _ = try await self.body.write(collateWriter) let buffer = collateWriter.buffer if let contentType = self.headers[.contentType] { - let type = contentType[..<(contentType.firstIndex(of: ";") ?? contentType.endIndex)] - switch type { - case "text/plain", "application/json", "application/x-www-form-urlencoded": + let mediaType = HBMediaType(from: contentType) + switch mediaType { + case .some(.text), .some(.applicationJson), .some(.applicationUrlEncoded): body = String(buffer: buffer) default: break diff --git a/Sources/HummingbirdLambdaTesting/APIGatewayLambda.swift b/Sources/HummingbirdLambdaTesting/APIGatewayLambda.swift new file mode 100644 index 0000000..fef033d --- /dev/null +++ b/Sources/HummingbirdLambdaTesting/APIGatewayLambda.swift @@ -0,0 +1,88 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Hummingbird server framework project +// +// Copyright (c) 2021-2024 the Hummingbird authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaEvents +import Foundation +import HTTPTypes +import HummingbirdCore +import NIOCore + +extension APIGatewayRequest: LambdaTestableEvent { + /// Construct APIGateway Event from uri, method, headers and body + public init(uri: String, method: HTTPRequest.Method, headers: HTTPFields, body: ByteBuffer?) throws { + let base64Body = body.map { "\"\(String(base64Encoding: $0.readableBytesView))\"" } ?? "null" + let url = HBURL(uri) + let queryValues: [String: [String]] = url.queryParameters.reduce([:]) { result, value in + var result = result + let key = String(value.key) + var values = result[key] ?? [] + values.append(.init(value.value)) + result[key] = values + return result + } + let singleQueryValues = queryValues.compactMapValues { $0.count == 1 ? $0.first : nil } + let queryValuesString = try String(decoding: JSONEncoder().encode(singleQueryValues), as: UTF8.self) + let multiQueryValuesString = try String(decoding: JSONEncoder().encode(queryValues), as: UTF8.self) + let headerValues: [String: [String]] = headers.reduce(["host": ["127.0.0.1:8080"]]) { result, value in + var result = result + let key = String(value.name) + var values = result[key] ?? [] + values.append(.init(value.value)) + result[key] = values + return result + } + let singleHeaderValues = headerValues.compactMapValues { $0.count == 1 ? $0.first : nil } + let headerValuesString = try String(decoding: JSONEncoder().encode(singleHeaderValues), as: UTF8.self) + let multiHeaderValuesString = try String(decoding: JSONEncoder().encode(headerValues), as: UTF8.self) + let eventJson = """ + { + "httpMethod": "\(method)", + "body": \(base64Body), + "resource": "\(url.path)", + "requestContext": { + "resourceId": "123456", + "apiId": "1234567890", + "resourcePath": "\(url.path)", + "httpMethod": "\(method)", + "requestId": "\(UUID().uuidString)", + "accountId": "123456789012", + "stage": "Prod", + "identity": { + "apiKey": null, + "userArn": null, + "cognitoAuthenticationType": null, + "caller": null, + "userAgent": "Custom User Agent String", + "user": null, + "cognitoIdentityPoolId": null, + "cognitoAuthenticationProvider": null, + "sourceIp": "127.0.0.1", + "accountId": null + }, + "extendedRequestId": null, + "path": "\(uri)" + }, + "queryStringParameters": \(queryValuesString), + "multiValueQueryStringParameters": \(multiQueryValuesString), + "headers": \(headerValuesString), + "multiValueHeaders": \(multiHeaderValuesString), + "pathParameters": null, + "stageVariables": null, + "path": "\(url.path)", + "isBase64Encoded": \(body != nil) + } + """ + self = try JSONDecoder().decode(Self.self, from: Data(eventJson.utf8)) + } +} diff --git a/Sources/HummingbirdLambdaTesting/APIGatewayV2Lambda.swift b/Sources/HummingbirdLambdaTesting/APIGatewayV2Lambda.swift new file mode 100644 index 0000000..4436d36 --- /dev/null +++ b/Sources/HummingbirdLambdaTesting/APIGatewayV2Lambda.swift @@ -0,0 +1,89 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Hummingbird server framework project +// +// Copyright (c) 2021-2024 the Hummingbird authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaEvents +import Foundation +import HTTPTypes +import HummingbirdCore +import NIOCore + +extension APIGatewayV2Request: LambdaTestableEvent { + /// Construct APIGatewayV2 Event from uri, method, headers and body + public init(uri: String, method: HTTPRequest.Method, headers: HTTPFields, body: ByteBuffer?) throws { + let base64Body = body.map { "\"\(String(base64Encoding: $0.readableBytesView))\"" } ?? "null" + let url = HBURL(uri) + let queryValues: [String: [String]] = url.queryParameters.reduce([:]) { result, value in + var result = result + let key = String(value.key) + var values = result[key] ?? [] + values.append(.init(value.value)) + result[key] = values + return result + } + let queryValueStrings = try String(decoding: JSONEncoder().encode(queryValues.mapValues { $0.joined(separator: ",") }), as: UTF8.self) + let headerValues: [String: [String]] = headers.reduce(["host": ["127.0.0.1:8080"]]) { result, value in + var result = result + let key = String(value.name) + var values = result[key] ?? [] + values.append(.init(value.value)) + result[key] = values + return result + } + let headerValueStrings = try String(decoding: JSONEncoder().encode(headerValues.mapValues { $0.joined(separator: ",") }), as: UTF8.self) + let eventJson = """ + { + "routeKey":"\(method) \(url.path)", + "version":"2.0", + "rawPath":"\(url.path)", + "stageVariables":null, + "requestContext":{ + "timeEpoch":1587750461466, + "domainPrefix":"hello", + "authorizer":{ + "jwt":{ + "scopes":[ + "hello" + ], + "claims":{ + "aud":"customers", + "iss":"https://hello.test.com/", + "iat":"1587749276", + "exp":"1587756476" + } + } + }, + "accountId":"0123456789", + "stage":"$default", + "domainName":"hello.test.com", + "apiId":"pb5dg6g3rg", + "requestId":"LgLpnibOFiAEPCA=", + "http":{ + "path":"\(url.path)", + "userAgent":"Paw/3.1.10 (Macintosh; OS X/10.15.4) GCDHTTPRequest", + "method":"\(method)", + "protocol":"HTTP/1.1", + "sourceIp":"91.64.117.86" + }, + "time":"24/Apr/2020:17:47:41 +0000" + }, + "body": \(base64Body), + "isBase64Encoded": \(body != nil), + "rawQueryString":"\(url.query ?? "")", + "queryStringParameters":\(queryValueStrings), + "headers":\(headerValueStrings) + } + """ + self = try JSONDecoder().decode(Self.self, from: Data(eventJson.utf8)) + } +} diff --git a/Sources/HummingbirdLambdaTesting/HBXCTLambda.swift b/Sources/HummingbirdLambdaTesting/HBXCTLambda.swift new file mode 100644 index 0000000..f82871f --- /dev/null +++ b/Sources/HummingbirdLambdaTesting/HBXCTLambda.swift @@ -0,0 +1,92 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Hummingbird server framework project +// +// Copyright (c) 2021-2024 the Hummingbird authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaEvents +@testable import AWSLambdaRuntimeCore +import Foundation +import HTTPTypes +@testable import HummingbirdLambda +import Logging +import NIOCore +import NIOPosix + +class HBLambdaTestFramework where Lambda.Event: LambdaTestableEvent { + let context: LambdaContext + var terminator: LambdaTerminator + + init(logLevel: Logger.Level) { + var logger = Logger(label: "HBTestLambda") + logger.logLevel = logLevel + self.context = .init( + requestID: UUID().uuidString, + traceID: "abc123", + invokedFunctionARN: "aws:arn:", + deadline: .now() + .seconds(15), + cognitoIdentity: nil, + clientContext: nil, + logger: logger, + eventLoop: MultiThreadedEventLoopGroup.singleton.any(), + allocator: ByteBufferAllocator() + ) + self.terminator = .init() + } + + var initializationContext: LambdaInitializationContext { + .init( + logger: self.context.logger, + eventLoop: self.context.eventLoop, + allocator: self.context.allocator, + terminator: .init() + ) + } + + func run(_ test: @escaping @Sendable (HBLambdaTestClient) async throws -> Value) async throws -> Value { + let handler = try await HBLambdaHandler(context: self.initializationContext) + let value = try await test(HBLambdaTestClient(handler: handler, context: context)) + try await self.terminator.terminate(eventLoop: self.context.eventLoop).get() + self.terminator = .init() + return value + } +} + +/// Client used to send requests to lambda test framework +public struct HBLambdaTestClient where Lambda.Event: LambdaTestableEvent { + let handler: HBLambdaHandler + let context: LambdaContext + + func execute(uri: String, method: HTTPRequest.Method, headers: HTTPFields, body: ByteBuffer?) async throws -> Lambda.Output { + let event = try Lambda.Event(uri: uri, method: method, headers: headers, body: body) + return try await self.handler.handle(event, context: self.context) + } + + /// Send request to lambda test framework and call `testCallback`` on the response returned + /// + /// - Parameters: + /// - uri: Path of request + /// - method: Request method + /// - headers: Request headers + /// - body: Request body + /// - testCallback: closure to call on response returned by test framework + /// - Returns: Return value of test closure + @discardableResult public func execute( + uri: String, + method: HTTPRequest.Method, + headers: HTTPFields = [:], + body: ByteBuffer? = nil, + testCallback: @escaping (Lambda.Output) async throws -> Return + ) async throws -> Return { + let response = try await execute(uri: uri, method: method, headers: headers, body: body) + return try await testCallback(response) + } +} diff --git a/Sources/HummingbirdLambdaTesting/Lambda+Testing.swift b/Sources/HummingbirdLambdaTesting/Lambda+Testing.swift new file mode 100644 index 0000000..427a994 --- /dev/null +++ b/Sources/HummingbirdLambdaTesting/Lambda+Testing.swift @@ -0,0 +1,51 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Hummingbird server framework project +// +// Copyright (c) 2021-2024 the Hummingbird authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import HummingbirdLambda +import Logging + +extension HBLambda where Event: LambdaTestableEvent { + /// Test `HBLambda` + /// + /// The `test` closure uses the provided test client to make calls to the + /// lambda via `execute`. You can verify the contents of the output + /// event returned. + /// + /// The example below is using the `.router` framework to test + /// ```swift + /// struct HelloLambda: HBAPIGatewayLambda { + /// init(context: LambdaInitializationContext) {} + /// + /// func buildResponder() -> some HBResponder { + /// let router = HBRouter(context: Context.self) + /// router.get("hello") { request, _ in + /// return "Hello" + /// } + /// return router.buildResponder() + /// } + /// } + /// try await HelloLambda.test { client in + /// try await client.execute(uri: "/hello", method: .get) { response in + /// XCTAssertEqual(response.body, "Hello") + /// } + /// } + /// ``` + public static func test( + logLevel: Logger.Level = .debug, + _ test: @escaping @Sendable (HBLambdaTestClient) async throws -> Value + ) async throws -> Value { + let lambda = HBLambdaTestFramework(logLevel: logLevel) + return try await lambda.run(test) + } +} diff --git a/Sources/HummingbirdLambdaTesting/LambdaEvent.swift b/Sources/HummingbirdLambdaTesting/LambdaEvent.swift new file mode 100644 index 0000000..560de1c --- /dev/null +++ b/Sources/HummingbirdLambdaTesting/LambdaEvent.swift @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Hummingbird server framework project +// +// Copyright (c) 2021-2024 the Hummingbird authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import HTTPTypes +import NIOCore + +public protocol LambdaTestableEvent { + init(uri: String, method: HTTPRequest.Method, headers: HTTPFields, body: ByteBuffer?) throws +} diff --git a/Tests/HummingbirdLambdaTests/LambdaTests.swift b/Tests/HummingbirdLambdaTests/LambdaTests.swift index dc5da64..2aa1d00 100644 --- a/Tests/HummingbirdLambdaTests/LambdaTests.swift +++ b/Tests/HummingbirdLambdaTests/LambdaTests.swift @@ -15,218 +15,142 @@ import AWSLambdaEvents @testable import AWSLambdaRuntimeCore @testable import HummingbirdLambda +import HummingbirdLambdaTesting import Logging import NIOCore import NIOPosix import XCTest final class LambdaTests: XCTestCase { - var eventLoopGroup: EventLoopGroup = NIOSingletons.posixEventLoopGroup - let allocator = ByteBufferAllocator() - let logger = Logger(label: "LambdaTests") - - var initializationContext: LambdaInitializationContext { - .init( - logger: self.logger, - eventLoop: self.eventLoopGroup.next(), - allocator: self.allocator, - terminator: .init() - ) - } + func testSimpleRoute() async throws { + struct HelloLambda: HBAPIGatewayLambda { + init(context: LambdaInitializationContext) {} - func newContext() -> LambdaContext { - LambdaContext( - requestID: UUID().uuidString, - traceID: "abc123", - invokedFunctionARN: "aws:arn:", - deadline: .now() + .seconds(3), - cognitoIdentity: nil, - clientContext: nil, - logger: Logger(label: "test"), - eventLoop: self.eventLoopGroup.next(), - allocator: ByteBufferAllocator() - ) + func buildResponder() -> some HBResponder { + let router = HBRouter(context: Context.self) + router.middlewares.add(HBLogRequestsMiddleware(.debug)) + router.get("hello") { request, _ in + XCTAssertEqual(request.head.authority, "127.0.0.1:8080") + return "Hello" + } + return router.buildResponder() + } + } + try await HelloLambda.test { client in + try await client.execute(uri: "/hello", method: .get) { response in + XCTAssertEqual(response.body, "Hello") + XCTAssertEqual(response.statusCode, .ok) + XCTAssertEqual(response.headers?["Content-Type"], "text/plain; charset=utf-8") + } + } } - func newEvent(uri: String, method: String, body: ByteBuffer? = nil) throws -> APIGatewayRequest { - let base64Body = body.map { "\"\(String(base64Encoding: $0.readableBytesView))\"" } ?? "null" - let request = """ - { - "httpMethod": "\(method)", - "body": \(base64Body), - "resource": "\(uri)", - "requestContext": { - "resourceId": "123456", - "apiId": "1234567890", - "resourcePath": "\(uri)", - "httpMethod": "\(method)", - "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", - "accountId": "123456789012", - "stage": "Prod", - "identity": { - "apiKey": null, - "userArn": null, - "cognitoAuthenticationType": null, - "caller": null, - "userAgent": "Custom User Agent String", - "user": null, - "cognitoIdentityPoolId": null, - "cognitoAuthenticationProvider": null, - "sourceIp": "127.0.0.1", - "accountId": null - }, - "extendedRequestId": null, - "path": "\(uri)" - }, - "queryStringParameters": null, - "multiValueQueryStringParameters": null, - "headers": { - "Host": "127.0.0.1:3000", - "Connection": "keep-alive", - "Cache-Control": "max-age=0", - "Dnt": "1", - "Upgrade-Insecure-Requests": "1", - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36 Edg/78.0.276.24", - "Sec-Fetch-User": "?1", - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", - "Sec-Fetch-Site": "none", - "Sec-Fetch-Mode": "navigate", - "Accept-Encoding": "gzip, deflate, br", - "Accept-Language": "en-US,en;q=0.9", - "X-Forwarded-Proto": "http", - "X-Forwarded-Port": "3000" - }, - "multiValueHeaders": { - "Host": ["127.0.0.1:3000"], - "Connection": ["keep-alive"], - "Cache-Control": ["max-age=0"], - "Dnt": ["1"], - "Upgrade-Insecure-Requests": ["1"], - "User-Agent": ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36 Edg/78.0.276.24"], - "Sec-Fetch-User": ["?1"], - "Accept": ["text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3"], - "Sec-Fetch-Site": ["none"], - "Sec-Fetch-Mode": ["navigate"], - "Accept-Encoding": ["gzip, deflate, br"], - "Accept-Language": ["en-US,en;q=0.9"], - "X-Forwarded-Proto": ["http"], - "X-Forwarded-Port": ["3000"] - }, - "pathParameters": null, - "stageVariables": null, - "path": "\(uri)", - "isBase64Encoded": \(body != nil) + func testBase64Encoding() async throws { + struct HelloLambda: HBAPIGatewayLambda { + init(context: LambdaInitializationContext) {} + func buildResponder() -> some HBResponder { + let router = HBRouter(context: Context.self) + router.middlewares.add(HBLogRequestsMiddleware(.debug)) + router.post { request, _ in + let buffer = try await request.body.collect(upTo: .max) + return HBResponse(status: .ok, body: .init(byteBuffer: buffer)) + } + return router.buildResponder() + } + } + try await HelloLambda.test { client in + let body = ByteBuffer(bytes: (0...255).map { _ in UInt8.random(in: 0...255) }) + try await client.execute(uri: "/", method: .post, body: body) { response in + XCTAssertEqual(response.isBase64Encoded, true) + XCTAssertEqual(response.body, String(base64Encoding: body.readableBytesView)) + } } - """ - return try JSONDecoder().decode(APIGatewayRequest.self, from: Data(request.utf8)) } - func newV2Event(uri: String, method: String) throws -> APIGatewayV2Request { - let request = """ - { - "routeKey":"\(method) \(uri)", - "version":"2.0", - "rawPath":"\(uri)", - "stageVariables":{ - "foo":"bar" - }, - "requestContext":{ - "timeEpoch":1587750461466, - "domainPrefix":"hello", - "authorizer":{ - "jwt":{ - "scopes":[ - "hello" - ], - "claims":{ - "aud":"customers", - "iss":"https://hello.test.com/", - "iat":"1587749276", - "exp":"1587756476" - } - } - }, - "accountId":"0123456789", - "stage":"$default", - "domainName":"hello.test.com", - "apiId":"pb5dg6g3rg", - "requestId":"LgLpnibOFiAEPCA=", - "http":{ - "path":"\(uri)", - "userAgent":"Paw/3.1.10 (Macintosh; OS X/10.15.4) GCDHTTPRequest", - "method":"\(method)", - "protocol":"HTTP/1.1", - "sourceIp":"91.64.117.86" - }, - "time":"24/Apr/2020:17:47:41 +0000" - }, - "isBase64Encoded":false, - "rawQueryString":"foo=bar", - "queryStringParameters":{ - "foo":"bar" - }, - "headers":{ - "x-forwarded-proto":"https", - "x-forwarded-for":"91.64.117.86", - "x-forwarded-port":"443", - "authorization":"Bearer abc123", - "host":"hello.test.com", - "x-amzn-trace-id":"Root=1-5ea3263d-07c5d5ddfd0788bed7dad831", - "user-agent":"Paw/3.1.10 (Macintosh; OS X/10.15.4) GCDHTTPRequest", - "content-length":"0" + func testHeaderValues() async throws { + struct HelloLambda: HBAPIGatewayLambda { + init(context: LambdaInitializationContext) {} + + func buildResponder() -> some HBResponder { + let router = HBRouter(context: Context.self) + router.middlewares.add(HBLogRequestsMiddleware(.debug)) + router.post { request, _ -> HTTPResponse.Status in + XCTAssertEqual(request.headers[.userAgent], "HBXCT/2.0") + XCTAssertEqual(request.headers[.acceptLanguage], "en") + return .ok + } + router.post("/multi") { request, _ -> HTTPResponse.Status in + XCTAssertEqual(request.headers[.userAgent], "HBXCT/2.0") + XCTAssertEqual(request.headers[values: .acceptLanguage], ["en", "fr"]) + return .ok + } + return router.buildResponder() + } + } + try await HelloLambda.test { client in + try await client.execute(uri: "/", method: .post, headers: [.userAgent: "HBXCT/2.0", .acceptLanguage: "en"]) { response in + XCTAssertEqual(response.statusCode, .ok) + } + var headers: HTTPFields = [.userAgent: "HBXCT/2.0", .acceptLanguage: "en"] + headers[values: .acceptLanguage].append("fr") + try await client.execute(uri: "/multi", method: .post, headers: headers) { response in + XCTAssertEqual(response.statusCode, .ok) } } - """ - return try JSONDecoder().decode(APIGatewayV2Request.self, from: Data(request.utf8)) } - func testSimpleRoute() async throws { + func testQueryValues() async throws { struct HelloLambda: HBAPIGatewayLambda { init(context: LambdaInitializationContext) {} func buildResponder() -> some HBResponder { let router = HBRouter(context: Context.self) router.middlewares.add(HBLogRequestsMiddleware(.debug)) - router.get("hello") { request, _ in - XCTAssertEqual(request.head.authority, "127.0.0.1:3000") - XCTAssertEqual(request.headers[.userAgent], "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36 Edg/78.0.276.24") - return "Hello" + router.post { request, _ -> HTTPResponse.Status in + XCTAssertEqual(request.uri.queryParameters["foo"], "bar") + return .ok + } + router.post("/multi") { request, _ -> HTTPResponse.Status in + XCTAssertEqual(request.uri.queryParameters.getAll("foo"), ["bar1", "bar2"]) + return .ok } return router.buildResponder() } } - let lambda = try await HBLambdaHandler(context: self.initializationContext) - let context = self.newContext() - let event = try newEvent(uri: "/hello", method: "GET") - let response = try await lambda.handle(event, context: context) - XCTAssertEqual(response.body, "Hello") - XCTAssertEqual(response.statusCode, .ok) - XCTAssertEqual(response.headers?["Content-Type"], "text/plain; charset=utf-8") + try await HelloLambda.test { client in + try await client.execute(uri: "/?foo=bar", method: .post) { response in + XCTAssertEqual(response.statusCode, .ok) + } + try await client.execute(uri: "/multi?foo=bar1&foo=bar2", method: .post) { response in + XCTAssertEqual(response.statusCode, .ok) + } + } } - func testBase64Encoding() async throws { + func testErrorEncoding() async throws { struct HelloLambda: HBAPIGatewayLambda { + static let body = "BadRequest" init(context: LambdaInitializationContext) {} + func buildResponder() -> some HBResponder { let router = HBRouter(context: Context.self) router.middlewares.add(HBLogRequestsMiddleware(.debug)) - router.post { request, _ in - let buffer = try await request.body.collect(upTo: .max) - return HBResponse(status: .ok, body: .init(byteBuffer: buffer)) + router.post { _, _ -> String in + throw HBHTTPError(.badRequest, message: Self.body) } return router.buildResponder() } } - let lambda = try await HBLambdaHandler(context: self.initializationContext) - let context = self.newContext() - let data = (0...255).map { _ in UInt8.random(in: 0...255) } - let event = try newEvent(uri: "/", method: "POST", body: ByteBufferAllocator().buffer(bytes: data)) - let response = try await lambda.handle(event, context: context) - XCTAssertEqual(response.isBase64Encoded, true) - XCTAssertEqual(response.body, String(base64Encoding: data)) + try await HelloLambda.test { client in + try await client.execute(uri: "/", method: .post) { response in + XCTAssertEqual(response.statusCode, .badRequest) + XCTAssertEqual(response.body, HelloLambda.body) + XCTAssertEqual(response.headers?["Content-Length"], HelloLambda.body.utf8.count.description) + } + } } - func testAPIGatewayV2Decoding() async throws { + func testSimpleRouteV2() async throws { struct HelloLambda: HBAPIGatewayV2Lambda { // define input and output typealias Event = APIGatewayV2Request @@ -239,42 +163,100 @@ final class LambdaTests: XCTestCase { let router = HBRouter(context: Context.self) router.middlewares.add(HBLogRequestsMiddleware(.debug)) router.post { request, _ in - XCTAssertEqual(request.headers[.authorization], "Bearer abc123") - XCTAssertEqual(request.head.authority, "hello.test.com") - return "hello" + XCTAssertEqual(request.head.authority, "127.0.0.1:8080") + return ["response": "hello"] } return router.buildResponder() } } - let lambda = try await HBLambdaHandler(context: self.initializationContext) - let context = self.newContext() - let event = try newV2Event(uri: "/", method: "POST") - let response = try await lambda.handle(event, context: context) - XCTAssertEqual(response.statusCode, .ok) - XCTAssertEqual(response.body, "hello") + try await HelloLambda.test { client in + try await client.execute(uri: "/", method: .post) { response in + XCTAssertEqual(response.statusCode, .ok) + XCTAssertEqual(response.headers?["Content-Type"], "application/json; charset=utf-8") + XCTAssertEqual(response.body, #"{"response":"hello"}"#) + } + } } - func testErrorEncoding() async throws { + func testBase64EncodingV2() async throws { + struct HelloLambda: HBAPIGatewayV2Lambda { + init(context: LambdaInitializationContext) {} + func buildResponder() -> some HBResponder { + let router = HBRouter(context: Context.self) + router.middlewares.add(HBLogRequestsMiddleware(.debug)) + router.post { request, _ in + let buffer = try await request.body.collect(upTo: .max) + return HBResponse(status: .ok, body: .init(byteBuffer: buffer)) + } + return router.buildResponder() + } + } + try await HelloLambda.test { client in + let body = ByteBuffer(bytes: (0...255).map { _ in UInt8.random(in: 0...255) }) + try await client.execute(uri: "/", method: .post, headers: [.userAgent: "HBXCT/2.0"], body: body) { response in + XCTAssertEqual(response.isBase64Encoded, true) + XCTAssertEqual(response.body, String(base64Encoding: body.readableBytesView)) + } + } + } + + func testHeaderValuesV2() async throws { struct HelloLambda: HBAPIGatewayV2Lambda { - static let body = "BadRequest" init(context: LambdaInitializationContext) {} func buildResponder() -> some HBResponder { let router = HBRouter(context: Context.self) router.middlewares.add(HBLogRequestsMiddleware(.debug)) - router.post { _, _ -> String in - throw HBHTTPError(.badRequest, message: Self.body) + router.post { request, _ -> HTTPResponse.Status in + XCTAssertEqual(request.headers[.userAgent], "HBXCT/2.0") + XCTAssertEqual(request.headers[.acceptLanguage], "en") + return .ok + } + router.post("/multi") { request, _ -> HTTPResponse.Status in + XCTAssertEqual(request.headers[.userAgent], "HBXCT/2.0") + XCTAssertEqual(request.headers[values: .acceptLanguage], ["en", "fr"]) + return .ok } return router.buildResponder() } } + try await HelloLambda.test { client in + try await client.execute(uri: "/", method: .post, headers: [.userAgent: "HBXCT/2.0", .acceptLanguage: "en"]) { response in + XCTAssertEqual(response.statusCode, .ok) + } + var headers: HTTPFields = [.userAgent: "HBXCT/2.0", .acceptLanguage: "en"] + headers[values: .acceptLanguage].append("fr") + try await client.execute(uri: "/multi", method: .post, headers: headers) { response in + XCTAssertEqual(response.statusCode, .ok) + } + } + } - let lambda = try await HBLambdaHandler(context: self.initializationContext) - let context = self.newContext() - let event = try newV2Event(uri: "/", method: "POST") - let response = try await lambda.handle(event, context: context) - XCTAssertEqual(response.statusCode, .badRequest) - XCTAssertEqual(response.body, HelloLambda.body) - XCTAssertEqual(response.headers?["Content-Length"], HelloLambda.body.utf8.count.description) + func testQueryValuesV2() async throws { + struct HelloLambda: HBAPIGatewayV2Lambda { + init(context: LambdaInitializationContext) {} + + func buildResponder() -> some HBResponder { + let router = HBRouter(context: Context.self) + router.middlewares.add(HBLogRequestsMiddleware(.debug)) + router.post { request, _ -> HTTPResponse.Status in + XCTAssertEqual(request.uri.queryParameters["foo"], "bar") + return .ok + } + router.post("/multi") { request, _ -> HTTPResponse.Status in + XCTAssertEqual(request.uri.queryParameters.getAll("foo"), ["bar1", "bar2"]) + return .ok + } + return router.buildResponder() + } + } + try await HelloLambda.test { client in + try await client.execute(uri: "/?foo=bar", method: .post) { response in + XCTAssertEqual(response.statusCode, .ok) + } + try await client.execute(uri: "/multi?foo=bar1&foo=bar2", method: .post) { response in + XCTAssertEqual(response.statusCode, .ok) + } + } } }