diff --git a/Sources/SLSAdapter/Function+Extensions.swift b/Sources/SLSAdapter/Function+Extensions.swift index 7551f74..9dacba1 100644 --- a/Sources/SLSAdapter/Function+Extensions.swift +++ b/Sources/SLSAdapter/Function+Extensions.swift @@ -91,4 +91,25 @@ public extension Function { events: [.init(httpAPI: event)] ) } + + static func httpApiLambda( + handler: String, + description: String?, + memorySize: Int?, + environment: YAMLContent?, + runtime: Runtime?, + package: Package?, + event: EventHTTPAPI + ) throws -> Function { + Function( + handler: handler, + runtime: runtime, + memorySize: memorySize, + environment: environment, + description: description ?? "[${sls:stage}] \(event.method) \(event.path)", + package: package, + layers: nil, + events: [.init(httpAPI: event)] + ) + } } diff --git a/Tests/SLSAdapterTests/Fixtures/serverless_webhook.yml b/Tests/SLSAdapterTests/Fixtures/serverless_webhook.yml new file mode 100644 index 0000000..b47f573 --- /dev/null +++ b/Tests/SLSAdapterTests/Fixtures/serverless_webhook.yml @@ -0,0 +1,59 @@ +service: swift-webhook +frameworkVersion: '3' +configValidationMode: warn +useDotenv: false +provider: + name: aws + region: us-east-1 + disableRollback: false + runtime: provided.al2 + httpApi: + payload: '2.0' + cors: false + architecture: arm64 + versionFunctions: true + iam: + role: + statements: + - Effect: Allow + Action: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents + Resource: '*' + +package: + individually: true +functions: + postWebHook: + handler: post-webhook + memorySize: 256 + description: '[${sls:stage}] post /webhook' + package: + artifact: build/WebHook/WebHook.zip + events: + - httpApi: + path: /webhook + method: post + getWebHook: + handler: get-webhook + memorySize: 256 + description: '[${sls:stage}] get /webhook' + package: + artifact: build/WebHook/WebHook.zip + events: + - httpApi: + path: /webhook + method: get + githubWebHook: + handler: github-webhook + memorySize: 256 + description: '[${sls:stage}] get /github-webhook' + package: + artifact: build/GitHubWebHook/GitHubWebHook.zip + environment: + WEBHOOK_SECRET: '${ssm:/dev/swift-webhook/webhook_secret}' + events: + - httpApi: + path: /github-webhook + method: post diff --git a/Tests/SLSAdapterTests/ServerlessConfig+Exensions.swift b/Tests/SLSAdapterTests/ServerlessConfig+Exensions.swift index acfcd9d..75321cc 100644 --- a/Tests/SLSAdapterTests/ServerlessConfig+Exensions.swift +++ b/Tests/SLSAdapterTests/ServerlessConfig+Exensions.swift @@ -129,75 +129,135 @@ extension ServerlessConfig { executable: String, artifact: String ) throws -> ServerlessConfig { - let keyedPath = "\(httpAPIPath)/{\(dynamoDBKey)}" - let dynamoResourceName = "\(executable)Table" - - let environmentTableName = "DYNAMO_DB_TABLE_NAME" - let environmentKeyName = "DYNAMO_DB_KEY" - - let iam = Iam( - role: Role( - statements: [.allowLogAccess(resource: try YAMLContent(with: "*")), - .allowDynamoDBReadWrite(resource: try YAMLContent(with: [["Fn::GetAtt": [dynamoResourceName, "Arn"]]]))] - ) + let keyedPath = "\(httpAPIPath)/{\(dynamoDBKey)}" + let dynamoResourceName = "\(executable)Table" + + let environmentTableName = "DYNAMO_DB_TABLE_NAME" + let environmentKeyName = "DYNAMO_DB_KEY" + let dynamoResource = try YAMLContent(with: [["Fn::GetAtt": [dynamoResourceName, "Arn"]]]) + let iam = Iam( + role: Role( + statements: [.allowLogAccess(resource: try YAMLContent(with: "*")), + .allowDynamoDBReadWrite(resource: dynamoResource)] ) - let environment = try YAMLContent(with: [environmentTableName: "${self:custom.tableName}", - environmentKeyName: "${self:custom.keyName}"]) - let provider = Provider( - name: .aws, - region: region, - runtime: runtime, - environment: environment, - architecture: architecture, - httpAPI: .init( - payload: "2.0", - cors: true, - authorizers: + ) + let environment = try YAMLContent(with: [environmentTableName: "${self:custom.tableName}", + environmentKeyName: "${self:custom.keyName}"]) + let provider = Provider( + name: .aws, + region: region, + runtime: runtime, + environment: environment, + architecture: architecture, + httpAPI: .init( + payload: "2.0", + cors: true, + authorizers: .dictionary([ "JWTAuthorizer": .buildJWTAuthorizer(issuerUrl: "https://appleid.apple.com", - audience: ["com.mydomain.myhost"]), + audience: ["com.mydomain.myhost"]), "customAuthorizer": .buildCustomAuthorizer(name: "LambdaAuthorizer", functionName: "lambdaAuthorizer", identitySource: ["$request.header.SEC-X-API-KEY", "$request.header.User-Agent"]) ]) - ), - iam: iam + ), + iam: iam + ) + let custom = try YAMLContent(with: ["tableName": "\(dynamoDBTableNamePrefix)-table-${sls:stage}", + "keyName": dynamoDBKey]) + + let endpoints = [ + Endpoint(handler: "create", method: .post, path: httpAPIPath), + Endpoint(handler: "read", method: .get, path: keyedPath), + Endpoint(handler: "update", method: .put, path: httpAPIPath), + Endpoint(handler: "delete", method: .delete, path: keyedPath), + Endpoint(handler: "list", method: .get, path: httpAPIPath) + ] + var functions: [String: Function] = [:] + for endpoint in endpoints { + let function = try Function.httpApiLambda( + handler: "\(endpoint.handler)", + description: nil, + memorySize: memorySize, + runtime: nil, + package: nil, + event: .init(path: endpoint.path, method: endpoint.method) ) - let custom = try YAMLContent(with: ["tableName": "\(dynamoDBTableNamePrefix)-table-${sls:stage}", - "keyName": dynamoDBKey]) - - let endpoints = [ - Endpoint(handler: "create", method: .post, path: httpAPIPath), - Endpoint(handler: "read", method: .get, path: keyedPath), - Endpoint(handler: "update", method: .put, path: httpAPIPath), - Endpoint(handler: "delete", method: .delete, path: keyedPath), - Endpoint(handler: "list", method: .get, path: httpAPIPath) - ] - var functions: [String: Function] = [:] - for endpoint in endpoints { - let function = try Function.httpApiLambda( - handler: "\(endpoint.handler)", - description: nil, - memorySize: memorySize, - runtime: nil, - package: nil, - event: .init(path: endpoint.path, method: endpoint.method) - ) - functions["\(endpoint.handler)\(executable)"] = function - } - - let resource = Resource.dynamoDBResource(tableName: "${self:custom.tableName}", key: "${self:custom.keyName}") - let resources = Resources.resources(with: [dynamoResourceName: resource]) - - return ServerlessConfig( - service: service, - provider: provider, - package: .init(patterns: nil, individually: nil, artifact: artifact), - custom: custom, - layers: nil, - functions: functions, - resources: try YAMLContent(with: resources) + functions["\(endpoint.handler)\(executable)"] = function + } + + let resource = Resource.dynamoDBResource(tableName: "${self:custom.tableName}", key: "${self:custom.keyName}") + let resources = Resources.resources(with: [dynamoResourceName: resource]) + + return ServerlessConfig( + service: service, + provider: provider, + package: .init(patterns: nil, individually: nil, artifact: artifact), + custom: custom, + layers: nil, + functions: functions, + resources: try YAMLContent(with: resources) + ) + } + + static func webhookLambdaAPI( + service: String, + httpAPIPath: String, + region: Region, + runtime: Runtime, + architecture: Architecture, + memorySize: Int, + executable: String, + artifact: String + ) throws -> ServerlessConfig { + let iam = Iam( + role: Role( + statements: [.allowLogAccess(resource: try YAMLContent(with: "*"))] + ) + ) + let environment = try YAMLContent(with: ["WEBHOOK_SECRET": "${ssm:/dev/swift-webhook/webhook_secret}"]) + let provider = Provider( + name: .aws, + region: region, + runtime: runtime, + environment: environment, + architecture: architecture, + httpAPI: .init( + payload: "2.0", + cors: false + ), + iam: iam + ) + let endpoints = [ + Endpoint(handler: "postWebHook", method: .post, path: "/webhook"), + Endpoint(handler: "getWebHook", method: .get, path: "/webhook"), + Endpoint(handler: "githubWebHook", method: .post, path: "/github-webhook") + ] + var functions: [String: Function] = [:] + for endpoint in endpoints { + let function = try Function.httpApiLambda( + handler: "\(endpoint.handler)", + description: nil, + memorySize: memorySize, + environment: environment, + runtime: nil, + package: .init(patterns: nil, + individually: nil, + artifact: "build/GitHubWebHook/GitHubWebHook.zip"), + event: .init(path: endpoint.path, method: endpoint.method) ) + functions["\(endpoint.handler)\(executable)"] = function } + + return ServerlessConfig( + service: service, + provider: provider, + package: .init(patterns: nil, individually: nil, artifact: artifact), + custom: nil, + layers: nil, + functions: functions, + resources: nil + ) + } } diff --git a/Tests/SLSAdapterTests/ServerlessHttpAPILambdaTests.swift b/Tests/SLSAdapterTests/ServerlessHttpAPILambdaTests.swift new file mode 100644 index 0000000..155b60e --- /dev/null +++ b/Tests/SLSAdapterTests/ServerlessHttpAPILambdaTests.swift @@ -0,0 +1,120 @@ +/* + Copyright 2023 (c) Andrea Scuderi - https://github.com/swift-sprinter + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation +import SLSAdapter +import XCTest +import Yams + +final class ServerlessHttpAPILambdaTests: XCTestCase { + + enum TestError: Error { + case missingFixture + } + + func fixture(name: String, type: String) throws -> Data { + guard let fixtureUrl = Bundle.module.url(forResource: name, withExtension: type, subdirectory: "Fixtures") else { + throw TestError.missingFixture + } + return try Data(contentsOf: fixtureUrl) + } + + func testReadServerlessWebhook() throws { + let serverlessYml = try fixture(name: "serverless_webhook", type: "yml") + + let decoder = YAMLDecoder() + let serverlessConfig = try decoder.decode(ServerlessConfig.self, from: serverlessYml) + XCTAssertEqual(serverlessConfig.service, "swift-webhook") + XCTAssertEqual(serverlessConfig.frameworkVersion, "3") + XCTAssertEqual(serverlessConfig.configValidationMode, .warn) + XCTAssertEqual(serverlessConfig.useDotenv, false) + + XCTAssertEqual(serverlessConfig.package?.individually, true) + + XCTAssertNil(serverlessConfig.custom) + + let provider = serverlessConfig.provider + XCTAssertEqual(provider.name, .aws) + XCTAssertEqual(provider.region, .us_east_1) + + let httpAPI = try XCTUnwrap(provider.httpAPI) + XCTAssertEqual(httpAPI.payload, "2.0") + XCTAssertEqual(httpAPI.cors, false) + + XCTAssertEqual(provider.runtime, .providedAl2) + XCTAssertEqual(provider.architecture, .arm64) + + let iam = try XCTUnwrap(provider.iam) + + var role: Role? + if case .role(let value) = iam { + role = value + } + + XCTAssertEqual(role?.statements.count, 1) + + let statement1 = try XCTUnwrap(role?.statements.first) + let actionExpectation1 = [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ] + XCTAssertEqual(statement1.effect, "Allow") + XCTAssertEqual(statement1.action, actionExpectation1) + XCTAssertEqual(statement1.resource.value, "*") + + let postWebHook = try XCTUnwrap(serverlessConfig.functions?["postWebHook"]) + XCTAssertEqual(postWebHook.handler, "post-webhook") + XCTAssertEqual(postWebHook.memorySize, 256) + XCTAssertEqual(postWebHook.description, "[${sls:stage}] post /webhook") + let artifact = postWebHook.package?.artifact + XCTAssertEqual(artifact, "build/WebHook/WebHook.zip") + XCTAssertEqual(postWebHook.events.first?.httpAPI?.path, "/webhook") + XCTAssertEqual(postWebHook.events.first?.httpAPI?.method, .post) + + let getWebHook = try XCTUnwrap(serverlessConfig.functions?["getWebHook"]) + XCTAssertEqual(getWebHook.handler, "get-webhook") + XCTAssertEqual(getWebHook.memorySize, 256) + XCTAssertEqual(getWebHook.description, "[${sls:stage}] get /webhook") + let artifact2 = getWebHook.package?.artifact + XCTAssertEqual(artifact2, "build/WebHook/WebHook.zip") + XCTAssertEqual(getWebHook.events.first?.httpAPI?.path, "/webhook") + XCTAssertEqual(getWebHook.events.first?.httpAPI?.method, .get) + + let githubWebHook = try XCTUnwrap(serverlessConfig.functions?["githubWebHook"]) + XCTAssertEqual(githubWebHook.handler, "github-webhook") + XCTAssertEqual(githubWebHook.memorySize, 256) + XCTAssertEqual(githubWebHook.description, "[${sls:stage}] get /github-webhook") + let artifact3 = githubWebHook.package?.artifact + XCTAssertEqual(artifact3, "build/GitHubWebHook/GitHubWebHook.zip") + let environment = githubWebHook.environment?.dictionary + let webBookSecret = try XCTUnwrap(environment?["WEBHOOK_SECRET"]?.string) + XCTAssertEqual(webBookSecret, "${ssm:/dev/swift-webhook/webhook_secret}") + XCTAssertEqual(githubWebHook.events.first?.httpAPI?.path, "/github-webhook") + XCTAssertEqual(githubWebHook.events.first?.httpAPI?.method, .post) + } + + func testWriteServerlessWebook() throws { + let serverlessYml = try fixture(name: "serverless_webhook", type: "yml") + let decoder = YAMLDecoder() + let serverlessConfig = try decoder.decode(ServerlessConfig.self, from: serverlessYml) + let encoder = YAMLEncoder() + let content = try encoder.encode(serverlessConfig) + let data = try XCTUnwrap(content.data(using: .utf8)) + let serverlessConfig2 = try decoder.decode(ServerlessConfig.self, from: data) + XCTAssertEqual(serverlessConfig, serverlessConfig2) + } +}