From 592d257a5f76b6c071d209a15356f2d94848c1ce Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Wed, 25 Feb 2026 12:34:02 -0500 Subject: [PATCH 01/14] authorizer --- .../Diagnostics/DiagnosticDescriptors.cs | 65 ++ .../Generator.cs | 168 ++++ .../Models/AnnotationReport.cs | 5 + .../Attributes/HttpApiAttributeBuilder.cs | 6 +- .../HttpApiAuthorizerAttributeBuilder.cs | 78 ++ .../Attributes/RestApiAttributeBuilder.cs | 9 +- .../RestApiAuthorizerAttributeBuilder.cs | 76 ++ .../Models/AuthorizerModel.cs | 99 +++ .../Models/ILambdaFunctionSerializable.cs | 8 +- .../Models/LambdaFunctionModel.cs | 5 +- .../Models/LambdaFunctionModelBuilder.cs | 22 + .../TypeFullNames.cs | 3 + .../Writers/CloudFormationWriter.cs | 253 +++++- .../APIGateway/HttpApiAttribute.cs | 7 + .../APIGateway/HttpApiAuthorizerAttribute.cs | 69 ++ .../APIGateway/RestApiAttribute.cs | 7 + .../APIGateway/RestApiAuthorizerAttribute.cs | 76 ++ .../src/Amazon.Lambda.Annotations/README.md | 232 ++++- .../WriterTests/CloudFormationWriterTests.cs | 573 +++++++++++- .../CloudFormationHelper.cs | 35 + .../CommandLineWrapper.cs | 7 + .../HealthCheckTests.cs | 2 +- .../IntegrationTestContextFixture.cs | 61 +- .../AuthorizerFunction.cs | 15 +- .../ProtectedFunction.cs | 14 +- .../serverless.template | 837 +++++++----------- .../src/Function/Function.cs | 16 + .../src/Function/Function.csproj | 12 + .../src/Function/serverless.template | 389 ++++++++ .../serverless.template | 322 +++---- .../DeploymentScript.ps1 | 10 + .../IntegrationTestContextFixture.cs | 81 +- .../aws-lambda-tools-defaults.json | 8 +- .../TestServerlessApp/serverless.template | 550 ++++++------ 34 files changed, 3056 insertions(+), 1064 deletions(-) create mode 100644 Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/HttpApiAuthorizerAttributeBuilder.cs create mode 100644 Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/RestApiAuthorizerAttributeBuilder.cs create mode 100644 Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/AuthorizerModel.cs create mode 100644 Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpApiAuthorizerAttribute.cs create mode 100644 Libraries/src/Amazon.Lambda.Annotations/APIGateway/RestApiAuthorizerAttribute.cs create mode 100644 Libraries/test/TestCustomAuthorizerApp/src/Function/Function.cs create mode 100644 Libraries/test/TestCustomAuthorizerApp/src/Function/Function.csproj create mode 100644 Libraries/test/TestCustomAuthorizerApp/src/Function/serverless.template diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs index a606e5e88..69ecec8eb 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs @@ -153,5 +153,70 @@ public static class DiagnosticDescriptors category: "AWSLambdaCSharpGenerator", DiagnosticSeverity.Error, isEnabledByDefault: true); + + // Authorizer diagnostics (ALA0019-ALA0027 per design document) + public static readonly DiagnosticDescriptor AuthorizerMissingName = new DiagnosticDescriptor( + id: "AWSLambda0120", + title: "Authorizer Name Required", + messageFormat: "The Name property is required on [{0}] attribute.", + category: "AWSLambdaCSharpGenerator", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor HttpApiAuthorizerNotFound = new DiagnosticDescriptor( + id: "AWSLambda0121", + title: "HTTP API Authorizer Not Found", + messageFormat: "Authorizer '{0}' referenced in [HttpApi] attribute does not exist. Add [HttpApiAuthorizer(Name = \"{0}\")] to an authorizer function.", + category: "AWSLambdaCSharpGenerator", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor RestApiAuthorizerNotFound = new DiagnosticDescriptor( + id: "AWSLambda0122", + title: "REST API Authorizer Not Found", + messageFormat: "Authorizer '{0}' referenced in [RestApi] attribute does not exist. Add [RestApiAuthorizer(Name = \"{0}\")] to an authorizer function.", + category: "AWSLambdaCSharpGenerator", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor HttpApiAuthorizerTypeMismatch = new DiagnosticDescriptor( + id: "AWSLambda0123", + title: "Authorizer Type Mismatch", + messageFormat: "Cannot use REST API authorizer '{0}' with [HttpApi] attribute. Use an [HttpApiAuthorizer] instead.", + category: "AWSLambdaCSharpGenerator", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor RestApiAuthorizerTypeMismatch = new DiagnosticDescriptor( + id: "AWSLambda0124", + title: "Authorizer Type Mismatch", + messageFormat: "Cannot use HTTP API authorizer '{0}' with [RestApi] attribute. Use a [RestApiAuthorizer] instead.", + category: "AWSLambdaCSharpGenerator", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor DuplicateAuthorizerName = new DiagnosticDescriptor( + id: "AWSLambda0125", + title: "Duplicate Authorizer Name", + messageFormat: "Duplicate authorizer name '{0}'. Authorizer names must be unique within the same API type.", + category: "AWSLambdaCSharpGenerator", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor InvalidAuthorizerPayloadFormatVersion = new DiagnosticDescriptor( + id: "AWSLambda0126", + title: "Invalid Payload Format Version", + messageFormat: "Invalid PayloadFormatVersion '{0}'. Must be \"1.0\" or \"2.0\".", + category: "AWSLambdaCSharpGenerator", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor InvalidAuthorizerResultTtl = new DiagnosticDescriptor( + id: "AWSLambda0127", + title: "Invalid Result TTL", + messageFormat: "Invalid ResultTtlInSeconds '{0}'. Must be between 0 and 3600.", + category: "AWSLambdaCSharpGenerator", + DiagnosticSeverity.Error, + isEnabledByDefault: true); } } diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Generator.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Generator.cs index 3001b6144..097dd5278 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Generator.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Generator.cs @@ -1,7 +1,9 @@ +using Amazon.Lambda.Annotations.APIGateway; using Amazon.Lambda.Annotations.SourceGenerator.Diagnostics; using Amazon.Lambda.Annotations.SourceGenerator.Extensions; using Amazon.Lambda.Annotations.SourceGenerator.FileIO; using Amazon.Lambda.Annotations.SourceGenerator.Models; +using Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes; using Amazon.Lambda.Annotations.SourceGenerator.Templates; using Amazon.Lambda.Annotations.SourceGenerator.Writers; using Microsoft.CodeAnalysis; @@ -168,6 +170,13 @@ public void Execute(GeneratorExecutionContext context) continue; } + // Check for authorizer attributes on this Lambda function + var authorizerModel = ExtractAuthorizerModel(lambdaMethodSymbol, lambdaFunctionModel.ResourceName); + if (authorizerModel != null) + { + annotationReport.Authorizers.Add(authorizerModel); + } + var template = new LambdaFunctionTemplate(lambdaFunctionModel); string sourceText; @@ -296,5 +305,164 @@ public void Initialize(GeneratorInitializationContext context) // Register a syntax receiver that will be created for each generation pass context.RegisterForSyntaxNotifications(() => new SyntaxReceiver(_fileManager, _directoryManager)); } + + /// + /// Extracts authorizer model from method symbol if it has HttpApiAuthorizer or RestApiAuthorizer attribute. + /// + /// The method symbol to check for authorizer attributes + /// The CloudFormation resource name for the Lambda function + /// AuthorizerModel if an authorizer attribute is found, null otherwise + private static AuthorizerModel ExtractAuthorizerModel(IMethodSymbol methodSymbol, string lambdaResourceName) + { + foreach (var attribute in methodSymbol.GetAttributes()) + { + var attributeFullName = attribute.AttributeClass?.ToDisplayString(); + + if (attributeFullName == TypeFullNames.HttpApiAuthorizerAttribute) + { + return HttpApiAuthorizerAttributeBuilder.BuildModel(attribute, lambdaResourceName); + } + + if (attributeFullName == TypeFullNames.RestApiAuthorizerAttribute) + { + return RestApiAuthorizerAttributeBuilder.BuildModel(attribute, lambdaResourceName); + } + } + + return null; + } + + /// + /// Validates an authorizer model. + /// + /// The authorizer model to validate + /// The name of the attribute for error messages + /// The location of the method for diagnostic reporting + /// The diagnostic reporter for validation errors + /// True if valid, false otherwise + private static bool ValidateAuthorizerModel(AuthorizerModel model, string attributeName, Location methodLocation, DiagnosticReporter diagnosticReporter) + { + var isValid = true; + + // Validate Name is provided + if (string.IsNullOrEmpty(model.Name)) + { + diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.AuthorizerMissingName, methodLocation, attributeName)); + isValid = false; + } + + // Validate PayloadFormatVersion for HTTP API authorizers + if (model.AuthorizerType == AuthorizerType.HttpApi) + { + if (model.PayloadFormatVersion != "1.0" && model.PayloadFormatVersion != "2.0") + { + diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.InvalidAuthorizerPayloadFormatVersion, methodLocation, model.PayloadFormatVersion)); + isValid = false; + } + } + + // Validate ResultTtlInSeconds + if (model.ResultTtlInSeconds < 0 || model.ResultTtlInSeconds > 3600) + { + diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.InvalidAuthorizerResultTtl, methodLocation, model.ResultTtlInSeconds.ToString())); + isValid = false; + } + + return isValid; + } + + /// + /// Validates authorizer references in lambda functions. + /// + /// The annotation report containing all functions and authorizers + /// The diagnostic reporter for validation errors + /// True if all authorizer references are valid, false otherwise + private static bool ValidateAuthorizerReferences(AnnotationReport annotationReport, DiagnosticReporter diagnosticReporter) + { + var isValid = true; + + // Build lookups for authorizers by type + var httpApiAuthorizers = annotationReport.Authorizers + .Where(a => a.AuthorizerType == AuthorizerType.HttpApi) + .ToDictionary(a => a.Name, a => a); + var restApiAuthorizers = annotationReport.Authorizers + .Where(a => a.AuthorizerType == AuthorizerType.RestApi) + .ToDictionary(a => a.Name, a => a); + + // Check for duplicate authorizer names within the same API type + var httpApiAuthorizerNames = annotationReport.Authorizers + .Where(a => a.AuthorizerType == AuthorizerType.HttpApi) + .GroupBy(a => a.Name) + .Where(g => g.Count() > 1) + .Select(g => g.Key); + + foreach (var duplicateName in httpApiAuthorizerNames) + { + diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.DuplicateAuthorizerName, Location.None, duplicateName)); + isValid = false; + } + + var restApiAuthorizerNames = annotationReport.Authorizers + .Where(a => a.AuthorizerType == AuthorizerType.RestApi) + .GroupBy(a => a.Name) + .Where(g => g.Count() > 1) + .Select(g => g.Key); + + foreach (var duplicateName in restApiAuthorizerNames) + { + diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.DuplicateAuthorizerName, Location.None, duplicateName)); + isValid = false; + } + + // Validate authorizer references in functions + foreach (var function in annotationReport.LambdaFunctions) + { + var authorizerName = function.Authorizer; + if (string.IsNullOrEmpty(authorizerName)) + { + continue; + } + + // Check if this function uses HttpApi or RestApi + var usesHttpApi = function.Attributes.Any(a => a is AttributeModel); + var usesRestApi = function.Attributes.Any(a => a is AttributeModel); + + if (usesHttpApi) + { + if (!httpApiAuthorizers.ContainsKey(authorizerName)) + { + // Check if it exists as a REST API authorizer (type mismatch) + if (restApiAuthorizers.ContainsKey(authorizerName)) + { + diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.HttpApiAuthorizerTypeMismatch, Location.None, authorizerName)); + } + else + { + diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.HttpApiAuthorizerNotFound, Location.None, authorizerName)); + } + isValid = false; + } + } + + if (usesRestApi) + { + if (!restApiAuthorizers.ContainsKey(authorizerName)) + { + // Check if it exists as an HTTP API authorizer (type mismatch) + if (httpApiAuthorizers.ContainsKey(authorizerName)) + { + diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.RestApiAuthorizerTypeMismatch, Location.None, authorizerName)); + } + else + { + diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.RestApiAuthorizerNotFound, Location.None, authorizerName)); + } + isValid = false; + } + } + } + + return isValid; + } } } diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/AnnotationReport.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/AnnotationReport.cs index dbd76b458..419817a14 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/AnnotationReport.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/AnnotationReport.cs @@ -9,6 +9,11 @@ public class AnnotationReport /// public IList LambdaFunctions { get; } = new List(); + /// + /// Collection of Lambda authorizers detected in the project + /// + public IList Authorizers { get; } = new List(); + /// /// Path to the CloudFormation template for the Lambda project /// diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/HttpApiAttributeBuilder.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/HttpApiAttributeBuilder.cs index 15a6767bf..d21abd928 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/HttpApiAttributeBuilder.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/HttpApiAttributeBuilder.cs @@ -17,12 +17,14 @@ public static HttpApiAttribute Build(AttributeData att) var method = (LambdaHttpMethod)att.ConstructorArguments[0].Value; var template = att.ConstructorArguments[1].Value as string; var version = att.NamedArguments.FirstOrDefault(arg => arg.Key == "Version").Value.Value; + var authorizer = att.NamedArguments.FirstOrDefault(arg => arg.Key == "Authorizer").Value.Value as string; var data = new HttpApiAttribute(method, template) { - Version = version == null ? HttpApiVersion.V2 : (HttpApiVersion)version + Version = version == null ? HttpApiVersion.V2 : (HttpApiVersion)version, + Authorizer = authorizer }; return data; } } -} \ No newline at end of file +} diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/HttpApiAuthorizerAttributeBuilder.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/HttpApiAuthorizerAttributeBuilder.cs new file mode 100644 index 000000000..8df840490 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/HttpApiAuthorizerAttributeBuilder.cs @@ -0,0 +1,78 @@ +using System.Linq; +using Amazon.Lambda.Annotations.APIGateway; +using Microsoft.CodeAnalysis; + +namespace Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes +{ + /// + /// Builder for . + /// + public static class HttpApiAuthorizerAttributeBuilder + { + /// + /// Builds an from the Roslyn attribute data. + /// + /// The attribute data from Roslyn + /// The populated attribute instance + public static HttpApiAuthorizerAttribute Build(AttributeData att) + { + var attribute = new HttpApiAuthorizerAttribute(); + + foreach (var namedArg in att.NamedArguments) + { + switch (namedArg.Key) + { + case nameof(HttpApiAuthorizerAttribute.Name): + attribute.Name = namedArg.Value.Value as string; + break; + case nameof(HttpApiAuthorizerAttribute.IdentityHeader): + attribute.IdentityHeader = namedArg.Value.Value as string ?? "Authorization"; + break; + case nameof(HttpApiAuthorizerAttribute.EnableSimpleResponses): + attribute.EnableSimpleResponses = namedArg.Value.Value is bool val ? val : true; + break; + case nameof(HttpApiAuthorizerAttribute.PayloadFormatVersion): + attribute.PayloadFormatVersion = namedArg.Value.Value as string ?? "2.0"; + break; + case nameof(HttpApiAuthorizerAttribute.ResultTtlInSeconds): + attribute.ResultTtlInSeconds = namedArg.Value.Value is int ttl ? ttl : 0; + break; + } + } + + return attribute; + } + + /// + /// Builds an from the attribute and lambda function resource name. + /// + /// The attribute data from Roslyn + /// The CloudFormation resource name for the Lambda function + /// The populated authorizer model + public static AuthorizerModel BuildModel(AttributeData att, string lambdaResourceName) + { + var attribute = Build(att); + return BuildModel(attribute, lambdaResourceName); + } + + /// + /// Builds an from the attribute and lambda function resource name. + /// + /// The parsed attribute + /// The CloudFormation resource name for the Lambda function + /// The populated authorizer model + public static AuthorizerModel BuildModel(HttpApiAuthorizerAttribute attribute, string lambdaResourceName) + { + return new AuthorizerModel + { + Name = attribute.Name, + LambdaResourceName = lambdaResourceName, + AuthorizerType = AuthorizerType.HttpApi, + IdentityHeader = attribute.IdentityHeader, + ResultTtlInSeconds = attribute.ResultTtlInSeconds, + EnableSimpleResponses = attribute.EnableSimpleResponses, + PayloadFormatVersion = attribute.PayloadFormatVersion + }; + } + } +} \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/RestApiAttributeBuilder.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/RestApiAttributeBuilder.cs index 1a9f44680..44ab96a10 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/RestApiAttributeBuilder.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/RestApiAttributeBuilder.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using Amazon.Lambda.Annotations.APIGateway; using Microsoft.CodeAnalysis; @@ -18,10 +19,14 @@ public static RestApiAttribute Build(AttributeData att) var method = (LambdaHttpMethod)att.ConstructorArguments[0].Value; var template = att.ConstructorArguments[1].Value as string; + var authorizer = att.NamedArguments.FirstOrDefault(arg => arg.Key == "Authorizer").Value.Value as string; - var data = new RestApiAttribute(method, template); + var data = new RestApiAttribute(method, template) + { + Authorizer = authorizer + }; return data; } } -} \ No newline at end of file +} diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/RestApiAuthorizerAttributeBuilder.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/RestApiAuthorizerAttributeBuilder.cs new file mode 100644 index 000000000..b981c6ed0 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/RestApiAuthorizerAttributeBuilder.cs @@ -0,0 +1,76 @@ +using System.Linq; +using Amazon.Lambda.Annotations.APIGateway; +using Microsoft.CodeAnalysis; + +namespace Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes +{ + /// + /// Builder for . + /// + public static class RestApiAuthorizerAttributeBuilder + { + /// + /// Builds a from the Roslyn attribute data. + /// + /// The attribute data from Roslyn + /// The populated attribute instance + public static RestApiAuthorizerAttribute Build(AttributeData att) + { + var attribute = new RestApiAuthorizerAttribute(); + + foreach (var namedArg in att.NamedArguments) + { + switch (namedArg.Key) + { + case nameof(RestApiAuthorizerAttribute.Name): + attribute.Name = namedArg.Value.Value as string; + break; + case nameof(RestApiAuthorizerAttribute.IdentityHeader): + attribute.IdentityHeader = namedArg.Value.Value as string ?? "Authorization"; + break; + case nameof(RestApiAuthorizerAttribute.Type): + attribute.Type = namedArg.Value.Value is int typeVal + ? (RestApiAuthorizerType)typeVal + : RestApiAuthorizerType.Token; + break; + case nameof(RestApiAuthorizerAttribute.ResultTtlInSeconds): + attribute.ResultTtlInSeconds = namedArg.Value.Value is int ttl ? ttl : 0; + break; + } + } + + return attribute; + } + + /// + /// Builds an from the attribute and lambda function resource name. + /// + /// The attribute data from Roslyn + /// The CloudFormation resource name for the Lambda function + /// The populated authorizer model + public static AuthorizerModel BuildModel(AttributeData att, string lambdaResourceName) + { + var attribute = Build(att); + return BuildModel(attribute, lambdaResourceName); + } + + /// + /// Builds an from the attribute and lambda function resource name. + /// + /// The parsed attribute + /// The CloudFormation resource name for the Lambda function + /// The populated authorizer model + public static AuthorizerModel BuildModel(RestApiAuthorizerAttribute attribute, string lambdaResourceName) + { + return new AuthorizerModel + { + Name = attribute.Name, + LambdaResourceName = lambdaResourceName, + AuthorizerType = AuthorizerType.RestApi, + IdentityHeader = attribute.IdentityHeader, + ResultTtlInSeconds = attribute.ResultTtlInSeconds, + RestApiAuthorizerType = attribute.Type + }; + } + } +} \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/AuthorizerModel.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/AuthorizerModel.cs new file mode 100644 index 000000000..645279b21 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/AuthorizerModel.cs @@ -0,0 +1,99 @@ +using Amazon.Lambda.Annotations.APIGateway; + +namespace Amazon.Lambda.Annotations.SourceGenerator.Models +{ + /// + /// Enumeration for the type of API Gateway authorizer + /// + public enum AuthorizerType + { + /// + /// HTTP API (API Gateway V2) authorizer + /// + HttpApi, + + /// + /// REST API (API Gateway V1) authorizer + /// + RestApi + } + + /// + /// Model representing a Lambda Authorizer configuration + /// + public class AuthorizerModel + { + /// + /// Unique name to identify this authorizer. Functions reference this name. + /// + public string Name { get; set; } + + /// + /// The CloudFormation resource name for the Lambda function that implements this authorizer. + /// This is derived from the LambdaFunctionAttribute's ResourceName or the generated method name. + /// + public string LambdaResourceName { get; set; } + + /// + /// The type of API Gateway authorizer (HTTP API or REST API) + /// + public AuthorizerType AuthorizerType { get; set; } + + /// + /// Header name to use as identity source. + /// + public string IdentityHeader { get; set; } + + /// + /// TTL in seconds for caching authorizer results. + /// + public int ResultTtlInSeconds { get; set; } + + // HTTP API specific properties + + /// + /// Whether to use simple responses (IsAuthorized: true/false) or IAM policy responses. + /// Only applicable for HTTP API authorizers. + /// + public bool EnableSimpleResponses { get; set; } + + /// + /// Authorizer payload format version. Valid values: "1.0" or "2.0". + /// Only applicable for HTTP API authorizers. + /// + public string PayloadFormatVersion { get; set; } + + // REST API specific properties + + /// + /// Type of REST API authorizer: Token or Request. + /// Only applicable for REST API authorizers. + /// + public RestApiAuthorizerType RestApiAuthorizerType { get; set; } + + /// + /// Gets the identity source string formatted for CloudFormation. + /// + /// The formatted identity source string + public string GetIdentitySource() + { + if (AuthorizerType == AuthorizerType.HttpApi) + { + return $"$request.header.{IdentityHeader}"; + } + else + { + return $"method.request.header.{IdentityHeader}"; + } + } + + /// + /// Gets the CloudFormation resource name for this authorizer. + /// + /// The CloudFormation resource name + public string GetAuthorizerResourceName() + { + return $"{Name}Authorizer"; + } + } +} \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/ILambdaFunctionSerializable.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/ILambdaFunctionSerializable.cs index 6cb6161fa..d0b7fd20f 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/ILambdaFunctionSerializable.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/ILambdaFunctionSerializable.cs @@ -77,5 +77,11 @@ public interface ILambdaFunctionSerializable /// The assembly version of the Amazon.Lambda.Annotations.SourceGenerator package. /// string SourceGeneratorVersion { get; set; } + + /// + /// The name of the authorizer protecting this Lambda function endpoint. + /// Null or empty for public (unauthenticated) endpoints. + /// + string Authorizer { get; } } -} \ No newline at end of file +} diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/LambdaFunctionModel.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/LambdaFunctionModel.cs index fad1f5f53..fd47d2dab 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/LambdaFunctionModel.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/LambdaFunctionModel.cs @@ -78,9 +78,12 @@ public class LambdaFunctionModel : ILambdaFunctionSerializable /// public string SourceGeneratorVersion { get; set; } + /// + public string Authorizer { get; set; } + /// /// Indicates if the model is valid. /// public bool IsValid { get; set; } } -} \ No newline at end of file +} diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/LambdaFunctionModelBuilder.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/LambdaFunctionModelBuilder.cs index e828bee3d..db7fa4d67 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/LambdaFunctionModelBuilder.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/LambdaFunctionModelBuilder.cs @@ -1,7 +1,9 @@ using System; using System.Linq; +using Amazon.Lambda.Annotations.APIGateway; using Amazon.Lambda.Annotations.SourceGenerator.Diagnostics; using Amazon.Lambda.Annotations.SourceGenerator.Extensions; +using Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes; using Amazon.Lambda.Annotations.SourceGenerator.Validation; using Microsoft.CodeAnalysis; @@ -40,11 +42,31 @@ private static LambdaFunctionModel Build(IMethodSymbol lambdaMethodSymbol, IMeth ?.Version.ToString(), IsExecutable = isExecutable, Runtime = runtime, + Authorizer = GetAuthorizerFromAttributes(lambdaMethod) }; return model; } + /// + /// Extracts the Authorizer name from HttpApi or RestApi attributes. + /// + private static string GetAuthorizerFromAttributes(LambdaMethodModel lambdaMethod) + { + foreach (var attribute in lambdaMethod.Attributes) + { + if (attribute is AttributeModel httpApiAttributeModel) + { + return httpApiAttributeModel.Data.Authorizer; + } + if (attribute is AttributeModel restApiAttributeModel) + { + return restApiAttributeModel.Data.Authorizer; + } + } + return null; + } + private static LambdaSerializerInfo GetSerializerInfoAttribute(GeneratorExecutionContext context, IMethodSymbol methodModel) { var serializerString = TypeFullNames.DefaultLambdaSerializer; diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs index a91cfaefa..56582a2e7 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs @@ -33,6 +33,9 @@ public static class TypeFullNames public const string FromRouteAttribute = "Amazon.Lambda.Annotations.APIGateway.FromRouteAttribute"; public const string FromCustomAuthorizerAttribute = "Amazon.Lambda.Annotations.APIGateway.FromCustomAuthorizerAttribute"; + public const string HttpApiAuthorizerAttribute = "Amazon.Lambda.Annotations.APIGateway.HttpApiAuthorizerAttribute"; + public const string RestApiAuthorizerAttribute = "Amazon.Lambda.Annotations.APIGateway.RestApiAuthorizerAttribute"; + public const string SQSEvent = "Amazon.Lambda.SQSEvents.SQSEvent"; public const string SQSBatchResponse = "Amazon.Lambda.SQSEvents.SQSBatchResponse"; public const string SQSEventAttribute = "Amazon.Lambda.Annotations.SQS.SQSEventAttribute"; diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs index 993a312ed..e01e1a65c 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs @@ -10,6 +10,7 @@ using System.ComponentModel; using System.Linq; using System.Reflection; +using AuthorizerType = Amazon.Lambda.Annotations.SourceGenerator.Models.AuthorizerType; namespace Amazon.Lambda.Annotations.SourceGenerator.Writers { @@ -25,6 +26,8 @@ public class CloudFormationWriter : IAnnotationReportWriter private const string PARAMETERS = "Parameters"; private const string GET_ATTRIBUTE = "Fn::GetAtt"; private const string REF = "Ref"; + private const string HTTP_API_RESOURCE_NAME = "AnnotationsHttpApi"; + private const string REST_API_RESOURCE_NAME = "AnnotationsRestApi"; // Constants related to the message we append to the CloudFormation template description private const string BASE_DESCRIPTION = "This template is partially managed by Amazon.Lambda.Annotations"; @@ -59,17 +62,24 @@ public void ApplyReport(AnnotationReport report) ProcessTemplateDescription(report); + // Build authorizer lookup for processing events with Auth configuration + var authorizerLookup = report.Authorizers.ToDictionary(a => a.Name, a => a); + + // Process authorizers first (they need to exist before functions reference them) + ProcessAuthorizers(report.Authorizers); + var processedLambdaFunctions = new HashSet(); foreach (var lambdaFunction in report.LambdaFunctions) { if (!ShouldProcessLambdaFunction(lambdaFunction)) continue; - ProcessLambdaFunction(lambdaFunction, relativeProjectUri); + ProcessLambdaFunction(lambdaFunction, relativeProjectUri, authorizerLookup); processedLambdaFunctions.Add(lambdaFunction.ResourceName); } RemoveOrphanedLambdaFunctions(processedLambdaFunctions); + RemoveOrphanedAuthorizers(report.Authorizers); var content = _templateWriter.GetContent(); _fileManager.WriteAllText(report.CloudFormationTemplatePath, content); @@ -98,7 +108,7 @@ private bool ShouldProcessLambdaFunction(ILambdaFunctionSerializable lambdaFunct /// Captures different properties specified by and attributes specified by /// and writes it to the serverless template. /// - private void ProcessLambdaFunction(ILambdaFunctionSerializable lambdaFunction, string relativeProjectUri) + private void ProcessLambdaFunction(ILambdaFunctionSerializable lambdaFunction, string relativeProjectUri, Dictionary authorizerLookup) { var lambdaFunctionPath = $"Resources.{lambdaFunction.ResourceName}"; var propertiesPath = $"{lambdaFunctionPath}.Properties"; @@ -107,7 +117,7 @@ private void ProcessLambdaFunction(ILambdaFunctionSerializable lambdaFunction, s ApplyLambdaFunctionDefaults(lambdaFunctionPath, propertiesPath, lambdaFunction.Runtime); ProcessLambdaFunctionProperties(lambdaFunction, propertiesPath, relativeProjectUri); - ProcessLambdaFunctionEventAttributes(lambdaFunction); + ProcessLambdaFunctionEventAttributes(lambdaFunction, authorizerLookup); } /// @@ -180,7 +190,7 @@ private void ProcessPackageTypeProperty(ILambdaFunctionSerializable lambdaFuncti /// It also removes all events that exist in the serverless template but were not encountered during the current source generation pass. /// All events are specified under 'Resources.FUNCTION_NAME.Properties.Events' path. /// - private void ProcessLambdaFunctionEventAttributes(ILambdaFunctionSerializable lambdaFunction) + private void ProcessLambdaFunctionEventAttributes(ILambdaFunctionSerializable lambdaFunction, Dictionary authorizerLookup) { var currentSyncedEvents = new List(); var currentSyncedEventProperties = new Dictionary>(); @@ -191,11 +201,11 @@ private void ProcessLambdaFunctionEventAttributes(ILambdaFunctionSerializable la switch (attributeModel) { case AttributeModel httpApiAttributeModel: - eventName = ProcessHttpApiAttribute(lambdaFunction, httpApiAttributeModel.Data, currentSyncedEventProperties); + eventName = ProcessHttpApiAttribute(lambdaFunction, httpApiAttributeModel.Data, currentSyncedEventProperties, authorizerLookup); currentSyncedEvents.Add(eventName); break; case AttributeModel restApiAttributeModel: - eventName = ProcessRestApiAttribute(lambdaFunction, restApiAttributeModel.Data, currentSyncedEventProperties); + eventName = ProcessRestApiAttribute(lambdaFunction, restApiAttributeModel.Data, currentSyncedEventProperties, authorizerLookup); currentSyncedEvents.Add(eventName); break; case AttributeModel sqsAttributeModel: @@ -211,7 +221,7 @@ private void ProcessLambdaFunctionEventAttributes(ILambdaFunctionSerializable la /// /// Writes all properties associated with to the serverless template. /// - private string ProcessRestApiAttribute(ILambdaFunctionSerializable lambdaFunction, RestApiAttribute restApiAttribute, Dictionary> syncedEventProperties) + private string ProcessRestApiAttribute(ILambdaFunctionSerializable lambdaFunction, RestApiAttribute restApiAttribute, Dictionary> syncedEventProperties, Dictionary authorizerLookup) { var eventName = $"Root{restApiAttribute.Method}"; var eventPath = $"Resources.{lambdaFunction.ResourceName}.Properties.Events.{eventName}"; @@ -220,13 +230,22 @@ private string ProcessRestApiAttribute(ILambdaFunctionSerializable lambdaFunctio SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Path", restApiAttribute.Template); SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Method", restApiAttribute.Method.ToString().ToUpper()); + // Set Auth configuration if authorizer is specified + // Use the authorizer name directly (not a CloudFormation Ref) since authorizers are defined inline in the API + // Also set RestApiId to link to our explicit ServerlessRestApi resource where the authorizer is defined + if (!string.IsNullOrEmpty(restApiAttribute.Authorizer) && authorizerLookup.TryGetValue(restApiAttribute.Authorizer, out var authorizer)) + { + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Auth.Authorizer", authorizer.Name); + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, $"RestApiId.{REF}", REST_API_RESOURCE_NAME); + } + return eventName; } /// /// Writes all properties associated with to the serverless template. /// - private string ProcessHttpApiAttribute(ILambdaFunctionSerializable lambdaFunction, HttpApiAttribute httpApiAttribute, Dictionary> syncedEventProperties) + private string ProcessHttpApiAttribute(ILambdaFunctionSerializable lambdaFunction, HttpApiAttribute httpApiAttribute, Dictionary> syncedEventProperties, Dictionary authorizerLookup) { var eventName = $"Root{httpApiAttribute.Method}"; var eventPath = $"Resources.{lambdaFunction.ResourceName}.Properties.Events.{eventName}"; @@ -240,9 +259,227 @@ private string ProcessHttpApiAttribute(ILambdaFunctionSerializable lambdaFunctio if (httpApiAttribute.Version == HttpApiVersion.V1) SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "PayloadFormatVersion", "1.0"); + // Set Auth configuration if authorizer is specified + // Use the authorizer name directly (not a CloudFormation Ref) since authorizers are defined inline in the API + // Also set ApiId to link to our explicit ServerlessHttpApi resource where the authorizer is defined + if (!string.IsNullOrEmpty(httpApiAttribute.Authorizer) && authorizerLookup.TryGetValue(httpApiAttribute.Authorizer, out var authorizer)) + { + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Auth.Authorizer", authorizer.Name); + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, $"ApiId.{REF}", HTTP_API_RESOURCE_NAME); + } + return eventName; } + /// + /// Processes all authorizers and writes them to the serverless template as inline authorizers within the API resources. + /// AWS SAM expects authorizers to be defined within the Auth.Authorizers property of AWS::Serverless::HttpApi or AWS::Serverless::Api resources. + /// + private void ProcessAuthorizers(IList authorizers) + { + // Group authorizers by type + var httpApiAuthorizers = authorizers.Where(a => a.AuthorizerType == AuthorizerType.HttpApi).ToList(); + var restApiAuthorizers = authorizers.Where(a => a.AuthorizerType == AuthorizerType.RestApi).ToList(); + + // Process HTTP API authorizers (add to AnnotationsHttpApi resource) + if (httpApiAuthorizers.Any()) + { + ProcessHttpApiAuthorizers(httpApiAuthorizers); + } + + // Process REST API authorizers (add to AnnotationsRestApi resource) + if (restApiAuthorizers.Any()) + { + ProcessRestApiAuthorizers(restApiAuthorizers); + } + } + + /// + /// Writes HTTP API (API Gateway V2) authorizers to the AnnotationsHttpApi resource. + /// SAM expects authorizers to be defined inline in the Auth.Authorizers property. + /// + private void ProcessHttpApiAuthorizers(IList authorizers) + { + const string httpApiResourcePath = "Resources." + HTTP_API_RESOURCE_NAME; + + // Create the AnnotationsHttpApi resource if it doesn't exist + if (!_templateWriter.Exists(httpApiResourcePath)) + { + _templateWriter.SetToken($"{httpApiResourcePath}.Type", "AWS::Serverless::HttpApi"); + } + + _templateWriter.SetToken($"{httpApiResourcePath}.Metadata.Tool", CREATION_TOOL); + + // Add each authorizer to the Auth.Authorizers map + foreach (var authorizer in authorizers) + { + var authorizerPath = $"{httpApiResourcePath}.Properties.Auth.Authorizers.{authorizer.Name}"; + + // FunctionArn - Reference to the Lambda function ARN + _templateWriter.SetToken($"{authorizerPath}.FunctionArn.{GET_ATTRIBUTE}", new List { authorizer.LambdaResourceName, "Arn" }, TokenType.List); + + // AuthorizerPayloadFormatVersion + _templateWriter.SetToken($"{authorizerPath}.AuthorizerPayloadFormatVersion", authorizer.PayloadFormatVersion); + + // EnableSimpleResponses + _templateWriter.SetToken($"{authorizerPath}.EnableSimpleResponses", authorizer.EnableSimpleResponses); + + // Identity.Headers - The header to use for identity source + _templateWriter.SetToken($"{authorizerPath}.Identity.Headers", new List { authorizer.IdentityHeader }, TokenType.List); + + // EnableFunctionDefaultPermissions tells SAM to automatically create the AWS::Lambda::Permission + // for the authorizer Lambda function. Unlike AWS::Serverless::Api (REST API), AWS::Serverless::HttpApi + // does NOT automatically create invoke permissions for Lambda authorizers defined in Auth.Authorizers. + // https://github.com/aws/serverless-application-model/issues/2933 + _templateWriter.SetToken($"{authorizerPath}.EnableFunctionDefaultPermissions", true); + + // AuthorizerResultTtlInSeconds (only if caching is enabled) + if (authorizer.ResultTtlInSeconds > 0) + { + _templateWriter.SetToken($"{authorizerPath}.FunctionInvokeRole", null); // Required for caching + } + } + } + + /// + /// Writes REST API (API Gateway V1) authorizers to the AnnotationsRestApi resource. + /// SAM expects authorizers to be defined inline in the Auth.Authorizers property. + /// + private void ProcessRestApiAuthorizers(IList authorizers) + { + const string restApiResourcePath = "Resources." + REST_API_RESOURCE_NAME; + + // Create the AnnotationsRestApi resource if it doesn't exist + if (!_templateWriter.Exists(restApiResourcePath)) + { + _templateWriter.SetToken($"{restApiResourcePath}.Type", "AWS::Serverless::Api"); + // REST API requires explicit stage name + _templateWriter.SetToken($"{restApiResourcePath}.Properties.StageName", "Prod"); + } + + _templateWriter.SetToken($"{restApiResourcePath}.Metadata.Tool", CREATION_TOOL); + + // Add each authorizer to the Auth.Authorizers map + foreach (var authorizer in authorizers) + { + var authorizerPath = $"{restApiResourcePath}.Properties.Auth.Authorizers.{authorizer.Name}"; + + // FunctionArn - Reference to the Lambda function ARN using GetAtt + _templateWriter.SetToken($"{authorizerPath}.FunctionArn.{GET_ATTRIBUTE}", new List { authorizer.LambdaResourceName, "Arn" }, TokenType.List); + + // Identity.Header - The header to use for identity source + _templateWriter.SetToken($"{authorizerPath}.Identity.Header", authorizer.IdentityHeader); + + // FunctionPayloadType - TOKEN or REQUEST + if (authorizer.RestApiAuthorizerType == RestApiAuthorizerType.Token) + { + _templateWriter.SetToken($"{authorizerPath}.FunctionPayloadType", "TOKEN"); + } + else + { + _templateWriter.SetToken($"{authorizerPath}.FunctionPayloadType", "REQUEST"); + } + } + } + + /// + /// Removes orphaned authorizers from the serverless template. + /// Authorizers are now defined inline within the API resources (AnnotationsHttpApi and AnnotationsRestApi). + /// This method removes authorizers that were created by Lambda Annotations but no longer exist in the current compilation. + /// It also cleans up legacy standalone authorizer resources (AWS::ApiGatewayV2::Authorizer, AWS::ApiGateway::Authorizer) + /// and their associated Lambda permissions. + /// + private void RemoveOrphanedAuthorizers(IList currentAuthorizers) + { + if (!_templateWriter.Exists("Resources")) + { + return; + } + + // Get current authorizer names by type + var currentHttpApiAuthorizerNames = new HashSet( + currentAuthorizers.Where(a => a.AuthorizerType == AuthorizerType.HttpApi).Select(a => a.Name)); + var currentRestApiAuthorizerNames = new HashSet( + currentAuthorizers.Where(a => a.AuthorizerType == AuthorizerType.RestApi).Select(a => a.Name)); + + // Clean up orphaned inline authorizers in AnnotationsHttpApi + const string httpApiAuthorizersPath = "Resources." + HTTP_API_RESOURCE_NAME + ".Properties.Auth.Authorizers"; + if (_templateWriter.Exists(httpApiAuthorizersPath)) + { + var httpApiCreationTool = _templateWriter.GetToken($"Resources.{HTTP_API_RESOURCE_NAME}.Metadata.Tool", string.Empty); + if (string.Equals(httpApiCreationTool, CREATION_TOOL, StringComparison.Ordinal)) + { + var existingAuthorizerNames = _templateWriter.GetKeys(httpApiAuthorizersPath); + foreach (var authorizerName in existingAuthorizerNames) + { + if (!currentHttpApiAuthorizerNames.Contains(authorizerName)) + { + _templateWriter.RemoveToken($"{httpApiAuthorizersPath}.{authorizerName}"); + } + } + + // Clean up empty Auth structure + _templateWriter.RemoveTokenIfNullOrEmpty(httpApiAuthorizersPath); + _templateWriter.RemoveTokenIfNullOrEmpty($"Resources.{HTTP_API_RESOURCE_NAME}.Properties.Auth"); + } + } + + // Clean up orphaned inline authorizers in AnnotationsRestApi + const string restApiAuthorizersPath = "Resources." + REST_API_RESOURCE_NAME + ".Properties.Auth.Authorizers"; + if (_templateWriter.Exists(restApiAuthorizersPath)) + { + var restApiCreationTool = _templateWriter.GetToken($"Resources.{REST_API_RESOURCE_NAME}.Metadata.Tool", string.Empty); + if (string.Equals(restApiCreationTool, CREATION_TOOL, StringComparison.Ordinal)) + { + var existingAuthorizerNames = _templateWriter.GetKeys(restApiAuthorizersPath); + foreach (var authorizerName in existingAuthorizerNames) + { + if (!currentRestApiAuthorizerNames.Contains(authorizerName)) + { + _templateWriter.RemoveToken($"{restApiAuthorizersPath}.{authorizerName}"); + } + } + + // Clean up empty Auth structure + _templateWriter.RemoveTokenIfNullOrEmpty(restApiAuthorizersPath); + _templateWriter.RemoveTokenIfNullOrEmpty($"Resources.{REST_API_RESOURCE_NAME}.Properties.Auth"); + } + } + + // Clean up legacy standalone authorizer resources and permissions from older versions + var toRemove = new List(); + foreach (var resourceName in _templateWriter.GetKeys("Resources")) + { + var resourcePath = $"Resources.{resourceName}"; + var type = _templateWriter.GetToken($"{resourcePath}.Type", string.Empty); + var creationTool = _templateWriter.GetToken($"{resourcePath}.Metadata.Tool", string.Empty); + + if (!string.Equals(creationTool, CREATION_TOOL, StringComparison.Ordinal)) + { + continue; + } + + // Remove legacy standalone authorizer resources + if (string.Equals(type, "AWS::ApiGatewayV2::Authorizer", StringComparison.Ordinal) || + string.Equals(type, "AWS::ApiGateway::Authorizer", StringComparison.Ordinal)) + { + toRemove.Add(resourceName); + } + + // Remove legacy authorizer Lambda permissions + if (string.Equals(type, "AWS::Lambda::Permission", StringComparison.Ordinal) && + resourceName.EndsWith("AuthorizerPermission")) + { + toRemove.Add(resourceName); + } + } + + foreach (var resourceName in toRemove) + { + _templateWriter.RemoveToken($"Resources.{resourceName}"); + } + } + /// /// Writes all properties associated with to the serverless template. /// diff --git a/Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpApiAttribute.cs b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpApiAttribute.cs index eb4fc5a3a..954832cd6 100644 --- a/Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpApiAttribute.cs +++ b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpApiAttribute.cs @@ -22,6 +22,13 @@ public class HttpApiAttribute : Attribute /// public LambdaHttpMethod Method { get; set; } + /// + /// Name of the HTTP API Lambda authorizer to protect this endpoint. + /// Must match the Name property of an in this project. + /// Leave null/empty for public (unauthenticated) endpoints. + /// + public string Authorizer { get; set; } + /// /// Constructs a /// diff --git a/Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpApiAuthorizerAttribute.cs b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpApiAuthorizerAttribute.cs new file mode 100644 index 000000000..e69e59b9a --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpApiAuthorizerAttribute.cs @@ -0,0 +1,69 @@ +using System; + +namespace Amazon.Lambda.Annotations.APIGateway +{ + /// + /// Marks this Lambda function as an HTTP API (API Gateway V2) authorizer. + /// Other functions can reference this authorizer using the HttpApi attribute's Authorizer property. + /// + /// + /// This attribute must be used in conjunction with the . + /// The authorizer function should return + /// when is true, or + /// when is false. + /// + /// + /// + /// [LambdaFunction] + /// [HttpApiAuthorizer(Name = "MyAuthorizer")] + /// public APIGatewayCustomAuthorizerV2SimpleResponse Authorize(APIGatewayCustomAuthorizerV2Request request) + /// { + /// // Validate token and return authorization response + /// } + /// + /// [LambdaFunction] + /// [HttpApi(LambdaHttpMethod.Get, "/api/protected", Authorizer = "MyAuthorizer")] + /// public string ProtectedEndpoint() + /// { + /// return "Hello, authenticated user!"; + /// } + /// + /// + [AttributeUsage(AttributeTargets.Method)] + public class HttpApiAuthorizerAttribute : Attribute + { + /// + /// Required. Unique name to identify this authorizer. Other functions reference this name + /// via the property. + /// + public string Name { get; set; } + + /// + /// Header name to use as identity source. Defaults to "Authorization". + /// The generator translates this to "$request.header.{IdentityHeader}" for CloudFormation. + /// + public string IdentityHeader { get; set; } = "Authorization"; + + /// + /// Whether to use simple responses (IsAuthorized: true/false) or IAM policy responses. + /// Defaults to true for simpler implementation. + /// + /// + /// When true, the authorizer should return . + /// When false, the authorizer should return . + /// + public bool EnableSimpleResponses { get; set; } = true; + + /// + /// Authorizer payload format version. Valid values: "1.0" or "2.0". + /// Defaults to "2.0". + /// + public string PayloadFormatVersion { get; set; } = "2.0"; + + /// + /// TTL in seconds for caching authorizer results. 0 = no caching. Max = 3600. + /// Defaults to 0 (no caching). + /// + public int ResultTtlInSeconds { get; set; } = 0; + } +} \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.Annotations/APIGateway/RestApiAttribute.cs b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/RestApiAttribute.cs index 85e91a516..be1cbebac 100644 --- a/Libraries/src/Amazon.Lambda.Annotations/APIGateway/RestApiAttribute.cs +++ b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/RestApiAttribute.cs @@ -19,6 +19,13 @@ public class RestApiAttribute : Attribute /// public LambdaHttpMethod Method { get; set; } + /// + /// Name of the REST API Lambda authorizer to protect this endpoint. + /// Must match the Name property of a in this project. + /// Leave null/empty for public (unauthenticated) endpoints. + /// + public string Authorizer { get; set; } + /// /// Constructs a /// diff --git a/Libraries/src/Amazon.Lambda.Annotations/APIGateway/RestApiAuthorizerAttribute.cs b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/RestApiAuthorizerAttribute.cs new file mode 100644 index 000000000..611b3be14 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/RestApiAuthorizerAttribute.cs @@ -0,0 +1,76 @@ +using System; + +namespace Amazon.Lambda.Annotations.APIGateway +{ + /// + /// Type of REST API Lambda authorizer + /// + public enum RestApiAuthorizerType + { + /// + /// Token-based authorizer. Receives the token directly from the identity source. + /// The token is available via request.AuthorizationToken. + /// + Token, + + /// + /// Request-based authorizer. Receives the full request context including + /// headers, query strings, path parameters, and stage variables. + /// + Request + } + + /// + /// Marks this Lambda function as a REST API (API Gateway V1) authorizer. + /// Other functions can reference this authorizer using the RestApi attribute's Authorizer property. + /// + /// + /// This attribute must be used in conjunction with the . + /// The authorizer function should return . + /// + /// + /// + /// [LambdaFunction] + /// [RestApiAuthorizer(Name = "TokenAuthorizer", Type = RestApiAuthorizerType.Token)] + /// public APIGatewayCustomAuthorizerResponse Authorize(APIGatewayCustomAuthorizerRequest request) + /// { + /// var token = request.AuthorizationToken; + /// // Validate token and return IAM policy response + /// } + /// + /// [LambdaFunction] + /// [RestApi(LambdaHttpMethod.Get, "/api/protected", Authorizer = "TokenAuthorizer")] + /// public string ProtectedEndpoint() + /// { + /// return "Hello, authenticated user!"; + /// } + /// + /// + [AttributeUsage(AttributeTargets.Method)] + public class RestApiAuthorizerAttribute : Attribute + { + /// + /// Required. Unique name to identify this authorizer. Other functions reference this name + /// via the property. + /// + public string Name { get; set; } + + /// + /// Header name to use as identity source. Defaults to "Authorization". + /// The generator translates this to "method.request.header.{IdentityHeader}" for CloudFormation. + /// + public string IdentityHeader { get; set; } = "Authorization"; + + /// + /// Type of authorizer: Token or Request. Defaults to Token. + /// Token authorizers receive just the token value; Request authorizers receive full request context. + /// + public RestApiAuthorizerType Type { get; set; } = RestApiAuthorizerType.Token; + + /// + /// TTL in seconds for caching authorizer results. 0 = no caching. Max = 3600. + /// Defaults to 0 (no caching). + /// + public int ResultTtlInSeconds { get; set; } = 0; + } +} \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.Annotations/README.md b/Libraries/src/Amazon.Lambda.Annotations/README.md index 93c0ac46b..6ced6f7df 100644 --- a/Libraries/src/Amazon.Lambda.Annotations/README.md +++ b/Libraries/src/Amazon.Lambda.Annotations/README.md @@ -19,6 +19,10 @@ Topics: - [Amazon API Gateway example](#amazon-api-gateway-example) - [Amazon S3 example](#amazon-s3-example) - [SQS Event Example](#sqs-event-example) + - [Custom Lambda Authorizer Example](#custom-lambda-authorizer-example) + - [HTTP API Authorizer](#http-api-authorizer) + - [REST API Authorizer](#rest-api-authorizer) + - [Authorizer Attribute Properties](#authorizer-attribute-properties) - [Getting build information](#getting-build-information) - [Lambda .NET Attributes Reference](#lambda-net-attributes-reference) - [Event Attributes](#event-attributes) @@ -847,6 +851,226 @@ The following SQS event source mapping will be generated for the `SQSMessageHand } ``` +## Custom Lambda Authorizer Example + +Lambda Annotations supports defining custom Lambda authorizers using attributes. Custom authorizers let you control access to your API Gateway endpoints by running a Lambda function that validates tokens or request parameters before the target function is invoked. The source generator automatically wires up the authorizer resources and references in the CloudFormation template. + +Two authorizer attributes are available: +* **`HttpApiAuthorizer`** — for HTTP API (API Gateway V2) endpoints +* **`RestApiAuthorizer`** — for REST API (API Gateway V1) endpoints + +### HTTP API Authorizer + +To create an HTTP API authorizer, decorate a Lambda function with the `[HttpApiAuthorizer]` attribute and give it a unique `Name`. Other functions can then reference that authorizer via the `Authorizer` property on `[HttpApi]`. + +**Step 1: Define the authorizer function** + +The authorizer function receives an `APIGatewayCustomAuthorizerV2Request` and returns an `APIGatewayCustomAuthorizerV2SimpleResponse` (when `EnableSimpleResponses` is true, which is the default). You can set context values that downstream functions can access via `[FromCustomAuthorizer]`. + +```csharp +[LambdaFunction(ResourceName = "HttpApiAuthorizer", PackageType = LambdaPackageType.Image)] +[HttpApiAuthorizer(Name = "MyHttpAuthorizer")] +public APIGatewayCustomAuthorizerV2SimpleResponse AuthorizeHttpApi( + APIGatewayCustomAuthorizerV2Request request, + ILambdaContext context) +{ + var token = request.Headers?.GetValueOrDefault("authorization", ""); + + if (IsValidToken(token)) + { + return new APIGatewayCustomAuthorizerV2SimpleResponse + { + IsAuthorized = true, + Context = new Dictionary + { + { "userId", "user-123" }, + { "email", "user@example.com" }, + { "role", "admin" } + } + }; + } + + return new APIGatewayCustomAuthorizerV2SimpleResponse { IsAuthorized = false }; +} +``` + +**Step 2: Protect an endpoint with the authorizer** + +Reference the authorizer by name in the `Authorizer` property of `[HttpApi]`. Use `[FromCustomAuthorizer]` on method parameters to automatically extract values from the authorizer context. + +```csharp +[LambdaFunction(ResourceName = "ProtectedHttpApiFunction", PackageType = LambdaPackageType.Image)] +[HttpApi(LambdaHttpMethod.Get, "/api/protected", Authorizer = "MyHttpAuthorizer")] +public object GetProtectedResource( + [FromCustomAuthorizer(Name = "userId")] string userId, + [FromCustomAuthorizer(Name = "email")] string email) +{ + return new { UserId = userId, Email = email, Message = "This is a protected resource" }; +} +``` + +Multiple endpoints can share the same authorizer: + +```csharp +[LambdaFunction(ResourceName = "AdminHttpApiFunction", PackageType = LambdaPackageType.Image)] +[HttpApi(LambdaHttpMethod.Get, "/api/admin", Authorizer = "MyHttpAuthorizer")] +public object AdminEndpoint( + [FromCustomAuthorizer(Name = "userId")] string userId, + [FromCustomAuthorizer(Name = "role")] string role) +{ + return new { Message = $"Hello admin {userId}!", Role = role }; +} +``` + +Endpoints without the `Authorizer` property remain public: + +```csharp +[LambdaFunction(ResourceName = "PublicHttpApiFunction", PackageType = LambdaPackageType.Image)] +[HttpApi(LambdaHttpMethod.Get, "/api/public")] +public string PublicEndpoint() +{ + return "This is a public endpoint"; +} +``` + +The source generator will produce CloudFormation that includes an `Auth` section on the `ServerlessHttpApi` resource with the authorizer configuration, and each protected function's event will reference the authorizer by name. + +### REST API Authorizer + +REST API authorizers work similarly but use `[RestApiAuthorizer]` and `[RestApi]`. REST API authorizers support two types: +* **`RestApiAuthorizerType.Token`** — receives just the authorization token (default) +* **`RestApiAuthorizerType.Request`** — receives the full request context + +**Define a token-based REST API authorizer:** + +```csharp +[LambdaFunction(ResourceName = "RestApiAuthorizer", PackageType = LambdaPackageType.Image)] +[RestApiAuthorizer(Name = "MyRestAuthorizer", Type = RestApiAuthorizerType.Token)] +public APIGatewayCustomAuthorizerResponse AuthorizeRestApi( + APIGatewayCustomAuthorizerRequest request, + ILambdaContext context) +{ + var token = request.AuthorizationToken; + + if (IsValidToken(token)) + { + return new APIGatewayCustomAuthorizerResponse + { + PrincipalID = "user-123", + PolicyDocument = new APIGatewayCustomAuthorizerPolicy + { + Version = "2012-10-17", + Statement = new List + { + new APIGatewayCustomAuthorizerPolicy.IAMPolicyStatement + { + Effect = "Allow", + Action = new HashSet { "execute-api:Invoke" }, + Resource = new HashSet { request.MethodArn } + } + } + }, + Context = new APIGatewayCustomAuthorizerContextOutput + { + ["userId"] = "user-123", + ["email"] = "user@example.com" + } + }; + } + + return new APIGatewayCustomAuthorizerResponse + { + PrincipalID = "anonymous", + PolicyDocument = new APIGatewayCustomAuthorizerPolicy + { + Version = "2012-10-17", + Statement = new List + { + new APIGatewayCustomAuthorizerPolicy.IAMPolicyStatement + { + Effect = "Deny", + Action = new HashSet { "execute-api:Invoke" }, + Resource = new HashSet { request.MethodArn } + } + } + } + }; +} +``` + +**Protect a REST API endpoint:** + +```csharp +[LambdaFunction(ResourceName = "ProtectedRestApiFunction", PackageType = LambdaPackageType.Image)] +[RestApi(LambdaHttpMethod.Get, "/api/rest/protected", Authorizer = "MyRestAuthorizer")] +public object GetRestProtectedResource( + [FromCustomAuthorizer(Name = "userId")] string userId) +{ + return new { UserId = userId, Source = "REST API" }; +} +``` + +### Authorizer Attribute Properties + +**`HttpApiAuthorizerAttribute`** properties: + +| Property | Type | Default | Description | +|---|---|---|---| +| `Name` | `string` | *(required)* | Unique name to identify this authorizer. Referenced by `HttpApi.Authorizer`. | +| `IdentityHeader` | `string` | `"Authorization"` | Header used as the identity source. Translated to `$request.header.{value}` in CloudFormation. | +| `EnableSimpleResponses` | `bool` | `true` | When `true`, use simple responses (`IsAuthorized: true/false`). When `false`, use IAM policy responses. | +| `PayloadFormatVersion` | `string` | `"2.0"` | Authorizer payload format version. Valid values: `"1.0"` or `"2.0"`. | +| `ResultTtlInSeconds` | `int` | `0` | TTL in seconds for caching authorizer results. `0` = no caching. Max = `3600`. | + +**`RestApiAuthorizerAttribute`** properties: + +| Property | Type | Default | Description | +|---|---|---|---| +| `Name` | `string` | *(required)* | Unique name to identify this authorizer. Referenced by `RestApi.Authorizer`. | +| `IdentityHeader` | `string` | `"Authorization"` | Header used as the identity source. Translated to `method.request.header.{value}` in CloudFormation. | +| `Type` | `RestApiAuthorizerType` | `Token` | Type of authorizer: `Token` (receives just the token) or `Request` (receives full request context). | +| `ResultTtlInSeconds` | `int` | `0` | TTL in seconds for caching authorizer results. `0` = no caching. Max = `3600`. | + +**Example with custom header and caching:** + +```csharp +[LambdaFunction(ResourceName = "ApiKeyAuthorizer", PackageType = LambdaPackageType.Image)] +[HttpApiAuthorizer( + Name = "ApiKeyAuth", + IdentityHeader = "X-Api-Key", + ResultTtlInSeconds = 300)] +public APIGatewayCustomAuthorizerV2SimpleResponse ValidateApiKey( + APIGatewayCustomAuthorizerV2Request request, + ILambdaContext context) +{ + var apiKey = request.Headers?.GetValueOrDefault("x-api-key", ""); + + if (!string.IsNullOrEmpty(apiKey) && apiKey.StartsWith("valid-")) + { + return new APIGatewayCustomAuthorizerV2SimpleResponse + { + IsAuthorized = true, + Context = new Dictionary + { + { "clientId", "client-456" }, + { "tier", "premium" } + } + }; + } + + return new APIGatewayCustomAuthorizerV2SimpleResponse { IsAuthorized = false }; +} + +[LambdaFunction(ResourceName = "ApiKeyProtectedFunction", PackageType = LambdaPackageType.Image)] +[HttpApi(LambdaHttpMethod.Get, "/api/external", Authorizer = "ApiKeyAuth")] +public object ExternalEndpoint( + [FromCustomAuthorizer(Name = "clientId")] string clientId, + [FromCustomAuthorizer(Name = "tier")] string tier) +{ + return new { ClientId = clientId, Tier = tier }; +} +``` + ## Getting build information The source generator integrates with MSBuild's compiler error and warning reporting when there are problems generating the boiler plate code. @@ -876,9 +1100,13 @@ Event attributes configuring the source generator for the type of event to expec parameter to the `LambdaFunction` must be the event object and the event source must be configured outside of the code. * RestApi - * Configures the Lambda function to be called from an API Gateway REST API. The HTTP method and resource path are required to be set on the attribute. + * Configures the Lambda function to be called from an API Gateway REST API. The HTTP method and resource path are required to be set on the attribute. Use the `Authorizer` property to reference a `RestApiAuthorizer` by name. * HttpApi - * Configures the Lambda function to be called from an API Gateway HTTP API. The HTTP method, HTTP API payload version and resource path are required to be set on the attribute. + * Configures the Lambda function to be called from an API Gateway HTTP API. The HTTP method, HTTP API payload version and resource path are required to be set on the attribute. Use the `Authorizer` property to reference an `HttpApiAuthorizer` by name. +* HttpApiAuthorizer + * Marks a Lambda function as an HTTP API (API Gateway V2) custom authorizer. Set the `Name` property to give the authorizer a unique name that can be referenced by `HttpApi.Authorizer`. +* RestApiAuthorizer + * Marks a Lambda function as a REST API (API Gateway V1) custom authorizer. Set the `Name` property to give the authorizer a unique name that can be referenced by `RestApi.Authorizer`. Use the `Type` property to choose between `Token` and `Request` authorizer types. * SQSEvent * Sets up event source mapping between the Lambda function and SQS queues. The SQS queue ARN is required to be set on the attribute. If users want to pass a reference to an existing SQS queue resource defined in their CloudFormation template, they can pass the SQS queue resource name prefixed with the '@' symbol. diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/CloudFormationWriterTests.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/CloudFormationWriterTests.cs index 965b61e1c..09ad90f0d 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/CloudFormationWriterTests.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/CloudFormationWriterTests.cs @@ -838,6 +838,13 @@ private LambdaFunctionModelTest GetLambdaFunctionModel(string handler = "MyAssem private AnnotationReport GetAnnotationReport(List lambdaFunctionModels, string projectRootDirectory = ProjectRootDirectory, string cloudFormationTemplatePath = ServerlessTemplateFilePath, bool isTelemetrySuppressed = false) + { + return GetAnnotationReport(lambdaFunctionModels, new List(), projectRootDirectory, cloudFormationTemplatePath, isTelemetrySuppressed); + } + + private AnnotationReport GetAnnotationReport(List lambdaFunctionModels, + List authorizers, + string projectRootDirectory = ProjectRootDirectory, string cloudFormationTemplatePath = ServerlessTemplateFilePath, bool isTelemetrySuppressed = false) { var annotationReport = new AnnotationReport { @@ -849,6 +856,10 @@ private AnnotationReport GetAnnotationReport(List l { annotationReport.LambdaFunctions.Add(model); } + foreach (var authorizer in authorizers) + { + annotationReport.Authorizers.Add(authorizer); + } return annotationReport; } @@ -859,21 +870,553 @@ private CloudFormationWriter GetCloudFormationWriter(IFileManager fileManager, I return new CloudFormationWriter(fileManager, directoryManager, templateWriter, diagnosticReporter); } - public class LambdaFunctionModelTest : ILambdaFunctionSerializable + #region Authorizer Tests + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void HttpApiAuthorizerProcessing(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Authorize", + "AuthorizerFunction", 30, 512, null, null); + var authorizer = new AuthorizerModel + { + Name = "MyHttpAuthorizer", + LambdaResourceName = "AuthorizerFunction", + AuthorizerType = AuthorizerType.HttpApi, + IdentityHeader = "Authorization", + ResultTtlInSeconds = 0, + EnableSimpleResponses = true, + PayloadFormatVersion = "2.0" + }; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport( + new List { lambdaFunctionModel }, + new List { authorizer }); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + // Verify AnnotationsHttpApi resource was created + const string httpApiPath = "Resources.AnnotationsHttpApi"; + Assert.True(templateWriter.Exists(httpApiPath)); + Assert.Equal("AWS::Serverless::HttpApi", templateWriter.GetToken($"{httpApiPath}.Type")); + Assert.Equal("Amazon.Lambda.Annotations", templateWriter.GetToken($"{httpApiPath}.Metadata.Tool")); + + // Verify authorizer configuration + const string authorizerPath = "Resources.AnnotationsHttpApi.Properties.Auth.Authorizers.MyHttpAuthorizer"; + Assert.True(templateWriter.Exists(authorizerPath)); + Assert.Equal(new List { "AuthorizerFunction", "Arn" }, templateWriter.GetToken>($"{authorizerPath}.FunctionArn.Fn::GetAtt")); + Assert.Equal("2.0", templateWriter.GetToken($"{authorizerPath}.AuthorizerPayloadFormatVersion")); + Assert.True(templateWriter.GetToken($"{authorizerPath}.EnableSimpleResponses")); + Assert.Equal(new List { "Authorization" }, templateWriter.GetToken>($"{authorizerPath}.Identity.Headers")); + Assert.True(templateWriter.GetToken($"{authorizerPath}.EnableFunctionDefaultPermissions")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void RestApiAuthorizerProcessing(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Authorize", + "AuthorizerFunction", 30, 512, null, null); + var authorizer = new AuthorizerModel + { + Name = "MyRestAuthorizer", + LambdaResourceName = "AuthorizerFunction", + AuthorizerType = AuthorizerType.RestApi, + IdentityHeader = "Authorization", + ResultTtlInSeconds = 0, + RestApiAuthorizerType = RestApiAuthorizerType.Token + }; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport( + new List { lambdaFunctionModel }, + new List { authorizer }); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + // Verify AnnotationsRestApi resource was created + const string restApiPath = "Resources.AnnotationsRestApi"; + Assert.True(templateWriter.Exists(restApiPath)); + Assert.Equal("AWS::Serverless::Api", templateWriter.GetToken($"{restApiPath}.Type")); + Assert.Equal("Amazon.Lambda.Annotations", templateWriter.GetToken($"{restApiPath}.Metadata.Tool")); + Assert.Equal("Prod", templateWriter.GetToken($"{restApiPath}.Properties.StageName")); + + // Verify authorizer configuration + const string authorizerPath = "Resources.AnnotationsRestApi.Properties.Auth.Authorizers.MyRestAuthorizer"; + Assert.True(templateWriter.Exists(authorizerPath)); + Assert.Equal(new List { "AuthorizerFunction", "Arn" }, templateWriter.GetToken>($"{authorizerPath}.FunctionArn.Fn::GetAtt")); + Assert.Equal("Authorization", templateWriter.GetToken($"{authorizerPath}.Identity.Header")); + Assert.Equal("TOKEN", templateWriter.GetToken($"{authorizerPath}.FunctionPayloadType")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void RestApiRequestAuthorizerProcessing(CloudFormationTemplateFormat templateFormat) { - public string MethodName { get; set; } - public string Handler { get; set; } - public bool IsExecutable { get; set; } - public string ResourceName { get; set; } - public uint? Timeout { get; set; } - public uint? MemorySize { get; set; } - public string Role { get; set; } - public string Policies { get; set; } - public string Runtime { get; set; } - public IList Attributes { get; set; } = new List(); - public string SourceGeneratorVersion { get; set; } - public LambdaPackageType PackageType { get; set; } = LambdaPackageType.Zip; - public string ReturnTypeFullName { get; set; } = "void"; + // ARRANGE + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Authorize", + "AuthorizerFunction", 30, 512, null, null); + var authorizer = new AuthorizerModel + { + Name = "MyRequestAuthorizer", + LambdaResourceName = "AuthorizerFunction", + AuthorizerType = AuthorizerType.RestApi, + IdentityHeader = "X-Custom-Header", + ResultTtlInSeconds = 0, + RestApiAuthorizerType = RestApiAuthorizerType.Request + }; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport( + new List { lambdaFunctionModel }, + new List { authorizer }); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + const string authorizerPath = "Resources.AnnotationsRestApi.Properties.Auth.Authorizers.MyRequestAuthorizer"; + Assert.True(templateWriter.Exists(authorizerPath)); + Assert.Equal("REQUEST", templateWriter.GetToken($"{authorizerPath}.FunctionPayloadType")); + Assert.Equal("X-Custom-Header", templateWriter.GetToken($"{authorizerPath}.Identity.Header")); } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void HttpApiWithAuthorizerReference(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE + var mockFileManager = GetMockFileManager(string.Empty); + + // Create the authorizer function + var authorizerFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Authorize", + "AuthorizerFunction", 30, 512, null, null); + + // Create the protected function with HttpApi attribute referencing the authorizer + var protectedFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Protected", + "ProtectedFunction", 30, 512, null, null); + var httpApiAttribute = new AttributeModel() + { + Data = new HttpApiAttribute(LambdaHttpMethod.Get, "/api/protected") + { + Authorizer = "MyHttpAuthorizer" + } + }; + protectedFunctionModel.Attributes = new List { httpApiAttribute }; + protectedFunctionModel.Authorizer = "MyHttpAuthorizer"; + + var authorizer = new AuthorizerModel + { + Name = "MyHttpAuthorizer", + LambdaResourceName = "AuthorizerFunction", + AuthorizerType = AuthorizerType.HttpApi, + IdentityHeader = "Authorization", + ResultTtlInSeconds = 0, + EnableSimpleResponses = true, + PayloadFormatVersion = "2.0" + }; + + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport( + new List { authorizerFunctionModel, protectedFunctionModel }, + new List { authorizer }); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + // Verify the protected function's event has Auth configuration + const string eventPath = "Resources.ProtectedFunction.Properties.Events.RootGet"; + Assert.True(templateWriter.Exists(eventPath)); + Assert.Equal("HttpApi", templateWriter.GetToken($"{eventPath}.Type")); + Assert.Equal("/api/protected", templateWriter.GetToken($"{eventPath}.Properties.Path")); + Assert.Equal("GET", templateWriter.GetToken($"{eventPath}.Properties.Method")); + Assert.Equal("MyHttpAuthorizer", templateWriter.GetToken($"{eventPath}.Properties.Auth.Authorizer")); + Assert.Equal("AnnotationsHttpApi", templateWriter.GetToken($"{eventPath}.Properties.ApiId.Ref")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void RestApiWithAuthorizerReference(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE + var mockFileManager = GetMockFileManager(string.Empty); + + // Create the authorizer function + var authorizerFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Authorize", + "AuthorizerFunction", 30, 512, null, null); + + // Create the protected function with RestApi attribute referencing the authorizer + var protectedFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Protected", + "ProtectedFunction", 30, 512, null, null); + var restApiAttribute = new AttributeModel() + { + Data = new RestApiAttribute(LambdaHttpMethod.Get, "/api/protected") + { + Authorizer = "MyRestAuthorizer" + } + }; + protectedFunctionModel.Attributes = new List { restApiAttribute }; + protectedFunctionModel.Authorizer = "MyRestAuthorizer"; + + var authorizer = new AuthorizerModel + { + Name = "MyRestAuthorizer", + LambdaResourceName = "AuthorizerFunction", + AuthorizerType = AuthorizerType.RestApi, + IdentityHeader = "Authorization", + ResultTtlInSeconds = 0, + RestApiAuthorizerType = RestApiAuthorizerType.Token + }; + + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport( + new List { authorizerFunctionModel, protectedFunctionModel }, + new List { authorizer }); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + // Verify the protected function's event has Auth configuration + const string eventPath = "Resources.ProtectedFunction.Properties.Events.RootGet"; + Assert.True(templateWriter.Exists(eventPath)); + Assert.Equal("Api", templateWriter.GetToken($"{eventPath}.Type")); + Assert.Equal("/api/protected", templateWriter.GetToken($"{eventPath}.Properties.Path")); + Assert.Equal("GET", templateWriter.GetToken($"{eventPath}.Properties.Method")); + Assert.Equal("MyRestAuthorizer", templateWriter.GetToken($"{eventPath}.Properties.Auth.Authorizer")); + Assert.Equal("AnnotationsRestApi", templateWriter.GetToken($"{eventPath}.Properties.RestApiId.Ref")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void HttpApiAuthorizerWithCustomTtl(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Authorize", + "AuthorizerFunction", 30, 512, null, null); + var authorizer = new AuthorizerModel + { + Name = "CachedAuthorizer", + LambdaResourceName = "AuthorizerFunction", + AuthorizerType = AuthorizerType.HttpApi, + IdentityHeader = "Authorization", + ResultTtlInSeconds = 300, + EnableSimpleResponses = true, + PayloadFormatVersion = "2.0" + }; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport( + new List { lambdaFunctionModel }, + new List { authorizer }); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + const string authorizerPath = "Resources.AnnotationsHttpApi.Properties.Auth.Authorizers.CachedAuthorizer"; + Assert.True(templateWriter.Exists(authorizerPath)); + + // When TTL > 0, FunctionInvokeRole is set (to null, signaling SAM to auto-generate the role for caching) + // Verify the authorizer was created with expected properties + Assert.Equal(new List { "AuthorizerFunction", "Arn" }, templateWriter.GetToken>($"{authorizerPath}.FunctionArn.Fn::GetAtt")); + Assert.Equal("2.0", templateWriter.GetToken($"{authorizerPath}.AuthorizerPayloadFormatVersion")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void HttpApiAuthorizerWithNoTtl_DoesNotSetFunctionInvokeRole(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Authorize", + "AuthorizerFunction", 30, 512, null, null); + var authorizer = new AuthorizerModel + { + Name = "NoCacheAuthorizer", + LambdaResourceName = "AuthorizerFunction", + AuthorizerType = AuthorizerType.HttpApi, + IdentityHeader = "Authorization", + ResultTtlInSeconds = 0, + EnableSimpleResponses = true, + PayloadFormatVersion = "2.0" + }; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport( + new List { lambdaFunctionModel }, + new List { authorizer }); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + const string authorizerPath = "Resources.AnnotationsHttpApi.Properties.Auth.Authorizers.NoCacheAuthorizer"; + Assert.True(templateWriter.Exists(authorizerPath)); + + // When TTL = 0, FunctionInvokeRole should NOT be set + Assert.False(templateWriter.Exists($"{authorizerPath}.FunctionInvokeRole")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void RemoveOrphanedHttpApiAuthorizer(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE - Start with an authorizer + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Authorize", + "AuthorizerFunction", 30, 512, null, null); + var authorizer = new AuthorizerModel + { + Name = "OldAuthorizer", + LambdaResourceName = "AuthorizerFunction", + AuthorizerType = AuthorizerType.HttpApi, + IdentityHeader = "Authorization", + ResultTtlInSeconds = 0, + EnableSimpleResponses = true, + PayloadFormatVersion = "2.0" + }; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport( + new List { lambdaFunctionModel }, + new List { authorizer }); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + // ACT - First pass: create the authorizer + cloudFormationWriter.ApplyReport(report); + + // ASSERT - Authorizer exists + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + Assert.True(templateWriter.Exists("Resources.AnnotationsHttpApi.Properties.Auth.Authorizers.OldAuthorizer")); + + // ACT - Second pass: remove the authorizer (empty authorizer list) + var reportWithoutAuthorizer = GetAnnotationReport( + new List { lambdaFunctionModel }, + new List()); + cloudFormationWriter.ApplyReport(reportWithoutAuthorizer); + + // ASSERT - Authorizer is removed + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + Assert.False(templateWriter.Exists("Resources.AnnotationsHttpApi.Properties.Auth.Authorizers.OldAuthorizer")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void RemoveOrphanedRestApiAuthorizer(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE - Start with an authorizer + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Authorize", + "AuthorizerFunction", 30, 512, null, null); + var authorizer = new AuthorizerModel + { + Name = "OldRestAuthorizer", + LambdaResourceName = "AuthorizerFunction", + AuthorizerType = AuthorizerType.RestApi, + IdentityHeader = "Authorization", + ResultTtlInSeconds = 0, + RestApiAuthorizerType = RestApiAuthorizerType.Token + }; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport( + new List { lambdaFunctionModel }, + new List { authorizer }); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + // ACT - First pass: create the authorizer + cloudFormationWriter.ApplyReport(report); + + // ASSERT - Authorizer exists + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + Assert.True(templateWriter.Exists("Resources.AnnotationsRestApi.Properties.Auth.Authorizers.OldRestAuthorizer")); + + // ACT - Second pass: remove the authorizer + var reportWithoutAuthorizer = GetAnnotationReport( + new List { lambdaFunctionModel }, + new List()); + cloudFormationWriter.ApplyReport(reportWithoutAuthorizer); + + // ASSERT - Authorizer is removed + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + Assert.False(templateWriter.Exists("Resources.AnnotationsRestApi.Properties.Auth.Authorizers.OldRestAuthorizer")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void CombinedAuthorizerAndProtectedFunction(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE + var mockFileManager = GetMockFileManager(string.Empty); + + // Create the authorizer function + var authorizerFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.Auth::Authorize", + "MyAuthorizerFunction", 30, 512, null, null); + + // Create a protected function + var protectedFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.Api::GetData", + "GetDataFunction", 30, 512, null, null); + var httpApiAttribute = new AttributeModel() + { + Data = new HttpApiAttribute(LambdaHttpMethod.Get, "/data") + { + Authorizer = "LambdaAuthorizer" + } + }; + protectedFunctionModel.Attributes = new List { httpApiAttribute }; + protectedFunctionModel.Authorizer = "LambdaAuthorizer"; + + // Create an unprotected function + var publicFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.Api::Health", + "HealthFunction", 30, 512, null, null); + var publicHttpApiAttribute = new AttributeModel() + { + Data = new HttpApiAttribute(LambdaHttpMethod.Get, "/health") + }; + publicFunctionModel.Attributes = new List { publicHttpApiAttribute }; + + var authorizer = new AuthorizerModel + { + Name = "LambdaAuthorizer", + LambdaResourceName = "MyAuthorizerFunction", + AuthorizerType = AuthorizerType.HttpApi, + IdentityHeader = "Authorization", + ResultTtlInSeconds = 0, + EnableSimpleResponses = true, + PayloadFormatVersion = "2.0" + }; + + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport( + new List { authorizerFunctionModel, protectedFunctionModel, publicFunctionModel }, + new List { authorizer }); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + // Verify all three Lambda functions exist + Assert.True(templateWriter.Exists("Resources.MyAuthorizerFunction")); + Assert.True(templateWriter.Exists("Resources.GetDataFunction")); + Assert.True(templateWriter.Exists("Resources.HealthFunction")); + + // Verify AnnotationsHttpApi resource with authorizer + Assert.True(templateWriter.Exists("Resources.AnnotationsHttpApi")); + Assert.Equal("AWS::Serverless::HttpApi", templateWriter.GetToken("Resources.AnnotationsHttpApi.Type")); + Assert.True(templateWriter.Exists("Resources.AnnotationsHttpApi.Properties.Auth.Authorizers.LambdaAuthorizer")); + Assert.Equal(new List { "MyAuthorizerFunction", "Arn" }, + templateWriter.GetToken>("Resources.AnnotationsHttpApi.Properties.Auth.Authorizers.LambdaAuthorizer.FunctionArn.Fn::GetAtt")); + + // Verify protected function references authorizer + const string protectedEventPath = "Resources.GetDataFunction.Properties.Events.RootGet"; + Assert.Equal("LambdaAuthorizer", templateWriter.GetToken($"{protectedEventPath}.Properties.Auth.Authorizer")); + Assert.Equal("AnnotationsHttpApi", templateWriter.GetToken($"{protectedEventPath}.Properties.ApiId.Ref")); + + // Verify public function does NOT reference authorizer + const string publicEventPath = "Resources.HealthFunction.Properties.Events.RootGet"; + Assert.True(templateWriter.Exists(publicEventPath)); + Assert.False(templateWriter.Exists($"{publicEventPath}.Properties.Auth")); + Assert.False(templateWriter.Exists($"{publicEventPath}.Properties.ApiId")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void HttpApiAuthorizerWithCustomSettings(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE - Test non-default authorizer settings (V1 payload, simple responses disabled, custom header) + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Authorize", + "AuthorizerFunction", 30, 512, null, null); + var authorizer = new AuthorizerModel + { + Name = "CustomAuthorizer", + LambdaResourceName = "AuthorizerFunction", + AuthorizerType = AuthorizerType.HttpApi, + IdentityHeader = "X-Api-Key", + ResultTtlInSeconds = 0, + EnableSimpleResponses = false, + PayloadFormatVersion = "1.0" + }; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport( + new List { lambdaFunctionModel }, + new List { authorizer }); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + const string authorizerPath = "Resources.AnnotationsHttpApi.Properties.Auth.Authorizers.CustomAuthorizer"; + Assert.True(templateWriter.Exists(authorizerPath)); + Assert.Equal("1.0", templateWriter.GetToken($"{authorizerPath}.AuthorizerPayloadFormatVersion")); + Assert.False(templateWriter.GetToken($"{authorizerPath}.EnableSimpleResponses")); + Assert.Equal(new List { "X-Api-Key" }, + templateWriter.GetToken>($"{authorizerPath}.Identity.Headers")); + Assert.True(templateWriter.GetToken($"{authorizerPath}.EnableFunctionDefaultPermissions")); + } + + #endregion + } + + public class LambdaFunctionModelTest : ILambdaFunctionSerializable + { + public string MethodName { get; set; } + public string Handler { get; set; } + public bool IsExecutable { get; set; } + public string ResourceName { get; set; } + public uint? Timeout { get; set; } + public uint? MemorySize { get; set; } + public string Role { get; set; } + public string Policies { get; set; } + public string Runtime { get; set; } + public IList Attributes { get; set; } = new List(); + public string SourceGeneratorVersion { get; set; } + public LambdaPackageType PackageType { get; set; } = LambdaPackageType.Zip; + public string ReturnTypeFullName { get; set; } = "void"; + public string Authorizer { get; set; } } -} \ No newline at end of file +} diff --git a/Libraries/test/IntegrationTests.Helpers/CloudFormationHelper.cs b/Libraries/test/IntegrationTests.Helpers/CloudFormationHelper.cs index 11e943b75..43f4005c1 100644 --- a/Libraries/test/IntegrationTests.Helpers/CloudFormationHelper.cs +++ b/Libraries/test/IntegrationTests.Helpers/CloudFormationHelper.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using System.Threading.Tasks; using Amazon.CloudFormation; @@ -37,6 +38,12 @@ public async Task IsDeletedAsync(string stackName) public async Task DeleteStackAsync(string stackName) { + if (string.IsNullOrEmpty(stackName)) + { + Console.WriteLine("[CloudFormationHelper] WARNING: DeleteStackAsync called with null/empty stack name. Skipping."); + return; + } + if (!await StackExistsAsync(stackName)) return; @@ -49,6 +56,34 @@ public async Task DeleteStackAsync(string stackName) return stack?.Outputs.FirstOrDefault(x => string.Equals(x.OutputKey, outputKey))?.OutputValue; } + public async Task GetResourcePhysicalIdAsync(string stackName, string logicalResourceId) + { + try + { + var response = await _cloudFormationClient.DescribeStackResourcesAsync( + new DescribeStackResourcesRequest { StackName = stackName }); + + Console.WriteLine($"[CloudFormationHelper] Stack '{stackName}' has {response.StackResources.Count} resources: " + + string.Join(", ", response.StackResources.Select(r => $"{r.LogicalResourceId}={r.PhysicalResourceId} ({r.ResourceStatus})"))); + + var physicalId = response.StackResources + .FirstOrDefault(r => string.Equals(r.LogicalResourceId, logicalResourceId)) + ?.PhysicalResourceId; + + if (physicalId == null) + { + Console.WriteLine($"[CloudFormationHelper] WARNING: Logical resource '{logicalResourceId}' not found in stack '{stackName}'."); + } + + return physicalId; + } + catch (Exception ex) + { + Console.WriteLine($"[CloudFormationHelper] ERROR querying resource '{logicalResourceId}' in stack '{stackName}': {ex.GetType().Name}: {ex.Message}"); + throw; + } + } + private async Task GetStackAsync(string stackName) { if (!await StackExistsAsync(stackName)) diff --git a/Libraries/test/IntegrationTests.Helpers/CommandLineWrapper.cs b/Libraries/test/IntegrationTests.Helpers/CommandLineWrapper.cs index 31f705a45..274246771 100644 --- a/Libraries/test/IntegrationTests.Helpers/CommandLineWrapper.cs +++ b/Libraries/test/IntegrationTests.Helpers/CommandLineWrapper.cs @@ -47,6 +47,13 @@ public static async Task RunAsync(string command, string workingDirectory = "", await process.WaitForExitAsync(cancelToken); var log = sb.ToString(); + + if (process.ExitCode != 0) + { + throw new Exception( + $"Command '{command}' exited with code {process.ExitCode}.{Environment.NewLine}" + + $"Output:{Environment.NewLine}{log}"); + } } private static string GetSystemShell() diff --git a/Libraries/test/TestCustomAuthorizerApp.IntegrationTests/HealthCheckTests.cs b/Libraries/test/TestCustomAuthorizerApp.IntegrationTests/HealthCheckTests.cs index 2360d7305..4bd057356 100644 --- a/Libraries/test/TestCustomAuthorizerApp.IntegrationTests/HealthCheckTests.cs +++ b/Libraries/test/TestCustomAuthorizerApp.IntegrationTests/HealthCheckTests.cs @@ -20,7 +20,7 @@ public HealthCheckTests(IntegrationTestContextFixture fixture) public async Task HealthCheck_ReturnsOk_WithoutAuthorization() { // Arrange & Act - var response = await _fixture.HttpClient.GetAsync($"{_fixture.HttpApiUrl}/api/health"); + var response = await _fixture.HttpClient.GetAsync($"{_fixture.ImplicitHttpApiUrl}/api/health"); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); diff --git a/Libraries/test/TestCustomAuthorizerApp.IntegrationTests/IntegrationTestContextFixture.cs b/Libraries/test/TestCustomAuthorizerApp.IntegrationTests/IntegrationTestContextFixture.cs index 6f8a4b002..2d2ab4c09 100644 --- a/Libraries/test/TestCustomAuthorizerApp.IntegrationTests/IntegrationTestContextFixture.cs +++ b/Libraries/test/TestCustomAuthorizerApp.IntegrationTests/IntegrationTestContextFixture.cs @@ -23,10 +23,16 @@ public class IntegrationTestContextFixture : IAsyncLifetime public readonly HttpClient HttpClient; /// - /// HTTP API base URL (no trailing slash) + /// HTTP API base URL for endpoints explicitly attached to AnnotationsHttpApi (no trailing slash) /// public string HttpApiUrl = string.Empty; + /// + /// HTTP API base URL for endpoints using the implicit SAM-generated ServerlessHttpApi (no trailing slash). + /// Functions without an explicit ApiId (e.g. HealthCheck) are placed on this API. + /// + public string ImplicitHttpApiUrl = string.Empty; + /// /// REST API base URL (no trailing slash) /// @@ -49,27 +55,42 @@ public IntegrationTestContextFixture() public async Task InitializeAsync() { var scriptPath = Path.Combine("..", "..", "..", "DeploymentScript.ps1"); + Console.WriteLine($"[IntegrationTest] Running deployment script: {scriptPath}"); await CommandLineWrapper.RunAsync($"pwsh {scriptPath}"); + Console.WriteLine("[IntegrationTest] Deployment script completed successfully."); _stackName = GetStackName(); _bucketName = GetBucketName(); + Console.WriteLine($"[IntegrationTest] Stack name: '{_stackName}', Bucket name: '{_bucketName}'"); Assert.False(string.IsNullOrEmpty(_stackName), "Stack name should not be empty"); Assert.False(string.IsNullOrEmpty(_bucketName), "Bucket name should not be empty"); - // Get API URLs from CloudFormation outputs - HttpApiUrl = await _cloudFormationHelper.GetOutputValueAsync(_stackName, "ApiUrl") ?? string.Empty; - RestApiUrl = await _cloudFormationHelper.GetOutputValueAsync(_stackName, "RestApiUrl") ?? string.Empty; + // Check stack status before querying resources + var stackStatus = await _cloudFormationHelper.GetStackStatusAsync(_stackName); + Console.WriteLine($"[IntegrationTest] Stack status after deployment: {stackStatus}"); + Assert.NotNull(stackStatus); + Assert.Equal(StackStatus.CREATE_COMPLETE, stackStatus); + + // Dynamically construct API URLs from stack resource physical IDs + // since the serverless.template is managed by the source generator and may not have Outputs + var region = "us-west-2"; + Console.WriteLine($"[IntegrationTest] Querying stack resources for '{_stackName}'..."); + var httpApiId = await _cloudFormationHelper.GetResourcePhysicalIdAsync(_stackName, "AnnotationsHttpApi"); + var implicitHttpApiId = await _cloudFormationHelper.GetResourcePhysicalIdAsync(_stackName, "ServerlessHttpApi"); + var restApiId = await _cloudFormationHelper.GetResourcePhysicalIdAsync(_stackName, "AnnotationsRestApi"); + Console.WriteLine($"[IntegrationTest] AnnotationsHttpApi: {httpApiId}, ServerlessHttpApi: {implicitHttpApiId}, AnnotationsRestApi: {restApiId}"); + HttpApiUrl = $"https://{httpApiId}.execute-api.{region}.amazonaws.com"; + ImplicitHttpApiUrl = $"https://{implicitHttpApiId}.execute-api.{region}.amazonaws.com"; + RestApiUrl = $"https://{restApiId}.execute-api.{region}.amazonaws.com/Prod"; LambdaFunctions = await LambdaHelper.FilterByCloudFormationStackAsync(_stackName); + Console.WriteLine($"[IntegrationTest] Found {LambdaFunctions.Count} Lambda functions: {string.Join(", ", LambdaFunctions.Select(f => f.Name ?? "(null)"))}"); - Assert.Equal(StackStatus.CREATE_COMPLETE, await _cloudFormationHelper.GetStackStatusAsync(_stackName)); Assert.True(await _s3Helper.BucketExistsAsync(_bucketName), $"S3 bucket {_bucketName} should exist"); // There are 9 Lambda functions in TestCustomAuthorizerApp: // CustomAuthorizer, RestApiAuthorizer, ProtectedEndpoint, GetUserInfo, HealthCheck, RestUserInfo, HttpApiV1UserInfo, IHttpResultUserInfo, NonStringUserInfo Assert.Equal(9, LambdaFunctions.Count); - Assert.False(string.IsNullOrEmpty(HttpApiUrl), "HTTP API URL should not be empty"); - Assert.False(string.IsNullOrEmpty(RestApiUrl), "REST API URL should not be empty"); await LambdaHelper.WaitTillNotPending(LambdaFunctions.Where(x => x.Name != null).Select(x => x.Name!).ToList()); @@ -79,11 +100,27 @@ public async Task InitializeAsync() public async Task DisposeAsync() { - await _cloudFormationHelper.DeleteStackAsync(_stackName); - Assert.True(await _cloudFormationHelper.IsDeletedAsync(_stackName), $"The stack '{_stackName}' still exists and will have to be manually deleted from the AWS console."); - - await _s3Helper.DeleteBucketAsync(_bucketName); - Assert.False(await _s3Helper.BucketExistsAsync(_bucketName), $"The bucket '{_bucketName}' still exists and will have to be manually deleted from the AWS console."); + if (!string.IsNullOrEmpty(_stackName)) + { + Console.WriteLine($"[IntegrationTest] Cleaning up stack '{_stackName}'..."); + await _cloudFormationHelper.DeleteStackAsync(_stackName); + Assert.True(await _cloudFormationHelper.IsDeletedAsync(_stackName), $"The stack '{_stackName}' still exists and will have to be manually deleted from the AWS console."); + } + else + { + Console.WriteLine("[IntegrationTest] WARNING: Stack name is null/empty, skipping stack deletion."); + } + + if (!string.IsNullOrEmpty(_bucketName)) + { + Console.WriteLine($"[IntegrationTest] Cleaning up bucket '{_bucketName}'..."); + await _s3Helper.DeleteBucketAsync(_bucketName); + Assert.False(await _s3Helper.BucketExistsAsync(_bucketName), $"The bucket '{_bucketName}' still exists and will have to be manually deleted from the AWS console."); + } + else + { + Console.WriteLine("[IntegrationTest] WARNING: Bucket name is null/empty, skipping bucket deletion."); + } // Reset the aws-lambda-tools-defaults.json to original values var filePath = Path.Combine("..", "..", "..", "..", "TestCustomAuthorizerApp", "aws-lambda-tools-defaults.json"); diff --git a/Libraries/test/TestCustomAuthorizerApp/AuthorizerFunction.cs b/Libraries/test/TestCustomAuthorizerApp/AuthorizerFunction.cs index 0b6193563..1fc03f2e1 100644 --- a/Libraries/test/TestCustomAuthorizerApp/AuthorizerFunction.cs +++ b/Libraries/test/TestCustomAuthorizerApp/AuthorizerFunction.cs @@ -1,3 +1,5 @@ +using Amazon.Lambda.Annotations; +using Amazon.Lambda.Annotations.APIGateway; using Amazon.Lambda.APIGatewayEvents; using Amazon.Lambda.Core; @@ -34,6 +36,12 @@ public class AuthorizerFunction /// HTTP API Lambda Authorizer (Payload format 2.0 with simple response) /// Returns authorized status along with custom context that can be accessed via [FromCustomAuthorizer] /// + [LambdaFunction(ResourceName = "CustomAuthorizer")] + [HttpApiAuthorizer( + Name = "HttpApiLambdaAuthorizer", + IdentityHeader = "authorization", + EnableSimpleResponses = true, + PayloadFormatVersion = "2.0")] public APIGatewayCustomAuthorizerV2SimpleResponse HttpApiAuthorize( APIGatewayCustomAuthorizerV2Request request, ILambdaContext context) @@ -110,6 +118,11 @@ public APIGatewayCustomAuthorizerV2SimpleResponse HttpApiAuthorize( /// REST API Lambda Authorizer (Token-based authorizer) /// Returns an IAM policy document along with custom context values /// + [LambdaFunction(ResourceName = "RestApiAuthorizer")] + [RestApiAuthorizer( + Name = "RestApiLambdaAuthorizer", + Type = RestApiAuthorizerType.Token, + IdentityHeader = "Authorization")] public APIGatewayCustomAuthorizerResponse RestApiAuthorize( APIGatewayCustomAuthorizerRequest request, ILambdaContext context) @@ -215,4 +228,4 @@ private APIGatewayCustomAuthorizerResponse GenerateDenyPolicy(string principalId } }; } -} +} \ No newline at end of file diff --git a/Libraries/test/TestCustomAuthorizerApp/ProtectedFunction.cs b/Libraries/test/TestCustomAuthorizerApp/ProtectedFunction.cs index d7a25ba65..39734a579 100644 --- a/Libraries/test/TestCustomAuthorizerApp/ProtectedFunction.cs +++ b/Libraries/test/TestCustomAuthorizerApp/ProtectedFunction.cs @@ -17,7 +17,7 @@ public class ProtectedFunction /// Debug endpoint to see what's in the RequestContext.Authorizer /// [LambdaFunction(ResourceName = "ProtectedEndpoint")] - [HttpApi(LambdaHttpMethod.Get, "/api/protected")] + [HttpApi(LambdaHttpMethod.Get, "/api/protected", Authorizer = "HttpApiLambdaAuthorizer")] public string GetProtectedData( APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context) @@ -87,7 +87,7 @@ public string GetProtectedData( /// Another protected endpoint showing different usage - just getting the email. /// [LambdaFunction(ResourceName = "GetUserInfo")] - [HttpApi(LambdaHttpMethod.Get, "/api/user-info")] + [HttpApi(LambdaHttpMethod.Get, "/api/user-info", Authorizer = "HttpApiLambdaAuthorizer")] public object GetUserInfo( [FromCustomAuthorizer(Name = "userId")] string userId, [FromCustomAuthorizer(Name = "email")] string email, @@ -122,7 +122,7 @@ public string HealthCheck(ILambdaContext context) /// REST API authorizers use a different context structure than HTTP API v2. /// [LambdaFunction(ResourceName = "RestUserInfo")] - [RestApi(LambdaHttpMethod.Get, "/api/rest-user-info")] + [RestApi(LambdaHttpMethod.Get, "/api/rest-user-info", Authorizer = "RestApiLambdaAuthorizer")] public object GetRestUserInfo( [FromCustomAuthorizer(Name = "userId")] string userId, [FromCustomAuthorizer(Name = "email")] string email, @@ -148,7 +148,7 @@ public object GetRestUserInfo( /// where RequestContext.Authorizer is a dictionary, not RequestContext.Authorizer.Lambda. /// [LambdaFunction(ResourceName = "HttpApiV1UserInfo")] - [HttpApi(LambdaHttpMethod.Get, "/api/http-v1-user-info", Version = HttpApiVersion.V1)] + [HttpApi(LambdaHttpMethod.Get, "/api/http-v1-user-info", Version = HttpApiVersion.V1, Authorizer = "HttpApiLambdaAuthorizer")] public object GetHttpApiV1UserInfo( [FromCustomAuthorizer(Name = "userId")] string userId, [FromCustomAuthorizer(Name = "email")] string email, @@ -174,7 +174,7 @@ public object GetHttpApiV1UserInfo( /// when authorizer context is missing (the handler returns Stream, not response object). /// [LambdaFunction(ResourceName = "IHttpResultUserInfo")] - [HttpApi(LambdaHttpMethod.Get, "/api/ihttpresult-user-info")] + [HttpApi(LambdaHttpMethod.Get, "/api/ihttpresult-user-info", Authorizer = "HttpApiLambdaAuthorizer")] public IHttpResult GetIHttpResult( [FromCustomAuthorizer(Name = "userId")] string userId, [FromCustomAuthorizer(Name = "email")] string email, @@ -196,7 +196,7 @@ public IHttpResult GetIHttpResult( /// the Lambda authorizer context. /// [LambdaFunction(ResourceName = "NonStringUserInfo")] - [HttpApi(LambdaHttpMethod.Get, "/api/nonstring-user-info")] + [HttpApi(LambdaHttpMethod.Get, "/api/nonstring-user-info", Authorizer = "HttpApiLambdaAuthorizer")] public object GetNonStringUserInfo( APIGatewayHttpApiV2ProxyRequest request, [FromCustomAuthorizer(Name = "numericTenantId")] int tenantId, @@ -244,4 +244,4 @@ public object GetNonStringUserInfo( Message = "Successfully extracted non-string types from custom authorizer context!" }; } -} +} \ No newline at end of file diff --git a/Libraries/test/TestCustomAuthorizerApp/serverless.template b/Libraries/test/TestCustomAuthorizerApp/serverless.template index 56ae0368d..d59e91d9b 100644 --- a/Libraries/test/TestCustomAuthorizerApp/serverless.template +++ b/Libraries/test/TestCustomAuthorizerApp/serverless.template @@ -1,622 +1,389 @@ { "AWSTemplateFormatVersion": "2010-09-09", "Transform": "AWS::Serverless-2016-10-31", - "Description": "Test application demonstrating FromCustomAuthorizer attribute with both HTTP API and REST API Lambda Authorizers. This template is partially managed by Amazon.Lambda.Annotations (v1.9.0.0).", - "Globals": { - "Function": { - "Runtime": "dotnet8", - "MemorySize": 512, - "Timeout": 30, - "CodeUri": "." - } - }, + "Description": "This template is partially managed by Amazon.Lambda.Annotations (v1.9.0.0).", "Resources": { - "CustomAuthorizer": { - "Type": "AWS::Serverless::Function", - "Properties": { - "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.AuthorizerFunction::HttpApiAuthorize", - "Description": "Lambda Authorizer that validates requests and provides custom context values", - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "Environment": { - "Variables": { - "LAMBDA_NET_SERIALIZER_DEBUG": "true" + "AnnotationsHttpApi": { + "Type": "AWS::Serverless::HttpApi", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "Auth": { + "Authorizers": { + "HttpApiLambdaAuthorizer": { + "FunctionArn": { + "Fn::GetAtt": [ + "CustomAuthorizer", + "Arn" + ] + }, + "AuthorizerPayloadFormatVersion": "2.0", + "EnableSimpleResponses": true, + "Identity": { + "Headers": [ + "authorization" + ] + }, + "EnableFunctionDefaultPermissions": true + } } } } }, - "CustomAuthorizerPermission": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "FunctionName": { - "Ref": "CustomAuthorizer" - }, - "Action": "lambda:InvokeFunction", - "Principal": "apigateway.amazonaws.com", - "SourceArn": { - "Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ServerlessHttpApi}/authorizers/*" + "AnnotationsRestApi": { + "Type": "AWS::Serverless::Api", + "Properties": { + "StageName": "Prod", + "Auth": { + "Authorizers": { + "RestApiLambdaAuthorizer": { + "FunctionArn": { + "Fn::GetAtt": [ + "RestApiAuthorizer", + "Arn" + ] + }, + "Identity": { + "Header": "Authorization" + }, + "FunctionPayloadType": "TOKEN" + } + } } + }, + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" } }, - "ServerlessHttpApi": { - "Type": "AWS::ApiGatewayV2::Api", - "Properties": { - "Name": "TestCustomAuthorizerApi", - "ProtocolType": "HTTP", - "Description": "HTTP API with custom Lambda authorizer for testing FromCustomAuthorizer attribute" - } - }, - "HttpApiAuthorizer": { - "Type": "AWS::ApiGatewayV2::Authorizer", + "CustomAuthorizer": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, "Properties": { - "ApiId": { - "Ref": "ServerlessHttpApi" - }, - "AuthorizerType": "REQUEST", - "AuthorizerUri": { - "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${CustomAuthorizer.Arn}/invocations" - }, - "AuthorizerPayloadFormatVersion": "2.0", - "EnableSimpleResponses": true, - "IdentitySource": [ - "$request.header.authorization" + "Runtime": "dotnet8", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" ], - "Name": "CustomLambdaAuthorizer" + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.AuthorizerFunction_HttpApiAuthorize_Generated::HttpApiAuthorize" } }, - "ProtectedEndpoint": { + "RestApiAuthorizer": { "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, "Properties": { - "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_GetProtectedData_Generated::GetProtectedData", - "Description": "Protected endpoint demonstrating FromCustomAuthorizer attribute", - "PackageType": "Zip", - "CodeUri": ".", "Runtime": "dotnet8", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" - ] - } - }, - "ProtectedEndpointPermission": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "FunctionName": { - "Ref": "ProtectedEndpoint" - }, - "Action": "lambda:InvokeFunction", - "Principal": "apigateway.amazonaws.com", - "SourceArn": { - "Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ServerlessHttpApi}/*/*/api/protected" - } + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.AuthorizerFunction_RestApiAuthorize_Generated::RestApiAuthorize" } }, - "ProtectedEndpointIntegration": { - "Type": "AWS::ApiGatewayV2::Integration", - "Properties": { - "ApiId": { - "Ref": "ServerlessHttpApi" - }, - "IntegrationType": "AWS_PROXY", - "IntegrationUri": { - "Fn::GetAtt": [ - "ProtectedEndpoint", - "Arn" + "ProtectedEndpoint": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "Auth.Authorizer", + "ApiId.Ref" ] - }, - "PayloadFormatVersion": "2.0" - } - }, - "ProtectedEndpointRoute": { - "Type": "AWS::ApiGatewayV2::Route", - "Properties": { - "ApiId": { - "Ref": "ServerlessHttpApi" - }, - "RouteKey": "GET /api/protected", - "AuthorizationType": "CUSTOM", - "AuthorizerId": { - "Ref": "HttpApiAuthorizer" - }, - "Target": { - "Fn::Sub": "integrations/${ProtectedEndpointIntegration}" } - } - }, - "GetUserInfo": { - "Type": "AWS::Serverless::Function", + }, "Properties": { - "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_GetUserInfo_Generated::GetUserInfo", - "Description": "Returns user info from custom authorizer context", - "PackageType": "Zip", - "CodeUri": ".", "Runtime": "dotnet8", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" - ] - } - }, - "GetUserInfoPermission": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "FunctionName": { - "Ref": "GetUserInfo" - }, - "Action": "lambda:InvokeFunction", - "Principal": "apigateway.amazonaws.com", - "SourceArn": { - "Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ServerlessHttpApi}/*/*/api/user-info" + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_GetProtectedData_Generated::GetProtectedData", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/protected", + "Method": "GET", + "Auth": { + "Authorizer": "HttpApiLambdaAuthorizer" + }, + "ApiId": { + "Ref": "AnnotationsHttpApi" + } + } + } } } }, - "GetUserInfoIntegration": { - "Type": "AWS::ApiGatewayV2::Integration", - "Properties": { - "ApiId": { - "Ref": "ServerlessHttpApi" - }, - "IntegrationType": "AWS_PROXY", - "IntegrationUri": { - "Fn::GetAtt": [ - "GetUserInfo", - "Arn" + "GetUserInfo": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "Auth.Authorizer", + "ApiId.Ref" ] - }, - "PayloadFormatVersion": "2.0" - } - }, - "GetUserInfoRoute": { - "Type": "AWS::ApiGatewayV2::Route", - "Properties": { - "ApiId": { - "Ref": "ServerlessHttpApi" - }, - "RouteKey": "GET /api/user-info", - "AuthorizationType": "CUSTOM", - "AuthorizerId": { - "Ref": "HttpApiAuthorizer" - }, - "Target": { - "Fn::Sub": "integrations/${GetUserInfoIntegration}" } - } - }, - "HealthCheck": { - "Type": "AWS::Serverless::Function", + }, "Properties": { - "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_HealthCheck_Generated::HealthCheck", - "Description": "Simple health check endpoint without authorizer", - "PackageType": "Zip", - "CodeUri": ".", "Runtime": "dotnet8", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" - ] - } - }, - "HealthCheckPermission": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "FunctionName": { - "Ref": "HealthCheck" - }, - "Action": "lambda:InvokeFunction", - "Principal": "apigateway.amazonaws.com", - "SourceArn": { - "Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ServerlessHttpApi}/*/*/api/health" + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_GetUserInfo_Generated::GetUserInfo", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/user-info", + "Method": "GET", + "Auth": { + "Authorizer": "HttpApiLambdaAuthorizer" + }, + "ApiId": { + "Ref": "AnnotationsHttpApi" + } + } + } } } }, - "HealthCheckIntegration": { - "Type": "AWS::ApiGatewayV2::Integration", - "Properties": { - "ApiId": { - "Ref": "ServerlessHttpApi" - }, - "IntegrationType": "AWS_PROXY", - "IntegrationUri": { - "Fn::GetAtt": [ - "HealthCheck", - "Arn" + "HealthCheck": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" ] - }, - "PayloadFormatVersion": "2.0" - } - }, - "HealthCheckRoute": { - "Type": "AWS::ApiGatewayV2::Route", - "Properties": { - "ApiId": { - "Ref": "ServerlessHttpApi" - }, - "RouteKey": "GET /api/health", - "Target": { - "Fn::Sub": "integrations/${HealthCheckIntegration}" } - } - }, - "HttpApiStage": { - "Type": "AWS::ApiGatewayV2::Stage", + }, "Properties": { - "ApiId": { - "Ref": "ServerlessHttpApi" - }, - "StageName": "$default", - "AutoDeploy": true - } - }, - "RestApiAuthorizer": { - "Type": "AWS::Serverless::Function", - "Properties": { - "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.AuthorizerFunction::RestApiAuthorize", - "Description": "REST API Lambda Authorizer that validates requests and provides custom context values", + "Runtime": "dotnet8", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" - ] - } - }, - "ServerlessRestApi": { - "Type": "AWS::ApiGateway::RestApi", - "Properties": { - "Name": "TestCustomAuthorizerRestApi", - "Description": "REST API with custom Lambda authorizer for testing FromCustomAuthorizer attribute" - } - }, - "RestApiLambdaAuthorizer": { - "Type": "AWS::ApiGateway::Authorizer", - "Properties": { - "Name": "RestApiLambdaAuthorizer", - "RestApiId": { - "Ref": "ServerlessRestApi" - }, - "Type": "TOKEN", - "AuthorizerUri": { - "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${RestApiAuthorizer.Arn}/invocations" - }, - "IdentitySource": "method.request.header.Authorization" - } - }, - "RestApiAuthorizerPermission": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "FunctionName": { - "Ref": "RestApiAuthorizer" - }, - "Action": "lambda:InvokeFunction", - "Principal": "apigateway.amazonaws.com", - "SourceArn": { - "Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ServerlessRestApi}/authorizers/*" + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_HealthCheck_Generated::HealthCheck", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/health", + "Method": "GET" + } + } } } }, "RestUserInfo": { "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "Auth.Authorizer", + "RestApiId.Ref" + ] + } + }, "Properties": { - "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_GetRestUserInfo_Generated::GetRestUserInfo", - "Description": "REST API endpoint demonstrating FromCustomAuthorizer attribute", - "PackageType": "Zip", - "CodeUri": ".", "Runtime": "dotnet8", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" - ] - } - }, - "RestUserInfoPermission": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "FunctionName": { - "Ref": "RestUserInfo" - }, - "Action": "lambda:InvokeFunction", - "Principal": "apigateway.amazonaws.com", - "SourceArn": { - "Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ServerlessRestApi}/*/*/*" - } - } - }, - "RestApiResource": { - "Type": "AWS::ApiGateway::Resource", - "Properties": { - "RestApiId": { - "Ref": "ServerlessRestApi" - }, - "ParentId": { - "Fn::GetAtt": [ - "ServerlessRestApi", - "RootResourceId" - ] - }, - "PathPart": "api" - } - }, - "RestApiUserInfoResource": { - "Type": "AWS::ApiGateway::Resource", - "Properties": { - "RestApiId": { - "Ref": "ServerlessRestApi" - }, - "ParentId": { - "Ref": "RestApiResource" - }, - "PathPart": "rest-user-info" - } - }, - "RestApiUserInfoMethod": { - "Type": "AWS::ApiGateway::Method", - "Properties": { - "RestApiId": { - "Ref": "ServerlessRestApi" - }, - "ResourceId": { - "Ref": "RestApiUserInfoResource" - }, - "HttpMethod": "GET", - "AuthorizationType": "CUSTOM", - "AuthorizerId": { - "Ref": "RestApiLambdaAuthorizer" - }, - "Integration": { - "Type": "AWS_PROXY", - "IntegrationHttpMethod": "POST", - "Uri": { - "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${RestUserInfo.Arn}/invocations" + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_GetRestUserInfo_Generated::GetRestUserInfo", + "Events": { + "RootGet": { + "Type": "Api", + "Properties": { + "Path": "/api/rest-user-info", + "Method": "GET", + "Auth": { + "Authorizer": "RestApiLambdaAuthorizer" + }, + "RestApiId": { + "Ref": "AnnotationsRestApi" + } + } } } } }, - "RestApiDeployment": { - "Type": "AWS::ApiGateway::Deployment", - "DependsOn": [ - "RestApiUserInfoMethod" - ], - "Properties": { - "RestApiId": { - "Ref": "ServerlessRestApi" - } - } - }, - "RestApiStage": { - "Type": "AWS::ApiGateway::Stage", - "Properties": { - "RestApiId": { - "Ref": "ServerlessRestApi" - }, - "DeploymentId": { - "Ref": "RestApiDeployment" - }, - "StageName": "Prod" - } - }, "HttpApiV1UserInfo": { "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "PayloadFormatVersion", + "Auth.Authorizer", + "ApiId.Ref" + ] + } + }, "Properties": { - "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_GetHttpApiV1UserInfo_Generated::GetHttpApiV1UserInfo", - "Description": "HTTP API v1 endpoint demonstrating FromCustomAuthorizer attribute with payload format 1.0", - "PackageType": "Zip", - "CodeUri": ".", "Runtime": "dotnet8", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" - ] - } - }, - "HttpApiV1UserInfoPermission": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "FunctionName": { - "Ref": "HttpApiV1UserInfo" - }, - "Action": "lambda:InvokeFunction", - "Principal": "apigateway.amazonaws.com", - "SourceArn": { - "Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ServerlessHttpApi}/*/*/api/http-v1-user-info" - } - } - }, - "HttpApiV1UserInfoIntegration": { - "Type": "AWS::ApiGatewayV2::Integration", - "Properties": { - "ApiId": { - "Ref": "ServerlessHttpApi" - }, - "IntegrationType": "AWS_PROXY", - "IntegrationUri": { - "Fn::GetAtt": [ - "HttpApiV1UserInfo", - "Arn" - ] - }, - "PayloadFormatVersion": "1.0" - } - }, - "HttpApiV1UserInfoRoute": { - "Type": "AWS::ApiGatewayV2::Route", - "Properties": { - "ApiId": { - "Ref": "ServerlessHttpApi" - }, - "RouteKey": "GET /api/http-v1-user-info", - "AuthorizationType": "CUSTOM", - "AuthorizerId": { - "Ref": "HttpApiAuthorizer" - }, - "Target": { - "Fn::Sub": "integrations/${HttpApiV1UserInfoIntegration}" + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_GetHttpApiV1UserInfo_Generated::GetHttpApiV1UserInfo", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/http-v1-user-info", + "Method": "GET", + "PayloadFormatVersion": "1.0", + "Auth": { + "Authorizer": "HttpApiLambdaAuthorizer" + }, + "ApiId": { + "Ref": "AnnotationsHttpApi" + } + } + } } } }, "IHttpResultUserInfo": { "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "Auth.Authorizer", + "ApiId.Ref" + ] + } + }, "Properties": { - "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_GetIHttpResult_Generated::GetIHttpResult", - "Description": "IHttpResult endpoint demonstrating FromCustomAuthorizer with IHttpResult return type", - "PackageType": "Zip", - "CodeUri": ".", "Runtime": "dotnet8", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" - ] - } - }, - "IHttpResultUserInfoPermission": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "FunctionName": { - "Ref": "IHttpResultUserInfo" - }, - "Action": "lambda:InvokeFunction", - "Principal": "apigateway.amazonaws.com", - "SourceArn": { - "Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ServerlessHttpApi}/*/*/api/ihttpresult-user-info" - } - } - }, - "IHttpResultUserInfoIntegration": { - "Type": "AWS::ApiGatewayV2::Integration", - "Properties": { - "ApiId": { - "Ref": "ServerlessHttpApi" - }, - "IntegrationType": "AWS_PROXY", - "IntegrationUri": { - "Fn::GetAtt": [ - "IHttpResultUserInfo", - "Arn" - ] - }, - "PayloadFormatVersion": "2.0" - } - }, - "IHttpResultUserInfoRoute": { - "Type": "AWS::ApiGatewayV2::Route", - "Properties": { - "ApiId": { - "Ref": "ServerlessHttpApi" - }, - "RouteKey": "GET /api/ihttpresult-user-info", - "AuthorizationType": "CUSTOM", - "AuthorizerId": { - "Ref": "HttpApiAuthorizer" - }, - "Target": { - "Fn::Sub": "integrations/${IHttpResultUserInfoIntegration}" + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_GetIHttpResult_Generated::GetIHttpResult", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/ihttpresult-user-info", + "Method": "GET", + "Auth": { + "Authorizer": "HttpApiLambdaAuthorizer" + }, + "ApiId": { + "Ref": "AnnotationsHttpApi" + } + } + } } } }, "NonStringUserInfo": { "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "Auth.Authorizer", + "ApiId.Ref" + ] + } + }, "Properties": { - "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_GetNonStringUserInfo_Generated::GetNonStringUserInfo", - "Description": "HTTP API v2 endpoint demonstrating FromCustomAuthorizer with non-string types (int, bool, double)", - "PackageType": "Zip", - "CodeUri": ".", "Runtime": "dotnet8", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" - ] - } - }, - "NonStringUserInfoPermission": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "FunctionName": { - "Ref": "NonStringUserInfo" - }, - "Action": "lambda:InvokeFunction", - "Principal": "apigateway.amazonaws.com", - "SourceArn": { - "Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ServerlessHttpApi}/*/*/api/nonstring-user-info" - } - } - }, - "NonStringUserInfoIntegration": { - "Type": "AWS::ApiGatewayV2::Integration", - "Properties": { - "ApiId": { - "Ref": "ServerlessHttpApi" - }, - "IntegrationType": "AWS_PROXY", - "IntegrationUri": { - "Fn::GetAtt": [ - "NonStringUserInfo", - "Arn" - ] - }, - "PayloadFormatVersion": "2.0" - } - }, - "NonStringUserInfoRoute": { - "Type": "AWS::ApiGatewayV2::Route", - "Properties": { - "ApiId": { - "Ref": "ServerlessHttpApi" - }, - "RouteKey": "GET /api/nonstring-user-info", - "AuthorizationType": "CUSTOM", - "AuthorizerId": { - "Ref": "HttpApiAuthorizer" - }, - "Target": { - "Fn::Sub": "integrations/${NonStringUserInfoIntegration}" + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_GetNonStringUserInfo_Generated::GetNonStringUserInfo", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/nonstring-user-info", + "Method": "GET", + "Auth": { + "Authorizer": "HttpApiLambdaAuthorizer" + }, + "ApiId": { + "Ref": "AnnotationsHttpApi" + } + } + } } } } - }, - "Outputs": { - "ApiUrl": { - "Description": "HTTP API endpoint URL", - "Value": { - "Fn::Sub": "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com" - } - }, - "ProtectedEndpointUrl": { - "Description": "Protected endpoint URL (requires Authorization header)", - "Value": { - "Fn::Sub": "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/api/protected" - } - }, - "UserInfoUrl": { - "Description": "User info endpoint URL (requires Authorization header)", - "Value": { - "Fn::Sub": "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/api/user-info" - } - }, - "HealthCheckUrl": { - "Description": "Health check endpoint URL (no auth required)", - "Value": { - "Fn::Sub": "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/api/health" - } - }, - "RestApiUrl": { - "Description": "REST API endpoint URL", - "Value": { - "Fn::Sub": "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod" - } - }, - "RestUserInfoUrl": { - "Description": "REST API User info endpoint URL (requires Authorization header)", - "Value": { - "Fn::Sub": "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/api/rest-user-info" - } - }, - "HttpApiV1UserInfoUrl": { - "Description": "HTTP API v1 User info endpoint URL (requires Authorization header, uses payload format 1.0)", - "Value": { - "Fn::Sub": "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/api/http-v1-user-info" - } - }, - "IHttpResultUserInfoUrl": { - "Description": "IHttpResult User info endpoint URL (requires Authorization header, demonstrates IHttpResult return type)", - "Value": { - "Fn::Sub": "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/api/ihttpresult-user-info" - } - }, - "NonStringUserInfoUrl": { - "Description": "Non-string types User info endpoint URL (requires Authorization header, demonstrates int/bool/double type conversion)", - "Value": { - "Fn::Sub": "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/api/nonstring-user-info" - } - } } } \ No newline at end of file diff --git a/Libraries/test/TestCustomAuthorizerApp/src/Function/Function.cs b/Libraries/test/TestCustomAuthorizerApp/src/Function/Function.cs new file mode 100644 index 000000000..8bfb9c2e7 --- /dev/null +++ b/Libraries/test/TestCustomAuthorizerApp/src/Function/Function.cs @@ -0,0 +1,16 @@ +using System; + +using Amazon.Lambda.Core; + +namespace Function +{ + public class Function + { + public dynamic FunctionHandler(dynamic eventTrigger) + { + Console.WriteLine(eventTrigger); + + return new {}; + } + } +} diff --git a/Libraries/test/TestCustomAuthorizerApp/src/Function/Function.csproj b/Libraries/test/TestCustomAuthorizerApp/src/Function/Function.csproj new file mode 100644 index 000000000..1c95e7ced --- /dev/null +++ b/Libraries/test/TestCustomAuthorizerApp/src/Function/Function.csproj @@ -0,0 +1,12 @@ + + + net8.0 + true + Lambda + + + + + + + diff --git a/Libraries/test/TestCustomAuthorizerApp/src/Function/serverless.template b/Libraries/test/TestCustomAuthorizerApp/src/Function/serverless.template new file mode 100644 index 000000000..9e188c148 --- /dev/null +++ b/Libraries/test/TestCustomAuthorizerApp/src/Function/serverless.template @@ -0,0 +1,389 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Transform": "AWS::Serverless-2016-10-31", + "Description": "This template is partially managed by Amazon.Lambda.Annotations (v1.9.0.0).", + "Resources": { + "AnnotationsHttpApi": { + "Type": "AWS::Serverless::HttpApi", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "Auth": { + "Authorizers": { + "HttpApiLambdaAuthorizer": { + "FunctionArn": { + "Fn::GetAtt": [ + "CustomAuthorizer", + "Arn" + ] + }, + "AuthorizerPayloadFormatVersion": "2.0", + "EnableSimpleResponses": true, + "Identity": { + "Headers": [ + "authorization" + ] + }, + "EnableFunctionDefaultPermissions": true + } + } + } + } + }, + "AnnotationsRestApi": { + "Type": "AWS::Serverless::Api", + "Properties": { + "StageName": "Prod", + "Auth": { + "Authorizers": { + "RestApiLambdaAuthorizer": { + "FunctionArn": { + "Fn::GetAtt": [ + "RestApiAuthorizer", + "Arn" + ] + }, + "Identity": { + "Header": "Authorization" + }, + "FunctionPayloadType": "TOKEN" + } + } + } + }, + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + } + }, + "ProtectedEndpoint": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "Auth.Authorizer", + "ApiId.Ref" + ] + } + }, + "Properties": { + "Runtime": "dotnet8", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_GetProtectedData_Generated::GetProtectedData", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/protected", + "Method": "GET", + "Auth": { + "Authorizer": "HttpApiLambdaAuthorizer" + }, + "ApiId": { + "Ref": "AnnotationsHttpApi" + } + } + } + } + } + }, + "GetUserInfo": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "Auth.Authorizer", + "ApiId.Ref" + ] + } + }, + "Properties": { + "Runtime": "dotnet8", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_GetUserInfo_Generated::GetUserInfo", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/user-info", + "Method": "GET", + "Auth": { + "Authorizer": "HttpApiLambdaAuthorizer" + }, + "ApiId": { + "Ref": "AnnotationsHttpApi" + } + } + } + } + } + }, + "HealthCheck": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "Runtime": "dotnet8", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_HealthCheck_Generated::HealthCheck", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/health", + "Method": "GET" + } + } + } + } + }, + "RestUserInfo": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "Auth.Authorizer", + "RestApiId.Ref" + ] + } + }, + "Properties": { + "Runtime": "dotnet8", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_GetRestUserInfo_Generated::GetRestUserInfo", + "Events": { + "RootGet": { + "Type": "Api", + "Properties": { + "Path": "/api/rest-user-info", + "Method": "GET", + "Auth": { + "Authorizer": "RestApiLambdaAuthorizer" + }, + "RestApiId": { + "Ref": "AnnotationsRestApi" + } + } + } + } + } + }, + "HttpApiV1UserInfo": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "PayloadFormatVersion", + "Auth.Authorizer", + "ApiId.Ref" + ] + } + }, + "Properties": { + "Runtime": "dotnet8", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_GetHttpApiV1UserInfo_Generated::GetHttpApiV1UserInfo", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/http-v1-user-info", + "Method": "GET", + "PayloadFormatVersion": "1.0", + "Auth": { + "Authorizer": "HttpApiLambdaAuthorizer" + }, + "ApiId": { + "Ref": "AnnotationsHttpApi" + } + } + } + } + } + }, + "IHttpResultUserInfo": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "Auth.Authorizer", + "ApiId.Ref" + ] + } + }, + "Properties": { + "Runtime": "dotnet8", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_GetIHttpResult_Generated::GetIHttpResult", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/ihttpresult-user-info", + "Method": "GET", + "Auth": { + "Authorizer": "HttpApiLambdaAuthorizer" + }, + "ApiId": { + "Ref": "AnnotationsHttpApi" + } + } + } + } + } + }, + "NonStringUserInfo": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "Auth.Authorizer", + "ApiId.Ref" + ] + } + }, + "Properties": { + "Runtime": "dotnet8", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_GetNonStringUserInfo_Generated::GetNonStringUserInfo", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/nonstring-user-info", + "Method": "GET", + "Auth": { + "Authorizer": "HttpApiLambdaAuthorizer" + }, + "ApiId": { + "Ref": "AnnotationsHttpApi" + } + } + } + } + } + }, + "CustomAuthorizer": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "Runtime": "dotnet8", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.AuthorizerFunction_HttpApiAuthorize_Generated::HttpApiAuthorize" + } + }, + "RestApiAuthorizer": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "Runtime": "dotnet8", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.AuthorizerFunction_RestApiAuthorize_Generated::RestApiAuthorize" + } + } + } +} \ No newline at end of file diff --git a/Libraries/test/TestExecutableServerlessApp/serverless.template b/Libraries/test/TestExecutableServerlessApp/serverless.template index ac43959b7..1912a4498 100644 --- a/Libraries/test/TestExecutableServerlessApp/serverless.template +++ b/Libraries/test/TestExecutableServerlessApp/serverless.template @@ -22,19 +22,10 @@ } }, "Resources": { - "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderGenerated": { + "TestServerlessAppVoidExampleVoidReturnGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } + "Tool": "Amazon.Lambda.Annotations" }, "Properties": { "MemorySize": 512, @@ -51,33 +42,15 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "OkResponseWithHeader" - } - }, - "Events": { - "RootGet": { - "Type": "Api", - "Properties": { - "Path": "/okresponsewithheader/{x}", - "Method": "GET" - } + "ANNOTATIONS_HANDLER": "VoidReturn" } } } }, - "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderAsyncGenerated": { + "TestServerlessAppDynamicExampleDynamicReturnGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } + "Tool": "Amazon.Lambda.Annotations" }, "Properties": { "MemorySize": 512, @@ -94,33 +67,15 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "OkResponseWithHeaderAsync" - } - }, - "Events": { - "RootGet": { - "Type": "Api", - "Properties": { - "Path": "/okresponsewithheaderasync/{x}", - "Method": "GET" - } + "ANNOTATIONS_HANDLER": "DynamicReturn" } } } }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2Generated": { + "TestServerlessAppDynamicExampleDynamicInputGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } + "Tool": "Amazon.Lambda.Annotations" }, "Properties": { "MemorySize": 512, @@ -137,21 +92,12 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV2" - } - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/notfoundwithheaderv2/{x}", - "Method": "GET" - } + "ANNOTATIONS_HANDLER": "DynamicInput" } } } }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2AsyncGenerated": { + "GreeterSayHello": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -161,12 +107,13 @@ "SyncedEventProperties": { "RootGet": [ "Path", - "Method" + "Method", + "PayloadFormatVersion" ] } }, "Properties": { - "MemorySize": 512, + "MemorySize": 1024, "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" @@ -180,21 +127,22 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV2Async" + "ANNOTATIONS_HANDLER": "SayHello" } }, "Events": { "RootGet": { "Type": "HttpApi", "Properties": { - "Path": "/notfoundwithheaderv2async/{x}", - "Method": "GET" + "Path": "/Greeter/SayHello", + "Method": "GET", + "PayloadFormatVersion": "1.0" } } } } }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1Generated": { + "GreeterSayHelloAsync": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -211,7 +159,7 @@ }, "Properties": { "MemorySize": 512, - "Timeout": 30, + "Timeout": 50, "Policies": [ "AWSLambdaBasicExecutionRole" ], @@ -224,14 +172,14 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV1" + "ANNOTATIONS_HANDLER": "SayHelloAsync" } }, "Events": { "RootGet": { "Type": "HttpApi", "Properties": { - "Path": "/notfoundwithheaderv1/{x}", + "Path": "/Greeter/SayHelloAsync", "Method": "GET", "PayloadFormatVersion": "1.0" } @@ -239,52 +187,29 @@ } } }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1AsyncGenerated": { + "ToLower": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method", - "PayloadFormatVersion" - ] - } + "Tool": "Amazon.Lambda.Annotations" }, "Properties": { + "Runtime": "provided.al2", + "CodeUri": ".", "MemorySize": 512, "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestExecutableServerlessApp" - ] - }, + "PackageType": "Zip", + "Handler": "TestExecutableServerlessApp", "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV1Async" - } - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/notfoundwithheaderv1async/{x}", - "Method": "GET", - "PayloadFormatVersion": "1.0" - } + "ANNOTATIONS_HANDLER": "ToLower" } } } }, - "TestServerlessAppDynamicExampleDynamicReturnGenerated": { + "TestServerlessAppIntrinsicExampleHasIntrinsicGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations" @@ -304,37 +229,34 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "DynamicReturn" + "ANNOTATIONS_HANDLER": "HasIntrinsic" } } } }, - "TestServerlessAppDynamicExampleDynamicInputGenerated": { + "TestServerlessAppParameterlessMethodWithResponseNoParameterWithResponseGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations" }, "Properties": { + "Runtime": "provided.al2", + "CodeUri": ".", "MemorySize": 512, "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestExecutableServerlessApp" - ] - }, + "PackageType": "Zip", + "Handler": "TestExecutableServerlessApp", "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "DynamicInput" + "ANNOTATIONS_HANDLER": "NoParameterWithResponse" } } } }, - "GreeterSayHello": { + "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -344,13 +266,12 @@ "SyncedEventProperties": { "RootGet": [ "Path", - "Method", - "PayloadFormatVersion" + "Method" ] } }, "Properties": { - "MemorySize": 1024, + "MemorySize": 512, "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" @@ -364,22 +285,21 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "SayHello" + "ANNOTATIONS_HANDLER": "OkResponseWithHeader" } }, "Events": { "RootGet": { - "Type": "HttpApi", + "Type": "Api", "Properties": { - "Path": "/Greeter/SayHello", - "Method": "GET", - "PayloadFormatVersion": "1.0" + "Path": "/okresponsewithheader/{x}", + "Method": "GET" } } } } }, - "GreeterSayHelloAsync": { + "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderAsyncGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -389,14 +309,13 @@ "SyncedEventProperties": { "RootGet": [ "Path", - "Method", - "PayloadFormatVersion" + "Method" ] } }, "Properties": { "MemorySize": 512, - "Timeout": 50, + "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" ], @@ -409,25 +328,33 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "SayHelloAsync" + "ANNOTATIONS_HANDLER": "OkResponseWithHeaderAsync" } }, "Events": { "RootGet": { - "Type": "HttpApi", + "Type": "Api", "Properties": { - "Path": "/Greeter/SayHelloAsync", - "Method": "GET", - "PayloadFormatVersion": "1.0" + "Path": "/okresponsewithheaderasync/{x}", + "Method": "GET" } } } } }, - "TestServerlessAppIntrinsicExampleHasIntrinsicGenerated": { + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2Generated": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations" + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } }, "Properties": { "MemorySize": 512, @@ -444,12 +371,21 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "HasIntrinsic" + "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV2" + } + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/notfoundwithheaderv2/{x}", + "Method": "GET" + } } } } }, - "TestServerlessAppNullableReferenceTypeExampleNullableHeaderHttpApiGenerated": { + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2AsyncGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -478,65 +414,111 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "NullableHeaderHttpApi" + "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV2Async" } }, "Events": { "RootGet": { "Type": "HttpApi", "Properties": { - "Path": "/nullableheaderhttpapi", + "Path": "/notfoundwithheaderv2async/{x}", "Method": "GET" } } } } }, - "TestServerlessAppParameterlessMethodsNoParameterGenerated": { + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1Generated": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations" + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "PayloadFormatVersion" + ] + } }, "Properties": { - "Runtime": "provided.al2", - "CodeUri": ".", "MemorySize": 512, "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" ], - "PackageType": "Zip", - "Handler": "TestExecutableServerlessApp", + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestExecutableServerlessApp" + ] + }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "NoParameter" + "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV1" + } + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/notfoundwithheaderv1/{x}", + "Method": "GET", + "PayloadFormatVersion": "1.0" + } } } } }, - "TestServerlessAppParameterlessMethodWithResponseNoParameterWithResponseGenerated": { + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1AsyncGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations" + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "PayloadFormatVersion" + ] + } }, "Properties": { - "Runtime": "provided.al2", - "CodeUri": ".", "MemorySize": 512, "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" ], - "PackageType": "Zip", - "Handler": "TestExecutableServerlessApp", + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestExecutableServerlessApp" + ] + }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "NoParameterWithResponse" + "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV1Async" + } + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/notfoundwithheaderv1async/{x}", + "Method": "GET", + "PayloadFormatVersion": "1.0" + } } } } }, - "TestExecutableServerlessAppSourceGenerationSerializationExampleGetPersonGenerated": { + "TestServerlessAppNullableReferenceTypeExampleNullableHeaderHttpApiGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -565,14 +547,14 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "GetPerson" + "ANNOTATIONS_HANDLER": "NullableHeaderHttpApi" } }, "Events": { "RootGet": { - "Type": "Api", + "Type": "HttpApi", "Properties": { - "Path": "/", + "Path": "/nullableheaderhttpapi", "Method": "GET" } } @@ -604,7 +586,7 @@ } } }, - "ToLower": { + "TestServerlessAppParameterlessMethodsNoParameterGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations" @@ -621,7 +603,7 @@ "Handler": "TestExecutableServerlessApp", "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "ToLower" + "ANNOTATIONS_HANDLER": "NoParameter" } } } @@ -651,10 +633,19 @@ } } }, - "TestServerlessAppVoidExampleVoidReturnGenerated": { + "TestExecutableServerlessAppSourceGenerationSerializationExampleGetPersonGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations" + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } }, "Properties": { "MemorySize": 512, @@ -671,7 +662,16 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "VoidReturn" + "ANNOTATIONS_HANDLER": "GetPerson" + } + }, + "Events": { + "RootGet": { + "Type": "Api", + "Properties": { + "Path": "/", + "Method": "GET" + } } } } diff --git a/Libraries/test/TestServerlessApp.IntegrationTests/DeploymentScript.ps1 b/Libraries/test/TestServerlessApp.IntegrationTests/DeploymentScript.ps1 index e47405192..7a5cd5644 100644 --- a/Libraries/test/TestServerlessApp.IntegrationTests/DeploymentScript.ps1 +++ b/Libraries/test/TestServerlessApp.IntegrationTests/DeploymentScript.ps1 @@ -58,6 +58,16 @@ try { throw "Failed to create the following bucket: $identifier" } + # Add TestQueue resource to serverless.template for integration testing + # The source generator creates an Fn::GetAtt reference to TestQueue but doesn't define the resource itself + $templatePath = ".\serverless.template" + $template = Get-Content $templatePath | Out-String | ConvertFrom-Json + if (-not $template.Resources.PSObject.Properties['TestQueue']) { + $template.Resources | Add-Member -NotePropertyName "TestQueue" -NotePropertyValue @{ Type = "AWS::SQS::Queue" } -Force + $template | ConvertTo-Json -Depth 100 | Set-Content $templatePath + Write-Host "Added TestQueue resource to serverless.template" + } + dotnet restore Write-Host "Creating CloudFormation Stack $identifier, Architecture $arch, Runtime $runtime" dotnet lambda deploy-serverless --template-parameters "ArchitectureTypeParameter=$arch" diff --git a/Libraries/test/TestServerlessApp.IntegrationTests/IntegrationTestContextFixture.cs b/Libraries/test/TestServerlessApp.IntegrationTests/IntegrationTestContextFixture.cs index 20ba65a32..3b6a5ceb3 100644 --- a/Libraries/test/TestServerlessApp.IntegrationTests/IntegrationTestContextFixture.cs +++ b/Libraries/test/TestServerlessApp.IntegrationTests/IntegrationTestContextFixture.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; @@ -43,20 +44,40 @@ public IntegrationTestContextFixture() public async Task InitializeAsync() { var scriptPath = Path.Combine("..", "..", "..", "DeploymentScript.ps1"); + Console.WriteLine($"[IntegrationTest] Running deployment script: {scriptPath}"); await CommandLineWrapper.RunAsync($"pwsh {scriptPath}"); + Console.WriteLine("[IntegrationTest] Deployment script completed successfully."); _stackName = GetStackName(); _bucketName = GetBucketName(); - Assert.False(string.IsNullOrEmpty(_stackName)); - Assert.False(string.IsNullOrEmpty(_bucketName)); - - RestApiUrlPrefix = await _cloudFormationHelper.GetOutputValueAsync(_stackName, "RestApiURL"); - HttpApiUrlPrefix = await _cloudFormationHelper.GetOutputValueAsync(_stackName, "HttpApiURL"); - TestQueueARN = await _cloudFormationHelper.GetOutputValueAsync(_stackName, "TestQueueARN"); + Console.WriteLine($"[IntegrationTest] Stack name: '{_stackName}', Bucket name: '{_bucketName}'"); + Assert.False(string.IsNullOrEmpty(_stackName), "Stack name should not be empty"); + Assert.False(string.IsNullOrEmpty(_bucketName), "Bucket name should not be empty"); + + // Check stack status before querying resources + var stackStatus = await _cloudFormationHelper.GetStackStatusAsync(_stackName); + Console.WriteLine($"[IntegrationTest] Stack status after deployment: {stackStatus}"); + Assert.NotNull(stackStatus); + Assert.Equal(StackStatus.CREATE_COMPLETE, stackStatus); + + // Dynamically construct API URLs from stack resource physical IDs + // since the serverless.template is managed by the source generator and may not have Outputs + var region = "us-west-2"; + Console.WriteLine($"[IntegrationTest] Querying stack resources for '{_stackName}'..."); + var httpApiId = await _cloudFormationHelper.GetResourcePhysicalIdAsync(_stackName, "ServerlessHttpApi"); + var restApiId = await _cloudFormationHelper.GetResourcePhysicalIdAsync(_stackName, "AnnotationsRestApi"); + Console.WriteLine($"[IntegrationTest] ServerlessHttpApi: {httpApiId}, AnnotationsRestApi: {restApiId}"); + HttpApiUrlPrefix = $"https://{httpApiId}.execute-api.{region}.amazonaws.com"; + RestApiUrlPrefix = $"https://{restApiId}.execute-api.{region}.amazonaws.com/Prod"; + + // Get the SQS queue ARN from the physical resource ID (which is the queue URL) + var queueUrl = await _cloudFormationHelper.GetResourcePhysicalIdAsync(_stackName, "TestQueue"); + Console.WriteLine($"[IntegrationTest] TestQueue URL: {queueUrl}"); + TestQueueARN = ConvertSqsUrlToArn(queueUrl); LambdaFunctions = await LambdaHelper.FilterByCloudFormationStackAsync(_stackName); + Console.WriteLine($"[IntegrationTest] Found {LambdaFunctions.Count} Lambda functions: {string.Join(", ", LambdaFunctions.Select(f => f.Name ?? "(null)"))}"); - Assert.Equal(StackStatus.CREATE_COMPLETE, await _cloudFormationHelper.GetStackStatusAsync(_stackName)); - Assert.True(await _s3Helper.BucketExistsAsync(_bucketName)); + Assert.True(await _s3Helper.BucketExistsAsync(_bucketName), $"S3 bucket {_bucketName} should exist"); Assert.Equal(34, LambdaFunctions.Count); Assert.False(string.IsNullOrEmpty(RestApiUrlPrefix)); Assert.False(string.IsNullOrEmpty(RestApiUrlPrefix)); @@ -69,11 +90,27 @@ public async Task InitializeAsync() public async Task DisposeAsync() { - await _cloudFormationHelper.DeleteStackAsync(_stackName); - Assert.True(await _cloudFormationHelper.IsDeletedAsync(_stackName), $"The stack '{_stackName}' still exists and will have to be manually deleted from the AWS console."); - - await _s3Helper.DeleteBucketAsync(_bucketName); - Assert.False(await _s3Helper.BucketExistsAsync(_bucketName), $"The bucket '{_bucketName}' still exists and will have to be manually deleted from the AWS console."); + if (!string.IsNullOrEmpty(_stackName)) + { + Console.WriteLine($"[IntegrationTest] Cleaning up stack '{_stackName}'..."); + await _cloudFormationHelper.DeleteStackAsync(_stackName); + Assert.True(await _cloudFormationHelper.IsDeletedAsync(_stackName), $"The stack '{_stackName}' still exists and will have to be manually deleted from the AWS console."); + } + else + { + Console.WriteLine("[IntegrationTest] WARNING: Stack name is null/empty, skipping stack deletion."); + } + + if (!string.IsNullOrEmpty(_bucketName)) + { + Console.WriteLine($"[IntegrationTest] Cleaning up bucket '{_bucketName}'..."); + await _s3Helper.DeleteBucketAsync(_bucketName); + Assert.False(await _s3Helper.BucketExistsAsync(_bucketName), $"The bucket '{_bucketName}' still exists and will have to be manually deleted from the AWS console."); + } + else + { + Console.WriteLine("[IntegrationTest] WARNING: Bucket name is null/empty, skipping bucket deletion."); + } var filePath = Path.Combine("..", "..", "..", "..", "TestServerlessApp", "aws-lambda-tools-defaults.json"); var token = JObject.Parse(await File.ReadAllTextAsync(filePath)); @@ -95,5 +132,21 @@ private string GetBucketName() var token = JObject.Parse(File.ReadAllText(filePath))["s3-bucket"]; return token.ToObject(); } + + /// + /// Converts an SQS queue URL (e.g. https://sqs.us-west-2.amazonaws.com/123456789012/queue-name) + /// to an ARN (e.g. arn:aws:sqs:us-west-2:123456789012:queue-name). + /// + private static string ConvertSqsUrlToArn(string queueUrl) + { + // SQS URL format: https://sqs.{region}.amazonaws.com/{account-id}/{queue-name} + var uri = new Uri(queueUrl); + var host = uri.Host; // sqs.us-west-2.amazonaws.com + var segments = uri.AbsolutePath.Trim('/').Split('/'); // [account-id, queue-name] + var region = host.Split('.')[1]; // us-west-2 + var accountId = segments[0]; + var queueName = segments[1]; + return $"arn:aws:sqs:{region}:{accountId}:{queueName}"; + } } } diff --git a/Libraries/test/TestServerlessApp/aws-lambda-tools-defaults.json b/Libraries/test/TestServerlessApp/aws-lambda-tools-defaults.json index 0b96350ff..d265c7e8d 100644 --- a/Libraries/test/TestServerlessApp/aws-lambda-tools-defaults.json +++ b/Libraries/test/TestServerlessApp/aws-lambda-tools-defaults.json @@ -13,7 +13,7 @@ "template": "serverless.template", "template-parameters": "", "docker-host-build-output-dir": "./bin/Release/lambda-publish", - "s3-bucket": "test-serverless-app", - "stack-name": "test-serverless-app", - "function-architecture": "x86_64" -} \ No newline at end of file +"s3-bucket" : "test-serverless-app-707259b4", +"stack-name" : "test-serverless-app-707259b4", +"function-architecture" : "x86_64" +} diff --git a/Libraries/test/TestServerlessApp/serverless.template b/Libraries/test/TestServerlessApp/serverless.template index 0e3befbe1..f58c37098 100644 --- a/Libraries/test/TestServerlessApp/serverless.template +++ b/Libraries/test/TestServerlessApp/serverless.template @@ -1,38 +1,21 @@ { "AWSTemplateFormatVersion": "2010-09-09", "Transform": "AWS::Serverless-2016-10-31", - "Description": "An AWS Serverless Application. This template is partially managed by Amazon.Lambda.Annotations (v1.9.0.0).", - "Parameters": { - "ArchitectureTypeParameter": { - "Type": "String", - "Default": "x86_64", - "AllowedValues": [ - "x86_64", - "arm64" - ] - } - }, - "Globals": { - "Function": { - "Architectures": [ - { - "Ref": "ArchitectureTypeParameter" - } - ], - "Tags": { - "aws-tests": "TestServerlessApp", - "aws-repo": "aws-lambda-dotnet" - } - } - }, + "Description": "This template is partially managed by Amazon.Lambda.Annotations (v1.9.0.0).", "Resources": { - "TestQueue": { - "Type": "AWS::SQS::Queue" - }, - "TestServerlessAppDynamicExampleDynamicReturnGenerated": { + "TestServerlessAppCustomAuthorizerWithIHttpResultsExampleAuthorizerWithIHttpResultsGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations" + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } }, "Properties": { "MemorySize": 512, @@ -44,18 +27,37 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.DynamicExample_DynamicReturn_Generated::DynamicReturn" + "TestServerlessApp::TestServerlessApp.CustomAuthorizerWithIHttpResultsExample_AuthorizerWithIHttpResults_Generated::AuthorizerWithIHttpResults" ] + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/authorizerihttpresults", + "Method": "GET" + } + } } } }, - "TestServerlessAppDynamicExampleDynamicInputGenerated": { + "GreeterSayHello": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations" + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "PayloadFormatVersion" + ] + } }, "Properties": { - "MemorySize": 512, + "MemorySize": 1024, "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" @@ -64,12 +66,22 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.DynamicExample_DynamicInput_Generated::DynamicInput" + "TestServerlessApp::TestServerlessApp.Greeter_SayHello_Generated::SayHello" ] + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/Greeter/SayHello", + "Method": "GET", + "PayloadFormatVersion": "1.0" + } + } } } }, - "TestServerlessAppFromScratchNoApiGatewayEventsReferenceToUpperGenerated": { + "GreeterSayHelloAsync": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -79,13 +91,14 @@ "SyncedEventProperties": { "RootGet": [ "Path", - "Method" + "Method", + "PayloadFormatVersion" ] } }, "Properties": { "MemorySize": 512, - "Timeout": 30, + "Timeout": 50, "Policies": [ "AWSLambdaBasicExecutionRole" ], @@ -93,21 +106,22 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.FromScratch.NoApiGatewayEventsReference_ToUpper_Generated::ToUpper" + "TestServerlessApp::TestServerlessApp.Greeter_SayHelloAsync_Generated::SayHelloAsync" ] }, "Events": { "RootGet": { "Type": "HttpApi", "Properties": { - "Path": "/{text}", - "Method": "GET" + "Path": "/Greeter/SayHelloAsync", + "Method": "GET", + "PayloadFormatVersion": "1.0" } } } } }, - "HttpApiAuthorizerTest": { + "TestServerlessAppNullableReferenceTypeExampleNullableHeaderHttpApiGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -131,21 +145,21 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.CustomAuthorizerHttpApiExample_HttpApiAuthorizer_Generated::HttpApiAuthorizer" + "TestServerlessApp::TestServerlessApp.NullableReferenceTypeExample_NullableHeaderHttpApi_Generated::NullableHeaderHttpApi" ] }, "Events": { "RootGet": { "Type": "HttpApi", "Properties": { - "Path": "/api/authorizer", + "Path": "/nullableheaderhttpapi", "Method": "GET" } } } } }, - "SimpleCalculatorAdd": { + "TestServerlessAppFromScratchNoApiGatewayEventsReferenceToUpperGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -169,29 +183,49 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.SimpleCalculator_Add_Generated::Add" + "TestServerlessApp::TestServerlessApp.FromScratch.NoApiGatewayEventsReference_ToUpper_Generated::ToUpper" ] }, "Events": { "RootGet": { - "Type": "Api", + "Type": "HttpApi", "Properties": { - "Path": "/SimpleCalculator/Add", + "Path": "/{text}", "Method": "GET" } } } } }, - "SimpleCalculatorSubtract": { + "ToUpper": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.Sub1.Functions_ToUpper_Generated::ToUpper" + ] + } + } + }, + "TestServerlessAppComplexCalculatorAddGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", "SyncedEvents": [ - "RootGet" + "RootPost" ], "SyncedEventProperties": { - "RootGet": [ + "RootPost": [ "Path", "Method" ] @@ -207,29 +241,29 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.SimpleCalculator_Subtract_Generated::Subtract" + "TestServerlessApp::TestServerlessApp.ComplexCalculator_Add_Generated::Add" ] }, "Events": { - "RootGet": { - "Type": "Api", + "RootPost": { + "Type": "HttpApi", "Properties": { - "Path": "/SimpleCalculator/Subtract", - "Method": "GET" + "Path": "/ComplexCalculator/Add", + "Method": "POST" } } } } }, - "SimpleCalculatorMultiply": { + "TestServerlessAppComplexCalculatorSubtractGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", "SyncedEvents": [ - "RootGet" + "RootPost" ], "SyncedEventProperties": { - "RootGet": [ + "RootPost": [ "Path", "Method" ] @@ -245,21 +279,21 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.SimpleCalculator_Multiply_Generated::Multiply" + "TestServerlessApp::TestServerlessApp.ComplexCalculator_Subtract_Generated::Subtract" ] }, "Events": { - "RootGet": { - "Type": "Api", + "RootPost": { + "Type": "HttpApi", "Properties": { - "Path": "/SimpleCalculator/Multiply/{x}/{y}", - "Method": "GET" + "Path": "/ComplexCalculator/Subtract", + "Method": "POST" } } } } }, - "SimpleCalculatorDivideAsync": { + "RestAuthorizerTest": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -283,21 +317,21 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.SimpleCalculator_DivideAsync_Generated::DivideAsync" + "TestServerlessApp::TestServerlessApp.CustomAuthorizerRestExample_RestAuthorizer_Generated::RestAuthorizer" ] }, "Events": { "RootGet": { "Type": "Api", "Properties": { - "Path": "/SimpleCalculator/DivideAsync/{x}/{y}", + "Path": "/rest/authorizer", "Method": "GET" } } } } }, - "PI": { + "TestServerlessAppTaskExampleTaskReturnGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations" @@ -312,15 +346,24 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.SimpleCalculator_Pi_Generated::Pi" + "TestServerlessApp::TestServerlessApp.TaskExample_TaskReturn_Generated::TaskReturn" ] } } }, - "Random": { + "AuthNameFallbackTest": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations" + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } }, "Properties": { "MemorySize": 512, @@ -332,12 +375,21 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.SimpleCalculator_Random_Generated::Random" + "TestServerlessApp::TestServerlessApp.AuthNameFallback_GetUserId_Generated::GetUserId" ] + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/authorizer-fallback", + "Method": "GET" + } + } } } }, - "Randoms": { + "TestServerlessAppVoidExampleVoidReturnGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations" @@ -352,26 +404,22 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.SimpleCalculator_Randoms_Generated::Randoms" + "TestServerlessApp::TestServerlessApp.VoidExample_VoidReturn_Generated::VoidReturn" ] } } }, - "SQSMessageHandler": { + "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", "SyncedEvents": [ - "TestQueueEvent" + "RootGet" ], "SyncedEventProperties": { - "TestQueueEvent": [ - "Queue.Fn::GetAtt", - "BatchSize", - "FilterCriteria.Filters", - "FunctionResponseTypes", - "MaximumBatchingWindowInSeconds", - "ScalingConfig.MaximumConcurrency" + "RootGet": [ + "Path", + "Method" ] } }, @@ -379,46 +427,27 @@ "MemorySize": 512, "Timeout": 30, "Policies": [ - "AWSLambdaSQSQueueExecutionRole" + "AWSLambdaBasicExecutionRole" ], "PackageType": "Image", "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.SqsMessageProcessing_HandleMessage_Generated::HandleMessage" + "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_OkResponseWithHeader_Generated::OkResponseWithHeader" ] }, "Events": { - "TestQueueEvent": { - "Type": "SQS", + "RootGet": { + "Type": "Api", "Properties": { - "BatchSize": 50, - "FilterCriteria": { - "Filters": [ - { - "Pattern": "{ \"body\" : { \"RequestCode\" : [ \"BBBB\" ] } }" - } - ] - }, - "FunctionResponseTypes": [ - "ReportBatchItemFailures" - ], - "MaximumBatchingWindowInSeconds": 5, - "ScalingConfig": { - "MaximumConcurrency": 5 - }, - "Queue": { - "Fn::GetAtt": [ - "TestQueue", - "Arn" - ] - } + "Path": "/okresponsewithheader/{x}", + "Method": "GET" } } } } }, - "HttpApiV1AuthorizerTest": { + "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderAsyncGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -428,8 +457,7 @@ "SyncedEventProperties": { "RootGet": [ "Path", - "Method", - "PayloadFormatVersion" + "Method" ] } }, @@ -443,22 +471,21 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.CustomAuthorizerHttpApiV1Example_HttpApiV1Authorizer_Generated::HttpApiV1Authorizer" + "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_OkResponseWithHeaderAsync_Generated::OkResponseWithHeaderAsync" ] }, "Events": { "RootGet": { - "Type": "HttpApi", + "Type": "Api", "Properties": { - "Path": "/api/authorizer-v1", - "Method": "GET", - "PayloadFormatVersion": "1.0" + "Path": "/okresponsewithheaderasync/{x}", + "Method": "GET" } } } } }, - "TestServerlessAppNullableReferenceTypeExampleNullableHeaderHttpApiGenerated": { + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2Generated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -482,21 +509,21 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.NullableReferenceTypeExample_NullableHeaderHttpApi_Generated::NullableHeaderHttpApi" + "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV2_Generated::NotFoundResponseWithHeaderV2" ] }, "Events": { "RootGet": { "Type": "HttpApi", "Properties": { - "Path": "/nullableheaderhttpapi", + "Path": "/notfoundwithheaderv2/{x}", "Method": "GET" } } } } }, - "TestServerlessAppCustomAuthorizerWithIHttpResultsExampleAuthorizerWithIHttpResultsGenerated": { + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2AsyncGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -520,21 +547,21 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.CustomAuthorizerWithIHttpResultsExample_AuthorizerWithIHttpResults_Generated::AuthorizerWithIHttpResults" + "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV2Async_Generated::NotFoundResponseWithHeaderV2Async" ] }, "Events": { "RootGet": { "Type": "HttpApi", "Properties": { - "Path": "/authorizerihttpresults", + "Path": "/notfoundwithheaderv2async/{x}", "Method": "GET" } } } } }, - "GreeterSayHello": { + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1Generated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -550,7 +577,7 @@ } }, "Properties": { - "MemorySize": 1024, + "MemorySize": 512, "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" @@ -559,14 +586,14 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.Greeter_SayHello_Generated::SayHello" + "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV1_Generated::NotFoundResponseWithHeaderV1" ] }, "Events": { "RootGet": { "Type": "HttpApi", "Properties": { - "Path": "/Greeter/SayHello", + "Path": "/notfoundwithheaderv1/{x}", "Method": "GET", "PayloadFormatVersion": "1.0" } @@ -574,7 +601,7 @@ } } }, - "GreeterSayHelloAsync": { + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1AsyncGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -591,7 +618,7 @@ }, "Properties": { "MemorySize": 512, - "Timeout": 50, + "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" ], @@ -599,14 +626,14 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.Greeter_SayHelloAsync_Generated::SayHelloAsync" + "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV1Async_Generated::NotFoundResponseWithHeaderV1Async" ] }, "Events": { "RootGet": { "Type": "HttpApi", "Properties": { - "Path": "/Greeter/SayHelloAsync", + "Path": "/notfoundwithheaderv1async/{x}", "Method": "GET", "PayloadFormatVersion": "1.0" } @@ -614,7 +641,7 @@ } } }, - "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderGenerated": { + "TestServerlessAppCustomizeResponseExamplesOkResponseWithCustomSerializerGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -624,7 +651,8 @@ "SyncedEventProperties": { "RootGet": [ "Path", - "Method" + "Method", + "PayloadFormatVersion" ] } }, @@ -638,21 +666,22 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_OkResponseWithHeader_Generated::OkResponseWithHeader" + "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_OkResponseWithCustomSerializer_Generated::OkResponseWithCustomSerializer" ] }, "Events": { "RootGet": { - "Type": "Api", + "Type": "HttpApi", "Properties": { - "Path": "/okresponsewithheader/{x}", - "Method": "GET" + "Path": "/okresponsewithcustomserializerasync/{firstName}/{lastName}", + "Method": "GET", + "PayloadFormatVersion": "1.0" } } } } }, - "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderAsyncGenerated": { + "HttpApiNonString": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -676,21 +705,21 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_OkResponseWithHeaderAsync_Generated::OkResponseWithHeaderAsync" + "TestServerlessApp::TestServerlessApp.CustomAuthorizerNonStringExample_HttpApiWithNonString_Generated::HttpApiWithNonString" ] }, "Events": { "RootGet": { - "Type": "Api", + "Type": "HttpApi", "Properties": { - "Path": "/okresponsewithheaderasync/{x}", + "Path": "/api/authorizer-non-string", "Method": "GET" } } } } }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2Generated": { + "SimpleCalculatorAdd": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -714,21 +743,21 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV2_Generated::NotFoundResponseWithHeaderV2" + "TestServerlessApp::TestServerlessApp.SimpleCalculator_Add_Generated::Add" ] }, "Events": { "RootGet": { - "Type": "HttpApi", + "Type": "Api", "Properties": { - "Path": "/notfoundwithheaderv2/{x}", + "Path": "/SimpleCalculator/Add", "Method": "GET" } } } } }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2AsyncGenerated": { + "SimpleCalculatorSubtract": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -752,21 +781,21 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV2Async_Generated::NotFoundResponseWithHeaderV2Async" + "TestServerlessApp::TestServerlessApp.SimpleCalculator_Subtract_Generated::Subtract" ] }, "Events": { "RootGet": { - "Type": "HttpApi", + "Type": "Api", "Properties": { - "Path": "/notfoundwithheaderv2async/{x}", + "Path": "/SimpleCalculator/Subtract", "Method": "GET" } } } } }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1Generated": { + "SimpleCalculatorMultiply": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -776,8 +805,7 @@ "SyncedEventProperties": { "RootGet": [ "Path", - "Method", - "PayloadFormatVersion" + "Method" ] } }, @@ -791,22 +819,21 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV1_Generated::NotFoundResponseWithHeaderV1" + "TestServerlessApp::TestServerlessApp.SimpleCalculator_Multiply_Generated::Multiply" ] }, "Events": { "RootGet": { - "Type": "HttpApi", + "Type": "Api", "Properties": { - "Path": "/notfoundwithheaderv1/{x}", - "Method": "GET", - "PayloadFormatVersion": "1.0" + "Path": "/SimpleCalculator/Multiply/{x}/{y}", + "Method": "GET" } } } } }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1AsyncGenerated": { + "SimpleCalculatorDivideAsync": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -816,8 +843,7 @@ "SyncedEventProperties": { "RootGet": [ "Path", - "Method", - "PayloadFormatVersion" + "Method" ] } }, @@ -831,35 +857,24 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV1Async_Generated::NotFoundResponseWithHeaderV1Async" + "TestServerlessApp::TestServerlessApp.SimpleCalculator_DivideAsync_Generated::DivideAsync" ] }, "Events": { "RootGet": { - "Type": "HttpApi", + "Type": "Api", "Properties": { - "Path": "/notfoundwithheaderv1async/{x}", - "Method": "GET", - "PayloadFormatVersion": "1.0" + "Path": "/SimpleCalculator/DivideAsync/{x}/{y}", + "Method": "GET" } } } } }, - "TestServerlessAppCustomizeResponseExamplesOkResponseWithCustomSerializerGenerated": { + "PI": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method", - "PayloadFormatVersion" - ] - } + "Tool": "Amazon.Lambda.Annotations" }, "Properties": { "MemorySize": 512, @@ -871,22 +886,12 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_OkResponseWithCustomSerializer_Generated::OkResponseWithCustomSerializer" + "TestServerlessApp::TestServerlessApp.SimpleCalculator_Pi_Generated::Pi" ] - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/okresponsewithcustomserializerasync/{firstName}/{lastName}", - "Method": "GET", - "PayloadFormatVersion": "1.0" - } - } } } }, - "TestServerlessAppFromScratchNoSerializerAttributeReferenceToUpperGenerated": { + "Random": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations" @@ -901,12 +906,12 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.FromScratch.NoSerializerAttributeReference_ToUpper_Generated::ToUpper" + "TestServerlessApp::TestServerlessApp.SimpleCalculator_Random_Generated::Random" ] } } }, - "TestServerlessAppIntrinsicExampleHasIntrinsicGenerated": { + "Randoms": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations" @@ -921,12 +926,12 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.IntrinsicExample_HasIntrinsic_Generated::HasIntrinsic" + "TestServerlessApp::TestServerlessApp.SimpleCalculator_Randoms_Generated::Randoms" ] } } }, - "HttpApiNonString": { + "HttpApiAuthorizerTest": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -950,31 +955,35 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.CustomAuthorizerNonStringExample_HttpApiWithNonString_Generated::HttpApiWithNonString" + "TestServerlessApp::TestServerlessApp.CustomAuthorizerHttpApiExample_HttpApiAuthorizer_Generated::HttpApiAuthorizer" ] }, "Events": { "RootGet": { "Type": "HttpApi", "Properties": { - "Path": "/api/authorizer-non-string", + "Path": "/api/authorizer", "Method": "GET" } } } } }, - "AuthNameFallbackTest": { + "SQSMessageHandler": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", "SyncedEvents": [ - "RootGet" + "TestQueueEvent" ], "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" + "TestQueueEvent": [ + "Queue.Fn::GetAtt", + "BatchSize", + "FilterCriteria.Filters", + "FunctionResponseTypes", + "MaximumBatchingWindowInSeconds", + "ScalingConfig.MaximumConcurrency" ] } }, @@ -982,47 +991,46 @@ "MemorySize": 512, "Timeout": 30, "Policies": [ - "AWSLambdaBasicExecutionRole" + "AWSLambdaSQSQueueExecutionRole" ], "PackageType": "Image", "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.AuthNameFallback_GetUserId_Generated::GetUserId" + "TestServerlessApp::TestServerlessApp.SqsMessageProcessing_HandleMessage_Generated::HandleMessage" ] }, "Events": { - "RootGet": { - "Type": "HttpApi", + "TestQueueEvent": { + "Type": "SQS", "Properties": { - "Path": "/api/authorizer-fallback", - "Method": "GET" + "BatchSize": 50, + "FilterCriteria": { + "Filters": [ + { + "Pattern": "{ \"body\" : { \"RequestCode\" : [ \"BBBB\" ] } }" + } + ] + }, + "FunctionResponseTypes": [ + "ReportBatchItemFailures" + ], + "MaximumBatchingWindowInSeconds": 5, + "ScalingConfig": { + "MaximumConcurrency": 5 + }, + "Queue": { + "Fn::GetAtt": [ + "TestQueue", + "Arn" + ] + } } } } } }, - "TestServerlessAppVoidExampleVoidReturnGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.VoidExample_VoidReturn_Generated::VoidReturn" - ] - } - } - }, - "RestAuthorizerTest": { + "HttpApiV1AuthorizerTest": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -1032,7 +1040,8 @@ "SyncedEventProperties": { "RootGet": [ "Path", - "Method" + "Method", + "PayloadFormatVersion" ] } }, @@ -1046,21 +1055,22 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.CustomAuthorizerRestExample_RestAuthorizer_Generated::RestAuthorizer" + "TestServerlessApp::TestServerlessApp.CustomAuthorizerHttpApiV1Example_HttpApiV1Authorizer_Generated::HttpApiV1Authorizer" ] }, "Events": { "RootGet": { - "Type": "Api", + "Type": "HttpApi", "Properties": { - "Path": "/rest/authorizer", - "Method": "GET" + "Path": "/api/authorizer-v1", + "Method": "GET", + "PayloadFormatVersion": "1.0" } } } } }, - "TestServerlessAppTaskExampleTaskReturnGenerated": { + "TestServerlessAppDynamicExampleDynamicReturnGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations" @@ -1075,12 +1085,12 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.TaskExample_TaskReturn_Generated::TaskReturn" + "TestServerlessApp::TestServerlessApp.DynamicExample_DynamicReturn_Generated::DynamicReturn" ] } } }, - "ToUpper": { + "TestServerlessAppDynamicExampleDynamicInputGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations" @@ -1095,24 +1105,15 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.Sub1.Functions_ToUpper_Generated::ToUpper" + "TestServerlessApp::TestServerlessApp.DynamicExample_DynamicInput_Generated::DynamicInput" ] } } }, - "TestServerlessAppComplexCalculatorAddGenerated": { + "TestServerlessAppIntrinsicExampleHasIntrinsicGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootPost" - ], - "SyncedEventProperties": { - "RootPost": [ - "Path", - "Method" - ] - } + "Tool": "Amazon.Lambda.Annotations" }, "Properties": { "MemorySize": 512, @@ -1124,33 +1125,15 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.ComplexCalculator_Add_Generated::Add" + "TestServerlessApp::TestServerlessApp.IntrinsicExample_HasIntrinsic_Generated::HasIntrinsic" ] - }, - "Events": { - "RootPost": { - "Type": "HttpApi", - "Properties": { - "Path": "/ComplexCalculator/Add", - "Method": "POST" - } - } } } }, - "TestServerlessAppComplexCalculatorSubtractGenerated": { + "TestServerlessAppFromScratchNoSerializerAttributeReferenceToUpperGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootPost" - ], - "SyncedEventProperties": { - "RootPost": [ - "Path", - "Method" - ] - } + "Tool": "Amazon.Lambda.Annotations" }, "Properties": { "MemorySize": 512, @@ -1162,42 +1145,13 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.ComplexCalculator_Subtract_Generated::Subtract" + "TestServerlessApp::TestServerlessApp.FromScratch.NoSerializerAttributeReference_ToUpper_Generated::ToUpper" ] - }, - "Events": { - "RootPost": { - "Type": "HttpApi", - "Properties": { - "Path": "/ComplexCalculator/Subtract", - "Method": "POST" - } - } } } - } - }, - "Outputs": { - "RestApiURL": { - "Description": "Rest API endpoint URL for Prod environment", - "Value": { - "Fn::Sub": "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod" - } - }, - "HttpApiURL": { - "Description": "HTTP API endpoint URL for Prod environment", - "Value": { - "Fn::Sub": "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com" - } }, - "TestQueueARN": { - "Description": "ARN of the TestQueue resource", - "Value": { - "Fn::GetAtt": [ - "TestQueue", - "Arn" - ] - } + "TestQueue": { + "Type": "AWS::SQS::Queue" } } } \ No newline at end of file From b02a29bbd5862791002ee1b003621a0545d867e5 Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Wed, 25 Feb 2026 13:28:27 -0500 Subject: [PATCH 02/14] pr comments --- .../Diagnostics/AnalyzerReleases.Unshipped.md | 13 + .../Diagnostics/DiagnosticDescriptors.cs | 2 +- .../Generator.cs | 17 + .../Writers/CloudFormationWriter.cs | 37 +- .../src/Amazon.Lambda.Annotations/README.md | 2 +- .../WriterTests/CloudFormationWriterTests.cs | 1 + .../IntegrationTestContextFixture.cs | 3 + .../serverless.template | 3 +- .../IntegrationTestContextFixture.cs | 10 +- .../aws-lambda-tools-defaults.json | 8 +- .../TestServerlessApp/serverless.template | 443 +++++++++--------- 11 files changed, 294 insertions(+), 245 deletions(-) diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/AnalyzerReleases.Unshipped.md b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/AnalyzerReleases.Unshipped.md index 17d4678ce..423deaf1a 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/AnalyzerReleases.Unshipped.md +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/AnalyzerReleases.Unshipped.md @@ -1,2 +1,15 @@ ; Unshipped analyzer release ; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +AWSLambda0120 | AWSLambdaCSharpGenerator | Error | Authorizer Name Required +AWSLambda0121 | AWSLambdaCSharpGenerator | Error | HTTP API Authorizer Not Found +AWSLambda0122 | AWSLambdaCSharpGenerator | Error | REST API Authorizer Not Found +AWSLambda0123 | AWSLambdaCSharpGenerator | Error | Authorizer Type Mismatch +AWSLambda0124 | AWSLambdaCSharpGenerator | Error | Authorizer Type Mismatch +AWSLambda0125 | AWSLambdaCSharpGenerator | Error | Duplicate Authorizer Name +AWSLambda0126 | AWSLambdaCSharpGenerator | Error | Invalid Payload Format Version +AWSLambda0127 | AWSLambdaCSharpGenerator | Error | Invalid Result TTL \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs index 69ecec8eb..d6edcaf1e 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs @@ -158,7 +158,7 @@ public static class DiagnosticDescriptors public static readonly DiagnosticDescriptor AuthorizerMissingName = new DiagnosticDescriptor( id: "AWSLambda0120", title: "Authorizer Name Required", - messageFormat: "The Name property is required on [{0}] attribute.", + messageFormat: "The Name property is required on [{0}] attribute", category: "AWSLambdaCSharpGenerator", DiagnosticSeverity.Error, isEnabledByDefault: true); diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Generator.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Generator.cs index 097dd5278..0afb427c0 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Generator.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Generator.cs @@ -215,6 +215,23 @@ public void Execute(GeneratorExecutionContext context) context.AddSource("Program.g.cs", SourceText.From(executableAssembly.TransformText().ToEnvironmentLineEndings(), Encoding.UTF8, SourceHashAlgorithm.Sha256)); } + // Validate authorizer models and references before syncing CloudFormation template + foreach (var authorizerModel in annotationReport.Authorizers) + { + var attributeName = authorizerModel.AuthorizerType == AuthorizerType.HttpApi + ? "HttpApiAuthorizer" + : "RestApiAuthorizer"; + if (!ValidateAuthorizerModel(authorizerModel, attributeName, Location.None, diagnosticReporter)) + { + foundFatalError = true; + } + } + + if (!ValidateAuthorizerReferences(annotationReport, diagnosticReporter)) + { + foundFatalError = true; + } + // Run the CloudFormation sync if any LambdaMethods exists. Also run if no LambdaMethods exists but there is a // CloudFormation template in case orphaned functions in the template need to be removed. // Both checks are required because if there is no template but there are LambdaMethods the CF template the template will be created. diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs index e01e1a65c..71e1e9adf 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs @@ -63,7 +63,16 @@ public void ApplyReport(AnnotationReport report) ProcessTemplateDescription(report); // Build authorizer lookup for processing events with Auth configuration - var authorizerLookup = report.Authorizers.ToDictionary(a => a.Name, a => a); + var authorizerLookup = new Dictionary(StringComparer.Ordinal); + foreach (var authorizer in report.Authorizers) + { + if (authorizerLookup.ContainsKey(authorizer.Name)) + { + _diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.DuplicateAuthorizerName, Location.None, authorizer.Name)); + continue; + } + authorizerLookup[authorizer.Name] = authorizer; + } // Process authorizers first (they need to exist before functions reference them) ProcessAuthorizers(report.Authorizers); @@ -306,9 +315,8 @@ private void ProcessHttpApiAuthorizers(IList authorizers) if (!_templateWriter.Exists(httpApiResourcePath)) { _templateWriter.SetToken($"{httpApiResourcePath}.Type", "AWS::Serverless::HttpApi"); + _templateWriter.SetToken($"{httpApiResourcePath}.Metadata.Tool", CREATION_TOOL); } - - _templateWriter.SetToken($"{httpApiResourcePath}.Metadata.Tool", CREATION_TOOL); // Add each authorizer to the Auth.Authorizers map foreach (var authorizer in authorizers) @@ -333,11 +341,9 @@ private void ProcessHttpApiAuthorizers(IList authorizers) // https://github.com/aws/serverless-application-model/issues/2933 _templateWriter.SetToken($"{authorizerPath}.EnableFunctionDefaultPermissions", true); - // AuthorizerResultTtlInSeconds (only if caching is enabled) - if (authorizer.ResultTtlInSeconds > 0) - { - _templateWriter.SetToken($"{authorizerPath}.FunctionInvokeRole", null); // Required for caching - } + // AuthorizerResultTtlInSeconds - always write this value so SAM does not apply its default TTL. + // A value of 0 disables caching, which matches the attribute default. + _templateWriter.SetToken($"{authorizerPath}.AuthorizerResultTtlInSeconds", authorizer.ResultTtlInSeconds); } } @@ -355,10 +361,9 @@ private void ProcessRestApiAuthorizers(IList authorizers) _templateWriter.SetToken($"{restApiResourcePath}.Type", "AWS::Serverless::Api"); // REST API requires explicit stage name _templateWriter.SetToken($"{restApiResourcePath}.Properties.StageName", "Prod"); + _templateWriter.SetToken($"{restApiResourcePath}.Metadata.Tool", CREATION_TOOL); } - _templateWriter.SetToken($"{restApiResourcePath}.Metadata.Tool", CREATION_TOOL); - // Add each authorizer to the Auth.Authorizers map foreach (var authorizer in authorizers) { @@ -379,6 +384,18 @@ private void ProcessRestApiAuthorizers(IList authorizers) { _templateWriter.SetToken($"{authorizerPath}.FunctionPayloadType", "REQUEST"); } + + // AuthorizerResultTtlInSeconds - cache TTL for authorizer result (only when valid) + // API Gateway REST Lambda authorizer TTL must be between 1 and 3600 seconds. + var resultTtlInSeconds = authorizer.ResultTtlInSeconds; + const int minTtlInSeconds = 1; + const int maxTtlInSeconds = 3600; + + if (resultTtlInSeconds >= minTtlInSeconds && + resultTtlInSeconds <= maxTtlInSeconds) + { + _templateWriter.SetToken($"{authorizerPath}.AuthorizerResultTtlInSeconds", resultTtlInSeconds); + } } } diff --git a/Libraries/src/Amazon.Lambda.Annotations/README.md b/Libraries/src/Amazon.Lambda.Annotations/README.md index 6ced6f7df..550d5b088 100644 --- a/Libraries/src/Amazon.Lambda.Annotations/README.md +++ b/Libraries/src/Amazon.Lambda.Annotations/README.md @@ -933,7 +933,7 @@ public string PublicEndpoint() } ``` -The source generator will produce CloudFormation that includes an `Auth` section on the `ServerlessHttpApi` resource with the authorizer configuration, and each protected function's event will reference the authorizer by name. +The source generator will produce CloudFormation that includes an `Auth` section on the `AnnotationsHttpApi` resource with the authorizer configuration, and each protected function's event will reference the authorizer by name. ### REST API Authorizer diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/CloudFormationWriterTests.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/CloudFormationWriterTests.cs index 09ad90f0d..6d5588ecc 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/CloudFormationWriterTests.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/CloudFormationWriterTests.cs @@ -1153,6 +1153,7 @@ public void HttpApiAuthorizerWithCustomTtl(CloudFormationTemplateFormat template // Verify the authorizer was created with expected properties Assert.Equal(new List { "AuthorizerFunction", "Arn" }, templateWriter.GetToken>($"{authorizerPath}.FunctionArn.Fn::GetAtt")); Assert.Equal("2.0", templateWriter.GetToken($"{authorizerPath}.AuthorizerPayloadFormatVersion")); + Assert.Equal(300, templateWriter.GetToken($"{authorizerPath}.AuthorizerResultTtlInSeconds")); } [Theory] diff --git a/Libraries/test/TestCustomAuthorizerApp.IntegrationTests/IntegrationTestContextFixture.cs b/Libraries/test/TestCustomAuthorizerApp.IntegrationTests/IntegrationTestContextFixture.cs index 2d2ab4c09..6062a965c 100644 --- a/Libraries/test/TestCustomAuthorizerApp.IntegrationTests/IntegrationTestContextFixture.cs +++ b/Libraries/test/TestCustomAuthorizerApp.IntegrationTests/IntegrationTestContextFixture.cs @@ -79,6 +79,9 @@ public async Task InitializeAsync() var implicitHttpApiId = await _cloudFormationHelper.GetResourcePhysicalIdAsync(_stackName, "ServerlessHttpApi"); var restApiId = await _cloudFormationHelper.GetResourcePhysicalIdAsync(_stackName, "AnnotationsRestApi"); Console.WriteLine($"[IntegrationTest] AnnotationsHttpApi: {httpApiId}, ServerlessHttpApi: {implicitHttpApiId}, AnnotationsRestApi: {restApiId}"); + Assert.False(string.IsNullOrEmpty(httpApiId), $"CloudFormation resource 'AnnotationsHttpApi' was not found in stack '{_stackName}'."); + Assert.False(string.IsNullOrEmpty(implicitHttpApiId), $"CloudFormation resource 'ServerlessHttpApi' was not found in stack '{_stackName}'."); + Assert.False(string.IsNullOrEmpty(restApiId), $"CloudFormation resource 'AnnotationsRestApi' was not found in stack '{_stackName}'."); HttpApiUrl = $"https://{httpApiId}.execute-api.{region}.amazonaws.com"; ImplicitHttpApiUrl = $"https://{implicitHttpApiId}.execute-api.{region}.amazonaws.com"; RestApiUrl = $"https://{restApiId}.execute-api.{region}.amazonaws.com/Prod"; diff --git a/Libraries/test/TestCustomAuthorizerApp/serverless.template b/Libraries/test/TestCustomAuthorizerApp/serverless.template index d59e91d9b..c547f710e 100644 --- a/Libraries/test/TestCustomAuthorizerApp/serverless.template +++ b/Libraries/test/TestCustomAuthorizerApp/serverless.template @@ -25,7 +25,8 @@ "authorization" ] }, - "EnableFunctionDefaultPermissions": true + "EnableFunctionDefaultPermissions": true, + "AuthorizerResultTtlInSeconds": 0 } } } diff --git a/Libraries/test/TestServerlessApp.IntegrationTests/IntegrationTestContextFixture.cs b/Libraries/test/TestServerlessApp.IntegrationTests/IntegrationTestContextFixture.cs index 3b6a5ceb3..8de9bab4a 100644 --- a/Libraries/test/TestServerlessApp.IntegrationTests/IntegrationTestContextFixture.cs +++ b/Libraries/test/TestServerlessApp.IntegrationTests/IntegrationTestContextFixture.cs @@ -65,8 +65,8 @@ public async Task InitializeAsync() var region = "us-west-2"; Console.WriteLine($"[IntegrationTest] Querying stack resources for '{_stackName}'..."); var httpApiId = await _cloudFormationHelper.GetResourcePhysicalIdAsync(_stackName, "ServerlessHttpApi"); - var restApiId = await _cloudFormationHelper.GetResourcePhysicalIdAsync(_stackName, "AnnotationsRestApi"); - Console.WriteLine($"[IntegrationTest] ServerlessHttpApi: {httpApiId}, AnnotationsRestApi: {restApiId}"); + var restApiId = await _cloudFormationHelper.GetResourcePhysicalIdAsync(_stackName, "ServerlessRestApi"); + Console.WriteLine($"[IntegrationTest] ServerlessHttpApi: {httpApiId}, ServerlessRestApi: {restApiId}"); HttpApiUrlPrefix = $"https://{httpApiId}.execute-api.{region}.amazonaws.com"; RestApiUrlPrefix = $"https://{restApiId}.execute-api.{region}.amazonaws.com/Prod"; @@ -79,10 +79,10 @@ public async Task InitializeAsync() Assert.True(await _s3Helper.BucketExistsAsync(_bucketName), $"S3 bucket {_bucketName} should exist"); Assert.Equal(34, LambdaFunctions.Count); - Assert.False(string.IsNullOrEmpty(RestApiUrlPrefix)); - Assert.False(string.IsNullOrEmpty(RestApiUrlPrefix)); + Assert.False(string.IsNullOrEmpty(RestApiUrlPrefix), "RestApiUrlPrefix should not be empty"); + Assert.False(string.IsNullOrEmpty(HttpApiUrlPrefix), "HttpApiUrlPrefix should not be empty"); - await LambdaHelper.WaitTillNotPending(LambdaFunctions.Select(x => x.Name).ToList()); + await LambdaHelper.WaitTillNotPending(LambdaFunctions.Where(x => x.Name != null).Select(x => x.Name).ToList()); // Wait an additional 10 seconds for any other eventually consistency state to finish up. await Task.Delay(10000); diff --git a/Libraries/test/TestServerlessApp/aws-lambda-tools-defaults.json b/Libraries/test/TestServerlessApp/aws-lambda-tools-defaults.json index d265c7e8d..0b96350ff 100644 --- a/Libraries/test/TestServerlessApp/aws-lambda-tools-defaults.json +++ b/Libraries/test/TestServerlessApp/aws-lambda-tools-defaults.json @@ -13,7 +13,7 @@ "template": "serverless.template", "template-parameters": "", "docker-host-build-output-dir": "./bin/Release/lambda-publish", -"s3-bucket" : "test-serverless-app-707259b4", -"stack-name" : "test-serverless-app-707259b4", -"function-architecture" : "x86_64" -} + "s3-bucket": "test-serverless-app", + "stack-name": "test-serverless-app", + "function-architecture": "x86_64" +} \ No newline at end of file diff --git a/Libraries/test/TestServerlessApp/serverless.template b/Libraries/test/TestServerlessApp/serverless.template index f58c37098..067b0adb3 100644 --- a/Libraries/test/TestServerlessApp/serverless.template +++ b/Libraries/test/TestServerlessApp/serverless.template @@ -3,7 +3,7 @@ "Transform": "AWS::Serverless-2016-10-31", "Description": "This template is partially managed by Amazon.Lambda.Annotations (v1.9.0.0).", "Resources": { - "TestServerlessAppCustomAuthorizerWithIHttpResultsExampleAuthorizerWithIHttpResultsGenerated": { + "AuthNameFallbackTest": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -27,37 +27,36 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.CustomAuthorizerWithIHttpResultsExample_AuthorizerWithIHttpResults_Generated::AuthorizerWithIHttpResults" + "TestServerlessApp::TestServerlessApp.AuthNameFallback_GetUserId_Generated::GetUserId" ] }, "Events": { "RootGet": { "Type": "HttpApi", "Properties": { - "Path": "/authorizerihttpresults", + "Path": "/api/authorizer-fallback", "Method": "GET" } } } } }, - "GreeterSayHello": { + "TestServerlessAppComplexCalculatorAddGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", "SyncedEvents": [ - "RootGet" + "RootPost" ], "SyncedEventProperties": { - "RootGet": [ + "RootPost": [ "Path", - "Method", - "PayloadFormatVersion" + "Method" ] } }, "Properties": { - "MemorySize": 1024, + "MemorySize": 512, "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" @@ -66,39 +65,37 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.Greeter_SayHello_Generated::SayHello" + "TestServerlessApp::TestServerlessApp.ComplexCalculator_Add_Generated::Add" ] }, "Events": { - "RootGet": { + "RootPost": { "Type": "HttpApi", "Properties": { - "Path": "/Greeter/SayHello", - "Method": "GET", - "PayloadFormatVersion": "1.0" + "Path": "/ComplexCalculator/Add", + "Method": "POST" } } } } }, - "GreeterSayHelloAsync": { + "TestServerlessAppComplexCalculatorSubtractGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", "SyncedEvents": [ - "RootGet" + "RootPost" ], "SyncedEventProperties": { - "RootGet": [ + "RootPost": [ "Path", - "Method", - "PayloadFormatVersion" + "Method" ] } }, "Properties": { "MemorySize": 512, - "Timeout": 50, + "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" ], @@ -106,22 +103,21 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.Greeter_SayHelloAsync_Generated::SayHelloAsync" + "TestServerlessApp::TestServerlessApp.ComplexCalculator_Subtract_Generated::Subtract" ] }, "Events": { - "RootGet": { + "RootPost": { "Type": "HttpApi", "Properties": { - "Path": "/Greeter/SayHelloAsync", - "Method": "GET", - "PayloadFormatVersion": "1.0" + "Path": "/ComplexCalculator/Subtract", + "Method": "POST" } } } } }, - "TestServerlessAppNullableReferenceTypeExampleNullableHeaderHttpApiGenerated": { + "HttpApiAuthorizerTest": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -145,21 +141,21 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.NullableReferenceTypeExample_NullableHeaderHttpApi_Generated::NullableHeaderHttpApi" + "TestServerlessApp::TestServerlessApp.CustomAuthorizerHttpApiExample_HttpApiAuthorizer_Generated::HttpApiAuthorizer" ] }, "Events": { "RootGet": { "Type": "HttpApi", "Properties": { - "Path": "/nullableheaderhttpapi", + "Path": "/api/authorizer", "Method": "GET" } } } } }, - "TestServerlessAppFromScratchNoApiGatewayEventsReferenceToUpperGenerated": { + "HttpApiV1AuthorizerTest": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -169,7 +165,8 @@ "SyncedEventProperties": { "RootGet": [ "Path", - "Method" + "Method", + "PayloadFormatVersion" ] } }, @@ -183,24 +180,34 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.FromScratch.NoApiGatewayEventsReference_ToUpper_Generated::ToUpper" + "TestServerlessApp::TestServerlessApp.CustomAuthorizerHttpApiV1Example_HttpApiV1Authorizer_Generated::HttpApiV1Authorizer" ] }, "Events": { "RootGet": { "Type": "HttpApi", "Properties": { - "Path": "/{text}", - "Method": "GET" + "Path": "/api/authorizer-v1", + "Method": "GET", + "PayloadFormatVersion": "1.0" } } } } }, - "ToUpper": { + "HttpApiNonString": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations" + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } }, "Properties": { "MemorySize": 512, @@ -212,20 +219,29 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.Sub1.Functions_ToUpper_Generated::ToUpper" + "TestServerlessApp::TestServerlessApp.CustomAuthorizerNonStringExample_HttpApiWithNonString_Generated::HttpApiWithNonString" ] + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/authorizer-non-string", + "Method": "GET" + } + } } } }, - "TestServerlessAppComplexCalculatorAddGenerated": { + "RestAuthorizerTest": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", "SyncedEvents": [ - "RootPost" + "RootGet" ], "SyncedEventProperties": { - "RootPost": [ + "RootGet": [ "Path", "Method" ] @@ -241,29 +257,29 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.ComplexCalculator_Add_Generated::Add" + "TestServerlessApp::TestServerlessApp.CustomAuthorizerRestExample_RestAuthorizer_Generated::RestAuthorizer" ] }, "Events": { - "RootPost": { - "Type": "HttpApi", + "RootGet": { + "Type": "Api", "Properties": { - "Path": "/ComplexCalculator/Add", - "Method": "POST" + "Path": "/rest/authorizer", + "Method": "GET" } } } } }, - "TestServerlessAppComplexCalculatorSubtractGenerated": { + "TestServerlessAppCustomAuthorizerWithIHttpResultsExampleAuthorizerWithIHttpResultsGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", "SyncedEvents": [ - "RootPost" + "RootGet" ], "SyncedEventProperties": { - "RootPost": [ + "RootGet": [ "Path", "Method" ] @@ -279,21 +295,21 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.ComplexCalculator_Subtract_Generated::Subtract" + "TestServerlessApp::TestServerlessApp.CustomAuthorizerWithIHttpResultsExample_AuthorizerWithIHttpResults_Generated::AuthorizerWithIHttpResults" ] }, "Events": { - "RootPost": { + "RootGet": { "Type": "HttpApi", "Properties": { - "Path": "/ComplexCalculator/Subtract", - "Method": "POST" + "Path": "/authorizerihttpresults", + "Method": "GET" } } } } }, - "RestAuthorizerTest": { + "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -317,24 +333,33 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.CustomAuthorizerRestExample_RestAuthorizer_Generated::RestAuthorizer" + "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_OkResponseWithHeader_Generated::OkResponseWithHeader" ] }, "Events": { "RootGet": { "Type": "Api", "Properties": { - "Path": "/rest/authorizer", + "Path": "/okresponsewithheader/{x}", "Method": "GET" } } } } }, - "TestServerlessAppTaskExampleTaskReturnGenerated": { + "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderAsyncGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations" + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } }, "Properties": { "MemorySize": 512, @@ -346,12 +371,21 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.TaskExample_TaskReturn_Generated::TaskReturn" + "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_OkResponseWithHeaderAsync_Generated::OkResponseWithHeaderAsync" ] + }, + "Events": { + "RootGet": { + "Type": "Api", + "Properties": { + "Path": "/okresponsewithheaderasync/{x}", + "Method": "GET" + } + } } } }, - "AuthNameFallbackTest": { + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2Generated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -375,24 +409,33 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.AuthNameFallback_GetUserId_Generated::GetUserId" + "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV2_Generated::NotFoundResponseWithHeaderV2" ] }, "Events": { "RootGet": { "Type": "HttpApi", "Properties": { - "Path": "/api/authorizer-fallback", + "Path": "/notfoundwithheaderv2/{x}", "Method": "GET" } } } } }, - "TestServerlessAppVoidExampleVoidReturnGenerated": { + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2AsyncGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations" + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } }, "Properties": { "MemorySize": 512, @@ -404,12 +447,21 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.VoidExample_VoidReturn_Generated::VoidReturn" + "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV2Async_Generated::NotFoundResponseWithHeaderV2Async" ] + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/notfoundwithheaderv2async/{x}", + "Method": "GET" + } + } } } }, - "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderGenerated": { + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1Generated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -419,7 +471,8 @@ "SyncedEventProperties": { "RootGet": [ "Path", - "Method" + "Method", + "PayloadFormatVersion" ] } }, @@ -433,21 +486,22 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_OkResponseWithHeader_Generated::OkResponseWithHeader" + "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV1_Generated::NotFoundResponseWithHeaderV1" ] }, "Events": { "RootGet": { - "Type": "Api", + "Type": "HttpApi", "Properties": { - "Path": "/okresponsewithheader/{x}", - "Method": "GET" + "Path": "/notfoundwithheaderv1/{x}", + "Method": "GET", + "PayloadFormatVersion": "1.0" } } } } }, - "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderAsyncGenerated": { + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1AsyncGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -457,7 +511,8 @@ "SyncedEventProperties": { "RootGet": [ "Path", - "Method" + "Method", + "PayloadFormatVersion" ] } }, @@ -471,21 +526,22 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_OkResponseWithHeaderAsync_Generated::OkResponseWithHeaderAsync" + "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV1Async_Generated::NotFoundResponseWithHeaderV1Async" ] }, "Events": { "RootGet": { - "Type": "Api", + "Type": "HttpApi", "Properties": { - "Path": "/okresponsewithheaderasync/{x}", - "Method": "GET" + "Path": "/notfoundwithheaderv1async/{x}", + "Method": "GET", + "PayloadFormatVersion": "1.0" } } } } }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2Generated": { + "TestServerlessAppCustomizeResponseExamplesOkResponseWithCustomSerializerGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -495,7 +551,8 @@ "SyncedEventProperties": { "RootGet": [ "Path", - "Method" + "Method", + "PayloadFormatVersion" ] } }, @@ -509,33 +566,45 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV2_Generated::NotFoundResponseWithHeaderV2" + "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_OkResponseWithCustomSerializer_Generated::OkResponseWithCustomSerializer" ] }, "Events": { "RootGet": { "Type": "HttpApi", "Properties": { - "Path": "/notfoundwithheaderv2/{x}", - "Method": "GET" + "Path": "/okresponsewithcustomserializerasync/{firstName}/{lastName}", + "Method": "GET", + "PayloadFormatVersion": "1.0" } } } } }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2AsyncGenerated": { + "TestServerlessAppDynamicExampleDynamicReturnGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.DynamicExample_DynamicReturn_Generated::DynamicReturn" ] } + } + }, + "TestServerlessAppDynamicExampleDynamicInputGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" }, "Properties": { "MemorySize": 512, @@ -547,21 +616,12 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV2Async_Generated::NotFoundResponseWithHeaderV2Async" + "TestServerlessApp::TestServerlessApp.DynamicExample_DynamicInput_Generated::DynamicInput" ] - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/notfoundwithheaderv2async/{x}", - "Method": "GET" - } - } } } }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1Generated": { + "TestServerlessAppFromScratchNoApiGatewayEventsReferenceToUpperGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -571,8 +631,7 @@ "SyncedEventProperties": { "RootGet": [ "Path", - "Method", - "PayloadFormatVersion" + "Method" ] } }, @@ -586,22 +645,41 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV1_Generated::NotFoundResponseWithHeaderV1" + "TestServerlessApp::TestServerlessApp.FromScratch.NoApiGatewayEventsReference_ToUpper_Generated::ToUpper" ] }, "Events": { "RootGet": { "Type": "HttpApi", "Properties": { - "Path": "/notfoundwithheaderv1/{x}", - "Method": "GET", - "PayloadFormatVersion": "1.0" + "Path": "/{text}", + "Method": "GET" } } } } }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1AsyncGenerated": { + "TestServerlessAppFromScratchNoSerializerAttributeReferenceToUpperGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.FromScratch.NoSerializerAttributeReference_ToUpper_Generated::ToUpper" + ] + } + } + }, + "GreeterSayHello": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -617,7 +695,7 @@ } }, "Properties": { - "MemorySize": 512, + "MemorySize": 1024, "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" @@ -626,14 +704,14 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV1Async_Generated::NotFoundResponseWithHeaderV1Async" + "TestServerlessApp::TestServerlessApp.Greeter_SayHello_Generated::SayHello" ] }, "Events": { "RootGet": { "Type": "HttpApi", "Properties": { - "Path": "/notfoundwithheaderv1async/{x}", + "Path": "/Greeter/SayHello", "Method": "GET", "PayloadFormatVersion": "1.0" } @@ -641,7 +719,7 @@ } } }, - "TestServerlessAppCustomizeResponseExamplesOkResponseWithCustomSerializerGenerated": { + "GreeterSayHelloAsync": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -658,7 +736,7 @@ }, "Properties": { "MemorySize": 512, - "Timeout": 30, + "Timeout": 50, "Policies": [ "AWSLambdaBasicExecutionRole" ], @@ -666,14 +744,14 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_OkResponseWithCustomSerializer_Generated::OkResponseWithCustomSerializer" + "TestServerlessApp::TestServerlessApp.Greeter_SayHelloAsync_Generated::SayHelloAsync" ] }, "Events": { "RootGet": { "Type": "HttpApi", "Properties": { - "Path": "/okresponsewithcustomserializerasync/{firstName}/{lastName}", + "Path": "/Greeter/SayHelloAsync", "Method": "GET", "PayloadFormatVersion": "1.0" } @@ -681,7 +759,27 @@ } } }, - "HttpApiNonString": { + "TestServerlessAppIntrinsicExampleHasIntrinsicGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.IntrinsicExample_HasIntrinsic_Generated::HasIntrinsic" + ] + } + } + }, + "TestServerlessAppNullableReferenceTypeExampleNullableHeaderHttpApiGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -705,14 +803,14 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.CustomAuthorizerNonStringExample_HttpApiWithNonString_Generated::HttpApiWithNonString" + "TestServerlessApp::TestServerlessApp.NullableReferenceTypeExample_NullableHeaderHttpApi_Generated::NullableHeaderHttpApi" ] }, "Events": { "RootGet": { "Type": "HttpApi", "Properties": { - "Path": "/api/authorizer-non-string", + "Path": "/nullableheaderhttpapi", "Method": "GET" } } @@ -931,44 +1029,6 @@ } } }, - "HttpApiAuthorizerTest": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.CustomAuthorizerHttpApiExample_HttpApiAuthorizer_Generated::HttpApiAuthorizer" - ] - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/api/authorizer", - "Method": "GET" - } - } - } - } - }, "SQSMessageHandler": { "Type": "AWS::Serverless::Function", "Metadata": { @@ -1030,67 +1090,7 @@ } } }, - "HttpApiV1AuthorizerTest": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method", - "PayloadFormatVersion" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.CustomAuthorizerHttpApiV1Example_HttpApiV1Authorizer_Generated::HttpApiV1Authorizer" - ] - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/api/authorizer-v1", - "Method": "GET", - "PayloadFormatVersion": "1.0" - } - } - } - } - }, - "TestServerlessAppDynamicExampleDynamicReturnGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.DynamicExample_DynamicReturn_Generated::DynamicReturn" - ] - } - } - }, - "TestServerlessAppDynamicExampleDynamicInputGenerated": { + "ToUpper": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations" @@ -1105,12 +1105,12 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.DynamicExample_DynamicInput_Generated::DynamicInput" + "TestServerlessApp::TestServerlessApp.Sub1.Functions_ToUpper_Generated::ToUpper" ] } } }, - "TestServerlessAppIntrinsicExampleHasIntrinsicGenerated": { + "TestServerlessAppTaskExampleTaskReturnGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations" @@ -1125,12 +1125,12 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.IntrinsicExample_HasIntrinsic_Generated::HasIntrinsic" + "TestServerlessApp::TestServerlessApp.TaskExample_TaskReturn_Generated::TaskReturn" ] } } }, - "TestServerlessAppFromScratchNoSerializerAttributeReferenceToUpperGenerated": { + "TestServerlessAppVoidExampleVoidReturnGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations" @@ -1145,13 +1145,10 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.FromScratch.NoSerializerAttributeReference_ToUpper_Generated::ToUpper" + "TestServerlessApp::TestServerlessApp.VoidExample_VoidReturn_Generated::VoidReturn" ] } } - }, - "TestQueue": { - "Type": "AWS::SQS::Queue" } } } \ No newline at end of file From 780a60ac23991e5bd46c40567a0cf67e05fff235 Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Wed, 25 Feb 2026 16:07:49 -0500 Subject: [PATCH 03/14] source gen tests --- ....Annotations.SourceGenerators.Tests.csproj | 8 + ...erFunction_HttpApiAuthorize_Generated.g.cs | 58 + ...erFunction_RestApiAuthorize_Generated.g.cs | 58 + ...nction_GetHttpApiV1UserInfo_Generated.g.cs | 225 ++++ ...ctedFunction_GetIHttpResult_Generated.g.cs | 185 +++ ...nction_GetNonStringUserInfo_Generated.g.cs | 225 ++++ ...edFunction_GetProtectedData_Generated.g.cs | 68 + ...tedFunction_GetRestUserInfo_Generated.g.cs | 225 ++++ ...otectedFunction_GetUserInfo_Generated.g.cs | 225 ++++ ...otectedFunction_HealthCheck_Generated.g.cs | 68 + .../customAuthorizerApp.template | 390 ++++++ .../SourceGeneratorTests.cs | 105 ++ .../AuthorizerFunction.cs | 2 + .../ProtectedFunction.cs | 1 + .../TestCustomAuthorizerApp.csproj | 2 +- .../serverless.template | 18 +- .../serverless.template | 657 +--------- .../IntegrationTestContextFixture.cs | 1 + .../serverless.template | 20 +- .../TestServerlessApp/serverless.template | 1150 +---------------- 20 files changed, 1857 insertions(+), 1834 deletions(-) create mode 100644 Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthorizerFunction_HttpApiAuthorize_Generated.g.cs create mode 100644 Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthorizerFunction_RestApiAuthorize_Generated.g.cs create mode 100644 Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetHttpApiV1UserInfo_Generated.g.cs create mode 100644 Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetIHttpResult_Generated.g.cs create mode 100644 Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetNonStringUserInfo_Generated.g.cs create mode 100644 Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetProtectedData_Generated.g.cs create mode 100644 Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetRestUserInfo_Generated.g.cs create mode 100644 Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetUserInfo_Generated.g.cs create mode 100644 Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_HealthCheck_Generated.g.cs create mode 100644 Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/customAuthorizerApp.template diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Amazon.Lambda.Annotations.SourceGenerators.Tests.csproj b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Amazon.Lambda.Annotations.SourceGenerators.Tests.csproj index 9b80950f6..c8cc6f306 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Amazon.Lambda.Annotations.SourceGenerators.Tests.csproj +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Amazon.Lambda.Annotations.SourceGenerators.Tests.csproj @@ -59,6 +59,14 @@ + + TestCustomAuthorizerApp\%(RecursiveDir)/%(FileName)%(Extension) + Always + + + + + TestExecutableServerlessApp\%(RecursiveDir)/%(FileName)%(Extension) Always diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthorizerFunction_HttpApiAuthorize_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthorizerFunction_HttpApiAuthorize_Generated.g.cs new file mode 100644 index 000000000..921c6aa02 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthorizerFunction_HttpApiAuthorize_Generated.g.cs @@ -0,0 +1,58 @@ +// + +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using Amazon.Lambda.Core; + +namespace TestCustomAuthorizerApp +{ + public class AuthorizerFunction_HttpApiAuthorize_Generated + { + private readonly AuthorizerFunction authorizerFunction; + private readonly Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer serializer; + + /// + /// Default constructor. This constructor is used by Lambda to construct the instance. When invoked in a Lambda environment + /// the AWS credentials will come from the IAM role associated with the function and the AWS region will be set to the + /// region the Lambda function is executed in. + /// + public AuthorizerFunction_HttpApiAuthorize_Generated() + { + SetExecutionEnvironment(); + authorizerFunction = new AuthorizerFunction(); + serializer = new Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer(); + } + + /// + /// The generated Lambda function handler for + /// + /// The request object that will be processed by the Lambda function handler. + /// The ILambdaContext that provides methods for logging and describing the Lambda environment. + /// Result of the Lambda function execution + public Amazon.Lambda.APIGatewayEvents.APIGatewayCustomAuthorizerV2SimpleResponse HttpApiAuthorize(Amazon.Lambda.APIGatewayEvents.APIGatewayCustomAuthorizerV2Request __request__, Amazon.Lambda.Core.ILambdaContext __context__) + { + return authorizerFunction.HttpApiAuthorize(__request__, __context__); + } + + private static void SetExecutionEnvironment() + { + const string envName = "AWS_EXECUTION_ENV"; + + var envValue = new StringBuilder(); + + // If there is an existing execution environment variable add the annotations package as a suffix. + if(!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(envName))) + { + envValue.Append($"{Environment.GetEnvironmentVariable(envName)}_"); + } + + envValue.Append("lib/amazon-lambda-annotations#1.9.0.0"); + + Environment.SetEnvironmentVariable(envName, envValue.ToString()); + } + } +} \ No newline at end of file diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthorizerFunction_RestApiAuthorize_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthorizerFunction_RestApiAuthorize_Generated.g.cs new file mode 100644 index 000000000..76acd4579 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthorizerFunction_RestApiAuthorize_Generated.g.cs @@ -0,0 +1,58 @@ +// + +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using Amazon.Lambda.Core; + +namespace TestCustomAuthorizerApp +{ + public class AuthorizerFunction_RestApiAuthorize_Generated + { + private readonly AuthorizerFunction authorizerFunction; + private readonly Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer serializer; + + /// + /// Default constructor. This constructor is used by Lambda to construct the instance. When invoked in a Lambda environment + /// the AWS credentials will come from the IAM role associated with the function and the AWS region will be set to the + /// region the Lambda function is executed in. + /// + public AuthorizerFunction_RestApiAuthorize_Generated() + { + SetExecutionEnvironment(); + authorizerFunction = new AuthorizerFunction(); + serializer = new Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer(); + } + + /// + /// The generated Lambda function handler for + /// + /// The request object that will be processed by the Lambda function handler. + /// The ILambdaContext that provides methods for logging and describing the Lambda environment. + /// Result of the Lambda function execution + public Amazon.Lambda.APIGatewayEvents.APIGatewayCustomAuthorizerResponse RestApiAuthorize(Amazon.Lambda.APIGatewayEvents.APIGatewayCustomAuthorizerRequest __request__, Amazon.Lambda.Core.ILambdaContext __context__) + { + return authorizerFunction.RestApiAuthorize(__request__, __context__); + } + + private static void SetExecutionEnvironment() + { + const string envName = "AWS_EXECUTION_ENV"; + + var envValue = new StringBuilder(); + + // If there is an existing execution environment variable add the annotations package as a suffix. + if(!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(envName))) + { + envValue.Append($"{Environment.GetEnvironmentVariable(envName)}_"); + } + + envValue.Append("lib/amazon-lambda-annotations#1.9.0.0"); + + Environment.SetEnvironmentVariable(envName, envValue.ToString()); + } + } +} \ No newline at end of file diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetHttpApiV1UserInfo_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetHttpApiV1UserInfo_Generated.g.cs new file mode 100644 index 000000000..62c34f770 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetHttpApiV1UserInfo_Generated.g.cs @@ -0,0 +1,225 @@ +// + +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using Amazon.Lambda.Core; + +namespace TestCustomAuthorizerApp +{ + public class ProtectedFunction_GetHttpApiV1UserInfo_Generated + { + private readonly ProtectedFunction protectedFunction; + private readonly Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer serializer; + + /// + /// Default constructor. This constructor is used by Lambda to construct the instance. When invoked in a Lambda environment + /// the AWS credentials will come from the IAM role associated with the function and the AWS region will be set to the + /// region the Lambda function is executed in. + /// + public ProtectedFunction_GetHttpApiV1UserInfo_Generated() + { + SetExecutionEnvironment(); + protectedFunction = new ProtectedFunction(); + serializer = new Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer(); + } + + /// + /// The generated Lambda function handler for + /// + /// The API Gateway request object that will be processed by the Lambda function handler. + /// The ILambdaContext that provides methods for logging and describing the Lambda environment. + /// Result of the Lambda function execution + public Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse GetHttpApiV1UserInfo(Amazon.Lambda.APIGatewayEvents.APIGatewayProxyRequest __request__, Amazon.Lambda.Core.ILambdaContext __context__) + { + var validationErrors = new List(); + + var userId = default(string); + if (__request__.RequestContext?.Authorizer == null || __request__.RequestContext?.Authorizer.ContainsKey("userId") == false) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogDebug("Authorizer attribute 'userId' was missing, returning unauthorized."); +#else + __context__.Logger.Log("Authorizer attribute 'userId' was missing, returning unauthorized."); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + try + { + var __authValue_userId__ = __request__.RequestContext.Authorizer["userId"]; + userId = (string)Convert.ChangeType(__authValue_userId__?.ToString(), typeof(string)); + } + catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogError(e, "Failed to convert authorizer attribute 'userId', returning unauthorized."); +#else + __context__.Logger.Log("Failed to convert authorizer attribute 'userId', returning unauthorized. Exception: " + e.ToString()); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + var email = default(string); + if (__request__.RequestContext?.Authorizer == null || __request__.RequestContext?.Authorizer.ContainsKey("email") == false) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogDebug("Authorizer attribute 'email' was missing, returning unauthorized."); +#else + __context__.Logger.Log("Authorizer attribute 'email' was missing, returning unauthorized."); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + try + { + var __authValue_email__ = __request__.RequestContext.Authorizer["email"]; + email = (string)Convert.ChangeType(__authValue_email__?.ToString(), typeof(string)); + } + catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogError(e, "Failed to convert authorizer attribute 'email', returning unauthorized."); +#else + __context__.Logger.Log("Failed to convert authorizer attribute 'email', returning unauthorized. Exception: " + e.ToString()); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + var tenantId = default(string); + if (__request__.RequestContext?.Authorizer == null || __request__.RequestContext?.Authorizer.ContainsKey("tenantId") == false) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogDebug("Authorizer attribute 'tenantId' was missing, returning unauthorized."); +#else + __context__.Logger.Log("Authorizer attribute 'tenantId' was missing, returning unauthorized."); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + try + { + var __authValue_tenantId__ = __request__.RequestContext.Authorizer["tenantId"]; + tenantId = (string)Convert.ChangeType(__authValue_tenantId__?.ToString(), typeof(string)); + } + catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogError(e, "Failed to convert authorizer attribute 'tenantId', returning unauthorized."); +#else + __context__.Logger.Log("Failed to convert authorizer attribute 'tenantId', returning unauthorized. Exception: " + e.ToString()); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + // return 400 Bad Request if there exists a validation error + if (validationErrors.Any()) + { + var errorResult = new Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse + { + Body = @$"{{""message"": ""{validationErrors.Count} validation error(s) detected: {string.Join(",", validationErrors)}""}}", + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "ValidationException"} + }, + StatusCode = 400 + }; + return errorResult; + } + + var response = protectedFunction.GetHttpApiV1UserInfo(userId, email, tenantId, __context__); + var memoryStream = new MemoryStream(); + serializer.Serialize(response, memoryStream); + memoryStream.Position = 0; + + // convert stream to string + StreamReader reader = new StreamReader( memoryStream ); + var body = reader.ReadToEnd(); + + return new Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse + { + Body = body, + Headers = new Dictionary + { + {"Content-Type", "application/json"} + }, + StatusCode = 200 + }; + } + + private static void SetExecutionEnvironment() + { + const string envName = "AWS_EXECUTION_ENV"; + + var envValue = new StringBuilder(); + + // If there is an existing execution environment variable add the annotations package as a suffix. + if(!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(envName))) + { + envValue.Append($"{Environment.GetEnvironmentVariable(envName)}_"); + } + + envValue.Append("lib/amazon-lambda-annotations#{ANNOTATIONS_ASSEMBLY_VERSION}"); + + Environment.SetEnvironmentVariable(envName, envValue.ToString()); + } + } +} \ No newline at end of file diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetIHttpResult_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetIHttpResult_Generated.g.cs new file mode 100644 index 000000000..b969dd378 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetIHttpResult_Generated.g.cs @@ -0,0 +1,185 @@ +// + +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using Amazon.Lambda.Core; +using Amazon.Lambda.Annotations.APIGateway; + +namespace TestCustomAuthorizerApp +{ + public class ProtectedFunction_GetIHttpResult_Generated + { + private readonly ProtectedFunction protectedFunction; + private readonly Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer serializer; + + /// + /// Default constructor. This constructor is used by Lambda to construct the instance. When invoked in a Lambda environment + /// the AWS credentials will come from the IAM role associated with the function and the AWS region will be set to the + /// region the Lambda function is executed in. + /// + public ProtectedFunction_GetIHttpResult_Generated() + { + SetExecutionEnvironment(); + protectedFunction = new ProtectedFunction(); + serializer = new Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer(); + } + + /// + /// The generated Lambda function handler for + /// + /// The API Gateway request object that will be processed by the Lambda function handler. + /// The ILambdaContext that provides methods for logging and describing the Lambda environment. + /// Result of the Lambda function execution + public System.IO.Stream GetIHttpResult(Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest __request__, Amazon.Lambda.Core.ILambdaContext __context__) + { + var validationErrors = new List(); + + var userId = default(string); + if (__request__.RequestContext?.Authorizer?.Lambda == null || __request__.RequestContext?.Authorizer?.Lambda.ContainsKey("userId") == false) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogDebug("Authorizer attribute 'userId' was missing, returning unauthorized."); +#else + __context__.Logger.Log("Authorizer attribute 'userId' was missing, returning unauthorized."); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + var __unauthorizedStream__ = new System.IO.MemoryStream(); + serializer.Serialize(__unauthorized__, __unauthorizedStream__); + __unauthorizedStream__.Position = 0; + return __unauthorizedStream__; + } + + try + { + var __authValue_userId__ = __request__.RequestContext.Authorizer.Lambda["userId"]; + userId = (string)Convert.ChangeType(__authValue_userId__?.ToString(), typeof(string)); + } + catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogError(e, "Failed to convert authorizer attribute 'userId', returning unauthorized."); +#else + __context__.Logger.Log("Failed to convert authorizer attribute 'userId', returning unauthorized. Exception: " + e.ToString()); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + var __unauthorizedStream__ = new System.IO.MemoryStream(); + serializer.Serialize(__unauthorized__, __unauthorizedStream__); + __unauthorizedStream__.Position = 0; + return __unauthorizedStream__; + } + + var email = default(string); + if (__request__.RequestContext?.Authorizer?.Lambda == null || __request__.RequestContext?.Authorizer?.Lambda.ContainsKey("email") == false) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogDebug("Authorizer attribute 'email' was missing, returning unauthorized."); +#else + __context__.Logger.Log("Authorizer attribute 'email' was missing, returning unauthorized."); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + var __unauthorizedStream__ = new System.IO.MemoryStream(); + serializer.Serialize(__unauthorized__, __unauthorizedStream__); + __unauthorizedStream__.Position = 0; + return __unauthorizedStream__; + } + + try + { + var __authValue_email__ = __request__.RequestContext.Authorizer.Lambda["email"]; + email = (string)Convert.ChangeType(__authValue_email__?.ToString(), typeof(string)); + } + catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogError(e, "Failed to convert authorizer attribute 'email', returning unauthorized."); +#else + __context__.Logger.Log("Failed to convert authorizer attribute 'email', returning unauthorized. Exception: " + e.ToString()); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + var __unauthorizedStream__ = new System.IO.MemoryStream(); + serializer.Serialize(__unauthorized__, __unauthorizedStream__); + __unauthorizedStream__.Position = 0; + return __unauthorizedStream__; + } + + // return 400 Bad Request if there exists a validation error + if (validationErrors.Any()) + { + var errorResult = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Body = @$"{{""message"": ""{validationErrors.Count} validation error(s) detected: {string.Join(",", validationErrors)}""}}", + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "ValidationException"} + }, + StatusCode = 400 + }; + var errorStream = new System.IO.MemoryStream(); + serializer.Serialize(errorResult, errorStream); + errorStream.Position = 0; + return errorStream; + } + + var httpResults = protectedFunction.GetIHttpResult(userId, email, __context__); + HttpResultSerializationOptions.ProtocolFormat serializationFormat = HttpResultSerializationOptions.ProtocolFormat.HttpApi; + HttpResultSerializationOptions.ProtocolVersion serializationVersion = HttpResultSerializationOptions.ProtocolVersion.V2; + var serializationOptions = new HttpResultSerializationOptions { Format = serializationFormat, Version = serializationVersion, Serializer = serializer }; + var response = httpResults.Serialize(serializationOptions); + return response; + } + + private static void SetExecutionEnvironment() + { + const string envName = "AWS_EXECUTION_ENV"; + + var envValue = new StringBuilder(); + + // If there is an existing execution environment variable add the annotations package as a suffix. + if(!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(envName))) + { + envValue.Append($"{Environment.GetEnvironmentVariable(envName)}_"); + } + + envValue.Append("lib/amazon-lambda-annotations#{ANNOTATIONS_ASSEMBLY_VERSION}"); + + Environment.SetEnvironmentVariable(envName, envValue.ToString()); + } + } +} \ No newline at end of file diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetNonStringUserInfo_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetNonStringUserInfo_Generated.g.cs new file mode 100644 index 000000000..10301f09c --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetNonStringUserInfo_Generated.g.cs @@ -0,0 +1,225 @@ +// + +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using Amazon.Lambda.Core; + +namespace TestCustomAuthorizerApp +{ + public class ProtectedFunction_GetNonStringUserInfo_Generated + { + private readonly ProtectedFunction protectedFunction; + private readonly Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer serializer; + + /// + /// Default constructor. This constructor is used by Lambda to construct the instance. When invoked in a Lambda environment + /// the AWS credentials will come from the IAM role associated with the function and the AWS region will be set to the + /// region the Lambda function is executed in. + /// + public ProtectedFunction_GetNonStringUserInfo_Generated() + { + SetExecutionEnvironment(); + protectedFunction = new ProtectedFunction(); + serializer = new Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer(); + } + + /// + /// The generated Lambda function handler for + /// + /// The API Gateway request object that will be processed by the Lambda function handler. + /// The ILambdaContext that provides methods for logging and describing the Lambda environment. + /// Result of the Lambda function execution + public Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse GetNonStringUserInfo(Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest __request__, Amazon.Lambda.Core.ILambdaContext __context__) + { + var validationErrors = new List(); + + var tenantId = default(int); + if (__request__.RequestContext?.Authorizer?.Lambda == null || __request__.RequestContext?.Authorizer?.Lambda.ContainsKey("numericTenantId") == false) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogDebug("Authorizer attribute 'numericTenantId' was missing, returning unauthorized."); +#else + __context__.Logger.Log("Authorizer attribute 'numericTenantId' was missing, returning unauthorized."); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + try + { + var __authValue_tenantId__ = __request__.RequestContext.Authorizer.Lambda["numericTenantId"]; + tenantId = (int)Convert.ChangeType(__authValue_tenantId__?.ToString(), typeof(int)); + } + catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogError(e, "Failed to convert authorizer attribute 'numericTenantId', returning unauthorized."); +#else + __context__.Logger.Log("Failed to convert authorizer attribute 'numericTenantId', returning unauthorized. Exception: " + e.ToString()); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + var isAdmin = default(bool); + if (__request__.RequestContext?.Authorizer?.Lambda == null || __request__.RequestContext?.Authorizer?.Lambda.ContainsKey("isAdmin") == false) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogDebug("Authorizer attribute 'isAdmin' was missing, returning unauthorized."); +#else + __context__.Logger.Log("Authorizer attribute 'isAdmin' was missing, returning unauthorized."); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + try + { + var __authValue_isAdmin__ = __request__.RequestContext.Authorizer.Lambda["isAdmin"]; + isAdmin = (bool)Convert.ChangeType(__authValue_isAdmin__?.ToString(), typeof(bool)); + } + catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogError(e, "Failed to convert authorizer attribute 'isAdmin', returning unauthorized."); +#else + __context__.Logger.Log("Failed to convert authorizer attribute 'isAdmin', returning unauthorized. Exception: " + e.ToString()); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + var score = default(double); + if (__request__.RequestContext?.Authorizer?.Lambda == null || __request__.RequestContext?.Authorizer?.Lambda.ContainsKey("score") == false) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogDebug("Authorizer attribute 'score' was missing, returning unauthorized."); +#else + __context__.Logger.Log("Authorizer attribute 'score' was missing, returning unauthorized."); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + try + { + var __authValue_score__ = __request__.RequestContext.Authorizer.Lambda["score"]; + score = (double)Convert.ChangeType(__authValue_score__?.ToString(), typeof(double)); + } + catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogError(e, "Failed to convert authorizer attribute 'score', returning unauthorized."); +#else + __context__.Logger.Log("Failed to convert authorizer attribute 'score', returning unauthorized. Exception: " + e.ToString()); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + // return 400 Bad Request if there exists a validation error + if (validationErrors.Any()) + { + var errorResult = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Body = @$"{{""message"": ""{validationErrors.Count} validation error(s) detected: {string.Join(",", validationErrors)}""}}", + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "ValidationException"} + }, + StatusCode = 400 + }; + return errorResult; + } + + var response = protectedFunction.GetNonStringUserInfo(__request__, tenantId, isAdmin, score, __context__); + var memoryStream = new MemoryStream(); + serializer.Serialize(response, memoryStream); + memoryStream.Position = 0; + + // convert stream to string + StreamReader reader = new StreamReader( memoryStream ); + var body = reader.ReadToEnd(); + + return new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Body = body, + Headers = new Dictionary + { + {"Content-Type", "application/json"} + }, + StatusCode = 200 + }; + } + + private static void SetExecutionEnvironment() + { + const string envName = "AWS_EXECUTION_ENV"; + + var envValue = new StringBuilder(); + + // If there is an existing execution environment variable add the annotations package as a suffix. + if(!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(envName))) + { + envValue.Append($"{Environment.GetEnvironmentVariable(envName)}_"); + } + + envValue.Append("lib/amazon-lambda-annotations#{ANNOTATIONS_ASSEMBLY_VERSION}"); + + Environment.SetEnvironmentVariable(envName, envValue.ToString()); + } + } +} \ No newline at end of file diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetProtectedData_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetProtectedData_Generated.g.cs new file mode 100644 index 000000000..0b5f94fb3 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetProtectedData_Generated.g.cs @@ -0,0 +1,68 @@ +// + +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using Amazon.Lambda.Core; + +namespace TestCustomAuthorizerApp +{ + public class ProtectedFunction_GetProtectedData_Generated + { + private readonly ProtectedFunction protectedFunction; + private readonly Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer serializer; + + /// + /// Default constructor. This constructor is used by Lambda to construct the instance. When invoked in a Lambda environment + /// the AWS credentials will come from the IAM role associated with the function and the AWS region will be set to the + /// region the Lambda function is executed in. + /// + public ProtectedFunction_GetProtectedData_Generated() + { + SetExecutionEnvironment(); + protectedFunction = new ProtectedFunction(); + serializer = new Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer(); + } + + /// + /// The generated Lambda function handler for + /// + /// The API Gateway request object that will be processed by the Lambda function handler. + /// The ILambdaContext that provides methods for logging and describing the Lambda environment. + /// Result of the Lambda function execution + public Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse GetProtectedData(Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest __request__, Amazon.Lambda.Core.ILambdaContext __context__) + { + var response = protectedFunction.GetProtectedData(__request__, __context__); + + return new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Body = response, + Headers = new Dictionary + { + {"Content-Type", "text/plain"} + }, + StatusCode = 200 + }; + } + + private static void SetExecutionEnvironment() + { + const string envName = "AWS_EXECUTION_ENV"; + + var envValue = new StringBuilder(); + + // If there is an existing execution environment variable add the annotations package as a suffix. + if(!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(envName))) + { + envValue.Append($"{Environment.GetEnvironmentVariable(envName)}_"); + } + + envValue.Append("lib/amazon-lambda-annotations#{ANNOTATIONS_ASSEMBLY_VERSION}"); + + Environment.SetEnvironmentVariable(envName, envValue.ToString()); + } + } +} \ No newline at end of file diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetRestUserInfo_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetRestUserInfo_Generated.g.cs new file mode 100644 index 000000000..0ae3d3107 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetRestUserInfo_Generated.g.cs @@ -0,0 +1,225 @@ +// + +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using Amazon.Lambda.Core; + +namespace TestCustomAuthorizerApp +{ + public class ProtectedFunction_GetRestUserInfo_Generated + { + private readonly ProtectedFunction protectedFunction; + private readonly Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer serializer; + + /// + /// Default constructor. This constructor is used by Lambda to construct the instance. When invoked in a Lambda environment + /// the AWS credentials will come from the IAM role associated with the function and the AWS region will be set to the + /// region the Lambda function is executed in. + /// + public ProtectedFunction_GetRestUserInfo_Generated() + { + SetExecutionEnvironment(); + protectedFunction = new ProtectedFunction(); + serializer = new Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer(); + } + + /// + /// The generated Lambda function handler for + /// + /// The API Gateway request object that will be processed by the Lambda function handler. + /// The ILambdaContext that provides methods for logging and describing the Lambda environment. + /// Result of the Lambda function execution + public Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse GetRestUserInfo(Amazon.Lambda.APIGatewayEvents.APIGatewayProxyRequest __request__, Amazon.Lambda.Core.ILambdaContext __context__) + { + var validationErrors = new List(); + + var userId = default(string); + if (__request__.RequestContext?.Authorizer == null || __request__.RequestContext?.Authorizer.ContainsKey("userId") == false) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogDebug("Authorizer attribute 'userId' was missing, returning unauthorized."); +#else + __context__.Logger.Log("Authorizer attribute 'userId' was missing, returning unauthorized."); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + try + { + var __authValue_userId__ = __request__.RequestContext.Authorizer["userId"]; + userId = (string)Convert.ChangeType(__authValue_userId__?.ToString(), typeof(string)); + } + catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogError(e, "Failed to convert authorizer attribute 'userId', returning unauthorized."); +#else + __context__.Logger.Log("Failed to convert authorizer attribute 'userId', returning unauthorized. Exception: " + e.ToString()); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + var email = default(string); + if (__request__.RequestContext?.Authorizer == null || __request__.RequestContext?.Authorizer.ContainsKey("email") == false) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogDebug("Authorizer attribute 'email' was missing, returning unauthorized."); +#else + __context__.Logger.Log("Authorizer attribute 'email' was missing, returning unauthorized."); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + try + { + var __authValue_email__ = __request__.RequestContext.Authorizer["email"]; + email = (string)Convert.ChangeType(__authValue_email__?.ToString(), typeof(string)); + } + catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogError(e, "Failed to convert authorizer attribute 'email', returning unauthorized."); +#else + __context__.Logger.Log("Failed to convert authorizer attribute 'email', returning unauthorized. Exception: " + e.ToString()); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + var tenantId = default(string); + if (__request__.RequestContext?.Authorizer == null || __request__.RequestContext?.Authorizer.ContainsKey("tenantId") == false) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogDebug("Authorizer attribute 'tenantId' was missing, returning unauthorized."); +#else + __context__.Logger.Log("Authorizer attribute 'tenantId' was missing, returning unauthorized."); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + try + { + var __authValue_tenantId__ = __request__.RequestContext.Authorizer["tenantId"]; + tenantId = (string)Convert.ChangeType(__authValue_tenantId__?.ToString(), typeof(string)); + } + catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogError(e, "Failed to convert authorizer attribute 'tenantId', returning unauthorized."); +#else + __context__.Logger.Log("Failed to convert authorizer attribute 'tenantId', returning unauthorized. Exception: " + e.ToString()); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + // return 400 Bad Request if there exists a validation error + if (validationErrors.Any()) + { + var errorResult = new Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse + { + Body = @$"{{""message"": ""{validationErrors.Count} validation error(s) detected: {string.Join(",", validationErrors)}""}}", + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "ValidationException"} + }, + StatusCode = 400 + }; + return errorResult; + } + + var response = protectedFunction.GetRestUserInfo(userId, email, tenantId, __context__); + var memoryStream = new MemoryStream(); + serializer.Serialize(response, memoryStream); + memoryStream.Position = 0; + + // convert stream to string + StreamReader reader = new StreamReader( memoryStream ); + var body = reader.ReadToEnd(); + + return new Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse + { + Body = body, + Headers = new Dictionary + { + {"Content-Type", "application/json"} + }, + StatusCode = 200 + }; + } + + private static void SetExecutionEnvironment() + { + const string envName = "AWS_EXECUTION_ENV"; + + var envValue = new StringBuilder(); + + // If there is an existing execution environment variable add the annotations package as a suffix. + if(!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(envName))) + { + envValue.Append($"{Environment.GetEnvironmentVariable(envName)}_"); + } + + envValue.Append("lib/amazon-lambda-annotations#{ANNOTATIONS_ASSEMBLY_VERSION}"); + + Environment.SetEnvironmentVariable(envName, envValue.ToString()); + } + } +} \ No newline at end of file diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetUserInfo_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetUserInfo_Generated.g.cs new file mode 100644 index 000000000..469a54036 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetUserInfo_Generated.g.cs @@ -0,0 +1,225 @@ +// + +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using Amazon.Lambda.Core; + +namespace TestCustomAuthorizerApp +{ + public class ProtectedFunction_GetUserInfo_Generated + { + private readonly ProtectedFunction protectedFunction; + private readonly Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer serializer; + + /// + /// Default constructor. This constructor is used by Lambda to construct the instance. When invoked in a Lambda environment + /// the AWS credentials will come from the IAM role associated with the function and the AWS region will be set to the + /// region the Lambda function is executed in. + /// + public ProtectedFunction_GetUserInfo_Generated() + { + SetExecutionEnvironment(); + protectedFunction = new ProtectedFunction(); + serializer = new Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer(); + } + + /// + /// The generated Lambda function handler for + /// + /// The API Gateway request object that will be processed by the Lambda function handler. + /// The ILambdaContext that provides methods for logging and describing the Lambda environment. + /// Result of the Lambda function execution + public Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse GetUserInfo(Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest __request__, Amazon.Lambda.Core.ILambdaContext __context__) + { + var validationErrors = new List(); + + var userId = default(string); + if (__request__.RequestContext?.Authorizer?.Lambda == null || __request__.RequestContext?.Authorizer?.Lambda.ContainsKey("userId") == false) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogDebug("Authorizer attribute 'userId' was missing, returning unauthorized."); +#else + __context__.Logger.Log("Authorizer attribute 'userId' was missing, returning unauthorized."); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + try + { + var __authValue_userId__ = __request__.RequestContext.Authorizer.Lambda["userId"]; + userId = (string)Convert.ChangeType(__authValue_userId__?.ToString(), typeof(string)); + } + catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogError(e, "Failed to convert authorizer attribute 'userId', returning unauthorized."); +#else + __context__.Logger.Log("Failed to convert authorizer attribute 'userId', returning unauthorized. Exception: " + e.ToString()); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + var email = default(string); + if (__request__.RequestContext?.Authorizer?.Lambda == null || __request__.RequestContext?.Authorizer?.Lambda.ContainsKey("email") == false) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogDebug("Authorizer attribute 'email' was missing, returning unauthorized."); +#else + __context__.Logger.Log("Authorizer attribute 'email' was missing, returning unauthorized."); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + try + { + var __authValue_email__ = __request__.RequestContext.Authorizer.Lambda["email"]; + email = (string)Convert.ChangeType(__authValue_email__?.ToString(), typeof(string)); + } + catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogError(e, "Failed to convert authorizer attribute 'email', returning unauthorized."); +#else + __context__.Logger.Log("Failed to convert authorizer attribute 'email', returning unauthorized. Exception: " + e.ToString()); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + var tenantId = default(string); + if (__request__.RequestContext?.Authorizer?.Lambda == null || __request__.RequestContext?.Authorizer?.Lambda.ContainsKey("tenantId") == false) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogDebug("Authorizer attribute 'tenantId' was missing, returning unauthorized."); +#else + __context__.Logger.Log("Authorizer attribute 'tenantId' was missing, returning unauthorized."); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + try + { + var __authValue_tenantId__ = __request__.RequestContext.Authorizer.Lambda["tenantId"]; + tenantId = (string)Convert.ChangeType(__authValue_tenantId__?.ToString(), typeof(string)); + } + catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogError(e, "Failed to convert authorizer attribute 'tenantId', returning unauthorized."); +#else + __context__.Logger.Log("Failed to convert authorizer attribute 'tenantId', returning unauthorized. Exception: " + e.ToString()); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + // return 400 Bad Request if there exists a validation error + if (validationErrors.Any()) + { + var errorResult = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Body = @$"{{""message"": ""{validationErrors.Count} validation error(s) detected: {string.Join(",", validationErrors)}""}}", + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "ValidationException"} + }, + StatusCode = 400 + }; + return errorResult; + } + + var response = protectedFunction.GetUserInfo(userId, email, tenantId, __context__); + var memoryStream = new MemoryStream(); + serializer.Serialize(response, memoryStream); + memoryStream.Position = 0; + + // convert stream to string + StreamReader reader = new StreamReader( memoryStream ); + var body = reader.ReadToEnd(); + + return new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Body = body, + Headers = new Dictionary + { + {"Content-Type", "application/json"} + }, + StatusCode = 200 + }; + } + + private static void SetExecutionEnvironment() + { + const string envName = "AWS_EXECUTION_ENV"; + + var envValue = new StringBuilder(); + + // If there is an existing execution environment variable add the annotations package as a suffix. + if(!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(envName))) + { + envValue.Append($"{Environment.GetEnvironmentVariable(envName)}_"); + } + + envValue.Append("lib/amazon-lambda-annotations#{ANNOTATIONS_ASSEMBLY_VERSION}"); + + Environment.SetEnvironmentVariable(envName, envValue.ToString()); + } + } +} \ No newline at end of file diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_HealthCheck_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_HealthCheck_Generated.g.cs new file mode 100644 index 000000000..df53d3aa0 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_HealthCheck_Generated.g.cs @@ -0,0 +1,68 @@ +// + +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using Amazon.Lambda.Core; + +namespace TestCustomAuthorizerApp +{ + public class ProtectedFunction_HealthCheck_Generated + { + private readonly ProtectedFunction protectedFunction; + private readonly Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer serializer; + + /// + /// Default constructor. This constructor is used by Lambda to construct the instance. When invoked in a Lambda environment + /// the AWS credentials will come from the IAM role associated with the function and the AWS region will be set to the + /// region the Lambda function is executed in. + /// + public ProtectedFunction_HealthCheck_Generated() + { + SetExecutionEnvironment(); + protectedFunction = new ProtectedFunction(); + serializer = new Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer(); + } + + /// + /// The generated Lambda function handler for + /// + /// The API Gateway request object that will be processed by the Lambda function handler. + /// The ILambdaContext that provides methods for logging and describing the Lambda environment. + /// Result of the Lambda function execution + public Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse HealthCheck(Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest __request__, Amazon.Lambda.Core.ILambdaContext __context__) + { + var response = protectedFunction.HealthCheck(__context__); + + return new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Body = response, + Headers = new Dictionary + { + {"Content-Type", "text/plain"} + }, + StatusCode = 200 + }; + } + + private static void SetExecutionEnvironment() + { + const string envName = "AWS_EXECUTION_ENV"; + + var envValue = new StringBuilder(); + + // If there is an existing execution environment variable add the annotations package as a suffix. + if(!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(envName))) + { + envValue.Append($"{Environment.GetEnvironmentVariable(envName)}_"); + } + + envValue.Append("lib/amazon-lambda-annotations#{ANNOTATIONS_ASSEMBLY_VERSION}"); + + Environment.SetEnvironmentVariable(envName, envValue.ToString()); + } + } +} \ No newline at end of file diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/customAuthorizerApp.template b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/customAuthorizerApp.template new file mode 100644 index 000000000..08786963a --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/customAuthorizerApp.template @@ -0,0 +1,390 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Transform": "AWS::Serverless-2016-10-31", + "Description": "This template is partially managed by Amazon.Lambda.Annotations (v{ANNOTATIONS_ASSEMBLY_VERSION}).", + "Resources": { + "AnnotationsHttpApi": { + "Type": "AWS::Serverless::HttpApi", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "Auth": { + "Authorizers": { + "HttpApiLambdaAuthorizer": { + "FunctionArn": { + "Fn::GetAtt": [ + "CustomAuthorizer", + "Arn" + ] + }, + "AuthorizerPayloadFormatVersion": "2.0", + "EnableSimpleResponses": true, + "Identity": { + "Headers": [ + "authorization" + ] + }, + "EnableFunctionDefaultPermissions": true, + "AuthorizerResultTtlInSeconds": 0 + } + } + } + } + }, + "AnnotationsRestApi": { + "Type": "AWS::Serverless::Api", + "Properties": { + "StageName": "Prod", + "Auth": { + "Authorizers": { + "RestApiLambdaAuthorizer": { + "FunctionArn": { + "Fn::GetAtt": [ + "RestApiAuthorizer", + "Arn" + ] + }, + "Identity": { + "Header": "Authorization" + }, + "FunctionPayloadType": "TOKEN" + } + } + } + }, + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + } + }, + "CustomAuthorizer": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestProject::TestCustomAuthorizerApp.AuthorizerFunction_HttpApiAuthorize_Generated::HttpApiAuthorize" + } + }, + "RestApiAuthorizer": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestProject::TestCustomAuthorizerApp.AuthorizerFunction_RestApiAuthorize_Generated::RestApiAuthorize" + } + }, + "ProtectedEndpoint": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "Auth.Authorizer", + "ApiId.Ref" + ] + } + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestProject::TestCustomAuthorizerApp.ProtectedFunction_GetProtectedData_Generated::GetProtectedData", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/protected", + "Method": "GET", + "Auth": { + "Authorizer": "HttpApiLambdaAuthorizer" + }, + "ApiId": { + "Ref": "AnnotationsHttpApi" + } + } + } + } + } + }, + "GetUserInfo": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "Auth.Authorizer", + "ApiId.Ref" + ] + } + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestProject::TestCustomAuthorizerApp.ProtectedFunction_GetUserInfo_Generated::GetUserInfo", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/user-info", + "Method": "GET", + "Auth": { + "Authorizer": "HttpApiLambdaAuthorizer" + }, + "ApiId": { + "Ref": "AnnotationsHttpApi" + } + } + } + } + } + }, + "HealthCheck": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestProject::TestCustomAuthorizerApp.ProtectedFunction_HealthCheck_Generated::HealthCheck", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/health", + "Method": "GET" + } + } + } + } + }, + "RestUserInfo": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "Auth.Authorizer", + "RestApiId.Ref" + ] + } + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestProject::TestCustomAuthorizerApp.ProtectedFunction_GetRestUserInfo_Generated::GetRestUserInfo", + "Events": { + "RootGet": { + "Type": "Api", + "Properties": { + "Path": "/api/rest-user-info", + "Method": "GET", + "Auth": { + "Authorizer": "RestApiLambdaAuthorizer" + }, + "RestApiId": { + "Ref": "AnnotationsRestApi" + } + } + } + } + } + }, + "HttpApiV1UserInfo": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "PayloadFormatVersion", + "Auth.Authorizer", + "ApiId.Ref" + ] + } + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestProject::TestCustomAuthorizerApp.ProtectedFunction_GetHttpApiV1UserInfo_Generated::GetHttpApiV1UserInfo", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/http-v1-user-info", + "Method": "GET", + "PayloadFormatVersion": "1.0", + "Auth": { + "Authorizer": "HttpApiLambdaAuthorizer" + }, + "ApiId": { + "Ref": "AnnotationsHttpApi" + } + } + } + } + } + }, + "IHttpResultUserInfo": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "Auth.Authorizer", + "ApiId.Ref" + ] + } + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestProject::TestCustomAuthorizerApp.ProtectedFunction_GetIHttpResult_Generated::GetIHttpResult", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/ihttpresult-user-info", + "Method": "GET", + "Auth": { + "Authorizer": "HttpApiLambdaAuthorizer" + }, + "ApiId": { + "Ref": "AnnotationsHttpApi" + } + } + } + } + } + }, + "NonStringUserInfo": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "Auth.Authorizer", + "ApiId.Ref" + ] + } + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestProject::TestCustomAuthorizerApp.ProtectedFunction_GetNonStringUserInfo_Generated::GetNonStringUserInfo", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/nonstring-user-info", + "Method": "GET", + "Auth": { + "Authorizer": "HttpApiLambdaAuthorizer" + }, + "ApiId": { + "Ref": "AnnotationsHttpApi" + } + } + } + } + } + } + } +} diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs index cfd8ac35f..569c59f45 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs @@ -1644,9 +1644,114 @@ public async Task CustomAuthorizerNonStringTest() Assert.Equal(expectedTemplateContent, actualTemplateContent); } + /// + /// Tests the full authorizer annotation template generation using TestCustomAuthorizerApp's AuthorizerFunction.cs. + /// This validates that [HttpApiAuthorizer] and [RestApiAuthorizer] attributes produce correct CloudFormation + /// template output including AnnotationsHttpApi/AnnotationsRestApi resources with Auth.Authorizers sections. + /// + [Fact] + public async Task CustomAuthorizerAppAuthorizerDefinitionsTest() + { + var expectedTemplateContent = await ReadSnapshotContent(Path.Combine("Snapshots", "ServerlessTemplates", "customAuthorizerApp.template")); + var expectedHttpApiAuthorizeGenerated = await ReadSnapshotContent(Path.Combine("Snapshots", "AuthorizerFunction_HttpApiAuthorize_Generated.g.cs")); + var expectedRestApiAuthorizeGenerated = await ReadSnapshotContent(Path.Combine("Snapshots", "AuthorizerFunction_RestApiAuthorize_Generated.g.cs")); + var expectedGetProtectedDataGenerated = await ReadSnapshotContent(Path.Combine("Snapshots", "ProtectedFunction_GetProtectedData_Generated.g.cs")); + var expectedGetUserInfoGenerated = await ReadSnapshotContent(Path.Combine("Snapshots", "ProtectedFunction_GetUserInfo_Generated.g.cs")); + var expectedHealthCheckGenerated = await ReadSnapshotContent(Path.Combine("Snapshots", "ProtectedFunction_HealthCheck_Generated.g.cs")); + var expectedGetRestUserInfoGenerated = await ReadSnapshotContent(Path.Combine("Snapshots", "ProtectedFunction_GetRestUserInfo_Generated.g.cs")); + var expectedGetHttpApiV1UserInfoGenerated = await ReadSnapshotContent(Path.Combine("Snapshots", "ProtectedFunction_GetHttpApiV1UserInfo_Generated.g.cs")); + var expectedGetIHttpResultGenerated = await ReadSnapshotContent(Path.Combine("Snapshots", "ProtectedFunction_GetIHttpResult_Generated.g.cs")); + var expectedGetNonStringUserInfoGenerated = await ReadSnapshotContent(Path.Combine("Snapshots", "ProtectedFunction_GetNonStringUserInfo_Generated.g.cs")); + + await new VerifyCS.Test + { + TestState = + { + Sources = + { + (Path.Combine("TestCustomAuthorizerApp", "AuthorizerFunction.cs"), File.ReadAllText(Path.Combine("TestCustomAuthorizerApp", "AuthorizerFunction.cs"))), + (Path.Combine("TestCustomAuthorizerApp", "ProtectedFunction.cs"), File.ReadAllText(Path.Combine("TestCustomAuthorizerApp", "ProtectedFunction.cs"))), + (Path.Combine("Amazon.Lambda.Annotations", "LambdaFunctionAttribute.cs"), File.ReadAllText(Path.Combine("Amazon.Lambda.Annotations", "LambdaFunctionAttribute.cs"))), + (Path.Combine("Amazon.Lambda.Annotations", "LambdaStartupAttribute.cs"), File.ReadAllText(Path.Combine("Amazon.Lambda.Annotations", "LambdaStartupAttribute.cs"))), + (Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "HttpApiAttribute.cs"), File.ReadAllText(Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "HttpApiAttribute.cs"))), + (Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "RestApiAttribute.cs"), File.ReadAllText(Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "RestApiAttribute.cs"))), + (Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "HttpApiAuthorizerAttribute.cs"), File.ReadAllText(Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "HttpApiAuthorizerAttribute.cs"))), + (Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "RestApiAuthorizerAttribute.cs"), File.ReadAllText(Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "RestApiAuthorizerAttribute.cs"))), + (Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "FromCustomAuthorizerAttribute.cs"), File.ReadAllText(Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "FromCustomAuthorizerAttribute.cs"))), + }, + GeneratedSources = + { + ( + typeof(SourceGenerator.Generator), + "AuthorizerFunction_HttpApiAuthorize_Generated.g.cs", + SourceText.From(expectedHttpApiAuthorizeGenerated, Encoding.UTF8, SourceHashAlgorithm.Sha256) + ), + ( + typeof(SourceGenerator.Generator), + "AuthorizerFunction_RestApiAuthorize_Generated.g.cs", + SourceText.From(expectedRestApiAuthorizeGenerated, Encoding.UTF8, SourceHashAlgorithm.Sha256) + ), + ( + typeof(SourceGenerator.Generator), + "ProtectedFunction_GetProtectedData_Generated.g.cs", + SourceText.From(expectedGetProtectedDataGenerated, Encoding.UTF8, SourceHashAlgorithm.Sha256) + ), + ( + typeof(SourceGenerator.Generator), + "ProtectedFunction_GetUserInfo_Generated.g.cs", + SourceText.From(expectedGetUserInfoGenerated, Encoding.UTF8, SourceHashAlgorithm.Sha256) + ), + ( + typeof(SourceGenerator.Generator), + "ProtectedFunction_HealthCheck_Generated.g.cs", + SourceText.From(expectedHealthCheckGenerated, Encoding.UTF8, SourceHashAlgorithm.Sha256) + ), + ( + typeof(SourceGenerator.Generator), + "ProtectedFunction_GetRestUserInfo_Generated.g.cs", + SourceText.From(expectedGetRestUserInfoGenerated, Encoding.UTF8, SourceHashAlgorithm.Sha256) + ), + ( + typeof(SourceGenerator.Generator), + "ProtectedFunction_GetHttpApiV1UserInfo_Generated.g.cs", + SourceText.From(expectedGetHttpApiV1UserInfoGenerated, Encoding.UTF8, SourceHashAlgorithm.Sha256) + ), + ( + typeof(SourceGenerator.Generator), + "ProtectedFunction_GetIHttpResult_Generated.g.cs", + SourceText.From(expectedGetIHttpResultGenerated, Encoding.UTF8, SourceHashAlgorithm.Sha256) + ), + ( + typeof(SourceGenerator.Generator), + "ProtectedFunction_GetNonStringUserInfo_Generated.g.cs", + SourceText.From(expectedGetNonStringUserInfoGenerated, Encoding.UTF8, SourceHashAlgorithm.Sha256) + ) + }, + ExpectedDiagnostics = + { + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("AuthorizerFunction_HttpApiAuthorize_Generated.g.cs", expectedHttpApiAuthorizeGenerated), + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("AuthorizerFunction_RestApiAuthorize_Generated.g.cs", expectedRestApiAuthorizeGenerated), + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("ProtectedFunction_GetProtectedData_Generated.g.cs", expectedGetProtectedDataGenerated), + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("ProtectedFunction_GetUserInfo_Generated.g.cs", expectedGetUserInfoGenerated), + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("ProtectedFunction_HealthCheck_Generated.g.cs", expectedHealthCheckGenerated), + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("ProtectedFunction_GetRestUserInfo_Generated.g.cs", expectedGetRestUserInfoGenerated), + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("ProtectedFunction_GetHttpApiV1UserInfo_Generated.g.cs", expectedGetHttpApiV1UserInfoGenerated), + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("ProtectedFunction_GetIHttpResult_Generated.g.cs", expectedGetIHttpResultGenerated), + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("ProtectedFunction_GetNonStringUserInfo_Generated.g.cs", expectedGetNonStringUserInfoGenerated), + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments($"TestCustomAuthorizerApp{Path.DirectorySeparatorChar}serverless.template", expectedTemplateContent), + }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net60 + } + }.RunAsync(); + + var actualTemplateContent = File.ReadAllText(Path.Combine("TestCustomAuthorizerApp", "serverless.template")); + Assert.Equal(expectedTemplateContent, actualTemplateContent); + } + public void Dispose() { File.Delete(Path.Combine("TestServerlessApp", "serverless.template")); + File.Delete(Path.Combine("TestCustomAuthorizerApp", "serverless.template")); } private async static Task ReadSnapshotContent(string snapshotPath, bool trimContent = true) diff --git a/Libraries/test/TestCustomAuthorizerApp/AuthorizerFunction.cs b/Libraries/test/TestCustomAuthorizerApp/AuthorizerFunction.cs index 1fc03f2e1..cfbd2adcd 100644 --- a/Libraries/test/TestCustomAuthorizerApp/AuthorizerFunction.cs +++ b/Libraries/test/TestCustomAuthorizerApp/AuthorizerFunction.cs @@ -2,6 +2,8 @@ using Amazon.Lambda.Annotations.APIGateway; using Amazon.Lambda.APIGatewayEvents; using Amazon.Lambda.Core; +using System.Collections.Generic; +using System; // Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class. [assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] diff --git a/Libraries/test/TestCustomAuthorizerApp/ProtectedFunction.cs b/Libraries/test/TestCustomAuthorizerApp/ProtectedFunction.cs index 39734a579..950b86064 100644 --- a/Libraries/test/TestCustomAuthorizerApp/ProtectedFunction.cs +++ b/Libraries/test/TestCustomAuthorizerApp/ProtectedFunction.cs @@ -1,3 +1,4 @@ +using System; using Amazon.Lambda.Annotations; using Amazon.Lambda.Annotations.APIGateway; using Amazon.Lambda.APIGatewayEvents; diff --git a/Libraries/test/TestCustomAuthorizerApp/TestCustomAuthorizerApp.csproj b/Libraries/test/TestCustomAuthorizerApp/TestCustomAuthorizerApp.csproj index a9b30addb..6abb25b6c 100644 --- a/Libraries/test/TestCustomAuthorizerApp/TestCustomAuthorizerApp.csproj +++ b/Libraries/test/TestCustomAuthorizerApp/TestCustomAuthorizerApp.csproj @@ -1,6 +1,6 @@ - net8.0 + net6.0 enable enable true diff --git a/Libraries/test/TestCustomAuthorizerApp/serverless.template b/Libraries/test/TestCustomAuthorizerApp/serverless.template index c547f710e..91e9f85a1 100644 --- a/Libraries/test/TestCustomAuthorizerApp/serverless.template +++ b/Libraries/test/TestCustomAuthorizerApp/serverless.template @@ -63,7 +63,7 @@ "Tool": "Amazon.Lambda.Annotations" }, "Properties": { - "Runtime": "dotnet8", + "Runtime": "dotnet6", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -80,7 +80,7 @@ "Tool": "Amazon.Lambda.Annotations" }, "Properties": { - "Runtime": "dotnet8", + "Runtime": "dotnet6", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -108,7 +108,7 @@ } }, "Properties": { - "Runtime": "dotnet8", + "Runtime": "dotnet6", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -151,7 +151,7 @@ } }, "Properties": { - "Runtime": "dotnet8", + "Runtime": "dotnet6", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -192,7 +192,7 @@ } }, "Properties": { - "Runtime": "dotnet8", + "Runtime": "dotnet6", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -229,7 +229,7 @@ } }, "Properties": { - "Runtime": "dotnet8", + "Runtime": "dotnet6", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -273,7 +273,7 @@ } }, "Properties": { - "Runtime": "dotnet8", + "Runtime": "dotnet6", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -317,7 +317,7 @@ } }, "Properties": { - "Runtime": "dotnet8", + "Runtime": "dotnet6", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -360,7 +360,7 @@ } }, "Properties": { - "Runtime": "dotnet8", + "Runtime": "dotnet6", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, diff --git a/Libraries/test/TestExecutableServerlessApp/serverless.template b/Libraries/test/TestExecutableServerlessApp/serverless.template index 1912a4498..8f71f0ecc 100644 --- a/Libraries/test/TestExecutableServerlessApp/serverless.template +++ b/Libraries/test/TestExecutableServerlessApp/serverless.template @@ -21,662 +21,7 @@ ] } }, - "Resources": { - "TestServerlessAppVoidExampleVoidReturnGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestExecutableServerlessApp" - ] - }, - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "VoidReturn" - } - } - } - }, - "TestServerlessAppDynamicExampleDynamicReturnGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestExecutableServerlessApp" - ] - }, - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "DynamicReturn" - } - } - } - }, - "TestServerlessAppDynamicExampleDynamicInputGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestExecutableServerlessApp" - ] - }, - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "DynamicInput" - } - } - } - }, - "GreeterSayHello": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method", - "PayloadFormatVersion" - ] - } - }, - "Properties": { - "MemorySize": 1024, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestExecutableServerlessApp" - ] - }, - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "SayHello" - } - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/Greeter/SayHello", - "Method": "GET", - "PayloadFormatVersion": "1.0" - } - } - } - } - }, - "GreeterSayHelloAsync": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method", - "PayloadFormatVersion" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 50, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestExecutableServerlessApp" - ] - }, - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "SayHelloAsync" - } - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/Greeter/SayHelloAsync", - "Method": "GET", - "PayloadFormatVersion": "1.0" - } - } - } - } - }, - "ToLower": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "Runtime": "provided.al2", - "CodeUri": ".", - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Zip", - "Handler": "TestExecutableServerlessApp", - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "ToLower" - } - } - } - }, - "TestServerlessAppIntrinsicExampleHasIntrinsicGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestExecutableServerlessApp" - ] - }, - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "HasIntrinsic" - } - } - } - }, - "TestServerlessAppParameterlessMethodWithResponseNoParameterWithResponseGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "Runtime": "provided.al2", - "CodeUri": ".", - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Zip", - "Handler": "TestExecutableServerlessApp", - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "NoParameterWithResponse" - } - } - } - }, - "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestExecutableServerlessApp" - ] - }, - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "OkResponseWithHeader" - } - }, - "Events": { - "RootGet": { - "Type": "Api", - "Properties": { - "Path": "/okresponsewithheader/{x}", - "Method": "GET" - } - } - } - } - }, - "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderAsyncGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestExecutableServerlessApp" - ] - }, - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "OkResponseWithHeaderAsync" - } - }, - "Events": { - "RootGet": { - "Type": "Api", - "Properties": { - "Path": "/okresponsewithheaderasync/{x}", - "Method": "GET" - } - } - } - } - }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2Generated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestExecutableServerlessApp" - ] - }, - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV2" - } - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/notfoundwithheaderv2/{x}", - "Method": "GET" - } - } - } - } - }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2AsyncGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestExecutableServerlessApp" - ] - }, - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV2Async" - } - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/notfoundwithheaderv2async/{x}", - "Method": "GET" - } - } - } - } - }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1Generated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method", - "PayloadFormatVersion" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestExecutableServerlessApp" - ] - }, - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV1" - } - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/notfoundwithheaderv1/{x}", - "Method": "GET", - "PayloadFormatVersion": "1.0" - } - } - } - } - }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1AsyncGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method", - "PayloadFormatVersion" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestExecutableServerlessApp" - ] - }, - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV1Async" - } - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/notfoundwithheaderv1async/{x}", - "Method": "GET", - "PayloadFormatVersion": "1.0" - } - } - } - } - }, - "TestServerlessAppNullableReferenceTypeExampleNullableHeaderHttpApiGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestExecutableServerlessApp" - ] - }, - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "NullableHeaderHttpApi" - } - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/nullableheaderhttpapi", - "Method": "GET" - } - } - } - } - }, - "ToUpper": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestExecutableServerlessApp" - ] - }, - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "ToUpper" - } - } - } - }, - "TestServerlessAppParameterlessMethodsNoParameterGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "Runtime": "provided.al2", - "CodeUri": ".", - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Zip", - "Handler": "TestExecutableServerlessApp", - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "NoParameter" - } - } - } - }, - "TestServerlessAppTaskExampleTaskReturnGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestExecutableServerlessApp" - ] - }, - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "TaskReturn" - } - } - } - }, - "TestExecutableServerlessAppSourceGenerationSerializationExampleGetPersonGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestExecutableServerlessApp" - ] - }, - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "GetPerson" - } - }, - "Events": { - "RootGet": { - "Type": "Api", - "Properties": { - "Path": "/", - "Method": "GET" - } - } - } - } - } - }, + "Resources": {}, "Outputs": { "RestApiURL": { "Description": "Rest API endpoint URL for Prod environment", diff --git a/Libraries/test/TestServerlessApp.IntegrationTests/IntegrationTestContextFixture.cs b/Libraries/test/TestServerlessApp.IntegrationTests/IntegrationTestContextFixture.cs index 8de9bab4a..2dcac07a1 100644 --- a/Libraries/test/TestServerlessApp.IntegrationTests/IntegrationTestContextFixture.cs +++ b/Libraries/test/TestServerlessApp.IntegrationTests/IntegrationTestContextFixture.cs @@ -73,6 +73,7 @@ public async Task InitializeAsync() // Get the SQS queue ARN from the physical resource ID (which is the queue URL) var queueUrl = await _cloudFormationHelper.GetResourcePhysicalIdAsync(_stackName, "TestQueue"); Console.WriteLine($"[IntegrationTest] TestQueue URL: {queueUrl}"); + Assert.False(string.IsNullOrEmpty(queueUrl), $"CloudFormation resource 'TestQueue' was not found in stack '{_stackName}'."); TestQueueARN = ConvertSqsUrlToArn(queueUrl); LambdaFunctions = await LambdaHelper.FilterByCloudFormationStackAsync(_stackName); Console.WriteLine($"[IntegrationTest] Found {LambdaFunctions.Count} Lambda functions: {string.Join(", ", LambdaFunctions.Select(f => f.Name ?? "(null)"))}"); diff --git a/Libraries/test/TestServerlessApp.NET8/serverless.template b/Libraries/test/TestServerlessApp.NET8/serverless.template index c42ff4a47..3cdbe66a9 100644 --- a/Libraries/test/TestServerlessApp.NET8/serverless.template +++ b/Libraries/test/TestServerlessApp.NET8/serverless.template @@ -2,23 +2,5 @@ "AWSTemplateFormatVersion": "2010-09-09", "Transform": "AWS::Serverless-2016-10-31", "Description": "This template is partially managed by Amazon.Lambda.Annotations (v1.9.0.0).", - "Resources": { - "TestServerlessAppNET8FunctionsToUpperGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "Runtime": "dotnet8", - "CodeUri": ".", - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Zip", - "Handler": "TestServerlessApp.NET8::TestServerlessApp.NET8.Functions_ToUpper_Generated::ToUpper" - } - } - } + "Resources": {} } \ No newline at end of file diff --git a/Libraries/test/TestServerlessApp/serverless.template b/Libraries/test/TestServerlessApp/serverless.template index 067b0adb3..3cdbe66a9 100644 --- a/Libraries/test/TestServerlessApp/serverless.template +++ b/Libraries/test/TestServerlessApp/serverless.template @@ -2,1153 +2,5 @@ "AWSTemplateFormatVersion": "2010-09-09", "Transform": "AWS::Serverless-2016-10-31", "Description": "This template is partially managed by Amazon.Lambda.Annotations (v1.9.0.0).", - "Resources": { - "AuthNameFallbackTest": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.AuthNameFallback_GetUserId_Generated::GetUserId" - ] - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/api/authorizer-fallback", - "Method": "GET" - } - } - } - } - }, - "TestServerlessAppComplexCalculatorAddGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootPost" - ], - "SyncedEventProperties": { - "RootPost": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.ComplexCalculator_Add_Generated::Add" - ] - }, - "Events": { - "RootPost": { - "Type": "HttpApi", - "Properties": { - "Path": "/ComplexCalculator/Add", - "Method": "POST" - } - } - } - } - }, - "TestServerlessAppComplexCalculatorSubtractGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootPost" - ], - "SyncedEventProperties": { - "RootPost": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.ComplexCalculator_Subtract_Generated::Subtract" - ] - }, - "Events": { - "RootPost": { - "Type": "HttpApi", - "Properties": { - "Path": "/ComplexCalculator/Subtract", - "Method": "POST" - } - } - } - } - }, - "HttpApiAuthorizerTest": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.CustomAuthorizerHttpApiExample_HttpApiAuthorizer_Generated::HttpApiAuthorizer" - ] - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/api/authorizer", - "Method": "GET" - } - } - } - } - }, - "HttpApiV1AuthorizerTest": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method", - "PayloadFormatVersion" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.CustomAuthorizerHttpApiV1Example_HttpApiV1Authorizer_Generated::HttpApiV1Authorizer" - ] - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/api/authorizer-v1", - "Method": "GET", - "PayloadFormatVersion": "1.0" - } - } - } - } - }, - "HttpApiNonString": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.CustomAuthorizerNonStringExample_HttpApiWithNonString_Generated::HttpApiWithNonString" - ] - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/api/authorizer-non-string", - "Method": "GET" - } - } - } - } - }, - "RestAuthorizerTest": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.CustomAuthorizerRestExample_RestAuthorizer_Generated::RestAuthorizer" - ] - }, - "Events": { - "RootGet": { - "Type": "Api", - "Properties": { - "Path": "/rest/authorizer", - "Method": "GET" - } - } - } - } - }, - "TestServerlessAppCustomAuthorizerWithIHttpResultsExampleAuthorizerWithIHttpResultsGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.CustomAuthorizerWithIHttpResultsExample_AuthorizerWithIHttpResults_Generated::AuthorizerWithIHttpResults" - ] - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/authorizerihttpresults", - "Method": "GET" - } - } - } - } - }, - "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_OkResponseWithHeader_Generated::OkResponseWithHeader" - ] - }, - "Events": { - "RootGet": { - "Type": "Api", - "Properties": { - "Path": "/okresponsewithheader/{x}", - "Method": "GET" - } - } - } - } - }, - "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderAsyncGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_OkResponseWithHeaderAsync_Generated::OkResponseWithHeaderAsync" - ] - }, - "Events": { - "RootGet": { - "Type": "Api", - "Properties": { - "Path": "/okresponsewithheaderasync/{x}", - "Method": "GET" - } - } - } - } - }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2Generated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV2_Generated::NotFoundResponseWithHeaderV2" - ] - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/notfoundwithheaderv2/{x}", - "Method": "GET" - } - } - } - } - }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2AsyncGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV2Async_Generated::NotFoundResponseWithHeaderV2Async" - ] - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/notfoundwithheaderv2async/{x}", - "Method": "GET" - } - } - } - } - }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1Generated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method", - "PayloadFormatVersion" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV1_Generated::NotFoundResponseWithHeaderV1" - ] - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/notfoundwithheaderv1/{x}", - "Method": "GET", - "PayloadFormatVersion": "1.0" - } - } - } - } - }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1AsyncGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method", - "PayloadFormatVersion" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV1Async_Generated::NotFoundResponseWithHeaderV1Async" - ] - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/notfoundwithheaderv1async/{x}", - "Method": "GET", - "PayloadFormatVersion": "1.0" - } - } - } - } - }, - "TestServerlessAppCustomizeResponseExamplesOkResponseWithCustomSerializerGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method", - "PayloadFormatVersion" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_OkResponseWithCustomSerializer_Generated::OkResponseWithCustomSerializer" - ] - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/okresponsewithcustomserializerasync/{firstName}/{lastName}", - "Method": "GET", - "PayloadFormatVersion": "1.0" - } - } - } - } - }, - "TestServerlessAppDynamicExampleDynamicReturnGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.DynamicExample_DynamicReturn_Generated::DynamicReturn" - ] - } - } - }, - "TestServerlessAppDynamicExampleDynamicInputGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.DynamicExample_DynamicInput_Generated::DynamicInput" - ] - } - } - }, - "TestServerlessAppFromScratchNoApiGatewayEventsReferenceToUpperGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.FromScratch.NoApiGatewayEventsReference_ToUpper_Generated::ToUpper" - ] - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/{text}", - "Method": "GET" - } - } - } - } - }, - "TestServerlessAppFromScratchNoSerializerAttributeReferenceToUpperGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.FromScratch.NoSerializerAttributeReference_ToUpper_Generated::ToUpper" - ] - } - } - }, - "GreeterSayHello": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method", - "PayloadFormatVersion" - ] - } - }, - "Properties": { - "MemorySize": 1024, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.Greeter_SayHello_Generated::SayHello" - ] - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/Greeter/SayHello", - "Method": "GET", - "PayloadFormatVersion": "1.0" - } - } - } - } - }, - "GreeterSayHelloAsync": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method", - "PayloadFormatVersion" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 50, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.Greeter_SayHelloAsync_Generated::SayHelloAsync" - ] - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/Greeter/SayHelloAsync", - "Method": "GET", - "PayloadFormatVersion": "1.0" - } - } - } - } - }, - "TestServerlessAppIntrinsicExampleHasIntrinsicGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.IntrinsicExample_HasIntrinsic_Generated::HasIntrinsic" - ] - } - } - }, - "TestServerlessAppNullableReferenceTypeExampleNullableHeaderHttpApiGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.NullableReferenceTypeExample_NullableHeaderHttpApi_Generated::NullableHeaderHttpApi" - ] - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/nullableheaderhttpapi", - "Method": "GET" - } - } - } - } - }, - "SimpleCalculatorAdd": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.SimpleCalculator_Add_Generated::Add" - ] - }, - "Events": { - "RootGet": { - "Type": "Api", - "Properties": { - "Path": "/SimpleCalculator/Add", - "Method": "GET" - } - } - } - } - }, - "SimpleCalculatorSubtract": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.SimpleCalculator_Subtract_Generated::Subtract" - ] - }, - "Events": { - "RootGet": { - "Type": "Api", - "Properties": { - "Path": "/SimpleCalculator/Subtract", - "Method": "GET" - } - } - } - } - }, - "SimpleCalculatorMultiply": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.SimpleCalculator_Multiply_Generated::Multiply" - ] - }, - "Events": { - "RootGet": { - "Type": "Api", - "Properties": { - "Path": "/SimpleCalculator/Multiply/{x}/{y}", - "Method": "GET" - } - } - } - } - }, - "SimpleCalculatorDivideAsync": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.SimpleCalculator_DivideAsync_Generated::DivideAsync" - ] - }, - "Events": { - "RootGet": { - "Type": "Api", - "Properties": { - "Path": "/SimpleCalculator/DivideAsync/{x}/{y}", - "Method": "GET" - } - } - } - } - }, - "PI": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.SimpleCalculator_Pi_Generated::Pi" - ] - } - } - }, - "Random": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.SimpleCalculator_Random_Generated::Random" - ] - } - } - }, - "Randoms": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.SimpleCalculator_Randoms_Generated::Randoms" - ] - } - } - }, - "SQSMessageHandler": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "TestQueueEvent" - ], - "SyncedEventProperties": { - "TestQueueEvent": [ - "Queue.Fn::GetAtt", - "BatchSize", - "FilterCriteria.Filters", - "FunctionResponseTypes", - "MaximumBatchingWindowInSeconds", - "ScalingConfig.MaximumConcurrency" - ] - } - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaSQSQueueExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.SqsMessageProcessing_HandleMessage_Generated::HandleMessage" - ] - }, - "Events": { - "TestQueueEvent": { - "Type": "SQS", - "Properties": { - "BatchSize": 50, - "FilterCriteria": { - "Filters": [ - { - "Pattern": "{ \"body\" : { \"RequestCode\" : [ \"BBBB\" ] } }" - } - ] - }, - "FunctionResponseTypes": [ - "ReportBatchItemFailures" - ], - "MaximumBatchingWindowInSeconds": 5, - "ScalingConfig": { - "MaximumConcurrency": 5 - }, - "Queue": { - "Fn::GetAtt": [ - "TestQueue", - "Arn" - ] - } - } - } - } - } - }, - "ToUpper": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.Sub1.Functions_ToUpper_Generated::ToUpper" - ] - } - } - }, - "TestServerlessAppTaskExampleTaskReturnGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.TaskExample_TaskReturn_Generated::TaskReturn" - ] - } - } - }, - "TestServerlessAppVoidExampleVoidReturnGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.VoidExample_VoidReturn_Generated::VoidReturn" - ] - } - } - } - } + "Resources": {} } \ No newline at end of file From 2c6e03be902787bf119687c05687055b1ebd2f38 Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Thu, 26 Feb 2026 12:55:43 -0500 Subject: [PATCH 04/14] make integration tests reliable --- .../Amazon.Lambda.Annotations.SourceGenerator.csproj | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Amazon.Lambda.Annotations.SourceGenerator.csproj b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Amazon.Lambda.Annotations.SourceGenerator.csproj index 9e68cf47f..3c894f59b 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Amazon.Lambda.Annotations.SourceGenerator.csproj +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Amazon.Lambda.Annotations.SourceGenerator.csproj @@ -63,6 +63,11 @@ + + From 8a97888aec2c016e62910c6e5421c7d543658d27 Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Mon, 9 Mar 2026 09:44:40 -0400 Subject: [PATCH 05/14] use one gateway --- .../Writers/CloudFormationWriter.cs | 10 +++ .../WriterTests/CloudFormationWriterTests.cs | 84 ++++++++++++++++++- .../serverless.template | 20 ++++- .../TestServerlessApp/serverless.template | 6 +- 4 files changed, 116 insertions(+), 4 deletions(-) diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs index 71e1e9adf..a63052207 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs @@ -245,6 +245,11 @@ private string ProcessRestApiAttribute(ILambdaFunctionSerializable lambdaFunctio if (!string.IsNullOrEmpty(restApiAttribute.Authorizer) && authorizerLookup.TryGetValue(restApiAttribute.Authorizer, out var authorizer)) { SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Auth.Authorizer", authorizer.Name); + } + + // Always reference the shared API resource if it exists, so all REST API functions share one endpoint + if (_templateWriter.Exists($"Resources.{REST_API_RESOURCE_NAME}")) + { SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, $"RestApiId.{REF}", REST_API_RESOURCE_NAME); } @@ -274,6 +279,11 @@ private string ProcessHttpApiAttribute(ILambdaFunctionSerializable lambdaFunctio if (!string.IsNullOrEmpty(httpApiAttribute.Authorizer) && authorizerLookup.TryGetValue(httpApiAttribute.Authorizer, out var authorizer)) { SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Auth.Authorizer", authorizer.Name); + } + + // Always reference the shared API resource if it exists, so all HTTP API functions share one endpoint + if (_templateWriter.Exists($"Resources.{HTTP_API_RESOURCE_NAME}")) + { SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, $"ApiId.{REF}", HTTP_API_RESOURCE_NAME); } diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/CloudFormationWriterTests.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/CloudFormationWriterTests.cs index 6d5588ecc..d96633663 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/CloudFormationWriterTests.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/CloudFormationWriterTests.cs @@ -1353,11 +1353,91 @@ public void CombinedAuthorizerAndProtectedFunction(CloudFormationTemplateFormat Assert.Equal("LambdaAuthorizer", templateWriter.GetToken($"{protectedEventPath}.Properties.Auth.Authorizer")); Assert.Equal("AnnotationsHttpApi", templateWriter.GetToken($"{protectedEventPath}.Properties.ApiId.Ref")); - // Verify public function does NOT reference authorizer + // Verify public function does NOT reference authorizer but DOES reference the shared API const string publicEventPath = "Resources.HealthFunction.Properties.Events.RootGet"; Assert.True(templateWriter.Exists(publicEventPath)); Assert.False(templateWriter.Exists($"{publicEventPath}.Properties.Auth")); - Assert.False(templateWriter.Exists($"{publicEventPath}.Properties.ApiId")); + Assert.Equal("AnnotationsHttpApi", templateWriter.GetToken($"{publicEventPath}.Properties.ApiId.Ref")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void CombinedRestApiAuthorizerAndProtectedFunction(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE + var mockFileManager = GetMockFileManager(string.Empty); + + // Create the authorizer function + var authorizerFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.Auth::Authorize", + "MyAuthorizerFunction", 30, 512, null, null); + + // Create a protected function + var protectedFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.Api::GetData", + "GetDataFunction", 30, 512, null, null); + var restApiAttribute = new AttributeModel() + { + Data = new RestApiAttribute(LambdaHttpMethod.Get, "/data") + { + Authorizer = "LambdaAuthorizer" + } + }; + protectedFunctionModel.Attributes = new List { restApiAttribute }; + protectedFunctionModel.Authorizer = "LambdaAuthorizer"; + + // Create an unprotected function + var publicFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.Api::Health", + "HealthFunction", 30, 512, null, null); + var publicRestApiAttribute = new AttributeModel() + { + Data = new RestApiAttribute(LambdaHttpMethod.Get, "/health") + }; + publicFunctionModel.Attributes = new List { publicRestApiAttribute }; + + var authorizer = new AuthorizerModel + { + Name = "LambdaAuthorizer", + LambdaResourceName = "MyAuthorizerFunction", + AuthorizerType = AuthorizerType.RestApi, + IdentityHeader = "Authorization", + ResultTtlInSeconds = 0, + RestApiAuthorizerType = RestApiAuthorizerType.Token + }; + + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport( + new List { authorizerFunctionModel, protectedFunctionModel, publicFunctionModel }, + new List { authorizer }); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + // Verify all three Lambda functions exist + Assert.True(templateWriter.Exists("Resources.MyAuthorizerFunction")); + Assert.True(templateWriter.Exists("Resources.GetDataFunction")); + Assert.True(templateWriter.Exists("Resources.HealthFunction")); + + // Verify AnnotationsRestApi resource with authorizer + Assert.True(templateWriter.Exists("Resources.AnnotationsRestApi")); + Assert.Equal("AWS::Serverless::Api", templateWriter.GetToken("Resources.AnnotationsRestApi.Type")); + Assert.True(templateWriter.Exists("Resources.AnnotationsRestApi.Properties.Auth.Authorizers.LambdaAuthorizer")); + Assert.Equal(new List { "MyAuthorizerFunction", "Arn" }, + templateWriter.GetToken>("Resources.AnnotationsRestApi.Properties.Auth.Authorizers.LambdaAuthorizer.FunctionArn.Fn::GetAtt")); + + // Verify protected function references authorizer and shared API + const string protectedEventPath = "Resources.GetDataFunction.Properties.Events.RootGet"; + Assert.Equal("LambdaAuthorizer", templateWriter.GetToken($"{protectedEventPath}.Properties.Auth.Authorizer")); + Assert.Equal("AnnotationsRestApi", templateWriter.GetToken($"{protectedEventPath}.Properties.RestApiId.Ref")); + + // Verify public function does NOT reference authorizer but DOES reference the shared API + const string publicEventPath = "Resources.HealthFunction.Properties.Events.RootGet"; + Assert.True(templateWriter.Exists(publicEventPath)); + Assert.False(templateWriter.Exists($"{publicEventPath}.Properties.Auth")); + Assert.Equal("AnnotationsRestApi", templateWriter.GetToken($"{publicEventPath}.Properties.RestApiId.Ref")); } [Theory] diff --git a/Libraries/test/TestServerlessApp.NET8/serverless.template b/Libraries/test/TestServerlessApp.NET8/serverless.template index 3cdbe66a9..c42ff4a47 100644 --- a/Libraries/test/TestServerlessApp.NET8/serverless.template +++ b/Libraries/test/TestServerlessApp.NET8/serverless.template @@ -2,5 +2,23 @@ "AWSTemplateFormatVersion": "2010-09-09", "Transform": "AWS::Serverless-2016-10-31", "Description": "This template is partially managed by Amazon.Lambda.Annotations (v1.9.0.0).", - "Resources": {} + "Resources": { + "TestServerlessAppNET8FunctionsToUpperGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "Runtime": "dotnet8", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestServerlessApp.NET8::TestServerlessApp.NET8.Functions_ToUpper_Generated::ToUpper" + } + } + } } \ No newline at end of file diff --git a/Libraries/test/TestServerlessApp/serverless.template b/Libraries/test/TestServerlessApp/serverless.template index 3cdbe66a9..8509a4b45 100644 --- a/Libraries/test/TestServerlessApp/serverless.template +++ b/Libraries/test/TestServerlessApp/serverless.template @@ -2,5 +2,9 @@ "AWSTemplateFormatVersion": "2010-09-09", "Transform": "AWS::Serverless-2016-10-31", "Description": "This template is partially managed by Amazon.Lambda.Annotations (v1.9.0.0).", - "Resources": {} + "Resources": { + "TestQueue": { + "Type": "AWS::SQS::Queue" + } + } } \ No newline at end of file From bc53a94c4df8d7872d630b1a0cd5839c9ea6f148 Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Mon, 9 Mar 2026 09:47:49 -0400 Subject: [PATCH 06/14] fix cleanup --- .../Writers/CloudFormationWriter.cs | 20 ++++ .../WriterTests/CloudFormationWriterTests.cs | 112 ++++++++++++++++++ 2 files changed, 132 insertions(+) diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs index a63052207..8d4d4fe15 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs @@ -505,6 +505,26 @@ private void RemoveOrphanedAuthorizers(IList currentAuthorizers { _templateWriter.RemoveToken($"Resources.{resourceName}"); } + + // Remove the entire AnnotationsHttpApi resource if it was created by us and no longer has any HTTP API authorizers + if (!currentHttpApiAuthorizerNames.Any() + && _templateWriter.Exists($"Resources.{HTTP_API_RESOURCE_NAME}") + && string.Equals( + _templateWriter.GetToken($"Resources.{HTTP_API_RESOURCE_NAME}.Metadata.Tool", string.Empty), + CREATION_TOOL, StringComparison.Ordinal)) + { + _templateWriter.RemoveToken($"Resources.{HTTP_API_RESOURCE_NAME}"); + } + + // Remove the entire AnnotationsRestApi resource if it was created by us and no longer has any REST API authorizers + if (!currentRestApiAuthorizerNames.Any() + && _templateWriter.Exists($"Resources.{REST_API_RESOURCE_NAME}") + && string.Equals( + _templateWriter.GetToken($"Resources.{REST_API_RESOURCE_NAME}.Metadata.Tool", string.Empty), + CREATION_TOOL, StringComparison.Ordinal)) + { + _templateWriter.RemoveToken($"Resources.{REST_API_RESOURCE_NAME}"); + } } /// diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/CloudFormationWriterTests.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/CloudFormationWriterTests.cs index d96633663..4b68f1fce 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/CloudFormationWriterTests.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/CloudFormationWriterTests.cs @@ -1279,6 +1279,118 @@ public void RemoveOrphanedRestApiAuthorizer(CloudFormationTemplateFormat templat Assert.False(templateWriter.Exists("Resources.AnnotationsRestApi.Properties.Auth.Authorizers.OldRestAuthorizer")); } + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void SwitchFromRestApiToHttpApiAuthorizer_RemovesOrphanedRestApiResource(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE - Start with a REST API authorizer + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Authorize", + "AuthorizerFunction", 30, 512, null, null); + var restAuthorizer = new AuthorizerModel + { + Name = "MyAuthorizer", + LambdaResourceName = "AuthorizerFunction", + AuthorizerType = AuthorizerType.RestApi, + IdentityHeader = "Authorization", + ResultTtlInSeconds = 0, + RestApiAuthorizerType = RestApiAuthorizerType.Token + }; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport( + new List { lambdaFunctionModel }, + new List { restAuthorizer }); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + // ACT - First pass: create REST API authorizer + cloudFormationWriter.ApplyReport(report); + + // ASSERT - AnnotationsRestApi exists, AnnotationsHttpApi does not + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + Assert.True(templateWriter.Exists("Resources.AnnotationsRestApi")); + Assert.False(templateWriter.Exists("Resources.AnnotationsHttpApi")); + + // ARRANGE - Switch to HTTP API authorizer + var httpAuthorizer = new AuthorizerModel + { + Name = "MyAuthorizer", + LambdaResourceName = "AuthorizerFunction", + AuthorizerType = AuthorizerType.HttpApi, + IdentityHeader = "Authorization", + ResultTtlInSeconds = 0, + EnableSimpleResponses = true, + PayloadFormatVersion = "2.0" + }; + var reportWithHttpApi = GetAnnotationReport( + new List { lambdaFunctionModel }, + new List { httpAuthorizer }); + + // ACT - Second pass: switch to HTTP API authorizer + cloudFormationWriter.ApplyReport(reportWithHttpApi); + + // ASSERT - AnnotationsHttpApi exists, AnnotationsRestApi is removed + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + Assert.True(templateWriter.Exists("Resources.AnnotationsHttpApi")); + Assert.False(templateWriter.Exists("Resources.AnnotationsRestApi")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void SwitchFromHttpApiToRestApiAuthorizer_RemovesOrphanedHttpApiResource(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE - Start with an HTTP API authorizer + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Authorize", + "AuthorizerFunction", 30, 512, null, null); + var httpAuthorizer = new AuthorizerModel + { + Name = "MyAuthorizer", + LambdaResourceName = "AuthorizerFunction", + AuthorizerType = AuthorizerType.HttpApi, + IdentityHeader = "Authorization", + ResultTtlInSeconds = 0, + EnableSimpleResponses = true, + PayloadFormatVersion = "2.0" + }; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport( + new List { lambdaFunctionModel }, + new List { httpAuthorizer }); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + // ACT - First pass: create HTTP API authorizer + cloudFormationWriter.ApplyReport(report); + + // ASSERT - AnnotationsHttpApi exists, AnnotationsRestApi does not + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + Assert.True(templateWriter.Exists("Resources.AnnotationsHttpApi")); + Assert.False(templateWriter.Exists("Resources.AnnotationsRestApi")); + + // ARRANGE - Switch to REST API authorizer + var restAuthorizer = new AuthorizerModel + { + Name = "MyAuthorizer", + LambdaResourceName = "AuthorizerFunction", + AuthorizerType = AuthorizerType.RestApi, + IdentityHeader = "Authorization", + ResultTtlInSeconds = 0, + RestApiAuthorizerType = RestApiAuthorizerType.Token + }; + var reportWithRestApi = GetAnnotationReport( + new List { lambdaFunctionModel }, + new List { restAuthorizer }); + + // ACT - Second pass: switch to REST API authorizer + cloudFormationWriter.ApplyReport(reportWithRestApi); + + // ASSERT - AnnotationsRestApi exists, AnnotationsHttpApi is removed + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + Assert.True(templateWriter.Exists("Resources.AnnotationsRestApi")); + Assert.False(templateWriter.Exists("Resources.AnnotationsHttpApi")); + } + [Theory] [InlineData(CloudFormationTemplateFormat.Json)] [InlineData(CloudFormationTemplateFormat.Yaml)] From f57fecabb931cac7db22f7cc4d472883e00f5161 Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Mon, 9 Mar 2026 10:35:38 -0400 Subject: [PATCH 07/14] payload updates --- .../Diagnostics/AnalyzerReleases.Unshipped.md | 4 +- .../Diagnostics/DiagnosticDescriptors.cs | 16 ++++---- .../Generator.cs | 39 ++++++++++++++----- .../HttpApiAuthorizerAttributeBuilder.cs | 8 ++-- .../Models/AuthorizerModel.cs | 4 +- .../Writers/CloudFormationWriter.cs | 3 +- .../AuthorizerPayloadFormatVersion.cs | 19 +++++++++ .../APIGateway/HttpApiAuthorizerAttribute.cs | 6 +-- .../WriterTests/CloudFormationWriterTests.cs | 18 ++++----- .../AuthorizerFunction.cs | 2 +- 10 files changed, 80 insertions(+), 39 deletions(-) create mode 100644 Libraries/src/Amazon.Lambda.Annotations/APIGateway/AuthorizerPayloadFormatVersion.cs diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/AnalyzerReleases.Unshipped.md b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/AnalyzerReleases.Unshipped.md index 423deaf1a..dfc1b52e5 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/AnalyzerReleases.Unshipped.md +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/AnalyzerReleases.Unshipped.md @@ -11,5 +11,5 @@ AWSLambda0122 | AWSLambdaCSharpGenerator | Error | REST API Authorizer Not Found AWSLambda0123 | AWSLambdaCSharpGenerator | Error | Authorizer Type Mismatch AWSLambda0124 | AWSLambdaCSharpGenerator | Error | Authorizer Type Mismatch AWSLambda0125 | AWSLambdaCSharpGenerator | Error | Duplicate Authorizer Name -AWSLambda0126 | AWSLambdaCSharpGenerator | Error | Invalid Payload Format Version -AWSLambda0127 | AWSLambdaCSharpGenerator | Error | Invalid Result TTL \ No newline at end of file +AWSLambda0127 | AWSLambdaCSharpGenerator | Error | Invalid Result TTL +AWSLambda0128 | AWSLambdaCSharpGenerator | Warning | Authorizer Payload Version Mismatch diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs index d6edcaf1e..d7d381ee5 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs @@ -203,14 +203,6 @@ public static class DiagnosticDescriptors DiagnosticSeverity.Error, isEnabledByDefault: true); - public static readonly DiagnosticDescriptor InvalidAuthorizerPayloadFormatVersion = new DiagnosticDescriptor( - id: "AWSLambda0126", - title: "Invalid Payload Format Version", - messageFormat: "Invalid PayloadFormatVersion '{0}'. Must be \"1.0\" or \"2.0\".", - category: "AWSLambdaCSharpGenerator", - DiagnosticSeverity.Error, - isEnabledByDefault: true); - public static readonly DiagnosticDescriptor InvalidAuthorizerResultTtl = new DiagnosticDescriptor( id: "AWSLambda0127", title: "Invalid Result TTL", @@ -218,5 +210,13 @@ public static class DiagnosticDescriptors category: "AWSLambdaCSharpGenerator", DiagnosticSeverity.Error, isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor AuthorizerPayloadVersionMismatch = new DiagnosticDescriptor( + id: "AWSLambda0128", + title: "Authorizer Payload Version Mismatch", + messageFormat: "The authorizer '{0}' uses AuthorizerPayloadFormatVersion {1} but the endpoint uses HttpApiVersion {2}. This may cause unexpected behavior.", + category: "AWSLambdaCSharpGenerator", + DiagnosticSeverity.Warning, + isEnabledByDefault: true); } } diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Generator.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Generator.cs index 0afb427c0..71d5cdca3 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Generator.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Generator.cs @@ -368,16 +368,6 @@ private static bool ValidateAuthorizerModel(AuthorizerModel model, string attrib isValid = false; } - // Validate PayloadFormatVersion for HTTP API authorizers - if (model.AuthorizerType == AuthorizerType.HttpApi) - { - if (model.PayloadFormatVersion != "1.0" && model.PayloadFormatVersion != "2.0") - { - diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.InvalidAuthorizerPayloadFormatVersion, methodLocation, model.PayloadFormatVersion)); - isValid = false; - } - } - // Validate ResultTtlInSeconds if (model.ResultTtlInSeconds < 0 || model.ResultTtlInSeconds > 3600) { @@ -459,6 +449,35 @@ private static bool ValidateAuthorizerReferences(AnnotationReport annotationRepo } isValid = false; } + else + { + // Check for payload format version mismatch between authorizer and endpoint + var matchedAuthorizer = httpApiAuthorizers[authorizerName]; + var httpApiAttr = function.Attributes + .OfType>() + .Select(a => a.Data) + .FirstOrDefault(); + + if (httpApiAttr != null) + { + var endpointVersion = httpApiAttr.Version; + var authorizerVersion = matchedAuthorizer.AuthorizerPayloadFormatVersion; + + // Compare: HttpApiVersion.V1 corresponds to AuthorizerPayloadFormatVersion.V1, etc. + if ((endpointVersion == HttpApiVersion.V1 && authorizerVersion != AuthorizerPayloadFormatVersion.V1) || + (endpointVersion == HttpApiVersion.V2 && authorizerVersion != AuthorizerPayloadFormatVersion.V2)) + { + var authorizerVersionStr = authorizerVersion == AuthorizerPayloadFormatVersion.V1 ? "1.0" : "2.0"; + var endpointVersionStr = endpointVersion == HttpApiVersion.V1 ? "V1" : "V2"; + diagnosticReporter.Report(Diagnostic.Create( + DiagnosticDescriptors.AuthorizerPayloadVersionMismatch, + Location.None, + authorizerName, + authorizerVersionStr, + endpointVersionStr)); + } + } + } } if (usesRestApi) diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/HttpApiAuthorizerAttributeBuilder.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/HttpApiAuthorizerAttributeBuilder.cs index 8df840490..341e6abf8 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/HttpApiAuthorizerAttributeBuilder.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/HttpApiAuthorizerAttributeBuilder.cs @@ -31,8 +31,10 @@ public static HttpApiAuthorizerAttribute Build(AttributeData att) case nameof(HttpApiAuthorizerAttribute.EnableSimpleResponses): attribute.EnableSimpleResponses = namedArg.Value.Value is bool val ? val : true; break; - case nameof(HttpApiAuthorizerAttribute.PayloadFormatVersion): - attribute.PayloadFormatVersion = namedArg.Value.Value as string ?? "2.0"; + case nameof(HttpApiAuthorizerAttribute.AuthorizerPayloadFormatVersion): + attribute.AuthorizerPayloadFormatVersion = namedArg.Value.Value is int enumVal + ? (AuthorizerPayloadFormatVersion)enumVal + : AuthorizerPayloadFormatVersion.V2; break; case nameof(HttpApiAuthorizerAttribute.ResultTtlInSeconds): attribute.ResultTtlInSeconds = namedArg.Value.Value is int ttl ? ttl : 0; @@ -71,7 +73,7 @@ public static AuthorizerModel BuildModel(HttpApiAuthorizerAttribute attribute, s IdentityHeader = attribute.IdentityHeader, ResultTtlInSeconds = attribute.ResultTtlInSeconds, EnableSimpleResponses = attribute.EnableSimpleResponses, - PayloadFormatVersion = attribute.PayloadFormatVersion + AuthorizerPayloadFormatVersion = attribute.AuthorizerPayloadFormatVersion }; } } diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/AuthorizerModel.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/AuthorizerModel.cs index 645279b21..ea6ee3833 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/AuthorizerModel.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/AuthorizerModel.cs @@ -58,10 +58,10 @@ public class AuthorizerModel public bool EnableSimpleResponses { get; set; } /// - /// Authorizer payload format version. Valid values: "1.0" or "2.0". + /// Authorizer payload format version. /// Only applicable for HTTP API authorizers. /// - public string PayloadFormatVersion { get; set; } + public AuthorizerPayloadFormatVersion AuthorizerPayloadFormatVersion { get; set; } // REST API specific properties diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs index 8d4d4fe15..4c41bdbc5 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs @@ -337,7 +337,8 @@ private void ProcessHttpApiAuthorizers(IList authorizers) _templateWriter.SetToken($"{authorizerPath}.FunctionArn.{GET_ATTRIBUTE}", new List { authorizer.LambdaResourceName, "Arn" }, TokenType.List); // AuthorizerPayloadFormatVersion - _templateWriter.SetToken($"{authorizerPath}.AuthorizerPayloadFormatVersion", authorizer.PayloadFormatVersion); + var payloadFormatVersionString = authorizer.AuthorizerPayloadFormatVersion == AuthorizerPayloadFormatVersion.V1 ? "1.0" : "2.0"; + _templateWriter.SetToken($"{authorizerPath}.AuthorizerPayloadFormatVersion", payloadFormatVersionString); // EnableSimpleResponses _templateWriter.SetToken($"{authorizerPath}.EnableSimpleResponses", authorizer.EnableSimpleResponses); diff --git a/Libraries/src/Amazon.Lambda.Annotations/APIGateway/AuthorizerPayloadFormatVersion.cs b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/AuthorizerPayloadFormatVersion.cs new file mode 100644 index 000000000..8ca1360a9 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/AuthorizerPayloadFormatVersion.cs @@ -0,0 +1,19 @@ +namespace Amazon.Lambda.Annotations.APIGateway +{ + /// + /// The payload format version for an API Gateway HTTP API Lambda authorizer. + /// This maps to the AuthorizerPayloadFormatVersion property in the SAM template. + /// + public enum AuthorizerPayloadFormatVersion + { + /// + /// Payload format version 1.0 + /// + V1, + + /// + /// Payload format version 2.0 + /// + V2 + } +} \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpApiAuthorizerAttribute.cs b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpApiAuthorizerAttribute.cs index e69e59b9a..942dc7b9b 100644 --- a/Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpApiAuthorizerAttribute.cs +++ b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpApiAuthorizerAttribute.cs @@ -55,10 +55,10 @@ public class HttpApiAuthorizerAttribute : Attribute public bool EnableSimpleResponses { get; set; } = true; /// - /// Authorizer payload format version. Valid values: "1.0" or "2.0". - /// Defaults to "2.0". + /// Authorizer payload format version. Defaults to . + /// Maps to the AuthorizerPayloadFormatVersion property in the SAM template. /// - public string PayloadFormatVersion { get; set; } = "2.0"; + public AuthorizerPayloadFormatVersion AuthorizerPayloadFormatVersion { get; set; } = AuthorizerPayloadFormatVersion.V2; /// /// TTL in seconds for caching authorizer results. 0 = no caching. Max = 3600. diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/CloudFormationWriterTests.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/CloudFormationWriterTests.cs index 4b68f1fce..d2a728d5e 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/CloudFormationWriterTests.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/CloudFormationWriterTests.cs @@ -889,7 +889,7 @@ public void HttpApiAuthorizerProcessing(CloudFormationTemplateFormat templateFor IdentityHeader = "Authorization", ResultTtlInSeconds = 0, EnableSimpleResponses = true, - PayloadFormatVersion = "2.0" + AuthorizerPayloadFormatVersion = AuthorizerPayloadFormatVersion.V2 }; var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); var report = GetAnnotationReport( @@ -1033,7 +1033,7 @@ public void HttpApiWithAuthorizerReference(CloudFormationTemplateFormat template IdentityHeader = "Authorization", ResultTtlInSeconds = 0, EnableSimpleResponses = true, - PayloadFormatVersion = "2.0" + AuthorizerPayloadFormatVersion = AuthorizerPayloadFormatVersion.V2 }; var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); @@ -1132,7 +1132,7 @@ public void HttpApiAuthorizerWithCustomTtl(CloudFormationTemplateFormat template IdentityHeader = "Authorization", ResultTtlInSeconds = 300, EnableSimpleResponses = true, - PayloadFormatVersion = "2.0" + AuthorizerPayloadFormatVersion = AuthorizerPayloadFormatVersion.V2 }; var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); var report = GetAnnotationReport( @@ -1173,7 +1173,7 @@ public void HttpApiAuthorizerWithNoTtl_DoesNotSetFunctionInvokeRole(CloudFormati IdentityHeader = "Authorization", ResultTtlInSeconds = 0, EnableSimpleResponses = true, - PayloadFormatVersion = "2.0" + AuthorizerPayloadFormatVersion = AuthorizerPayloadFormatVersion.V2 }; var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); var report = GetAnnotationReport( @@ -1211,7 +1211,7 @@ public void RemoveOrphanedHttpApiAuthorizer(CloudFormationTemplateFormat templat IdentityHeader = "Authorization", ResultTtlInSeconds = 0, EnableSimpleResponses = true, - PayloadFormatVersion = "2.0" + AuthorizerPayloadFormatVersion = AuthorizerPayloadFormatVersion.V2 }; var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); var report = GetAnnotationReport( @@ -1320,7 +1320,7 @@ public void SwitchFromRestApiToHttpApiAuthorizer_RemovesOrphanedRestApiResource( IdentityHeader = "Authorization", ResultTtlInSeconds = 0, EnableSimpleResponses = true, - PayloadFormatVersion = "2.0" + AuthorizerPayloadFormatVersion = AuthorizerPayloadFormatVersion.V2 }; var reportWithHttpApi = GetAnnotationReport( new List { lambdaFunctionModel }, @@ -1352,7 +1352,7 @@ public void SwitchFromHttpApiToRestApiAuthorizer_RemovesOrphanedHttpApiResource( IdentityHeader = "Authorization", ResultTtlInSeconds = 0, EnableSimpleResponses = true, - PayloadFormatVersion = "2.0" + AuthorizerPayloadFormatVersion = AuthorizerPayloadFormatVersion.V2 }; var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); var report = GetAnnotationReport( @@ -1433,7 +1433,7 @@ public void CombinedAuthorizerAndProtectedFunction(CloudFormationTemplateFormat IdentityHeader = "Authorization", ResultTtlInSeconds = 0, EnableSimpleResponses = true, - PayloadFormatVersion = "2.0" + AuthorizerPayloadFormatVersion = AuthorizerPayloadFormatVersion.V2 }; var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); @@ -1569,7 +1569,7 @@ public void HttpApiAuthorizerWithCustomSettings(CloudFormationTemplateFormat tem IdentityHeader = "X-Api-Key", ResultTtlInSeconds = 0, EnableSimpleResponses = false, - PayloadFormatVersion = "1.0" + AuthorizerPayloadFormatVersion = AuthorizerPayloadFormatVersion.V1 }; var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); var report = GetAnnotationReport( diff --git a/Libraries/test/TestCustomAuthorizerApp/AuthorizerFunction.cs b/Libraries/test/TestCustomAuthorizerApp/AuthorizerFunction.cs index cfbd2adcd..0b8b60260 100644 --- a/Libraries/test/TestCustomAuthorizerApp/AuthorizerFunction.cs +++ b/Libraries/test/TestCustomAuthorizerApp/AuthorizerFunction.cs @@ -43,7 +43,7 @@ public class AuthorizerFunction Name = "HttpApiLambdaAuthorizer", IdentityHeader = "authorization", EnableSimpleResponses = true, - PayloadFormatVersion = "2.0")] + AuthorizerPayloadFormatVersion = AuthorizerPayloadFormatVersion.V2)] public APIGatewayCustomAuthorizerV2SimpleResponse HttpApiAuthorize( APIGatewayCustomAuthorizerV2Request request, ILambdaContext context) From 7c977a45dac46650fa771358cdb6653242c4a60b Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Mon, 9 Mar 2026 10:59:46 -0400 Subject: [PATCH 08/14] update IT tests --- ...Function_HttpApiAuthorizeV1_Generated.g.cs | 58 + .../customAuthorizerApp.template | 44 +- .../SourceGeneratorTests.cs | 8 + .../HealthCheckTests.cs | 2 +- .../IntegrationTestContextFixture.cs | 19 +- .../AuthorizerFunction.cs | 83 ++ .../ProtectedFunction.cs | 2 +- .../aws-lambda-tools-defaults.json | 8 +- .../serverless.template | 44 +- .../src/Function/serverless.template | 65 +- .../serverless.template | 657 +++++++++- .../TestServerlessApp/serverless.template | 1147 +++++++++++++++++ 12 files changed, 2097 insertions(+), 40 deletions(-) create mode 100644 Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthorizerFunction_HttpApiAuthorizeV1_Generated.g.cs diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthorizerFunction_HttpApiAuthorizeV1_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthorizerFunction_HttpApiAuthorizeV1_Generated.g.cs new file mode 100644 index 000000000..2611b8c62 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthorizerFunction_HttpApiAuthorizeV1_Generated.g.cs @@ -0,0 +1,58 @@ +// + +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using Amazon.Lambda.Core; + +namespace TestCustomAuthorizerApp +{ + public class AuthorizerFunction_HttpApiAuthorizeV1_Generated + { + private readonly AuthorizerFunction authorizerFunction; + private readonly Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer serializer; + + /// + /// Default constructor. This constructor is used by Lambda to construct the instance. When invoked in a Lambda environment + /// the AWS credentials will come from the IAM role associated with the function and the AWS region will be set to the + /// region the Lambda function is executed in. + /// + public AuthorizerFunction_HttpApiAuthorizeV1_Generated() + { + SetExecutionEnvironment(); + authorizerFunction = new AuthorizerFunction(); + serializer = new Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer(); + } + + /// + /// The generated Lambda function handler for + /// + /// The request object that will be processed by the Lambda function handler. + /// The ILambdaContext that provides methods for logging and describing the Lambda environment. + /// Result of the Lambda function execution + public Amazon.Lambda.APIGatewayEvents.APIGatewayCustomAuthorizerResponse HttpApiAuthorizeV1(Amazon.Lambda.APIGatewayEvents.APIGatewayCustomAuthorizerRequest __request__, Amazon.Lambda.Core.ILambdaContext __context__) + { + return authorizerFunction.HttpApiAuthorizeV1(__request__, __context__); + } + + private static void SetExecutionEnvironment() + { + const string envName = "AWS_EXECUTION_ENV"; + + var envValue = new StringBuilder(); + + // If there is an existing execution environment variable add the annotations package as a suffix. + if(!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(envName))) + { + envValue.Append($"{Environment.GetEnvironmentVariable(envName)}_"); + } + + envValue.Append("lib/amazon-lambda-annotations#{ANNOTATIONS_ASSEMBLY_VERSION}"); + + Environment.SetEnvironmentVariable(envName, envValue.ToString()); + } + } +} \ No newline at end of file diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/customAuthorizerApp.template b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/customAuthorizerApp.template index 08786963a..4270c197f 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/customAuthorizerApp.template +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/customAuthorizerApp.template @@ -27,6 +27,23 @@ }, "EnableFunctionDefaultPermissions": true, "AuthorizerResultTtlInSeconds": 0 + }, + "HttpApiLambdaAuthorizerV1": { + "FunctionArn": { + "Fn::GetAtt": [ + "CustomAuthorizerV1", + "Arn" + ] + }, + "AuthorizerPayloadFormatVersion": "1.0", + "EnableSimpleResponses": false, + "Identity": { + "Headers": [ + "authorization" + ] + }, + "EnableFunctionDefaultPermissions": true, + "AuthorizerResultTtlInSeconds": 0 } } } @@ -74,6 +91,23 @@ "Handler": "TestProject::TestCustomAuthorizerApp.AuthorizerFunction_HttpApiAuthorize_Generated::HttpApiAuthorize" } }, + "CustomAuthorizerV1": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestProject::TestCustomAuthorizerApp.AuthorizerFunction_HttpApiAuthorizeV1_Generated::HttpApiAuthorizeV1" + } + }, "RestApiAuthorizer": { "Type": "AWS::Serverless::Function", "Metadata": { @@ -187,7 +221,8 @@ "SyncedEventProperties": { "RootGet": [ "Path", - "Method" + "Method", + "ApiId.Ref" ] } }, @@ -206,7 +241,10 @@ "Type": "HttpApi", "Properties": { "Path": "/api/health", - "Method": "GET" + "Method": "GET", + "ApiId": { + "Ref": "AnnotationsHttpApi" + } } } } @@ -290,7 +328,7 @@ "Method": "GET", "PayloadFormatVersion": "1.0", "Auth": { - "Authorizer": "HttpApiLambdaAuthorizer" + "Authorizer": "HttpApiLambdaAuthorizerV1" }, "ApiId": { "Ref": "AnnotationsHttpApi" diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs index 569c59f45..2e0be9536 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs @@ -1654,6 +1654,7 @@ public async Task CustomAuthorizerAppAuthorizerDefinitionsTest() { var expectedTemplateContent = await ReadSnapshotContent(Path.Combine("Snapshots", "ServerlessTemplates", "customAuthorizerApp.template")); var expectedHttpApiAuthorizeGenerated = await ReadSnapshotContent(Path.Combine("Snapshots", "AuthorizerFunction_HttpApiAuthorize_Generated.g.cs")); + var expectedHttpApiAuthorizeV1Generated = await ReadSnapshotContent(Path.Combine("Snapshots", "AuthorizerFunction_HttpApiAuthorizeV1_Generated.g.cs")); var expectedRestApiAuthorizeGenerated = await ReadSnapshotContent(Path.Combine("Snapshots", "AuthorizerFunction_RestApiAuthorize_Generated.g.cs")); var expectedGetProtectedDataGenerated = await ReadSnapshotContent(Path.Combine("Snapshots", "ProtectedFunction_GetProtectedData_Generated.g.cs")); var expectedGetUserInfoGenerated = await ReadSnapshotContent(Path.Combine("Snapshots", "ProtectedFunction_GetUserInfo_Generated.g.cs")); @@ -1676,6 +1677,7 @@ public async Task CustomAuthorizerAppAuthorizerDefinitionsTest() (Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "HttpApiAttribute.cs"), File.ReadAllText(Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "HttpApiAttribute.cs"))), (Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "RestApiAttribute.cs"), File.ReadAllText(Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "RestApiAttribute.cs"))), (Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "HttpApiAuthorizerAttribute.cs"), File.ReadAllText(Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "HttpApiAuthorizerAttribute.cs"))), + (Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "AuthorizerPayloadFormatVersion.cs"), File.ReadAllText(Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "AuthorizerPayloadFormatVersion.cs"))), (Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "RestApiAuthorizerAttribute.cs"), File.ReadAllText(Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "RestApiAuthorizerAttribute.cs"))), (Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "FromCustomAuthorizerAttribute.cs"), File.ReadAllText(Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "FromCustomAuthorizerAttribute.cs"))), }, @@ -1686,6 +1688,11 @@ public async Task CustomAuthorizerAppAuthorizerDefinitionsTest() "AuthorizerFunction_HttpApiAuthorize_Generated.g.cs", SourceText.From(expectedHttpApiAuthorizeGenerated, Encoding.UTF8, SourceHashAlgorithm.Sha256) ), + ( + typeof(SourceGenerator.Generator), + "AuthorizerFunction_HttpApiAuthorizeV1_Generated.g.cs", + SourceText.From(expectedHttpApiAuthorizeV1Generated, Encoding.UTF8, SourceHashAlgorithm.Sha256) + ), ( typeof(SourceGenerator.Generator), "AuthorizerFunction_RestApiAuthorize_Generated.g.cs", @@ -1729,6 +1736,7 @@ public async Task CustomAuthorizerAppAuthorizerDefinitionsTest() }, ExpectedDiagnostics = { + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("AuthorizerFunction_HttpApiAuthorizeV1_Generated.g.cs", expectedHttpApiAuthorizeV1Generated), new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("AuthorizerFunction_HttpApiAuthorize_Generated.g.cs", expectedHttpApiAuthorizeGenerated), new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("AuthorizerFunction_RestApiAuthorize_Generated.g.cs", expectedRestApiAuthorizeGenerated), new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("ProtectedFunction_GetProtectedData_Generated.g.cs", expectedGetProtectedDataGenerated), diff --git a/Libraries/test/TestCustomAuthorizerApp.IntegrationTests/HealthCheckTests.cs b/Libraries/test/TestCustomAuthorizerApp.IntegrationTests/HealthCheckTests.cs index 4bd057356..2360d7305 100644 --- a/Libraries/test/TestCustomAuthorizerApp.IntegrationTests/HealthCheckTests.cs +++ b/Libraries/test/TestCustomAuthorizerApp.IntegrationTests/HealthCheckTests.cs @@ -20,7 +20,7 @@ public HealthCheckTests(IntegrationTestContextFixture fixture) public async Task HealthCheck_ReturnsOk_WithoutAuthorization() { // Arrange & Act - var response = await _fixture.HttpClient.GetAsync($"{_fixture.ImplicitHttpApiUrl}/api/health"); + var response = await _fixture.HttpClient.GetAsync($"{_fixture.HttpApiUrl}/api/health"); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); diff --git a/Libraries/test/TestCustomAuthorizerApp.IntegrationTests/IntegrationTestContextFixture.cs b/Libraries/test/TestCustomAuthorizerApp.IntegrationTests/IntegrationTestContextFixture.cs index 6062a965c..da9169201 100644 --- a/Libraries/test/TestCustomAuthorizerApp.IntegrationTests/IntegrationTestContextFixture.cs +++ b/Libraries/test/TestCustomAuthorizerApp.IntegrationTests/IntegrationTestContextFixture.cs @@ -23,16 +23,10 @@ public class IntegrationTestContextFixture : IAsyncLifetime public readonly HttpClient HttpClient; /// - /// HTTP API base URL for endpoints explicitly attached to AnnotationsHttpApi (no trailing slash) + /// HTTP API base URL for endpoints attached to AnnotationsHttpApi (no trailing slash) /// public string HttpApiUrl = string.Empty; - /// - /// HTTP API base URL for endpoints using the implicit SAM-generated ServerlessHttpApi (no trailing slash). - /// Functions without an explicit ApiId (e.g. HealthCheck) are placed on this API. - /// - public string ImplicitHttpApiUrl = string.Empty; - /// /// REST API base URL (no trailing slash) /// @@ -76,14 +70,11 @@ public async Task InitializeAsync() var region = "us-west-2"; Console.WriteLine($"[IntegrationTest] Querying stack resources for '{_stackName}'..."); var httpApiId = await _cloudFormationHelper.GetResourcePhysicalIdAsync(_stackName, "AnnotationsHttpApi"); - var implicitHttpApiId = await _cloudFormationHelper.GetResourcePhysicalIdAsync(_stackName, "ServerlessHttpApi"); var restApiId = await _cloudFormationHelper.GetResourcePhysicalIdAsync(_stackName, "AnnotationsRestApi"); - Console.WriteLine($"[IntegrationTest] AnnotationsHttpApi: {httpApiId}, ServerlessHttpApi: {implicitHttpApiId}, AnnotationsRestApi: {restApiId}"); + Console.WriteLine($"[IntegrationTest] AnnotationsHttpApi: {httpApiId}, AnnotationsRestApi: {restApiId}"); Assert.False(string.IsNullOrEmpty(httpApiId), $"CloudFormation resource 'AnnotationsHttpApi' was not found in stack '{_stackName}'."); - Assert.False(string.IsNullOrEmpty(implicitHttpApiId), $"CloudFormation resource 'ServerlessHttpApi' was not found in stack '{_stackName}'."); Assert.False(string.IsNullOrEmpty(restApiId), $"CloudFormation resource 'AnnotationsRestApi' was not found in stack '{_stackName}'."); HttpApiUrl = $"https://{httpApiId}.execute-api.{region}.amazonaws.com"; - ImplicitHttpApiUrl = $"https://{implicitHttpApiId}.execute-api.{region}.amazonaws.com"; RestApiUrl = $"https://{restApiId}.execute-api.{region}.amazonaws.com/Prod"; LambdaFunctions = await LambdaHelper.FilterByCloudFormationStackAsync(_stackName); @@ -91,9 +82,9 @@ public async Task InitializeAsync() Assert.True(await _s3Helper.BucketExistsAsync(_bucketName), $"S3 bucket {_bucketName} should exist"); - // There are 9 Lambda functions in TestCustomAuthorizerApp: - // CustomAuthorizer, RestApiAuthorizer, ProtectedEndpoint, GetUserInfo, HealthCheck, RestUserInfo, HttpApiV1UserInfo, IHttpResultUserInfo, NonStringUserInfo - Assert.Equal(9, LambdaFunctions.Count); + // There are 10 Lambda functions in TestCustomAuthorizerApp: + // CustomAuthorizer, CustomAuthorizerV1, RestApiAuthorizer, ProtectedEndpoint, GetUserInfo, HealthCheck, RestUserInfo, HttpApiV1UserInfo, IHttpResultUserInfo, NonStringUserInfo + Assert.Equal(10, LambdaFunctions.Count); await LambdaHelper.WaitTillNotPending(LambdaFunctions.Where(x => x.Name != null).Select(x => x.Name!).ToList()); diff --git a/Libraries/test/TestCustomAuthorizerApp/AuthorizerFunction.cs b/Libraries/test/TestCustomAuthorizerApp/AuthorizerFunction.cs index 0b8b60260..3442d64cd 100644 --- a/Libraries/test/TestCustomAuthorizerApp/AuthorizerFunction.cs +++ b/Libraries/test/TestCustomAuthorizerApp/AuthorizerFunction.cs @@ -116,6 +116,89 @@ public APIGatewayCustomAuthorizerV2SimpleResponse HttpApiAuthorize( }; } + /// + /// HTTP API Lambda Authorizer (Payload format 1.0) + /// Used by HTTP API V1 endpoints that need payload format 1.0 + /// + [LambdaFunction(ResourceName = "CustomAuthorizerV1")] + [HttpApiAuthorizer( + Name = "HttpApiLambdaAuthorizerV1", + IdentityHeader = "authorization", + EnableSimpleResponses = false, + AuthorizerPayloadFormatVersion = AuthorizerPayloadFormatVersion.V1)] + public APIGatewayCustomAuthorizerResponse HttpApiAuthorizeV1( + APIGatewayCustomAuthorizerRequest request, + ILambdaContext context) + { + context.Logger.LogLine($"V1 Authorizer invoked"); + context.Logger.LogLine($"Authorization token: {request.AuthorizationToken}"); + + var authToken = request.AuthorizationToken ?? ""; + + // Deny if not a valid or partial-context token + if (!ValidTokens.Contains(authToken) && !PartialContextTokens.Contains(authToken)) + { + context.Logger.LogLine("V1 Authorizer: Denying request"); + return GenerateDenyPolicy("anonymous", request.MethodArn); + } + + // Check for partial-context tokens - authorize but return incomplete context + if (PartialContextTokens.Contains(authToken)) + { + context.Logger.LogLine("V1 Authorizer: Authorizing with partial context (missing expected keys)"); + + return new APIGatewayCustomAuthorizerResponse + { + PrincipalID = "partial-user", + PolicyDocument = new APIGatewayCustomAuthorizerPolicy + { + Version = "2012-10-17", + Statement = new List + { + new APIGatewayCustomAuthorizerPolicy.IAMPolicyStatement + { + Action = new HashSet { "execute-api:Invoke" }, + Effect = "Allow", + Resource = new HashSet { request.MethodArn } + } + } + }, + Context = new APIGatewayCustomAuthorizerContextOutput + { + // Intentionally return only unexpected keys, omitting userId, tenantId, email + ["unexpectedKey"] = "some-value" + } + }; + } + + context.Logger.LogLine("V1 Authorizer: Authorizing request with valid token"); + + return new APIGatewayCustomAuthorizerResponse + { + PrincipalID = "user-12345", + PolicyDocument = new APIGatewayCustomAuthorizerPolicy + { + Version = "2012-10-17", + Statement = new List + { + new APIGatewayCustomAuthorizerPolicy.IAMPolicyStatement + { + Action = new HashSet { "execute-api:Invoke" }, + Effect = "Allow", + Resource = new HashSet { request.MethodArn } + } + } + }, + Context = new APIGatewayCustomAuthorizerContextOutput + { + ["userId"] = "user-12345", + ["tenantId"] = "42", + ["userRole"] = "admin", + ["email"] = "test@example.com" + } + }; + } + /// /// REST API Lambda Authorizer (Token-based authorizer) /// Returns an IAM policy document along with custom context values diff --git a/Libraries/test/TestCustomAuthorizerApp/ProtectedFunction.cs b/Libraries/test/TestCustomAuthorizerApp/ProtectedFunction.cs index 950b86064..1f4b6d04f 100644 --- a/Libraries/test/TestCustomAuthorizerApp/ProtectedFunction.cs +++ b/Libraries/test/TestCustomAuthorizerApp/ProtectedFunction.cs @@ -149,7 +149,7 @@ public object GetRestUserInfo( /// where RequestContext.Authorizer is a dictionary, not RequestContext.Authorizer.Lambda. /// [LambdaFunction(ResourceName = "HttpApiV1UserInfo")] - [HttpApi(LambdaHttpMethod.Get, "/api/http-v1-user-info", Version = HttpApiVersion.V1, Authorizer = "HttpApiLambdaAuthorizer")] + [HttpApi(LambdaHttpMethod.Get, "/api/http-v1-user-info", Version = HttpApiVersion.V1, Authorizer = "HttpApiLambdaAuthorizerV1")] public object GetHttpApiV1UserInfo( [FromCustomAuthorizer(Name = "userId")] string userId, [FromCustomAuthorizer(Name = "email")] string email, diff --git a/Libraries/test/TestCustomAuthorizerApp/aws-lambda-tools-defaults.json b/Libraries/test/TestCustomAuthorizerApp/aws-lambda-tools-defaults.json index 53198ee82..704f3e445 100644 --- a/Libraries/test/TestCustomAuthorizerApp/aws-lambda-tools-defaults.json +++ b/Libraries/test/TestCustomAuthorizerApp/aws-lambda-tools-defaults.json @@ -8,8 +8,8 @@ "configuration": "Release", "template": "serverless.template", "template-parameters": "", - "s3-bucket": "test-custom-authorizer-app", +"s3-bucket" : "test-custom-authorizer-112e1ec1", "s3-prefix": "TestCustomAuthorizerApp/", - "stack-name": "test-custom-authorizer", - "function-architecture": "x86_64" -} \ No newline at end of file +"stack-name" : "test-custom-authorizer-112e1ec1", +"function-architecture" : "x86_64" +} diff --git a/Libraries/test/TestCustomAuthorizerApp/serverless.template b/Libraries/test/TestCustomAuthorizerApp/serverless.template index 91e9f85a1..e392a4a43 100644 --- a/Libraries/test/TestCustomAuthorizerApp/serverless.template +++ b/Libraries/test/TestCustomAuthorizerApp/serverless.template @@ -27,6 +27,23 @@ }, "EnableFunctionDefaultPermissions": true, "AuthorizerResultTtlInSeconds": 0 + }, + "HttpApiLambdaAuthorizerV1": { + "FunctionArn": { + "Fn::GetAtt": [ + "CustomAuthorizerV1", + "Arn" + ] + }, + "AuthorizerPayloadFormatVersion": "1.0", + "EnableSimpleResponses": false, + "Identity": { + "Headers": [ + "authorization" + ] + }, + "EnableFunctionDefaultPermissions": true, + "AuthorizerResultTtlInSeconds": 0 } } } @@ -187,7 +204,8 @@ "SyncedEventProperties": { "RootGet": [ "Path", - "Method" + "Method", + "ApiId.Ref" ] } }, @@ -206,7 +224,10 @@ "Type": "HttpApi", "Properties": { "Path": "/api/health", - "Method": "GET" + "Method": "GET", + "ApiId": { + "Ref": "AnnotationsHttpApi" + } } } } @@ -290,7 +311,7 @@ "Method": "GET", "PayloadFormatVersion": "1.0", "Auth": { - "Authorizer": "HttpApiLambdaAuthorizer" + "Authorizer": "HttpApiLambdaAuthorizerV1" }, "ApiId": { "Ref": "AnnotationsHttpApi" @@ -385,6 +406,23 @@ } } } + }, + "CustomAuthorizerV1": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.AuthorizerFunction_HttpApiAuthorizeV1_Generated::HttpApiAuthorizeV1" + } } } } \ No newline at end of file diff --git a/Libraries/test/TestCustomAuthorizerApp/src/Function/serverless.template b/Libraries/test/TestCustomAuthorizerApp/src/Function/serverless.template index 9e188c148..efd73d7fd 100644 --- a/Libraries/test/TestCustomAuthorizerApp/src/Function/serverless.template +++ b/Libraries/test/TestCustomAuthorizerApp/src/Function/serverless.template @@ -25,7 +25,25 @@ "authorization" ] }, - "EnableFunctionDefaultPermissions": true + "EnableFunctionDefaultPermissions": true, + "AuthorizerResultTtlInSeconds": 0 + }, + "HttpApiLambdaAuthorizerV1": { + "FunctionArn": { + "Fn::GetAtt": [ + "CustomAuthorizerV1", + "Arn" + ] + }, + "AuthorizerPayloadFormatVersion": "1.0", + "EnableSimpleResponses": false, + "Identity": { + "Headers": [ + "authorization" + ] + }, + "EnableFunctionDefaultPermissions": true, + "AuthorizerResultTtlInSeconds": 0 } } } @@ -73,7 +91,7 @@ } }, "Properties": { - "Runtime": "dotnet8", + "Runtime": "dotnet6", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -116,7 +134,7 @@ } }, "Properties": { - "Runtime": "dotnet8", + "Runtime": "dotnet6", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -152,12 +170,13 @@ "SyncedEventProperties": { "RootGet": [ "Path", - "Method" + "Method", + "ApiId.Ref" ] } }, "Properties": { - "Runtime": "dotnet8", + "Runtime": "dotnet6", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -171,7 +190,10 @@ "Type": "HttpApi", "Properties": { "Path": "/api/health", - "Method": "GET" + "Method": "GET", + "ApiId": { + "Ref": "AnnotationsHttpApi" + } } } } @@ -194,7 +216,7 @@ } }, "Properties": { - "Runtime": "dotnet8", + "Runtime": "dotnet6", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -238,7 +260,7 @@ } }, "Properties": { - "Runtime": "dotnet8", + "Runtime": "dotnet6", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -255,7 +277,7 @@ "Method": "GET", "PayloadFormatVersion": "1.0", "Auth": { - "Authorizer": "HttpApiLambdaAuthorizer" + "Authorizer": "HttpApiLambdaAuthorizerV1" }, "ApiId": { "Ref": "AnnotationsHttpApi" @@ -282,7 +304,7 @@ } }, "Properties": { - "Runtime": "dotnet8", + "Runtime": "dotnet6", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -325,7 +347,7 @@ } }, "Properties": { - "Runtime": "dotnet8", + "Runtime": "dotnet6", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -357,7 +379,7 @@ "Tool": "Amazon.Lambda.Annotations" }, "Properties": { - "Runtime": "dotnet8", + "Runtime": "dotnet6", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -374,7 +396,7 @@ "Tool": "Amazon.Lambda.Annotations" }, "Properties": { - "Runtime": "dotnet8", + "Runtime": "dotnet6", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -384,6 +406,23 @@ "PackageType": "Zip", "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.AuthorizerFunction_RestApiAuthorize_Generated::RestApiAuthorize" } + }, + "CustomAuthorizerV1": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.AuthorizerFunction_HttpApiAuthorizeV1_Generated::HttpApiAuthorizeV1" + } } } } \ No newline at end of file diff --git a/Libraries/test/TestExecutableServerlessApp/serverless.template b/Libraries/test/TestExecutableServerlessApp/serverless.template index 8f71f0ecc..ac43959b7 100644 --- a/Libraries/test/TestExecutableServerlessApp/serverless.template +++ b/Libraries/test/TestExecutableServerlessApp/serverless.template @@ -21,7 +21,662 @@ ] } }, - "Resources": {}, + "Resources": { + "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestExecutableServerlessApp" + ] + }, + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "OkResponseWithHeader" + } + }, + "Events": { + "RootGet": { + "Type": "Api", + "Properties": { + "Path": "/okresponsewithheader/{x}", + "Method": "GET" + } + } + } + } + }, + "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderAsyncGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestExecutableServerlessApp" + ] + }, + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "OkResponseWithHeaderAsync" + } + }, + "Events": { + "RootGet": { + "Type": "Api", + "Properties": { + "Path": "/okresponsewithheaderasync/{x}", + "Method": "GET" + } + } + } + } + }, + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2Generated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestExecutableServerlessApp" + ] + }, + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV2" + } + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/notfoundwithheaderv2/{x}", + "Method": "GET" + } + } + } + } + }, + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2AsyncGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestExecutableServerlessApp" + ] + }, + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV2Async" + } + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/notfoundwithheaderv2async/{x}", + "Method": "GET" + } + } + } + } + }, + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1Generated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "PayloadFormatVersion" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestExecutableServerlessApp" + ] + }, + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV1" + } + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/notfoundwithheaderv1/{x}", + "Method": "GET", + "PayloadFormatVersion": "1.0" + } + } + } + } + }, + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1AsyncGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "PayloadFormatVersion" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestExecutableServerlessApp" + ] + }, + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV1Async" + } + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/notfoundwithheaderv1async/{x}", + "Method": "GET", + "PayloadFormatVersion": "1.0" + } + } + } + } + }, + "TestServerlessAppDynamicExampleDynamicReturnGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestExecutableServerlessApp" + ] + }, + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "DynamicReturn" + } + } + } + }, + "TestServerlessAppDynamicExampleDynamicInputGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestExecutableServerlessApp" + ] + }, + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "DynamicInput" + } + } + } + }, + "GreeterSayHello": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "PayloadFormatVersion" + ] + } + }, + "Properties": { + "MemorySize": 1024, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestExecutableServerlessApp" + ] + }, + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "SayHello" + } + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/Greeter/SayHello", + "Method": "GET", + "PayloadFormatVersion": "1.0" + } + } + } + } + }, + "GreeterSayHelloAsync": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "PayloadFormatVersion" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 50, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestExecutableServerlessApp" + ] + }, + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "SayHelloAsync" + } + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/Greeter/SayHelloAsync", + "Method": "GET", + "PayloadFormatVersion": "1.0" + } + } + } + } + }, + "TestServerlessAppIntrinsicExampleHasIntrinsicGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestExecutableServerlessApp" + ] + }, + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "HasIntrinsic" + } + } + } + }, + "TestServerlessAppNullableReferenceTypeExampleNullableHeaderHttpApiGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestExecutableServerlessApp" + ] + }, + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "NullableHeaderHttpApi" + } + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/nullableheaderhttpapi", + "Method": "GET" + } + } + } + } + }, + "TestServerlessAppParameterlessMethodsNoParameterGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "Runtime": "provided.al2", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestExecutableServerlessApp", + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "NoParameter" + } + } + } + }, + "TestServerlessAppParameterlessMethodWithResponseNoParameterWithResponseGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "Runtime": "provided.al2", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestExecutableServerlessApp", + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "NoParameterWithResponse" + } + } + } + }, + "TestExecutableServerlessAppSourceGenerationSerializationExampleGetPersonGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestExecutableServerlessApp" + ] + }, + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "GetPerson" + } + }, + "Events": { + "RootGet": { + "Type": "Api", + "Properties": { + "Path": "/", + "Method": "GET" + } + } + } + } + }, + "ToUpper": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestExecutableServerlessApp" + ] + }, + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "ToUpper" + } + } + } + }, + "ToLower": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "Runtime": "provided.al2", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestExecutableServerlessApp", + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "ToLower" + } + } + } + }, + "TestServerlessAppTaskExampleTaskReturnGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestExecutableServerlessApp" + ] + }, + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "TaskReturn" + } + } + } + }, + "TestServerlessAppVoidExampleVoidReturnGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestExecutableServerlessApp" + ] + }, + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "VoidReturn" + } + } + } + } + }, "Outputs": { "RestApiURL": { "Description": "Rest API endpoint URL for Prod environment", diff --git a/Libraries/test/TestServerlessApp/serverless.template b/Libraries/test/TestServerlessApp/serverless.template index 8509a4b45..8a8ca51ee 100644 --- a/Libraries/test/TestServerlessApp/serverless.template +++ b/Libraries/test/TestServerlessApp/serverless.template @@ -5,6 +5,1153 @@ "Resources": { "TestQueue": { "Type": "AWS::SQS::Queue" + }, + "AuthNameFallbackTest": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.AuthNameFallback_GetUserId_Generated::GetUserId" + ] + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/authorizer-fallback", + "Method": "GET" + } + } + } + } + }, + "TestServerlessAppComplexCalculatorAddGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootPost" + ], + "SyncedEventProperties": { + "RootPost": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.ComplexCalculator_Add_Generated::Add" + ] + }, + "Events": { + "RootPost": { + "Type": "HttpApi", + "Properties": { + "Path": "/ComplexCalculator/Add", + "Method": "POST" + } + } + } + } + }, + "TestServerlessAppComplexCalculatorSubtractGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootPost" + ], + "SyncedEventProperties": { + "RootPost": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.ComplexCalculator_Subtract_Generated::Subtract" + ] + }, + "Events": { + "RootPost": { + "Type": "HttpApi", + "Properties": { + "Path": "/ComplexCalculator/Subtract", + "Method": "POST" + } + } + } + } + }, + "HttpApiAuthorizerTest": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.CustomAuthorizerHttpApiExample_HttpApiAuthorizer_Generated::HttpApiAuthorizer" + ] + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/authorizer", + "Method": "GET" + } + } + } + } + }, + "HttpApiV1AuthorizerTest": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "PayloadFormatVersion" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.CustomAuthorizerHttpApiV1Example_HttpApiV1Authorizer_Generated::HttpApiV1Authorizer" + ] + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/authorizer-v1", + "Method": "GET", + "PayloadFormatVersion": "1.0" + } + } + } + } + }, + "HttpApiNonString": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.CustomAuthorizerNonStringExample_HttpApiWithNonString_Generated::HttpApiWithNonString" + ] + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/authorizer-non-string", + "Method": "GET" + } + } + } + } + }, + "RestAuthorizerTest": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.CustomAuthorizerRestExample_RestAuthorizer_Generated::RestAuthorizer" + ] + }, + "Events": { + "RootGet": { + "Type": "Api", + "Properties": { + "Path": "/rest/authorizer", + "Method": "GET" + } + } + } + } + }, + "TestServerlessAppCustomAuthorizerWithIHttpResultsExampleAuthorizerWithIHttpResultsGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.CustomAuthorizerWithIHttpResultsExample_AuthorizerWithIHttpResults_Generated::AuthorizerWithIHttpResults" + ] + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/authorizerihttpresults", + "Method": "GET" + } + } + } + } + }, + "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_OkResponseWithHeader_Generated::OkResponseWithHeader" + ] + }, + "Events": { + "RootGet": { + "Type": "Api", + "Properties": { + "Path": "/okresponsewithheader/{x}", + "Method": "GET" + } + } + } + } + }, + "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderAsyncGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_OkResponseWithHeaderAsync_Generated::OkResponseWithHeaderAsync" + ] + }, + "Events": { + "RootGet": { + "Type": "Api", + "Properties": { + "Path": "/okresponsewithheaderasync/{x}", + "Method": "GET" + } + } + } + } + }, + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2Generated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV2_Generated::NotFoundResponseWithHeaderV2" + ] + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/notfoundwithheaderv2/{x}", + "Method": "GET" + } + } + } + } + }, + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2AsyncGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV2Async_Generated::NotFoundResponseWithHeaderV2Async" + ] + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/notfoundwithheaderv2async/{x}", + "Method": "GET" + } + } + } + } + }, + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1Generated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "PayloadFormatVersion" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV1_Generated::NotFoundResponseWithHeaderV1" + ] + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/notfoundwithheaderv1/{x}", + "Method": "GET", + "PayloadFormatVersion": "1.0" + } + } + } + } + }, + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1AsyncGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "PayloadFormatVersion" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV1Async_Generated::NotFoundResponseWithHeaderV1Async" + ] + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/notfoundwithheaderv1async/{x}", + "Method": "GET", + "PayloadFormatVersion": "1.0" + } + } + } + } + }, + "TestServerlessAppCustomizeResponseExamplesOkResponseWithCustomSerializerGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "PayloadFormatVersion" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_OkResponseWithCustomSerializer_Generated::OkResponseWithCustomSerializer" + ] + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/okresponsewithcustomserializerasync/{firstName}/{lastName}", + "Method": "GET", + "PayloadFormatVersion": "1.0" + } + } + } + } + }, + "TestServerlessAppDynamicExampleDynamicReturnGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.DynamicExample_DynamicReturn_Generated::DynamicReturn" + ] + } + } + }, + "TestServerlessAppDynamicExampleDynamicInputGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.DynamicExample_DynamicInput_Generated::DynamicInput" + ] + } + } + }, + "TestServerlessAppFromScratchNoApiGatewayEventsReferenceToUpperGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.FromScratch.NoApiGatewayEventsReference_ToUpper_Generated::ToUpper" + ] + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/{text}", + "Method": "GET" + } + } + } + } + }, + "TestServerlessAppFromScratchNoSerializerAttributeReferenceToUpperGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.FromScratch.NoSerializerAttributeReference_ToUpper_Generated::ToUpper" + ] + } + } + }, + "GreeterSayHello": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "PayloadFormatVersion" + ] + } + }, + "Properties": { + "MemorySize": 1024, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.Greeter_SayHello_Generated::SayHello" + ] + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/Greeter/SayHello", + "Method": "GET", + "PayloadFormatVersion": "1.0" + } + } + } + } + }, + "GreeterSayHelloAsync": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "PayloadFormatVersion" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 50, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.Greeter_SayHelloAsync_Generated::SayHelloAsync" + ] + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/Greeter/SayHelloAsync", + "Method": "GET", + "PayloadFormatVersion": "1.0" + } + } + } + } + }, + "TestServerlessAppIntrinsicExampleHasIntrinsicGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.IntrinsicExample_HasIntrinsic_Generated::HasIntrinsic" + ] + } + } + }, + "TestServerlessAppNullableReferenceTypeExampleNullableHeaderHttpApiGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.NullableReferenceTypeExample_NullableHeaderHttpApi_Generated::NullableHeaderHttpApi" + ] + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/nullableheaderhttpapi", + "Method": "GET" + } + } + } + } + }, + "SimpleCalculatorAdd": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.SimpleCalculator_Add_Generated::Add" + ] + }, + "Events": { + "RootGet": { + "Type": "Api", + "Properties": { + "Path": "/SimpleCalculator/Add", + "Method": "GET" + } + } + } + } + }, + "SimpleCalculatorSubtract": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.SimpleCalculator_Subtract_Generated::Subtract" + ] + }, + "Events": { + "RootGet": { + "Type": "Api", + "Properties": { + "Path": "/SimpleCalculator/Subtract", + "Method": "GET" + } + } + } + } + }, + "SimpleCalculatorMultiply": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.SimpleCalculator_Multiply_Generated::Multiply" + ] + }, + "Events": { + "RootGet": { + "Type": "Api", + "Properties": { + "Path": "/SimpleCalculator/Multiply/{x}/{y}", + "Method": "GET" + } + } + } + } + }, + "SimpleCalculatorDivideAsync": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.SimpleCalculator_DivideAsync_Generated::DivideAsync" + ] + }, + "Events": { + "RootGet": { + "Type": "Api", + "Properties": { + "Path": "/SimpleCalculator/DivideAsync/{x}/{y}", + "Method": "GET" + } + } + } + } + }, + "PI": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.SimpleCalculator_Pi_Generated::Pi" + ] + } + } + }, + "Random": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.SimpleCalculator_Random_Generated::Random" + ] + } + } + }, + "Randoms": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.SimpleCalculator_Randoms_Generated::Randoms" + ] + } + } + }, + "SQSMessageHandler": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "TestQueueEvent" + ], + "SyncedEventProperties": { + "TestQueueEvent": [ + "Queue.Fn::GetAtt", + "BatchSize", + "FilterCriteria.Filters", + "FunctionResponseTypes", + "MaximumBatchingWindowInSeconds", + "ScalingConfig.MaximumConcurrency" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaSQSQueueExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.SqsMessageProcessing_HandleMessage_Generated::HandleMessage" + ] + }, + "Events": { + "TestQueueEvent": { + "Type": "SQS", + "Properties": { + "BatchSize": 50, + "FilterCriteria": { + "Filters": [ + { + "Pattern": "{ \"body\" : { \"RequestCode\" : [ \"BBBB\" ] } }" + } + ] + }, + "FunctionResponseTypes": [ + "ReportBatchItemFailures" + ], + "MaximumBatchingWindowInSeconds": 5, + "ScalingConfig": { + "MaximumConcurrency": 5 + }, + "Queue": { + "Fn::GetAtt": [ + "TestQueue", + "Arn" + ] + } + } + } + } + } + }, + "ToUpper": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.Sub1.Functions_ToUpper_Generated::ToUpper" + ] + } + } + }, + "TestServerlessAppTaskExampleTaskReturnGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.TaskExample_TaskReturn_Generated::TaskReturn" + ] + } + } + }, + "TestServerlessAppVoidExampleVoidReturnGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.VoidExample_VoidReturn_Generated::VoidReturn" + ] + } + } } } } \ No newline at end of file From d171555fba9109b09c2c192a401f23525cb3e038 Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Mon, 9 Mar 2026 12:14:15 -0400 Subject: [PATCH 09/14] add diagnostic for missing lambdafunction attribute --- .../Diagnostics/AnalyzerReleases.Unshipped.md | 1 + .../Diagnostics/DiagnosticDescriptors.cs | 8 +++++ .../Generator.cs | 11 ++++++ .../SyntaxReceiver.cs | 35 ++++++++++++++++++- .../SourceGeneratorTests.cs | 30 ++++++++++++++++ .../aws-lambda-tools-defaults.json | 8 ++--- .../MissingLambdaFunctionWithAuthorizer.cs | 23 ++++++++++++ 7 files changed, 111 insertions(+), 5 deletions(-) create mode 100644 Libraries/test/TestServerlessApp/FromScratch/MissingLambdaFunctionWithAuthorizer.cs diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/AnalyzerReleases.Unshipped.md b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/AnalyzerReleases.Unshipped.md index dfc1b52e5..a13794296 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/AnalyzerReleases.Unshipped.md +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/AnalyzerReleases.Unshipped.md @@ -13,3 +13,4 @@ AWSLambda0124 | AWSLambdaCSharpGenerator | Error | Authorizer Type Mismatch AWSLambda0125 | AWSLambdaCSharpGenerator | Error | Duplicate Authorizer Name AWSLambda0127 | AWSLambdaCSharpGenerator | Error | Invalid Result TTL AWSLambda0128 | AWSLambdaCSharpGenerator | Warning | Authorizer Payload Version Mismatch +AWSLambda0129 | AWSLambdaCSharpGenerator | Error | Missing LambdaFunction Attribute diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs index d7d381ee5..7e843ca52 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs @@ -218,5 +218,13 @@ public static class DiagnosticDescriptors category: "AWSLambdaCSharpGenerator", DiagnosticSeverity.Warning, isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor MissingLambdaFunctionAttribute = new DiagnosticDescriptor( + id: "AWSLambda0129", + title: "Missing LambdaFunction Attribute", + messageFormat: "Method has [{0}] attribute but is missing the required [LambdaFunction] attribute. Add [LambdaFunction] to this method.", + category: "AWSLambdaCSharpGenerator", + DiagnosticSeverity.Error, + isEnabledByDefault: true); } } diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Generator.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Generator.cs index 71d5cdca3..f68c3644c 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Generator.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Generator.cs @@ -151,6 +151,17 @@ public void Execute(GeneratorExecutionContext context) configureMethodSymbol = configureHostBuilderMethodSymbol; } + // Check for methods that have secondary attributes (HttpApi, RestApi, HttpApiAuthorizer, etc.) + // but are missing the required [LambdaFunction] attribute + foreach (var (method, attributeName) in receiver.MethodsWithMissingLambdaFunction) + { + diagnosticReporter.Report(Diagnostic.Create( + DiagnosticDescriptors.MissingLambdaFunctionAttribute, + Location.Create(method.SyntaxTree, method.Span), + attributeName)); + foundFatalError = true; + } + var annotationReport = new AnnotationReport(); var templateHandler = new CloudFormationTemplateHandler(_fileManager, _directoryManager); diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/SyntaxReceiver.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/SyntaxReceiver.cs index ddc167b1e..a5d7ce9ab 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/SyntaxReceiver.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/SyntaxReceiver.cs @@ -11,12 +11,31 @@ namespace Amazon.Lambda.Annotations.SourceGenerator internal class SyntaxReceiver : ISyntaxContextReceiver { + /// + /// Secondary attribute names that require the [LambdaFunction] attribute to also be present. + /// The key is the attribute class name, the value is the user-friendly name for diagnostics. + /// + private static readonly Dictionary _secondaryAttributeNames = new Dictionary + { + { "HttpApiAuthorizerAttribute", "HttpApiAuthorizer" }, + { "RestApiAuthorizerAttribute", "RestApiAuthorizer" }, + { "HttpApiAttribute", "HttpApi" }, + { "RestApiAttribute", "RestApi" }, + { "SQSEventAttribute", "SQSEvent" } + }; + public List LambdaMethods { get; } = new List(); public List StartupClasses { get; private set; } = new List(); public List MethodDeclarations { get; } = new List(); + /// + /// Methods that have a secondary Lambda annotation attribute but are missing [LambdaFunction]. + /// Each entry is a tuple of the method syntax, its location, and the friendly name of the secondary attribute found. + /// + public List<(MethodDeclarationSyntax Method, string AttributeName)> MethodsWithMissingLambdaFunction { get; } = new List<(MethodDeclarationSyntax, string)>(); + /// /// Path to the directory containing the .csproj file /// @@ -57,10 +76,24 @@ public void OnVisitSyntaxNode(GeneratorSyntaxContext context) var methodSymbol = ModelExtensions.GetDeclaredSymbol( context.SemanticModel, methodDeclarationSyntax); - if (methodSymbol.GetAttributes().Any(attr => attr.AttributeClass.Name == nameof(LambdaFunctionAttribute))) + var attributes = methodSymbol.GetAttributes(); + if (attributes.Any(attr => attr.AttributeClass.Name == nameof(LambdaFunctionAttribute))) { LambdaMethods.Add(methodDeclarationSyntax); } + else + { + // Check if the method has a secondary attribute without [LambdaFunction] + foreach (var attr in attributes) + { + var attrName = attr.AttributeClass?.Name; + if (attrName != null && _secondaryAttributeNames.TryGetValue(attrName, out var friendlyName)) + { + MethodsWithMissingLambdaFunction.Add((methodDeclarationSyntax, friendlyName)); + break; // Only report once per method + } + } + } } // any class with at least one attribute is a candidate of Startup class diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs index 2e0be9536..035555488 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs @@ -1756,6 +1756,36 @@ public async Task CustomAuthorizerAppAuthorizerDefinitionsTest() Assert.Equal(expectedTemplateContent, actualTemplateContent); } + [Fact] + public async Task VerifyMissingLambdaFunctionWithAuthorizerAttribute() + { + var test = new VerifyCS.Test() + { + TestState = + { + Sources = + { + (Path.Combine("TestServerlessApp", "FromScratch", "MissingLambdaFunctionWithAuthorizer.cs"), await File.ReadAllTextAsync(Path.Combine("TestServerlessApp", "FromScratch", "MissingLambdaFunctionWithAuthorizer.cs"))), + (Path.Combine("Amazon.Lambda.Annotations", "LambdaFunctionAttribute.cs"), await File.ReadAllTextAsync(Path.Combine("Amazon.Lambda.Annotations", "LambdaFunctionAttribute.cs"))), + (Path.Combine("Amazon.Lambda.Annotations", "LambdaStartupAttribute.cs"), await File.ReadAllTextAsync(Path.Combine("Amazon.Lambda.Annotations", "LambdaStartupAttribute.cs"))), + (Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "HttpApiAuthorizerAttribute.cs"), await File.ReadAllTextAsync(Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "HttpApiAuthorizerAttribute.cs"))), + (Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "AuthorizerPayloadFormatVersion.cs"), await File.ReadAllTextAsync(Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "AuthorizerPayloadFormatVersion.cs"))), + }, + GeneratedSources = + { + }, + ExpectedDiagnostics = + { + new DiagnosticResult("AWSLambda0129", DiagnosticSeverity.Error) + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}FromScratch{Path.DirectorySeparatorChar}MissingLambdaFunctionWithAuthorizer.cs", 10, 9, 21, 10) + .WithMessage("Method has [HttpApiAuthorizer] attribute but is missing the required [LambdaFunction] attribute. Add [LambdaFunction] to this method."), + }, + }, + }; + + await test.RunAsync(); + } + public void Dispose() { File.Delete(Path.Combine("TestServerlessApp", "serverless.template")); diff --git a/Libraries/test/TestCustomAuthorizerApp/aws-lambda-tools-defaults.json b/Libraries/test/TestCustomAuthorizerApp/aws-lambda-tools-defaults.json index 704f3e445..53198ee82 100644 --- a/Libraries/test/TestCustomAuthorizerApp/aws-lambda-tools-defaults.json +++ b/Libraries/test/TestCustomAuthorizerApp/aws-lambda-tools-defaults.json @@ -8,8 +8,8 @@ "configuration": "Release", "template": "serverless.template", "template-parameters": "", -"s3-bucket" : "test-custom-authorizer-112e1ec1", + "s3-bucket": "test-custom-authorizer-app", "s3-prefix": "TestCustomAuthorizerApp/", -"stack-name" : "test-custom-authorizer-112e1ec1", -"function-architecture" : "x86_64" -} + "stack-name": "test-custom-authorizer", + "function-architecture": "x86_64" +} \ No newline at end of file diff --git a/Libraries/test/TestServerlessApp/FromScratch/MissingLambdaFunctionWithAuthorizer.cs b/Libraries/test/TestServerlessApp/FromScratch/MissingLambdaFunctionWithAuthorizer.cs new file mode 100644 index 000000000..157ebcbb2 --- /dev/null +++ b/Libraries/test/TestServerlessApp/FromScratch/MissingLambdaFunctionWithAuthorizer.cs @@ -0,0 +1,23 @@ +using Amazon.Lambda.Annotations; +using Amazon.Lambda.Annotations.APIGateway; +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.Core; + +namespace TestServerlessApp.FromScratch +{ + public class MissingLambdaFunctionWithAuthorizer + { + [HttpApiAuthorizer( + Name = "MyAuthorizer", + AuthorizerPayloadFormatVersion = AuthorizerPayloadFormatVersion.V2)] + public APIGatewayCustomAuthorizerV2SimpleResponse Authorize( + APIGatewayCustomAuthorizerV2Request request, + ILambdaContext context) + { + return new APIGatewayCustomAuthorizerV2SimpleResponse + { + IsAuthorized = true + }; + } + } +} \ No newline at end of file From a4608e648524c5051dc2bc7e9e263d34e4a4b5af Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Mon, 9 Mar 2026 12:21:06 -0400 Subject: [PATCH 10/14] use constructor --- .../Attributes/HttpApiAuthorizerAttributeBuilder.cs | 10 ++++++---- .../Attributes/RestApiAuthorizerAttributeBuilder.cs | 10 ++++++---- .../APIGateway/HttpApiAuthorizerAttribute.cs | 12 +++++++++++- .../APIGateway/RestApiAuthorizerAttribute.cs | 12 +++++++++++- Libraries/src/Amazon.Lambda.Annotations/README.md | 11 +++++------ .../TestCustomAuthorizerApp/AuthorizerFunction.cs | 9 +++------ .../MissingLambdaFunctionWithAuthorizer.cs | 3 +-- 7 files changed, 43 insertions(+), 24 deletions(-) diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/HttpApiAuthorizerAttributeBuilder.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/HttpApiAuthorizerAttributeBuilder.cs index 341e6abf8..bc5d7a7d5 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/HttpApiAuthorizerAttributeBuilder.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/HttpApiAuthorizerAttributeBuilder.cs @@ -16,15 +16,17 @@ public static class HttpApiAuthorizerAttributeBuilder /// The populated attribute instance public static HttpApiAuthorizerAttribute Build(AttributeData att) { - var attribute = new HttpApiAuthorizerAttribute(); + // Name is the first constructor argument + var name = att.ConstructorArguments.Length > 0 + ? att.ConstructorArguments[0].Value as string + : null; + + var attribute = new HttpApiAuthorizerAttribute(name); foreach (var namedArg in att.NamedArguments) { switch (namedArg.Key) { - case nameof(HttpApiAuthorizerAttribute.Name): - attribute.Name = namedArg.Value.Value as string; - break; case nameof(HttpApiAuthorizerAttribute.IdentityHeader): attribute.IdentityHeader = namedArg.Value.Value as string ?? "Authorization"; break; diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/RestApiAuthorizerAttributeBuilder.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/RestApiAuthorizerAttributeBuilder.cs index b981c6ed0..c47ddc753 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/RestApiAuthorizerAttributeBuilder.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/RestApiAuthorizerAttributeBuilder.cs @@ -16,15 +16,17 @@ public static class RestApiAuthorizerAttributeBuilder /// The populated attribute instance public static RestApiAuthorizerAttribute Build(AttributeData att) { - var attribute = new RestApiAuthorizerAttribute(); + // Name is the first constructor argument + var name = att.ConstructorArguments.Length > 0 + ? att.ConstructorArguments[0].Value as string + : null; + + var attribute = new RestApiAuthorizerAttribute(name); foreach (var namedArg in att.NamedArguments) { switch (namedArg.Key) { - case nameof(RestApiAuthorizerAttribute.Name): - attribute.Name = namedArg.Value.Value as string; - break; case nameof(RestApiAuthorizerAttribute.IdentityHeader): attribute.IdentityHeader = namedArg.Value.Value as string ?? "Authorization"; break; diff --git a/Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpApiAuthorizerAttribute.cs b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpApiAuthorizerAttribute.cs index 942dc7b9b..10894ec73 100644 --- a/Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpApiAuthorizerAttribute.cs +++ b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpApiAuthorizerAttribute.cs @@ -15,7 +15,7 @@ namespace Amazon.Lambda.Annotations.APIGateway /// /// /// [LambdaFunction] - /// [HttpApiAuthorizer(Name = "MyAuthorizer")] + /// [HttpApiAuthorizer("MyAuthorizer")] /// public APIGatewayCustomAuthorizerV2SimpleResponse Authorize(APIGatewayCustomAuthorizerV2Request request) /// { /// // Validate token and return authorization response @@ -32,6 +32,16 @@ namespace Amazon.Lambda.Annotations.APIGateway [AttributeUsage(AttributeTargets.Method)] public class HttpApiAuthorizerAttribute : Attribute { + /// + /// Creates a new HTTP API authorizer attribute with the specified name. + /// + /// Unique name to identify this authorizer. Other functions reference this name + /// via the property. + public HttpApiAuthorizerAttribute(string name) + { + Name = name; + } + /// /// Required. Unique name to identify this authorizer. Other functions reference this name /// via the property. diff --git a/Libraries/src/Amazon.Lambda.Annotations/APIGateway/RestApiAuthorizerAttribute.cs b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/RestApiAuthorizerAttribute.cs index 611b3be14..d726c83b6 100644 --- a/Libraries/src/Amazon.Lambda.Annotations/APIGateway/RestApiAuthorizerAttribute.cs +++ b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/RestApiAuthorizerAttribute.cs @@ -31,7 +31,7 @@ public enum RestApiAuthorizerType /// /// /// [LambdaFunction] - /// [RestApiAuthorizer(Name = "TokenAuthorizer", Type = RestApiAuthorizerType.Token)] + /// [RestApiAuthorizer("TokenAuthorizer", Type = RestApiAuthorizerType.Token)] /// public APIGatewayCustomAuthorizerResponse Authorize(APIGatewayCustomAuthorizerRequest request) /// { /// var token = request.AuthorizationToken; @@ -49,6 +49,16 @@ public enum RestApiAuthorizerType [AttributeUsage(AttributeTargets.Method)] public class RestApiAuthorizerAttribute : Attribute { + /// + /// Creates a new REST API authorizer attribute with the specified name. + /// + /// Unique name to identify this authorizer. Other functions reference this name + /// via the property. + public RestApiAuthorizerAttribute(string name) + { + Name = name; + } + /// /// Required. Unique name to identify this authorizer. Other functions reference this name /// via the property. diff --git a/Libraries/src/Amazon.Lambda.Annotations/README.md b/Libraries/src/Amazon.Lambda.Annotations/README.md index 550d5b088..af8aedf2f 100644 --- a/Libraries/src/Amazon.Lambda.Annotations/README.md +++ b/Libraries/src/Amazon.Lambda.Annotations/README.md @@ -869,7 +869,7 @@ The authorizer function receives an `APIGatewayCustomAuthorizerV2Request` and re ```csharp [LambdaFunction(ResourceName = "HttpApiAuthorizer", PackageType = LambdaPackageType.Image)] -[HttpApiAuthorizer(Name = "MyHttpAuthorizer")] +[HttpApiAuthorizer("MyHttpAuthorizer")] public APIGatewayCustomAuthorizerV2SimpleResponse AuthorizeHttpApi( APIGatewayCustomAuthorizerV2Request request, ILambdaContext context) @@ -945,7 +945,7 @@ REST API authorizers work similarly but use `[RestApiAuthorizer]` and `[RestApi] ```csharp [LambdaFunction(ResourceName = "RestApiAuthorizer", PackageType = LambdaPackageType.Image)] -[RestApiAuthorizer(Name = "MyRestAuthorizer", Type = RestApiAuthorizerType.Token)] +[RestApiAuthorizer("MyRestAuthorizer", Type = RestApiAuthorizerType.Token)] public APIGatewayCustomAuthorizerResponse AuthorizeRestApi( APIGatewayCustomAuthorizerRequest request, ILambdaContext context) @@ -1035,8 +1035,7 @@ public object GetRestProtectedResource( ```csharp [LambdaFunction(ResourceName = "ApiKeyAuthorizer", PackageType = LambdaPackageType.Image)] -[HttpApiAuthorizer( - Name = "ApiKeyAuth", +[HttpApiAuthorizer("ApiKeyAuth", IdentityHeader = "X-Api-Key", ResultTtlInSeconds = 300)] public APIGatewayCustomAuthorizerV2SimpleResponse ValidateApiKey( @@ -1104,9 +1103,9 @@ parameter to the `LambdaFunction` must be the event object and the event source * HttpApi * Configures the Lambda function to be called from an API Gateway HTTP API. The HTTP method, HTTP API payload version and resource path are required to be set on the attribute. Use the `Authorizer` property to reference an `HttpApiAuthorizer` by name. * HttpApiAuthorizer - * Marks a Lambda function as an HTTP API (API Gateway V2) custom authorizer. Set the `Name` property to give the authorizer a unique name that can be referenced by `HttpApi.Authorizer`. + * Marks a Lambda function as an HTTP API (API Gateway V2) custom authorizer. Pass the authorizer name as the constructor argument to give it a unique name that can be referenced by `HttpApi.Authorizer`. * RestApiAuthorizer - * Marks a Lambda function as a REST API (API Gateway V1) custom authorizer. Set the `Name` property to give the authorizer a unique name that can be referenced by `RestApi.Authorizer`. Use the `Type` property to choose between `Token` and `Request` authorizer types. + * Marks a Lambda function as a REST API (API Gateway V1) custom authorizer. Pass the authorizer name as the constructor argument to give it a unique name that can be referenced by `RestApi.Authorizer`. Use the `Type` property to choose between `Token` and `Request` authorizer types. * SQSEvent * Sets up event source mapping between the Lambda function and SQS queues. The SQS queue ARN is required to be set on the attribute. If users want to pass a reference to an existing SQS queue resource defined in their CloudFormation template, they can pass the SQS queue resource name prefixed with the '@' symbol. diff --git a/Libraries/test/TestCustomAuthorizerApp/AuthorizerFunction.cs b/Libraries/test/TestCustomAuthorizerApp/AuthorizerFunction.cs index 3442d64cd..e1529a26e 100644 --- a/Libraries/test/TestCustomAuthorizerApp/AuthorizerFunction.cs +++ b/Libraries/test/TestCustomAuthorizerApp/AuthorizerFunction.cs @@ -39,8 +39,7 @@ public class AuthorizerFunction /// Returns authorized status along with custom context that can be accessed via [FromCustomAuthorizer] /// [LambdaFunction(ResourceName = "CustomAuthorizer")] - [HttpApiAuthorizer( - Name = "HttpApiLambdaAuthorizer", + [HttpApiAuthorizer("HttpApiLambdaAuthorizer", IdentityHeader = "authorization", EnableSimpleResponses = true, AuthorizerPayloadFormatVersion = AuthorizerPayloadFormatVersion.V2)] @@ -121,8 +120,7 @@ public APIGatewayCustomAuthorizerV2SimpleResponse HttpApiAuthorize( /// Used by HTTP API V1 endpoints that need payload format 1.0 /// [LambdaFunction(ResourceName = "CustomAuthorizerV1")] - [HttpApiAuthorizer( - Name = "HttpApiLambdaAuthorizerV1", + [HttpApiAuthorizer("HttpApiLambdaAuthorizerV1", IdentityHeader = "authorization", EnableSimpleResponses = false, AuthorizerPayloadFormatVersion = AuthorizerPayloadFormatVersion.V1)] @@ -204,8 +202,7 @@ public APIGatewayCustomAuthorizerResponse HttpApiAuthorizeV1( /// Returns an IAM policy document along with custom context values /// [LambdaFunction(ResourceName = "RestApiAuthorizer")] - [RestApiAuthorizer( - Name = "RestApiLambdaAuthorizer", + [RestApiAuthorizer("RestApiLambdaAuthorizer", Type = RestApiAuthorizerType.Token, IdentityHeader = "Authorization")] public APIGatewayCustomAuthorizerResponse RestApiAuthorize( diff --git a/Libraries/test/TestServerlessApp/FromScratch/MissingLambdaFunctionWithAuthorizer.cs b/Libraries/test/TestServerlessApp/FromScratch/MissingLambdaFunctionWithAuthorizer.cs index 157ebcbb2..7e3350d2d 100644 --- a/Libraries/test/TestServerlessApp/FromScratch/MissingLambdaFunctionWithAuthorizer.cs +++ b/Libraries/test/TestServerlessApp/FromScratch/MissingLambdaFunctionWithAuthorizer.cs @@ -7,8 +7,7 @@ namespace TestServerlessApp.FromScratch { public class MissingLambdaFunctionWithAuthorizer { - [HttpApiAuthorizer( - Name = "MyAuthorizer", + [HttpApiAuthorizer("MyAuthorizer", AuthorizerPayloadFormatVersion = AuthorizerPayloadFormatVersion.V2)] public APIGatewayCustomAuthorizerV2SimpleResponse Authorize( APIGatewayCustomAuthorizerV2Request request, From 9e665d9b21aebc34a3c0979ed88e24bc93c3cd77 Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Mon, 9 Mar 2026 12:28:29 -0400 Subject: [PATCH 11/14] dev config --- .../changes/09045e59-ab51-459e-b450-43fcb082d084.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .autover/changes/09045e59-ab51-459e-b450-43fcb082d084.json diff --git a/.autover/changes/09045e59-ab51-459e-b450-43fcb082d084.json b/.autover/changes/09045e59-ab51-459e-b450-43fcb082d084.json new file mode 100644 index 000000000..66108b5e5 --- /dev/null +++ b/.autover/changes/09045e59-ab51-459e-b450-43fcb082d084.json @@ -0,0 +1,11 @@ +{ + "Projects": [ + { + "Name": "Amazon.Lambda.Annotations", + "Type": "Patch", + "ChangelogMessages": [ + "Added Authorizers annotation" + ] + } + ] +} \ No newline at end of file From feb3f8e038003edd0b1fff82f08df1131f1fcfaf Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Mon, 9 Mar 2026 12:30:53 -0400 Subject: [PATCH 12/14] fix test --- .../SourceGeneratorTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs index 035555488..1995743a2 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs @@ -1777,7 +1777,7 @@ public async Task VerifyMissingLambdaFunctionWithAuthorizerAttribute() ExpectedDiagnostics = { new DiagnosticResult("AWSLambda0129", DiagnosticSeverity.Error) - .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}FromScratch{Path.DirectorySeparatorChar}MissingLambdaFunctionWithAuthorizer.cs", 10, 9, 21, 10) + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}FromScratch{Path.DirectorySeparatorChar}MissingLambdaFunctionWithAuthorizer.cs", 10, 9, 20, 10) .WithMessage("Method has [HttpApiAuthorizer] attribute but is missing the required [LambdaFunction] attribute. Add [LambdaFunction] to this method."), }, }, From 4effbadceea6c2c25d9d58a8002339bf34c452e7 Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Mon, 9 Mar 2026 15:52:23 -0400 Subject: [PATCH 13/14] use nameof --- .../Generator.cs | 14 +++++++++--- .../HttpApiAuthorizerAttributeBuilder.cs | 8 +------ .../RestApiAuthorizerAttributeBuilder.cs | 8 +------ .../APIGateway/HttpApiAuthorizerAttribute.cs | 22 ++++--------------- .../APIGateway/RestApiAuthorizerAttribute.cs | 22 ++++--------------- .../src/Amazon.Lambda.Annotations/README.md | 22 +++++++++---------- .../customAuthorizerApp.template | 18 +++++++-------- .../AuthorizerFunction.cs | 6 ++--- .../ProtectedFunction.cs | 12 +++++----- .../serverless.template | 20 +---------------- .../MissingLambdaFunctionWithAuthorizer.cs | 2 +- 11 files changed, 51 insertions(+), 103 deletions(-) diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Generator.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Generator.cs index f68c3644c..5caca1cee 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Generator.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Generator.cs @@ -346,14 +346,22 @@ private static AuthorizerModel ExtractAuthorizerModel(IMethodSymbol methodSymbol { var attributeFullName = attribute.AttributeClass?.ToDisplayString(); + AuthorizerModel model = null; + if (attributeFullName == TypeFullNames.HttpApiAuthorizerAttribute) { - return HttpApiAuthorizerAttributeBuilder.BuildModel(attribute, lambdaResourceName); + model = HttpApiAuthorizerAttributeBuilder.BuildModel(attribute, lambdaResourceName); + } + else if (attributeFullName == TypeFullNames.RestApiAuthorizerAttribute) + { + model = RestApiAuthorizerAttributeBuilder.BuildModel(attribute, lambdaResourceName); } - if (attributeFullName == TypeFullNames.RestApiAuthorizerAttribute) + if (model != null) { - return RestApiAuthorizerAttributeBuilder.BuildModel(attribute, lambdaResourceName); + // The authorizer name is always derived from the method name + model.Name = methodSymbol.Name; + return model; } } diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/HttpApiAuthorizerAttributeBuilder.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/HttpApiAuthorizerAttributeBuilder.cs index bc5d7a7d5..d5c1776a6 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/HttpApiAuthorizerAttributeBuilder.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/HttpApiAuthorizerAttributeBuilder.cs @@ -16,12 +16,7 @@ public static class HttpApiAuthorizerAttributeBuilder /// The populated attribute instance public static HttpApiAuthorizerAttribute Build(AttributeData att) { - // Name is the first constructor argument - var name = att.ConstructorArguments.Length > 0 - ? att.ConstructorArguments[0].Value as string - : null; - - var attribute = new HttpApiAuthorizerAttribute(name); + var attribute = new HttpApiAuthorizerAttribute(); foreach (var namedArg in att.NamedArguments) { @@ -69,7 +64,6 @@ public static AuthorizerModel BuildModel(HttpApiAuthorizerAttribute attribute, s { return new AuthorizerModel { - Name = attribute.Name, LambdaResourceName = lambdaResourceName, AuthorizerType = AuthorizerType.HttpApi, IdentityHeader = attribute.IdentityHeader, diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/RestApiAuthorizerAttributeBuilder.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/RestApiAuthorizerAttributeBuilder.cs index c47ddc753..881a43bff 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/RestApiAuthorizerAttributeBuilder.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/RestApiAuthorizerAttributeBuilder.cs @@ -16,12 +16,7 @@ public static class RestApiAuthorizerAttributeBuilder /// The populated attribute instance public static RestApiAuthorizerAttribute Build(AttributeData att) { - // Name is the first constructor argument - var name = att.ConstructorArguments.Length > 0 - ? att.ConstructorArguments[0].Value as string - : null; - - var attribute = new RestApiAuthorizerAttribute(name); + var attribute = new RestApiAuthorizerAttribute(); foreach (var namedArg in att.NamedArguments) { @@ -66,7 +61,6 @@ public static AuthorizerModel BuildModel(RestApiAuthorizerAttribute attribute, s { return new AuthorizerModel { - Name = attribute.Name, LambdaResourceName = lambdaResourceName, AuthorizerType = AuthorizerType.RestApi, IdentityHeader = attribute.IdentityHeader, diff --git a/Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpApiAuthorizerAttribute.cs b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpApiAuthorizerAttribute.cs index 10894ec73..17b7d0bf7 100644 --- a/Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpApiAuthorizerAttribute.cs +++ b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpApiAuthorizerAttribute.cs @@ -14,15 +14,17 @@ namespace Amazon.Lambda.Annotations.APIGateway /// /// /// + /// // The authorizer name is automatically derived from the method name ("Authorize"). /// [LambdaFunction] - /// [HttpApiAuthorizer("MyAuthorizer")] + /// [HttpApiAuthorizer] /// public APIGatewayCustomAuthorizerV2SimpleResponse Authorize(APIGatewayCustomAuthorizerV2Request request) /// { /// // Validate token and return authorization response /// } /// + /// // Reference the authorizer using nameof for compile-time safety /// [LambdaFunction] - /// [HttpApi(LambdaHttpMethod.Get, "/api/protected", Authorizer = "MyAuthorizer")] + /// [HttpApi(LambdaHttpMethod.Get, "/api/protected", Authorizer = nameof(Authorize))] /// public string ProtectedEndpoint() /// { /// return "Hello, authenticated user!"; @@ -32,22 +34,6 @@ namespace Amazon.Lambda.Annotations.APIGateway [AttributeUsage(AttributeTargets.Method)] public class HttpApiAuthorizerAttribute : Attribute { - /// - /// Creates a new HTTP API authorizer attribute with the specified name. - /// - /// Unique name to identify this authorizer. Other functions reference this name - /// via the property. - public HttpApiAuthorizerAttribute(string name) - { - Name = name; - } - - /// - /// Required. Unique name to identify this authorizer. Other functions reference this name - /// via the property. - /// - public string Name { get; set; } - /// /// Header name to use as identity source. Defaults to "Authorization". /// The generator translates this to "$request.header.{IdentityHeader}" for CloudFormation. diff --git a/Libraries/src/Amazon.Lambda.Annotations/APIGateway/RestApiAuthorizerAttribute.cs b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/RestApiAuthorizerAttribute.cs index d726c83b6..b578e0d97 100644 --- a/Libraries/src/Amazon.Lambda.Annotations/APIGateway/RestApiAuthorizerAttribute.cs +++ b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/RestApiAuthorizerAttribute.cs @@ -30,16 +30,18 @@ public enum RestApiAuthorizerType /// /// /// + /// // The authorizer name is automatically derived from the method name ("Authorize"). /// [LambdaFunction] - /// [RestApiAuthorizer("TokenAuthorizer", Type = RestApiAuthorizerType.Token)] + /// [RestApiAuthorizer(Type = RestApiAuthorizerType.Token)] /// public APIGatewayCustomAuthorizerResponse Authorize(APIGatewayCustomAuthorizerRequest request) /// { /// var token = request.AuthorizationToken; /// // Validate token and return IAM policy response /// } /// + /// // Reference the authorizer using nameof for compile-time safety /// [LambdaFunction] - /// [RestApi(LambdaHttpMethod.Get, "/api/protected", Authorizer = "TokenAuthorizer")] + /// [RestApi(LambdaHttpMethod.Get, "/api/protected", Authorizer = nameof(Authorize))] /// public string ProtectedEndpoint() /// { /// return "Hello, authenticated user!"; @@ -49,22 +51,6 @@ public enum RestApiAuthorizerType [AttributeUsage(AttributeTargets.Method)] public class RestApiAuthorizerAttribute : Attribute { - /// - /// Creates a new REST API authorizer attribute with the specified name. - /// - /// Unique name to identify this authorizer. Other functions reference this name - /// via the property. - public RestApiAuthorizerAttribute(string name) - { - Name = name; - } - - /// - /// Required. Unique name to identify this authorizer. Other functions reference this name - /// via the property. - /// - public string Name { get; set; } - /// /// Header name to use as identity source. Defaults to "Authorization". /// The generator translates this to "method.request.header.{IdentityHeader}" for CloudFormation. diff --git a/Libraries/src/Amazon.Lambda.Annotations/README.md b/Libraries/src/Amazon.Lambda.Annotations/README.md index af8aedf2f..681cc4be5 100644 --- a/Libraries/src/Amazon.Lambda.Annotations/README.md +++ b/Libraries/src/Amazon.Lambda.Annotations/README.md @@ -861,7 +861,7 @@ Two authorizer attributes are available: ### HTTP API Authorizer -To create an HTTP API authorizer, decorate a Lambda function with the `[HttpApiAuthorizer]` attribute and give it a unique `Name`. Other functions can then reference that authorizer via the `Authorizer` property on `[HttpApi]`. +To create an HTTP API authorizer, decorate a Lambda function with the `[HttpApiAuthorizer]` attribute. The authorizer name is automatically derived from the method name, so other functions can reference it using `nameof()` for compile-time safety. **Step 1: Define the authorizer function** @@ -869,7 +869,7 @@ The authorizer function receives an `APIGatewayCustomAuthorizerV2Request` and re ```csharp [LambdaFunction(ResourceName = "HttpApiAuthorizer", PackageType = LambdaPackageType.Image)] -[HttpApiAuthorizer("MyHttpAuthorizer")] +[HttpApiAuthorizer] public APIGatewayCustomAuthorizerV2SimpleResponse AuthorizeHttpApi( APIGatewayCustomAuthorizerV2Request request, ILambdaContext context) @@ -896,11 +896,11 @@ public APIGatewayCustomAuthorizerV2SimpleResponse AuthorizeHttpApi( **Step 2: Protect an endpoint with the authorizer** -Reference the authorizer by name in the `Authorizer` property of `[HttpApi]`. Use `[FromCustomAuthorizer]` on method parameters to automatically extract values from the authorizer context. +Reference the authorizer using `nameof()` in the `Authorizer` property of `[HttpApi]` for compile-time safety. If the authorizer method is renamed, both references update automatically. Use `[FromCustomAuthorizer]` on method parameters to automatically extract values from the authorizer context. ```csharp [LambdaFunction(ResourceName = "ProtectedHttpApiFunction", PackageType = LambdaPackageType.Image)] -[HttpApi(LambdaHttpMethod.Get, "/api/protected", Authorizer = "MyHttpAuthorizer")] +[HttpApi(LambdaHttpMethod.Get, "/api/protected", Authorizer = nameof(AuthorizeHttpApi))] public object GetProtectedResource( [FromCustomAuthorizer(Name = "userId")] string userId, [FromCustomAuthorizer(Name = "email")] string email) @@ -913,7 +913,7 @@ Multiple endpoints can share the same authorizer: ```csharp [LambdaFunction(ResourceName = "AdminHttpApiFunction", PackageType = LambdaPackageType.Image)] -[HttpApi(LambdaHttpMethod.Get, "/api/admin", Authorizer = "MyHttpAuthorizer")] +[HttpApi(LambdaHttpMethod.Get, "/api/admin", Authorizer = nameof(AuthorizeHttpApi))] public object AdminEndpoint( [FromCustomAuthorizer(Name = "userId")] string userId, [FromCustomAuthorizer(Name = "role")] string role) @@ -945,7 +945,7 @@ REST API authorizers work similarly but use `[RestApiAuthorizer]` and `[RestApi] ```csharp [LambdaFunction(ResourceName = "RestApiAuthorizer", PackageType = LambdaPackageType.Image)] -[RestApiAuthorizer("MyRestAuthorizer", Type = RestApiAuthorizerType.Token)] +[RestApiAuthorizer(Type = RestApiAuthorizerType.Token)] public APIGatewayCustomAuthorizerResponse AuthorizeRestApi( APIGatewayCustomAuthorizerRequest request, ILambdaContext context) @@ -1002,7 +1002,7 @@ public APIGatewayCustomAuthorizerResponse AuthorizeRestApi( ```csharp [LambdaFunction(ResourceName = "ProtectedRestApiFunction", PackageType = LambdaPackageType.Image)] -[RestApi(LambdaHttpMethod.Get, "/api/rest/protected", Authorizer = "MyRestAuthorizer")] +[RestApi(LambdaHttpMethod.Get, "/api/rest/protected", Authorizer = nameof(AuthorizeRestApi))] public object GetRestProtectedResource( [FromCustomAuthorizer(Name = "userId")] string userId) { @@ -1016,7 +1016,6 @@ public object GetRestProtectedResource( | Property | Type | Default | Description | |---|---|---|---| -| `Name` | `string` | *(required)* | Unique name to identify this authorizer. Referenced by `HttpApi.Authorizer`. | | `IdentityHeader` | `string` | `"Authorization"` | Header used as the identity source. Translated to `$request.header.{value}` in CloudFormation. | | `EnableSimpleResponses` | `bool` | `true` | When `true`, use simple responses (`IsAuthorized: true/false`). When `false`, use IAM policy responses. | | `PayloadFormatVersion` | `string` | `"2.0"` | Authorizer payload format version. Valid values: `"1.0"` or `"2.0"`. | @@ -1026,7 +1025,6 @@ public object GetRestProtectedResource( | Property | Type | Default | Description | |---|---|---|---| -| `Name` | `string` | *(required)* | Unique name to identify this authorizer. Referenced by `RestApi.Authorizer`. | | `IdentityHeader` | `string` | `"Authorization"` | Header used as the identity source. Translated to `method.request.header.{value}` in CloudFormation. | | `Type` | `RestApiAuthorizerType` | `Token` | Type of authorizer: `Token` (receives just the token) or `Request` (receives full request context). | | `ResultTtlInSeconds` | `int` | `0` | TTL in seconds for caching authorizer results. `0` = no caching. Max = `3600`. | @@ -1035,7 +1033,7 @@ public object GetRestProtectedResource( ```csharp [LambdaFunction(ResourceName = "ApiKeyAuthorizer", PackageType = LambdaPackageType.Image)] -[HttpApiAuthorizer("ApiKeyAuth", +[HttpApiAuthorizer( IdentityHeader = "X-Api-Key", ResultTtlInSeconds = 300)] public APIGatewayCustomAuthorizerV2SimpleResponse ValidateApiKey( @@ -1103,9 +1101,9 @@ parameter to the `LambdaFunction` must be the event object and the event source * HttpApi * Configures the Lambda function to be called from an API Gateway HTTP API. The HTTP method, HTTP API payload version and resource path are required to be set on the attribute. Use the `Authorizer` property to reference an `HttpApiAuthorizer` by name. * HttpApiAuthorizer - * Marks a Lambda function as an HTTP API (API Gateway V2) custom authorizer. Pass the authorizer name as the constructor argument to give it a unique name that can be referenced by `HttpApi.Authorizer`. + * Marks a Lambda function as an HTTP API (API Gateway V2) custom authorizer. The authorizer name is automatically derived from the method name. Other functions reference it via `HttpApi.Authorizer` using `nameof()` for compile-time safety. * RestApiAuthorizer - * Marks a Lambda function as a REST API (API Gateway V1) custom authorizer. Pass the authorizer name as the constructor argument to give it a unique name that can be referenced by `RestApi.Authorizer`. Use the `Type` property to choose between `Token` and `Request` authorizer types. + * Marks a Lambda function as a REST API (API Gateway V1) custom authorizer. The authorizer name is automatically derived from the method name. Other functions reference it via `RestApi.Authorizer` using `nameof()`. Use the `Type` property to choose between `Token` and `Request` authorizer types. * SQSEvent * Sets up event source mapping between the Lambda function and SQS queues. The SQS queue ARN is required to be set on the attribute. If users want to pass a reference to an existing SQS queue resource defined in their CloudFormation template, they can pass the SQS queue resource name prefixed with the '@' symbol. diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/customAuthorizerApp.template b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/customAuthorizerApp.template index 4270c197f..f592ff292 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/customAuthorizerApp.template +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/customAuthorizerApp.template @@ -11,7 +11,7 @@ "Properties": { "Auth": { "Authorizers": { - "HttpApiLambdaAuthorizer": { + "HttpApiAuthorize": { "FunctionArn": { "Fn::GetAtt": [ "CustomAuthorizer", @@ -28,7 +28,7 @@ "EnableFunctionDefaultPermissions": true, "AuthorizerResultTtlInSeconds": 0 }, - "HttpApiLambdaAuthorizerV1": { + "HttpApiAuthorizeV1": { "FunctionArn": { "Fn::GetAtt": [ "CustomAuthorizerV1", @@ -55,7 +55,7 @@ "StageName": "Prod", "Auth": { "Authorizers": { - "RestApiLambdaAuthorizer": { + "RestApiAuthorize": { "FunctionArn": { "Fn::GetAtt": [ "RestApiAuthorizer", @@ -158,7 +158,7 @@ "Path": "/api/protected", "Method": "GET", "Auth": { - "Authorizer": "HttpApiLambdaAuthorizer" + "Authorizer": "HttpApiAuthorize" }, "ApiId": { "Ref": "AnnotationsHttpApi" @@ -201,7 +201,7 @@ "Path": "/api/user-info", "Method": "GET", "Auth": { - "Authorizer": "HttpApiLambdaAuthorizer" + "Authorizer": "HttpApiAuthorize" }, "ApiId": { "Ref": "AnnotationsHttpApi" @@ -283,7 +283,7 @@ "Path": "/api/rest-user-info", "Method": "GET", "Auth": { - "Authorizer": "RestApiLambdaAuthorizer" + "Authorizer": "RestApiAuthorize" }, "RestApiId": { "Ref": "AnnotationsRestApi" @@ -328,7 +328,7 @@ "Method": "GET", "PayloadFormatVersion": "1.0", "Auth": { - "Authorizer": "HttpApiLambdaAuthorizerV1" + "Authorizer": "HttpApiAuthorizeV1" }, "ApiId": { "Ref": "AnnotationsHttpApi" @@ -371,7 +371,7 @@ "Path": "/api/ihttpresult-user-info", "Method": "GET", "Auth": { - "Authorizer": "HttpApiLambdaAuthorizer" + "Authorizer": "HttpApiAuthorize" }, "ApiId": { "Ref": "AnnotationsHttpApi" @@ -414,7 +414,7 @@ "Path": "/api/nonstring-user-info", "Method": "GET", "Auth": { - "Authorizer": "HttpApiLambdaAuthorizer" + "Authorizer": "HttpApiAuthorize" }, "ApiId": { "Ref": "AnnotationsHttpApi" diff --git a/Libraries/test/TestCustomAuthorizerApp/AuthorizerFunction.cs b/Libraries/test/TestCustomAuthorizerApp/AuthorizerFunction.cs index e1529a26e..27e369893 100644 --- a/Libraries/test/TestCustomAuthorizerApp/AuthorizerFunction.cs +++ b/Libraries/test/TestCustomAuthorizerApp/AuthorizerFunction.cs @@ -39,7 +39,7 @@ public class AuthorizerFunction /// Returns authorized status along with custom context that can be accessed via [FromCustomAuthorizer] /// [LambdaFunction(ResourceName = "CustomAuthorizer")] - [HttpApiAuthorizer("HttpApiLambdaAuthorizer", + [HttpApiAuthorizer( IdentityHeader = "authorization", EnableSimpleResponses = true, AuthorizerPayloadFormatVersion = AuthorizerPayloadFormatVersion.V2)] @@ -120,7 +120,7 @@ public APIGatewayCustomAuthorizerV2SimpleResponse HttpApiAuthorize( /// Used by HTTP API V1 endpoints that need payload format 1.0 /// [LambdaFunction(ResourceName = "CustomAuthorizerV1")] - [HttpApiAuthorizer("HttpApiLambdaAuthorizerV1", + [HttpApiAuthorizer( IdentityHeader = "authorization", EnableSimpleResponses = false, AuthorizerPayloadFormatVersion = AuthorizerPayloadFormatVersion.V1)] @@ -202,7 +202,7 @@ public APIGatewayCustomAuthorizerResponse HttpApiAuthorizeV1( /// Returns an IAM policy document along with custom context values /// [LambdaFunction(ResourceName = "RestApiAuthorizer")] - [RestApiAuthorizer("RestApiLambdaAuthorizer", + [RestApiAuthorizer( Type = RestApiAuthorizerType.Token, IdentityHeader = "Authorization")] public APIGatewayCustomAuthorizerResponse RestApiAuthorize( diff --git a/Libraries/test/TestCustomAuthorizerApp/ProtectedFunction.cs b/Libraries/test/TestCustomAuthorizerApp/ProtectedFunction.cs index 1f4b6d04f..0a68532f9 100644 --- a/Libraries/test/TestCustomAuthorizerApp/ProtectedFunction.cs +++ b/Libraries/test/TestCustomAuthorizerApp/ProtectedFunction.cs @@ -18,7 +18,7 @@ public class ProtectedFunction /// Debug endpoint to see what's in the RequestContext.Authorizer /// [LambdaFunction(ResourceName = "ProtectedEndpoint")] - [HttpApi(LambdaHttpMethod.Get, "/api/protected", Authorizer = "HttpApiLambdaAuthorizer")] + [HttpApi(LambdaHttpMethod.Get, "/api/protected", Authorizer = nameof(AuthorizerFunction.HttpApiAuthorize))] public string GetProtectedData( APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context) @@ -88,7 +88,7 @@ public string GetProtectedData( /// Another protected endpoint showing different usage - just getting the email. /// [LambdaFunction(ResourceName = "GetUserInfo")] - [HttpApi(LambdaHttpMethod.Get, "/api/user-info", Authorizer = "HttpApiLambdaAuthorizer")] + [HttpApi(LambdaHttpMethod.Get, "/api/user-info", Authorizer = nameof(AuthorizerFunction.HttpApiAuthorize))] public object GetUserInfo( [FromCustomAuthorizer(Name = "userId")] string userId, [FromCustomAuthorizer(Name = "email")] string email, @@ -123,7 +123,7 @@ public string HealthCheck(ILambdaContext context) /// REST API authorizers use a different context structure than HTTP API v2. /// [LambdaFunction(ResourceName = "RestUserInfo")] - [RestApi(LambdaHttpMethod.Get, "/api/rest-user-info", Authorizer = "RestApiLambdaAuthorizer")] + [RestApi(LambdaHttpMethod.Get, "/api/rest-user-info", Authorizer = nameof(AuthorizerFunction.RestApiAuthorize))] public object GetRestUserInfo( [FromCustomAuthorizer(Name = "userId")] string userId, [FromCustomAuthorizer(Name = "email")] string email, @@ -149,7 +149,7 @@ public object GetRestUserInfo( /// where RequestContext.Authorizer is a dictionary, not RequestContext.Authorizer.Lambda. /// [LambdaFunction(ResourceName = "HttpApiV1UserInfo")] - [HttpApi(LambdaHttpMethod.Get, "/api/http-v1-user-info", Version = HttpApiVersion.V1, Authorizer = "HttpApiLambdaAuthorizerV1")] + [HttpApi(LambdaHttpMethod.Get, "/api/http-v1-user-info", Version = HttpApiVersion.V1, Authorizer = nameof(AuthorizerFunction.HttpApiAuthorizeV1))] public object GetHttpApiV1UserInfo( [FromCustomAuthorizer(Name = "userId")] string userId, [FromCustomAuthorizer(Name = "email")] string email, @@ -175,7 +175,7 @@ public object GetHttpApiV1UserInfo( /// when authorizer context is missing (the handler returns Stream, not response object). /// [LambdaFunction(ResourceName = "IHttpResultUserInfo")] - [HttpApi(LambdaHttpMethod.Get, "/api/ihttpresult-user-info", Authorizer = "HttpApiLambdaAuthorizer")] + [HttpApi(LambdaHttpMethod.Get, "/api/ihttpresult-user-info", Authorizer = nameof(AuthorizerFunction.HttpApiAuthorize))] public IHttpResult GetIHttpResult( [FromCustomAuthorizer(Name = "userId")] string userId, [FromCustomAuthorizer(Name = "email")] string email, @@ -197,7 +197,7 @@ public IHttpResult GetIHttpResult( /// the Lambda authorizer context. /// [LambdaFunction(ResourceName = "NonStringUserInfo")] - [HttpApi(LambdaHttpMethod.Get, "/api/nonstring-user-info", Authorizer = "HttpApiLambdaAuthorizer")] + [HttpApi(LambdaHttpMethod.Get, "/api/nonstring-user-info", Authorizer = nameof(AuthorizerFunction.HttpApiAuthorize))] public object GetNonStringUserInfo( APIGatewayHttpApiV2ProxyRequest request, [FromCustomAuthorizer(Name = "numericTenantId")] int tenantId, diff --git a/Libraries/test/TestServerlessApp.NET8/serverless.template b/Libraries/test/TestServerlessApp.NET8/serverless.template index c42ff4a47..3cdbe66a9 100644 --- a/Libraries/test/TestServerlessApp.NET8/serverless.template +++ b/Libraries/test/TestServerlessApp.NET8/serverless.template @@ -2,23 +2,5 @@ "AWSTemplateFormatVersion": "2010-09-09", "Transform": "AWS::Serverless-2016-10-31", "Description": "This template is partially managed by Amazon.Lambda.Annotations (v1.9.0.0).", - "Resources": { - "TestServerlessAppNET8FunctionsToUpperGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "Runtime": "dotnet8", - "CodeUri": ".", - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Zip", - "Handler": "TestServerlessApp.NET8::TestServerlessApp.NET8.Functions_ToUpper_Generated::ToUpper" - } - } - } + "Resources": {} } \ No newline at end of file diff --git a/Libraries/test/TestServerlessApp/FromScratch/MissingLambdaFunctionWithAuthorizer.cs b/Libraries/test/TestServerlessApp/FromScratch/MissingLambdaFunctionWithAuthorizer.cs index 7e3350d2d..0833df387 100644 --- a/Libraries/test/TestServerlessApp/FromScratch/MissingLambdaFunctionWithAuthorizer.cs +++ b/Libraries/test/TestServerlessApp/FromScratch/MissingLambdaFunctionWithAuthorizer.cs @@ -7,7 +7,7 @@ namespace TestServerlessApp.FromScratch { public class MissingLambdaFunctionWithAuthorizer { - [HttpApiAuthorizer("MyAuthorizer", + [HttpApiAuthorizer( AuthorizerPayloadFormatVersion = AuthorizerPayloadFormatVersion.V2)] public APIGatewayCustomAuthorizerV2SimpleResponse Authorize( APIGatewayCustomAuthorizerV2Request request, From 1299bbccfa273b77b87df2851748054bad6af3f3 Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Mon, 9 Mar 2026 16:10:56 -0400 Subject: [PATCH 14/14] fix test --- .../serverless.template | 18 ++++++++--------- .../serverless.template | 20 ++++++++++++++++++- .../TestServerlessApp.csproj | 1 + .../aws-lambda-tools-defaults.json | 8 ++++---- 4 files changed, 33 insertions(+), 14 deletions(-) diff --git a/Libraries/test/TestCustomAuthorizerApp/serverless.template b/Libraries/test/TestCustomAuthorizerApp/serverless.template index e392a4a43..f1714a728 100644 --- a/Libraries/test/TestCustomAuthorizerApp/serverless.template +++ b/Libraries/test/TestCustomAuthorizerApp/serverless.template @@ -11,7 +11,7 @@ "Properties": { "Auth": { "Authorizers": { - "HttpApiLambdaAuthorizer": { + "HttpApiAuthorize": { "FunctionArn": { "Fn::GetAtt": [ "CustomAuthorizer", @@ -28,7 +28,7 @@ "EnableFunctionDefaultPermissions": true, "AuthorizerResultTtlInSeconds": 0 }, - "HttpApiLambdaAuthorizerV1": { + "HttpApiAuthorizeV1": { "FunctionArn": { "Fn::GetAtt": [ "CustomAuthorizerV1", @@ -55,7 +55,7 @@ "StageName": "Prod", "Auth": { "Authorizers": { - "RestApiLambdaAuthorizer": { + "RestApiAuthorize": { "FunctionArn": { "Fn::GetAtt": [ "RestApiAuthorizer", @@ -141,7 +141,7 @@ "Path": "/api/protected", "Method": "GET", "Auth": { - "Authorizer": "HttpApiLambdaAuthorizer" + "Authorizer": "HttpApiAuthorize" }, "ApiId": { "Ref": "AnnotationsHttpApi" @@ -184,7 +184,7 @@ "Path": "/api/user-info", "Method": "GET", "Auth": { - "Authorizer": "HttpApiLambdaAuthorizer" + "Authorizer": "HttpApiAuthorize" }, "ApiId": { "Ref": "AnnotationsHttpApi" @@ -266,7 +266,7 @@ "Path": "/api/rest-user-info", "Method": "GET", "Auth": { - "Authorizer": "RestApiLambdaAuthorizer" + "Authorizer": "RestApiAuthorize" }, "RestApiId": { "Ref": "AnnotationsRestApi" @@ -311,7 +311,7 @@ "Method": "GET", "PayloadFormatVersion": "1.0", "Auth": { - "Authorizer": "HttpApiLambdaAuthorizerV1" + "Authorizer": "HttpApiAuthorizeV1" }, "ApiId": { "Ref": "AnnotationsHttpApi" @@ -354,7 +354,7 @@ "Path": "/api/ihttpresult-user-info", "Method": "GET", "Auth": { - "Authorizer": "HttpApiLambdaAuthorizer" + "Authorizer": "HttpApiAuthorize" }, "ApiId": { "Ref": "AnnotationsHttpApi" @@ -397,7 +397,7 @@ "Path": "/api/nonstring-user-info", "Method": "GET", "Auth": { - "Authorizer": "HttpApiLambdaAuthorizer" + "Authorizer": "HttpApiAuthorize" }, "ApiId": { "Ref": "AnnotationsHttpApi" diff --git a/Libraries/test/TestServerlessApp.NET8/serverless.template b/Libraries/test/TestServerlessApp.NET8/serverless.template index 3cdbe66a9..c42ff4a47 100644 --- a/Libraries/test/TestServerlessApp.NET8/serverless.template +++ b/Libraries/test/TestServerlessApp.NET8/serverless.template @@ -2,5 +2,23 @@ "AWSTemplateFormatVersion": "2010-09-09", "Transform": "AWS::Serverless-2016-10-31", "Description": "This template is partially managed by Amazon.Lambda.Annotations (v1.9.0.0).", - "Resources": {} + "Resources": { + "TestServerlessAppNET8FunctionsToUpperGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "Runtime": "dotnet8", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestServerlessApp.NET8::TestServerlessApp.NET8.Functions_ToUpper_Generated::ToUpper" + } + } + } } \ No newline at end of file diff --git a/Libraries/test/TestServerlessApp/TestServerlessApp.csproj b/Libraries/test/TestServerlessApp/TestServerlessApp.csproj index a790ac481..921e3d372 100644 --- a/Libraries/test/TestServerlessApp/TestServerlessApp.csproj +++ b/Libraries/test/TestServerlessApp/TestServerlessApp.csproj @@ -8,6 +8,7 @@ + diff --git a/Libraries/test/TestServerlessApp/aws-lambda-tools-defaults.json b/Libraries/test/TestServerlessApp/aws-lambda-tools-defaults.json index 0b96350ff..86ffa5624 100644 --- a/Libraries/test/TestServerlessApp/aws-lambda-tools-defaults.json +++ b/Libraries/test/TestServerlessApp/aws-lambda-tools-defaults.json @@ -13,7 +13,7 @@ "template": "serverless.template", "template-parameters": "", "docker-host-build-output-dir": "./bin/Release/lambda-publish", - "s3-bucket": "test-serverless-app", - "stack-name": "test-serverless-app", - "function-architecture": "x86_64" -} \ No newline at end of file +"s3-bucket" : "test-serverless-app-2c68d818", +"stack-name" : "test-serverless-app-2c68d818", +"function-architecture" : "x86_64" +}