From 102f68f57ead5ee688ad9ce086ff1cb1391136be Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Fri, 1 Mar 2024 18:34:30 +0000 Subject: [PATCH 1/5] Start HummingbirdLambdaXCT --- Package.swift | 5 ++ .../Response+APIGateway.swift | 6 +- .../APIGatewayLambda.swift | 87 ++++++++++++++++++ .../APIGatewayV2Lambda.swift | 80 +++++++++++++++++ .../HummingbirdLambdaXCT/HBXCTLambda.swift | 90 +++++++++++++++++++ Sources/HummingbirdLambdaXCT/Lambda+XCT.swift | 25 ++++++ .../HummingbirdLambdaXCT/LambdaEvent.swift | 21 +++++ .../HummingbirdLambdaTests/LambdaTests.swift | 24 +++++ 8 files changed, 335 insertions(+), 3 deletions(-) create mode 100644 Sources/HummingbirdLambdaXCT/APIGatewayLambda.swift create mode 100644 Sources/HummingbirdLambdaXCT/APIGatewayV2Lambda.swift create mode 100644 Sources/HummingbirdLambdaXCT/HBXCTLambda.swift create mode 100644 Sources/HummingbirdLambdaXCT/Lambda+XCT.swift create mode 100644 Sources/HummingbirdLambdaXCT/LambdaEvent.swift diff --git a/Package.swift b/Package.swift index b9633a6..537f3ed 100644 --- a/Package.swift +++ b/Package.swift @@ -25,11 +25,16 @@ let package = Package( .product(name: "ExtrasBase64", package: "swift-extras-base64"), .product(name: "Hummingbird", package: "hummingbird"), ]), + .target(name: "HummingbirdLambdaXCT", dependencies: [ + .byName(name: "HummingbirdLambda"), + .product(name: "HummingbirdXCT", package: "hummingbird"), + ]), .executableTarget(name: "HBLambdaTest", dependencies: [ .byName(name: "HummingbirdLambda"), ]), .testTarget(name: "HummingbirdLambdaTests", dependencies: [ .byName(name: "HummingbirdLambda"), + .byName(name: "HummingbirdLambdaXCT"), .product(name: "HummingbirdXCT", package: "hummingbird"), .product(name: "NIOPosix", package: "swift-nio"), ]), 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/HummingbirdLambdaXCT/APIGatewayLambda.swift b/Sources/HummingbirdLambdaXCT/APIGatewayLambda.swift new file mode 100644 index 0000000..0ea52a7 --- /dev/null +++ b/Sources/HummingbirdLambdaXCT/APIGatewayLambda.swift @@ -0,0 +1,87 @@ +//===----------------------------------------------------------------------===// +// +// 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: XCTLambdaEvent { + 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([:]) { 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": "\(uri)", + "requestContext": { + "resourceId": "123456", + "apiId": "1234567890", + "resourcePath": "\(uri)", + "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": "\(uri)", + "isBase64Encoded": \(body != nil) + } + """ + self = try JSONDecoder().decode(Self.self, from: Data(eventJson.utf8)) + } +} diff --git a/Sources/HummingbirdLambdaXCT/APIGatewayV2Lambda.swift b/Sources/HummingbirdLambdaXCT/APIGatewayV2Lambda.swift new file mode 100644 index 0000000..f5b0816 --- /dev/null +++ b/Sources/HummingbirdLambdaXCT/APIGatewayV2Lambda.swift @@ -0,0 +1,80 @@ +//===----------------------------------------------------------------------===// +// +// 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 +@_spi(HBXCT) import HummingbirdXCT +import NIOCore + +extension APIGatewayV2Request: XCTLambdaEvent { + public init(uri: String, method: HTTPRequest.Method, headers: HTTPFields, body: ByteBuffer?) throws { + let url = HBURL(uri) + let eventJson = """ + { + "routeKey":"\(method) \(uri)", + "version":"2.0", + "rawPath":"\(uri)", + "stageVariables":none, + "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":"\(url.query ?? "")", + "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" + } + } + """ + self = try JSONDecoder().decode(Self.self, from: Data(eventJson.utf8)) + } +} diff --git a/Sources/HummingbirdLambdaXCT/HBXCTLambda.swift b/Sources/HummingbirdLambdaXCT/HBXCTLambda.swift new file mode 100644 index 0000000..d6c20a2 --- /dev/null +++ b/Sources/HummingbirdLambdaXCT/HBXCTLambda.swift @@ -0,0 +1,90 @@ +//===----------------------------------------------------------------------===// +// +// 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 HummingbirdXCT +import Logging +import NIOCore +import NIOPosix + +class HBXCTLambda where Lambda.Event: XCTLambdaEvent { + let context: LambdaContext + var terminator: LambdaTerminator + + init() { + self.context = .init( + requestID: UUID().uuidString, + traceID: "abc123", + invokedFunctionARN: "aws:arn:", + deadline: .now() + .seconds(15), + cognitoIdentity: nil, + clientContext: nil, + logger: Logger(label: "HBXCTLambda"), + 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 (HBXCTLambdaClient) async throws -> Value) async throws -> Value { + let handler = try await HBLambdaHandler(context: self.initializationContext) + let value = try await test(HBXCTLambdaClient(handler: handler, context: context)) + try await self.terminator.terminate(eventLoop: self.context.eventLoop).get() + self.terminator = .init() + return value + } +} + +public struct HBXCTLambdaClient where Lambda.Event: XCTLambdaEvent { + 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 associated test framework and call test callback 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 XCTExecute( + 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/HummingbirdLambdaXCT/Lambda+XCT.swift b/Sources/HummingbirdLambdaXCT/Lambda+XCT.swift new file mode 100644 index 0000000..64097c2 --- /dev/null +++ b/Sources/HummingbirdLambdaXCT/Lambda+XCT.swift @@ -0,0 +1,25 @@ +//===----------------------------------------------------------------------===// +// +// 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 HummingbirdXCT + +extension HBLambda where Event: XCTLambdaEvent { + public static func test( + _ test: @escaping @Sendable (HBXCTLambdaClient) async throws -> Value + ) async throws -> Value { + let lambda = HBXCTLambda() + return try await lambda.run(test) + } +} diff --git a/Sources/HummingbirdLambdaXCT/LambdaEvent.swift b/Sources/HummingbirdLambdaXCT/LambdaEvent.swift new file mode 100644 index 0000000..cf914fe --- /dev/null +++ b/Sources/HummingbirdLambdaXCT/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 XCTLambdaEvent { + 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..6865c95 100644 --- a/Tests/HummingbirdLambdaTests/LambdaTests.swift +++ b/Tests/HummingbirdLambdaTests/LambdaTests.swift @@ -15,6 +15,7 @@ import AWSLambdaEvents @testable import AWSLambdaRuntimeCore @testable import HummingbirdLambda +import HummingbirdLambdaXCT import Logging import NIOCore import NIOPosix @@ -277,4 +278,27 @@ final class LambdaTests: XCTestCase { XCTAssertEqual(response.body, HelloLambda.body) XCTAssertEqual(response.headers?["Content-Length"], HelloLambda.body.utf8.count.description) } + + func testXCT() async throws { + struct HelloLambda: HBAPIGatewayLambda { + // define input and output + init(context: LambdaInitializationContext) {} + + func buildResponder() -> some HBResponder { + let router = HBRouter(context: Context.self) + router.middlewares.add(HBLogRequestsMiddleware(.debug)) + router.post { request, _ in + XCTAssertEqual(request.headers[.authorization], "Bearer abc123") + return "hello" + } + return router.buildResponder() + } + } + try await HelloLambda.test { client in + try await client.XCTExecute(uri: "/", method: .post, headers: [.authorization: "Bearer abc123"]) { response in + XCTAssertEqual(response.statusCode, .ok) + XCTAssertEqual(response.body, "hello") + } + } + } } From 51ccae1a9caba2815d3584b9664d8d6b530c60f5 Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Sun, 3 Mar 2024 08:04:40 +0000 Subject: [PATCH 2/5] Add APIGatewayV2 event init --- .../APIGatewayV2Lambda.swift | 46 +++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/Sources/HummingbirdLambdaXCT/APIGatewayV2Lambda.swift b/Sources/HummingbirdLambdaXCT/APIGatewayV2Lambda.swift index f5b0816..1d264b8 100644 --- a/Sources/HummingbirdLambdaXCT/APIGatewayV2Lambda.swift +++ b/Sources/HummingbirdLambdaXCT/APIGatewayV2Lambda.swift @@ -16,18 +16,36 @@ import AWSLambdaEvents import Foundation import HTTPTypes import HummingbirdCore -@_spi(HBXCT) import HummingbirdXCT import NIOCore extension APIGatewayV2Request: XCTLambdaEvent { 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) \(uri)", + "routeKey":"\(method) \(url.path)", "version":"2.0", - "rawPath":"\(uri)", - "stageVariables":none, + "rawPath":"\(url.path)", + "stageVariables":null, "requestContext":{ "timeEpoch":1587750461466, "domainPrefix":"hello", @@ -50,7 +68,7 @@ extension APIGatewayV2Request: XCTLambdaEvent { "apiId":"pb5dg6g3rg", "requestId":"LgLpnibOFiAEPCA=", "http":{ - "path":"\(uri)", + "path":"\(url.path)", "userAgent":"Paw/3.1.10 (Macintosh; OS X/10.15.4) GCDHTTPRequest", "method":"\(method)", "protocol":"HTTP/1.1", @@ -58,21 +76,11 @@ extension APIGatewayV2Request: XCTLambdaEvent { }, "time":"24/Apr/2020:17:47:41 +0000" }, - "isBase64Encoded":false, + "body": \(base64Body), + "isBase64Encoded": \(body != nil), "rawQueryString":"\(url.query ?? "")", - "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" - } + "queryStringParameters":\(queryValueStrings), + "headers":\(headerValueStrings) } """ self = try JSONDecoder().decode(Self.self, from: Data(eventJson.utf8)) From 9c6272d767a314c9a9d57eee830ee1d45f4ee4b8 Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Sun, 3 Mar 2024 08:05:05 +0000 Subject: [PATCH 3/5] Tests now use HummingbirdLambdaXCT --- Package.swift | 2 - Sources/HummingbirdLambda/Lambda.swift | 3 - .../APIGatewayLambda.swift | 8 +- .../HummingbirdLambdaXCT/HBXCTLambda.swift | 7 +- Sources/HummingbirdLambdaXCT/Lambda+XCT.swift | 5 +- .../HummingbirdLambdaTests/LambdaTests.swift | 209 ++++++++++++++---- 6 files changed, 174 insertions(+), 60 deletions(-) diff --git a/Package.swift b/Package.swift index 537f3ed..96978c0 100644 --- a/Package.swift +++ b/Package.swift @@ -27,7 +27,6 @@ let package = Package( ]), .target(name: "HummingbirdLambdaXCT", dependencies: [ .byName(name: "HummingbirdLambda"), - .product(name: "HummingbirdXCT", package: "hummingbird"), ]), .executableTarget(name: "HBLambdaTest", dependencies: [ .byName(name: "HummingbirdLambda"), @@ -35,7 +34,6 @@ let package = Package( .testTarget(name: "HummingbirdLambdaTests", dependencies: [ .byName(name: "HummingbirdLambda"), .byName(name: "HummingbirdLambdaXCT"), - .product(name: "HummingbirdXCT", package: "hummingbird"), .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/HummingbirdLambdaXCT/APIGatewayLambda.swift b/Sources/HummingbirdLambdaXCT/APIGatewayLambda.swift index 0ea52a7..16dd24b 100644 --- a/Sources/HummingbirdLambdaXCT/APIGatewayLambda.swift +++ b/Sources/HummingbirdLambdaXCT/APIGatewayLambda.swift @@ -33,7 +33,7 @@ extension APIGatewayRequest: XCTLambdaEvent { 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([:]) { result, value in + 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] ?? [] @@ -48,11 +48,11 @@ extension APIGatewayRequest: XCTLambdaEvent { { "httpMethod": "\(method)", "body": \(base64Body), - "resource": "\(uri)", + "resource": "\(url.path)", "requestContext": { "resourceId": "123456", "apiId": "1234567890", - "resourcePath": "\(uri)", + "resourcePath": "\(url.path)", "httpMethod": "\(method)", "requestId": "\(UUID().uuidString)", "accountId": "123456789012", @@ -78,7 +78,7 @@ extension APIGatewayRequest: XCTLambdaEvent { "multiValueHeaders": \(multiHeaderValuesString), "pathParameters": null, "stageVariables": null, - "path": "\(uri)", + "path": "\(url.path)", "isBase64Encoded": \(body != nil) } """ diff --git a/Sources/HummingbirdLambdaXCT/HBXCTLambda.swift b/Sources/HummingbirdLambdaXCT/HBXCTLambda.swift index d6c20a2..1180d1f 100644 --- a/Sources/HummingbirdLambdaXCT/HBXCTLambda.swift +++ b/Sources/HummingbirdLambdaXCT/HBXCTLambda.swift @@ -17,7 +17,6 @@ import AWSLambdaEvents import Foundation import HTTPTypes @testable import HummingbirdLambda -import HummingbirdXCT import Logging import NIOCore import NIOPosix @@ -26,7 +25,9 @@ class HBXCTLambda where Lambda.Event: XCTLambdaEvent { let context: LambdaContext var terminator: LambdaTerminator - init() { + init(logLevel: Logger.Level) { + var logger = Logger(label: "HBXCTLambda") + logger.logLevel = logLevel self.context = .init( requestID: UUID().uuidString, traceID: "abc123", @@ -34,7 +35,7 @@ class HBXCTLambda where Lambda.Event: XCTLambdaEvent { deadline: .now() + .seconds(15), cognitoIdentity: nil, clientContext: nil, - logger: Logger(label: "HBXCTLambda"), + logger: logger, eventLoop: MultiThreadedEventLoopGroup.singleton.any(), allocator: ByteBufferAllocator() ) diff --git a/Sources/HummingbirdLambdaXCT/Lambda+XCT.swift b/Sources/HummingbirdLambdaXCT/Lambda+XCT.swift index 64097c2..7b530fa 100644 --- a/Sources/HummingbirdLambdaXCT/Lambda+XCT.swift +++ b/Sources/HummingbirdLambdaXCT/Lambda+XCT.swift @@ -13,13 +13,14 @@ //===----------------------------------------------------------------------===// import HummingbirdLambda -import HummingbirdXCT +import Logging extension HBLambda where Event: XCTLambdaEvent { public static func test( + logLevel: Logger.Level = .debug, _ test: @escaping @Sendable (HBXCTLambdaClient) async throws -> Value ) async throws -> Value { - let lambda = HBXCTLambda() + let lambda = HBXCTLambda(logLevel: logLevel) return try await lambda.run(test) } } diff --git a/Tests/HummingbirdLambdaTests/LambdaTests.swift b/Tests/HummingbirdLambdaTests/LambdaTests.swift index 6865c95..860e3e9 100644 --- a/Tests/HummingbirdLambdaTests/LambdaTests.swift +++ b/Tests/HummingbirdLambdaTests/LambdaTests.swift @@ -189,20 +189,19 @@ final class LambdaTests: XCTestCase { 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") + XCTAssertEqual(request.head.authority, "127.0.0.1:8080") return "Hello" } 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.XCTExecute(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 testBase64Encoding() async throws { @@ -218,16 +217,99 @@ final class LambdaTests: XCTestCase { 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 + let body = ByteBuffer(bytes: (0...255).map { _ in UInt8.random(in: 0...255) }) + try await client.XCTExecute(uri: "/", method: .post, body: body) { response in + XCTAssertEqual(response.isBase64Encoded, true) + XCTAssertEqual(response.body, String(base64Encoding: body.readableBytesView)) + } + } + } + + 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.XCTExecute(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.XCTExecute(uri: "/multi", method: .post, headers: headers) { response in + XCTAssertEqual(response.statusCode, .ok) + } + } + } + + 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.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.XCTExecute(uri: "/?foo=bar", method: .post) { response in + XCTAssertEqual(response.statusCode, .ok) + } + try await client.XCTExecute(uri: "/multi?foo=bar1&foo=bar2", method: .post) { response in + XCTAssertEqual(response.statusCode, .ok) + } + } + } + + 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 { _, _ -> String in + throw HBHTTPError(.badRequest, message: Self.body) + } + return router.buildResponder() + } + } + try await HelloLambda.test { client in + try await client.XCTExecute(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 @@ -240,64 +322,99 @@ 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.XCTExecute(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 { - 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, _ 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.XCTExecute(uri: "/", method: .post, headers: [.userAgent: "HBXCT/2.0"], body: body) { response in + XCTAssertEqual(response.isBase64Encoded, true) + XCTAssertEqual(response.body, String(base64Encoding: body.readableBytesView)) + } + } + } - 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 testHeaderValuesV2() 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.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.XCTExecute(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.XCTExecute(uri: "/multi", method: .post, headers: headers) { response in + XCTAssertEqual(response.statusCode, .ok) + } + } } - func testXCT() async throws { - struct HelloLambda: HBAPIGatewayLambda { - // define input and output + 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, _ in - XCTAssertEqual(request.headers[.authorization], "Bearer abc123") - 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() } } try await HelloLambda.test { client in - try await client.XCTExecute(uri: "/", method: .post, headers: [.authorization: "Bearer abc123"]) { response in + try await client.XCTExecute(uri: "/?foo=bar", method: .post) { response in + XCTAssertEqual(response.statusCode, .ok) + } + try await client.XCTExecute(uri: "/multi?foo=bar1&foo=bar2", method: .post) { response in XCTAssertEqual(response.statusCode, .ok) - XCTAssertEqual(response.body, "hello") } } } From f173c0d24cdfebed41842e1fc19b051c740ad8f7 Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Sun, 3 Mar 2024 08:16:12 +0000 Subject: [PATCH 4/5] Comments/cleanup --- .../APIGatewayLambda.swift | 1 + .../APIGatewayV2Lambda.swift | 1 + .../HummingbirdLambdaXCT/HBXCTLambda.swift | 3 +- Sources/HummingbirdLambdaXCT/Lambda+XCT.swift | 25 +++ .../HummingbirdLambdaTests/LambdaTests.swift | 159 ------------------ 5 files changed, 29 insertions(+), 160 deletions(-) diff --git a/Sources/HummingbirdLambdaXCT/APIGatewayLambda.swift b/Sources/HummingbirdLambdaXCT/APIGatewayLambda.swift index 16dd24b..ea9dc69 100644 --- a/Sources/HummingbirdLambdaXCT/APIGatewayLambda.swift +++ b/Sources/HummingbirdLambdaXCT/APIGatewayLambda.swift @@ -19,6 +19,7 @@ import HummingbirdCore import NIOCore extension APIGatewayRequest: XCTLambdaEvent { + /// 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) diff --git a/Sources/HummingbirdLambdaXCT/APIGatewayV2Lambda.swift b/Sources/HummingbirdLambdaXCT/APIGatewayV2Lambda.swift index 1d264b8..fccb980 100644 --- a/Sources/HummingbirdLambdaXCT/APIGatewayV2Lambda.swift +++ b/Sources/HummingbirdLambdaXCT/APIGatewayV2Lambda.swift @@ -19,6 +19,7 @@ import HummingbirdCore import NIOCore extension APIGatewayV2Request: XCTLambdaEvent { + /// 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) diff --git a/Sources/HummingbirdLambdaXCT/HBXCTLambda.swift b/Sources/HummingbirdLambdaXCT/HBXCTLambda.swift index 1180d1f..6f16e36 100644 --- a/Sources/HummingbirdLambdaXCT/HBXCTLambda.swift +++ b/Sources/HummingbirdLambdaXCT/HBXCTLambda.swift @@ -60,6 +60,7 @@ class HBXCTLambda where Lambda.Event: XCTLambdaEvent { } } +/// Client used to send requests to lambda test framework public struct HBXCTLambdaClient where Lambda.Event: XCTLambdaEvent { let handler: HBLambdaHandler let context: LambdaContext @@ -69,7 +70,7 @@ public struct HBXCTLambdaClient where Lambda.Event: XCTLambdaE return try await self.handler.handle(event, context: self.context) } - /// Send request to associated test framework and call test callback on the response returned + /// Send request to lambda test framework and call `testCallback`` on the response returned /// /// - Parameters: /// - uri: Path of request diff --git a/Sources/HummingbirdLambdaXCT/Lambda+XCT.swift b/Sources/HummingbirdLambdaXCT/Lambda+XCT.swift index 7b530fa..c8f918e 100644 --- a/Sources/HummingbirdLambdaXCT/Lambda+XCT.swift +++ b/Sources/HummingbirdLambdaXCT/Lambda+XCT.swift @@ -16,6 +16,31 @@ import HummingbirdLambda import Logging extension HBLambda where Event: XCTLambdaEvent { + /// Test `HBLambda` + /// + /// The `test` closure uses the provided test client to make calls to the + /// lambda via `XCTExecute`. 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.XCTExecute(uri: "/hello", method: .get) { response in + /// XCTAssertEqual(response.body, "Hello") + /// } + /// } + /// ``` public static func test( logLevel: Logger.Level = .debug, _ test: @escaping @Sendable (HBXCTLambdaClient) async throws -> Value diff --git a/Tests/HummingbirdLambdaTests/LambdaTests.swift b/Tests/HummingbirdLambdaTests/LambdaTests.swift index 860e3e9..ec16309 100644 --- a/Tests/HummingbirdLambdaTests/LambdaTests.swift +++ b/Tests/HummingbirdLambdaTests/LambdaTests.swift @@ -22,165 +22,6 @@ 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 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 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) - } - """ - 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" - } - } - """ - return try JSONDecoder().decode(APIGatewayV2Request.self, from: Data(request.utf8)) - } - func testSimpleRoute() async throws { struct HelloLambda: HBAPIGatewayLambda { init(context: LambdaInitializationContext) {} From 7e2ea22e1f6c82c369007636c5e05de330e230e7 Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Wed, 6 Mar 2024 10:14:14 +0000 Subject: [PATCH 5/5] XCT -> Testing --- Package.swift | 4 +-- .../APIGatewayLambda.swift | 2 +- .../APIGatewayV2Lambda.swift | 2 +- .../HBXCTLambda.swift | 12 ++++---- .../Lambda+Testing.swift} | 10 +++---- .../LambdaEvent.swift | 2 +- .../HummingbirdLambdaTests/LambdaTests.swift | 28 +++++++++---------- 7 files changed, 30 insertions(+), 30 deletions(-) rename Sources/{HummingbirdLambdaXCT => HummingbirdLambdaTesting}/APIGatewayLambda.swift (98%) rename Sources/{HummingbirdLambdaXCT => HummingbirdLambdaTesting}/APIGatewayV2Lambda.swift (98%) rename Sources/{HummingbirdLambdaXCT => HummingbirdLambdaTesting}/HBXCTLambda.swift (85%) rename Sources/{HummingbirdLambdaXCT/Lambda+XCT.swift => HummingbirdLambdaTesting/Lambda+Testing.swift} (80%) rename Sources/{HummingbirdLambdaXCT => HummingbirdLambdaTesting}/LambdaEvent.swift (94%) diff --git a/Package.swift b/Package.swift index 96978c0..d9ed4a6 100644 --- a/Package.swift +++ b/Package.swift @@ -25,7 +25,7 @@ let package = Package( .product(name: "ExtrasBase64", package: "swift-extras-base64"), .product(name: "Hummingbird", package: "hummingbird"), ]), - .target(name: "HummingbirdLambdaXCT", dependencies: [ + .target(name: "HummingbirdLambdaTesting", dependencies: [ .byName(name: "HummingbirdLambda"), ]), .executableTarget(name: "HBLambdaTest", dependencies: [ @@ -33,7 +33,7 @@ let package = Package( ]), .testTarget(name: "HummingbirdLambdaTests", dependencies: [ .byName(name: "HummingbirdLambda"), - .byName(name: "HummingbirdLambdaXCT"), + .byName(name: "HummingbirdLambdaTesting"), .product(name: "NIOPosix", package: "swift-nio"), ]), ] diff --git a/Sources/HummingbirdLambdaXCT/APIGatewayLambda.swift b/Sources/HummingbirdLambdaTesting/APIGatewayLambda.swift similarity index 98% rename from Sources/HummingbirdLambdaXCT/APIGatewayLambda.swift rename to Sources/HummingbirdLambdaTesting/APIGatewayLambda.swift index ea9dc69..fef033d 100644 --- a/Sources/HummingbirdLambdaXCT/APIGatewayLambda.swift +++ b/Sources/HummingbirdLambdaTesting/APIGatewayLambda.swift @@ -18,7 +18,7 @@ import HTTPTypes import HummingbirdCore import NIOCore -extension APIGatewayRequest: XCTLambdaEvent { +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" diff --git a/Sources/HummingbirdLambdaXCT/APIGatewayV2Lambda.swift b/Sources/HummingbirdLambdaTesting/APIGatewayV2Lambda.swift similarity index 98% rename from Sources/HummingbirdLambdaXCT/APIGatewayV2Lambda.swift rename to Sources/HummingbirdLambdaTesting/APIGatewayV2Lambda.swift index fccb980..4436d36 100644 --- a/Sources/HummingbirdLambdaXCT/APIGatewayV2Lambda.swift +++ b/Sources/HummingbirdLambdaTesting/APIGatewayV2Lambda.swift @@ -18,7 +18,7 @@ import HTTPTypes import HummingbirdCore import NIOCore -extension APIGatewayV2Request: XCTLambdaEvent { +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" diff --git a/Sources/HummingbirdLambdaXCT/HBXCTLambda.swift b/Sources/HummingbirdLambdaTesting/HBXCTLambda.swift similarity index 85% rename from Sources/HummingbirdLambdaXCT/HBXCTLambda.swift rename to Sources/HummingbirdLambdaTesting/HBXCTLambda.swift index 6f16e36..f82871f 100644 --- a/Sources/HummingbirdLambdaXCT/HBXCTLambda.swift +++ b/Sources/HummingbirdLambdaTesting/HBXCTLambda.swift @@ -21,12 +21,12 @@ import Logging import NIOCore import NIOPosix -class HBXCTLambda where Lambda.Event: XCTLambdaEvent { +class HBLambdaTestFramework where Lambda.Event: LambdaTestableEvent { let context: LambdaContext var terminator: LambdaTerminator init(logLevel: Logger.Level) { - var logger = Logger(label: "HBXCTLambda") + var logger = Logger(label: "HBTestLambda") logger.logLevel = logLevel self.context = .init( requestID: UUID().uuidString, @@ -51,9 +51,9 @@ class HBXCTLambda where Lambda.Event: XCTLambdaEvent { ) } - func run(_ test: @escaping @Sendable (HBXCTLambdaClient) async throws -> Value) async throws -> Value { + 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(HBXCTLambdaClient(handler: handler, context: context)) + 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 @@ -61,7 +61,7 @@ class HBXCTLambda where Lambda.Event: XCTLambdaEvent { } /// Client used to send requests to lambda test framework -public struct HBXCTLambdaClient where Lambda.Event: XCTLambdaEvent { +public struct HBLambdaTestClient where Lambda.Event: LambdaTestableEvent { let handler: HBLambdaHandler let context: LambdaContext @@ -79,7 +79,7 @@ public struct HBXCTLambdaClient where Lambda.Event: XCTLambdaE /// - body: Request body /// - testCallback: closure to call on response returned by test framework /// - Returns: Return value of test closure - @discardableResult public func XCTExecute( + @discardableResult public func execute( uri: String, method: HTTPRequest.Method, headers: HTTPFields = [:], diff --git a/Sources/HummingbirdLambdaXCT/Lambda+XCT.swift b/Sources/HummingbirdLambdaTesting/Lambda+Testing.swift similarity index 80% rename from Sources/HummingbirdLambdaXCT/Lambda+XCT.swift rename to Sources/HummingbirdLambdaTesting/Lambda+Testing.swift index c8f918e..427a994 100644 --- a/Sources/HummingbirdLambdaXCT/Lambda+XCT.swift +++ b/Sources/HummingbirdLambdaTesting/Lambda+Testing.swift @@ -15,11 +15,11 @@ import HummingbirdLambda import Logging -extension HBLambda where Event: XCTLambdaEvent { +extension HBLambda where Event: LambdaTestableEvent { /// Test `HBLambda` /// /// The `test` closure uses the provided test client to make calls to the - /// lambda via `XCTExecute`. You can verify the contents of the output + /// lambda via `execute`. You can verify the contents of the output /// event returned. /// /// The example below is using the `.router` framework to test @@ -36,16 +36,16 @@ extension HBLambda where Event: XCTLambdaEvent { /// } /// } /// try await HelloLambda.test { client in - /// try await client.XCTExecute(uri: "/hello", method: .get) { response 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 (HBXCTLambdaClient) async throws -> Value + _ test: @escaping @Sendable (HBLambdaTestClient) async throws -> Value ) async throws -> Value { - let lambda = HBXCTLambda(logLevel: logLevel) + let lambda = HBLambdaTestFramework(logLevel: logLevel) return try await lambda.run(test) } } diff --git a/Sources/HummingbirdLambdaXCT/LambdaEvent.swift b/Sources/HummingbirdLambdaTesting/LambdaEvent.swift similarity index 94% rename from Sources/HummingbirdLambdaXCT/LambdaEvent.swift rename to Sources/HummingbirdLambdaTesting/LambdaEvent.swift index cf914fe..560de1c 100644 --- a/Sources/HummingbirdLambdaXCT/LambdaEvent.swift +++ b/Sources/HummingbirdLambdaTesting/LambdaEvent.swift @@ -16,6 +16,6 @@ import Foundation import HTTPTypes import NIOCore -public protocol XCTLambdaEvent { +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 ec16309..2aa1d00 100644 --- a/Tests/HummingbirdLambdaTests/LambdaTests.swift +++ b/Tests/HummingbirdLambdaTests/LambdaTests.swift @@ -15,7 +15,7 @@ import AWSLambdaEvents @testable import AWSLambdaRuntimeCore @testable import HummingbirdLambda -import HummingbirdLambdaXCT +import HummingbirdLambdaTesting import Logging import NIOCore import NIOPosix @@ -37,7 +37,7 @@ final class LambdaTests: XCTestCase { } } try await HelloLambda.test { client in - try await client.XCTExecute(uri: "/hello", method: .get) { response 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") @@ -60,7 +60,7 @@ final class LambdaTests: XCTestCase { } try await HelloLambda.test { client in let body = ByteBuffer(bytes: (0...255).map { _ in UInt8.random(in: 0...255) }) - try await client.XCTExecute(uri: "/", method: .post, body: body) { response in + try await client.execute(uri: "/", method: .post, body: body) { response in XCTAssertEqual(response.isBase64Encoded, true) XCTAssertEqual(response.body, String(base64Encoding: body.readableBytesView)) } @@ -88,12 +88,12 @@ final class LambdaTests: XCTestCase { } } try await HelloLambda.test { client in - try await client.XCTExecute(uri: "/", method: .post, headers: [.userAgent: "HBXCT/2.0", .acceptLanguage: "en"]) { response 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.XCTExecute(uri: "/multi", method: .post, headers: headers) { response in + try await client.execute(uri: "/multi", method: .post, headers: headers) { response in XCTAssertEqual(response.statusCode, .ok) } } @@ -118,10 +118,10 @@ final class LambdaTests: XCTestCase { } } try await HelloLambda.test { client in - try await client.XCTExecute(uri: "/?foo=bar", method: .post) { response in + try await client.execute(uri: "/?foo=bar", method: .post) { response in XCTAssertEqual(response.statusCode, .ok) } - try await client.XCTExecute(uri: "/multi?foo=bar1&foo=bar2", method: .post) { response in + try await client.execute(uri: "/multi?foo=bar1&foo=bar2", method: .post) { response in XCTAssertEqual(response.statusCode, .ok) } } @@ -142,7 +142,7 @@ final class LambdaTests: XCTestCase { } } try await HelloLambda.test { client in - try await client.XCTExecute(uri: "/", method: .post) { response 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) @@ -170,7 +170,7 @@ final class LambdaTests: XCTestCase { } } try await HelloLambda.test { client in - try await client.XCTExecute(uri: "/", method: .post) { response 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"}"#) @@ -193,7 +193,7 @@ final class LambdaTests: XCTestCase { } try await HelloLambda.test { client in let body = ByteBuffer(bytes: (0...255).map { _ in UInt8.random(in: 0...255) }) - try await client.XCTExecute(uri: "/", method: .post, headers: [.userAgent: "HBXCT/2.0"], body: body) { response in + 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)) } @@ -221,12 +221,12 @@ final class LambdaTests: XCTestCase { } } try await HelloLambda.test { client in - try await client.XCTExecute(uri: "/", method: .post, headers: [.userAgent: "HBXCT/2.0", .acceptLanguage: "en"]) { response 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.XCTExecute(uri: "/multi", method: .post, headers: headers) { response in + try await client.execute(uri: "/multi", method: .post, headers: headers) { response in XCTAssertEqual(response.statusCode, .ok) } } @@ -251,10 +251,10 @@ final class LambdaTests: XCTestCase { } } try await HelloLambda.test { client in - try await client.XCTExecute(uri: "/?foo=bar", method: .post) { response in + try await client.execute(uri: "/?foo=bar", method: .post) { response in XCTAssertEqual(response.statusCode, .ok) } - try await client.XCTExecute(uri: "/multi?foo=bar1&foo=bar2", method: .post) { response in + try await client.execute(uri: "/multi?foo=bar1&foo=bar2", method: .post) { response in XCTAssertEqual(response.statusCode, .ok) } }