diff --git a/Sources/AWSLambdaEvents/SNS.swift b/Sources/AWSLambdaEvents/SNS.swift new file mode 100644 index 00000000..b222f5b9 --- /dev/null +++ b/Sources/AWSLambdaEvents/SNS.swift @@ -0,0 +1,107 @@ +//===----------------------------------------------------------------------===// +// +// 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/sns.go +public enum SNS { + public struct Event: Decodable { + public struct Record: Decodable { + public let eventVersion: String + public let eventSubscriptionArn: String + public let eventSource: String + public let sns: Message + + public enum CodingKeys: String, CodingKey { + case eventVersion = "EventVersion" + case eventSubscriptionArn = "EventSubscriptionArn" + case eventSource = "EventSource" + case sns = "Sns" + } + } + + public let records: [Record] + + public enum CodingKeys: String, CodingKey { + case records = "Records" + } + } + + public struct Message { + public enum Attribute { + case string(String) + case binary([UInt8]) + } + + public let signature: String + public let messageId: String + public let type: String + public let topicArn: String + public let messageAttributes: [String: Attribute] + public let signatureVersion: String + + @ISO8601WithFractionalSecondsCoding + public var timestamp: Date + public let signingCertURL: String + public let message: String + public let unsubscribeUrl: String + public let subject: String? + } +} + +extension SNS.Message: Decodable { + enum CodingKeys: String, CodingKey { + case signature = "Signature" + case messageId = "MessageId" + case type = "Type" + case topicArn = "TopicArn" + case messageAttributes = "MessageAttributes" + case signatureVersion = "SignatureVersion" + case timestamp = "Timestamp" + case signingCertURL = "SigningCertUrl" + case message = "Message" + case unsubscribeUrl = "UnsubscribeUrl" + case subject = "Subject" + } +} + +extension SNS.Message.Attribute: Equatable {} + +extension SNS.Message.Attribute: Decodable { + enum CodingKeys: String, CodingKey { + case dataType = "Type" + case dataValue = "Value" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let dataType = try container.decode(String.self, forKey: .dataType) + // https://docs.aws.amazon.com/sns/latest/dg/sns-message-attributes.html#SNSMessageAttributes.DataTypes + switch dataType { + case "String": + let value = try container.decode(String.self, forKey: .dataValue) + self = .string(value) + case "Binary": + let base64encoded = try container.decode(String.self, forKey: .dataValue) + let bytes = try base64encoded.base64decoded() + self = .binary(bytes) + default: + throw DecodingError.dataCorruptedError(forKey: .dataType, in: container, debugDescription: """ + Unexpected value \"\(dataType)\" for key \(CodingKeys.dataType). + Expected `String` or `Binary`. + """) + } + } +} diff --git a/Sources/AWSLambdaEvents/SQS.swift b/Sources/AWSLambdaEvents/SQS.swift new file mode 100644 index 00000000..e4d25d46 --- /dev/null +++ b/Sources/AWSLambdaEvents/SQS.swift @@ -0,0 +1,96 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +/// https://github.com/aws/aws-lambda-go/blob/master/events/sqs.go +public enum SQS { + public struct Event: Decodable { + public let records: [Message] + + enum CodingKeys: String, CodingKey { + case records = "Records" + } + } + + public struct Message { + /// https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_MessageAttributeValue.html + public enum Attribute { + case string(String) + case binary([UInt8]) + case number(String) + } + + public let messageId: String + public let receiptHandle: String + public var body: String + public let md5OfBody: String + public let md5OfMessageAttributes: String? + public let attributes: [String: String] + public let messageAttributes: [String: Attribute] + public let eventSourceArn: String + public let eventSource: String + public let awsRegion: AWSRegion + } +} + +extension SQS.Message: Decodable { + enum CodingKeys: String, CodingKey { + case messageId + case receiptHandle + case body + case md5OfBody + case md5OfMessageAttributes + case attributes + case messageAttributes + case eventSourceArn = "eventSourceARN" + case eventSource + case awsRegion + } +} + +extension SQS.Message.Attribute: Equatable {} + +extension SQS.Message.Attribute: Decodable { + enum CodingKeys: String, CodingKey { + case dataType + case stringValue + case binaryValue + + // BinaryListValue and StringListValue are unimplemented since + // they are not implemented as discussed here: + // https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_MessageAttributeValue.html + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let dataType = try container.decode(String.self, forKey: .dataType) + switch dataType { + case "String": + let value = try container.decode(String.self, forKey: .stringValue) + self = .string(value) + case "Number": + let value = try container.decode(String.self, forKey: .stringValue) + self = .number(value) + case "Binary": + let base64encoded = try container.decode(String.self, forKey: .binaryValue) + let bytes = try base64encoded.base64decoded() + self = .binary(bytes) + default: + throw DecodingError.dataCorruptedError(forKey: .dataType, in: container, debugDescription: """ + Unexpected value \"\(dataType)\" for key \(CodingKeys.dataType). + Expected `String`, `Binary` or `Number`. + """) + } + } +} diff --git a/Sources/AWSLambdaEvents/Utils/Base64.swift b/Sources/AWSLambdaEvents/Utils/Base64.swift new file mode 100644 index 00000000..66f16080 --- /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, Equatable { + 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/DateWrappers.swift b/Sources/AWSLambdaEvents/Utils/DateWrappers.swift similarity index 100% rename from Sources/AWSLambdaEvents/DateWrappers.swift rename to Sources/AWSLambdaEvents/Utils/DateWrappers.swift diff --git a/Tests/AWSLambdaEventsTests/SNSTests.swift b/Tests/AWSLambdaEventsTests/SNSTests.swift new file mode 100644 index 00000000..ea7cc047 --- /dev/null +++ b/Tests/AWSLambdaEventsTests/SNSTests.swift @@ -0,0 +1,82 @@ +//===----------------------------------------------------------------------===// +// +// 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 SNSTests: XCTestCase { + static let eventPayload = """ + { + "Records": [ + { + "EventSource": "aws:sns", + "EventVersion": "1.0", + "EventSubscriptionArn": "arn:aws:sns:eu-central-1:079477498937:EventSources-SNSTopic-1NHENSE2MQKF5:6fabdb7f-b27e-456d-8e8a-14679db9e40c", + "Sns": { + "Type": "Notification", + "MessageId": "bdb6900e-1ae9-5b4b-b7fc-c681fde222e3", + "TopicArn": "arn:aws:sns:eu-central-1:079477498937:EventSources-SNSTopic-1NHENSE2MQKF5", + "Subject": null, + "Message": "{\\\"hello\\\": \\\"world\\\"}", + "Timestamp": "2020-01-08T14:18:51.203Z", + "SignatureVersion": "1", + "Signature": "LJMF/xmMH7A1gNy2unLA3hmzyf6Be+zS/Yeiiz9tZbu6OG8fwvWZeNOcEZardhSiIStc0TF7h9I+4Qz3omCntaEfayzTGmWN8itGkn2mfn/hMFmPbGM8gEUz3+jp1n6p+iqP3XTx92R0LBIFrU3ylOxSo8+SCOjA015M93wfZzwj0WPtynji9iAvvtf15d8JxPUu1T05BRitpFd5s6ZXDHtVQ4x/mUoLUN8lOVp+rs281/ZdYNUG/V5CwlyUDTOERdryTkBJ/GO1NNPa+6m04ywJFa5d+BC8mDcUcHhhXXjpTEbt8AHBmswK3nudHrVMRO/G4zmssxU2P7ii5+gCfA==", + "SigningCertUrl": "https://sns.eu-central-1.amazonaws.com/SimpleNotificationService-6aad65c2f9911b05cd53efda11f913f9.pem", + "UnsubscribeUrl": "https://sns.eu-central-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-central-1:079477498937:EventSources-SNSTopic-1NHENSE2MQKF5:6fabdb7f-b27e-456d-8e8a-14679db9e40c", + "MessageAttributes": { + "binary":{ + "Type": "Binary", + "Value": "YmFzZTY0" + }, + "string":{ + "Type": "String", + "Value": "abc123" + } + } + } + } + ] + } + """ + + func testSimpleEventFromJSON() { + let data = SNSTests.eventPayload.data(using: .utf8)! + var event: SNS.Event? + XCTAssertNoThrow(event = try JSONDecoder().decode(SNS.Event.self, from: data)) + + guard let record = event?.records.first else { + XCTFail("Expected to have one record") + return + } + + XCTAssertEqual(record.eventSource, "aws:sns") + XCTAssertEqual(record.eventVersion, "1.0") + XCTAssertEqual(record.eventSubscriptionArn, "arn:aws:sns:eu-central-1:079477498937:EventSources-SNSTopic-1NHENSE2MQKF5:6fabdb7f-b27e-456d-8e8a-14679db9e40c") + + XCTAssertEqual(record.sns.type, "Notification") + XCTAssertEqual(record.sns.messageId, "bdb6900e-1ae9-5b4b-b7fc-c681fde222e3") + XCTAssertEqual(record.sns.topicArn, "arn:aws:sns:eu-central-1:079477498937:EventSources-SNSTopic-1NHENSE2MQKF5") + XCTAssertEqual(record.sns.message, "{\"hello\": \"world\"}") + XCTAssertEqual(record.sns.timestamp, Date(timeIntervalSince1970: 1_578_493_131.203)) + XCTAssertEqual(record.sns.signatureVersion, "1") + XCTAssertEqual(record.sns.signature, "LJMF/xmMH7A1gNy2unLA3hmzyf6Be+zS/Yeiiz9tZbu6OG8fwvWZeNOcEZardhSiIStc0TF7h9I+4Qz3omCntaEfayzTGmWN8itGkn2mfn/hMFmPbGM8gEUz3+jp1n6p+iqP3XTx92R0LBIFrU3ylOxSo8+SCOjA015M93wfZzwj0WPtynji9iAvvtf15d8JxPUu1T05BRitpFd5s6ZXDHtVQ4x/mUoLUN8lOVp+rs281/ZdYNUG/V5CwlyUDTOERdryTkBJ/GO1NNPa+6m04ywJFa5d+BC8mDcUcHhhXXjpTEbt8AHBmswK3nudHrVMRO/G4zmssxU2P7ii5+gCfA==") + XCTAssertEqual(record.sns.signingCertURL, "https://sns.eu-central-1.amazonaws.com/SimpleNotificationService-6aad65c2f9911b05cd53efda11f913f9.pem") + XCTAssertEqual(record.sns.unsubscribeUrl, "https://sns.eu-central-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-central-1:079477498937:EventSources-SNSTopic-1NHENSE2MQKF5:6fabdb7f-b27e-456d-8e8a-14679db9e40c") + + XCTAssertEqual(record.sns.messageAttributes.count, 2) + + XCTAssertEqual(record.sns.messageAttributes["binary"], .binary([UInt8]("base64".utf8))) + XCTAssertEqual(record.sns.messageAttributes["string"], .string("abc123")) + } +} diff --git a/Tests/AWSLambdaEventsTests/SQSTests.swift b/Tests/AWSLambdaEventsTests/SQSTests.swift new file mode 100644 index 00000000..f337f075 --- /dev/null +++ b/Tests/AWSLambdaEventsTests/SQSTests.swift @@ -0,0 +1,87 @@ +//===----------------------------------------------------------------------===// +// +// 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 SQSTests: XCTestCase { + static let testPayload = """ + { + "Records": [ + { + "messageId": "19dd0b57-b21e-4ac1-bd88-01bbb068cb78", + "receiptHandle": "MessageReceiptHandle", + "body": "Hello from SQS!", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1523232000000", + "SenderId": "123456789012", + "ApproximateFirstReceiveTimestamp": "1523232000001" + }, + "messageAttributes": { + "number":{ + "stringValue":"123", + "stringListValues":[], + "binaryListValues":[], + "dataType":"Number" + }, + "string":{ + "stringValue":"abc123", + "stringListValues":[], + "binaryListValues":[], + "dataType":"String" + }, + "binary":{ + "dataType": "Binary", + "stringListValues":[], + "binaryListValues":[], + "binaryValue":"YmFzZTY0" + }, + + }, + "md5OfBody": "7b270e59b47ff90a553787216d55d91d", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:us-east-1:123456789012:MyQueue", + "awsRegion": "us-east-1" + } + ] + } + """ + + func testSimpleEventFromJSON() { + let data = SQSTests.testPayload.data(using: .utf8)! + var event: SQS.Event? + XCTAssertNoThrow(event = try JSONDecoder().decode(SQS.Event.self, from: data)) + + guard let message = event?.records.first else { + XCTFail("Expected to have one message in the event") + return + } + + XCTAssertEqual(message.messageId, "19dd0b57-b21e-4ac1-bd88-01bbb068cb78") + XCTAssertEqual(message.receiptHandle, "MessageReceiptHandle") + XCTAssertEqual(message.body, "Hello from SQS!") + XCTAssertEqual(message.attributes.count, 4) + + XCTAssertEqual(message.messageAttributes, [ + "number": .number("123"), + "string": .string("abc123"), + "binary": .binary([UInt8]("base64".utf8)), + ]) + XCTAssertEqual(message.md5OfBody, "7b270e59b47ff90a553787216d55d91d") + XCTAssertEqual(message.eventSource, "aws:sqs") + XCTAssertEqual(message.eventSourceArn, "arn:aws:sqs:us-east-1:123456789012:MyQueue") + XCTAssertEqual(message.awsRegion, .us_east_1) + } +} diff --git a/Tests/AWSLambdaEventsTests/Utils/Base64Tests.swift b/Tests/AWSLambdaEventsTests/Utils/Base64Tests.swift new file mode 100644 index 00000000..8e3c8695 --- /dev/null +++ b/Tests/AWSLambdaEventsTests/Utils/Base64Tests.swift @@ -0,0 +1,98 @@ +//===----------------------------------------------------------------------===// +// +// 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 Base64Tests: XCTestCase { + // MARK: - Encoding - + + func testEncodeEmptyData() throws { + let data = [UInt8]() + let encodedData = String(base64Encoding: data) + XCTAssertEqual(encodedData.count, 0) + } + + func testBase64EncodingArrayOfNulls() throws { + let data = Array(repeating: UInt8(0), count: 10) + let encodedData = String(base64Encoding: data) + XCTAssertEqual(encodedData, "AAAAAAAAAAAAAA==") + } + + func testBase64EncodingAllTheBytesSequentially() throws { + let data = Array(UInt8(0) ... UInt8(255)) + let encodedData = String(base64Encoding: data) + XCTAssertEqual(encodedData, "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w==") + } + + func testBase64UrlEncodingAllTheBytesSequentially() throws { + let data = Array(UInt8(0) ... UInt8(255)) + let encodedData = String(base64Encoding: data, options: .base64UrlAlphabet) + XCTAssertEqual(encodedData, "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0-P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn-AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq-wsbKztLW2t7i5uru8vb6_wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t_g4eLj5OXm5-jp6uvs7e7v8PHy8_T19vf4-fr7_P3-_w==") + } + + // MARK: - Decoding - + + func testDecodeEmptyString() throws { + var decoded: [UInt8]? + XCTAssertNoThrow(decoded = try "".base64decoded()) + XCTAssertEqual(decoded?.count, 0) + } + + func testBase64DecodingArrayOfNulls() throws { + let expected = Array(repeating: UInt8(0), count: 10) + var decoded: [UInt8]? + XCTAssertNoThrow(decoded = try "AAAAAAAAAAAAAA==".base64decoded()) + XCTAssertEqual(decoded, expected) + } + + func testBase64DecodingAllTheBytesSequentially() { + let base64 = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w==" + + let expected = Array(UInt8(0) ... UInt8(255)) + var decoded: [UInt8]? + XCTAssertNoThrow(decoded = try base64.base64decoded()) + + XCTAssertEqual(decoded, expected) + } + + func testBase64UrlDecodingAllTheBytesSequentially() { + let base64 = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0-P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn-AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq-wsbKztLW2t7i5uru8vb6_wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t_g4eLj5OXm5-jp6uvs7e7v8PHy8_T19vf4-fr7_P3-_w==" + + let expected = Array(UInt8(0) ... UInt8(255)) + var decoded: [UInt8]? + XCTAssertNoThrow(decoded = try base64.base64decoded(options: .base64UrlAlphabet)) + + XCTAssertEqual(decoded, expected) + } + + func testBase64DecodingWithPoop() { + XCTAssertThrowsError(_ = try "💩".base64decoded()) { error in + XCTAssertEqual(error as? Base64.DecodingError, .invalidCharacter(240)) + } + } + + func testBase64DecodingWithInvalidLength() { + XCTAssertThrowsError(_ = try "AAAAA".base64decoded()) { error in + XCTAssertEqual(error as? Base64.DecodingError, .invalidLength) + } + } + + func testNSStringToDecode() { + let test = "1234567" + let nsstring = test.data(using: .utf8)!.base64EncodedString() + + XCTAssertNoThrow(try nsstring.base64decoded()) + } +} diff --git a/Tests/AWSLambdaEventsTests/DateWrapperTests.swift b/Tests/AWSLambdaEventsTests/Utils/DateWrapperTests.swift similarity index 100% rename from Tests/AWSLambdaEventsTests/DateWrapperTests.swift rename to Tests/AWSLambdaEventsTests/Utils/DateWrapperTests.swift