diff --git a/Package.swift b/Package.swift index cdd1453a..624cf6b5 100644 --- a/Package.swift +++ b/Package.swift @@ -8,8 +8,12 @@ let package = Package( .macOS(.v10_13), ], products: [ + // core library .library(name: "AWSLambdaRuntime", targets: ["AWSLambdaRuntime"]), + // common AWS events .library(name: "AWSLambdaEvents", targets: ["AWSLambdaEvents"]), + // for testing only + .library(name: "AWSLambdaTesting", targets: ["AWSLambdaTesting"]), ], dependencies: [ .package(url: "https://github.com/apple/swift-nio.git", from: "2.8.0"), @@ -26,6 +30,12 @@ let package = Package( .testTarget(name: "AWSLambdaRuntimeTests", dependencies: ["AWSLambdaRuntime"]), .target(name: "AWSLambdaEvents", dependencies: []), .testTarget(name: "AWSLambdaEventsTests", dependencies: ["AWSLambdaEvents"]), + // testing helper + .target(name: "AWSLambdaTesting", dependencies: [ + "AWSLambdaRuntime", + .product(name: "NIO", package: "swift-nio"), + ]), + .testTarget(name: "AWSLambdaTestingTests", dependencies: ["AWSLambdaTesting"]), // samples .target(name: "StringSample", dependencies: ["AWSLambdaRuntime"]), .target(name: "CodableSample", dependencies: ["AWSLambdaRuntime"]), diff --git a/Sources/AWSLambdaTesting/Lambda+Testing.swift b/Sources/AWSLambdaTesting/Lambda+Testing.swift new file mode 100644 index 00000000..7fa71221 --- /dev/null +++ b/Sources/AWSLambdaTesting/Lambda+Testing.swift @@ -0,0 +1,89 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// @testable for access of internal functions - this would only work for testing by design +@testable import AWSLambdaRuntime +import Dispatch +import Logging +import NIO + +extension Lambda { + public struct TestConfig { + public var requestId: String + public var traceId: String + public var invokedFunctionArn: String + public var timeout: DispatchTimeInterval + + public init(requestId: String = "\(DispatchTime.now().uptimeNanoseconds)", + traceId: String = "Root=\(DispatchTime.now().uptimeNanoseconds);Parent=\(DispatchTime.now().uptimeNanoseconds);Sampled=1", + invokedFunctionArn: String = "arn:aws:lambda:us-west-1:\(DispatchTime.now().uptimeNanoseconds):function:custom-runtime", + timeout: DispatchTimeInterval = .seconds(5)) { + self.requestId = requestId + self.traceId = traceId + self.invokedFunctionArn = invokedFunctionArn + self.timeout = timeout + } + } + + public static func test(_ closure: @escaping StringLambdaClosure, + with payload: String, + using config: TestConfig = .init()) throws -> String { + try Self.test(StringLambdaClosureWrapper(closure), with: payload, using: config) + } + + public static func test(_ closure: @escaping StringVoidLambdaClosure, + with payload: String, + using config: TestConfig = .init()) throws { + _ = try Self.test(StringVoidLambdaClosureWrapper(closure), with: payload, using: config) + } + + public static func test( + _ closure: @escaping CodableLambdaClosure, + with payload: In, + using config: TestConfig = .init() + ) throws -> Out { + try Self.test(CodableLambdaClosureWrapper(closure), with: payload, using: config) + } + + public static func test( + _ closure: @escaping CodableVoidLambdaClosure, + with payload: In, + using config: TestConfig = .init() + ) throws { + _ = try Self.test(CodableVoidLambdaClosureWrapper(closure), with: payload, using: config) + } + + public static func test( + _ handler: Handler, + with payload: In, + using config: TestConfig = .init() + ) throws -> Out where Handler.In == In, Handler.Out == Out { + let logger = Logger(label: "test") + let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) + defer { + try! eventLoopGroup.syncShutdownGracefully() + } + let eventLoop = eventLoopGroup.next() + let context = Context(requestId: config.requestId, + traceId: config.traceId, + invokedFunctionArn: config.invokedFunctionArn, + deadline: .now() + config.timeout, + logger: logger, + eventLoop: eventLoop) + + return try eventLoop.flatSubmit { + handler.handle(context: context, payload: payload) + }.wait() + } +} diff --git a/Tests/AWSLambdaTestingTests/Tests.swift b/Tests/AWSLambdaTestingTests/Tests.swift new file mode 100644 index 00000000..9db7b9b8 --- /dev/null +++ b/Tests/AWSLambdaTestingTests/Tests.swift @@ -0,0 +1,147 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaRuntime +import AWSLambdaTesting +import NIO +import XCTest + +class LambdaTestingTests: XCTestCase { + func testCodableClosure() { + struct Request: Codable { + let name: String + } + + struct Response: Codable { + let message: String + } + + let myLambda = { (_: Lambda.Context, request: Request, callback: (Result) -> Void) in + callback(.success(Response(message: "echo" + request.name))) + } + + let request = Request(name: UUID().uuidString) + var response: Response? + XCTAssertNoThrow(response = try Lambda.test(myLambda, with: request)) + XCTAssertEqual(response?.message, "echo" + request.name) + } + + func testCodableVoidClosure() { + struct Request: Codable { + let name: String + } + + let myLambda = { (_: Lambda.Context, _: Request, callback: (Result) -> Void) in + callback(.success(())) + } + + let request = Request(name: UUID().uuidString) + XCTAssertNoThrow(try Lambda.test(myLambda, with: request)) + } + + func testLambdaHandler() { + struct Request: Codable { + let name: String + } + + struct Response: Codable { + let message: String + } + + struct MyLambda: LambdaHandler { + typealias In = Request + typealias Out = Response + + func handle(context: Lambda.Context, payload: In, callback: @escaping (Result) -> Void) { + XCTAssertFalse(context.eventLoop.inEventLoop) + callback(.success(Response(message: "echo" + payload.name))) + } + } + + let request = Request(name: UUID().uuidString) + var response: Response? + XCTAssertNoThrow(response = try Lambda.test(MyLambda(), with: request)) + XCTAssertEqual(response?.message, "echo" + request.name) + } + + func testEventLoopLambdaHandler() { + struct MyLambda: EventLoopLambdaHandler { + typealias In = String + typealias Out = String + + func handle(context: Lambda.Context, payload: String) -> EventLoopFuture { + XCTAssertTrue(context.eventLoop.inEventLoop) + return context.eventLoop.makeSucceededFuture("echo" + payload) + } + } + + let input = UUID().uuidString + var result: String? + XCTAssertNoThrow(result = try Lambda.test(MyLambda(), with: input)) + XCTAssertEqual(result, "echo" + input) + } + + func testFailure() { + struct MyError: Error {} + + struct MyLambda: LambdaHandler { + typealias In = String + typealias Out = Void + + func handle(context: Lambda.Context, payload: In, callback: @escaping (Result) -> Void) { + callback(.failure(MyError())) + } + } + + XCTAssertThrowsError(try Lambda.test(MyLambda(), with: UUID().uuidString)) { error in + XCTAssert(error is MyError) + } + } + + func testAsyncLongRunning() { + var executed: Bool = false + let myLambda = { (_: Lambda.Context, _: String, callback: @escaping (Result) -> Void) in + DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 0.5) { + executed = true + callback(.success(())) + } + } + + XCTAssertNoThrow(try Lambda.test(myLambda, with: UUID().uuidString)) + XCTAssertTrue(executed) + } + + func testConfigValues() { + let timeout: TimeInterval = 4 + let config = Lambda.TestConfig( + requestId: UUID().uuidString, + traceId: UUID().uuidString, + invokedFunctionArn: "arn:\(UUID().uuidString)", + timeout: .seconds(4) + ) + + let myLambda = { (ctx: Lambda.Context, _: String, callback: @escaping (Result) -> Void) in + XCTAssertEqual(ctx.requestId, config.requestId) + XCTAssertEqual(ctx.traceId, config.traceId) + XCTAssertEqual(ctx.invokedFunctionArn, config.invokedFunctionArn) + + let secondsSinceEpoch = Double(Int64(bitPattern: ctx.deadline.rawValue)) / -1_000_000_000 + XCTAssertEqual(Date(timeIntervalSince1970: secondsSinceEpoch).timeIntervalSinceNow, timeout, accuracy: 0.1) + + callback(.success(())) + } + + XCTAssertNoThrow(try Lambda.test(myLambda, with: UUID().uuidString, using: config)) + } +}