From c1af9de7dfd2a25b6465563f5891d824c810ed1d Mon Sep 17 00:00:00 2001 From: Richard Kendall Wolf Date: Sun, 23 Jul 2023 16:19:07 -0500 Subject: [PATCH 1/6] Add WebSockets event types (and tests) --- .../APIGateway+WebSockets.swift | 68 ++++++++++++++++ .../APIGateway+WebsocketsTests.swift | 77 +++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 Sources/AWSLambdaEvents/APIGateway+WebSockets.swift create mode 100644 Tests/AWSLambdaEventsTests/APIGateway+WebsocketsTests.swift diff --git a/Sources/AWSLambdaEvents/APIGateway+WebSockets.swift b/Sources/AWSLambdaEvents/APIGateway+WebSockets.swift new file mode 100644 index 0000000..d9bcef6 --- /dev/null +++ b/Sources/AWSLambdaEvents/APIGateway+WebSockets.swift @@ -0,0 +1,68 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) YEARS 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 +// +//===----------------------------------------------------------------------===// + +/// `APIGatewayWebSocketRequest` is a variation of the`APIGatewayV2Request` +/// and contains data coming from the WebSockets API Gateway. +public struct APIGatewayWebSocketRequest: Codable { + /// `Context` contains information to identify the AWS account and resources invoking the Lambda function. + public struct Context: Codable { + public struct Identity: Codable { + let sourceIp: String + } + + let routeKey: String + let eventType: String + let extendedRequestId: String + /// The request time in format: 23/Apr/2020:11:08:18 +0000 + let requestTime: String + let messageDirection: String + let stage: String + let connectedAt: UInt64 + let requestTimeEpoch: UInt64 + let identity: Identity + let requestId: String + let domainName: String + let connectionId: String + let apiId: String + } + + public let headers: HTTPHeaders? + public let multiValueHeaders: HTTPMultiValueHeaders? + public let context: Context + public let body: String? + public let isBase64Encoded: Bool? + + enum CodingKeys: String, CodingKey { + case headers + case multiValueHeaders + case context = "requestContext" + case body + case isBase64Encoded + } +} + +/// `APIGatewayWebSocketResponse` is a type alias for `APIGatewayV2Request`. +/// Typically, lambda WebSockets servers send clients data via +/// the ApiGatewayManagementApi mechanism. However, APIGateway does require +/// lambda servers to return some kind of status when APIGateway invokes them. +/// This can be as simple as always returning a 200 "OK" response for all +/// WebSockets requests (the ApiGatewayManagementApi can return any errors to +/// WebSockets clients). +public typealias APIGatewayWebSocketResponse = APIGatewayV2Response + +#if swift(>=5.6) +extension APIGatewayWebSocketRequest: Sendable {} +extension APIGatewayWebSocketRequest.Context: Sendable {} +extension APIGatewayWebSocketRequest.Context.Identity: Sendable {} +#endif diff --git a/Tests/AWSLambdaEventsTests/APIGateway+WebsocketsTests.swift b/Tests/AWSLambdaEventsTests/APIGateway+WebsocketsTests.swift new file mode 100644 index 0000000..19402b7 --- /dev/null +++ b/Tests/AWSLambdaEventsTests/APIGateway+WebsocketsTests.swift @@ -0,0 +1,77 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) YEARS 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 APIGatewayWebSocketsTests: XCTestCase { + static let exampleConnectEventBody = """ + { + "headers": { + "Host": "lqrlmblaa2.execute-api.us-east-1.amazonaws.com", + "Origin": "wss://lqrlmblaa2.execute-api.us-east-1.amazonaws.com", + "Sec-WebSocket-Extensions": "permessage-deflate; client_max_window_bits; server_max_window_bits=15", + "Sec-WebSocket-Key": "am5ubWVpbHd3bmNyYXF0ag==", + "Sec-WebSocket-Version": "13", + "X-Amzn-Trace-Id": "Root=1-64b83950-42de8e247b4c2b43091ef67c", + "X-Forwarded-For": "24.148.42.16", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "multiValueHeaders": { + "Host": [ "lqrlmblaa2.execute-api.us-east-1.amazonaws.com" ], + "Origin": [ "wss://lqrlmblaa2.execute-api.us-east-1.amazonaws.com" ], + "Sec-WebSocket-Extensions": [ + "permessage-deflate; client_max_window_bits; server_max_window_bits=15" + ], + "Sec-WebSocket-Key": [ "am5ubWVpbHd3bmNyYXF0ag==" ], + "Sec-WebSocket-Version": [ "13" ], + "X-Amzn-Trace-Id": [ "Root=1-64b83950-42de8e247b4c2b43091ef67c" ], + "X-Forwarded-For": [ "24.148.42.16" ], + "X-Forwarded-Port": [ "443" ], + "X-Forwarded-Proto": [ "https" ] + }, + "requestContext": { + "routeKey": "$connect", + "eventType": "CONNECT", + "extendedRequestId": "IU3kkGyEoAMFwZQ=", + "requestTime": "19/Jul/2023:19:28:16 +0000", + "messageDirection": "IN", + "stage": "dev", + "connectedAt": 1689794896145, + "requestTimeEpoch": 1689794896162, + "identity": { "sourceIp": "24.148.42.16" }, + "requestId": "IU3kkGyEoAMFwZQ=", + "domainName": "lqrlmblaa2.execute-api.us-east-1.amazonaws.com", + "connectionId": "IU3kkeN4IAMCJwA=", + "apiId": "lqrlmblaa2" + }, + "isBase64Encoded": false + } + """ + + // MARK: - Request - + + // MARK: Decoding + + func testRequestDecodingExampleConnectRequest() { + let data = APIGatewayWebSocketsTests.exampleConnectEventBody.data(using: .utf8)! + var req: APIGatewayWebSocketRequest? + XCTAssertNoThrow(req = try JSONDecoder().decode(APIGatewayWebSocketRequest.self, from: data)) + + XCTAssertEqual(req?.context.routeKey, "$connect") + XCTAssertEqual(req?.context.connectionId, "IU3kkeN4IAMCJwA=") + XCTAssertNil(req?.body) + } +} From 1dea37ef0e452d122a51662c00360545f6614c73 Mon Sep 17 00:00:00 2001 From: Richard Kendall Wolf Date: Thu, 26 Jun 2025 20:20:04 -0500 Subject: [PATCH 2/6] Make context properties public --- .../APIGateway+WebSockets.swift | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Sources/AWSLambdaEvents/APIGateway+WebSockets.swift b/Sources/AWSLambdaEvents/APIGateway+WebSockets.swift index d9bcef6..339478e 100644 --- a/Sources/AWSLambdaEvents/APIGateway+WebSockets.swift +++ b/Sources/AWSLambdaEvents/APIGateway+WebSockets.swift @@ -18,23 +18,23 @@ public struct APIGatewayWebSocketRequest: Codable { /// `Context` contains information to identify the AWS account and resources invoking the Lambda function. public struct Context: Codable { public struct Identity: Codable { - let sourceIp: String + public let sourceIp: String } - let routeKey: String - let eventType: String - let extendedRequestId: String + public let routeKey: String + public let eventType: String + public let extendedRequestId: String /// The request time in format: 23/Apr/2020:11:08:18 +0000 - let requestTime: String - let messageDirection: String - let stage: String - let connectedAt: UInt64 - let requestTimeEpoch: UInt64 - let identity: Identity - let requestId: String - let domainName: String - let connectionId: String - let apiId: String + public let requestTime: String + public let messageDirection: String + public let stage: String + public let connectedAt: UInt64 + public let requestTimeEpoch: UInt64 + public let identity: Identity + public let requestId: String + public let domainName: String + public let connectionId: String + public let apiId: String } public let headers: HTTPHeaders? From 638bad1e8c89fbe7d626e37a5c67693d71edea33 Mon Sep 17 00:00:00 2001 From: Richard Kendall Wolf Date: Thu, 26 Jun 2025 20:20:59 -0500 Subject: [PATCH 3/6] Fix JSON error in test case data --- Tests/AWSLambdaEventsTests/APIGateway+WebsocketsTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/AWSLambdaEventsTests/APIGateway+WebsocketsTests.swift b/Tests/AWSLambdaEventsTests/APIGateway+WebsocketsTests.swift index 19402b7..bf17bc1 100644 --- a/Tests/AWSLambdaEventsTests/APIGateway+WebsocketsTests.swift +++ b/Tests/AWSLambdaEventsTests/APIGateway+WebsocketsTests.swift @@ -21,7 +21,7 @@ class APIGatewayWebSocketsTests: XCTestCase { "headers": { "Host": "lqrlmblaa2.execute-api.us-east-1.amazonaws.com", "Origin": "wss://lqrlmblaa2.execute-api.us-east-1.amazonaws.com", - "Sec-WebSocket-Extensions": "permessage-deflate; client_max_window_bits; server_max_window_bits=15", + "Sec-WebSocket-Extensions": "", "Sec-WebSocket-Key": "am5ubWVpbHd3bmNyYXF0ag==", "Sec-WebSocket-Version": "13", "X-Amzn-Trace-Id": "Root=1-64b83950-42de8e247b4c2b43091ef67c", From 10ffcabc4cfc74b4fd1289f613b3340a6df4b566 Mon Sep 17 00:00:00 2001 From: Richard Kendall Wolf Date: Sun, 29 Jun 2025 15:34:37 -0500 Subject: [PATCH 4/6] Fix formatting issue in test case --- .../APIGateway+WebsocketsTests.swift | 80 +++++++++---------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/Tests/AWSLambdaEventsTests/APIGateway+WebsocketsTests.swift b/Tests/AWSLambdaEventsTests/APIGateway+WebsocketsTests.swift index bf17bc1..29dc5da 100644 --- a/Tests/AWSLambdaEventsTests/APIGateway+WebsocketsTests.swift +++ b/Tests/AWSLambdaEventsTests/APIGateway+WebsocketsTests.swift @@ -18,46 +18,46 @@ import XCTest class APIGatewayWebSocketsTests: XCTestCase { static let exampleConnectEventBody = """ { - "headers": { - "Host": "lqrlmblaa2.execute-api.us-east-1.amazonaws.com", - "Origin": "wss://lqrlmblaa2.execute-api.us-east-1.amazonaws.com", - "Sec-WebSocket-Extensions": "", - "Sec-WebSocket-Key": "am5ubWVpbHd3bmNyYXF0ag==", - "Sec-WebSocket-Version": "13", - "X-Amzn-Trace-Id": "Root=1-64b83950-42de8e247b4c2b43091ef67c", - "X-Forwarded-For": "24.148.42.16", - "X-Forwarded-Port": "443", - "X-Forwarded-Proto": "https" - }, - "multiValueHeaders": { - "Host": [ "lqrlmblaa2.execute-api.us-east-1.amazonaws.com" ], - "Origin": [ "wss://lqrlmblaa2.execute-api.us-east-1.amazonaws.com" ], - "Sec-WebSocket-Extensions": [ - "permessage-deflate; client_max_window_bits; server_max_window_bits=15" - ], - "Sec-WebSocket-Key": [ "am5ubWVpbHd3bmNyYXF0ag==" ], - "Sec-WebSocket-Version": [ "13" ], - "X-Amzn-Trace-Id": [ "Root=1-64b83950-42de8e247b4c2b43091ef67c" ], - "X-Forwarded-For": [ "24.148.42.16" ], - "X-Forwarded-Port": [ "443" ], - "X-Forwarded-Proto": [ "https" ] - }, - "requestContext": { - "routeKey": "$connect", - "eventType": "CONNECT", - "extendedRequestId": "IU3kkGyEoAMFwZQ=", - "requestTime": "19/Jul/2023:19:28:16 +0000", - "messageDirection": "IN", - "stage": "dev", - "connectedAt": 1689794896145, - "requestTimeEpoch": 1689794896162, - "identity": { "sourceIp": "24.148.42.16" }, - "requestId": "IU3kkGyEoAMFwZQ=", - "domainName": "lqrlmblaa2.execute-api.us-east-1.amazonaws.com", - "connectionId": "IU3kkeN4IAMCJwA=", - "apiId": "lqrlmblaa2" - }, - "isBase64Encoded": false + "headers": { + "Host": "lqrlmblaa2.execute-api.us-east-1.amazonaws.com", + "Origin": "wss://lqrlmblaa2.execute-api.us-east-1.amazonaws.com", + "Sec-WebSocket-Extensions": "", + "Sec-WebSocket-Key": "am5ubWVpbHd3bmNyYXF0ag==", + "Sec-WebSocket-Version": "13", + "X-Amzn-Trace-Id": "Root=1-64b83950-42de8e247b4c2b43091ef67c", + "X-Forwarded-For": "24.148.42.16", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "multiValueHeaders": { + "Host": [ "lqrlmblaa2.execute-api.us-east-1.amazonaws.com" ], + "Origin": [ "wss://lqrlmblaa2.execute-api.us-east-1.amazonaws.com" ], + "Sec-WebSocket-Extensions": [ + "permessage-deflate; client_max_window_bits; server_max_window_bits=15" + ], + "Sec-WebSocket-Key": [ "am5ubWVpbHd3bmNyYXF0ag==" ], + "Sec-WebSocket-Version": [ "13" ], + "X-Amzn-Trace-Id": [ "Root=1-64b83950-42de8e247b4c2b43091ef67c" ], + "X-Forwarded-For": [ "24.148.42.16" ], + "X-Forwarded-Port": [ "443" ], + "X-Forwarded-Proto": [ "https" ] + }, + "requestContext": { + "routeKey": "$connect", + "eventType": "CONNECT", + "extendedRequestId": "IU3kkGyEoAMFwZQ=", + "requestTime": "19/Jul/2023:19:28:16 +0000", + "messageDirection": "IN", + "stage": "dev", + "connectedAt": 1689794896145, + "requestTimeEpoch": 1689794896162, + "identity": { "sourceIp": "24.148.42.16" }, + "requestId": "IU3kkGyEoAMFwZQ=", + "domainName": "lqrlmblaa2.execute-api.us-east-1.amazonaws.com", + "connectionId": "IU3kkeN4IAMCJwA=", + "apiId": "lqrlmblaa2" + }, + "isBase64Encoded": false } """ From a050d03cc3602b83efc5d8205a2d0bbfcb078746 Mon Sep 17 00:00:00 2001 From: Richard Kendall Wolf Date: Mon, 30 Jun 2025 12:21:27 -0500 Subject: [PATCH 5/6] Fix formatting issues in test case --- .../APIGateway+WebsocketsTests.swift | 89 ++++++++++--------- 1 file changed, 45 insertions(+), 44 deletions(-) diff --git a/Tests/AWSLambdaEventsTests/APIGateway+WebsocketsTests.swift b/Tests/AWSLambdaEventsTests/APIGateway+WebsocketsTests.swift index 29dc5da..15dcd63 100644 --- a/Tests/AWSLambdaEventsTests/APIGateway+WebsocketsTests.swift +++ b/Tests/AWSLambdaEventsTests/APIGateway+WebsocketsTests.swift @@ -12,54 +12,55 @@ // //===----------------------------------------------------------------------===// -@testable import AWSLambdaEvents import XCTest +@testable import AWSLambdaEvents + class APIGatewayWebSocketsTests: XCTestCase { static let exampleConnectEventBody = """ - { - "headers": { - "Host": "lqrlmblaa2.execute-api.us-east-1.amazonaws.com", - "Origin": "wss://lqrlmblaa2.execute-api.us-east-1.amazonaws.com", - "Sec-WebSocket-Extensions": "", - "Sec-WebSocket-Key": "am5ubWVpbHd3bmNyYXF0ag==", - "Sec-WebSocket-Version": "13", - "X-Amzn-Trace-Id": "Root=1-64b83950-42de8e247b4c2b43091ef67c", - "X-Forwarded-For": "24.148.42.16", - "X-Forwarded-Port": "443", - "X-Forwarded-Proto": "https" - }, - "multiValueHeaders": { - "Host": [ "lqrlmblaa2.execute-api.us-east-1.amazonaws.com" ], - "Origin": [ "wss://lqrlmblaa2.execute-api.us-east-1.amazonaws.com" ], - "Sec-WebSocket-Extensions": [ - "permessage-deflate; client_max_window_bits; server_max_window_bits=15" - ], - "Sec-WebSocket-Key": [ "am5ubWVpbHd3bmNyYXF0ag==" ], - "Sec-WebSocket-Version": [ "13" ], - "X-Amzn-Trace-Id": [ "Root=1-64b83950-42de8e247b4c2b43091ef67c" ], - "X-Forwarded-For": [ "24.148.42.16" ], - "X-Forwarded-Port": [ "443" ], - "X-Forwarded-Proto": [ "https" ] - }, - "requestContext": { - "routeKey": "$connect", - "eventType": "CONNECT", - "extendedRequestId": "IU3kkGyEoAMFwZQ=", - "requestTime": "19/Jul/2023:19:28:16 +0000", - "messageDirection": "IN", - "stage": "dev", - "connectedAt": 1689794896145, - "requestTimeEpoch": 1689794896162, - "identity": { "sourceIp": "24.148.42.16" }, - "requestId": "IU3kkGyEoAMFwZQ=", - "domainName": "lqrlmblaa2.execute-api.us-east-1.amazonaws.com", - "connectionId": "IU3kkeN4IAMCJwA=", - "apiId": "lqrlmblaa2" - }, - "isBase64Encoded": false - } - """ + { + "headers": { + "Host": "lqrlmblaa2.execute-api.us-east-1.amazonaws.com", + "Origin": "wss://lqrlmblaa2.execute-api.us-east-1.amazonaws.com", + "Sec-WebSocket-Extensions": "", + "Sec-WebSocket-Key": "am5ubWVpbHd3bmNyYXF0ag==", + "Sec-WebSocket-Version": "13", + "X-Amzn-Trace-Id": "Root=1-64b83950-42de8e247b4c2b43091ef67c", + "X-Forwarded-For": "24.148.42.16", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "multiValueHeaders": { + "Host": [ "lqrlmblaa2.execute-api.us-east-1.amazonaws.com" ], + "Origin": [ "wss://lqrlmblaa2.execute-api.us-east-1.amazonaws.com" ], + "Sec-WebSocket-Extensions": [ + "permessage-deflate; client_max_window_bits; server_max_window_bits=15" + ], + "Sec-WebSocket-Key": [ "am5ubWVpbHd3bmNyYXF0ag==" ], + "Sec-WebSocket-Version": [ "13" ], + "X-Amzn-Trace-Id": [ "Root=1-64b83950-42de8e247b4c2b43091ef67c" ], + "X-Forwarded-For": [ "24.148.42.16" ], + "X-Forwarded-Port": [ "443" ], + "X-Forwarded-Proto": [ "https" ] + }, + "requestContext": { + "routeKey": "$connect", + "eventType": "CONNECT", + "extendedRequestId": "IU3kkGyEoAMFwZQ=", + "requestTime": "19/Jul/2023:19:28:16 +0000", + "messageDirection": "IN", + "stage": "dev", + "connectedAt": 1689794896145, + "requestTimeEpoch": 1689794896162, + "identity": { "sourceIp": "24.148.42.16" }, + "requestId": "IU3kkGyEoAMFwZQ=", + "domainName": "lqrlmblaa2.execute-api.us-east-1.amazonaws.com", + "connectionId": "IU3kkeN4IAMCJwA=", + "apiId": "lqrlmblaa2" + }, + "isBase64Encoded": false + } + """ // MARK: - Request - From 3ddb1b944bdee42bb15dd4bbe69b55436bbe53fd Mon Sep 17 00:00:00 2001 From: Richard Kendall Wolf Date: Mon, 30 Jun 2025 12:40:11 -0500 Subject: [PATCH 6/6] Adopt Swift Testing in test WebSockets test case --- .../APIGateway+WebsocketsTests.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Tests/AWSLambdaEventsTests/APIGateway+WebsocketsTests.swift b/Tests/AWSLambdaEventsTests/APIGateway+WebsocketsTests.swift index 15dcd63..c9da46f 100644 --- a/Tests/AWSLambdaEventsTests/APIGateway+WebsocketsTests.swift +++ b/Tests/AWSLambdaEventsTests/APIGateway+WebsocketsTests.swift @@ -12,11 +12,13 @@ // //===----------------------------------------------------------------------===// -import XCTest +import Foundation +import Testing @testable import AWSLambdaEvents -class APIGatewayWebSocketsTests: XCTestCase { +@Suite +class APIGatewayWebSocketsTests { static let exampleConnectEventBody = """ { "headers": { @@ -65,14 +67,12 @@ class APIGatewayWebSocketsTests: XCTestCase { // MARK: - Request - // MARK: Decoding - - func testRequestDecodingExampleConnectRequest() { + @Test func testRequestDecodingExampleConnectRequest() async throws { let data = APIGatewayWebSocketsTests.exampleConnectEventBody.data(using: .utf8)! - var req: APIGatewayWebSocketRequest? - XCTAssertNoThrow(req = try JSONDecoder().decode(APIGatewayWebSocketRequest.self, from: data)) + let req = try JSONDecoder().decode(APIGatewayWebSocketRequest.self, from: data) - XCTAssertEqual(req?.context.routeKey, "$connect") - XCTAssertEqual(req?.context.connectionId, "IU3kkeN4IAMCJwA=") - XCTAssertNil(req?.body) + #expect(req.context.routeKey == "$connect") + #expect(req.context.connectionId == "IU3kkeN4IAMCJwA=") + #expect(req.body == nil) } }