diff --git a/Package.swift b/Package.swift index cdd1453a..eb47f596 100644 --- a/Package.swift +++ b/Package.swift @@ -24,7 +24,10 @@ let package = Package( .product(name: "NIOFoundationCompat", package: "swift-nio"), ]), .testTarget(name: "AWSLambdaRuntimeTests", dependencies: ["AWSLambdaRuntime"]), - .target(name: "AWSLambdaEvents", dependencies: []), + .target(name: "AWSLambdaEvents", dependencies: [ + .product(name: "NIOHTTP1", package: "swift-nio"), + .product(name: "NIOFoundationCompat", package: "swift-nio"), + ]), .testTarget(name: "AWSLambdaEventsTests", dependencies: ["AWSLambdaEvents"]), // samples .target(name: "StringSample", dependencies: ["AWSLambdaRuntime"]), diff --git a/Sources/AWSLambdaEvents/ALB.swift b/Sources/AWSLambdaEvents/ALB.swift new file mode 100644 index 00000000..587538d3 --- /dev/null +++ b/Sources/AWSLambdaEvents/ALB.swift @@ -0,0 +1,210 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-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 class Foundation.JSONEncoder +import NIOHTTP1 + +// https://github.com/aws/aws-lambda-go/blob/master/events/alb.go +public enum ALB { + /// ALBTargetGroupRequest contains data originating from the ALB Lambda target group integration + public struct TargetGroupRequest: DecodableBody { + /// ALBTargetGroupRequestContext contains the information to identify the load balancer invoking the lambda + public struct Context: Codable { + public let elb: ELBContext + } + + public let httpMethod: HTTPMethod + public let path: String + public let queryStringParameters: [String: [String]] + public let headers: HTTPHeaders + public let requestContext: Context + public let isBase64Encoded: Bool + public let body: String? + } + + /// ELBContext contains the information to identify the ARN invoking the lambda + public struct ELBContext: Codable { + public let targetGroupArn: String + } + + public struct TargetGroupResponse { + public let statusCode: HTTPResponseStatus + public let statusDescription: String? + public let headers: HTTPHeaders? + public let body: String + public let isBase64Encoded: Bool + + public init( + statusCode: HTTPResponseStatus, + statusDescription: String? = nil, + headers: HTTPHeaders? = nil, + body: String = "", + isBase64Encoded: Bool = false + ) { + self.statusCode = statusCode + self.statusDescription = statusDescription + self.headers = headers + self.body = body + self.isBase64Encoded = isBase64Encoded + } + } +} + +// MARK: - Request - + +extension ALB.TargetGroupRequest: Decodable { + enum CodingKeys: String, CodingKey { + case httpMethod + case path + case queryStringParameters + case multiValueQueryStringParameters + case headers + case multiValueHeaders + case requestContext + case isBase64Encoded + case body + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let method = try container.decode(String.self, forKey: .httpMethod) + self.httpMethod = HTTPMethod(rawValue: method) + + self.path = try container.decode(String.self, forKey: .path) + + // crazy multiple headers + // https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html#multi-value-headers + + if let multiValueQueryStringParameters = + try container.decodeIfPresent([String: [String]].self, forKey: .multiValueQueryStringParameters) { + self.queryStringParameters = multiValueQueryStringParameters + } else { + let singleValueQueryStringParameters = try container.decode( + [String: String].self, + forKey: .queryStringParameters + ) + self.queryStringParameters = singleValueQueryStringParameters.mapValues { [$0] } + } + + if let multiValueHeaders = + try container.decodeIfPresent([String: [String]].self, forKey: .multiValueHeaders) { + self.headers = HTTPHeaders(awsHeaders: multiValueHeaders) + } else { + let singleValueHeaders = try container.decode( + [String: String].self, + forKey: .headers + ) + let multiValueHeaders = singleValueHeaders.mapValues { [$0] } + self.headers = HTTPHeaders(awsHeaders: multiValueHeaders) + } + + self.requestContext = try container.decode(Context.self, forKey: .requestContext) + self.isBase64Encoded = try container.decode(Bool.self, forKey: .isBase64Encoded) + + let body = try container.decode(String.self, forKey: .body) + self.body = body != "" ? body : nil + } +} + +// MARK: - Response - + +extension ALB.TargetGroupResponse: Encodable { + static let MultiValueHeadersEnabledKey = + CodingUserInfoKey(rawValue: "ALB.TargetGroupResponse.MultiValueHeadersEnabledKey")! + + enum CodingKeys: String, CodingKey { + case statusCode + case statusDescription + case headers + case multiValueHeaders + case body + case isBase64Encoded + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(statusCode.code, forKey: .statusCode) + + let multiValueHeaderSupport = + encoder.userInfo[ALB.TargetGroupResponse.MultiValueHeadersEnabledKey] as? Bool ?? false + + switch (multiValueHeaderSupport, headers) { + case (true, .none): + try container.encode([String: String](), forKey: .multiValueHeaders) + case (false, .none): + try container.encode([String: [String]](), forKey: .headers) + case (true, .some(let headers)): + var multiValueHeaders: [String: [String]] = [:] + headers.forEach { name, value in + var values = multiValueHeaders[name] ?? [] + values.append(value) + multiValueHeaders[name] = values + } + try container.encode(multiValueHeaders, forKey: .multiValueHeaders) + case (false, .some(let headers)): + var singleValueHeaders: [String: String] = [:] + headers.forEach { name, value in + singleValueHeaders[name] = value + } + try container.encode(singleValueHeaders, forKey: .headers) + } + + try container.encodeIfPresent(statusDescription, forKey: .statusDescription) + try container.encodeIfPresent(body, forKey: .body) + try container.encodeIfPresent(isBase64Encoded, forKey: .isBase64Encoded) + } +} + +extension ALB.TargetGroupResponse { + public init( + statusCode: HTTPResponseStatus, + statusDescription: String? = nil, + headers: HTTPHeaders? = nil, + payload: Payload, + encoder: JSONEncoder = JSONEncoder() + ) throws { + var headers = headers ?? HTTPHeaders() + if !headers.contains(name: "Content-Type") { + headers.add(name: "Content-Type", value: "application/json") + } + + self.statusCode = statusCode + self.statusDescription = statusDescription + self.headers = headers + + let data = try encoder.encode(payload) + self.body = String(decoding: data, as: Unicode.UTF8.self) + self.isBase64Encoded = false + } + + public init( + statusCode: HTTPResponseStatus, + statusDescription: String? = nil, + headers: HTTPHeaders? = nil, + bytes: [UInt8]? + ) { + let headers = headers ?? HTTPHeaders() + + self.statusCode = statusCode + self.statusDescription = statusDescription + self.headers = headers + if let bytes = bytes { + self.body = String(base64Encoding: bytes) + } else { + self.body = "" + } + self.isBase64Encoded = true + } +} diff --git a/Sources/AWSLambdaEvents/APIGateway.swift b/Sources/AWSLambdaEvents/APIGateway.swift new file mode 100644 index 00000000..9728913e --- /dev/null +++ b/Sources/AWSLambdaEvents/APIGateway.swift @@ -0,0 +1,212 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-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 class Foundation.JSONEncoder +import NIOHTTP1 + +// https://github.com/aws/aws-lambda-go/blob/master/events/apigw.go + +public enum APIGateway { + /// APIGatewayRequest contains data coming from the API Gateway + public struct Request: DecodableBody { + public struct Context: Codable { + public struct Identity: Codable { + public let cognitoIdentityPoolId: String? + + public let apiKey: String? + public let userArn: String? + public let cognitoAuthenticationType: String? + public let caller: String? + public let userAgent: String? + public let user: String? + + public let cognitoAuthenticationProvider: String? + public let sourceIp: String? + public let accountId: String? + } + + public let resourceId: String + public let apiId: String + public let resourcePath: String + public let httpMethod: String + public let requestId: String + public let accountId: String + public let stage: String + + public let identity: Identity + public let extendedRequestId: String? + public let path: String + } + + public let resource: String + public let path: String + public let httpMethod: HTTPMethod + + public let queryStringParameters: [String: String]? + public let multiValueQueryStringParameters: [String: [String]]? + public let headers: HTTPHeaders + public let pathParameters: [String: String]? + public let stageVariables: [String: String]? + + public let requestContext: Context + public let body: String? + public let isBase64Encoded: Bool + } + + public struct Response { + public let statusCode: HTTPResponseStatus + public let headers: HTTPHeaders? + public let body: String? + public let isBase64Encoded: Bool? + + public init( + statusCode: HTTPResponseStatus, + headers: HTTPHeaders? = nil, + body: String? = nil, + isBase64Encoded: Bool? = nil + ) { + self.statusCode = statusCode + self.headers = headers + self.body = body + self.isBase64Encoded = isBase64Encoded + } + } +} + +// MARK: - Request - + +extension APIGateway.Request: Decodable { + enum CodingKeys: String, CodingKey { + case resource + case path + case httpMethod + + case queryStringParameters + case multiValueQueryStringParameters + case headers + case multiValueHeaders + case pathParameters + case stageVariables + + case requestContext + case body + case isBase64Encoded + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let method = try container.decode(String.self, forKey: .httpMethod) + self.httpMethod = HTTPMethod(rawValue: method) + self.path = try container.decode(String.self, forKey: .path) + self.resource = try container.decode(String.self, forKey: .resource) + + self.queryStringParameters = try container.decodeIfPresent( + [String: String].self, + forKey: .queryStringParameters + ) + self.multiValueQueryStringParameters = try container.decodeIfPresent( + [String: [String]].self, + forKey: .multiValueQueryStringParameters + ) + + let awsHeaders = try container.decode([String: [String]].self, forKey: .multiValueHeaders) + self.headers = HTTPHeaders(awsHeaders: awsHeaders) + + self.pathParameters = try container.decodeIfPresent([String: String].self, forKey: .pathParameters) + self.stageVariables = try container.decodeIfPresent([String: String].self, forKey: .stageVariables) + + self.requestContext = try container.decode(Context.self, forKey: .requestContext) + self.isBase64Encoded = try container.decode(Bool.self, forKey: .isBase64Encoded) + self.body = try container.decodeIfPresent(String.self, forKey: .body) + } +} + +// MARK: - Response - + +extension APIGateway.Response: Encodable { + enum CodingKeys: String, CodingKey { + case statusCode + case headers + case body + case isBase64Encoded + } + + private struct HeaderKeys: CodingKey { + var stringValue: String + + init?(stringValue: String) { + self.stringValue = stringValue + } + + var intValue: Int? { + fatalError("unexpected use") + } + + init?(intValue: Int) { + fatalError("unexpected use") + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(statusCode.code, forKey: .statusCode) + + if let headers = headers { + var headerContainer = container.nestedContainer(keyedBy: HeaderKeys.self, forKey: .headers) + try headers.forEach { name, value in + try headerContainer.encode(value, forKey: HeaderKeys(stringValue: name)!) + } + } + + try container.encodeIfPresent(body, forKey: .body) + try container.encodeIfPresent(isBase64Encoded, forKey: .isBase64Encoded) + } +} + +extension APIGateway.Response { + public init( + statusCode: HTTPResponseStatus, + headers: HTTPHeaders? = nil, + payload: Payload, + encoder: JSONEncoder = JSONEncoder() + ) throws { + var headers = headers ?? HTTPHeaders() + headers.add(name: "Content-Type", value: "application/json") + + self.statusCode = statusCode + self.headers = headers + + let data = try encoder.encode(payload) + self.body = String(decoding: data, as: Unicode.UTF8.self) + self.isBase64Encoded = false + } + + public init( + statusCode: HTTPResponseStatus, + headers: HTTPHeaders? = nil, + bytes: [UInt8]? + ) { + let headers = headers ?? HTTPHeaders() + + self.statusCode = statusCode + self.headers = headers + if let bytes = bytes { + self.body = String(base64Encoding: bytes) + } else { + self.body = "" + } + self.isBase64Encoded = true + } +} diff --git a/Sources/AWSLambdaEvents/AWSNumber.swift b/Sources/AWSLambdaEvents/AWSNumber.swift new file mode 100644 index 00000000..198a9a67 --- /dev/null +++ b/Sources/AWSLambdaEvents/AWSNumber.swift @@ -0,0 +1,43 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-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 +// +//===----------------------------------------------------------------------===// + +public struct AWSNumber: Codable, Equatable { + public let stringValue: String + + public var int: Int? { + Int(self.stringValue) + } + + public var double: Double? { + Double(self.stringValue) + } + + public init(int: Int) { + self.stringValue = String(int) + } + + public init(double: Double) { + self.stringValue = String(double) + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + self.stringValue = try container.decode(String.self) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.stringValue) + } +} diff --git a/Sources/AWSLambdaEvents/DynamoDB+AttributeValue.swift b/Sources/AWSLambdaEvents/DynamoDB+AttributeValue.swift new file mode 100644 index 00000000..57a8b5af --- /dev/null +++ b/Sources/AWSLambdaEvents/DynamoDB+AttributeValue.swift @@ -0,0 +1,128 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-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 +// +//===----------------------------------------------------------------------===// + +extension DynamoDB { + public enum AttributeValue { + case boolean(Bool) + case binary([UInt8]) + case binarySet([[UInt8]]) + case string(String) + case stringSet([String]) + case null + case number(AWSNumber) + case numberSet([AWSNumber]) + + case list([AttributeValue]) + case map([String: AttributeValue]) + } +} + +extension DynamoDB.AttributeValue: Decodable { + enum CodingKeys: String, CodingKey { + case binary = "B" + case bool = "BOOL" + case binarySet = "BS" + case list = "L" + case map = "M" + case number = "N" + case numberSet = "NS" + case null = "NULL" + case string = "S" + case stringSet = "SS" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + guard container.allKeys.count == 1, let key = container.allKeys.first else { + let context = DecodingError.Context( + codingPath: container.codingPath, + debugDescription: "Expected exactly one key, but got \(container.allKeys.count)" + ) + throw DecodingError.dataCorrupted(context) + } + + switch key { + case .binary: + let encoded = try container.decode(String.self, forKey: .binary) + let bytes = try encoded.base64decoded() + self = .binary(bytes) + + case .bool: + let value = try container.decode(Bool.self, forKey: .bool) + self = .boolean(value) + + case .binarySet: + let values = try container.decode([String].self, forKey: .binarySet) + let bytesArray = try values.map { try $0.base64decoded() } + self = .binarySet(bytesArray) + + case .list: + let values = try container.decode([DynamoDB.AttributeValue].self, forKey: .list) + self = .list(values) + + case .map: + let value = try container.decode([String: DynamoDB.AttributeValue].self, forKey: .map) + self = .map(value) + + case .number: + let value = try container.decode(AWSNumber.self, forKey: .number) + self = .number(value) + + case .numberSet: + let values = try container.decode([AWSNumber].self, forKey: .numberSet) + self = .numberSet(values) + + case .null: + self = .null + + case .string: + let value = try container.decode(String.self, forKey: .string) + self = .string(value) + + case .stringSet: + let values = try container.decode([String].self, forKey: .stringSet) + self = .stringSet(values) + } + } +} + +extension DynamoDB.AttributeValue: Equatable { + public static func == (lhs: DynamoDB.AttributeValue, rhs: DynamoDB.AttributeValue) -> Bool { + switch (lhs, rhs) { + case (.boolean(let lhs), .boolean(let rhs)): + return lhs == rhs + case (.binary(let lhs), .binary(let rhs)): + return lhs == rhs + case (.binarySet(let lhs), .binarySet(let rhs)): + return lhs == rhs + case (.string(let lhs), .string(let rhs)): + return lhs == rhs + case (.stringSet(let lhs), .stringSet(let rhs)): + return lhs == rhs + case (.null, .null): + return true + case (.number(let lhs), .number(let rhs)): + return lhs == rhs + case (.numberSet(let lhs), .numberSet(let rhs)): + return lhs == rhs + case (.list(let lhs), .list(let rhs)): + return lhs == rhs + case (.map(let lhs), .map(let rhs)): + return lhs == rhs + default: + return false + } + } +} diff --git a/Sources/AWSLambdaEvents/DynamoDB.swift b/Sources/AWSLambdaEvents/DynamoDB.swift new file mode 100644 index 00000000..886f5868 --- /dev/null +++ b/Sources/AWSLambdaEvents/DynamoDB.swift @@ -0,0 +1,188 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-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 struct Foundation.Date +import struct Foundation.TimeInterval + +/// https://github.com/aws/aws-lambda-go/blob/master/events/dynamodb.go +public enum DynamoDB { + public struct Event: Decodable { + public let records: [EventRecord] + + public enum CodingKeys: String, CodingKey { + case records = "Records" + } + } + + public enum KeyType: String, Codable { + case hash = "HASH" + case range = "RANGE" + } + + public enum OperationType: String, Codable { + case insert = "INSERT" + case modify = "MODIFY" + case remove = "REMOVE" + } + + public enum SharedIteratorType: String, Codable { + case trimHorizon = "TRIM_HORIZON" + case latest = "LATEST" + case atSequenceNumber = "AT_SEQUENCE_NUMBER" + case afterSequenceNumber = "AFTER_SEQUENCE_NUMBER" + } + + public enum StreamStatus: String, Codable { + case enabling = "ENABLING" + case enabled = "ENABLED" + case disabling = "DISABLING" + case disabled = "DISABLED" + } + + public enum StreamViewType: String, Codable { + /// the entire item, as it appeared after it was modified. + case newImage = "NEW_IMAGE" + /// the entire item, as it appeared before it was modified. + case oldImage = "OLD_IMAGE" + /// both the new and the old item images of the item. + case newAndOldImages = "NEW_AND_OLD_IMAGES" + /// only the key attributes of the modified item. + case keysOnly = "KEYS_ONLY" + } + + public struct EventRecord: Decodable { + /// The region in which the GetRecords request was received. + public let awsRegion: String + + /// The main body of the stream record, containing all of the DynamoDB-specific + /// fields. + public let change: StreamRecord + + /// A globally unique identifier for the event that was recorded in this stream + /// record. + public let eventId: String + + /// The type of data modification that was performed on the DynamoDB table: + /// * INSERT - a new item was added to the table. + /// * MODIFY - one or more of an existing item's attributes were modified. + /// * REMOVE - the item was deleted from the table + public let eventName: OperationType + + /// The AWS service from which the stream record originated. For DynamoDB Streams, + /// this is aws:dynamodb. + public let eventSource: String + + /// The version number of the stream record format. This number is updated whenever + /// the structure of Record is modified. + /// + /// Client applications must not assume that eventVersion will remain at a particular + /// value, as this number is subject to change at any time. In general, eventVersion + /// will only increase as the low-level DynamoDB Streams API evolves. + public let eventVersion: String + + /// The event source ARN of DynamoDB + public let eventSourceArn: String + + /// Items that are deleted by the Time to Live process after expiration have + /// the following fields: + /// * Records[].userIdentity.type + /// + /// "Service" + /// * Records[].userIdentity.principalId + /// + /// "dynamodb.amazonaws.com" + public let userIdentity: UserIdentity? + + public enum CodingKeys: String, CodingKey { + case awsRegion + case change = "dynamodb" + case eventId = "eventID" + case eventName + case eventSource + case eventVersion + case eventSourceArn = "eventSourceARN" + case userIdentity + } + } + + public struct StreamRecord { + /// The approximate date and time when the stream record was created, in UNIX + /// epoch time (http://www.epochconverter.com/) format. + public let approximateCreationDateTime: Date? + + /// The primary key attribute(s) for the DynamoDB item that was modified. + public let keys: [String: AttributeValue] + + /// The item in the DynamoDB table as it appeared after it was modified. + public let newImage: [String: AttributeValue]? + + /// The item in the DynamoDB table as it appeared before it was modified. + public let oldImage: [String: AttributeValue]? + + /// The sequence number of the stream record. + public let sequenceNumber: String + + /// The size of the stream record, in bytes. + public let sizeBytes: Int64 + + /// The type of data from the modified DynamoDB item that was captured in this + /// stream record. + public let streamViewType: StreamViewType + } + + public struct UserIdentity: Codable { + public let type: String + public let principalId: String + } +} + +extension DynamoDB.StreamRecord: Decodable { + enum CodingKeys: String, CodingKey { + case approximateCreationDateTime = "ApproximateCreationDateTime" + case keys = "Keys" + case newImage = "NewImage" + case oldImage = "OldImage" + case sequenceNumber = "SequenceNumber" + case sizeBytes = "SizeBytes" + case streamViewType = "StreamViewType" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.keys = try container.decode( + [String: DynamoDB.AttributeValue].self, + forKey: .keys + ) + + self.newImage = try container.decodeIfPresent( + [String: DynamoDB.AttributeValue].self, + forKey: .newImage + ) + self.oldImage = try container.decodeIfPresent( + [String: DynamoDB.AttributeValue].self, + forKey: .oldImage + ) + + self.sequenceNumber = try container.decode(String.self, forKey: .sequenceNumber) + self.sizeBytes = try container.decode(Int64.self, forKey: .sizeBytes) + self.streamViewType = try container.decode(DynamoDB.StreamViewType.self, forKey: .streamViewType) + + if let timestamp = try container.decodeIfPresent(TimeInterval.self, forKey: .approximateCreationDateTime) { + self.approximateCreationDateTime = Date(timeIntervalSince1970: timestamp) + } else { + self.approximateCreationDateTime = nil + } + } +} diff --git a/Sources/AWSLambdaEvents/S3.swift b/Sources/AWSLambdaEvents/S3.swift deleted file mode 100644 index bc9c4f6d..00000000 --- a/Sources/AWSLambdaEvents/S3.swift +++ /dev/null @@ -1,77 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-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 struct Foundation.Date - -// https://github.com/aws/aws-lambda-go/blob/master/events/s3.go -public enum S3 { - public struct Event: Decodable { - public struct Record: Decodable { - public let eventVersion: String - public let eventSource: String - public let awsRegion: String - - @ISO8601WithFractionalSecondsCoding - public var eventTime: Date - public let eventName: String - public let userIdentity: UserIdentity - public let requestParameters: RequestParameters - public let responseElements: [String: String] - public let s3: Entity - } - - public let records: [Record] - - public enum CodingKeys: String, CodingKey { - case records = "Records" - } - } - - public struct RequestParameters: Codable, Equatable { - public let sourceIPAddress: String - } - - public struct UserIdentity: Codable, Equatable { - public let principalId: String - } - - public struct Entity: Codable { - public let configurationId: String - public let schemaVersion: String - public let bucket: Bucket - public let object: Object - - enum CodingKeys: String, CodingKey { - case configurationId - case schemaVersion = "s3SchemaVersion" - case bucket - case object - } - } - - public struct Bucket: Codable { - public let name: String - public let ownerIdentity: UserIdentity - public let arn: String - } - - public struct Object: Codable { - public let key: String - public let size: UInt64 - public let urlDecodedKey: String? - public let versionId: String? - public let eTag: String - public let sequencer: String - } -} diff --git a/Sources/AWSLambdaEvents/Utils/Base64.swift b/Sources/AWSLambdaEvents/Utils/Base64.swift new file mode 100644 index 00000000..65950818 --- /dev/null +++ b/Sources/AWSLambdaEvents/Utils/Base64.swift @@ -0,0 +1,354 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-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 +// +//===----------------------------------------------------------------------===// + +//===----------------------------------------------------------------------===// +// This is a vendored version from: +// https://github.com/fabianfett/swift-base64-kit + +struct Base64 {} + +// MARK: Encoding + +extension Base64 { + struct EncodingOptions: OptionSet { + let rawValue: UInt + init(rawValue: UInt) { self.rawValue = rawValue } + + static let base64UrlAlphabet = EncodingOptions(rawValue: UInt(1 << 0)) + } + + /// Base64 encode a collection of UInt8 to a string, without the use of Foundation. + /// + /// This function performs the world's most naive Base64 encoding: no attempts to use a larger + /// lookup table or anything intelligent like that, just shifts and masks. This works fine, for + /// now: the purpose of this encoding is to avoid round-tripping through Data, and the perf gain + /// from avoiding that is more than enough to outweigh the silliness of this code. + @inline(__always) + static func encode(bytes: Buffer, options: EncodingOptions = []) + -> String where Buffer.Element == UInt8 { + // In Base64, 3 bytes become 4 output characters, and we pad to the + // nearest multiple of four. + let newCapacity = ((bytes.count + 2) / 3) * 4 + let alphabet = options.contains(.base64UrlAlphabet) + ? Base64.encodeBase64Url + : Base64.encodeBase64 + + var outputBytes = [UInt8]() + outputBytes.reserveCapacity(newCapacity) + + var input = bytes.makeIterator() + + while let firstByte = input.next() { + let secondByte = input.next() + let thirdByte = input.next() + + let firstChar = Base64.encode(alphabet: alphabet, firstByte: firstByte) + let secondChar = Base64.encode(alphabet: alphabet, firstByte: firstByte, secondByte: secondByte) + let thirdChar = Base64.encode(alphabet: alphabet, secondByte: secondByte, thirdByte: thirdByte) + let forthChar = Base64.encode(alphabet: alphabet, thirdByte: thirdByte) + + outputBytes.append(firstChar) + outputBytes.append(secondChar) + outputBytes.append(thirdChar) + outputBytes.append(forthChar) + } + + return String(decoding: outputBytes, as: Unicode.UTF8.self) + } + + // MARK: Internal + + // The base64 unicode table. + @usableFromInline + static let encodeBase64: [UInt8] = [ + UInt8(ascii: "A"), UInt8(ascii: "B"), UInt8(ascii: "C"), UInt8(ascii: "D"), + UInt8(ascii: "E"), UInt8(ascii: "F"), UInt8(ascii: "G"), UInt8(ascii: "H"), + UInt8(ascii: "I"), UInt8(ascii: "J"), UInt8(ascii: "K"), UInt8(ascii: "L"), + UInt8(ascii: "M"), UInt8(ascii: "N"), UInt8(ascii: "O"), UInt8(ascii: "P"), + UInt8(ascii: "Q"), UInt8(ascii: "R"), UInt8(ascii: "S"), UInt8(ascii: "T"), + UInt8(ascii: "U"), UInt8(ascii: "V"), UInt8(ascii: "W"), UInt8(ascii: "X"), + UInt8(ascii: "Y"), UInt8(ascii: "Z"), UInt8(ascii: "a"), UInt8(ascii: "b"), + UInt8(ascii: "c"), UInt8(ascii: "d"), UInt8(ascii: "e"), UInt8(ascii: "f"), + UInt8(ascii: "g"), UInt8(ascii: "h"), UInt8(ascii: "i"), UInt8(ascii: "j"), + UInt8(ascii: "k"), UInt8(ascii: "l"), UInt8(ascii: "m"), UInt8(ascii: "n"), + UInt8(ascii: "o"), UInt8(ascii: "p"), UInt8(ascii: "q"), UInt8(ascii: "r"), + UInt8(ascii: "s"), UInt8(ascii: "t"), UInt8(ascii: "u"), UInt8(ascii: "v"), + UInt8(ascii: "w"), UInt8(ascii: "x"), UInt8(ascii: "y"), UInt8(ascii: "z"), + UInt8(ascii: "0"), UInt8(ascii: "1"), UInt8(ascii: "2"), UInt8(ascii: "3"), + UInt8(ascii: "4"), UInt8(ascii: "5"), UInt8(ascii: "6"), UInt8(ascii: "7"), + UInt8(ascii: "8"), UInt8(ascii: "9"), UInt8(ascii: "+"), UInt8(ascii: "/"), + ] + + @usableFromInline + static let encodeBase64Url: [UInt8] = [ + UInt8(ascii: "A"), UInt8(ascii: "B"), UInt8(ascii: "C"), UInt8(ascii: "D"), + UInt8(ascii: "E"), UInt8(ascii: "F"), UInt8(ascii: "G"), UInt8(ascii: "H"), + UInt8(ascii: "I"), UInt8(ascii: "J"), UInt8(ascii: "K"), UInt8(ascii: "L"), + UInt8(ascii: "M"), UInt8(ascii: "N"), UInt8(ascii: "O"), UInt8(ascii: "P"), + UInt8(ascii: "Q"), UInt8(ascii: "R"), UInt8(ascii: "S"), UInt8(ascii: "T"), + UInt8(ascii: "U"), UInt8(ascii: "V"), UInt8(ascii: "W"), UInt8(ascii: "X"), + UInt8(ascii: "Y"), UInt8(ascii: "Z"), UInt8(ascii: "a"), UInt8(ascii: "b"), + UInt8(ascii: "c"), UInt8(ascii: "d"), UInt8(ascii: "e"), UInt8(ascii: "f"), + UInt8(ascii: "g"), UInt8(ascii: "h"), UInt8(ascii: "i"), UInt8(ascii: "j"), + UInt8(ascii: "k"), UInt8(ascii: "l"), UInt8(ascii: "m"), UInt8(ascii: "n"), + UInt8(ascii: "o"), UInt8(ascii: "p"), UInt8(ascii: "q"), UInt8(ascii: "r"), + UInt8(ascii: "s"), UInt8(ascii: "t"), UInt8(ascii: "u"), UInt8(ascii: "v"), + UInt8(ascii: "w"), UInt8(ascii: "x"), UInt8(ascii: "y"), UInt8(ascii: "z"), + UInt8(ascii: "0"), UInt8(ascii: "1"), UInt8(ascii: "2"), UInt8(ascii: "3"), + UInt8(ascii: "4"), UInt8(ascii: "5"), UInt8(ascii: "6"), UInt8(ascii: "7"), + UInt8(ascii: "8"), UInt8(ascii: "9"), UInt8(ascii: "-"), UInt8(ascii: "_"), + ] + + static let encodePaddingCharacter: UInt8 = 61 + + static func encode(alphabet: [UInt8], firstByte: UInt8) -> UInt8 { + let index = firstByte >> 2 + return alphabet[Int(index)] + } + + static func encode(alphabet: [UInt8], firstByte: UInt8, secondByte: UInt8?) -> UInt8 { + var index = (firstByte & 0b0000_0011) << 4 + if let secondByte = secondByte { + index += (secondByte & 0b1111_0000) >> 4 + } + return alphabet[Int(index)] + } + + static func encode(alphabet: [UInt8], secondByte: UInt8?, thirdByte: UInt8?) -> UInt8 { + guard let secondByte = secondByte else { + // No second byte means we are just emitting padding. + return Base64.encodePaddingCharacter + } + var index = (secondByte & 0b0000_1111) << 2 + if let thirdByte = thirdByte { + index += (thirdByte & 0b1100_0000) >> 6 + } + return alphabet[Int(index)] + } + + @inlinable + static func encode(alphabet: [UInt8], thirdByte: UInt8?) -> UInt8 { + guard let thirdByte = thirdByte else { + // No third byte means just padding. + return Base64.encodePaddingCharacter + } + let index = thirdByte & 0b0011_1111 + return alphabet[Int(index)] + } +} + +// MARK: - Decode - + +extension Base64 { + struct DecodingOptions: OptionSet { + let rawValue: UInt + init(rawValue: UInt) { self.rawValue = rawValue } + + static let base64UrlAlphabet = DecodingOptions(rawValue: UInt(1 << 0)) + } + + enum DecodingError: Error { + case invalidLength + case invalidCharacter(UInt8) + case unexpectedPaddingCharacter + case unexpectedEnd + } + + @inlinable + static func decode(encoded: Buffer, options: DecodingOptions = []) + throws -> [UInt8] where Buffer.Element == UInt8 { + let alphabet = options.contains(.base64UrlAlphabet) + ? Base64.decodeBase64Url + : Base64.decodeBase64 + + // In Base64 4 encoded bytes, become 3 decoded bytes. We pad to the + // nearest multiple of three. + let inputLength = encoded.count + guard inputLength > 0 else { return [] } + guard inputLength % 4 == 0 else { + throw DecodingError.invalidLength + } + + let inputBlocks = (inputLength + 3) / 4 + let fullQualified = inputBlocks - 1 + let outputLength = ((encoded.count + 3) / 4) * 3 + var iterator = encoded.makeIterator() + var outputBytes = [UInt8]() + outputBytes.reserveCapacity(outputLength) + + // fast loop. we don't expect any padding in here. + for _ in 0 ..< fullQualified { + let firstValue: UInt8 = try iterator.nextValue(alphabet: alphabet) + let secondValue: UInt8 = try iterator.nextValue(alphabet: alphabet) + let thirdValue: UInt8 = try iterator.nextValue(alphabet: alphabet) + let forthValue: UInt8 = try iterator.nextValue(alphabet: alphabet) + + outputBytes.append((firstValue << 2) | (secondValue >> 4)) + outputBytes.append((secondValue << 4) | (thirdValue >> 2)) + outputBytes.append((thirdValue << 6) | forthValue) + } + + // last 4 bytes. we expect padding characters in three and four + let firstValue: UInt8 = try iterator.nextValue(alphabet: alphabet) + let secondValue: UInt8 = try iterator.nextValue(alphabet: alphabet) + let thirdValue: UInt8? = try iterator.nextValueOrEmpty(alphabet: alphabet) + let forthValue: UInt8? = try iterator.nextValueOrEmpty(alphabet: alphabet) + + outputBytes.append((firstValue << 2) | (secondValue >> 4)) + if let thirdValue = thirdValue { + outputBytes.append((secondValue << 4) | (thirdValue >> 2)) + + if let forthValue = forthValue { + outputBytes.append((thirdValue << 6) | forthValue) + } + } + + return outputBytes + } + + @inlinable + static func decode(encoded: String, options: DecodingOptions = []) throws -> [UInt8] { + // A string can be backed by a contiguous storage (pure swift string) + // or a nsstring (bridged string from objc). We only get a pointer + // to the contiguous storage, if the input string is a swift string. + // Therefore to transform the nsstring backed input into a swift + // string we concat the input with nothing, causing a copy on write + // into a swift string. + let decoded = try encoded.utf8.withContiguousStorageIfAvailable { pointer in + try self.decode(encoded: pointer, options: options) + } + + if decoded != nil { + return decoded! + } + + return try self.decode(encoded: encoded + "", options: options) + } + + // MARK: Internal + + @usableFromInline + static let decodeBase64: [UInt8] = [ + // 0 1 2 3 4 5 6 7 8 9 + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 0 + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 1 + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 2 + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 3 + 255, 255, 255, 62, 255, 255, 255, 63, 52, 53, // 4 + 54, 55, 56, 57, 58, 59, 60, 61, 255, 255, // 5 + 255, 254, 255, 255, 255, 0, 1, 2, 3, 4, // 6 + 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // 7 + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, // 8 + 25, 255, 255, 255, 255, 255, 255, 26, 27, 28, // 9 + 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // 10 + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, // 11 + 49, 50, 51, 255, 255, 255, 255, 255, 255, 255, // 12 + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 13 + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 14 + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 15 + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 16 + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 17 + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 18 + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 19 + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 20 + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 21 + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 22 + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 23 + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 24 + 255, 255, 255, 255, 255, // 25 + ] + + @usableFromInline + static let decodeBase64Url: [UInt8] = [ + // 0 1 2 3 4 5 6 7 8 9 + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 0 + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 1 + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 2 + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 3 + 255, 255, 255, 255, 255, 62, 255, 255, 52, 53, // 4 + 54, 55, 56, 57, 58, 59, 60, 61, 255, 255, // 5 + 255, 254, 255, 255, 255, 0, 1, 2, 3, 4, // 6 + 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // 7 + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, // 8 + 25, 255, 255, 255, 255, 63, 255, 26, 27, 28, // 9 + 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // 10 + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, // 11 + 49, 50, 51, 255, 255, 255, 255, 255, 255, 255, // 12 + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 13 + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 14 + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 15 + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 16 + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 17 + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 18 + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 19 + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 20 + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 21 + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 22 + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 23 + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 24 + 255, 255, 255, 255, 255, // 25 + ] + + @usableFromInline + static let paddingCharacter: UInt8 = 254 +} + +extension IteratorProtocol where Self.Element == UInt8 { + mutating func nextValue(alphabet: [UInt8]) throws -> UInt8 { + let ascii = self.next()! + + let value = alphabet[Int(ascii)] + + if value < 64 { + return value + } + + if value == Base64.paddingCharacter { + throw Base64.DecodingError.unexpectedPaddingCharacter + } + + throw Base64.DecodingError.invalidCharacter(ascii) + } + + mutating func nextValueOrEmpty(alphabet: [UInt8]) throws -> UInt8? { + let ascii = self.next()! + + let value = alphabet[Int(ascii)] + + if value < 64 { + return value + } + + if value == Base64.paddingCharacter { + return nil + } + + throw Base64.DecodingError.invalidCharacter(ascii) + } +} + +// MARK: - Extensions - + +extension String { + init(base64Encoding bytes: Buffer, options: Base64.EncodingOptions = []) + where Buffer.Element == UInt8 { + self = Base64.encode(bytes: bytes, options: options) + } + + func base64decoded(options: Base64.DecodingOptions = []) throws -> [UInt8] { + // In Base64, 3 bytes become 4 output characters, and we pad to the nearest multiple + // of four. + try Base64.decode(encoded: self, options: options) + } +} diff --git a/Sources/AWSLambdaEvents/Utils/DecodableBody.swift b/Sources/AWSLambdaEvents/Utils/DecodableBody.swift new file mode 100644 index 00000000..219d94cd --- /dev/null +++ b/Sources/AWSLambdaEvents/Utils/DecodableBody.swift @@ -0,0 +1,47 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-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 struct Foundation.Data +import class Foundation.JSONDecoder + +public protocol DecodableBody { + var body: String? { get } + var isBase64Encoded: Bool { get } +} + +public extension DecodableBody { + var isBase64Encoded: Bool { + false + } +} + +public extension DecodableBody { + func decodeBody(_ type: T.Type, decoder: JSONDecoder = JSONDecoder()) throws -> T { + // I would really like to not use Foundation.Data at all, but well + // the NIOFoundationCompat just creates an internal Data as well. + // So let's save one malloc and copy and just use Data. + let payload = self.body ?? "" + + let data: Data + if self.isBase64Encoded { + let bytes = try payload.base64decoded() + data = Data(bytes) + } else { + // TBD: Can this ever fail? I wouldn't think so... + data = payload.data(using: .utf8)! + } + + return try decoder.decode(T.self, from: data) + } +} diff --git a/Sources/AWSLambdaEvents/Utils/HTTPHeaders+Codable.swift b/Sources/AWSLambdaEvents/Utils/HTTPHeaders+Codable.swift new file mode 100644 index 00000000..3cb289c0 --- /dev/null +++ b/Sources/AWSLambdaEvents/Utils/HTTPHeaders+Codable.swift @@ -0,0 +1,28 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-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 NIOHTTP1 + +extension HTTPHeaders { + init(awsHeaders: [String: [String]]) { + var nioHeaders: [(String, String)] = [] + awsHeaders.forEach { key, values in + values.forEach { value in + nioHeaders.append((key, value)) + } + } + + self = HTTPHeaders(nioHeaders) + } +} diff --git a/Tests/AWSLambdaEventsTests/ALBTests.swift b/Tests/AWSLambdaEventsTests/ALBTests.swift new file mode 100644 index 00000000..08cfae99 --- /dev/null +++ b/Tests/AWSLambdaEventsTests/ALBTests.swift @@ -0,0 +1,148 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-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 import AWSLambdaEvents +import XCTest + +class ALBTests: XCTestCase { + static let exampleSingleValueHeadersPayload = """ + { + "requestContext":{ + "elb":{ + "targetGroupArn": "arn:aws:elasticloadbalancing:eu-central-1:079477498937:targetgroup/EinSternDerDeinenNamenTraegt/621febf5a44b2ce5" + } + }, + "httpMethod": "GET", + "path": "/", + "queryStringParameters": {}, + "headers":{ + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "accept-encoding": "gzip, deflate", + "accept-language": "en-us", + "connection": "keep-alive", + "host": "event-testl-1wa3wrvmroilb-358275751.eu-central-1.elb.amazonaws.com", + "upgrade-insecure-requests": "1", + "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.2 Safari/605.1.15", + "x-amzn-trace-id": "Root=1-5e189143-ad18a2b0a7728cd0dac45e10", + "x-forwarded-for": "90.187.8.137", + "x-forwarded-port": "80", + "x-forwarded-proto": "http" + }, + "body":"", + "isBase64Encoded":false + } + """ + + func testRequestWithSingleValueHeadersPayload() { + let data = ALBTests.exampleSingleValueHeadersPayload.data(using: .utf8)! + do { + let decoder = JSONDecoder() + + let event = try decoder.decode(ALB.TargetGroupRequest.self, from: data) + + XCTAssertEqual(event.httpMethod, .GET) + XCTAssertEqual(event.body, nil) + XCTAssertEqual(event.isBase64Encoded, false) + XCTAssertEqual(event.headers.count, 11) + XCTAssertEqual(event.path, "/") + XCTAssertEqual(event.queryStringParameters, [:]) + } catch { + XCTFail("Unexpected error: \(error)") + } + } + + // MARK: - Response - + + private struct TestStruct: Codable { + let hello: String + } + + private struct SingleValueHeadersResponse: Codable, Equatable { + let statusCode: Int + let body: String + let isBase64Encoded: Bool + let headers: [String: String] + } + + private struct MultiValueHeadersResponse: Codable, Equatable { + let statusCode: Int + let body: String + let isBase64Encoded: Bool + let multiValueHeaders: [String: [String]] + } + + func testJSONResponseWithSingleValueHeaders() throws { + let response = try ALB.TargetGroupResponse(statusCode: .ok, payload: TestStruct(hello: "world")) + let encoder = JSONEncoder() + encoder.userInfo[ALB.TargetGroupResponse.MultiValueHeadersEnabledKey] = false + let data = try encoder.encode(response) + + let expected = SingleValueHeadersResponse( + statusCode: 200, body: "{\"hello\":\"world\"}", + isBase64Encoded: false, + headers: ["Content-Type": "application/json"] + ) + + let result = try JSONDecoder().decode(SingleValueHeadersResponse.self, from: data) + XCTAssertEqual(result, expected) + } + + func testJSONResponseWithMultiValueHeaders() throws { + let response = try ALB.TargetGroupResponse(statusCode: .ok, payload: TestStruct(hello: "world")) + let encoder = JSONEncoder() + encoder.userInfo[ALB.TargetGroupResponse.MultiValueHeadersEnabledKey] = true + let data = try encoder.encode(response) + + let expected = MultiValueHeadersResponse( + statusCode: 200, body: "{\"hello\":\"world\"}", + isBase64Encoded: false, + multiValueHeaders: ["Content-Type": ["application/json"]] + ) + + let result = try JSONDecoder().decode(MultiValueHeadersResponse.self, from: data) + XCTAssertEqual(result, expected) + } + + func testEmptyResponseWithMultiValueHeaders() throws { + let response = ALB.TargetGroupResponse(statusCode: .ok) + let encoder = JSONEncoder() + encoder.userInfo[ALB.TargetGroupResponse.MultiValueHeadersEnabledKey] = true + let data = try encoder.encode(response) + + let expected = MultiValueHeadersResponse( + statusCode: 200, body: "", + isBase64Encoded: false, + multiValueHeaders: [:] + ) + + let result = try JSONDecoder().decode(MultiValueHeadersResponse.self, from: data) + XCTAssertEqual(result, expected) + } + + func testEmptyResponseWithSingleValueHeaders() throws { + let response = ALB.TargetGroupResponse(statusCode: .ok) + let encoder = JSONEncoder() + encoder.userInfo[ALB.TargetGroupResponse.MultiValueHeadersEnabledKey] = false + let data = try encoder.encode(response) + + let expected = SingleValueHeadersResponse( + statusCode: 200, body: "", + isBase64Encoded: false, + headers: [:] + ) + + let result = try JSONDecoder().decode(SingleValueHeadersResponse.self, from: data) + XCTAssertEqual(result, expected) + } +} diff --git a/Tests/AWSLambdaEventsTests/APIGatewayTests.swift b/Tests/AWSLambdaEventsTests/APIGatewayTests.swift new file mode 100644 index 00000000..7fc709fb --- /dev/null +++ b/Tests/AWSLambdaEventsTests/APIGatewayTests.swift @@ -0,0 +1,94 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-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 import AWSLambdaEvents +import NIO +import NIOFoundationCompat +import NIOHTTP1 +import XCTest + +class APIGatewayTests: XCTestCase { + static let exampleGetPayload = """ + {"httpMethod": "GET", "body": null, "resource": "/test", "requestContext": {"resourceId": "123456", "apiId": "1234567890", "resourcePath": "/test", "httpMethod": "GET", "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": "/test"}, "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": "/test", "isBase64Encoded": false} + """ + + static let todoPostPayload = """ + {"httpMethod": "POST", "body": "{\\"title\\":\\"a todo\\"}", "resource": "/todos", "requestContext": {"resourceId": "123456", "apiId": "1234567890", "resourcePath": "/todos", "httpMethod": "POST", "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", "accountId": "123456789012", "stage": "test", "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": "/todos"}, "queryStringParameters": null, "multiValueQueryStringParameters": null, "headers": {"Host": "127.0.0.1:3000", "Connection": "keep-alive", "Content-Length": "18", "Pragma": "no-cache", "Cache-Control": "no-cache", "Accept": "text/plain, */*; q=0.01", "Origin": "http://todobackend.com", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.36 Safari/537.36 Edg/79.0.309.25", "Dnt": "1", "Content-Type": "application/json", "Sec-Fetch-Site": "cross-site", "Sec-Fetch-Mode": "cors", "Referer": "http://todobackend.com/specs/index.html?http://127.0.0.1:3000/todos", "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"], "Content-Length": ["18"], "Pragma": ["no-cache"], "Cache-Control": ["no-cache"], "Accept": ["text/plain, */*; q=0.01"], "Origin": ["http://todobackend.com"], "User-Agent": ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.36 Safari/537.36 Edg/79.0.309.25"], "Dnt": ["1"], "Content-Type": ["application/json"], "Sec-Fetch-Site": ["cross-site"], "Sec-Fetch-Mode": ["cors"], "Referer": ["http://todobackend.com/specs/index.html?http://127.0.0.1:3000/todos"], "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": "/todos", "isBase64Encoded": false} + """ + + // MARK: - Request - + + // MARK: Decoding + + func testRequestDecodingExampleGetRequest() { + do { + let data = APIGatewayTests.exampleGetPayload.data(using: .utf8)! + let request = try JSONDecoder().decode(APIGateway.Request.self, from: data) + + XCTAssertEqual(request.path, "/test") + XCTAssertEqual(request.httpMethod, .GET) + } catch { + XCTFail("Unexpected error: \(error)") + } + } + + func testRequestDecodingTodoPostRequest() { + struct Todo: Decodable { + let title: String + } + + do { + let data = APIGatewayTests.todoPostPayload.data(using: .utf8)! + let request = try JSONDecoder().decode(APIGateway.Request.self, from: data) + + XCTAssertEqual(request.path, "/todos") + XCTAssertEqual(request.httpMethod, .POST) + + let todo = try request.decodeBody(Todo.self) + XCTAssertEqual(todo.title, "a todo") + } catch { + XCTFail("Unexpected error: \(error)") + } + } + + // MARK: - Response - + + // MARK: Encoding + + struct JSONResponse: Codable { + let statusCode: UInt + let headers: [String: String]? + let body: String? + let isBase64Encoded: Bool? + } + + func testResponseEncoding() { + let resp = APIGateway.Response( + statusCode: .ok, + headers: HTTPHeaders([("Server", "Test")]), + body: "abc123" + ) + + do { + let data = try JSONEncoder().encodeAsByteBuffer(resp, allocator: ByteBufferAllocator()) + let json = try JSONDecoder().decode(JSONResponse.self, from: data) + + XCTAssertEqual(json.statusCode, resp.statusCode.code) + XCTAssertEqual(json.body, resp.body) + XCTAssertEqual(json.isBase64Encoded, resp.isBase64Encoded) + } catch { + XCTFail("unexpected error: \(error)") + } + } +} diff --git a/Tests/AWSLambdaEventsTests/DecodableBodyTests.swift b/Tests/AWSLambdaEventsTests/DecodableBodyTests.swift new file mode 100644 index 00000000..e81a696d --- /dev/null +++ b/Tests/AWSLambdaEventsTests/DecodableBodyTests.swift @@ -0,0 +1,62 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-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 import AWSLambdaEvents +import XCTest + +class DecodableBodyTests: XCTestCase { + struct TestEvent: DecodableBody { + let body: String? + let isBase64Encoded: Bool + } + + struct TestPayload: Codable { + let hello: String + } + + func testSimplePayloadFromEvent() { + do { + let event = TestEvent(body: "{\"hello\":\"world\"}", isBase64Encoded: false) + let payload = try event.decodeBody(TestPayload.self) + + XCTAssertEqual(payload.hello, "world") + } catch { + XCTFail("Unexpected error: \(error)") + } + } + + func testBase64PayloadFromEvent() { + do { + let event = TestEvent(body: "eyJoZWxsbyI6IndvcmxkIn0=", isBase64Encoded: true) + let payload = try event.decodeBody(TestPayload.self) + + XCTAssertEqual(payload.hello, "world") + } catch { + XCTFail("Unexpected error: \(error)") + } + } + + func testNoDataFromEvent() { + do { + let event = TestEvent(body: "", isBase64Encoded: false) + _ = try event.decodeBody(TestPayload.self) + + XCTFail("Did not expect to reach this point") + } catch DecodingError.dataCorrupted(_) { + return // expected error + } catch { + XCTFail("Unexpected error: \(error)") + } + } +} diff --git a/Tests/AWSLambdaEventsTests/DynamoDB+AttributeValueTests.swift b/Tests/AWSLambdaEventsTests/DynamoDB+AttributeValueTests.swift new file mode 100644 index 00000000..61f89953 --- /dev/null +++ b/Tests/AWSLambdaEventsTests/DynamoDB+AttributeValueTests.swift @@ -0,0 +1,114 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-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 import AWSLambdaEvents +import NIO +import XCTest + +class DynamoDBAttributeValueTests: XCTestCase { + func testBoolDecoding() throws { + let json = "{\"BOOL\": true}" + let value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!) + + XCTAssertEqual(value, .boolean(true)) + } + + func testBinaryDecoding() throws { + let json = "{\"B\": \"YmFzZTY0\"}" + let value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!) + + XCTAssertEqual(value, .binary([UInt8]("base64".utf8))) + } + + func testBinarySetDecoding() throws { + let json = "{\"BS\": [\"YmFzZTY0\", \"YWJjMTIz\"]}" + let value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!) + + XCTAssertEqual(value, .binarySet([[UInt8]("base64".utf8), [UInt8]("abc123".utf8)])) + } + + func testStringDecoding() throws { + let json = "{\"S\": \"huhu\"}" + let value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!) + + XCTAssertEqual(value, .string("huhu")) + } + + func testStringSetDecoding() throws { + let json = "{\"SS\": [\"huhu\", \"haha\"]}" + let value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!) + + XCTAssertEqual(value, .stringSet(["huhu", "haha"])) + } + + func testNullDecoding() throws { + let json = "{\"NULL\": true}" + let value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!) + + XCTAssertEqual(value, .null) + } + + func testNumberDecoding() throws { + let json = "{\"N\": \"1.2345\"}" + let value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!) + + XCTAssertEqual(value, .number(AWSNumber(double: 1.2345))) + } + + func testNumberSetDecoding() throws { + let json = "{\"NS\": [\"1.2345\", \"-19\"]}" + let value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!) + + XCTAssertEqual(value, .numberSet([AWSNumber(double: 1.2345), AWSNumber(int: -19)])) + } + + func testListDecoding() throws { + let json = "{\"L\": [{\"NS\": [\"1.2345\", \"-19\"]}, {\"S\": \"huhu\"}]}" + let value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!) + + XCTAssertEqual(value, .list([.numberSet([AWSNumber(double: 1.2345), AWSNumber(int: -19)]), .string("huhu")])) + } + + func testMapDecoding() throws { + let json = "{\"M\": {\"numbers\": {\"NS\": [\"1.2345\", \"-19\"]}, \"string\": {\"S\": \"huhu\"}}}" + let value = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!) + + XCTAssertEqual(value, .map([ + "numbers": .numberSet([AWSNumber(double: 1.2345), AWSNumber(int: -19)]), + "string": .string("huhu"), + ])) + } + + func testEmptyDecoding() throws { + let json = "{\"haha\": 1}" + do { + _ = try JSONDecoder().decode(DynamoDB.AttributeValue.self, from: json.data(using: .utf8)!) + XCTFail("Did not expect to reach this point") + } catch { + switch error { + case DecodingError.dataCorrupted(let context): + // expected error + XCTAssertEqual(context.codingPath.count, 0) + default: + XCTFail("Unexpected error: \(String(describing: error))") + } + } + } + + func testEquatable() { + XCTAssertEqual(DynamoDB.AttributeValue.boolean(true), .boolean(true)) + XCTAssertNotEqual(DynamoDB.AttributeValue.boolean(true), .boolean(false)) + XCTAssertNotEqual(DynamoDB.AttributeValue.boolean(true), .string("haha")) + } +} diff --git a/Tests/AWSLambdaEventsTests/DynamoDBTests.swift b/Tests/AWSLambdaEventsTests/DynamoDBTests.swift new file mode 100644 index 00000000..43c5c706 --- /dev/null +++ b/Tests/AWSLambdaEventsTests/DynamoDBTests.swift @@ -0,0 +1,125 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-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 import AWSLambdaEvents +import XCTest + +class DynamoDBTests: XCTestCase { + static let streamEventPayload = """ + { + "Records": [ + { + "eventID": "1", + "eventVersion": "1.0", + "dynamodb": { + "ApproximateCreationDateTime": 1.578648338E9, + "Keys": { + "Id": { + "N": "101" + } + }, + "NewImage": { + "Message": { + "S": "New item!" + }, + "Id": { + "N": "101" + } + }, + "StreamViewType": "NEW_AND_OLD_IMAGES", + "SequenceNumber": "111", + "SizeBytes": 26 + }, + "awsRegion": "eu-central-1", + "eventName": "INSERT", + "eventSourceARN": "arn:aws:dynamodb:eu-central-1:account-id:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899", + "eventSource": "aws:dynamodb" + }, + { + "eventID": "2", + "eventVersion": "1.0", + "dynamodb": { + "ApproximateCreationDateTime": 1.578648338E9, + "OldImage": { + "Message": { + "S": "New item!" + }, + "Id": { + "N": "101" + } + }, + "SequenceNumber": "222", + "Keys": { + "Id": { + "N": "101" + } + }, + "SizeBytes": 59, + "NewImage": { + "Message": { + "S": "This item has changed" + }, + "Id": { + "N": "101" + } + }, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "awsRegion": "eu-central-1", + "eventName": "MODIFY", + "eventSourceARN": "arn:aws:dynamodb:eu-central-1:account-id:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899", + "eventSource": "aws:dynamodb" + }, + { + "eventID": "3", + "eventVersion": "1.0", + "dynamodb": { + "ApproximateCreationDateTime":1.578648338E9, + "Keys": { + "Id": { + "N": "101" + } + }, + "SizeBytes": 38, + "SequenceNumber": "333", + "OldImage": { + "Message": { + "S": "This item has changed" + }, + "Id": { + "N": "101" + } + }, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "awsRegion": "eu-central-1", + "eventName": "REMOVE", + "eventSourceARN": "arn:aws:dynamodb:eu-central-1:account-id:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899", + "eventSource": "aws:dynamodb" + } + ] + } + """ + + func testScheduledEventFromJSON() { + let data = DynamoDBTests.streamEventPayload.data(using: .utf8)! + do { + let event = try JSONDecoder().decode(DynamoDB.Event.self, from: data) + + XCTAssertEqual(event.records.count, 3) + } catch { + XCTFail("Unexpected error: \(error)") + } + } +} diff --git a/Tests/AWSLambdaEventsTests/S3Tests.swift b/Tests/AWSLambdaEventsTests/S3Tests.swift deleted file mode 100644 index 6f87c05d..00000000 --- a/Tests/AWSLambdaEventsTests/S3Tests.swift +++ /dev/null @@ -1,88 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-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 import AWSLambdaEvents -import XCTest - -class S3Tests: XCTestCase { - static let eventPayload = """ - { - "Records": [ - { - "eventVersion":"2.1", - "eventSource":"aws:s3", - "awsRegion":"eu-central-1", - "eventTime":"2020-01-13T09:25:40.621Z", - "eventName":"ObjectCreated:Put", - "userIdentity":{ - "principalId":"AWS:AAAAAAAJ2MQ4YFQZ7AULJ" - }, - "requestParameters":{ - "sourceIPAddress":"123.123.123.123" - }, - "responseElements":{ - "x-amz-request-id":"01AFA1430E18C358", - "x-amz-id-2":"JsbNw6sHGFwgzguQjbYcew//bfAeZITyTYLfjuu1U4QYqCq5CPlSyYLtvWQS+gw0RxcroItGwm8=" - }, - "s3":{ - "s3SchemaVersion":"1.0", - "configurationId":"98b55bc4-3c0c-4007-b727-c6b77a259dde", - "bucket":{ - "name":"eventsources", - "ownerIdentity":{ - "principalId":"AAAAAAAAAAAAAA" - }, - "arn":"arn:aws:s3:::eventsources" - }, - "object":{ - "key":"Hi.md", - "size":2880, - "eTag":"91a7f2c3ae81bcc6afef83979b463f0e", - "sequencer":"005E1C37948E783A6E" - } - } - } - ] - } - """ - - func testSimpleEventFromJSON() { - let data = S3Tests.eventPayload.data(using: .utf8)! - var event: S3.Event? - XCTAssertNoThrow(event = try JSONDecoder().decode(S3.Event.self, from: data)) - - guard let record = event?.records.first else { - XCTFail("Expected to have one record") - return - } - - XCTAssertEqual(record.eventVersion, "2.1") - XCTAssertEqual(record.eventSource, "aws:s3") - XCTAssertEqual(record.awsRegion, "eu-central-1") - XCTAssertEqual(record.eventName, "ObjectCreated:Put") - XCTAssertEqual(record.eventTime, Date(timeIntervalSince1970: 1_578_907_540.621)) - XCTAssertEqual(record.userIdentity, S3.UserIdentity(principalId: "AWS:AAAAAAAJ2MQ4YFQZ7AULJ")) - XCTAssertEqual(record.requestParameters, S3.RequestParameters(sourceIPAddress: "123.123.123.123")) - XCTAssertEqual(record.responseElements.count, 2) - XCTAssertEqual(record.s3.schemaVersion, "1.0") - XCTAssertEqual(record.s3.configurationId, "98b55bc4-3c0c-4007-b727-c6b77a259dde") - XCTAssertEqual(record.s3.bucket.name, "eventsources") - XCTAssertEqual(record.s3.bucket.ownerIdentity, S3.UserIdentity(principalId: "AAAAAAAAAAAAAA")) - XCTAssertEqual(record.s3.bucket.arn, "arn:aws:s3:::eventsources") - XCTAssertEqual(record.s3.object.key, "Hi.md") - XCTAssertEqual(record.s3.object.size, 2880) - XCTAssertEqual(record.s3.object.eTag, "91a7f2c3ae81bcc6afef83979b463f0e") - XCTAssertEqual(record.s3.object.sequencer, "005E1C37948E783A6E") - } -}