diff --git a/.agents/skills/new-event-source/SKILL.md b/.agents/skills/new-event-source/SKILL.md new file mode 100644 index 000000000..697aa7b3e --- /dev/null +++ b/.agents/skills/new-event-source/SKILL.md @@ -0,0 +1,195 @@ +--- +name: new-event-source +description: Add a new AWS event source attribute (e.g., Kinesis, Kafka, MQ) to the Lambda .NET Annotations framework, including the attribute class, source generator integration, CloudFormation writer, unit tests, writer tests, source generator tests, and integration tests +--- + +# Adding a New Event Source to Lambda Annotations + +This skill guides you through adding a complete new event source attribute to the AWS Lambda .NET Annotations framework. Use this when a user asks to add support for a new AWS event source like Kinesis, Kafka, MQ, etc. + +## Prerequisites + +Before starting, gather from the user: +1. **Service name** (e.g., "Kinesis", "Kafka", "MQ") +2. **Primary resource identifier** (e.g., stream ARN, topic ARN, broker ARN) +3. **CloudFormation event type string** (e.g., "Kinesis", "MSK", "MQ") +4. **Event class name** from the corresponding `Amazon.Lambda.*Events` NuGet package (e.g., `KinesisEvent`) +5. **Optional properties** the attribute should support (e.g., BatchSize, StartingPosition, Filters) +6. **Whether `@` references use `Fn::GetAtt` or `Ref`** — event source mappings use `Fn::GetAtt`, subscriptions use `Ref` + +## Reference Examples + +Read these files to understand existing patterns before creating new ones: +- **SNS (simplest, subscription-based)**: `Libraries/src/Amazon.Lambda.Annotations/SNS/SNSEventAttribute.cs` +- **SQS (event source mapping with batching)**: `Libraries/src/Amazon.Lambda.Annotations/SQS/SQSEventAttribute.cs` +- **DynamoDB (stream-based)**: `Libraries/src/Amazon.Lambda.Annotations/DynamoDB/DynamoDBEventAttribute.cs` +- **S3 (notification-based)**: `Libraries/src/Amazon.Lambda.Annotations/S3/S3EventAttribute.cs` + +## Steps + +### Step 1: Create the Event Attribute Class + +**Create**: `Libraries/src/Amazon.Lambda.Annotations/{ServiceName}/{ServiceName}EventAttribute.cs` + +Key patterns: +- Add copyright header: `// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.` + `// SPDX-License-Identifier: Apache-2.0` +- Inherit from `Attribute` with `[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]` (use `AllowMultiple = false` for event types where multiple triggers on the same function don't make sense, e.g., Schedule events) +- Constructor takes the primary resource identifier as a required `string` parameter +- All optional properties use nullable backing fields with `IsSet` internal properties +- Include auto-derived `ResourceName` property (strips `@` prefix or extracts name from ARN) +- Include `internal List Validate()` method with all validation rules +- Use `Regex("^[a-zA-Z0-9]+$")` for ResourceName validation + +### Step 2: Register Type Full Names + +**Modify**: `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs` + +Add constants: +```csharp +public const string {ServiceName}EventAttribute = "Amazon.Lambda.Annotations.{ServiceName}.{ServiceName}EventAttribute"; +public const string {ServiceName}Event = "Amazon.Lambda.{ServiceName}Events.{ServiceName}Event"; +``` + +Also add to `EventType` enum if needed in `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/EventType.cs`. + +### Step 3: Create the Attribute Builder + +**Create**: `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/{ServiceName}EventAttributeBuilder.cs` + +Extracts attribute data from Roslyn `AttributeData`. Use consistent `else if` chaining. Reference: `SNSEventAttributeBuilder.cs`. + +### Step 4: Register in AttributeModelBuilder + +**Modify**: `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/AttributeModelBuilder.cs` + +Add `else if` block for the new attribute type after the existing event attribute blocks. + +### Step 5: Register in EventTypeBuilder + +**Modify**: `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/EventTypeBuilder.cs` + +Add `else if` block mapping the attribute to the `EventType` enum value. + +### Step 6: Add DiagnosticDescriptor + +**Modify**: `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs` + +Add descriptor with the next available `AWSLambda0XXX` ID for invalid attribute validation errors. + +**Modify**: `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/AnalyzerReleases.Unshipped.md` — add the new diagnostic ID. + +### Step 7: Add Validation in LambdaFunctionValidator + +**Modify**: `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Validation/LambdaFunctionValidator.cs` + +1. Add `Validate{ServiceName}Events()` call in `ValidateFunction` method +2. Create private `Validate{ServiceName}Events()` method that validates: + - Attribute properties via `Validate()` method + - Method parameters (first must be event type, optional second is `ILambdaContext`) + - Return type (usually `void` or `Task`) + +### Step 8: Add Dependency Check + +**Modify**: `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Validation/LambdaFunctionValidator.cs` + +In `ValidateDependencies`, add check for `Amazon.Lambda.{ServiceName}Events` NuGet package. + +### Step 9: Check SyntaxReceiver + +**Check**: `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/SyntaxReceiver.cs` + +Add the new attribute name if the SyntaxReceiver filters by attribute name strings. + +### Step 10: Add CloudFormation Writer Logic + +**Modify**: `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs` + +1. Add `case AttributeModel<{ServiceName}EventAttribute>` in the event processing switch +2. Create `Process{ServiceName}Attribute()` method that writes CF template properties + - Event source mappings (SQS, DynamoDB, Kinesis): use `Fn::GetAtt` for `@` references + - Subscription events (SNS): use `Ref` for `@` references + - Track synced properties in metadata + +### Step 11: Create Attribute Unit Tests + +**Create**: `Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/{ServiceName}EventAttributeTests.cs` + +Cover: constructor, defaults, property tracking, ResourceName derivation, all validation paths. Reference: `SQSEventAttributeTests.cs`, `DynamoDBEventAttributeTests.cs`, `SNSEventAttributeTests.cs`. + +### Step 12: Create CloudFormation Writer Tests + +**Create**: `Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/{ServiceName}EventsTests.cs` + +This is a `partial class CloudFormationWriterTests`. Include tests for: +1. `Verify{ServiceName}EventAttributes_AreCorrectlyApplied` — Theory with JSON/YAML and property combinations +2. `Verify{ServiceName}EventProperties_AreSyncedCorrectly` — Synced properties update when attributes change +3. `SwitchBetweenArnAndRef_For{Resource}` — ARN to `@` reference switching +4. `Verify{Resource}CanBeSet_FromCloudFormationParameter` — CF Parameters handling +5. `VerifyManuallySet{ServiceName}EventProperties_ArePreserved` — Hand-edited template preservation + +Reference: `SQSEventsTests.cs`, `DynamoDBEventsTests.cs`, `SNSEventsTests.cs`. + +### Step 13: Create Valid Event Examples + Source Generator Test + +**Create**: `Libraries/test/TestServerlessApp/{ServiceName}EventExamples/Valid{ServiceName}Events.cs.txt` +**Create**: `Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/{ServiceName}/` (generated handler snapshots) +**Create**: `Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/{serviceName}Events.template` +**Modify**: `Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs` — add `VerifyValid{ServiceName}Events()` test + +### Step 14: Create Invalid Event Examples + Source Generator Test + +**Create**: `Libraries/test/TestServerlessApp/{ServiceName}EventExamples/Invalid{ServiceName}Events.cs.error` + +Cover: invalid property values, invalid params, invalid return type, multiple events, invalid ARN, invalid resource name. + +**Modify**: `Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs` — add `VerifyInvalid{ServiceName}Events_ThrowsCompilationErrors()` test with diagnostic assertions including line spans. + +### Step 15: Create Generated Code Snapshots + +**Create**: `Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/{ServiceName}/` + +Tip: Run the source generator once to get actual output, then use as snapshot. + +### Step 16: Create Integration Test + +**Create**: `Libraries/test/TestServerlessApp.IntegrationTests/{ServiceName}EventSourceMapping.cs` +**Modify**: `Libraries/test/TestServerlessApp.IntegrationTests/IntegrationTestContextFixture.cs` — resource lookup +**Modify**: `Libraries/test/TestServerlessApp.IntegrationTests/DeploymentScript.ps1` — if needed + +### Step 17: Update AnalyzerReleases.Unshipped.md + +**Modify**: `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/AnalyzerReleases.Unshipped.md` + +## File Map Summary + +| Action | File Path | +|--------|-----------| +| Create | `src/Amazon.Lambda.Annotations/{ServiceName}/{ServiceName}EventAttribute.cs` | +| Modify | `src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs` | +| Create | `src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/{ServiceName}EventAttributeBuilder.cs` | +| Modify | `src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/AttributeModelBuilder.cs` | +| Modify | `src/Amazon.Lambda.Annotations.SourceGenerator/Models/EventTypeBuilder.cs` | +| Modify | `src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs` | +| Modify | `src/Amazon.Lambda.Annotations.SourceGenerator/Validation/LambdaFunctionValidator.cs` | +| Modify | `src/Amazon.Lambda.Annotations.SourceGenerator/SyntaxReceiver.cs` | +| Modify | `src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs` | +| Modify | `src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/AnalyzerReleases.Unshipped.md` | +| Create | `test/Amazon.Lambda.Annotations.SourceGenerators.Tests/{ServiceName}EventAttributeTests.cs` | +| Create | `test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/{ServiceName}EventsTests.cs` | +| Create | `test/TestServerlessApp/{ServiceName}EventExamples/Valid{ServiceName}Events.cs.txt` | +| Create | `test/TestServerlessApp/{ServiceName}EventExamples/Invalid{ServiceName}Events.cs.error` | +| Create | `test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/{ServiceName}/` | +| Create | `test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/{serviceName}Events.template` | +| Modify | `test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs` | +| Create | `test/TestServerlessApp.IntegrationTests/{ServiceName}EventSourceMapping.cs` | +| Modify | `test/TestServerlessApp.IntegrationTests/IntegrationTestContextFixture.cs` | + +## Important Conventions + +- **Copyright header** on every new `.cs` file +- **Consistent `else if` chaining** in attribute builders (never `if` then `if` for the same loop) +- **Both JSON and YAML** template formats must be tested in writer tests +- **Invalid event test spans** must reference exact line numbers in the `.cs.error` file +- **`.cs.txt` extension** for valid test files (prevents deployment) +- **`.cs.error` extension** for invalid test files (prevents compilation) +- **Use enums instead of strings** when you need to represent a fixed, known set of constants that do not change frequently (e.g., `StartingPosition`, `AuthType`, `HttpApiVersion`). Enums provide compile-time type safety, eliminate the need for manual string validation in the `Validate()` method, and prevent invalid values from being set. In attribute builders, enum values come through Roslyn's `AttributeData` as their underlying `int` value and must be cast accordingly (e.g., `(MyEnum)(int)pair.Value.Value`). When writing enum values to CloudFormation templates, use `.ToString()` to convert back to the string representation. diff --git a/.autover/changes/9cf844c3-1a90-46ec-9430-93724dcb4512.json b/.autover/changes/9cf844c3-1a90-46ec-9430-93724dcb4512.json deleted file mode 100644 index 880709e72..000000000 --- a/.autover/changes/9cf844c3-1a90-46ec-9430-93724dcb4512.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "Projects": [ - { - "Name": "Amazon.Lambda.TestTool.BlazorTester", - "Type": "Patch", - "ChangelogMessages": [ - "Minor fixes to improve the testability of the package" - ] - } - ] -} \ No newline at end of file diff --git a/.autover/changes/a119819e-ba57-424d-a0be-d941eea599f9.json b/.autover/changes/a119819e-ba57-424d-a0be-d941eea599f9.json deleted file mode 100644 index 7b2746bbb..000000000 --- a/.autover/changes/a119819e-ba57-424d-a0be-d941eea599f9.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "Projects": [ - { - "Name": "Amazon.Lambda.RuntimeSupport", - "Type": "Patch", - "ChangelogMessages": [ - "Minor fixes to improve the testability of the package" - ] - } - ] -} \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..7903494e9 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @aws/aws-sdk-devex-reviewers @aws/aws-sdk-dotnet-team diff --git a/.github/workflows/auto-update-Dockerfiles.yml b/.github/workflows/auto-update-Dockerfiles.yml index 6814af252..d2fb2b9d6 100644 --- a/.github/workflows/auto-update-Dockerfiles.yml +++ b/.github/workflows/auto-update-Dockerfiles.yml @@ -11,8 +11,6 @@ on: # Allows to run this workflow manually from the Actions tab for testing workflow_dispatch: - - jobs: auto-update: runs-on: ubuntu-latest @@ -28,7 +26,7 @@ jobs: steps: # Checks-out the repository under $GITHUB_WORKSPACE - - uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2 #v4.2.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: 'dev' @@ -149,34 +147,49 @@ jobs: id: commit-push shell: pwsh run: | + git config --global user.email "github-aws-sdk-dotnet-automation@amazon.com" + git config --global user.name "aws-sdk-dotnet-automation" + + $remoteBranch = "chore/auto-update-Dockerfiles-daily" + + # Try to fetch the remote branch (it may not exist yet) + git fetch origin $remoteBranch 2>$null + $remoteExists = ($LASTEXITCODE -eq 0) + # Check if there are any changes to commit if (git status --porcelain) { - git config --global user.email "github-aws-sdk-dotnet-automation@amazon.com" - git config --global user.name "aws-sdk-dotnet-automation" - + # Generate timestamp for unique local branch name $timestamp = Get-Date -Format "yyyyMMddHHmmss" $localBranch = "chore/auto-update-Dockerfiles-daily-$timestamp" - $remoteBranch = "chore/auto-update-Dockerfiles-daily" - + # Always create a new unique local branch git checkout -b $localBranch - + git add "**/*Dockerfile" git commit -m "chore: Daily ASP.NET Core version update in Dockerfiles" - - # Always delete the remote branch before pushing to avoid stale branch errors + + # If remote branch exists and there is no diff vs remote branch, skip pushing and PR creation + if ($remoteExists) { + git diff --quiet "origin/$remoteBranch...HEAD" + if ($LASTEXITCODE -eq 0) { + echo "No diff vs origin/$remoteBranch. Skipping push/PR creation." + Add-Content -Path $env:GITHUB_OUTPUT -Value "CHANGES_MADE=false" + exit 0 + } + } + + # Only now delete + push (because we know it's different) git push origin --delete $remoteBranch 2>$null - - # Push local branch to remote branch (force push to consistent remote branch name) git push --force origin "${localBranch}:${remoteBranch}" - + # Write the remote branch name to GITHUB_OUTPUT for use in the PR step Add-Content -Path $env:GITHUB_OUTPUT -Value "BRANCH=$remoteBranch" Add-Content -Path $env:GITHUB_OUTPUT -Value "CHANGES_MADE=true" echo "Changes committed to local branch $localBranch and pushed to remote branch $remoteBranch" } else { echo "No changes detected in Dockerfiles, skipping PR creation" + Add-Content -Path $env:GITHUB_OUTPUT -Value "CHANGES_MADE=false" } # Create a Pull Request diff --git a/.github/workflows/aws-ci.yml b/.github/workflows/aws-ci.yml index 84ff355c1..dc8491927 100644 --- a/.github/workflows/aws-ci.yml +++ b/.github/workflows/aws-ci.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Configure Load Balancer Credentials - uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 #v4 + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0 with: role-to-assume: ${{ secrets.CI_MAIN_TESTING_ACCOUNT_ROLE_ARN }} role-duration-seconds: 7200 @@ -29,7 +29,7 @@ jobs: $roleArn=$(cat ./response.json) "roleArn=$($roleArn -replace '"', '')" >> $env:GITHUB_OUTPUT - name: Configure Test Runner Credentials - uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 #v4 + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0 with: role-to-assume: ${{ steps.lambda.outputs.roleArn }} role-duration-seconds: 7200 @@ -41,7 +41,7 @@ jobs: project-name: ${{ secrets.CI_TESTING_CODE_BUILD_PROJECT_NAME }} - name: Configure Test Sweeper Lambda Credentials if: always() - uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 #v4 + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0 with: role-to-assume: ${{ steps.lambda.outputs.roleArn }} role-duration-seconds: 7200 diff --git a/.github/workflows/change-file-in-pr.yml b/.github/workflows/change-file-in-pr.yml index adbf3cbc5..51a2fb001 100644 --- a/.github/workflows/change-file-in-pr.yml +++ b/.github/workflows/change-file-in-pr.yml @@ -12,11 +12,11 @@ jobs: steps: - name: Checkout PR code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Get List of Changed Files id: changed-files - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 #v45 + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 - name: Check for Change File(s) in .autover/changes/ run: | diff --git a/.github/workflows/create-release-pr.yml b/.github/workflows/create-release-pr.yml index d0a901eb2..02c7fb641 100644 --- a/.github/workflows/create-release-pr.yml +++ b/.github/workflows/create-release-pr.yml @@ -13,6 +13,7 @@ on: permissions: id-token: write + repository-projects: read jobs: release-pr: @@ -25,31 +26,31 @@ jobs: steps: # Assume an AWS Role that provides access to the Access Token - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 #v6.0.0 + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0 with: role-to-assume: ${{ secrets.RELEASE_WORKFLOW_ACCESS_TOKEN_ROLE_ARN }} aws-region: us-west-2 - # Retrieve the Access Token from Secrets Manager - - name: Retrieve secret from AWS Secrets Manager + # Retrieve the per-repo deploy key + FG PAT from Secrets Manager + - name: Retrieve secrets from AWS Secrets Manager uses: aws-actions/aws-secretsmanager-get-secrets@3a411b6ec5cace3d626412dd917e7bfeac242cfa #v3.0.0 with: secret-ids: | - AWS_SECRET, ${{ secrets.RELEASE_WORKFLOW_ACCESS_TOKEN_NAME }} - parse-json-secrets: true - # Checkout a full clone of the repo + DEPLOY_KEY, prod/devops/aws-lambda-dotnet-deploy-key + FG_PAT, prod/devops/aws-lambda-dotnet-fg-pat + # Checkout a full clone of the repo using the deploy key (push runs over SSH) - name: Checkout - uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2 #v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: "0" - token: ${{ env.AWS_SECRET_TOKEN }} + ssh-key: ${{ env.DEPLOY_KEY }} # Install .NET9 which is needed for AutoVer - name: Setup .NET 9.0 - uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 #v5.2.0 + uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0 with: dotnet-version: 9.0.x # Install .NET10 which is needed for building the solution - name: Setup .NET 10.0 - uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 #v5.2.0 + uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0 with: dotnet-version: 10.0.x # Install .NET11 which is needed for building the solution @@ -115,9 +116,8 @@ jobs: # Create the Release PR and label it - name: Create Pull Request env: - GITHUB_TOKEN: ${{ env.AWS_SECRET_TOKEN }} + GITHUB_TOKEN: ${{ env.FG_PAT }} run: | - pr_url="$(gh pr create --title "${{ steps.read-release-name.outputs.VERSION }}" --body "${{ steps.read-changelog.outputs.CHANGELOG }}" --base dev --head ${{ steps.create-release-branch.outputs.BRANCH }})" gh label create "Release PR" --description "A Release PR that includes versioning and changelog changes" -c "#FF0000" -f - gh pr edit $pr_url --add-label "Release PR" + pr_url="$(gh pr create --title "${{ steps.read-release-name.outputs.VERSION }}" --label "Release PR" --body "${{ steps.read-changelog.outputs.CHANGELOG }}" --base dev --head ${{ steps.create-release-branch.outputs.BRANCH }})" diff --git a/.github/workflows/semgrep-analysis.yml b/.github/workflows/semgrep-analysis.yml index 1c6f0e013..da6e998de 100644 --- a/.github/workflows/semgrep-analysis.yml +++ b/.github/workflows/semgrep-analysis.yml @@ -25,7 +25,7 @@ jobs: if: (github.actor != 'dependabot[bot]') steps: # Fetch project source - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - run: semgrep ci --sarif > semgrep.sarif env: @@ -35,7 +35,7 @@ jobs: p/owasp-top-ten - name: Upload SARIF file for GitHub Advanced Security Dashboard - uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 #v4.35.1 + uses: github/codeql-action/upload-sarif@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 with: sarif_file: semgrep.sarif if: always() diff --git a/.github/workflows/stale_issues.yml b/.github/workflows/stale_issues.yml index 2eb129c00..90932eea5 100644 --- a/.github/workflows/stale_issues.yml +++ b/.github/workflows/stale_issues.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest name: Stale issue job steps: - - uses: aws-actions/stale-issue-cleanup@v6 + - uses: aws-actions/stale-issue-cleanup@v7 with: # Setting messages to an empty string will cause the automation to skip # that category diff --git a/.github/workflows/sync-master-dev.yml b/.github/workflows/sync-master-dev.yml index f910f51f0..ae1f6e923 100644 --- a/.github/workflows/sync-master-dev.yml +++ b/.github/workflows/sync-master-dev.yml @@ -26,32 +26,32 @@ jobs: steps: # Assume an AWS Role that provides access to the Access Token - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 #v6.0.0 + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0 with: role-to-assume: ${{ secrets.RELEASE_WORKFLOW_ACCESS_TOKEN_ROLE_ARN }} aws-region: us-west-2 - # Retrieve the Access Token from Secrets Manager - - name: Retrieve secret from AWS Secrets Manager + # Retrieve the per-repo deploy key + FG PAT from Secrets Manager + - name: Retrieve secrets from AWS Secrets Manager uses: aws-actions/aws-secretsmanager-get-secrets@3a411b6ec5cace3d626412dd917e7bfeac242cfa #v3.0.0 with: secret-ids: | - AWS_SECRET, ${{ secrets.RELEASE_WORKFLOW_ACCESS_TOKEN_NAME }} - parse-json-secrets: true - # Checkout a full clone of the repo + DEPLOY_KEY, prod/devops/aws-lambda-dotnet-deploy-key + FG_PAT, prod/devops/aws-lambda-dotnet-fg-pat + # Checkout a full clone of the repo using the deploy key (push runs over SSH) - name: Checkout code - uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2 #v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: dev fetch-depth: 0 - token: ${{ env.AWS_SECRET_TOKEN }} + ssh-key: ${{ env.DEPLOY_KEY }} # Install .NET9 which is needed for AutoVer - name: Setup .NET 9.0 - uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 #v5.2.0 + uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0 with: dotnet-version: 9.0.x # Install .NET10 which is needed for building the solution - name: Setup .NET 10.0 - uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 #v5.2.0 + uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0 with: dotnet-version: 10.0.x # Install .NET11 which is needed for building the solution @@ -95,7 +95,7 @@ jobs: # Create the GitHub Release - name: Create GitHub Release env: - GITHUB_TOKEN: ${{ env.AWS_SECRET_TOKEN }} + GITHUB_TOKEN: ${{ env.FG_PAT }} run: | gh release create "${{ steps.read-tag-name.outputs.TAG }}" --title "${{ steps.read-release-name.outputs.VERSION }}" --notes "${{ steps.read-changelog.outputs.CHANGELOG }}" # Delete the `releases/next-release` branch @@ -119,20 +119,20 @@ jobs: github.event.pull_request.base.ref == 'dev' runs-on: ubuntu-latest steps: - # Checkout a full clone of the repo + # Checkout a full clone of the repo using the deploy key (push runs over SSH) - name: Checkout code - uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2 #v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: releases/next-release fetch-depth: 0 # Install .NET9 which is needed for AutoVer - name: Setup .NET 9.0 - uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 #v5.2.0 + uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0 with: dotnet-version: 9.0.x # Install .NET10 which is needed for building the solution - name: Setup .NET 10.0 - uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 #v5.2.0 + uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0 with: dotnet-version: 10.0.x # Install .NET11 which is needed for building the solution diff --git a/.github/workflows/update-Dockerfiles.yml b/.github/workflows/update-Dockerfiles.yml index cfa754f4f..7709115fd 100644 --- a/.github/workflows/update-Dockerfiles.yml +++ b/.github/workflows/update-Dockerfiles.yml @@ -81,7 +81,7 @@ jobs: # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2 #v4.2.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: 'dev' diff --git a/.gitignore b/.gitignore index f91715274..1caae6fe4 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ *.suo *.user +**/.kiro/ + #################### # Build/Test folders #################### diff --git a/Blueprints/BlueprintDefinitions/vs2026/Templates.csproj b/Blueprints/BlueprintDefinitions/vs2026/Templates.csproj index b59ca111d..88c84b633 100644 --- a/Blueprints/BlueprintDefinitions/vs2026/Templates.csproj +++ b/Blueprints/BlueprintDefinitions/vs2026/Templates.csproj @@ -2,7 +2,7 @@ Template - 8.0.2 + 8.0.3 Amazon.Lambda.Templates AWS Lambda Templates Amazon Web Services diff --git a/CHANGELOG.md b/CHANGELOG.md index b541fbd38..16faec739 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,224 @@ +## Release 2026-05-30 + +### AWSLambdaPSCore PowerShell Module (5.0.2) +* Reduce Lambda cold start INIT times by stripping files that are not used at runtime (PowerShell help XML and .pdb debug symbols) from AWS-authored modules (AWSPowerShell.NetCore and AWS.Tools.*) during packaging + +## Release 2026-05-28 + +### Amazon.Lambda.RuntimeSupport (2.1.1) +* Fix thread pool starvation under multi-concurrency + +## Release 2026-05-18 + +### Amazon.Lambda.Core (3.1.0) +* [Preview] Add LambdaLogger.ConfigureStructuredLogging to customize the JsonSerializerOptions used for serializing logging parameters. +* Add preview ILambdaSerializer Serializer property to ILambdaContext (default-implemented to null on net8.0+) so user code can access the serializer registered with the runtime. Marked [Experimental("AWSLAMBDA001")]; class-library mode requires an updated managed Lambda runtime to populate this property. The Experimental flag will be removed in a follow-up release once the managed runtime is deployed. +### Amazon.Lambda.RuntimeSupport (2.1.0) +* Add support for handling the structured logging customization from Amazon.Lambda.Core. +* Propagate the registered ILambdaSerializer to the per-invocation ILambdaContext.Serializer. Surfaces the new preview ILambdaContext.Serializer (AWSLAMBDA001); the Experimental flag will be removed in a follow-up release once the managed runtime is deployed. +### Amazon.Lambda.Annotations (2.0.1) +* Fix CS0121 ambiguity error in generated Program.g.cs when a Lambda handler has no input parameters and returns Task. The source generator now uses the unambiguous LambdaBootstrapBuilder.Create(Func) overload for this case. +### Amazon.Lambda.TestUtilities (4.1.0) +* Add Serializer setter to TestLambdaContext to mirror the new preview ILambdaContext.Serializer property. Marked [Experimental("AWSLAMBDA001")]; the Experimental flag will be removed in a follow-up release once the managed runtime is deployed. +### Amazon.Lambda.AspNetCoreServer (10.1.1) +* Fix InvokeFeatures.Set to bump the feature collection revision so middleware that wraps the response body (e.g. OutputCache, ResponseCompression) is properly visible to ASP.NET Core's FeatureReferences cache. Resolves https://github.com/aws/aws-lambda-dotnet/issues/1702 where IOutputCache stored empty response bodies. + +## Release 2026-05-15 + +### Amazon.Lambda.AspNetCoreServer (10.1.0) +* Add APIGatewayWebsocketApiProxyFunction (and TStartup variant) so API Gateway WebSocket APIs can be hosted via LambdaServer (DI, controllers). WebSocket events are dispatched as POST requests whose path is the RouteKey, allowing controller actions like [HttpPost("$default")] to handle them. +* Expose ParseHttpPath, ParseHttpMethod, and AddMissingRequestHeaders as protected virtual hooks on APIGatewayProxyFunction so subclasses can customize how the API Gateway request is mapped onto the ASP.NET Core request feature. +### Amazon.Lambda.AspNetCoreServer.Hosting (2.1.0) +* Add LambdaEventSource.WebsocketApi so AddAWSLambdaHosting can wire ASP.NET Core minimal APIs and controllers up to API Gateway WebSocket events. + +## Release 2026-05-15 + +### AWSLambdaPSCore PowerShell Module (5.0.1) +* Fix dotnet10 runtime failures by updating template package references to versions with net10.0 assets. Bumped Amazon.Lambda.PowerShellHost from 3.0.3 to 4.0.0, Amazon.Lambda.Core from 2.8.1 to 3.0.0 (required transitively by PowerShellHost 4.0.0), and the default Microsoft.PowerShell.SDK from 7.5.4 to 7.6.0. + +## Release 2026-05-13 + +### Amazon.Lambda.TestTool (0.14.1) +* Include AWSSDK.SecurityToken to resolve AWS credential profiles +* Fixed static assets (CSS, JS, BlazorMonaco) failing to load when ASPNETCORE_ENVIRONMENT or DOTNET_ENVIRONMENT is set to a non-Production value by always serving static files from the tool's install directory. + +## Release 2026-05-12 + +### Amazon.Lambda.TestTool (0.14.0) +* Add support emulating Lambda DynamoDB Stream event source + +## Release 2026-05-06 + +### Amazon.Lambda.RuntimeSupport (2.0.0) +* Remove .NET Standard 2.0, .NET Core 3.1 and .NET 6 build targets +* (Preview) Add response streaming support +### Amazon.Lambda.Annotations (2.0.0) +* Update Build targets from .NET Standard 2.0, .NET 6 and .NET 8 to .NET Standard 2.0, .NET 8 and .NET 10 +### Amazon.Lambda.APIGatewayEvents (3.0.0) +* Update Build targets from .NET Standard 2.0, .NET Core 3.1 and .NET 8 to .NET 8 and .NET 10 +### Amazon.Lambda.ApplicationLoadBalancerEvents (3.0.0) +* Update Build targets from .NET Standard 2.0, .NET Core 3.1 and .NET 8 to .NET 8 and .NET 10 +### Amazon.Lambda.AppSyncEvents (2.0.0) +* Update Build targets from .NET 8 to .NET 8 and .NET 10 +### Amazon.Lambda.AspNetCoreServer.Hosting (2.0.0) +* Update Build targets from .NET 6 and .NET 8 to .NET 8 and .NET 10 +* [Breaking] Update build targets from .NET 6 and 8 to .NET 8 and 10 +* [Preview] Add support for Lambda Response Streaming enabled by setting the EnableResponseStreaming property on the HostingOptions object passed into the AddAWSLambdaHosting method +### Amazon.Lambda.AspNetCoreServer (10.0.0) +* Update Build targets from .NET 6 and .NET 8 to .NET 8 and .NET 10 +* [Breaking] Update build targets from .NET 6 and 8 to .NET 8 and 10 +* [Preview] Add support for Lambda Response Streaming enabled by setting the EnableResponseStreaming property from the base class AbstractAspNetCoreFunction +### Amazon.Lambda.CloudWatchEvents (5.0.0) +* Update Build targets from .NET Standard 2.0, .NET Core 3.1 and .NET 8 to .NET 8 and .NET 10 +### Amazon.Lambda.CloudWatchLogsEvents (3.0.0) +* Update Build targets from .NET Standard 2.0, .NET Core 3.1 and .NET 8 to .NET 8 and .NET 10 +### Amazon.Lambda.CognitoEvents (5.0.0) +* Update Build targets from .NET Standard 2.0, .NET Core 3.1 and .NET 8 to .NET 8 and .NET 10 +### Amazon.Lambda.ConfigEvents (3.0.0) +* Update Build targets from .NET Standard 2.0 and .NET 8 to .NET 8 and .NET 10 +### Amazon.Lambda.ConnectEvents (2.0.0) +* Update Build targets from .NET Standard 2.0 and .NET 8 to .NET 8 and .NET 10 +### Amazon.Lambda.Core (3.0.0) +* Update Build targets from .NET Standard 2.0, .NET 6 and .NET 8 to .NET Standard 2.0, .NET 8 and .NET 10 +* (Preview) Add response streaming support +### Amazon.Lambda.DynamoDBEvents.SDK.Convertor (3.0.0) +* Update Build targets from .NET 8 to .NET 8 and .NET 10 +### Amazon.Lambda.DynamoDBEvents (4.0.0) +* Update Build targets from .NET Core 3.1 and .NET 8 to .NET 8 and .NET 10 +### Amazon.Lambda.KafkaEvents (3.0.0) +* Update Build targets from .NET Standard 2.0 and .NET 8 to .NET Standard 2.0, .NET 8 and .NET 10 +### Amazon.Lambda.KinesisAnalyticsEvents (3.0.0) +* Update Build targets from .NET Standard 2.0, .NET Core 3.1 and .NET 8 to .NET 8 and .NET 10 +### Amazon.Lambda.KinesisEvents (4.0.0) +* Update Build targets from .NET Standard 2.0, .NET Core 3.1 and .NET 8 to .NET 8 and .NET 10 +### Amazon.Lambda.KinesisFirehoseEvents (3.0.0) +* Update Build targets from .NET Standard 2.0, .NET Core 3.1 and .NET 8 to .NET 8 and .NET 10 +### Amazon.Lambda.LexEvents (4.0.0) +* Update Build targets from .NET Standard 2.0, .NET Core 3.1 and .NET 8 to .NET 8 and .NET 10 +### Amazon.Lambda.LexV2Events (2.0.0) +* Update Build targets from .NET Core 3.1 and .NET 8 to .NET 8 and .NET 10 +### Amazon.Lambda.Logging.AspNetCore (5.0.0) +* Update Build targets from .NET 6 and .NET 8 to .NET 8 and .NET 10 +* [Breaking] Update build targets from .NET 6 and 8 to .NET 8 and 10 +### Amazon.Lambda.MQEvents (3.0.0) +* Update Build targets from .NET Standard 2.0, .NET Core 3.1 and .NET 8 to .NET 8 and .NET 10 +### Amazon.Lambda.PowerShellHost (4.0.0) +* Update Build targets from .NET 6 and .NET 8 to .NET 8 and .NET 10 +* Update Microsoft.PowerShell.SDK package dependency to version 7.4.14 +* Update System.Security.Cryptography.Xml package dependency to version 8.0.3 +### Amazon.Lambda.S3Events (4.0.0) +* Update Build targets from .NET Core 3.1 and .NET 8 to .NET 8 and .NET 10 +### Amazon.Lambda.Serialization.Json (3.0.0) +* Update Build targets from .NET Standard 2.0 to .NET 8 and .NET 10 +### Amazon.Lambda.Serialization.SystemTextJson (3.0.0) +* Update Build targets from .NET Core 3.1, .NET 6 and .NET 8 to .NET 8 and .NET 10 +### Amazon.Lambda.SimpleEmailEvents (4.0.0) +* Update Build targets from .NET Standard 2.0 and .NET 8 to .NET 8 and .NET 10 +### Amazon.Lambda.SNSEvents (3.0.0) +* Update Build targets from .NET Standard 2.0 and .NET 8 to .NET 8 and .NET 10 +### Amazon.Lambda.SQSEvents (3.0.0) +* Update Build targets from .NET Standard 2.0, .NET Core 3.1 and .NET 8 to .NET 8 and .NET 10 +### Amazon.Lambda.TestUtilities (4.0.0) +* Update Build targets from .NET 6 and .NET 8 to .NET 8 and .NET 10 + +## Release 2026-04-29 + +### Amazon.Lambda.Annotations (1.15.2) +* Set RepositoryUrl and RepositoryType on NuGet package. +### Amazon.Lambda.APIGatewayEvents (2.7.4) +* Set RepositoryUrl and RepositoryType on NuGet package. +### Amazon.Lambda.ApplicationLoadBalancerEvents (2.2.2) +* Set RepositoryUrl and RepositoryType on NuGet package. +### Amazon.Lambda.AspNetCoreServer (9.2.2) +* Set RepositoryUrl and RepositoryType on NuGet package. +### Amazon.Lambda.AspNetCoreServer.Hosting (1.10.1) +* Set RepositoryUrl and RepositoryType on NuGet package. +### Amazon.Lambda.CloudWatchEvents (4.4.2) +* Set RepositoryUrl and RepositoryType on NuGet package. +### Amazon.Lambda.CloudWatchLogsEvents (2.2.2) +* Set RepositoryUrl and RepositoryType on NuGet package. +### Amazon.Lambda.CognitoEvents (4.0.2) +* Set RepositoryUrl and RepositoryType on NuGet package. +### Amazon.Lambda.ConfigEvents (2.1.2) +* Set RepositoryUrl and RepositoryType on NuGet package. +### Amazon.Lambda.ConnectEvents (1.1.2) +* Set RepositoryUrl and RepositoryType on NuGet package. +### Amazon.Lambda.Core (2.8.2) +* Set RepositoryUrl and RepositoryType on NuGet package. +### Amazon.Lambda.DynamoDBEvents (3.1.3) +* Set RepositoryUrl and RepositoryType on NuGet package. +### Amazon.Lambda.DynamoDBEvents.SDK.Convertor (2.0.3) +* Set RepositoryUrl and RepositoryType on NuGet package. +### Amazon.Lambda.KafkaEvents (2.1.2) +* Set RepositoryUrl and RepositoryType on NuGet package. +### Amazon.Lambda.KinesisAnalyticsEvents (2.3.2) +* Set RepositoryUrl and RepositoryType on NuGet package. +### Amazon.Lambda.KinesisEvents (3.0.3) +* Set RepositoryUrl and RepositoryType on NuGet package. +### Amazon.Lambda.KinesisFirehoseEvents (2.3.2) +* Set RepositoryUrl and RepositoryType on NuGet package. +### Amazon.Lambda.LexEvents (3.1.2) +* Set RepositoryUrl and RepositoryType on NuGet package. +### Amazon.Lambda.LexV2Events (1.1.2) +* Set RepositoryUrl and RepositoryType on NuGet package. +### Amazon.Lambda.Logging.AspNetCore (4.1.2) +* Set RepositoryUrl and RepositoryType on NuGet package. +### Amazon.Lambda.MQEvents (2.1.2) +* Set RepositoryUrl and RepositoryType on NuGet package. +### Amazon.Lambda.PowerShellHost (3.0.4) +* Set RepositoryUrl and RepositoryType on NuGet package. +### Amazon.Lambda.RuntimeSupport (1.14.4) +* Set RepositoryUrl and RepositoryType on NuGet package. +### Amazon.Lambda.S3Events (3.1.3) +* Set RepositoryUrl and RepositoryType on NuGet package. +### Amazon.Lambda.Serialization.Json (2.2.6) +* Set RepositoryUrl and RepositoryType on NuGet package. +### Amazon.Lambda.Serialization.SystemTextJson (2.4.6) +* Set RepositoryUrl and RepositoryType on NuGet package. +### Amazon.Lambda.SimpleEmailEvents (3.1.2) +* Set RepositoryUrl and RepositoryType on NuGet package. +### Amazon.Lambda.SNSEvents (2.1.2) +* Set RepositoryUrl and RepositoryType on NuGet package. +### Amazon.Lambda.SQSEvents (2.2.2) +* Set RepositoryUrl and RepositoryType on NuGet package. +### Amazon.Lambda.TestUtilities (3.0.2) +* Set RepositoryUrl and RepositoryType on NuGet package. +### Amazon.Lambda.TestTool.BlazorTester (0.17.2) +* Set RepositoryUrl and RepositoryType on NuGet package. +### Amazon.Lambda.Templates (8.0.3) +* Set RepositoryUrl and RepositoryType on NuGet package. +### SnapshotRestore.Registry (1.0.2) +* Set RepositoryUrl and RepositoryType on NuGet package. +### Amazon.Lambda.TestTool (0.13.1) +* Set RepositoryUrl and RepositoryType on NuGet package. +### Amazon.Lambda.AppSyncEvents (1.0.1) +* Set RepositoryUrl and RepositoryType on NuGet package. + +## Release 2026-04-22 #2 + +### Amazon.Lambda.Annotations (1.15.1) +* The ScheduleEvent Input property now supports file paths (relative to the project root or absolute) in addition to literal JSON strings. If the value resolves to an existing file, its contents are read and used as the input in the CloudFormation template. + +## Release 2026-04-22 + +### Amazon.Lambda.Annotations (1.15.0) +* Added [DynamoDBEvent] annotation attribute for declaratively configuring DynamoDB stream-triggered Lambda functions with support for stream reference, batch size, starting position, batching window, filters, and enabled state. +* Added [ScheduleEvent] annotation attribute for declaratively configuring schedule-triggered Lambda functions with support for rate and cron expressions, description, input, and enabled state. + +## Release 2026-04-16 + +### Amazon.Lambda.Annotations (1.14.0) +* Added [SNSEvent] annotation attribute for declaratively configuring SNS topic-triggered Lambda functions with support for topic reference, filter policy, and enabled state. + +## Release 2026-04-14 + +### Amazon.Lambda.TestTool.BlazorTester (0.17.1) +* Minor fixes to improve the testability of the package +### Amazon.Lambda.RuntimeSupport (1.14.3) +* Minor fixes to improve the testability of the package +### Amazon.Lambda.Annotations (1.13.0) +* Added [FunctionUrl] attribute for configuring Lambda functions with Function URL endpoints, including optional CORS support + ## Release 2026-04-13 #2 ### Amazon.Lambda.Annotations (1.12.0) diff --git a/LambdaRuntimeDockerfiles/Images/net10/amd64/Dockerfile b/LambdaRuntimeDockerfiles/Images/net10/amd64/Dockerfile index 6b08ef4cb..1a82b2bb8 100644 --- a/LambdaRuntimeDockerfiles/Images/net10/amd64/Dockerfile +++ b/LambdaRuntimeDockerfiles/Images/net10/amd64/Dockerfile @@ -1,7 +1,7 @@ # Based on Docker image from: https://github.com/dotnet/dotnet-docker/ -ARG ASPNET_VERSION=10.0.5 -ARG ASPNET_SHA512=7108ecdda8e2607fa80e2b45f1209d7af5301d53438b65d2269605b8415aebd49db23455d8dcd77d8fdccc904c9202b4834f9ca2e00e27a501d2006174d76cc4 +ARG ASPNET_VERSION=10.0.8 +ARG ASPNET_SHA512=e397fe8522af794b37cb313047fb786c060850a1191d0ec0a1ae248943b3cf515b25650aaf0717981dacda7851e418f3fd90c4c2827b7b6495d3db4bebf1d756 ARG LAMBDA_RUNTIME_NAME=dotnet10 ARG AMAZON_LINUX=public.ecr.aws/lambda/provided:al2023 diff --git a/LambdaRuntimeDockerfiles/Images/net10/arm64/Dockerfile b/LambdaRuntimeDockerfiles/Images/net10/arm64/Dockerfile index 773f877f6..9e2511c24 100644 --- a/LambdaRuntimeDockerfiles/Images/net10/arm64/Dockerfile +++ b/LambdaRuntimeDockerfiles/Images/net10/arm64/Dockerfile @@ -1,7 +1,7 @@ # Based on Docker image from: https://github.com/dotnet/dotnet-docker/ -ARG ASPNET_VERSION=10.0.5 -ARG ASPNET_SHA512=6cab3b81910ba3e6e118595a45948331f5d1506b42af0942f79ea3db6623e820557a1757973becb9afd3d6f8ead9e9a641667860f2a7fbbd598bcafa38f4739c +ARG ASPNET_VERSION=10.0.8 +ARG ASPNET_SHA512=4bbdc0586b1b192da43606069b9298927d06527cdc5d61bf4fc401bfd5e0bf4303607dcb09d822df94dcc7f9447486ebb3990593c2e5cbedecc35fe2de37457d ARG LAMBDA_RUNTIME_NAME=dotnet10 ARG AMAZON_LINUX=public.ecr.aws/lambda/provided:al2023 diff --git a/LambdaRuntimeDockerfiles/Images/net11/amd64/Dockerfile b/LambdaRuntimeDockerfiles/Images/net11/amd64/Dockerfile index df5a89084..fe4d9bd4c 100644 --- a/LambdaRuntimeDockerfiles/Images/net11/amd64/Dockerfile +++ b/LambdaRuntimeDockerfiles/Images/net11/amd64/Dockerfile @@ -1,7 +1,7 @@ # Based on Docker image from: https://github.com/dotnet/dotnet-docker/ -ARG ASPNET_VERSION=11.0.0-preview.2.26159.112 -ARG ASPNET_SHA512=4c02fbb66bc4b7389e0f43c0ea96a3046954eb3c7ad06bf0f8d90997c2e603dd0a3929e2b29b9e8933cc76341fb5e62ebe5bcb83d9a31419bdd1195904ff5af6 +ARG ASPNET_VERSION=11.0.0-preview.4.26230.115 +ARG ASPNET_SHA512=f659ed502ea2c2329deb9c4ae5e33793f7b73bfd59aa47461f8b5a180848de7597e0b157a4aa8d06279b3163d592c635942e7a09880c8b28ca376e823da1709d ARG LAMBDA_RUNTIME_NAME=dotnet11 ARG AMAZON_LINUX=public.ecr.aws/lambda/provided:al2023 diff --git a/LambdaRuntimeDockerfiles/Images/net11/arm64/Dockerfile b/LambdaRuntimeDockerfiles/Images/net11/arm64/Dockerfile index 9f150d25b..997f9a9d4 100644 --- a/LambdaRuntimeDockerfiles/Images/net11/arm64/Dockerfile +++ b/LambdaRuntimeDockerfiles/Images/net11/arm64/Dockerfile @@ -1,7 +1,7 @@ # Based on Docker image from: https://github.com/dotnet/dotnet-docker/ -ARG ASPNET_VERSION=11.0.0-preview.2.26159.112 -ARG ASPNET_SHA512=711914b72530c8b6ba49e5077942893a52bf5508cc082b09ae04f5c72ae4a09dd78699ffcad16dd25a2fe43d586533f897dcdb5c82ab2982ff6b4ad6fdfe5a58 +ARG ASPNET_VERSION=11.0.0-preview.4.26230.115 +ARG ASPNET_SHA512=5e7bf7503b2106557b6e26be6a071d53e917bf040972426c9c9224d031924c244c0bcb2ca8f67d0194f1b099e652c2a3584a0d938ac19598bda558dba4e3b03b ARG LAMBDA_RUNTIME_NAME=dotnet11 ARG AMAZON_LINUX=public.ecr.aws/lambda/provided:al2023 diff --git a/LambdaRuntimeDockerfiles/Images/net8/amd64/Dockerfile b/LambdaRuntimeDockerfiles/Images/net8/amd64/Dockerfile index 3fa341885..c68222193 100644 --- a/LambdaRuntimeDockerfiles/Images/net8/amd64/Dockerfile +++ b/LambdaRuntimeDockerfiles/Images/net8/amd64/Dockerfile @@ -1,7 +1,7 @@ # Based on Docker image from: https://github.com/dotnet/dotnet-docker/ -ARG ASPNET_VERSION=8.0.25 -ARG ASPNET_SHA512=ddb66ac366252ab382271241b3e53a75201d2c848c9ec870a27fb178a6db18e4d949b9896a3d8530d03d255f4fd51d635367bedda3d9f3c677cb596784dbcb9c +ARG ASPNET_VERSION=8.0.27 +ARG ASPNET_SHA512=4f948af91423636c8a8bbd9ad2f9e2b5997b84e84ef36707e3e3250eeb4f00d0bc98ed1442151d1913ba21bb0475ba2adb2d6d6328a93dbe7105f2c0bca2abc9 ARG LAMBDA_RUNTIME_NAME=dotnet8 ARG AMAZON_LINUX=public.ecr.aws/lambda/provided:al2023 diff --git a/LambdaRuntimeDockerfiles/Images/net8/arm64/Dockerfile b/LambdaRuntimeDockerfiles/Images/net8/arm64/Dockerfile index 7180c4277..0119aa00f 100644 --- a/LambdaRuntimeDockerfiles/Images/net8/arm64/Dockerfile +++ b/LambdaRuntimeDockerfiles/Images/net8/arm64/Dockerfile @@ -1,7 +1,7 @@ # Based on Docker image from: https://github.com/dotnet/dotnet-docker/ -ARG ASPNET_VERSION=8.0.25 -ARG ASPNET_SHA512=65d8b16bbef90c44daac906aaf92818f43a8482191dbf3f20bddcdd1ad6077d17b6178364bd08249501ddd3021a4c8f5f98a1c0360e126870db14cf06cd12727 +ARG ASPNET_VERSION=8.0.27 +ARG ASPNET_SHA512=5f76d6183e4d5d90f8672abc865436606ed551593a75771e56b3521797a48dadd6f6de1ff93c2624662ac59795a7ce8acd7298c5de889b198b106d7a79b8c114 ARG LAMBDA_RUNTIME_NAME=dotnet8 ARG AMAZON_LINUX=public.ecr.aws/lambda/provided:al2023 diff --git a/LambdaRuntimeDockerfiles/Images/net9/amd64/Dockerfile b/LambdaRuntimeDockerfiles/Images/net9/amd64/Dockerfile index 6fcff677b..ff3b2ecef 100644 --- a/LambdaRuntimeDockerfiles/Images/net9/amd64/Dockerfile +++ b/LambdaRuntimeDockerfiles/Images/net9/amd64/Dockerfile @@ -1,7 +1,7 @@ # Based on Docker image from: https://github.com/dotnet/dotnet-docker/ -ARG ASPNET_VERSION=9.0.14 -ARG ASPNET_SHA512=6d0947390f9ec316297f21a9ec022528a27205a44328f9417fb8675783edc56022e5e15ea65650c5e73cb73917c09c52305d3e85e4c12acd5e11074871bb0679 +ARG ASPNET_VERSION=9.0.16 +ARG ASPNET_SHA512=00c0c73a5d3ca5607d8e1db020409a201bcb42469e4e4f7fb992a99e8a8efa41657e8c2bd29c99cc1f97331d0b47bb75256479d9453649621a3aca18a202f48e ARG LAMBDA_RUNTIME_NAME=dotnet9 ARG AMAZON_LINUX=public.ecr.aws/lambda/provided:al2023 diff --git a/LambdaRuntimeDockerfiles/Images/net9/arm64/Dockerfile b/LambdaRuntimeDockerfiles/Images/net9/arm64/Dockerfile index 2294235c1..1a1f7b327 100644 --- a/LambdaRuntimeDockerfiles/Images/net9/arm64/Dockerfile +++ b/LambdaRuntimeDockerfiles/Images/net9/arm64/Dockerfile @@ -1,7 +1,7 @@ # Based on Docker image from: https://github.com/dotnet/dotnet-docker/ -ARG ASPNET_VERSION=9.0.14 -ARG ASPNET_SHA512=c5cd05971c9cba0c211ceb55e226cbfb62f172db8345fa5d4e476a4ccb3a4f61d6e51dc2b2d178eb55f3673ad2d61cf72ae649fb84ccf2c3dbbbd4acdbb78132 +ARG ASPNET_VERSION=9.0.16 +ARG ASPNET_SHA512=85f241cf89b8f6fb1f97b452e10d19e2f021f9e595eedd342ddfed8a3a53596a5283e1b6b5e49e7e1d67c2a03cfa52615835e3c7cf00639ed40b082c4e429dff ARG LAMBDA_RUNTIME_NAME=dotnet9 ARG AMAZON_LINUX=public.ecr.aws/lambda/provided:al2023 diff --git a/Libraries/Amazon.Lambda.RuntimeSupport.slnf b/Libraries/Amazon.Lambda.RuntimeSupport.slnf index fb03ebc05..cd6d74977 100644 --- a/Libraries/Amazon.Lambda.RuntimeSupport.slnf +++ b/Libraries/Amazon.Lambda.RuntimeSupport.slnf @@ -14,12 +14,13 @@ "src\\SnapshotRestore.Registry\\SnapshotRestore.Registry.csproj", "test\\Amazon.Lambda.RuntimeSupport.Tests\\Amazon.Lambda.RuntimeSupport.IntegrationTests\\Amazon.Lambda.RuntimeSupport.IntegrationTests.csproj", "test\\Amazon.Lambda.RuntimeSupport.Tests\\Amazon.Lambda.RuntimeSupport.UnitTests\\Amazon.Lambda.RuntimeSupport.UnitTests.csproj", + "test\\Amazon.Lambda.RuntimeSupport.Tests\\AspNetCoreStreamingApiGatewayTest\\AspNetCoreStreamingApiGatewayTest.csproj", "test\\Amazon.Lambda.RuntimeSupport.Tests\\CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest\\CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest.csproj", "test\\Amazon.Lambda.RuntimeSupport.Tests\\CustomRuntimeAspNetCoreMinimalApiTest\\CustomRuntimeAspNetCoreMinimalApiTest.csproj", "test\\Amazon.Lambda.RuntimeSupport.Tests\\CustomRuntimeFunctionTest\\CustomRuntimeFunctionTest.csproj", - "test\\SnapshotRestore.Registry.Tests\\SnapshotRestore.Registry.Tests.csproj", "test\\HandlerTestNoSerializer\\HandlerTestNoSerializer.csproj", - "test\\HandlerTest\\HandlerTest.csproj" + "test\\HandlerTest\\HandlerTest.csproj", + "test\\SnapshotRestore.Registry.Tests\\SnapshotRestore.Registry.Tests.csproj" ] } } \ No newline at end of file diff --git a/Libraries/Libraries.sln b/Libraries/Libraries.sln index 17e39c553..e42c40045 100644 --- a/Libraries/Libraries.sln +++ b/Libraries/Libraries.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31717.71 +# Visual Studio Version 18 +VisualStudioVersion = 18.5.11709.299 stable MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{AAB54E74-20B1-42ED-BC3D-CE9F7BC7FD12}" EndProject @@ -95,8 +95,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Amazon.Lambda.Serialization EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "EventsTests.Shared", "test\EventsTests.Shared\EventsTests.Shared.shproj", "{A2CB78BB-E54F-48CA-BBFB-9553D27EF23D}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventsTests.NETCore31", "test\EventsTests.NETCore31\EventsTests.NETCore31.csproj", "{44E9D925-B61D-4234-97B7-61424C963BA6}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HandlerTest", "test\HandlerTest\HandlerTest.csproj", "{E88231E0-B249-49AE-B764-DB6C9615F6CA}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HandlerTestNoSerializer", "test\HandlerTestNoSerializer\HandlerTestNoSerializer.csproj", "{9736E38B-B67F-42BD-882E-CE9C8AEE1BC4}" @@ -115,8 +113,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Amazon.Lambda.AspNetCoreSer EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomRuntimeAspNetCoreMinimalApiTest", "test\Amazon.Lambda.RuntimeSupport.Tests\CustomRuntimeAspNetCoreMinimalApiTest\CustomRuntimeAspNetCoreMinimalApiTest.csproj", "{2FFBE745-B7D5-4E44-B76D-88A0C2402FEB}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventsTests.NET6", "test\EventsTests.NET6\EventsTests.NET6.csproj", "{C1BB30D2-3237-4CFC-BA93-627471148EC2}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Amazon.Lambda.KafkaEvents", "src\Amazon.Lambda.KafkaEvents\Amazon.Lambda.KafkaEvents.csproj", "{982A26C7-A5D1-4783-A7F8-F2B28AA2459E}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestMinimalAPIApp", "test\TestMinimalAPIApp\TestMinimalAPIApp.csproj", "{8AB1CBD7-2D08-492F-9C09-3E754364046C}" @@ -155,6 +151,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestServerlessApp.ALB", "te EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestServerlessApp.ALB.IntegrationTests", "test\TestServerlessApp.ALB.IntegrationTests\TestServerlessApp.ALB.IntegrationTests.csproj", "{80594C21-C6EB-469E-83CC-68F9F661CA5E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResponseStreamingFunctionHandlers", "test\Amazon.Lambda.RuntimeSupport.Tests\ResponseStreamingFunctionHandlers\ResponseStreamingFunctionHandlers.csproj", "{E404A7AC-812B-BC03-CA76-02C0BC2BA7F9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCoreStreamingApiGatewayTest", "test\Amazon.Lambda.RuntimeSupport.Tests\AspNetCoreStreamingApiGatewayTest\AspNetCoreStreamingApiGatewayTest.csproj", "{0768FA72-CF49-2B59-BC4C-E4CE579E5D93}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -609,18 +609,6 @@ Global {AA6BA0B8-D61E-49E7-BC1B-19410E25F005}.Release|x64.Build.0 = Release|Any CPU {AA6BA0B8-D61E-49E7-BC1B-19410E25F005}.Release|x86.ActiveCfg = Release|Any CPU {AA6BA0B8-D61E-49E7-BC1B-19410E25F005}.Release|x86.Build.0 = Release|Any CPU - {44E9D925-B61D-4234-97B7-61424C963BA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {44E9D925-B61D-4234-97B7-61424C963BA6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {44E9D925-B61D-4234-97B7-61424C963BA6}.Debug|x64.ActiveCfg = Debug|Any CPU - {44E9D925-B61D-4234-97B7-61424C963BA6}.Debug|x64.Build.0 = Debug|Any CPU - {44E9D925-B61D-4234-97B7-61424C963BA6}.Debug|x86.ActiveCfg = Debug|Any CPU - {44E9D925-B61D-4234-97B7-61424C963BA6}.Debug|x86.Build.0 = Debug|Any CPU - {44E9D925-B61D-4234-97B7-61424C963BA6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {44E9D925-B61D-4234-97B7-61424C963BA6}.Release|Any CPU.Build.0 = Release|Any CPU - {44E9D925-B61D-4234-97B7-61424C963BA6}.Release|x64.ActiveCfg = Release|Any CPU - {44E9D925-B61D-4234-97B7-61424C963BA6}.Release|x64.Build.0 = Release|Any CPU - {44E9D925-B61D-4234-97B7-61424C963BA6}.Release|x86.ActiveCfg = Release|Any CPU - {44E9D925-B61D-4234-97B7-61424C963BA6}.Release|x86.Build.0 = Release|Any CPU {E88231E0-B249-49AE-B764-DB6C9615F6CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E88231E0-B249-49AE-B764-DB6C9615F6CA}.Debug|Any CPU.Build.0 = Debug|Any CPU {E88231E0-B249-49AE-B764-DB6C9615F6CA}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -729,18 +717,6 @@ Global {2FFBE745-B7D5-4E44-B76D-88A0C2402FEB}.Release|x64.Build.0 = Release|Any CPU {2FFBE745-B7D5-4E44-B76D-88A0C2402FEB}.Release|x86.ActiveCfg = Release|Any CPU {2FFBE745-B7D5-4E44-B76D-88A0C2402FEB}.Release|x86.Build.0 = Release|Any CPU - {C1BB30D2-3237-4CFC-BA93-627471148EC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C1BB30D2-3237-4CFC-BA93-627471148EC2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C1BB30D2-3237-4CFC-BA93-627471148EC2}.Debug|x64.ActiveCfg = Debug|Any CPU - {C1BB30D2-3237-4CFC-BA93-627471148EC2}.Debug|x64.Build.0 = Debug|Any CPU - {C1BB30D2-3237-4CFC-BA93-627471148EC2}.Debug|x86.ActiveCfg = Debug|Any CPU - {C1BB30D2-3237-4CFC-BA93-627471148EC2}.Debug|x86.Build.0 = Debug|Any CPU - {C1BB30D2-3237-4CFC-BA93-627471148EC2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C1BB30D2-3237-4CFC-BA93-627471148EC2}.Release|Any CPU.Build.0 = Release|Any CPU - {C1BB30D2-3237-4CFC-BA93-627471148EC2}.Release|x64.ActiveCfg = Release|Any CPU - {C1BB30D2-3237-4CFC-BA93-627471148EC2}.Release|x64.Build.0 = Release|Any CPU - {C1BB30D2-3237-4CFC-BA93-627471148EC2}.Release|x86.ActiveCfg = Release|Any CPU - {C1BB30D2-3237-4CFC-BA93-627471148EC2}.Release|x86.Build.0 = Release|Any CPU {982A26C7-A5D1-4783-A7F8-F2B28AA2459E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {982A26C7-A5D1-4783-A7F8-F2B28AA2459E}.Debug|Any CPU.Build.0 = Debug|Any CPU {982A26C7-A5D1-4783-A7F8-F2B28AA2459E}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -969,6 +945,30 @@ Global {80594C21-C6EB-469E-83CC-68F9F661CA5E}.Release|x64.Build.0 = Release|Any CPU {80594C21-C6EB-469E-83CC-68F9F661CA5E}.Release|x86.ActiveCfg = Release|Any CPU {80594C21-C6EB-469E-83CC-68F9F661CA5E}.Release|x86.Build.0 = Release|Any CPU + {E404A7AC-812B-BC03-CA76-02C0BC2BA7F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E404A7AC-812B-BC03-CA76-02C0BC2BA7F9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E404A7AC-812B-BC03-CA76-02C0BC2BA7F9}.Debug|x64.ActiveCfg = Debug|Any CPU + {E404A7AC-812B-BC03-CA76-02C0BC2BA7F9}.Debug|x64.Build.0 = Debug|Any CPU + {E404A7AC-812B-BC03-CA76-02C0BC2BA7F9}.Debug|x86.ActiveCfg = Debug|Any CPU + {E404A7AC-812B-BC03-CA76-02C0BC2BA7F9}.Debug|x86.Build.0 = Debug|Any CPU + {E404A7AC-812B-BC03-CA76-02C0BC2BA7F9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E404A7AC-812B-BC03-CA76-02C0BC2BA7F9}.Release|Any CPU.Build.0 = Release|Any CPU + {E404A7AC-812B-BC03-CA76-02C0BC2BA7F9}.Release|x64.ActiveCfg = Release|Any CPU + {E404A7AC-812B-BC03-CA76-02C0BC2BA7F9}.Release|x64.Build.0 = Release|Any CPU + {E404A7AC-812B-BC03-CA76-02C0BC2BA7F9}.Release|x86.ActiveCfg = Release|Any CPU + {E404A7AC-812B-BC03-CA76-02C0BC2BA7F9}.Release|x86.Build.0 = Release|Any CPU + {0768FA72-CF49-2B59-BC4C-E4CE579E5D93}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0768FA72-CF49-2B59-BC4C-E4CE579E5D93}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0768FA72-CF49-2B59-BC4C-E4CE579E5D93}.Debug|x64.ActiveCfg = Debug|Any CPU + {0768FA72-CF49-2B59-BC4C-E4CE579E5D93}.Debug|x64.Build.0 = Debug|Any CPU + {0768FA72-CF49-2B59-BC4C-E4CE579E5D93}.Debug|x86.ActiveCfg = Debug|Any CPU + {0768FA72-CF49-2B59-BC4C-E4CE579E5D93}.Debug|x86.Build.0 = Debug|Any CPU + {0768FA72-CF49-2B59-BC4C-E4CE579E5D93}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0768FA72-CF49-2B59-BC4C-E4CE579E5D93}.Release|Any CPU.Build.0 = Release|Any CPU + {0768FA72-CF49-2B59-BC4C-E4CE579E5D93}.Release|x64.ActiveCfg = Release|Any CPU + {0768FA72-CF49-2B59-BC4C-E4CE579E5D93}.Release|x64.Build.0 = Release|Any CPU + {0768FA72-CF49-2B59-BC4C-E4CE579E5D93}.Release|x86.ActiveCfg = Release|Any CPU + {0768FA72-CF49-2B59-BC4C-E4CE579E5D93}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1015,7 +1015,6 @@ Global {10E47FE4-8620-4933-A14D-E33F25CA557A} = {B5BD0336-7D08-492C-8489-42C987E29B39} {AA6BA0B8-D61E-49E7-BC1B-19410E25F005} = {AAB54E74-20B1-42ED-BC3D-CE9F7BC7FD12} {A2CB78BB-E54F-48CA-BBFB-9553D27EF23D} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69} - {44E9D925-B61D-4234-97B7-61424C963BA6} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69} {E88231E0-B249-49AE-B764-DB6C9615F6CA} = {B5BD0336-7D08-492C-8489-42C987E29B39} {9736E38B-B67F-42BD-882E-CE9C8AEE1BC4} = {B5BD0336-7D08-492C-8489-42C987E29B39} {3D322CAB-0DDD-4C84-B3ED-0862F244AF5C} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69} @@ -1025,7 +1024,6 @@ Global {2D956162-04BE-402E-9487-AE785AA14DE4} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69} {02908C6F-FBDF-4949-B039-0F4632265B90} = {AAB54E74-20B1-42ED-BC3D-CE9F7BC7FD12} {2FFBE745-B7D5-4E44-B76D-88A0C2402FEB} = {B5BD0336-7D08-492C-8489-42C987E29B39} - {C1BB30D2-3237-4CFC-BA93-627471148EC2} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69} {982A26C7-A5D1-4783-A7F8-F2B28AA2459E} = {AAB54E74-20B1-42ED-BC3D-CE9F7BC7FD12} {8AB1CBD7-2D08-492F-9C09-3E754364046C} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69} {BF85932E-2DFF-41CD-8090-A672468B8FBB} = {AAB54E74-20B1-42ED-BC3D-CE9F7BC7FD12} @@ -1045,14 +1043,14 @@ Global {3BFA4B73-BA61-4578-833B-C5B3A16EDA9E} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69} {8F7C617D-C611-4DC6-A07C-033F13C1835D} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69} {80594C21-C6EB-469E-83CC-68F9F661CA5E} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69} + {E404A7AC-812B-BC03-CA76-02C0BC2BA7F9} = {B5BD0336-7D08-492C-8489-42C987E29B39} + {0768FA72-CF49-2B59-BC4C-E4CE579E5D93} = {B5BD0336-7D08-492C-8489-42C987E29B39} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {503678A4-B8D1-4486-8915-405A3E9CF0EB} EndGlobalSection GlobalSection(SharedMSBuildProjectFiles) = preSolution test\EventsTests.Shared\EventsTests.Shared.projitems*{1fb22337-5d88-4ce7-adff-ffd89204f0e9}*SharedItemsImports = 5 - test\EventsTests.Shared\EventsTests.Shared.projitems*{44e9d925-b61d-4234-97b7-61424c963ba6}*SharedItemsImports = 5 test\EventsTests.Shared\EventsTests.Shared.projitems*{a2cb78bb-e54f-48ca-bbfb-9553d27ef23d}*SharedItemsImports = 13 - test\EventsTests.Shared\EventsTests.Shared.projitems*{c1bb30d2-3237-4cfc-ba93-627471148ec2}*SharedItemsImports = 5 EndGlobalSection EndGlobal diff --git a/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayCustomAuthorizerContext.cs b/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayCustomAuthorizerContext.cs index 15ed85503..0ed160ccb 100644 --- a/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayCustomAuthorizerContext.cs +++ b/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayCustomAuthorizerContext.cs @@ -1,14 +1,10 @@ -namespace Amazon.Lambda.APIGatewayEvents +namespace Amazon.Lambda.APIGatewayEvents { using System; using System.Collections.Generic; using System.Runtime.Serialization; -#if NETSTANDARD_2_0 - using Newtonsoft.Json.Linq; -#else using System.Text.Json; -#endif /// @@ -26,7 +22,7 @@ public string PrincipalId get { object value; - if (this.TryGetValue("principalId", out value)) + if (TryGetValue("principalId", out value)) return value.ToString(); return null; } @@ -45,7 +41,7 @@ public string StringKey get { object value; - if (this.TryGetValue("stringKey", out value)) + if (TryGetValue("stringKey", out value)) return value.ToString(); return null; } @@ -64,7 +60,7 @@ public int? NumKey get { object value; - if (this.TryGetValue("numKey", out value)) + if (TryGetValue("numKey", out value)) { int i; if (int.TryParse(value?.ToString(), out i)) @@ -90,7 +86,7 @@ public bool? BoolKey get { object value; - if (this.TryGetValue("boolKey", out value)) + if (TryGetValue("boolKey", out value)) { bool b; if(bool.TryParse(value?.ToString(), out b)) @@ -120,19 +116,8 @@ public Dictionary Claims _claims = new Dictionary(); object value; - if(this.TryGetValue("claims", out value)) + if(TryGetValue("claims", out value)) { -#if NETSTANDARD_2_0 - JObject jsonClaims = value as JObject; - if (jsonClaims != null) - { - foreach (JProperty property in jsonClaims.Properties()) - { - _claims[property.Name] = property.Value?.ToString(); - - } - } -#else if(value is JsonElement jsonClaims) { foreach(JsonProperty property in jsonClaims.EnumerateObject()) @@ -143,7 +128,6 @@ public Dictionary Claims } } } -#endif } } @@ -151,7 +135,7 @@ public Dictionary Claims } set { - this._claims = value; + _claims = value; } } } diff --git a/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayCustomAuthorizerContextOutput.cs b/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayCustomAuthorizerContextOutput.cs index 2f5acf092..1f905d839 100644 --- a/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayCustomAuthorizerContextOutput.cs +++ b/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayCustomAuthorizerContextOutput.cs @@ -1,4 +1,4 @@ -namespace Amazon.Lambda.APIGatewayEvents +namespace Amazon.Lambda.APIGatewayEvents { using System; using System.Collections.Generic; @@ -19,7 +19,7 @@ public string StringKey get { object value; - if (this.TryGetValue("stringKey", out value)) + if (TryGetValue("stringKey", out value)) return value.ToString(); return null; } @@ -38,7 +38,7 @@ public int? NumKey get { object value; - if (this.TryGetValue("numKey", out value)) + if (TryGetValue("numKey", out value)) { int i; if (int.TryParse(value?.ToString(), out i)) @@ -64,7 +64,7 @@ public bool? BoolKey get { object value; - if (this.TryGetValue("boolKey", out value)) + if (TryGetValue("boolKey", out value)) { bool b; if (bool.TryParse(value?.ToString(), out b)) diff --git a/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayCustomAuthorizerPolicy.cs b/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayCustomAuthorizerPolicy.cs index 59fd940f3..887d05c0e 100644 --- a/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayCustomAuthorizerPolicy.cs +++ b/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayCustomAuthorizerPolicy.cs @@ -10,17 +10,13 @@ public class APIGatewayCustomAuthorizerPolicy /// /// Gets or sets the IAM API version. /// -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("Version")] -#endif public string Version { get; set; } = "2012-10-17"; /// /// Gets or sets a list of IAM policy statements to apply. /// -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("Statement")] -#endif public List Statement { get; set; } = new List(); /// @@ -31,42 +27,32 @@ public class IAMPolicyStatement /// /// Gets or sets the effect the statement has. /// -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("Effect")] -#endif public string Effect { get; set; } = "Allow"; /// /// Gets or sets the action/s the statement has. /// -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("Action")] -#endif public HashSet Action { get; set; } /// /// Gets or sets the resources the statement applies to. /// -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("Resource")] -#endif public HashSet Resource { get; set; } /// /// Gets or sets the resources the statement does not apply to. /// -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("NotResource")] -#endif public HashSet NotResource { get; set; } /// /// Gets or sets the conditions for when a policy is in effect. /// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html /// -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("Condition")] -#endif public IDictionary> Condition { get; set; } } } diff --git a/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayCustomAuthorizerResponse.cs b/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayCustomAuthorizerResponse.cs index e2209f2e0..83e8a4a6d 100644 --- a/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayCustomAuthorizerResponse.cs +++ b/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayCustomAuthorizerResponse.cs @@ -1,4 +1,4 @@ -namespace Amazon.Lambda.APIGatewayEvents +namespace Amazon.Lambda.APIGatewayEvents { using System.Runtime.Serialization; @@ -12,36 +12,28 @@ public class APIGatewayCustomAuthorizerResponse /// Gets or sets the ID of the principal. /// [DataMember(Name = "principalId")] -#if NETCOREAPP3_1_OR_GREATER - [System.Text.Json.Serialization.JsonPropertyName("principalId")] -#endif + [System.Text.Json.Serialization.JsonPropertyName("principalId")] public string PrincipalID { get; set; } /// /// Gets or sets the policy document. /// [DataMember(Name = "policyDocument")] -#if NETCOREAPP3_1_OR_GREATER - [System.Text.Json.Serialization.JsonPropertyName("policyDocument")] -#endif + [System.Text.Json.Serialization.JsonPropertyName("policyDocument")] public APIGatewayCustomAuthorizerPolicy PolicyDocument { get; set; } = new APIGatewayCustomAuthorizerPolicy(); /// /// Gets or sets the property. /// [DataMember(Name = "context")] -#if NETCOREAPP3_1_OR_GREATER - [System.Text.Json.Serialization.JsonPropertyName("context")] -#endif + [System.Text.Json.Serialization.JsonPropertyName("context")] public APIGatewayCustomAuthorizerContextOutput Context { get; set; } /// /// Gets or sets the usageIdentifierKey. /// [DataMember(Name = "usageIdentifierKey")] -#if NETCOREAPP3_1_OR_GREATER - [System.Text.Json.Serialization.JsonPropertyName("usageIdentifierKey")] -#endif + [System.Text.Json.Serialization.JsonPropertyName("usageIdentifierKey")] public string UsageIdentifierKey { get; set; } } } diff --git a/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayCustomAuthorizerV2IamResponse.cs b/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayCustomAuthorizerV2IamResponse.cs index 20d0fe56a..96c74ab22 100644 --- a/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayCustomAuthorizerV2IamResponse.cs +++ b/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayCustomAuthorizerV2IamResponse.cs @@ -1,4 +1,4 @@ -namespace Amazon.Lambda.APIGatewayEvents +namespace Amazon.Lambda.APIGatewayEvents { using System.Collections.Generic; using System.Runtime.Serialization; @@ -14,27 +14,21 @@ public class APIGatewayCustomAuthorizerV2IamResponse /// Gets or sets the ID of the principal. /// [DataMember(Name = "principalId")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("principalId")] -#endif public string PrincipalID { get; set; } /// /// Gets or sets the policy document. /// [DataMember(Name = "policyDocument")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("policyDocument")] -#endif public APIGatewayCustomAuthorizerPolicy PolicyDocument { get; set; } = new APIGatewayCustomAuthorizerPolicy(); /// /// Gets or sets the property. /// [DataMember(Name = "context")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("context")] -#endif public Dictionary Context { get; set; } } } diff --git a/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayCustomAuthorizerV2SimpleResponse.cs b/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayCustomAuthorizerV2SimpleResponse.cs index a64561833..14100785b 100644 --- a/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayCustomAuthorizerV2SimpleResponse.cs +++ b/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayCustomAuthorizerV2SimpleResponse.cs @@ -1,4 +1,4 @@ -namespace Amazon.Lambda.APIGatewayEvents +namespace Amazon.Lambda.APIGatewayEvents { using System.Collections.Generic; using System.Runtime.Serialization; @@ -14,18 +14,14 @@ public class APIGatewayCustomAuthorizerV2SimpleResponse /// Gets or sets authorization result. /// [DataMember(Name = "isAuthorized")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("isAuthorized")] -#endif public bool IsAuthorized { get; set; } /// /// Gets or sets the property. /// [DataMember(Name = "context")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("context")] -#endif public Dictionary Context { get; set; } } } diff --git a/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayHttpApiV2ProxyResponse.cs b/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayHttpApiV2ProxyResponse.cs index 6e7d1185d..151439a4d 100644 --- a/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayHttpApiV2ProxyResponse.cs +++ b/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayHttpApiV2ProxyResponse.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Text; @@ -17,18 +17,14 @@ public class APIGatewayHttpApiV2ProxyResponse /// The HTTP status code for the request /// [DataMember(Name = "statusCode")] -#if NETCOREAPP3_1_OR_GREATER - [System.Text.Json.Serialization.JsonPropertyName("statusCode")] -#endif + [System.Text.Json.Serialization.JsonPropertyName("statusCode")] public int StatusCode { get; set; } /// /// The Http headers returned in the response. Multiple header values set for the the same header should be separate by a comma. /// [DataMember(Name = "headers")] -#if NETCOREAPP3_1_OR_GREATER - [System.Text.Json.Serialization.JsonPropertyName("headers")] -#endif + [System.Text.Json.Serialization.JsonPropertyName("headers")] public IDictionary Headers { get; set; } /// @@ -50,16 +46,16 @@ public void SetHeaderValues(string headerName, string value, bool append) /// If true it will append the values to the existing value in the Headers collection. public void SetHeaderValues(string headerName, IEnumerable values, bool append) { - if (this.Headers == null) - this.Headers = new Dictionary(); + if (Headers == null) + Headers = new Dictionary(); - if(this.Headers.ContainsKey(headerName) && append) + if(Headers.ContainsKey(headerName) && append) { - this.Headers[headerName] = this.Headers[headerName] + "," + string.Join(",", values); + Headers[headerName] = Headers[headerName] + "," + string.Join(",", values); } else { - this.Headers[headerName] = string.Join(",", values); + Headers[headerName] = string.Join(",", values); } } @@ -67,27 +63,21 @@ public void SetHeaderValues(string headerName, IEnumerable values, bool /// The cookies returned in the response. /// [DataMember(Name = "cookies")] -#if NETCOREAPP3_1_OR_GREATER - [System.Text.Json.Serialization.JsonPropertyName("cookies")] -#endif + [System.Text.Json.Serialization.JsonPropertyName("cookies")] public string[] Cookies { get; set; } /// /// The response body /// [DataMember(Name = "body")] -#if NETCOREAPP3_1_OR_GREATER - [System.Text.Json.Serialization.JsonPropertyName("body")] -#endif + [System.Text.Json.Serialization.JsonPropertyName("body")] public string Body { get; set; } /// /// Flag indicating whether the body should be treated as a base64-encoded string /// [DataMember(Name = "isBase64Encoded")] -#if NETCOREAPP3_1_OR_GREATER - [System.Text.Json.Serialization.JsonPropertyName("isBase64Encoded")] -#endif + [System.Text.Json.Serialization.JsonPropertyName("isBase64Encoded")] public bool IsBase64Encoded { get; set; } } } diff --git a/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayProxyResponse.cs b/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayProxyResponse.cs index 48191a601..8ef412b4e 100644 --- a/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayProxyResponse.cs +++ b/Libraries/src/Amazon.Lambda.APIGatewayEvents/APIGatewayProxyResponse.cs @@ -1,4 +1,4 @@ -namespace Amazon.Lambda.APIGatewayEvents +namespace Amazon.Lambda.APIGatewayEvents { using System.Collections.Generic; using System.Runtime.Serialization; @@ -14,9 +14,7 @@ public class APIGatewayProxyResponse /// The HTTP status code for the request /// [DataMember(Name = "statusCode")] -#if NETCOREAPP3_1_OR_GREATER - [System.Text.Json.Serialization.JsonPropertyName("statusCode")] -#endif + [System.Text.Json.Serialization.JsonPropertyName("statusCode")] public int StatusCode { get; set; } /// @@ -25,9 +23,7 @@ public class APIGatewayProxyResponse /// before returning back the headers to the caller. /// [DataMember(Name = "headers")] -#if NETCOREAPP3_1_OR_GREATER - [System.Text.Json.Serialization.JsonPropertyName("headers")] -#endif + [System.Text.Json.Serialization.JsonPropertyName("headers")] public IDictionary Headers { get; set; } /// @@ -36,27 +32,21 @@ public class APIGatewayProxyResponse /// before returning back the headers to the caller. /// [DataMember(Name = "multiValueHeaders")] -#if NETCOREAPP3_1_OR_GREATER - [System.Text.Json.Serialization.JsonPropertyName("multiValueHeaders")] -#endif + [System.Text.Json.Serialization.JsonPropertyName("multiValueHeaders")] public IDictionary> MultiValueHeaders { get; set; } /// /// The response body /// [DataMember(Name = "body")] -#if NETCOREAPP3_1_OR_GREATER - [System.Text.Json.Serialization.JsonPropertyName("body")] -#endif + [System.Text.Json.Serialization.JsonPropertyName("body")] public string Body { get; set; } /// /// Flag indicating whether the body should be treated as a base64-encoded string /// [DataMember(Name = "isBase64Encoded")] -#if NETCOREAPP3_1_OR_GREATER - [System.Text.Json.Serialization.JsonPropertyName("isBase64Encoded")] -#endif + [System.Text.Json.Serialization.JsonPropertyName("isBase64Encoded")] public bool IsBase64Encoded { get; set; } } } diff --git a/Libraries/src/Amazon.Lambda.APIGatewayEvents/Amazon.Lambda.APIGatewayEvents.csproj b/Libraries/src/Amazon.Lambda.APIGatewayEvents/Amazon.Lambda.APIGatewayEvents.csproj index 0472bb651..3aabb9811 100644 --- a/Libraries/src/Amazon.Lambda.APIGatewayEvents/Amazon.Lambda.APIGatewayEvents.csproj +++ b/Libraries/src/Amazon.Lambda.APIGatewayEvents/Amazon.Lambda.APIGatewayEvents.csproj @@ -3,10 +3,10 @@ - netstandard2.0;netcoreapp3.1;net8.0 + $(DefaultPackageTargets) Amazon Lambda .NET Core support - API Gateway package. Amazon.Lambda.APIGatewayEvents - 2.7.3 + 3.0.0 Amazon.Lambda.APIGatewayEvents Amazon.Lambda.APIGatewayEvents AWS;Amazon;Lambda @@ -19,10 +19,6 @@ --> false - - - NETSTANDARD_2_0 - 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 0e90d254a..3bd7d98f4 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 @@ -1,7 +1,8 @@ - netstandard2.0;net6.0;net8.0 + + netstandard2.0;net8.0;net10.0 Amazon Web Services AWS Amazon Lambda @@ -20,7 +21,7 @@ true false - 1.12.0 + 2.0.1 true @@ -34,11 +35,11 @@ - - - + + + 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 d1a9a89d0..01f6e1693 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/AnalyzerReleases.Unshipped.md +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/AnalyzerReleases.Unshipped.md @@ -21,3 +21,6 @@ AWSLambda0133 | AWSLambdaCSharpGenerator | Error | ALB Listener Reference Not Fo AWSLambda0134 | AWSLambdaCSharpGenerator | Error | FromRoute not supported on ALB functions AWSLambda0135 | AWSLambdaCSharpGenerator | Error | Unmapped parameter on ALB function AWSLambda0136 | AWSLambdaCSharpGenerator | Error | Invalid S3EventAttribute +AWSLambda0137 | AWSLambdaCSharpGenerator | Error | Invalid DynamoDBEventAttribute +AWSLambda0138 | AWSLambdaCSharpGenerator | Error | Invalid SNSEventAttribute +AWSLambda0139 | AWSLambdaCSharpGenerator | Error | Invalid ScheduleEventAttribute diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs index e1a11087f..6de39018b 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs @@ -281,5 +281,26 @@ public static class DiagnosticDescriptors category: "AWSLambdaCSharpGenerator", DiagnosticSeverity.Error, isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor InvalidDynamoDBEventAttribute = new DiagnosticDescriptor(id: "AWSLambda0137", + title: "Invalid DynamoDBEventAttribute", + messageFormat: "Invalid DynamoDBEventAttribute encountered: {0}", + category: "AWSLambdaCSharpGenerator", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor InvalidSnsEventAttribute = new DiagnosticDescriptor(id: "AWSLambda0138", + title: "Invalid SNSEventAttribute", + messageFormat: "Invalid SNSEventAttribute encountered: {0}", + category: "AWSLambdaCSharpGenerator", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor InvalidScheduleEventAttribute = new DiagnosticDescriptor(id: "AWSLambda0139", + title: "Invalid ScheduleEventAttribute", + messageFormat: "Invalid ScheduleEventAttribute encountered: {0}", + 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 c35030190..2b92d958c 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Generator.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Generator.cs @@ -26,14 +26,12 @@ public class Generator : ISourceGenerator /// internal static readonly Dictionary _targetFrameworksToRuntimes = new Dictionary(2) { - { "net6.0", "dotnet6" }, { "net8.0", "dotnet8" }, { "net10.0", "dotnet10" } }; internal static readonly List _allowedRuntimeValues = new List(4) { - "dotnet6", "provided.al2", "provided.al2023", "dotnet8", @@ -102,7 +100,7 @@ public void Execute(GeneratorExecutionContext context) var globalPropertiesAttribute = assemblyAttributes .FirstOrDefault(attr => attr.AttributeClass.Name == nameof(LambdaGlobalPropertiesAttribute)); - var defaultRuntime = "dotnet6"; + var defaultRuntime = "dotnet10"; // Try to determine the target framework from the source generator context if (context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.TargetFramework", out var targetFramework)) diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/AttributeModelBuilder.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/AttributeModelBuilder.cs index 0d1067bb6..4a8e08c0c 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/AttributeModelBuilder.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/AttributeModelBuilder.cs @@ -1,6 +1,12 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + using System; using Amazon.Lambda.Annotations.ALB; +using Amazon.Lambda.Annotations.Schedule; using Amazon.Lambda.Annotations.APIGateway; +using Amazon.Lambda.Annotations.DynamoDB; +using Amazon.Lambda.Annotations.SNS; using Amazon.Lambda.Annotations.S3; using Amazon.Lambda.Annotations.SQS; using Microsoft.CodeAnalysis; @@ -101,6 +107,42 @@ public static AttributeModel Build(AttributeData att, GeneratorExecutionContext Type = TypeModelBuilder.Build(att.AttributeClass, context) }; } + else if (att.AttributeClass.Equals(context.Compilation.GetTypeByMetadataName(TypeFullNames.FunctionUrlAttribute), SymbolEqualityComparer.Default)) + { + var data = FunctionUrlAttributeBuilder.Build(att); + model = new AttributeModel + { + Data = data, + Type = TypeModelBuilder.Build(att.AttributeClass, context) + }; + } + else if (att.AttributeClass.Equals(context.Compilation.GetTypeByMetadataName(TypeFullNames.DynamoDBEventAttribute), SymbolEqualityComparer.Default)) + { + var data = DynamoDBEventAttributeBuilder.Build(att); + model = new AttributeModel + { + Data = data, + Type = TypeModelBuilder.Build(att.AttributeClass, context) + }; + } + else if (att.AttributeClass.Equals(context.Compilation.GetTypeByMetadataName(TypeFullNames.SNSEventAttribute), SymbolEqualityComparer.Default)) + { + var data = SNSEventAttributeBuilder.Build(att); + model = new AttributeModel + { + Data = data, + Type = TypeModelBuilder.Build(att.AttributeClass, context) + }; + } + else if (att.AttributeClass.Equals(context.Compilation.GetTypeByMetadataName(TypeFullNames.ScheduleEventAttribute), SymbolEqualityComparer.Default)) + { + var data = ScheduleEventAttributeBuilder.Build(att); + model = new AttributeModel + { + Data = data, + Type = TypeModelBuilder.Build(att.AttributeClass, context) + }; + } else if (att.AttributeClass.Equals(context.Compilation.GetTypeByMetadataName(TypeFullNames.HttpApiAuthorizerAttribute), SymbolEqualityComparer.Default)) { var data = HttpApiAuthorizerAttributeBuilder.Build(att); @@ -166,4 +208,4 @@ public static AttributeModel Build(AttributeData att, GeneratorExecutionContext return model; } } -} \ No newline at end of file +} diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/DynamoDBEventAttributeBuilder.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/DynamoDBEventAttributeBuilder.cs new file mode 100644 index 000000000..76c45c3c4 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/DynamoDBEventAttributeBuilder.cs @@ -0,0 +1,55 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Lambda.Annotations.DynamoDB; +using Microsoft.CodeAnalysis; +using System; + +namespace Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes +{ + /// + /// Builder for . + /// + public class DynamoDBEventAttributeBuilder + { + public static DynamoDBEventAttribute Build(AttributeData att) + { + if (att.ConstructorArguments.Length != 1) + { + throw new NotSupportedException($"{TypeFullNames.DynamoDBEventAttribute} must have constructor with 1 argument."); + } + var stream = att.ConstructorArguments[0].Value as string; + var data = new DynamoDBEventAttribute(stream); + + foreach (var pair in att.NamedArguments) + { + if (pair.Key == nameof(data.ResourceName) && pair.Value.Value is string resourceName) + { + data.ResourceName = resourceName; + } + else if (pair.Key == nameof(data.BatchSize) && pair.Value.Value is uint batchSize) + { + data.BatchSize = batchSize; + } + else if (pair.Key == nameof(data.StartingPosition) && pair.Value.Value is int startingPosition) + { + data.StartingPosition = (StartingPosition)startingPosition; + } + else if (pair.Key == nameof(data.Enabled) && pair.Value.Value is bool enabled) + { + data.Enabled = enabled; + } + else if (pair.Key == nameof(data.MaximumBatchingWindowInSeconds) && pair.Value.Value is uint maximumBatchingWindowInSeconds) + { + data.MaximumBatchingWindowInSeconds = maximumBatchingWindowInSeconds; + } + else if (pair.Key == nameof(data.Filters) && pair.Value.Value is string filters) + { + data.Filters = filters; + } + } + + return data; + } + } +} diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/FunctionUrlAttributeBuilder.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/FunctionUrlAttributeBuilder.cs new file mode 100644 index 000000000..48bb69ea8 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/FunctionUrlAttributeBuilder.cs @@ -0,0 +1,48 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Linq; +using Amazon.Lambda.Annotations.APIGateway; +using Microsoft.CodeAnalysis; + +namespace Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes +{ + public static class FunctionUrlAttributeBuilder + { + public static FunctionUrlAttribute Build(AttributeData att) + { + var authType = att.NamedArguments.FirstOrDefault(arg => arg.Key == "AuthType").Value.Value; + + var data = new FunctionUrlAttribute + { + AuthType = authType == null ? FunctionUrlAuthType.NONE : (FunctionUrlAuthType)authType + }; + + var allowOrigins = att.NamedArguments.FirstOrDefault(arg => arg.Key == "AllowOrigins").Value; + if (!allowOrigins.IsNull) + data.AllowOrigins = allowOrigins.Values.Select(v => v.Value as string).ToArray(); + + var allowMethods = att.NamedArguments.FirstOrDefault(arg => arg.Key == "AllowMethods").Value; + if (!allowMethods.IsNull) + data.AllowMethods = allowMethods.Values.Select(v => (LambdaHttpMethod)(int)v.Value).ToArray(); + + var allowHeaders = att.NamedArguments.FirstOrDefault(arg => arg.Key == "AllowHeaders").Value; + if (!allowHeaders.IsNull) + data.AllowHeaders = allowHeaders.Values.Select(v => v.Value as string).ToArray(); + + var exposeHeaders = att.NamedArguments.FirstOrDefault(arg => arg.Key == "ExposeHeaders").Value; + if (!exposeHeaders.IsNull) + data.ExposeHeaders = exposeHeaders.Values.Select(v => v.Value as string).ToArray(); + + var allowCredentials = att.NamedArguments.FirstOrDefault(arg => arg.Key == "AllowCredentials").Value.Value; + if (allowCredentials != null) + data.AllowCredentials = (bool)allowCredentials; + + var maxAge = att.NamedArguments.FirstOrDefault(arg => arg.Key == "MaxAge").Value.Value; + if (maxAge != null) + data.MaxAge = (int)maxAge; + + return data; + } + } +} diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/SNSEventAttributeBuilder.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/SNSEventAttributeBuilder.cs new file mode 100644 index 000000000..9e9890334 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/SNSEventAttributeBuilder.cs @@ -0,0 +1,43 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Lambda.Annotations.SNS; +using Microsoft.CodeAnalysis; +using System; + +namespace Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes +{ + /// + /// Builder for . + /// + public class SNSEventAttributeBuilder + { + public static SNSEventAttribute Build(AttributeData att) + { + if (att.ConstructorArguments.Length != 1) + { + throw new NotSupportedException($"{TypeFullNames.SNSEventAttribute} must have constructor with 1 argument."); + } + var topic = att.ConstructorArguments[0].Value as string; + var data = new SNSEventAttribute(topic); + + foreach (var pair in att.NamedArguments) + { + if (pair.Key == nameof(data.ResourceName) && pair.Value.Value is string resourceName) + { + data.ResourceName = resourceName; + } + else if (pair.Key == nameof(data.FilterPolicy) && pair.Value.Value is string filterPolicy) + { + data.FilterPolicy = filterPolicy; + } + else if (pair.Key == nameof(data.Enabled) && pair.Value.Value is bool enabled) + { + data.Enabled = enabled; + } + } + + return data; + } + } +} diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/SQSEventAttributeBuilder.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/SQSEventAttributeBuilder.cs index c58063c48..fa1202dc7 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/SQSEventAttributeBuilder.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/SQSEventAttributeBuilder.cs @@ -1,4 +1,7 @@ -using Amazon.Lambda.Annotations.SQS; +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Lambda.Annotations.SQS; using Microsoft.CodeAnalysis; using System; @@ -24,7 +27,7 @@ public static SQSEventAttribute Build(AttributeData att) { data.ResourceName = resourceName; } - if (pair.Key == nameof(data.BatchSize) && pair.Value.Value is uint batchSize) + else if (pair.Key == nameof(data.BatchSize) && pair.Value.Value is uint batchSize) { data.BatchSize = batchSize; } diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/ScheduleEventAttributeBuilder.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/ScheduleEventAttributeBuilder.cs new file mode 100644 index 000000000..91ba4403a --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/ScheduleEventAttributeBuilder.cs @@ -0,0 +1,47 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Lambda.Annotations.Schedule; +using Microsoft.CodeAnalysis; +using System; + +namespace Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes +{ + /// + /// Builder for . + /// + public class ScheduleEventAttributeBuilder + { + public static ScheduleEventAttribute Build(AttributeData att) + { + if (att.ConstructorArguments.Length != 1) + { + throw new NotSupportedException($"{TypeFullNames.ScheduleEventAttribute} must have constructor with 1 argument."); + } + var schedule = att.ConstructorArguments[0].Value as string; + var data = new ScheduleEventAttribute(schedule); + + foreach (var pair in att.NamedArguments) + { + if (pair.Key == nameof(data.ResourceName) && pair.Value.Value is string resourceName) + { + data.ResourceName = resourceName; + } + else if (pair.Key == nameof(data.Description) && pair.Value.Value is string description) + { + data.Description = description; + } + else if (pair.Key == nameof(data.Input) && pair.Value.Value is string input) + { + data.Input = input; + } + else if (pair.Key == nameof(data.Enabled) && pair.Value.Value is bool enabled) + { + data.Enabled = enabled; + } + } + + return data; + } + } +} diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/EventType.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/EventType.cs index 1b392572d..15eea9db4 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/EventType.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/EventType.cs @@ -9,6 +9,7 @@ public enum EventType API, S3, SQS, + SNS, DynamoDB, Schedule, Authorizer, diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/EventTypeBuilder.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/EventTypeBuilder.cs index 3dfc51799..4912a97a4 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/EventTypeBuilder.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/EventTypeBuilder.cs @@ -1,3 +1,6 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + using System; using System.Collections.Generic; using System.Linq; @@ -18,7 +21,8 @@ public static HashSet Build(IMethodSymbol lambdaMethodSymbol, foreach (var attribute in lambdaMethodSymbol.GetAttributes()) { if (attribute.AttributeClass.ToDisplayString() == TypeFullNames.RestApiAttribute - || attribute.AttributeClass.ToDisplayString() == TypeFullNames.HttpApiAttribute) + || attribute.AttributeClass.ToDisplayString() == TypeFullNames.HttpApiAttribute + || attribute.AttributeClass.ToDisplayString() == TypeFullNames.FunctionUrlAttribute) { events.Add(EventType.API); } @@ -30,6 +34,18 @@ public static HashSet Build(IMethodSymbol lambdaMethodSymbol, { events.Add(EventType.S3); } + else if (attribute.AttributeClass.ToDisplayString() == TypeFullNames.DynamoDBEventAttribute) + { + events.Add(EventType.DynamoDB); + } + else if (attribute.AttributeClass.ToDisplayString() == TypeFullNames.SNSEventAttribute) + { + events.Add(EventType.SNS); + } + else if (attribute.AttributeClass.ToDisplayString() == TypeFullNames.ScheduleEventAttribute) + { + events.Add(EventType.Schedule); + } else if (attribute.AttributeClass.ToDisplayString() == TypeFullNames.HttpApiAuthorizerAttribute || attribute.AttributeClass.ToDisplayString() == TypeFullNames.RestApiAuthorizerAttribute) { @@ -44,4 +60,4 @@ public static HashSet Build(IMethodSymbol lambdaMethodSymbol, return events; } } -} \ No newline at end of file +} diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/GeneratedMethodModelBuilder.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/GeneratedMethodModelBuilder.cs index 2dcd58fe0..e3c6a020e 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/GeneratedMethodModelBuilder.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/GeneratedMethodModelBuilder.cs @@ -1,3 +1,6 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + using System; using System.Collections.Generic; using System.Linq; @@ -144,6 +147,14 @@ private static TypeModel BuildResponseType(IMethodSymbol lambdaMethodSymbol, context.Compilation.GetTypeByMetadataName(TypeFullNames.ApplicationLoadBalancerResponse); return TypeModelBuilder.Build(symbol, context); } + else if (lambdaMethodSymbol.HasAttribute(context, TypeFullNames.FunctionUrlAttribute)) + { + // Function URLs use the same payload format as HTTP API v2 + var symbol = lambdaMethodModel.ReturnsVoidOrGenericTask ? + task.Construct(context.Compilation.GetTypeByMetadataName(TypeFullNames.APIGatewayHttpApiV2ProxyResponse)): + context.Compilation.GetTypeByMetadataName(TypeFullNames.APIGatewayHttpApiV2ProxyResponse); + return TypeModelBuilder.Build(symbol, context); + } else { return lambdaMethodModel.ReturnType; @@ -304,6 +315,20 @@ private static IList BuildParameters(IMethodSymbol lambdaMethodS parameters.Add(requestParameter); parameters.Add(contextParameter); } + else if (lambdaMethodSymbol.HasAttribute(context, TypeFullNames.FunctionUrlAttribute)) + { + // Function URLs use the same payload format as HTTP API v2 + var symbol = context.Compilation.GetTypeByMetadataName(TypeFullNames.APIGatewayHttpApiV2ProxyRequest); + var type = TypeModelBuilder.Build(symbol, context); + var requestParameter = new ParameterModel + { + Name = "__request__", + Type = type, + Documentation = "The Function URL request object that will be processed by the Lambda function handler." + }; + parameters.Add(requestParameter); + parameters.Add(contextParameter); + } else { // Lambda method with no event attribute are plain lambda functions, therefore, generated method will have diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/SyntaxReceiver.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/SyntaxReceiver.cs index 2091d6c94..57c9adfa8 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/SyntaxReceiver.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/SyntaxReceiver.cs @@ -1,4 +1,7 @@ -using System; +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; using System.Collections.Generic; using System.Linq; using Amazon.Lambda.Annotations.SourceGenerator.FileIO; @@ -21,9 +24,13 @@ internal class SyntaxReceiver : ISyntaxContextReceiver { "RestApiAuthorizerAttribute", "RestApiAuthorizer" }, { "HttpApiAttribute", "HttpApi" }, { "RestApiAttribute", "RestApi" }, + { "FunctionUrlAttribute", "FunctionUrl" }, { "SQSEventAttribute", "SQSEvent" }, { "ALBApiAttribute", "ALBApi" }, - { "S3EventAttribute", "S3Event" } + { "S3EventAttribute", "S3Event" }, + { "DynamoDBEventAttribute", "DynamoDBEvent" }, + { "SNSEventAttribute", "SNSEvent" }, + { "ScheduleEventAttribute", "ScheduleEvent" } }; public List LambdaMethods { get; } = new List(); @@ -122,4 +129,4 @@ public void OnVisitSyntaxNode(GeneratorSyntaxContext context) } } } -} \ No newline at end of file +} diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/APIGatewaySetupParameters.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/APIGatewaySetupParameters.cs index b8c54a105..179eafb48 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/APIGatewaySetupParameters.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/APIGatewaySetupParameters.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------ // // This code was generated by a tool. // Runtime Version: 18.0.0.0 @@ -668,7 +668,7 @@ public virtual string TransformText() this.Write("\") == false)\r\n {\r\n"); #line 267 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\APIGatewaySetupParameters.tt" - this.Write(this.ToStringHelper.ToStringWithCulture("#if NET6_0_OR_GREATER")); + this.Write(this.ToStringHelper.ToStringWithCulture("#if NET8_0_OR_GREATER")); #line default #line hidden @@ -798,7 +798,7 @@ public virtual string TransformText() "\n {\r\n"); #line 307 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\APIGatewaySetupParameters.tt" - this.Write(this.ToStringHelper.ToStringWithCulture("#if NET6_0_OR_GREATER")); + this.Write(this.ToStringHelper.ToStringWithCulture("#if NET8_0_OR_GREATER")); #line default #line hidden @@ -918,7 +918,7 @@ public virtual string TransformText() this.Write("\") == false)\r\n {\r\n"); #line 348 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\APIGatewaySetupParameters.tt" - this.Write(this.ToStringHelper.ToStringWithCulture("#if NET6_0_OR_GREATER")); + this.Write(this.ToStringHelper.ToStringWithCulture("#if NET8_0_OR_GREATER")); #line default #line hidden @@ -1048,7 +1048,7 @@ public virtual string TransformText() "\n {\r\n"); #line 388 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\APIGatewaySetupParameters.tt" - this.Write(this.ToStringHelper.ToStringWithCulture("#if NET6_0_OR_GREATER")); + this.Write(this.ToStringHelper.ToStringWithCulture("#if NET8_0_OR_GREATER")); #line default #line hidden diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/APIGatewaySetupParameters.tt b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/APIGatewaySetupParameters.tt index 53d6b47e8..8ee0aaaf1 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/APIGatewaySetupParameters.tt +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/APIGatewaySetupParameters.tt @@ -1,4 +1,4 @@ -<#@ template language="C#" #> +<#@ template language="C#" #> <#@ assembly name="System.Core" #> <#@ import namespace="System.Linq" #> <#@ import namespace="System.Text" #> @@ -264,7 +264,7 @@ var <#= parameter.Name #> = default(<#= parameter.Type.FullName #>); if (__request__.RequestContext?.Authorizer == null || __request__.RequestContext?.Authorizer.ContainsKey("<#= authKey #>") == false) { -<#= "#if NET6_0_OR_GREATER" #> +<#= "#if NET8_0_OR_GREATER" #> __context__.Logger.LogDebug("Authorizer attribute '<#= authKey #>' was missing, returning unauthorized."); <#= "#else" #> __context__.Logger.Log("Authorizer attribute '<#= authKey #>' was missing, returning unauthorized."); @@ -304,7 +304,7 @@ } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -<#= "#if NET6_0_OR_GREATER" #> +<#= "#if NET8_0_OR_GREATER" #> __context__.Logger.LogError(e, "Failed to convert authorizer attribute '<#= authKey #>', returning unauthorized."); <#= "#else" #> __context__.Logger.Log("Failed to convert authorizer attribute '<#= authKey #>', returning unauthorized. Exception: " + e.ToString()); @@ -345,7 +345,7 @@ var <#= parameter.Name #> = default(<#= parameter.Type.FullName #>); if (__request__.RequestContext?.Authorizer?.Lambda == null || __request__.RequestContext?.Authorizer?.Lambda.ContainsKey("<#= authKey #>") == false) { -<#= "#if NET6_0_OR_GREATER" #> +<#= "#if NET8_0_OR_GREATER" #> __context__.Logger.LogDebug("Authorizer attribute '<#= authKey #>' was missing, returning unauthorized."); <#= "#else" #> __context__.Logger.Log("Authorizer attribute '<#= authKey #>' was missing, returning unauthorized."); @@ -385,7 +385,7 @@ } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -<#= "#if NET6_0_OR_GREATER" #> +<#= "#if NET8_0_OR_GREATER" #> __context__.Logger.LogError(e, "Failed to convert authorizer attribute '<#= authKey #>', returning unauthorized."); <#= "#else" #> __context__.Logger.Log("Failed to convert authorizer attribute '<#= authKey #>', returning unauthorized. Exception: " + e.ToString()); diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/AuthorizerSetupParameters.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/AuthorizerSetupParameters.cs index d43c7c770..d61abbaaf 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/AuthorizerSetupParameters.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/AuthorizerSetupParameters.cs @@ -157,7 +157,7 @@ public virtual string TransformText() "eption)\r\n {\r\n"); #line 79 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" - this.Write(this.ToStringHelper.ToStringWithCulture("#if NET6_0_OR_GREATER")); + this.Write(this.ToStringHelper.ToStringWithCulture("#if NET8_0_OR_GREATER")); #line default #line hidden @@ -244,7 +244,7 @@ public virtual string TransformText() "eption)\r\n {\r\n"); #line 101 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" - this.Write(this.ToStringHelper.ToStringWithCulture("#if NET6_0_OR_GREATER")); + this.Write(this.ToStringHelper.ToStringWithCulture("#if NET8_0_OR_GREATER")); #line default #line hidden @@ -353,7 +353,7 @@ public virtual string TransformText() "eption)\r\n {\r\n"); #line 128 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" - this.Write(this.ToStringHelper.ToStringWithCulture("#if NET6_0_OR_GREATER")); + this.Write(this.ToStringHelper.ToStringWithCulture("#if NET8_0_OR_GREATER")); #line default #line hidden @@ -455,7 +455,7 @@ public virtual string TransformText() "eption)\r\n {\r\n"); #line 152 "C:\dev\repos\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\AuthorizerSetupParameters.tt" - this.Write(this.ToStringHelper.ToStringWithCulture("#if NET6_0_OR_GREATER")); + this.Write(this.ToStringHelper.ToStringWithCulture("#if NET8_0_OR_GREATER")); #line default #line hidden diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/AuthorizerSetupParameters.tt b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/AuthorizerSetupParameters.tt index 7d555a6e8..9745212f9 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/AuthorizerSetupParameters.tt +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/AuthorizerSetupParameters.tt @@ -76,7 +76,7 @@ } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -<#= "#if NET6_0_OR_GREATER" #> +<#= "#if NET8_0_OR_GREATER" #> __context__.Logger.LogError(e, "Failed to extract authorization token."); <#= "#else" #> __context__.Logger.Log("Failed to extract authorization token. Exception: " + e.ToString()); @@ -98,7 +98,7 @@ } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -<#= "#if NET6_0_OR_GREATER" #> +<#= "#if NET8_0_OR_GREATER" #> __context__.Logger.LogError(e, "Failed to extract header '<#= headerKey #>'."); <#= "#else" #> __context__.Logger.Log("Failed to extract header '<#= headerKey #>'. Exception: " + e.ToString()); @@ -125,7 +125,7 @@ } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -<#= "#if NET6_0_OR_GREATER" #> +<#= "#if NET8_0_OR_GREATER" #> __context__.Logger.LogError(e, "Failed to extract query parameter '<#= parameterKey #>'."); <#= "#else" #> __context__.Logger.Log("Failed to extract query parameter '<#= parameterKey #>'. Exception: " + e.ToString()); @@ -149,7 +149,7 @@ } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -<#= "#if NET6_0_OR_GREATER" #> +<#= "#if NET8_0_OR_GREATER" #> __context__.Logger.LogError(e, "Failed to extract route parameter '<#= routeKey #>'."); <#= "#else" #> __context__.Logger.Log("Failed to extract route parameter '<#= routeKey #>'. Exception: " + e.ToString()); diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/ExecutableAssembly.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/ExecutableAssembly.cs index 1ecd7c311..ba6a0dad4 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/ExecutableAssembly.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/ExecutableAssembly.cs @@ -142,6 +142,64 @@ public static async Task Main(string[] args) #line 45 "C:\codebase\V3\HLL\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\ExecutableAssembly.tt" + } + else if (!model.GeneratedMethod.Parameters.Any() && model.LambdaMethod.ReturnsVoidTask) + { + + + #line default + #line hidden + this.Write(" Func "); + + #line default + #line hidden + this.Write(this.ToStringHelper.ToStringWithCulture(model.LambdaMethod.ExecutableAssemblyHandlerName)); + + #line default + #line hidden + this.Write(" = new "); + + #line default + #line hidden + this.Write(this.ToStringHelper.ToStringWithCulture(model.LambdaMethod.ContainingNamespace)); + + #line default + #line hidden + this.Write("."); + + #line default + #line hidden + this.Write(this.ToStringHelper.ToStringWithCulture(model.LambdaMethod.ContainingType.Name)); + + #line default + #line hidden + this.Write("_"); + + #line default + #line hidden + this.Write(this.ToStringHelper.ToStringWithCulture(model.LambdaMethod.Name)); + + #line default + #line hidden + this.Write("_Generated()."); + + #line default + #line hidden + this.Write(this.ToStringHelper.ToStringWithCulture(model.LambdaMethod.Name)); + + #line default + #line hidden + this.Write(";\r\n await Amazon.Lambda.RuntimeSupport.LambdaBootstrapBuilder.Crea" + + "te("); + + #line default + #line hidden + this.Write(this.ToStringHelper.ToStringWithCulture(model.LambdaMethod.ExecutableAssemblyHandlerName)); + + #line default + #line hidden + this.Write(").Build().RunAsync();\r\n break;\r\n"); + } else { diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/ExecutableAssembly.tt b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/ExecutableAssembly.tt index f387a999b..d3e3ee7e0 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/ExecutableAssembly.tt +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/ExecutableAssembly.tt @@ -43,6 +43,18 @@ public class GeneratedProgram await Amazon.Lambda.RuntimeSupport.LambdaBootstrapBuilder.Create(<#= model.LambdaMethod.ExecutableAssemblyHandlerName #>, new <#= this._lambdaFunctions[0].SerializerInfo.SerializerName #>()).Build().RunAsync(); break; <# + } + else if (!model.GeneratedMethod.Parameters.Any() && model.LambdaMethod.ReturnsVoidTask) + { + // No typed input and Task return: Func is ambiguous between + // Create(Func, ILambdaSerializer) and + // Create(Func, ILambdaSerializer). + // Use the non-generic, non-serializer overload to resolve CS0121. +#> + Func <#= model.LambdaMethod.ExecutableAssemblyHandlerName #> = new <#= model.LambdaMethod.ContainingNamespace #>.<#= model.LambdaMethod.ContainingType.Name #>_<#= model.LambdaMethod.Name #>_Generated().<#= model.LambdaMethod.Name #>; + await Amazon.Lambda.RuntimeSupport.LambdaBootstrapBuilder.Create(<#= model.LambdaMethod.ExecutableAssemblyHandlerName #>).Build().RunAsync(); + break; +<# } else { diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs index 59fa1d830..0e7e76a89 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs @@ -1,3 +1,6 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + using System.Collections.Generic; namespace Amazon.Lambda.Annotations.SourceGenerator @@ -34,6 +37,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 FunctionUrlAttribute = "Amazon.Lambda.Annotations.APIGateway.FunctionUrlAttribute"; + public const string FunctionUrlAuthType = "Amazon.Lambda.Annotations.APIGateway.FunctionUrlAuthType"; + public const string HttpApiAuthorizerAttribute = "Amazon.Lambda.Annotations.APIGateway.HttpApiAuthorizerAttribute"; public const string RestApiAuthorizerAttribute = "Amazon.Lambda.Annotations.APIGateway.RestApiAuthorizerAttribute"; @@ -56,6 +62,15 @@ public static class TypeFullNames public const string S3Event = "Amazon.Lambda.S3Events.S3Event"; public const string S3EventAttribute = "Amazon.Lambda.Annotations.S3.S3EventAttribute"; + public const string DynamoDBEvent = "Amazon.Lambda.DynamoDBEvents.DynamoDBEvent"; + public const string DynamoDBEventAttribute = "Amazon.Lambda.Annotations.DynamoDB.DynamoDBEventAttribute"; + + public const string SNSEvent = "Amazon.Lambda.SNSEvents.SNSEvent"; + public const string SNSEventAttribute = "Amazon.Lambda.Annotations.SNS.SNSEventAttribute"; + + public const string ScheduledEvent = "Amazon.Lambda.CloudWatchEvents.ScheduledEvents.ScheduledEvent"; + public const string ScheduleEventAttribute = "Amazon.Lambda.Annotations.Schedule.ScheduleEventAttribute"; + public const string LambdaSerializerAttribute = "Amazon.Lambda.Core.LambdaSerializerAttribute"; public const string DefaultLambdaSerializer = "Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer"; @@ -82,9 +97,13 @@ public static class TypeFullNames { RestApiAttribute, HttpApiAttribute, + FunctionUrlAttribute, SQSEventAttribute, ALBApiAttribute, - S3EventAttribute + S3EventAttribute, + DynamoDBEventAttribute, + SNSEventAttribute, + ScheduleEventAttribute }; } -} \ No newline at end of file +} diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Validation/LambdaFunctionValidator.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Validation/LambdaFunctionValidator.cs index 7e728660a..25e9d486d 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Validation/LambdaFunctionValidator.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Validation/LambdaFunctionValidator.cs @@ -1,9 +1,15 @@ -using Amazon.Lambda.Annotations.ALB; +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Lambda.Annotations.ALB; using Amazon.Lambda.Annotations.S3; using Amazon.Lambda.Annotations.SourceGenerator.Diagnostics; using Amazon.Lambda.Annotations.SourceGenerator.Extensions; using Amazon.Lambda.Annotations.SourceGenerator.Models; using Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes; +using Amazon.Lambda.Annotations.DynamoDB; +using Amazon.Lambda.Annotations.SNS; +using Amazon.Lambda.Annotations.Schedule; using Amazon.Lambda.Annotations.SQS; using Microsoft.CodeAnalysis; using System.Collections.Generic; @@ -61,6 +67,9 @@ internal static bool ValidateFunction(GeneratorExecutionContext context, IMethod // Validate Events ValidateApiGatewayEvents(lambdaFunctionModel, methodLocation, diagnostics); ValidateSqsEvents(lambdaFunctionModel, methodLocation, diagnostics); + ValidateDynamoDBEvents(lambdaFunctionModel, methodLocation, diagnostics); + ValidateSnsEvents(lambdaFunctionModel, methodLocation, diagnostics); + ValidateScheduleEvents(lambdaFunctionModel, methodLocation, diagnostics); ValidateAlbEvents(lambdaFunctionModel, methodLocation, diagnostics); ValidateS3Events(lambdaFunctionModel, methodLocation, diagnostics); @@ -71,6 +80,7 @@ internal static bool ValidateDependencies(GeneratorExecutionContext context, IMe { // Check for references to "Amazon.Lambda.APIGatewayEvents" if the Lambda method is annotated with RestApi, HttpApi, or authorizer attributes. if (lambdaMethodSymbol.HasAttribute(context, TypeFullNames.RestApiAttribute) || lambdaMethodSymbol.HasAttribute(context, TypeFullNames.HttpApiAttribute) + || lambdaMethodSymbol.HasAttribute(context, TypeFullNames.FunctionUrlAttribute) || lambdaMethodSymbol.HasAttribute(context, TypeFullNames.HttpApiAuthorizerAttribute) || lambdaMethodSymbol.HasAttribute(context, TypeFullNames.RestApiAuthorizerAttribute)) { if (context.Compilation.ReferencedAssemblyNames.FirstOrDefault(x => x.Name == "Amazon.Lambda.APIGatewayEvents") == null) @@ -110,6 +120,36 @@ internal static bool ValidateDependencies(GeneratorExecutionContext context, IMe } } + // Check for references to "Amazon.Lambda.DynamoDBEvents" if the Lambda method is annotated with DynamoDBEvent attribute. + if (lambdaMethodSymbol.HasAttribute(context, TypeFullNames.DynamoDBEventAttribute)) + { + if (context.Compilation.ReferencedAssemblyNames.FirstOrDefault(x => x.Name == "Amazon.Lambda.DynamoDBEvents") == null) + { + diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.MissingDependencies, methodLocation, "Amazon.Lambda.DynamoDBEvents")); + return false; + } + } + + // Check for references to "Amazon.Lambda.SNSEvents" if the Lambda method is annotated with SNSEvent attribute. + if (lambdaMethodSymbol.HasAttribute(context, TypeFullNames.SNSEventAttribute)) + { + if (context.Compilation.ReferencedAssemblyNames.FirstOrDefault(x => x.Name == "Amazon.Lambda.SNSEvents") == null) + { + diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.MissingDependencies, methodLocation, "Amazon.Lambda.SNSEvents")); + return false; + } + } + + // Check for references to "Amazon.Lambda.CloudWatchEvents" if the Lambda method is annotated with ScheduleEvent attribute. + if (lambdaMethodSymbol.HasAttribute(context, TypeFullNames.ScheduleEventAttribute)) + { + if (context.Compilation.ReferencedAssemblyNames.FirstOrDefault(x => x.Name == "Amazon.Lambda.CloudWatchEvents") == null) + { + diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.MissingDependencies, methodLocation, "Amazon.Lambda.CloudWatchEvents")); + return false; + } + } + return true; } @@ -420,6 +460,122 @@ private static void ValidateS3Events(LambdaFunctionModel lambdaFunctionModel, Lo } } + private static void ValidateDynamoDBEvents(LambdaFunctionModel lambdaFunctionModel, Location methodLocation, List diagnostics) + { + if (!lambdaFunctionModel.LambdaMethod.Events.Contains(EventType.DynamoDB)) + { + return; + } + + // Validate DynamoDBEventAttributes + foreach (var att in lambdaFunctionModel.Attributes) + { + if (att.Type.FullName != TypeFullNames.DynamoDBEventAttribute) + continue; + + var dynamoDBEventAttribute = ((AttributeModel)att).Data; + var validationErrors = dynamoDBEventAttribute.Validate(); + validationErrors.ForEach(errorMessage => diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidDynamoDBEventAttribute, methodLocation, errorMessage))); + } + + // Validate method parameters - When using DynamoDBEventAttribute, the method signature must be (DynamoDBEvent evnt) or (DynamoDBEvent evnt, ILambdaContext context) + var parameters = lambdaFunctionModel.LambdaMethod.Parameters; + if (parameters.Count == 0 || + parameters.Count > 2 || + (parameters.Count == 1 && parameters[0].Type.FullName != TypeFullNames.DynamoDBEvent) || + (parameters.Count == 2 && (parameters[0].Type.FullName != TypeFullNames.DynamoDBEvent || parameters[1].Type.FullName != TypeFullNames.ILambdaContext))) + { + var errorMessage = $"When using the {nameof(DynamoDBEventAttribute)}, the Lambda method can accept at most 2 parameters. " + + $"The first parameter is required and must be of type {TypeFullNames.DynamoDBEvent}. " + + $"The second parameter is optional and must be of type {TypeFullNames.ILambdaContext}."; + diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidLambdaMethodSignature, methodLocation, errorMessage)); + } + + // Validate method return type - When using DynamoDBEventAttribute, the return type must be either void or Task + if (!lambdaFunctionModel.LambdaMethod.ReturnsVoid && !lambdaFunctionModel.LambdaMethod.ReturnsVoidTask) + { + var errorMessage = $"When using the {nameof(DynamoDBEventAttribute)}, the Lambda method can return either void or {TypeFullNames.Task}"; + diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidLambdaMethodSignature, methodLocation, errorMessage)); + } + } + + private static void ValidateSnsEvents(LambdaFunctionModel lambdaFunctionModel, Location methodLocation, List diagnostics) + { + if (!lambdaFunctionModel.LambdaMethod.Events.Contains(EventType.SNS)) + { + return; + } + + // Validate SNSEventAttributes + foreach (var att in lambdaFunctionModel.Attributes) + { + if (att.Type.FullName != TypeFullNames.SNSEventAttribute) + continue; + + var snsEventAttribute = ((AttributeModel)att).Data; + var validationErrors = snsEventAttribute.Validate(); + validationErrors.ForEach(errorMessage => diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidSnsEventAttribute, methodLocation, errorMessage))); + } + + // Validate method parameters - When using SNSEventAttribute, the method signature must be (SNSEvent snsEvent) or (SNSEvent snsEvent, ILambdaContext context) + var parameters = lambdaFunctionModel.LambdaMethod.Parameters; + if (parameters.Count == 0 || + parameters.Count > 2 || + (parameters.Count == 1 && parameters[0].Type.FullName != TypeFullNames.SNSEvent) || + (parameters.Count == 2 && (parameters[0].Type.FullName != TypeFullNames.SNSEvent || parameters[1].Type.FullName != TypeFullNames.ILambdaContext))) + { + var errorMessage = $"When using the {nameof(SNSEventAttribute)}, the Lambda method can accept at most 2 parameters. " + + $"The first parameter is required and must be of type {TypeFullNames.SNSEvent}. " + + $"The second parameter is optional and must be of type {TypeFullNames.ILambdaContext}."; + diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidLambdaMethodSignature, methodLocation, errorMessage)); + } + + // Validate method return type - When using SNSEventAttribute, the return type must be either void or Task + if (!lambdaFunctionModel.LambdaMethod.ReturnsVoid && !lambdaFunctionModel.LambdaMethod.ReturnsVoidTask) + { + var errorMessage = $"When using the {nameof(SNSEventAttribute)}, the Lambda method can return either void or {TypeFullNames.Task}"; + diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidLambdaMethodSignature, methodLocation, errorMessage)); + } + } + + private static void ValidateScheduleEvents(LambdaFunctionModel lambdaFunctionModel, Location methodLocation, List diagnostics) + { + if (!lambdaFunctionModel.LambdaMethod.Events.Contains(EventType.Schedule)) + { + return; + } + + foreach (var att in lambdaFunctionModel.Attributes) + { + if (att.Type.FullName != TypeFullNames.ScheduleEventAttribute) + continue; + + var scheduleEventAttribute = ((AttributeModel)att).Data; + var validationErrors = scheduleEventAttribute.Validate(); + validationErrors.ForEach(errorMessage => diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidScheduleEventAttribute, methodLocation, errorMessage))); + } + + // Validate method parameters - When using ScheduleEventAttribute, the method signature must be (ScheduledEvent evnt) or (ScheduledEvent evnt, ILambdaContext context) + var parameters = lambdaFunctionModel.LambdaMethod.Parameters; + if (parameters.Count == 0 || + parameters.Count > 2 || + (parameters.Count == 1 && parameters[0].Type.FullName != TypeFullNames.ScheduledEvent) || + (parameters.Count == 2 && (parameters[0].Type.FullName != TypeFullNames.ScheduledEvent || parameters[1].Type.FullName != TypeFullNames.ILambdaContext))) + { + var errorMessage = $"When using the {nameof(ScheduleEventAttribute)}, the Lambda method can accept at most 2 parameters. " + + $"The first parameter is required and must be of type {TypeFullNames.ScheduledEvent}. " + + $"The second parameter is optional and must be of type {TypeFullNames.ILambdaContext}."; + diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidLambdaMethodSignature, methodLocation, errorMessage)); + } + + // Validate return type - must be void or Task + if (!lambdaFunctionModel.LambdaMethod.ReturnsVoid && !lambdaFunctionModel.LambdaMethod.ReturnsVoidTask) + { + var errorMessage = $"When using the {nameof(ScheduleEventAttribute)}, the Lambda method can return either void or {TypeFullNames.Task}"; + diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidLambdaMethodSignature, methodLocation, errorMessage)); + } + } + private static bool ReportDiagnostics(DiagnosticReporter diagnosticReporter, List diagnostics) { var isValid = true; diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs index a72e3241b..bb8e3733c 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs @@ -1,8 +1,14 @@ -using Amazon.Lambda.Annotations.ALB; +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Lambda.Annotations.ALB; +using Amazon.Lambda.Annotations.DynamoDB; using Amazon.Lambda.Annotations.APIGateway; using Amazon.Lambda.Annotations.SourceGenerator.Diagnostics; using Amazon.Lambda.Annotations.SourceGenerator.FileIO; using Amazon.Lambda.Annotations.SourceGenerator.Models; +using Amazon.Lambda.Annotations.SNS; +using Amazon.Lambda.Annotations.Schedule; using Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes; using Amazon.Lambda.Annotations.S3; using Amazon.Lambda.Annotations.SQS; @@ -10,6 +16,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.IO; using System.Linq; using System.Reflection; using AuthorizerType = Amazon.Lambda.Annotations.SourceGenerator.Models.AuthorizerType; @@ -39,6 +46,7 @@ public class CloudFormationWriter : IAnnotationReportWriter private readonly IDirectoryManager _directoryManager; private readonly ITemplateWriter _templateWriter; private readonly IDiagnosticReporter _diagnosticReporter; + private string _projectRootDirectory; public CloudFormationWriter(IFileManager fileManager, IDirectoryManager directoryManager, ITemplateWriter templateWriter, IDiagnosticReporter diagnosticReporter) { @@ -53,6 +61,7 @@ public CloudFormationWriter(IFileManager fileManager, IDirectoryManager director /// public void ApplyReport(AnnotationReport report) { + _projectRootDirectory = report.ProjectRootDirectory; var originalContent = _fileManager.ReadAllText(report.CloudFormationTemplatePath); var templateDirectory = _directoryManager.GetDirectoryName(report.CloudFormationTemplatePath); var relativeProjectUri = _directoryManager.GetRelativePath(templateDirectory, report.ProjectRootDirectory); @@ -206,6 +215,7 @@ private void ProcessLambdaFunctionEventAttributes(ILambdaFunctionSerializable la var currentSyncedEvents = new List(); var currentSyncedEventProperties = new Dictionary>(); var currentAlbResources = new List(); + var hasFunctionUrl = false; foreach (var attributeModel in lambdaFunction.Attributes) { @@ -232,6 +242,35 @@ private void ProcessLambdaFunctionEventAttributes(ILambdaFunctionSerializable la eventName = ProcessS3Attribute(lambdaFunction, s3AttributeModel.Data, currentSyncedEventProperties); currentSyncedEvents.Add(eventName); break; + case AttributeModel functionUrlAttributeModel: + ProcessFunctionUrlAttribute(lambdaFunction, functionUrlAttributeModel.Data); + _templateWriter.SetToken($"Resources.{lambdaFunction.ResourceName}.Metadata.SyncedFunctionUrlConfig", true); + hasFunctionUrl = true; + break; + case AttributeModel dynamoDBAttributeModel: + eventName = ProcessDynamoDBAttribute(lambdaFunction, dynamoDBAttributeModel.Data, currentSyncedEventProperties); + currentSyncedEvents.Add(eventName); + break; + case AttributeModel snsAttributeModel: + eventName = ProcessSnsAttribute(lambdaFunction, snsAttributeModel.Data, currentSyncedEventProperties); + currentSyncedEvents.Add(eventName); + break; + case AttributeModel scheduleAttributeModel: + eventName = ProcessScheduleAttribute(lambdaFunction, scheduleAttributeModel.Data, currentSyncedEventProperties); + currentSyncedEvents.Add(eventName); + break; + } + } + + // Remove FunctionUrlConfig only if it was previously created by Annotations (tracked via metadata). + // This preserves any manually-added FunctionUrlConfig that was not created by the source generator. + if (!hasFunctionUrl) + { + var syncedFunctionUrlConfigPath = $"Resources.{lambdaFunction.ResourceName}.Metadata.SyncedFunctionUrlConfig"; + if (_templateWriter.GetToken(syncedFunctionUrlConfigPath, false)) + { + _templateWriter.RemoveToken($"Resources.{lambdaFunction.ResourceName}.Properties.FunctionUrlConfig"); + _templateWriter.RemoveToken(syncedFunctionUrlConfigPath); } } @@ -302,6 +341,50 @@ private string ProcessHttpApiAttribute(ILambdaFunctionSerializable lambdaFunctio return eventName; } + /// + /// Writes the configuration to the serverless template. + /// Unlike HttpApi/RestApi, Function URLs are configured as a property on the function resource + /// rather than as an event source. + /// + private void ProcessFunctionUrlAttribute(ILambdaFunctionSerializable lambdaFunction, FunctionUrlAttribute functionUrlAttribute) + { + var functionUrlConfigPath = $"Resources.{lambdaFunction.ResourceName}.Properties.FunctionUrlConfig"; + _templateWriter.SetToken($"{functionUrlConfigPath}.AuthType", functionUrlAttribute.AuthType.ToString()); + + // Always remove the existing Cors block first to clear any stale properties + // from a previous generation pass, then re-emit only the currently configured values. + var corsPath = $"{functionUrlConfigPath}.Cors"; + _templateWriter.RemoveToken(corsPath); + + var hasCors = functionUrlAttribute.AllowOrigins != null + || functionUrlAttribute.AllowMethods != null + || functionUrlAttribute.AllowHeaders != null + || functionUrlAttribute.ExposeHeaders != null + || functionUrlAttribute.AllowCredentials + || functionUrlAttribute.MaxAge > 0; + + if (hasCors) + { + if (functionUrlAttribute.AllowOrigins != null) + _templateWriter.SetToken($"{corsPath}.AllowOrigins", new List(functionUrlAttribute.AllowOrigins), TokenType.List); + + if (functionUrlAttribute.AllowMethods != null) + _templateWriter.SetToken($"{corsPath}.AllowMethods", functionUrlAttribute.AllowMethods.Select(m => m == LambdaHttpMethod.Any ? "*" : m.ToString().ToUpper()).ToList(), TokenType.List); + + if (functionUrlAttribute.AllowHeaders != null) + _templateWriter.SetToken($"{corsPath}.AllowHeaders", new List(functionUrlAttribute.AllowHeaders), TokenType.List); + + if (functionUrlAttribute.ExposeHeaders != null) + _templateWriter.SetToken($"{corsPath}.ExposeHeaders", new List(functionUrlAttribute.ExposeHeaders), TokenType.List); + + if (functionUrlAttribute.AllowCredentials) + _templateWriter.SetToken($"{corsPath}.AllowCredentials", true); + + if (functionUrlAttribute.MaxAge > 0) + _templateWriter.SetToken($"{corsPath}.MaxAge", functionUrlAttribute.MaxAge); + } + } + /// /// 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. @@ -608,6 +691,174 @@ private string ProcessSqsAttribute(ILambdaFunctionSerializable lambdaFunction, S return att.ResourceName; } + /// + /// Writes all properties associated with to the serverless template. + /// + private string ProcessDynamoDBAttribute(ILambdaFunctionSerializable lambdaFunction, DynamoDBEventAttribute att, Dictionary> syncedEventProperties) + { + var eventName = att.ResourceName; + var eventPath = $"Resources.{lambdaFunction.ResourceName}.Properties.Events.{eventName}"; + + _templateWriter.SetToken($"{eventPath}.Type", "DynamoDB"); + + // Stream + _templateWriter.RemoveToken($"{eventPath}.Properties.Stream"); + if (!att.Stream.StartsWith("@")) + { + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Stream", att.Stream); + } + else + { + var resource = att.Stream.Substring(1); + if (_templateWriter.Exists($"{PARAMETERS}.{resource}")) + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, $"Stream.{REF}", resource); + else + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, $"Stream.{GET_ATTRIBUTE}", new List { resource, "StreamArn" }, TokenType.List); + } + + // StartingPosition + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "StartingPosition", att.StartingPosition.ToString()); + + // BatchSize + if (att.IsBatchSizeSet) + { + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "BatchSize", att.BatchSize); + } + + // Enabled + if (att.IsEnabledSet) + { + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Enabled", att.Enabled); + } + + // MaximumBatchingWindowInSeconds + if (att.IsMaximumBatchingWindowInSecondsSet) + { + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "MaximumBatchingWindowInSeconds", att.MaximumBatchingWindowInSeconds); + } + + // FilterCriteria + if (att.IsFiltersSet) + { + const char SEPERATOR = ';'; + var filters = att.Filters.Split(SEPERATOR).Select(x => x.Trim()).ToList(); + var filterList = new List>(); + foreach (var filter in filters) + { + filterList.Add(new Dictionary { { "Pattern", filter } }); + } + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "FilterCriteria.Filters", filterList, TokenType.List); + } + + return att.ResourceName; + } + + /// + /// Writes all properties associated with to the serverless template. + /// + private string ProcessSnsAttribute(ILambdaFunctionSerializable lambdaFunction, SNSEventAttribute att, Dictionary> syncedEventProperties) + { + var eventName = att.ResourceName; + var eventPath = $"Resources.{lambdaFunction.ResourceName}.Properties.Events.{eventName}"; + + _templateWriter.SetToken($"{eventPath}.Type", "SNS"); + + // Topic - SNS topics use Ref to get the ARN + _templateWriter.RemoveToken($"{eventPath}.Properties.Topic"); + if (!att.Topic.StartsWith("@")) + { + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Topic", att.Topic); + } + else + { + var topic = att.Topic.Substring(1); + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, $"Topic.{REF}", topic); + } + + // FilterPolicy + if (att.IsFilterPolicySet) + { + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "FilterPolicy", att.FilterPolicy); + } + + // Enabled + if (att.IsEnabledSet) + { + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Enabled", att.Enabled); + } + + return att.ResourceName; + } + + /// + /// Writes all properties associated with to the serverless template. + /// + private string ProcessScheduleAttribute(ILambdaFunctionSerializable lambdaFunction, ScheduleEventAttribute att, Dictionary> syncedEventProperties) + { + var eventName = att.ResourceName; + var eventPath = $"Resources.{lambdaFunction.ResourceName}.Properties.Events.{eventName}"; + + _templateWriter.SetToken($"{eventPath}.Type", "Schedule"); + + // Schedule expression + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Schedule", att.Schedule); + + // Description + if (att.IsDescriptionSet) + { + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Description", att.Description); + } + + // Input - supports literal JSON strings or file paths (relative to project root or absolute) + if (att.IsInputSet) + { + var inputValue = ResolveInputValue(att.Input); + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Input", inputValue); + } + + // Enabled + if (att.IsEnabledSet) + { + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Enabled", att.Enabled); + } + + return att.ResourceName; + } + + /// + /// Resolves the Input value for a schedule event. If the value is a file path (relative to the project root + /// or absolute) that points to an existing file, the file contents are read and returned. + /// Otherwise, the original value is returned as-is (treated as a literal JSON string). + /// + /// The Input value from the attribute, which may be a JSON string or a file path. + /// The resolved input value — either the file contents or the original string. + private string ResolveInputValue(string input) + { + if (string.IsNullOrEmpty(input)) + { + return input; + } + + // Try as a path relative to the project root directory + if (!string.IsNullOrEmpty(_projectRootDirectory)) + { + var relativePath = Path.Combine(_projectRootDirectory, input); + if (_fileManager.Exists(relativePath)) + { + return _fileManager.ReadAllText(relativePath); + } + } + + // Try as an absolute path + if (Path.IsPathRooted(input) && _fileManager.Exists(input)) + { + return _fileManager.ReadAllText(input); + } + + // Not a file path — return as-is (literal JSON string) + return input; + } + /// /// Writes all properties associated with to the serverless template. /// @@ -1182,4 +1433,4 @@ private void SynchronizeEventsAndProperties(List syncedEvents, Dictionar _templateWriter.SetToken(syncedEventPropertiesPath, syncedEventProperties, TokenType.KeyVal); } } -} \ No newline at end of file +} diff --git a/Libraries/src/Amazon.Lambda.Annotations/APIGateway/FunctionUrlAttribute.cs b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/FunctionUrlAttribute.cs new file mode 100644 index 000000000..a92387762 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/FunctionUrlAttribute.cs @@ -0,0 +1,51 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; + +namespace Amazon.Lambda.Annotations.APIGateway +{ + /// + /// Configures the Lambda function to be invoked via a Lambda Function URL. + /// + /// + /// Function URLs use the same payload format as HTTP API v2 (APIGatewayHttpApiV2ProxyRequest/Response). + /// + [AttributeUsage(AttributeTargets.Method)] + public class FunctionUrlAttribute : Attribute + { + /// + public FunctionUrlAuthType AuthType { get; set; } = FunctionUrlAuthType.NONE; + + /// + /// The allowed origins for CORS requests. Example: new[] { "https://example.com" } + /// + public string[] AllowOrigins { get; set; } + + /// + /// The allowed HTTP methods for CORS requests. Example: new[] { LambdaHttpMethod.Get, LambdaHttpMethod.Post } + /// + public LambdaHttpMethod[] AllowMethods { get; set; } + + /// + /// The allowed headers for CORS requests. + /// + public string[] AllowHeaders { get; set; } + + /// + /// Whether credentials are included in the CORS request. + /// + public bool AllowCredentials { get; set; } + + /// + /// The expose headers for CORS responses. + /// + public string[] ExposeHeaders { get; set; } + + /// + /// The maximum time in seconds that a browser can cache the CORS preflight response. + /// A value of 0 means the property is not set. + /// + public int MaxAge { get; set; } + } +} diff --git a/Libraries/src/Amazon.Lambda.Annotations/APIGateway/FunctionUrlAuthType.cs b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/FunctionUrlAuthType.cs new file mode 100644 index 000000000..31a1c2397 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/FunctionUrlAuthType.cs @@ -0,0 +1,21 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace Amazon.Lambda.Annotations.APIGateway +{ + /// + /// The type of authentication for a Lambda Function URL. + /// + public enum FunctionUrlAuthType + { + /// + /// No authentication. Anyone with the Function URL can invoke the function. + /// + NONE, + + /// + /// IAM authentication. Only authenticated IAM users and roles can invoke the function. + /// + AWS_IAM + } +} diff --git a/Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpApiAuthorizerAttribute.cs b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpApiAuthorizerAttribute.cs index 725c2842a..743fbb614 100644 --- a/Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpApiAuthorizerAttribute.cs +++ b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpApiAuthorizerAttribute.cs @@ -62,4 +62,4 @@ public class HttpApiAuthorizerAttribute : Attribute /// public int ResultTtlInSeconds { get; set; } = 0; } -} \ No newline at end of file +} diff --git a/Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpResults.cs b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpResults.cs index 539da4546..ece87ff63 100644 --- a/Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpResults.cs +++ b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpResults.cs @@ -5,7 +5,7 @@ using System.Net; using Amazon.Lambda.Core; -#if NET6_0_OR_GREATER +#if !NETSTANDARD2_0 using System.Buffers; using System.Text.Json; using System.Text.Json.Serialization; diff --git a/Libraries/src/Amazon.Lambda.Annotations/APIGateway/RestApiAuthorizerAttribute.cs b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/RestApiAuthorizerAttribute.cs index d6db9fa04..935597261 100644 --- a/Libraries/src/Amazon.Lambda.Annotations/APIGateway/RestApiAuthorizerAttribute.cs +++ b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/RestApiAuthorizerAttribute.cs @@ -69,4 +69,4 @@ public class RestApiAuthorizerAttribute : Attribute /// public int ResultTtlInSeconds { get; set; } = 0; } -} \ No newline at end of file +} diff --git a/Libraries/src/Amazon.Lambda.Annotations/Amazon.Lambda.Annotations.csproj b/Libraries/src/Amazon.Lambda.Annotations/Amazon.Lambda.Annotations.csproj index 1fcc58ea6..956466ead 100644 --- a/Libraries/src/Amazon.Lambda.Annotations/Amazon.Lambda.Annotations.csproj +++ b/Libraries/src/Amazon.Lambda.Annotations/Amazon.Lambda.Annotations.csproj @@ -2,7 +2,13 @@ Amazon.Lambda.Annotations - netstandard2.0;net6.0;net8.0 + + netstandard2.0;net8.0;net10.0 true false @@ -11,11 +17,11 @@ ..\..\..\buildtools\public.snk true - 1.12.0 + 2.0.1 true - + IL2026,IL2067,IL2075,IL3050 true true diff --git a/Libraries/src/Amazon.Lambda.Annotations/DynamoDB/DynamoDBEventAttribute.cs b/Libraries/src/Amazon.Lambda.Annotations/DynamoDB/DynamoDBEventAttribute.cs new file mode 100644 index 000000000..f407ba609 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations/DynamoDB/DynamoDBEventAttribute.cs @@ -0,0 +1,154 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Amazon.Lambda.Annotations.DynamoDB +{ + /// + /// This attribute defines the DynamoDB event source configuration for a Lambda function. + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + public class DynamoDBEventAttribute : Attribute + { + private static readonly Regex _resourceNameRegex = new Regex("^[a-zA-Z0-9]+$"); + + /// + /// The DynamoDB stream that will act as the event trigger for the Lambda function. + /// This can either be the stream ARN or reference to the DynamoDB table resource that is already defined in the serverless template. + /// To reference a DynamoDB table resource in the serverless template, prefix the resource name with "@" symbol. + /// + public string Stream { get; set; } + + /// + /// The CloudFormation resource name for the DynamoDB event source mapping. + /// + public string ResourceName + { + get + { + if (IsResourceNameSet) + { + return resourceName; + } + + if (string.IsNullOrWhiteSpace(Stream)) + { + return string.Empty; + } + + if (Stream.StartsWith("@")) + { + return Stream.Length > 1 ? Stream.Substring(1) : string.Empty; + } + + // DynamoDB stream ARN format: arn:aws:dynamodb:region:account:table/TableName/stream/timestamp + var arnParts = Stream.Split('/'); + if (arnParts.Length >= 2) + { + var tableName = arnParts[1]; + return string.Join(string.Empty, tableName.Where(char.IsLetterOrDigit)); + } + return string.Join(string.Empty, Stream.Where(char.IsLetterOrDigit)); + } + set => resourceName = value; + } + + private string resourceName { get; set; } = null; + internal bool IsResourceNameSet => resourceName != null; + + /// + /// The maximum number of records in each batch that Lambda pulls from the stream. + /// Default value is 100. + /// + public uint BatchSize + { + get => batchSize.GetValueOrDefault(100); + set => batchSize = value; + } + private uint? batchSize { get; set; } + internal bool IsBatchSizeSet => batchSize.HasValue; + + /// + /// The position in the stream where Lambda starts reading. Valid values are TRIM_HORIZON and LATEST. + /// Default value is LATEST. + /// + public StartingPosition StartingPosition { get; set; } = StartingPosition.LATEST; + internal bool IsStartingPositionSet => true; + + /// + /// If set to false, the event source mapping will be disabled. Default value is true. + /// + public bool Enabled + { + get => enabled.GetValueOrDefault(true); + set => enabled = value; + } + private bool? enabled { get; set; } + internal bool IsEnabledSet => enabled.HasValue; + + /// + /// The maximum amount of time, in seconds, to gather records before invoking the function. + /// + public uint MaximumBatchingWindowInSeconds + { + get => maximumBatchingWindowInSeconds.GetValueOrDefault(); + set => maximumBatchingWindowInSeconds = value; + } + private uint? maximumBatchingWindowInSeconds { get; set; } + internal bool IsMaximumBatchingWindowInSecondsSet => maximumBatchingWindowInSeconds.HasValue; + + /// + /// A collection of semicolon (;) separated strings where each string denotes a filter pattern. + /// + public string Filters { get; set; } = null; + internal bool IsFiltersSet => Filters != null; + + /// + /// Creates an instance of the class. + /// + /// property + public DynamoDBEventAttribute(string stream) + { + Stream = stream; + } + + internal List Validate() + { + var validationErrors = new List(); + + if (IsBatchSizeSet && (BatchSize < 1 || BatchSize > 10000)) + { + validationErrors.Add($"{nameof(DynamoDBEventAttribute.BatchSize)} = {BatchSize}. It must be between 1 and 10000"); + } + if (IsMaximumBatchingWindowInSecondsSet && MaximumBatchingWindowInSeconds > 300) + { + validationErrors.Add($"{nameof(DynamoDBEventAttribute.MaximumBatchingWindowInSeconds)} = {MaximumBatchingWindowInSeconds}. It must be between 0 and 300"); + } + if (string.IsNullOrWhiteSpace(Stream)) + { + validationErrors.Add($"{nameof(DynamoDBEventAttribute.Stream)} must not be null or empty"); + } + else if (Stream.StartsWith("@") && string.IsNullOrWhiteSpace(Stream.Substring(1))) + { + validationErrors.Add($"{nameof(DynamoDBEventAttribute.Stream)} = {Stream}. The '@' prefix must be followed by a non-empty resource or parameter name"); + } + else if (!Stream.StartsWith("@")) + { + if (!Stream.Contains(":dynamodb:") || !Stream.Contains("/stream/")) + { + validationErrors.Add($"{nameof(DynamoDBEventAttribute.Stream)} = {Stream}. The DynamoDB stream ARN is invalid"); + } + } + if (IsResourceNameSet && !_resourceNameRegex.IsMatch(ResourceName)) + { + validationErrors.Add($"{nameof(DynamoDBEventAttribute.ResourceName)} = {ResourceName}. It must only contain alphanumeric characters and must not be an empty string"); + } + + return validationErrors; + } + } +} diff --git a/Libraries/src/Amazon.Lambda.Annotations/DynamoDB/StartingPosition.cs b/Libraries/src/Amazon.Lambda.Annotations/DynamoDB/StartingPosition.cs new file mode 100644 index 000000000..5b1cc881a --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations/DynamoDB/StartingPosition.cs @@ -0,0 +1,22 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace Amazon.Lambda.Annotations.DynamoDB +{ + /// + /// The position in the DynamoDB stream where Lambda starts reading. + /// + public enum StartingPosition + { + /// + /// Start reading at the most recent record in the shard. + /// + LATEST, + + /// + /// Start reading at the last untrimmed record in the shard. + /// This is the oldest record in the shard. + /// + TRIM_HORIZON + } +} diff --git a/Libraries/src/Amazon.Lambda.Annotations/README.md b/Libraries/src/Amazon.Lambda.Annotations/README.md index d736e06be..56cb7bbd7 100644 --- a/Libraries/src/Amazon.Lambda.Annotations/README.md +++ b/Libraries/src/Amazon.Lambda.Annotations/README.md @@ -20,6 +20,7 @@ Topics: - [Amazon S3 example](#amazon-s3-example) - [SQS Event Example](#sqs-event-example) - [Application Load Balancer (ALB) Example](#application-load-balancer-alb-example) + - [Lambda Function URL Example](#lambda-function-url-example) - [Custom Lambda Authorizer Example](#custom-lambda-authorizer-example) - [HTTP API Authorizer](#http-api-authorizer) - [REST API Authorizer](#rest-api-authorizer) @@ -1073,6 +1074,128 @@ The `ALBApi` attribute requires an existing ALB listener. Here is a minimal exam Then your Lambda function references `@MyListener` in the `ALBApi` attribute. +## SNS Event Example +This example shows how to use the `SNSEvent` attribute to subscribe a Lambda function to an SNS topic. + +The `SNSEvent` attribute contains the following properties: +* **Topic** (Required) - The SNS topic ARN or a reference to an SNS topic resource prefixed with "@". +* **ResourceName** (Optional) - The CloudFormation resource name for the SNS event. +* **FilterPolicy** (Optional) - A JSON filter policy applied to the subscription. +* **Enabled** (Optional) - If false, the event source is disabled. Default is true. + +```csharp +[LambdaFunction(ResourceName = "SNSMessageHandler", Policies = "AWSLambdaSNSTopicExecutionRole")] +[SNSEvent("@TestTopic", ResourceName = "TestTopicEvent", FilterPolicy = "{ \"store\": [\"example_corp\"] }")] +public void HandleMessage(SNSEvent evnt, ILambdaContext lambdaContext) +{ + lambdaContext.Logger.Log($"Received {evnt.Records.Count} messages"); +} +``` + +## Lambda Function URL Example + +[Lambda Function URLs](https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html) provide a dedicated HTTPS endpoint for your Lambda function without needing API Gateway or an Application Load Balancer. The `FunctionUrl` attribute configures the function to be invoked via a Function URL. Function URLs use the same payload format as HTTP API v2 (`APIGatewayHttpApiV2ProxyRequest`/`APIGatewayHttpApiV2ProxyResponse`). + +The `FunctionUrl` attribute contains the following properties: + +| Property | Type | Required | Default | Description | +|---|---|---|---|---| +| `AuthType` | `FunctionUrlAuthType` | No | `NONE` | The authentication type: `NONE` (public) or `AWS_IAM` (IAM-authenticated). | +| `AllowOrigins` | `string[]` | No | `null` | Allowed origins for CORS requests (e.g., `new[] { "https://example.com" }`). | +| `AllowMethods` | `LambdaHttpMethod[]` | No | `null` | Allowed HTTP methods for CORS requests, using the `LambdaHttpMethod` enum (e.g., `new[] { LambdaHttpMethod.Get, LambdaHttpMethod.Post }`). | +| `AllowHeaders` | `string[]` | No | `null` | Allowed headers for CORS requests. | +| `ExposeHeaders` | `string[]` | No | `null` | Headers to expose in CORS responses. | +| `AllowCredentials` | `bool` | No | `false` | Whether credentials are included in the CORS request. | +| `MaxAge` | `int` | No | `0` | Maximum time in seconds that a browser can cache the CORS preflight response. `0` means not set. | + +### Basic Example + +A simple function with a public Function URL (no authentication): + +```csharp +using Amazon.Lambda.Annotations; +using Amazon.Lambda.Annotations.APIGateway; +using Amazon.Lambda.Core; + +public class Functions +{ + [LambdaFunction(PackageType = LambdaPackageType.Image)] + [FunctionUrl(AuthType = FunctionUrlAuthType.NONE)] + public IHttpResult GetItems([FromQuery] string category, ILambdaContext context) + { + context.Logger.LogLine($"Getting items for category: {category}"); + return HttpResults.Ok(new { items = new[] { "item1", "item2" }, category }); + } +} +``` + +### With IAM Authentication + +Use `FunctionUrlAuthType.AWS_IAM` to require IAM authentication for the Function URL: + +```csharp +[LambdaFunction(PackageType = LambdaPackageType.Image)] +[FunctionUrl(AuthType = FunctionUrlAuthType.AWS_IAM)] +public IHttpResult SecureEndpoint(ILambdaContext context) +{ + return HttpResults.Ok(new { message = "This endpoint requires IAM auth" }); +} +``` + +### With CORS Configuration + +Configure CORS settings directly on the attribute. The `AllowMethods` property uses the type-safe `LambdaHttpMethod` enum, consistent with the `HttpApi` and `RestApi` attributes: + +```csharp +[LambdaFunction(PackageType = LambdaPackageType.Image)] +[FunctionUrl( + AuthType = FunctionUrlAuthType.NONE, + AllowOrigins = new[] { "https://example.com", "https://app.example.com" }, + AllowMethods = new[] { LambdaHttpMethod.Get, LambdaHttpMethod.Post }, + AllowHeaders = new[] { "Content-Type", "Authorization" }, + AllowCredentials = true, + MaxAge = 3600)] +public IHttpResult GetData([FromQuery] string id, ILambdaContext context) +{ + return HttpResults.Ok(new { id, data = "some data" }); +} +``` + +### Generated CloudFormation + +The source generator creates a `FunctionUrlConfig` property on the Lambda function resource (not a SAM event source). Here is an example with CORS: + +```json +"GetDataFunction": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedFunctionUrlConfig": true + }, + "Properties": { + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": ["MyAssembly::MyNamespace.Functions_GetData_Generated::GetData"] + }, + "MemorySize": 512, + "Timeout": 30, + "FunctionUrlConfig": { + "AuthType": "NONE", + "Cors": { + "AllowOrigins": ["https://example.com", "https://app.example.com"], + "AllowMethods": ["GET", "POST"], + "AllowHeaders": ["Content-Type", "Authorization"], + "AllowCredentials": true, + "MaxAge": 3600 + } + } + } +} +``` + +> **Note:** Unlike `HttpApi` and `RestApi` which create SAM event sources, `FunctionUrl` configures the `FunctionUrlConfig` property directly on the function resource. If the `FunctionUrl` attribute is removed from the code, the source generator will automatically clean up the `FunctionUrlConfig` from the CloudFormation template. + ## 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. @@ -1420,8 +1543,12 @@ parameter to the `LambdaFunction` must be the event object and the event source * 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. +* SNSEvent + * Subscribes the Lambda function to an SNS topic. The topic ARN or resource reference (prefixed with '@') is required. * ALBApi * Configures the Lambda function to be called from an Application Load Balancer. The listener ARN (or `@ResourceName` template reference), path pattern, and priority are required. The source generator creates standalone CloudFormation resources (TargetGroup, ListenerRule, Lambda Permission) rather than SAM event types. +* FunctionUrl + * Configures the Lambda function to be invoked via a Lambda Function URL. Supports `AuthType` (`NONE` or `AWS_IAM`) and CORS configuration including `AllowMethods` (using the `LambdaHttpMethod` enum), `AllowOrigins`, `AllowHeaders`, `AllowCredentials`, and `MaxAge`. The source generator writes a `FunctionUrlConfig` property on the function resource rather than a SAM event source. ### Parameter Attributes diff --git a/Libraries/src/Amazon.Lambda.Annotations/SNS/SNSEventAttribute.cs b/Libraries/src/Amazon.Lambda.Annotations/SNS/SNSEventAttribute.cs new file mode 100644 index 000000000..044b26499 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations/SNS/SNSEventAttribute.cs @@ -0,0 +1,116 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Amazon.Lambda.Annotations.SNS +{ + /// + /// This attribute defines the SNS event source configuration for a Lambda function. + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + public class SNSEventAttribute : Attribute + { + private static readonly Regex _resourceNameRegex = new Regex("^[a-zA-Z0-9]+$"); + + /// + /// The SNS topic that will act as the event trigger for the Lambda function. + /// This can either be the topic ARN or reference to the SNS topic resource that is already defined in the serverless template. + /// To reference an SNS topic resource in the serverless template, prefix the resource name with "@" symbol. + /// + public string Topic { get; set; } + + /// + /// The CloudFormation resource name for the SNS event. By default this is set to the SNS topic name if the is set to an SNS topic ARN. + /// If is set to an existing CloudFormation resource, than that is used as the default value without the "@" prefix. + /// + public string ResourceName + { + get + { + if (IsResourceNameSet) + { + return resourceName; + } + if (string.IsNullOrEmpty(Topic)) + { + return string.Empty; + } + if (Topic.StartsWith("@")) + { + return Topic.Substring(1); + } + + var arnTokens = Topic.Split(new char[] { ':' }, 6); + if (arnTokens.Length < 6) + { + return Topic; + } + var topicName = arnTokens[5]; + var sanitizedTopicName = string.Join(string.Empty, topicName.Where(char.IsLetterOrDigit)); + return sanitizedTopicName; + } + set => resourceName = value; + } + + private string resourceName { get; set; } = null; + internal bool IsResourceNameSet => resourceName != null; + + /// + /// A JSON filter policy that is applied to the SNS subscription. + /// Only messages matching the filter policy will be delivered to the Lambda function. + /// + public string FilterPolicy { get; set; } = null; + internal bool IsFilterPolicySet => FilterPolicy != null; + + /// + /// If set to false, the event source will be disabled. + /// Default value is true. + /// + public bool Enabled + { + get => enabled.GetValueOrDefault(true); + set => enabled = value; + } + private bool? enabled { get; set; } + internal bool IsEnabledSet => enabled.HasValue; + + /// + /// Creates an instance of the class. + /// + /// property + public SNSEventAttribute(string topic) + { + Topic = topic; + } + + internal List Validate() + { + var validationErrors = new List(); + + if (string.IsNullOrEmpty(Topic)) + { + validationErrors.Add($"{nameof(SNSEventAttribute.Topic)} is required and must not be null or empty"); + return validationErrors; + } + + if (!Topic.StartsWith("@")) + { + var arnTokens = Topic.Split(new char[] { ':' }, 6); + if (arnTokens.Length != 6) + { + validationErrors.Add($"{nameof(SNSEventAttribute.Topic)} = {Topic}. The SNS topic ARN is invalid. The ARN format is 'arn::sns:::'"); + } + } + if (IsResourceNameSet && !_resourceNameRegex.IsMatch(ResourceName)) + { + validationErrors.Add($"{nameof(SNSEventAttribute.ResourceName)} = {ResourceName}. It must only contain alphanumeric characters and must not be an empty string"); + } + + return validationErrors; + } + } +} diff --git a/Libraries/src/Amazon.Lambda.Annotations/SQS/SQSEventAttribute.cs b/Libraries/src/Amazon.Lambda.Annotations/SQS/SQSEventAttribute.cs index 3358eee36..8f2ca892e 100644 --- a/Libraries/src/Amazon.Lambda.Annotations/SQS/SQSEventAttribute.cs +++ b/Libraries/src/Amazon.Lambda.Annotations/SQS/SQSEventAttribute.cs @@ -1,4 +1,7 @@ -using System; +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; @@ -60,7 +63,7 @@ public string ResourceName /// public bool Enabled { - get => enabled.GetValueOrDefault(); + get => enabled.GetValueOrDefault(true); set => enabled = value; } private bool? enabled { get; set; } diff --git a/Libraries/src/Amazon.Lambda.Annotations/Schedule/ScheduleEventAttribute.cs b/Libraries/src/Amazon.Lambda.Annotations/Schedule/ScheduleEventAttribute.cs new file mode 100644 index 000000000..a6b750425 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations/Schedule/ScheduleEventAttribute.cs @@ -0,0 +1,102 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Amazon.Lambda.Annotations.Schedule +{ + /// + /// This attribute defines the Schedule event source configuration for a Lambda function. + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public class ScheduleEventAttribute : Attribute + { + private static readonly Regex _resourceNameRegex = new Regex("^[a-zA-Z0-9]+$"); + + /// + /// The schedule expression. Supports rate and cron expressions. + /// Examples: "rate(5 minutes)", "cron(0 12 * * ? *)" + /// + public string Schedule { get; set; } + + /// + /// The CloudFormation resource name for the schedule event. + /// + public string ResourceName + { + get + { + if (IsResourceNameSet) + { + return resourceName; + } + // Generate a default resource name from the schedule expression + var sanitized = string.Join(string.Empty, (Schedule ?? string.Empty).Where(char.IsLetterOrDigit)); + return sanitized.Length > 0 ? sanitized : "ScheduleEvent"; + } + set => resourceName = value; + } + + private string resourceName { get; set; } = null; + internal bool IsResourceNameSet => resourceName != null; + + /// + /// A description for the schedule rule. + /// + public string Description { get; set; } = null; + internal bool IsDescriptionSet => Description != null; + + /// + /// A JSON string to pass as input to the Lambda function. + /// This can also be a file path (relative to the project root or absolute) pointing to a JSON file. + /// If the value resolves to an existing file, its contents will be read and used as the input. + /// Examples: "{\"key\": \"value\"}", "./schedule-input.json", "C:\config\input.json" + /// + public string Input { get; set; } = null; + internal bool IsInputSet => Input != null; + + /// + /// If set to false, the event source mapping will be disabled. Default value is true. + /// + public bool Enabled + { + get => enabled.GetValueOrDefault(true); + set => enabled = value; + } + private bool? enabled { get; set; } + internal bool IsEnabledSet => enabled.HasValue; + + /// + /// Creates an instance of the class. + /// + /// property + public ScheduleEventAttribute(string schedule) + { + Schedule = schedule; + } + + internal List Validate() + { + var validationErrors = new List(); + + if (string.IsNullOrEmpty(Schedule)) + { + validationErrors.Add($"{nameof(ScheduleEventAttribute.Schedule)} must not be null or empty"); + } + else if (!Schedule.StartsWith("rate(") && !Schedule.StartsWith("cron(")) + { + validationErrors.Add($"{nameof(ScheduleEventAttribute.Schedule)} = {Schedule}. It must start with 'rate(' or 'cron('"); + } + + if (IsResourceNameSet && !_resourceNameRegex.IsMatch(ResourceName)) + { + validationErrors.Add($"{nameof(ScheduleEventAttribute.ResourceName)} = {ResourceName}. It must only contain alphanumeric characters and must not be an empty string"); + } + + return validationErrors; + } + } +} diff --git a/Libraries/src/Amazon.Lambda.AppSyncEvents/Amazon.Lambda.AppSyncEvents.csproj b/Libraries/src/Amazon.Lambda.AppSyncEvents/Amazon.Lambda.AppSyncEvents.csproj index 6fd4746ca..0aaf9afcf 100644 --- a/Libraries/src/Amazon.Lambda.AppSyncEvents/Amazon.Lambda.AppSyncEvents.csproj +++ b/Libraries/src/Amazon.Lambda.AppSyncEvents/Amazon.Lambda.AppSyncEvents.csproj @@ -4,9 +4,9 @@ Amazon Lambda .NET support - AWS AppSync package. - net8.0 + $(DefaultPackageTargets) Amazon.Lambda.AppSyncEvents - 1.0.0 + 2.0.0 Amazon.Lambda.AppSyncEvents Amazon.Lambda.AppSyncEvents AWS;Amazon;Lambda diff --git a/Libraries/src/Amazon.Lambda.ApplicationLoadBalancerEvents/Amazon.Lambda.ApplicationLoadBalancerEvents.csproj b/Libraries/src/Amazon.Lambda.ApplicationLoadBalancerEvents/Amazon.Lambda.ApplicationLoadBalancerEvents.csproj index c24491ec6..1a8212adf 100644 --- a/Libraries/src/Amazon.Lambda.ApplicationLoadBalancerEvents/Amazon.Lambda.ApplicationLoadBalancerEvents.csproj +++ b/Libraries/src/Amazon.Lambda.ApplicationLoadBalancerEvents/Amazon.Lambda.ApplicationLoadBalancerEvents.csproj @@ -3,10 +3,10 @@ - netstandard2.0;netcoreapp3.1;net8.0 + $(DefaultPackageTargets) Amazon Lambda .NET Core support - Application Load Balancer package. Amazon.Lambda.ApplicationLoadBalancerEvents - 2.2.1 + 3.0.0 Amazon.Lambda.ApplicationLoadBalancerEvents Amazon.Lambda.ApplicationLoadBalancerEvents AWS;Amazon;Lambda @@ -18,7 +18,7 @@ false - + IL2026,IL2067,IL2075 true true diff --git a/Libraries/src/Amazon.Lambda.ApplicationLoadBalancerEvents/ApplicationLoadBalancerResponse.cs b/Libraries/src/Amazon.Lambda.ApplicationLoadBalancerEvents/ApplicationLoadBalancerResponse.cs index 70b200fc8..39e9a0888 100644 --- a/Libraries/src/Amazon.Lambda.ApplicationLoadBalancerEvents/ApplicationLoadBalancerResponse.cs +++ b/Libraries/src/Amazon.Lambda.ApplicationLoadBalancerEvents/ApplicationLoadBalancerResponse.cs @@ -14,18 +14,14 @@ public class ApplicationLoadBalancerResponse /// The HTTP status code for the request /// [DataMember(Name = "statusCode")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("statusCode")] -#endif public int StatusCode { get; set; } /// /// The HTTP status description for the request /// [DataMember(Name = "statusDescription")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("statusDescription")] -#endif public string StatusDescription { get; set; } /// @@ -33,9 +29,7 @@ public class ApplicationLoadBalancerResponse /// Note: Use this property when "Multi value headers" is disabled on ELB Target Group. /// [DataMember(Name = "headers")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("headers")] -#endif public IDictionary Headers { get; set; } /// @@ -43,27 +37,21 @@ public class ApplicationLoadBalancerResponse /// Note: Use this property when "Multi value headers" is enabled on ELB Target Group. /// [DataMember(Name = "multiValueHeaders")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("multiValueHeaders")] -#endif public IDictionary> MultiValueHeaders { get; set; } /// /// The response body /// [DataMember(Name = "body")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("body")] -#endif public string Body { get; set; } /// /// Flag indicating whether the body should be treated as a base64-encoded string /// [DataMember(Name = "isBase64Encoded")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("isBase64Encoded")] -#endif public bool IsBase64Encoded { get; set; } } -} \ No newline at end of file +} diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Amazon.Lambda.AspNetCoreServer.Hosting.csproj b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Amazon.Lambda.AspNetCoreServer.Hosting.csproj index 2ed732314..f805b50ff 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Amazon.Lambda.AspNetCoreServer.Hosting.csproj +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Amazon.Lambda.AspNetCoreServer.Hosting.csproj @@ -4,10 +4,10 @@ Package for running ASP.NET Core applications using the Minimal API style as a AWS Lambda function. - net6.0;net8.0 + $(DefaultPackageTargets) enable enable - 1.10.0 + 2.1.0 README.md Amazon.Lambda.AspNetCoreServer.Hosting Amazon.Lambda.AspNetCoreServer.Hosting diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/HostingOptions.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/HostingOptions.cs index d4fd7937c..cf7ef34ba 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/HostingOptions.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/HostingOptions.cs @@ -9,11 +9,17 @@ namespace Amazon.Lambda.AspNetCoreServer.Hosting; /// public class HostingOptions { + internal const string ParameterizedPreviewMessage = + "Response streaming is in preview till a new version of .NET Lambda runtime client that supports response streaming " + + "has been deployed to the .NET Lambda managed runtime. Till deployment has been made the feature can be used by deploying as an " + + "executable including the latest version of Amazon.Lambda.RuntimeSupport and setting the \"EnablePreviewFeatures\" in the Lambda " + + "project file to \"true\""; + /// /// The ILambdaSerializer used by Lambda to convert the incoming event JSON into the .NET event type and serialize the .NET response type /// back to JSON to return to Lambda. /// - public ILambdaSerializer Serializer { get; set; } + public ILambdaSerializer? Serializer { get; set; } /// /// The default response content encoding to use when no explicit content type or content encoding mapping is registered. @@ -27,6 +33,15 @@ public class HostingOptions /// public bool IncludeUnhandledExceptionDetailInResponse { get; set; } = false; + /// + /// When true, the Lambda hosting server enables Lambda response streaming behavior + /// when invoking FunctionHandlerAsync. In streaming mode, + /// FunctionHandlerAsync writes directly to the Lambda response stream and + /// returns null. Requires net8.0 or later. + /// + [System.Runtime.Versioning.RequiresPreviewFeatures(ParameterizedPreviewMessage)] + public bool EnableResponseStreaming { get; set; } = false; + /// /// Callback invoked after request marshalling to customize the HTTP request feature. /// Receives the IHttpRequestFeature, Lambda request object, and ILambdaContext. diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/GetBeforeSnapshotRequestsCollector.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/GetBeforeSnapshotRequestsCollector.cs index 8cbb12d8f..1d9ee854f 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/GetBeforeSnapshotRequestsCollector.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/GetBeforeSnapshotRequestsCollector.cs @@ -5,7 +5,6 @@ namespace Amazon.Lambda.AspNetCoreServer.Hosting.Internal; -#if NET8_0_OR_GREATER /// /// Helper class for storing Requests for /// @@ -14,4 +13,3 @@ internal class GetBeforeSnapshotRequestsCollector { public HttpRequestMessage? Request { get; set; } } -#endif diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs index f50a37f7b..b99d62a86 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/Internal/LambdaRuntimeSupportServer.cs @@ -78,8 +78,13 @@ public APIGatewayHttpApiV2LambdaRuntimeSupportServer(IServiceProvider servicePro /// protected override HandlerWrapper CreateHandlerWrapper(IServiceProvider serviceProvider) { - var handler = new APIGatewayHttpApiV2MinimalApi(serviceProvider).FunctionHandlerAsync; - return HandlerWrapper.GetHandlerWrapper(handler, this.Serializer); + var handler = new APIGatewayHttpApiV2MinimalApi(serviceProvider); +#pragma warning disable CA2252 + var hostingOptions = serviceProvider.GetService(); + handler.EnableResponseStreaming = hostingOptions?.EnableResponseStreaming ?? false; +#pragma warning restore CA2252 + Func> bufferedHandler = handler.FunctionHandlerAsync; + return HandlerWrapper.GetHandlerWrapper(bufferedHandler, Serializer); } /// @@ -87,9 +92,7 @@ protected override HandlerWrapper CreateHandlerWrapper(IServiceProvider serviceP /// public class APIGatewayHttpApiV2MinimalApi : APIGatewayHttpApiV2ProxyFunction { - #if NET8_0_OR_GREATER private readonly IEnumerable _beforeSnapshotRequestsCollectors; - #endif private readonly HostingOptions? _hostingOptions; /// @@ -99,9 +102,7 @@ public class APIGatewayHttpApiV2MinimalApi : APIGatewayHttpApiV2ProxyFunction public APIGatewayHttpApiV2MinimalApi(IServiceProvider serviceProvider) : base(serviceProvider) { - #if NET8_0_OR_GREATER _beforeSnapshotRequestsCollectors = serviceProvider.GetServices(); - #endif // Retrieve HostingOptions from service provider (may be null for backward compatibility) _hostingOptions = serviceProvider.GetService(); @@ -127,15 +128,15 @@ public APIGatewayHttpApiV2MinimalApi(IServiceProvider serviceProvider) } } - #if NET8_0_OR_GREATER + /// protected override IEnumerable GetBeforeSnapshotRequests() { foreach (var collector in _beforeSnapshotRequestsCollectors) if (collector.Request != null) yield return collector.Request; } - #endif + /// protected override void PostMarshallRequestFeature(IHttpRequestFeature aspNetCoreRequestFeature, APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest lambdaRequest, ILambdaContext lambdaContext) { base.PostMarshallRequestFeature(aspNetCoreRequestFeature, lambdaRequest, lambdaContext); @@ -144,6 +145,7 @@ protected override void PostMarshallRequestFeature(IHttpRequestFeature aspNetCor _hostingOptions?.PostMarshallRequestFeature?.Invoke(aspNetCoreRequestFeature, lambdaRequest, lambdaContext); } + /// protected override void PostMarshallResponseFeature(IHttpResponseFeature aspNetCoreResponseFeature, APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse lambdaResponse, ILambdaContext lambdaContext) { base.PostMarshallResponseFeature(aspNetCoreResponseFeature, lambdaResponse, lambdaContext); @@ -152,6 +154,7 @@ protected override void PostMarshallResponseFeature(IHttpResponseFeature aspNetC _hostingOptions?.PostMarshallResponseFeature?.Invoke(aspNetCoreResponseFeature, lambdaResponse, lambdaContext); } + /// protected override void PostMarshallConnectionFeature(IHttpConnectionFeature aspNetCoreConnectionFeature, APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest lambdaRequest, ILambdaContext lambdaContext) { base.PostMarshallConnectionFeature(aspNetCoreConnectionFeature, lambdaRequest, lambdaContext); @@ -160,6 +163,7 @@ protected override void PostMarshallConnectionFeature(IHttpConnectionFeature asp _hostingOptions?.PostMarshallConnectionFeature?.Invoke(aspNetCoreConnectionFeature, lambdaRequest, lambdaContext); } + /// protected override void PostMarshallHttpAuthenticationFeature(IHttpAuthenticationFeature aspNetCoreHttpAuthenticationFeature, APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest lambdaRequest, ILambdaContext lambdaContext) { base.PostMarshallHttpAuthenticationFeature(aspNetCoreHttpAuthenticationFeature, lambdaRequest, lambdaContext); @@ -168,6 +172,7 @@ protected override void PostMarshallHttpAuthenticationFeature(IHttpAuthenticatio _hostingOptions?.PostMarshallHttpAuthenticationFeature?.Invoke(aspNetCoreHttpAuthenticationFeature, lambdaRequest, lambdaContext); } + /// protected override void PostMarshallTlsConnectionFeature(ITlsConnectionFeature aspNetCoreConnectionFeature, APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest lambdaRequest, ILambdaContext lambdaContext) { base.PostMarshallTlsConnectionFeature(aspNetCoreConnectionFeature, lambdaRequest, lambdaContext); @@ -176,6 +181,7 @@ protected override void PostMarshallTlsConnectionFeature(ITlsConnectionFeature a _hostingOptions?.PostMarshallTlsConnectionFeature?.Invoke(aspNetCoreConnectionFeature, lambdaRequest, lambdaContext); } + /// protected override void PostMarshallItemsFeatureFeature(IItemsFeature aspNetCoreItemFeature, APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest lambdaRequest, ILambdaContext lambdaContext) { base.PostMarshallItemsFeatureFeature(aspNetCoreItemFeature, lambdaRequest, lambdaContext); @@ -208,8 +214,13 @@ public APIGatewayRestApiLambdaRuntimeSupportServer(IServiceProvider serviceProvi /// protected override HandlerWrapper CreateHandlerWrapper(IServiceProvider serviceProvider) { - var handler = new APIGatewayRestApiMinimalApi(serviceProvider).FunctionHandlerAsync; - return HandlerWrapper.GetHandlerWrapper(handler, this.Serializer); + var handler = new APIGatewayRestApiMinimalApi(serviceProvider); +#pragma warning disable CA2252 + var hostingOptions = serviceProvider.GetService(); + handler.EnableResponseStreaming = hostingOptions?.EnableResponseStreaming ?? false; +#pragma warning restore CA2252 + Func> bufferedHandler = handler.FunctionHandlerAsync; + return HandlerWrapper.GetHandlerWrapper(bufferedHandler, Serializer); } /// @@ -217,9 +228,7 @@ protected override HandlerWrapper CreateHandlerWrapper(IServiceProvider serviceP /// public class APIGatewayRestApiMinimalApi : APIGatewayProxyFunction { - #if NET8_0_OR_GREATER private readonly IEnumerable _beforeSnapshotRequestsCollectors; - #endif private readonly HostingOptions? _hostingOptions; /// @@ -229,9 +238,7 @@ public class APIGatewayRestApiMinimalApi : APIGatewayProxyFunction public APIGatewayRestApiMinimalApi(IServiceProvider serviceProvider) : base(serviceProvider) { - #if NET8_0_OR_GREATER _beforeSnapshotRequestsCollectors = serviceProvider.GetServices(); - #endif // Retrieve HostingOptions from service provider (may be null for backward compatibility) _hostingOptions = serviceProvider.GetService(); @@ -257,15 +264,15 @@ public APIGatewayRestApiMinimalApi(IServiceProvider serviceProvider) } } - #if NET8_0_OR_GREATER + /// protected override IEnumerable GetBeforeSnapshotRequests() { foreach (var collector in _beforeSnapshotRequestsCollectors) if (collector.Request != null) yield return collector.Request; } - #endif + /// protected override void PostMarshallRequestFeature(IHttpRequestFeature aspNetCoreRequestFeature, APIGatewayEvents.APIGatewayProxyRequest lambdaRequest, ILambdaContext lambdaContext) { base.PostMarshallRequestFeature(aspNetCoreRequestFeature, lambdaRequest, lambdaContext); @@ -274,6 +281,7 @@ protected override void PostMarshallRequestFeature(IHttpRequestFeature aspNetCor _hostingOptions?.PostMarshallRequestFeature?.Invoke(aspNetCoreRequestFeature, lambdaRequest, lambdaContext); } + /// protected override void PostMarshallResponseFeature(IHttpResponseFeature aspNetCoreResponseFeature, APIGatewayEvents.APIGatewayProxyResponse lambdaResponse, ILambdaContext lambdaContext) { base.PostMarshallResponseFeature(aspNetCoreResponseFeature, lambdaResponse, lambdaContext); @@ -282,6 +290,7 @@ protected override void PostMarshallResponseFeature(IHttpResponseFeature aspNetC _hostingOptions?.PostMarshallResponseFeature?.Invoke(aspNetCoreResponseFeature, lambdaResponse, lambdaContext); } + /// protected override void PostMarshallConnectionFeature(IHttpConnectionFeature aspNetCoreConnectionFeature, APIGatewayEvents.APIGatewayProxyRequest lambdaRequest, ILambdaContext lambdaContext) { base.PostMarshallConnectionFeature(aspNetCoreConnectionFeature, lambdaRequest, lambdaContext); @@ -290,6 +299,7 @@ protected override void PostMarshallConnectionFeature(IHttpConnectionFeature asp _hostingOptions?.PostMarshallConnectionFeature?.Invoke(aspNetCoreConnectionFeature, lambdaRequest, lambdaContext); } + /// protected override void PostMarshallHttpAuthenticationFeature(IHttpAuthenticationFeature aspNetCoreHttpAuthenticationFeature, APIGatewayEvents.APIGatewayProxyRequest lambdaRequest, ILambdaContext lambdaContext) { base.PostMarshallHttpAuthenticationFeature(aspNetCoreHttpAuthenticationFeature, lambdaRequest, lambdaContext); @@ -298,6 +308,7 @@ protected override void PostMarshallHttpAuthenticationFeature(IHttpAuthenticatio _hostingOptions?.PostMarshallHttpAuthenticationFeature?.Invoke(aspNetCoreHttpAuthenticationFeature, lambdaRequest, lambdaContext); } + /// protected override void PostMarshallTlsConnectionFeature(ITlsConnectionFeature aspNetCoreConnectionFeature, APIGatewayEvents.APIGatewayProxyRequest lambdaRequest, ILambdaContext lambdaContext) { base.PostMarshallTlsConnectionFeature(aspNetCoreConnectionFeature, lambdaRequest, lambdaContext); @@ -306,6 +317,7 @@ protected override void PostMarshallTlsConnectionFeature(ITlsConnectionFeature a _hostingOptions?.PostMarshallTlsConnectionFeature?.Invoke(aspNetCoreConnectionFeature, lambdaRequest, lambdaContext); } + /// protected override void PostMarshallItemsFeatureFeature(IItemsFeature aspNetCoreItemFeature, APIGatewayEvents.APIGatewayProxyRequest lambdaRequest, ILambdaContext lambdaContext) { base.PostMarshallItemsFeatureFeature(aspNetCoreItemFeature, lambdaRequest, lambdaContext); @@ -338,8 +350,13 @@ public ApplicationLoadBalancerLambdaRuntimeSupportServer(IServiceProvider servic /// protected override HandlerWrapper CreateHandlerWrapper(IServiceProvider serviceProvider) { - var handler = new ApplicationLoadBalancerMinimalApi(serviceProvider).FunctionHandlerAsync; - return HandlerWrapper.GetHandlerWrapper(handler, this.Serializer); + var handler = new ApplicationLoadBalancerMinimalApi(serviceProvider); +#pragma warning disable CA2252 + var hostingOptions = serviceProvider.GetService(); + handler.EnableResponseStreaming = hostingOptions?.EnableResponseStreaming ?? false; +#pragma warning restore CA2252 + Func> bufferedHandler = handler.FunctionHandlerAsync; + return HandlerWrapper.GetHandlerWrapper(bufferedHandler, Serializer); } /// @@ -347,9 +364,7 @@ protected override HandlerWrapper CreateHandlerWrapper(IServiceProvider serviceP /// public class ApplicationLoadBalancerMinimalApi : ApplicationLoadBalancerFunction { - #if NET8_0_OR_GREATER private readonly IEnumerable _beforeSnapshotRequestsCollectors; - #endif private readonly HostingOptions? _hostingOptions; /// @@ -359,9 +374,7 @@ public class ApplicationLoadBalancerMinimalApi : ApplicationLoadBalancerFunction public ApplicationLoadBalancerMinimalApi(IServiceProvider serviceProvider) : base(serviceProvider) { - #if NET8_0_OR_GREATER _beforeSnapshotRequestsCollectors = serviceProvider.GetServices(); - #endif // Retrieve HostingOptions from service provider (may be null for backward compatibility) _hostingOptions = serviceProvider.GetService(); @@ -387,15 +400,15 @@ public ApplicationLoadBalancerMinimalApi(IServiceProvider serviceProvider) } } - #if NET8_0_OR_GREATER + /// protected override IEnumerable GetBeforeSnapshotRequests() { foreach (var collector in _beforeSnapshotRequestsCollectors) if (collector.Request != null) yield return collector.Request; } - #endif + /// protected override void PostMarshallRequestFeature(IHttpRequestFeature aspNetCoreRequestFeature, ApplicationLoadBalancerEvents.ApplicationLoadBalancerRequest lambdaRequest, ILambdaContext lambdaContext) { base.PostMarshallRequestFeature(aspNetCoreRequestFeature, lambdaRequest, lambdaContext); @@ -404,6 +417,7 @@ protected override void PostMarshallRequestFeature(IHttpRequestFeature aspNetCor _hostingOptions?.PostMarshallRequestFeature?.Invoke(aspNetCoreRequestFeature, lambdaRequest, lambdaContext); } + /// protected override void PostMarshallResponseFeature(IHttpResponseFeature aspNetCoreResponseFeature, ApplicationLoadBalancerEvents.ApplicationLoadBalancerResponse lambdaResponse, ILambdaContext lambdaContext) { base.PostMarshallResponseFeature(aspNetCoreResponseFeature, lambdaResponse, lambdaContext); @@ -412,6 +426,7 @@ protected override void PostMarshallResponseFeature(IHttpResponseFeature aspNetC _hostingOptions?.PostMarshallResponseFeature?.Invoke(aspNetCoreResponseFeature, lambdaResponse, lambdaContext); } + /// protected override void PostMarshallConnectionFeature(IHttpConnectionFeature aspNetCoreConnectionFeature, ApplicationLoadBalancerEvents.ApplicationLoadBalancerRequest lambdaRequest, ILambdaContext lambdaContext) { base.PostMarshallConnectionFeature(aspNetCoreConnectionFeature, lambdaRequest, lambdaContext); @@ -420,6 +435,7 @@ protected override void PostMarshallConnectionFeature(IHttpConnectionFeature asp _hostingOptions?.PostMarshallConnectionFeature?.Invoke(aspNetCoreConnectionFeature, lambdaRequest, lambdaContext); } + /// protected override void PostMarshallHttpAuthenticationFeature(IHttpAuthenticationFeature aspNetCoreHttpAuthenticationFeature, ApplicationLoadBalancerEvents.ApplicationLoadBalancerRequest lambdaRequest, ILambdaContext lambdaContext) { base.PostMarshallHttpAuthenticationFeature(aspNetCoreHttpAuthenticationFeature, lambdaRequest, lambdaContext); @@ -428,6 +444,7 @@ protected override void PostMarshallHttpAuthenticationFeature(IHttpAuthenticatio _hostingOptions?.PostMarshallHttpAuthenticationFeature?.Invoke(aspNetCoreHttpAuthenticationFeature, lambdaRequest, lambdaContext); } + /// protected override void PostMarshallTlsConnectionFeature(ITlsConnectionFeature aspNetCoreConnectionFeature, ApplicationLoadBalancerEvents.ApplicationLoadBalancerRequest lambdaRequest, ILambdaContext lambdaContext) { base.PostMarshallTlsConnectionFeature(aspNetCoreConnectionFeature, lambdaRequest, lambdaContext); @@ -436,6 +453,7 @@ protected override void PostMarshallTlsConnectionFeature(ITlsConnectionFeature a _hostingOptions?.PostMarshallTlsConnectionFeature?.Invoke(aspNetCoreConnectionFeature, lambdaRequest, lambdaContext); } + /// protected override void PostMarshallItemsFeatureFeature(IItemsFeature aspNetCoreItemFeature, ApplicationLoadBalancerEvents.ApplicationLoadBalancerRequest lambdaRequest, ILambdaContext lambdaContext) { base.PostMarshallItemsFeatureFeature(aspNetCoreItemFeature, lambdaRequest, lambdaContext); @@ -446,4 +464,70 @@ protected override void PostMarshallItemsFeatureFeature(IItemsFeature aspNetCore } } } + + /// + /// IServer for handling Lambda events from an API Gateway Websocket API. + /// + public class APIGatewayWebsocketApiLambdaRuntimeSupportServer : APIGatewayRestApiLambdaRuntimeSupportServer + { + /// + /// Create instances + /// + /// The IServiceProvider created for the ASP.NET Core application + public APIGatewayWebsocketApiLambdaRuntimeSupportServer(IServiceProvider serviceProvider) + : base(serviceProvider) + { + } + + /// + /// Creates HandlerWrapper for processing events from API Gateway Websocket API + /// + /// + /// + protected override HandlerWrapper CreateHandlerWrapper(IServiceProvider serviceProvider) + { + var handler = new APIGatewayWebsocketMinimalApi(serviceProvider); +#pragma warning disable CA2252 + var hostingOptions = serviceProvider.GetService(); + handler.EnableResponseStreaming = hostingOptions?.EnableResponseStreaming ?? false; +#pragma warning restore CA2252 + Func> bufferedHandler = handler.FunctionHandlerAsync; + return HandlerWrapper.GetHandlerWrapper(bufferedHandler, Serializer); + } + + /// + /// MinimalApi variant of APIGatewayWebsocketApiProxyFunction. Reuses the REST API MinimalApi plumbing + /// (snapshot collectors, hosting options, PostMarshall callbacks) and applies the websocket-specific + /// request transformations on top. + /// + public class APIGatewayWebsocketMinimalApi : APIGatewayRestApiLambdaRuntimeSupportServer.APIGatewayRestApiMinimalApi + { + /// + /// Create instances + /// + /// The IServiceProvider created for the ASP.NET Core application + public APIGatewayWebsocketMinimalApi(IServiceProvider serviceProvider) + : base(serviceProvider) + { + } + + /// + protected override string ParseHttpPath(APIGatewayEvents.APIGatewayProxyRequest apiGatewayRequest) + => "/" + System.Net.WebUtility.UrlDecode(apiGatewayRequest.RequestContext.RouteKey ?? string.Empty); + + /// + protected override string ParseHttpMethod(APIGatewayEvents.APIGatewayProxyRequest apiGatewayRequest) => "POST"; + + /// + protected override Microsoft.AspNetCore.Http.IHeaderDictionary AddMissingRequestHeaders(APIGatewayEvents.APIGatewayProxyRequest apiGatewayRequest, Microsoft.AspNetCore.Http.IHeaderDictionary headers) + { + headers = base.AddMissingRequestHeaders(apiGatewayRequest, headers); + if (!headers.ContainsKey("Content-Type")) + { + headers["Content-Type"] = "application/json"; + } + return headers; + } + } + } } diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/README.md b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/README.md index 68d287fa5..0098747bf 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/README.md +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/README.md @@ -50,6 +50,10 @@ app.Run(); ``` +## Handler Configuration + +The Lambda function handler must be set to the assembly name (e.g., `MyLambdaProject`). The `AddAWSLambdaHosting` method sets up the Lambda runtime client and registers the callback for processing Lambda events, so the handler should not use the class library format (`::::`). + ## Extension Points `AddAWSLambdaHosting` accepts an optional `HostingOptions` configuration action that exposes the same customization hooks available in the traditional `AbstractAspNetCoreFunction` base class approach. diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs index aa952bc54..7471b22dd 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer.Hosting/ServiceCollectionExtensions.cs @@ -27,7 +27,12 @@ public enum LambdaEventSource /// /// ELB Application Load Balancer /// - ApplicationLoadBalancer + ApplicationLoadBalancer, + + /// + /// API Gateway WebSocket API + /// + WebsocketApi } /// @@ -82,17 +87,20 @@ public static IServiceCollection AddAWSLambdaHosting(this IServiceCollection ser { if(TryLambdaSetup(services, eventSource, configure, out var hostingOptions)) { - services.TryAddSingleton(serializer ?? hostingOptions!.Serializer); + var localSerializer = serializer ?? hostingOptions!.Serializer; + if (localSerializer == null) + throw new ArgumentNullException(nameof(serializer)); + + services.TryAddSingleton(localSerializer); } return services; } - #if NET8_0_OR_GREATER /// /// Adds a > that will be used to invoke /// Routes in your lambda function in order to initialize the ASP.NET Core and Lambda pipelines - /// during . This improves the performance gains + /// during . This improves the performance gains /// offered by SnapStart. /// /// must have a relative @@ -105,7 +113,7 @@ public static IServiceCollection AddAWSLambdaHosting(this IServiceCollection ser /// When the function handler is called as part of SnapStart warm up, the instance will use a /// mock , which will not be fully populated. /// - /// This method automatically registers with . + /// This method automatically registers with . /// /// This method can be called multiple times to register additional urls. /// @@ -142,7 +150,6 @@ public static IServiceCollection AddAWSLambdaBeforeSnapshotRequest(this IService return services; } - #endif private static bool TryLambdaSetup(IServiceCollection services, LambdaEventSource eventSource, Action? configure, out HostingOptions? hostingOptions) { @@ -165,6 +172,7 @@ private static bool TryLambdaSetup(IServiceCollection services, LambdaEventSourc LambdaEventSource.HttpApi => typeof(APIGatewayHttpApiV2LambdaRuntimeSupportServer), LambdaEventSource.RestApi => typeof(APIGatewayRestApiLambdaRuntimeSupportServer), LambdaEventSource.ApplicationLoadBalancer => typeof(ApplicationLoadBalancerLambdaRuntimeSupportServer), + LambdaEventSource.WebsocketApi => typeof(APIGatewayWebsocketApiLambdaRuntimeSupportServer), _ => throw new ArgumentException($"Event source type {eventSource} unknown") }; diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayHttpApiV2ProxyFunction.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayHttpApiV2ProxyFunction.cs index a7bcd519d..819306c30 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayHttpApiV2ProxyFunction.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayHttpApiV2ProxyFunction.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -54,6 +54,44 @@ private protected override void InternalCustomResponseExceptionHandling(APIGatew apiGatewayResponse.SetHeaderValues("ErrorType", ex.GetType().Name, false); } + /// + /// Override for HTTP API v2 to use single-value headers in the streaming prelude + /// instead of multiValueHeaders. API Gateway HTTP API v2 expects the headers + /// format; using multiValueHeaders causes a 500 Internal Server Error. + /// + [System.Runtime.Versioning.RequiresPreviewFeatures(ParameterizedPreviewMessage)] + protected override Amazon.Lambda.Core.ResponseStreaming.HttpResponseStreamPrelude BuildStreamingPrelude(IHttpResponseFeature responseFeature) + { + var prelude = new Amazon.Lambda.Core.ResponseStreaming.HttpResponseStreamPrelude + { + StatusCode = (System.Net.HttpStatusCode)(responseFeature.StatusCode != 0 ? responseFeature.StatusCode : 200) + }; + + foreach (var kvp in responseFeature.Headers) + { + if (string.Equals(kvp.Key, "Content-Length", StringComparison.OrdinalIgnoreCase) || + string.Equals(kvp.Key, "Transfer-Encoding", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + if (string.Equals(kvp.Key, "Set-Cookie", StringComparison.OrdinalIgnoreCase)) + { + foreach (var value in kvp.Value) + { + prelude.Cookies.Add(value); + } + } + else + { + // HTTP API v2 uses single-value headers. Join multiple values with ", ". + prelude.Headers[kvp.Key] = string.Join(", ", kvp.Value); + } + } + + return prelude; + } + /// /// Convert the JSON document received from API Gateway into the InvokeFeatures object. /// InvokeFeatures is then passed into IHttpApplication to create the ASP.NET Core request objects. @@ -247,6 +285,8 @@ protected override APIGatewayHttpApiV2ProxyResponse MarshallResponse(IHttpRespon response.Headers["Content-Type"] = null; } +// Disabled in case the user's ASP.NET Core application is still using the older API that set the body on the response feature instead of the new API that sets the body on the HttpResponse object. +#pragma warning disable CS0618 if (responseFeatures.Body != null) { // Figure out how we should treat the response content, check encoding first to see if body is compressed, then check content type @@ -259,6 +299,7 @@ protected override APIGatewayHttpApiV2ProxyResponse MarshallResponse(IHttpRespon (response.Body, response.IsBase64Encoded) = Utilities.ConvertAspNetCoreBodyToLambdaBody(responseFeatures.Body, rcEncoding); } +#pragma warning restore CS0618 PostMarshallResponseFeature(responseFeatures, response, lambdaContext); diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayProxyFunction.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayProxyFunction.cs index 841b3b1d5..1398239de 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayProxyFunction.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayProxyFunction.cs @@ -1,10 +1,11 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Security.Claims; using System.Text; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; @@ -104,6 +105,49 @@ private protected override void InternalCustomResponseExceptionHandling(APIGatew apiGatewayResponse.MultiValueHeaders["ErrorType"] = new List { ex.GetType().Name }; } + /// + /// Builds an from the current + /// ASP.NET Core response feature. The status code defaults to 200 when + /// is 0. Set-Cookie header values are moved to ; + /// all other headers are placed in . + /// + /// The ASP.NET Core response feature for the current invocation. + /// A populated . + [System.Runtime.Versioning.RequiresPreviewFeatures(ParameterizedPreviewMessage)] + protected override Amazon.Lambda.Core.ResponseStreaming.HttpResponseStreamPrelude BuildStreamingPrelude(IHttpResponseFeature responseFeature) + { + var prelude = new Amazon.Lambda.Core.ResponseStreaming.HttpResponseStreamPrelude + { + StatusCode = (System.Net.HttpStatusCode)(responseFeature.StatusCode != 0 ? responseFeature.StatusCode : 200) + }; + + foreach (var kvp in responseFeature.Headers) + { + // Skip hop-by-hop and framing headers that are meaningless for streaming + // responses. Content-Length conflicts with chunked transfer encoding and + // can cause API Gateway to reject the response with a 502. + if (string.Equals(kvp.Key, "Content-Length", StringComparison.OrdinalIgnoreCase) || + string.Equals(kvp.Key, "Transfer-Encoding", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + if (string.Equals(kvp.Key, "Set-Cookie", StringComparison.OrdinalIgnoreCase)) + { + foreach (var value in kvp.Value) + { + prelude.Cookies.Add(value); + } + } + else + { + prelude.MultiValueHeaders[kvp.Key] = kvp.Value.ToArray(); + } + } + + return prelude; + } + /// /// Convert the JSON document received from API Gateway into the InvokeFeatures object. @@ -151,33 +195,9 @@ protected override void MarshallRequest(InvokeFeatures features, APIGatewayProxy { var requestFeatures = (IHttpRequestFeature)features; requestFeatures.Scheme = "https"; - requestFeatures.Method = apiGatewayRequest.HttpMethod; - - string path = null; - - // Replaces {proxy+} in path, if exists - if (apiGatewayRequest.PathParameters != null && apiGatewayRequest.PathParameters.TryGetValue("proxy", out var proxy) && - !string.IsNullOrEmpty(apiGatewayRequest.Resource)) - { - var proxyPath = proxy; - path = apiGatewayRequest.Resource.Replace("{proxy+}", proxyPath); - - // Adds all the rest of non greedy parameters in apiGateway.Resource to the path - foreach (var pathParameter in apiGatewayRequest.PathParameters.Where(pp => pp.Key != "proxy")) - { - path = path.Replace($"{{{pathParameter.Key}}}", pathParameter.Value); - } - } - - if (string.IsNullOrEmpty(path)) - { - path = apiGatewayRequest.Path; - } + requestFeatures.Method = this.ParseHttpMethod(apiGatewayRequest); - if (!path.StartsWith("/")) - { - path = "/" + path; - } + string path = this.ParseHttpPath(apiGatewayRequest); var rawQueryString = Utilities.CreateQueryStringParameters( apiGatewayRequest.QueryStringParameters, apiGatewayRequest.MultiValueQueryStringParameters, true); @@ -214,13 +234,7 @@ protected override void MarshallRequest(InvokeFeatures features, APIGatewayProxy Utilities.SetHeadersCollection(requestFeatures.Headers, apiGatewayRequest.Headers, apiGatewayRequest.MultiValueHeaders); - if (!requestFeatures.Headers.ContainsKey("Host")) - { - var apiId = apiGatewayRequest?.RequestContext?.ApiId ?? ""; - var stage = apiGatewayRequest?.RequestContext?.Stage ?? ""; - - requestFeatures.Headers["Host"] = $"apigateway-{apiId}-{stage}"; - } + requestFeatures.Headers = this.AddMissingRequestHeaders(apiGatewayRequest, requestFeatures.Headers); if (!string.IsNullOrEmpty(apiGatewayRequest.Body)) @@ -256,6 +270,7 @@ protected override void MarshallRequest(InvokeFeatures features, APIGatewayProxy { connectionFeatures.RemotePort = int.Parse(forwardedPort, CultureInfo.InvariantCulture); } + connectionFeatures.ConnectionId = apiGatewayRequest.RequestContext?.ConnectionId; // Call consumers customize method in case they want to change how API Gateway's request // was marshalled into ASP.NET Core request. @@ -316,6 +331,8 @@ protected override APIGatewayProxyResponse MarshallResponse(IHttpResponseFeature response.MultiValueHeaders["Content-Type"] = new List() { null }; } +// Disabled in case the user's ASP.NET Core application is still using the older API that set the body on the response feature instead of the new API that sets the body on the HttpResponse object. +#pragma warning disable CS0618 if (responseFeatures.Body != null) { // Figure out how we should treat the response content, check encoding first to see if body is compressed, then check content type @@ -328,12 +345,74 @@ protected override APIGatewayProxyResponse MarshallResponse(IHttpResponseFeature (response.Body, response.IsBase64Encoded) = Utilities.ConvertAspNetCoreBodyToLambdaBody(responseFeatures.Body, rcEncoding); } - +#pragma warning restore CS0618 PostMarshallResponseFeature(responseFeatures, response, lambdaContext); _logger.LogDebug($"Response Base 64 Encoded: {response.IsBase64Encoded}"); return response; } + + /// + /// Determines the path that should be assigned to for this request. + /// The default implementation honors {proxy+} resource templates and falls back to . + /// Subclasses can override to derive the path from a different source (e.g. websocket route keys). + /// + protected virtual string ParseHttpPath(APIGatewayProxyRequest apiGatewayRequest) + { + string path = null; + + // Replaces {proxy+} in path, if exists + if (apiGatewayRequest.PathParameters != null && apiGatewayRequest.PathParameters.TryGetValue("proxy", out var proxy) && + !string.IsNullOrEmpty(apiGatewayRequest.Resource)) + { + var proxyPath = proxy; + path = apiGatewayRequest.Resource.Replace("{proxy+}", proxyPath); + + // Adds all the rest of non greedy parameters in apiGateway.Resource to the path + foreach (var pathParameter in apiGatewayRequest.PathParameters.Where(pp => pp.Key != "proxy")) + { + path = path.Replace($"{{{pathParameter.Key}}}", pathParameter.Value); + } + } + + if (string.IsNullOrEmpty(path)) + { + path = apiGatewayRequest.Path; + } + + if (!path.StartsWith("/")) + { + path = "/" + path; + } + return path; + } + + /// + /// Determines the HTTP method that should be assigned to . + /// The default returns ; subclasses can override to force + /// a fixed method (e.g. websocket events are always exposed as POST). + /// + protected virtual string ParseHttpMethod(APIGatewayProxyRequest apiGatewayRequest) + { + return apiGatewayRequest.HttpMethod; + } + + /// + /// Adds any headers that API Gateway did not include but ASP.NET Core needs in order to route or bind the request. + /// The default implementation adds a synthesized Host header. Subclasses can override to add additional defaults + /// (e.g. a default Content-Type for websocket payloads). + /// + protected virtual IHeaderDictionary AddMissingRequestHeaders(APIGatewayProxyRequest apiGatewayRequest, IHeaderDictionary headers) + { + if (!headers.ContainsKey("Host")) + { + var apiId = apiGatewayRequest?.RequestContext?.ApiId ?? ""; + var stage = apiGatewayRequest?.RequestContext?.Stage ?? ""; + + headers["Host"] = $"apigateway-{apiId}-{stage}"; + } + return headers; + } } } diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayWebsocketApiProxyFunction.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayWebsocketApiProxyFunction.cs new file mode 100644 index 000000000..2c5024c38 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayWebsocketApiProxyFunction.cs @@ -0,0 +1,78 @@ +using System; + +using Microsoft.AspNetCore.Http; + +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.AspNetCoreServer.Internal; + +namespace Amazon.Lambda.AspNetCoreServer +{ + /// + /// Base class for ASP.NET Core Lambda functions invoked by API Gateway Websocket APIs. + /// + /// Websocket events are surfaced as POST requests whose path is the RouteKey, so the same Lambda + /// should be referenced by every websocket route that has a matching ASP.NET Core controller route + /// (e.g. [HttpPost("$default")], [HttpPost("$connect")]) for the ASP.NET Core IServer + /// to successfully dispatch requests. + /// + public abstract class APIGatewayWebsocketApiProxyFunction : APIGatewayProxyFunction + { + /// + /// Default constructor. The ASP.NET Core framework is initialized as part of construction. + /// + protected APIGatewayWebsocketApiProxyFunction() + : base() + { + } + + /// + /// Constructor that lets the caller defer ASP.NET Core framework initialization until the first request. + /// + /// Configures when the ASP.NET Core framework is initialized. + protected APIGatewayWebsocketApiProxyFunction(StartupMode startupMode) + : base(startupMode) + { + } + + /// + /// Constructor used by Amazon.Lambda.AspNetCoreServer.Hosting to support ASP.NET Core projects using the Minimal API style. + /// + /// The service provider built by the ASP.NET Core host. + protected APIGatewayWebsocketApiProxyFunction(IServiceProvider hostedServices) + : base(hostedServices) + { + } + + /// + /// Maps the websocket event to a request path of /{RouteKey} so ASP.NET Core can dispatch to a controller + /// route declared with [HttpPost("{RouteKey}")]. + /// + protected override string ParseHttpPath(APIGatewayProxyRequest apiGatewayRequest) + { + return "/" + Utilities.DecodeResourcePath(apiGatewayRequest.RequestContext.RouteKey); + } + + /// + /// Always returns POST for websocket events. Combined with , this lets the same + /// Lambda route every websocket event into an ASP.NET Core controller action. + /// + protected override string ParseHttpMethod(APIGatewayProxyRequest apiGatewayRequest) + { + return "POST"; + } + + /// + /// Adds a default Content-Type of application/json when API Gateway did not supply one. + /// Websocket message payloads are typically JSON, but the gateway does not set headers automatically. + /// + protected override IHeaderDictionary AddMissingRequestHeaders(APIGatewayProxyRequest apiGatewayRequest, IHeaderDictionary headers) + { + headers = base.AddMissingRequestHeaders(apiGatewayRequest, headers); + if (!headers.ContainsKey("Content-Type")) + { + headers["Content-Type"] = "application/json"; + } + return headers; + } + } +} diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayWebsocketApiProxyFunction{TStartup}.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayWebsocketApiProxyFunction{TStartup}.cs new file mode 100644 index 000000000..c264483c4 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayWebsocketApiProxyFunction{TStartup}.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Hosting; +using System.Diagnostics.CodeAnalysis; + +namespace Amazon.Lambda.AspNetCoreServer +{ + /// + /// Strongly-typed variant of that wires up an ASP.NET Core Startup class. + /// The Lambda function handler should point at the inherited FunctionHandlerAsync method. + /// + /// The type containing the startup methods for the application. + public abstract class APIGatewayWebsocketApiProxyFunction<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicConstructors)] TStartup> : APIGatewayWebsocketApiProxyFunction where TStartup : class + { + /// + /// Default constructor. The ASP.NET Core framework is initialized as part of construction. + /// + protected APIGatewayWebsocketApiProxyFunction() + : base() + { + } + + /// + /// Constructor that lets the caller defer ASP.NET Core framework initialization until the first request. + /// + /// Configures when the ASP.NET Core framework is initialized. + protected APIGatewayWebsocketApiProxyFunction(StartupMode startupMode) + : base(startupMode) + { + } + + /// + protected override void Init(IWebHostBuilder builder) + { + builder.UseStartup(); + } + } +} diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/AbstractAspNetCoreFunction.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/AbstractAspNetCoreFunction.cs index b24a9fd61..51c930333 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/AbstractAspNetCoreFunction.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/AbstractAspNetCoreFunction.cs @@ -24,6 +24,12 @@ namespace Amazon.Lambda.AspNetCoreServer /// public abstract class AbstractAspNetCoreFunction { + internal const string ParameterizedPreviewMessage = + "Response streaming is in preview till a new version of .NET Lambda runtime client that supports response streaming " + + "has been deployed to the .NET Lambda managed runtime. Till deployment has been made the feature can be used by deploying as an " + + "executable including the latest version of Amazon.Lambda.RuntimeSupport and setting the \"EnablePreviewFeatures\" in the Lambda " + + "project file to \"true\""; + /// /// Key to access the ILambdaContext object from the HttpContext.Items collection. /// @@ -120,8 +126,8 @@ protected AbstractAspNetCoreFunction(StartupMode startupMode) protected AbstractAspNetCoreFunction(IServiceProvider hostedServices) { _hostServices = hostedServices; - _server = this._hostServices.GetService(typeof(Microsoft.AspNetCore.Hosting.Server.IServer)) as LambdaServer; - _logger = ActivatorUtilities.CreateInstance>>(this._hostServices); + _server = _hostServices.GetService(typeof(Microsoft.AspNetCore.Hosting.Server.IServer)) as LambdaServer; + _logger = ActivatorUtilities.CreateInstance>>(_hostServices); AddRegisterBeforeSnapshot(); } @@ -194,6 +200,15 @@ public void RegisterResponseContentEncodingForContentEncoding(string contentEnco /// public bool IncludeUnhandledExceptionDetailInResponse { get; set; } + /// + /// When true, writes the response directly to a + /// instead of + /// buffering it and returning a typed response object (which will be null). + /// Requires net8.0 or later. + /// + [System.Runtime.Versioning.RequiresPreviewFeatures(ParameterizedPreviewMessage)] + public virtual bool EnableResponseStreaming { get; set; } = false; + /// /// Method to initialize the web builder before starting the web host. In a typical Web API this is similar to the main function. @@ -255,7 +270,6 @@ protected virtual IHostBuilder CreateHostBuilder() return builder; } - #if NET8_0_OR_GREATER /// /// Return one or more s that will be used to invoke /// Routes in your lambda function in order to initialize the ASP.NET Core and Lambda pipelines @@ -294,7 +308,6 @@ protected virtual IHostBuilder CreateHostBuilder() /// protected virtual IEnumerable GetBeforeSnapshotRequests() => Enumerable.Empty(); - #endif private protected bool IsStarted { @@ -306,8 +319,6 @@ private protected bool IsStarted private void AddRegisterBeforeSnapshot() { - #if NET8_0_OR_GREATER - Amazon.Lambda.Core.SnapshotRestore.RegisterBeforeSnapshot(async () => { var beforeSnapstartRequests = GetBeforeSnapshotRequests(); @@ -339,8 +350,6 @@ private void AddRegisterBeforeSnapshot() } } }); - - #endif } /// @@ -358,16 +367,16 @@ protected void Start() PostCreateHost(host); host.Start(); - this._hostServices = host.Services; + _hostServices = host.Services; - _server = this._hostServices.GetService(typeof(Microsoft.AspNetCore.Hosting.Server.IServer)) as LambdaServer; + _server = _hostServices.GetService(typeof(Microsoft.AspNetCore.Hosting.Server.IServer)) as LambdaServer; if (_server == null) { throw new Exception("Failed to find the Lambda implementation for the IServer interface in the IServiceProvider for the Host. This happens if UseLambdaServer was " + "not called when constructing the IWebHostBuilder. If CreateHostBuilder was overridden it is recommended that ConfigureWebHostLambdaDefaults should be used " + "instead of ConfigureWebHostDefaults to make sure the property Lambda services are registered."); } - _logger = ActivatorUtilities.CreateInstance>>(this._hostServices); + _logger = ActivatorUtilities.CreateInstance>>(_hostServices); AddRegisterBeforeSnapshot(); } @@ -475,13 +484,21 @@ public virtual async Task FunctionHandlerAsync(TREQUEST request, ILam PostMarshallItemsFeatureFeature(itemFeatures, request, lambdaContext); } - var scope = this._hostServices.CreateScope(); +#pragma warning disable CA2252 + if (EnableResponseStreaming) + { + await ExecuteStreamingRequestAsync(features); + return default; + } +#pragma warning restore CA2252 + + var scope = _hostServices.CreateScope(); try { ((IServiceProvidersFeature)features).RequestServices = scope.ServiceProvider; - var context = this.CreateContext(features); - var response = await this.ProcessRequest(lambdaContext, context, features); + var context = CreateContext(features); + var response = await ProcessRequest(lambdaContext, context, features); return response; } @@ -499,7 +516,7 @@ public virtual async Task FunctionHandlerAsync(TREQUEST request, ILam /// An instance. /// /// If specified, an unhandled exception will be rethrown for custom error handling. - /// Ensure that the error handling code calls 'this.MarshallResponse(features, 500);' after handling the error to return a the typed Lambda object to the user. + /// Ensure that the error handling code calls 'MarshallResponse(features, 500);' after handling the error to return a the typed Lambda object to the user. /// protected async Task ProcessRequest(ILambdaContext lambdaContext, object context, InvokeFeatures features, bool rethrowUnhandledError = false) { @@ -509,47 +526,13 @@ protected async Task ProcessRequest(ILambdaContext lambdaContext, obj { try { - await this._server.Application.ProcessRequestAsync(context); - } - catch (AggregateException agex) - { - ex = agex; - _logger.LogError(agex, $"Caught AggregateException: '{agex}'"); - var sb = new StringBuilder(); - foreach (var newEx in agex.InnerExceptions) - { - sb.AppendLine(this.ErrorReport(newEx)); - } - - _logger.LogError(sb.ToString()); - ((IHttpResponseFeature)features).StatusCode = 500; - } - catch (ReflectionTypeLoadException rex) - { - ex = rex; - _logger.LogError(rex, $"Caught ReflectionTypeLoadException: '{rex}'"); - var sb = new StringBuilder(); - foreach (var loaderException in rex.LoaderExceptions) - { - var fileNotFoundException = loaderException as FileNotFoundException; - if (fileNotFoundException != null && !string.IsNullOrEmpty(fileNotFoundException.FileName)) - { - sb.AppendLine($"Missing file: {fileNotFoundException.FileName}"); - } - else - { - sb.AppendLine(this.ErrorReport(loaderException)); - } - } - - _logger.LogError(sb.ToString()); - ((IHttpResponseFeature)features).StatusCode = 500; + await RunPipelineAsync(context, features); } catch (Exception e) { ex = e; if (rethrowUnhandledError) throw; - _logger.LogError(e, $"Unknown error responding to request: {this.ErrorReport(e)}"); + _logger.LogError(e, $"Unknown error responding to request: {ErrorReport(e)}"); ((IHttpResponseFeature)features).StatusCode = 500; } @@ -557,7 +540,7 @@ protected async Task ProcessRequest(ILambdaContext lambdaContext, obj { await features.ResponseStartingEvents.ExecuteAsync(); } - var response = this.MarshallResponse(features, lambdaContext, defaultStatusCode); + var response = MarshallResponse(features, lambdaContext, defaultStatusCode); if (ex != null && IncludeUnhandledExceptionDetailInResponse) { @@ -573,7 +556,7 @@ protected async Task ProcessRequest(ILambdaContext lambdaContext, obj } finally { - this._server.Application.DisposeContext(context, ex); + _server.Application.DisposeContext(context, ex); } } @@ -583,15 +566,6 @@ private protected virtual void InternalCustomResponseExceptionHandling(TRESPONSE } - /// - /// This method is called after the IWebHost is created from the IWebHostBuilder and the services have been configured. The - /// WebHost hasn't been started yet. - /// - /// - protected virtual void PostCreateWebHost(IWebHost webHost) - { - - } /// /// This method is called after the IHost is created from the IHostBuilder and the services have been configured. The @@ -697,5 +671,158 @@ protected virtual void PostMarshallResponseFeature(IHttpResponseFeature aspNetCo /// /// protected abstract TRESPONSE MarshallResponse(IHttpResponseFeature responseFeatures, ILambdaContext lambdaContext, int statusCodeIfNotSet = 200); + + /// + /// Builds an from the current + /// ASP.NET Core response feature. + /// + /// The ASP.NET Core response feature for the current invocation. + /// A populated . + [System.Runtime.Versioning.RequiresPreviewFeatures(ParameterizedPreviewMessage)] + protected abstract Amazon.Lambda.Core.ResponseStreaming.HttpResponseStreamPrelude BuildStreamingPrelude(IHttpResponseFeature responseFeature); + + /// + /// Creates a for writing the streaming Lambda response. + /// The default implementation calls . + /// Subclasses may override this method to substitute a different stream (e.g. a + /// in unit tests). + /// + /// The HTTP response prelude containing status code and headers. + /// A writable for the response body. + [System.Runtime.Versioning.RequiresPreviewFeatures(ParameterizedPreviewMessage)] + protected virtual System.IO.Stream CreateLambdaResponseStream( + Amazon.Lambda.Core.ResponseStreaming.HttpResponseStreamPrelude prelude) + { + return Amazon.Lambda.Core.ResponseStreaming.LambdaResponseStreamFactory.CreateHttpStream(prelude); + } + + /// + /// Executes the streaming response path. Called by when + /// is true. Writes the response directly to a + /// . + /// + [System.Runtime.Versioning.RequiresPreviewFeatures(ParameterizedPreviewMessage)] + private async Task ExecuteStreamingRequestAsync(InvokeFeatures features) + { + var responseFeature = (IHttpResponseFeature)features; + System.IO.Stream lambdaStream = null; + bool streamOpened = false; + + async Task OpenStream() + { + var prelude = BuildStreamingPrelude(responseFeature); + _logger.LogDebug("Opening Lambda response stream with Status code {StatusCode}", prelude.StatusCode); + var stream = CreateLambdaResponseStream(prelude); + lambdaStream = stream; + streamOpened = true; + return stream; + } + + var streamingBodyFeature = new Internal.StreamingResponseBodyFeature(_logger, responseFeature, OpenStream); + features[typeof(IHttpResponseBodyFeature)] = streamingBodyFeature; + + var scope = _hostServices.CreateScope(); + Exception pipelineException = null; + try + { + ((IServiceProvidersFeature)features).RequestServices = scope.ServiceProvider; + + var context = CreateContext(features); + try + { + try + { + await RunPipelineAsync(context, features); + await streamingBodyFeature.CompleteAsync(); + } + catch (Exception e) + { + pipelineException = e; + _logger.LogError(e, "Error in streaming request pipeline"); + + if (!streamOpened) + { + var errorPrelude = new Amazon.Lambda.Core.ResponseStreaming.HttpResponseStreamPrelude + { + StatusCode = System.Net.HttpStatusCode.InternalServerError + }; + var errorStream = CreateLambdaResponseStream(errorPrelude); + lambdaStream = errorStream; + streamOpened = true; + if (IncludeUnhandledExceptionDetailInResponse) + { + var errorBytes = System.Text.Encoding.UTF8.GetBytes(ErrorReport(e)); + await errorStream.WriteAsync(errorBytes, 0, errorBytes.Length); + } + } + else if (streamOpened) + { + _logger.LogError(e, $"Unhandled exception after response stream was opened: {ErrorReport(e)}"); + } + else + { + _logger.LogError(e, $"Unknown error responding to request: {ErrorReport(e)}"); + } + } + } + finally + { + if (lambdaStream != null) + { + lambdaStream.Dispose(); + } + + if (features.ResponseCompletedEvents != null) + { + await features.ResponseCompletedEvents.ExecuteAsync(); + } + + _server.Application.DisposeContext(context, pipelineException); + } + } + finally + { + scope.Dispose(); + } + } + + /// + /// Invokes the ASP.NET Core pipeline for the given context, handling + /// and with + /// detailed logging. Any other exception is rethrown to the caller. + /// + private async Task RunPipelineAsync(object context, InvokeFeatures features) + { + try + { + await _server.Application.ProcessRequestAsync(context); + } + catch (AggregateException agex) + { + _logger.LogError(agex, $"Caught AggregateException: '{agex}'"); + var sb = new StringBuilder(); + foreach (var newEx in agex.InnerExceptions) + sb.AppendLine(ErrorReport(newEx)); + _logger.LogError(sb.ToString()); + ((IHttpResponseFeature)features).StatusCode = 500; + throw; + } + catch (ReflectionTypeLoadException rex) + { + _logger.LogError(rex, $"Caught ReflectionTypeLoadException: '{rex}'"); + var sb = new StringBuilder(); + foreach (var loaderException in rex.LoaderExceptions) + { + var fileNotFoundException = loaderException as FileNotFoundException; + if (fileNotFoundException != null && !string.IsNullOrEmpty(fileNotFoundException.FileName)) + sb.AppendLine($"Missing file: {fileNotFoundException.FileName}"); + else + sb.AppendLine(ErrorReport(loaderException)); + } + _logger.LogError(sb.ToString()); + ((IHttpResponseFeature)features).StatusCode = 500; + throw; + } + } } } diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Amazon.Lambda.AspNetCoreServer.csproj b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Amazon.Lambda.AspNetCoreServer.csproj index 561616cd6..dba607e64 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Amazon.Lambda.AspNetCoreServer.csproj +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Amazon.Lambda.AspNetCoreServer.csproj @@ -4,9 +4,9 @@ Amazon.Lambda.AspNetCoreServer makes it easy to run ASP.NET Core Web API applications as AWS Lambda functions. - net6.0;net8.0 + $(DefaultPackageTargets) Amazon.Lambda.AspNetCoreServer - 9.2.1 + 10.1.1 Amazon.Lambda.AspNetCoreServer Amazon.Lambda.AspNetCoreServer AWS;Amazon;Lambda;aspnetcore @@ -28,6 +28,10 @@ + + + + diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/ApplicationLoadBalancerFunction.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/ApplicationLoadBalancerFunction.cs index 3048284b2..2bb08c711 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/ApplicationLoadBalancerFunction.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/ApplicationLoadBalancerFunction.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Collections.Generic; using System.Text; @@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Http.Features.Authentication; using Microsoft.Extensions.Primitives; using System.Globalization; +using Amazon.Lambda.Core.ResponseStreaming; namespace Amazon.Lambda.AspNetCoreServer { @@ -76,7 +77,7 @@ protected override void MarshallRequest(InvokeFeatures features, ApplicationLoad // marshalling the response to know whether to fill in the the Headers or MultiValueHeaders collection. // Since a Lambda function compute environment is only one processing one event at a time it is safe to store // this as a member variable. - this._multiHeaderValuesEnabled = lambdaRequest.MultiValueHeaders != null; + _multiHeaderValuesEnabled = lambdaRequest.MultiValueHeaders != null; { var requestFeatures = (IHttpRequestFeature)features; @@ -159,14 +160,14 @@ protected override ApplicationLoadBalancerResponse MarshallResponse(IHttpRespons if (responseFeatures.Headers != null) { - if (this._multiHeaderValuesEnabled) + if (_multiHeaderValuesEnabled) response.MultiValueHeaders = new Dictionary>(); else response.Headers = new Dictionary(); foreach (var kvp in responseFeatures.Headers) { - if (this._multiHeaderValuesEnabled) + if (_multiHeaderValuesEnabled) { response.MultiValueHeaders[kvp.Key] = kvp.Value.ToList(); } @@ -187,6 +188,8 @@ protected override ApplicationLoadBalancerResponse MarshallResponse(IHttpRespons } } +// Disabled in case the user's ASP.NET Core application is still using the older API that set the body on the response feature instead of the new API that sets the body on the HttpResponse object. +#pragma warning disable CS0618 if (responseFeatures.Body != null) { // Figure out how we should treat the response content, check encoding first to see if body is compressed, then check content type @@ -198,6 +201,7 @@ protected override ApplicationLoadBalancerResponse MarshallResponse(IHttpRespons (response.Body, response.IsBase64Encoded) = Utilities.ConvertAspNetCoreBodyToLambdaBody(responseFeatures.Body, rcEncoding); } +#pragma warning restore CS0618 PostMarshallResponseFeature(responseFeatures, response, lambdaContext); @@ -210,7 +214,7 @@ private protected override void InternalCustomResponseExceptionHandling(Applicat { var errorName = ex.GetType().Name; - if (this._multiHeaderValuesEnabled) + if (_multiHeaderValuesEnabled) { lambdaResponse.MultiValueHeaders.Add(new KeyValuePair>("ErrorType", new List { errorName })); } @@ -220,9 +224,13 @@ private protected override void InternalCustomResponseExceptionHandling(Applicat } } + /// + [System.Runtime.Versioning.RequiresPreviewFeatures(ParameterizedPreviewMessage)] + protected override HttpResponseStreamPrelude BuildStreamingPrelude(IHttpResponseFeature responseFeature) => throw new NotImplementedException(); + private string GetSingleHeaderValue(ApplicationLoadBalancerRequest request, string headerName) { - if (this._multiHeaderValuesEnabled) + if (_multiHeaderValuesEnabled) { var kvp = request.MultiValueHeaders.FirstOrDefault(x => string.Equals(x.Key, headerName, StringComparison.OrdinalIgnoreCase)); if (!kvp.Equals(default(KeyValuePair>))) diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/HttpRequestMessageConverter.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/HttpRequestMessageConverter.cs index 285fb3898..3f0b9a99b 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/HttpRequestMessageConverter.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/HttpRequestMessageConverter.cs @@ -1,17 +1,13 @@ -#if NET8_0_OR_GREATER using System; -using System.Collections.Generic; using System.Linq; using System.Net.Http; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; using System.Threading.Tasks; using Amazon.Lambda.APIGatewayEvents; using Amazon.Lambda.ApplicationLoadBalancerEvents; -using Microsoft.AspNetCore.Identity.Data; using Microsoft.AspNetCore.WebUtilities; -using Microsoft.Extensions.Primitives; + +#pragma warning disable CS1591 // Since this class is treated as internal, we can ignore the missing XML comments for public members. + namespace Amazon.Lambda.AspNetCoreServer.Internal { @@ -118,4 +114,3 @@ private static async Task ReadContent(HttpRequestMessage r) } } } -#endif diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/InvokeFeatures.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/InvokeFeatures.cs index 398817af2..de275038f 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/InvokeFeatures.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/InvokeFeatures.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; @@ -28,13 +28,9 @@ public class InvokeFeatures : IFeatureCollection, IServiceProvidersFeature, ITlsConnectionFeature, IHttpRequestIdentifierFeature, - IHttpResponseBodyFeature - -#if NET6_0_OR_GREATER ,IHttpRequestBodyDetectionFeature ,IHttpActivityFeature -#endif /* , IHttpUpgradeFeature, @@ -54,11 +50,8 @@ public InvokeFeatures() this[typeof(ITlsConnectionFeature)] = this; this[typeof(IHttpResponseBodyFeature)] = this; this[typeof(IHttpRequestIdentifierFeature)] = this; - -#if NET6_0_OR_GREATER this[typeof(IHttpRequestBodyDetectionFeature)] = this; this[typeof(IHttpActivityFeature)] = this; -#endif } #region IFeatureCollection @@ -119,20 +112,21 @@ public TFeature Get() public IEnumerator> GetEnumerator() { - return this._features.GetEnumerator(); + return _features.GetEnumerator(); } public void Set(TFeature instance) { - if (instance == null) - return; - - this._features[typeof(TFeature)] = instance; + // Delegate to the indexer so _containerRevision is bumped, otherwise + // ASP.NET Core's FeatureReferences cache will return stale feature + // references after middleware (e.g. OutputCache, ResponseCompression) + // wraps the response body via HttpContext.Response.Body = wrapper. + this[typeof(TFeature)] = instance; } IEnumerator IEnumerable.GetEnumerator() { - return this._features.GetEnumerator(); + return _features.GetEnumerator(); } #endregion @@ -199,27 +193,27 @@ Stream IHttpResponseFeature.Body void IHttpResponseFeature.OnStarting(Func callback, object state) { if (ResponseStartingEvents == null) - this.ResponseStartingEvents = new EventCallbacks(); + ResponseStartingEvents = new EventCallbacks(); - this.ResponseStartingEvents.Add(callback, state); + ResponseStartingEvents.Add(callback, state); } internal EventCallbacks ResponseCompletedEvents { get; private set; } void IHttpResponseFeature.OnCompleted(Func callback, object state) { - if (this.ResponseCompletedEvents == null) - this.ResponseCompletedEvents = new EventCallbacks(); + if (ResponseCompletedEvents == null) + ResponseCompletedEvents = new EventCallbacks(); - this.ResponseCompletedEvents.Add(callback, state); + ResponseCompletedEvents.Add(callback, state); } internal class EventCallbacks { - List _callbacks = new List(); + readonly List _callbacks = new List(); internal void Add(Func callback, object state) { - this._callbacks.Add(new EventCallback(callback, state)); + _callbacks.Add(new EventCallback(callback, state)); } internal async Task ExecuteAsync() @@ -234,8 +228,8 @@ internal class EventCallback { internal EventCallback(Func callback, object state) { - this.Callback = callback; - this.State = state; + Callback = callback; + State = state; } Func Callback { get; } @@ -243,7 +237,7 @@ internal EventCallback(Func callback, object state) internal Task ExecuteAsync() { - var task = Callback(this.State); + var task = Callback(State); return task; } } @@ -252,7 +246,10 @@ internal Task ExecuteAsync() #endregion #region IHttpResponseBodyFeature +// Disabled in case the user's ASP.NET Core application is still using the older API that set the body on the response feature instead of the new API that sets the body on the HttpResponse object. +#pragma warning disable CS0618 Stream IHttpResponseBodyFeature.Stream => ((IHttpResponseFeature)this).Body; +#pragma warning restore CS0618 private PipeWriter _pipeWriter; @@ -380,12 +377,11 @@ string IHttpRequestIdentifierFeature.TraceIdentifier _traceIdentifier = (new Microsoft.AspNetCore.Http.Features.HttpRequestIdentifierFeature()).TraceIdentifier; return _traceIdentifier; } - set { this._traceIdentifier = value; } + set { _traceIdentifier = value; } } #endregion -#if NET6_0_OR_GREATER bool IHttpRequestBodyDetectionFeature.CanHaveBody { get @@ -396,6 +392,5 @@ bool IHttpRequestBodyDetectionFeature.CanHaveBody } Activity IHttpActivityFeature.Activity { get; set; } -#endif } } diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/LambdaServer.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/LambdaServer.cs index b6c218dfe..257936a76 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/LambdaServer.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/LambdaServer.cs @@ -1,13 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting.Server; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; +#pragma warning disable CS1591 // Since this class is treated as internal, we can ignore the missing XML comments for public members. + namespace Amazon.Lambda.AspNetCoreServer.Internal { /// @@ -28,7 +27,7 @@ public void Dispose() public virtual Task StartAsync(IHttpApplication application, CancellationToken cancellationToken) { - this.Application = new ApplicationWrapper(application); + Application = new ApplicationWrapper(application); return Task.CompletedTask; } diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/StreamingResponseBodyFeature.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/StreamingResponseBodyFeature.cs new file mode 100644 index 000000000..03ec929a1 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/StreamingResponseBodyFeature.cs @@ -0,0 +1,250 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +using System; +using System.IO; +using System.IO.Pipelines; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Http.Features; + +using Amazon.Lambda.Core.ResponseStreaming; +using Microsoft.Extensions.Logging; + +namespace Amazon.Lambda.AspNetCoreServer.Internal +{ + /// + /// An implementation that supports Lambda response streaming. + /// Uses a two-phase approach: bytes written before are buffered in a + /// ; after all writes go directly to the + /// obtained from the stream opener delegate. + /// + [RequiresPreviewFeatures(AbstractAspNetCoreFunction.ParameterizedPreviewMessage)] + internal class StreamingResponseBodyFeature : IHttpResponseBodyFeature + { + private readonly ILogger _logger; + private readonly IHttpResponseFeature _responseFeature; + private readonly Func> _streamOpener; + + private Stream _lambdaStream; // null until StartAsync completes + private MemoryStream _preStartBuffer; // accumulates bytes written before StartAsync + private bool _started; + private PipeWriter _pipeWriter; // lazily created; always wraps the live Stream + + /// + /// Initializes a new instance of . + /// + /// + /// + /// The for the current invocation. Used to fire + /// OnStarting callbacks when is called. + /// + /// + /// A delegate that, when invoked, builds the from + /// the response headers and calls + /// to obtain the . + /// + public StreamingResponseBodyFeature( + ILogger logger, + IHttpResponseFeature responseFeature, + Func> streamOpener) + { + _logger = logger; + _responseFeature = responseFeature ?? throw new ArgumentNullException(nameof(responseFeature)); + _streamOpener = streamOpener ?? throw new ArgumentNullException(nameof(streamOpener)); + } + + /// + /// Initializes a new instance without a logger (for use in tests). + /// + internal StreamingResponseBodyFeature( + IHttpResponseFeature responseFeature, + Func> streamOpener) + : this(null, responseFeature, streamOpener) { } + + /// + /// + /// Returns the once has been + /// called; otherwise returns a lazy-initialized that buffers + /// bytes until the stream is opened. + /// + public Stream Stream => _lambdaStream ?? (_preStartBuffer ??= new MemoryStream()); + + /// + /// + /// Returns a that calls on first + /// flush/write so that the Lambda stream is opened (and the HTTP prelude is sent) + /// as soon as the application first flushes, rather than waiting until the end. + /// + public PipeWriter Writer => _pipeWriter ??= new StartOnFlushPipeWriter(this); + + /// + /// + /// Fires all registered OnStarting callbacks, then calls the stream opener delegate + /// to obtain the , and finally flushes any bytes that + /// were buffered before this method was called. + /// + public async Task StartAsync(CancellationToken cancellationToken = default) + { + _logger?.LogInformation("Starting response streaming"); + + if (_started) + return; + + // Fire OnStarting callbacks registered on the response feature. + // InvokeFeatures (which implements IHttpResponseFeature) stores these in + // ResponseStartingEvents, which is internal to this assembly. + if (_responseFeature is InvokeFeatures invokeFeatures && + invokeFeatures.ResponseStartingEvents != null) + { + await invokeFeatures.ResponseStartingEvents.ExecuteAsync(); + } + + // Open the Lambda response stream (this writes the HTTP prelude). + _lambdaStream = await _streamOpener(); + + // Flush any bytes that were written before StartAsync was called. + if (_preStartBuffer != null && _preStartBuffer.Length > 0) + { + _preStartBuffer.Position = 0; + await _preStartBuffer.CopyToAsync(_lambdaStream, cancellationToken); + } + + _started = true; + } + + /// + public async Task CompleteAsync() + { + await StartAsync(); + + if (_pipeWriter != null) + { + await _pipeWriter.FlushAsync(); + } + } + + /// + /// No-op: the stream is already unbuffered once opened. + public void DisableBuffering() + { + // Intentional no-op per design: the Lambda response stream is already unbuffered. + } + + /// + /// + /// Calls to ensure the stream is open, then reads the specified + /// byte range from the file and writes it to the . + /// + public async Task SendFileAsync( + string path, + long offset, + long? count, + CancellationToken cancellationToken = default) + { + await StartAsync(cancellationToken); + + var fileInfo = new FileInfo(path); + if (offset < 0 || offset > fileInfo.Length) + throw new ArgumentOutOfRangeException(nameof(offset), offset, string.Empty); + if (count.HasValue && (count.Value < 0 || count.Value > fileInfo.Length - offset)) + throw new ArgumentOutOfRangeException(nameof(count), count, string.Empty); + + cancellationToken.ThrowIfCancellationRequested(); + + const int bufferSize = 1024 * 16; + var fileStream = new FileStream( + path, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite, + bufferSize: bufferSize, + options: FileOptions.Asynchronous | FileOptions.SequentialScan); + + using (fileStream) + { + fileStream.Seek(offset, SeekOrigin.Begin); + await Utilities.CopyToAsync(fileStream, _lambdaStream, count, bufferSize, cancellationToken); + } + } + + // ----------------------------------------------------------------------- + // StartOnFlushPipeWriter + // + // A PipeWriter wrapper that ensures StartAsync is called (opening the Lambda + // stream and sending the HTTP prelude) the first time the application flushes + // or completes the writer — not just at the very end of the request. + // + // The inner PipeWriter is created lazily against the *live* Stream property + // so it always targets the correct underlying stream (Lambda stream after + // StartAsync, pre-start buffer before). + // ----------------------------------------------------------------------- + private sealed class StartOnFlushPipeWriter : PipeWriter + { + private readonly StreamingResponseBodyFeature _feature; + private PipeWriter _inner; + + // The inner writer must be recreated after StartAsync because Stream + // switches from _preStartBuffer to _lambdaStream at that point. + private PipeWriter Inner => _inner ??= PipeWriter.Create(_feature.Stream); + + public StartOnFlushPipeWriter(StreamingResponseBodyFeature feature) + { + _feature = feature; + } + + public override void Advance(int bytes) => Inner.Advance(bytes); + + public override bool CanGetUnflushedBytes => true; + + public override long UnflushedBytes => Inner.UnflushedBytes; + + public override Memory GetMemory(int sizeHint = 0) => Inner.GetMemory(sizeHint); + + public override Span GetSpan(int sizeHint = 0) => Inner.GetSpan(sizeHint); + + public override async ValueTask FlushAsync(CancellationToken cancellationToken = default) + { + if (!_feature._started) + { + // Flush buffered bytes into the pre-start buffer first, then open the stream. + var innerFlushResult = await Inner.FlushAsync(cancellationToken); + // Recreate inner writer against the Lambda stream after StartAsync. + _inner = null; + await _feature.StartAsync(cancellationToken); + + return innerFlushResult; + } + + return await Inner.FlushAsync(cancellationToken); + } + + public override async ValueTask CompleteAsync(Exception exception = null) + { + if (!_feature._started) + { + await Inner.FlushAsync(); + _inner = null; + await _feature.StartAsync(); + } + await Inner.CompleteAsync(exception); + } + + // Complete (sync) — mirror CompleteAsync behavior to ensure the response is started. + public override void Complete(Exception exception = null) + { + if (!_feature._started) + { + // Flush buffered bytes into the pre-start buffer, then open the stream. + Inner.FlushAsync().GetAwaiter().GetResult(); + _inner = null; + _feature.StartAsync().GetAwaiter().GetResult(); + } + + Inner.Complete(exception); + } + public override void CancelPendingFlush() => Inner.CancelPendingFlush(); + } + } +} diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/Utilities.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/Utilities.cs index 43fac1a96..6e96840c8 100644 --- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/Utilities.cs +++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/Utilities.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Primitives; @@ -275,7 +275,11 @@ internal static X509Certificate2 GetX509Certificate2FromPem(string clientCertPem // Remove "-----BEGIN CERTIFICATE-----\n" and "-----END CERTIFICATE-----" clientCertPem = clientCertPem.Substring(28, clientCertPem.Length - 53); +#if NET10_0_OR_GREATER + return X509CertificateLoader.LoadCertificate(Convert.FromBase64String(clientCertPem)); +#else return new X509Certificate2(Convert.FromBase64String(clientCertPem)); +#endif } } } diff --git a/Libraries/src/Amazon.Lambda.CloudWatchEvents/Amazon.Lambda.CloudWatchEvents.csproj b/Libraries/src/Amazon.Lambda.CloudWatchEvents/Amazon.Lambda.CloudWatchEvents.csproj index f64435895..05e46c3bf 100644 --- a/Libraries/src/Amazon.Lambda.CloudWatchEvents/Amazon.Lambda.CloudWatchEvents.csproj +++ b/Libraries/src/Amazon.Lambda.CloudWatchEvents/Amazon.Lambda.CloudWatchEvents.csproj @@ -3,16 +3,16 @@ - netstandard2.0;netcoreapp3.1;net8.0 + $(DefaultPackageTargets) Amazon Lambda .NET Core support - CloudWatchEvents package. Amazon.Lambda.CloudWatchEvents - 4.4.1 + 5.0.0 Amazon.Lambda.CloudWatchEvents Amazon.Lambda.CloudWatchEvents AWS;Amazon;Lambda - + IL2026,IL2067,IL2075 true true diff --git a/Libraries/src/Amazon.Lambda.CloudWatchEvents/CloudWatchEvent.cs b/Libraries/src/Amazon.Lambda.CloudWatchEvents/CloudWatchEvent.cs index 6f5259717..2128a980f 100644 --- a/Libraries/src/Amazon.Lambda.CloudWatchEvents/CloudWatchEvent.cs +++ b/Libraries/src/Amazon.Lambda.CloudWatchEvents/CloudWatchEvent.cs @@ -1,4 +1,4 @@ -namespace Amazon.Lambda.CloudWatchEvents +namespace Amazon.Lambda.CloudWatchEvents { using System; using System.Collections.Generic; @@ -40,9 +40,7 @@ public class CloudWatchEvent /// For example, ScheduledEvent will be null /// For example, ECSEvent could be "ECS Container Instance State Change" or "ECS Task State Change" /// -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("detail-type")] -#endif public string DetailType { get; set; } /// diff --git a/Libraries/src/Amazon.Lambda.CloudWatchEvents/S3Events/S3Object.cs b/Libraries/src/Amazon.Lambda.CloudWatchEvents/S3Events/S3Object.cs index 2e9abecee..2941ac5f0 100644 --- a/Libraries/src/Amazon.Lambda.CloudWatchEvents/S3Events/S3Object.cs +++ b/Libraries/src/Amazon.Lambda.CloudWatchEvents/S3Events/S3Object.cs @@ -30,9 +30,7 @@ public class S3Object /// The version ID of the object. /// [DataMember(Name = "version-id")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("version-id")] -#endif public string VersionId { get; set; } /// diff --git a/Libraries/src/Amazon.Lambda.CloudWatchEvents/S3Events/S3ObjectCreate.cs b/Libraries/src/Amazon.Lambda.CloudWatchEvents/S3Events/S3ObjectCreate.cs index 8f691bafe..cde7304ba 100644 --- a/Libraries/src/Amazon.Lambda.CloudWatchEvents/S3Events/S3ObjectCreate.cs +++ b/Libraries/src/Amazon.Lambda.CloudWatchEvents/S3Events/S3ObjectCreate.cs @@ -12,9 +12,7 @@ public class S3ObjectCreate : S3ObjectEventDetails /// The source IP of the API request. /// [DataMember(Name = "source-ip-address")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("source-ip-address")] -#endif public string SourceIpAddress { get; set; } /// diff --git a/Libraries/src/Amazon.Lambda.CloudWatchEvents/S3Events/S3ObjectDelete.cs b/Libraries/src/Amazon.Lambda.CloudWatchEvents/S3Events/S3ObjectDelete.cs index db8febc6a..99c39c8e6 100644 --- a/Libraries/src/Amazon.Lambda.CloudWatchEvents/S3Events/S3ObjectDelete.cs +++ b/Libraries/src/Amazon.Lambda.CloudWatchEvents/S3Events/S3ObjectDelete.cs @@ -12,9 +12,7 @@ public class S3ObjectDelete : S3ObjectEventDetails /// The source IP of the API request. /// [DataMember(Name = "source-ip-address")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("source-ip-address")] -#endif public string SourceIpAddress { get; set; } /// @@ -27,9 +25,7 @@ public class S3ObjectDelete : S3ObjectEventDetails /// The type of object deletion event. /// [DataMember(Name = "deletion-type")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("deletion-type")] -#endif public string DeletionType { get; set; } } } diff --git a/Libraries/src/Amazon.Lambda.CloudWatchEvents/S3Events/S3ObjectEventDetails.cs b/Libraries/src/Amazon.Lambda.CloudWatchEvents/S3Events/S3ObjectEventDetails.cs index 83352dfd8..66ac3edf1 100644 --- a/Libraries/src/Amazon.Lambda.CloudWatchEvents/S3Events/S3ObjectEventDetails.cs +++ b/Libraries/src/Amazon.Lambda.CloudWatchEvents/S3Events/S3ObjectEventDetails.cs @@ -30,9 +30,7 @@ public class S3ObjectEventDetails /// The ID of the API request. /// [DataMember(Name = "request-id")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("request-id")] -#endif public string RequestId { get; set; } /// diff --git a/Libraries/src/Amazon.Lambda.CloudWatchEvents/S3Events/S3ObjectRestore.cs b/Libraries/src/Amazon.Lambda.CloudWatchEvents/S3Events/S3ObjectRestore.cs index 27719f200..7a16bdc9a 100644 --- a/Libraries/src/Amazon.Lambda.CloudWatchEvents/S3Events/S3ObjectRestore.cs +++ b/Libraries/src/Amazon.Lambda.CloudWatchEvents/S3Events/S3ObjectRestore.cs @@ -12,18 +12,14 @@ public class S3ObjectRestore : S3ObjectEventDetails /// The time when the temporary copy of the object will be deleted from S3. /// [DataMember(Name = "restore-expiry-time")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("restore-expiry-time")] -#endif public string RestoreExpiryTime { get; set; } /// /// The storage class of the object being restored. /// [DataMember(Name = "source-storage-class")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("source-storage-class")] -#endif public string SourceStorageClass { get; set; } } } diff --git a/Libraries/src/Amazon.Lambda.CloudWatchEvents/TranslateEvents/TranslateParallelDataStateChange.cs b/Libraries/src/Amazon.Lambda.CloudWatchEvents/TranslateEvents/TranslateParallelDataStateChange.cs index 4edb431f0..8f8ea6e49 100644 --- a/Libraries/src/Amazon.Lambda.CloudWatchEvents/TranslateEvents/TranslateParallelDataStateChange.cs +++ b/Libraries/src/Amazon.Lambda.CloudWatchEvents/TranslateEvents/TranslateParallelDataStateChange.cs @@ -4,7 +4,7 @@ namespace Amazon.Lambda.CloudWatchEvents.TranslateEvents { /// /// This class represents the details of a Translate Parallel Data State Change - // for CreateParallelData and UpdateParallelData events sent via EventBridge. + /// for CreateParallelData and UpdateParallelData events sent via EventBridge. /// For more see - https://docs.aws.amazon.com/translate/latest/dg/monitoring-with-eventbridge.html /// public class TranslateParallelDataStateChange diff --git a/Libraries/src/Amazon.Lambda.CloudWatchLogsEvents/Amazon.Lambda.CloudWatchLogsEvents.csproj b/Libraries/src/Amazon.Lambda.CloudWatchLogsEvents/Amazon.Lambda.CloudWatchLogsEvents.csproj index ded1aba67..74e603c07 100644 --- a/Libraries/src/Amazon.Lambda.CloudWatchLogsEvents/Amazon.Lambda.CloudWatchLogsEvents.csproj +++ b/Libraries/src/Amazon.Lambda.CloudWatchLogsEvents/Amazon.Lambda.CloudWatchLogsEvents.csproj @@ -2,15 +2,15 @@ Amazon Lambda .NET Core support - CloudWatchLogsEvents package. - netstandard2.0;netcoreapp3.1;net8.0 + $(DefaultPackageTargets) Amazon.Lambda.CloudWatchLogsEvents - 2.2.1 + 3.0.0 Amazon.Lambda.CloudWatchLogsEvents Amazon.Lambda.CloudWatchLogsEvents AWS;Amazon;Lambda - + IL2026,IL2067,IL2075 true true diff --git a/Libraries/src/Amazon.Lambda.CloudWatchLogsEvents/CloudWatchLogsEvents.cs b/Libraries/src/Amazon.Lambda.CloudWatchLogsEvents/CloudWatchLogsEvents.cs index c25b39884..88d27ef89 100644 --- a/Libraries/src/Amazon.Lambda.CloudWatchLogsEvents/CloudWatchLogsEvents.cs +++ b/Libraries/src/Amazon.Lambda.CloudWatchLogsEvents/CloudWatchLogsEvents.cs @@ -30,9 +30,7 @@ public class Log /// The data that are base64 encoded and gziped messages in LogStreams. /// [DataMember(Name = "data", IsRequired = false)] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("data")] -#endif public string EncodedData { get; set; } /// @@ -40,10 +38,10 @@ public class Log /// public string DecodeData() { - if (string.IsNullOrEmpty(this.EncodedData)) - return this.EncodedData; + if (string.IsNullOrEmpty(EncodedData)) + return EncodedData; - var bytes = Convert.FromBase64String(this.EncodedData); + var bytes = Convert.FromBase64String(EncodedData); var uncompressedStream = new MemoryStream(); using (var stream = new GZipStream(new MemoryStream(bytes), CompressionMode.Decompress)) diff --git a/Libraries/src/Amazon.Lambda.CognitoEvents/AccessTokenGeneration.cs b/Libraries/src/Amazon.Lambda.CognitoEvents/AccessTokenGeneration.cs index daea448eb..77749a623 100644 --- a/Libraries/src/Amazon.Lambda.CognitoEvents/AccessTokenGeneration.cs +++ b/Libraries/src/Amazon.Lambda.CognitoEvents/AccessTokenGeneration.cs @@ -14,18 +14,14 @@ public class AccessTokenGeneration /// groupOverrideDetails instead. /// [DataMember(Name = "claimsToAddOrOverride")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("claimsToAddOrOverride")] -# endif public Dictionary ClaimsToAddOrOverride { get; set; } = new Dictionary(); /// /// A list that contains claims to be suppressed from the identity token. /// [DataMember(Name = "claimsToSuppress")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("claimsToSuppress")] -# endif public List ClaimsToSuppress { get; set; } = new List(); /// @@ -33,18 +29,14 @@ public class AccessTokenGeneration /// add scope values that contain one or more blank-space characters. /// [DataMember(Name = "scopesToAdd")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("scopesToAdd")] -# endif public List ScopesToAdd { get; set; } = new List(); /// /// A list of OAuth 2.0 scopes that you want to remove from the scope claim in your user's access token. /// [DataMember(Name = "scopesToSuppress")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("scopesToSuppress")] -# endif public List ScopesToSuppress { get; set; } = new List(); } } diff --git a/Libraries/src/Amazon.Lambda.CognitoEvents/Amazon.Lambda.CognitoEvents.csproj b/Libraries/src/Amazon.Lambda.CognitoEvents/Amazon.Lambda.CognitoEvents.csproj index f8c17ffe9..b38d1c031 100644 --- a/Libraries/src/Amazon.Lambda.CognitoEvents/Amazon.Lambda.CognitoEvents.csproj +++ b/Libraries/src/Amazon.Lambda.CognitoEvents/Amazon.Lambda.CognitoEvents.csproj @@ -4,15 +4,15 @@ Amazon Lambda .NET Core support - CognitoEvents package. - netstandard2.0;netcoreapp3.1;net8.0 + $(DefaultPackageTargets) Amazon.Lambda.CognitoEvents - 4.0.1 + 5.0.0 Amazon.Lambda.CognitoEvents Amazon.Lambda.CognitoEvents AWS;Amazon;Lambda - + IL2026,IL2067,IL2075 true true diff --git a/Libraries/src/Amazon.Lambda.CognitoEvents/ChallengeResultElement.cs b/Libraries/src/Amazon.Lambda.CognitoEvents/ChallengeResultElement.cs index a67025566..ba8f18eaf 100644 --- a/Libraries/src/Amazon.Lambda.CognitoEvents/ChallengeResultElement.cs +++ b/Libraries/src/Amazon.Lambda.CognitoEvents/ChallengeResultElement.cs @@ -1,4 +1,4 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; namespace Amazon.Lambda.CognitoEvents { @@ -12,27 +12,21 @@ public class ChallengeResultElement /// The challenge type.One of: CUSTOM_CHALLENGE, SRP_A, PASSWORD_VERIFIER, SMS_MFA, DEVICE_SRP_AUTH, DEVICE_PASSWORD_VERIFIER, or ADMIN_NO_SRP_AUTH. /// [DataMember(Name = "challengeName")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("challengeName")] -# endif public string ChallengeName { get; set; } /// /// Set to true if the user successfully completed the challenge, or false otherwise. /// [DataMember(Name = "challengeResult")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("challengeResult")] -# endif public bool ChallengeResult { get; set; } /// /// Your name for the custom challenge.Used only if challengeName is CUSTOM_CHALLENGE. /// [DataMember(Name = "challengeMetadata")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("challengeMetadata")] -# endif public string ChallengeMetadata { get; set; } } } diff --git a/Libraries/src/Amazon.Lambda.CognitoEvents/ClaimOverrideDetails.cs b/Libraries/src/Amazon.Lambda.CognitoEvents/ClaimOverrideDetails.cs index 190146102..2b99ac2e9 100644 --- a/Libraries/src/Amazon.Lambda.CognitoEvents/ClaimOverrideDetails.cs +++ b/Libraries/src/Amazon.Lambda.CognitoEvents/ClaimOverrideDetails.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Runtime.Serialization; namespace Amazon.Lambda.CognitoEvents @@ -13,27 +13,21 @@ public class ClaimOverrideDetails /// A map of one or more key-value pairs of claims to add or override. For group related claims, use groupOverrideDetails instead. /// [DataMember(Name = "claimsToAddOrOverride")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("claimsToAddOrOverride")] -# endif public Dictionary ClaimsToAddOrOverride { get; set; } = new Dictionary(); /// /// A list that contains claims to be suppressed from the identity token. /// [DataMember(Name = "claimsToSuppress")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("claimsToSuppress")] -# endif public List ClaimsToSuppress { get; set; } = new List(); /// /// The output object containing the current group configuration. It includes groupsToOverride, iamRolesToOverride, and preferredRole. /// [DataMember(Name = "groupOverrideDetails")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("groupOverrideDetails")] -# endif public GroupConfiguration GroupOverrideDetails { get; set; } = new GroupConfiguration(); } } diff --git a/Libraries/src/Amazon.Lambda.CognitoEvents/ClaimsAndScopeOverrideDetails.cs b/Libraries/src/Amazon.Lambda.CognitoEvents/ClaimsAndScopeOverrideDetails.cs index 83b483e37..f94125410 100644 --- a/Libraries/src/Amazon.Lambda.CognitoEvents/ClaimsAndScopeOverrideDetails.cs +++ b/Libraries/src/Amazon.Lambda.CognitoEvents/ClaimsAndScopeOverrideDetails.cs @@ -12,27 +12,21 @@ public class ClaimsAndScopeOverrideDetails /// The claims that you want to override, add, or suppress in your user’s ID token. /// [DataMember(Name = "idTokenGeneration")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("idTokenGeneration")] -# endif public IdTokenGeneration IdTokenGeneration { get; set; } = new IdTokenGeneration(); /// /// The claims and scopes that you want to override, add, or suppress in your user’s access token. /// [DataMember(Name = "accessTokenGeneration")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("accessTokenGeneration")] -# endif public AccessTokenGeneration AccessTokenGeneration { get; set; } = new AccessTokenGeneration(); /// /// The output object containing the current group configuration. It includes groupsToOverride, iamRolesToOverride, and preferredRole. /// [DataMember(Name = "groupOverrideDetails")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("groupOverrideDetails")] -# endif public GroupConfiguration GroupOverrideDetails { get; set; } = new GroupConfiguration(); } } diff --git a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoCreateAuthChallengeRequest.cs b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoCreateAuthChallengeRequest.cs index fc3937e45..e706fedb0 100644 --- a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoCreateAuthChallengeRequest.cs +++ b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoCreateAuthChallengeRequest.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Runtime.Serialization; namespace Amazon.Lambda.CognitoEvents @@ -12,36 +12,28 @@ public class CognitoCreateAuthChallengeRequest : CognitoTriggerRequest /// One or more key-value pairs that you can provide as custom input to the Lambda function that you specify for the pre sign-up trigger. You can pass this data to your Lambda function by using the ClientMetadata parameter in the following API actions: AdminCreateUser, AdminRespondToAuthChallenge, ForgotPassword, and SignUp. /// [DataMember(Name = "clientMetadata")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("clientMetadata")] -# endif public Dictionary ClientMetadata { get; set; } = new Dictionary(); /// /// The name of the new challenge. /// [DataMember(Name = "challengeName")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("challengeName")] -#endif public string ChallengeName { get; set; } /// /// an array of ChallengeResult elements /// [DataMember(Name = "session")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("session")] -#endif public List Session { get; set; } = new List(); /// /// A Boolean that is populated when PreventUserExistenceErrors is set to ENABLED for your user pool client. A value of true means that the user id (user name, email address, etc.) did not match any existing users. When PreventUserExistenceErrors is set to ENABLED, the service will not report back to the app that the user does not exist. The recommended best practice is for your Lambda functions to maintain the same user experience including latency so the caller cannot detect different behavior when the user exists or doesn’t exist. /// [DataMember(Name = "userNotFound")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("userNotFound")] -#endif public bool UserNotFound { get; set; } } } diff --git a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoCreateAuthChallengeResponse.cs b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoCreateAuthChallengeResponse.cs index 2af64454b..6ec61af73 100644 --- a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoCreateAuthChallengeResponse.cs +++ b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoCreateAuthChallengeResponse.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Runtime.Serialization; namespace Amazon.Lambda.CognitoEvents @@ -12,27 +12,21 @@ public class CognitoCreateAuthChallengeResponse : CognitoTriggerResponse /// One or more key-value pairs for the client app to use in the challenge to be presented to the user.This parameter should contain all of the necessary information to accurately present the challenge to the user. /// [DataMember(Name = "publicChallengeParameters")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("publicChallengeParameters")] -#endif public Dictionary PublicChallengeParameters { get; set; } = new Dictionary(); /// /// This parameter is only used by the Verify Auth Challenge Response Lambda trigger. This parameter should contain all of the information that is required to validate the user's response to the challenge. In other words, the publicChallengeParameters parameter contains the question that is presented to the user and privateChallengeParameters contains the valid answers for the question. /// [DataMember(Name = "privateChallengeParameters")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("privateChallengeParameters")] -#endif public Dictionary PrivateChallengeParameters { get; set; } = new Dictionary(); /// /// Your name for the custom challenge, if this is a custom challenge. /// [DataMember(Name = "challengeMetadata")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("challengeMetadata")] -#endif public string ChallengeMetadata { get; set; } } } diff --git a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoCustomEmailSenderRequest.cs b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoCustomEmailSenderRequest.cs index 73deb6935..f2c52e240 100644 --- a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoCustomEmailSenderRequest.cs +++ b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoCustomEmailSenderRequest.cs @@ -1,4 +1,4 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; namespace Amazon.Lambda.CognitoEvents { @@ -11,18 +11,14 @@ public class CognitoCustomEmailSenderRequest : CognitoTriggerRequest /// The type of sender request. /// [DataMember(Name = "type")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("type")] -# endif public string Type { get; set; } /// /// The encrypted temporary authorization code. /// [DataMember(Name = "code")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("code")] -#endif public string Code { get; set; } } } diff --git a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoCustomMessageRequest.cs b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoCustomMessageRequest.cs index 7730c5d92..d276c837f 100644 --- a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoCustomMessageRequest.cs +++ b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoCustomMessageRequest.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Runtime.Serialization; namespace Amazon.Lambda.CognitoEvents @@ -12,27 +12,21 @@ public class CognitoCustomMessageRequest : CognitoTriggerRequest /// A string for you to use as the placeholder for the verification code in the custom message. /// [DataMember(Name = "codeParameter")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("codeParameter")] -#endif public string CodeParameter { get; set; } /// /// The username parameter. It is a required request parameter for the admin create user flow. /// [DataMember(Name = "usernameParameter")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("usernameParameter")] -#endif public string UsernameParameter { get; set; } /// /// One or more key-value pairs that you can provide as custom input to the Lambda function that you specify for the pre sign-up trigger. You can pass this data to your Lambda function by using the ClientMetadata parameter in the following API actions: AdminCreateUser, AdminRespondToAuthChallenge, ForgotPassword, and SignUp. /// [DataMember(Name = "clientMetadata")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("clientMetadata")] -#endif public Dictionary ClientMetadata { get; set; } = new Dictionary(); } } diff --git a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoCustomMessageResponse.cs b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoCustomMessageResponse.cs index 5dc25737c..979bdeb51 100644 --- a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoCustomMessageResponse.cs +++ b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoCustomMessageResponse.cs @@ -1,4 +1,4 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; namespace Amazon.Lambda.CognitoEvents { @@ -11,27 +11,21 @@ public class CognitoCustomMessageResponse : CognitoTriggerResponse /// The custom SMS message to be sent to your users. Must include the codeParameter value received in the request. /// [DataMember(Name = "smsMessage")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("smsMessage")] -#endif public string SmsMessage { get; set; } /// /// The custom email message to be sent to your users. Must include the codeParameter value received in the request. /// [DataMember(Name = "emailMessage")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("emailMessage")] -#endif public string EmailMessage { get; set; } /// /// The subject line for the custom message. /// [DataMember(Name = "emailSubject")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("emailSubject")] -#endif public string EmailSubject { get; set; } } } diff --git a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoCustomSmsSenderRequest.cs b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoCustomSmsSenderRequest.cs index 64c830c00..6ef4b1dcd 100644 --- a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoCustomSmsSenderRequest.cs +++ b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoCustomSmsSenderRequest.cs @@ -1,4 +1,4 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; namespace Amazon.Lambda.CognitoEvents { @@ -11,18 +11,14 @@ public class CognitoCustomSmsSenderRequest : CognitoTriggerRequest /// The type of sender request. /// [DataMember(Name = "type")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("type")] -#endif public string Type { get; set; } /// /// The encrypted temporary authorization code. /// [DataMember(Name = "code")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("code")] -#endif public string Code { get; set; } } } diff --git a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoDefineAuthChallengeRequest.cs b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoDefineAuthChallengeRequest.cs index 7cacf29ae..357840f8b 100644 --- a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoDefineAuthChallengeRequest.cs +++ b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoDefineAuthChallengeRequest.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Runtime.Serialization; namespace Amazon.Lambda.CognitoEvents @@ -12,27 +12,21 @@ public class CognitoDefineAuthChallengeRequest : CognitoTriggerRequest /// One or more key-value pairs that you can provide as custom input to the Lambda function that you specify for the pre sign-up trigger. You can pass this data to your Lambda function by using the ClientMetadata parameter in the following API actions: AdminCreateUser, AdminRespondToAuthChallenge, ForgotPassword, and SignUp. /// [DataMember(Name = "clientMetadata")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("clientMetadata")] -# endif public Dictionary ClientMetadata { get; set; } = new Dictionary(); /// /// an array of ChallengeResult elements /// [DataMember(Name = "session")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("session")] -# endif public List Session { get; set; } = new List(); /// /// A Boolean that is populated when PreventUserExistenceErrors is set to ENABLED for your user pool client. A value of true means that the user id (user name, email address, etc.) did not match any existing users. When PreventUserExistenceErrors is set to ENABLED, the service will not report back to the app that the user does not exist. The recommended best practice is for your Lambda functions to maintain the same user experience including latency so the caller cannot detect different behavior when the user exists or doesn’t exist. /// [DataMember(Name = "userNotFound")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("userNotFound")] -#endif public bool UserNotFound { get; set; } } } diff --git a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoDefineAuthChallengeResponse.cs b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoDefineAuthChallengeResponse.cs index 4ab868f7c..5f9057013 100644 --- a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoDefineAuthChallengeResponse.cs +++ b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoDefineAuthChallengeResponse.cs @@ -1,4 +1,4 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; namespace Amazon.Lambda.CognitoEvents { @@ -11,27 +11,21 @@ public class CognitoDefineAuthChallengeResponse : CognitoTriggerResponse /// A string containing the name of the next challenge. If you want to present a new challenge to your user, specify the challenge name here. /// [DataMember(Name = "challengeName")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("challengeName")] -#endif public string ChallengeName { get; set; } /// /// Set to true if you determine that the user has been sufficiently authenticated by completing the challenges, or false otherwise. /// [DataMember(Name = "issueTokens")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("issueTokens")] -#endif public bool? IssueTokens { get; set; } /// /// Set to true if you want to terminate the current authentication process, or false otherwise. /// [DataMember(Name = "failAuthentication")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("failAuthentication")] -#endif public bool? FailAuthentication { get; set; } } } diff --git a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoMigrateUserRequest.cs b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoMigrateUserRequest.cs index 2570ac473..dd0c4ec9d 100644 --- a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoMigrateUserRequest.cs +++ b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoMigrateUserRequest.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Runtime.Serialization; namespace Amazon.Lambda.CognitoEvents @@ -12,36 +12,28 @@ public class CognitoMigrateUserRequest : CognitoTriggerRequest /// The username entered by the user. /// [DataMember(Name = "userName")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("userName")] -#endif public string UserName { get; set; } /// /// The password entered by the user for sign-in. It is not set in the forgot-password flow. /// [DataMember(Name = "password")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("password")] -#endif public string Password { get; set; } /// /// One or more name-value pairs containing the validation data in the request to register a user. The validation data is set and then passed from the client in the request to register a user. You can pass this data to your Lambda function by using the ClientMetadata parameter in the InitiateAuth and AdminInitiateAuth API actions. /// [DataMember(Name = "validationData")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("validationData")] -#endif public Dictionary ValidationData { get; set; } = new Dictionary(); /// /// One or more key-value pairs that you can provide as custom input to the Lambda function that you specify for the pre sign-up trigger. You can pass this data to your Lambda function by using the ClientMetadata parameter in the following API actions: AdminCreateUser, AdminRespondToAuthChallenge, ForgotPassword, and SignUp. /// [DataMember(Name = "clientMetadata")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("clientMetadata")] -#endif public Dictionary ClientMetadata { get; set; } = new Dictionary(); } } diff --git a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoMigrateUserResponse.cs b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoMigrateUserResponse.cs index 7babcd417..267a5f405 100644 --- a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoMigrateUserResponse.cs +++ b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoMigrateUserResponse.cs @@ -12,45 +12,35 @@ public class CognitoMigrateUserResponse : CognitoTriggerResponse /// It must contain one or more name-value pairs representing user attributes to be stored in the user profile in your user pool. You can include both standard and custom user attributes. Custom attributes require the custom: prefix to distinguish them from standard attributes. /// [DataMember(Name = "userAttributes")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("userAttributes")] -#endif public Dictionary UserAttributes { get; set; } = new Dictionary(); /// /// During sign-in, this attribute can be set to CONFIRMED, or not set, to auto-confirm your users and allow them to sign-in with their previous passwords. This is the simplest experience for the user. /// [DataMember(Name = "finalUserStatus")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("finalUserStatus")] -#endif public string FinalUserStatus { get; set; } /// /// This attribute can be set to "SUPPRESS" to suppress the welcome message usually sent by Amazon Cognito to new users. If this attribute is not returned, the welcome message will be sent. /// [DataMember(Name = "messageAction")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("messageAction")] -#endif public string MessageAction { get; set; } /// /// This attribute can be set to "EMAIL" to send the welcome message by email, or "SMS" to send the welcome message by SMS. If this attribute is not returned, the welcome message will be sent by SMS. /// [DataMember(Name = "desiredDeliveryMediums")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("desiredDeliveryMediums")] -#endif public List DesiredDeliveryMediums { get; set; } = new List(); /// /// If this parameter is set to "true" and the phone number or email address specified in the UserAttributes parameter already exists as an alias with a different user, the API call will migrate the alias from the previous user to the newly created user. The previous user will no longer be able to log in using that alias. /// [DataMember(Name = "forceAliasCreation")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("forceAliasCreation")] -#endif public bool? ForceAliasCreation { get; set; } } } diff --git a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoPostAuthenticationRequest.cs b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoPostAuthenticationRequest.cs index 96e95d5cf..87aff4ac5 100644 --- a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoPostAuthenticationRequest.cs +++ b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoPostAuthenticationRequest.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Runtime.Serialization; namespace Amazon.Lambda.CognitoEvents @@ -13,27 +13,21 @@ public class CognitoPostAuthenticationRequest : CognitoTriggerRequest /// One or more name-value pairs containing the validation data in the request to register a user. The validation data is set and then passed from the client in the request to register a user. You can pass this data to your Lambda function by using the ClientMetadata parameter in the InitiateAuth and AdminInitiateAuth API actions. /// [DataMember(Name = "validationData")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("validationData")] -# endif public Dictionary ValidationData { get; set; } = new Dictionary(); /// /// This flag indicates if the user has signed in on a new device. It is set only if the remembered devices value of the user pool is set to Always or User Opt-In. /// [DataMember(Name = "newDeviceUsed")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("newDeviceUsed")] -#endif public bool NewDevicedUsed { get; set; } /// /// One or more key-value pairs that you can provide as custom input to the Lambda function that you specify for the pre sign-up trigger. You can pass this data to your Lambda function by using the ClientMetadata parameter in the following API actions: AdminCreateUser, AdminRespondToAuthChallenge, ForgotPassword, and SignUp. /// [DataMember(Name = "clientMetadata")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("clientMetadata")] -# endif public Dictionary ClientMetadata { get; set; } = new Dictionary(); } } diff --git a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoPostConfirmationRequest.cs b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoPostConfirmationRequest.cs index 8446bc599..97486f0e5 100644 --- a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoPostConfirmationRequest.cs +++ b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoPostConfirmationRequest.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Runtime.Serialization; namespace Amazon.Lambda.CognitoEvents @@ -13,9 +13,7 @@ public class CognitoPostConfirmationRequest : CognitoTriggerRequest /// One or more key-value pairs that you can provide as custom input to the Lambda function that you specify for the pre sign-up trigger. You can pass this data to your Lambda function by using the ClientMetadata parameter in the following API actions: AdminCreateUser, AdminRespondToAuthChallenge, ForgotPassword, and SignUp. /// [DataMember(Name = "clientMetadata")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("clientMetadata")] -#endif public Dictionary ClientMetadata { get; set; } = new Dictionary(); } } diff --git a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoPreAuthenticationRequest.cs b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoPreAuthenticationRequest.cs index 3db08a010..bd1a0153c 100644 --- a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoPreAuthenticationRequest.cs +++ b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoPreAuthenticationRequest.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Runtime.Serialization; namespace Amazon.Lambda.CognitoEvents @@ -13,18 +13,14 @@ public class CognitoPreAuthenticationRequest : CognitoTriggerRequest /// One or more name-value pairs containing the validation data in the request to register a user. The validation data is set and then passed from the client in the request to register a user. You can pass this data to your Lambda function by using the ClientMetadata parameter in the InitiateAuth and AdminInitiateAuth API actions. /// [DataMember(Name = "validationData")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("validationData")] -# endif public Dictionary ValidationData { get; set; } = new Dictionary(); /// /// This boolean is populated when PreventUserExistenceErrors is set to ENABLED for your User Pool client. /// [DataMember(Name = "userNotFound")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("userNotFound")] -#endif public bool UserNotFound { get; set; } } } diff --git a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoPreSignupRequest.cs b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoPreSignupRequest.cs index 3df29217a..891038b9c 100644 --- a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoPreSignupRequest.cs +++ b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoPreSignupRequest.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Runtime.Serialization; namespace Amazon.Lambda.CognitoEvents @@ -13,18 +13,14 @@ public class CognitoPreSignupRequest : CognitoTriggerRequest /// One or more name-value pairs containing the validation data in the request to register a user. The validation data is set and then passed from the client in the request to register a user. You can pass this data to your Lambda function by using the ClientMetadata parameter in the InitiateAuth and AdminInitiateAuth API actions. /// [DataMember(Name = "validationData")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("validationData")] -#endif public Dictionary ValidationData { get; set; } = new Dictionary(); /// /// One or more key-value pairs that you can provide as custom input to the Lambda function that you specify for the pre sign-up trigger. You can pass this data to your Lambda function by using the ClientMetadata parameter in the following API actions: AdminCreateUser, AdminRespondToAuthChallenge, ForgotPassword, and SignUp. /// [DataMember(Name = "clientMetadata")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("clientMetadata")] -#endif public Dictionary ClientMetadata { get; set; } = new Dictionary(); } } diff --git a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoPreSignupResponse.cs b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoPreSignupResponse.cs index d7ac1b6a9..acc5733fc 100644 --- a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoPreSignupResponse.cs +++ b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoPreSignupResponse.cs @@ -1,4 +1,4 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; namespace Amazon.Lambda.CognitoEvents { @@ -12,27 +12,21 @@ public class CognitoPreSignupResponse : CognitoTriggerResponse /// Set to true to auto-confirm the user, or false otherwise. /// [DataMember(Name = "autoConfirmUser")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("autoConfirmUser")] -#endif public bool AutoConfirmUser { get; set; } /// /// Set to true to set as verified the email of a user who is signing up, or false otherwise. If autoVerifyEmail is set to true, the email attribute must have a valid, non-null value. Otherwise an error will occur and the user will not be able to complete sign-up. /// [DataMember(Name = "autoVerifyPhone")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("autoVerifyPhone")] -#endif public bool AutoVerifyPhone { get; set; } /// /// Set to true to set as verified the phone number of a user who is signing up, or false otherwise. If autoVerifyPhone is set to true, the phone_number attribute must have a valid, non-null value. Otherwise an error will occur and the user will not be able to complete sign-up. /// [DataMember(Name = "autoVerifyEmail")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("autoVerifyEmail")] -#endif public bool AutoVerifyEmail { get; set; } } } diff --git a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoPreTokenGenerationRequest.cs b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoPreTokenGenerationRequest.cs index 78c1395bb..6d4ae42f6 100644 --- a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoPreTokenGenerationRequest.cs +++ b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoPreTokenGenerationRequest.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Runtime.Serialization; namespace Amazon.Lambda.CognitoEvents @@ -12,18 +12,14 @@ public class CognitoPreTokenGenerationRequest : CognitoTriggerRequest /// The input object containing the current group configuration. It includes groupsToOverride, iamRolesToOverride, and preferredRole. /// [DataMember(Name = "groupConfiguration")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("groupConfiguration")] -# endif public GroupConfiguration GroupConfiguration { get; set; } = new GroupConfiguration(); /// /// One or more key-value pairs that you can provide as custom input to the Lambda function that you specify for the pre sign-up trigger. You can pass this data to your Lambda function by using the ClientMetadata parameter in the following API actions: AdminVerifyUser, AdminRespondToAuthChallenge, ForgotPassword, and SignUp. /// [DataMember(Name = "clientMetadata")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("clientMetadata")] -# endif public Dictionary ClientMetadata { get; set; } = new Dictionary(); } } diff --git a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoPreTokenGenerationResponse.cs b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoPreTokenGenerationResponse.cs index 819f39603..3a8adacd2 100644 --- a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoPreTokenGenerationResponse.cs +++ b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoPreTokenGenerationResponse.cs @@ -1,4 +1,4 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; namespace Amazon.Lambda.CognitoEvents { @@ -11,9 +11,7 @@ public class CognitoPreTokenGenerationResponse : CognitoTriggerResponse /// Pre token generation response parameters /// [DataMember(Name = "claimsOverrideDetails")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("claimsOverrideDetails")] -# endif public ClaimOverrideDetails ClaimsOverrideDetails { get; set; } = new ClaimOverrideDetails(); } } diff --git a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoPreTokenGenerationV2Request.cs b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoPreTokenGenerationV2Request.cs index 3ef60f7ed..1ba3be186 100644 --- a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoPreTokenGenerationV2Request.cs +++ b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoPreTokenGenerationV2Request.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Runtime.Serialization; namespace Amazon.Lambda.CognitoEvents @@ -12,27 +12,21 @@ public class CognitoPreTokenGenerationV2Request : CognitoTriggerRequest /// The input object containing the current group configuration. It includes groupsToOverride, iamRolesToOverride, and preferredRole. /// [DataMember(Name = "groupConfiguration")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("groupConfiguration")] -# endif public GroupConfiguration GroupConfiguration { get; set; } = new GroupConfiguration(); /// /// One or more key-value pairs that you can provide as custom input to the Lambda function that you specify for the pre sign-up trigger. You can pass this data to your Lambda function by using the ClientMetadata parameter in the following API actions: AdminVerifyUser, AdminRespondToAuthChallenge, ForgotPassword, and SignUp. /// [DataMember(Name = "clientMetadata")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("clientMetadata")] -# endif public Dictionary ClientMetadata { get; set; } = new Dictionary(); /// /// A list that contains the OAuth 2.0 user scopes. /// [DataMember(Name = "scopes")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("scopes")] -# endif public List Scopes { get; set; } = new List(); } } diff --git a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoPreTokenGenerationV2Response.cs b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoPreTokenGenerationV2Response.cs index 981a1aa83..2f4e8e40f 100644 --- a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoPreTokenGenerationV2Response.cs +++ b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoPreTokenGenerationV2Response.cs @@ -1,4 +1,4 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; namespace Amazon.Lambda.CognitoEvents { @@ -11,9 +11,7 @@ public class CognitoPreTokenGenerationV2Response : CognitoTriggerResponse /// A container for all elements in a V2_0 trigger event. /// [DataMember(Name = "claimsAndScopeOverrideDetails")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("claimsAndScopeOverrideDetails")] -# endif public ClaimsAndScopeOverrideDetails ClaimsAndScopeOverrideDetails { get; set; } = new ClaimsAndScopeOverrideDetails(); } } diff --git a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoTriggerCallerContext.cs b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoTriggerCallerContext.cs index a9f73bb70..4030a529b 100644 --- a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoTriggerCallerContext.cs +++ b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoTriggerCallerContext.cs @@ -1,4 +1,4 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; namespace Amazon.Lambda.CognitoEvents { @@ -12,18 +12,14 @@ public class CognitoTriggerCallerContext /// The AWS SDK version number. /// [DataMember(Name = "awsSdkVersion")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("awsSdkVersion")] -#endif public string AwsSdkVersion { get; set; } /// /// The ID of the client associated with the user pool. /// [DataMember(Name = "clientId")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("clientId")] -#endif public string ClientId { get; set; } } diff --git a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoTriggerEvent.cs b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoTriggerEvent.cs index ff57fc458..997a66175 100644 --- a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoTriggerEvent.cs +++ b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoTriggerEvent.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Runtime.Serialization; namespace Amazon.Lambda.CognitoEvents @@ -16,72 +16,56 @@ public abstract class CognitoTriggerEvent /// The version number of your Lambda function. /// [DataMember(Name = "version")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("version")] -#endif public string Version { get; set; } /// /// The AWS Region, as an AWSRegion instance. /// [DataMember(Name = "region")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("region")] -#endif public string Region { get; set; } /// /// The user pool ID for the user pool. /// [DataMember(Name = "userPoolId")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("userPoolId")] -#endif public string UserPoolId { get; set; } /// /// The username of the current user. /// [DataMember(Name = "userName")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("userName")] -#endif public string UserName { get; set; } /// /// The caller context /// [DataMember(Name = "callerContext")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("callerContext")] -#endif public CognitoTriggerCallerContext CallerContext { get; set; } = new CognitoTriggerCallerContext(); /// /// The name of the event that triggered the Lambda function.For a description of each triggerSource see User pool Lambda trigger sources. /// [DataMember(Name = "triggerSource")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("triggerSource")] -#endif public string TriggerSource { get; set; } /// /// The request from the Amazon Cognito service /// [DataMember(Name = "request")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("request")] -#endif public TRequest Request { get; set; } = new TRequest(); /// /// The response from your Lambda trigger.The return parameters in the response depend on the triggering event. /// [DataMember(Name = "response")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("response")] -#endif public TResponse Response { get; set; } = new TResponse(); } -} \ No newline at end of file +} diff --git a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoTriggerRequest.cs b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoTriggerRequest.cs index 11f8749b8..ce5fe46f2 100644 --- a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoTriggerRequest.cs +++ b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoTriggerRequest.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Runtime.Serialization; namespace Amazon.Lambda.CognitoEvents @@ -13,9 +13,7 @@ public abstract class CognitoTriggerRequest /// One or more pairs of user attribute names and values.Each pair is in the form "name": "value". /// [DataMember(Name = "userAttributes")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("userAttributes")] -#endif public Dictionary UserAttributes { get; set; } = new Dictionary(); } } diff --git a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoVerifyAuthChallengeRequest.cs b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoVerifyAuthChallengeRequest.cs index c03f15b02..c099e04d0 100644 --- a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoVerifyAuthChallengeRequest.cs +++ b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoVerifyAuthChallengeRequest.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Runtime.Serialization; namespace Amazon.Lambda.CognitoEvents @@ -12,36 +12,28 @@ public class CognitoVerifyAuthChallengeRequest : CognitoTriggerRequest /// This parameter comes from the Create Auth Challenge trigger, and is compared against a user’s challengeAnswer to determine whether the user passed the challenge. /// [DataMember(Name = "privateChallengeParameters")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("privateChallengeParameters")] -# endif public Dictionary PrivateChallengeParameters { get; set; } = new Dictionary(); /// /// This parameter comes from the Create Auth Challenge trigger, and is compared against a user’s challengeAnswer to determine whether the user passed the challenge. /// [DataMember(Name = "challengeAnswer")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("challengeAnswer")] -# endif public string ChallengeAnswer { get; set; } = string.Empty; /// /// One or more key-value pairs that you can provide as custom input to the Lambda function that you specify for the pre sign-up trigger. You can pass this data to your Lambda function by using the ClientMetadata parameter in the following API actions: AdminVerifyUser, AdminRespondToAuthChallenge, ForgotPassword, and SignUp. /// [DataMember(Name = "clientMetadata")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("clientMetadata")] -# endif public Dictionary ClientMetadata { get; set; } /// /// This boolean is populated when PreventUserExistenceErrors is set to ENABLED for your User Pool client. /// [DataMember(Name = "userNotFound")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("userNotFound")] -# endif public bool UserNotFound { get; set; } } } diff --git a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoVerifyAuthChallengeResponse.cs b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoVerifyAuthChallengeResponse.cs index 4b1d3e527..af147aad5 100644 --- a/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoVerifyAuthChallengeResponse.cs +++ b/Libraries/src/Amazon.Lambda.CognitoEvents/CognitoVerifyAuthChallengeResponse.cs @@ -1,4 +1,4 @@ -using System.Runtime.Serialization; +using System.Runtime.Serialization; namespace Amazon.Lambda.CognitoEvents { @@ -11,9 +11,7 @@ public class CognitoVerifyAuthChallengeResponse : CognitoTriggerResponse /// Set to true if the user has successfully completed the challenge, or false otherwise. /// [DataMember(Name = "answerCorrect")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("answerCorrect")] -#endif public bool? AnswerCorrect { get; set; } } } diff --git a/Libraries/src/Amazon.Lambda.CognitoEvents/GroupConfiguration.cs b/Libraries/src/Amazon.Lambda.CognitoEvents/GroupConfiguration.cs index f8a261db0..2e0d4f132 100644 --- a/Libraries/src/Amazon.Lambda.CognitoEvents/GroupConfiguration.cs +++ b/Libraries/src/Amazon.Lambda.CognitoEvents/GroupConfiguration.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Runtime.Serialization; namespace Amazon.Lambda.CognitoEvents @@ -13,27 +13,21 @@ public class GroupConfiguration /// A list of the group names that are associated with the user that the identity token is issued for. /// [DataMember(Name = "groupsToOverride")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("groupsToOverride")] -# endif public List GroupsToOverride { get; set; } = new List(); /// /// A list of the current IAM roles associated with these groups. /// [DataMember(Name = "iamRolesToOverride")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("iamRolesToOverride")] -# endif public List IamRolesToOverride { get; set; } = new List(); /// /// A string indicating the preferred IAM role. /// [DataMember(Name = "preferredRole")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("preferredRole")] -# endif public string PreferredRole { get; set; } } } diff --git a/Libraries/src/Amazon.Lambda.CognitoEvents/IdTokenGeneration.cs b/Libraries/src/Amazon.Lambda.CognitoEvents/IdTokenGeneration.cs index 296458a70..cc14e502e 100644 --- a/Libraries/src/Amazon.Lambda.CognitoEvents/IdTokenGeneration.cs +++ b/Libraries/src/Amazon.Lambda.CognitoEvents/IdTokenGeneration.cs @@ -13,18 +13,14 @@ public class IdTokenGeneration /// A map of one or more key-value pairs of claims to add or override. For group related claims, use groupOverrideDetails instead. /// [DataMember(Name = "claimsToAddOrOverride")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("claimsToAddOrOverride")] -# endif public Dictionary ClaimsToAddOrOverride { get; set; } = new Dictionary(); /// /// A list that contains claims to be suppressed from the identity token. /// [DataMember(Name = "claimsToSuppress")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("claimsToSuppress")] -# endif public List ClaimsToSuppress { get; set; } = new List(); } } diff --git a/Libraries/src/Amazon.Lambda.ConfigEvents/Amazon.Lambda.ConfigEvents.csproj b/Libraries/src/Amazon.Lambda.ConfigEvents/Amazon.Lambda.ConfigEvents.csproj index 61478e5bb..17a16ae3c 100644 --- a/Libraries/src/Amazon.Lambda.ConfigEvents/Amazon.Lambda.ConfigEvents.csproj +++ b/Libraries/src/Amazon.Lambda.ConfigEvents/Amazon.Lambda.ConfigEvents.csproj @@ -3,16 +3,16 @@ - netstandard2.0;net8.0 + $(DefaultPackageTargets) Amazon Lambda .NET Core support - ConfigEvents package. Amazon.Lambda.ConfigEvents - 2.1.1 + 3.0.0 Amazon.Lambda.ConfigEvents Amazon.Lambda.ConfigEvents AWS;Amazon;Lambda - + IL2026,IL2067,IL2075 true true diff --git a/Libraries/src/Amazon.Lambda.ConnectEvents/Amazon.Lambda.ConnectEvents.csproj b/Libraries/src/Amazon.Lambda.ConnectEvents/Amazon.Lambda.ConnectEvents.csproj index 1a21daefb..f68a56baf 100644 --- a/Libraries/src/Amazon.Lambda.ConnectEvents/Amazon.Lambda.ConnectEvents.csproj +++ b/Libraries/src/Amazon.Lambda.ConnectEvents/Amazon.Lambda.ConnectEvents.csproj @@ -4,15 +4,15 @@ Amazon Lambda .NET Core support - Amazon Connect package. - netstandard2.0;net8.0 + $(DefaultPackageTargets) Amazon.Lambda.ConnectEvents - 1.1.1 + 2.0.0 Amazon.Lambda.ConnectEvents Amazon.Lambda.ConnectEvents AWS;Amazon;Lambda;Connect - + IL2026,IL2067,IL2075 true true diff --git a/Libraries/src/Amazon.Lambda.Core/Amazon.Lambda.Core.csproj b/Libraries/src/Amazon.Lambda.Core/Amazon.Lambda.Core.csproj index 580096c1f..2b45e1c87 100644 --- a/Libraries/src/Amazon.Lambda.Core/Amazon.Lambda.Core.csproj +++ b/Libraries/src/Amazon.Lambda.Core/Amazon.Lambda.Core.csproj @@ -3,10 +3,10 @@ - netstandard2.0;net6.0;net8.0 + netstandard2.0;$(DefaultPackageTargets) Amazon Lambda .NET Core support - Core package. Amazon.Lambda.Core - 2.8.1 + 3.1.0 Amazon.Lambda.Core Amazon.Lambda.Core AWS;Amazon;Lambda @@ -29,7 +29,7 @@ - + IL2026,IL2067,IL2075 true true diff --git a/Libraries/src/Amazon.Lambda.Core/ILambdaContext.cs b/Libraries/src/Amazon.Lambda.Core/ILambdaContext.cs index 81f290408..21f7a54f6 100644 --- a/Libraries/src/Amazon.Lambda.Core/ILambdaContext.cs +++ b/Libraries/src/Amazon.Lambda.Core/ILambdaContext.cs @@ -1,6 +1,7 @@ namespace Amazon.Lambda.Core { using System; + using System.Diagnostics.CodeAnalysis; /// /// Object that allows you to access useful information available within @@ -95,6 +96,25 @@ public interface ILambdaContext /// The trace id generated by Lambda for distributed tracing across AWS services. /// string TraceId { get { return string.Empty; } } + + /// + /// The the Lambda function registered with the + /// runtime — either the instance passed to + /// LambdaBootstrapBuilder.Create(handler, serializer) / + /// HandlerWrapper.GetHandlerWrapper(handler, serializer), or the type set + /// via [assembly: LambdaSerializer(typeof(...))] in class-library mode. + /// User code can reuse it for ad-hoc (de)serialization without re-instantiating. + /// Can be null when the function did not register a serializer (e.g., raw-stream + /// handlers). + /// + /// + /// Preview API. Class-library mode requires an updated managed + /// Lambda runtime to populate this property; until that ships, the value will + /// be null when running in class-library mode. The + /// is applied to surface this caveat at the call site. + /// + [Experimental("AWSLAMBDA001")] + ILambdaSerializer Serializer { get { return null; } } #endif } } diff --git a/Libraries/src/Amazon.Lambda.Core/ILambdaLogger.cs b/Libraries/src/Amazon.Lambda.Core/ILambdaLogger.cs index 9d4fdeeb0..327cc5015 100644 --- a/Libraries/src/Amazon.Lambda.Core/ILambdaLogger.cs +++ b/Libraries/src/Amazon.Lambda.Core/ILambdaLogger.cs @@ -3,7 +3,7 @@ namespace Amazon.Lambda.Core { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER /// /// Log Level for logging messages /// @@ -62,7 +62,7 @@ public interface ILambdaLogger /// void LogLine(string message); -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER /// /// Log message categorized by the given log level diff --git a/Libraries/src/Amazon.Lambda.Core/LambdaLogger.cs b/Libraries/src/Amazon.Lambda.Core/LambdaLogger.cs index 8e74a380b..38afe7371 100644 --- a/Libraries/src/Amazon.Lambda.Core/LambdaLogger.cs +++ b/Libraries/src/Amazon.Lambda.Core/LambdaLogger.cs @@ -36,7 +36,7 @@ public static void Log(string message) _loggingAction(message); } -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER // The name of this field must not change or be readonly because Amazon.Lambda.RuntimeSupport will use reflection to replace the // value with an Action that directs the logging into its logging system. @@ -126,6 +126,33 @@ public static void Log(string level, Exception exception, string message, params /// Message to log. The message may have format arguments. /// Arguments to format the message with. public static void Log(LogLevel level, Exception exception, string message, params object[] args) => Log(level.ToString(), exception, message, args); + + // This field must not be readonly because SetConfigureStructuredLoggingAction replaces the value + // when Amazon.Lambda.RuntimeSupport registers its structured logging callback. + private static Action _configureStructuredLoggingAction = (options) => _placeHolderStructuredLoggingOptions = options; + + // Because a user might call ConfigureStructuredLogging before the Lambda runtime has a chance to replace the _configureStructuredLoggingAction with the + // real implementation, we need to hold onto the options they provided until the Lambda runtime can use them to configure structured logging. + private static StructuredLoggingOptions _placeHolderStructuredLoggingOptions; + + /// + /// When structured logging is enabled this method will allow overriding the default configuration the Lambda runtime uses for structured logging. + /// + /// The options to use for configuring structured logging. + [RequiresPreviewFeatures("This method is in preview until the latest changes of the .NET Lambda runtime client have been deployed to the Lambda managed runtimes")] + public static void ConfigureStructuredLogging(StructuredLoggingOptions options) => _configureStructuredLoggingAction(options); + + internal static void SetConfigureStructuredLoggingAction(Action configureStructuredLoggingAction) + { + _configureStructuredLoggingAction = configureStructuredLoggingAction; + + // If a user set the structured logging options before the Lambda runtime set the real _configureStructuredLoggingAction, + // we need to call it now to make sure the user's options are applied. + if (_placeHolderStructuredLoggingOptions != null) + { + _configureStructuredLoggingAction(_placeHolderStructuredLoggingOptions); + } + } #endif } } diff --git a/Libraries/src/Amazon.Lambda.Core/LambdaSerializerAttribute.cs b/Libraries/src/Amazon.Lambda.Core/LambdaSerializerAttribute.cs index ca751cbad..56c18167e 100644 --- a/Libraries/src/Amazon.Lambda.Core/LambdaSerializerAttribute.cs +++ b/Libraries/src/Amazon.Lambda.Core/LambdaSerializerAttribute.cs @@ -26,7 +26,7 @@ public sealed class LambdaSerializerAttribute : System.Attribute /// public LambdaSerializerAttribute(Type serializerType) { - this.SerializerType = serializerType; + SerializerType = serializerType; } } diff --git a/Libraries/src/Amazon.Lambda.Core/ResponseStreaming/HttpResponseStreamPrelude.cs b/Libraries/src/Amazon.Lambda.Core/ResponseStreaming/HttpResponseStreamPrelude.cs new file mode 100644 index 000000000..1a10aa2dc --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Core/ResponseStreaming/HttpResponseStreamPrelude.cs @@ -0,0 +1,101 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +#if NET8_0_OR_GREATER +using System; +using System.Collections.Generic; +using System.Net; +using System.Runtime.Versioning; +using System.Text.Json; + +namespace Amazon.Lambda.Core.ResponseStreaming +{ + /// + /// The HTTP response prelude to be sent as the first chunk of a streaming response when using . + /// When using Lambda response streaming with a Lambda Function URL or API Gateway, the response prelude is used to set the HTTP status code, + /// headers, and cookies for the response. The prelude must be sent as the first chunk of the response stream, followed by the response body chunks. + /// This allows you to set the status code and headers for the response before sending any of the response body. + /// + [RequiresPreviewFeatures(LambdaResponseStreamFactory.PreviewMessage)] + public class HttpResponseStreamPrelude + { + /// + /// The Http status code. + /// + public HttpStatusCode? StatusCode { get; set; } + + /// + /// The response headers. This collection supports setting single value for the same headers. When using + /// Lambda Function URLs as this event source this collection should be used. + /// + public IDictionary Headers { get; set; } = new Dictionary(); + + /// + /// The response headers. This collection supports setting multiple values for the same headers. When using + /// API Gateway REST APIs as this event source this collection should be used. + /// + public IDictionary> MultiValueHeaders { get; set; } = new Dictionary>(); + + /// + /// The list of cookies. This is used for Lambda Function URL responses, which support a separate "cookies" field in + /// the response JSON for setting cookies, rather than requiring cookies to be set via the "Set-Cookie" header. + /// + public IList Cookies { get; set; } = new List(); + + internal byte[] ToByteArray() + { + var bufferWriter = new System.Buffers.ArrayBufferWriter(); + using (var writer = new Utf8JsonWriter(bufferWriter)) + { + writer.WriteStartObject(); + + if (StatusCode.HasValue) + writer.WriteNumber("statusCode", (int)StatusCode); + + if (Headers?.Count > 0) + { + writer.WriteStartObject("headers"); + foreach (var header in Headers) + { + writer.WriteString(header.Key, header.Value); + } + writer.WriteEndObject(); + } + + if (MultiValueHeaders?.Count > 0) + { + writer.WriteStartObject("multiValueHeaders"); + foreach (var header in MultiValueHeaders) + { + writer.WriteStartArray(header.Key); + foreach (var value in header.Value) + { + writer.WriteStringValue(value); + } + writer.WriteEndArray(); + } + writer.WriteEndObject(); + } + + if (Cookies?.Count > 0) + { + writer.WriteStartArray("cookies"); + foreach (var cookie in Cookies) + { + writer.WriteStringValue(cookie); + } + writer.WriteEndArray(); + } + + writer.WriteEndObject(); + } + + if (string.Equals(Environment.GetEnvironmentVariable("LAMBDA_NET_SERIALIZER_DEBUG"), "true", StringComparison.OrdinalIgnoreCase)) + { + LambdaLogger.Log(LogLevel.Information, "HTTP Response Stream Prelude JSON: {Prelude}", System.Text.Encoding.UTF8.GetString(bufferWriter.WrittenSpan)); + } + + return bufferWriter.WrittenSpan.ToArray(); + } + } +} +#endif diff --git a/Libraries/src/Amazon.Lambda.Core/ResponseStreaming/ILambdaResponseStream.cs b/Libraries/src/Amazon.Lambda.Core/ResponseStreaming/ILambdaResponseStream.cs new file mode 100644 index 000000000..4b604cc58 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Core/ResponseStreaming/ILambdaResponseStream.cs @@ -0,0 +1,38 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +#if NET8_0_OR_GREATER +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Amazon.Lambda.Core.ResponseStreaming +{ + /// + /// Interface for writing streaming responses in AWS Lambda functions. + /// Obtained by calling within a handler. + /// + internal interface ILambdaResponseStream : IDisposable + { + /// + /// Asynchronously writes a portion of a byte array to the response stream. + /// + /// The byte array containing data to write. + /// The zero-based byte offset in buffer at which to begin copying bytes. + /// The number of bytes to write. + /// Optional cancellation token. + /// A task representing the asynchronous operation. + /// Thrown if the stream is already completed or an error has been reported. + Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken = default); + + /// + /// Gets the total number of bytes written to the stream so far. + /// + long BytesWritten { get; } + + /// + /// Gets whether an error has been reported. + /// + bool HasError { get; } + } +} +#endif diff --git a/Libraries/src/Amazon.Lambda.Core/ResponseStreaming/LambdaResponseStream.cs b/Libraries/src/Amazon.Lambda.Core/ResponseStreaming/LambdaResponseStream.cs new file mode 100644 index 000000000..83ac446a4 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Core/ResponseStreaming/LambdaResponseStream.cs @@ -0,0 +1,123 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +#if NET8_0_OR_GREATER + +using System; +using System.IO; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; + +namespace Amazon.Lambda.Core.ResponseStreaming +{ + /// + /// A write-only, non-seekable subclass that streams response data + /// to the Lambda Runtime API. Returned by . + /// Integrates with standard .NET stream consumers such as . + /// + [RequiresPreviewFeatures(LambdaResponseStreamFactory.PreviewMessage)] + public class LambdaResponseStream : Stream + { + private readonly ILambdaResponseStream _responseStream; + + internal LambdaResponseStream(ILambdaResponseStream responseStream) + { + _responseStream = responseStream; + } + + /// + /// The number of bytes written to the Lambda response stream so far. + /// + public long BytesWritten => _responseStream.BytesWritten; + + /// + /// Asynchronously writes a byte array to the response stream. + /// + /// The byte array to write. + /// Optional cancellation token. + /// A task representing the asynchronous operation. + /// Thrown if the stream is already completed or an error has been reported. + public async Task WriteAsync(byte[] buffer, CancellationToken cancellationToken = default) + { + if (buffer == null) + throw new ArgumentNullException(nameof(buffer)); + + await WriteAsync(buffer, 0, buffer.Length, cancellationToken); + } + + /// + /// Asynchronously writes a portion of a byte array to the response stream. + /// + /// The byte array containing data to write. + /// The zero-based byte offset in buffer at which to begin copying bytes. + /// The number of bytes to write. + /// Optional cancellation token. + /// A task representing the asynchronous operation. + /// Thrown if the stream is already completed or an error has been reported. + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken = default) + { + await _responseStream.WriteAsync(buffer, offset, count, cancellationToken); + } + + #region Noop Overrides + + /// Gets a value indicating whether the stream supports reading. Always false. + public override bool CanRead => false; + + /// Gets a value indicating whether the stream supports seeking. Always false. + public override bool CanSeek => false; + + /// Gets a value indicating whether the stream supports writing. Always true. + public override bool CanWrite => true; + + /// + /// Gets the total number of bytes written to the stream so far. + /// Equivalent to . + /// + public override long Length => BytesWritten; + + /// + /// Getting or setting the position is not supported. + /// + /// Always thrown. + public override long Position + { + get => throw new NotSupportedException($"{nameof(LambdaResponseStream)} does not support seeking."); + set => throw new NotSupportedException($"{nameof(LambdaResponseStream)} does not support seeking."); + } + + /// Not supported. + /// Always thrown. + public override long Seek(long offset, SeekOrigin origin) + => throw new NotImplementedException($"{nameof(LambdaResponseStream)} does not support seeking."); + + /// Not supported. + /// Always thrown. + public override int Read(byte[] buffer, int offset, int count) + => throw new NotImplementedException($"{nameof(LambdaResponseStream)} does not support reading."); + + /// Not supported. + /// Always thrown. + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => throw new NotImplementedException($"{nameof(LambdaResponseStream)} does not support reading."); + + /// + /// Writes a sequence of bytes to the stream. Delegates to the async path synchronously. + /// Prefer to avoid blocking. + /// + public override void Write(byte[] buffer, int offset, int count) + => WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); + + /// + /// Flush is a no-op; data is sent to the Runtime API immediately on each write. + /// + public override void Flush() { } + + /// Not supported. + /// Always thrown. + public override void SetLength(long value) + => throw new NotSupportedException($"{nameof(LambdaResponseStream)} does not support SetLength."); + #endregion + } +} +#endif diff --git a/Libraries/src/Amazon.Lambda.Core/ResponseStreaming/LambdaResponseStreamFactory.cs b/Libraries/src/Amazon.Lambda.Core/ResponseStreaming/LambdaResponseStreamFactory.cs new file mode 100644 index 000000000..1b9e6d3b6 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Core/ResponseStreaming/LambdaResponseStreamFactory.cs @@ -0,0 +1,72 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +#if NET8_0_OR_GREATER +using System; +using System.IO; +using System.Runtime.Versioning; + +namespace Amazon.Lambda.Core.ResponseStreaming +{ + /// + /// Factory to create Lambda response streams for writing streaming responses in AWS Lambda functions. The created streams are write-only and non-seekable. + /// + [RequiresPreviewFeatures(LambdaResponseStreamFactory.PreviewMessage)] + public class LambdaResponseStreamFactory + { + internal const string PreviewMessage = + "Response streaming is in preview till a new version of .NET Lambda runtime client that supports response streaming " + + "has been deployed to the .NET Lambda managed runtime. Till deployment has been made the feature can be used by deploying as an " + + "executable including the latest version of Amazon.Lambda.RuntimeSupport and setting the \"EnablePreviewFeatures\" in the Lambda " + + "project file to \"true\""; + + internal const string UninitializedFactoryMessage = + "LambdaResponseStreamFactory is not initialized. This is caused by mismatch versions of Amazon.Lambda.Core and Amazon.Lambda.RuntimeSupport. " + + "Update both packages to the current version to address the issue."; + + private static Func _streamFactory; + + internal static void SetLambdaResponseStream(Func streamFactory) + { + _streamFactory = streamFactory ?? throw new ArgumentNullException(nameof(streamFactory)); + } + + /// + /// Creates a a subclass of that can be used to write streaming responses back to callers of the Lambda function. Once + /// a Lambda function creates a response stream all output must be returned by writing to the stream; the Lambda function's handler + /// return value will be ignored. The stream is write-only and non-seekable. + /// + /// + public static LambdaResponseStream CreateStream() + { + if (_streamFactory == null) + throw new InvalidOperationException(UninitializedFactoryMessage); + + var runtimeResponseStream = _streamFactory(Array.Empty()); + return new LambdaResponseStream(runtimeResponseStream); + } + + /// + /// Creates a a subclass of for writing streaming responses, with an HTTP response prelude containing status code and headers. This should be used for + /// Lambda functions using response streaming that are invoked via the Lambda Function URLs or API Gateway HTTP APIs, where the response format is expected to be an HTTP response. + /// The prelude will be serialized and sent as the first chunk of the response stream, and should contain any necessary HTTP status code and headers. + /// + /// Once a Lambda function creates a response stream all output must be returned by writing to the stream; the Lambda function's handler + /// return value will be ignored. The stream is write-only and non-seekable. + /// + /// + /// The HTTP response prelude including status code and headers. + /// + public static LambdaResponseStream CreateHttpStream(HttpResponseStreamPrelude prelude) + { + if (_streamFactory == null) + throw new InvalidOperationException(UninitializedFactoryMessage); + + if (prelude is null) + throw new ArgumentNullException(nameof(prelude)); + + var runtimeResponseStream = _streamFactory(prelude.ToByteArray()); + return new LambdaResponseStream(runtimeResponseStream); + } + } +} +#endif diff --git a/Libraries/src/Amazon.Lambda.Core/SnapshotRestore.cs b/Libraries/src/Amazon.Lambda.Core/SnapshotRestore.cs index 87854bf01..bebf326d4 100644 --- a/Libraries/src/Amazon.Lambda.Core/SnapshotRestore.cs +++ b/Libraries/src/Amazon.Lambda.Core/SnapshotRestore.cs @@ -56,4 +56,4 @@ public static void RegisterAfterRestore(Func afterRestoreAction) } } #endif -} \ No newline at end of file +} diff --git a/Libraries/src/Amazon.Lambda.Core/StructuredLoggingOptions.cs b/Libraries/src/Amazon.Lambda.Core/StructuredLoggingOptions.cs new file mode 100644 index 000000000..4ee425c96 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Core/StructuredLoggingOptions.cs @@ -0,0 +1,18 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +#if NET8_0_OR_GREATER +using System.Text.Json; + +namespace Amazon.Lambda.Core; + +/// +/// The options that can be overridden for structured logging. +/// +public class StructuredLoggingOptions +{ + /// + /// Override the default JsonSerializerOptions used by the Lambda runtime for serializing object parameters in structured logs. + /// + public JsonSerializerOptions OverrideSerializerOptions { get; set; } +} +#endif diff --git a/Libraries/src/Amazon.Lambda.DynamoDBEvents.SDK.Convertor/Amazon.Lambda.DynamoDBEvents.SDK.Convertor.csproj b/Libraries/src/Amazon.Lambda.DynamoDBEvents.SDK.Convertor/Amazon.Lambda.DynamoDBEvents.SDK.Convertor.csproj index 512a580cd..53216739a 100644 --- a/Libraries/src/Amazon.Lambda.DynamoDBEvents.SDK.Convertor/Amazon.Lambda.DynamoDBEvents.SDK.Convertor.csproj +++ b/Libraries/src/Amazon.Lambda.DynamoDBEvents.SDK.Convertor/Amazon.Lambda.DynamoDBEvents.SDK.Convertor.csproj @@ -4,12 +4,12 @@ Amazon Lambda .NET Core support - DynamoDBEvents SDK Convertor package. - net8.0 + $(DefaultPackageTargets) Amazon.Lambda.DynamoDBEvents.SDK.Convertor Amazon.Lambda.DynamoDBEvents.SDK.Convertor Amazon.Lambda.DynamoDBEvents.SDK.Convertor AWS;Amazon;Lambda - 2.0.2 + 3.0.0 diff --git a/Libraries/src/Amazon.Lambda.DynamoDBEvents/Amazon.Lambda.DynamoDBEvents.csproj b/Libraries/src/Amazon.Lambda.DynamoDBEvents/Amazon.Lambda.DynamoDBEvents.csproj index 38f8f76bf..421e86bac 100644 --- a/Libraries/src/Amazon.Lambda.DynamoDBEvents/Amazon.Lambda.DynamoDBEvents.csproj +++ b/Libraries/src/Amazon.Lambda.DynamoDBEvents/Amazon.Lambda.DynamoDBEvents.csproj @@ -3,10 +3,10 @@ - netcoreapp3.1;net8.0 + $(DefaultPackageTargets) Amazon Lambda .NET Core support - DynamoDBEvents package. Amazon.Lambda.DynamoDBEvents - 3.1.2 + 4.0.0 Amazon.Lambda.DynamoDBEvents Amazon.Lambda.DynamoDBEvents AWS;Amazon;Lambda @@ -20,7 +20,7 @@ - + IL2026,IL2067,IL2075 true true diff --git a/Libraries/src/Amazon.Lambda.DynamoDBEvents/Converters/DictionaryLongToStringJsonConverter.cs b/Libraries/src/Amazon.Lambda.DynamoDBEvents/Converters/DictionaryLongToStringJsonConverter.cs index 7d68b7ff9..b33fdcc63 100644 --- a/Libraries/src/Amazon.Lambda.DynamoDBEvents/Converters/DictionaryLongToStringJsonConverter.cs +++ b/Libraries/src/Amazon.Lambda.DynamoDBEvents/Converters/DictionaryLongToStringJsonConverter.cs @@ -1,12 +1,16 @@ -using System; +using System; using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; namespace Amazon.Lambda.DynamoDBEvents.Converters { + /// + /// JSON converter to convert a JSON object with string keys and long values to a Dictionary<string, string> where the long values are converted to strings. + /// public class DictionaryLongToStringJsonConverter : JsonConverter> { + /// public override Dictionary Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options) { if (reader.TokenType != JsonTokenType.StartObject) @@ -38,25 +42,21 @@ public override Dictionary Read(ref Utf8JsonReader reader, Type // Get the value. reader.Read(); - var keyValue = ExtractValue(ref reader, options); + var keyValue = ExtractValue(ref reader); dictionary.Add(propertyName, keyValue); } return dictionary; } + /// public override void Write(Utf8JsonWriter writer, Dictionary value, JsonSerializerOptions options) { -#if NET8_0_OR_GREATER // For .NET 8+ use source generation for serialization to be trimming complaint JsonSerializer.Serialize(writer, value, typeof(Dictionary), new DictionaryStringStringJsonSerializerContext(options)); -#else - // Use the built-in serializer, because it can handle dictionaries with string keys. - JsonSerializer.Serialize(writer, value, options); -#endif } - private string ExtractValue(ref Utf8JsonReader reader, JsonSerializerOptions options) + private string ExtractValue(ref Utf8JsonReader reader) { switch (reader.TokenType) { @@ -76,7 +76,6 @@ private string ExtractValue(ref Utf8JsonReader reader, JsonSerializerOptions opt } -#if NET8_0_OR_GREATER /// /// Context used for writing converter /// @@ -85,5 +84,4 @@ public partial class DictionaryStringStringJsonSerializerContext : JsonSerialize { } -#endif -} \ No newline at end of file +} diff --git a/Libraries/src/Amazon.Lambda.DynamoDBEvents/DynamoDBTimeWindowEvent.cs b/Libraries/src/Amazon.Lambda.DynamoDBEvents/DynamoDBTimeWindowEvent.cs index 97e5644e8..7ff8a51db 100644 --- a/Libraries/src/Amazon.Lambda.DynamoDBEvents/DynamoDBTimeWindowEvent.cs +++ b/Libraries/src/Amazon.Lambda.DynamoDBEvents/DynamoDBTimeWindowEvent.cs @@ -3,10 +3,8 @@ namespace Amazon.Lambda.DynamoDBEvents using System; using System.Collections.Generic; -#if NETCOREAPP3_1_OR_GREATER using Amazon.Lambda.DynamoDBEvents.Converters; using System.Text.Json.Serialization; -#endif /// /// Represents an Amazon DynamodDB event when using time windows. @@ -22,9 +20,7 @@ public class DynamoDBTimeWindowEvent : DynamoDBEvent /// /// State being built up to this invoke in the time window. /// -#if NETCOREAPP3_1_OR_GREATER [JsonConverter(typeof(DictionaryLongToStringJsonConverter))] -#endif public Dictionary State { get; set; } /// diff --git a/Libraries/src/Amazon.Lambda.DynamoDBEvents/DynamoDBTimeWindowResponse.cs b/Libraries/src/Amazon.Lambda.DynamoDBEvents/DynamoDBTimeWindowResponse.cs index 73a14ab7e..2361e38f9 100644 --- a/Libraries/src/Amazon.Lambda.DynamoDBEvents/DynamoDBTimeWindowResponse.cs +++ b/Libraries/src/Amazon.Lambda.DynamoDBEvents/DynamoDBTimeWindowResponse.cs @@ -1,13 +1,11 @@ -namespace Amazon.Lambda.DynamoDBEvents +namespace Amazon.Lambda.DynamoDBEvents { using System; using System.Collections.Generic; using System.Runtime.Serialization; -#if NETCOREAPP3_1_OR_GREATER using Amazon.Lambda.DynamoDBEvents.Converters; using System.Text.Json.Serialization; -#endif /// /// Response type to return a new state for the time window and to report batch item failures. @@ -19,10 +17,8 @@ public class DynamoDBTimeWindowResponse /// New state after processing a batch of records. /// [DataMember(Name = "state")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("state")] [JsonConverter(typeof(DictionaryLongToStringJsonConverter))] -#endif public Dictionary State { get; set; } /// @@ -30,9 +26,7 @@ public class DynamoDBTimeWindowResponse /// Returning the first record which failed would retry all remaining records from the batch. /// [DataMember(Name = "batchItemFailures")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("batchItemFailures")] -#endif public IList BatchItemFailures { get; set; } /// @@ -45,9 +39,7 @@ public class BatchItemFailure /// Sequence number of the record which failed processing. /// [DataMember(Name = "itemIdentifier")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("itemIdentifier")] -#endif public string ItemIdentifier { get; set; } } } diff --git a/Libraries/src/Amazon.Lambda.DynamoDBEvents/ExtensionMethods.cs b/Libraries/src/Amazon.Lambda.DynamoDBEvents/ExtensionMethods.cs index 29158f4ef..59911feb8 100644 --- a/Libraries/src/Amazon.Lambda.DynamoDBEvents/ExtensionMethods.cs +++ b/Libraries/src/Amazon.Lambda.DynamoDBEvents/ExtensionMethods.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Text; using System.Text.Json; @@ -88,12 +88,7 @@ private static void WriteJsonValue(Utf8JsonWriter writer, AttributeValue attribu } else if (attribute.N != null) { -#if NETCOREAPP3_1 // WriteRawValue was added in .NET 6, but we need to write out Number values without quotes - using var document = JsonDocument.Parse(attribute.N); - document.WriteTo(writer); -#else writer.WriteRawValue(attribute.N); -#endif } else if (attribute.B != null) { @@ -134,12 +129,7 @@ private static void WriteJsonValue(Utf8JsonWriter writer, AttributeValue attribu writer.WriteStartArray(); foreach (var item in attribute.NS) { -#if NETCOREAPP3_1 // WriteRawValue was added in .NET 6, but we need to write out Number values without quotes - using var document = JsonDocument.Parse(item); - document.WriteTo(writer); -#else writer.WriteRawValue(item); -#endif } writer.WriteEndArray(); } diff --git a/Libraries/src/Amazon.Lambda.DynamoDBEvents/StreamsEventResponse.cs b/Libraries/src/Amazon.Lambda.DynamoDBEvents/StreamsEventResponse.cs index cb5c3a754..0a8337ee3 100644 --- a/Libraries/src/Amazon.Lambda.DynamoDBEvents/StreamsEventResponse.cs +++ b/Libraries/src/Amazon.Lambda.DynamoDBEvents/StreamsEventResponse.cs @@ -1,4 +1,4 @@ -namespace Amazon.Lambda.DynamoDBEvents +namespace Amazon.Lambda.DynamoDBEvents { using System.Collections.Generic; using System.Runtime.Serialization; @@ -14,9 +14,7 @@ public class StreamsEventResponse /// A list of records which failed processing. Returning the first record which failed would retry all remaining records from the batch. /// [DataMember(Name = "batchItemFailures", EmitDefaultValue = false)] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("batchItemFailures")] -#endif public IList BatchItemFailures { get; set; } /// @@ -29,10 +27,8 @@ public class BatchItemFailure /// Sequence number of the record which failed processing. /// [DataMember(Name = "itemIdentifier", EmitDefaultValue = false)] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("itemIdentifier")] -#endif public string ItemIdentifier { get; set; } } } -} \ No newline at end of file +} diff --git a/Libraries/src/Amazon.Lambda.KafkaEvents/Amazon.Lambda.KafkaEvents.csproj b/Libraries/src/Amazon.Lambda.KafkaEvents/Amazon.Lambda.KafkaEvents.csproj index 325133e2a..9077cfb1a 100644 --- a/Libraries/src/Amazon.Lambda.KafkaEvents/Amazon.Lambda.KafkaEvents.csproj +++ b/Libraries/src/Amazon.Lambda.KafkaEvents/Amazon.Lambda.KafkaEvents.csproj @@ -3,16 +3,16 @@ - netstandard2.0;net8.0 + $(DefaultPackageTargets) Amazon Lambda .NET Core support - KafkaEvents package. Amazon.Lambda.KafkaEvents - 2.1.1 + 3.0.0 Amazon.Lambda.KafkaEvents Amazon.Lambda.KafkaEvents AWS;Amazon;Lambda;Kafka - + IL2026,IL2067,IL2075 true true diff --git a/Libraries/src/Amazon.Lambda.KinesisAnalyticsEvents/Amazon.Lambda.KinesisAnalyticsEvents.csproj b/Libraries/src/Amazon.Lambda.KinesisAnalyticsEvents/Amazon.Lambda.KinesisAnalyticsEvents.csproj index 449ba7879..cc8c80119 100644 --- a/Libraries/src/Amazon.Lambda.KinesisAnalyticsEvents/Amazon.Lambda.KinesisAnalyticsEvents.csproj +++ b/Libraries/src/Amazon.Lambda.KinesisAnalyticsEvents/Amazon.Lambda.KinesisAnalyticsEvents.csproj @@ -3,16 +3,16 @@ - netstandard2.0;netcoreapp3.1;net8.0 + $(DefaultPackageTargets) Amazon Lambda .NET Core support - Amazon Kinesis Analytics package. Amazon.Lambda.KinesisAnalyticsEvents - 2.3.1 + 3.0.0 Amazon.Lambda.KinesisAnalyticsEvents Amazon.Lambda.KinesisAnalyticsEvents AWS;Amazon;Lambda;KinesisAnalytics - + IL2026,IL2067,IL2075 true true diff --git a/Libraries/src/Amazon.Lambda.KinesisAnalyticsEvents/KinesisAnalyticsFirehoseInputPreprocessingEvent.cs b/Libraries/src/Amazon.Lambda.KinesisAnalyticsEvents/KinesisAnalyticsFirehoseInputPreprocessingEvent.cs index 9108aa3cd..04307b32c 100644 --- a/Libraries/src/Amazon.Lambda.KinesisAnalyticsEvents/KinesisAnalyticsFirehoseInputPreprocessingEvent.cs +++ b/Libraries/src/Amazon.Lambda.KinesisAnalyticsEvents/KinesisAnalyticsFirehoseInputPreprocessingEvent.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Runtime.Serialization; @@ -69,9 +69,7 @@ public class FirehoseRecord /// The record metadata. /// [DataMember(Name = "kinesisFirehoseRecordMetadata")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("kinesisFirehoseRecordMetadata")] -#endif public KinesisFirehoseRecordMetadata RecordMetadata { get; set; } /// @@ -84,9 +82,7 @@ public class KinesisFirehoseRecordMetadata /// The approximate time the record was sent to Kinesis Firehose. /// [IgnoreDataMember] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonIgnore] -#endif public DateTime ApproximateArrivalTimestamp { get @@ -100,9 +96,7 @@ public DateTime ApproximateArrivalTimestamp /// The approximate time the record was sent to Kinesis Firehose in epoch. /// [DataMember(Name = "approximateArrivalTimestamp")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("approximateArrivalTimestamp")] -#endif public long ApproximateArrivalEpoch { get; set; } } @@ -114,9 +108,7 @@ public DateTime ApproximateArrivalTimestamp /// The base64 encoded data. /// [DataMember(Name = "data")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("data")] -#endif public string Base64EncodedData { get; set; } /// @@ -125,7 +117,7 @@ public DateTime ApproximateArrivalTimestamp /// public string DecodeData() { - var decodedData = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(this.Base64EncodedData)); + var decodedData = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(Base64EncodedData)); return decodedData; } } diff --git a/Libraries/src/Amazon.Lambda.KinesisAnalyticsEvents/KinesisAnalyticsInputPreprocessingResponse.cs b/Libraries/src/Amazon.Lambda.KinesisAnalyticsEvents/KinesisAnalyticsInputPreprocessingResponse.cs index 7d99991f4..a2cd08035 100644 --- a/Libraries/src/Amazon.Lambda.KinesisAnalyticsEvents/KinesisAnalyticsInputPreprocessingResponse.cs +++ b/Libraries/src/Amazon.Lambda.KinesisAnalyticsEvents/KinesisAnalyticsInputPreprocessingResponse.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Runtime.Serialization; @@ -33,9 +33,7 @@ public class KinesisAnalyticsInputPreprocessingResponse /// The records. /// [DataMember(Name = "records")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("records")] -#endif public IList Records { get; set; } /// @@ -51,9 +49,7 @@ public class Record /// The record identifier. /// [DataMember(Name = "recordId")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("recordId")] -#endif public string RecordId { get; set; } /// @@ -63,9 +59,7 @@ public class Record /// The result. /// [DataMember(Name = "result")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("result")] -#endif public string Result { get; set; } /// @@ -75,9 +69,7 @@ public class Record /// The base64 encoded data. /// [DataMember(Name = "data")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("data")] -#endif public string Base64EncodedData { get; set; } /// @@ -86,7 +78,7 @@ public class Record /// The data. public void EncodeData(string data) { - this.Base64EncodedData = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(data)); + Base64EncodedData = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(data)); } } } diff --git a/Libraries/src/Amazon.Lambda.KinesisAnalyticsEvents/KinesisAnalyticsOutputDeliveryEvent.cs b/Libraries/src/Amazon.Lambda.KinesisAnalyticsEvents/KinesisAnalyticsOutputDeliveryEvent.cs index 36b6364aa..1646be331 100644 --- a/Libraries/src/Amazon.Lambda.KinesisAnalyticsEvents/KinesisAnalyticsOutputDeliveryEvent.cs +++ b/Libraries/src/Amazon.Lambda.KinesisAnalyticsEvents/KinesisAnalyticsOutputDeliveryEvent.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Runtime.Serialization; @@ -85,9 +85,7 @@ public class LambdaDeliveryRecordMetadata /// The base64 encoded data. /// [DataMember(Name = "data")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("data")] -#endif public string Base64EncodedData { get; set; } /// @@ -96,9 +94,9 @@ public class LambdaDeliveryRecordMetadata /// public string DecodeData() { - var decodedData = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(this.Base64EncodedData)); + var decodedData = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(Base64EncodedData)); return decodedData; } } } -} \ No newline at end of file +} diff --git a/Libraries/src/Amazon.Lambda.KinesisAnalyticsEvents/KinesisAnalyticsOutputDeliveryResponse.cs b/Libraries/src/Amazon.Lambda.KinesisAnalyticsEvents/KinesisAnalyticsOutputDeliveryResponse.cs index b844df0b1..28b233310 100644 --- a/Libraries/src/Amazon.Lambda.KinesisAnalyticsEvents/KinesisAnalyticsOutputDeliveryResponse.cs +++ b/Libraries/src/Amazon.Lambda.KinesisAnalyticsEvents/KinesisAnalyticsOutputDeliveryResponse.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Runtime.Serialization; @@ -28,9 +28,7 @@ public class KinesisAnalyticsOutputDeliveryResponse /// The records. /// [DataMember(Name = "records")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("records")] -#endif public IList Records { get; set; } /// @@ -46,9 +44,7 @@ public class Record /// The record identifier. /// [DataMember(Name = "recordId")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("recordId")] -#endif public string RecordId { get; set; } /// @@ -58,9 +54,7 @@ public class Record /// The result. /// [DataMember(Name = "result")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("result")] -#endif public string Result { get; set; } } } diff --git a/Libraries/src/Amazon.Lambda.KinesisAnalyticsEvents/KinesisAnalyticsStreamsInputPreprocessingEvent.cs b/Libraries/src/Amazon.Lambda.KinesisAnalyticsEvents/KinesisAnalyticsStreamsInputPreprocessingEvent.cs index 2aab44a5e..d590cf5aa 100644 --- a/Libraries/src/Amazon.Lambda.KinesisAnalyticsEvents/KinesisAnalyticsStreamsInputPreprocessingEvent.cs +++ b/Libraries/src/Amazon.Lambda.KinesisAnalyticsEvents/KinesisAnalyticsStreamsInputPreprocessingEvent.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Runtime.Serialization; @@ -68,9 +68,7 @@ public class StreamsRecord /// The record metadata. /// [DataMember(Name = "kinesisStreamRecordMetadata")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("kinesisStreamRecordMetadata")] -#endif public KinesisStreamRecordMetadata RecordMetadata { get; set; } /// @@ -101,9 +99,7 @@ public class KinesisStreamRecordMetadata /// The approximate time the record was sent to Kinesis Steam. /// [IgnoreDataMember] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonIgnore] -#endif public DateTime ApproximateArrivalTimestamp { get @@ -117,9 +113,7 @@ public DateTime ApproximateArrivalTimestamp /// The approximate time the record was sent to Kinesis stream in epoch. /// [DataMember(Name = "approximateArrivalTimestamp")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("approximateArrivalTimestamp")] -#endif public long ApproximateArrivalEpoch { get; set; } /// @@ -140,9 +134,7 @@ public DateTime ApproximateArrivalTimestamp /// The base64 encoded data. /// [DataMember(Name = "data")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("data")] -#endif public string Base64EncodedData { get; set; } /// @@ -151,7 +143,7 @@ public DateTime ApproximateArrivalTimestamp /// public string DecodeData() { - var decodedData = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(this.Base64EncodedData)); + var decodedData = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(Base64EncodedData)); return decodedData; } } diff --git a/Libraries/src/Amazon.Lambda.KinesisEvents/Amazon.Lambda.KinesisEvents.csproj b/Libraries/src/Amazon.Lambda.KinesisEvents/Amazon.Lambda.KinesisEvents.csproj index a10527f8b..304692c30 100644 --- a/Libraries/src/Amazon.Lambda.KinesisEvents/Amazon.Lambda.KinesisEvents.csproj +++ b/Libraries/src/Amazon.Lambda.KinesisEvents/Amazon.Lambda.KinesisEvents.csproj @@ -3,10 +3,10 @@ - netstandard2.0;netcoreapp3.1;net8.0 + $(DefaultPackageTargets) Amazon Lambda .NET Core support - KinesisEvents package. Amazon.Lambda.KinesisEvents - 3.0.2 + 4.0.0 Amazon.Lambda.KinesisEvents Amazon.Lambda.KinesisEvents AWS;Amazon;Lambda @@ -25,7 +25,7 @@ - + IL2026,IL2067,IL2075 true true diff --git a/Libraries/src/Amazon.Lambda.KinesisEvents/Converters/DictionaryLongToStringJsonConverter.cs b/Libraries/src/Amazon.Lambda.KinesisEvents/Converters/DictionaryLongToStringJsonConverter.cs index 73d39b5a4..bc50d5019 100644 --- a/Libraries/src/Amazon.Lambda.KinesisEvents/Converters/DictionaryLongToStringJsonConverter.cs +++ b/Libraries/src/Amazon.Lambda.KinesisEvents/Converters/DictionaryLongToStringJsonConverter.cs @@ -1,12 +1,16 @@ -using System; +using System; using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; namespace Amazon.Lambda.KinesisEvents.Converters { + /// + /// JSON converter to convert a JSON object with string keys and long values to a Dictionary<string, string> where the long values are converted to strings. + /// public class DictionaryLongToStringJsonConverter : JsonConverter> { + /// public override Dictionary Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options) { if (reader.TokenType != JsonTokenType.StartObject) @@ -38,25 +42,21 @@ public override Dictionary Read(ref Utf8JsonReader reader, Type // Get the value. reader.Read(); - var keyValue = ExtractValue(ref reader, options); + var keyValue = ExtractValue(ref reader); dictionary.Add(propertyName, keyValue); } return dictionary; } + /// public override void Write(Utf8JsonWriter writer, Dictionary value, JsonSerializerOptions options) { -#if NET8_0_OR_GREATER // For .NET 8+ use source generation for serialization to be trimming complaint JsonSerializer.Serialize(writer, value, typeof(Dictionary), new DictionaryStringStringJsonSerializerContext(options)); -#else - // Use the built-in serializer, because it can handle dictionaries with string keys. - JsonSerializer.Serialize(writer, value, options); -#endif } - private string ExtractValue(ref Utf8JsonReader reader, JsonSerializerOptions options) + private string ExtractValue(ref Utf8JsonReader reader) { switch (reader.TokenType) { @@ -74,7 +74,6 @@ private string ExtractValue(ref Utf8JsonReader reader, JsonSerializerOptions opt } } -#if NET8_0_OR_GREATER /// /// Context used for writing converter /// @@ -83,5 +82,4 @@ public partial class DictionaryStringStringJsonSerializerContext : JsonSerialize { } -#endif -} \ No newline at end of file +} diff --git a/Libraries/src/Amazon.Lambda.KinesisEvents/KinesisTimeWindowEvent.cs b/Libraries/src/Amazon.Lambda.KinesisEvents/KinesisTimeWindowEvent.cs index d0a55658d..a60227f1e 100644 --- a/Libraries/src/Amazon.Lambda.KinesisEvents/KinesisTimeWindowEvent.cs +++ b/Libraries/src/Amazon.Lambda.KinesisEvents/KinesisTimeWindowEvent.cs @@ -2,11 +2,8 @@ namespace Amazon.Lambda.KinesisEvents { using System; using System.Collections.Generic; - -#if NETCOREAPP3_1_OR_GREATER using Amazon.Lambda.KinesisEvents.Converters; using System.Text.Json.Serialization; -#endif /// /// Represents an Amazon Kinesis event when using time windows. @@ -22,9 +19,7 @@ public class KinesisTimeWindowEvent : KinesisEvent /// /// State being built up to this invoke in the time window. /// -#if NETCOREAPP3_1_OR_GREATER [JsonConverter(typeof(DictionaryLongToStringJsonConverter))] -#endif public Dictionary State { get; set; } /// diff --git a/Libraries/src/Amazon.Lambda.KinesisEvents/KinesisTimeWindowResponse.cs b/Libraries/src/Amazon.Lambda.KinesisEvents/KinesisTimeWindowResponse.cs index 7454f7ab0..1a746431d 100644 --- a/Libraries/src/Amazon.Lambda.KinesisEvents/KinesisTimeWindowResponse.cs +++ b/Libraries/src/Amazon.Lambda.KinesisEvents/KinesisTimeWindowResponse.cs @@ -1,13 +1,10 @@ -namespace Amazon.Lambda.KinesisEvents +namespace Amazon.Lambda.KinesisEvents { using System; using System.Collections.Generic; using System.Runtime.Serialization; - -#if NETCOREAPP3_1_OR_GREATER using Amazon.Lambda.KinesisEvents.Converters; using System.Text.Json.Serialization; -#endif /// /// Response type to return a new state for the time window and to report batch item failures. @@ -19,10 +16,8 @@ public class KinesisTimeWindowResponse /// New state after processing a batch of records. /// [DataMember(Name = "state")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("state")] [JsonConverter(typeof(DictionaryLongToStringJsonConverter))] -#endif public Dictionary State { get; set; } /// @@ -30,9 +25,7 @@ public class KinesisTimeWindowResponse /// Returning the first record which failed would retry all remaining records from the batch. /// [DataMember(Name = "batchItemFailures")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("batchItemFailures")] -#endif public IList BatchItemFailures { get; set; } /// @@ -45,9 +38,7 @@ public class BatchItemFailure /// Sequence number of the record which failed processing. /// [DataMember(Name = "itemIdentifier")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("itemIdentifier")] -#endif public string ItemIdentifier { get; set; } } } diff --git a/Libraries/src/Amazon.Lambda.KinesisEvents/Properties/AssemblyInfo.cs b/Libraries/src/Amazon.Lambda.KinesisEvents/Properties/AssemblyInfo.cs index 62c7092c5..cb6f89240 100644 --- a/Libraries/src/Amazon.Lambda.KinesisEvents/Properties/AssemblyInfo.cs +++ b/Libraries/src/Amazon.Lambda.KinesisEvents/Properties/AssemblyInfo.cs @@ -8,6 +8,5 @@ [assembly: AssemblyCompany("Amazon.com, Inc")] [assembly: AssemblyCopyright("Copyright 2009-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.")] [assembly: ComVisible(false)] -[assembly: System.CLSCompliant(true)] [assembly: AssemblyVersion("1.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Libraries/src/Amazon.Lambda.KinesisEvents/StreamsEventResponse.cs b/Libraries/src/Amazon.Lambda.KinesisEvents/StreamsEventResponse.cs index a6dfc7c0c..046a1a08a 100644 --- a/Libraries/src/Amazon.Lambda.KinesisEvents/StreamsEventResponse.cs +++ b/Libraries/src/Amazon.Lambda.KinesisEvents/StreamsEventResponse.cs @@ -1,4 +1,4 @@ -namespace Amazon.Lambda.KinesisEvents +namespace Amazon.Lambda.KinesisEvents { using System.Collections.Generic; using System.Runtime.Serialization; @@ -14,9 +14,7 @@ public class StreamsEventResponse /// A list of records which failed processing. Returning the first record which failed would retry all remaining records from the batch. /// [DataMember(Name = "batchItemFailures", EmitDefaultValue = false)] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("batchItemFailures")] -#endif public IList BatchItemFailures { get; set; } /// @@ -29,10 +27,8 @@ public class BatchItemFailure /// Sequence number of the record which failed processing. /// [DataMember(Name = "itemIdentifier", EmitDefaultValue = false)] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("itemIdentifier")] -#endif public string ItemIdentifier { get; set; } } } -} \ No newline at end of file +} diff --git a/Libraries/src/Amazon.Lambda.KinesisFirehoseEvents/Amazon.Lambda.KinesisFirehoseEvents.csproj b/Libraries/src/Amazon.Lambda.KinesisFirehoseEvents/Amazon.Lambda.KinesisFirehoseEvents.csproj index 572b86b78..72c09f62a 100644 --- a/Libraries/src/Amazon.Lambda.KinesisFirehoseEvents/Amazon.Lambda.KinesisFirehoseEvents.csproj +++ b/Libraries/src/Amazon.Lambda.KinesisFirehoseEvents/Amazon.Lambda.KinesisFirehoseEvents.csproj @@ -2,16 +2,16 @@ - netstandard2.0;netcoreapp3.1;net8.0 + $(DefaultPackageTargets) Amazon Lambda .NET Core support - Amazon Kinesis Firehose package. Amazon.Lambda.KinesisFirehoseEvents - 2.3.1 + 3.0.0 Amazon.Lambda.KinesisFirehoseEvents Amazon.Lambda.KinesisFirehoseEvents AWS;Amazon;Lambda;KinesisFirehose - + IL2026,IL2067,IL2075 true true diff --git a/Libraries/src/Amazon.Lambda.KinesisFirehoseEvents/KinesisFirehoseEvent.cs b/Libraries/src/Amazon.Lambda.KinesisFirehoseEvents/KinesisFirehoseEvent.cs index f70b228cb..065822ae4 100644 --- a/Libraries/src/Amazon.Lambda.KinesisFirehoseEvents/KinesisFirehoseEvent.cs +++ b/Libraries/src/Amazon.Lambda.KinesisFirehoseEvents/KinesisFirehoseEvent.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; @@ -51,18 +51,14 @@ public class FirehoseRecord /// The approximate time the record was sent to Kinesis Firehose as a Unix epoch. /// [DataMember(Name = "approximateArrivalTimestamp")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("approximateArrivalTimestamp")] -#endif public long ApproximateArrivalEpoch { get; set; } /// /// The approximate time the record was sent to Kinesis Firehose. /// [IgnoreDataMember] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonIgnore()] -#endif public DateTime ApproximateArrivalTimestamp { get @@ -76,9 +72,7 @@ public DateTime ApproximateArrivalTimestamp /// The data sent through as a Kinesis Firehose record. The data is sent to the Lambda function base64 encoded. /// [DataMember(Name = "data")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("data")] -#endif public string Base64EncodedData { get; set; } /// @@ -87,7 +81,7 @@ public DateTime ApproximateArrivalTimestamp /// public string DecodeData() { - var decodedData = Encoding.UTF8.GetString(Convert.FromBase64String(this.Base64EncodedData)); + var decodedData = Encoding.UTF8.GetString(Convert.FromBase64String(Base64EncodedData)); return decodedData; } } diff --git a/Libraries/src/Amazon.Lambda.KinesisFirehoseEvents/KinesisFirehoseResponse.cs b/Libraries/src/Amazon.Lambda.KinesisFirehoseEvents/KinesisFirehoseResponse.cs index d9aecea80..0b99c9cb1 100644 --- a/Libraries/src/Amazon.Lambda.KinesisFirehoseEvents/KinesisFirehoseResponse.cs +++ b/Libraries/src/Amazon.Lambda.KinesisFirehoseEvents/KinesisFirehoseResponse.cs @@ -1,6 +1,5 @@ -using System; +using System; using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; using System.Text; @@ -32,9 +31,7 @@ public class KinesisFirehoseResponse /// The transformed records from the KinesisFirehoseEvent. /// [DataMember(Name = "records")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("records")] -#endif public IList Records { get; set; } /// @@ -49,9 +46,7 @@ public class FirehoseRecord ///transformed record is treated as a data transformation failure. /// [DataMember(Name = "recordId")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("recordId")] -#endif public string RecordId { get; set; } /// @@ -78,27 +73,21 @@ public class FirehoseRecord /// /// [DataMember(Name = "result")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("result")] -#endif public string Result { get; set; } /// /// The transformed data payload, after base64-encoding. /// [DataMember(Name = "data")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("data")] -#endif public string Base64EncodedData { get; set; } /// /// The response record metadata. /// [DataMember(Name = "metadata")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("metadata")] -#endif public FirehoseResponseRecordMetadata Metadata { get; set; } @@ -108,7 +97,7 @@ public class FirehoseRecord /// The data to be base64 encoded. public void EncodeData(string data) { - this.Base64EncodedData = Convert.ToBase64String(Encoding.UTF8.GetBytes(data)); + Base64EncodedData = Convert.ToBase64String(Encoding.UTF8.GetBytes(data)); } } @@ -123,9 +112,7 @@ public class FirehoseResponseRecordMetadata /// https://docs.aws.amazon.com/firehose/latest/dev/dynamic-partitioning.html /// [DataMember(Name = "partitionKeys")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("partitionKeys")] -#endif public Dictionary PartitionKeys { get; set; } } } diff --git a/Libraries/src/Amazon.Lambda.LexEvents/Amazon.Lambda.LexEvents.csproj b/Libraries/src/Amazon.Lambda.LexEvents/Amazon.Lambda.LexEvents.csproj index 06c138cea..d60f1a135 100644 --- a/Libraries/src/Amazon.Lambda.LexEvents/Amazon.Lambda.LexEvents.csproj +++ b/Libraries/src/Amazon.Lambda.LexEvents/Amazon.Lambda.LexEvents.csproj @@ -4,15 +4,15 @@ Amazon Lambda .NET Core support - Amazon Lex package. - netstandard2.0;netcoreapp3.1;net8.0 + $(DefaultPackageTargets) Amazon.Lambda.LexEvents - 3.1.1 + 4.0.0 Amazon.Lambda.LexEvents Amazon.Lambda.LexEvents AWS;Amazon;Lambda;Lex - + IL2026,IL2067,IL2075 true true diff --git a/Libraries/src/Amazon.Lambda.LexEvents/LexActiveContext.cs b/Libraries/src/Amazon.Lambda.LexEvents/LexActiveContext.cs index 25d1f6248..4b2c175a0 100644 --- a/Libraries/src/Amazon.Lambda.LexEvents/LexActiveContext.cs +++ b/Libraries/src/Amazon.Lambda.LexEvents/LexActiveContext.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Runtime.Serialization; namespace Amazon.Lambda.LexEvents @@ -13,27 +13,21 @@ public class LexActiveContext /// The length of time or number of turns in the conversation with the user that the context remains active. /// [DataMember(Name = "timeToLive", EmitDefaultValue = false)] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("timeToLive")] -#endif public TimeToLive TimeToLive { get; set; } /// /// The name of the context. /// [DataMember(Name = "name", EmitDefaultValue = false)] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("name")] -#endif public string Name { get; set; } /// /// A list of key/value pairs the contains the name and value of the slots from the intent that activated the context. /// [DataMember(Name = "parameters", EmitDefaultValue = false)] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("parameters")] -#endif public IDictionary Parameters { get; set; } } @@ -47,18 +41,14 @@ public class TimeToLive /// The length of time that the context remains active. /// [DataMember(Name = "timeToLiveInSeconds", EmitDefaultValue = false)] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("timeToLiveInSeconds")] -#endif public int TimeToLiveInSeconds { get; set; } /// /// The number of turns in the conversation with the user that the context remains active. /// [DataMember(Name = "turnsToLive", EmitDefaultValue = false)] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("turnsToLive")] -#endif public int TurnsToLive { get; set; } } -} \ No newline at end of file +} diff --git a/Libraries/src/Amazon.Lambda.LexEvents/LexRecentIntentSummaryViewType.cs b/Libraries/src/Amazon.Lambda.LexEvents/LexRecentIntentSummaryViewType.cs index 3bf075087..aa3b6870b 100644 --- a/Libraries/src/Amazon.Lambda.LexEvents/LexRecentIntentSummaryViewType.cs +++ b/Libraries/src/Amazon.Lambda.LexEvents/LexRecentIntentSummaryViewType.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Runtime.Serialization; namespace Amazon.Lambda.LexEvents @@ -13,63 +13,49 @@ public class LexRecentIntentSummaryViewType /// Gets and sets the IntentName /// [DataMember(Name = "intentName", EmitDefaultValue = false)] -#if NETCOREAPP3_1_OR_GREATER - [System.Text.Json.Serialization.JsonPropertyName("intentName")] -#endif + [System.Text.Json.Serialization.JsonPropertyName("intentName")] public string IntentName { get; set; } /// /// Gets and sets the CheckpointLabel /// [DataMember(Name = "checkpointLabel", EmitDefaultValue = false)] -#if NETCOREAPP3_1_OR_GREATER - [System.Text.Json.Serialization.JsonPropertyName("checkpointLabel")] -#endif + [System.Text.Json.Serialization.JsonPropertyName("checkpointLabel")] public string CheckpointLabel { get; set; } /// /// Gets and sets the Slots /// [DataMember(Name = "slots", EmitDefaultValue = false)] -#if NETCOREAPP3_1_OR_GREATER - [System.Text.Json.Serialization.JsonPropertyName("slots")] -#endif + [System.Text.Json.Serialization.JsonPropertyName("slots")] public IDictionary Slots { get; set; } /// /// Gets and sets the ConfirmationStatus /// [DataMember(Name = "confirmationStatus", EmitDefaultValue = false)] -#if NETCOREAPP3_1_OR_GREATER - [System.Text.Json.Serialization.JsonPropertyName("confirmationStatus")] -#endif + [System.Text.Json.Serialization.JsonPropertyName("confirmationStatus")] public string ConfirmationStatus { get; set; } /// /// Gets and sets the DialogActionType /// [DataMember(Name = "dialogActionType", EmitDefaultValue = false)] -#if NETCOREAPP3_1_OR_GREATER - [System.Text.Json.Serialization.JsonPropertyName("dialogActionType")] -#endif + [System.Text.Json.Serialization.JsonPropertyName("dialogActionType")] public string DialogActionType { get; set; } /// /// Gets and sets the FulfillmentState /// [DataMember(Name = "fulfillmentState", EmitDefaultValue = false)] -#if NETCOREAPP3_1_OR_GREATER - [System.Text.Json.Serialization.JsonPropertyName("fulfillmentState")] -#endif + [System.Text.Json.Serialization.JsonPropertyName("fulfillmentState")] public string FulfillmentState { get; set; } /// /// Gets and sets the SlotToElicit /// [DataMember(Name = "slotToElicit", EmitDefaultValue = false)] -#if NETCOREAPP3_1_OR_GREATER - [System.Text.Json.Serialization.JsonPropertyName("slotToElicit")] -#endif + [System.Text.Json.Serialization.JsonPropertyName("slotToElicit")] public string SlotToElicit { get; set; } } } diff --git a/Libraries/src/Amazon.Lambda.LexEvents/LexResponse.cs b/Libraries/src/Amazon.Lambda.LexEvents/LexResponse.cs index 1d049c323..c1023adeb 100644 --- a/Libraries/src/Amazon.Lambda.LexEvents/LexResponse.cs +++ b/Libraries/src/Amazon.Lambda.LexEvents/LexResponse.cs @@ -1,4 +1,4 @@ -namespace Amazon.Lambda.LexEvents +namespace Amazon.Lambda.LexEvents { using System; using System.Collections.Generic; @@ -15,9 +15,7 @@ public class LexResponse /// Application-specific session attributes. This is an optional field. /// [DataMember(Name = "sessionAttributes", EmitDefaultValue=false)] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("sessionAttributes")] -#endif public IDictionary SessionAttributes { get; set; } /// @@ -26,9 +24,7 @@ public class LexResponse /// after Amazon Lex returns a response to the client. /// \ [DataMember(Name = "dialogAction", EmitDefaultValue=false)] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("dialogAction")] -#endif public LexDialogAction DialogAction { get; set; } /// @@ -36,18 +32,14 @@ public class LexResponse /// For example, you can include a context to make one or more intents that have that context as an input eligible for recognition in the next turn of the conversation. /// [DataMember(Name = "activeContexts", EmitDefaultValue=false)] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("activeContexts")] -#endif public IList ActiveContexts { get; set; } /// /// If included, sets values for one or more recent intents. You can include information for up to three intents. /// [DataMember(Name = "recentIntentSummaryView", EmitDefaultValue = false)] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("recentIntentSummaryView")] -#endif public IList RecentIntentSummaryView { get; set; } /// @@ -60,63 +52,49 @@ public class LexDialogAction /// The type of action for Lex to take with the response from the Lambda function. /// [DataMember(Name = "type", EmitDefaultValue=false)] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("type")] -#endif public string Type { get; set; } /// /// The state of the fullfillment. "Fulfilled" or "Failed" /// [DataMember(Name = "fulfillmentState", EmitDefaultValue=false)] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("fulfillmentState")] -#endif public string FulfillmentState { get; set; } /// /// The message to be sent to the user. /// [DataMember(Name = "message", EmitDefaultValue=false)] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("message")] -#endif public LexMessage Message { get; set; } /// /// The intent name you want to confirm or elicit. /// [DataMember(Name = "intentName", EmitDefaultValue=false)] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("intentName")] -#endif public string IntentName { get; set; } /// /// The values for all of the slots when response is of type "Delegate". /// [DataMember(Name = "slots", EmitDefaultValue=false)] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("slots")] -#endif public IDictionary Slots { get; set; } /// /// The slot to elicit when the Type is "ElicitSlot" /// [DataMember(Name = "slotToElicit", EmitDefaultValue=false)] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("slotToElicit")] -#endif public string SlotToElicit { get; set; } /// /// The response card provides information back to the bot to display for the user. /// [DataMember(Name = "responseCard", EmitDefaultValue=false)] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("responseCard")] -#endif public LexResponseCard ResponseCard { get; set; } } @@ -130,18 +108,14 @@ public class LexMessage /// The content type of the message. PlainText or SSML /// [DataMember(Name = "contentType", EmitDefaultValue=false)] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("contentType")] -#endif public string ContentType { get; set; } /// /// The message to be asked to the user by the bot. /// [DataMember(Name = "content", EmitDefaultValue=false)] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("content")] -#endif public string Content { get; set; } } @@ -155,27 +129,21 @@ public class LexResponseCard /// The version of the response card. /// [DataMember(Name = "version", EmitDefaultValue=false)] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("version")] -#endif public int? Version { get; set; } /// /// The content type of the response card. The default is "application/vnd.amazonaws.card.generic". /// [DataMember(Name = "contentType", EmitDefaultValue=false)] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("contentType")] -#endif public string ContentType { get; set; } = "application/vnd.amazonaws.card.generic"; /// /// The list of attachments sent back with the response card. /// [DataMember(Name = "genericAttachments", EmitDefaultValue=false)] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("genericAttachments")] -#endif public IList GenericAttachments { get; set; } } @@ -189,45 +157,35 @@ public class LexGenericAttachments /// The card's title. /// [DataMember(Name = "title", EmitDefaultValue=false)] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("title")] -#endif public string Title { get; set; } /// /// The card's sub title. /// [DataMember(Name = "subTitle", EmitDefaultValue=false)] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("subTitle")] -#endif public string SubTitle { get; set; } /// /// URL to an image to be shown. /// [DataMember(Name = "imageUrl", EmitDefaultValue=false)] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("imageUrl")] -#endif public string ImageUrl { get; set; } /// /// URL of the attachment to be associated with the card. /// [DataMember(Name = "attachmentLinkUrl", EmitDefaultValue=false)] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("attachmentLinkUrl")] -#endif public string AttachmentLinkUrl { get; set; } /// /// The list of buttons to be displayed with the response card. /// [DataMember(Name = "buttons", EmitDefaultValue=false)] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("buttons")] -#endif public IList Buttons { get; set; } } @@ -241,18 +199,14 @@ public class LexButton /// The text for the button. /// [DataMember(Name = "text", EmitDefaultValue=false)] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("text")] -#endif public string Text { get; set; } /// /// The value of the button sent back to the server. /// [DataMember(Name = "value", EmitDefaultValue=false)] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("value")] -#endif public string Value { get; set; } } } diff --git a/Libraries/src/Amazon.Lambda.LexV2Events/Amazon.Lambda.LexV2Events.csproj b/Libraries/src/Amazon.Lambda.LexV2Events/Amazon.Lambda.LexV2Events.csproj index e7b0035a8..b7b07c685 100644 --- a/Libraries/src/Amazon.Lambda.LexV2Events/Amazon.Lambda.LexV2Events.csproj +++ b/Libraries/src/Amazon.Lambda.LexV2Events/Amazon.Lambda.LexV2Events.csproj @@ -4,15 +4,15 @@ Amazon Lambda .NET Core support - Amazon Lex V2 package. - netcoreapp3.1;net8.0 + $(DefaultPackageTargets) Amazon.Lambda.LexV2Events - 1.1.1 + 2.0.0 Amazon.Lambda.LexV2Events Amazon.Lambda.LexV2Events AWS;Amazon;Lambda;Lex;LexV2 - + IL2026,IL2067,IL2075 true true diff --git a/Libraries/src/Amazon.Lambda.Logging.AspNetCore/Amazon.Lambda.Logging.AspNetCore.csproj b/Libraries/src/Amazon.Lambda.Logging.AspNetCore/Amazon.Lambda.Logging.AspNetCore.csproj index 673a9ca30..ba743af47 100644 --- a/Libraries/src/Amazon.Lambda.Logging.AspNetCore/Amazon.Lambda.Logging.AspNetCore.csproj +++ b/Libraries/src/Amazon.Lambda.Logging.AspNetCore/Amazon.Lambda.Logging.AspNetCore.csproj @@ -4,9 +4,9 @@ Amazon Lambda .NET Core support - Logging ASP.NET Core package. - net6.0;net8.0 + $(DefaultPackageTargets) Amazon.Lambda.Logging.AspNetCore - 4.1.1 + 5.0.0 Amazon.Lambda.Logging.AspNetCore Amazon.Lambda.Logging.AspNetCore AWS;Amazon;Lambda;Logging diff --git a/Libraries/src/Amazon.Lambda.MQEvents/Amazon.Lambda.MQEvents.csproj b/Libraries/src/Amazon.Lambda.MQEvents/Amazon.Lambda.MQEvents.csproj index fb0a42580..2e63eaf40 100644 --- a/Libraries/src/Amazon.Lambda.MQEvents/Amazon.Lambda.MQEvents.csproj +++ b/Libraries/src/Amazon.Lambda.MQEvents/Amazon.Lambda.MQEvents.csproj @@ -3,17 +3,17 @@ - netstandard2.0;netcoreapp3.1;net8.0 + $(DefaultPackageTargets) Amazon Lambda .NET Core support - MQEvents package. Amazon.Lambda.MQEvents - 2.1.1 + 3.0.0 Amazon.Lambda.MQEvents Amazon.Lambda.MQEvents AWS;Amazon;Lambda;Amazon MQ;Rabbit MQ;Apache Active MQ Amazon.Lambda.MQEvents - + IL2026,IL2067,IL2075 true true diff --git a/Libraries/src/Amazon.Lambda.PowerShellHost/Amazon.Lambda.PowerShellHost.csproj b/Libraries/src/Amazon.Lambda.PowerShellHost/Amazon.Lambda.PowerShellHost.csproj index 0e025dc33..3af3022e1 100644 --- a/Libraries/src/Amazon.Lambda.PowerShellHost/Amazon.Lambda.PowerShellHost.csproj +++ b/Libraries/src/Amazon.Lambda.PowerShellHost/Amazon.Lambda.PowerShellHost.csproj @@ -3,20 +3,22 @@ - net6.0;net8.0 + $(DefaultPackageTargets) AWS Lambda PowerShell Host. Amazon.Lambda.PowerShellHost - 3.0.3 + 4.0.0 Amazon.Lambda.PowerShellHost Amazon.Lambda.PowerShellHost AWS;Amazon;Lambda;PowerShell - - - - + + + + + + diff --git a/Libraries/src/Amazon.Lambda.PowerShellHost/PowerShellFunctionHost.cs b/Libraries/src/Amazon.Lambda.PowerShellHost/PowerShellFunctionHost.cs index ae358459c..52012c1ca 100644 --- a/Libraries/src/Amazon.Lambda.PowerShellHost/PowerShellFunctionHost.cs +++ b/Libraries/src/Amazon.Lambda.PowerShellHost/PowerShellFunctionHost.cs @@ -1,4 +1,4 @@ -using Amazon.Lambda.Core; +using Amazon.Lambda.Core; using System; using System.IO; using System.Linq; @@ -61,27 +61,27 @@ protected PowerShellFunctionHost() { var state = InitialSessionState.CreateDefault(); state.ExecutionPolicy = Microsoft.PowerShell.ExecutionPolicy.Unrestricted; - this._ps = PowerShell.Create(state); + _ps = PowerShell.Create(state); } else { - this._ps = PowerShell.Create(); + _ps = PowerShell.Create(); } - this.SetupStreamHandlers(); - this.LoadModules(); + SetupStreamHandlers(); + LoadModules(); // Can be true if there was an exception importing modules packaged with the function. - if(this._lastException != null) + if(_lastException != null) { - Console.WriteLine(this._constructorLoggingBuffer.ToString()); - throw this._lastException; + Console.WriteLine(_constructorLoggingBuffer.ToString()); + throw _lastException; } - this.PowerShellFunctionName = Environment.GetEnvironmentVariable(POWERSHELL_FUNCTION_ENV); - if(!string.IsNullOrEmpty(this.PowerShellFunctionName)) + PowerShellFunctionName = Environment.GetEnvironmentVariable(POWERSHELL_FUNCTION_ENV); + if(!string.IsNullOrEmpty(PowerShellFunctionName)) { - this._constructorLoggingBuffer.AppendLine($"Configured to call function {this.PowerShellFunctionName} from the PowerShell script."); + _constructorLoggingBuffer.AppendLine($"Configured to call function {PowerShellFunctionName} from the PowerShell script."); } } @@ -93,7 +93,7 @@ protected PowerShellFunctionHost() protected PowerShellFunctionHost(string powerShellScriptFileName) : this() { - this._powerShellScriptFileName = powerShellScriptFileName; + _powerShellScriptFileName = powerShellScriptFileName; } /// @@ -104,19 +104,19 @@ protected PowerShellFunctionHost(string powerShellScriptFileName) /// public Stream ExecuteFunction(Stream inputStream, ILambdaContext context) { - this._lastException = null; + _lastException = null; - if (this._runFirstTimeInitialization) + if (_runFirstTimeInitialization) { - this._logger = context.Logger; + _logger = context.Logger; - if (this._constructorLoggingBuffer?.Length > 0) + if (_constructorLoggingBuffer?.Length > 0) { - context.Logger.Log(this._constructorLoggingBuffer.ToString()); - this._constructorLoggingBuffer = null; + context.Logger.Log(_constructorLoggingBuffer.ToString()); + _constructorLoggingBuffer = null; } - this._runFirstTimeInitialization = false; + _runFirstTimeInitialization = false; } string inputString; @@ -125,16 +125,16 @@ public Stream ExecuteFunction(Stream inputStream, ILambdaContext context) inputString = reader.ReadToEnd(); } - var result = this.BeginInvoke(inputString, context); - this.WaitPowerShellExecution(result); + var result = BeginInvoke(inputString, context); + WaitPowerShellExecution(result); - if (this._lastException != null || this._ps.InvocationStateInfo.State == PSInvocationState.Failed) + if (_lastException != null || _ps.InvocationStateInfo.State == PSInvocationState.Failed) { - var exception = this._exceptionManager.DetermineExceptionToThrow(this._lastException ?? this._ps.InvocationStateInfo.Reason); + var exception = _exceptionManager.DetermineExceptionToThrow(_lastException ?? _ps.InvocationStateInfo.Reason); throw exception; } - return new MemoryStream(Encoding.UTF8.GetBytes(this.GetExecutionOutput())); + return new MemoryStream(Encoding.UTF8.GetBytes(GetExecutionOutput())); } /// @@ -146,14 +146,14 @@ public Stream ExecuteFunction(Stream inputStream, ILambdaContext context) private IAsyncResult BeginInvoke(string input, ILambdaContext context) { // Clear all previous PowerShell executions, variables and outputs - this._ps.Commands?.Clear(); - this._ps.Streams.Verbose?.Clear(); - this._ps.Streams.Debug?.Clear(); - this._ps.Streams.Information?.Clear(); - this._ps.Streams.Warning?.Clear(); - this._ps.Streams.Error?.Clear(); - this._ps.Runspace?.ResetRunspaceState(); - this._output.Clear(); + _ps.Commands?.Clear(); + _ps.Streams.Verbose?.Clear(); + _ps.Streams.Debug?.Clear(); + _ps.Streams.Information?.Clear(); + _ps.Streams.Warning?.Clear(); + _ps.Streams.Error?.Clear(); + _ps.Runspace?.ResetRunspaceState(); + _output.Clear(); var providedScript = LoadScript(input, context); @@ -187,18 +187,18 @@ private IAsyncResult BeginInvoke(string input, ILambdaContext context) executingScript += providedScript; - if (!string.IsNullOrEmpty(this.PowerShellFunctionName)) + if (!string.IsNullOrEmpty(PowerShellFunctionName)) { - executingScript += $"{Environment.NewLine}{this.PowerShellFunctionName} $LambdaInput $LambdaContext{Environment.NewLine}"; + executingScript += $"{Environment.NewLine}{PowerShellFunctionName} $LambdaInput $LambdaContext{Environment.NewLine}"; } - this._ps.AddScript(executingScript); - this._ps.AddParameter("LambdaInputString", input); - this._ps.AddParameter("LambdaContext", context); + _ps.AddScript(executingScript); + _ps.AddParameter("LambdaInputString", input); + _ps.AddParameter("LambdaContext", context); - return this._ps.BeginInvoke(null, this._output); + return _ps.BeginInvoke(null, _output); } /// @@ -210,23 +210,23 @@ private IAsyncResult BeginInvoke(string input, ILambdaContext context) protected virtual string LoadScript(string input, ILambdaContext context) { // Check to see if the file contents have already been read. - if(this._powerShellScriptFileContent != null) + if(_powerShellScriptFileContent != null) { - return this._powerShellScriptFileContent; + return _powerShellScriptFileContent; } - if(string.IsNullOrEmpty(this._powerShellScriptFileName)) + if(string.IsNullOrEmpty(_powerShellScriptFileName)) { throw new LambdaPowerShellException("No PowerShell script specified to be executed. Either specify a script in the constructor or override the LoadScript method."); } - if(!File.Exists(this._powerShellScriptFileName)) + if(!File.Exists(_powerShellScriptFileName)) { - throw new LambdaPowerShellException($"Failed to find PowerShell script {this._powerShellScriptFileName}. Make sure the script is included with the package bundle."); + throw new LambdaPowerShellException($"Failed to find PowerShell script {_powerShellScriptFileName}. Make sure the script is included with the package bundle."); } - this._powerShellScriptFileContent = File.ReadAllText(this._powerShellScriptFileName); + _powerShellScriptFileContent = File.ReadAllText(_powerShellScriptFileName); - return this._powerShellScriptFileContent; + return _powerShellScriptFileContent; } /// @@ -245,7 +245,7 @@ private void WaitPowerShellExecution(IAsyncResult result) /// private string GetExecutionOutput() { - var responseObject = this._output?.LastOrDefault(); + var responseObject = _output?.LastOrDefault(); if (responseObject == null) { return string.Empty; @@ -255,8 +255,8 @@ private string GetExecutionOutput() return baseObj; } - this._ps.Commands?.Clear(); - this._ps.Runspace?.ResetRunspaceState(); + _ps.Commands?.Clear(); + _ps.Runspace?.ResetRunspaceState(); string executingScript = @" Param( @@ -266,16 +266,16 @@ private string GetExecutionOutput() ConvertTo-Json $Response "; - this._ps.AddScript(executingScript); - this._ps.AddParameter("Response", responseObject); - var marshalled = this._ps.Invoke(); + _ps.AddScript(executingScript); + _ps.AddParameter("Response", responseObject); + var marshalled = _ps.Invoke(); return marshalled.FirstOrDefault()?.BaseObject as string; } private void SetupStreamHandlers() { - this._output = new PSDataCollection(); + _output = new PSDataCollection(); Func> _loggerFactory = (prefix) => { @@ -288,17 +288,17 @@ private void SetupStreamHandlers() var errorRecord = e?.ItemAdded as ErrorRecord; if (errorRecord?.Exception != null) { - this._lastException = errorRecord.Exception; + _lastException = errorRecord.Exception; } }; return handler; }; - this._ps.Streams.Verbose.DataAdding += _loggerFactory("Verbose"); - this._ps.Streams.Debug.DataAdding += _loggerFactory("Debug"); - this._ps.Streams.Information.DataAdding += _loggerFactory("Information"); - this._ps.Streams.Warning.DataAdding += _loggerFactory("Warning"); - this._ps.Streams.Error.DataAdding += _loggerFactory("Error"); + _ps.Streams.Verbose.DataAdding += _loggerFactory("Verbose"); + _ps.Streams.Debug.DataAdding += _loggerFactory("Debug"); + _ps.Streams.Information.DataAdding += _loggerFactory("Information"); + _ps.Streams.Warning.DataAdding += _loggerFactory("Warning"); + _ps.Streams.Error.DataAdding += _loggerFactory("Error"); } private void LogMessage(string prefix, string message) @@ -313,13 +313,13 @@ private void LogMessage(string prefix, string message) message = $"[{prefix}] - {message}"; } - if (this._logger != null) + if (_logger != null) { - this._logger.LogLine(message); + _logger.LogLine(message); } else { - this._constructorLoggingBuffer.AppendLine(message); + _constructorLoggingBuffer.AppendLine(message); } } @@ -357,7 +357,7 @@ private void LoadModules() } _constructorLoggingBuffer.AppendLine($"Importing module {psd1Path}"); - var result = this._ps.AddScript($"Import-Module \"{psd1Path}\"").BeginInvoke(); + var result = _ps.AddScript($"Import-Module \"{psd1Path}\"").BeginInvoke(); WaitPowerShellExecution(result); } } diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Amazon.Lambda.RuntimeSupport.csproj b/Libraries/src/Amazon.Lambda.RuntimeSupport/Amazon.Lambda.RuntimeSupport.csproj index b3bfb0488..a52a8c43b 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Amazon.Lambda.RuntimeSupport.csproj +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Amazon.Lambda.RuntimeSupport.csproj @@ -3,8 +3,8 @@ - netstandard2.0;net6.0;net8.0;net9.0;net10.0;net11.0 - 1.14.2 + net8.0;net9.0;net10.0 + 2.1.1 Provides a bootstrap and Lambda Runtime API Client to help you to develop custom .NET Core Lambda Runtimes. Amazon.Lambda.RuntimeSupport Amazon.Lambda.RuntimeSupport @@ -35,7 +35,7 @@ $(DefineConstants);ExecutableOutputType - + true true diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/Constants.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/Constants.cs index 5ce238804..f3ecf0f55 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/Constants.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/Constants.cs @@ -33,6 +33,11 @@ internal class Constants // used if AWS_LAMBDA_MAX_CONCURRENCY environment variable is set. internal const string ENVIRONMENT_VARIABLE_AWS_LAMBDA_DOTNET_PROCESSING_TASKS = "AWS_LAMBDA_DOTNET_PROCESSING_TASKS"; + // .NET Lambda runtime specific environment variable used to override the minimum number of ThreadPool + // worker threads. When set, this value is used instead of the default (2 * processorCount). + // This allows customers to tune thread pool sizing for their specific workload. + internal const string ENVIRONMENT_VARIABLE_AWS_LAMBDA_DOTNET_MIN_THREADS = "AWS_LAMBDA_DOTNET_MIN_THREADS"; + internal const string ENVIRONMENT_VARIABLE_DISABLE_HEAP_MEMORY_LIMIT = "AWS_LAMBDA_DOTNET_DISABLE_MEMORY_LIMIT_CHECK"; internal const string ENVIRONMENT_VARIABLE_AWS_LAMBDA_DOTNET_PREJIT = "AWS_LAMBDA_DOTNET_PREJIT"; diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/HandlerWrapper.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/HandlerWrapper.cs index e3a74d04b..1981d5509 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/HandlerWrapper.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/HandlerWrapper.cs @@ -36,6 +36,14 @@ public class HandlerWrapper : IDisposable /// public LambdaBootstrapHandler Handler { get; private set; } + /// + /// The serializer registered with the wrapper, if any. Surfaced so the + /// runtime bootstrap can attach it to the per-invocation + /// , allowing user code to reuse it. + /// Null for handlers that don't take a typed input/output. + /// + internal ILambdaSerializer Serializer { get; set; } + private HandlerWrapper(LambdaBootstrapHandler handler) { Handler = handler; @@ -121,7 +129,7 @@ public static HandlerWrapper GetHandlerWrapper(Func handle TInput input = serializer.Deserialize(invocation.InputStream); await handler(input); return EmptyInvocationResponse; - }); + }) { Serializer = serializer }; } /// @@ -171,7 +179,7 @@ public static HandlerWrapper GetHandlerWrapper(Func(invocation.InputStream); await handler(input, invocation.LambdaContext); return EmptyInvocationResponse; - }); + }) { Serializer = serializer }; } /// @@ -218,7 +226,7 @@ public static HandlerWrapper GetHandlerWrapper(Func { TInput input = serializer.Deserialize(invocation.InputStream); return new InvocationResponse(await handler(input)); - }); + }) { Serializer = serializer }; } /// @@ -265,7 +273,7 @@ public static HandlerWrapper GetHandlerWrapper(Func(invocation.InputStream); return new InvocationResponse(await handler(input, invocation.LambdaContext)); - }); + }) { Serializer = serializer }; } /// @@ -278,7 +286,7 @@ public static HandlerWrapper GetHandlerWrapper(FuncA HandlerWrapper public static HandlerWrapper GetHandlerWrapper(Func> handler, ILambdaSerializer serializer) { - var handlerWrapper = new HandlerWrapper(); + var handlerWrapper = new HandlerWrapper { Serializer = serializer }; handlerWrapper.Handler = async (invocation) => { TOutput output = await handler(); @@ -300,7 +308,7 @@ public static HandlerWrapper GetHandlerWrapper(Func> hand /// A HandlerWrapper public static HandlerWrapper GetHandlerWrapper(Func> handler, ILambdaSerializer serializer) { - var handlerWrapper = new HandlerWrapper(); + var handlerWrapper = new HandlerWrapper { Serializer = serializer }; handlerWrapper.Handler = async (invocation) => { TOutput output = await handler(invocation.InputStream); @@ -322,7 +330,7 @@ public static HandlerWrapper GetHandlerWrapper(FuncA HandlerWrapper public static HandlerWrapper GetHandlerWrapper(Func> handler, ILambdaSerializer serializer) { - var handlerWrapper = new HandlerWrapper(); + var handlerWrapper = new HandlerWrapper { Serializer = serializer }; handlerWrapper.Handler = async (invocation) => { TInput input = serializer.Deserialize(invocation.InputStream); @@ -345,7 +353,7 @@ public static HandlerWrapper GetHandlerWrapper(FuncA HandlerWrapper public static HandlerWrapper GetHandlerWrapper(Func> handler, ILambdaSerializer serializer) { - var handlerWrapper = new HandlerWrapper(); + var handlerWrapper = new HandlerWrapper { Serializer = serializer }; handlerWrapper.Handler = async (invocation) => { TOutput output = await handler(invocation.LambdaContext); @@ -367,7 +375,7 @@ public static HandlerWrapper GetHandlerWrapper(FuncA HandlerWrapper public static HandlerWrapper GetHandlerWrapper(Func> handler, ILambdaSerializer serializer) { - var handlerWrapper = new HandlerWrapper(); + var handlerWrapper = new HandlerWrapper { Serializer = serializer }; handlerWrapper.Handler = async (invocation) => { TOutput output = await handler(invocation.InputStream, invocation.LambdaContext); @@ -389,7 +397,7 @@ public static HandlerWrapper GetHandlerWrapper(FuncA HandlerWrapper public static HandlerWrapper GetHandlerWrapper(Func> handler, ILambdaSerializer serializer) { - var handlerWrapper = new HandlerWrapper(); + var handlerWrapper = new HandlerWrapper { Serializer = serializer }; handlerWrapper.Handler = async (invocation) => { TInput input = serializer.Deserialize(invocation.InputStream); @@ -449,7 +457,7 @@ public static HandlerWrapper GetHandlerWrapper(Action handler, I TInput input = serializer.Deserialize(invocation.InputStream); handler(input); return Task.FromResult(EmptyInvocationResponse); - }); + }) { Serializer = serializer }; } /// @@ -499,7 +507,7 @@ public static HandlerWrapper GetHandlerWrapper(Action(invocation.InputStream); handler(input, invocation.LambdaContext); return Task.FromResult(EmptyInvocationResponse); - }); + }) { Serializer = serializer }; } /// @@ -546,7 +554,7 @@ public static HandlerWrapper GetHandlerWrapper(Func hand { TInput input = serializer.Deserialize(invocation.InputStream); return Task.FromResult(new InvocationResponse(handler(input))); - }); + }) { Serializer = serializer }; } /// @@ -593,7 +601,7 @@ public static HandlerWrapper GetHandlerWrapper(Func(invocation.InputStream); return Task.FromResult(new InvocationResponse(handler(input, invocation.LambdaContext))); - }); + }) { Serializer = serializer }; } /// @@ -606,7 +614,7 @@ public static HandlerWrapper GetHandlerWrapper(FuncA HandlerWrapper public static HandlerWrapper GetHandlerWrapper(Func handler, ILambdaSerializer serializer) { - var handlerWrapper = new HandlerWrapper(); + var handlerWrapper = new HandlerWrapper { Serializer = serializer }; handlerWrapper.Handler = (invocation) => { TOutput output = handler(); @@ -628,7 +636,7 @@ public static HandlerWrapper GetHandlerWrapper(Func handler, I /// A HandlerWrapper public static HandlerWrapper GetHandlerWrapper(Func handler, ILambdaSerializer serializer) { - var handlerWrapper = new HandlerWrapper(); + var handlerWrapper = new HandlerWrapper { Serializer = serializer }; handlerWrapper.Handler = (invocation) => { TOutput output = handler(invocation.InputStream); @@ -650,7 +658,7 @@ public static HandlerWrapper GetHandlerWrapper(Func ha /// A HandlerWrapper public static HandlerWrapper GetHandlerWrapper(Func handler, ILambdaSerializer serializer) { - var handlerWrapper = new HandlerWrapper(); + var handlerWrapper = new HandlerWrapper { Serializer = serializer }; handlerWrapper.Handler = (invocation) => { TInput input = serializer.Deserialize(invocation.InputStream); @@ -673,7 +681,7 @@ public static HandlerWrapper GetHandlerWrapper(FuncA HandlerWrapper public static HandlerWrapper GetHandlerWrapper(Func handler, ILambdaSerializer serializer) { - var handlerWrapper = new HandlerWrapper(); + var handlerWrapper = new HandlerWrapper { Serializer = serializer }; handlerWrapper.Handler = (invocation) => { TOutput output = handler(invocation.LambdaContext); @@ -695,7 +703,7 @@ public static HandlerWrapper GetHandlerWrapper(FuncA HandlerWrapper public static HandlerWrapper GetHandlerWrapper(Func handler, ILambdaSerializer serializer) { - var handlerWrapper = new HandlerWrapper(); + var handlerWrapper = new HandlerWrapper { Serializer = serializer }; handlerWrapper.Handler = (invocation) => { TOutput output = handler(invocation.InputStream, invocation.LambdaContext); @@ -717,7 +725,7 @@ public static HandlerWrapper GetHandlerWrapper(FuncA HandlerWrapper public static HandlerWrapper GetHandlerWrapper(Func handler, ILambdaSerializer serializer) { - var handlerWrapper = new HandlerWrapper(); + var handlerWrapper = new HandlerWrapper { Serializer = serializer }; handlerWrapper.Handler = (invocation) => { TInput input = serializer.Deserialize(invocation.InputStream); diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/InvokeDelegateBuilder.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/InvokeDelegateBuilder.cs index 316f2ee78..022a8d3ac 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/InvokeDelegateBuilder.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/InvokeDelegateBuilder.cs @@ -29,9 +29,7 @@ namespace Amazon.Lambda.RuntimeSupport.Bootstrap /// /// Builds user delegate from the handler information. /// -#if NET8_0_OR_GREATER [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("InvokeDelegateBuilder does not support trimming and is meant to be used in class library based Lambda functions.")] -#endif internal class InvokeDelegateBuilder { private readonly InternalLogger _logger; @@ -65,9 +63,7 @@ public InvokeDelegateBuilder(InternalLogger logger, HandlerInfo handler, MethodI /// Instance of lambda input & output serializer. /// If true forces more .NET code to get loaded during startup for jitting. /// Action delegate pointing to customer's handler. -#if NET8_0_OR_GREATER [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("ConstructInvokeDelegate does not support trimming and is meant to be used in class library based Lambda functions.")] -#endif public Action ConstructInvokeDelegate(object customerObject, object customerSerializerInstance, bool isPreJit) { var inStreamParameter = Expression.Parameter(Types.StreamType, "inStream"); @@ -120,9 +116,7 @@ public Action ConstructInvokeDelegate(object cus /// Type of context passed for the invocation. /// Expression that deserializes incoming stream to the customer method inputs or null if customer method takes no input. /// Thrown when customer method inputs don't meet lambda requirements. -#if NET8_0_OR_GREATER [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("BuildInputExpressionOrNull does not support trimming and is meant to be used in class library based Lambda functions.")] -#endif private Expression BuildInputExpressionOrNull(object customerSerializerInstance, Expression inStreamParameter, out Type iLambdaContextType) { Type inputType = null; @@ -201,9 +195,7 @@ private static Expression BuildContextExpressionOrNull(Type iLambdaContextType, /// Input expression that defines customer input. /// Context expression that defines context passed for the invocation. /// Expression that unwraps customer object. -#if NET8_0_OR_GREATER [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("CreateHandlerCallExpression does not support trimming and is meant to be used in class library based Lambda functions.")] -#endif private Expression CreateHandlerCallExpression(object customerObject, Expression inputExpression, Expression contextExpression) { @@ -262,9 +254,7 @@ private Expression CreateHandlerCallExpression(object customerObject, Expression /// Expression that defines customer output. /// Expression that defines customer handler call. /// Expression that serializes customer method output to outgoing stream. -#if NET8_0_OR_GREATER [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("CreateOutputExpression does not support trimming and is meant to be used in class library based Lambda functions.")] -#endif private Expression CreateOutputExpression(object customerSerializerInstance, Expression outStreamParameter, Expression handlerCallExpression) { var outputType = _customerMethodInfo.ReturnType; @@ -320,9 +310,7 @@ private static Type GetTaskTSubclassOrNull(Type type) /// Expression that defines customer output. /// Expression that serializes returned object to output stream. /// Thrown when customer input is serializable & serializer instance is null. -#if NET8_0_OR_GREATER [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("CreateSerializeExpression does not support trimming and is meant to be used in class library based Lambda functions.")] -#endif private Expression CreateSerializeExpression(object customerSerializerInstance, Type dataType, Expression customerObject, Expression outStreamParameter) { // generic types, null for String and Stream converters @@ -377,9 +365,7 @@ private Expression CreateSerializeExpression(object customerSerializerInstance, /// Input expression that defines customer input. /// Expression that deserializes incoming data to customer method input. /// Thrown when customer serializer doesn't match with expected serializer definition -#if NET8_0_OR_GREATER [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("CreateDeserializeExpression does not support trimming and is meant to be used in class library based Lambda functions.")] -#endif private Expression CreateDeserializeExpression(object customerSerializerInstance, Type dataType, Expression inStream) { // generic types, null for String and Stream converters diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs index 0e00f3e7f..9b8186dda 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs @@ -20,6 +20,7 @@ using System.Threading; using System.Threading.Tasks; using Amazon.Lambda.RuntimeSupport.Bootstrap; +using Amazon.Lambda.RuntimeSupport.Client.ResponseStreaming; using Amazon.Lambda.RuntimeSupport.Helpers; namespace Amazon.Lambda.RuntimeSupport @@ -51,6 +52,10 @@ public class LambdaBootstrap : IDisposable private readonly LambdaBootstrapInitializer _initializer; private readonly LambdaBootstrapHandler _handler; + // Mutable so RuntimeSupportInitializer (class-library mode) can set this after + // UserCodeLoader.Init resolves [assembly: LambdaSerializer]. Read on every + // invocation to populate ILambdaContext.Serializer. + private Amazon.Lambda.Core.ILambdaSerializer _serializer; private readonly bool _ownsHttpClient; private readonly InternalLogger _logger = InternalLogger.GetDefaultLogger(); @@ -61,6 +66,18 @@ public class LambdaBootstrap : IDisposable internal IRuntimeApiClient Client { get; set; } + /// + /// Set the serializer to surface on + /// for each invocation. Used by to plumb the + /// serializer constructed from [assembly: LambdaSerializer] after + /// has initialized. Setter is internal — public + /// callers register the serializer via instead. + /// + internal void SetSerializer(Amazon.Lambda.Core.ILambdaSerializer serializer) + { + _serializer = serializer; + } + /// /// Create a LambdaBootstrap that will call the given initializer and handler. @@ -100,7 +117,7 @@ public LambdaBootstrap(LambdaBootstrapHandler handler, LambdaBootstrapOptions la /// Delegate called to initialize the Lambda function. If not provided the initialization step is skipped. /// public LambdaBootstrap(HandlerWrapper handlerWrapper, LambdaBootstrapInitializer initializer = null) - : this(handlerWrapper.Handler, initializer) + : this(ConstructHttpClient(), handlerWrapper.Handler, initializer, ownsHttpClient: true, serializer: handlerWrapper.Serializer) { } /// @@ -110,7 +127,7 @@ public LambdaBootstrap(HandlerWrapper handlerWrapper, LambdaBootstrapInitializer /// Lambda bootstrap configuration options. /// Delegate called to initialize the Lambda function. If not provided the initialization step is skipped. public LambdaBootstrap(HandlerWrapper handlerWrapper, LambdaBootstrapOptions lambdaBootstrapOptions, LambdaBootstrapInitializer initializer = null) - : this(handlerWrapper.Handler, lambdaBootstrapOptions, initializer) + : this(ConstructHttpClient(), handlerWrapper.Handler, initializer, ownsHttpClient: true, lambdaBootstrapOptions: lambdaBootstrapOptions, serializer: handlerWrapper.Serializer) { } /// @@ -121,7 +138,7 @@ public LambdaBootstrap(HandlerWrapper handlerWrapper, LambdaBootstrapOptions lam /// Delegate called to initialize the Lambda function. If not provided the initialization step is skipped. /// public LambdaBootstrap(HttpClient httpClient, HandlerWrapper handlerWrapper, LambdaBootstrapInitializer initializer = null) - : this(httpClient, handlerWrapper.Handler, initializer, ownsHttpClient: false) + : this(httpClient, handlerWrapper.Handler, initializer, ownsHttpClient: false, serializer: handlerWrapper.Serializer) { } /// @@ -132,7 +149,7 @@ public LambdaBootstrap(HttpClient httpClient, HandlerWrapper handlerWrapper, Lam /// Lambda bootstrap configuration options. /// Delegate called to initialize the Lambda function. If not provided the initialization step is skipped. public LambdaBootstrap(HttpClient httpClient, HandlerWrapper handlerWrapper, LambdaBootstrapOptions lambdaBootstrapOptions, LambdaBootstrapInitializer initializer = null) - : this(httpClient, handlerWrapper.Handler, initializer, ownsHttpClient: false, lambdaBootstrapOptions: lambdaBootstrapOptions) + : this(httpClient, handlerWrapper.Handler, initializer, ownsHttpClient: false, lambdaBootstrapOptions: lambdaBootstrapOptions, serializer: handlerWrapper.Serializer) { } /// @@ -169,7 +186,8 @@ internal LambdaBootstrap(LambdaBootstrapHandler handler, LambdaBootstrapInitiali /// Get configuration to check if Invoke is with Pre JIT or SnapStart enabled /// Lambda bootstrap configuration options. /// - internal LambdaBootstrap(HttpClient httpClient, LambdaBootstrapHandler handler, LambdaBootstrapInitializer initializer, bool ownsHttpClient, LambdaBootstrapConfiguration configuration = null, LambdaBootstrapOptions lambdaBootstrapOptions = null, IEnvironmentVariables environmentVariables = null) + /// The Lambda serializer to expose on the per-invocation . May be null. + internal LambdaBootstrap(HttpClient httpClient, LambdaBootstrapHandler handler, LambdaBootstrapInitializer initializer, bool ownsHttpClient, LambdaBootstrapConfiguration configuration = null, LambdaBootstrapOptions lambdaBootstrapOptions = null, IEnvironmentVariables environmentVariables = null, Amazon.Lambda.Core.ILambdaSerializer serializer = null) { if (ownsHttpClient && httpClient == null) { @@ -178,6 +196,7 @@ internal LambdaBootstrap(HttpClient httpClient, LambdaBootstrapHandler handler, _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); _handler = handler ?? throw new ArgumentNullException(nameof(handler)); + _serializer = serializer; _ownsHttpClient = ownsHttpClient; _initializer = initializer; _httpClient.Timeout = RuntimeApiHttpTimeout; @@ -194,16 +213,12 @@ internal LambdaBootstrap(HttpClient httpClient, LambdaBootstrapHandler handler, /// /// /// A Task that represents the operation. -#if NET8_0_OR_GREATER [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "Unreferenced code paths are excluded when RuntimeFeature.IsDynamicCodeSupported is false.")] -#endif - public async Task RunAsync(CancellationToken cancellationToken = default(CancellationToken)) { -#if NET8_0_OR_GREATER AdjustMemorySettings(); -#endif + AdjustThreadPoolSettings(); if (_configuration.IsCallPreJit) { @@ -224,7 +239,20 @@ internal LambdaBootstrap(HttpClient httpClient, LambdaBootstrapHandler handler, { return; } -#if NET8_0_OR_GREATER + + + try + { + // Initalize in Amazon.Lambda.Core the factory for creating the response stream and related logic for supporting response streaming. + ResponseStreamLambdaCoreInitializerIsolated.InitializeCore(); + } + catch (TypeLoadException) + { + _logger.LogDebug("Failed to configure Amazon.Lambda.Core with factory to create response stream. This happens when the version of Amazon.Lambda.Core referenced by the Lambda function is out of date."); + } + + + // Check if Initialization type is SnapStart, and invoke the snapshot restore logic. if (_configuration.IsInitTypeSnapstart) { @@ -262,7 +290,6 @@ internal LambdaBootstrap(HttpClient httpClient, LambdaBootstrapHandler handler, return; }; } -#endif var processingTasksCount = Utils.DetermineProcessingTaskCount(_environmentVariables, Environment.ProcessorCount); _logger.LogInformation($"Using {processingTasksCount} tasks for invoke processing loops"); @@ -334,12 +361,12 @@ internal async Task InitializeAsync() { WriteUnhandledExceptionToLog(exception); await Client.ReportInitializationErrorAsync(exception); -#if NET8_0_OR_GREATER + if (_configuration.IsInitTypeSnapstart) { System.Environment.Exit(1); // This needs to be non-zero for Lambda Sandbox to know that Runtime client encountered an exception } -#endif + throw; } } @@ -349,6 +376,7 @@ internal async Task InvokeOnceAsync(CancellationToken cancellationToken = defaul _logger.LogInformation("Starting InvokeOnceAsync"); var invocation = await Client.GetNextInvocationAsync(cancellationToken); + var isMultiConcurrency = Utils.IsUsingMultiConcurrency(_environmentVariables); Func processingFunc = async () => { @@ -356,6 +384,18 @@ internal async Task InvokeOnceAsync(CancellationToken cancellationToken = defaul { Client.ConsoleLogger.SetRuntimeHeaders(impl.RuntimeApiHeaders); SetInvocationTraceId(impl.RuntimeApiHeaders.TraceId); + SetSerializerOnContext(impl); + } + + // Initialize ResponseStreamFactory — includes RuntimeApiClient reference + var runtimeApiClient = Client as RuntimeApiClient; + if (runtimeApiClient != null) + { + ResponseStreamFactory.InitializeInvocation( + invocation.LambdaContext.AwsRequestId, + isMultiConcurrency, + runtimeApiClient, + cancellationToken); } try @@ -372,15 +412,41 @@ internal async Task InvokeOnceAsync(CancellationToken cancellationToken = defaul catch (Exception exception) { WriteUnhandledExceptionToLog(exception); + + var responseStream = ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency); + if (responseStream != null) + { + responseStream.ReportError(exception); + } + else + { await Client.ReportInvocationErrorAsync(invocation.LambdaContext.AwsRequestId, exception, cancellationToken); } + } finally { _logger.LogInformation("Finished invoking handler"); } - if (invokeSucceeded) + var streamIfCreated = ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency); + if (streamIfCreated != null) + { + streamIfCreated.MarkCompleted(); + + // If streaming was started, await the HTTP send task to ensure it completes + var sendTask = ResponseStreamFactory.GetSendTask(isMultiConcurrency); + if (sendTask != null) + { + // Wait for the streaming response to finish sending before allowing the next invocation to be processed. This ensures that responses are sent in the order the invocations were received. + await sendTask; + sendTask.Result.Dispose(); + } + + streamIfCreated.Dispose(); + } + else if (invokeSucceeded) { + // No streaming — send buffered response _logger.LogInformation("Starting sending response"); try { @@ -415,6 +481,7 @@ internal async Task InvokeOnceAsync(CancellationToken cancellationToken = defaul } finally { + ResponseStreamFactory.CleanupInvocation(isMultiConcurrency); invocation.Dispose(); } }; @@ -434,6 +501,16 @@ internal async Task InvokeOnceAsync(CancellationToken cancellationToken = defaul } } + private void SetSerializerOnContext(LambdaContext context) + { + // No serializer was registered with this bootstrap (raw-stream handler, or + // the user constructed LambdaBootstrap with a LambdaBootstrapHandler directly). + // Nothing to surface — leave context.Serializer null. + if (_serializer == null) return; + + context.Serializer = _serializer; + } + volatile bool _disableTraceProvider = false; private void SetInvocationTraceId(string traceId) { @@ -487,7 +564,6 @@ public static HttpClient ConstructHttpClient() } var amazonLambdaRuntimeSupport = typeof(LambdaBootstrap).Assembly.GetName().Version; -#if NET6_0_OR_GREATER // Create the SocketsHttpHandler directly to avoid spending cold start time creating the wrapper HttpClientHandler var handler = new SocketsHttpHandler { @@ -500,24 +576,15 @@ public static HttpClient ConstructHttpClient() : $"aws-lambda-dotnet/{dotnetRuntimeVersion}-{amazonLambdaRuntimeSupport}"; var client = new HttpClient(handler); -#else - var userAgentString = $"aws-lambda-dotnet/{dotnetRuntimeVersion}-{amazonLambdaRuntimeSupport}"; - var client = new HttpClient(); -#endif client.DefaultRequestHeaders.Add("User-Agent", userAgentString); return client; } private void WriteUnhandledExceptionToLog(Exception exception) { -#if NET6_0_OR_GREATER Client.ConsoleLogger.FormattedWriteLine(Amazon.Lambda.RuntimeSupport.Helpers.LogLevelLoggerWriter.LogLevel.Error.ToString(), exception, null); -#else - Console.Error.WriteLine(exception); -#endif } -#if NET8_0_OR_GREATER /// /// The .NET runtime does not recognize the memory limits placed by Lambda via Lambda's cgroups. This method is run during startup to inform the /// .NET runtime the max memory configured for Lambda function. The max memory can be determined using the AWS_LAMBDA_FUNCTION_MEMORY_SIZE environment variable @@ -561,9 +628,63 @@ private void AdjustMemorySettings() _logger.LogError(ex, "Failed to communicate to the .NET runtime the amount of memory configured for the Lambda function via the AWS_LAMBDA_FUNCTION_MEMORY_SIZE environment variable."); } } -#endif #region IDisposable Support + + /// + /// When running in multi-concurrency mode, pre-size the .NET ThreadPool to ensure there are enough + /// threads available for both handler execution and polling task continuations. Without this, + /// blocking handlers (Thread.Sleep, .Result, .Wait()) can exhaust the ThreadPool, preventing + /// polling tasks from cycling back to /next and causing Runtime.Unavailable errors from RAPID. + /// + /// The default minimum is 2 * processorCount. Customers can override this via the + /// AWS_LAMBDA_DOTNET_MIN_THREADS environment variable. + /// + private void AdjustThreadPoolSettings() + { + try + { + var maxConcurrency = Utils.GetMaxConcurrency(_environmentVariables); + if (maxConcurrency <= 0) + return; + + // Check for customer override via environment variable + int desiredMinThreads; + var overrideValue = _environmentVariables.GetEnvironmentVariable(Constants.ENVIRONMENT_VARIABLE_AWS_LAMBDA_DOTNET_MIN_THREADS); + if (!string.IsNullOrEmpty(overrideValue) && int.TryParse(overrideValue, out var parsedOverride) && parsedOverride > 0) + { + desiredMinThreads = parsedOverride; + } + else + { + // Default: modest bump to ensure polling task continuations have threads + // available without pre-creating too many threads. + desiredMinThreads = 2 * Environment.ProcessorCount; + } + + ThreadPool.GetMinThreads(out int currentMinWorker, out int currentMinIO); + + // Only increase, never decrease — respect any higher value already set + // (e.g., by the customer in their code). + if (currentMinWorker >= desiredMinThreads) + return; + + var success = ThreadPool.SetMinThreads(desiredMinThreads, currentMinIO); + if (success) + { + _logger.LogInformation($"Adjusted ThreadPool minimum worker threads from {currentMinWorker} to {desiredMinThreads} for multi-concurrency mode (max concurrency: {maxConcurrency})."); + } + else + { + _logger.LogError(null, $"Failed to set ThreadPool minimum worker threads to {desiredMinThreads}."); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to adjust ThreadPool settings for multi-concurrency mode."); + } + } + private bool disposedValue = false; // To detect redundant calls /// diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrapBuilder.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrapBuilder.cs index b24d76b4c..fff7710ca 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrapBuilder.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrapBuilder.cs @@ -36,7 +36,7 @@ public class LambdaBootstrapBuilder private LambdaBootstrapBuilder(HandlerWrapper handlerWrapper) { - this._handlerWrapper = handlerWrapper; + _handlerWrapper = handlerWrapper; } /// diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/ResponseStreaming/RawStreamingHttpClient.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/ResponseStreaming/RawStreamingHttpClient.cs new file mode 100644 index 000000000..11f2f1a49 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/ResponseStreaming/RawStreamingHttpClient.cs @@ -0,0 +1,293 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System; +using System.Globalization; +using System.IO; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Amazon.Lambda.RuntimeSupport.Helpers; + +namespace Amazon.Lambda.RuntimeSupport.Client.ResponseStreaming +{ + /// + /// A raw HTTP/1.1 client for sending streaming responses to the Lambda Runtime API + /// with support for HTTP trailing headers (used for error reporting). + /// + /// .NET's HttpClient/SocketsHttpHandler does not support sending HTTP/1.1 trailing headers. + /// The Lambda Runtime API requires error information to be sent as trailing headers + /// (Lambda-Runtime-Function-Error-Type and Lambda-Runtime-Function-Error-Body) after + /// the chunked transfer encoding body. This class gives us full control over the + /// HTTP wire format to properly send those trailers. + /// + internal class RawStreamingHttpClient : IDisposable + { + private readonly string _host; + private readonly int _port; + private TcpClient _tcpClient; + internal Stream _networkStream; + private bool _disposed; + + private readonly InternalLogger _logger = InternalLogger.GetDefaultLogger(); + + public RawStreamingHttpClient(string hostAndPort) + { + var parts = hostAndPort.Split(':'); + if (parts.Length != 2) + throw new ArgumentException($"Invalid host and port format: {hostAndPort}. Expected format is 'host:port'"); + + _host = parts[0]; + _port = int.Parse(parts[1], CultureInfo.InvariantCulture); + } + + /// + /// Sends a streaming response to the Lambda Runtime API. + /// Connects via TCP, sends HTTP headers, then streams the response body + /// using chunked transfer encoding. When the response stream completes, + /// writes the chunked encoding terminator with optional trailing headers + /// for error reporting. + /// + /// The Lambda request ID. + /// The response stream that provides data and error state. + /// The User-Agent header value. + /// Cancellation token. + public async Task SendStreamingResponseAsync( + string awsRequestId, + ResponseStream responseStream, + string userAgent, + CancellationToken cancellationToken = default) + { + _tcpClient = new TcpClient(); + _tcpClient.NoDelay = true; + await _tcpClient.ConnectAsync(_host, _port, cancellationToken); + _networkStream = _tcpClient.GetStream(); + + // Send HTTP request line and headers + var path = $"/2018-06-01/runtime/invocation/{awsRequestId}/response"; + var headers = new StringBuilder(); + headers.Append($"POST {path} HTTP/1.1\r\n"); + headers.Append($"Host: {_host}:{_port}\r\n"); + headers.Append($"User-Agent: {userAgent}\r\n"); + headers.Append($"Content-Type: application/vnd.awslambda.http-integration-response\r\n"); + headers.Append($"{StreamingConstants.ResponseModeHeader}: {StreamingConstants.StreamingResponseMode}\r\n"); + headers.Append("Transfer-Encoding: chunked\r\n"); + headers.Append($"Trailer: {StreamingConstants.ErrorTypeTrailer}, {StreamingConstants.ErrorBodyTrailer}\r\n"); + headers.Append("\r\n"); + + var headerBytes = Encoding.ASCII.GetBytes(headers.ToString()); + await _networkStream.WriteAsync(headerBytes, cancellationToken); + await _networkStream.FlushAsync(cancellationToken); + + // Hand the network stream (wrapped in a chunked writer) to the ResponseStream + var chunkedWriter = new ChunkedStreamWriter(_networkStream); + await responseStream.SetHttpOutputStreamAsync(chunkedWriter, cancellationToken); + + _logger.LogInformation("In SendStreamingResponseAsync waiting for the underlying Lambda response stream to indicate it is complete."); + + // Wait for the handler to finish writing + await responseStream.WaitForCompletionAsync(cancellationToken); + + // Write the chunked encoding terminator with optional trailers + if (responseStream.HasError) + { + _logger.LogInformation("Adding response stream trailing error headers"); + await WriteTerminatorWithTrailersAsync(responseStream.ReportedError, cancellationToken); + } + else + { + // No error — write simple terminator: 0\r\n\r\n + var terminator = Encoding.ASCII.GetBytes("0\r\n\r\n"); + await _networkStream.WriteAsync(terminator, cancellationToken); + } + + await _networkStream.FlushAsync(cancellationToken); + + // Read and discard the HTTP response (we don't need it, but must consume it) + await ReadAndDiscardResponseAsync(cancellationToken); + } + + /// + /// Writes the chunked encoding terminator with HTTP trailing headers for error reporting. + /// Format: + /// 0\r\n + /// Lambda-Runtime-Function-Error-Type: errorType\r\n + /// Lambda-Runtime-Function-Error-Body: base64EncodedErrorBodyJson\r\n + /// \r\n + /// + /// The error body JSON is Base64-encoded because LambdaJsonExceptionWriter produces + /// pretty-printed multi-line JSON. HTTP trailer values cannot contain raw CR/LF characters + /// as they would break the HTTP framing — the Runtime API would see the first newline + /// inside the JSON as the end of the trailer and treat the rest as malformed data, + /// resulting in Runtime.TruncatedResponse instead of the actual error. + /// + internal async Task WriteTerminatorWithTrailersAsync(Exception exception, CancellationToken cancellationToken) + { + var exceptionInfo = ExceptionInfo.GetExceptionInfo(exception); + var errorBodyJson = LambdaJsonExceptionWriter.WriteJson(exceptionInfo); + var errorBodyBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(errorBodyJson)); + + InternalLogger.GetDefaultLogger().LogInformation($"Writing trailing header {StreamingConstants.ErrorTypeTrailer} with error type {exceptionInfo.ErrorType}."); + var trailers = new StringBuilder(); + trailers.Append("0\r\n"); // zero-length chunk (end of body) + trailers.Append($"{StreamingConstants.ErrorTypeTrailer}: {exceptionInfo.ErrorType}\r\n"); + trailers.Append($"{StreamingConstants.ErrorBodyTrailer}: {errorBodyBase64}\r\n"); + trailers.Append("\r\n"); // end of trailers + + var trailerBytes = Encoding.UTF8.GetBytes(trailers.ToString()); + await _networkStream.WriteAsync(trailerBytes, cancellationToken); + } + + /// + /// Reads and discards the HTTP response from the Runtime API. + /// We need to consume the response to properly close the connection, + /// but we don't need to process it. + /// + internal async Task ReadAndDiscardResponseAsync(CancellationToken cancellationToken) + { + const string headerDelimiter = "\r\n\r\n"; + var buffer = new byte[4096]; + try + { + // Read until we get the full response. The Runtime API sends a short response. + var totalRead = 0; + var responseText = new StringBuilder(); + while (true) + { + var bytesRead = await _networkStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken); + if (bytesRead == 0) + break; + + totalRead += bytesRead; + responseText.Append(Encoding.ASCII.GetString(buffer, 0, bytesRead)); + + // Check if we've received the complete response (ends with \r\n\r\n for headers, + // or we've read the content-length worth of body) + var text = responseText.ToString(); + if (text.Contains(headerDelimiter)) + { + // Find Content-Length to know if there's a body to read + var headerEnd = text.IndexOf(headerDelimiter, StringComparison.Ordinal); + var headers = text.Substring(0, headerEnd); + + var contentLengthMatch = System.Text.RegularExpressions.Regex.Match( + headers, @"Content-Length:\s*(\d+)", System.Text.RegularExpressions.RegexOptions.IgnoreCase); + + if (contentLengthMatch.Success) + { + var contentLength = int.Parse(contentLengthMatch.Groups[1].Value, CultureInfo.InvariantCulture); + var bodyStart = headerEnd + 4; // skip \r\n\r\n + var bodyRead = text.Length - bodyStart; + if (bodyRead >= contentLength) + break; + } + else + { + // No Content-Length, assume response is complete after headers + break; + } + } + + // 16KB is more than enough for the Runtime API response, so we can break here to avoid an infinite loop in case of malformed response + if (totalRead > 16384) + break; // Safety limit + } + } + catch (Exception ex) + { + // Log but don't throw — the streaming response was already sent + _logger.LogDebug($"Error reading Runtime API response: {ex.Message}"); + } + } + + public void Dispose() + { + if (!_disposed) + { + _networkStream?.Dispose(); + _tcpClient?.Dispose(); + _disposed = true; + } + } + } + + /// + /// A write-only Stream wrapper that writes data in HTTP/1.1 chunked transfer encoding format. + /// Each write produces a chunk: {size in hex}\r\n{data}\r\n + /// FlushAsync flushes the underlying network stream to ensure data is sent immediately. + /// The chunked encoding terminator (0\r\n...\r\n) is NOT written by this class — + /// it is handled by RawStreamingHttpClient to support trailing headers. + /// + internal class ChunkedStreamWriter : Stream + { + private readonly Stream _innerStream; + + public ChunkedStreamWriter(Stream innerStream) + { + _innerStream = innerStream ?? throw new ArgumentNullException(nameof(innerStream)); + } + + public override bool CanRead => false; + public override bool CanSeek => false; + public override bool CanWrite => true; + public override long Length => throw new NotSupportedException(); + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); + } + + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (count == 0) return; + + // Write chunk header: size in hex + \r\n + var chunkHeader = Encoding.ASCII.GetBytes($"{count:X}\r\n"); + await _innerStream.WriteAsync(chunkHeader, 0, chunkHeader.Length, cancellationToken); + + // Write chunk data + await _innerStream.WriteAsync(buffer, offset, count, cancellationToken); + + // Write chunk trailer: \r\n + var crlf = Encoding.ASCII.GetBytes("\r\n"); + await _innerStream.WriteAsync(crlf, 0, crlf.Length, cancellationToken); + } + + public override async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + if (buffer.Length == 0) return; + + var chunkHeader = Encoding.ASCII.GetBytes($"{buffer.Length:X}\r\n"); + await _innerStream.WriteAsync(chunkHeader, cancellationToken); + await _innerStream.WriteAsync(buffer, cancellationToken); + await _innerStream.WriteAsync(Encoding.ASCII.GetBytes("\r\n"), cancellationToken); + } + + public override void Flush() => _innerStream.Flush(); + + public override Task FlushAsync(CancellationToken cancellationToken) => + _innerStream.FlushAsync(cancellationToken); + + public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); + } +} diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/ResponseStreaming/ResponseStream.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/ResponseStreaming/ResponseStream.cs new file mode 100644 index 000000000..8109e9253 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/ResponseStreaming/ResponseStream.cs @@ -0,0 +1,259 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System; +using System.Buffers; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Amazon.Lambda.RuntimeSupport.Helpers; + +namespace Amazon.Lambda.RuntimeSupport.Client.ResponseStreaming +{ + /// + /// Represents the writable stream used by Lambda handlers to write response data for streaming invocations. + /// + internal class ResponseStream + { + private long _bytesWritten; + private bool _isCompleted; + private bool _hasError; + private Exception _reportedError; + private readonly object _lock = new object(); + + // The live HTTP output stream, set by RawStreamingHttpClient when sending the streaming response. + private Stream _httpOutputStream; + private int _disposedFlag; + + // The wait time is a sanity timeout to avoid waiting indefinitely if SetHttpOutputStreamAsync is not called or takes too long to call. + // Reality is that SetHttpOutputStreamAsync should be called very quickly after CreateStream, so this timeout is generous to avoid false positives but still protects against hanging indefinitely. + private readonly static TimeSpan _httpStreamWaitTimeout = TimeSpan.FromSeconds(30); + + private readonly SemaphoreSlim _httpStreamReady = new SemaphoreSlim(0, 1); + private readonly SemaphoreSlim _completionSignal = new SemaphoreSlim(0, 1); + + private static readonly byte[] PreludeDelimiter = new byte[8]; + + /// + /// The number of bytes written to the Lambda response stream so far. + /// + public long BytesWritten => _bytesWritten; + + /// + /// Gets a value indicating whether an error has occurred. + /// + public bool HasError => _hasError; + + private readonly byte[] _prelude; + + + private readonly InternalLogger _logger; + + + internal Exception ReportedError => _reportedError; + + internal ResponseStream(byte[] prelude) + { + _logger = InternalLogger.GetDefaultLogger(); + _prelude = prelude; + } + + /// + /// Called by RawStreamingHttpClient to provide the HTTP output stream (a ChunkedStreamWriter). + /// + internal async Task SetHttpOutputStreamAsync(Stream httpOutputStream, CancellationToken cancellationToken = default) + { + _httpOutputStream = httpOutputStream; + + // Write the prelude BEFORE releasing _httpStreamReady. This prevents a race + // where a handler WriteAsync that is already waiting on the semaphore could + // sneak in and write body data before the prelude, causing intermittent + // "Failed to parse prelude JSON" errors from API Gateway. + // + // Note: we intentionally do NOT check ThrowIfCompletedOrError() here. + // SetHttpOutputStreamAsync is infrastructure setup called by RawStreamingHttpClient, + // not a handler write. For fast-completing responses (e.g. Results.Json), + // LambdaBootstrap may call MarkCompleted() before the TCP connection is established + // and this method is called. The prelude still needs to be written to the wire + // so the response is properly framed. + if (_prelude?.Length > 0) + { + _logger.LogDebug("Writing prelude to HTTP stream."); + + var combinedLength = _prelude.Length + PreludeDelimiter.Length; + var combined = ArrayPool.Shared.Rent(combinedLength); + try + { + Buffer.BlockCopy(_prelude, 0, combined, 0, _prelude.Length); + Buffer.BlockCopy(PreludeDelimiter, 0, combined, _prelude.Length, PreludeDelimiter.Length); + + await _httpOutputStream.WriteAsync(combined, 0, combinedLength, cancellationToken); + await _httpOutputStream.FlushAsync(cancellationToken); + } + finally + { + ArrayPool.Shared.Return(combined); + } + } + + _httpStreamReady.Release(); + } + + /// + /// Called by RawStreamingHttpClient to wait until the handler + /// finishes writing (MarkCompleted or ReportError). + /// + internal async Task WaitForCompletionAsync(CancellationToken cancellationToken = default) + { + await _completionSignal.WaitAsync(cancellationToken); + } + + internal async Task WriteAsync(byte[] buffer, CancellationToken cancellationToken = default) + { + if (buffer == null) + throw new ArgumentNullException(nameof(buffer)); + await WriteAsync(buffer, 0, buffer.Length, cancellationToken); + } + + /// + /// Asynchronously writes a portion of a byte array to the response stream. + /// + /// The byte array containing data to write. + /// The zero-based byte offset in buffer at which to begin copying bytes. + /// The number of bytes to write. + /// Optional cancellation token. + /// A task representing the asynchronous operation. + /// Thrown if the stream is already completed or an error has been reported. + public async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken = default) + { + if (buffer == null) + throw new ArgumentNullException(nameof(buffer)); + if (offset < 0 || offset > buffer.Length) + throw new ArgumentOutOfRangeException(nameof(offset)); + if (count < 0 || offset + count > buffer.Length) + throw new ArgumentOutOfRangeException(nameof(count)); + + // Wait for the HTTP stream to be ready (first write only blocks) + await _httpStreamReady.WaitAsync(_httpStreamWaitTimeout, cancellationToken); + try + { + _logger.LogDebug("Writing chunk to HTTP response stream."); + + lock (_lock) + { + // Only throw on error, not on completed. For buffered ASP.NET Core responses + // (e.g. Results.Json), the pipeline completes and LambdaBootstrap calls + // MarkCompleted() before the pre-start buffer has been flushed to the wire. + // The buffered data still needs to be written even after MarkCompleted. + if (_hasError) + throw new InvalidOperationException("Cannot write to a stream after an error has been reported."); + _bytesWritten += count; + } + + await _httpOutputStream.WriteAsync(buffer, offset, count, cancellationToken); + await _httpOutputStream.FlushAsync(cancellationToken); + } + finally + { + // Re-release so subsequent writes don't block + _httpStreamReady.Release(); + } + } + + /// + /// Reports an error that occurred during streaming. + /// This will send error information via HTTP trailing headers. + /// + /// The exception to report. + /// Thrown if the stream is already completed or an error has already been reported. + internal void ReportError(Exception exception) + { + if (exception == null) + throw new ArgumentNullException(nameof(exception)); + + lock (_lock) + { + if (_isCompleted) + throw new InvalidOperationException("Cannot report an error after the stream has been completed."); + if (_hasError) + throw new InvalidOperationException("An error has already been reported for this stream."); + + _hasError = true; + _reportedError = exception; + _isCompleted = true; + } + // Signal completion so RawStreamingHttpClient can write error trailers and finish + _completionSignal.Release(); + } + + internal void MarkCompleted() + { + bool shouldReleaseLock = false; + lock (_lock) + { + // Release lock if not already completed, otherwise do nothing (idempotent) + if (!_isCompleted) + { + shouldReleaseLock = true; + } + _isCompleted = true; + } + + if (shouldReleaseLock) + { + // Signal completion so RawStreamingHttpClient can write the final chunk and finish + _completionSignal.Release(); + } + } + + private void ThrowIfCompletedOrError() + { + if (_isCompleted) + throw new InvalidOperationException("Cannot write to a completed stream."); + if (_hasError) + throw new InvalidOperationException("Cannot write to a stream after an error has been reported."); + } + + /// + /// Disposes the stream. After calling Dispose, no further writes or error reports should be made. + /// + /// + protected virtual void Dispose(bool disposing) + { + if (Interlocked.Exchange(ref _disposedFlag, 1) != 0) + return; + + if (disposing) + { + try { _httpStreamReady.Release(); } catch (SemaphoreFullException) { /* Ignore if already released */ } + _httpStreamReady.Dispose(); + + try { _completionSignal.Release(); } catch (SemaphoreFullException) { /* Ignore if already released */ } + _completionSignal.Dispose(); + } + } + + /// + /// Dispose of the stream. After calling Dispose, no further writes or error reports should be made. + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/ResponseStreaming/ResponseStreamContext.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/ResponseStreaming/ResponseStreamContext.cs new file mode 100644 index 000000000..970c43138 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/ResponseStreaming/ResponseStreamContext.cs @@ -0,0 +1,59 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Amazon.Lambda.RuntimeSupport.Client.ResponseStreaming +{ + /// + /// Internal context class used by ResponseStreamFactory to track per-invocation streaming state. + /// + internal class ResponseStreamContext + { + /// + /// The AWS request ID for the current invocation. + /// + public string AwsRequestId { get; set; } + + /// + /// Whether CreateStream() has been called for this invocation. + /// + public bool StreamCreated { get; set; } + + /// + /// The ResponseStream instance if created. + /// + public ResponseStream Stream { get; set; } + + /// + /// The RuntimeApiClient used to start the streaming HTTP POST. + /// + public RuntimeApiClient RuntimeApiClient { get; set; } + + /// + /// Cancellation token for the current invocation. + /// + public CancellationToken CancellationToken { get; set; } + + /// + /// The Task representing the in-flight HTTP POST to the Runtime API. + /// Started when CreateStream() is called, completes when the stream is finalized. + /// + public Task SendTask { get; set; } + } +} diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/ResponseStreaming/ResponseStreamFactory.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/ResponseStreaming/ResponseStreamFactory.cs new file mode 100644 index 000000000..0170ddb27 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/ResponseStreaming/ResponseStreamFactory.cs @@ -0,0 +1,129 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Amazon.Lambda.RuntimeSupport.Client.ResponseStreaming +{ + /// + /// Factory for creating streaming responses in AWS Lambda functions. + /// Call CreateStream() within your handler to opt into response streaming for that invocation. + /// + internal static class ResponseStreamFactory + { + // For on-demand mode (single invocation at a time) + private static ResponseStreamContext _onDemandContext; + + // For multi-concurrency mode (multiple concurrent invocations) + private static readonly AsyncLocal _asyncLocalContext = new AsyncLocal(); + + /// + /// Creates a streaming response for the current invocation. + /// Can only be called once per invocation. + /// + /// + /// + /// Thrown if called outside an invocation context. + /// Thrown if called more than once per invocation. + public static ResponseStream CreateStream(byte[] prelude) + { + var context = GetCurrentContext(); + + if (context == null) + { + throw new InvalidOperationException( + "ResponseStreamFactory.CreateStream() can only be called within a Lambda handler invocation."); + } + + if (context.StreamCreated) + { + throw new InvalidOperationException( + "ResponseStreamFactory.CreateStream() can only be called once per invocation."); + } + + var lambdaStream = new ResponseStream(prelude); + context.Stream = lambdaStream; + context.StreamCreated = true; + + // Start the HTTP POST to the Runtime API. + // This runs concurrently — SerializeToStreamAsync will block + // until the handler finishes writing or reports an error. + context.SendTask = context.RuntimeApiClient.StartStreamingResponseAsync( + context.AwsRequestId, lambdaStream, context.CancellationToken); + + return lambdaStream; + } + + // Internal methods for LambdaBootstrap to manage state + + internal static void InitializeInvocation( + string awsRequestId, bool isMultiConcurrency, + RuntimeApiClient runtimeApiClient, CancellationToken cancellationToken) + { + var context = new ResponseStreamContext + { + AwsRequestId = awsRequestId, + StreamCreated = false, + Stream = null, + RuntimeApiClient = runtimeApiClient, + CancellationToken = cancellationToken + }; + + if (isMultiConcurrency) + { + _asyncLocalContext.Value = context; + } + else + { + _onDemandContext = context; + } + } + + internal static ResponseStream GetStreamIfCreated(bool isMultiConcurrency) + { + var context = isMultiConcurrency ? _asyncLocalContext.Value : _onDemandContext; + return context?.Stream; + } + + /// + /// Returns the Task for the in-flight HTTP send, or null if streaming wasn't started. + /// LambdaBootstrap awaits this after the handler returns to ensure the HTTP request completes. + /// + internal static Task GetSendTask(bool isMultiConcurrency) + { + var context = isMultiConcurrency ? _asyncLocalContext.Value : _onDemandContext; + return context?.SendTask; + } + + internal static void CleanupInvocation(bool isMultiConcurrency) + { + if (isMultiConcurrency) + { + _asyncLocalContext.Value = null; + } + else + { + _onDemandContext = null; + } + } + + private static ResponseStreamContext GetCurrentContext() + { + // Check multi-concurrency first (AsyncLocal), then on-demand + return _asyncLocalContext.Value ?? _onDemandContext; + } + } +} diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/ResponseStreaming/ResponseStreamLambdaCoreInitializerIsolated.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/ResponseStreaming/ResponseStreamLambdaCoreInitializerIsolated.cs new file mode 100644 index 000000000..2cb46e3ce --- /dev/null +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/ResponseStreaming/ResponseStreamLambdaCoreInitializerIsolated.cs @@ -0,0 +1,59 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Threading; +using System.Threading.Tasks; +using Amazon.Lambda.Core.ResponseStreaming; +using Amazon.Lambda.RuntimeSupport.Client.ResponseStreaming; +#pragma warning disable CA2252 +namespace Amazon.Lambda.RuntimeSupport +{ + /// + /// This class is used to connect the created by to Amazon.Lambda.Core with it's public interfaces. + /// The deployed Lambda function might be referencing an older version of Amazon.Lambda.Core that does not have the public interfaces for response streaming, + /// so this class is used to avoid a direct dependency on Amazon.Lambda.Core in the rest of the response streaming implementation. + /// + /// Any code referencing this class must wrap the code around a try/catch for to allow for the case where the Lambda function + /// is deployed with an older version of Amazon.Lambda.Core that does not have the response streaming interfaces. + /// + /// + internal class ResponseStreamLambdaCoreInitializerIsolated + { + /// + /// Initalize Amazon.Lambda.Core with a factory method for creating that wraps the internal implementation. + /// + internal static void InitializeCore() + { +#if !ANALYZER_UNIT_TESTS // This precompiler directive is used to avoid the unit tests from needing a dependency on Amazon.Lambda.Core. + Func factory = (byte[] prelude) => new ImplLambdaResponseStream(ResponseStreamFactory.CreateStream(prelude)); + LambdaResponseStreamFactory.SetLambdaResponseStream(factory); +#endif + } + + /// + /// Implements the interface by wrapping a . This is used to connect the internal response streaming implementation to the public interfaces in Amazon.Lambda.Core. + /// + internal class ImplLambdaResponseStream : ILambdaResponseStream + { + private readonly ResponseStream _innerStream; + + internal ImplLambdaResponseStream(ResponseStream innerStream) + { + _innerStream = innerStream; + } + + /// + public long BytesWritten => _innerStream.BytesWritten; + + /// + public bool HasError => _innerStream.HasError; + + /// + public void Dispose() => _innerStream.Dispose(); + + /// + public Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken = default) => _innerStream.WriteAsync(buffer, offset, count, cancellationToken); + } + } +} diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/ResponseStreaming/StreamingConstants.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/ResponseStreaming/StreamingConstants.cs new file mode 100644 index 000000000..43ac607b7 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/ResponseStreaming/StreamingConstants.cs @@ -0,0 +1,43 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +namespace Amazon.Lambda.RuntimeSupport.Client.ResponseStreaming +{ + /// + /// Constants used for Lambda response streaming. + /// + internal static class StreamingConstants + { + /// + /// Header name for Lambda response mode. + /// + public const string ResponseModeHeader = "Lambda-Runtime-Function-Response-Mode"; + + /// + /// Value for streaming response mode. + /// + public const string StreamingResponseMode = "streaming"; + + /// + /// Trailer header name for error type. + /// + public const string ErrorTypeTrailer = "Lambda-Runtime-Function-Error-Type"; + + /// + /// Trailer header name for error body. + /// + public const string ErrorBodyTrailer = "Lambda-Runtime-Function-Error-Body"; + } +} diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/UserCodeInit.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/UserCodeInit.cs index b59ba67f5..ece57f72f 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/UserCodeInit.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/UserCodeInit.cs @@ -31,14 +31,12 @@ internal class UserCodeInit { public static bool IsCallPreJit(IEnvironmentVariables environmentVariables) { -#if NET6_0_OR_GREATER // If we are running in an AOT environment, there is no point in doing any prejit optmization // and will most likely cause errors using APIs that are not supported in AOT. if(Utils.IsRunningNativeAot()) { return false; } -#endif string awsLambdaDotNetPreJitStr = environmentVariables.GetEnvironmentVariable(ENVIRONMENT_VARIABLE_AWS_LAMBDA_DOTNET_PREJIT); string awsLambdaInitTypeStr = environmentVariables.GetEnvironmentVariable(ENVIRONMENT_VARIABLE_AWS_LAMBDA_INITIALIZATION_TYPE); @@ -140,9 +138,7 @@ public static void LoadStringCultureInfo(IEnvironmentVariables environmentVariab } } -#if NET8_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("PreJitAssembly is not used for Native AOT")] -#endif + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("PreJitAssembly is not used for Native AOT")] public static void PreJitAssembly(Assembly a) { // Storage to ensure not loading the same assembly twice and optimize calls to GetAssemblies() diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/UserCodeLoader.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/UserCodeLoader.cs index 340648405..5c079a7d8 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/UserCodeLoader.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/UserCodeLoader.cs @@ -28,9 +28,7 @@ namespace Amazon.Lambda.RuntimeSupport.Bootstrap /// /// Loads user code and prepares to invoke it. /// -#if NET8_0_OR_GREATER [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("UserCodeLoader does not support trimming and is meant to be used in class library based Lambda functions.")] -#endif internal class UserCodeLoader { private const string UserInvokeException = "An exception occurred while invoking customer handler."; @@ -48,6 +46,17 @@ internal class UserCodeLoader private Action _invokeDelegate; internal MethodInfo CustomerMethodInfo { get; private set; } + /// + /// The serializer instance constructed from the customer's + /// [LambdaSerializer(typeof(...))] attribute (if any). Populated by + /// . Typed as here because the value is + /// produced via reflection in and validated + /// against the loaded ILambdaSerializer interface there; + /// casts it back to ILambdaSerializer + /// before handing it to . + /// + internal object CustomerSerializerInstance { get; private set; } + /// /// Initializes UserCodeLoader with a given handler and internal logger. /// @@ -131,6 +140,7 @@ public void Init(Action customerLoggingAction) var customerObject = GetCustomerObject(customerType); var customerSerializerInstance = GetSerializerObject(customerAssembly); + CustomerSerializerInstance = customerSerializerInstance; _logger.LogDebug($"UCL : Constructing invoke delegate"); var isPreJit = UserCodeInit.IsCallPreJit(_environmentVariables); diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/UserCodeValidator.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/UserCodeValidator.cs index 5f6100770..2684e5498 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/UserCodeValidator.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/UserCodeValidator.cs @@ -21,9 +21,7 @@ namespace Amazon.Lambda.RuntimeSupport.Bootstrap { -#if NET8_0_OR_GREATER [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("UserCodeValidator does not support trimming and is meant to be used in class library based Lambda functions.")] -#endif internal static class UserCodeValidator { /// diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/IRuntimeApiClient.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/IRuntimeApiClient.cs index 8dbb34257..c43a949c9 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/IRuntimeApiClient.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/IRuntimeApiClient.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). @@ -66,7 +66,6 @@ public interface IRuntimeApiClient /// A Task representing the asynchronous operation. Task ReportInvocationErrorAsync(string awsRequestId, Exception exception, CancellationToken cancellationToken = default); -#if NET8_0_OR_GREATER /// /// Triggers the snapshot to be taken, and then after resume, restores the lambda /// context from the Runtime API as an asynchronous operation when SnapStart is enabled. @@ -83,7 +82,6 @@ public interface IRuntimeApiClient /// The optional cancellation token to use. /// A Task representing the asynchronous operation. Task ReportRestoreErrorAsync(Exception exception, String errorType = null, CancellationToken cancellationToken = default); -#endif /// /// Send a response to a function invocation to the Runtime API as an asynchronous operation. @@ -94,4 +92,4 @@ public interface IRuntimeApiClient /// Task SendResponseAsync(string awsRequestId, Stream outputStream, CancellationToken cancellationToken = default); } -} \ No newline at end of file +} diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/InternalClientAdapted.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/InternalClientAdapted.cs index 0ea7fdbbd..aeeb7ac4b 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/InternalClientAdapted.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/InternalClientAdapted.cs @@ -34,7 +34,6 @@ internal partial interface IInternalRuntimeApiClient Task> ErrorAsync(string lambda_Runtime_Function_Error_Type, string errorJson, CancellationToken cancellationToken); -#if NET8_0_OR_GREATER /// /// Triggers the snapshot to be taken, and then after resume, restores the lambda /// context from the Runtime API as an asynchronous operation when SnapStart is enabled. @@ -45,7 +44,6 @@ internal partial interface IInternalRuntimeApiClient Task> RestoreErrorAsync(string lambda_Runtime_Function_Error_Type, string errorJson, CancellationToken cancellationToken); -#endif /// Runtime makes this HTTP request when it is ready to receive and process a new invoke. /// This is an iterator-style blocking API call. Response contains event JSON document, specific to the invoking service. @@ -82,16 +80,12 @@ Task> RestoreErrorAsync(string lambda_Runtime_Fu internal partial class InternalRuntimeApiClient : IInternalRuntimeApiClient { -#if NET6_0_OR_GREATER - [JsonSerializable(typeof(StatusResponse))] [JsonSerializable(typeof(ErrorResponse))] public partial class RuntimeApiSerializationContext : JsonSerializerContext { } -#endif - private const int MAX_HEADER_SIZE_BYTES = 1024 * 1024; private const string ErrorContentType = "application/vnd.aws.lambda.error+json"; @@ -160,11 +154,7 @@ private async System.Threading.Tasks.Task> Error var result_ = default(StatusResponse); try { -#if NET6_0_OR_GREATER result_ = JsonSerializer.Deserialize(responseData_, RuntimeApiSerializationContext.Default.StatusResponse); -#else - result_ = JsonSerializer.Deserialize(responseData_); -#endif return new SwaggerResponse((int)response_.StatusCode, headers_, result_); } catch (System.Exception exception_) @@ -179,11 +169,7 @@ private async System.Threading.Tasks.Task> Error var result_ = default(ErrorResponse); try { -#if NET6_0_OR_GREATER result_ = JsonSerializer.Deserialize(responseData_, RuntimeApiSerializationContext.Default.ErrorResponse); -#else - result_ = JsonSerializer.Deserialize(responseData_); -#endif } catch (System.Exception exception_) { @@ -227,7 +213,6 @@ private async System.Threading.Tasks.Task> Error return NextAsync("/runtime/invocation/next", cancellationToken); } -#if NET8_0_OR_GREATER /// /// Restores the lambda context from the Runtime API as an asynchronous operation when SnapStart is enabled /// @@ -247,8 +232,6 @@ public async Task> RestoreErrorAsync(string lamb return await ErrorAsync(lambda_Runtime_Function_Error_Type, errorJson, "/runtime/restore/error", cancellationToken); } -#endif - /// Runtime makes this HTTP request when it is ready to receive and process a new invoke. /// This is an iterator-style blocking API call. Response contains event JSON document, specific to the invoking service. @@ -289,11 +272,7 @@ public async Task> RestoreErrorAsync(string lamb var result_ = default(ErrorResponse); try { -#if NET6_0_OR_GREATER result_ = JsonSerializer.Deserialize(responseData_, RuntimeApiSerializationContext.Default.ErrorResponse); -#else - result_ = JsonSerializer.Deserialize(responseData_); -#endif } catch (System.Exception exception_) { @@ -389,11 +368,7 @@ public async System.Threading.Tasks.Task> Respon var result_ = default(ErrorResponse); try { -#if NET6_0_OR_GREATER result_ = JsonSerializer.Deserialize(responseData_, RuntimeApiSerializationContext.Default.ErrorResponse); -#else - result_ = JsonSerializer.Deserialize(responseData_); -#endif } catch (System.Exception exception_) { @@ -408,11 +383,7 @@ public async System.Threading.Tasks.Task> Respon var result_ = default(ErrorResponse); try { -#if NET6_0_OR_GREATER result_ = JsonSerializer.Deserialize(responseData_, RuntimeApiSerializationContext.Default.ErrorResponse); -#else - result_ = JsonSerializer.Deserialize(responseData_); -#endif } catch (System.Exception exception_) { @@ -427,11 +398,7 @@ public async System.Threading.Tasks.Task> Respon var result_ = default(ErrorResponse); try { -#if NET6_0_OR_GREATER result_ = JsonSerializer.Deserialize(responseData_, RuntimeApiSerializationContext.Default.ErrorResponse); -#else - result_ = JsonSerializer.Deserialize(responseData_); -#endif } catch (System.Exception exception_) { @@ -540,11 +507,7 @@ public async System.Threading.Tasks.Task> ErrorW var result_ = default(StatusResponse); try { -#if NET6_0_OR_GREATER result_ = JsonSerializer.Deserialize(responseData_, RuntimeApiSerializationContext.Default.StatusResponse); -#else - result_ = JsonSerializer.Deserialize(responseData_); -#endif return new SwaggerResponse((int)response_.StatusCode, headers_, result_); } catch (System.Exception exception_) @@ -559,11 +522,7 @@ public async System.Threading.Tasks.Task> ErrorW var result_ = default(ErrorResponse); try { -#if NET6_0_OR_GREATER result_ = JsonSerializer.Deserialize(responseData_, RuntimeApiSerializationContext.Default.ErrorResponse); -#else - result_ = JsonSerializer.Deserialize(responseData_); -#endif } catch (System.Exception exception_) { @@ -578,11 +537,7 @@ public async System.Threading.Tasks.Task> ErrorW var result_ = default(ErrorResponse); try { -#if NET6_0_OR_GREATER result_ = JsonSerializer.Deserialize(responseData_, RuntimeApiSerializationContext.Default.ErrorResponse); -#else - result_ = JsonSerializer.Deserialize(responseData_); -#endif } catch (System.Exception exception_) { diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/RuntimeApiClient.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/RuntimeApiClient.cs index daa9fff24..39cc7d055 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/RuntimeApiClient.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/RuntimeApiClient.cs @@ -20,6 +20,7 @@ using System.Threading; using System.Threading.Tasks; using Amazon.Lambda.RuntimeSupport.Bootstrap; +using Amazon.Lambda.RuntimeSupport.Client.ResponseStreaming; namespace Amazon.Lambda.RuntimeSupport { @@ -31,11 +32,7 @@ public class RuntimeApiClient : IRuntimeApiClient private readonly HttpClient _httpClient; private readonly IInternalRuntimeApiClient _internalClient; -#if NET6_0_OR_GREATER private readonly IConsoleLoggerWriter _consoleLoggerRedirector; -#else - private readonly IConsoleLoggerWriter _consoleLoggerRedirector; -#endif internal Func ExceptionConverter { get; set; } internal LambdaEnvironment LambdaEnvironment { get; set; } @@ -54,11 +51,7 @@ public RuntimeApiClient(HttpClient httpClient) internal RuntimeApiClient(IEnvironmentVariables environmentVariables, HttpClient httpClient, LambdaBootstrapOptions lambdaBootstrapOptions = null) { -#if NET6_0_OR_GREATER _consoleLoggerRedirector = new LogLevelLoggerWriter(environmentVariables); -#else - _consoleLoggerRedirector = new SimpleLoggerWriter(environmentVariables); -#endif ExceptionConverter = ExceptionInfo.GetExceptionInfo; _httpClient = httpClient; @@ -147,8 +140,6 @@ public Task ReportInvocationErrorAsync(string awsRequestId, Exception exception, return _internalClient.ErrorWithXRayCauseAsync(awsRequestId, exceptionInfo.ErrorType, exceptionInfoJson, exceptionInfoXRayJson, cancellationToken); } -#if NET8_0_OR_GREATER - /// /// Triggers the snapshot to be taken, and then after resume, restores the lambda /// context from the Runtime API as an asynchronous operation when SnapStart is enabled. @@ -174,8 +165,32 @@ public Task ReportRestoreErrorAsync(Exception exception, String errorType = null return _internalClient.RestoreErrorAsync(errorType, LambdaJsonExceptionWriter.WriteJson(ExceptionInfo.GetExceptionInfo(exception)), cancellationToken); } -#endif + /// + /// Start sending a streaming response to the Lambda Runtime API. + /// Uses a raw TCP connection with chunked transfer encoding to support HTTP/1.1 + /// trailing headers for error reporting, which .NET's HttpClient does not support. + /// The actual data is written by the handler via ResponseStream.WriteAsync, which flows + /// through a ChunkedStreamWriter to the TCP connection. + /// This Task completes when the stream is finalized (MarkCompleted or error). + /// + /// The ID of the function request being responded to. + /// The ResponseStream that will provide the streaming data. + /// The optional cancellation token to use. + /// A Task representing the in-flight HTTP POST. The returned IDisposable is the RawStreamingHttpClient that owns the TCP connection. + internal virtual async Task StartStreamingResponseAsync( + string awsRequestId, ResponseStream responseStream, CancellationToken cancellationToken = default) + { + if (awsRequestId == null) throw new ArgumentNullException(nameof(awsRequestId)); + if (responseStream == null) throw new ArgumentNullException(nameof(responseStream)); + + var userAgent = _httpClient.DefaultRequestHeaders.UserAgent.ToString(); + var rawClient = new RawStreamingHttpClient(LambdaEnvironment.RuntimeServerHostAndPort); + + await rawClient.SendStreamingResponseAsync(awsRequestId, responseStream, userAgent, cancellationToken); + + return rawClient; + } /// /// Send a response to a function invocation to the Runtime API as an asynchronous operation. diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Context/LambdaBootstrapConfiguration.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Context/LambdaBootstrapConfiguration.cs index 96e913312..cdf5e5435 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Context/LambdaBootstrapConfiguration.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Context/LambdaBootstrapConfiguration.cs @@ -21,16 +21,12 @@ internal LambdaBootstrapConfiguration(bool isCallPreJit, bool isInitTypeSnapstar internal static LambdaBootstrapConfiguration GetDefaultConfiguration(IEnvironmentVariables environmentVariables) { bool isCallPreJit = UserCodeInit.IsCallPreJit(environmentVariables); -#if NET8_0_OR_GREATER bool isInitTypeSnapstart = string.Equals( environmentVariables.GetEnvironmentVariable(Constants.ENVIRONMENT_VARIABLE_AWS_LAMBDA_INITIALIZATION_TYPE), Constants.AWS_LAMBDA_INITIALIZATION_TYPE_SNAP_START); return new LambdaBootstrapConfiguration(isCallPreJit, isInitTypeSnapstart); -#else - return new LambdaBootstrapConfiguration(isCallPreJit, false); -#endif } } } diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Context/LambdaConsoleLogger.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Context/LambdaConsoleLogger.cs index e0faac022..133d20bab 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Context/LambdaConsoleLogger.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Context/LambdaConsoleLogger.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). @@ -40,7 +40,6 @@ public void LogLine(string message) public string CurrentAwsRequestId { get; set; } -#if NET6_0_OR_GREATER public void Log(string level, string message) { _consoleLoggerRedirector.FormattedWriteLine(level, message); @@ -55,6 +54,5 @@ public void Log(string level, Exception exception, string message, params object { _consoleLoggerRedirector.FormattedWriteLine(level, exception, message, args); } -#endif } } diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Context/LambdaContext.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Context/LambdaContext.cs index cca6f4e5d..fea4a6bd2 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Context/LambdaContext.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Context/LambdaContext.cs @@ -77,6 +77,13 @@ public LambdaContext(RuntimeApiHeaders runtimeApiHeaders, LambdaEnvironment lamb public string TenantId => _runtimeApiHeaders.TenantId; + /// + /// The serializer the Lambda function registered with the runtime, surfaced via + /// . Assigned per-invocation by + /// . + /// + public ILambdaSerializer Serializer { get; internal set; } + internal IRuntimeApiHeaders RuntimeApiHeaders => _runtimeApiHeaders; } } diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/ExceptionHandling/StackFrameInfo.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/ExceptionHandling/StackFrameInfo.cs index a5c4b49fc..0eebd721c 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/ExceptionHandling/StackFrameInfo.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/ExceptionHandling/StackFrameInfo.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). @@ -27,10 +27,8 @@ public StackFrameInfo(string path, int line, string label) Label = label; } -#if NET8_0_OR_GREATER [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "Constructor has defensive code in place in case the method for the stack frame has been trimmed.")] -#endif public StackFrameInfo(StackFrame stackFrame) { Path = stackFrame.GetFileName(); diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/ConsoleLoggerWriter.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/ConsoleLoggerWriter.cs index a2417cbcc..3dfb43730 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/ConsoleLoggerWriter.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/ConsoleLoggerWriter.cs @@ -22,9 +22,7 @@ using System.Threading.Tasks; -#if NET6_0_OR_GREATER using Amazon.Lambda.RuntimeSupport.Helpers.Logging; -#endif namespace Amazon.Lambda.RuntimeSupport.Helpers { @@ -63,71 +61,6 @@ public interface IConsoleLoggerWriter void FormattedWriteLine(string level, Exception exception, string message, params object[] args); } - /// - /// Simple logger to maintain compatibility with versions of .NET before .NET 6 - /// - public class SimpleLoggerWriter : IConsoleLoggerWriter - { - readonly TextWriter _writer; - - /// - /// Default Constructor - /// - public SimpleLoggerWriter(IEnvironmentVariables environmentVariables) - { - // Look to see if Lambda's telemetry log file descriptor is available. If so use that for logging. - // This will make sure multiline log messages use a single CloudWatch Logs record. - var fileDescriptorLogId = environmentVariables.GetEnvironmentVariable(Constants.ENVIRONMENT_VARIABLE_TELEMETRY_LOG_FD); - if (fileDescriptorLogId != null) - { - try - { - _writer = FileDescriptorLogFactory.GetWriter(environmentVariables, fileDescriptorLogId); - InternalLogger.GetDefaultLogger().LogInformation("Using file descriptor stream writer for logging"); - } - catch (Exception ex) - { - _writer = Console.Out; - InternalLogger.GetDefaultLogger().LogError(ex, "Error creating file descriptor log stream writer. Fallback to stdout."); - } - } - else - { - _writer = Console.Out; - InternalLogger.GetDefaultLogger().LogInformation("Using stdout for logging"); - } - } - - /// - public void SetRuntimeHeaders(IRuntimeApiHeaders runtimeApiHeaders) - { - } - - /// - public void FormattedWriteLine(string message) - { - _writer.WriteLine(message); - } - - /// - public void FormattedWriteLine(string level, string message, params object[] args) - { - _writer.WriteLine(message); - } - - /// - public void FormattedWriteLine(string level, Exception exception, string message, params object[] args) - { - _writer.WriteLine(message); - if (exception != null) - { - _writer.WriteLine(exception.ToString()); - } - } - } - -#if NET6_0_OR_GREATER - /// /// Formats log messages with time, request id, log level and message /// @@ -598,5 +531,4 @@ public override string NewLine #endregion } } -#endif } diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/AbstractLogMessageFormatter.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/AbstractLogMessageFormatter.cs index 75535970f..e18a57c15 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/AbstractLogMessageFormatter.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/AbstractLogMessageFormatter.cs @@ -1,4 +1,3 @@ -#if NET6_0_OR_GREATER using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -285,4 +284,3 @@ public bool UsingPositionalArguments(IReadOnlyList messagePrope } } } -#endif diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/ConfigureJsonLogMessageFormatterIsolated.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/ConfigureJsonLogMessageFormatterIsolated.cs new file mode 100644 index 000000000..b78a47d15 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/ConfigureJsonLogMessageFormatterIsolated.cs @@ -0,0 +1,35 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Text.Json; + +namespace Amazon.Lambda.RuntimeSupport.Helpers.Logging +{ + internal class ConfigureJsonLogMessageFormatterIsolated + { + internal static void ConfigureCallbackInCore(Action callback) + { + Amazon.Lambda.Core.LambdaLogger.SetConfigureStructuredLoggingAction((Amazon.Lambda.Core.StructuredLoggingOptions coreOptions) => + { + if (coreOptions == null) + { + callback(null); + return; + } + + var isolatedOptions = new StructuredLoggingOptions(); + try + { + isolatedOptions.OverrideSerializerOptions = coreOptions.OverrideSerializerOptions; + } + catch (Exception ex) + { + InternalLogger.GetDefaultLogger().LogDebug("Failed to configure structured logging. This generally happens when the version of Amazon.Lambda.Core is out of date. Update to latest version of Amazon.Lambda.Core: " + ex.ToString()); + } + + callback(isolatedOptions); + }); + } + } +} diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/DefaultLogMessageFormatter.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/DefaultLogMessageFormatter.cs index 13bdca8ad..f03c65eff 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/DefaultLogMessageFormatter.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/DefaultLogMessageFormatter.cs @@ -1,5 +1,3 @@ -#if NET6_0_OR_GREATER - using System.Text; namespace Amazon.Lambda.RuntimeSupport.Helpers.Logging @@ -120,4 +118,3 @@ private string ConvertLogLevelToLabel(LogLevelLoggerWriter.LogLevel level) } } } -#endif \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/ILogMessageFormatter.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/ILogMessageFormatter.cs index 732fe7f3d..44ffb3060 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/ILogMessageFormatter.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/ILogMessageFormatter.cs @@ -1,5 +1,3 @@ -#if NET6_0_OR_GREATER - namespace Amazon.Lambda.RuntimeSupport.Helpers.Logging { /// @@ -16,4 +14,3 @@ public interface ILogMessageFormatter string FormatMessage(MessageState state); } } -#endif \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/JsonLogMessageFormatter.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/JsonLogMessageFormatter.cs index cfe46c563..ac4943188 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/JsonLogMessageFormatter.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/JsonLogMessageFormatter.cs @@ -1,4 +1,3 @@ -#if NET6_0_OR_GREATER using System; using System.Buffers; using System.Collections; @@ -19,7 +18,7 @@ public class JsonLogMessageFormatter : AbstractLogMessageFormatter private static readonly UTF8Encoding UTF8NoBomNoThrow = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: false); // Options used when serializing any message property values as a JSON to be added to the structured log message. - private readonly JsonSerializerOptions _jsonSerializationOptions; + private JsonSerializerOptions _jsonSerializationOptions; /// /// Constructs an instance of JsonLogMessageFormatter. @@ -31,10 +30,28 @@ public JsonLogMessageFormatter() DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, WriteIndented = false }; + + try + { + ConfigureJsonLogMessageFormatterIsolated.ConfigureCallbackInCore(ConfigureStructuredLogging); + } + catch (TypeLoadException) + { + InternalLogger.GetDefaultLogger().LogDebug("Failed to configure Amazon.Lambda.Core with callback for configuring structured logging. This happens when the version of Amazon.Lambda.Core referenced by the Lambda function is out of date."); + } } private static readonly IReadOnlyList _emptyMessageProperties = new List(); + private void ConfigureStructuredLogging(StructuredLoggingOptions options) + { + if (options == null) + return; + + if (options.OverrideSerializerOptions != null) + _jsonSerializationOptions = options.OverrideSerializerOptions; + } + /// /// Format the log message as a structured JSON log message. /// @@ -314,5 +331,13 @@ private void FormatJsonValue(Utf8JsonWriter writer, object value, string formatA private static string ToInvariantString(object obj) => Convert.ToString(obj, CultureInfo.InvariantCulture); } + + /// + /// Mirror the version of StructuredLoggingOptions in Amazon.Lambda.Core because we can't guarantee that the version of Amazon.Lambda.Core used by the customer + /// will be the same as the version of Amazon.Lambda.RuntimeSupport. + /// + internal class StructuredLoggingOptions + { + public JsonSerializerOptions OverrideSerializerOptions { get; set; } + } } -#endif diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/MessageProperty.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/MessageProperty.cs index 1d463163a..9f4f77284 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/MessageProperty.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/MessageProperty.cs @@ -1,4 +1,3 @@ -#if NET6_0_OR_GREATER using System; using System.Collections; using System.Globalization; @@ -25,29 +24,29 @@ public MessageProperty(ReadOnlySpan messageToken) // messageToken format is: // : - this.MessageToken = "{" + messageToken.ToString() + "}"; + MessageToken = "{" + messageToken.ToString() + "}"; - this.FormatDirective = Directive.Default; + FormatDirective = Directive.Default; if (messageToken[0] == '@') { - this.FormatDirective = Directive.JsonSerialization; + FormatDirective = Directive.JsonSerialization; messageToken = messageToken.Slice(1); } var idxOfDelimeter = messageToken.IndexOfAny(PARAM_FORMAT_DELIMITERS); if (idxOfDelimeter < 0) { - this.Name = messageToken.ToString().Trim(); - this.FormatArgument = null; + Name = messageToken.ToString().Trim(); + FormatArgument = null; } else { - this.Name = messageToken.Slice(0, idxOfDelimeter).ToString().Trim(); - this.FormatArgument = messageToken.Slice(idxOfDelimeter + 1).ToString().Trim(); - if(this.FormatArgument == string.Empty) + Name = messageToken.Slice(0, idxOfDelimeter).ToString().Trim(); + FormatArgument = messageToken.Slice(idxOfDelimeter + 1).ToString().Trim(); + if(FormatArgument == string.Empty) { - this.FormatArgument = null; + FormatArgument = null; } } } @@ -108,11 +107,11 @@ public string FormatForMessage(object value) } if (value == null || value is IList || value is IDictionary) { - return this.MessageToken; + return MessageToken; } - if(!string.IsNullOrEmpty(this.FormatArgument)) + if(!string.IsNullOrEmpty(FormatArgument)) { - return ApplyFormatArgument(value, this.FormatArgument); + return ApplyFormatArgument(value, FormatArgument); } if(value is DateTime dt) { @@ -188,4 +187,3 @@ public static string FormatByteArray(ReadOnlySpan bytes) } } } -#endif \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/MessageState.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/MessageState.cs index 444aea46a..f59fe79d1 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/MessageState.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/MessageState.cs @@ -1,4 +1,3 @@ -#if NET6_0_OR_GREATER using System; namespace Amazon.Lambda.RuntimeSupport.Helpers.Logging @@ -51,4 +50,3 @@ public class MessageState public Exception Exception { get; set; } } } -#endif diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/SnapstartHelperCopySnapshotCallbacksIsolated.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/SnapstartHelperCopySnapshotCallbacksIsolated.cs index 740b29b0d..c3d6fcd11 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/SnapstartHelperCopySnapshotCallbacksIsolated.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/SnapstartHelperCopySnapshotCallbacksIsolated.cs @@ -1,10 +1,9 @@ -using System; +using System; using System.Collections.Generic; using System.Text; namespace Amazon.Lambda.RuntimeSupport.Helpers { -#if NET8_0_OR_GREATER internal static class SnapstartHelperCopySnapshotCallbacksIsolated { internal static object CopySnapshotCallbacks() @@ -17,5 +16,4 @@ internal static object CopySnapshotCallbacks() return restoreHooksRegistry; } } -#endif } diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/SnapstartHelperInitializeWithSnapstartIsolatedAsync.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/SnapstartHelperInitializeWithSnapstartIsolatedAsync.cs index e0874f50b..83d343f76 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/SnapstartHelperInitializeWithSnapstartIsolatedAsync.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/SnapstartHelperInitializeWithSnapstartIsolatedAsync.cs @@ -1,10 +1,9 @@ -using System; +using System; using System.Threading.Tasks; using Amazon.Lambda.RuntimeSupport.Bootstrap; namespace Amazon.Lambda.RuntimeSupport.Helpers { -#if NET8_0_OR_GREATER /// /// Anywhere this class is used in RuntimeSupport it should be wrapped around a try/catch block catching TypeLoadException. /// If the version of Amazon.Lambda.Core in the deployment bundle is out of date the type that is accessing SnapshotRestore @@ -50,5 +49,4 @@ internal static async Task InitializeWithSnapstartAsync(IRuntimeApiClient return true; } } -#endif } diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Utils.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Utils.cs index e23c0e588..1b99448b8 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Utils.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Utils.cs @@ -24,12 +24,7 @@ internal static class Utils public static bool IsRunningNativeAot() { // If dynamic code is not supported we are most likely running in an AOT environment. -#if NET6_0_OR_GREATER return !RuntimeFeature.IsDynamicCodeSupported; -#else - return false; -#endif - } /// @@ -61,22 +56,40 @@ internal static int DetermineProcessingTaskCount(IEnvironmentVariables environme } else { - processingTaskCount = Math.Max(2, processorCount); + // Use the max concurrency value as the default polling task count so there are + // enough polling tasks to fill all available concurrency slots. Fall back to + // the processor-based heuristic if the value cannot be parsed. + var maxConcurrency = GetMaxConcurrency(environmentVariables); + processingTaskCount = maxConcurrency > 0 + ? maxConcurrency + : Math.Max(2, processorCount); } } return processingTaskCount; } + /// + /// Parses the AWS_LAMBDA_MAX_CONCURRENCY environment variable as an integer. + /// Returns the parsed value if valid and greater than 0, otherwise returns 0. + /// + internal static int GetMaxConcurrency(IEnvironmentVariables environmentVariables) + { + var value = environmentVariables.GetEnvironmentVariable(Constants.ENVIRONMENT_VARIABLE_AWS_LAMBDA_MAX_CONCURRENCY); + if (!string.IsNullOrEmpty(value) && int.TryParse(value, out var maxConcurrency) && maxConcurrency > 0) + { + return maxConcurrency; + } + return 0; + } + /// /// Create an Action callback that can be used for setting the trace id on the AWS SDK for .NET if the SDK is present. /// If the AWS .NET SDK is not found then null is returned. /// /// -#if NET8_0_OR_GREATER [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Loading the type is okay to fail if the user is not using the AWS SDK for .NET or it is an old version. If they are using an SDK with the SDKTaskContext the SDK has the attributes to avoid the Set method being trimmed.")] -#endif internal static Action FindAWSSDKTraceIdSetter(IEnvironmentVariables environmentVariables) { if (!Utils.IsUsingMultiConcurrency(environmentVariables)) diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Program.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Program.cs index d51dc0ea7..2c94fbd5d 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Program.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Program.cs @@ -29,16 +29,12 @@ class Program // the Main exists in the Lambda class library mode which will never be used for Native AOT. #pragma warning disable IL2123 #if ExecutableOutputType -#if NET8_0_OR_GREATER [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode( "The Main entry point is used in the managed runtime which loads Lambda functions as a class library. " + "The class library mode does not support Native AOT and trimming.")] -#endif private static async Task Main(string[] args) { -#if NET8_0_OR_GREATER AssemblyLoadContext.Default.Resolving += ResolveSnapshotRestoreAssembly; -#endif if (args.Length == 0) { throw new ArgumentException("The function handler was not provided via command line arguments.", nameof(args)); @@ -53,7 +49,6 @@ private static async Task Main(string[] args) #endif #pragma warning restore IL2123 -#if NET8_0_OR_GREATER [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("This code is only exercised in the class library programming model. Native AOT will not use this code path.")] private static System.Reflection.Assembly ResolveSnapshotRestoreAssembly(AssemblyLoadContext assemblyContext, System.Reflection.AssemblyName assemblyName) { @@ -66,7 +61,6 @@ private static System.Reflection.Assembly ResolveSnapshotRestoreAssembly(Assembl return null; } -#endif /// /// Parse the command line args to create a object diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/RuntimeSupportInitializer.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/RuntimeSupportInitializer.cs index b7f36cc31..0de36f58e 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/RuntimeSupportInitializer.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/RuntimeSupportInitializer.cs @@ -23,9 +23,7 @@ namespace Amazon.Lambda.RuntimeSupport /// /// RuntimeSupportInitializer class responsible for initializing the UserCodeLoader and LambdaBootstrap given a function handler. /// -#if NET8_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("RuntimeSupportInitializer does not support trimming and is meant to be used in class library based Lambda functions.")] -#endif + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("RuntimeSupportInitializer does not support trimming and is meant to be used in class library based Lambda functions.")] public class RuntimeSupportInitializer { private readonly string _handler; @@ -65,11 +63,27 @@ public async Task RunLambdaBootstrap() var environmentVariables = new SystemEnvironmentVariables(); var userCodeLoader = new UserCodeLoader(environmentVariables, _handler, _logger); var initializer = new UserCodeInitializer(userCodeLoader, _logger); + // Pre-declare so the wrapped initializer can reference it. The closure runs + // later (inside bootstrap.RunAsync) by which time bootstrap is assigned. + LambdaBootstrap bootstrap = null; + // Wrap init to plumb the serializer ([assembly: LambdaSerializer]) onto the + // bootstrap right after UserCodeLoader resolves it. The bootstrap then + // surfaces it on ILambdaContext.Serializer for every invocation via the + // Isolated shim. + LambdaBootstrapInitializer wrappedInit = async () => + { + var initResult = await initializer.InitializeAsync(); + if (initResult) + { + bootstrap.SetSerializer(userCodeLoader.CustomerSerializerInstance as Amazon.Lambda.Core.ILambdaSerializer); + } + return initResult; + }; using (var handlerWrapper = HandlerWrapper.GetHandlerWrapper(userCodeLoader.Invoke)) - using (var bootstrap = new LambdaBootstrap( + using (bootstrap = new LambdaBootstrap( httpClient: null, handler: handlerWrapper.Handler, - initializer: initializer.InitializeAsync, + initializer: wrappedInit, ownsHttpClient: true, lambdaBootstrapOptions: _lambdaBootstrapOptions, environmentVariables: environmentVariables)) diff --git a/Libraries/src/Amazon.Lambda.S3Events/Amazon.Lambda.S3Events.csproj b/Libraries/src/Amazon.Lambda.S3Events/Amazon.Lambda.S3Events.csproj index b8c6f3ecb..9b926d46b 100644 --- a/Libraries/src/Amazon.Lambda.S3Events/Amazon.Lambda.S3Events.csproj +++ b/Libraries/src/Amazon.Lambda.S3Events/Amazon.Lambda.S3Events.csproj @@ -4,15 +4,15 @@ Amazon Lambda .NET Core support - S3Events package. - netcoreapp3.1;net8.0 + $(DefaultPackageTargets) Amazon.Lambda.S3Events - 3.1.2 + 4.0.0 Amazon.Lambda.S3Events Amazon.Lambda.S3Events AWS;Amazon;Lambda - + IL2026,IL2067,IL2075 true true diff --git a/Libraries/src/Amazon.Lambda.S3Events/S3ObjectLambdaEvent.cs b/Libraries/src/Amazon.Lambda.S3Events/S3ObjectLambdaEvent.cs index d6d7a0ca2..a03b5e89f 100644 --- a/Libraries/src/Amazon.Lambda.S3Events/S3ObjectLambdaEvent.cs +++ b/Libraries/src/Amazon.Lambda.S3Events/S3ObjectLambdaEvent.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Text; @@ -15,9 +15,7 @@ public class S3ObjectLambdaEvent /// /// The Amazon S3 request ID for this request. We recommend that you log this value to help with debugging. /// -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("xAmzRequestId")] -#endif public string XAmzRequestId { get; set; } /// diff --git a/Libraries/src/Amazon.Lambda.SNSEvents/Amazon.Lambda.SNSEvents.csproj b/Libraries/src/Amazon.Lambda.SNSEvents/Amazon.Lambda.SNSEvents.csproj index da157bbad..f93f0830c 100644 --- a/Libraries/src/Amazon.Lambda.SNSEvents/Amazon.Lambda.SNSEvents.csproj +++ b/Libraries/src/Amazon.Lambda.SNSEvents/Amazon.Lambda.SNSEvents.csproj @@ -4,9 +4,9 @@ Amazon Lambda .NET Core support - SNSEvents package. - netstandard2.0;net8.0 + $(DefaultPackageTargets) Amazon.Lambda.SNSEvents - 2.1.1 + 3.0.0 Amazon.Lambda.SNSEvents Amazon.Lambda.SNSEvents AWS;Amazon;Lambda diff --git a/Libraries/src/Amazon.Lambda.SQSEvents/Amazon.Lambda.SQSEvents.csproj b/Libraries/src/Amazon.Lambda.SQSEvents/Amazon.Lambda.SQSEvents.csproj index 6683a8462..a5b84398b 100644 --- a/Libraries/src/Amazon.Lambda.SQSEvents/Amazon.Lambda.SQSEvents.csproj +++ b/Libraries/src/Amazon.Lambda.SQSEvents/Amazon.Lambda.SQSEvents.csproj @@ -4,9 +4,9 @@ Amazon Lambda .NET Core support - SQSEvents package. - netstandard2.0;netcoreapp3.1;net8.0 + $(DefaultPackageTargets) Amazon.Lambda.SQSEvents - 2.2.1 + 3.0.0 Amazon.Lambda.SQSEvents Amazon.Lambda.SQSEvents AWS;Amazon;Lambda diff --git a/Libraries/src/Amazon.Lambda.SQSEvents/SQSBatchResponse.cs b/Libraries/src/Amazon.Lambda.SQSEvents/SQSBatchResponse.cs index 35241b21f..a58f3cb6a 100644 --- a/Libraries/src/Amazon.Lambda.SQSEvents/SQSBatchResponse.cs +++ b/Libraries/src/Amazon.Lambda.SQSEvents/SQSBatchResponse.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Runtime.Serialization; namespace Amazon.Lambda.SQSEvents @@ -30,9 +30,7 @@ public SQSBatchResponse(List batchItemFailures) /// Gets or sets the message failures within the batch failures /// [DataMember(Name = "batchItemFailures")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("batchItemFailures")] -#endif public List BatchItemFailures { get; set; } /// @@ -45,9 +43,7 @@ public class BatchItemFailure /// MessageId that failed processing /// [DataMember(Name = "itemIdentifier")] -#if NETCOREAPP3_1_OR_GREATER [System.Text.Json.Serialization.JsonPropertyName("itemIdentifier")] -#endif public string ItemIdentifier { get; set; } } } diff --git a/Libraries/src/Amazon.Lambda.Serialization.Json/Amazon.Lambda.Serialization.Json.csproj b/Libraries/src/Amazon.Lambda.Serialization.Json/Amazon.Lambda.Serialization.Json.csproj index 6c03c035a..90591a72d 100644 --- a/Libraries/src/Amazon.Lambda.Serialization.Json/Amazon.Lambda.Serialization.Json.csproj +++ b/Libraries/src/Amazon.Lambda.Serialization.Json/Amazon.Lambda.Serialization.Json.csproj @@ -4,12 +4,12 @@ Amazon Lambda .NET Core support - Serialization.Json package. - netstandard2.0 + $(DefaultPackageTargets) Amazon.Lambda.Serialization.Json Amazon.Lambda.Serialization.Json Amazon.Lambda.Serialization.Json AWS;Amazon;Lambda - 2.2.5 + 3.0.0 diff --git a/Libraries/src/Amazon.Lambda.Serialization.Json/AwsResolver.cs b/Libraries/src/Amazon.Lambda.Serialization.Json/AwsResolver.cs index 056b954db..3d1853a79 100644 --- a/Libraries/src/Amazon.Lambda.Serialization.Json/AwsResolver.cs +++ b/Libraries/src/Amazon.Lambda.Serialization.Json/AwsResolver.cs @@ -78,11 +78,11 @@ protected override IList CreateProperties(Type type, MemberSeriali { if (property.PropertyName.Equals("Data", StringComparison.Ordinal)) { - property.MemberConverter = StreamDataConverter; + property.Converter = StreamDataConverter; } else if (property.PropertyName.Equals("ApproximateArrivalTimestamp", StringComparison.Ordinal)) { - property.MemberConverter = DateTimeConverter; + property.Converter = DateTimeConverter; } } } @@ -95,7 +95,7 @@ protected override IList CreateProperties(Type type, MemberSeriali { if (property.PropertyName.Equals("ApproximateCreationDateTime", StringComparison.Ordinal)) { - property.MemberConverter = DateTimeConverter; + property.Converter = DateTimeConverter; } } } @@ -108,11 +108,11 @@ protected override IList CreateProperties(Type type, MemberSeriali { if (property.PropertyName.Equals("B", StringComparison.Ordinal)) { - property.MemberConverter = StreamDataConverter; + property.Converter = StreamDataConverter; } else if (property.PropertyName.Equals("BS", StringComparison.Ordinal)) { - property.MemberConverter = StreamListDataConverter; + property.Converter = StreamListDataConverter; } } } @@ -122,11 +122,11 @@ protected override IList CreateProperties(Type type, MemberSeriali { if (property.PropertyName.Equals("BinaryValue", StringComparison.Ordinal)) { - property.MemberConverter = StreamDataConverter; + property.Converter = StreamDataConverter; } else if (property.PropertyName.Equals("BinaryListValues", StringComparison.Ordinal)) { - property.MemberConverter = StreamListDataConverter; + property.Converter = StreamListDataConverter; } } } @@ -149,7 +149,7 @@ protected override IList CreateProperties(Type type, MemberSeriali { if (property.PropertyName.Equals("Value", StringComparison.Ordinal)) { - property.MemberConverter = StreamDataConverter; + property.Converter = StreamDataConverter; } } } @@ -157,4 +157,4 @@ protected override IList CreateProperties(Type type, MemberSeriali return properties; } } -} \ No newline at end of file +} diff --git a/Libraries/src/Amazon.Lambda.Serialization.Json/JsonSerializer.cs b/Libraries/src/Amazon.Lambda.Serialization.Json/JsonSerializer.cs index 0555e2f9c..6386a6e1e 100644 --- a/Libraries/src/Amazon.Lambda.Serialization.Json/JsonSerializer.cs +++ b/Libraries/src/Amazon.Lambda.Serialization.Json/JsonSerializer.cs @@ -57,7 +57,7 @@ public JsonSerializer(Action customizeSerializerSettings if (string.Equals(Environment.GetEnvironmentVariable(DEBUG_ENVIRONMENT_VARIABLE_NAME), "true", StringComparison.OrdinalIgnoreCase)) { - this.debug = true; + debug = true; } } diff --git a/Libraries/src/Amazon.Lambda.Serialization.SystemTextJson/AbstractLambdaJsonSerializer.cs b/Libraries/src/Amazon.Lambda.Serialization.SystemTextJson/AbstractLambdaJsonSerializer.cs index 2656c535b..c3ba049d7 100644 --- a/Libraries/src/Amazon.Lambda.Serialization.SystemTextJson/AbstractLambdaJsonSerializer.cs +++ b/Libraries/src/Amazon.Lambda.Serialization.SystemTextJson/AbstractLambdaJsonSerializer.cs @@ -1,4 +1,4 @@ -using Amazon.Lambda.Serialization.SystemTextJson.Converters; +using Amazon.Lambda.Serialization.SystemTextJson.Converters; using System; using System.Collections.Generic; using System.IO; @@ -32,9 +32,9 @@ protected AbstractLambdaJsonSerializer(Action jsonWriterCusto { Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; - jsonWriterCustomizer?.Invoke(this.WriterOptions); + jsonWriterCustomizer?.Invoke(WriterOptions); - this._debug = string.Equals(Environment.GetEnvironmentVariable(DEBUG_ENVIRONMENT_VARIABLE_NAME), "true", + _debug = string.Equals(Environment.GetEnvironmentVariable(DEBUG_ENVIRONMENT_VARIABLE_NAME), "true", StringComparison.OrdinalIgnoreCase); } @@ -130,11 +130,7 @@ protected virtual JsonSerializerOptions CreateDefaultJsonSerializationOptions() { var serializer = new JsonSerializerOptions() { -#if NET6_0_OR_GREATER DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, -#else - IgnoreNullValues = true, -#endif PropertyNameCaseInsensitive = true, PropertyNamingPolicy = new AwsNamingPolicy(), Converters = diff --git a/Libraries/src/Amazon.Lambda.Serialization.SystemTextJson/Amazon.Lambda.Serialization.SystemTextJson.csproj b/Libraries/src/Amazon.Lambda.Serialization.SystemTextJson/Amazon.Lambda.Serialization.SystemTextJson.csproj index d3160d1ee..679bc30ac 100644 --- a/Libraries/src/Amazon.Lambda.Serialization.SystemTextJson/Amazon.Lambda.Serialization.SystemTextJson.csproj +++ b/Libraries/src/Amazon.Lambda.Serialization.SystemTextJson/Amazon.Lambda.Serialization.SystemTextJson.csproj @@ -3,13 +3,13 @@ - netcoreapp3.1;net6;net8.0 + $(DefaultPackageTargets) Amazon Lambda .NET Core support - Serialization.Json with System.Text.Json. Amazon.Lambda.Serialization.SystemTextJson Amazon.Lambda.Serialization.SystemTextJson Amazon.Lambda.Serialization.SystemTextJson AWS;Amazon;Lambda - 2.4.5 + 3.0.0 README.md - + diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/CSharpSourceGeneratorVerifier.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/CSharpSourceGeneratorVerifier.cs index 63d122bf2..74ae8928e 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/CSharpSourceGeneratorVerifier.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/CSharpSourceGeneratorVerifier.cs @@ -3,6 +3,9 @@ using Amazon.Lambda.Core; using Amazon.Lambda.RuntimeSupport; using Amazon.Lambda.Serialization.SystemTextJson; +using Amazon.Lambda.DynamoDBEvents; +using Amazon.Lambda.SNSEvents; +using Amazon.Lambda.CloudWatchEvents.ScheduledEvents; using Amazon.Lambda.ApplicationLoadBalancerEvents; using Amazon.Lambda.SQSEvents; using Microsoft.CodeAnalysis; @@ -25,30 +28,41 @@ namespace Amazon.Lambda.Annotations.SourceGenerators.Tests public static class CSharpSourceGeneratorVerifier where TSourceGenerator : ISourceGenerator, new() { - public class Test : CSharpSourceGeneratorTest + public class Test : CSharpSourceGeneratorTest { public enum ReferencesMode {All, NoApiGatewayEvents} - public enum TargetFramework { Net60, Net80 } + public enum TargetFramework { Net8_0, Net10_0 } private ImmutableArray PreprocessorSymbols { get; set; } = ImmutableArray.Empty; - public Test(ReferencesMode referencesMode = ReferencesMode.All, TargetFramework targetFramework = TargetFramework.Net60) + public Test(ReferencesMode referencesMode = ReferencesMode.All, TargetFramework targetFramework = TargetFramework.Net10_0) { PreprocessorSymbols = ImmutableArray.Create("ANALYZER_UNIT_TESTS"); + var assemblyResolver = (Type t) => + { + var path = t.Assembly.Location; + if (targetFramework == TargetFramework.Net8_0 && path.Contains("net10.0")) + path = path.Replace("net10.0", "net8.0"); + + return path; + }; + if (referencesMode == ReferencesMode.NoApiGatewayEvents) { SolutionTransforms.Add((solution, projectId) => { - return solution.AddMetadataReference(projectId, MetadataReference.CreateFromFile(typeof(ILambdaContext).Assembly.Location)) - .AddMetadataReference(projectId, MetadataReference.CreateFromFile(typeof(IServiceCollection).Assembly.Location)) - .AddMetadataReference(projectId, MetadataReference.CreateFromFile(typeof(ServiceProvider).Assembly.Location)) - .AddMetadataReference(projectId, MetadataReference.CreateFromFile(typeof(RestApiAttribute).Assembly.Location)) - .AddMetadataReference(projectId, MetadataReference.CreateFromFile(typeof(DefaultLambdaJsonSerializer).Assembly.Location)) - .AddMetadataReference(projectId, MetadataReference.CreateFromFile(typeof(HostApplicationBuilder).Assembly.Location)) - .AddMetadataReference(projectId, MetadataReference.CreateFromFile(typeof(IHost).Assembly.Location)) - .AddMetadataReference(projectId, MetadataReference.CreateFromFile(typeof(LambdaBootstrapBuilder).Assembly.Location)); + return solution + .AddMetadataReference(projectId, MetadataReference.CreateFromFile(assemblyResolver(typeof(ILambdaContext)))) + .AddMetadataReference(projectId, MetadataReference.CreateFromFile(assemblyResolver(typeof(IServiceCollection)))) + .AddMetadataReference(projectId, MetadataReference.CreateFromFile(assemblyResolver(typeof(ServiceProvider)))) + .AddMetadataReference(projectId, MetadataReference.CreateFromFile(assemblyResolver(typeof(RestApiAttribute)))) + .AddMetadataReference(projectId, MetadataReference.CreateFromFile(assemblyResolver(typeof(DefaultLambdaJsonSerializer)))) + .AddMetadataReference(projectId, MetadataReference.CreateFromFile(assemblyResolver(typeof(HostApplicationBuilder)))) + .AddMetadataReference(projectId, MetadataReference.CreateFromFile(assemblyResolver(typeof(IHost)))) + .AddMetadataReference(projectId, MetadataReference.CreateFromFile(assemblyResolver(typeof(SnapshotRestore.Registry.RestoreHooksRegistry)))) + .AddMetadataReference(projectId, MetadataReference.CreateFromFile(assemblyResolver(typeof(LambdaBootstrapBuilder)))); }); } @@ -56,22 +70,27 @@ public Test(ReferencesMode referencesMode = ReferencesMode.All, TargetFramework { SolutionTransforms.Add((solution, projectId) => { - return solution.AddMetadataReference(projectId, MetadataReference.CreateFromFile(typeof(ILambdaContext).Assembly.Location)) - .AddMetadataReference(projectId, MetadataReference.CreateFromFile(typeof(APIGatewayProxyRequest).Assembly.Location)) - .AddMetadataReference(projectId, MetadataReference.CreateFromFile(typeof(SQSEvent).Assembly.Location)) - .AddMetadataReference(projectId, MetadataReference.CreateFromFile(typeof(ApplicationLoadBalancerRequest).Assembly.Location)) - .AddMetadataReference(projectId, MetadataReference.CreateFromFile(typeof(IServiceCollection).Assembly.Location)) - .AddMetadataReference(projectId, MetadataReference.CreateFromFile(typeof(ServiceProvider).Assembly.Location)) - .AddMetadataReference(projectId, MetadataReference.CreateFromFile(typeof(RestApiAttribute).Assembly.Location)) - .AddMetadataReference(projectId, MetadataReference.CreateFromFile(typeof(DefaultLambdaJsonSerializer).Assembly.Location)) - .AddMetadataReference(projectId, MetadataReference.CreateFromFile(typeof(HostApplicationBuilder).Assembly.Location)) - .AddMetadataReference(projectId, MetadataReference.CreateFromFile(typeof(IHost).Assembly.Location)) - .AddMetadataReference(projectId, MetadataReference.CreateFromFile(typeof(LambdaBootstrapBuilder).Assembly.Location)); + return solution + .AddMetadataReference(projectId, MetadataReference.CreateFromFile(assemblyResolver(typeof(ILambdaContext)))) + .AddMetadataReference(projectId, MetadataReference.CreateFromFile(assemblyResolver(typeof(APIGatewayProxyRequest)))) + .AddMetadataReference(projectId, MetadataReference.CreateFromFile(assemblyResolver(typeof(DynamoDBEvent)))) + .AddMetadataReference(projectId, MetadataReference.CreateFromFile(assemblyResolver(typeof(SNSEvent)))) + .AddMetadataReference(projectId, MetadataReference.CreateFromFile(assemblyResolver(typeof(SQSEvent)))) + .AddMetadataReference(projectId, MetadataReference.CreateFromFile(assemblyResolver(typeof(ScheduledEvent)))) + .AddMetadataReference(projectId, MetadataReference.CreateFromFile(assemblyResolver(typeof(ApplicationLoadBalancerRequest)))) + .AddMetadataReference(projectId, MetadataReference.CreateFromFile(assemblyResolver(typeof(IServiceCollection)))) + .AddMetadataReference(projectId, MetadataReference.CreateFromFile(assemblyResolver(typeof(ServiceProvider)))) + .AddMetadataReference(projectId, MetadataReference.CreateFromFile(assemblyResolver(typeof(RestApiAttribute)))) + .AddMetadataReference(projectId, MetadataReference.CreateFromFile(assemblyResolver(typeof(DefaultLambdaJsonSerializer)))) + .AddMetadataReference(projectId, MetadataReference.CreateFromFile(assemblyResolver(typeof(HostApplicationBuilder)))) + .AddMetadataReference(projectId, MetadataReference.CreateFromFile(assemblyResolver(typeof(IHost)))) + .AddMetadataReference(projectId, MetadataReference.CreateFromFile(assemblyResolver(typeof(SnapshotRestore.Registry.RestoreHooksRegistry)))) + .AddMetadataReference(projectId, MetadataReference.CreateFromFile(assemblyResolver(typeof(LambdaBootstrapBuilder)))); }); } // Set up the target framework moniker and reference assemblies - if (targetFramework == TargetFramework.Net60) + if (targetFramework == TargetFramework.Net10_0) { SolutionTransforms.Add((solution, projectId) => { @@ -80,13 +99,13 @@ public Test(ReferencesMode referencesMode = ReferencesMode.All, TargetFramework "TargetFrameworkConfig.editorconfig", SourceText.From(""" is_global = true - build_property.TargetFramework = net6.0 + build_property.TargetFramework = net10.0 """), filePath: "/TargetFrameworkConfig.editorconfig"); }); - ReferenceAssemblies = ReferenceAssemblies.Net.Net60; + ReferenceAssemblies = new ReferenceAssemblies("net10.0", new PackageIdentity("Microsoft.NETCore.App.Ref", "10.0.0"), Path.Combine("ref", "net10.0")); } - else if (targetFramework == TargetFramework.Net80) + else if (targetFramework == TargetFramework.Net8_0) { SolutionTransforms.Add((solution, projectId) => { diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/CloudFormationTemplateHandlerTests/FindTemplateTests.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/CloudFormationTemplateHandlerTests/FindTemplateTests.cs index 0a1e33627..ee710606b 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/CloudFormationTemplateHandlerTests/FindTemplateTests.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/CloudFormationTemplateHandlerTests/FindTemplateTests.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using Amazon.Lambda.Annotations.SourceGenerator; using Amazon.Lambda.Annotations.SourceGenerator.FileIO; using Xunit; @@ -39,7 +39,7 @@ public void FindTemplate_FromDefaultConfigFile() 'profile': 'default', 'region': 'us-west-2', 'configuration': 'Release', - 'framework': 'netcoreapp3.1', + 'framework': 'net10.0', 's3-prefix': 'AWSServerless1/', 'template': 'serverless.template', 'template-parameters': '' @@ -75,7 +75,7 @@ public void FindTemplate_DefaultConfigFileDoesNotHaveTemplateProperty() 'profile': 'default', 'region': 'us-west-2', 'configuration': 'Release', - 'framework': 'netcoreapp3.1', + 'framework': 'net10.0', 's3-prefix': 'AWSServerless1/' }"; @@ -121,4 +121,4 @@ public void FindTemplate_DefaultConfigFile_Template_Is_AboveProjectRoot() Assert.True(File.Exists(templatePath)); } } -} \ No newline at end of file +} diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/DynamoDBEventAttributeTests.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/DynamoDBEventAttributeTests.cs new file mode 100644 index 000000000..4f2a0fe91 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/DynamoDBEventAttributeTests.cs @@ -0,0 +1,313 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Lambda.Annotations.DynamoDB; +using Xunit; + +namespace Amazon.Lambda.Annotations.SourceGenerators.Tests +{ + public class DynamoDBEventAttributeTests + { + // ===== Constructor and Default Values ===== + + [Fact] + public void Constructor_SetsStreamProperty() + { + var attr = new DynamoDBEventAttribute("@MyTable"); + + Assert.Equal("@MyTable", attr.Stream); + } + + [Fact] + public void DefaultValues_AreCorrect() + { + var attr = new DynamoDBEventAttribute("@MyTable"); + + Assert.Equal(StartingPosition.LATEST, attr.StartingPosition); + Assert.False(attr.IsBatchSizeSet); + Assert.False(attr.IsEnabledSet); + Assert.False(attr.IsMaximumBatchingWindowInSecondsSet); + Assert.Null(attr.Filters); + Assert.False(attr.IsFiltersSet); + Assert.False(attr.IsResourceNameSet); + Assert.Equal("MyTable", attr.ResourceName); + } + + // ===== ResourceName Tests ===== + + [Fact] + public void ResourceName_DerivedFromStream_WithAtPrefix() + { + var attr = new DynamoDBEventAttribute("@TestTable"); + + Assert.False(attr.IsResourceNameSet); + Assert.Equal("TestTable", attr.ResourceName); + } + + [Fact] + public void ResourceName_DerivedFromStreamArn() + { + var attr = new DynamoDBEventAttribute("arn:aws:dynamodb:us-east-1:123456789012:table/MyTable/stream/2024-01-01T00:00:00.000"); + + Assert.False(attr.IsResourceNameSet); + Assert.Equal("MyTable", attr.ResourceName); + } + + [Fact] + public void ResourceName_WhenExplicitlySet_IsTracked() + { + var attr = new DynamoDBEventAttribute("@MyTable"); + + Assert.False(attr.IsResourceNameSet); + + attr.ResourceName = "CustomEventName"; + Assert.True(attr.IsResourceNameSet); + Assert.Equal("CustomEventName", attr.ResourceName); + } + + // ===== BatchSize Tests ===== + + [Fact] + public void BatchSize_DefaultNotSet() + { + var attr = new DynamoDBEventAttribute("@MyTable"); + + Assert.False(attr.IsBatchSizeSet); + } + + [Fact] + public void BatchSize_WhenSet_IsTracked() + { + var attr = new DynamoDBEventAttribute("@MyTable") + { + BatchSize = 50 + }; + + Assert.True(attr.IsBatchSizeSet); + Assert.Equal((uint)50, attr.BatchSize); + } + + // ===== StartingPosition Tests ===== + + [Fact] + public void StartingPosition_DefaultValue_IsLatest() + { + var attr = new DynamoDBEventAttribute("@MyTable"); + + Assert.Equal(StartingPosition.LATEST, attr.StartingPosition); + } + + [Fact] + public void StartingPosition_CanBeSetToTrimHorizon() + { + var attr = new DynamoDBEventAttribute("@MyTable") + { + StartingPosition = StartingPosition.TRIM_HORIZON + }; + + Assert.Equal(StartingPosition.TRIM_HORIZON, attr.StartingPosition); + } + + // ===== MaximumBatchingWindowInSeconds Tests ===== + + [Fact] + public void MaximumBatchingWindowInSeconds_DefaultNotSet() + { + var attr = new DynamoDBEventAttribute("@MyTable"); + + Assert.False(attr.IsMaximumBatchingWindowInSecondsSet); + } + + [Fact] + public void MaximumBatchingWindowInSeconds_WhenSet_IsTracked() + { + var attr = new DynamoDBEventAttribute("@MyTable") + { + MaximumBatchingWindowInSeconds = 60 + }; + + Assert.True(attr.IsMaximumBatchingWindowInSecondsSet); + Assert.Equal((uint)60, attr.MaximumBatchingWindowInSeconds); + } + + // ===== Enabled Property Tests ===== + + [Fact] + public void Enabled_DefaultNotSet() + { + var attr = new DynamoDBEventAttribute("@MyTable"); + + Assert.False(attr.IsEnabledSet); + } + + [Fact] + public void Enabled_WhenSetToFalse_IsTracked() + { + var attr = new DynamoDBEventAttribute("@MyTable") + { + Enabled = false + }; + + Assert.True(attr.IsEnabledSet); + Assert.False(attr.Enabled); + } + + [Fact] + public void Enabled_WhenSetToTrue_IsTracked() + { + var attr = new DynamoDBEventAttribute("@MyTable") + { + Enabled = true + }; + + Assert.True(attr.IsEnabledSet); + Assert.True(attr.Enabled); + } + + // ===== Filters Tests ===== + + [Fact] + public void Filters_DefaultIsNull() + { + var attr = new DynamoDBEventAttribute("@MyTable"); + + Assert.Null(attr.Filters); + Assert.False(attr.IsFiltersSet); + } + + [Fact] + public void Filters_WhenSet_IsTracked() + { + var attr = new DynamoDBEventAttribute("@MyTable") + { + Filters = "{\"eventName\": [\"INSERT\"]}" + }; + + Assert.True(attr.IsFiltersSet); + Assert.Equal("{\"eventName\": [\"INSERT\"]}", attr.Filters); + } + + // ===== Validation Tests ===== + + [Fact] + public void Validate_ValidResourceReference_ReturnsNoErrors() + { + var attr = new DynamoDBEventAttribute("@MyTable"); + + var errors = attr.Validate(); + Assert.Empty(errors); + } + + [Fact] + public void Validate_ValidStreamArn_ReturnsNoErrors() + { + var attr = new DynamoDBEventAttribute("arn:aws:dynamodb:us-east-1:123456789012:table/MyTable/stream/2024-01-01T00:00:00.000"); + + var errors = attr.Validate(); + Assert.Empty(errors); + } + + [Fact] + public void Validate_InvalidStreamArn_ReturnsError() + { + var attr = new DynamoDBEventAttribute("not-a-valid-arn"); + + var errors = attr.Validate(); + Assert.Single(errors); + Assert.Contains("Stream", errors[0]); + } + + [Fact] + public void Validate_InvalidResourceName_ReturnsError() + { + var attr = new DynamoDBEventAttribute("@MyTable") + { + ResourceName = "invalid-name!" + }; + + var errors = attr.Validate(); + Assert.Single(errors); + Assert.Contains("ResourceName", errors[0]); + Assert.Contains("alphanumeric", errors[0]); + } + + [Fact] + public void Validate_BatchSizeTooLarge_ReturnsError() + { + var attr = new DynamoDBEventAttribute("@MyTable") + { + BatchSize = 10001 + }; + + var errors = attr.Validate(); + Assert.Single(errors); + Assert.Contains("BatchSize", errors[0]); + } + + [Fact] + public void Validate_MaxBatchingWindowTooLarge_ReturnsError() + { + var attr = new DynamoDBEventAttribute("@MyTable") + { + MaximumBatchingWindowInSeconds = 301 + }; + + var errors = attr.Validate(); + Assert.Single(errors); + Assert.Contains("MaximumBatchingWindowInSeconds", errors[0]); + } + + + [Fact] + public void Validate_AtSignOnly_ReturnsError() + { + var attr = new DynamoDBEventAttribute("@"); + + var errors = attr.Validate(); + Assert.Single(errors); + Assert.Contains("Stream", errors[0]); + Assert.Contains("'@' prefix must be followed by a non-empty resource or parameter name", errors[0]); + } + + [Fact] + public void Validate_AtSignWithWhitespace_ReturnsError() + { + var attr = new DynamoDBEventAttribute("@ "); + + var errors = attr.Validate(); + Assert.Single(errors); + Assert.Contains("Stream", errors[0]); + Assert.Contains("'@' prefix must be followed by a non-empty resource or parameter name", errors[0]); + } + + [Fact] + public void Validate_MultipleErrors_ReturnsAll() + { + var attr = new DynamoDBEventAttribute("not-valid") + { + ResourceName = "invalid!", + BatchSize = 10001 + }; + + var errors = attr.Validate(); + Assert.Equal(3, errors.Count); + } + + [Fact] + public void Validate_AllValidWithOptionals_ReturnsNoErrors() + { + var attr = new DynamoDBEventAttribute("@MyTable") + { + ResourceName = "MyDynamoDBEvent", + BatchSize = 100, + StartingPosition = StartingPosition.TRIM_HORIZON, + MaximumBatchingWindowInSeconds = 60, + Filters = "{\"eventName\": [\"INSERT\"]}", + Enabled = true + }; + + var errors = attr.Validate(); + Assert.Empty(errors); + } + } +} diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SNSEventAttributeTests.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SNSEventAttributeTests.cs new file mode 100644 index 000000000..7adf242ce --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SNSEventAttributeTests.cs @@ -0,0 +1,193 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Lambda.Annotations.SNS; +using Xunit; + +namespace Amazon.Lambda.Annotations.SourceGenerators.Tests +{ + public class SNSEventAttributeTests + { + // ===== Constructor and Default Values ===== + + [Fact] + public void Constructor_SetsTopicProperty() + { + var attr = new SNSEventAttribute("@MyTopic"); + + Assert.Equal("@MyTopic", attr.Topic); + } + + [Fact] + public void DefaultValues_AreCorrect() + { + var attr = new SNSEventAttribute("@MyTopic"); + + Assert.False(attr.IsEnabledSet); + Assert.Null(attr.FilterPolicy); + Assert.False(attr.IsFilterPolicySet); + Assert.False(attr.IsResourceNameSet); + Assert.Equal("MyTopic", attr.ResourceName); + } + + // ===== ResourceName Tests ===== + + [Fact] + public void ResourceName_DerivedFromTopic_WithAtPrefix() + { + var attr = new SNSEventAttribute("@TestTopic"); + + Assert.False(attr.IsResourceNameSet); + Assert.Equal("TestTopic", attr.ResourceName); + } + + [Fact] + public void ResourceName_DerivedFromTopicArn() + { + var attr = new SNSEventAttribute("arn:aws:sns:us-east-1:123456789012:MyTopic"); + + Assert.False(attr.IsResourceNameSet); + Assert.Equal("MyTopic", attr.ResourceName); + } + + [Fact] + public void ResourceName_WhenExplicitlySet_IsTracked() + { + var attr = new SNSEventAttribute("@MyTopic"); + + Assert.False(attr.IsResourceNameSet); + + attr.ResourceName = "CustomEventName"; + Assert.True(attr.IsResourceNameSet); + Assert.Equal("CustomEventName", attr.ResourceName); + } + + // ===== FilterPolicy Tests ===== + + [Fact] + public void FilterPolicy_DefaultIsNull() + { + var attr = new SNSEventAttribute("@MyTopic"); + + Assert.Null(attr.FilterPolicy); + Assert.False(attr.IsFilterPolicySet); + } + + [Fact] + public void FilterPolicy_WhenSet_IsTracked() + { + var attr = new SNSEventAttribute("@MyTopic") + { + FilterPolicy = "{\"store\": [\"example_corp\"]}" + }; + + Assert.True(attr.IsFilterPolicySet); + Assert.Equal("{\"store\": [\"example_corp\"]}", attr.FilterPolicy); + } + + // ===== Enabled Property Tests ===== + + [Fact] + public void Enabled_DefaultNotSet() + { + var attr = new SNSEventAttribute("@MyTopic"); + + Assert.False(attr.IsEnabledSet); + } + + [Fact] + public void Enabled_WhenSetToFalse_IsTracked() + { + var attr = new SNSEventAttribute("@MyTopic") + { + Enabled = false + }; + + Assert.True(attr.IsEnabledSet); + Assert.False(attr.Enabled); + } + + [Fact] + public void Enabled_WhenSetToTrue_IsTracked() + { + var attr = new SNSEventAttribute("@MyTopic") + { + Enabled = true + }; + + Assert.True(attr.IsEnabledSet); + Assert.True(attr.Enabled); + } + + // ===== Validation Tests ===== + + [Fact] + public void Validate_ValidResourceReference_ReturnsNoErrors() + { + var attr = new SNSEventAttribute("@MyTopic"); + + var errors = attr.Validate(); + Assert.Empty(errors); + } + + [Fact] + public void Validate_ValidTopicArn_ReturnsNoErrors() + { + var attr = new SNSEventAttribute("arn:aws:sns:us-east-1:123456789012:MyTopic"); + + var errors = attr.Validate(); + Assert.Empty(errors); + } + + [Fact] + public void Validate_InvalidTopicArn_ReturnsError() + { + var attr = new SNSEventAttribute("not-a-valid-arn"); + + var errors = attr.Validate(); + Assert.Single(errors); + Assert.Contains("Topic", errors[0]); + Assert.Contains("ARN", errors[0]); + } + + [Fact] + public void Validate_InvalidResourceName_ReturnsError() + { + var attr = new SNSEventAttribute("@MyTopic") + { + ResourceName = "invalid-name!" + }; + + var errors = attr.Validate(); + Assert.Single(errors); + Assert.Contains("ResourceName", errors[0]); + Assert.Contains("alphanumeric", errors[0]); + } + + [Fact] + public void Validate_MultipleErrors_ReturnsAll() + { + var attr = new SNSEventAttribute("not-valid") + { + ResourceName = "invalid!" + }; + + var errors = attr.Validate(); + Assert.Equal(2, errors.Count); + } + + [Fact] + public void Validate_AllValidWithOptionals_ReturnsNoErrors() + { + var attr = new SNSEventAttribute("@MyTopic") + { + ResourceName = "MySNSEvent", + FilterPolicy = "{\"store\": [\"example_corp\"]}", + Enabled = true + }; + + var errors = attr.Validate(); + Assert.Empty(errors); + } + } +} diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SQSEventAttributeTests.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SQSEventAttributeTests.cs new file mode 100644 index 000000000..bf57c36c0 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SQSEventAttributeTests.cs @@ -0,0 +1,366 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Lambda.Annotations.SQS; +using Xunit; + +namespace Amazon.Lambda.Annotations.SourceGenerators.Tests +{ + public class SQSEventAttributeTests + { + // ===== Constructor and Default Values ===== + + [Fact] + public void Constructor_SetsQueueProperty() + { + var attr = new SQSEventAttribute("@MyQueue"); + + Assert.Equal("@MyQueue", attr.Queue); + } + + [Fact] + public void DefaultValues_AreCorrect() + { + var attr = new SQSEventAttribute("@MyQueue"); + + Assert.False(attr.IsBatchSizeSet); + Assert.False(attr.IsEnabledSet); + Assert.False(attr.IsMaximumBatchingWindowInSecondsSet); + Assert.False(attr.IsMaximumConcurrencySet); + Assert.Null(attr.Filters); + Assert.False(attr.IsFiltersSet); + Assert.False(attr.IsResourceNameSet); + Assert.Equal("MyQueue", attr.ResourceName); + } + + // ===== ResourceName Tests ===== + + [Fact] + public void ResourceName_DerivedFromQueue_WithAtPrefix() + { + var attr = new SQSEventAttribute("@TestQueue"); + + Assert.False(attr.IsResourceNameSet); + Assert.Equal("TestQueue", attr.ResourceName); + } + + [Fact] + public void ResourceName_DerivedFromQueueArn() + { + var attr = new SQSEventAttribute("arn:aws:sqs:us-east-1:123456789012:MyQueue"); + + Assert.False(attr.IsResourceNameSet); + Assert.Equal("MyQueue", attr.ResourceName); + } + + [Fact] + public void ResourceName_WhenExplicitlySet_IsTracked() + { + var attr = new SQSEventAttribute("@MyQueue"); + + Assert.False(attr.IsResourceNameSet); + + attr.ResourceName = "CustomEventName"; + Assert.True(attr.IsResourceNameSet); + Assert.Equal("CustomEventName", attr.ResourceName); + } + + // ===== BatchSize Tests ===== + + [Fact] + public void BatchSize_DefaultNotSet() + { + var attr = new SQSEventAttribute("@MyQueue"); + + Assert.False(attr.IsBatchSizeSet); + } + + [Fact] + public void BatchSize_WhenSet_IsTracked() + { + var attr = new SQSEventAttribute("@MyQueue") + { + BatchSize = 50 + }; + + Assert.True(attr.IsBatchSizeSet); + Assert.Equal((uint)50, attr.BatchSize); + } + + // ===== MaximumBatchingWindowInSeconds Tests ===== + + [Fact] + public void MaximumBatchingWindowInSeconds_DefaultNotSet() + { + var attr = new SQSEventAttribute("@MyQueue"); + + Assert.False(attr.IsMaximumBatchingWindowInSecondsSet); + } + + [Fact] + public void MaximumBatchingWindowInSeconds_WhenSet_IsTracked() + { + var attr = new SQSEventAttribute("@MyQueue") + { + MaximumBatchingWindowInSeconds = 60 + }; + + Assert.True(attr.IsMaximumBatchingWindowInSecondsSet); + Assert.Equal((uint)60, attr.MaximumBatchingWindowInSeconds); + } + + // ===== MaximumConcurrency Tests ===== + + [Fact] + public void MaximumConcurrency_DefaultNotSet() + { + var attr = new SQSEventAttribute("@MyQueue"); + + Assert.False(attr.IsMaximumConcurrencySet); + } + + [Fact] + public void MaximumConcurrency_WhenSet_IsTracked() + { + var attr = new SQSEventAttribute("@MyQueue") + { + MaximumConcurrency = 10 + }; + + Assert.True(attr.IsMaximumConcurrencySet); + Assert.Equal((uint)10, attr.MaximumConcurrency); + } + + // ===== Enabled Property Tests ===== + + [Fact] + public void Enabled_DefaultNotSet() + { + var attr = new SQSEventAttribute("@MyQueue"); + + Assert.False(attr.IsEnabledSet); + } + + [Fact] + public void Enabled_WhenSetToFalse_IsTracked() + { + var attr = new SQSEventAttribute("@MyQueue") + { + Enabled = false + }; + + Assert.True(attr.IsEnabledSet); + Assert.False(attr.Enabled); + } + + [Fact] + public void Enabled_WhenSetToTrue_IsTracked() + { + var attr = new SQSEventAttribute("@MyQueue") + { + Enabled = true + }; + + Assert.True(attr.IsEnabledSet); + Assert.True(attr.Enabled); + } + + // ===== Filters Tests ===== + + [Fact] + public void Filters_DefaultIsNull() + { + var attr = new SQSEventAttribute("@MyQueue"); + + Assert.Null(attr.Filters); + Assert.False(attr.IsFiltersSet); + } + + [Fact] + public void Filters_WhenSet_IsTracked() + { + var attr = new SQSEventAttribute("@MyQueue") + { + Filters = "{ \"body\" : { \"RequestCode\" : [ \"BBBB\" ] } }" + }; + + Assert.True(attr.IsFiltersSet); + Assert.Equal("{ \"body\" : { \"RequestCode\" : [ \"BBBB\" ] } }", attr.Filters); + } + + // ===== Validation Tests ===== + + [Fact] + public void Validate_ValidResourceReference_ReturnsNoErrors() + { + var attr = new SQSEventAttribute("@MyQueue"); + + var errors = attr.Validate(); + Assert.Empty(errors); + } + + [Fact] + public void Validate_ValidQueueArn_ReturnsNoErrors() + { + var attr = new SQSEventAttribute("arn:aws:sqs:us-east-1:123456789012:MyQueue"); + + var errors = attr.Validate(); + Assert.Empty(errors); + } + + [Fact] + public void Validate_InvalidQueueArn_ReturnsError() + { + var attr = new SQSEventAttribute("not-a-valid-arn"); + + var errors = attr.Validate(); + Assert.Single(errors); + Assert.Contains("Queue", errors[0]); + Assert.Contains("ARN", errors[0]); + } + + [Fact] + public void Validate_InvalidResourceName_ReturnsError() + { + var attr = new SQSEventAttribute("@MyQueue") + { + ResourceName = "invalid-name!" + }; + + var errors = attr.Validate(); + Assert.Single(errors); + Assert.Contains("ResourceName", errors[0]); + Assert.Contains("alphanumeric", errors[0]); + } + + [Fact] + public void Validate_BatchSizeTooLarge_ReturnsError() + { + var attr = new SQSEventAttribute("@MyQueue") + { + BatchSize = 10001 + }; + + var errors = attr.Validate(); + Assert.Contains(errors, e => e.Contains("BatchSize")); + } + + [Fact] + public void Validate_BatchSizeZero_ReturnsError() + { + var attr = new SQSEventAttribute("@MyQueue") + { + BatchSize = 0 + }; + + var errors = attr.Validate(); + Assert.Contains(errors, e => e.Contains("BatchSize")); + } + + [Fact] + public void Validate_MaxBatchingWindowTooLarge_ReturnsError() + { + var attr = new SQSEventAttribute("@MyQueue") + { + MaximumBatchingWindowInSeconds = 301 + }; + + var errors = attr.Validate(); + Assert.Single(errors); + Assert.Contains("MaximumBatchingWindowInSeconds", errors[0]); + } + + [Fact] + public void Validate_MaximumConcurrencyTooLow_ReturnsError() + { + var attr = new SQSEventAttribute("@MyQueue") + { + MaximumConcurrency = 1 + }; + + var errors = attr.Validate(); + Assert.Single(errors); + Assert.Contains("MaximumConcurrency", errors[0]); + } + + [Fact] + public void Validate_MaximumConcurrencyTooHigh_ReturnsError() + { + var attr = new SQSEventAttribute("@MyQueue") + { + MaximumConcurrency = 1001 + }; + + var errors = attr.Validate(); + Assert.Single(errors); + Assert.Contains("MaximumConcurrency", errors[0]); + } + + [Fact] + public void Validate_BatchSizeGreaterThan10_RequiresMaximumBatchingWindow() + { + var attr = new SQSEventAttribute("@MyQueue") + { + BatchSize = 100 + }; + + var errors = attr.Validate(); + Assert.Contains(errors, e => e.Contains("MaximumBatchingWindowInSeconds")); + } + + [Fact] + public void Validate_FifoQueue_MaximumBatchingWindowNotAllowed() + { + var attr = new SQSEventAttribute("arn:aws:sqs:us-east-2:444455556666:test-queue.fifo") + { + MaximumBatchingWindowInSeconds = 5 + }; + + var errors = attr.Validate(); + Assert.Contains(errors, e => e.Contains("FIFO")); + } + + [Fact] + public void Validate_FifoQueue_BatchSizeGreaterThan10NotAllowed() + { + var attr = new SQSEventAttribute("arn:aws:sqs:us-east-2:444455556666:test-queue.fifo") + { + BatchSize = 100, + MaximumBatchingWindowInSeconds = 5 + }; + + var errors = attr.Validate(); + Assert.Contains(errors, e => e.Contains("FIFO") && e.Contains("BatchSize")); + } + + [Fact] + public void Validate_EmptyResourceName_ReturnsError() + { + var attr = new SQSEventAttribute("@MyQueue") + { + ResourceName = "" + }; + + var errors = attr.Validate(); + Assert.Single(errors); + Assert.Contains("ResourceName", errors[0]); + } + + [Fact] + public void Validate_AllValidWithOptionals_ReturnsNoErrors() + { + var attr = new SQSEventAttribute("@MyQueue") + { + ResourceName = "MySQSEvent", + BatchSize = 5, + MaximumBatchingWindowInSeconds = 60, + MaximumConcurrency = 30, + Filters = "{ \"body\" : { \"RequestCode\" : [ \"BBBB\" ] } }", + Enabled = true + }; + + var errors = attr.Validate(); + Assert.Empty(errors); + } + } +} diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/ScheduleEventAttributeTests.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/ScheduleEventAttributeTests.cs new file mode 100644 index 000000000..c5fe1ce28 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/ScheduleEventAttributeTests.cs @@ -0,0 +1,258 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Lambda.Annotations.Schedule; +using Xunit; + +namespace Amazon.Lambda.Annotations.SourceGenerators.Tests +{ + public class ScheduleEventAttributeTests + { + // ===== Constructor and Default Values ===== + + [Fact] + public void Constructor_SetsScheduleProperty() + { + var attr = new ScheduleEventAttribute("rate(5 minutes)"); + + Assert.Equal("rate(5 minutes)", attr.Schedule); + } + + [Fact] + public void DefaultValues_AreCorrect() + { + var attr = new ScheduleEventAttribute("rate(5 minutes)"); + + Assert.False(attr.IsEnabledSet); + Assert.Null(attr.Description); + Assert.False(attr.IsDescriptionSet); + Assert.Null(attr.Input); + Assert.False(attr.IsInputSet); + Assert.False(attr.IsResourceNameSet); + Assert.Equal("rate5minutes", attr.ResourceName); + } + + // ===== ResourceName Tests ===== + + [Fact] + public void ResourceName_DerivedFromScheduleExpression() + { + var attr = new ScheduleEventAttribute("rate(1 hour)"); + + Assert.False(attr.IsResourceNameSet); + Assert.Equal("rate1hour", attr.ResourceName); + } + + [Fact] + public void ResourceName_DerivedFromCronExpression() + { + var attr = new ScheduleEventAttribute("cron(0 12 * * ? *)"); + + Assert.False(attr.IsResourceNameSet); + Assert.Equal("cron012", attr.ResourceName); + } + + [Fact] + public void ResourceName_WhenExplicitlySet_IsTracked() + { + var attr = new ScheduleEventAttribute("rate(5 minutes)"); + + Assert.False(attr.IsResourceNameSet); + + attr.ResourceName = "CustomSchedule"; + Assert.True(attr.IsResourceNameSet); + Assert.Equal("CustomSchedule", attr.ResourceName); + } + + // ===== Description Tests ===== + + [Fact] + public void Description_DefaultIsNull() + { + var attr = new ScheduleEventAttribute("rate(5 minutes)"); + + Assert.Null(attr.Description); + Assert.False(attr.IsDescriptionSet); + } + + [Fact] + public void Description_WhenSet_IsTracked() + { + var attr = new ScheduleEventAttribute("rate(5 minutes)") + { + Description = "Run every 5 minutes" + }; + + Assert.True(attr.IsDescriptionSet); + Assert.Equal("Run every 5 minutes", attr.Description); + } + + // ===== Input Tests ===== + + [Fact] + public void Input_DefaultIsNull() + { + var attr = new ScheduleEventAttribute("rate(5 minutes)"); + + Assert.Null(attr.Input); + Assert.False(attr.IsInputSet); + } + + [Fact] + public void Input_WhenSet_IsTracked() + { + var attr = new ScheduleEventAttribute("rate(5 minutes)") + { + Input = "{\"key\": \"value\"}" + }; + + Assert.True(attr.IsInputSet); + Assert.Equal("{\"key\": \"value\"}", attr.Input); + } + + // ===== Enabled Property Tests ===== + + [Fact] + public void Enabled_DefaultNotSet() + { + var attr = new ScheduleEventAttribute("rate(5 minutes)"); + + Assert.False(attr.IsEnabledSet); + } + + [Fact] + public void Enabled_WhenSetToFalse_IsTracked() + { + var attr = new ScheduleEventAttribute("rate(5 minutes)") + { + Enabled = false + }; + + Assert.True(attr.IsEnabledSet); + Assert.False(attr.Enabled); + } + + [Fact] + public void Enabled_WhenSetToTrue_IsTracked() + { + var attr = new ScheduleEventAttribute("rate(5 minutes)") + { + Enabled = true + }; + + Assert.True(attr.IsEnabledSet); + Assert.True(attr.Enabled); + } + + // ===== Validation Tests ===== + + [Fact] + public void Validate_ValidRateExpression_ReturnsNoErrors() + { + var attr = new ScheduleEventAttribute("rate(5 minutes)"); + + var errors = attr.Validate(); + Assert.Empty(errors); + } + + [Fact] + public void Validate_ValidCronExpression_ReturnsNoErrors() + { + var attr = new ScheduleEventAttribute("cron(0 12 * * ? *)"); + + var errors = attr.Validate(); + Assert.Empty(errors); + } + + [Fact] + public void Validate_NullSchedule_ReturnsError() + { + var attr = new ScheduleEventAttribute(null); + + var errors = attr.Validate(); + Assert.Single(errors); + Assert.Contains("Schedule", errors[0]); + } + + [Fact] + public void Validate_EmptySchedule_ReturnsError() + { + var attr = new ScheduleEventAttribute(""); + + var errors = attr.Validate(); + Assert.Single(errors); + Assert.Contains("Schedule", errors[0]); + } + + [Fact] + public void Validate_InvalidScheduleExpression_ReturnsError() + { + var attr = new ScheduleEventAttribute("every 5 minutes"); + + var errors = attr.Validate(); + Assert.Single(errors); + Assert.Contains("Schedule", errors[0]); + Assert.Contains("rate(", errors[0]); + } + + [Fact] + public void Validate_InvalidResourceName_ReturnsError() + { + var attr = new ScheduleEventAttribute("rate(5 minutes)") + { + ResourceName = "invalid-name!" + }; + + var errors = attr.Validate(); + Assert.Single(errors); + Assert.Contains("ResourceName", errors[0]); + Assert.Contains("alphanumeric", errors[0]); + } + + [Fact] + public void Validate_MultipleErrors_ReturnsAll() + { + var attr = new ScheduleEventAttribute("invalid") + { + ResourceName = "invalid!" + }; + + var errors = attr.Validate(); + Assert.Equal(2, errors.Count); + } + + [Fact] + public void Enabled_DefaultValueIsTrue_WhenNotExplicitlySet() + { + var attr = new ScheduleEventAttribute("rate(5 minutes)"); + + Assert.False(attr.IsEnabledSet); + Assert.True(attr.Enabled); + } + + [Fact] + public void ResourceName_NullSchedule_DoesNotThrow() + { + var attr = new ScheduleEventAttribute(null); + + // Should not throw NullReferenceException + var resourceName = attr.ResourceName; + Assert.Equal("ScheduleEvent", resourceName); + } + + [Fact] + public void Validate_AllValidWithOptionals_ReturnsNoErrors() + { + var attr = new ScheduleEventAttribute("rate(1 hour)") + { + ResourceName = "MySchedule", + Description = "Hourly job", + Input = "{\"action\": \"cleanup\"}", + Enabled = true + }; + + var errors = attr.Validate(); + Assert.Empty(errors); + } + } +} diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthNameFallback_GetUserId_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthNameFallback_GetUserId_Generated.g.cs index 36f99cbfe..138c0d461 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthNameFallback_GetUserId_Generated.g.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthNameFallback_GetUserId_Generated.g.cs @@ -40,7 +40,7 @@ public AuthNameFallback_GetUserId_Generated() var userId = default(string); if (__request__.RequestContext?.Authorizer?.Lambda == null || __request__.RequestContext?.Authorizer?.Lambda.ContainsKey("userId") == false) { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER __context__.Logger.LogDebug("Authorizer attribute 'userId' was missing, returning unauthorized."); #else __context__.Logger.Log("Authorizer attribute 'userId' was missing, returning unauthorized."); @@ -64,7 +64,7 @@ public AuthNameFallback_GetUserId_Generated() } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -#if NET6_0_OR_GREATER +#if NET8_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()); diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthorizerFunction_SimpleHttpApiAuthorize_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthorizerFunction_SimpleHttpApiAuthorize_Generated.g.cs index e5dc0f74d..cb60c9ae3 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthorizerFunction_SimpleHttpApiAuthorize_Generated.g.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthorizerFunction_SimpleHttpApiAuthorize_Generated.g.cs @@ -45,7 +45,7 @@ public System.IO.Stream SimpleHttpApiAuthorize(Amazon.Lambda.APIGatewayEvents.AP } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER __context__.Logger.LogError(e, "Failed to extract header 'Authorization'."); #else __context__.Logger.Log("Failed to extract header 'Authorization'. Exception: " + e.ToString()); diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthorizerFunction_SimpleRestApiAuthorize_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthorizerFunction_SimpleRestApiAuthorize_Generated.g.cs index 05b9f6e71..f030e1938 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthorizerFunction_SimpleRestApiAuthorize_Generated.g.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthorizerFunction_SimpleRestApiAuthorize_Generated.g.cs @@ -45,7 +45,7 @@ public System.IO.Stream SimpleRestApiAuthorize(Amazon.Lambda.APIGatewayEvents.AP } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER __context__.Logger.LogError(e, "Failed to extract authorization token."); #else __context__.Logger.Log("Failed to extract authorization token. Exception: " + e.ToString()); diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/CustomAuthorizerHttpApiExample_HttpApiAuthorizer_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/CustomAuthorizerHttpApiExample_HttpApiAuthorizer_Generated.g.cs index 62bdd521f..888fe077b 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/CustomAuthorizerHttpApiExample_HttpApiAuthorizer_Generated.g.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/CustomAuthorizerHttpApiExample_HttpApiAuthorizer_Generated.g.cs @@ -1,4 +1,4 @@ -// +// using System; using System.Linq; @@ -40,7 +40,7 @@ public CustomAuthorizerHttpApiExample_HttpApiAuthorizer_Generated() var authorizerValue = default(string); if (__request__.RequestContext?.Authorizer?.Lambda == null || __request__.RequestContext?.Authorizer?.Lambda.ContainsKey("authKey") == false) { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER __context__.Logger.LogDebug("Authorizer attribute 'authKey' was missing, returning unauthorized."); #else __context__.Logger.Log("Authorizer attribute 'authKey' was missing, returning unauthorized."); @@ -64,7 +64,7 @@ public CustomAuthorizerHttpApiExample_HttpApiAuthorizer_Generated() } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER __context__.Logger.LogError(e, "Failed to convert authorizer attribute 'authKey', returning unauthorized."); #else __context__.Logger.Log("Failed to convert authorizer attribute 'authKey', returning unauthorized. Exception: " + e.ToString()); diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/CustomAuthorizerHttpApiV1Example_HttpApiV1Authorizer_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/CustomAuthorizerHttpApiV1Example_HttpApiV1Authorizer_Generated.g.cs index c7cc8eb9f..9b3e474a4 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/CustomAuthorizerHttpApiV1Example_HttpApiV1Authorizer_Generated.g.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/CustomAuthorizerHttpApiV1Example_HttpApiV1Authorizer_Generated.g.cs @@ -40,7 +40,7 @@ public CustomAuthorizerHttpApiV1Example_HttpApiV1Authorizer_Generated() var authorizerValue = default(string); if (__request__.RequestContext?.Authorizer == null || __request__.RequestContext?.Authorizer.ContainsKey("authKey") == false) { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER __context__.Logger.LogDebug("Authorizer attribute 'authKey' was missing, returning unauthorized."); #else __context__.Logger.Log("Authorizer attribute 'authKey' was missing, returning unauthorized."); @@ -64,7 +64,7 @@ public CustomAuthorizerHttpApiV1Example_HttpApiV1Authorizer_Generated() } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER __context__.Logger.LogError(e, "Failed to convert authorizer attribute 'authKey', returning unauthorized."); #else __context__.Logger.Log("Failed to convert authorizer attribute 'authKey', returning unauthorized. Exception: " + e.ToString()); diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/CustomAuthorizerNonStringExample_HttpApiWithNonString_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/CustomAuthorizerNonStringExample_HttpApiWithNonString_Generated.g.cs index 2847aae15..0614accf0 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/CustomAuthorizerNonStringExample_HttpApiWithNonString_Generated.g.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/CustomAuthorizerNonStringExample_HttpApiWithNonString_Generated.g.cs @@ -40,7 +40,7 @@ public CustomAuthorizerNonStringExample_HttpApiWithNonString_Generated() var userId = default(int); if (__request__.RequestContext?.Authorizer?.Lambda == null || __request__.RequestContext?.Authorizer?.Lambda.ContainsKey("userId") == false) { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER __context__.Logger.LogDebug("Authorizer attribute 'userId' was missing, returning unauthorized."); #else __context__.Logger.Log("Authorizer attribute 'userId' was missing, returning unauthorized."); @@ -64,7 +64,7 @@ public CustomAuthorizerNonStringExample_HttpApiWithNonString_Generated() } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -#if NET6_0_OR_GREATER +#if NET8_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()); @@ -84,7 +84,7 @@ public CustomAuthorizerNonStringExample_HttpApiWithNonString_Generated() var isAdmin = default(bool); if (__request__.RequestContext?.Authorizer?.Lambda == null || __request__.RequestContext?.Authorizer?.Lambda.ContainsKey("isAdmin") == false) { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER __context__.Logger.LogDebug("Authorizer attribute 'isAdmin' was missing, returning unauthorized."); #else __context__.Logger.Log("Authorizer attribute 'isAdmin' was missing, returning unauthorized."); @@ -108,7 +108,7 @@ public CustomAuthorizerNonStringExample_HttpApiWithNonString_Generated() } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -#if NET6_0_OR_GREATER +#if NET8_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()); @@ -128,7 +128,7 @@ public CustomAuthorizerNonStringExample_HttpApiWithNonString_Generated() var score = default(double); if (__request__.RequestContext?.Authorizer?.Lambda == null || __request__.RequestContext?.Authorizer?.Lambda.ContainsKey("score") == false) { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER __context__.Logger.LogDebug("Authorizer attribute 'score' was missing, returning unauthorized."); #else __context__.Logger.Log("Authorizer attribute 'score' was missing, returning unauthorized."); @@ -152,7 +152,7 @@ public CustomAuthorizerNonStringExample_HttpApiWithNonString_Generated() } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -#if NET6_0_OR_GREATER +#if NET8_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()); diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/CustomAuthorizerRestExample_RestAuthorizer_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/CustomAuthorizerRestExample_RestAuthorizer_Generated.g.cs index 29c7b4ada..98c620ef8 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/CustomAuthorizerRestExample_RestAuthorizer_Generated.g.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/CustomAuthorizerRestExample_RestAuthorizer_Generated.g.cs @@ -1,4 +1,4 @@ -// +// using System; using System.Linq; @@ -40,7 +40,7 @@ public CustomAuthorizerRestExample_RestAuthorizer_Generated() var authorizerValue = default(string); if (__request__.RequestContext?.Authorizer == null || __request__.RequestContext?.Authorizer.ContainsKey("theAuthKey") == false) { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER __context__.Logger.LogDebug("Authorizer attribute 'theAuthKey' was missing, returning unauthorized."); #else __context__.Logger.Log("Authorizer attribute 'theAuthKey' was missing, returning unauthorized."); @@ -64,7 +64,7 @@ public CustomAuthorizerRestExample_RestAuthorizer_Generated() } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER __context__.Logger.LogError(e, "Failed to convert authorizer attribute 'theAuthKey', returning unauthorized."); #else __context__.Logger.Log("Failed to convert authorizer attribute 'theAuthKey', returning unauthorized. Exception: " + e.ToString()); diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/CustomAuthorizerWithIHttpResultsExample_AuthorizerWithIHttpResults_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/CustomAuthorizerWithIHttpResultsExample_AuthorizerWithIHttpResults_Generated.g.cs index 2a99d1b8d..4e0adf6fc 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/CustomAuthorizerWithIHttpResultsExample_AuthorizerWithIHttpResults_Generated.g.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/CustomAuthorizerWithIHttpResultsExample_AuthorizerWithIHttpResults_Generated.g.cs @@ -41,7 +41,7 @@ public System.IO.Stream AuthorizerWithIHttpResults(Amazon.Lambda.APIGatewayEvent var userId = default(string); if (__request__.RequestContext?.Authorizer?.Lambda == null || __request__.RequestContext?.Authorizer?.Lambda.ContainsKey("userId") == false) { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER __context__.Logger.LogDebug("Authorizer attribute 'userId' was missing, returning unauthorized."); #else __context__.Logger.Log("Authorizer attribute 'userId' was missing, returning unauthorized."); @@ -68,7 +68,7 @@ public System.IO.Stream AuthorizerWithIHttpResults(Amazon.Lambda.APIGatewayEvent } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -#if NET6_0_OR_GREATER +#if NET8_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()); diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/DynamoDB/ValidDynamoDBEvents_ProcessMessagesAsync_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/DynamoDB/ValidDynamoDBEvents_ProcessMessagesAsync_Generated.g.cs new file mode 100644 index 000000000..986b51396 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/DynamoDB/ValidDynamoDBEvents_ProcessMessagesAsync_Generated.g.cs @@ -0,0 +1,57 @@ +// + +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using Amazon.Lambda.Core; + +namespace TestServerlessApp.DynamoDBEventExamples +{ + public class ValidDynamoDBEvents_ProcessMessagesAsync_Generated + { + private readonly ValidDynamoDBEvents validDynamoDBEvents; + 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 ValidDynamoDBEvents_ProcessMessagesAsync_Generated() + { + SetExecutionEnvironment(); + validDynamoDBEvents = new ValidDynamoDBEvents(); + 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. + /// Result of the Lambda function execution + public async System.Threading.Tasks.Task ProcessMessagesAsync(Amazon.Lambda.DynamoDBEvents.DynamoDBEvent __evnt__) + { + await validDynamoDBEvents.ProcessMessagesAsync(__evnt__); + } + + 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()); + } + } +} diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/DynamoDB/ValidDynamoDBEvents_ProcessMessages_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/DynamoDB/ValidDynamoDBEvents_ProcessMessages_Generated.g.cs new file mode 100644 index 000000000..814090041 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/DynamoDB/ValidDynamoDBEvents_ProcessMessages_Generated.g.cs @@ -0,0 +1,57 @@ +// + +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using Amazon.Lambda.Core; + +namespace TestServerlessApp.DynamoDBEventExamples +{ + public class ValidDynamoDBEvents_ProcessMessages_Generated + { + private readonly ValidDynamoDBEvents validDynamoDBEvents; + 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 ValidDynamoDBEvents_ProcessMessages_Generated() + { + SetExecutionEnvironment(); + validDynamoDBEvents = new ValidDynamoDBEvents(); + 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. + /// Result of the Lambda function execution + public void ProcessMessages(Amazon.Lambda.DynamoDBEvents.DynamoDBEvent __evnt__) + { + validDynamoDBEvents.ProcessMessages(__evnt__); + } + + 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()); + } + } +} diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/FunctionUrlExample_GetItems_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/FunctionUrlExample_GetItems_Generated.g.cs new file mode 100644 index 000000000..002cbfe60 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/FunctionUrlExample_GetItems_Generated.g.cs @@ -0,0 +1,98 @@ +// + +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 TestServerlessApp +{ + public class FunctionUrlExample_GetItems_Generated + { + private readonly FunctionUrlExample functionUrlExample; + 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 FunctionUrlExample_GetItems_Generated() + { + SetExecutionEnvironment(); + functionUrlExample = new FunctionUrlExample(); + serializer = new Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer(); + } + + /// + /// The generated Lambda function handler for + /// + /// The Function URL 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 GetItems(Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest __request__, Amazon.Lambda.Core.ILambdaContext __context__) + { + var validationErrors = new List(); + + var category = default(string); + if (__request__.QueryStringParameters?.ContainsKey("category") == true) + { + try + { + category = (string)Convert.ChangeType(__request__.QueryStringParameters["category"], typeof(string)); + } + catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) + { + validationErrors.Add($"Value {__request__.QueryStringParameters["category"]} at 'category' failed to satisfy constraint: {e.Message}"); + } + } + + // 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 = functionUrlExample.GetItems(category, __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()); + } + } +} diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/IAuthorizerResultExample_SimpleHttpApiAuthorizer_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/IAuthorizerResultExample_SimpleHttpApiAuthorizer_Generated.g.cs index 274071032..515fae050 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/IAuthorizerResultExample_SimpleHttpApiAuthorizer_Generated.g.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/IAuthorizerResultExample_SimpleHttpApiAuthorizer_Generated.g.cs @@ -45,7 +45,7 @@ public System.IO.Stream SimpleHttpApiAuthorizer(Amazon.Lambda.APIGatewayEvents.A } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER __context__.Logger.LogError(e, "Failed to extract header 'Authorization'."); #else __context__.Logger.Log("Failed to extract header 'Authorization'. Exception: " + e.ToString()); diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/IAuthorizerResultExample_SimpleRestApiAuthorizer_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/IAuthorizerResultExample_SimpleRestApiAuthorizer_Generated.g.cs index 015545556..3d70b27f6 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/IAuthorizerResultExample_SimpleRestApiAuthorizer_Generated.g.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/IAuthorizerResultExample_SimpleRestApiAuthorizer_Generated.g.cs @@ -45,7 +45,7 @@ public System.IO.Stream SimpleRestApiAuthorizer(Amazon.Lambda.APIGatewayEvents.A } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER __context__.Logger.LogError(e, "Failed to extract authorization token."); #else __context__.Logger.Log("Failed to extract authorization token. Exception: " + e.ToString()); diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ParameterlessTaskMethods_NoParameterTask_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ParameterlessTaskMethods_NoParameterTask_Generated.g.cs new file mode 100644 index 000000000..7c1d8fe74 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ParameterlessTaskMethods_NoParameterTask_Generated.g.cs @@ -0,0 +1,56 @@ +// + +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using Amazon.Lambda.Core; + +namespace TestServerlessApp +{ + public class ParameterlessTaskMethods_NoParameterTask_Generated + { + private readonly ParameterlessTaskMethods parameterlessTaskMethods; + 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 ParameterlessTaskMethods_NoParameterTask_Generated() + { + SetExecutionEnvironment(); + parameterlessTaskMethods = new ParameterlessTaskMethods(); + serializer = new Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer(); + } + + /// + /// The generated Lambda function handler for + /// + /// Result of the Lambda function execution + public async System.Threading.Tasks.Task NoParameterTask(Stream stream) + { + await parameterlessTaskMethods.NoParameterTask(); + } + + 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()); + } + } +} diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProgramParameterlessTask.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProgramParameterlessTask.g.cs new file mode 100644 index 000000000..2eec52323 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProgramParameterlessTask.g.cs @@ -0,0 +1,30 @@ +// + +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using Amazon.Lambda.Core; + +namespace TestServerlessApp; + +public class GeneratedProgram +{ + /// + /// This is responsible for inspecting the 'ANNOTATIONS_HANDLER' environment variable and invoking the appropriate Lambda function handler. + /// + public static async Task Main(string[] args) + { + + switch (Environment.GetEnvironmentVariable("ANNOTATIONS_HANDLER")) + { + case "NoParameterTask": + Func noparametertask_handler = new TestServerlessApp.ParameterlessTaskMethods_NoParameterTask_Generated().NoParameterTask; + await Amazon.Lambda.RuntimeSupport.LambdaBootstrapBuilder.Create(noparametertask_handler).Build().RunAsync(); + break; + + } + } +} 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 index 62c34f770..cf0912e40 100644 --- 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 @@ -40,7 +40,7 @@ public Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse GetHttpApiV1UserIn var userId = default(string); if (__request__.RequestContext?.Authorizer == null || __request__.RequestContext?.Authorizer.ContainsKey("userId") == false) { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER __context__.Logger.LogDebug("Authorizer attribute 'userId' was missing, returning unauthorized."); #else __context__.Logger.Log("Authorizer attribute 'userId' was missing, returning unauthorized."); @@ -64,7 +64,7 @@ public Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse GetHttpApiV1UserIn } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -#if NET6_0_OR_GREATER +#if NET8_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()); @@ -84,7 +84,7 @@ public Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse GetHttpApiV1UserIn var email = default(string); if (__request__.RequestContext?.Authorizer == null || __request__.RequestContext?.Authorizer.ContainsKey("email") == false) { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER __context__.Logger.LogDebug("Authorizer attribute 'email' was missing, returning unauthorized."); #else __context__.Logger.Log("Authorizer attribute 'email' was missing, returning unauthorized."); @@ -108,7 +108,7 @@ public Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse GetHttpApiV1UserIn } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -#if NET6_0_OR_GREATER +#if NET8_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()); @@ -128,7 +128,7 @@ public Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse GetHttpApiV1UserIn var tenantId = default(string); if (__request__.RequestContext?.Authorizer == null || __request__.RequestContext?.Authorizer.ContainsKey("tenantId") == false) { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER __context__.Logger.LogDebug("Authorizer attribute 'tenantId' was missing, returning unauthorized."); #else __context__.Logger.Log("Authorizer attribute 'tenantId' was missing, returning unauthorized."); @@ -152,7 +152,7 @@ public Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse GetHttpApiV1UserIn } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -#if NET6_0_OR_GREATER +#if NET8_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()); 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 index b969dd378..133a2cb65 100644 --- 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 @@ -41,7 +41,7 @@ public System.IO.Stream GetIHttpResult(Amazon.Lambda.APIGatewayEvents.APIGateway var userId = default(string); if (__request__.RequestContext?.Authorizer?.Lambda == null || __request__.RequestContext?.Authorizer?.Lambda.ContainsKey("userId") == false) { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER __context__.Logger.LogDebug("Authorizer attribute 'userId' was missing, returning unauthorized."); #else __context__.Logger.Log("Authorizer attribute 'userId' was missing, returning unauthorized."); @@ -68,7 +68,7 @@ public System.IO.Stream GetIHttpResult(Amazon.Lambda.APIGatewayEvents.APIGateway } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -#if NET6_0_OR_GREATER +#if NET8_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()); @@ -91,7 +91,7 @@ public System.IO.Stream GetIHttpResult(Amazon.Lambda.APIGatewayEvents.APIGateway var email = default(string); if (__request__.RequestContext?.Authorizer?.Lambda == null || __request__.RequestContext?.Authorizer?.Lambda.ContainsKey("email") == false) { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER __context__.Logger.LogDebug("Authorizer attribute 'email' was missing, returning unauthorized."); #else __context__.Logger.Log("Authorizer attribute 'email' was missing, returning unauthorized."); @@ -118,7 +118,7 @@ public System.IO.Stream GetIHttpResult(Amazon.Lambda.APIGatewayEvents.APIGateway } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -#if NET6_0_OR_GREATER +#if NET8_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()); 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 index 10301f09c..fc339b197 100644 --- 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 @@ -40,7 +40,7 @@ public Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse GetNonStr var tenantId = default(int); if (__request__.RequestContext?.Authorizer?.Lambda == null || __request__.RequestContext?.Authorizer?.Lambda.ContainsKey("numericTenantId") == false) { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER __context__.Logger.LogDebug("Authorizer attribute 'numericTenantId' was missing, returning unauthorized."); #else __context__.Logger.Log("Authorizer attribute 'numericTenantId' was missing, returning unauthorized."); @@ -64,7 +64,7 @@ public Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse GetNonStr } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -#if NET6_0_OR_GREATER +#if NET8_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()); @@ -84,7 +84,7 @@ public Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse GetNonStr var isAdmin = default(bool); if (__request__.RequestContext?.Authorizer?.Lambda == null || __request__.RequestContext?.Authorizer?.Lambda.ContainsKey("isAdmin") == false) { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER __context__.Logger.LogDebug("Authorizer attribute 'isAdmin' was missing, returning unauthorized."); #else __context__.Logger.Log("Authorizer attribute 'isAdmin' was missing, returning unauthorized."); @@ -108,7 +108,7 @@ public Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse GetNonStr } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -#if NET6_0_OR_GREATER +#if NET8_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()); @@ -128,7 +128,7 @@ public Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse GetNonStr var score = default(double); if (__request__.RequestContext?.Authorizer?.Lambda == null || __request__.RequestContext?.Authorizer?.Lambda.ContainsKey("score") == false) { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER __context__.Logger.LogDebug("Authorizer attribute 'score' was missing, returning unauthorized."); #else __context__.Logger.Log("Authorizer attribute 'score' was missing, returning unauthorized."); @@ -152,7 +152,7 @@ public Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse GetNonStr } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -#if NET6_0_OR_GREATER +#if NET8_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()); 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 index 0ae3d3107..fcf4fd4ea 100644 --- 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 @@ -40,7 +40,7 @@ public Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse GetRestUserInfo(Am var userId = default(string); if (__request__.RequestContext?.Authorizer == null || __request__.RequestContext?.Authorizer.ContainsKey("userId") == false) { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER __context__.Logger.LogDebug("Authorizer attribute 'userId' was missing, returning unauthorized."); #else __context__.Logger.Log("Authorizer attribute 'userId' was missing, returning unauthorized."); @@ -64,7 +64,7 @@ public Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse GetRestUserInfo(Am } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -#if NET6_0_OR_GREATER +#if NET8_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()); @@ -84,7 +84,7 @@ public Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse GetRestUserInfo(Am var email = default(string); if (__request__.RequestContext?.Authorizer == null || __request__.RequestContext?.Authorizer.ContainsKey("email") == false) { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER __context__.Logger.LogDebug("Authorizer attribute 'email' was missing, returning unauthorized."); #else __context__.Logger.Log("Authorizer attribute 'email' was missing, returning unauthorized."); @@ -108,7 +108,7 @@ public Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse GetRestUserInfo(Am } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -#if NET6_0_OR_GREATER +#if NET8_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()); @@ -128,7 +128,7 @@ public Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse GetRestUserInfo(Am var tenantId = default(string); if (__request__.RequestContext?.Authorizer == null || __request__.RequestContext?.Authorizer.ContainsKey("tenantId") == false) { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER __context__.Logger.LogDebug("Authorizer attribute 'tenantId' was missing, returning unauthorized."); #else __context__.Logger.Log("Authorizer attribute 'tenantId' was missing, returning unauthorized."); @@ -152,7 +152,7 @@ public Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse GetRestUserInfo(Am } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -#if NET6_0_OR_GREATER +#if NET8_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()); diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetSimpleHttpApiUserInfo_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetSimpleHttpApiUserInfo_Generated.g.cs index 784c96531..be1099135 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetSimpleHttpApiUserInfo_Generated.g.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetSimpleHttpApiUserInfo_Generated.g.cs @@ -40,7 +40,7 @@ public Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse GetSimple var userId = default(string); if (__request__.RequestContext?.Authorizer?.Lambda == null || __request__.RequestContext?.Authorizer?.Lambda.ContainsKey("userId") == false) { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER __context__.Logger.LogDebug("Authorizer attribute 'userId' was missing, returning unauthorized."); #else __context__.Logger.Log("Authorizer attribute 'userId' was missing, returning unauthorized."); @@ -64,7 +64,7 @@ public Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse GetSimple } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -#if NET6_0_OR_GREATER +#if NET8_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()); @@ -84,7 +84,7 @@ public Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse GetSimple var email = default(string); if (__request__.RequestContext?.Authorizer?.Lambda == null || __request__.RequestContext?.Authorizer?.Lambda.ContainsKey("email") == false) { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER __context__.Logger.LogDebug("Authorizer attribute 'email' was missing, returning unauthorized."); #else __context__.Logger.Log("Authorizer attribute 'email' was missing, returning unauthorized."); @@ -108,7 +108,7 @@ public Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse GetSimple } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -#if NET6_0_OR_GREATER +#if NET8_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()); @@ -128,7 +128,7 @@ public Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse GetSimple var tenantId = default(string); if (__request__.RequestContext?.Authorizer?.Lambda == null || __request__.RequestContext?.Authorizer?.Lambda.ContainsKey("tenantId") == false) { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER __context__.Logger.LogDebug("Authorizer attribute 'tenantId' was missing, returning unauthorized."); #else __context__.Logger.Log("Authorizer attribute 'tenantId' was missing, returning unauthorized."); @@ -152,7 +152,7 @@ public Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse GetSimple } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -#if NET6_0_OR_GREATER +#if NET8_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()); diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetSimpleRestApiUserInfo_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetSimpleRestApiUserInfo_Generated.g.cs index f81b75d86..6ac1d1b4a 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetSimpleRestApiUserInfo_Generated.g.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetSimpleRestApiUserInfo_Generated.g.cs @@ -40,7 +40,7 @@ public Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse GetSimpleRestApiUs var userId = default(string); if (__request__.RequestContext?.Authorizer == null || __request__.RequestContext?.Authorizer.ContainsKey("userId") == false) { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER __context__.Logger.LogDebug("Authorizer attribute 'userId' was missing, returning unauthorized."); #else __context__.Logger.Log("Authorizer attribute 'userId' was missing, returning unauthorized."); @@ -64,7 +64,7 @@ public Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse GetSimpleRestApiUs } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -#if NET6_0_OR_GREATER +#if NET8_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()); @@ -84,7 +84,7 @@ public Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse GetSimpleRestApiUs var email = default(string); if (__request__.RequestContext?.Authorizer == null || __request__.RequestContext?.Authorizer.ContainsKey("email") == false) { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER __context__.Logger.LogDebug("Authorizer attribute 'email' was missing, returning unauthorized."); #else __context__.Logger.Log("Authorizer attribute 'email' was missing, returning unauthorized."); @@ -108,7 +108,7 @@ public Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse GetSimpleRestApiUs } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -#if NET6_0_OR_GREATER +#if NET8_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()); @@ -128,7 +128,7 @@ public Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse GetSimpleRestApiUs var tenantId = default(string); if (__request__.RequestContext?.Authorizer == null || __request__.RequestContext?.Authorizer.ContainsKey("tenantId") == false) { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER __context__.Logger.LogDebug("Authorizer attribute 'tenantId' was missing, returning unauthorized."); #else __context__.Logger.Log("Authorizer attribute 'tenantId' was missing, returning unauthorized."); @@ -152,7 +152,7 @@ public Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse GetSimpleRestApiUs } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -#if NET6_0_OR_GREATER +#if NET8_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()); 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 index 469a54036..3cc64ec37 100644 --- 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 @@ -40,7 +40,7 @@ public Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse GetUserIn var userId = default(string); if (__request__.RequestContext?.Authorizer?.Lambda == null || __request__.RequestContext?.Authorizer?.Lambda.ContainsKey("userId") == false) { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER __context__.Logger.LogDebug("Authorizer attribute 'userId' was missing, returning unauthorized."); #else __context__.Logger.Log("Authorizer attribute 'userId' was missing, returning unauthorized."); @@ -64,7 +64,7 @@ public Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse GetUserIn } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -#if NET6_0_OR_GREATER +#if NET8_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()); @@ -84,7 +84,7 @@ public Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse GetUserIn var email = default(string); if (__request__.RequestContext?.Authorizer?.Lambda == null || __request__.RequestContext?.Authorizer?.Lambda.ContainsKey("email") == false) { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER __context__.Logger.LogDebug("Authorizer attribute 'email' was missing, returning unauthorized."); #else __context__.Logger.Log("Authorizer attribute 'email' was missing, returning unauthorized."); @@ -108,7 +108,7 @@ public Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse GetUserIn } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -#if NET6_0_OR_GREATER +#if NET8_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()); @@ -128,7 +128,7 @@ public Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse GetUserIn var tenantId = default(string); if (__request__.RequestContext?.Authorizer?.Lambda == null || __request__.RequestContext?.Authorizer?.Lambda.ContainsKey("tenantId") == false) { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER __context__.Logger.LogDebug("Authorizer attribute 'tenantId' was missing, returning unauthorized."); #else __context__.Logger.Log("Authorizer attribute 'tenantId' was missing, returning unauthorized."); @@ -152,7 +152,7 @@ public Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse GetUserIn } catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) { -#if NET6_0_OR_GREATER +#if NET8_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()); diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/SNS/ValidSNSEvents_ProcessMessagesAsync_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/SNS/ValidSNSEvents_ProcessMessagesAsync_Generated.g.cs new file mode 100644 index 000000000..3b854fa30 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/SNS/ValidSNSEvents_ProcessMessagesAsync_Generated.g.cs @@ -0,0 +1,57 @@ +// + +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using Amazon.Lambda.Core; + +namespace TestServerlessApp.SNSEventExamples +{ + public class ValidSNSEvents_ProcessMessagesAsync_Generated + { + private readonly ValidSNSEvents validSNSEvents; + 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 ValidSNSEvents_ProcessMessagesAsync_Generated() + { + SetExecutionEnvironment(); + validSNSEvents = new ValidSNSEvents(); + 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. + /// Result of the Lambda function execution + public async System.Threading.Tasks.Task ProcessMessagesAsync(Amazon.Lambda.SNSEvents.SNSEvent __evnt__) + { + await validSNSEvents.ProcessMessagesAsync(__evnt__); + } + + 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()); + } + } +} diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/SNS/ValidSNSEvents_ProcessMessages_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/SNS/ValidSNSEvents_ProcessMessages_Generated.g.cs new file mode 100644 index 000000000..08743c2c9 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/SNS/ValidSNSEvents_ProcessMessages_Generated.g.cs @@ -0,0 +1,57 @@ +// + +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using Amazon.Lambda.Core; + +namespace TestServerlessApp.SNSEventExamples +{ + public class ValidSNSEvents_ProcessMessages_Generated + { + private readonly ValidSNSEvents validSNSEvents; + 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 ValidSNSEvents_ProcessMessages_Generated() + { + SetExecutionEnvironment(); + validSNSEvents = new ValidSNSEvents(); + 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. + /// Result of the Lambda function execution + public void ProcessMessages(Amazon.Lambda.SNSEvents.SNSEvent __evnt__) + { + validSNSEvents.ProcessMessages(__evnt__); + } + + 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()); + } + } +} diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/Schedule/ValidScheduleEvents_ProcessScheduledEventAsync_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/Schedule/ValidScheduleEvents_ProcessScheduledEventAsync_Generated.g.cs new file mode 100644 index 000000000..53b9b43b0 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/Schedule/ValidScheduleEvents_ProcessScheduledEventAsync_Generated.g.cs @@ -0,0 +1,57 @@ +// + +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using Amazon.Lambda.Core; + +namespace TestServerlessApp.ScheduleEventExamples +{ + public class ValidScheduleEvents_ProcessScheduledEventAsync_Generated + { + private readonly ValidScheduleEvents validScheduleEvents; + 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 ValidScheduleEvents_ProcessScheduledEventAsync_Generated() + { + SetExecutionEnvironment(); + validScheduleEvents = new ValidScheduleEvents(); + 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. + /// Result of the Lambda function execution + public async System.Threading.Tasks.Task ProcessScheduledEventAsync(Amazon.Lambda.CloudWatchEvents.ScheduledEvents.ScheduledEvent __evnt__) + { + await validScheduleEvents.ProcessScheduledEventAsync(__evnt__); + } + + 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()); + } + } +} diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/Schedule/ValidScheduleEvents_ProcessScheduledEvent_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/Schedule/ValidScheduleEvents_ProcessScheduledEvent_Generated.g.cs new file mode 100644 index 000000000..fe9d08df4 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/Schedule/ValidScheduleEvents_ProcessScheduledEvent_Generated.g.cs @@ -0,0 +1,57 @@ +// + +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using Amazon.Lambda.Core; + +namespace TestServerlessApp.ScheduleEventExamples +{ + public class ValidScheduleEvents_ProcessScheduledEvent_Generated + { + private readonly ValidScheduleEvents validScheduleEvents; + 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 ValidScheduleEvents_ProcessScheduledEvent_Generated() + { + SetExecutionEnvironment(); + validScheduleEvents = new ValidScheduleEvents(); + 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. + /// Result of the Lambda function execution + public void ProcessScheduledEvent(Amazon.Lambda.CloudWatchEvents.ScheduledEvents.ScheduledEvent __evnt__) + { + validScheduleEvents.ProcessScheduledEvent(__evnt__); + } + + 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()); + } + } +} diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/albEvents.template b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/albEvents.template index 43133fd00..6d5d4dac4 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/albEvents.template +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/albEvents.template @@ -14,7 +14,7 @@ ] }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -67,7 +67,6 @@ "Tool": "Amazon.Lambda.Annotations" }, "Properties": { - "ListenerArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:listener/app/my-alb/abc/def", "Priority": 1, "Conditions": [ { @@ -86,7 +85,8 @@ "Ref": "ALBHelloWorldALBTargetGroup" } } - ] + ], + "ListenerArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:listener/app/my-alb/abc/def" } }, "ALBWithOptions": { @@ -100,7 +100,7 @@ ] }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -159,9 +159,6 @@ "Tool": "Amazon.Lambda.Annotations" }, "Properties": { - "ListenerArn": { - "Ref": "MyALBListener" - }, "Priority": 5, "Conditions": [ { @@ -196,7 +193,10 @@ "Ref": "ALBWithOptionsALBTargetGroup" } } - ] + ], + "ListenerArn": { + "Ref": "MyALBListener" + } } } } 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 76ca3e862..7298d7d01 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 @@ -111,7 +111,7 @@ "Tool": "Amazon.Lambda.Annotations" }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -128,7 +128,7 @@ "Tool": "Amazon.Lambda.Annotations" }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -145,7 +145,7 @@ "Tool": "Amazon.Lambda.Annotations" }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -162,7 +162,7 @@ "Tool": "Amazon.Lambda.Annotations" }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -179,7 +179,7 @@ "Tool": "Amazon.Lambda.Annotations" }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -207,7 +207,7 @@ } }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -250,7 +250,7 @@ } }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -292,7 +292,7 @@ } }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -332,7 +332,7 @@ } }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -376,7 +376,7 @@ } }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -420,7 +420,7 @@ } }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -463,7 +463,7 @@ } }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -506,7 +506,7 @@ } }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -549,7 +549,7 @@ } }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/dynamoDBEvents.template b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/dynamoDBEvents.template new file mode 100644 index 000000000..d5d5c8234 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/dynamoDBEvents.template @@ -0,0 +1,129 @@ +{ + "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": { + "TestServerlessAppDynamoDBEventExamplesValidDynamoDBEventsProcessMessagesGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "MyTable", + "MyTable2", + "testTableEvent" + ], + "SyncedEventProperties": { + "MyTable": [ + "Stream", + "StartingPosition", + "BatchSize", + "MaximumBatchingWindowInSeconds", + "FilterCriteria.Filters" + ], + "MyTable2": [ + "Stream", + "StartingPosition", + "Enabled" + ], + "testTableEvent": [ + "Stream.Fn::GetAtt", + "StartingPosition" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestProject::TestServerlessApp.DynamoDBEventExamples.ValidDynamoDBEvents_ProcessMessages_Generated::ProcessMessages" + ] + }, + "Events": { + "MyTable": { + "Type": "DynamoDB", + "Properties": { + "StartingPosition": "LATEST", + "BatchSize": 50, + "MaximumBatchingWindowInSeconds": 2, + "FilterCriteria": { + "Filters": [ + { + "Pattern": "My-Filter-1" + }, + { + "Pattern": "My-Filter-2" + } + ] + }, + "Stream": "arn:aws:dynamodb:us-east-2:444455556666:table/MyTable/stream/2024-01-01T00:00:00" + } + }, + "MyTable2": { + "Type": "DynamoDB", + "Properties": { + "StartingPosition": "TRIM_HORIZON", + "Enabled": false, + "Stream": "arn:aws:dynamodb:us-east-2:444455556666:table/MyTable2/stream/2024-01-01T00:00:00" + } + }, + "testTableEvent": { + "Type": "DynamoDB", + "Properties": { + "StartingPosition": "LATEST", + "Stream": { + "Fn::GetAtt": [ + "testTable", + "StreamArn" + ] + } + } + } + } + } + }, + "TestServerlessAppDynamoDBEventExamplesValidDynamoDBEventsProcessMessagesAsyncGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "MyTable" + ], + "SyncedEventProperties": { + "MyTable": [ + "Stream", + "StartingPosition" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestProject::TestServerlessApp.DynamoDBEventExamples.ValidDynamoDBEvents_ProcessMessagesAsync_Generated::ProcessMessagesAsync" + ] + }, + "Events": { + "MyTable": { + "Type": "DynamoDB", + "Properties": { + "StartingPosition": "LATEST", + "Stream": "arn:aws:dynamodb:us-east-2:444455556666:table/MyTable/stream/2024-01-01T00:00:00" + } + } + } + } + } + } +} diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/functionUrlExample.template b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/functionUrlExample.template new file mode 100644 index 000000000..7187fc6ad --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/functionUrlExample.template @@ -0,0 +1,31 @@ +{ + "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": { + "TestServerlessAppFunctionUrlExampleGetItemsGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedFunctionUrlConfig": true + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestProject::TestServerlessApp.FunctionUrlExample_GetItems_Generated::GetItems" + ] + }, + "FunctionUrlConfig": { + "AuthType": "NONE" + } + } + } + } +} diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/hostbuild.serverless.template b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/hostbuild.serverless.template index 94e4d7da9..e1e14bf92 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/hostbuild.serverless.template +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/hostbuild.serverless.template @@ -18,7 +18,7 @@ } }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/parameterlesstask.template b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/parameterlesstask.template new file mode 100644 index 000000000..9b8a018a2 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/parameterlesstask.template @@ -0,0 +1,29 @@ +{ + "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": { + "TestServerlessAppParameterlessTaskMethodsNoParameterTaskGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "Runtime": "provided.al2", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestProject", + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "NoParameterTask" + } + } + } + } + } +} diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/scheduleEvents.template b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/scheduleEvents.template new file mode 100644 index 000000000..0584bd007 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/scheduleEvents.template @@ -0,0 +1,83 @@ +{ + "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": { + "TestServerlessAppScheduleEventExamplesValidScheduleEventsProcessScheduledEventGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "rate5minutes" + ], + "SyncedEventProperties": { + "rate5minutes": [ + "Schedule", + "Description", + "Input" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestProject::TestServerlessApp.ScheduleEventExamples.ValidScheduleEvents_ProcessScheduledEvent_Generated::ProcessScheduledEvent" + ] + }, + "Events": { + "rate5minutes": { + "Type": "Schedule", + "Properties": { + "Schedule": "rate(5 minutes)", + "Description": "Runs every 5 minutes", + "Input": "{ \"key\": \"value\" }" + } + } + } + } + }, + "TestServerlessAppScheduleEventExamplesValidScheduleEventsProcessScheduledEventAsyncGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "DailyNoonSchedule" + ], + "SyncedEventProperties": { + "DailyNoonSchedule": [ + "Schedule" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestProject::TestServerlessApp.ScheduleEventExamples.ValidScheduleEvents_ProcessScheduledEventAsync_Generated::ProcessScheduledEventAsync" + ] + }, + "Events": { + "DailyNoonSchedule": { + "Type": "Schedule", + "Properties": { + "Schedule": "cron(0 12 * * ? *)" + } + } + } + } + } + } +} diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/snsEvents.template b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/snsEvents.template new file mode 100644 index 000000000..857bd3fd4 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/snsEvents.template @@ -0,0 +1,93 @@ +{ + "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": { + "TestServerlessAppSNSEventExamplesValidSNSEventsProcessMessagesGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "MyTopic", + "testTopicEvent" + ], + "SyncedEventProperties": { + "MyTopic": [ + "Topic", + "FilterPolicy" + ], + "testTopicEvent": [ + "Topic.Ref" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestProject::TestServerlessApp.SNSEventExamples.ValidSNSEvents_ProcessMessages_Generated::ProcessMessages" + ] + }, + "Events": { + "MyTopic": { + "Type": "SNS", + "Properties": { + "FilterPolicy": "{ \"store\": [\"example_corp\"] }", + "Topic": "arn:aws:sns:us-east-2:444455556666:MyTopic" + } + }, + "testTopicEvent": { + "Type": "SNS", + "Properties": { + "Topic": { + "Ref": "testTopic" + } + } + } + } + } + }, + "TestServerlessAppSNSEventExamplesValidSNSEventsProcessMessagesAsyncGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "MyTopic" + ], + "SyncedEventProperties": { + "MyTopic": [ + "Topic" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestProject::TestServerlessApp.SNSEventExamples.ValidSNSEvents_ProcessMessagesAsync_Generated::ProcessMessagesAsync" + ] + }, + "Events": { + "MyTopic": { + "Type": "SNS", + "Properties": { + "Topic": "arn:aws:sns:us-east-2:444455556666:MyTopic" + } + } + } + } + } + } +} diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/sqsEvents.template b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/sqsEvents.template index 7e2d1bfa2..3f004e48d 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/sqsEvents.template +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/sqsEvents.template @@ -51,7 +51,6 @@ "queue1": { "Type": "SQS", "Properties": { - "Queue": "arn:aws:sqs:us-east-2:444455556666:queue1", "BatchSize": 50, "FilterCriteria": { "Filters": [ @@ -66,15 +65,16 @@ "MaximumBatchingWindowInSeconds": 2, "ScalingConfig": { "MaximumConcurrency": 30 - } + }, + "Queue": "arn:aws:sqs:us-east-2:444455556666:queue1" } }, "queue2": { "Type": "SQS", "Properties": { - "Queue": "arn:aws:sqs:us-east-2:444455556666:queue2", "Enabled": false, - "MaximumBatchingWindowInSeconds": 5 + "MaximumBatchingWindowInSeconds": 5, + "Queue": "arn:aws:sqs:us-east-2:444455556666:queue2" } }, "myqueue": { @@ -164,14 +164,14 @@ "queue3": { "Type": "SQS", "Properties": { - "Queue": "arn:aws:sqs:us-east-2:444455556666:queue3", "FunctionResponseTypes": [ "ReportBatchItemFailures" - ] + ], + "Queue": "arn:aws:sqs:us-east-2:444455556666:queue3" } } } } } } -} \ No newline at end of file +} diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs index d14bb893f..dae6a0d69 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs @@ -1,3 +1,6 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + using System; using System.IO; using System.Text; @@ -264,7 +267,7 @@ public async Task TestInvalidGlobalRuntime_ShouldError() }, ExpectedDiagnostics = { - new DiagnosticResult("AWSLambda0112", DiagnosticSeverity.Error).WithMessage("The runtime selected in the Amazon.Lambda.Annotations.LambdaGlobalPropertiesAttribute is not a supported value. The valid values are: dotnet6, provided.al2, provided.al2023, dotnet8, dotnet10"), + new DiagnosticResult("AWSLambda0112", DiagnosticSeverity.Error).WithMessage("The runtime selected in the Amazon.Lambda.Annotations.LambdaGlobalPropertiesAttribute is not a supported value. The valid values are: provided.al2, provided.al2023, dotnet8, dotnet10"), } } }; @@ -276,6 +279,8 @@ public async Task TestInvalidGlobalRuntime_ShouldError() test.TestState.Sources.Add((file, await File.ReadAllTextAsync(file))); } + test.TestState.ExpectedDiagnostics.AddRange(GetExpectedRuntimeSupportDiagnostics()); + await test.RunAsync(); } @@ -372,6 +377,8 @@ public async Task VerifyExecutableAssemblyWithZipAndHandler() test.TestState.Sources.Add((file, await File.ReadAllTextAsync(file))); } + test.TestState.ExpectedDiagnostics.AddRange(GetExpectedRuntimeSupportDiagnostics()); + await test.RunAsync(); } @@ -431,6 +438,8 @@ public async Task VerifyExecutableAssembly() test.TestState.Sources.Add((file, await File.ReadAllTextAsync(file))); } + test.TestState.ExpectedDiagnostics.AddRange(GetExpectedRuntimeSupportDiagnostics()); + await test.RunAsync(); } @@ -487,6 +496,8 @@ public async Task VerifyExecutableAssemblyWithParameterlessConstructor() test.TestState.Sources.Add((file, await File.ReadAllTextAsync(file))); } + test.TestState.ExpectedDiagnostics.AddRange(GetExpectedRuntimeSupportDiagnostics()); + await test.RunAsync(); } @@ -543,6 +554,70 @@ public async Task VerifyExecutableAssemblyWithParameterlessConstructorAndRespons test.TestState.Sources.Add((file, await File.ReadAllTextAsync(file))); } + test.TestState.ExpectedDiagnostics.AddRange(GetExpectedRuntimeSupportDiagnostics()); + + await test.RunAsync(); + } + + /// + /// Regression test for https://github.com/aws/aws-lambda-dotnet/issues/1907. + /// A handler with no input and Task return type must generate an unambiguous Create call. + /// + [Fact] + public async Task VerifyExecutableAssemblyWithParameterlessTaskReturnConstructor() + { + var expectedTemplateContent = await ReadSnapshotContent(Path.Combine("Snapshots", "ServerlessTemplates", "parameterlesstask.template")); + var expectedSubNamespaceGenerated = await ReadSnapshotContent(Path.Combine("Snapshots", "ParameterlessTaskMethods_NoParameterTask_Generated.g.cs")); + var expectedProgramGenerated = await ReadSnapshotContent(Path.Combine("Snapshots", "ProgramParameterlessTask.g.cs")); + + var test = new VerifyCS.Test + { + TestState = + { + OutputKind = OutputKind.ConsoleApplication, + Sources = + { + (Path.Combine("TestExecutableServerlessApp", "ParameterlessTaskMethods.cs"), await File.ReadAllTextAsync(Path.Combine("TestExecutableServerlessApp", "ParameterlessTaskMethods.cs"))), + (Path.Combine("Amazon.Lambda.Annotations", "LambdaFunctionAttribute.cs"), await File.ReadAllTextAsync(Path.Combine("Amazon.Lambda.Annotations", "LambdaFunctionAttribute.cs"))), + (Path.Combine("Amazon.Lambda.Annotations", "LambdaGlobalPropertiesAttribute.cs"), await File.ReadAllTextAsync(Path.Combine("Amazon.Lambda.Annotations", "LambdaGlobalPropertiesAttribute.cs"))), + (Path.Combine("TestExecutableServerlessApp", "AssemblyAttributes.cs"), await File.ReadAllTextAsync(Path.Combine("TestExecutableServerlessApp", "AssemblyAttributes.cs"))), + }, + GeneratedSources = + { + ( + typeof(SourceGenerator.Generator), + "ParameterlessTaskMethods_NoParameterTask_Generated.g.cs", + SourceText.From(expectedSubNamespaceGenerated, Encoding.UTF8, SourceHashAlgorithm.Sha256) + ), + ( + typeof(SourceGenerator.Generator), + "Program.g.cs", + SourceText.From(expectedProgramGenerated, Encoding.UTF8, SourceHashAlgorithm.Sha256) + ) + }, + ExpectedDiagnostics = + { + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("ParameterlessTaskMethods_NoParameterTask_Generated.g.cs", expectedSubNamespaceGenerated), + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments($"TestExecutableServerlessApp{Path.DirectorySeparatorChar}serverless.template", expectedTemplateContent), + } + } + }; + + foreach (var file in Directory.GetFiles( + Path.Combine("Amazon.Lambda.RuntimeSupport"), + "*.cs", SearchOption.AllDirectories)) + { + var content = await File.ReadAllTextAsync(file); + + // Don't include RuntimeSupport's entry point. + if (file.EndsWith("Program.cs") && content.Contains("Task Main(string[] args)")) + continue; + + test.TestState.Sources.Add((file, await File.ReadAllTextAsync(file))); + } + + test.TestState.ExpectedDiagnostics.AddRange(GetExpectedRuntimeSupportDiagnostics()); + await test.RunAsync(); } @@ -646,6 +721,8 @@ public async Task VerifyExecutableAssemblyWithMultipleHandler() test.TestState.Sources.Add((file, await File.ReadAllTextAsync(file))); } + test.TestState.ExpectedDiagnostics.AddRange(GetExpectedRuntimeSupportDiagnostics()); + await test.RunAsync(); } @@ -709,6 +786,8 @@ public async Task VerifySourceGeneratorSerializerWithHttpResultsBody() test.TestState.Sources.Add((file, content)); } + test.TestState.ExpectedDiagnostics.AddRange(GetExpectedRuntimeSupportDiagnostics()); + await test.RunAsync(); } @@ -944,9 +1023,6 @@ public async Task CustomizeResponses() new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("CustomizeResponseExamples_NotFoundResponseWithHeaderV1Async_Generated.g.cs", expectedNotFoundResponseWithHeaderV1AsyncGenerated), new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("CustomizeResponseExamples_OkResponseWithCustomSerializer_Generated.g.cs", expectedOkResponseWithCustomSerializerGenerated), - // The test framework doesn't appear to also execute the System.Text.Json source generator so Annotations generated code relying on the generated System.Text.Json code does not exist - // so we get compile errors. In an real world scenario they are both run and the applicaton compiles correctly. - DiagnosticResult.CompilerError("CS0117").WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}CustomizeResponseExamples.cs", 99, 65, 99, 79).WithArguments("System.Text.Json.JsonNamingPolicy", "SnakeCaseUpper"), new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments($"TestServerlessApp{Path.DirectorySeparatorChar}serverless.template", expectedTemplateContent) } @@ -1178,7 +1254,7 @@ public async Task ToUpper_Net8() var expectedFunctionContent = await ReadSnapshotContent(Path.Combine("Snapshots", "Functions_ToUpper_Generated_NET8.g.cs")); var expectedTemplateContent = await ReadSnapshotContent(Path.Combine("Snapshots", "ServerlessTemplates", "net8.template")); - await new VerifyCS.Test(targetFramework: VerifyCS.Test.TargetFramework.Net80) + await new VerifyCS.Test(targetFramework: VerifyCS.Test.TargetFramework.Net8_0) { TestState = { @@ -1197,7 +1273,7 @@ public async Task ToUpper_Net8() ExpectedDiagnostics = { new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("Functions_ToUpper_Generated.g.cs", expectedFunctionContent), - new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments($"TestServerlessApp.NET8{Path.DirectorySeparatorChar}serverless.template", expectedTemplateContent) + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments($"TestServerlessApp.NET8{Path.DirectorySeparatorChar}serverless.template", expectedTemplateContent), } } }.RunAsync(); @@ -1224,59 +1300,59 @@ public async Task VerifyInvalidSQSEvents_ThrowsCompilationErrors() ExpectedDiagnostics = { DiagnosticResult.CompilerError("AWSLambda0116") - .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}SQSEventExamples{Path.DirectorySeparatorChar}InvalidSQSEvents.cs", 15, 9, 20, 10) + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}SQSEventExamples{Path.DirectorySeparatorChar}InvalidSQSEvents.cs", 18, 9, 23, 10) .WithArguments("BatchSize = 0. It must be between 1 and 10000"), DiagnosticResult.CompilerError("AWSLambda0116") - .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}SQSEventExamples{Path.DirectorySeparatorChar}InvalidSQSEvents.cs", 15, 9, 20, 10) + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}SQSEventExamples{Path.DirectorySeparatorChar}InvalidSQSEvents.cs", 18, 9, 23, 10) .WithArguments("MaximumBatchingWindowInSeconds = 302. It must be between 0 and 300"), DiagnosticResult.CompilerError("AWSLambda0116") - .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}SQSEventExamples{Path.DirectorySeparatorChar}InvalidSQSEvents.cs", 15, 9, 20, 10) + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}SQSEventExamples{Path.DirectorySeparatorChar}InvalidSQSEvents.cs", 18, 9, 23, 10) .WithArguments("MaximumConcurrency = 1. It must be between 2 and 1000"), DiagnosticResult.CompilerError("AWSLambda0117") - .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}SQSEventExamples{Path.DirectorySeparatorChar}InvalidSQSEvents.cs", 22, 9, 27, 10) + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}SQSEventExamples{Path.DirectorySeparatorChar}InvalidSQSEvents.cs", 25, 9, 30, 10) .WithArguments("When using the SQSEventAttribute, the Lambda method can accept at most 2 parameters. " + "The first parameter is required and must be of type Amazon.Lambda.SQSEvents.SQSEvent. " + "The second parameter is optional and must be of type Amazon.Lambda.Core.ILambdaContext."), DiagnosticResult.CompilerError("AWSLambda0117") - .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}SQSEventExamples{Path.DirectorySeparatorChar}InvalidSQSEvents.cs", 29, 9, 35, 10) + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}SQSEventExamples{Path.DirectorySeparatorChar}InvalidSQSEvents.cs", 32, 9, 38, 10) .WithArguments("When using the SQSEventAttribute, the Lambda method can return either " + "void, System.Threading.Tasks.Task, Amazon.Lambda.SQSEvents.SQSBatchResponse or Task"), DiagnosticResult .CompilerError("AWSLambda0102") - .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}SQSEventExamples{Path.DirectorySeparatorChar}InvalidSQSEvents.cs", 37, 9, 43, 10) + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}SQSEventExamples{Path.DirectorySeparatorChar}InvalidSQSEvents.cs", 40, 9, 46, 10) .WithMessage("Multiple event attributes on LambdaFunction are not supported"), DiagnosticResult.CompilerError("AWSLambda0116") - .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}SQSEventExamples{Path.DirectorySeparatorChar}InvalidSQSEvents.cs", 45, 9, 50, 10) + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}SQSEventExamples{Path.DirectorySeparatorChar}InvalidSQSEvents.cs", 48, 9, 53, 10) .WithArguments("Queue = test-queue. The SQS queue ARN is invalid. The ARN format is 'arn::sqs:::'"), DiagnosticResult.CompilerError("AWSLambda0116") - .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}SQSEventExamples{Path.DirectorySeparatorChar}InvalidSQSEvents.cs", 52, 9, 57, 10) + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}SQSEventExamples{Path.DirectorySeparatorChar}InvalidSQSEvents.cs", 55, 9, 60, 10) .WithArguments("ResourceName = sqs-event-source. It must only contain alphanumeric characters and must not be an empty string"), DiagnosticResult.CompilerError("AWSLambda0116") - .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}SQSEventExamples{Path.DirectorySeparatorChar}InvalidSQSEvents.cs", 59, 9, 64, 10) + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}SQSEventExamples{Path.DirectorySeparatorChar}InvalidSQSEvents.cs", 62, 9, 67, 10) .WithArguments("ResourceName = . It must only contain alphanumeric characters and must not be an empty string"), DiagnosticResult.CompilerError("AWSLambda0116") - .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}SQSEventExamples{Path.DirectorySeparatorChar}InvalidSQSEvents.cs", 66, 9, 71, 10) + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}SQSEventExamples{Path.DirectorySeparatorChar}InvalidSQSEvents.cs", 69, 9, 74, 10) .WithArguments("MaximumBatchingWindowInSeconds is not set or set to a value less than 1. It must be set to at least 1 when BatchSize is greater than 10"), DiagnosticResult.CompilerError("AWSLambda0116") - .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}SQSEventExamples{Path.DirectorySeparatorChar}InvalidSQSEvents.cs", 73, 9, 78, 10) + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}SQSEventExamples{Path.DirectorySeparatorChar}InvalidSQSEvents.cs", 76, 9, 81, 10) .WithArguments("MaximumBatchingWindowInSeconds is not set or set to a value less than 1. It must be set to at least 1 when BatchSize is greater than 10"), DiagnosticResult.CompilerError("AWSLambda0116") - .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}SQSEventExamples{Path.DirectorySeparatorChar}InvalidSQSEvents.cs", 80, 9, 85, 10) + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}SQSEventExamples{Path.DirectorySeparatorChar}InvalidSQSEvents.cs", 83, 9, 88, 10) .WithArguments("BatchSize = 100. It must be less than or equal to 10 when the event source mapping is for a FIFO queue"), DiagnosticResult.CompilerError("AWSLambda0116") - .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}SQSEventExamples{Path.DirectorySeparatorChar}InvalidSQSEvents.cs", 80, 9, 85, 10) + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}SQSEventExamples{Path.DirectorySeparatorChar}InvalidSQSEvents.cs", 83, 9, 88, 10) .WithArguments("MaximumBatchingWindowInSeconds must not be set when the event source mapping is for a FIFO queue") } } @@ -1324,16 +1400,16 @@ public async Task VerifyValidSQSEvents() ExpectedDiagnostics = { new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info) - .WithArguments("ValidSQSEvents_ProcessMessages_Generated.g.cs", validSqsEventsProcessMessagesGeneratedContent), + .WithArguments($"TestServerlessApp{Path.DirectorySeparatorChar}serverless.template", expectedTemplateContent), new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info) - .WithArguments("ValidSQSEvents_ProcessMessagesWithReservedParameterName_Generated.g.cs", validSqsEventsProcessMessagesWithReservedParameterNameGeneratedContent), + .WithArguments("ValidSQSEvents_ProcessMessagesWithBatchFailureReporting_Generated.g.cs", validSqsEventsProcessMessagesWithBatchFailureReportingGeneratedContent), new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info) - .WithArguments("ValidSQSEvents_ProcessMessagesWithBatchFailureReporting_Generated.g.cs", validSqsEventsProcessMessagesWithBatchFailureReportingGeneratedContent), + .WithArguments("ValidSQSEvents_ProcessMessagesWithReservedParameterName_Generated.g.cs", validSqsEventsProcessMessagesWithReservedParameterNameGeneratedContent), new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info) - .WithArguments($"TestServerlessApp{Path.DirectorySeparatorChar}serverless.template", expectedTemplateContent) + .WithArguments("ValidSQSEvents_ProcessMessages_Generated.g.cs", validSqsEventsProcessMessagesGeneratedContent) } } }.RunAsync(); @@ -1374,13 +1450,13 @@ public async Task VerifyValidALBEvents() ExpectedDiagnostics = { new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info) - .WithArguments("ValidALBEvents_Hello_Generated.g.cs", validALBEventsHelloGeneratedContent), + .WithArguments($"TestServerlessApp{Path.DirectorySeparatorChar}serverless.template", expectedTemplateContent), new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info) .WithArguments("ValidALBEvents_HandleRequest_Generated.g.cs", validALBEventsHandleRequestGeneratedContent), new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info) - .WithArguments($"TestServerlessApp{Path.DirectorySeparatorChar}serverless.template", expectedTemplateContent), + .WithArguments("ValidALBEvents_Hello_Generated.g.cs", validALBEventsHelloGeneratedContent), new DiagnosticResult("AWSLambda0133", DiagnosticSeverity.Error) } @@ -1388,6 +1464,321 @@ public async Task VerifyValidALBEvents() }.RunAsync(); } + [Fact] + public async Task VerifyInvalidDynamoDBEvents_ThrowsCompilationErrors() + { + await new VerifyCS.Test + { + TestState = + { + Sources = + { + (Path.Combine("TestServerlessApp", "PlaceholderClass.cs"), await File.ReadAllTextAsync(Path.Combine("TestServerlessApp", "PlaceholderClass.cs"))), + (Path.Combine("TestServerlessApp", "DynamoDBEventExamples", "InvalidDynamoDBEvents.cs"), await File.ReadAllTextAsync(Path.Combine("TestServerlessApp", "DynamoDBEventExamples", "InvalidDynamoDBEvents.cs.error"))), + (Path.Combine("Amazon.Lambda.Annotations", "LambdaFunctionAttribute.cs"), await File.ReadAllTextAsync(Path.Combine("Amazon.Lambda.Annotations", "LambdaFunctionAttribute.cs"))), + (Path.Combine("Amazon.Lambda.Annotations", "DynamoDB", "DynamoDBEventAttribute.cs"), await File.ReadAllTextAsync(Path.Combine("Amazon.Lambda.Annotations", "DynamoDB", "DynamoDBEventAttribute.cs"))), + (Path.Combine("Amazon.Lambda.Annotations", "DynamoDB", "StartingPosition.cs"), await File.ReadAllTextAsync(Path.Combine("Amazon.Lambda.Annotations", "DynamoDB", "StartingPosition.cs"))), + (Path.Combine("TestServerlessApp", "AssemblyAttributes.cs"), await File.ReadAllTextAsync(Path.Combine("TestServerlessApp", "AssemblyAttributes.cs"))), + }, + ExpectedDiagnostics = + { + // ProcessMessageWithInvalidDynamoDBEventAttributes: BatchSize = 10001 + DiagnosticResult.CompilerError("AWSLambda0137") + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}DynamoDBEventExamples{Path.DirectorySeparatorChar}InvalidDynamoDBEvents.cs", 17, 9, 22, 10) + .WithArguments("BatchSize = 10001. It must be between 1 and 10000"), + + // ProcessMessageWithInvalidDynamoDBEventAttributes: MaximumBatchingWindowInSeconds = 301 + DiagnosticResult.CompilerError("AWSLambda0137") + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}DynamoDBEventExamples{Path.DirectorySeparatorChar}InvalidDynamoDBEvents.cs", 17, 9, 22, 10) + .WithArguments("MaximumBatchingWindowInSeconds = 301. It must be between 0 and 300"), + + // ProcessMessageWithInvalidParameters: too many parameters + DiagnosticResult.CompilerError("AWSLambda0117") + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}DynamoDBEventExamples{Path.DirectorySeparatorChar}InvalidDynamoDBEvents.cs", 24, 9, 29, 10) + .WithArguments("When using the DynamoDBEventAttribute, the Lambda method can accept at most 2 parameters. " + + "The first parameter is required and must be of type Amazon.Lambda.DynamoDBEvents.DynamoDBEvent. " + + "The second parameter is optional and must be of type Amazon.Lambda.Core.ILambdaContext."), + + // ProcessMessageWithInvalidReturnType: returns bool + DiagnosticResult.CompilerError("AWSLambda0117") + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}DynamoDBEventExamples{Path.DirectorySeparatorChar}InvalidDynamoDBEvents.cs", 31, 9, 37, 10) + .WithArguments("When using the DynamoDBEventAttribute, the Lambda method can return either void or System.Threading.Tasks.Task"), + + // ProcessMessageWithMultipleEventTypes: multiple events + DiagnosticResult + .CompilerError("AWSLambda0102") + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}DynamoDBEventExamples{Path.DirectorySeparatorChar}InvalidDynamoDBEvents.cs", 39, 9, 45, 10) + .WithMessage("Multiple event attributes on LambdaFunction are not supported"), + + // ProcessMessageWithInvalidStreamArn: invalid ARN + DiagnosticResult.CompilerError("AWSLambda0137") + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}DynamoDBEventExamples{Path.DirectorySeparatorChar}InvalidDynamoDBEvents.cs", 47, 9, 52, 10) + .WithArguments("Stream = not-a-valid-arn. The DynamoDB stream ARN is invalid"), + + // ProcessMessageWithInvalidResourceName: invalid characters + DiagnosticResult.CompilerError("AWSLambda0137") + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}DynamoDBEventExamples{Path.DirectorySeparatorChar}InvalidDynamoDBEvents.cs", 54, 9, 59, 10) + .WithArguments("ResourceName = dynamo-event-source. It must only contain alphanumeric characters and must not be an empty string"), + + // ProcessMessageWithEmptyResourceName: empty string + DiagnosticResult.CompilerError("AWSLambda0137") + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}DynamoDBEventExamples{Path.DirectorySeparatorChar}InvalidDynamoDBEvents.cs", 61, 9, 66, 10) + .WithArguments("ResourceName = . It must only contain alphanumeric characters and must not be an empty string"), + } + } + }.RunAsync(); + } + + [Fact] + public async Task VerifyValidDynamoDBEvents() + { + var expectedTemplateContent = await ReadSnapshotContent(Path.Combine("Snapshots", "ServerlessTemplates", "dynamoDBEvents.template")); + var validDynamoDBEventsProcessMessagesGeneratedContent = await ReadSnapshotContent(Path.Combine("Snapshots", "DynamoDB", "ValidDynamoDBEvents_ProcessMessages_Generated.g.cs")); + var validDynamoDBEventsProcessMessagesAsyncGeneratedContent = await ReadSnapshotContent(Path.Combine("Snapshots", "DynamoDB", "ValidDynamoDBEvents_ProcessMessagesAsync_Generated.g.cs")); + + await new VerifyCS.Test + { + TestState = + { + Sources = + { + (Path.Combine("TestServerlessApp", "PlaceholderClass.cs"), await File.ReadAllTextAsync(Path.Combine("TestServerlessApp", "PlaceholderClass.cs"))), + (Path.Combine("TestServerlessApp", "DynamoDBEventExamples", "ValidDynamoDBEvents.cs"), await File.ReadAllTextAsync(Path.Combine("TestServerlessApp", "DynamoDBEventExamples", "ValidDynamoDBEvents.cs.txt"))), + (Path.Combine("Amazon.Lambda.Annotations", "LambdaFunctionAttribute.cs"), await File.ReadAllTextAsync(Path.Combine("Amazon.Lambda.Annotations", "LambdaFunctionAttribute.cs"))), + (Path.Combine("Amazon.Lambda.Annotations", "DynamoDB", "DynamoDBEventAttribute.cs"), await File.ReadAllTextAsync(Path.Combine("Amazon.Lambda.Annotations", "DynamoDB", "DynamoDBEventAttribute.cs"))), + (Path.Combine("Amazon.Lambda.Annotations", "DynamoDB", "StartingPosition.cs"), await File.ReadAllTextAsync(Path.Combine("Amazon.Lambda.Annotations", "DynamoDB", "StartingPosition.cs"))), + (Path.Combine("TestServerlessApp", "AssemblyAttributes.cs"), await File.ReadAllTextAsync(Path.Combine("TestServerlessApp", "AssemblyAttributes.cs"))), + }, + GeneratedSources = + { + ( + typeof(SourceGenerator.Generator), + "ValidDynamoDBEvents_ProcessMessages_Generated.g.cs", + SourceText.From(validDynamoDBEventsProcessMessagesGeneratedContent, Encoding.UTF8, SourceHashAlgorithm.Sha256) + ), + ( + typeof(SourceGenerator.Generator), + "ValidDynamoDBEvents_ProcessMessagesAsync_Generated.g.cs", + SourceText.From(validDynamoDBEventsProcessMessagesAsyncGeneratedContent, Encoding.UTF8, SourceHashAlgorithm.Sha256) + ) + }, + ExpectedDiagnostics = + { + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info) + .WithArguments($"TestServerlessApp{Path.DirectorySeparatorChar}serverless.template", expectedTemplateContent), + + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info) + .WithArguments("ValidDynamoDBEvents_ProcessMessagesAsync_Generated.g.cs", validDynamoDBEventsProcessMessagesAsyncGeneratedContent), + + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info) + .WithArguments("ValidDynamoDBEvents_ProcessMessages_Generated.g.cs", validDynamoDBEventsProcessMessagesGeneratedContent) + } + } + }.RunAsync(); + } + + [Fact] + public async Task VerifyInvalidSNSEvents_ThrowsCompilationErrors() + { + await new VerifyCS.Test + { + TestState = + { + Sources = + { + (Path.Combine("TestServerlessApp", "PlaceholderClass.cs"), await File.ReadAllTextAsync(Path.Combine("TestServerlessApp", "PlaceholderClass.cs"))), + (Path.Combine("TestServerlessApp", "SNSEventExamples", "InvalidSNSEvents.cs"), await File.ReadAllTextAsync(Path.Combine("TestServerlessApp", "SNSEventExamples", "InvalidSNSEvents.cs.error"))), + (Path.Combine("Amazon.Lambda.Annotations", "LambdaFunctionAttribute.cs"), await File.ReadAllTextAsync(Path.Combine("Amazon.Lambda.Annotations", "LambdaFunctionAttribute.cs"))), + (Path.Combine("Amazon.Lambda.Annotations", "SNS", "SNSEventAttribute.cs"), await File.ReadAllTextAsync(Path.Combine("Amazon.Lambda.Annotations", "SNS", "SNSEventAttribute.cs"))), + (Path.Combine("TestServerlessApp", "AssemblyAttributes.cs"), await File.ReadAllTextAsync(Path.Combine("TestServerlessApp", "AssemblyAttributes.cs"))), + }, + ExpectedDiagnostics = + { + // ProcessMessageWithInvalidTopicArn: invalid ARN + DiagnosticResult.CompilerError("AWSLambda0138") + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}SNSEventExamples{Path.DirectorySeparatorChar}InvalidSNSEvents.cs", 17, 9, 22, 10) + .WithArguments("Topic = not-a-valid-arn. The SNS topic ARN is invalid. The ARN format is 'arn::sns:::'"), + + // ProcessMessageWithInvalidParameters: too many parameters + DiagnosticResult.CompilerError("AWSLambda0117") + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}SNSEventExamples{Path.DirectorySeparatorChar}InvalidSNSEvents.cs", 24, 9, 29, 10) + .WithArguments("When using the SNSEventAttribute, the Lambda method can accept at most 2 parameters. " + + "The first parameter is required and must be of type Amazon.Lambda.SNSEvents.SNSEvent. " + + "The second parameter is optional and must be of type Amazon.Lambda.Core.ILambdaContext."), + + // ProcessMessageWithInvalidReturnType: returns bool + DiagnosticResult.CompilerError("AWSLambda0117") + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}SNSEventExamples{Path.DirectorySeparatorChar}InvalidSNSEvents.cs", 31, 9, 37, 10) + .WithArguments("When using the SNSEventAttribute, the Lambda method can return either void or System.Threading.Tasks.Task"), + + // ProcessMessageWithMultipleEventTypes: multiple events + DiagnosticResult + .CompilerError("AWSLambda0102") + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}SNSEventExamples{Path.DirectorySeparatorChar}InvalidSNSEvents.cs", 39, 9, 45, 10) + .WithMessage("Multiple event attributes on LambdaFunction are not supported"), + + // ProcessMessageWithInvalidResourceName: invalid characters + DiagnosticResult.CompilerError("AWSLambda0138") + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}SNSEventExamples{Path.DirectorySeparatorChar}InvalidSNSEvents.cs", 47, 9, 52, 10) + .WithArguments("ResourceName = sns-event-source. It must only contain alphanumeric characters and must not be an empty string"), + + // ProcessMessageWithEmptyResourceName: empty string + DiagnosticResult.CompilerError("AWSLambda0138") + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}SNSEventExamples{Path.DirectorySeparatorChar}InvalidSNSEvents.cs", 54, 9, 59, 10) + .WithArguments("ResourceName = . It must only contain alphanumeric characters and must not be an empty string"), + } + } + }.RunAsync(); + } + + [Fact] + public async Task VerifyValidSNSEvents() + { + var expectedTemplateContent = await ReadSnapshotContent(Path.Combine("Snapshots", "ServerlessTemplates", "snsEvents.template")); + var validSNSEventsProcessMessagesGeneratedContent = await ReadSnapshotContent(Path.Combine("Snapshots", "SNS", "ValidSNSEvents_ProcessMessages_Generated.g.cs")); + var validSNSEventsProcessMessagesAsyncGeneratedContent = await ReadSnapshotContent(Path.Combine("Snapshots", "SNS", "ValidSNSEvents_ProcessMessagesAsync_Generated.g.cs")); + + await new VerifyCS.Test + { + TestState = + { + Sources = + { + (Path.Combine("TestServerlessApp", "PlaceholderClass.cs"), await File.ReadAllTextAsync(Path.Combine("TestServerlessApp", "PlaceholderClass.cs"))), + (Path.Combine("TestServerlessApp", "SNSEventExamples", "ValidSNSEvents.cs"), await File.ReadAllTextAsync(Path.Combine("TestServerlessApp", "SNSEventExamples", "ValidSNSEvents.cs.txt"))), + (Path.Combine("Amazon.Lambda.Annotations", "LambdaFunctionAttribute.cs"), await File.ReadAllTextAsync(Path.Combine("Amazon.Lambda.Annotations", "LambdaFunctionAttribute.cs"))), + (Path.Combine("Amazon.Lambda.Annotations", "SNS", "SNSEventAttribute.cs"), await File.ReadAllTextAsync(Path.Combine("Amazon.Lambda.Annotations", "SNS", "SNSEventAttribute.cs"))), + (Path.Combine("TestServerlessApp", "AssemblyAttributes.cs"), await File.ReadAllTextAsync(Path.Combine("TestServerlessApp", "AssemblyAttributes.cs"))), + }, + GeneratedSources = + { + ( + typeof(SourceGenerator.Generator), + "ValidSNSEvents_ProcessMessages_Generated.g.cs", + SourceText.From(validSNSEventsProcessMessagesGeneratedContent, Encoding.UTF8, SourceHashAlgorithm.Sha256) + ), + ( + typeof(SourceGenerator.Generator), + "ValidSNSEvents_ProcessMessagesAsync_Generated.g.cs", + SourceText.From(validSNSEventsProcessMessagesAsyncGeneratedContent, Encoding.UTF8, SourceHashAlgorithm.Sha256) + ) + }, + ExpectedDiagnostics = + { + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info) + .WithArguments($"TestServerlessApp{Path.DirectorySeparatorChar}serverless.template", expectedTemplateContent), + + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info) + .WithArguments("ValidSNSEvents_ProcessMessagesAsync_Generated.g.cs", validSNSEventsProcessMessagesAsyncGeneratedContent), + + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info) + .WithArguments("ValidSNSEvents_ProcessMessages_Generated.g.cs", validSNSEventsProcessMessagesGeneratedContent) + } + } + }.RunAsync(); + } + + [Fact] + public async Task VerifyValidScheduleEvents() + { + var expectedTemplateContent = await ReadSnapshotContent(Path.Combine("Snapshots", "ServerlessTemplates", "scheduleEvents.template")); + var validScheduleEventsProcessScheduledEventGeneratedContent = await ReadSnapshotContent(Path.Combine("Snapshots", "Schedule", "ValidScheduleEvents_ProcessScheduledEvent_Generated.g.cs")); + var validScheduleEventsProcessScheduledEventAsyncGeneratedContent = await ReadSnapshotContent(Path.Combine("Snapshots", "Schedule", "ValidScheduleEvents_ProcessScheduledEventAsync_Generated.g.cs")); + + await new VerifyCS.Test + { + TestState = + { + Sources = + { + (Path.Combine("TestServerlessApp", "PlaceholderClass.cs"), await File.ReadAllTextAsync(Path.Combine("TestServerlessApp", "PlaceholderClass.cs"))), + (Path.Combine("TestServerlessApp", "ScheduleEventExamples", "ValidScheduleEvents.cs"), await File.ReadAllTextAsync(Path.Combine("TestServerlessApp", "ScheduleEventExamples", "ValidScheduleEvents.cs.txt"))), + (Path.Combine("Amazon.Lambda.Annotations", "LambdaFunctionAttribute.cs"), await File.ReadAllTextAsync(Path.Combine("Amazon.Lambda.Annotations", "LambdaFunctionAttribute.cs"))), + (Path.Combine("Amazon.Lambda.Annotations", "Schedule", "ScheduleEventAttribute.cs"), await File.ReadAllTextAsync(Path.Combine("Amazon.Lambda.Annotations", "Schedule", "ScheduleEventAttribute.cs"))), + (Path.Combine("TestServerlessApp", "AssemblyAttributes.cs"), await File.ReadAllTextAsync(Path.Combine("TestServerlessApp", "AssemblyAttributes.cs"))), + }, + GeneratedSources = + { + ( + typeof(SourceGenerator.Generator), + "ValidScheduleEvents_ProcessScheduledEvent_Generated.g.cs", + SourceText.From(validScheduleEventsProcessScheduledEventGeneratedContent, Encoding.UTF8, SourceHashAlgorithm.Sha256) + ), + ( + typeof(SourceGenerator.Generator), + "ValidScheduleEvents_ProcessScheduledEventAsync_Generated.g.cs", + SourceText.From(validScheduleEventsProcessScheduledEventAsyncGeneratedContent, Encoding.UTF8, SourceHashAlgorithm.Sha256) + ) + }, + ExpectedDiagnostics = + { + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info) + .WithArguments("ValidScheduleEvents_ProcessScheduledEvent_Generated.g.cs", validScheduleEventsProcessScheduledEventGeneratedContent), + + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info) + .WithArguments("ValidScheduleEvents_ProcessScheduledEventAsync_Generated.g.cs", validScheduleEventsProcessScheduledEventAsyncGeneratedContent), + + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info) + .WithArguments($"TestServerlessApp{Path.DirectorySeparatorChar}serverless.template", expectedTemplateContent) + } + } + }.RunAsync(); + } + + [Fact] + public async Task VerifyInvalidScheduleEvents_ThrowsCompilationErrors() + { + await new VerifyCS.Test + { + TestState = + { + Sources = + { + (Path.Combine("TestServerlessApp", "PlaceholderClass.cs"), await File.ReadAllTextAsync(Path.Combine("TestServerlessApp", "PlaceholderClass.cs"))), + (Path.Combine("TestServerlessApp", "ScheduleEventExamples", "InvalidScheduleEvents.cs"), await File.ReadAllTextAsync(Path.Combine("TestServerlessApp", "ScheduleEventExamples", "InvalidScheduleEvents.cs.error"))), + (Path.Combine("Amazon.Lambda.Annotations", "LambdaFunctionAttribute.cs"), await File.ReadAllTextAsync(Path.Combine("Amazon.Lambda.Annotations", "LambdaFunctionAttribute.cs"))), + (Path.Combine("Amazon.Lambda.Annotations", "Schedule", "ScheduleEventAttribute.cs"), await File.ReadAllTextAsync(Path.Combine("Amazon.Lambda.Annotations", "Schedule", "ScheduleEventAttribute.cs"))), + (Path.Combine("TestServerlessApp", "AssemblyAttributes.cs"), await File.ReadAllTextAsync(Path.Combine("TestServerlessApp", "AssemblyAttributes.cs"))), + }, + ExpectedDiagnostics = + { + // ProcessMessageWithInvalidScheduleExpression: invalid schedule expression + DiagnosticResult.CompilerError("AWSLambda0139") + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}ScheduleEventExamples{Path.DirectorySeparatorChar}InvalidScheduleEvents.cs", 17, 9, 22, 10) + .WithArguments("Schedule = every 5 minutes. It must start with 'rate(' or 'cron('"), + + // ProcessMessageWithInvalidParameters: too many parameters + DiagnosticResult.CompilerError("AWSLambda0117") + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}ScheduleEventExamples{Path.DirectorySeparatorChar}InvalidScheduleEvents.cs", 24, 9, 29, 10) + .WithArguments("When using the ScheduleEventAttribute, the Lambda method can accept at most 2 parameters. " + + "The first parameter is required and must be of type Amazon.Lambda.CloudWatchEvents.ScheduledEvents.ScheduledEvent. " + + "The second parameter is optional and must be of type Amazon.Lambda.Core.ILambdaContext."), + + // ProcessMessageWithInvalidReturnType: returns bool + DiagnosticResult.CompilerError("AWSLambda0117") + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}ScheduleEventExamples{Path.DirectorySeparatorChar}InvalidScheduleEvents.cs", 31, 9, 37, 10) + .WithArguments("When using the ScheduleEventAttribute, the Lambda method can return either void or System.Threading.Tasks.Task"), + + // ProcessMessageWithMultipleEventTypes: multiple events + DiagnosticResult + .CompilerError("AWSLambda0102") + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}ScheduleEventExamples{Path.DirectorySeparatorChar}InvalidScheduleEvents.cs", 39, 9, 45, 10) + .WithMessage("Multiple event attributes on LambdaFunction are not supported"), + + // ProcessMessageWithInvalidResourceName: invalid characters + DiagnosticResult.CompilerError("AWSLambda0139") + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}ScheduleEventExamples{Path.DirectorySeparatorChar}InvalidScheduleEvents.cs", 47, 9, 52, 10) + .WithArguments("ResourceName = invalid-name!. It must only contain alphanumeric characters and must not be an empty string"), + + // ProcessMessageWithEmptyResourceName: empty string + DiagnosticResult.CompilerError("AWSLambda0139") + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}ScheduleEventExamples{Path.DirectorySeparatorChar}InvalidScheduleEvents.cs", 54, 9, 59, 10) + .WithArguments("ResourceName = . It must only contain alphanumeric characters and must not be an empty string"), + } + } + }.RunAsync(); + } + [Fact] public async Task ExceededMaximumHandlerLength() { @@ -1480,7 +1871,6 @@ public async Task CustomAuthorizerRestTest() new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("CustomAuthorizerRestExample_RestAuthorizer_Generated.g.cs", expectedRestAuthorizerGenerated), new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments($"TestServerlessApp{Path.DirectorySeparatorChar}serverless.template", expectedTemplateContent) }, - ReferenceAssemblies = ReferenceAssemblies.Net.Net60 } }.RunAsync(); @@ -1519,7 +1909,6 @@ public async Task CustomAuthorizerHttpApiTest() new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("CustomAuthorizerHttpApiExample_HttpApiAuthorizer_Generated.g.cs", expectedRestAuthorizerGenerated), new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments($"TestServerlessApp{Path.DirectorySeparatorChar}serverless.template", expectedTemplateContent) }, - ReferenceAssemblies = ReferenceAssemblies.Net.Net60 } }.RunAsync(); @@ -1563,7 +1952,6 @@ public async Task CustomAuthorizerAttributeParameterNameFallbackTest() new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("AuthNameFallback_GetUserId_Generated.g.cs", expectedAuthorizerGenerated), new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments($"TestServerlessApp{Path.DirectorySeparatorChar}serverless.template", expectedTemplateContent) }, - ReferenceAssemblies = ReferenceAssemblies.Net.Net60 } }.RunAsync(); @@ -1602,7 +1990,6 @@ public async Task CustomAuthorizerHttpApiV1Test() new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("CustomAuthorizerHttpApiV1Example_HttpApiV1Authorizer_Generated.g.cs", expectedHttpApiV1AuthorizerGenerated), new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments($"TestServerlessApp{Path.DirectorySeparatorChar}serverless.template", expectedTemplateContent) }, - ReferenceAssemblies = ReferenceAssemblies.Net.Net60 } }.RunAsync(); @@ -1641,7 +2028,6 @@ public async Task CustomAuthorizerWithIHttpResultsTest() new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("CustomAuthorizerWithIHttpResultsExample_AuthorizerWithIHttpResults_Generated.g.cs", expectedAuthorizerGenerated), new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments($"TestServerlessApp{Path.DirectorySeparatorChar}serverless.template", expectedTemplateContent) }, - ReferenceAssemblies = ReferenceAssemblies.Net.Net60 } }.RunAsync(); @@ -1685,7 +2071,6 @@ public async Task CustomAuthorizerNonStringTest() new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("CustomAuthorizerNonStringExample_HttpApiWithNonString_Generated.g.cs", expectedAuthorizerGenerated), new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments($"TestServerlessApp{Path.DirectorySeparatorChar}serverless.template", expectedTemplateContent) }, - ReferenceAssemblies = ReferenceAssemblies.Net.Net60 } }.RunAsync(); @@ -1825,7 +2210,6 @@ public async Task CustomAuthorizerAppAuthorizerDefinitionsTest() new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("ProtectedFunction_HealthCheck_Generated.g.cs", expectedHealthCheckGenerated), new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments($"TestCustomAuthorizerApp{Path.DirectorySeparatorChar}serverless.template", expectedTemplateContent), }, - ReferenceAssemblies = ReferenceAssemblies.Net.Net60 } }.RunAsync(); @@ -1909,7 +2293,6 @@ public async Task IAuthorizerResultHttpApiTest() new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("IAuthorizerResultExample_SimpleRestApiAuthorizer_Generated.g.cs", expectedRestApiGenerated), new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments($"TestServerlessApp{Path.DirectorySeparatorChar}serverless.template", expectedTemplateContent), }, - ReferenceAssemblies = ReferenceAssemblies.Net.Net60 } }.RunAsync(); @@ -1917,6 +2300,44 @@ public async Task IAuthorizerResultHttpApiTest() Assert.Equal(expectedTemplateContent, actualTemplateContent); } + [Fact] + public async Task FunctionUrlExample() + { + var expectedTemplateContent = await ReadSnapshotContent(Path.Combine("Snapshots", "ServerlessTemplates", "functionUrlExample.template")); + var expectedGetItemsGenerated = await ReadSnapshotContent(Path.Combine("Snapshots", "FunctionUrlExample_GetItems_Generated.g.cs")); + + await new VerifyCS.Test + { + TestState = + { + Sources = + { + (Path.Combine("TestServerlessApp", "FunctionUrlExample.cs"), await File.ReadAllTextAsync(Path.Combine("TestServerlessApp", "FunctionUrlExample.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", "FunctionUrlAttribute.cs"), await File.ReadAllTextAsync(Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "FunctionUrlAttribute.cs"))), + (Path.Combine("TestServerlessApp", "AssemblyAttributes.cs"), await File.ReadAllTextAsync(Path.Combine("TestServerlessApp", "AssemblyAttributes.cs"))), + }, + GeneratedSources = + { + ( + typeof(SourceGenerator.Generator), + "FunctionUrlExample_GetItems_Generated.g.cs", + SourceText.From(expectedGetItemsGenerated, Encoding.UTF8, SourceHashAlgorithm.Sha256) + ) + }, + ExpectedDiagnostics = + { + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("FunctionUrlExample_GetItems_Generated.g.cs", expectedGetItemsGenerated), + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments($"TestServerlessApp{Path.DirectorySeparatorChar}serverless.template", expectedTemplateContent) + } + } + }.RunAsync(); + + var actualTemplateContent = await File.ReadAllTextAsync(Path.Combine("TestServerlessApp", "serverless.template")); + Assert.Equal(expectedTemplateContent, actualTemplateContent); + } + public void Dispose() { File.Delete(Path.Combine("TestServerlessApp", "serverless.template")); @@ -1934,14 +2355,51 @@ private async static Task ReadSnapshotContent(string snapshotPath, bool return content.ToEnvironmentLineEndings().ApplyReplacements(); } - private static string InvalidAssemblyAttributeString = "using Amazon.Lambda.Annotations;" + + private readonly static string InvalidAssemblyAttributeString = "using Amazon.Lambda.Annotations;" + "using Amazon.Lambda.Core;" + "[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]" + "[assembly: LambdaGlobalProperties(GenerateMain = true, Runtime = \"notavalidruntime\")]"; - private static string NullAssemblyAttributeString = "using Amazon.Lambda.Annotations;" + + private readonly static string NullAssemblyAttributeString = "using Amazon.Lambda.Annotations;" + "using Amazon.Lambda.Core;" + "[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]" + "[assembly: LambdaGlobalProperties(Runtime = null)]"; + + /// + /// Returns expected compiler diagnostics for RuntimeSupport source files included in test compilations. + /// In .NET 10, JsonSerializerContext has new abstract members and a required constructor parameter that + /// the System.Text.Json source generator would normally implement. Since only the Lambda Annotations + /// source generator runs in tests, these produce expected compiler errors. + /// + private static DiagnosticResult[] GetExpectedRuntimeSupportDiagnostics() + { + var runtimeApiContext = "Amazon.Lambda.RuntimeSupport.InternalRuntimeApiClient.RuntimeApiSerializationContext"; + var clientFile = $"Amazon.Lambda.RuntimeSupport{Path.DirectorySeparatorChar}Client{Path.DirectorySeparatorChar}InternalClientAdapted.cs"; + var snapFile = $"Amazon.Lambda.RuntimeSupport{Path.DirectorySeparatorChar}Helpers{Path.DirectorySeparatorChar}SnapstartHelperCopySnapshotCallbacksIsolated.cs"; + + return new[] + { + // These are here because the System.Text.Json source generator isn't included in test compilations, so these members aren't generated. + DiagnosticResult.CompilerError("CS0534").WithSpan(clientFile, 85, 30, 85, 60).WithArguments(runtimeApiContext, "System.Text.Json.Serialization.JsonSerializerContext.GeneratedSerializerOptions.get"), + DiagnosticResult.CompilerError("CS0534").WithSpan(clientFile, 85, 30, 85, 60).WithArguments(runtimeApiContext, "System.Text.Json.Serialization.JsonSerializerContext.GetTypeInfo(System.Type)"), + DiagnosticResult.CompilerError("CS7036").WithSpan(clientFile, 85, 30, 85, 60).WithArguments("options", "System.Text.Json.Serialization.JsonSerializerContext.JsonSerializerContext(System.Text.Json.JsonSerializerOptions?)"), + DiagnosticResult.CompilerError("CS0117").WithSpan(clientFile, 157, 136, 157, 143).WithArguments(runtimeApiContext, "Default"), + DiagnosticResult.CompilerError("CS0117").WithSpan(clientFile, 172, 135, 172, 142).WithArguments(runtimeApiContext, "Default"), + DiagnosticResult.CompilerError("CS0117").WithSpan(clientFile, 275, 131, 275, 138).WithArguments(runtimeApiContext, "Default"), + DiagnosticResult.CompilerError("CS0117").WithSpan(clientFile, 371, 135, 371, 142).WithArguments(runtimeApiContext, "Default"), + DiagnosticResult.CompilerError("CS0117").WithSpan(clientFile, 386, 135, 386, 142).WithArguments(runtimeApiContext, "Default"), + DiagnosticResult.CompilerError("CS0117").WithSpan(clientFile, 401, 135, 401, 142).WithArguments(runtimeApiContext, "Default"), + DiagnosticResult.CompilerError("CS0117").WithSpan(clientFile, 510, 136, 510, 143).WithArguments(runtimeApiContext, "Default"), + DiagnosticResult.CompilerError("CS0117").WithSpan(clientFile, 525, 135, 525, 142).WithArguments(runtimeApiContext, "Default"), + DiagnosticResult.CompilerError("CS0117").WithSpan(clientFile, 540, 135, 540, 142).WithArguments(runtimeApiContext, "Default"), + + + // These are here because the internalvisibleto attribute isn't included in test compilations, so these types are inaccessible. + DiagnosticResult.CompilerError("CS0117").WithSpan(snapFile, 13, 34, 13, 71).WithArguments("Amazon.Lambda.Core.SnapshotRestore", "CopyBeforeSnapshotCallbacksToRegistry"), + DiagnosticResult.CompilerError("CS0117").WithSpan(snapFile, 14, 34, 14, 69).WithArguments("Amazon.Lambda.Core.SnapshotRestore", "CopyAfterRestoreCallbacksToRegistry"), + DiagnosticResult.CompilerError("CS0122").WithSpan($"Amazon.Lambda.RuntimeSupport{Path.DirectorySeparatorChar}Bootstrap{Path.DirectorySeparatorChar}ResponseStreaming{Path.DirectorySeparatorChar}ResponseStreamLambdaCoreInitializerIsolated.cs", 37, 51, 37, 72).WithArguments("Amazon.Lambda.Core.ResponseStreaming.ILambdaResponseStream"), + DiagnosticResult.CompilerError("CS0117").WithSpan($"Amazon.Lambda.RuntimeSupport{Path.DirectorySeparatorChar}Helpers{Path.DirectorySeparatorChar}Logging{Path.DirectorySeparatorChar}ConfigureJsonLogMessageFormatterIsolated.cs", 13, 45, 13, 80).WithArguments("Amazon.Lambda.Core.LambdaLogger", "SetConfigureStructuredLoggingAction"), + }; + } } } diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/DynamoDBEventsTests.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/DynamoDBEventsTests.cs new file mode 100644 index 000000000..065d04d95 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/DynamoDBEventsTests.cs @@ -0,0 +1,357 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Lambda.Annotations.SourceGenerator; +using Amazon.Lambda.Annotations.SourceGenerator.Models; +using Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes; +using Amazon.Lambda.Annotations.SourceGenerator.Writers; +using Amazon.Lambda.Annotations.DynamoDB; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Amazon.Lambda.Annotations.SourceGenerators.Tests.WriterTests +{ + public partial class CloudFormationWriterTests + { + const string streamArn1 = "arn:aws:dynamodb:us-east-2:444455556666:table/MyTable/stream/2024-01-01T00:00:00"; + const string streamArn2 = "arn:aws:dynamodb:us-east-2:444455556666:table/MyTable2/stream/2024-01-01T00:00:00"; + + [Theory] + [ClassData(typeof(DynamoDBEventsTestData))] + public void VerifyDynamoDBEventAttributes_AreCorrectlyApplied(CloudFormationTemplateFormat templateFormat, IEnumerable dynamoDBEventAttributes) + { + // ARRANGE + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel(); + lambdaFunctionModel.PackageType = LambdaPackageType.Zip; + lambdaFunctionModel.ReturnTypeFullName = "void"; + foreach (var att in dynamoDBEventAttributes) + { + lambdaFunctionModel.Attributes.Add(new AttributeModel { Data = att }); + } + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport([lambdaFunctionModel]); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + foreach (var att in dynamoDBEventAttributes) + { + var eventName = att.ResourceName; + var eventPath = $"Resources.{lambdaFunctionModel.ResourceName}.Properties.Events.{eventName}"; + var eventPropertiesPath = $"{eventPath}.Properties"; + + Assert.True(templateWriter.Exists(eventPath)); + Assert.Equal("DynamoDB", templateWriter.GetToken($"{eventPath}.Type")); + + if (!att.Stream.StartsWith("@")) + { + Assert.Equal(att.Stream, templateWriter.GetToken($"{eventPropertiesPath}.Stream")); + } + else + { + Assert.Equal([att.Stream.Substring(1), "StreamArn"], templateWriter.GetToken>($"{eventPropertiesPath}.Stream.Fn::GetAtt")); + } + + Assert.Equal(att.StartingPosition.ToString(), templateWriter.GetToken($"{eventPropertiesPath}.StartingPosition")); + + Assert.Equal(att.IsBatchSizeSet, templateWriter.Exists($"{eventPropertiesPath}.BatchSize")); + if (att.IsBatchSizeSet) + { + Assert.Equal(att.BatchSize, templateWriter.GetToken($"{eventPropertiesPath}.BatchSize")); + } + + Assert.Equal(att.IsEnabledSet, templateWriter.Exists($"{eventPropertiesPath}.Enabled")); + if (att.IsEnabledSet) + { + Assert.Equal(att.Enabled, templateWriter.GetToken($"{eventPropertiesPath}.Enabled")); + } + + Assert.Equal(att.IsFiltersSet, templateWriter.Exists($"{eventPropertiesPath}.FilterCriteria")); + if (att.IsFiltersSet) + { + var filtersList = templateWriter.GetToken>>($"{eventPropertiesPath}.FilterCriteria.Filters"); + var index = 0; + foreach (var filter in att.Filters.Split(';').Select(x => x.Trim())) + { + Assert.Equal(filter, filtersList[index]["Pattern"]); + index++; + } + } + + Assert.Equal(att.IsMaximumBatchingWindowInSecondsSet, templateWriter.Exists($"{eventPropertiesPath}.MaximumBatchingWindowInSeconds")); + if (att.IsMaximumBatchingWindowInSecondsSet) + { + Assert.Equal(att.MaximumBatchingWindowInSeconds, templateWriter.GetToken($"{eventPropertiesPath}.MaximumBatchingWindowInSeconds")); + } + } + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void VerifyDynamoDBEventProperties_AreSyncedCorrectly(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel(); + lambdaFunctionModel.PackageType = LambdaPackageType.Zip; + var eventResourceName = "MyDynamoDBEvent"; + var eventPropertiesPath = $"Resources.{lambdaFunctionModel.ResourceName}.Properties.Events.{eventResourceName}.Properties"; + var syncedEventPropertiesPath = $"Resources.{lambdaFunctionModel.ResourceName}.Metadata.SyncedEventProperties"; + + var initialAttribute = new DynamoDBEventAttribute(streamArn1) + { + ResourceName = eventResourceName, + MaximumBatchingWindowInSeconds = 15 + }; + lambdaFunctionModel.Attributes = [new AttributeModel { Data = initialAttribute }]; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport([lambdaFunctionModel]); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // Assert initial properties + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + Assert.Equal(15, templateWriter.GetToken($"{eventPropertiesPath}.MaximumBatchingWindowInSeconds")); + Assert.Equal(streamArn1, templateWriter.GetToken($"{eventPropertiesPath}.Stream")); + Assert.Equal("LATEST", templateWriter.GetToken($"{eventPropertiesPath}.StartingPosition")); + Assert.False(templateWriter.Exists($"{eventPropertiesPath}.BatchSize")); + Assert.False(templateWriter.Exists($"{eventPropertiesPath}.Enabled")); + Assert.False(templateWriter.Exists($"{eventPropertiesPath}.FilterCriteria")); + + var syncedEventProperties = templateWriter.GetToken>>($"{syncedEventPropertiesPath}"); + Assert.Equal(3, syncedEventProperties[eventResourceName].Count); + Assert.Contains("Stream", syncedEventProperties[eventResourceName]); + Assert.Contains("StartingPosition", syncedEventProperties[eventResourceName]); + Assert.Contains("MaximumBatchingWindowInSeconds", syncedEventProperties[eventResourceName]); + + // Update attribute + var updatedAttribute = new DynamoDBEventAttribute(streamArn2) + { + ResourceName = eventResourceName, + BatchSize = 10 + }; + lambdaFunctionModel.Attributes = [new AttributeModel { Data = updatedAttribute }]; + report = GetAnnotationReport([lambdaFunctionModel]); + cloudFormationWriter.ApplyReport(report); + + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + Assert.Equal(10, templateWriter.GetToken($"{eventPropertiesPath}.BatchSize")); + Assert.Equal(streamArn2, templateWriter.GetToken($"{eventPropertiesPath}.Stream")); + Assert.Equal("LATEST", templateWriter.GetToken($"{eventPropertiesPath}.StartingPosition")); + Assert.False(templateWriter.Exists($"{eventPropertiesPath}.MaximumBatchingWindowInSeconds")); + Assert.False(templateWriter.Exists($"{eventPropertiesPath}.Enabled")); + + syncedEventProperties = templateWriter.GetToken>>($"{syncedEventPropertiesPath}"); + Assert.Equal(3, syncedEventProperties[eventResourceName].Count); + Assert.Contains("BatchSize", syncedEventProperties[eventResourceName]); + Assert.Contains("Stream", syncedEventProperties[eventResourceName]); + Assert.Contains("StartingPosition", syncedEventProperties[eventResourceName]); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void SwitchBetweenArnAndRef_ForDynamoDBStream(CloudFormationTemplateFormat templateFormat) + { + // Arrange + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + var mockFileManager = GetMockFileManager(string.Empty); + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + + var lambdaFunctionModel = GetLambdaFunctionModel(); + var eventResourceName = "MyDynamoDBEvent"; + + var syncedEventPropertiesPath = $"Resources.{lambdaFunctionModel.ResourceName}.Metadata.SyncedEventProperties"; + var eventPropertiesPath = $"Resources.{lambdaFunctionModel.ResourceName}.Properties.Events.{eventResourceName}.Properties"; + + // Start with Stream ARN + var dynamoDBEventAttribute = new DynamoDBEventAttribute(streamArn1) { ResourceName = eventResourceName }; + lambdaFunctionModel.Attributes = [new AttributeModel { Data = dynamoDBEventAttribute }]; + + // Act + var report = GetAnnotationReport([lambdaFunctionModel]); + cloudFormationWriter.ApplyReport(report); + + // Assert - Stream as ARN + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + var syncedEventProperties = templateWriter.GetToken>>($"{syncedEventPropertiesPath}"); + + Assert.Equal(streamArn1, templateWriter.GetToken($"{eventPropertiesPath}.Stream")); + Assert.False(templateWriter.Exists($"{eventPropertiesPath}.Stream.Fn::GetAtt")); + Assert.Contains("Stream", syncedEventProperties[eventResourceName]); + + // Switch to Stream reference + dynamoDBEventAttribute.Stream = "@MyTable"; + cloudFormationWriter.ApplyReport(report); + + // Assert - Stream as Ref + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + syncedEventProperties = templateWriter.GetToken>>($"{syncedEventPropertiesPath}"); + + Assert.Equal(["MyTable", "StreamArn"], templateWriter.GetToken>($"{eventPropertiesPath}.Stream.Fn::GetAtt")); + Assert.Contains("Stream.Fn::GetAtt", syncedEventProperties[eventResourceName]); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void VerifyStreamCanBeSet_FromCloudFormationParameter(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE + const string jsonContent = @"{ + 'Parameters':{ + 'MyTable':{ + 'Type':'String', + 'Default':'arn:aws:dynamodb:us-east-2:444455556666:table/MyTable/stream/2024-01-01T00:00:00' + } + } + }"; + + const string yamlContent = @"Parameters: + MyTable: + Type: String + Default: arn:aws:dynamodb:us-east-2:444455556666:table/MyTable/stream/2024-01-01T00:00:00"; + + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + var content = templateFormat == CloudFormationTemplateFormat.Json ? jsonContent : yamlContent; + + var mockFileManager = GetMockFileManager(content); + var lambdaFunctionModel = GetLambdaFunctionModel(); + var eventResourceName = "MyDynamoDBEvent"; + var dynamoDBEventAttribute = new DynamoDBEventAttribute("@MyTable") { ResourceName = eventResourceName }; + lambdaFunctionModel.Attributes = [new AttributeModel { Data = dynamoDBEventAttribute }]; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport([lambdaFunctionModel]); + + var eventPropertiesPath = $"Resources.{lambdaFunctionModel.ResourceName}.Properties.Events.{eventResourceName}.Properties"; + var syncedEventPropertiesPath = $"Resources.{lambdaFunctionModel.ResourceName}.Metadata.SyncedEventProperties"; + + // ACT + cloudFormationWriter.ApplyReport(report); + + // Verify Stream property exists as a Ref (when @name matches a CF Parameter, writer uses Ref) + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + Assert.Equal("MyTable", templateWriter.GetToken($"{eventPropertiesPath}.Stream.Ref")); + Assert.False(templateWriter.Exists($"{eventPropertiesPath}.Stream.Fn::GetAtt")); + + // Verify the list of synced event properties + var syncedEventProperties = templateWriter.GetToken>>($"{syncedEventPropertiesPath}"); + Assert.Contains("Stream.Ref", syncedEventProperties[eventResourceName]); + + // Change the Stream property to be an ARN and re-generate the template + dynamoDBEventAttribute.Stream = streamArn1; + lambdaFunctionModel.Attributes = [new AttributeModel { Data = dynamoDBEventAttribute }]; + report = GetAnnotationReport([lambdaFunctionModel]); + cloudFormationWriter.ApplyReport(report); + + // Verify Stream property exists as an ARN + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + Assert.Equal(streamArn1, templateWriter.GetToken($"{eventPropertiesPath}.Stream")); + Assert.False(templateWriter.Exists($"{eventPropertiesPath}.Stream.Fn::GetAtt")); + Assert.False(templateWriter.Exists($"{eventPropertiesPath}.Stream.Ref")); + + // Verify the list of synced event properties + syncedEventProperties = templateWriter.GetToken>>($"{syncedEventPropertiesPath}"); + Assert.Contains("Stream", syncedEventProperties[eventResourceName]); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void VerifyManuallySetDynamoDBEventProperties_ArePreserved(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel(); + lambdaFunctionModel.PackageType = LambdaPackageType.Zip; + var eventResourceName = "MyDynamoDBEvent"; + var eventPropertiesPath = $"Resources.{lambdaFunctionModel.ResourceName}.Properties.Events.{eventResourceName}.Properties"; + var syncedEventPropertiesPath = $"Resources.{lambdaFunctionModel.ResourceName}.Metadata.SyncedEventProperties"; + + var initialAttribute = new DynamoDBEventAttribute(streamArn1) + { + ResourceName = eventResourceName, + BatchSize = 20 + }; + lambdaFunctionModel.Attributes = [new AttributeModel { Data = initialAttribute }]; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport([lambdaFunctionModel]); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // Assert that initial attributes properties are correctly set + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + Assert.Equal(20, templateWriter.GetToken($"{eventPropertiesPath}.BatchSize")); + Assert.Equal(streamArn1, templateWriter.GetToken($"{eventPropertiesPath}.Stream")); + + // Verify initial attribute properties are synced + var syncedEventProperties = templateWriter.GetToken>>($"{syncedEventPropertiesPath}"); + Assert.Contains("BatchSize", syncedEventProperties[eventResourceName]); + Assert.Contains("Stream", syncedEventProperties[eventResourceName]); + + // Modify the serverless template by hand and add a new property + templateWriter.SetToken($"{eventPropertiesPath}.MaximumBatchingWindowInSeconds", 30); + mockFileManager.WriteAllText(ServerlessTemplateFilePath, templateWriter.GetContent()); + + // Perform another source generation + cloudFormationWriter.ApplyReport(report); + + // Assert that both the initial properties and the manually added property exists + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + Assert.Equal(20, templateWriter.GetToken($"{eventPropertiesPath}.BatchSize")); + Assert.Equal(streamArn1, templateWriter.GetToken($"{eventPropertiesPath}.Stream")); + Assert.Equal(30, templateWriter.GetToken($"{eventPropertiesPath}.MaximumBatchingWindowInSeconds")); + + // Assert that the synced event properties are still the same and the manually set property is not synced + syncedEventProperties = templateWriter.GetToken>>($"{syncedEventPropertiesPath}"); + Assert.Contains("BatchSize", syncedEventProperties[eventResourceName]); + Assert.Contains("Stream", syncedEventProperties[eventResourceName]); + Assert.DoesNotContain("MaximumBatchingWindowInSeconds", syncedEventProperties[eventResourceName]); + } + + public class DynamoDBEventsTestData : TheoryData> + { + public DynamoDBEventsTestData() + { + foreach (var templateFormat in new List { CloudFormationTemplateFormat.Json, CloudFormationTemplateFormat.Yaml }) + { + // Simple attribute + Add(templateFormat, [new(streamArn1)]); + + // Multiple DynamoDBEvent attributes + Add(templateFormat, [new(streamArn1), new(streamArn2)]); + + // Use table reference + Add(templateFormat, [new("@MyTable")]); + + // Specify filters + Add(templateFormat, [new(streamArn1) { Filters = "SOME-FILTER1; SOME-FILTER2" }]); + + // Explicitly specify all properties + Add(templateFormat, + [new(streamArn1) + { + BatchSize = 10, + Filters = "SOME-FILTER1; SOME-FILTER2", + MaximumBatchingWindowInSeconds = 15, + Enabled = false, + StartingPosition = StartingPosition.TRIM_HORIZON + }]); + } + } + } + } +} diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/FunctionUrlTests.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/FunctionUrlTests.cs new file mode 100644 index 000000000..18d5ce3a3 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/FunctionUrlTests.cs @@ -0,0 +1,408 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Collections.Generic; +using Amazon.Lambda.Annotations.APIGateway; +using Amazon.Lambda.Annotations.SourceGenerator; +using Amazon.Lambda.Annotations.SourceGenerator.Models; +using Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes; +using Amazon.Lambda.Annotations.SourceGenerator.Writers; +using Xunit; + +namespace Amazon.Lambda.Annotations.SourceGenerators.Tests.WriterTests +{ + public partial class CloudFormationWriterTests + { + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void FunctionUrlWithDefaultAuthType(CloudFormationTemplateFormat templateFormat) + { + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Handler", + "TestMethod", 30, 512, null, null); + lambdaFunctionModel.Attributes = new List + { + new AttributeModel { Data = new FunctionUrlAttribute() } + }; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport(new List { lambdaFunctionModel }); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + cloudFormationWriter.ApplyReport(report); + + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + Assert.Equal("NONE", templateWriter.GetToken("Resources.TestMethod.Properties.FunctionUrlConfig.AuthType")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void FunctionUrlWithIamAuth(CloudFormationTemplateFormat templateFormat) + { + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Handler", + "TestMethod", 30, 512, null, null); + lambdaFunctionModel.Attributes = new List + { + new AttributeModel + { + Data = new FunctionUrlAttribute { AuthType = FunctionUrlAuthType.AWS_IAM } + } + }; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport(new List { lambdaFunctionModel }); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + cloudFormationWriter.ApplyReport(report); + + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + Assert.Equal("AWS_IAM", templateWriter.GetToken("Resources.TestMethod.Properties.FunctionUrlConfig.AuthType")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void FunctionUrlDoesNotCreateEventEntry(CloudFormationTemplateFormat templateFormat) + { + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Handler", + "TestMethod", 30, 512, null, null); + lambdaFunctionModel.Attributes = new List + { + new AttributeModel { Data = new FunctionUrlAttribute() } + }; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport(new List { lambdaFunctionModel }); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + cloudFormationWriter.ApplyReport(report); + + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + Assert.True(templateWriter.Exists("Resources.TestMethod.Properties.FunctionUrlConfig")); + Assert.False(templateWriter.Exists("Resources.TestMethod.Properties.Events")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void FunctionUrlWithCors(CloudFormationTemplateFormat templateFormat) + { + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Handler", + "TestMethod", 30, 512, null, null); + lambdaFunctionModel.Attributes = new List + { + new AttributeModel + { + Data = new FunctionUrlAttribute + { + AuthType = FunctionUrlAuthType.NONE, + AllowOrigins = new[] { "https://example.com" }, + AllowMethods = new[] { LambdaHttpMethod.Get, LambdaHttpMethod.Post }, + AllowHeaders = new[] { "Content-Type", "Authorization" }, + AllowCredentials = true, + MaxAge = 3600 + } + } + }; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport(new List { lambdaFunctionModel }); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + cloudFormationWriter.ApplyReport(report); + + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + var corsPath = "Resources.TestMethod.Properties.FunctionUrlConfig.Cors"; + Assert.Equal(new List { "https://example.com" }, templateWriter.GetToken>($"{corsPath}.AllowOrigins")); + Assert.Equal(new List { "GET", "POST" }, templateWriter.GetToken>($"{corsPath}.AllowMethods")); + Assert.Equal(new List { "Content-Type", "Authorization" }, templateWriter.GetToken>($"{corsPath}.AllowHeaders")); + Assert.True(templateWriter.GetToken($"{corsPath}.AllowCredentials")); + Assert.Equal(3600, templateWriter.GetToken($"{corsPath}.MaxAge")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void FunctionUrlWithoutCorsDoesNotEmitCorsBlock(CloudFormationTemplateFormat templateFormat) + { + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Handler", + "TestMethod", 30, 512, null, null); + lambdaFunctionModel.Attributes = new List + { + new AttributeModel + { + Data = new FunctionUrlAttribute { AuthType = FunctionUrlAuthType.AWS_IAM } + } + }; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport(new List { lambdaFunctionModel }); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + cloudFormationWriter.ApplyReport(report); + + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + Assert.True(templateWriter.Exists("Resources.TestMethod.Properties.FunctionUrlConfig.AuthType")); + Assert.False(templateWriter.Exists("Resources.TestMethod.Properties.FunctionUrlConfig.Cors")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void FunctionUrlCorsRemovedWhenCorsCleared(CloudFormationTemplateFormat templateFormat) + { + // First pass: emit full CORS config + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Handler", + "TestMethod", 30, 512, null, null); + lambdaFunctionModel.Attributes = new List + { + new AttributeModel + { + Data = new FunctionUrlAttribute + { + AuthType = FunctionUrlAuthType.NONE, + AllowOrigins = new[] { "https://example.com" }, + AllowMethods = new[] { LambdaHttpMethod.Get, LambdaHttpMethod.Post }, + AllowHeaders = new[] { "Content-Type" }, + AllowCredentials = true, + MaxAge = 3600 + } + } + }; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport(new List { lambdaFunctionModel }); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + cloudFormationWriter.ApplyReport(report); + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + var corsPath = "Resources.TestMethod.Properties.FunctionUrlConfig.Cors"; + Assert.True(templateWriter.Exists(corsPath)); + Assert.Equal(new List { "https://example.com" }, templateWriter.GetToken>($"{corsPath}.AllowOrigins")); + Assert.True(templateWriter.GetToken($"{corsPath}.AllowCredentials")); + Assert.Equal(3600, templateWriter.GetToken($"{corsPath}.MaxAge")); + + // Second pass: clear all CORS properties (AllowOrigins=null, AllowCredentials=false, MaxAge=0) + lambdaFunctionModel.Attributes = new List + { + new AttributeModel + { + Data = new FunctionUrlAttribute { AuthType = FunctionUrlAuthType.NONE } + } + }; + cloudFormationWriter.ApplyReport(GetAnnotationReport(new List { lambdaFunctionModel })); + + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + Assert.Equal("NONE", templateWriter.GetToken("Resources.TestMethod.Properties.FunctionUrlConfig.AuthType")); + Assert.False(templateWriter.Exists(corsPath)); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void FunctionUrlCorsUpdatedBetweenPasses(CloudFormationTemplateFormat templateFormat) + { + // First pass: emit CORS with AllowOrigins and AllowMethods + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Handler", + "TestMethod", 30, 512, null, null); + lambdaFunctionModel.Attributes = new List + { + new AttributeModel + { + Data = new FunctionUrlAttribute + { + AuthType = FunctionUrlAuthType.NONE, + AllowOrigins = new[] { "https://example.com" }, + AllowMethods = new[] { LambdaHttpMethod.Get, LambdaHttpMethod.Post }, + AllowCredentials = true, + MaxAge = 3600 + } + } + }; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + cloudFormationWriter.ApplyReport(GetAnnotationReport(new List { lambdaFunctionModel })); + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + var corsPath = "Resources.TestMethod.Properties.FunctionUrlConfig.Cors"; + Assert.True(templateWriter.Exists($"{corsPath}.AllowOrigins")); + Assert.True(templateWriter.Exists($"{corsPath}.AllowMethods")); + Assert.True(templateWriter.Exists($"{corsPath}.AllowCredentials")); + Assert.True(templateWriter.Exists($"{corsPath}.MaxAge")); + + // Second pass: change to only AllowOrigins with a different value, remove everything else + lambdaFunctionModel.Attributes = new List + { + new AttributeModel + { + Data = new FunctionUrlAttribute + { + AuthType = FunctionUrlAuthType.NONE, + AllowOrigins = new[] { "https://other.com" } + } + } + }; + cloudFormationWriter.ApplyReport(GetAnnotationReport(new List { lambdaFunctionModel })); + + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + Assert.Equal(new List { "https://other.com" }, templateWriter.GetToken>($"{corsPath}.AllowOrigins")); + Assert.False(templateWriter.Exists($"{corsPath}.AllowMethods")); + Assert.False(templateWriter.Exists($"{corsPath}.AllowCredentials")); + Assert.False(templateWriter.Exists($"{corsPath}.MaxAge")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void FunctionUrlConfigRemovedWhenAttributeRemoved(CloudFormationTemplateFormat templateFormat) + { + // First pass: create FunctionUrlConfig + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Handler", + "TestMethod", 30, 512, null, null); + lambdaFunctionModel.Attributes = new List + { + new AttributeModel + { + Data = new FunctionUrlAttribute { AllowOrigins = new[] { "*" } } + } + }; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport(new List { lambdaFunctionModel }); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + cloudFormationWriter.ApplyReport(report); + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + Assert.True(templateWriter.Exists("Resources.TestMethod.Properties.FunctionUrlConfig")); + + // Second pass: remove the attribute, FunctionUrlConfig should be cleaned up + lambdaFunctionModel.Attributes = new List(); + cloudFormationWriter.ApplyReport(GetAnnotationReport(new List { lambdaFunctionModel })); + + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + Assert.False(templateWriter.Exists("Resources.TestMethod.Properties.FunctionUrlConfig")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void ManualFunctionUrlConfigPreservedWhenNoAttribute(CloudFormationTemplateFormat templateFormat) + { + // Simulate a template where FunctionUrlConfig was manually added (no SyncedFunctionUrlConfig metadata) + var content = templateFormat == CloudFormationTemplateFormat.Json + ? @"{ + 'AWSTemplateFormatVersion': '2010-09-09', + 'Transform': 'AWS::Serverless-2016-10-31', + 'Resources': { + 'TestMethod': { + 'Type': 'AWS::Serverless::Function', + 'Metadata': { + 'Tool': 'Amazon.Lambda.Annotations' + }, + 'Properties': { + 'Runtime': 'dotnet8', + 'CodeUri': '', + 'MemorySize': 512, + 'Timeout': 30, + 'Policies': ['AWSLambdaBasicExecutionRole'], + 'PackageType': 'Image', + 'ImageUri': '.', + 'ImageConfig': { 'Command': ['MyAssembly::MyNamespace.MyType::Handler'] }, + 'FunctionUrlConfig': { + 'AuthType': 'AWS_IAM' + } + } + } + } + }" + : "AWSTemplateFormatVersion: '2010-09-09'\nTransform: AWS::Serverless-2016-10-31\nResources:\n TestMethod:\n Type: AWS::Serverless::Function\n Metadata:\n Tool: Amazon.Lambda.Annotations\n Properties:\n Runtime: dotnet8\n CodeUri: ''\n MemorySize: 512\n Timeout: 30\n Policies:\n - AWSLambdaBasicExecutionRole\n PackageType: Image\n ImageUri: .\n ImageConfig:\n Command:\n - 'MyAssembly::MyNamespace.MyType::Handler'\n FunctionUrlConfig:\n AuthType: AWS_IAM"; + + var mockFileManager = GetMockFileManager(content); + var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Handler", + "TestMethod", 30, 512, null, null); + // No FunctionUrl attribute + lambdaFunctionModel.Attributes = new List(); + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport(new List { lambdaFunctionModel }); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + cloudFormationWriter.ApplyReport(report); + + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + // The manually-added FunctionUrlConfig should be preserved + Assert.True(templateWriter.Exists("Resources.TestMethod.Properties.FunctionUrlConfig")); + Assert.Equal("AWS_IAM", templateWriter.GetToken("Resources.TestMethod.Properties.FunctionUrlConfig.AuthType")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void FunctionUrlMetadataTrackedAndCleanedUp(CloudFormationTemplateFormat templateFormat) + { + // First pass: create FunctionUrlConfig via attribute + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Handler", + "TestMethod", 30, 512, null, null); + lambdaFunctionModel.Attributes = new List + { + new AttributeModel { Data = new FunctionUrlAttribute() } + }; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + cloudFormationWriter.ApplyReport(GetAnnotationReport(new List { lambdaFunctionModel })); + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + // Verify metadata is set + Assert.True(templateWriter.Exists("Resources.TestMethod.Properties.FunctionUrlConfig")); + Assert.True(templateWriter.GetToken("Resources.TestMethod.Metadata.SyncedFunctionUrlConfig")); + + // Second pass: remove the attribute + lambdaFunctionModel.Attributes = new List(); + cloudFormationWriter.ApplyReport(GetAnnotationReport(new List { lambdaFunctionModel })); + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + // Verify both FunctionUrlConfig and metadata are cleaned up + Assert.False(templateWriter.Exists("Resources.TestMethod.Properties.FunctionUrlConfig")); + Assert.False(templateWriter.Exists("Resources.TestMethod.Metadata.SyncedFunctionUrlConfig")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void SwitchFromFunctionUrlToHttpApi(CloudFormationTemplateFormat templateFormat) + { + // First pass: FunctionUrl + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Handler", + "TestMethod", 30, 512, null, null); + lambdaFunctionModel.Attributes = new List + { + new AttributeModel { Data = new FunctionUrlAttribute() } + }; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + cloudFormationWriter.ApplyReport(GetAnnotationReport(new List { lambdaFunctionModel })); + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + Assert.True(templateWriter.Exists("Resources.TestMethod.Properties.FunctionUrlConfig")); + + // Second pass: switch to HttpApi + lambdaFunctionModel.Attributes = new List + { + new AttributeModel + { + Data = new HttpApiAttribute(LambdaHttpMethod.Get, "/items") + } + }; + cloudFormationWriter.ApplyReport(GetAnnotationReport(new List { lambdaFunctionModel })); + + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + Assert.False(templateWriter.Exists("Resources.TestMethod.Properties.FunctionUrlConfig")); + Assert.True(templateWriter.Exists("Resources.TestMethod.Properties.Events.RootGet")); + Assert.Equal("HttpApi", templateWriter.GetToken("Resources.TestMethod.Properties.Events.RootGet.Type")); + } + } +} diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/InMemoryFileManager.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/InMemoryFileManager.cs index 18c550c05..b07655e36 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/InMemoryFileManager.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/InMemoryFileManager.cs @@ -20,7 +20,7 @@ public string ReadAllText(string path) public void WriteAllText(string path, string contents) => _cacheContent[path] = contents; - public bool Exists(string path) => throw new System.NotImplementedException(); + public bool Exists(string path) => _cacheContent.ContainsKey(path); public FileStream Create(string path) => throw new System.NotImplementedException(); } diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/SNSEventsTests.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/SNSEventsTests.cs new file mode 100644 index 000000000..caf7f94fd --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/SNSEventsTests.cs @@ -0,0 +1,317 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Lambda.Annotations.SourceGenerator; +using Amazon.Lambda.Annotations.SourceGenerator.Models; +using Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes; +using Amazon.Lambda.Annotations.SourceGenerator.Writers; +using Amazon.Lambda.Annotations.SNS; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Amazon.Lambda.Annotations.SourceGenerators.Tests.WriterTests +{ + public partial class CloudFormationWriterTests + { + const string topicArn1 = "arn:aws:sns:us-east-2:444455556666:topic1"; + const string topicArn2 = "arn:aws:sns:us-east-2:444455556666:topic2"; + + [Theory] + [ClassData(typeof(SnsEventsTestData))] + public void VerifySNSEventAttributes_AreCorrectlyApplied(CloudFormationTemplateFormat templateFormat, IEnumerable snsEventAttributes) + { + // ARRANGE + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel(); + lambdaFunctionModel.PackageType = LambdaPackageType.Zip; + foreach (var att in snsEventAttributes) + { + lambdaFunctionModel.Attributes.Add(new AttributeModel { Data = att }); + } + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport([lambdaFunctionModel]); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + foreach (var att in snsEventAttributes) + { + var eventName = att.ResourceName; + var eventPath = $"Resources.{lambdaFunctionModel.ResourceName}.Properties.Events.{eventName}"; + var eventPropertiesPath = $"{eventPath}.Properties"; + + Assert.True(templateWriter.Exists(eventPath)); + Assert.Equal("SNS", templateWriter.GetToken($"{eventPath}.Type")); + + if (!att.Topic.StartsWith("@")) + { + Assert.Equal(att.Topic, templateWriter.GetToken($"{eventPropertiesPath}.Topic")); + } + else + { + Assert.Equal(att.Topic.Substring(1), templateWriter.GetToken($"{eventPropertiesPath}.Topic.Ref")); + } + + Assert.Equal(att.IsFilterPolicySet, templateWriter.Exists($"{eventPropertiesPath}.FilterPolicy")); + if (att.IsFilterPolicySet) + { + Assert.Equal(att.FilterPolicy, templateWriter.GetToken($"{eventPropertiesPath}.FilterPolicy")); + } + + Assert.Equal(att.IsEnabledSet, templateWriter.Exists($"{eventPropertiesPath}.Enabled")); + if (att.IsEnabledSet) + { + Assert.Equal(att.Enabled, templateWriter.GetToken($"{eventPropertiesPath}.Enabled")); + } + } + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void VerifySNSEventProperties_AreSyncedCorrectly(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel(); + lambdaFunctionModel.PackageType = LambdaPackageType.Zip; + var eventResourceName = "MySNSEvent"; + var eventPropertiesPath = $"Resources.{lambdaFunctionModel.ResourceName}.Properties.Events.{eventResourceName}.Properties"; + var syncedEventPropertiesPath = $"Resources.{lambdaFunctionModel.ResourceName}.Metadata.SyncedEventProperties"; + + var initialAttribute = new SNSEventAttribute(topicArn1) + { + ResourceName = eventResourceName, + FilterPolicy = "{ \"store\": [\"example_corp\"] }" + }; + lambdaFunctionModel.Attributes = [new AttributeModel { Data = initialAttribute }]; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport([lambdaFunctionModel]); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + Assert.Equal(topicArn1, templateWriter.GetToken($"{eventPropertiesPath}.Topic")); + Assert.Equal("{ \"store\": [\"example_corp\"] }", templateWriter.GetToken($"{eventPropertiesPath}.FilterPolicy")); + Assert.False(templateWriter.Exists($"{eventPropertiesPath}.Enabled")); + + var syncedEventProperties = templateWriter.GetToken>>($"{syncedEventPropertiesPath}"); + Assert.Equal(2, syncedEventProperties[eventResourceName].Count); + Assert.Contains("Topic", syncedEventProperties[eventResourceName]); + Assert.Contains("FilterPolicy", syncedEventProperties[eventResourceName]); + + // Update attribute - remove FilterPolicy, add Enabled + var updatedAttribute = new SNSEventAttribute(topicArn2) + { + ResourceName = eventResourceName, + Enabled = false + }; + lambdaFunctionModel.Attributes = [new AttributeModel { Data = updatedAttribute }]; + report = GetAnnotationReport([lambdaFunctionModel]); + + cloudFormationWriter.ApplyReport(report); + + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + Assert.Equal(topicArn2, templateWriter.GetToken($"{eventPropertiesPath}.Topic")); + Assert.False(templateWriter.Exists($"{eventPropertiesPath}.FilterPolicy")); + Assert.False(templateWriter.GetToken($"{eventPropertiesPath}.Enabled")); + + syncedEventProperties = templateWriter.GetToken>>($"{syncedEventPropertiesPath}"); + Assert.Equal(2, syncedEventProperties[eventResourceName].Count); + Assert.Contains("Topic", syncedEventProperties[eventResourceName]); + Assert.Contains("Enabled", syncedEventProperties[eventResourceName]); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void VerifySNSTopicCanBeSet_FromCloudFormationParameter(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE + const string jsonContent = @"{ + 'Parameters':{ + 'MyTopic':{ + 'Type':'String', + 'Default':'arn:aws:sns:us-east-2:444455556666:topic1' + } + } + }"; + + const string yamlContent = @"Parameters: + MyTopic: + Type: String + Default: arn:aws:sns:us-east-2:444455556666:topic1"; + + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + var content = templateFormat == CloudFormationTemplateFormat.Json ? jsonContent : yamlContent; + + var mockFileManager = GetMockFileManager(content); + var lambdaFunctionModel = GetLambdaFunctionModel(); + var eventResourceName = "MySNSEvent"; + var snsEventAttribute = new SNSEventAttribute("@MyTopic") { ResourceName = eventResourceName }; + lambdaFunctionModel.Attributes = [new AttributeModel { Data = snsEventAttribute }]; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport([lambdaFunctionModel]); + + var snsEventPropertiesPath = $"Resources.{lambdaFunctionModel.ResourceName}.Properties.Events.{eventResourceName}.Properties"; + var syncedEventPropertiesPath = $"Resources.{lambdaFunctionModel.ResourceName}.Metadata.SyncedEventProperties"; + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT - Topic uses Ref (SNS topics use Ref to get the ARN) + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + Assert.Equal("MyTopic", templateWriter.GetToken($"{snsEventPropertiesPath}.Topic.Ref")); + Assert.False(templateWriter.Exists($"{snsEventPropertiesPath}.Topic.Fn::GetAtt")); + + var syncedEventProperties = templateWriter.GetToken>>($"{syncedEventPropertiesPath}"); + Assert.Single(syncedEventProperties[eventResourceName]); + Assert.Equal("Topic.Ref", syncedEventProperties[eventResourceName][0]); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void SwitchBetweenArnAndRef_ForTopic(CloudFormationTemplateFormat templateFormat) + { + // Arrange + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + var mockFileManager = GetMockFileManager(string.Empty); + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + + var lambdaFunctionModel = GetLambdaFunctionModel(); + var eventResourceName = "MySNSEvent"; + + var syncedEventPropertiesPath = $"Resources.{lambdaFunctionModel.ResourceName}.Metadata.SyncedEventProperties"; + var snsEventPropertiesPath = $"Resources.{lambdaFunctionModel.ResourceName}.Properties.Events.{eventResourceName}.Properties"; + + // Start with Topic ARN + var snsEventAttribute = new SNSEventAttribute(topicArn1) { ResourceName = eventResourceName }; + lambdaFunctionModel.Attributes = [new AttributeModel { Data = snsEventAttribute }]; + + // Act + var report = GetAnnotationReport([lambdaFunctionModel]); + cloudFormationWriter.ApplyReport(report); + + // Assert - Topic is ARN + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + Assert.Equal(topicArn1, templateWriter.GetToken($"{snsEventPropertiesPath}.Topic")); + Assert.False(templateWriter.Exists($"{snsEventPropertiesPath}.Topic.Ref")); + + var syncedEventProperties = templateWriter.GetToken>>($"{syncedEventPropertiesPath}"); + Assert.Single(syncedEventProperties[eventResourceName]); + Assert.Equal("Topic", syncedEventProperties[eventResourceName][0]); + + // Switch to Topic reference + snsEventAttribute.Topic = "@MyTopic"; + cloudFormationWriter.ApplyReport(report); + + // Assert - Topic is Ref + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + Assert.Equal("MyTopic", templateWriter.GetToken($"{snsEventPropertiesPath}.Topic.Ref")); + + syncedEventProperties = templateWriter.GetToken>>($"{syncedEventPropertiesPath}"); + Assert.Single(syncedEventProperties[eventResourceName]); + Assert.Equal("Topic.Ref", syncedEventProperties[eventResourceName][0]); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void VerifyManuallySetSNSEventProperties_ArePreserved(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel(); + lambdaFunctionModel.PackageType = LambdaPackageType.Zip; + var eventResourceName = "MySNSEvent"; + var eventPropertiesPath = $"Resources.{lambdaFunctionModel.ResourceName}.Properties.Events.{eventResourceName}.Properties"; + var syncedEventPropertiesPath = $"Resources.{lambdaFunctionModel.ResourceName}.Metadata.SyncedEventProperties"; + + var initialAttribute = new SNSEventAttribute(topicArn1) + { + ResourceName = eventResourceName, + FilterPolicy = "{ \"store\": [\"example_corp\"] }" + }; + lambdaFunctionModel.Attributes = [new AttributeModel { Data = initialAttribute }]; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport([lambdaFunctionModel]); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // Assert that initial attributes properties are correctly set + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + Assert.Equal(topicArn1, templateWriter.GetToken($"{eventPropertiesPath}.Topic")); + Assert.Equal("{ \"store\": [\"example_corp\"] }", templateWriter.GetToken($"{eventPropertiesPath}.FilterPolicy")); + + // Verify initial attribute properties are synced + var syncedEventProperties = templateWriter.GetToken>>($"{syncedEventPropertiesPath}"); + Assert.Contains("Topic", syncedEventProperties[eventResourceName]); + Assert.Contains("FilterPolicy", syncedEventProperties[eventResourceName]); + + // Modify the serverless template by hand and add a new property (Enabled) + templateWriter.SetToken($"{eventPropertiesPath}.Enabled", false); + mockFileManager.WriteAllText(ServerlessTemplateFilePath, templateWriter.GetContent()); + + // Perform another source generation + cloudFormationWriter.ApplyReport(report); + + // Assert that both the initial properties and the manually added property exists + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + Assert.Equal(topicArn1, templateWriter.GetToken($"{eventPropertiesPath}.Topic")); + Assert.Equal("{ \"store\": [\"example_corp\"] }", templateWriter.GetToken($"{eventPropertiesPath}.FilterPolicy")); + Assert.False(templateWriter.GetToken($"{eventPropertiesPath}.Enabled")); + + // Assert that the synced event properties are still the same and the manually set property is not synced + syncedEventProperties = templateWriter.GetToken>>($"{syncedEventPropertiesPath}"); + Assert.Contains("Topic", syncedEventProperties[eventResourceName]); + Assert.Contains("FilterPolicy", syncedEventProperties[eventResourceName]); + Assert.DoesNotContain("Enabled", syncedEventProperties[eventResourceName]); + } + + public class SnsEventsTestData : TheoryData> + { + public SnsEventsTestData() + { + foreach (var templateFormat in new List { CloudFormationTemplateFormat.Json, CloudFormationTemplateFormat.Yaml }) + { + // Simple attribute + Add(templateFormat, [new(topicArn1)]); + + // Multiple SNSEvent attributes + Add(templateFormat, [new(topicArn1), new(topicArn2)]); + + // Use topic reference + Add(templateFormat, [new("@MyTopic")]); + + // Use both ARN and topic reference + Add(templateFormat, [new(topicArn1), new("@MyTopic")]); + + // Specify filter policy + Add(templateFormat, [new(topicArn1) { FilterPolicy = "{ \"store\": [\"example_corp\"] }" }]); + + // Explicitly specify all properties + Add(templateFormat, + [new(topicArn1) + { + FilterPolicy = "{ \"store\": [\"example_corp\"] }", + Enabled = false + }]); + } + } + } + } +} diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/SQSEventsTests.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/SQSEventsTests.cs index 94b5bb2b9..4ec53e4e0 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/SQSEventsTests.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/SQSEventsTests.cs @@ -1,4 +1,7 @@ -using Amazon.Lambda.Annotations.SourceGenerator; +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Lambda.Annotations.SourceGenerator; using Amazon.Lambda.Annotations.SourceGenerator.Models; using Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes; using Amazon.Lambda.Annotations.SourceGenerator.Writers; diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/ScheduleEventsTests.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/ScheduleEventsTests.cs new file mode 100644 index 000000000..a76a639d7 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/ScheduleEventsTests.cs @@ -0,0 +1,277 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Lambda.Annotations.SourceGenerator; +using Amazon.Lambda.Annotations.SourceGenerator.FileIO; +using Amazon.Lambda.Annotations.SourceGenerator.Models; +using Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes; +using Amazon.Lambda.Annotations.SourceGenerator.Writers; +using Amazon.Lambda.Annotations.Schedule; +using System.Collections.Generic; +using System.IO; +using Xunit; + +namespace Amazon.Lambda.Annotations.SourceGenerators.Tests.WriterTests +{ + public partial class CloudFormationWriterTests + { + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void VerifyScheduleEventAttributes_AreCorrectlyApplied(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel(); + lambdaFunctionModel.PackageType = LambdaPackageType.Zip; + + var att = new ScheduleEventAttribute("rate(5 minutes)") + { + ResourceName = "MySchedule", + Description = "Process every 5 minutes", + Input = "{\"key\": \"value\"}", + Enabled = true + }; + lambdaFunctionModel.Attributes.Add(new AttributeModel { Data = att }); + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport([lambdaFunctionModel]); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + var eventPath = $"Resources.{lambdaFunctionModel.ResourceName}.Properties.Events.MySchedule"; + var eventPropertiesPath = $"{eventPath}.Properties"; + + Assert.True(templateWriter.Exists(eventPath)); + Assert.Equal("Schedule", templateWriter.GetToken($"{eventPath}.Type")); + Assert.Equal("rate(5 minutes)", templateWriter.GetToken($"{eventPropertiesPath}.Schedule")); + Assert.Equal("Process every 5 minutes", templateWriter.GetToken($"{eventPropertiesPath}.Description")); + Assert.Equal("{\"key\": \"value\"}", templateWriter.GetToken($"{eventPropertiesPath}.Input")); + Assert.True(templateWriter.GetToken($"{eventPropertiesPath}.Enabled")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void VerifyScheduleEventProperties_AreSyncedCorrectly(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel(); + lambdaFunctionModel.PackageType = LambdaPackageType.Zip; + var eventResourceName = "MySchedule"; + var eventPropertiesPath = $"Resources.{lambdaFunctionModel.ResourceName}.Properties.Events.{eventResourceName}.Properties"; + var syncedEventPropertiesPath = $"Resources.{lambdaFunctionModel.ResourceName}.Metadata.SyncedEventProperties"; + + var initialAttribute = new ScheduleEventAttribute("rate(5 minutes)") + { + ResourceName = eventResourceName, + Description = "Every 5 minutes" + }; + lambdaFunctionModel.Attributes = [new AttributeModel { Data = initialAttribute }]; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport([lambdaFunctionModel]); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + Assert.Equal("rate(5 minutes)", templateWriter.GetToken($"{eventPropertiesPath}.Schedule")); + Assert.Equal("Every 5 minutes", templateWriter.GetToken($"{eventPropertiesPath}.Description")); + Assert.False(templateWriter.Exists($"{eventPropertiesPath}.Input")); + Assert.False(templateWriter.Exists($"{eventPropertiesPath}.Enabled")); + + var syncedEventProperties = templateWriter.GetToken>>($"{syncedEventPropertiesPath}"); + Assert.Equal(2, syncedEventProperties[eventResourceName].Count); + Assert.Contains("Schedule", syncedEventProperties[eventResourceName]); + Assert.Contains("Description", syncedEventProperties[eventResourceName]); + + // Update to cron with Input + var updatedAttribute = new ScheduleEventAttribute("cron(0 12 * * ? *)") + { + ResourceName = eventResourceName, + Input = "{\"type\": \"daily\"}" + }; + lambdaFunctionModel.Attributes = [new AttributeModel { Data = updatedAttribute }]; + report = GetAnnotationReport([lambdaFunctionModel]); + cloudFormationWriter.ApplyReport(report); + + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + Assert.Equal("cron(0 12 * * ? *)", templateWriter.GetToken($"{eventPropertiesPath}.Schedule")); + Assert.Equal("{\"type\": \"daily\"}", templateWriter.GetToken($"{eventPropertiesPath}.Input")); + Assert.False(templateWriter.Exists($"{eventPropertiesPath}.Description")); + + syncedEventProperties = templateWriter.GetToken>>($"{syncedEventPropertiesPath}"); + Assert.Equal(2, syncedEventProperties[eventResourceName].Count); + Assert.Contains("Schedule", syncedEventProperties[eventResourceName]); + Assert.Contains("Input", syncedEventProperties[eventResourceName]); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void VerifyScheduleEvent_MinimalAttributes(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel(); + lambdaFunctionModel.PackageType = LambdaPackageType.Zip; + + var att = new ScheduleEventAttribute("rate(1 hour)") { ResourceName = "HourlySchedule" }; + lambdaFunctionModel.Attributes.Add(new AttributeModel { Data = att }); + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport([lambdaFunctionModel]); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + var eventPropertiesPath = $"Resources.{lambdaFunctionModel.ResourceName}.Properties.Events.HourlySchedule.Properties"; + Assert.Equal("rate(1 hour)", templateWriter.GetToken($"{eventPropertiesPath}.Schedule")); + Assert.False(templateWriter.Exists($"{eventPropertiesPath}.Description")); + Assert.False(templateWriter.Exists($"{eventPropertiesPath}.Input")); + Assert.False(templateWriter.Exists($"{eventPropertiesPath}.Enabled")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void VerifyScheduleEventInput_RelativeFilePath_ReadsFileContents(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE - Set up a mock file manager with a JSON file at a relative path + var mockFileManager = GetMockFileManager(string.Empty); + var expectedJson = "{\"action\": \"cleanup\", \"target\": \"logs\"}"; + var inputFilePath = Path.Combine(ProjectRootDirectory, "schedule-input.json"); + mockFileManager.WriteAllText(inputFilePath, expectedJson); + + var lambdaFunctionModel = GetLambdaFunctionModel(); + lambdaFunctionModel.PackageType = LambdaPackageType.Zip; + + var att = new ScheduleEventAttribute("rate(1 hour)") + { + ResourceName = "HourlyCleanup", + Input = "schedule-input.json" + }; + lambdaFunctionModel.Attributes.Add(new AttributeModel { Data = att }); + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport([lambdaFunctionModel]); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT - The file contents should be used instead of the file path + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + var eventPropertiesPath = $"Resources.{lambdaFunctionModel.ResourceName}.Properties.Events.HourlyCleanup.Properties"; + Assert.Equal(expectedJson, templateWriter.GetToken($"{eventPropertiesPath}.Input")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void VerifyScheduleEventInput_AbsoluteFilePath_ReadsFileContents(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE - Set up a mock file manager with a JSON file at an absolute path + // Use Path.GetTempPath() to ensure the path is rooted on both Windows and Linux + var mockFileManager = GetMockFileManager(string.Empty); + var expectedJson = "{\"environment\": \"production\"}"; + var absoluteInputPath = Path.Combine(Path.GetTempPath(), "config", "schedule-input.json"); + mockFileManager.WriteAllText(absoluteInputPath, expectedJson); + + var lambdaFunctionModel = GetLambdaFunctionModel(); + lambdaFunctionModel.PackageType = LambdaPackageType.Zip; + + var att = new ScheduleEventAttribute("rate(5 minutes)") + { + ResourceName = "FrequentCheck", + Input = absoluteInputPath + }; + lambdaFunctionModel.Attributes.Add(new AttributeModel { Data = att }); + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport([lambdaFunctionModel]); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT - The file contents should be used instead of the file path + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + var eventPropertiesPath = $"Resources.{lambdaFunctionModel.ResourceName}.Properties.Events.FrequentCheck.Properties"; + Assert.Equal(expectedJson, templateWriter.GetToken($"{eventPropertiesPath}.Input")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void VerifyScheduleEventInput_LiteralJson_UsedAsIs(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE - Input is a literal JSON string, not a file path + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel(); + lambdaFunctionModel.PackageType = LambdaPackageType.Zip; + + var literalJson = "{\"key\": \"value\"}"; + var att = new ScheduleEventAttribute("rate(5 minutes)") + { + ResourceName = "LiteralInputSchedule", + Input = literalJson + }; + lambdaFunctionModel.Attributes.Add(new AttributeModel { Data = att }); + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport([lambdaFunctionModel]); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT - The literal JSON should be used as-is since it doesn't resolve to a file + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + var eventPropertiesPath = $"Resources.{lambdaFunctionModel.ResourceName}.Properties.Events.LiteralInputSchedule.Properties"; + Assert.Equal(literalJson, templateWriter.GetToken($"{eventPropertiesPath}.Input")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void VerifyScheduleEventInput_NonExistentFilePath_UsedAsIs(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE - Input looks like a file path but the file doesn't exist + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel(); + lambdaFunctionModel.PackageType = LambdaPackageType.Zip; + + var nonExistentPath = "does-not-exist.json"; + var att = new ScheduleEventAttribute("rate(5 minutes)") + { + ResourceName = "MissingFileSchedule", + Input = nonExistentPath + }; + lambdaFunctionModel.Attributes.Add(new AttributeModel { Data = att }); + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport([lambdaFunctionModel]); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT - The path string should be used as-is since the file doesn't exist + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + var eventPropertiesPath = $"Resources.{lambdaFunctionModel.ResourceName}.Properties.Events.MissingFileSchedule.Properties"; + Assert.Equal(nonExistentPath, templateWriter.GetToken($"{eventPropertiesPath}.Input")); + } + } +} diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/AddAWSLambdaBeforeSnapshotRequestTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/AddAWSLambdaBeforeSnapshotRequestTests.cs index b4419b1a7..3505d8bb3 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/AddAWSLambdaBeforeSnapshotRequestTests.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/AddAWSLambdaBeforeSnapshotRequestTests.cs @@ -14,7 +14,6 @@ namespace Amazon.Lambda.AspNetCoreServer.Hosting.Tests; /// public class AddAWSLambdaBeforeSnapshotRequestTests { - #if NET8_0_OR_GREATER [Theory] [InlineData(LambdaEventSource.HttpApi)] [InlineData(LambdaEventSource.RestApi)] @@ -55,5 +54,4 @@ await Task.WhenAny( Assert.True(callbackDidTheCallback); } - #endif } diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/Amazon.Lambda.AspNetCoreServer.Hosting.Tests.csproj b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/Amazon.Lambda.AspNetCoreServer.Hosting.Tests.csproj index 276bdd5c7..705fdabd5 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/Amazon.Lambda.AspNetCoreServer.Hosting.Tests.csproj +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/Amazon.Lambda.AspNetCoreServer.Hosting.Tests.csproj @@ -3,20 +3,23 @@ - net8.0 + net8.0;net10.0 enable enable true false false false - 1701;1702;1705;CS0618 + 1701;1702;1705;CS0618;CS1591 - - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/HostingOptionsTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/HostingOptionsTests.cs index 580095a2e..9214e9194 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/HostingOptionsTests.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/HostingOptionsTests.cs @@ -68,7 +68,7 @@ public void RegisterResponseContentEncodingForContentType_NullContentType_Ignore var options = new HostingOptions(); // Act - options.RegisterResponseContentEncodingForContentType(null, ResponseContentEncoding.Base64); + options.RegisterResponseContentEncodingForContentType(null!, ResponseContentEncoding.Base64); // Assert Assert.Empty(options.ContentTypeEncodings); @@ -144,7 +144,7 @@ public void RegisterResponseContentEncodingForContentEncoding_NullContentEncodin var options = new HostingOptions(); // Act - options.RegisterResponseContentEncodingForContentEncoding(null, ResponseContentEncoding.Base64); + options.RegisterResponseContentEncodingForContentEncoding(null!, ResponseContentEncoding.Base64); // Assert Assert.Empty(options.ContentEncodingEncodings); diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ResponseStreamingHostingTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ResponseStreamingHostingTests.cs new file mode 100644 index 000000000..f70f91629 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ResponseStreamingHostingTests.cs @@ -0,0 +1,254 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + + +using System.Runtime.Versioning; +using Amazon.Lambda.AspNetCoreServer.Hosting.Internal; +using Amazon.Lambda.AspNetCoreServer.Test; +using Amazon.Lambda.Core; +using Amazon.Lambda.RuntimeSupport; +using Amazon.Lambda.Serialization.SystemTextJson; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Amazon.Lambda.AspNetCoreServer.Hosting.Tests; + +/// +/// Tests for response streaming integration in hosting (Requirement 10). +/// +[RequiresPreviewFeatures] +public class ResponseStreamingHostingTests +{ + [Fact] + public void EnableResponseStreaming_DefaultsToFalse() + { + var options = new HostingOptions(); + Assert.False(options.EnableResponseStreaming); + } + + [Fact] + public void EnableResponseStreaming_CanBeSetToTrue() + { + var options = new HostingOptions { EnableResponseStreaming = true }; + Assert.True(options.EnableResponseStreaming); + } + + [Fact] + public void AddAWSLambdaHosting_ConfigureCallback_CanSetEnableResponseStreamingTrue() + { + var services = new ServiceCollection(); + using var envHelper = new EnvironmentVariableHelper("AWS_LAMBDA_FUNCTION_NAME", "test-function"); + +#pragma warning disable CA2252 + services.AddAWSLambdaHosting(LambdaEventSource.HttpApi, options => + { + options.EnableResponseStreaming = true; + }); +#pragma warning restore CA2252 + + var sp = services.BuildServiceProvider(); + var hostingOptions = sp.GetService(); + + Assert.NotNull(hostingOptions); + Assert.True(hostingOptions.EnableResponseStreaming); + } + + [Fact] + public void AddAWSLambdaHosting_WithoutCallback_EnableResponseStreamingRemainsDefault() + { + var services = new ServiceCollection(); + using var envHelper = new EnvironmentVariableHelper("AWS_LAMBDA_FUNCTION_NAME", "test-function"); + + services.AddAWSLambdaHosting(LambdaEventSource.HttpApi); + + var sp = services.BuildServiceProvider(); + var hostingOptions = sp.GetService(); + + Assert.NotNull(hostingOptions); + Assert.False(hostingOptions.EnableResponseStreaming); + } + + + // Helper: build a minimal IServiceProvider with the given HostingOptions + private static IServiceProvider BuildServiceProvider(HostingOptions hostingOptions) + { + var services = new ServiceCollection(); + services.AddSingleton(hostingOptions); + services.AddSingleton(new DefaultLambdaJsonSerializer()); + services.AddLogging(); + return services.BuildServiceProvider(); + } + + // ---- APIGatewayHttpApiV2 ---- + + [Fact] + public void HttpApiV2_CreateHandlerWrapper_StreamingFalse_TargetsFunctionHandlerAsync() + { + var options = new HostingOptions { EnableResponseStreaming = false }; + var sp = BuildServiceProvider(options); + + var server = new TestableHttpApiV2Server(sp); + var wrapper = server.PublicCreateHandlerWrapper(sp); + + // The handler delegate target method should be FunctionHandlerAsync (not streaming) + var methodName = GetHandlerDelegateMethodName(wrapper); + Assert.Contains("FunctionHandlerAsync", methodName); + Assert.DoesNotContain("Streaming", methodName); + } + + [Fact] + public void HttpApiV2_CreateHandlerWrapper_StreamingTrue_TargetsFunctionHandlerAsync() + { + var options = new HostingOptions { EnableResponseStreaming = true }; + var sp = BuildServiceProvider(options); + + var server = new TestableHttpApiV2Server(sp); + var wrapper = server.PublicCreateHandlerWrapper(sp); + + var methodName = GetHandlerDelegateMethodName(wrapper); + Assert.Contains("FunctionHandlerAsync", methodName); + } + + // ---- APIGatewayRestApi ---- + + [Fact] + public void RestApi_CreateHandlerWrapper_StreamingFalse_TargetsFunctionHandlerAsync() + { + var options = new HostingOptions { EnableResponseStreaming = false }; + var sp = BuildServiceProvider(options); + + var server = new TestableRestApiServer(sp); + var wrapper = server.PublicCreateHandlerWrapper(sp); + + var methodName = GetHandlerDelegateMethodName(wrapper); + Assert.Contains("FunctionHandlerAsync", methodName); + Assert.DoesNotContain("Streaming", methodName); + } + + [Fact] + public void RestApi_CreateHandlerWrapper_StreamingTrue_TargetsFunctionHandlerAsync() + { + var options = new HostingOptions { EnableResponseStreaming = true }; + var sp = BuildServiceProvider(options); + + var server = new TestableRestApiServer(sp); + var wrapper = server.PublicCreateHandlerWrapper(sp); + + var methodName = GetHandlerDelegateMethodName(wrapper); + Assert.Contains("FunctionHandlerAsync", methodName); + } + + // ---- ApplicationLoadBalancer ---- + + [Fact] + public void Alb_CreateHandlerWrapper_StreamingFalse_TargetsFunctionHandlerAsync() + { + var options = new HostingOptions { EnableResponseStreaming = false }; + var sp = BuildServiceProvider(options); + + var server = new TestableAlbServer(sp); + var wrapper = server.PublicCreateHandlerWrapper(sp); + + var methodName = GetHandlerDelegateMethodName(wrapper); + Assert.Contains("FunctionHandlerAsync", methodName); + Assert.DoesNotContain("Streaming", methodName); + } + + [Fact] + public void Alb_CreateHandlerWrapper_StreamingTrue_TargetsFunctionHandlerAsync() + { + var options = new HostingOptions { EnableResponseStreaming = true }; + var sp = BuildServiceProvider(options); + + var server = new TestableAlbServer(sp); + var wrapper = server.PublicCreateHandlerWrapper(sp); + + var methodName = GetHandlerDelegateMethodName(wrapper); + Assert.Contains("FunctionHandlerAsync", methodName); + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + /// + /// Extracts the method name from the delegate stored inside a HandlerWrapper. + /// HandlerWrapper.Handler is a LambdaBootstrapHandler (a delegate). The actual + /// user-supplied delegate is captured in a closure, so we walk the closure's + /// fields to find the innermost Func/delegate and read its Method.Name. + /// + private static string GetHandlerDelegateMethodName(HandlerWrapper wrapper) + { + // HandlerWrapper.Handler is the LambdaBootstrapHandler delegate. + // It is an async lambda that closes over the user-supplied handler delegate. + // We use reflection to dig through the closure chain until we find a field + // whose type is a delegate with a Method.Name we can inspect. + var handler = wrapper.Handler; + return FindDelegateMethodName(handler.Target, visited: new HashSet(ReferenceEqualityComparer.Instance)); + } + + private static string FindDelegateMethodName(object? target, HashSet visited) + { + if (target == null || !visited.Add(target)) + return string.Empty; + + foreach (var field in target.GetType().GetFields( + System.Reflection.BindingFlags.Instance | + System.Reflection.BindingFlags.NonPublic | + System.Reflection.BindingFlags.Public)) + { + var value = field.GetValue(target); + if (value == null) continue; + + if (value is Delegate d) + { + var name = d.Method.Name; + // Skip compiler-generated method names (lambdas / state machines) + if (!name.StartsWith("<") && !name.Contains("MoveNext")) + return name; + + // Recurse into the delegate's own closure + var inner = FindDelegateMethodName(d.Target, visited); + if (!string.IsNullOrEmpty(inner)) + return inner; + } + else if (value.GetType().IsClass && !value.GetType().IsPrimitive + && value.GetType().Namespace?.StartsWith("System") == false) + { + var inner = FindDelegateMethodName(value, visited); + if (!string.IsNullOrEmpty(inner)) + return inner; + } + } + + return string.Empty; + } + + // ------------------------------------------------------------------------- + // Testable server subclasses that expose CreateHandlerWrapper publicly + // ------------------------------------------------------------------------- + + private class TestableHttpApiV2Server : APIGatewayHttpApiV2LambdaRuntimeSupportServer + { + public TestableHttpApiV2Server(IServiceProvider sp) : base(sp) { } + + public HandlerWrapper PublicCreateHandlerWrapper(IServiceProvider sp) + => CreateHandlerWrapper(sp); + } + + private class TestableRestApiServer : APIGatewayRestApiLambdaRuntimeSupportServer + { + public TestableRestApiServer(IServiceProvider sp) : base(sp) { } + + public HandlerWrapper PublicCreateHandlerWrapper(IServiceProvider sp) + => CreateHandlerWrapper(sp); + } + + private class TestableAlbServer : ApplicationLoadBalancerLambdaRuntimeSupportServer + { + public TestableAlbServer(IServiceProvider sp) : base(sp) { } + + public HandlerWrapper PublicCreateHandlerWrapper(IServiceProvider sp) + => CreateHandlerWrapper(sp); + } +} diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ResponseStreamingPropertyTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ResponseStreamingPropertyTests.cs new file mode 100644 index 000000000..43ebc4dd4 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Hosting.Tests/ResponseStreamingPropertyTests.cs @@ -0,0 +1,129 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Runtime.Versioning; + +using Amazon.Lambda.AspNetCoreServer.Hosting.Internal; +using Amazon.Lambda.Core; +using Amazon.Lambda.RuntimeSupport; +using Amazon.Lambda.Serialization.SystemTextJson; + +using Microsoft.Extensions.DependencyInjection; + +using Xunit; + +namespace Amazon.Lambda.AspNetCoreServer.Hosting.Tests; + +[RequiresPreviewFeatures] +public class ResponseStreamingPropertyTests +{ + private static IServiceProvider BuildServiceProvider(HostingOptions hostingOptions) + { + var services = new ServiceCollection(); + services.AddSingleton(hostingOptions); + services.AddSingleton(new DefaultLambdaJsonSerializer()); + services.AddLogging(); + return services.BuildServiceProvider(); + } + + private static string GetHandlerDelegateMethodName(HandlerWrapper wrapper) + { + var handler = wrapper.Handler; + return FindDelegateMethodName(handler.Target, new HashSet(ReferenceEqualityComparer.Instance)); + } + + private static string FindDelegateMethodName(object? target, HashSet visited) + { + if (target == null || !visited.Add(target)) + return string.Empty; + + foreach (var field in target.GetType().GetFields( + System.Reflection.BindingFlags.Instance | + System.Reflection.BindingFlags.NonPublic | + System.Reflection.BindingFlags.Public)) + { + var value = field.GetValue(target); + if (value == null) continue; + + if (value is Delegate d) + { + var name = d.Method.Name; + if (!name.StartsWith("<") && !name.Contains("MoveNext")) + return name; + var inner = FindDelegateMethodName(d.Target, visited); + if (!string.IsNullOrEmpty(inner)) return inner; + } + else if (value.GetType().IsClass && !value.GetType().IsPrimitive + && value.GetType().Namespace?.StartsWith("System") == false) + { + var inner = FindDelegateMethodName(value, visited); + if (!string.IsNullOrEmpty(inner)) return inner; + } + } + + return string.Empty; + } + + private class TestableHttpApiV2Server : APIGatewayHttpApiV2LambdaRuntimeSupportServer + { + public TestableHttpApiV2Server(IServiceProvider sp) : base(sp) { } + public HandlerWrapper PublicCreateHandlerWrapper(IServiceProvider sp) => CreateHandlerWrapper(sp); + } + + private class TestableRestApiServer : APIGatewayRestApiLambdaRuntimeSupportServer + { + public TestableRestApiServer(IServiceProvider sp) : base(sp) { } + public HandlerWrapper PublicCreateHandlerWrapper(IServiceProvider sp) => CreateHandlerWrapper(sp); + } + + private class TestableAlbServer : ApplicationLoadBalancerLambdaRuntimeSupportServer + { + public TestableAlbServer(IServiceProvider sp) : base(sp) { } + public HandlerWrapper PublicCreateHandlerWrapper(IServiceProvider sp) => CreateHandlerWrapper(sp); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Property9_HttpApiV2_StreamingFlag_RoutesCorrectly(bool enableStreaming) + { + var options = new HostingOptions { EnableResponseStreaming = enableStreaming }; + var sp = BuildServiceProvider(options); + var server = new TestableHttpApiV2Server(sp); + var wrapper = server.PublicCreateHandlerWrapper(sp); + var methodName = GetHandlerDelegateMethodName(wrapper); + + Assert.Contains("FunctionHandlerAsync", methodName); + Assert.DoesNotContain("StreamingFunctionHandlerAsync", methodName); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Property9_RestApi_StreamingFlag_RoutesCorrectly(bool enableStreaming) + { + var options = new HostingOptions { EnableResponseStreaming = enableStreaming }; + var sp = BuildServiceProvider(options); + var server = new TestableRestApiServer(sp); + var wrapper = server.PublicCreateHandlerWrapper(sp); + var methodName = GetHandlerDelegateMethodName(wrapper); + + Assert.Contains("FunctionHandlerAsync", methodName); + Assert.DoesNotContain("StreamingFunctionHandlerAsync", methodName); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Property9_Alb_StreamingFlag_RoutesCorrectly(bool enableStreaming) + { + var options = new HostingOptions { EnableResponseStreaming = enableStreaming }; + var sp = BuildServiceProvider(options); + var server = new TestableAlbServer(sp); + var wrapper = server.PublicCreateHandlerWrapper(sp); + var methodName = GetHandlerDelegateMethodName(wrapper); + + Assert.Contains("FunctionHandlerAsync", methodName); + Assert.DoesNotContain("StreamingFunctionHandlerAsync", methodName); + } +} diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/Amazon.Lambda.AspNetCoreServer.Test.csproj b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/Amazon.Lambda.AspNetCoreServer.Test.csproj index 9ace52777..30a0c672e 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/Amazon.Lambda.AspNetCoreServer.Test.csproj +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/Amazon.Lambda.AspNetCoreServer.Test.csproj @@ -1,7 +1,9 @@  + + - net6.0;net8.0 + net8.0;net10.0 Amazon.Lambda.AspNetCoreServer.Test Library Amazon.Lambda.AspNetCoreServer.Test @@ -9,7 +11,7 @@ false false false - 1701;1702;1705;CS0618 + 1701;1702;1705;CS0618;CS1591 @@ -47,13 +49,17 @@ - - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/BuildStreamingPreludeTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/BuildStreamingPreludeTests.cs new file mode 100644 index 000000000..c2971d0ab --- /dev/null +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/BuildStreamingPreludeTests.cs @@ -0,0 +1,267 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +using System.Net; +using System.Runtime.Versioning; + +using Amazon.Lambda.AspNetCoreServer.Internal; +using Microsoft.AspNetCore.Http.Features; +using Xunit; + +namespace Amazon.Lambda.AspNetCoreServer.Test +{ + [RequiresPreviewFeatures] + public class BuildStreamingPreludeTests + { + // Subclass that skips host startup entirely and + // just exposes BuildStreamingPrelude directly without needing a running host. + private class StandalonePreludeBuilder : APIGatewayHttpApiV2ProxyFunction + { + // Use the StartupMode.FirstRequest constructor so no host is started eagerly. + public StandalonePreludeBuilder() + : base(StartupMode.FirstRequest) { } + + public Amazon.Lambda.Core.ResponseStreaming.HttpResponseStreamPrelude + InvokeBuildStreamingPrelude(IHttpResponseFeature responseFeature) + => BuildStreamingPrelude(responseFeature); + } + + private static StandalonePreludeBuilder CreateBuilder() => new StandalonePreludeBuilder(); + + // Helper: create an InvokeFeatures, set StatusCode and Headers, return as IHttpResponseFeature. + private static IHttpResponseFeature MakeResponseFeature(int statusCode, System.Collections.Generic.Dictionary headers = null) + { + var features = new InvokeFeatures(); + var rf = (IHttpResponseFeature)features; + rf.StatusCode = statusCode; + if (headers != null) + { + foreach (var kvp in headers) + rf.Headers[kvp.Key] = new Microsoft.Extensions.Primitives.StringValues(kvp.Value); + } + return rf; + } + + // ----------------------------------------------------------------------- + // 6.1 Status code is copied correctly for values 100–599 + // ----------------------------------------------------------------------- + [Theory] + [InlineData(100)] + [InlineData(200)] + [InlineData(201)] + [InlineData(204)] + [InlineData(301)] + [InlineData(302)] + [InlineData(400)] + [InlineData(401)] + [InlineData(403)] + [InlineData(404)] + [InlineData(500)] + [InlineData(503)] + [InlineData(599)] + public void StatusCode_IsCopiedCorrectly(int statusCode) + { + var builder = CreateBuilder(); + var rf = MakeResponseFeature(statusCode); + + var prelude = builder.InvokeBuildStreamingPrelude(rf); + + Assert.Equal((HttpStatusCode)statusCode, prelude.StatusCode); + } + + // ----------------------------------------------------------------------- + // 6.2 Status code defaults to 200 when IHttpResponseFeature.StatusCode is 0 + // ----------------------------------------------------------------------- + [Fact] + public void StatusCode_DefaultsTo200_WhenFeatureStatusCodeIsZero() + { + var builder = CreateBuilder(); + var rf = MakeResponseFeature(0); + + var prelude = builder.InvokeBuildStreamingPrelude(rf); + + Assert.Equal(HttpStatusCode.OK, prelude.StatusCode); + } + + // ----------------------------------------------------------------------- + // 6.3 Non-Set-Cookie headers appear in MultiValueHeaders with all values preserved + // ----------------------------------------------------------------------- + [Fact] + public void NonSetCookieHeaders_AppearInMultiValueHeaders_WithAllValuesPreserved() + { + var builder = CreateBuilder(); + var rf = MakeResponseFeature(200, new System.Collections.Generic.Dictionary + { + ["Content-Type"] = new[] { "application/json" }, + ["X-Custom"] = new[] { "val1", "val2" }, + ["Cache-Control"] = new[] { "no-cache", "no-store" } + }); + + var prelude = builder.InvokeBuildStreamingPrelude(rf); + + Assert.True(prelude.MultiValueHeaders.ContainsKey("Content-Type")); + Assert.Equal(new[] { "application/json" }, prelude.MultiValueHeaders["Content-Type"]); + + Assert.True(prelude.MultiValueHeaders.ContainsKey("X-Custom")); + Assert.Equal(new[] { "val1", "val2" }, prelude.MultiValueHeaders["X-Custom"]); + + Assert.True(prelude.MultiValueHeaders.ContainsKey("Cache-Control")); + Assert.Equal(new[] { "no-cache", "no-store" }, prelude.MultiValueHeaders["Cache-Control"]); + } + + [Fact] + public void NonSetCookieHeaders_MultiValueHeaders_PreservesMultipleValues() + { + var builder = CreateBuilder(); + var rf = MakeResponseFeature(200, new System.Collections.Generic.Dictionary + { + ["Accept"] = new[] { "text/html", "application/xhtml+xml", "application/xml" } + }); + + var prelude = builder.InvokeBuildStreamingPrelude(rf); + + Assert.Equal(new[] { "text/html", "application/xhtml+xml", "application/xml" }, + prelude.MultiValueHeaders["Accept"]); + } + + // ----------------------------------------------------------------------- + // 6.4 Set-Cookie header values are moved to Cookies and absent from MultiValueHeaders + // ----------------------------------------------------------------------- + [Fact] + public void SetCookieHeader_MovedToCookies_AbsentFromMultiValueHeaders() + { + var builder = CreateBuilder(); + var rf = MakeResponseFeature(200, new System.Collections.Generic.Dictionary + { + ["Set-Cookie"] = new[] { "session=abc123; Path=/; HttpOnly" }, + ["Content-Type"] = new[] { "text/html" } + }); + + var prelude = builder.InvokeBuildStreamingPrelude(rf); + + // Cookie value is in Cookies + Assert.Contains("session=abc123; Path=/; HttpOnly", prelude.Cookies); + + // Set-Cookie is NOT in MultiValueHeaders + Assert.False(prelude.MultiValueHeaders.ContainsKey("Set-Cookie")); + Assert.False(prelude.MultiValueHeaders.ContainsKey("set-cookie")); + + // Other headers are still present + Assert.True(prelude.MultiValueHeaders.ContainsKey("Content-Type")); + } + + [Fact] + public void SetCookieHeader_IsCaseInsensitive() + { + // The implementation uses StringComparison.OrdinalIgnoreCase, so + // "set-cookie" (lowercase) should also be routed to Cookies. + var builder = CreateBuilder(); + var features = new InvokeFeatures(); + var rf = (IHttpResponseFeature)features; + rf.StatusCode = 200; + // HeaderDictionary is case-insensitive, so "set-cookie" and "Set-Cookie" are the same key. + rf.Headers["set-cookie"] = new Microsoft.Extensions.Primitives.StringValues("id=xyz; Path=/"); + + var prelude = builder.InvokeBuildStreamingPrelude(rf); + + Assert.Contains("id=xyz; Path=/", prelude.Cookies); + Assert.False(prelude.MultiValueHeaders.ContainsKey("Set-Cookie")); + Assert.False(prelude.MultiValueHeaders.ContainsKey("set-cookie")); + } + + // ----------------------------------------------------------------------- + // 6.5 Multiple Set-Cookie values all appear in Cookies + // ----------------------------------------------------------------------- + [Fact] + public void MultipleSetCookieValues_AllAppearInCookies() + { + var builder = CreateBuilder(); + var rf = MakeResponseFeature(200, new System.Collections.Generic.Dictionary + { + ["Set-Cookie"] = new[] + { + "session=abc; Path=/; HttpOnly", + "theme=dark; Path=/", + "lang=en; Path=/; SameSite=Strict" + } + }); + + var prelude = builder.InvokeBuildStreamingPrelude(rf); + + Assert.Equal(3, prelude.Cookies.Count); + Assert.Contains("session=abc; Path=/; HttpOnly", prelude.Cookies); + Assert.Contains("theme=dark; Path=/", prelude.Cookies); + Assert.Contains("lang=en; Path=/; SameSite=Strict", prelude.Cookies); + + // None of them should be in MultiValueHeaders + Assert.False(prelude.MultiValueHeaders.ContainsKey("Set-Cookie")); + } + + [Fact] + public void MultipleSetCookieValues_WithOtherHeaders_CookiesAndHeadersAreSeparated() + { + var builder = CreateBuilder(); + var rf = MakeResponseFeature(201, new System.Collections.Generic.Dictionary + { + ["Set-Cookie"] = new[] { "a=1", "b=2" }, + ["Location"] = new[] { "/new-resource" } + }); + + var prelude = builder.InvokeBuildStreamingPrelude(rf); + + Assert.Equal((HttpStatusCode)201, prelude.StatusCode); + Assert.Equal(2, prelude.Cookies.Count); + Assert.Contains("a=1", prelude.Cookies); + Assert.Contains("b=2", prelude.Cookies); + Assert.True(prelude.MultiValueHeaders.ContainsKey("Location")); + Assert.False(prelude.MultiValueHeaders.ContainsKey("Set-Cookie")); + } + + [Fact] + public void EmptyHeaders_ProducesEmptyMultiValueHeadersAndCookies() + { + var builder = CreateBuilder(); + var rf = MakeResponseFeature(204); + + var prelude = builder.InvokeBuildStreamingPrelude(rf); + + Assert.Equal(HttpStatusCode.NoContent, prelude.StatusCode); + Assert.Empty(prelude.MultiValueHeaders); + Assert.Empty(prelude.Cookies); + } + + // ----------------------------------------------------------------------- + // Content-Length and Transfer-Encoding are excluded from the prelude + // ----------------------------------------------------------------------- + [Fact] + public void ContentLengthHeader_ExcludedFromPrelude() + { + var builder = CreateBuilder(); + var rf = MakeResponseFeature(200, new System.Collections.Generic.Dictionary + { + ["Content-Type"] = new[] { "application/json" }, + ["Content-Length"] = new[] { "42" } + }); + + var prelude = builder.InvokeBuildStreamingPrelude(rf); + + Assert.True(prelude.MultiValueHeaders.ContainsKey("Content-Type")); + Assert.False(prelude.MultiValueHeaders.ContainsKey("Content-Length")); + } + + [Fact] + public void TransferEncodingHeader_ExcludedFromPrelude() + { + var builder = CreateBuilder(); + var rf = MakeResponseFeature(200, new System.Collections.Generic.Dictionary + { + ["Content-Type"] = new[] { "text/plain" }, + ["Transfer-Encoding"] = new[] { "chunked" } + }); + + var prelude = builder.InvokeBuildStreamingPrelude(rf); + + Assert.True(prelude.MultiValueHeaders.ContainsKey("Content-Type")); + Assert.False(prelude.MultiValueHeaders.ContainsKey("Transfer-Encoding")); + } + } +} diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/ResponseStreamingPropertyTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/ResponseStreamingPropertyTests.cs new file mode 100644 index 000000000..05c6bed87 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/ResponseStreamingPropertyTests.cs @@ -0,0 +1,477 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.Versioning; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.AspNetCoreServer.Internal; +using Amazon.Lambda.Core; +using Amazon.Lambda.TestUtilities; + +using Microsoft.AspNetCore.Http.Features; + +using Xunit; + +namespace Amazon.Lambda.AspNetCoreServer.Test +{ + [RequiresPreviewFeatures] + public class ResponseStreamingPropertyTests + { + // ----------------------------------------------------------------------- + // Shared test infrastructure + // ----------------------------------------------------------------------- + + private class PropertyTestStreamingFunction : APIGatewayHttpApiV2ProxyFunction + { + public InvokeFeatures CapturedFeatures { get; private set; } + public MemoryStream CapturedLambdaStream { get; private set; } + public bool MarshallResponseCalled { get; private set; } + + public PropertyTestStreamingFunction() + : base(StartupMode.FirstRequest) + { + EnableResponseStreaming = true; + } + + public void PublicMarshallRequest(InvokeFeatures features, + APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context) + => MarshallRequest(features, request, context); + + protected override void PostMarshallItemsFeatureFeature( + IItemsFeature aspNetCoreItemFeature, + APIGatewayHttpApiV2ProxyRequest lambdaRequest, + ILambdaContext lambdaContext) + { + CapturedFeatures = aspNetCoreItemFeature as InvokeFeatures; + base.PostMarshallItemsFeatureFeature(aspNetCoreItemFeature, lambdaRequest, lambdaContext); + } + + [RequiresPreviewFeatures] + protected override Stream CreateLambdaResponseStream( + Amazon.Lambda.Core.ResponseStreaming.HttpResponseStreamPrelude prelude) + { + var ms = new MemoryStream(); + CapturedLambdaStream = ms; + return ms; + } + + protected override APIGatewayHttpApiV2ProxyResponse MarshallResponse( + IHttpResponseFeature responseFeatures, + ILambdaContext lambdaContext, + int statusCodeIfNotSet = 200) + { + MarshallResponseCalled = true; + return base.MarshallResponse(responseFeatures, lambdaContext, statusCodeIfNotSet); + } + } + + private class StandalonePreludeBuilder : APIGatewayHttpApiV2ProxyFunction + { + public StandalonePreludeBuilder() : base(StartupMode.FirstRequest) { } + + public Amazon.Lambda.Core.ResponseStreaming.HttpResponseStreamPrelude + InvokeBuildStreamingPrelude(IHttpResponseFeature responseFeature) + => BuildStreamingPrelude(responseFeature); + } + + private static APIGatewayHttpApiV2ProxyRequest MakeRequest( + string method = "GET", string path = "/api/values", + Dictionary headers = null, string body = null) + => new APIGatewayHttpApiV2ProxyRequest + { + RequestContext = new APIGatewayHttpApiV2ProxyRequest.ProxyRequestContext + { + Http = new APIGatewayHttpApiV2ProxyRequest.HttpDescription { Method = method, Path = path }, + Stage = "$default" + }, + RawPath = path, + Headers = headers ?? new Dictionary { ["accept"] = "application/json" }, + Body = body + }; + + + public static IEnumerable RequestMarshallingCases() => + [ + ["GET", "/api/values", null, null], + ["POST", "/api/values", new Dictionary{["content-type"]="application/json"}, "{\"k\":\"v\"}"], + ["PUT", "/api/items/42", new Dictionary{["x-custom-header"]="abc"}, null], + ["DELETE", "/api/items/1", null, null], + ["PATCH", "/api/values", new Dictionary{["accept"]="text/html"}, null], + ]; + + [Theory] + [MemberData(nameof(RequestMarshallingCases))] + public async Task Property1_RequestMarshalling_IdenticalInStreamingAndBufferedModes( + string method, string path, Dictionary headers, string body) + { + var function = new PropertyTestStreamingFunction(); + var context = new TestLambdaContext(); + + // Warm up so the host is started + await function.FunctionHandlerAsync(MakeRequest(), context); + + var request = MakeRequest(method, path, headers, body); + await function.FunctionHandlerAsync(request, context); + var streamingReq = (IHttpRequestFeature)function.CapturedFeatures; + + var bufferedFeatures = new InvokeFeatures(); + function.PublicMarshallRequest(bufferedFeatures, request, context); + var bufferedReq = (IHttpRequestFeature)bufferedFeatures; + + Assert.NotNull(streamingReq); + Assert.Equal(bufferedReq.Method, streamingReq.Method); + Assert.Equal(bufferedReq.Path, streamingReq.Path); + Assert.Equal(bufferedReq.PathBase, streamingReq.PathBase); + Assert.Equal(bufferedReq.QueryString, streamingReq.QueryString); + Assert.Equal(bufferedReq.Scheme, streamingReq.Scheme); + + foreach (var key in bufferedReq.Headers.Keys) + { + Assert.True(streamingReq.Headers.ContainsKey(key), + $"Streaming features missing header '{key}'"); + Assert.Equal(bufferedReq.Headers[key], streamingReq.Headers[key]); + } + } + + + public static IEnumerable BufferedModeCases() => + [ + ["GET", "/api/values", null, null], + ["POST", "/api/values", null, "{\"key\":\"value\"}"], + ["PUT", "/api/items/5", null, null], + ["DELETE", "/api/items/5", null, null], + ["GET", "/api/values", new Dictionary{["accept"]="text/html"}, null], + ]; + + [Theory] + [MemberData(nameof(BufferedModeCases))] + public async Task Property2_BufferedMode_Unaffected( + string method, string path, Dictionary headers, string body) + { + // Use a fresh function with streaming OFF + var function = new PropertyTestStreamingFunction(); + function.EnableResponseStreaming = false; + var context = new TestLambdaContext(); + + var response = await function.FunctionHandlerAsync(MakeRequest(method, path, headers, body), context); + + Assert.NotNull(response); + Assert.True(function.MarshallResponseCalled, "MarshallResponse must be called in buffered mode"); + Assert.IsType(response); + Assert.True(response.StatusCode >= 100 && response.StatusCode <= 599, + $"Status code {response.StatusCode} out of valid range"); + } + + + public static IEnumerable PreludeStatusAndHeaderCases() => + [ + // (statusCode, headerKey, headerValues[]) + [0, "accept", new[] { "application/json" }], + [200, "content-type", new[] { "text/plain" }], + [201, "x-request-id", new[] { "abc-123" }], + [404, "cache-control", new[] { "no-cache", "no-store" }], + [500, "x-custom-header", new[] { "val1", "val2", "val3" }], + ]; + + [Theory] + [MemberData(nameof(PreludeStatusAndHeaderCases))] + public void Property3_Prelude_StatusCodeAndNonCookieHeaders_Correct( + int statusCode, string headerKey, string[] headerValues) + { + var builder = new StandalonePreludeBuilder(); + var features = new InvokeFeatures(); + var rf = (IHttpResponseFeature)features; + rf.StatusCode = statusCode; + rf.Headers[headerKey] = new Microsoft.Extensions.Primitives.StringValues(headerValues); + + var prelude = builder.InvokeBuildStreamingPrelude(rf); + + int expectedStatus = statusCode == 0 ? 200 : statusCode; + Assert.Equal((System.Net.HttpStatusCode)expectedStatus, prelude.StatusCode); + + Assert.True(prelude.MultiValueHeaders.ContainsKey(headerKey), + $"Header '{headerKey}' missing from MultiValueHeaders"); + Assert.Equal(headerValues, prelude.MultiValueHeaders[headerKey].ToArray()); + + Assert.False(prelude.MultiValueHeaders.ContainsKey("Set-Cookie")); + Assert.False(prelude.MultiValueHeaders.ContainsKey("set-cookie")); + } + + + public static IEnumerable SetCookieCases() => + [ + [new[] { "session=abc; Path=/" }], + [new[] { "a=1; Path=/", "b=2; Path=/" }], + [new[] { "x=foo; Path=/", "y=bar; Path=/", "z=baz; Path=/" }], + ]; + + [Theory] + [MemberData(nameof(SetCookieCases))] + public void Property4_SetCookieHeaders_MovedToCookies_AbsentFromMultiValueHeaders(string[] cookies) + { + var builder = new StandalonePreludeBuilder(); + var features = new InvokeFeatures(); + var rf = (IHttpResponseFeature)features; + rf.StatusCode = 200; + rf.Headers["Set-Cookie"] = new Microsoft.Extensions.Primitives.StringValues(cookies); + rf.Headers["content-type"] = "application/json"; + + var prelude = builder.InvokeBuildStreamingPrelude(rf); + + foreach (var cookie in cookies) + Assert.Contains(cookie, prelude.Cookies); + + Assert.False(prelude.MultiValueHeaders.ContainsKey("Set-Cookie"), + "Set-Cookie must not appear in MultiValueHeaders"); + Assert.False(prelude.MultiValueHeaders.ContainsKey("set-cookie"), + "set-cookie must not appear in MultiValueHeaders"); + + Assert.True(prelude.MultiValueHeaders.ContainsKey("content-type")); + } + + + public static IEnumerable BodyBytesCases() => + [ + [new[] { new byte[] { 1, 2, 3 } }], + [new[] { new byte[] { 10, 20 }, new byte[] { 30, 40, 50 } }], + [new[] { new byte[] { 0xFF }, new byte[] { 0x00 }, new byte[] { 0xAB, 0xCD } }], + [new[] { Encoding.UTF8.GetBytes("hello "), Encoding.UTF8.GetBytes("world") }], + ]; + + [Theory] + [MemberData(nameof(BodyBytesCases))] + public async Task Property5_BodyBytes_ForwardedToLambdaResponseStream_InOrder(byte[][] chunks) + { + var lambdaStream = new MemoryStream(); + var invokeFeatures = new InvokeFeatures(); + var feature = new StreamingResponseBodyFeature( + (IHttpResponseFeature)invokeFeatures, + () => Task.FromResult(lambdaStream)); + + await feature.StartAsync(); + + foreach (var chunk in chunks) + await feature.Stream.WriteAsync(chunk, 0, chunk.Length); + + lambdaStream.Position = 0; + var actual = lambdaStream.ToArray(); + var expected = chunks.SelectMany(c => c).ToArray(); + + Assert.Equal(expected, actual); + } + + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(5)] + public async Task Property6_OnStartingCallbacks_FireBeforeFirstByte(int cbCount) + { + int sequenceCounter = 0; + var callbackSequences = new List(); + int firstWriteSequence = -1; + + var trackingStream = new WriteTrackingStream(() => firstWriteSequence = sequenceCounter++); + var invokeFeatures = new InvokeFeatures(); + var responseFeature = (IHttpResponseFeature)invokeFeatures; + + for (int i = 0; i < cbCount; i++) + { + responseFeature.OnStarting(_ => + { + callbackSequences.Add(sequenceCounter++); + return Task.CompletedTask; + }, null); + } + + var feature = new StreamingResponseBodyFeature( + responseFeature, + () => Task.FromResult(trackingStream)); + + await feature.StartAsync(); + var bytes = new byte[] { 1, 2, 3 }; + await feature.Stream.WriteAsync(bytes, 0, bytes.Length); + + Assert.Equal(cbCount, callbackSequences.Count); + Assert.True(firstWriteSequence >= 0, "No write reached the lambda stream"); + foreach (var seq in callbackSequences) + Assert.True(seq < firstWriteSequence, + $"Callback (seq={seq}) did not fire before first write (seq={firstWriteSequence})"); + } + + + public static IEnumerable FileRangeCases() => + [ + // (fileBytes, offset, count) — null count means read to end + [new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }, 0L, (long?)8L], + [new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }, 2L, (long?)4L], + [new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }, 0L, (long?)null], + [new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }, 5L, (long?)null], + [new byte[] { 0xAA, 0xBB, 0xCC, 0xDD }, 1L, (long?)2L], + ]; + + [Theory] + [MemberData(nameof(FileRangeCases))] + public async Task Property7_SendFileAsync_WritesCorrectByteRange( + byte[] fileBytes, long offset, long? count) + { + var lambdaStream = new MemoryStream(); + var invokeFeatures = new InvokeFeatures(); + var feature = new StreamingResponseBodyFeature( + (IHttpResponseFeature)invokeFeatures, + () => Task.FromResult(lambdaStream)); + + var tempFile = Path.GetTempFileName(); + try + { + await File.WriteAllBytesAsync(tempFile, fileBytes); + await feature.SendFileAsync(tempFile, offset, count); + + lambdaStream.Position = 0; + var actual = lambdaStream.ToArray(); + + long actualCount = count ?? (fileBytes.Length - offset); + var expected = fileBytes.Skip((int)offset).Take((int)actualCount).ToArray(); + + Assert.Equal(expected, actual); + } + finally + { + File.Delete(tempFile); + } + } + + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(5)] + public async Task Property8_OnCompletedCallbacks_FireAfterStreamClose(int cbCount) + { + int sequenceCounter = 0; + var completedSequences = new List(); + int streamClosedSequence = -1; + + var function = new OnCompletedTrackingFunction( + cbCount: cbCount, + completedSequences: completedSequences, + getAndIncrementCounter: () => sequenceCounter++, + onStreamClosed: () => streamClosedSequence = sequenceCounter++); + + var context = new TestLambdaContext(); + var request = MakeRequest(); + + await function.FunctionHandlerAsync(request, context); + + Assert.Equal(cbCount, completedSequences.Count); + Assert.True(streamClosedSequence >= 0, "Stream was never closed"); + foreach (var seq in completedSequences) + Assert.True(seq > streamClosedSequence, + $"OnCompleted callback (seq={seq}) fired before stream closed (seq={streamClosedSequence})"); + } + + // ----------------------------------------------------------------------- + // Helpers + // ----------------------------------------------------------------------- + + private class WriteTrackingStream : MemoryStream + { + private readonly Action _onFirstWrite; + private bool _fired; + + public WriteTrackingStream(Action onFirstWrite) => _onFirstWrite = onFirstWrite; + + public override void Write(byte[] buffer, int offset, int count) + { + FireOnce(); + base.Write(buffer, offset, count); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, + CancellationToken cancellationToken) + { + FireOnce(); + return base.WriteAsync(buffer, offset, count, cancellationToken); + } + + private void FireOnce() + { + if (!_fired) { _fired = true; _onFirstWrite?.Invoke(); } + } + } + + private class OnCompletedTrackingFunction : APIGatewayHttpApiV2ProxyFunction + { + private readonly int _cbCount; + private readonly List _completedSequences; + private readonly Func _getAndIncrementCounter; + private readonly Action _onStreamClosed; + + public OnCompletedTrackingFunction( + int cbCount, + List completedSequences, + Func getAndIncrementCounter, + Action onStreamClosed) + : base(StartupMode.FirstRequest) + { + EnableResponseStreaming = true; + _cbCount = cbCount; + _completedSequences = completedSequences; + _getAndIncrementCounter = getAndIncrementCounter; + _onStreamClosed = onStreamClosed; + } + + protected override void PostMarshallItemsFeatureFeature( + IItemsFeature aspNetCoreItemFeature, + APIGatewayHttpApiV2ProxyRequest lambdaRequest, + ILambdaContext lambdaContext) + { + var responseFeature = (IHttpResponseFeature)aspNetCoreItemFeature; + for (int i = 0; i < _cbCount; i++) + { + responseFeature.OnCompleted(_ => + { + _completedSequences.Add(_getAndIncrementCounter()); + return Task.CompletedTask; + }, null); + } + base.PostMarshallItemsFeatureFeature(aspNetCoreItemFeature, lambdaRequest, lambdaContext); + } + + [RequiresPreviewFeatures] + protected override Stream CreateLambdaResponseStream( + Amazon.Lambda.Core.ResponseStreaming.HttpResponseStreamPrelude prelude) + { + return new CloseTrackingStream(_onStreamClosed); + } + } + + private class CloseTrackingStream : MemoryStream + { + private readonly Action _onClose; + private bool _closed; + + public CloseTrackingStream(Action onClose) => _onClose = onClose; + + protected override void Dispose(bool disposing) + { + if (!_closed) { _closed = true; _onClose?.Invoke(); } + base.Dispose(disposing); + } + + public override ValueTask DisposeAsync() + { + if (!_closed) { _closed = true; _onClose?.Invoke(); } + return base.DisposeAsync(); + } + } + } +} diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/StreamingFunctionHandlerAsyncTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/StreamingFunctionHandlerAsyncTests.cs new file mode 100644 index 000000000..6ba2a6291 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/StreamingFunctionHandlerAsyncTests.cs @@ -0,0 +1,703 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Runtime.Versioning; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.AspNetCoreServer.Internal; +using Amazon.Lambda.Core; +using Amazon.Lambda.TestUtilities; + +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +using Xunit; + +namespace Amazon.Lambda.AspNetCoreServer.Test +{ + /// + /// Unit tests for the streaming path in + /// when EnableResponseStreaming is true. + /// + /// overrides CreateLambdaResponseStream to inject + /// a instead of calling LambdaResponseStreamFactory.CreateHttpStream, + /// allowing tests to run without the Lambda runtime. + /// + [RequiresPreviewFeatures] + public class StreamingFunctionHandlerAsyncTests + { + // ----------------------------------------------------------------------- + // Base testable subclass — overrides CreateLambdaResponseStream + // ----------------------------------------------------------------------- + + private class TestableStreamingFunction : APIGatewayHttpApiV2ProxyFunction + { + // Captured in PostMarshallItemsFeatureFeature — the InvokeFeatures after MarshallRequest + public InvokeFeatures CapturedFeatures { get; private set; } + + // The MemoryStream used as the Lambda response stream + public MemoryStream CapturedLambdaStream { get; private set; } + + // Whether CreateLambdaResponseStream was called (stream was opened) + public bool StreamOpened { get; private set; } + + // Whether MarshallResponse was called (buffered mode check) + public bool MarshallResponseCalled { get; private set; } + + // Optional setup action invoked inside PostMarshallItemsFeatureFeature + public Func PipelineSetupAction { get; set; } + + public TestableStreamingFunction() + : base(StartupMode.FirstRequest) + { + EnableResponseStreaming = true; + } + + // Expose MarshallRequest publicly so tests can call it after the host is started + public void PublicMarshallRequest(InvokeFeatures features, + APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context) + => MarshallRequest(features, request, context); + + protected override void PostMarshallItemsFeatureFeature( + IItemsFeature aspNetCoreItemFeature, + APIGatewayHttpApiV2ProxyRequest lambdaRequest, + ILambdaContext lambdaContext) + { + CapturedFeatures = aspNetCoreItemFeature as InvokeFeatures; + PipelineSetupAction?.Invoke(CapturedFeatures); + base.PostMarshallItemsFeatureFeature(aspNetCoreItemFeature, lambdaRequest, lambdaContext); + } + + [RequiresPreviewFeatures] + protected override Stream CreateLambdaResponseStream( + Amazon.Lambda.Core.ResponseStreaming.HttpResponseStreamPrelude prelude) + { + var ms = new MemoryStream(); + CapturedLambdaStream = ms; + StreamOpened = true; + return ms; + } + + protected override APIGatewayHttpApiV2ProxyResponse MarshallResponse( + IHttpResponseFeature responseFeatures, + ILambdaContext lambdaContext, + int statusCodeIfNotSet = 200) + { + MarshallResponseCalled = true; + return base.MarshallResponse(responseFeatures, lambdaContext, statusCodeIfNotSet); + } + } + + // ----------------------------------------------------------------------- + // Helper: build a minimal APIGatewayHttpApiV2ProxyRequest + // ----------------------------------------------------------------------- + private static APIGatewayHttpApiV2ProxyRequest MakeRequest( + string method = "GET", + string path = "/api/values", + Dictionary headers = null, + string body = null) + { + return new APIGatewayHttpApiV2ProxyRequest + { + RequestContext = new APIGatewayHttpApiV2ProxyRequest.ProxyRequestContext + { + Http = new APIGatewayHttpApiV2ProxyRequest.HttpDescription + { + Method = method, + Path = path + }, + Stage = "$default" + }, + RawPath = path, + Headers = headers ?? new Dictionary + { + ["accept"] = "application/json" + }, + Body = body + }; + } + + [Fact] + public async Task RequestMarshalling_ProducesSameHttpRequestFeatureState_AsBufferedMode() + { + var function = new TestableStreamingFunction(); + var context = new TestLambdaContext(); + var request = MakeRequest( + method: "POST", + path: "/api/values", + headers: new Dictionary + { + ["content-type"] = "application/json", + ["x-custom-header"] = "test-value" + }, + body: "{\"key\":\"value\"}" + ); + + // Run the streaming path first — this starts the host and captures features + await function.FunctionHandlerAsync(request, context); + var streamingReq = (IHttpRequestFeature)function.CapturedFeatures; + + // Now call MarshallRequest directly (host is started, _logger is initialized) + var bufferedFeatures = new InvokeFeatures(); + function.PublicMarshallRequest(bufferedFeatures, request, context); + var bufferedReq = (IHttpRequestFeature)bufferedFeatures; + + Assert.NotNull(streamingReq); + Assert.Equal(bufferedReq.Method, streamingReq.Method); + Assert.Equal(bufferedReq.Path, streamingReq.Path); + Assert.Equal(bufferedReq.PathBase, streamingReq.PathBase); + Assert.Equal(bufferedReq.QueryString, streamingReq.QueryString); + Assert.Equal(bufferedReq.Scheme, streamingReq.Scheme); + } + + [Fact] + public async Task RequestMarshalling_PreservesHeaders_InStreamingMode() + { + var function = new TestableStreamingFunction(); + var context = new TestLambdaContext(); + var request = MakeRequest( + headers: new Dictionary + { + ["x-forwarded-for"] = "1.2.3.4", + ["accept"] = "text/html" + } + ); + + // Run streaming path first to start the host + await function.FunctionHandlerAsync(request, context); + var streamingReq = (IHttpRequestFeature)function.CapturedFeatures; + + // Compare with buffered path + var bufferedFeatures = new InvokeFeatures(); + function.PublicMarshallRequest(bufferedFeatures, request, context); + var bufferedReq = (IHttpRequestFeature)bufferedFeatures; + + foreach (var key in bufferedReq.Headers.Keys) + { + Assert.True(streamingReq.Headers.ContainsKey(key), + $"Streaming features missing header '{key}' that buffered features has"); + Assert.Equal(bufferedReq.Headers[key], streamingReq.Headers[key]); + } + } + + [Fact] + public async Task AfterSetup_BodyFeature_IsStreamingResponseBodyFeature() + { + IHttpResponseBodyFeature capturedBodyFeature = null; + + var function = new TestableStreamingFunction(); + function.PipelineSetupAction = features => + { + var responseFeature = (IHttpResponseFeature)features; + responseFeature.OnStarting(_ => + { + capturedBodyFeature = (IHttpResponseBodyFeature)features[typeof(IHttpResponseBodyFeature)]; + return Task.CompletedTask; + }, null); + return Task.CompletedTask; + }; + + var context = new TestLambdaContext(); + var request = MakeRequest(); + + await function.FunctionHandlerAsync(request, context); + + // Verify via CapturedFeatures directly — the body feature was replaced before pipeline ran + var bodyFeatureFromCapture = function.CapturedFeatures[typeof(IHttpResponseBodyFeature)]; + Assert.IsType(bodyFeatureFromCapture); + } + + [Fact] + public async Task AfterSetup_BodyFeature_IsStreamingResponseBodyFeature_ViaOnStarting() + { + IHttpResponseBodyFeature capturedBodyFeature = null; + + var function = new TestableStreamingFunction(); + function.PipelineSetupAction = features => + { + var responseFeature = (IHttpResponseFeature)features; + responseFeature.OnStarting(_ => + { + capturedBodyFeature = (IHttpResponseBodyFeature)features[typeof(IHttpResponseBodyFeature)]; + return Task.CompletedTask; + }, null); + return Task.CompletedTask; + }; + + var context = new TestLambdaContext(); + var request = MakeRequest(); + + await function.FunctionHandlerAsync(request, context); + + if (capturedBodyFeature != null) + { + Assert.IsType(capturedBodyFeature); + } + else + { + var bodyFeature = function.CapturedFeatures[typeof(IHttpResponseBodyFeature)]; + Assert.IsType(bodyFeature); + } + } + + [Fact] + public async Task FunctionHandlerAsync_BufferedMode_StillReturnsResponse_ViaMarshallResponse() + { + // Buffered mode: EnableResponseStreaming defaults to false + var function = new TestableStreamingFunction(); + function.EnableResponseStreaming = false; + var context = new TestLambdaContext(); + var request = MakeRequest(); + + var response = await function.FunctionHandlerAsync(request, context); + + Assert.NotNull(response); + Assert.True(function.MarshallResponseCalled, + "MarshallResponse should have been called in buffered mode"); + Assert.IsType(response); + } + + [Fact] + public async Task FunctionHandlerAsync_BufferedMode_ReturnsStatusCode_FromPipeline() + { + var function = new TestableStreamingFunction(); + function.EnableResponseStreaming = false; + var context = new TestLambdaContext(); + var request = MakeRequest(path: "/api/values"); + + var response = await function.FunctionHandlerAsync(request, context); + + Assert.Equal(200, response.StatusCode); + } + + [Fact] + public async Task FunctionHandlerAsync_BufferedMode_DoesNotOpenLambdaStream() + { + var function = new TestableStreamingFunction(); + function.EnableResponseStreaming = false; + var context = new TestLambdaContext(); + var request = MakeRequest(); + + await function.FunctionHandlerAsync(request, context); + + Assert.False(function.StreamOpened, + "FunctionHandlerAsync (buffered mode) should not open the Lambda response stream"); + } + + // ----------------------------------------------------------------------- + // 7.4 OnCompleted callbacks fire after LambdaResponseStream is closed + // on success path + // ----------------------------------------------------------------------- + [Fact] + public async Task OnCompleted_FiresAfterStreamClosed_OnSuccessPath() + { + bool callbackFired = false; + + var function = new TestableStreamingFunction(); + function.PipelineSetupAction = features => + { + var responseFeature = (IHttpResponseFeature)features; + responseFeature.OnCompleted(_ => + { + callbackFired = true; + return Task.CompletedTask; + }, null); + return Task.CompletedTask; + }; + + var context = new TestLambdaContext(); + var request = MakeRequest(); + + await function.FunctionHandlerAsync(request, context); + + Assert.True(callbackFired, "OnCompleted callback should have fired on the success path"); + } + + [Fact] + public async Task OnCompleted_MultipleCallbacks_AllFire() + { + int firedCount = 0; + + var function = new TestableStreamingFunction(); + function.PipelineSetupAction = features => + { + var responseFeature = (IHttpResponseFeature)features; + for (int i = 0; i < 3; i++) + { + responseFeature.OnCompleted(_ => + { + firedCount++; + return Task.CompletedTask; + }, null); + } + return Task.CompletedTask; + }; + + var context = new TestLambdaContext(); + var request = MakeRequest(); + + await function.FunctionHandlerAsync(request, context); + + Assert.Equal(3, firedCount); + } + + [Fact] + public async Task ExceptionBeforeStreamOpen_StreamClosedCleanly_OnCompletedFires() + { + bool onCompletedFired = false; + + var function = new ThrowingBeforeStreamOpenFunction( + onCompleted: () => onCompletedFired = true); + + var context = new TestLambdaContext(); + var request = MakeRequest(); + + await function.FunctionHandlerAsync(request, context); + + Assert.False(function.StreamOpened, + "Stream should not have been opened when exception occurs before stream open"); + Assert.True(onCompletedFired, + "OnCompleted should fire even when exception occurs before stream open"); + } + + [Fact] + public async Task ExceptionBeforeStreamOpen_WithIncludeExceptionDetail_Writes500ErrorBody() + { + const string exceptionMessage = "Deliberate test failure for 500 response"; + + var function = new ThrowingBeforeStreamOpenFunction( + exceptionMessage: exceptionMessage, + onCompleted: null) + { + IncludeUnhandledExceptionDetailInResponse = true + }; + + var context = new TestLambdaContext(); + var request = MakeRequest(); + + await function.FunctionHandlerAsync(request, context); + + Assert.True(function.StreamOpened, + "An error stream should have been opened for the 500 response"); + Assert.NotNull(function.CapturedLambdaStream); + + var errorBody = Encoding.UTF8.GetString(function.CapturedLambdaStream.ToArray()); + Assert.Contains(exceptionMessage, errorBody); + } + + [Fact] + public async Task ExceptionBeforeStreamOpen_WithoutIncludeExceptionDetail_NoStreamOpened() + { + var function = new ThrowingBeforeStreamOpenFunction( + exceptionMessage: "Should not appear in response", + onCompleted: null) + { + IncludeUnhandledExceptionDetailInResponse = false + }; + + var context = new TestLambdaContext(); + var request = MakeRequest(); + + await function.FunctionHandlerAsync(request, context); + + Assert.False(function.StreamOpened, + "Stream should not be opened when IncludeUnhandledExceptionDetailInResponse=false"); + } + + // ----------------------------------------------------------------------- + // 7.7 Exception after stream open → stream closed after logging, OnCompleted fires + // ----------------------------------------------------------------------- + [Fact] + public async Task ExceptionAfterStreamOpen_StreamClosedAfterLogging_OnCompletedFires() + { + bool onCompletedFired = false; + + var function = new ThrowingAfterStreamOpenFunction( + onCompleted: () => onCompletedFired = true); + + var context = new TestLambdaContext(); + var request = MakeRequest(); + + await function.FunctionHandlerAsync(request, context); + + Assert.True(function.StreamOpened, + "Stream should have been opened before the exception"); + Assert.True(onCompletedFired, + "OnCompleted should fire even when exception occurs after stream open"); + } + + [Fact] + public async Task ExceptionAfterStreamOpen_DoesNotWriteNewErrorBody() + { + var function = new ThrowingAfterStreamOpenFunction(onCompleted: null) + { + IncludeUnhandledExceptionDetailInResponse = true + }; + + var context = new TestLambdaContext(); + var request = MakeRequest(); + + await function.FunctionHandlerAsync(request, context); + + Assert.True(function.StreamOpened); + var streamContent = function.CapturedLambdaStream.ToArray(); + var bodyText = Encoding.UTF8.GetString(streamContent); + Assert.DoesNotContain("InvalidOperationException", bodyText); + } + + [Fact] + public void FunctionHandlerAsync_HasLambdaSerializerAttribute() + { + var method = typeof(APIGatewayHttpApiV2ProxyFunction) + .GetMethod(nameof(APIGatewayHttpApiV2ProxyFunction.FunctionHandlerAsync)); + + Assert.NotNull(method); + + var attr = method.GetCustomAttribute(); + Assert.NotNull(attr); + Assert.Equal( + typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer), + attr.SerializerType); + } + + [Fact] + public void EnableResponseStreaming_Property_HasRequiresPreviewFeaturesAttribute() + { + var prop = typeof(APIGatewayHttpApiV2ProxyFunction) + .GetProperty(nameof(APIGatewayHttpApiV2ProxyFunction.EnableResponseStreaming)); + + Assert.NotNull(prop); + + var attr = prop.GetCustomAttribute(); + Assert.NotNull(attr); + } + + [Fact] + public void EnableResponseStreaming_Property_DefaultsToFalse() + { + var function = new TestableStreamingFunction(); + function.EnableResponseStreaming = false; // reset to default + Assert.False(function.EnableResponseStreaming); + } + + [Fact] + public void FunctionHandlerAsync_ReturnsTaskOfT() + { + var method = typeof(APIGatewayHttpApiV2ProxyFunction) + .GetMethod(nameof(APIGatewayHttpApiV2ProxyFunction.FunctionHandlerAsync)); + + Assert.NotNull(method); + Assert.True(method.ReturnType.IsGenericType); + Assert.Equal(typeof(Task<>), method.ReturnType.GetGenericTypeDefinition()); + } + + [Fact] + public void FunctionHandlerAsync_IsPublicVirtual() + { + var method = typeof(APIGatewayHttpApiV2ProxyFunction) + .GetMethod(nameof(APIGatewayHttpApiV2ProxyFunction.FunctionHandlerAsync)); + + Assert.NotNull(method); + Assert.True(method.IsPublic); + Assert.True(method.IsVirtual); + } + + // ----------------------------------------------------------------------- + // Helper subclasses for exception-path tests + // ----------------------------------------------------------------------- + + /// + /// Base class for exception-path tests. Overrides ExecuteStreamingRequestAsync + /// indirectly by overriding the pipeline via a custom ProcessRequest-equivalent. + /// Uses EnableResponseStreaming = true so FunctionHandlerAsync takes the + /// streaming path, then injects custom pipeline logic via . + /// + private abstract class CustomPipelineStreamingFunction + : APIGatewayHttpApiV2ProxyFunction + { + public MemoryStream CapturedLambdaStream { get; protected set; } + public bool StreamOpened { get; protected set; } + + protected CustomPipelineStreamingFunction() + : base(StartupMode.FirstRequest) + { + EnableResponseStreaming = true; + } + + [RequiresPreviewFeatures] + protected override Stream CreateLambdaResponseStream( + Amazon.Lambda.Core.ResponseStreaming.HttpResponseStreamPrelude prelude) + { + var ms = new MemoryStream(); + CapturedLambdaStream = ms; + StreamOpened = true; + return ms; + } + + // Override FunctionHandlerAsync to inject custom pipeline logic. + // We replicate the streaming setup from ExecuteStreamingRequestAsync so we can + // call RunPipelineAsync instead of the real ASP.NET Core pipeline. + public override async Task FunctionHandlerAsync( + APIGatewayHttpApiV2ProxyRequest request, + ILambdaContext lambdaContext) + { + if (!IsStarted) Start(); + + var features = new InvokeFeatures(); + MarshallRequest(features, request, lambdaContext); + + var itemFeatures = (IItemsFeature)features; + itemFeatures.Items = new System.Collections.Generic.Dictionary(); + itemFeatures.Items[LAMBDA_CONTEXT] = lambdaContext; + itemFeatures.Items[LAMBDA_REQUEST_OBJECT] = request; + PostMarshallItemsFeatureFeature(itemFeatures, request, lambdaContext); + + var responseFeature = (IHttpResponseFeature)features; + + async Task OpenStream() + { + var prelude = BuildStreamingPrelude(responseFeature); + return CreateLambdaResponseStream(prelude); + } + + var streamingBodyFeature = new StreamingResponseBodyFeature(_logger, responseFeature, OpenStream); + features[typeof(IHttpResponseBodyFeature)] = streamingBodyFeature; + + var scope = _hostServices.CreateScope(); + Exception pipelineException = null; + try + { + ((IServiceProvidersFeature)features).RequestServices = scope.ServiceProvider; + + try + { + try + { + await RunPipelineAsync(features, streamingBodyFeature); + } + catch (Exception e) + { + pipelineException = e; + + if (!StreamOpened && IncludeUnhandledExceptionDetailInResponse) + { + var errorPrelude = new Amazon.Lambda.Core.ResponseStreaming.HttpResponseStreamPrelude + { + StatusCode = System.Net.HttpStatusCode.InternalServerError + }; + var errorStream = CreateLambdaResponseStream(errorPrelude); + var errorBytes = Encoding.UTF8.GetBytes(ErrorReport(e)); + await errorStream.WriteAsync(errorBytes, 0, errorBytes.Length); + } + else if (StreamOpened) + { + _logger.LogError(e, $"Unhandled exception after response stream was opened: {ErrorReport(e)}"); + } + else + { + _logger.LogError(e, $"Unknown error responding to request: {ErrorReport(e)}"); + } + } + } + finally + { + if (features.ResponseCompletedEvents != null) + { + await features.ResponseCompletedEvents.ExecuteAsync(); + } + } + } + finally + { + scope.Dispose(); + } + + return default; + } + + protected abstract Task RunPipelineAsync( + InvokeFeatures features, + StreamingResponseBodyFeature bodyFeature); + } + + private class ThrowingBeforeStreamOpenFunction : CustomPipelineStreamingFunction + { + private readonly string _exceptionMessage; + private readonly Action _onCompleted; + + public ThrowingBeforeStreamOpenFunction( + string exceptionMessage = "Test exception before stream open", + Action onCompleted = null) + { + _exceptionMessage = exceptionMessage; + _onCompleted = onCompleted; + } + + protected override void PostMarshallItemsFeatureFeature( + IItemsFeature aspNetCoreItemFeature, + APIGatewayHttpApiV2ProxyRequest lambdaRequest, + ILambdaContext lambdaContext) + { + if (_onCompleted != null) + { + ((IHttpResponseFeature)aspNetCoreItemFeature).OnCompleted(_ => + { + _onCompleted(); + return Task.CompletedTask; + }, null); + } + base.PostMarshallItemsFeatureFeature(aspNetCoreItemFeature, lambdaRequest, lambdaContext); + } + + protected override Task RunPipelineAsync( + InvokeFeatures features, + StreamingResponseBodyFeature bodyFeature) + { + throw new InvalidOperationException(_exceptionMessage); + } + } + + private class ThrowingAfterStreamOpenFunction : CustomPipelineStreamingFunction + { + private readonly Action _onCompleted; + + public ThrowingAfterStreamOpenFunction(Action onCompleted = null) + { + _onCompleted = onCompleted; + } + + protected override void PostMarshallItemsFeatureFeature( + IItemsFeature aspNetCoreItemFeature, + APIGatewayHttpApiV2ProxyRequest lambdaRequest, + ILambdaContext lambdaContext) + { + if (_onCompleted != null) + { + ((IHttpResponseFeature)aspNetCoreItemFeature).OnCompleted(_ => + { + _onCompleted(); + return Task.CompletedTask; + }, null); + } + base.PostMarshallItemsFeatureFeature(aspNetCoreItemFeature, lambdaRequest, lambdaContext); + } + + protected override async Task RunPipelineAsync( + InvokeFeatures features, + StreamingResponseBodyFeature bodyFeature) + { + await bodyFeature.StartAsync(); + var partial = Encoding.UTF8.GetBytes("partial"); + await bodyFeature.Stream.WriteAsync(partial, 0, partial.Length); + throw new InvalidOperationException("Test exception after stream open"); + } + } + } +} diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/StreamingResponseBodyFeatureTests.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/StreamingResponseBodyFeatureTests.cs new file mode 100644 index 000000000..cdbd403e4 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/StreamingResponseBodyFeatureTests.cs @@ -0,0 +1,286 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +using System; +using System.IO; +using System.Runtime.Versioning; +using System.Threading.Tasks; + +using Amazon.Lambda.AspNetCoreServer.Internal; +using Microsoft.AspNetCore.Http.Features; +using Xunit; + +namespace Amazon.Lambda.AspNetCoreServer.Test +{ + [RequiresPreviewFeatures] + public class StreamingResponseBodyFeatureTests + { + // Helper: creates a StreamingResponseBodyFeature backed by a MemoryStream stand-in. + // Returns the feature and the MemoryStream that acts as the LambdaResponseStream. + private static (StreamingResponseBodyFeature feature, MemoryStream lambdaStream, InvokeFeatures invokeFeatures) + CreateFeature() + { + var lambdaStream = new MemoryStream(); + var invokeFeatures = new InvokeFeatures(); + var feature = new StreamingResponseBodyFeature( + (IHttpResponseFeature)invokeFeatures, + () => Task.FromResult(lambdaStream)); + return (feature, lambdaStream, invokeFeatures); + } + + [Fact] + public async Task PreStartBytes_AreBuffered_ThenFlushedToLambdaStream_OnStartAsync() + { + var (feature, lambdaStream, _) = CreateFeature(); + + // Write before StartAsync — should go to the pre-start buffer, NOT to lambdaStream yet. + var preBytes = new byte[] { 1, 2, 3 }; + await feature.Stream.WriteAsync(preBytes, 0, preBytes.Length); + + Assert.Equal(0, lambdaStream.Length); // nothing in lambda stream yet + + // Now call StartAsync — buffered bytes should be flushed. + await feature.StartAsync(); + + lambdaStream.Position = 0; + var result = lambdaStream.ToArray(); + Assert.Equal(preBytes, result); + } + + [Fact] + public async Task PostStartBytes_GoDirectlyToLambdaStream() + { + var (feature, lambdaStream, _) = CreateFeature(); + + await feature.StartAsync(); + + var postBytes = new byte[] { 10, 20, 30, 40 }; + await feature.Stream.WriteAsync(postBytes, 0, postBytes.Length); + + lambdaStream.Position = 0; + var result = lambdaStream.ToArray(); + Assert.Equal(postBytes, result); + } + + [Fact] + public async Task OnStartingCallbacks_FireBeforeFirstByteReachesLambdaStream() + { + var lambdaStream = new SequenceTrackingStream(); + var invokeFeatures = new InvokeFeatures(); + var responseFeature = (IHttpResponseFeature)invokeFeatures; + + int callbackSequence = -1; + int writeSequence = -1; + int sequenceCounter = 0; + + // Register an OnStarting callback that records its sequence number. + responseFeature.OnStarting(_ => + { + callbackSequence = sequenceCounter++; + return Task.CompletedTask; + }, null); + + // The stream opener records the sequence when the stream is first written to. + var feature = new StreamingResponseBodyFeature( + responseFeature, + () => + { + lambdaStream.OnFirstWrite = () => writeSequence = sequenceCounter++; + return Task.FromResult(lambdaStream); + }); + + // Write a byte — this should trigger StartAsync internally (via Stream property + // returning the pre-start buffer), but we explicitly call StartAsync here. + await feature.StartAsync(); + + // Write after start to trigger the first actual write to lambdaStream. + var bytes = new byte[] { 0xFF }; + await feature.Stream.WriteAsync(bytes, 0, bytes.Length); + + Assert.True(callbackSequence >= 0, "OnStarting callback was never called"); + Assert.True(writeSequence >= 0, "No write reached the lambda stream"); + Assert.True(callbackSequence < writeSequence, + $"OnStarting callback (seq={callbackSequence}) should fire before first write (seq={writeSequence})"); + } + + [Fact] + public async Task DisableBuffering_IsNoOp_DoesNotThrow_DoesNotChangeBehavior() + { + var (feature, lambdaStream, _) = CreateFeature(); + + // Should not throw. + feature.DisableBuffering(); + + // Behavior should be unchanged: bytes still flow through normally. + await feature.StartAsync(); + var bytes = new byte[] { 7, 8, 9 }; + await feature.Stream.WriteAsync(bytes, 0, bytes.Length); + + lambdaStream.Position = 0; + Assert.Equal(bytes, lambdaStream.ToArray()); + } + + [Fact] + public void DisableBuffering_BeforeStart_DoesNotThrow() + { + var (feature, _, _) = CreateFeature(); + var ex = Record.Exception(() => feature.DisableBuffering()); + Assert.Null(ex); + } + + [Fact] + public async Task SendFileAsync_WritesFullFile_WhenNoOffsetOrCount() + { + var (feature, lambdaStream, _) = CreateFeature(); + + var fileBytes = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }; + var tempFile = Path.GetTempFileName(); + try + { + await File.WriteAllBytesAsync(tempFile, fileBytes); + + await feature.SendFileAsync(tempFile, 0, null); + + lambdaStream.Position = 0; + Assert.Equal(fileBytes, lambdaStream.ToArray()); + } + finally + { + File.Delete(tempFile); + } + } + + [Fact] + public async Task SendFileAsync_WritesCorrectByteRange_WithOffsetAndCount() + { + var (feature, lambdaStream, _) = CreateFeature(); + + var fileBytes = new byte[] { 10, 20, 30, 40, 50, 60, 70, 80 }; + var tempFile = Path.GetTempFileName(); + try + { + await File.WriteAllBytesAsync(tempFile, fileBytes); + + // Read bytes at offset=2, count=4 → should get [30, 40, 50, 60] + await feature.SendFileAsync(tempFile, offset: 2, count: 4); + + lambdaStream.Position = 0; + Assert.Equal(new byte[] { 30, 40, 50, 60 }, lambdaStream.ToArray()); + } + finally + { + File.Delete(tempFile); + } + } + + [Fact] + public async Task SendFileAsync_WithOffset_SkipsLeadingBytes() + { + var (feature, lambdaStream, _) = CreateFeature(); + + var fileBytes = new byte[] { 1, 2, 3, 4, 5 }; + var tempFile = Path.GetTempFileName(); + try + { + await File.WriteAllBytesAsync(tempFile, fileBytes); + + // offset=3, count=null → should get [4, 5] + await feature.SendFileAsync(tempFile, offset: 3, count: null); + + lambdaStream.Position = 0; + Assert.Equal(new byte[] { 4, 5 }, lambdaStream.ToArray()); + } + finally + { + File.Delete(tempFile); + } + } + + [Fact] + public async Task CompleteAsync_CallsStartAsync_WhenNotYetStarted() + { + bool streamOpenerCalled = false; + var lambdaStream = new MemoryStream(); + var invokeFeatures = new InvokeFeatures(); + + var feature = new StreamingResponseBodyFeature( + (IHttpResponseFeature)invokeFeatures, + () => + { + streamOpenerCalled = true; + return Task.FromResult(lambdaStream); + }); + + Assert.False(streamOpenerCalled); + + await feature.CompleteAsync(); + + Assert.True(streamOpenerCalled, "CompleteAsync should have triggered StartAsync which calls the stream opener"); + } + + [Fact] + public async Task CompleteAsync_WhenAlreadyStarted_DoesNotCallStreamOpenerAgain() + { + int streamOpenerCallCount = 0; + var lambdaStream = new MemoryStream(); + var invokeFeatures = new InvokeFeatures(); + + var feature = new StreamingResponseBodyFeature( + (IHttpResponseFeature)invokeFeatures, + () => + { + streamOpenerCallCount++; + return Task.FromResult(lambdaStream); + }); + + await feature.StartAsync(); + await feature.CompleteAsync(); + + Assert.Equal(1, streamOpenerCallCount); + } + + [Fact] + public async Task PreAndPostStartBytes_AreForwardedInOrder() + { + var (feature, lambdaStream, _) = CreateFeature(); + + var preBytes = new byte[] { 1, 2, 3 }; + var postBytes = new byte[] { 4, 5, 6 }; + + await feature.Stream.WriteAsync(preBytes, 0, preBytes.Length); + await feature.StartAsync(); + await feature.Stream.WriteAsync(postBytes, 0, postBytes.Length); + + lambdaStream.Position = 0; + var result = lambdaStream.ToArray(); + Assert.Equal(new byte[] { 1, 2, 3, 4, 5, 6 }, result); + } + + private class SequenceTrackingStream : MemoryStream + { + public Action OnFirstWrite { get; set; } + private bool _firstWriteDone; + + public override void Write(byte[] buffer, int offset, int count) + { + FireFirstWrite(); + base.Write(buffer, offset, count); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, + System.Threading.CancellationToken cancellationToken) + { + FireFirstWrite(); + return base.WriteAsync(buffer, offset, count, cancellationToken); + } + + private void FireFirstWrite() + { + if (!_firstWriteDone) + { + _firstWriteDone = true; + OnFirstWrite?.Invoke(); + } + } + } + } +} diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayHttpApiV2Calls.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayHttpApiV2Calls.cs index 1b844bf1e..b28f82cc2 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayHttpApiV2Calls.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayHttpApiV2Calls.cs @@ -1,11 +1,8 @@ using System; -using System.Collections.Generic; using System.IO; -using System.IO.Compression; using System.Linq; using System.Net; using System.Reflection; -using System.Text; using System.Threading; using System.Threading.Tasks; @@ -32,7 +29,7 @@ public async Task TestValuesGetAllFromBetaStage() { var context = new TestLambdaContext(); - var response = await this.InvokeAPIGatewayRequest(context, "values-get-all-httpapi-v2-with-stage.json"); + var response = await InvokeAPIGatewayRequest(context, "values-get-all-httpapi-v2-with-stage.json"); Assert.Equal(200, response.StatusCode); Assert.Equal("[\"value1\",\"value2\"]", response.Body); @@ -45,7 +42,7 @@ public async Task TestValuesGetAllFromBetaStage() [Fact] public async Task TestGetBinaryContent() { - var response = await this.InvokeAPIGatewayRequest("values-get-binary-httpapi-v2-request.json"); + var response = await InvokeAPIGatewayRequest("values-get-binary-httpapi-v2-request.json"); Assert.Equal((int)HttpStatusCode.OK, response.StatusCode); @@ -68,7 +65,7 @@ public async Task TestGetBinaryContent() [Fact] public async Task TestEncodePlusInResourcePath() { - var response = await this.InvokeAPIGatewayRequest("encode-plus-in-resource-path-httpapi-v2.json"); + var response = await InvokeAPIGatewayRequest("encode-plus-in-resource-path-httpapi-v2.json"); Assert.Equal(200, response.StatusCode); @@ -79,7 +76,7 @@ public async Task TestEncodePlusInResourcePath() [Fact] public async Task TestGetQueryStringValueMV() { - var response = await this.InvokeAPIGatewayRequest("values-get-querystring-httpapi-v2-mv-request.json"); + var response = await InvokeAPIGatewayRequest("values-get-querystring-httpapi-v2-mv-request.json"); Assert.Equal("value1,value2", response.Body); Assert.True(response.Headers.ContainsKey("Content-Type")); @@ -89,7 +86,7 @@ public async Task TestGetQueryStringValueMV() [Fact] public async Task TestGetEncodingQueryStringGateway() { - var response = await this.InvokeAPIGatewayRequest("values-get-querystring-httpapi-v2-encoding-request.json"); + var response = await InvokeAPIGatewayRequest("values-get-querystring-httpapi-v2-encoding-request.json"); var results = JsonConvert.DeserializeObject(response.Body); Assert.Equal("http://www.google.com", results.Url); Assert.Equal(DateTimeOffset.Parse("2019-03-12T16:06:06.549817+00:00"), results.TestDateTimeOffset); @@ -101,7 +98,7 @@ public async Task TestGetEncodingQueryStringGateway() [Fact] public async Task TestPutWithBody() { - var response = await this.InvokeAPIGatewayRequest("values-put-withbody-httpapi-v2-request.json"); + var response = await InvokeAPIGatewayRequest("values-put-withbody-httpapi-v2-request.json"); Assert.Equal(200, response.StatusCode); Assert.Equal("Agent, Smith", response.Body); @@ -112,7 +109,7 @@ public async Task TestPutWithBody() [Fact] public async Task TestDefaultResponseErrorCode() { - var response = await this.InvokeAPIGatewayRequest("values-get-error-httpapi-v2-request.json"); + var response = await InvokeAPIGatewayRequest("values-get-error-httpapi-v2-request.json"); Assert.Equal(500, response.StatusCode); Assert.Equal(string.Empty, response.Body); @@ -125,7 +122,7 @@ public async Task TestDefaultResponseErrorCode() [InlineData("values-get-typeloaderror-httpapi-v2-request.json", "ReflectionTypeLoadException", false)] public async Task TestEnhancedExceptions(string requestFileName, string expectedExceptionType, bool configureApiToReturnExceptionDetail) { - var response = await this.InvokeAPIGatewayRequest(requestFileName, configureApiToReturnExceptionDetail); + var response = await InvokeAPIGatewayRequest(requestFileName, configureApiToReturnExceptionDetail); Assert.Equal(500, response.StatusCode); Assert.Equal(string.Empty, response.Body); @@ -143,7 +140,7 @@ public async Task TestEnhancedExceptions(string requestFileName, string expected [Fact] public async Task TestGettingSwaggerDefinition() { - var response = await this.InvokeAPIGatewayRequest("swagger-get-httpapi-v2-request.json"); + var response = await InvokeAPIGatewayRequest("swagger-get-httpapi-v2-request.json"); Assert.Equal(200, response.StatusCode); Assert.True(response.Body.Length > 0); @@ -153,7 +150,7 @@ public async Task TestGettingSwaggerDefinition() [Fact] public async Task TestEncodeSpaceInResourcePath() { - var response = await this.InvokeAPIGatewayRequest("encode-space-in-resource-path-httpapi-v2.json"); + var response = await InvokeAPIGatewayRequest("encode-space-in-resource-path-httpapi-v2.json"); Assert.Equal(200, response.StatusCode); Assert.Equal("value=tmh/file name.xml", response.Body); @@ -164,12 +161,12 @@ public async Task TestEncodeSpaceInResourcePath() public async Task TestEncodeSlashInResourcePath() { var requestStr = GetRequestContent("encode-slash-in-resource-path-httpapi-v2.json"); - var response = await this.InvokeAPIGatewayRequestWithContent(new TestLambdaContext(), requestStr); + var response = await InvokeAPIGatewayRequestWithContent(new TestLambdaContext(), requestStr); Assert.Equal(200, response.StatusCode); Assert.Equal("{\"only\":\"a%2Fb\"}", response.Body); - response = await this.InvokeAPIGatewayRequestWithContent(new TestLambdaContext(), requestStr.Replace("a%2Fb", "a/b")); + response = await InvokeAPIGatewayRequestWithContent(new TestLambdaContext(), requestStr.Replace("a%2Fb", "a/b")); Assert.Equal(200, response.StatusCode); Assert.Equal("{\"first\":\"a\",\"second\":\"b\"}", response.Body); @@ -178,7 +175,7 @@ public async Task TestEncodeSlashInResourcePath() [Fact] public async Task TestTrailingSlashInPath() { - var response = await this.InvokeAPIGatewayRequest("trailing-slash-in-path-httpapi-v2.json"); + var response = await InvokeAPIGatewayRequest("trailing-slash-in-path-httpapi-v2.json"); Assert.Equal(200, response.StatusCode); @@ -194,7 +191,7 @@ public async Task TestTrailingSlashInPath() [InlineData("rawtarget-escaped-slash-in-path-httpapi-v2.json", "/foo%2Fbar")] public async Task TestRawTarget(string requestFileName, string expectedRawTarget) { - var response = await this.InvokeAPIGatewayRequest(requestFileName); + var response = await InvokeAPIGatewayRequest(requestFileName); Assert.Equal(200, response.StatusCode); @@ -205,7 +202,7 @@ public async Task TestRawTarget(string requestFileName, string expectedRawTarget [Fact] public async Task TestAuthTestAccess() { - var response = await this.InvokeAPIGatewayRequest("authtest-access-request-httpapi-v2.json"); + var response = await InvokeAPIGatewayRequest("authtest-access-request-httpapi-v2.json"); Assert.Equal(200, response.StatusCode); Assert.Equal("You Have Access", response.Body); @@ -214,7 +211,7 @@ public async Task TestAuthTestAccess() [Fact] public async Task TestAuthTestNoAccess() { - var response = await this.InvokeAPIGatewayRequest("authtest-noaccess-request-httpapi-v2.json"); + var response = await InvokeAPIGatewayRequest("authtest-noaccess-request-httpapi-v2.json"); Assert.NotEqual(200, response.StatusCode); } @@ -222,7 +219,7 @@ public async Task TestAuthTestNoAccess() [Fact] public async Task TestAuthMTls() { - var response = await this.InvokeAPIGatewayRequest("mtls-request-httpapi-v2.json"); + var response = await InvokeAPIGatewayRequest("mtls-request-httpapi-v2.json"); Assert.Equal(200, response.StatusCode); Assert.Equal("O=Internet Widgits Pty Ltd, S=Some-State, C=AU", response.Body); } @@ -230,7 +227,7 @@ public async Task TestAuthMTls() [Fact] public async Task TestReturningCookie() { - var response = await this.InvokeAPIGatewayRequest("cookies-get-returned-httpapi-v2-request.json"); + var response = await InvokeAPIGatewayRequest("cookies-get-returned-httpapi-v2-request.json"); Assert.Collection(response.Cookies, actual => Assert.StartsWith("TestCookie=TestValue", actual)); @@ -239,7 +236,7 @@ public async Task TestReturningCookie() [Fact] public async Task TestReturningMultipleCookies() { - var response = await this.InvokeAPIGatewayRequest("cookies-get-multiple-returned-httpapi-v2-request.json"); + var response = await InvokeAPIGatewayRequest("cookies-get-multiple-returned-httpapi-v2-request.json"); Assert.Collection(response.Cookies.OrderBy(s => s), actual => Assert.StartsWith("TestCookie1=TestValue1", actual), @@ -249,7 +246,7 @@ public async Task TestReturningMultipleCookies() [Fact] public async Task TestSingleCookie() { - var response = await this.InvokeAPIGatewayRequest("cookies-get-single-httpapi-v2-request.json"); + var response = await InvokeAPIGatewayRequest("cookies-get-single-httpapi-v2-request.json"); Assert.Equal("TestValue", response.Body); } @@ -257,7 +254,7 @@ public async Task TestSingleCookie() [Fact] public async Task TestMultipleCookie() { - var response = await this.InvokeAPIGatewayRequest("cookies-get-multiple-httpapi-v2-request.json"); + var response = await InvokeAPIGatewayRequest("cookies-get-multiple-httpapi-v2-request.json"); Assert.Equal("TestValue3", response.Body); } @@ -268,15 +265,15 @@ public async Task TestTraceIdSetFromLambdaContext() try { Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "MyTraceId-1"); - var response = await this.InvokeAPIGatewayRequest("traceid-get-httpapi-v2-request.json"); + var response = await InvokeAPIGatewayRequest("traceid-get-httpapi-v2-request.json"); Assert.Equal("MyTraceId-1", response.Body); Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", "MyTraceId-2"); - response = await this.InvokeAPIGatewayRequest("traceid-get-httpapi-v2-request.json"); + response = await InvokeAPIGatewayRequest("traceid-get-httpapi-v2-request.json"); Assert.Equal("MyTraceId-2", response.Body); Environment.SetEnvironmentVariable("_X_AMZN_TRACE_ID", null); - response = await this.InvokeAPIGatewayRequest("traceid-get-httpapi-v2-request.json"); + response = await InvokeAPIGatewayRequest("traceid-get-httpapi-v2-request.json"); Assert.True(!string.IsNullOrEmpty(response.Body) && !string.Equals(response.Body, "MyTraceId-2")); } finally @@ -285,7 +282,6 @@ public async Task TestTraceIdSetFromLambdaContext() } } - #if NET8_0_OR_GREATER /// /// Verifies that is invoked during startup. /// @@ -313,7 +309,6 @@ public async Task TestSnapStartInitialization() Assert.True(SnapStartController.Invoked); } - #endif private async Task InvokeAPIGatewayRequest(string fileName, bool configureApiToReturnExceptionDetail = false) { @@ -338,7 +333,7 @@ private async Task InvokeAPIGatewayRequestWith private string GetRequestContent(string fileName) { - var filePath = Path.Combine(Path.GetDirectoryName(this.GetType().GetTypeInfo().Assembly.Location), fileName); + var filePath = Path.Combine(Path.GetDirectoryName(GetType().GetTypeInfo().Assembly.Location), fileName); var requestStr = File.ReadAllText(filePath); return requestStr; } @@ -346,8 +341,8 @@ private string GetRequestContent(string fileName) public class EnvironmentVariableHelper : IDisposable { - private string _name; - private string? _oldValue; + private readonly string _name; + private readonly string _oldValue; public EnvironmentVariableHelper(string name, string value) { _name = name; diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayWebsocketApiCalls.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayWebsocketApiCalls.cs new file mode 100644 index 000000000..07d0551da --- /dev/null +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApiGatewayWebsocketApiCalls.cs @@ -0,0 +1,58 @@ +using System; +using System.IO; +using System.Reflection; +using System.Threading.Tasks; + +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.TestUtilities; + +using TestWebApp; + +using Xunit; + + + +namespace Amazon.Lambda.AspNetCoreServer.Test +{ + public class TestApiGatewayWebsocketApiCalls + { + [Fact] + public async Task TestPostWithBody() + { + var response = await InvokeAPIGatewayRequest("values-post-withbody-websocketapi-request.json"); + + Assert.Equal(200, response.StatusCode); + Assert.Equal("Agent, Smith", response.Body); + Assert.True(response.MultiValueHeaders.ContainsKey("Content-Type")); + Assert.Equal("text/plain; charset=utf-8", response.MultiValueHeaders["Content-Type"][0]); + } + + private async Task InvokeAPIGatewayRequest(string fileName, bool configureApiToReturnExceptionDetail = false) + { + return await InvokeAPIGatewayRequestWithContent(new TestLambdaContext(), GetRequestContent(fileName), configureApiToReturnExceptionDetail); + } + + private async Task InvokeAPIGatewayRequest(TestLambdaContext context, string fileName, bool configureApiToReturnExceptionDetail = false) + { + return await InvokeAPIGatewayRequestWithContent(context, GetRequestContent(fileName), configureApiToReturnExceptionDetail); + } + + private async Task InvokeAPIGatewayRequestWithContent(TestLambdaContext context, string requestContent, bool configureApiToReturnExceptionDetail = false) + { + var lambdaFunction = new TestWebApp.WebsocketLambdaFunction(); + if (configureApiToReturnExceptionDetail) + lambdaFunction.IncludeUnhandledExceptionDetailInResponse = true; + var requestStream = new MemoryStream(System.Text.UTF8Encoding.UTF8.GetBytes(requestContent)); + var request = new Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer().Deserialize(requestStream); + + return await lambdaFunction.FunctionHandlerAsync(request, context); + } + + private string GetRequestContent(string fileName) + { + var filePath = Path.Combine(Path.GetDirectoryName(GetType().GetTypeInfo().Assembly.Location), fileName); + var requestStr = File.ReadAllText(filePath); + return requestStr; + } + } +} diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApplicationLoadBalancerCalls.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApplicationLoadBalancerCalls.cs index ed2272045..5a76d8b54 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApplicationLoadBalancerCalls.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestApplicationLoadBalancerCalls.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.IO.Compression; using System.Linq; @@ -25,7 +25,7 @@ public async Task TestGetAllValues() { var context = new TestLambdaContext(); - var response = await this.InvokeApplicationLoadBalancerRequest(context, "values-get-all-alb-request.json"); + var response = await InvokeApplicationLoadBalancerRequest(context, "values-get-all-alb-request.json"); Assert.Equal(200, response.StatusCode); Assert.Equal("[\"value1\",\"value2\"]", response.Body); @@ -38,7 +38,7 @@ public async Task TestGetAllValues() [Fact] public async Task TestGetQueryStringValue() { - var response = await this.InvokeApplicationLoadBalancerRequest("values-get-querystring-alb-request.json"); + var response = await InvokeApplicationLoadBalancerRequest("values-get-querystring-alb-request.json"); Assert.Equal("Lewis, Meriwether", response.Body); Assert.True(response.Headers.ContainsKey("Content-Type")); @@ -48,7 +48,7 @@ public async Task TestGetQueryStringValue() [Fact] public async Task TestGetNoQueryStringAlb() { - var response = await this.InvokeApplicationLoadBalancerRequest("values-get-no-querystring-alb-request.json"); + var response = await InvokeApplicationLoadBalancerRequest("values-get-no-querystring-alb-request.json"); Assert.Equal(string.Empty, response.Body); Assert.True(response.Headers.ContainsKey("Content-Type")); @@ -58,14 +58,14 @@ public async Task TestGetNoQueryStringAlb() [Fact] public async Task TestGetNoQueryStringAlbMv() { - var response = await this.InvokeApplicationLoadBalancerRequest("values-get-no-querystring-alb-mv-request.json"); + var response = await InvokeApplicationLoadBalancerRequest("values-get-no-querystring-alb-mv-request.json"); Assert.Equal(string.Empty, response.Body); } [Fact] public async Task TestGetEncodingQueryStringAlb() { - var response = await this.InvokeApplicationLoadBalancerRequest("values-get-querystring-alb-encoding-request.json"); + var response = await InvokeApplicationLoadBalancerRequest("values-get-querystring-alb-encoding-request.json"); var results = JsonConvert.DeserializeObject(response.Body); Assert.Equal("http://www.gooogle.com", results.Url); Assert.Equal(DateTimeOffset.Parse("2019-03-12T16:06:06.549817+00:00"), results.TestDateTimeOffset); @@ -77,7 +77,7 @@ public async Task TestGetEncodingQueryStringAlb() [Fact] public async Task TestGetEncodingQueryStringAlbMv() { - var response = await this.InvokeApplicationLoadBalancerRequest("values-get-querystring-alb-mv-encoding-request.json"); + var response = await InvokeApplicationLoadBalancerRequest("values-get-querystring-alb-mv-encoding-request.json"); var results = JsonConvert.DeserializeObject(response.Body); Assert.Equal("http://www.gooogle.com", results.Url); Assert.Equal(DateTimeOffset.Parse("2019-03-12T16:06:06.549817+00:00"), results.TestDateTimeOffset); @@ -89,7 +89,7 @@ public async Task TestGetEncodingQueryStringAlbMv() [Fact] public async Task TestGetQueryStringValueMV() { - var response = await this.InvokeApplicationLoadBalancerRequest("values-get-querystring-alb-mv-request.json"); + var response = await InvokeApplicationLoadBalancerRequest("values-get-querystring-alb-mv-request.json"); Assert.Equal("value1,value2", response.Body); Assert.True(response.MultiValueHeaders.ContainsKey("Content-Type")); @@ -99,7 +99,7 @@ public async Task TestGetQueryStringValueMV() [Fact] public async Task TestPutWithBody() { - var response = await this.InvokeApplicationLoadBalancerRequest("values-put-withbody-alb-request.json"); + var response = await InvokeApplicationLoadBalancerRequest("values-put-withbody-alb-request.json"); Assert.Equal(200, response.StatusCode); Assert.Equal("Agent, Smith", response.Body); @@ -110,7 +110,7 @@ public async Task TestPutWithBody() [Fact] public async Task TestPutWithBodyMV() { - var response = await this.InvokeApplicationLoadBalancerRequest("values-put-withbody-alb-mv-request.json"); + var response = await InvokeApplicationLoadBalancerRequest("values-put-withbody-alb-mv-request.json"); Assert.Equal(200, response.StatusCode); Assert.Equal("Agent, Smith", response.Body); @@ -121,7 +121,7 @@ public async Task TestPutWithBodyMV() [Fact] public async Task TestGetSingleValue() { - var response = await this.InvokeApplicationLoadBalancerRequest("values-get-single-alb-request.json"); + var response = await InvokeApplicationLoadBalancerRequest("values-get-single-alb-request.json"); Assert.Equal("value=5", response.Body); Assert.True(response.Headers.ContainsKey("Content-Type")); @@ -131,7 +131,7 @@ public async Task TestGetSingleValue() [Fact] public async Task TestGetBinaryContent() { - var response = await this.InvokeApplicationLoadBalancerRequest("values-get-binary-alb-request.json"); + var response = await InvokeApplicationLoadBalancerRequest("values-get-binary-alb-request.json"); Assert.Equal((int)HttpStatusCode.OK, response.StatusCode); @@ -154,7 +154,7 @@ public async Task TestGetBinaryContent() [Fact] public async Task TestPutBinaryContent() { - var response = await this.InvokeApplicationLoadBalancerRequest("values-put-binary-alb-request.json"); + var response = await InvokeApplicationLoadBalancerRequest("values-put-binary-alb-request.json"); Assert.Equal((int)HttpStatusCode.OK, response.StatusCode); @@ -167,7 +167,7 @@ public async Task TestPutBinaryContent() [Fact] public async Task TestHealthCheck() { - var response = await this.InvokeApplicationLoadBalancerRequest("alb-healthcheck.json"); + var response = await InvokeApplicationLoadBalancerRequest("alb-healthcheck.json"); Assert.Equal(200, response.StatusCode); Assert.Equal("[\"value1\",\"value2\"]", response.Body); @@ -178,14 +178,14 @@ public async Task TestHealthCheck() [Fact] public async Task TestContentLengthWithContent() { - var response = await this.InvokeApplicationLoadBalancerRequest("check-content-length-withcontent-alb.json"); + var response = await InvokeApplicationLoadBalancerRequest("check-content-length-withcontent-alb.json"); Assert.Equal("Request content length: 17", response.Body.Trim()); } [Fact] public async Task TestContentLengthNoContent() { - var response = await this.InvokeApplicationLoadBalancerRequest("check-content-length-nocontent-alb.json"); + var response = await InvokeApplicationLoadBalancerRequest("check-content-length-nocontent-alb.json"); Assert.Equal("Request content length: 0", response.Body.Trim()); } @@ -194,7 +194,7 @@ public async Task TestGetCompressResponse() { var context = new TestLambdaContext(); - var response = await this.InvokeApplicationLoadBalancerRequest(context, "compressresponse-get-alb-request.json"); + var response = await InvokeApplicationLoadBalancerRequest(context, "compressresponse-get-alb-request.json"); Assert.Equal(200, response.StatusCode); @@ -227,7 +227,7 @@ public async Task TestGetCompressResponse() [InlineData("rawtarget-escaped-slash-in-path-alb.json", "/foo%2Fbar")] public async Task TestRawTarget(string requestFileName, string expectedRawTarget) { - var response = await this.InvokeApplicationLoadBalancerRequest(requestFileName); + var response = await InvokeApplicationLoadBalancerRequest(requestFileName); Assert.Equal(200, response.StatusCode); @@ -243,7 +243,7 @@ private async Task InvokeApplicationLoadBalance private async Task InvokeApplicationLoadBalancerRequest(TestLambdaContext context, string fileName) { var lambdaFunction = new ALBLambdaFunction(); - var filePath = Path.Combine(Path.GetDirectoryName(this.GetType().GetTypeInfo().Assembly.Location), fileName); + var filePath = Path.Combine(Path.GetDirectoryName(GetType().GetTypeInfo().Assembly.Location), fileName); var requestStr = File.ReadAllText(filePath); var request = JsonConvert.DeserializeObject(requestStr); return await lambdaFunction.FunctionHandlerAsync(request, context); diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestCallingWebAPI.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestCallingWebAPI.cs index 042b2e46b..9743da7db 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestCallingWebAPI.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestCallingWebAPI.cs @@ -33,7 +33,7 @@ public async Task TestHttpApiGetAllValues() { var context = new TestLambdaContext(); - var response = await this.InvokeAPIGatewayRequest(context, "values-get-all-httpapi-request.json"); + var response = await InvokeAPIGatewayRequest(context, "values-get-all-httpapi-request.json"); Assert.Equal(200, response.StatusCode); Assert.Equal("[\"value1\",\"value2\"]", response.Body); @@ -49,7 +49,7 @@ public async Task TestGetAllValues() { var context = new TestLambdaContext(); - var response = await this.InvokeAPIGatewayRequest(context, "values-get-all-apigateway-request.json"); + var response = await InvokeAPIGatewayRequest(context, "values-get-all-apigateway-request.json"); Assert.Equal(200, response.StatusCode); Assert.Equal("[\"value1\",\"value2\"]", response.Body); @@ -62,7 +62,7 @@ public async Task TestGetAllValues() [Fact] public async Task TestGetAllValuesWithCustomPath() { - var response = await this.InvokeAPIGatewayRequest("values-get-different-proxypath-apigateway-request.json"); + var response = await InvokeAPIGatewayRequest("values-get-different-proxypath-apigateway-request.json"); Assert.Equal(200, response.StatusCode); Assert.Equal("[\"value1\",\"value2\"]", response.Body); @@ -73,7 +73,7 @@ public async Task TestGetAllValuesWithCustomPath() [Fact] public async Task TestGetSingleValue() { - var response = await this.InvokeAPIGatewayRequest("values-get-single-apigateway-request.json"); + var response = await InvokeAPIGatewayRequest("values-get-single-apigateway-request.json"); Assert.Equal("value=5", response.Body); Assert.True(response.MultiValueHeaders.ContainsKey("Content-Type")); @@ -83,7 +83,7 @@ public async Task TestGetSingleValue() [Fact] public async Task TestGetQueryStringValue() { - var response = await this.InvokeAPIGatewayRequest("values-get-querystring-apigateway-request.json"); + var response = await InvokeAPIGatewayRequest("values-get-querystring-apigateway-request.json"); Assert.Equal("Lewis, Meriwether", response.Body); Assert.True(response.MultiValueHeaders.ContainsKey("Content-Type")); @@ -93,7 +93,7 @@ public async Task TestGetQueryStringValue() [Fact] public async Task TestGetNoQueryStringApiGateway() { - var response = await this.InvokeAPIGatewayRequest("values-get-no-querystring-apigateway-request.json"); + var response = await InvokeAPIGatewayRequest("values-get-no-querystring-apigateway-request.json"); Assert.Equal(string.Empty, response.Body); Assert.True(response.MultiValueHeaders.ContainsKey("Content-Type")); @@ -103,7 +103,7 @@ public async Task TestGetNoQueryStringApiGateway() [Fact] public async Task TestGetEncodingQueryStringGateway() { - var response = await this.InvokeAPIGatewayRequest("values-get-querystring-apigateway-encoding-request.json"); + var response = await InvokeAPIGatewayRequest("values-get-querystring-apigateway-encoding-request.json"); var results = JsonSerializer.Deserialize(response.Body, new JsonSerializerOptions { PropertyNameCaseInsensitive = true @@ -118,7 +118,7 @@ public async Task TestGetEncodingQueryStringGateway() [Fact] public async Task TestPutWithBody() { - var response = await this.InvokeAPIGatewayRequest("values-put-withbody-apigateway-request.json"); + var response = await InvokeAPIGatewayRequest("values-put-withbody-apigateway-request.json"); Assert.Equal(200, response.StatusCode); Assert.Equal("Agent, Smith", response.Body); @@ -129,7 +129,7 @@ public async Task TestPutWithBody() [Fact] public async Task TestPutNoBody() { - var response = await this.InvokeAPIGatewayRequest("values-put-no-body-request.json"); + var response = await InvokeAPIGatewayRequest("values-put-no-body-request.json"); Assert.Equal(string.Empty, response.Body); Assert.Equal(202, response.StatusCode); @@ -138,7 +138,7 @@ public async Task TestPutNoBody() [Fact] public async Task TestDefaultResponseErrorCode() { - var response = await this.InvokeAPIGatewayRequest("values-get-error-apigateway-request.json"); + var response = await InvokeAPIGatewayRequest("values-get-error-apigateway-request.json"); Assert.Equal(500, response.StatusCode); Assert.Equal(string.Empty, response.Body); @@ -151,7 +151,7 @@ public async Task TestDefaultResponseErrorCode() [InlineData("values-get-typeloaderror-apigateway-request.json", "ReflectionTypeLoadException", false)] public async Task TestEnhancedExceptions(string requestFileName, string expectedExceptionType, bool configureApiToReturnExceptionDetail) { - var response = await this.InvokeAPIGatewayRequest(requestFileName, configureApiToReturnExceptionDetail); + var response = await InvokeAPIGatewayRequest(requestFileName, configureApiToReturnExceptionDetail); Assert.Equal(500, response.StatusCode); Assert.Equal(string.Empty, response.Body); @@ -169,7 +169,7 @@ public async Task TestEnhancedExceptions(string requestFileName, string expected [Fact] public async Task TestGettingSwaggerDefinition() { - var response = await this.InvokeAPIGatewayRequest("swagger-get-apigateway-request.json"); + var response = await InvokeAPIGatewayRequest("swagger-get-apigateway-request.json"); Assert.Equal(200, response.StatusCode); Assert.True(response.Body.Length > 0); @@ -232,7 +232,7 @@ public void TestCustomAuthorizerSerialization() [Fact] public async Task TestGetBinaryContent() { - var response = await this.InvokeAPIGatewayRequest("values-get-binary-apigateway-request.json"); + var response = await InvokeAPIGatewayRequest("values-get-binary-apigateway-request.json"); Assert.Equal((int) HttpStatusCode.OK, response.StatusCode); @@ -255,7 +255,7 @@ public async Task TestGetBinaryContent() [Fact] public async Task TestEncodePlusInResourcePath() { - var response = await this.InvokeAPIGatewayRequest("encode-plus-in-resource-path.json"); + var response = await InvokeAPIGatewayRequest("encode-plus-in-resource-path.json"); Assert.Equal(200, response.StatusCode); @@ -267,7 +267,7 @@ public async Task TestEncodePlusInResourcePath() public async Task TestEncodeSpaceInResourcePath() { var requestStr = GetRequestContent("encode-space-in-resource-path.json"); - var response = await this.InvokeAPIGatewayRequest("encode-space-in-resource-path.json"); + var response = await InvokeAPIGatewayRequest("encode-space-in-resource-path.json"); Assert.Equal(200, response.StatusCode); Assert.Equal("value=tmh/file name.xml", response.Body); @@ -278,12 +278,12 @@ public async Task TestEncodeSpaceInResourcePath() public async Task TestEncodeSlashInResourcePath() { var requestStr = GetRequestContent("encode-slash-in-resource-path.json"); - var response = await this.InvokeAPIGatewayRequestWithContent(new TestLambdaContext(), requestStr); + var response = await InvokeAPIGatewayRequestWithContent(new TestLambdaContext(), requestStr); Assert.Equal(200, response.StatusCode); Assert.Equal("{\"only\":\"a%2Fb\"}", response.Body); - response = await this.InvokeAPIGatewayRequestWithContent(new TestLambdaContext(), requestStr.Replace("a%2Fb", "a/b")); + response = await InvokeAPIGatewayRequestWithContent(new TestLambdaContext(), requestStr.Replace("a%2Fb", "a/b")); Assert.Equal(200, response.StatusCode); Assert.Equal("{\"first\":\"a\",\"second\":\"b\"}", response.Body); @@ -292,7 +292,7 @@ public async Task TestEncodeSlashInResourcePath() [Fact] public async Task TestAdditionalPathParametersInProxyPath() { - var response = await this.InvokeAPIGatewayRequest("additional-path-parameters-in-proxy-path.json"); + var response = await InvokeAPIGatewayRequest("additional-path-parameters-in-proxy-path.json"); Assert.Equal(200, response.StatusCode); var root = JsonSerializer.Deserialize(response.Body); @@ -302,7 +302,7 @@ public async Task TestAdditionalPathParametersInProxyPath() [Fact] public async Task TestAdditionalPathParametersInNonProxyPath() { - var response = await this.InvokeAPIGatewayRequest("additional-path-parameters-in-non-proxy-path.json"); + var response = await InvokeAPIGatewayRequest("additional-path-parameters-in-non-proxy-path.json"); Assert.Equal(200, response.StatusCode); var root = JsonSerializer.Deserialize(response.Body); @@ -312,7 +312,7 @@ public async Task TestAdditionalPathParametersInNonProxyPath() [Fact] public async Task TestSpaceInResourcePathAndQueryString() { - var response = await this.InvokeAPIGatewayRequest("encode-space-in-resource-path-and-query.json"); + var response = await InvokeAPIGatewayRequest("encode-space-in-resource-path-and-query.json"); Assert.Equal(200, response.StatusCode); @@ -326,7 +326,7 @@ public async Task TestSpaceInResourcePathAndQueryString() [Fact] public async Task TestTrailingSlashInPath() { - var response = await this.InvokeAPIGatewayRequest("trailing-slash-in-path.json"); + var response = await InvokeAPIGatewayRequest("trailing-slash-in-path.json"); Assert.Equal(200, response.StatusCode); @@ -342,7 +342,7 @@ public async Task TestTrailingSlashInPath() [InlineData("rawtarget-escaped-slash-in-path.json", "/foo%2Fbar")] public async Task TestRawTarget(string requestFileName, string expectedRawTarget) { - var response = await this.InvokeAPIGatewayRequest(requestFileName); + var response = await InvokeAPIGatewayRequest(requestFileName); Assert.Equal(200, response.StatusCode); @@ -353,7 +353,7 @@ public async Task TestRawTarget(string requestFileName, string expectedRawTarget [Fact] public async Task TestAuthTestAccess() { - var response = await this.InvokeAPIGatewayRequest("authtest-access-request.json"); + var response = await InvokeAPIGatewayRequest("authtest-access-request.json"); Assert.Equal(200, response.StatusCode); Assert.Equal("You Have Access", response.Body); @@ -362,7 +362,7 @@ public async Task TestAuthTestAccess() [Fact] public async Task TestAuthMTls() { - var response = await this.InvokeAPIGatewayRequest("mtls-request.json"); + var response = await InvokeAPIGatewayRequest("mtls-request.json"); Assert.Equal(200, response.StatusCode); Assert.Equal("O=Internet Widgits Pty Ltd, S=Some-State, C=AU", response.Body); } @@ -370,7 +370,7 @@ public async Task TestAuthMTls() [Fact] public async Task TestAuthMTlsWithTrailingNewLine() { - var response = await this.InvokeAPIGatewayRequest("mtls-request-trailing-newline.json"); + var response = await InvokeAPIGatewayRequest("mtls-request-trailing-newline.json"); Assert.Equal(200, response.StatusCode); Assert.Equal("O=Internet Widgits Pty Ltd, S=Some-State, C=AU", response.Body); } @@ -379,7 +379,7 @@ public async Task TestAuthMTlsWithTrailingNewLine() public async Task TestAuthTestAccess_CustomLambdaAuthorizerClaims() { var response = - await this.InvokeAPIGatewayRequest("authtest-access-request-custom-lambda-authorizer-output.json"); + await InvokeAPIGatewayRequest("authtest-access-request-custom-lambda-authorizer-output.json"); Assert.Equal(200, response.StatusCode); Assert.Equal("You Have Access", response.Body); @@ -388,7 +388,7 @@ public async Task TestAuthTestAccess_CustomLambdaAuthorizerClaims() [Fact] public async Task TestAuthTestNoAccess() { - var response = await this.InvokeAPIGatewayRequest("authtest-noaccess-request.json"); + var response = await InvokeAPIGatewayRequest("authtest-noaccess-request.json"); Assert.NotEqual(200, response.StatusCode); } @@ -397,7 +397,7 @@ public async Task TestAuthTestNoAccess() [Fact] public async Task TestMissingResourceInRequest() { - var response = await this.InvokeAPIGatewayRequest("missing-resource-request.json"); + var response = await InvokeAPIGatewayRequest("missing-resource-request.json"); Assert.Equal(200, response.StatusCode); Assert.True(response.Body.Length > 0); @@ -408,22 +408,22 @@ public async Task TestMissingResourceInRequest() [Fact] public async Task TestDeleteNoContentContentType() { - var response = await this.InvokeAPIGatewayRequest("values-delete-no-content-type-apigateway-request.json"); + var response = await InvokeAPIGatewayRequest("values-delete-no-content-type-apigateway-request.json"); Assert.Equal(200, response.StatusCode); Assert.True(response.Body.Length == 0); - Assert.Equal(1, response.MultiValueHeaders["Content-Type"].Count); + Assert.Single(response.MultiValueHeaders["Content-Type"]); Assert.Null(response.MultiValueHeaders["Content-Type"][0]); } [Fact] public async Task TestRedirectNoContentType() { - var response = await this.InvokeAPIGatewayRequest("redirect-apigateway-request.json"); + var response = await InvokeAPIGatewayRequest("redirect-apigateway-request.json"); Assert.Equal(302, response.StatusCode); Assert.True(response.Body.Length == 0); - Assert.Equal(1, response.MultiValueHeaders["Content-Type"].Count); + Assert.Single(response.MultiValueHeaders["Content-Type"]); Assert.Null(response.MultiValueHeaders["Content-Type"][0]); Assert.Equal("redirecttarget", response.MultiValueHeaders["Location"][0]); @@ -432,14 +432,14 @@ public async Task TestRedirectNoContentType() [Fact] public async Task TestContentLengthWithContent() { - var response = await this.InvokeAPIGatewayRequest("check-content-length-withcontent-apigateway.json"); + var response = await InvokeAPIGatewayRequest("check-content-length-withcontent-apigateway.json"); Assert.Equal("Request content length: 17", response.Body.Trim()); } [Fact] public async Task TestContentLengthNoContent() { - var response = await this.InvokeAPIGatewayRequest("check-content-length-nocontent-apigateway.json"); + var response = await InvokeAPIGatewayRequest("check-content-length-nocontent-apigateway.json"); Assert.Equal("Request content length: 0", response.Body.Trim()); } @@ -448,7 +448,7 @@ public async Task TestGetCompressResponse() { var context = new TestLambdaContext(); - var response = await this.InvokeAPIGatewayRequest(context, "compressresponse-get-apigateway-request.json"); + var response = await InvokeAPIGatewayRequest(context, "compressresponse-get-apigateway-request.json"); Assert.Equal(200, response.StatusCode); @@ -476,7 +476,7 @@ public async Task TestGetCompressResponse() public async Task TestRequestServicesAreAvailable() { var requestStr = GetRequestContent("requestservices-get-apigateway-request.json"); - var response = await this.InvokeAPIGatewayRequestWithContent(new TestLambdaContext(), requestStr); + var response = await InvokeAPIGatewayRequestWithContent(new TestLambdaContext(), requestStr); Assert.Equal(200, response.StatusCode); Assert.Equal("Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope", response.Body); @@ -540,7 +540,7 @@ private async Task InvokeAPIGatewayRequestWithContent(T private string GetRequestContent(string fileName) { - var filePath = Path.Combine(Path.GetDirectoryName(this.GetType().GetTypeInfo().Assembly.Location), fileName); + var filePath = Path.Combine(Path.GetDirectoryName(GetType().GetTypeInfo().Assembly.Location), fileName); var requestStr = File.ReadAllText(filePath); return requestStr; } diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestMinimalAPI.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestMinimalAPI.cs index 385856acd..22709a1e3 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestMinimalAPI.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/TestMinimalAPI.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -17,11 +17,11 @@ namespace Amazon.Lambda.AspNetCoreServer.Test { public class TestMinimalAPI : IClassFixture { - TestMinimalAPIAppFixture _fixture; + readonly TestMinimalAPIAppFixture _fixture; public TestMinimalAPI(TestMinimalAPI.TestMinimalAPIAppFixture fixture) { - this._fixture = fixture; + _fixture = fixture; } [Fact] @@ -34,7 +34,7 @@ public void TestMapPostComplexType() public class TestMinimalAPIAppFixture : IDisposable { - object lock_process = new object(); + readonly object lock_process = new object(); public TestMinimalAPIAppFixture() { } @@ -46,7 +46,7 @@ public void Dispose() public T ExecuteRequest(string eventFilePath) { - var requestFilePath = Path.Combine(Path.GetDirectoryName(this.GetType().GetTypeInfo().Assembly.Location), eventFilePath); + var requestFilePath = Path.Combine(Path.GetDirectoryName(GetType().GetTypeInfo().Assembly.Location), eventFilePath); var responseFilePath = Path.GetTempFileName(); var comamndArgument = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? $"/c" : $"-c"; @@ -61,6 +61,11 @@ public T ExecuteRequest(string eventFilePath) using var process = Process.Start(processStartInfo); process.WaitForExit(15000); + if (process.ExitCode != 0) + { + throw new Exception("Process failed with exit code: " + process.ExitCode); + } + if(!File.Exists(responseFilePath)) { throw new Exception("No response file found"); @@ -77,7 +82,7 @@ public T ExecuteRequest(string eventFilePath) private string GetTestAppDirectory() { - var path = this.GetType().GetTypeInfo().Assembly.Location; + var path = GetType().GetTypeInfo().Assembly.Location; while(!string.Equals(new DirectoryInfo(path).Name, "test")) { path = Directory.GetParent(path).FullName; @@ -102,7 +107,7 @@ private string GetSystemShell() return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "cmd.exe" : "/bin/sh"; } - private bool TryGetEnvironmentVariable(string variable, out string? value) + private bool TryGetEnvironmentVariable(string variable, out string value) { value = Environment.GetEnvironmentVariable(variable); return !string.IsNullOrEmpty(value); diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/UtilitiesTest.cs b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/UtilitiesTest.cs index 8b9463d7e..f05c0f156 100644 --- a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/UtilitiesTest.cs +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/UtilitiesTest.cs @@ -29,5 +29,57 @@ public void EnsureStatusCodeStartsAtIs200() var feature = new InvokeFeatures() as IHttpResponseFeature; Assert.Equal(200, feature.StatusCode); } + + // Regression test for https://github.com/aws/aws-lambda-dotnet/issues/1702. + // ASP.NET Core's FeatureReferences cache uses Revision to detect when a + // feature has been swapped (e.g. OutputCache/ResponseCompression replacing + // IHttpResponseBodyFeature to wrap the response body). If Set + // does not bump the revision, cached references stay stale and writes + // bypass the wrapper. + [Fact] + public void SetFeatureBumpsRevision() + { + IFeatureCollection features = new InvokeFeatures(); + var initialRevision = features.Revision; + + features.Set(new TestResponseBodyFeature()); + + Assert.NotEqual(initialRevision, features.Revision); + } + + [Fact] + public void SetFeatureStoresAndRetrievesInstance() + { + IFeatureCollection features = new InvokeFeatures(); + var replacement = new TestResponseBodyFeature(); + + features.Set(replacement); + + Assert.Same(replacement, features.Get()); + } + + [Fact] + public void SetFeatureNullRemovesEntryAndBumpsRevision() + { + IFeatureCollection features = new InvokeFeatures(); + // InvokeFeatures seeds itself as the IHttpResponseBodyFeature in its constructor. + Assert.NotNull(features.Get()); + var revisionBeforeRemove = features.Revision; + + features.Set(null); + + Assert.Null(features.Get()); + Assert.NotEqual(revisionBeforeRemove, features.Revision); + } + + private sealed class TestResponseBodyFeature : IHttpResponseBodyFeature + { + public System.IO.Stream Stream => System.IO.Stream.Null; + public System.IO.Pipelines.PipeWriter Writer => System.IO.Pipelines.PipeWriter.Create(System.IO.Stream.Null); + public System.Threading.Tasks.Task CompleteAsync() => System.Threading.Tasks.Task.CompletedTask; + public void DisableBuffering() { } + public System.Threading.Tasks.Task SendFileAsync(string path, long offset, long? count, System.Threading.CancellationToken cancellationToken) => System.Threading.Tasks.Task.CompletedTask; + public System.Threading.Tasks.Task StartAsync(System.Threading.CancellationToken cancellationToken) => System.Threading.Tasks.Task.CompletedTask; + } } } diff --git a/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/values-post-withbody-websocketapi-request.json b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/values-post-withbody-websocketapi-request.json new file mode 100644 index 000000000..20760653c --- /dev/null +++ b/Libraries/test/Amazon.Lambda.AspNetCoreServer.Test/values-post-withbody-websocketapi-request.json @@ -0,0 +1,50 @@ +{ + "resource": null, + "path": null, + "httpMethod": null, + "headers": null, + "queryStringParameters": null, + "stageVariables": null, + "requestContext": { + "path": null, + "accountId": null, + "resourceId": null, + "stage": "test-invoke-stage", + "requestId": "test-invoke-request", + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "apiKey": null, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": null, + "user": null, + "ClientCert": null + }, + "resourcePath": null, + "httpMethod": null, + "apiId": "t2yh6sjnmk", + "extendedRequestId": "amJGnGBlIBMFiTw=", + "connectionId": "amJTNfeGLAMCLCQ=", + "connectedAt": 1725479956267, + "domainName": "8d611s53xy.execute-api.us-east-1.amazonaws.com", + "domainPrefix": null, + "eventType": "MESSAGE", + "messageId": "amJTNfeGLAMCLCQ=", + "routeKey": "$default", + "authorizer": null, + "operationName": null, + "error": null, + "integrationLatency": null, + "messageDirection": "IN", + "requestTime": "04/Sep/2024:19:59:18 +0000", + "requestTimeEpoch": 1725479958896, + "status": null + }, + "body": "{\"firstName\":\"Smith\",\"lastName\": \"Agent\"}", + "isBase64Encoded": false +} \ No newline at end of file diff --git a/Libraries/test/Amazon.Lambda.Core.Tests/Amazon.Lambda.Core.Tests.csproj b/Libraries/test/Amazon.Lambda.Core.Tests/Amazon.Lambda.Core.Tests.csproj index 755b6b95e..5dd21b0b8 100644 --- a/Libraries/test/Amazon.Lambda.Core.Tests/Amazon.Lambda.Core.Tests.csproj +++ b/Libraries/test/Amazon.Lambda.Core.Tests/Amazon.Lambda.Core.Tests.csproj @@ -7,6 +7,8 @@ true ..\..\..\buildtools\public.snk true + + CA2252 @@ -15,10 +17,13 @@ - - - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/Libraries/test/Amazon.Lambda.Core.Tests/StructuredLoggingTests.cs b/Libraries/test/Amazon.Lambda.Core.Tests/StructuredLoggingTests.cs new file mode 100644 index 000000000..f662af109 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Core.Tests/StructuredLoggingTests.cs @@ -0,0 +1,96 @@ +using System; +using System.Reflection; +using System.Text.Json; +using Xunit; + +using Amazon.Lambda.Core; + +namespace Amazon.Lambda.Tests +{ + public class StructuredLoggingTests + { + [Fact] + public void SetConfigureStructuredLoggingAction_CallbackReceivesOptions() + { + StructuredLoggingOptions receivedOptions = null; + + LambdaLogger.SetConfigureStructuredLoggingAction(options => + { + receivedOptions = options; + }); + + var expectedOptions = new StructuredLoggingOptions + { + OverrideSerializerOptions = new JsonSerializerOptions { WriteIndented = true } + }; + + LambdaLogger.ConfigureStructuredLogging(expectedOptions); + + Assert.NotNull(receivedOptions); + Assert.Same(expectedOptions, receivedOptions); + Assert.True(receivedOptions.OverrideSerializerOptions.WriteIndented); + } + + [Fact] + public void ConfigureStructuredLogging_BeforeRuntimeSetsAction_OptionsFlowThroughCurrentAction() + { + StructuredLoggingOptions capturedOptions = null; + LambdaLogger.SetConfigureStructuredLoggingAction(options => + { + capturedOptions = options; + }); + + var userOptions = new StructuredLoggingOptions + { + OverrideSerializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase } + }; + + LambdaLogger.ConfigureStructuredLogging(userOptions); + Assert.Same(userOptions, capturedOptions); + } + + [Fact] + public void SetConfigureStructuredLoggingAction_PendingOptionsForwardedToNewAction() + { + // Reset _placeHolderStructuredLoggingOptions and _configureStructuredLoggingAction to initial state + // so we can test the forwarding path. + var placeholderField = typeof(LambdaLogger).GetField("_placeHolderStructuredLoggingOptions", BindingFlags.NonPublic | BindingFlags.Static); + var actionField = typeof(LambdaLogger).GetField("_configureStructuredLoggingAction", BindingFlags.NonPublic | BindingFlags.Static); + + // Reset to initial state: action stores to placeholder, placeholder is null + placeholderField.SetValue(null, null); + actionField.SetValue(null, new Action(options => + { + placeholderField.SetValue(null, options); + })); + + // Step 1: User calls ConfigureStructuredLogging before runtime is ready. + // This stores the options in _placeHolderStructuredLoggingOptions. + var userOptions = new StructuredLoggingOptions + { + OverrideSerializerOptions = new JsonSerializerOptions { WriteIndented = true } + }; + LambdaLogger.ConfigureStructuredLogging(userOptions); + + Assert.Same(userOptions, placeholderField.GetValue(null)); + + // Step 2: Runtime calls SetConfigureStructuredLoggingAction. + // The pending options should be forwarded to the new action. + StructuredLoggingOptions forwardedOptions = null; + LambdaLogger.SetConfigureStructuredLoggingAction(options => + { + forwardedOptions = options; + }); + + Assert.NotNull(forwardedOptions); + Assert.Same(userOptions, forwardedOptions); + } + + [Fact] + public void StructuredLoggingOptions_DefaultsToNull() + { + var options = new StructuredLoggingOptions(); + Assert.Null(options.OverrideSerializerOptions); + } + } +} diff --git a/Libraries/test/Amazon.Lambda.Core.Tests/TestLambdaContextSerializerTest.cs b/Libraries/test/Amazon.Lambda.Core.Tests/TestLambdaContextSerializerTest.cs new file mode 100644 index 000000000..c806609ee --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Core.Tests/TestLambdaContextSerializerTest.cs @@ -0,0 +1,37 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +#pragma warning disable AWSLAMBDA001 // ILambdaContext.Serializer is preview; this is the test that proves it works. +using System.IO; +using Amazon.Lambda.Core; +using Amazon.Lambda.TestUtilities; +using Xunit; + +namespace Amazon.Lambda.Tests +{ + public class TestLambdaContextSerializerTest + { + [Fact] + public void Serializer_DefaultsToNull() + { + var context = new TestLambdaContext(); + + Assert.Null(context.Serializer); + } + + [Fact] + public void Serializer_RoundTripsThroughTestContext() + { + var stub = new StubSerializer(); + var context = new TestLambdaContext { Serializer = stub }; + + ILambdaContext asInterface = context; + Assert.Same(stub, asInterface.Serializer); + } + + private sealed class StubSerializer : ILambdaSerializer + { + public T Deserialize(Stream requestStream) => default; + public void Serialize(T response, Stream responseStream) { } + } + } +} diff --git a/Libraries/test/Amazon.Lambda.DynamoDBEvents.SDK.Convertor.Tests/Amazon.Lambda.DynamoDBEvents.SDK.Convertor.Tests.csproj b/Libraries/test/Amazon.Lambda.DynamoDBEvents.SDK.Convertor.Tests/Amazon.Lambda.DynamoDBEvents.SDK.Convertor.Tests.csproj index a6d081721..24216ee51 100644 --- a/Libraries/test/Amazon.Lambda.DynamoDBEvents.SDK.Convertor.Tests/Amazon.Lambda.DynamoDBEvents.SDK.Convertor.Tests.csproj +++ b/Libraries/test/Amazon.Lambda.DynamoDBEvents.SDK.Convertor.Tests/Amazon.Lambda.DynamoDBEvents.SDK.Convertor.Tests.csproj @@ -10,9 +10,12 @@ - - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Libraries/test/Amazon.Lambda.DynamoDBEvents.SDK.Convertor.Tests/DynamodbIdentityConvertorTests.cs b/Libraries/test/Amazon.Lambda.DynamoDBEvents.SDK.Convertor.Tests/DynamodbIdentityConvertorTests.cs index ee5557878..2120900cc 100644 --- a/Libraries/test/Amazon.Lambda.DynamoDBEvents.SDK.Convertor.Tests/DynamodbIdentityConvertorTests.cs +++ b/Libraries/test/Amazon.Lambda.DynamoDBEvents.SDK.Convertor.Tests/DynamodbIdentityConvertorTests.cs @@ -6,7 +6,7 @@ public class DynamodbIdentityConvertorTests public void ConvertToSdkIdentity_NullLambdaIdentity_ReturnsNull() { // Arrange - DynamoDBEvent.Identity lambdaIdentity = null; + DynamoDBEvent.Identity? lambdaIdentity = null; // Act var result = lambdaIdentity.ConvertToSdkIdentity(); diff --git a/Libraries/test/Amazon.Lambda.DynamoDBEvents.SDK.Convertor.Tests/DynamodbStreamRecordConvertorTests.cs b/Libraries/test/Amazon.Lambda.DynamoDBEvents.SDK.Convertor.Tests/DynamodbStreamRecordConvertorTests.cs index a56deb0dd..ff52dd3ba 100644 --- a/Libraries/test/Amazon.Lambda.DynamoDBEvents.SDK.Convertor.Tests/DynamodbStreamRecordConvertorTests.cs +++ b/Libraries/test/Amazon.Lambda.DynamoDBEvents.SDK.Convertor.Tests/DynamodbStreamRecordConvertorTests.cs @@ -6,7 +6,7 @@ public class DynamodbStreamRecordConvertorTests public void ConvertToSdkStreamRecord_NullLambdaStreamRecord_ReturnsNull() { // Arrange - DynamoDBEvent.StreamRecord lambdaStreamRecord = null; + DynamoDBEvent.StreamRecord? lambdaStreamRecord = null; // Act var result = lambdaStreamRecord.ConvertToSdkStreamRecord(); diff --git a/Libraries/test/Amazon.Lambda.Logging.AspNetCore.Tests/Amazon.Lambda.Logging.AspNetCore.Tests.csproj b/Libraries/test/Amazon.Lambda.Logging.AspNetCore.Tests/Amazon.Lambda.Logging.AspNetCore.Tests.csproj index df332b1a5..cf907724c 100644 --- a/Libraries/test/Amazon.Lambda.Logging.AspNetCore.Tests/Amazon.Lambda.Logging.AspNetCore.Tests.csproj +++ b/Libraries/test/Amazon.Lambda.Logging.AspNetCore.Tests/Amazon.Lambda.Logging.AspNetCore.Tests.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0;net10.0 Amazon.Lambda.Logging.AspNetCore.Tests Amazon.Lambda.Logging.AspNetCore.Tests true @@ -34,16 +34,16 @@ - - - + + + all runtime; build; native; contentfiles; analyzers - - - - + + + + diff --git a/Libraries/test/Amazon.Lambda.Logging.AspNetCore.Tests/LoggingTests.cs b/Libraries/test/Amazon.Lambda.Logging.AspNetCore.Tests/LoggingTests.cs index 241237e00..e9997cdf3 100644 --- a/Libraries/test/Amazon.Lambda.Logging.AspNetCore.Tests/LoggingTests.cs +++ b/Libraries/test/Amazon.Lambda.Logging.AspNetCore.Tests/LoggingTests.cs @@ -18,7 +18,7 @@ public class LoggingTests private const string SHOULD_NOT_APPEAR = "TextThatShouldNotAppear"; private const string SHOULD_APPEAR_EVENT = "EventThatShouldAppear"; private const string SHOULD_APPEAR_EXCEPTION = "ExceptionThatShouldAppear"; - private static string APPSETTINGS_DIR = Directory.GetCurrentDirectory(); + private static readonly string APPSETTINGS_DIR = Directory.GetCurrentDirectory(); private static readonly Func GET_SHOULD_APPEAR_EVENT = (id) => new EventId(451, SHOULD_APPEAR_EVENT + id); private static readonly EventId SHOULD_NOT_APPEAR_EVENT = new EventId(333, "EventThatShoulNotdAppear"); private static readonly Func GET_SHOULD_APPEAR_EXCEPTION = (id) => new Exception(SHOULD_APPEAR_EXCEPTION + id); @@ -436,7 +436,6 @@ public void TestLoggingWithTypeCategories() // act var httpClientLogger = loggerFactory.CreateLogger(); - var authMngrLogger = loggerFactory.CreateLogger(); var arrayLogger = loggerFactory.CreateLogger(); httpClientLogger.LogTrace(SHOULD_NOT_APPEAR); @@ -446,13 +445,6 @@ public void TestLoggingWithTypeCategories() httpClientLogger.LogError(SHOULD_APPEAR); httpClientLogger.LogCritical(SHOULD_APPEAR); - authMngrLogger.LogTrace(SHOULD_NOT_APPEAR); - authMngrLogger.LogDebug(SHOULD_NOT_APPEAR); - authMngrLogger.LogInformation(SHOULD_APPEAR); - authMngrLogger.LogWarning(SHOULD_APPEAR); - authMngrLogger.LogError(SHOULD_APPEAR); - authMngrLogger.LogCritical(SHOULD_APPEAR); - arrayLogger.LogTrace(SHOULD_NOT_APPEAR); arrayLogger.LogDebug(SHOULD_NOT_APPEAR); arrayLogger.LogInformation(SHOULD_NOT_APPEAR); diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/Amazon.Lambda.RuntimeSupport.IntegrationTests.csproj b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/Amazon.Lambda.RuntimeSupport.IntegrationTests.csproj index 86a3b5c1e..336d524ab 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/Amazon.Lambda.RuntimeSupport.IntegrationTests.csproj +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/Amazon.Lambda.RuntimeSupport.IntegrationTests.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 @@ -19,19 +19,20 @@ - - - - - - + + + + + + + all runtime; build; native; contentfiles; analyzers - + - + diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/ApiGatewayStreamingTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/ApiGatewayStreamingTests.cs new file mode 100644 index 000000000..d357ee50f --- /dev/null +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/ApiGatewayStreamingTests.cs @@ -0,0 +1,478 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon.CloudFormation; +using Amazon.CloudFormation.Model; +using Amazon.Lambda.RuntimeSupport.IntegrationTests.Helpers; +using Xunit; +using Xunit.Abstractions; + +using InvalidOperationException = System.InvalidOperationException; + +namespace Amazon.Lambda.RuntimeSupport.IntegrationTests +{ + // ───────────────────────────────────────────────────────────────────────────── + // Shared test logic + // ───────────────────────────────────────────────────────────────────────────── + + /// + /// Base class containing all streaming integration test scenarios. + /// Subclasses provide the fixture for a specific deployment type + /// (API Gateway REST API or Lambda Function URL). + /// + public abstract class StreamingTestBase + { + private readonly StreamingFixture _fixture; + protected readonly ITestOutputHelper Output; + + protected StreamingTestBase(StreamingFixture fixture, ITestOutputHelper output) + { + _fixture = fixture; + Output = output; + _fixture.Initialize(output); + } + + [Fact] + public async Task RootEndpoint_ReturnsWelcomeMessage() + { + var apiUrl = await _fixture.GetApiUrlAsync(); + using var httpClient = new HttpClient(); + + var response = await httpClient.GetWithRetryAsync(apiUrl); + + Output.WriteLine($"Status: {response.StatusCode}"); + var body = await response.Content.ReadAsStringAsync(); + Output.WriteLine($"Body: {body}"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("Welcome to ASP.NET Core streaming on Lambda", body); + } + + [Fact] + public async Task StreamingEndpoint_ReturnsAllLines() + { + var apiUrl = await _fixture.GetApiUrlAsync(); + using var httpClient = new HttpClient(); + + var response = await httpClient.GetWithRetryAsync($"{apiUrl}streaming-test"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + Output.WriteLine($"Body length: {body.Length}"); + + Assert.Contains("Line 1", body); + Assert.Contains("Line 50", body); + Assert.Contains("Line 100", body); + } + + [Fact] + public async Task StreamingEndpoint_ContentTypeIsTextPlain() + { + var apiUrl = await _fixture.GetApiUrlAsync(); + using var httpClient = new HttpClient(); + + var response = await httpClient.GetWithRetryAsync($"{apiUrl}streaming-test"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("text/plain", response.Content.Headers.ContentType?.MediaType); + } + + [Fact] + public async Task JsonEndpoint_ReturnsValidJson() + { + var apiUrl = await _fixture.GetApiUrlAsync(); + using var httpClient = new HttpClient(); + + var response = await httpClient.GetWithRetryAsync($"{apiUrl}json-response"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + Output.WriteLine($"Body: {body}"); + + var doc = JsonDocument.Parse(body); + Assert.True(doc.RootElement.TryGetProperty("message", out var msg)); + Assert.Equal("Hello from streaming Lambda", msg.GetString()); + } + + [Fact] + public async Task StreamingErrorEndpoint_StreamIsTruncated() + { + var apiUrl = await _fixture.GetApiUrlAsync(); + using var httpClient = new HttpClient(); + + try + { + var response = await httpClient.GetWithRetryAsync($"{apiUrl}streaming-error"); + var body = await response.Content.ReadAsStringAsync(); + Output.WriteLine($"Status: {response.StatusCode}"); + Output.WriteLine($"Body: {body}"); + + if (response.StatusCode == HttpStatusCode.OK) + { + Assert.Contains("Line 1", body); + } + } + catch (HttpRequestException ex) + { + Output.WriteLine($"Expected error: {ex.Message}"); + } + } + + [Fact] + public async Task CustomHeaders_PassedThrough() + { + var apiUrl = await _fixture.GetApiUrlAsync(); + using var httpClient = new HttpClient(); + + var response = await httpClient.GetWithRetryAsync($"{apiUrl}custom-headers", HttpStatusCode.Created); + + Output.WriteLine($"Status: {response.StatusCode}"); + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + Assert.Contains("Custom headers response", body); + + Assert.True(response.Headers.Contains("X-Custom-Header"), "X-Custom-Header should be present"); + Assert.Equal("custom-value", response.Headers.GetValues("X-Custom-Header").First()); + Assert.True(response.Headers.Contains("X-Another-Header"), "X-Another-Header should be present"); + Assert.Equal("another-value", response.Headers.GetValues("X-Another-Header").First()); + } + + [Fact] + public async Task SetCookie_PassedThrough() + { + var apiUrl = await _fixture.GetApiUrlAsync(); + var handler = new HttpClientHandler { UseCookies = false }; + using var httpClient = new HttpClient(handler); + + var response = await httpClient.GetWithRetryAsync($"{apiUrl}set-cookie"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + Output.WriteLine($"Body: {body}"); + Assert.Contains("Cookies set", body); + + Assert.True(response.Headers.Contains("Set-Cookie"), "Set-Cookie header should be present"); + var cookies = response.Headers.GetValues("Set-Cookie").ToList(); + Output.WriteLine($"Cookies: {string.Join("; ", cookies)}"); + Assert.True(cookies.Any(c => c.Contains("session=abc123")), "session cookie should be present"); + Assert.True(cookies.Any(c => c.Contains("theme=dark")), "theme cookie should be present"); + } + + [Fact] + public async Task PostWithBody_EchoesRequestBody() + { + var apiUrl = await _fixture.GetApiUrlAsync(); + using var httpClient = new HttpClient(); + + var response = await httpClient.PostWithRetryAsync( + $"{apiUrl}echo-body", + new StringContent("Hello from integration test", Encoding.UTF8, "text/plain")); + + Output.WriteLine($"Status: {response.StatusCode}"); + var body = await response.Content.ReadAsStringAsync(); + Output.WriteLine($"Body: {body}"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("Echo: Hello from integration test", body); + } + } + + // ───────────────────────────────────────────────────────────────────────────── + // Concrete test classes + // ───────────────────────────────────────────────────────────────────────────── + + /// + /// Tests streaming through API Gateway REST API. + /// + [Collection("Integration Tests")] + public class RestApiStreamingTests : StreamingTestBase, IClassFixture + { + public RestApiStreamingTests(RestApiStreamingFixture fixture, ITestOutputHelper output) + : base(fixture, output) { } + } + + /// + /// Tests streaming through Lambda Function URL. + /// Function URL uses the same payload format as HTTP API v2. + /// + [Collection("Integration Tests")] + + public class FunctionUrlStreamingTests : StreamingTestBase, IClassFixture + { + public FunctionUrlStreamingTests(FunctionUrlStreamingFixture fixture, ITestOutputHelper output) + : base(fixture, output) { } + } + + // ───────────────────────────────────────────────────────────────────────────── + // Fixtures + // ───────────────────────────────────────────────────────────────────────────── + + public class RestApiStreamingFixture : StreamingFixture + { + public RestApiStreamingFixture() + : base("serverless-restapi.template", "RestApi") { } + } + + public class FunctionUrlStreamingFixture : StreamingFixture + { + public FunctionUrlStreamingFixture() + : base("serverless-functionurl.template", "FunctionUrl") { } + } + + /// + /// Shared fixture that deploys the ASP.NET Core streaming test app to AWS using + /// "dotnet lambda deploy-serverless" and tears it down after tests complete. + /// Parameterized by template file and deployment type. + /// + public class StreamingFixture : IAsyncLifetime + { + private static readonly RegionEndpoint TestRegion = BaseCustomRuntimeTest.TestRegion; + + private readonly string _templateFile; + private readonly string _deploymentType; + private readonly string _stackName; + + private string _apiUrl; + private string _toolPath; + private string _testAppPath; + private bool _deployed; + private string _s3BucketName; + + private ITestOutputHelper _outputHelper; + + protected StreamingFixture(string templateFile, string deploymentType) + { + _templateFile = templateFile; + _deploymentType = deploymentType; + _stackName = $"IntegTest-Streaming-{deploymentType}-{DateTime.UtcNow.Ticks}"; + } + + public void Initialize(ITestOutputHelper outputHelper) + { + _outputHelper = outputHelper; + } + + public Task GetApiUrlAsync() + { + if (!_deployed) + { + throw new InvalidOperationException("Test infrastructure not deployed. InitializeAsync must complete first."); + } + return Task.FromResult(_apiUrl); + } + + public async Task InitializeAsync() + { + _toolPath = await LambdaToolsHelper.InstallLambdaTools(); + + _testAppPath = LambdaToolsHelper.GetTempTestAppDirectory( + "../../../../../../..", + "Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/AspNetCoreStreamingApiGatewayTest"); + + var lambdaToolPath = Path.Combine(_toolPath, "dotnet-lambda"); + _s3BucketName = await GetOrCreateDeploymentBucketAsync(); + await CommandLineWrapper.Run( + lambdaToolPath, + $"deploy-serverless --stack-name {_stackName} --template {_templateFile} --s3-bucket {_s3BucketName} --region {TestRegion.SystemName} --disable-interactive true", + _testAppPath, + _outputHelper); + + _apiUrl = await GetStackOutputAsync(_stackName, "ApiURL"); + if (!_apiUrl.EndsWith("/")) + { + _apiUrl += "/"; + } + + _deployed = true; + + await WaitForEndpointAsync(); + } + + public async Task DisposeAsync() + { + if (_deployed) + { + try + { + var lambdaToolPath = Path.Combine(_toolPath, "dotnet-lambda"); + await CommandLineWrapper.Run( + lambdaToolPath, + $"delete-serverless --stack-name {_stackName} --region {TestRegion.SystemName}", + _testAppPath, + _outputHelper); + + if (_s3BucketName != null) + { + using var s3Client = new Amazon.S3.AmazonS3Client(TestRegion); + try + { + await Amazon.S3.Util.AmazonS3Util.DeleteS3BucketWithObjectsAsync(s3Client, _s3BucketName); + } + catch (Exception ex) + { + Console.WriteLine($"Warning: Failed to delete S3 bucket {_s3BucketName}: {ex.Message}"); + } + } + } + catch (Exception ex) + { + Console.WriteLine($"Warning: Failed to delete stack {_stackName}: {ex.Message}"); + } + } + +#if !DEBUG + LambdaToolsHelper.CleanUp(_toolPath); + LambdaToolsHelper.CleanUp(_testAppPath); +#endif + } + + private async Task GetStackOutputAsync(string stackName, string outputKey) + { + using var cfnClient = new AmazonCloudFormationClient(TestRegion); + var response = await cfnClient.DescribeStacksAsync(new DescribeStacksRequest + { + StackName = stackName + }); + + var stack = response.Stacks.FirstOrDefault() + ?? throw new Exception($"Stack {stackName} not found"); + + var output = stack.Outputs.FirstOrDefault(o => o.OutputKey == outputKey) + ?? throw new Exception($"Output {outputKey} not found in stack {stackName}"); + + return output.OutputValue; + } + + private async Task GetOrCreateDeploymentBucketAsync() + { + using var stsClient = new Amazon.SecurityToken.AmazonSecurityTokenServiceClient(TestRegion); + var identity = await stsClient.GetCallerIdentityAsync(new Amazon.SecurityToken.Model.GetCallerIdentityRequest()); + var name = $"integ-test-streaming-{identity.Account}-{TestRegion.SystemName}"; + using var s3Client = new Amazon.S3.AmazonS3Client(TestRegion); + try + { + await s3Client.PutBucketAsync(new Amazon.S3.Model.PutBucketRequest + { + BucketName = name, + UseClientRegion = true + }); + } + catch (Amazon.S3.AmazonS3Exception ex) when (ex.ErrorCode == "BucketAlreadyOwnedByYou") + { + // Bucket already exists from a previous run — reuse it + } + + return name; + } + + private async Task WaitForEndpointAsync() + { + using var httpClient = new HttpClient(); + var maxRetries = 20; + for (var i = 0; i < maxRetries; i++) + { + try + { + var response = await httpClient.GetAsync(_apiUrl); + if (response.StatusCode != HttpStatusCode.InternalServerError) + { + return; + } + } + catch + { + // Ignore — endpoint may not be ready yet + } + await Task.Delay(TimeSpan.FromSeconds(5)); + } + } + } + + internal static class HttpClientExtension + { + public static async Task GetWithRetryAsync( + this HttpClient httpClient, string url, + HttpStatusCode expectedCode = HttpStatusCode.OK, + int maxRetries = 5, int delaySeconds = 5) + { + return await GetWithRetryAsync(httpClient, url, expectedCode, null, maxRetries, delaySeconds); + } + + public static async Task GetWithRetryAsync( + this HttpClient httpClient, string url, + HttpStatusCode expectedCode, + Func> contentValidator, + int maxRetries = 5, int delaySeconds = 5) + { + for (var i = 0; i < maxRetries; i++) + { + try + { + var response = await httpClient.GetAsync(url); + if (response.StatusCode == expectedCode) + { + if (contentValidator == null || await contentValidator(response)) + { + return response; + } + } + } + catch + { + // Ignore and retry + } + await Task.Delay(TimeSpan.FromSeconds(delaySeconds)); + } + throw new Exception($"Failed to get expected response from {url} after {maxRetries} attempts"); + } + + public static async Task PostWithRetryAsync( + this HttpClient httpClient, string url, HttpContent content, + HttpStatusCode expectedCode = HttpStatusCode.OK, + int maxRetries = 5, int delaySeconds = 5) + { + for (var i = 0; i < maxRetries; i++) + { + try + { + var response = await httpClient.PostAsync(url, content); + if (response.StatusCode == expectedCode) + { + return response; + } + } + catch + { + // Ignore and retry + } + // HttpContent can only be consumed once; create fresh content for retries + content = await CloneHttpContentAsync(content); + await Task.Delay(TimeSpan.FromSeconds(delaySeconds)); + } + throw new Exception($"Failed to get expected status code {expectedCode} from {url} after {maxRetries} attempts"); + } + + private static async Task CloneHttpContentAsync(HttpContent original) + { + var bytes = await original.ReadAsByteArrayAsync(); + var clone = new ByteArrayContent(bytes); + if (original.Headers.ContentType != null) + { + clone.Headers.ContentType = original.Headers.ContentType; + } + return clone; + } + } +} diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/BaseCustomRuntimeTest.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/BaseCustomRuntimeTest.cs index c220a671e..148b52649 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/BaseCustomRuntimeTest.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/BaseCustomRuntimeTest.cs @@ -15,9 +15,11 @@ namespace Amazon.Lambda.RuntimeSupport.IntegrationTests { public class BaseCustomRuntimeTest { + protected readonly Runtime _providedRuntime = Runtime.ProvidedAl2023; + public const int FUNCTION_MEMORY_MB = 512; - protected static readonly RegionEndpoint TestRegion = RegionEndpoint.USWest2; + public static readonly RegionEndpoint TestRegion = RegionEndpoint.USWest2; protected static readonly string LAMBDA_ASSUME_ROLE_POLICY = @" { @@ -44,7 +46,7 @@ public class BaseCustomRuntimeTest protected string ExecutionRoleArn { get; set; } private const string TestsProjectDirectoryName = "Amazon.Lambda.RuntimeSupport.Tests"; - private IntegrationTestFixture _fixture; + private readonly IntegrationTestFixture _fixture; protected BaseCustomRuntimeTest(IntegrationTestFixture fixture, string functionName, string deploymentZipKey, string deploymentPackageZipRelativePath, string handler) { @@ -63,7 +65,7 @@ protected BaseCustomRuntimeTest(IntegrationTestFixture fixture, string functionN /// /// /// - protected async Task CleanUpTestResources(AmazonS3Client s3Client, AmazonLambdaClient lambdaClient, + public async Task CleanUpTestResources(AmazonS3Client s3Client, AmazonLambdaClient lambdaClient, AmazonIdentityManagementServiceClient iamClient, bool roleAlreadyExisted) { await DeleteFunctionIfExistsAsync(lambdaClient); @@ -109,14 +111,14 @@ await iamClient.DetachRolePolicyAsync(new DetachRolePolicyRequest } } - protected async Task PrepareTestResources(IAmazonS3 s3Client, IAmazonLambda lambdaClient, - AmazonIdentityManagementServiceClient iamClient) + public async Task PrepareTestResources(IAmazonS3 s3Client, IAmazonLambda lambdaClient, + AmazonIdentityManagementServiceClient iamClient, Runtime runtime) { var roleAlreadyExisted = await ValidateAndSetIamRoleArn(iamClient); var testBucketName = TestBucketRoot + Guid.NewGuid().ToString(); await CreateBucketWithDeploymentZipAsync(s3Client, testBucketName); - await CreateFunctionAsync(lambdaClient, testBucketName); + await CreateFunctionAsync(lambdaClient, testBucketName, runtime); return roleAlreadyExisted; } @@ -273,7 +275,7 @@ private async Task WaitForFunctionToBeReady(IAmazonLambda lambdaClient) await Task.Delay(1000); } - protected async Task CreateFunctionAsync(IAmazonLambda lambdaClient, string bucketName) + protected async Task CreateFunctionAsync(IAmazonLambda lambdaClient, string bucketName, Runtime runtime) { await DeleteFunctionIfExistsAsync(lambdaClient); @@ -288,7 +290,7 @@ protected async Task CreateFunctionAsync(IAmazonLambda lambdaClient, string buck Handler = Handler, MemorySize = FUNCTION_MEMORY_MB, Timeout = 30, - Runtime = Runtime.Dotnet6, + Runtime = runtime, Role = ExecutionRoleArn }; @@ -351,7 +353,16 @@ private string GetDeploymentZipPath() if (!File.Exists(deploymentZipFile)) { - throw new NoDeploymentPackageFoundException(); + var message = new StringBuilder(); + message.AppendLine($"Deployment package for {DeploymentPackageZipRelativePath} not found at expected path: {deploymentZipFile}"); + message.AppendLine("Available Test Bundles:"); + foreach (var kvp in _fixture.TestAppPaths) + { + message.AppendLine($"{kvp.Key}: {kvp.Value}"); + } + + + throw new NoDeploymentPackageFoundException(message.ToString()); } return deploymentZipFile; @@ -380,7 +391,9 @@ private static string FindUp(string path, string fileOrDirectoryName, bool combi protected class NoDeploymentPackageFoundException : Exception { + public NoDeploymentPackageFoundException() { } + public NoDeploymentPackageFoundException(string message) : base(message) { } } private ApplicationLogLevel ConvertRuntimeLogLevel(RuntimeLogLevel runtimeLogLevel) diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest.cs index d8bb17103..c2d585b0a 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest.cs @@ -22,7 +22,7 @@ namespace Amazon.Lambda.RuntimeSupport.IntegrationTests public class CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest : BaseCustomRuntimeTest { public CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest(IntegrationTestFixture fixture) - : base(fixture, "CustomRuntimeMinimalApiCustomSerializerTest-" + DateTime.Now.Ticks, "CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest.zip", @"CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest\bin\Release\net8.0\CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest.zip", "bootstrap") + : base(fixture, "CustomRuntimeMinimalApiCustomSerializerTest-" + DateTime.Now.Ticks, "CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest.zip", @"CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest\bin\Release\net10.0\CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest.zip", "bootstrap") { } @@ -39,7 +39,7 @@ public async Task TestMinimalApi() try { - roleAlreadyExisted = await PrepareTestResources(s3Client, lambdaClient, iamClient); + roleAlreadyExisted = await PrepareTestResources(s3Client, lambdaClient, iamClient, _providedRuntime); await InvokeSuccessToWeatherForecastController(lambdaClient); } catch (NoDeploymentPackageFoundException) @@ -69,7 +69,7 @@ public async Task TestThreadingLogging() try { - roleAlreadyExisted = await PrepareTestResources(s3Client, lambdaClient, iamClient); + roleAlreadyExisted = await PrepareTestResources(s3Client, lambdaClient, iamClient, _providedRuntime); await InvokeLoggerTestController(lambdaClient); } catch (NoDeploymentPackageFoundException) diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/CustomRuntimeAspNetCoreMinimalApiTest.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/CustomRuntimeAspNetCoreMinimalApiTest.cs index 53fd92608..e3f73416b 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/CustomRuntimeAspNetCoreMinimalApiTest.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/CustomRuntimeAspNetCoreMinimalApiTest.cs @@ -22,7 +22,7 @@ namespace Amazon.Lambda.RuntimeSupport.IntegrationTests public class CustomRuntimeAspNetCoreMinimalApiTest : BaseCustomRuntimeTest { public CustomRuntimeAspNetCoreMinimalApiTest(IntegrationTestFixture fixture) - : base(fixture, "CustomRuntimeAspNetCoreMinimalApiTest-" + DateTime.Now.Ticks, "CustomRuntimeAspNetCoreMinimalApiTest.zip", @"CustomRuntimeAspNetCoreMinimalApiTest\bin\Release\net8.0\CustomRuntimeAspNetCoreMinimalApiTest.zip", "bootstrap") + : base(fixture, "CustomRuntimeAspNetCoreMinimalApiTest-" + DateTime.Now.Ticks, "CustomRuntimeAspNetCoreMinimalApiTest.zip", @"CustomRuntimeAspNetCoreMinimalApiTest\bin\Release\net10.0\CustomRuntimeAspNetCoreMinimalApiTest.zip", "bootstrap") { } @@ -39,7 +39,7 @@ public async Task TestMinimalApi() try { - roleAlreadyExisted = await PrepareTestResources(s3Client, lambdaClient, iamClient); + roleAlreadyExisted = await PrepareTestResources(s3Client, lambdaClient, iamClient, _providedRuntime); await InvokeSuccessToWeatherForecastController(lambdaClient); } catch (NoDeploymentPackageFoundException) @@ -69,7 +69,7 @@ public async Task TestThreadingLogging() try { - roleAlreadyExisted = await PrepareTestResources(s3Client, lambdaClient, iamClient); + roleAlreadyExisted = await PrepareTestResources(s3Client, lambdaClient, iamClient, _providedRuntime); await InvokeLoggerTestController(lambdaClient); } catch (NoDeploymentPackageFoundException) diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/CustomRuntimeTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/CustomRuntimeTests.cs index b548d5ba0..70a0b77e8 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/CustomRuntimeTests.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/CustomRuntimeTests.cs @@ -32,15 +32,15 @@ namespace Amazon.Lambda.RuntimeSupport.IntegrationTests { [Collection("Integration Tests")] - public class CustomRuntimeNET8Tests : CustomRuntimeTests + public class CustomRuntimeNET10Tests : CustomRuntimeTests { - public CustomRuntimeNET8Tests(IntegrationTestFixture fixture) - : base(fixture, "CustomRuntimeNET8FunctionTest-" + DateTime.Now.Ticks, "CustomRuntimeFunctionTest.zip", @"CustomRuntimeFunctionTest\bin\Release\net8.0\CustomRuntimeFunctionTest.zip", "CustomRuntimeFunctionTest", TargetFramework.NET8) + public CustomRuntimeNET10Tests(IntegrationTestFixture fixture) + : base(fixture, "CustomRuntimeNET10FunctionTest-" + DateTime.Now.Ticks, "CustomRuntimeFunctionTest.zip", @"CustomRuntimeFunctionTest\bin\Release\net10.0\CustomRuntimeFunctionTest.zip", "CustomRuntimeFunctionTest", TargetFramework.NET10) { } [Fact] - public async Task TestAllNET8HandlersAsync() + public async Task TestAllNET10HandlersAsync() { await base.TestAllHandlersAsync(); } @@ -48,9 +48,9 @@ public async Task TestAllNET8HandlersAsync() public class CustomRuntimeTests : BaseCustomRuntimeTest { - public enum TargetFramework { NET6, NET8} + public enum TargetFramework { NET8, NET10} - private TargetFramework _targetFramework; + private readonly TargetFramework _targetFramework; public CustomRuntimeTests(IntegrationTestFixture fixture, string functionName, string deploymentZipKey, string deploymentPackageZipRelativePath, string handler, TargetFramework targetFramework) : base(fixture, functionName, deploymentZipKey, deploymentPackageZipRelativePath, handler) @@ -69,15 +69,11 @@ protected virtual async Task TestAllHandlersAsync() try { - roleAlreadyExisted = await PrepareTestResources(s3Client, lambdaClient, iamClient); - - // .NET API to address setting memory constraint was added for .NET 8 - if (_targetFramework == TargetFramework.NET8) - { - await RunMaxHeapMemoryCheck(lambdaClient, "GetTotalAvailableMemoryBytes"); - await RunWithoutMaxHeapMemoryCheck(lambdaClient, "GetTotalAvailableMemoryBytes"); - await RunMaxHeapMemoryCheckWithCustomMemorySettings(lambdaClient, "GetTotalAvailableMemoryBytes"); - } + roleAlreadyExisted = await PrepareTestResources(s3Client, lambdaClient, iamClient, _providedRuntime); + + await RunMaxHeapMemoryCheck(lambdaClient, "GetTotalAvailableMemoryBytes"); + await RunWithoutMaxHeapMemoryCheck(lambdaClient, "GetTotalAvailableMemoryBytes"); + await RunMaxHeapMemoryCheckWithCustomMemorySettings(lambdaClient, "GetTotalAvailableMemoryBytes"); await RunTestExceptionAsync(lambdaClient, "ExceptionNonAsciiCharacterUnwrappedAsync", "", "Exception", "Unhandled exception with non ASCII character: ♂"); await RunTestSuccessAsync(lambdaClient, "UnintendedDisposeTest", "not-used", "UnintendedDisposeTest-SUCCESS"); diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/Helpers/CommandLineWrapper.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/Helpers/CommandLineWrapper.cs index aa8651eae..d68c73d32 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/Helpers/CommandLineWrapper.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/Helpers/CommandLineWrapper.cs @@ -1,14 +1,16 @@ using System; using System.Diagnostics; +using System.Text; using System.Threading; using System.Threading.Tasks; using Xunit; +using Xunit.Abstractions; namespace Amazon.Lambda.RuntimeSupport.IntegrationTests.Helpers; public static class CommandLineWrapper { - public static async Task Run(string command, string arguments, string workingDirectory, CancellationToken cancellationToken = default) + public static async Task Run(string command, string arguments, string workingDirectory, ITestOutputHelper outputHelper, CancellationToken cancellationToken = default) { var processStartInfo = new ProcessStartInfo { @@ -31,6 +33,7 @@ public static async Task Run(string command, string arguments, string workingDir tcs.TrySetResult(true); }; + var output = new StringBuilder(); try { // Attach event handlers @@ -39,6 +42,7 @@ public static async Task Run(string command, string arguments, string workingDir if (!string.IsNullOrEmpty(args.Data)) { Console.WriteLine(args.Data); + output.AppendLine(args.Data); } }; @@ -47,6 +51,7 @@ public static async Task Run(string command, string arguments, string workingDir if (!string.IsNullOrEmpty(args.Data)) { Console.WriteLine(args.Data); + output.AppendLine(args.Data); } }; @@ -78,13 +83,20 @@ public static async Task Run(string command, string arguments, string workingDir catch (Exception ex) { Console.WriteLine("Exception: " + ex); + Console.WriteLine(output.ToString()); if (!process.HasExited) { process.Kill(); } } - + + if (process.ExitCode != 0 && outputHelper != null) + { + outputHelper.WriteLine($"Command '{command} {arguments}' failed."); + outputHelper.WriteLine(output.ToString()); + } + Assert.True(process.ExitCode == 0, $"Command '{command} {arguments}' failed."); } } -} \ No newline at end of file +} diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/Helpers/LambdaToolsHelper.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/Helpers/LambdaToolsHelper.cs index 42a02aac6..5f649d923 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/Helpers/LambdaToolsHelper.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/Helpers/LambdaToolsHelper.cs @@ -10,6 +10,9 @@ public static class LambdaToolsHelper public static string GetTempTestAppDirectory(string workingDirectory, string testAppPath) { +#if DEBUG + return Path.GetFullPath(Path.Combine(workingDirectory, testAppPath)); +#else var customTestAppPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); Directory.CreateDirectory(customTestAppPath); @@ -17,6 +20,7 @@ public static string GetTempTestAppDirectory(string workingDirectory, string tes CopyDirectory(currentDir, customTestAppPath); return Path.Combine(customTestAppPath, testAppPath); +#endif } public static async Task InstallLambdaTools() @@ -26,7 +30,7 @@ public static async Task InstallLambdaTools() await CommandLineWrapper.Run( "dotnet", $"tool install Amazon.Lambda.Tools --tool-path {customToolPath}", - Directory.GetCurrentDirectory()); + Directory.GetCurrentDirectory(), null); return customToolPath; } @@ -36,7 +40,7 @@ public static async Task LambdaPackage(string toolPath, string framework, string await CommandLineWrapper.Run( lambdaToolPath, $"package -c Release --framework {framework} --function-architecture {FunctionArchitecture}", - workingDirectory); + workingDirectory, null); } public static void CleanUp(string toolPath) @@ -78,4 +82,4 @@ private static void CopyDirectory(DirectoryInfo dir, string destDirName) CopyDirectory(subDir, tempPath); } } -} \ No newline at end of file +} diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/IntegrationTestCollection.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/IntegrationTestCollection.cs index c9ce90e35..9b637b547 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/IntegrationTestCollection.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/IntegrationTestCollection.cs @@ -3,7 +3,7 @@ namespace Amazon.Lambda.RuntimeSupport.IntegrationTests; [CollectionDefinition("Integration Tests")] -public class IntegrationTestCollection : ICollectionFixture +public class IntegrationTestCollection : ICollectionFixture, ICollectionFixture { -} \ No newline at end of file +} diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/IntegrationTestFixture.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/IntegrationTestFixture.cs index 89d62d61f..e86a37e2d 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/IntegrationTestFixture.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/IntegrationTestFixture.cs @@ -14,38 +14,46 @@ public class IntegrationTestFixture : IAsyncLifetime public async Task InitializeAsync() { + var toolPath = await LambdaToolsHelper.InstallLambdaTools(); + var testAppPath = LambdaToolsHelper.GetTempTestAppDirectory( "../../../../../../..", "Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeFunctionTest"); - var toolPath = await LambdaToolsHelper.InstallLambdaTools(); _tempPaths.AddRange([testAppPath, toolPath] ); - await LambdaToolsHelper.LambdaPackage(toolPath, "net8.0", testAppPath); - TestAppPaths[@"CustomRuntimeFunctionTest\bin\Release\net8.0\CustomRuntimeFunctionTest.zip"] = Path.Combine(testAppPath, @"bin\Release\net8.0\CustomRuntimeFunctionTest.zip"); + await LambdaToolsHelper.LambdaPackage(toolPath, "net10.0", testAppPath); + TestAppPaths[@"CustomRuntimeFunctionTest\bin\Release\net10.0\CustomRuntimeFunctionTest.zip"] = Path.Combine(testAppPath, @"bin\Release\net10.0\CustomRuntimeFunctionTest.zip"); testAppPath = LambdaToolsHelper.GetTempTestAppDirectory( "../../../../../../..", "Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeAspNetCoreMinimalApiTest"); - toolPath = await LambdaToolsHelper.InstallLambdaTools(); _tempPaths.AddRange([testAppPath, toolPath] ); - await LambdaToolsHelper.LambdaPackage(toolPath, "net8.0", testAppPath); - TestAppPaths[@"CustomRuntimeAspNetCoreMinimalApiTest\bin\Release\net8.0\CustomRuntimeAspNetCoreMinimalApiTest.zip"] = Path.Combine(testAppPath, @"bin\Release\net8.0\CustomRuntimeAspNetCoreMinimalApiTest.zip"); + await LambdaToolsHelper.LambdaPackage(toolPath, "net10.0", testAppPath); + TestAppPaths[@"CustomRuntimeAspNetCoreMinimalApiTest\bin\Release\net10.0\CustomRuntimeAspNetCoreMinimalApiTest.zip"] = Path.Combine(testAppPath, @"bin\Release\net10.0\CustomRuntimeAspNetCoreMinimalApiTest.zip"); testAppPath = LambdaToolsHelper.GetTempTestAppDirectory( "../../../../../../..", "Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest"); - toolPath = await LambdaToolsHelper.InstallLambdaTools(); _tempPaths.AddRange([testAppPath, toolPath] ); - await LambdaToolsHelper.LambdaPackage(toolPath, "net8.0", testAppPath); - TestAppPaths[@"CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest\bin\Release\net8.0\CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest.zip"] = Path.Combine(testAppPath, @"bin\Release\net8.0\CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest.zip"); + await LambdaToolsHelper.LambdaPackage(toolPath, "net10.0", testAppPath); + TestAppPaths[@"CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest\bin\Release\net10.0\CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest.zip"] = Path.Combine(testAppPath, @"bin\Release\net10.0\CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest.zip"); + + testAppPath = LambdaToolsHelper.GetTempTestAppDirectory( + "../../../../../../..", + "Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/ResponseStreamingFunctionHandlers"); + _tempPaths.AddRange([testAppPath, toolPath]); + await LambdaToolsHelper.LambdaPackage(toolPath, "net10.0", testAppPath); + TestAppPaths[@"ResponseStreamingFunctionHandlers\bin\Release\net10.0\ResponseStreamingFunctionHandlers.zip"] = Path.Combine(testAppPath, "bin", "Release", "net10.0", "ResponseStreamingFunctionHandlers.zip"); } public Task DisposeAsync() { +#if !DEBUG foreach (var tempPath in _tempPaths) { LambdaToolsHelper.CleanUp(tempPath); } +#endif return Task.CompletedTask; } diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/ResponseStreamingTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/ResponseStreamingTests.cs new file mode 100644 index 000000000..30e14832d --- /dev/null +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.IntegrationTests/ResponseStreamingTests.cs @@ -0,0 +1,133 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Amazon.IdentityManagement; +using Amazon.Lambda.Model; +using Amazon.Runtime.EventStreams; +using Amazon.S3; +using Xunit; + +namespace Amazon.Lambda.RuntimeSupport.IntegrationTests +{ + [Collection("Integration Tests")] + public class ResponseStreamingTests : BaseCustomRuntimeTest + { + private readonly static string s_functionName = "IntegTestResponseStreamingFunctionHandlers" + DateTime.Now.Ticks; + + private readonly ResponseStreamingTestsFixture _streamFixture; + + public ResponseStreamingTests(IntegrationTestFixture fixture, ResponseStreamingTestsFixture streamFixture) + : base(fixture, s_functionName, "ResponseStreamingFunctionHandlers.zip", @"ResponseStreamingFunctionHandlers\bin\Release\net10.0\ResponseStreamingFunctionHandlers.zip", "ResponseStreamingFunctionHandlers") + { + _streamFixture = streamFixture; + } + + [Fact] + public async Task SimpleFunctionHandler() + { + await _streamFixture.EnsureResourcesDeployedAsync(this); + + var evnts = await InvokeFunctionAsync(nameof(SimpleFunctionHandler)); + Assert.True(evnts.Any()); + + var content = GetCombinedStreamContent(evnts); + Assert.Equal("Hello, World!", content); + } + + [Fact] + public async Task StreamContentHandler() + { + await _streamFixture.EnsureResourcesDeployedAsync(this); + + var evnts = await InvokeFunctionAsync(nameof(StreamContentHandler)); + Assert.True(evnts.Length > 5); + + var content = GetCombinedStreamContent(evnts); + Assert.Contains("Line 9999", content); + Assert.EndsWith("Finish stream content\n", content); + } + + [Fact] + public async Task UnhandledExceptionHandler() + { + await _streamFixture.EnsureResourcesDeployedAsync(this); + + var evnts = await InvokeFunctionAsync(nameof(UnhandledExceptionHandler)); + Assert.True(evnts.Any()); + + var completeEvent = evnts.Last() as InvokeWithResponseStreamCompleteEvent; + Assert.Equal("InvalidOperationException", completeEvent.ErrorCode); + Assert.Contains("This is an unhandled exception", completeEvent.ErrorDetails); + Assert.Contains("stackTrace", completeEvent.ErrorDetails); + } + + private async Task InvokeFunctionAsync(string handlerScenario) + { + using var client = new AmazonLambdaClient(TestRegion); + + var request = new InvokeWithResponseStreamRequest + { + FunctionName = base.FunctionName, + Payload = new MemoryStream(System.Text.Encoding.UTF8.GetBytes($"\"{handlerScenario}\"")), + InvocationType = ResponseStreamingInvocationType.RequestResponse + }; + + var response = await client.InvokeWithResponseStreamAsync(request); + var evnts = response.EventStream.AsEnumerable().ToArray(); + return evnts; + } + + private string GetCombinedStreamContent(IEventStreamEvent[] events) + { + var sb = new StringBuilder(); + foreach (var evnt in events) + { + if (evnt is InvokeResponseStreamUpdate chunk) + { + var text = System.Text.Encoding.UTF8.GetString(chunk.Payload.ToArray()); + sb.Append(text); + } + } + return sb.ToString(); + } + } + + public class ResponseStreamingTestsFixture : IAsyncLifetime + { + private readonly AmazonLambdaClient _lambdaClient = new AmazonLambdaClient(BaseCustomRuntimeTest.TestRegion); + private readonly AmazonS3Client _s3Client = new AmazonS3Client(BaseCustomRuntimeTest.TestRegion); + private readonly AmazonIdentityManagementServiceClient _iamClient = new AmazonIdentityManagementServiceClient(BaseCustomRuntimeTest.TestRegion); + bool _resourcesCreated; + bool _roleAlreadyExisted; + + ResponseStreamingTests _tests; + + public async Task EnsureResourcesDeployedAsync(ResponseStreamingTests tests) + { + if (_resourcesCreated) + return; + + _tests = tests; + _roleAlreadyExisted = await _tests.PrepareTestResources(_s3Client, _lambdaClient, _iamClient, Runtime.Dotnet10); + + _resourcesCreated = true; + } + + public async Task DisposeAsync() + { + await _tests.CleanUpTestResources(_s3Client, _lambdaClient, _iamClient, _roleAlreadyExisted); + + _lambdaClient.Dispose(); + _s3Client.Dispose(); + _iamClient.Dispose(); + } + + public Task InitializeAsync() => Task.CompletedTask; + } +} diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/Amazon.Lambda.RuntimeSupport.UnitTests.csproj b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/Amazon.Lambda.RuntimeSupport.UnitTests.csproj index cc96b0a0b..e951ed51a 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/Amazon.Lambda.RuntimeSupport.UnitTests.csproj +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/Amazon.Lambda.RuntimeSupport.UnitTests.csproj @@ -5,16 +5,17 @@ ..\..\..\..\buildtools\public.snk true - IDE0060 + + IDE0060;CA2252 - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/Common.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/Common.cs index 010d7c37f..eacaeb43d 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/Common.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/Common.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). @@ -55,7 +55,7 @@ public static void CheckException(Exception e, string expectedPartialMessage) { if (!FindMatchingExceptionMessage(e, expectedPartialMessage)) { - Assert.True(false, $"Unable to match up expected message '{expectedPartialMessage}' in exception: {GetAllMessages(e)}"); + Assert.Fail($"Unable to match up expected message '{expectedPartialMessage}' in exception: {GetAllMessages(e)}"); } } @@ -89,4 +89,4 @@ public static string GetAllMessages(Exception e) } } } -} \ No newline at end of file +} diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/HandlerTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/HandlerTests.cs index 20340f561..dacd01f87 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/HandlerTests.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/HandlerTests.cs @@ -31,7 +31,7 @@ namespace Amazon.Lambda.RuntimeSupport.UnitTests { - [Collection("Bootstrap")] + [Collection("RuntimeSupportStateCheck")] public class HandlerTests { private const string AggregateExceptionTestMarker = "AggregateExceptionTesting"; @@ -285,6 +285,7 @@ await Record.ExceptionAsync(async () => private async Task InvokeAsync(LambdaBootstrap bootstrap, string dataIn, TestRuntimeApiClient testRuntimeApiClient) { testRuntimeApiClient.FunctionInput = dataIn != null ? Encoding.UTF8.GetBytes(dataIn) : new byte[0]; + testRuntimeApiClient.LastOutputStream = null; using (var cancellationTokenSource = new CancellationTokenSource()) { diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/HandlerWrapperTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/HandlerWrapperTests.cs index 88e93834f..fb4aae2e1 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/HandlerWrapperTests.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/HandlerWrapperTests.cs @@ -23,6 +23,7 @@ namespace Amazon.Lambda.RuntimeSupport.UnitTests { + [Collection("RuntimeSupportStateCheck")] public class HandlerWrapperTests { private static readonly JsonSerializer Serializer = new JsonSerializer(); @@ -652,7 +653,7 @@ public async Task TestOutputStreamReuse(bool onDemand) var invocation1 = new InvocationRequest { InputStream = new MemoryStream(UTF8Encoding.UTF8.GetBytes("\"Hello\"")), - LambdaContext = new LambdaContext(_runtimeApiHeaders, _lambdaEnvironment, new Helpers.SimpleLoggerWriter(new SystemEnvironmentVariables())) + LambdaContext = new LambdaContext(_runtimeApiHeaders, _lambdaEnvironment, new Helpers.LogLevelLoggerWriter(new SystemEnvironmentVariables())) }; var invocationResponse1 = await handlerWrapper.Handler(invocation1); @@ -660,7 +661,7 @@ public async Task TestOutputStreamReuse(bool onDemand) var invocation2 = new InvocationRequest { InputStream = new MemoryStream(UTF8Encoding.UTF8.GetBytes("\"World\"")), - LambdaContext = new LambdaContext(_runtimeApiHeaders, _lambdaEnvironment, new Helpers.SimpleLoggerWriter(new SystemEnvironmentVariables())) + LambdaContext = new LambdaContext(_runtimeApiHeaders, _lambdaEnvironment, new Helpers.LogLevelLoggerWriter(new SystemEnvironmentVariables())) }; var invocationResponse2 = await handlerWrapper.Handler(invocation2); @@ -684,7 +685,7 @@ private async Task TestHandlerWrapper(HandlerWrapper handlerWrapper, byte[] inpu var invocation = new InvocationRequest { InputStream = new MemoryStream(input ?? new byte[0]), - LambdaContext = new LambdaContext(_runtimeApiHeaders, _lambdaEnvironment, new Helpers.SimpleLoggerWriter(new SystemEnvironmentVariables())) + LambdaContext = new LambdaContext(_runtimeApiHeaders, _lambdaEnvironment, new Helpers.LogLevelLoggerWriter(new SystemEnvironmentVariables())) }; var invocationResponse = await handlerWrapper.Handler(invocation); diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaBootstrapMultiConcurrencyTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaBootstrapMultiConcurrencyTests.cs index 2bee73286..3eb4bbe6e 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaBootstrapMultiConcurrencyTests.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaBootstrapMultiConcurrencyTests.cs @@ -112,6 +112,93 @@ public async Task ConfirmConcurrentInvocations(bool useAsyncHandler) Assert.Equal("end-request1,traceId-trace1", handlerEvents[3]); } + /// + /// Bug condition exploration test: Demonstrates thread pool starvation when MC is enabled + /// with blocking handlers. This test encodes the EXPECTED behavior after the fix is applied. + /// On unfixed code, this test is EXPECTED TO FAIL with a timeout because: + /// - Only 2 polling tasks are created (Math.Max(2, processorCount)) + /// - ThreadPool.MinThreads is constrained to 2 worker threads + /// - 10 blocking handlers (Thread.Sleep) exhaust the thread pool + /// - Polling task continuations cannot resume to call GetNextInvocationAsync + /// - Not all 10 invocations get dequeued within the timeout + /// + /// Validates: Requirements 1.1, 1.2, 1.3, 1.4 + /// + [Fact] + public async Task ThreadPoolStarvation_BlockingHandlers_AllInvocationsDequeued() + { + // Save original ThreadPool settings to restore after test + ThreadPool.GetMinThreads(out int originalMinWorker, out int originalMinIO); + + try + { + // Constrain ThreadPool to simulate Lambda's default environment (low thread count) + ThreadPool.SetMinThreads(2, 2); + + TestEnvironmentVariables environmentVariables = new TestEnvironmentVariables(); + environmentVariables.SetEnvironmentVariable( + Amazon.Lambda.RuntimeSupport.Bootstrap.Constants.ENVIRONMENT_VARIABLE_AWS_LAMBDA_MAX_CONCURRENCY, "10"); + + // Create 10 invocation events with blocking handlers + var invocationEvents = new TestMultiConcurrencyRuntimeApiClient.InvocationEvent[10]; + for (int i = 0; i < 10; i++) + { + invocationEvents[i] = new TestMultiConcurrencyRuntimeApiClient.InvocationEvent + { + Headers = CreateDefaultHeaders($"request{i}", $"trace{i}"), + FunctionInput = CreateFunctionInput(new SleepTimeEvent(3000, 0)) + }; + } + + var testRuntimeApiClient = new TestMultiConcurrencyRuntimeApiClient(environmentVariables, invocationEvents); + + // Use a thread-safe counter to track dequeued invocations + int dequeuedCount = 0; + var allDequeuedEvent = new ManualResetEventSlim(false); + + // Wrap the test client to track dequeue operations in a thread-safe manner + var originalGetNext = testRuntimeApiClient; + + // Handler that performs blocking work (Thread.Sleep) to exhaust the thread pool + var handler = HandlerWrapper.GetHandlerWrapper((SleepTimeEvent sleepTime, ILambdaContext context) => + { + // Blocking sleep to simulate CPU-bound or synchronous I/O work + Thread.Sleep(sleepTime.StartSleep); + }, _serializer).Handler; + + var lambdaBootstrap = new LambdaBootstrap( + httpClient: null, + handler: handler, + initializer: null, + ownsHttpClient: true, + environmentVariables: environmentVariables); + lambdaBootstrap.Client = testRuntimeApiClient; + + // Run with a 10-second timeout - if all 10 invocations are dequeued, the test passes + CancellationTokenSource cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromSeconds(10)); + + try + { + await lambdaBootstrap.RunAsync(cts.Token); + } + catch (OperationCanceledException) + { + // Expected when the cancellation token is triggered (timeout) + } + + // Assert that all 10 invocations were dequeued from the test client within the timeout. + // On unfixed code, this will fail because thread pool starvation prevents polling tasks + // from cycling back to GetNextInvocationAsync. + Assert.Equal(10, testRuntimeApiClient.ProcessInvocationEvents.Count); + } + finally + { + // Restore original ThreadPool settings + ThreadPool.SetMinThreads(originalMinWorker, originalMinIO); + } + } + private Dictionary> CreateDefaultHeaders(string requestId, string traceId) { return new Dictionary> diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaBootstrapMultiConcurrencyThreadPoolStarvationTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaBootstrapMultiConcurrencyThreadPoolStarvationTests.cs new file mode 100644 index 000000000..1e64c75c8 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaBootstrapMultiConcurrencyThreadPoolStarvationTests.cs @@ -0,0 +1,249 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Amazon.Lambda.Core; +using Amazon.Lambda.RuntimeSupport.UnitTests.TestHelpers; +using Amazon.Lambda.Serialization.Json; +using Xunit; + +namespace Amazon.Lambda.RuntimeSupport.UnitTests +{ + /// + /// Tests for multi-concurrency thread pool starvation fix. + /// + /// The fix ensures that when AWS_LAMBDA_MAX_CONCURRENCY is set: + /// 1. The polling task count matches the MC value (not just Math.Max(2, processorCount)) + /// 2. ThreadPool.SetMinThreads is called to pre-size the pool for handler threads + polling continuations + /// + /// Without both changes, blocking handlers (Thread.Sleep, .Result, .Wait()) exhaust the + /// ThreadPool, preventing polling tasks from cycling back to /next. + /// + public class LambdaBootstrapMultiConcurrencyThreadPoolStarvationTests + { + private readonly JsonSerializer _serializer = new JsonSerializer(); + + /// + /// Verifies the fix works: with AdjustThreadPoolSettings pre-sizing the pool + /// and DetermineProcessingTaskCount using the MC value, all invocations are + /// dequeued promptly even with blocking handlers. + /// + /// This simulates the real Lambda environment where MaxThreads is not capped + /// but MinThreads starts low. The fix raises MinThreads so threads are + /// immediately available for both handlers and polling continuations. + /// + [Fact] + public async Task MultiConcurrency_BlockingHandlers_AllInvocationsDequeued() + { + const int mcCount = 10; + const int invocationCount = 10; + const int handlerBlockTimeMs = 3000; + var timeout = TimeSpan.FromSeconds(5); + + var environmentVariables = new TestEnvironmentVariables(); + environmentVariables.SetEnvironmentVariable( + Amazon.Lambda.RuntimeSupport.Bootstrap.Constants.ENVIRONMENT_VARIABLE_AWS_LAMBDA_MAX_CONCURRENCY, + mcCount.ToString()); + + var invocationEvents = new TestMultiConcurrencyRuntimeApiClient.InvocationEvent[invocationCount]; + for (int i = 0; i < invocationCount; i++) + { + invocationEvents[i] = new TestMultiConcurrencyRuntimeApiClient.InvocationEvent + { + Headers = CreateDefaultHeaders($"request{i + 1}", $"trace{i + 1}"), + FunctionInput = CreateFunctionInput(new BlockingEvent { BlockTimeMs = handlerBlockTimeMs }) + }; + } + + var testRuntimeApiClient = new TestMultiConcurrencyRuntimeApiClient(environmentVariables, invocationEvents); + var startedInvocations = new ConcurrentBag(); + + var handler = HandlerWrapper.GetHandlerWrapper((BlockingEvent input, ILambdaContext context) => + { + startedInvocations.Add(context.AwsRequestId); + Thread.Sleep(input.BlockTimeMs); + }, _serializer).Handler; + + var lambdaBootstrap = new LambdaBootstrap( + httpClient: null, + handler: handler, + initializer: null, + ownsHttpClient: true, + environmentVariables: environmentVariables); + lambdaBootstrap.Client = testRuntimeApiClient; + + var cts = new CancellationTokenSource(); + cts.CancelAfter(timeout); + + try + { + await lambdaBootstrap.RunAsync(cts.Token); + } + catch (OperationCanceledException) + { + // Expected — the bootstrap runs until cancelled + } + + // With the fix: 10 polling tasks + pre-sized ThreadPool = all invocations dequeued + Assert.Equal(invocationCount, testRuntimeApiClient.ProcessInvocationEvents.Count); + Assert.Equal(invocationCount, startedInvocations.Count); + } + + /// + /// Verifies the fix works at higher concurrency (MC=20). + /// + [Fact] + public async Task MultiConcurrency_HigherConcurrency_AllInvocationsDequeued() + { + const int mcCount = 20; + const int invocationCount = 20; + const int handlerBlockTimeMs = 2000; + var timeout = TimeSpan.FromSeconds(5); + + var environmentVariables = new TestEnvironmentVariables(); + environmentVariables.SetEnvironmentVariable( + Amazon.Lambda.RuntimeSupport.Bootstrap.Constants.ENVIRONMENT_VARIABLE_AWS_LAMBDA_MAX_CONCURRENCY, + mcCount.ToString()); + + var invocationEvents = new TestMultiConcurrencyRuntimeApiClient.InvocationEvent[invocationCount]; + for (int i = 0; i < invocationCount; i++) + { + invocationEvents[i] = new TestMultiConcurrencyRuntimeApiClient.InvocationEvent + { + Headers = CreateDefaultHeaders($"request{i + 1}", $"trace{i + 1}"), + FunctionInput = CreateFunctionInput(new BlockingEvent { BlockTimeMs = handlerBlockTimeMs }) + }; + } + + var testRuntimeApiClient = new TestMultiConcurrencyRuntimeApiClient(environmentVariables, invocationEvents); + var startedInvocations = new ConcurrentBag(); + + var handler = HandlerWrapper.GetHandlerWrapper((BlockingEvent input, ILambdaContext context) => + { + startedInvocations.Add(context.AwsRequestId); + Thread.Sleep(input.BlockTimeMs); + }, _serializer).Handler; + + var lambdaBootstrap = new LambdaBootstrap( + httpClient: null, + handler: handler, + initializer: null, + ownsHttpClient: true, + environmentVariables: environmentVariables); + lambdaBootstrap.Client = testRuntimeApiClient; + + var cts = new CancellationTokenSource(); + cts.CancelAfter(timeout); + + try + { + await lambdaBootstrap.RunAsync(cts.Token); + } + catch (OperationCanceledException) + { + // Expected + } + + Assert.Equal(invocationCount, testRuntimeApiClient.ProcessInvocationEvents.Count); + Assert.Equal(invocationCount, startedInvocations.Count); + } + + /// + /// Verifies that non-numeric AWS_LAMBDA_MAX_CONCURRENCY still works + /// (falls back to Math.Max(2, processorCount) for polling tasks, no ThreadPool adjustment). + /// + [Fact] + public async Task MultiConcurrency_NonNumericMcValue_FallsBackToProcessorCount() + { + const int invocationCount = 2; + const int handlerBlockTimeMs = 500; + var timeout = TimeSpan.FromSeconds(3); + + var environmentVariables = new TestEnvironmentVariables(); + // Non-numeric value — MC is enabled but value can't be parsed + environmentVariables.SetEnvironmentVariable( + Amazon.Lambda.RuntimeSupport.Bootstrap.Constants.ENVIRONMENT_VARIABLE_AWS_LAMBDA_MAX_CONCURRENCY, + "enabled"); + + var invocationEvents = new TestMultiConcurrencyRuntimeApiClient.InvocationEvent[invocationCount]; + for (int i = 0; i < invocationCount; i++) + { + invocationEvents[i] = new TestMultiConcurrencyRuntimeApiClient.InvocationEvent + { + Headers = CreateDefaultHeaders($"request{i + 1}", $"trace{i + 1}"), + FunctionInput = CreateFunctionInput(new BlockingEvent { BlockTimeMs = handlerBlockTimeMs }) + }; + } + + var testRuntimeApiClient = new TestMultiConcurrencyRuntimeApiClient(environmentVariables, invocationEvents); + var startedInvocations = new ConcurrentBag(); + + var handler = HandlerWrapper.GetHandlerWrapper((BlockingEvent input, ILambdaContext context) => + { + startedInvocations.Add(context.AwsRequestId); + Thread.Sleep(input.BlockTimeMs); + }, _serializer).Handler; + + var lambdaBootstrap = new LambdaBootstrap( + httpClient: null, + handler: handler, + initializer: null, + ownsHttpClient: true, + environmentVariables: environmentVariables); + lambdaBootstrap.Client = testRuntimeApiClient; + + var cts = new CancellationTokenSource(); + cts.CancelAfter(timeout); + + try + { + await lambdaBootstrap.RunAsync(cts.Token); + } + catch (OperationCanceledException) + { + // Expected + } + + // Should still process both invocations (fallback behavior works) + Assert.Equal(invocationCount, testRuntimeApiClient.ProcessInvocationEvents.Count); + Assert.Equal(invocationCount, startedInvocations.Count); + } + + #region Helper Methods + + private Dictionary> CreateDefaultHeaders(string requestId, string traceId) + { + return new Dictionary> + { + { RuntimeApiHeaders.HeaderAwsRequestId, new List { requestId } }, + { RuntimeApiHeaders.HeaderInvokedFunctionArn, new List { "invoked_function_arn" } }, + { RuntimeApiHeaders.HeaderTraceId, new List { traceId } } + }; + } + + private byte[] CreateFunctionInput(object input) + { + using (var ms = new System.IO.MemoryStream()) + { + _serializer.Serialize(input, ms); + return ms.ToArray(); + } + } + + #endregion + + #region Test Models + + public class BlockingEvent + { + public int BlockTimeMs { get; set; } + } + + #endregion + } +} diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaBootstrapTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaBootstrapTests.cs index 97b40cb60..d2b1a1556 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaBootstrapTests.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaBootstrapTests.cs @@ -14,12 +14,14 @@ */ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net.Http; using System.Text; using System.Threading.Tasks; using Xunit; +using Amazon.Lambda.RuntimeSupport.Client.ResponseStreaming; using Amazon.Lambda.RuntimeSupport.Bootstrap; using static Amazon.Lambda.RuntimeSupport.Bootstrap.Constants; @@ -29,6 +31,7 @@ namespace Amazon.Lambda.RuntimeSupport.UnitTests /// Tests to test LambdaBootstrap when it's constructed using its actual constructor. /// Tests of the static GetLambdaBootstrap methods can be found in LambdaBootstrapWrapperTests. /// + [Collection("RuntimeSupportStateCheck")] public class LambdaBootstrapTests { readonly TestHandler _testFunction; @@ -283,5 +286,159 @@ public void IsCallPreJitTest() environmentVariables.SetEnvironmentVariable(ENVIRONMENT_VARIABLE_AWS_LAMBDA_INITIALIZATION_TYPE, AWS_LAMBDA_INITIALIZATION_TYPE_PC); Assert.True(UserCodeInit.IsCallPreJit(environmentVariables)); } + + // --- Streaming Integration Tests --- + + private TestStreamingRuntimeApiClient CreateStreamingClient() + { + var envVars = new TestEnvironmentVariables(); + var headers = new Dictionary> + { + { RuntimeApiHeaders.HeaderAwsRequestId, new List { "streaming-request-id" } }, + { RuntimeApiHeaders.HeaderInvokedFunctionArn, new List { "invoked_function_arn" } }, + { RuntimeApiHeaders.HeaderAwsTenantId, new List { "tenant_id" } } + }; + return new TestStreamingRuntimeApiClient(envVars, headers); + } + + /// + /// Property 2: CreateStream Enables Streaming Mode + /// When a handler calls ResponseStreamFactory.CreateStream(), the response is transmitted + /// using streaming mode. LambdaBootstrap awaits the send task. + /// **Validates: Requirements 1.4, 6.1, 6.2, 6.3, 6.4** + /// + [Fact] + public async Task StreamingMode_HandlerCallsCreateStream_SendTaskAwaited() + { + var streamingClient = CreateStreamingClient(); + + LambdaBootstrapHandler handler = async (invocation) => + { + var stream = ResponseStreamFactory.CreateStream(Array.Empty()); + await stream.WriteAsync(Encoding.UTF8.GetBytes("hello")); + return new InvocationResponse(Stream.Null, false); + }; + + using (var bootstrap = new LambdaBootstrap(handler, null, null, new TestEnvironmentVariables())) + { + bootstrap.Client = streamingClient; + await bootstrap.InvokeOnceAsync(); + } + + Assert.True(streamingClient.StartStreamingResponseAsyncCalled); + Assert.False(streamingClient.SendResponseAsyncCalled); + } + + /// + /// Property 3: Default Mode Is Buffered + /// When a handler does not call ResponseStreamFactory.CreateStream(), the response + /// is transmitted using buffered mode via SendResponseAsync. + /// **Validates: Requirements 1.5, 7.2** + /// + [Fact] + public async Task BufferedMode_HandlerDoesNotCallCreateStream_UsesSendResponse() + { + var streamingClient = CreateStreamingClient(); + + LambdaBootstrapHandler handler = async (invocation) => + { + var outputStream = new MemoryStream(Encoding.UTF8.GetBytes("buffered response")); + return new InvocationResponse(outputStream); + }; + + using (var bootstrap = new LambdaBootstrap(handler, null, null, new TestEnvironmentVariables())) + { + bootstrap.Client = streamingClient; + await bootstrap.InvokeOnceAsync(); + } + + Assert.False(streamingClient.StartStreamingResponseAsyncCalled); + Assert.True(streamingClient.SendResponseAsyncCalled); + } + + /// + /// Property 14: Exception After Writes Uses Trailers + /// When a handler throws an exception after writing data to an IResponseStream, + /// the error is reported via trailers (ReportErrorAsync) rather than standard error reporting. + /// **Validates: Requirements 5.6, 5.7** + /// + [Fact] + public async Task MidstreamError_ExceptionAfterWrites_ReportsViaTrailers() + { + var streamingClient = CreateStreamingClient(); + + LambdaBootstrapHandler handler = async (invocation) => + { + var stream = ResponseStreamFactory.CreateStream(Array.Empty()); + await stream.WriteAsync(Encoding.UTF8.GetBytes("partial data")); + throw new InvalidOperationException("midstream failure"); + }; + + using (var bootstrap = new LambdaBootstrap(handler, null, null, new TestEnvironmentVariables())) + { + bootstrap.Client = streamingClient; + await bootstrap.InvokeOnceAsync(); + } + + // Error should be reported via trailers on the stream, not via standard error reporting + Assert.True(streamingClient.StartStreamingResponseAsyncCalled); + Assert.NotNull(streamingClient.LastStreamingResponseStream); + Assert.True(streamingClient.LastStreamingResponseStream.HasError); + Assert.False(streamingClient.ReportInvocationErrorAsyncExceptionCalled); + } + + /// + /// Property 15: Exception Before CreateStream Uses Standard Error + /// When a handler throws an exception before calling ResponseStreamFactory.CreateStream(), + /// the error is reported using the standard Lambda error reporting mechanism. + /// **Validates: Requirements 5.7, 7.1** + /// + [Fact] + public async Task PreStreamError_ExceptionBeforeCreateStream_UsesStandardErrorReporting() + { + var streamingClient = CreateStreamingClient(); + + LambdaBootstrapHandler handler = async (invocation) => + { + await Task.Yield(); + throw new InvalidOperationException("pre-stream failure"); + }; + + using (var bootstrap = new LambdaBootstrap(handler, null, null, new TestEnvironmentVariables())) + { + bootstrap.Client = streamingClient; + await bootstrap.InvokeOnceAsync(); + } + + Assert.False(streamingClient.StartStreamingResponseAsyncCalled); + Assert.True(streamingClient.ReportInvocationErrorAsyncExceptionCalled); + } + + /// + /// State Isolation: ResponseStreamFactory state is cleared after each invocation. + /// **Validates: Requirements 6.5, 8.9** + /// + [Fact] + public async Task Cleanup_ResponseStreamFactoryStateCleared_AfterInvocation() + { + var streamingClient = CreateStreamingClient(); + + LambdaBootstrapHandler handler = async (invocation) => + { + var stream = ResponseStreamFactory.CreateStream(Array.Empty()); + await stream.WriteAsync(Encoding.UTF8.GetBytes("data")); + return new InvocationResponse(Stream.Null, false); + }; + + using (var bootstrap = new LambdaBootstrap(handler, null, null, new TestEnvironmentVariables())) + { + bootstrap.Client = streamingClient; + await bootstrap.InvokeOnceAsync(); + } + + // After invocation, factory state should be cleaned up + Assert.Null(ResponseStreamFactory.GetStreamIfCreated(false)); + Assert.Null(ResponseStreamFactory.GetSendTask(false)); + } } } diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaContextSerializerTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaContextSerializerTests.cs new file mode 100644 index 000000000..8b62c25ef --- /dev/null +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaContextSerializerTests.cs @@ -0,0 +1,224 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +#pragma warning disable AWSLAMBDA001 // ILambdaContext.Serializer is preview; this is the test that proves it works. +using Amazon.Lambda.Core; +using Amazon.Lambda.RuntimeSupport.Bootstrap; +using Amazon.Lambda.RuntimeSupport.Helpers; +using Amazon.Lambda.Serialization.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Xunit; + +namespace Amazon.Lambda.RuntimeSupport.UnitTests +{ + /// + /// Verifies that the serializer registered with a / + /// is exposed on the per-invocation + /// via . + /// + public class LambdaContextSerializerTests + { + private static readonly JsonSerializer SharedSerializer = new JsonSerializer(); + + private readonly TestEnvironmentVariables _environmentVariables; + private readonly LambdaEnvironment _lambdaEnvironment; + private readonly RuntimeApiHeaders _runtimeApiHeaders; + private readonly Dictionary> _headers; + + public LambdaContextSerializerTests() + { + _environmentVariables = new TestEnvironmentVariables(); + _lambdaEnvironment = new LambdaEnvironment(_environmentVariables); + + _headers = new Dictionary> + { + [RuntimeApiHeaders.HeaderAwsRequestId] = new[] { "request-id" }, + [RuntimeApiHeaders.HeaderInvokedFunctionArn] = new[] { "invoked-function-arn" } + }; + _runtimeApiHeaders = new RuntimeApiHeaders(_headers); + } + + [Fact] + public void LambdaContext_Serializer_DefaultsToNull() + { + var context = new LambdaContext(_runtimeApiHeaders, _lambdaEnvironment, new LogLevelLoggerWriter(new SystemEnvironmentVariables())); + + Assert.Null(context.Serializer); + } + + [Fact] + public void HandlerWrapper_PocoInOut_ExposesSerializer() + { + using var handlerWrapper = HandlerWrapper.GetHandlerWrapper( + input => Task.FromResult(new PocoOutput()), + SharedSerializer); + + Assert.Same(SharedSerializer, handlerWrapper.Serializer); + } + + [Fact] + public void HandlerWrapper_RawStreamOverloads_HaveNullSerializer() + { + using var handlerWrapper = HandlerWrapper.GetHandlerWrapper( + (Func>)((input) => Task.FromResult(new MemoryStream()))); + + Assert.Null(handlerWrapper.Serializer); + } + + [Fact] + public void HandlerWrapper_SerializerOverloadFamilies_PropagateSerializer() + { + // One sample per overload family (Func/Action × Task/non-Task × in/out × ILambdaContext) — + // they share the same field-assignment line. This guards against future overloads being + // added without setting Serializer, but only spot-checks each family rather than every + // overload signature. + using (var w = HandlerWrapper.GetHandlerWrapper((input) => Task.CompletedTask, SharedSerializer)) + Assert.Same(SharedSerializer, w.Serializer); + + using (var w = HandlerWrapper.GetHandlerWrapper((input, ctx) => Task.CompletedTask, SharedSerializer)) + Assert.Same(SharedSerializer, w.Serializer); + + using (var w = HandlerWrapper.GetHandlerWrapper( + (Func>)((input) => Task.FromResult(new MemoryStream())), SharedSerializer)) + Assert.Same(SharedSerializer, w.Serializer); + + using (var w = HandlerWrapper.GetHandlerWrapper(() => Task.FromResult(new PocoOutput()), SharedSerializer)) + Assert.Same(SharedSerializer, w.Serializer); + + using (var w = HandlerWrapper.GetHandlerWrapper( + (input, ctx) => Task.FromResult(new PocoOutput()), SharedSerializer)) + Assert.Same(SharedSerializer, w.Serializer); + + using (var w = HandlerWrapper.GetHandlerWrapper((Action)(input => { }), SharedSerializer)) + Assert.Same(SharedSerializer, w.Serializer); + + using (var w = HandlerWrapper.GetHandlerWrapper((Func)(() => new PocoOutput()), SharedSerializer)) + Assert.Same(SharedSerializer, w.Serializer); + } + + [Fact] + public async Task LambdaBootstrap_InvokeOnce_SetsSerializerOnContext() + { + // End-to-end: a HandlerWrapper-backed bootstrap invokes once against a test + // RuntimeApiClient. The user's handler reads context.Serializer mid-invocation + // and must see the registered instance — proving SetSerializerOnContext fires + // during the invoke loop. + ILambdaSerializer observed = null; + using var handlerWrapper = HandlerWrapper.GetHandlerWrapper( + (input, ctx) => + { + observed = ctx.Serializer; + return Task.FromResult(new PocoOutput()); + }, + SharedSerializer); + + using var bootstrap = new LambdaBootstrap(handlerWrapper); + var testClient = new TestRuntimeApiClient(_environmentVariables, _headers) + { + FunctionInput = SerializeToBytes(new PocoInput { InputInt = 1, InputString = "x" }) + }; + bootstrap.Client = testClient; + + await bootstrap.InvokeOnceAsync(); + + Assert.Same(SharedSerializer, observed); + } + + [Fact] + public async Task LambdaBootstrap_InvokeOnce_RawStreamHandler_LeavesSerializerNull() + { + // Raw-stream handlers don't register a serializer — context.Serializer must + // stay null even after the invoke loop runs. + ILambdaSerializer observed = SharedSerializer; // start non-null to prove it's set to null + using var handlerWrapper = HandlerWrapper.GetHandlerWrapper( + (Func)((input, ctx) => + { + observed = ctx.Serializer; + return Task.CompletedTask; + })); + + using var bootstrap = new LambdaBootstrap(handlerWrapper); + var testClient = new TestRuntimeApiClient(_environmentVariables, _headers); + bootstrap.Client = testClient; + + await bootstrap.InvokeOnceAsync(); + + Assert.Null(observed); + } + + [Fact] + public void UserCodeLoader_Init_PopulatesCustomerSerializerFromAssemblyAttribute() + { + // Class-library mode: [assembly: LambdaSerializer(typeof(JsonSerializer))] on the + // HandlerTest assembly should make UserCodeLoader.Init resolve a JsonSerializer + // instance. This is what RuntimeSupportInitializer reads and pushes onto + // LambdaBootstrap.SetSerializer in production. + var ucl = new UserCodeLoader( + new SystemEnvironmentVariables(), + "HandlerTest::HandlerTest.CustomerType::ZeroInZeroOut", + InternalLogger.NoOpLogger); + + ucl.Init(message => { }); + + Assert.NotNull(ucl.CustomerSerializerInstance); + Assert.IsType(ucl.CustomerSerializerInstance); + } + + [Fact] + public async Task LambdaBootstrap_SetSerializer_FlowsAssemblySerializerToContext() + { + // End-to-end class-library wiring: the value UserCodeLoader.Init resolves from + // [assembly: LambdaSerializer] is what RuntimeSupportInitializer pushes onto the + // bootstrap via SetSerializer, after which the invoke loop must surface it on + // ILambdaContext.Serializer for every invocation. + var ucl = new UserCodeLoader( + new SystemEnvironmentVariables(), + "HandlerTest::HandlerTest.CustomerType::ZeroInZeroOut", + InternalLogger.NoOpLogger); + ucl.Init(message => { }); + var assemblySerializer = (ILambdaSerializer)ucl.CustomerSerializerInstance; + + ILambdaSerializer observed = null; + using var handlerWrapper = HandlerWrapper.GetHandlerWrapper( + (input, ctx) => + { + observed = ctx.Serializer; + return Task.FromResult(new PocoOutput()); + }, + SharedSerializer); + + using var bootstrap = new LambdaBootstrap(handlerWrapper); + bootstrap.SetSerializer(assemblySerializer); + var testClient = new TestRuntimeApiClient(_environmentVariables, _headers) + { + FunctionInput = SerializeToBytes(new PocoInput { InputInt = 1, InputString = "x" }) + }; + bootstrap.Client = testClient; + + await bootstrap.InvokeOnceAsync(); + + Assert.Same(assemblySerializer, observed); + } + + private static byte[] SerializeToBytes(T value) + { + using var ms = new MemoryStream(); + SharedSerializer.Serialize(value, ms); + return ms.ToArray(); + } + } +} diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaContextTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaContextTests.cs index d8964b912..56cf83819 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaContextTests.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaContextTests.cs @@ -31,7 +31,7 @@ public void RemainingTimeIsPositive() var runtimeApiHeaders = new RuntimeApiHeaders(headers); var lambdaEnvironment = new LambdaEnvironment(_environmentVariables); - var context = new LambdaContext(runtimeApiHeaders, lambdaEnvironment, new Helpers.SimpleLoggerWriter(new SystemEnvironmentVariables())); + var context = new LambdaContext(runtimeApiHeaders, lambdaEnvironment, new Helpers.LogLevelLoggerWriter(new SystemEnvironmentVariables())); Assert.True(context.RemainingTime >= TimeSpan.Zero, $"Remaining time is not a positive value: {context.RemainingTime}"); } @@ -49,7 +49,7 @@ public void RuntimeApiHeadersAddedToContext() var runtimeApiHeaders = new RuntimeApiHeaders(headers); var lambdaEnvironment = new LambdaEnvironment(_environmentVariables); - var context = new LambdaContext(runtimeApiHeaders, lambdaEnvironment, new Helpers.SimpleLoggerWriter(new SystemEnvironmentVariables())); + var context = new LambdaContext(runtimeApiHeaders, lambdaEnvironment, new Helpers.LogLevelLoggerWriter(new SystemEnvironmentVariables())); Assert.Equal("request-generated-id", context.AwsRequestId); Assert.Equal("my-function-arn", context.InvokedFunctionArn); diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaResponseStreamingCoreTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaResponseStreamingCoreTests.cs new file mode 100644 index 000000000..5da3d5e8b --- /dev/null +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaResponseStreamingCoreTests.cs @@ -0,0 +1,556 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +#pragma warning disable CA2252 + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Amazon.Lambda.Core.ResponseStreaming; +using Amazon.Lambda.RuntimeSupport.Client.ResponseStreaming; +using Xunit; + +namespace Amazon.Lambda.RuntimeSupport.UnitTests +{ + // ───────────────────────────────────────────────────────────────────────────── + // HttpResponseStreamPrelude.ToByteArray() tests + // ───────────────────────────────────────────────────────────────────────────── + + public class HttpResponseStreamPreludeTests + { + private static JsonDocument ParsePrelude(HttpResponseStreamPrelude prelude) + => JsonDocument.Parse(prelude.ToByteArray()); + + [Fact] + public void ToByteArray_EmptyPrelude_ProducesEmptyJsonObject() + { + var prelude = new HttpResponseStreamPrelude(); + var doc = ParsePrelude(prelude); + + Assert.Equal(JsonValueKind.Object, doc.RootElement.ValueKind); + // No properties should be present + Assert.False(doc.RootElement.TryGetProperty("statusCode", out _)); + Assert.False(doc.RootElement.TryGetProperty("headers", out _)); + Assert.False(doc.RootElement.TryGetProperty("multiValueHeaders", out _)); + Assert.False(doc.RootElement.TryGetProperty("cookies", out _)); + } + + [Fact] + public void ToByteArray_WithStatusCode_IncludesStatusCode() + { + var prelude = new HttpResponseStreamPrelude { StatusCode = HttpStatusCode.OK }; + var doc = ParsePrelude(prelude); + + Assert.True(doc.RootElement.TryGetProperty("statusCode", out var sc)); + Assert.Equal(200, sc.GetInt32()); + } + + [Fact] + public void ToByteArray_WithHeaders_IncludesHeaders() + { + var prelude = new HttpResponseStreamPrelude + { + Headers = new Dictionary + { + ["Content-Type"] = "application/json", + ["X-Custom"] = "value" + } + }; + var doc = ParsePrelude(prelude); + + Assert.True(doc.RootElement.TryGetProperty("headers", out var headers)); + Assert.Equal("application/json", headers.GetProperty("Content-Type").GetString()); + Assert.Equal("value", headers.GetProperty("X-Custom").GetString()); + } + + [Fact] + public void ToByteArray_WithMultiValueHeaders_IncludesMultiValueHeaders() + { + var prelude = new HttpResponseStreamPrelude + { + MultiValueHeaders = new Dictionary> + { + ["Set-Cookie"] = new List { "a=1", "b=2" } + } + }; + var doc = ParsePrelude(prelude); + + Assert.True(doc.RootElement.TryGetProperty("multiValueHeaders", out var mvh)); + var cookies = mvh.GetProperty("Set-Cookie"); + Assert.Equal(JsonValueKind.Array, cookies.ValueKind); + Assert.Equal(2, cookies.GetArrayLength()); + } + + [Fact] + public void ToByteArray_WithCookies_IncludesCookies() + { + var prelude = new HttpResponseStreamPrelude + { + Cookies = new List { "session=abc", "pref=dark" } + }; + var doc = ParsePrelude(prelude); + + Assert.True(doc.RootElement.TryGetProperty("cookies", out var cookies)); + Assert.Equal(JsonValueKind.Array, cookies.ValueKind); + Assert.Equal(2, cookies.GetArrayLength()); + Assert.Equal("session=abc", cookies[0].GetString()); + } + + [Fact] + public void ToByteArray_AllFieldsPopulated_ProducesCorrectJson() + { + var prelude = new HttpResponseStreamPrelude + { + StatusCode = HttpStatusCode.Created, + Headers = new Dictionary { ["X-Req"] = "1" }, + MultiValueHeaders = new Dictionary> { ["X-Multi"] = new List { "a", "b" } }, + Cookies = new List { "c=1" } + }; + var doc = ParsePrelude(prelude); + + Assert.Equal(201, doc.RootElement.GetProperty("statusCode").GetInt32()); + Assert.Equal("1", doc.RootElement.GetProperty("headers").GetProperty("X-Req").GetString()); + Assert.Equal(2, doc.RootElement.GetProperty("multiValueHeaders").GetProperty("X-Multi").GetArrayLength()); + Assert.Equal("c=1", doc.RootElement.GetProperty("cookies")[0].GetString()); + } + + [Fact] + public void ToByteArray_EmptyCollections_OmitsThoseFields() + { + var prelude = new HttpResponseStreamPrelude + { + StatusCode = HttpStatusCode.OK, + Headers = new Dictionary(), // empty — should be omitted + MultiValueHeaders = new Dictionary>(), // empty + Cookies = new List() // empty + }; + var doc = ParsePrelude(prelude); + + Assert.True(doc.RootElement.TryGetProperty("statusCode", out _)); + Assert.False(doc.RootElement.TryGetProperty("headers", out _)); + Assert.False(doc.RootElement.TryGetProperty("multiValueHeaders", out _)); + Assert.False(doc.RootElement.TryGetProperty("cookies", out _)); + } + + [Fact] + public void ToByteArray_ProducesValidUtf8() + { + var prelude = new HttpResponseStreamPrelude + { + StatusCode = HttpStatusCode.OK, + Headers = new Dictionary { ["Content-Type"] = "text/plain; charset=utf-8" } + }; + var bytes = prelude.ToByteArray(); + + // Should not throw + var text = Encoding.UTF8.GetString(bytes); + Assert.NotEmpty(text); + } + } + + // ───────────────────────────────────────────────────────────────────────────── + // LambdaResponseStream (Stream subclass) tests + // ───────────────────────────────────────────────────────────────────────────── + + public class LambdaResponseStreamTests + { + /// + /// Creates a LambdaResponseStream backed by a real ResponseStream wired to a MemoryStream. + /// + private static async Task<(LambdaResponseStream lambdaStream, MemoryStream httpOutput)> CreateWiredLambdaStream() + { + var inner = new ResponseStream(Array.Empty()); + var output = new MemoryStream(); + await inner.SetHttpOutputStreamAsync(output); + + var implStream = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var lambdaStream = new LambdaResponseStream(implStream); + return (lambdaStream, output); + } + + [Fact] + public void LambdaResponseStream_IsStreamSubclass() + { + var inner = new ResponseStream(Array.Empty()); + var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var stream = new LambdaResponseStream(impl); + + Assert.IsAssignableFrom(stream); + } + + [Fact] + public void CanWrite_IsTrue() + { + var inner = new ResponseStream(Array.Empty()); + var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var stream = new LambdaResponseStream(impl); + + Assert.True(stream.CanWrite); + } + + [Fact] + public void CanRead_IsFalse() + { + var inner = new ResponseStream(Array.Empty()); + var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var stream = new LambdaResponseStream(impl); + + Assert.False(stream.CanRead); + } + + [Fact] + public void CanSeek_IsFalse() + { + var inner = new ResponseStream(Array.Empty()); + var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var stream = new LambdaResponseStream(impl); + + Assert.False(stream.CanSeek); + } + + [Fact] + public void Read_ThrowsNotImplementedException() + { + var inner = new ResponseStream(Array.Empty()); + var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var stream = new LambdaResponseStream(impl); + + Assert.Throws(() => stream.Read(new byte[1], 0, 1)); + } + + [Fact] + public void ReadAsync_ThrowsNotImplementedException() + { + var inner = new ResponseStream(Array.Empty()); + var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var stream = new LambdaResponseStream(impl); + + // ReadAsync throws synchronously (not async) — capture the thrown task + var ex = Assert.Throws( + () => { var _ = stream.ReadAsync(new byte[1], 0, 1, CancellationToken.None); }); + Assert.NotNull(ex); + } + + [Fact] + public void Seek_ThrowsNotImplementedException() + { + var inner = new ResponseStream(Array.Empty()); + var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var stream = new LambdaResponseStream(impl); + + Assert.Throws(() => stream.Seek(0, SeekOrigin.Begin)); + } + + [Fact] + public void Position_Get_ThrowsNotSupportedException() + { + var inner = new ResponseStream(Array.Empty()); + var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var stream = new LambdaResponseStream(impl); + + Assert.Throws(() => _ = stream.Position); + } + + [Fact] + public void Position_Set_ThrowsNotSupportedException() + { + var inner = new ResponseStream(Array.Empty()); + var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var stream = new LambdaResponseStream(impl); + + Assert.Throws(() => stream.Position = 0); + } + + [Fact] + public void SetLength_ThrowsNotSupportedException() + { + var inner = new ResponseStream(Array.Empty()); + var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var stream = new LambdaResponseStream(impl); + + Assert.Throws(() => stream.SetLength(100)); + } + + [Fact] + public async Task WriteAsync_WritesRawBytesToHttpStream() + { + var (stream, output) = await CreateWiredLambdaStream(); + var data = Encoding.UTF8.GetBytes("hello streaming"); + + await stream.WriteAsync(data, 0, data.Length); + + Assert.Equal(data, output.ToArray()); + } + + [Fact] + public async Task Write_SyncOverload_WritesRawBytes() + { + var (stream, output) = await CreateWiredLambdaStream(); + var data = new byte[] { 1, 2, 3 }; + + stream.Write(data, 0, data.Length); + + Assert.Equal(data, output.ToArray()); + } + + [Fact] + public async Task Length_ReflectsBytesWritten() + { + var (stream, _) = await CreateWiredLambdaStream(); + var data = new byte[42]; + + await stream.WriteAsync(data, 0, data.Length); + + Assert.Equal(42, stream.Length); + Assert.Equal(42, stream.BytesWritten); + } + + [Fact] + public async Task Flush_IsNoOp() + { + var (stream, _) = await CreateWiredLambdaStream(); + // Should not throw + stream.Flush(); + } + + [Fact] + public async Task WriteAsync_ByteArrayOverload_WritesFullArray() + { + var (stream, output) = await CreateWiredLambdaStream(); + var data = new byte[] { 0xDE, 0xAD, 0xBE, 0xEF }; + + await stream.WriteAsync(data); + + Assert.Equal(data, output.ToArray()); + } + } + + // ───────────────────────────────────────────────────────────────────────────── + // ImplLambdaResponseStream (bridge class) tests + // ───────────────────────────────────────────────────────────────────────────── + + public class ImplLambdaResponseStreamTests + { + [Fact] + public async Task WriteAsync_DelegatesToInnerResponseStream() + { + var inner = new ResponseStream(Array.Empty()); + var output = new MemoryStream(); + await inner.SetHttpOutputStreamAsync(output); + + var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + var data = new byte[] { 1, 2, 3 }; + + await impl.WriteAsync(data, 0, data.Length); + + Assert.Equal(data, output.ToArray()); + } + + [Fact] + public async Task BytesWritten_ReflectsInnerStreamBytesWritten() + { + var inner = new ResponseStream(Array.Empty()); + var output = new MemoryStream(); + await inner.SetHttpOutputStreamAsync(output); + + var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + await impl.WriteAsync(new byte[7], 0, 7); + + Assert.Equal(7, impl.BytesWritten); + } + + [Fact] + public void HasError_InitiallyFalse() + { + var inner = new ResponseStream(Array.Empty()); + var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + + Assert.False(impl.HasError); + } + + [Fact] + public void HasError_TrueAfterReportError() + { + var inner = new ResponseStream(Array.Empty()); + inner.ReportError(new Exception("test")); + + var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + + Assert.True(impl.HasError); + } + + [Fact] + public void Dispose_DisposesInnerStream() + { + var inner = new ResponseStream(Array.Empty()); + var impl = new ResponseStreamLambdaCoreInitializerIsolated.ImplLambdaResponseStream(inner); + + // Should not throw + impl.Dispose(); + } + } + + // ───────────────────────────────────────────────────────────────────────────── + // LambdaResponseStreamFactory tests + // ───────────────────────────────────────────────────────────────────────────── + + [Collection("RuntimeSupportStateCheck")] + public class LambdaResponseStreamFactoryTests : IDisposable + { + + public LambdaResponseStreamFactoryTests() + { + // Wire up the factory via the initializer (same as production bootstrap does) + ResponseStreamLambdaCoreInitializerIsolated.InitializeCore(); + } + + public void Dispose() + { + ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: false); + } + + private void InitializeInvocation(string requestId = "test-req") + { + var envVars = new TestEnvironmentVariables(); + var client = new NoOpStreamingRuntimeApiClient(envVars); + ResponseStreamFactory.InitializeInvocation(requestId, false, client, CancellationToken.None); + } + + /// + /// Minimal RuntimeApiClient that accepts StartStreamingResponseAsync without real HTTP. + /// + private class NoOpStreamingRuntimeApiClient : RuntimeApiClient + { + public NoOpStreamingRuntimeApiClient(IEnvironmentVariables envVars) + : base(envVars, new TestHelpers.NoOpInternalRuntimeApiClient()) { } + + internal override async Task StartStreamingResponseAsync( + string awsRequestId, ResponseStream responseStream, CancellationToken cancellationToken = default) + { + // Provide the HTTP output stream so writes don't block + await responseStream.SetHttpOutputStreamAsync(new MemoryStream(), cancellationToken); + await responseStream.WaitForCompletionAsync(cancellationToken); + return new NoOpDisposable(); + } + } + + [Fact] + public void CreateStream_ReturnsLambdaResponseStream() + { + InitializeInvocation(); + + var stream = LambdaResponseStreamFactory.CreateStream(); + + Assert.NotNull(stream); + Assert.IsType(stream); + } + + [Fact] + public void CreateStream_ReturnsStreamSubclass() + { + InitializeInvocation(); + + var stream = LambdaResponseStreamFactory.CreateStream(); + + Assert.IsAssignableFrom(stream); + } + + [Fact] + public void CreateStream_ReturnedStream_IsWritable() + { + InitializeInvocation(); + + var stream = LambdaResponseStreamFactory.CreateStream(); + + Assert.True(stream.CanWrite); + } + + [Fact] + public void CreateStream_ReturnedStream_IsNotSeekable() + { + InitializeInvocation(); + + var stream = LambdaResponseStreamFactory.CreateStream(); + + Assert.False(stream.CanSeek); + } + + [Fact] + public void CreateStream_ReturnedStream_IsNotReadable() + { + InitializeInvocation(); + + var stream = LambdaResponseStreamFactory.CreateStream(); + + Assert.False(stream.CanRead); + } + + [Fact] + public void CreateHttpStream_WithPrelude_ReturnsLambdaResponseStream() + { + InitializeInvocation(); + + var prelude = new HttpResponseStreamPrelude { StatusCode = HttpStatusCode.OK }; + var stream = LambdaResponseStreamFactory.CreateHttpStream(prelude); + + Assert.NotNull(stream); + Assert.IsType(stream); + } + + [Fact] + public void CreateHttpStream_PassesSerializedPreludeToFactory() + { + // Capture the prelude bytes passed to the inner factory + byte[] capturedPrelude = null; + LambdaResponseStreamFactory.SetLambdaResponseStream(prelude => + { + capturedPrelude = prelude; + // Return a minimal stub that satisfies the interface + return new StubLambdaResponseStream(); + }); + + var httpPrelude = new HttpResponseStreamPrelude + { + StatusCode = HttpStatusCode.Created, + Headers = new Dictionary { ["X-Test"] = "1" } + }; + LambdaResponseStreamFactory.CreateHttpStream(httpPrelude); + + Assert.NotNull(capturedPrelude); + Assert.True(capturedPrelude.Length > 0); + + // Verify the bytes are valid JSON containing the status code + var doc = JsonDocument.Parse(capturedPrelude); + Assert.Equal(201, doc.RootElement.GetProperty("statusCode").GetInt32()); + } + + [Fact] + public void CreateStream_PassesEmptyPreludeToFactory() + { + byte[] capturedPrelude = null; + LambdaResponseStreamFactory.SetLambdaResponseStream(prelude => + { + capturedPrelude = prelude; + return new StubLambdaResponseStream(); + }); + + LambdaResponseStreamFactory.CreateStream(); + + Assert.NotNull(capturedPrelude); + Assert.Empty(capturedPrelude); + } + + private class StubLambdaResponseStream : ILambdaResponseStream + { + public long BytesWritten => 0; + public bool HasError => false; + public void Dispose() { } + public Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken = default) + => Task.CompletedTask; + } + } +} diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LogMessageFormatterTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LogMessageFormatterTests.cs index 4cc5b57c5..6bd724de5 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LogMessageFormatterTests.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LogMessageFormatterTests.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Runtime.Serialization; using System.Text.Json; +using System.Text.Json.Serialization; using Xunit; using static Amazon.Lambda.RuntimeSupport.UnitTests.LogMessageFormatterTests; @@ -47,7 +48,7 @@ public void ParseLogMessageWithOpenBracketAndNoClosing() var formatter = new JsonLogMessageFormatter(); var properties = formatter.ParseProperties("{hello} before { after"); - Assert.Equal(1, properties.Count); + Assert.Single(properties); Assert.Equal("hello", properties[0].Name); Assert.Equal(MessageProperty.Directive.Default, properties[0].FormatDirective); @@ -580,6 +581,141 @@ public void CheckIfPositional(string message, bool expected) Assert.Equal(expected, isPositional); } + [Fact] + public void ConfigureStructuredLogging_OverrideSerializerOptions_UsesCustomOptions() + { + var customOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = false + }; + + // Constructing the formatter sets it as the callback target in LambdaLogger + var formatter = new JsonLogMessageFormatter(); + + // Call through the public API on LambdaLogger which flows to the formatter + Amazon.Lambda.Core.LambdaLogger.ConfigureStructuredLogging(new Amazon.Lambda.Core.StructuredLoggingOptions + { + OverrideSerializerOptions = customOptions + }); + + var timestamp = DateTime.UtcNow; + var product = new Product() { Name = "Widget", Inventory = 100 }; + + var state = new MessageState() + { + AwsRequestId = "1234", + Level = Helpers.LogLevelLoggerWriter.LogLevel.Information, + MessageTemplate = "Product is {@product}", + MessageArguments = new object[] { product }, + TimeStamp = timestamp + }; + + var json = formatter.FormatMessage(state); + var doc = JsonDocument.Parse(json); + + // With camelCase naming policy, the serialized product properties should be camelCase + Assert.Equal(JsonValueKind.Object, doc.RootElement.GetProperty("product").ValueKind); + Assert.Equal("Widget", doc.RootElement.GetProperty("product").GetProperty("name").GetString()); + Assert.Equal(100, doc.RootElement.GetProperty("product").GetProperty("inventory").GetInt32()); + } + + [Fact] + public void ConfigureStructuredLogging_NullOptions_DoesNotThrow() + { + var formatter = new JsonLogMessageFormatter(); + + Amazon.Lambda.Core.LambdaLogger.ConfigureStructuredLogging(null); + + var timestamp = DateTime.UtcNow; + var product = new Product() { Name = "Widget", Inventory = 100 }; + + var state = new MessageState() + { + AwsRequestId = "1234", + Level = Helpers.LogLevelLoggerWriter.LogLevel.Information, + MessageTemplate = "Product is {@product}", + MessageArguments = new object[] { product }, + TimeStamp = timestamp + }; + + // Should still work with default options (PascalCase) + var json = formatter.FormatMessage(state); + var doc = JsonDocument.Parse(json); + + Assert.Equal(JsonValueKind.Object, doc.RootElement.GetProperty("product").ValueKind); + Assert.Equal("Widget", doc.RootElement.GetProperty("product").GetProperty("Name").GetString()); + Assert.Equal(100, doc.RootElement.GetProperty("product").GetProperty("Inventory").GetInt32()); + } + + [Fact] + public void ConfigureStructuredLogging_NullOverrideSerializerOptions_KeepsDefaults() + { + var formatter = new JsonLogMessageFormatter(); + + Amazon.Lambda.Core.LambdaLogger.ConfigureStructuredLogging(new Amazon.Lambda.Core.StructuredLoggingOptions + { + OverrideSerializerOptions = null + }); + + var timestamp = DateTime.UtcNow; + var product = new Product() { Name = "Widget", Inventory = 100 }; + + var state = new MessageState() + { + AwsRequestId = "1234", + Level = Helpers.LogLevelLoggerWriter.LogLevel.Information, + MessageTemplate = "Product is {@product}", + MessageArguments = new object[] { product }, + TimeStamp = timestamp + }; + + // Default options use PascalCase property names + var json = formatter.FormatMessage(state); + var doc = JsonDocument.Parse(json); + + Assert.Equal(JsonValueKind.Object, doc.RootElement.GetProperty("product").ValueKind); + Assert.Equal("Widget", doc.RootElement.GetProperty("product").GetProperty("Name").GetString()); + Assert.Equal(100, doc.RootElement.GetProperty("product").GetProperty("Inventory").GetInt32()); + } + + [Fact] + public void ConfigureStructuredLogging_CustomOptionsAffectsNullHandling() + { + var customOptions = new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.Never, + WriteIndented = false + }; + + var formatter = new JsonLogMessageFormatter(); + + Amazon.Lambda.Core.LambdaLogger.ConfigureStructuredLogging(new Amazon.Lambda.Core.StructuredLoggingOptions + { + OverrideSerializerOptions = customOptions + }); + + var timestamp = DateTime.UtcNow; + var product = new Product() { Name = "Widget", Inventory = 100, Cat = null }; + + var state = new MessageState() + { + AwsRequestId = "1234", + Level = Helpers.LogLevelLoggerWriter.LogLevel.Information, + MessageTemplate = "Product is {@product}", + MessageArguments = new object[] { product }, + TimeStamp = timestamp + }; + + var json = formatter.FormatMessage(state); + var doc = JsonDocument.Parse(json); + + // With DefaultIgnoreCondition.Never, null properties should be included + Assert.Equal(JsonValueKind.Object, doc.RootElement.GetProperty("product").ValueKind); + Assert.True(doc.RootElement.GetProperty("product").TryGetProperty("Cat", out var catProp)); + Assert.Equal(JsonValueKind.Null, catProp.ValueKind); + } + public class Product { public enum Category { Food, Electronics } diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/NativeAOTTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/NativeAOTTests.cs index ae7f7aaf7..313abdbc1 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/NativeAOTTests.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/NativeAOTTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.Text; using System.Reflection; @@ -7,7 +7,6 @@ using Xunit.Abstractions; using System.IO; -#if NET8_0_OR_GREATER namespace Amazon.Lambda.RuntimeSupport.UnitTests { public class NativeAOTTests @@ -97,4 +96,3 @@ private string FindProject(string projectName) } } } -#endif \ No newline at end of file diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/RawStreamingHttpClientTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/RawStreamingHttpClientTests.cs new file mode 100644 index 000000000..34af19450 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/RawStreamingHttpClientTests.cs @@ -0,0 +1,493 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Amazon.Lambda.RuntimeSupport.Client.ResponseStreaming; +using Xunit; + +namespace Amazon.Lambda.RuntimeSupport.UnitTests +{ + // ───────────────────────────────────────────────────────────────────────────── + // RawStreamingHttpClient tests + // ───────────────────────────────────────────────────────────────────────────── + + public class RawStreamingHttpClientTests + { + // --- Constructor / host parsing --- + + [Fact] + public void Constructor_HostAndPort_ParsedCorrectly() + { + using var client = new RawStreamingHttpClient("localhost:9001"); + // No exception means parsing succeeded. Fields are private but + // we verify indirectly via Dispose not throwing. + } + + [Fact] + public void Constructor_HighPort_ParsedCorrectly() + { + using var client = new RawStreamingHttpClient("127.0.0.1:65535"); + } + + // --- Dispose --- + + [Fact] + public void Dispose_CalledTwice_DoesNotThrow() + { + var client = new RawStreamingHttpClient("localhost:9001"); + client.Dispose(); + client.Dispose(); + } + + [Fact] + public void Dispose_WithoutConnect_DoesNotThrow() + { + var client = new RawStreamingHttpClient("localhost:9001"); + client.Dispose(); + } + } + + // ───────────────────────────────────────────────────────────────────────────── + // WriteTerminatorWithTrailersAsync tests + // ───────────────────────────────────────────────────────────────────────────── + + public class WriteTerminatorWithTrailersAsyncTests + { + private static (RawStreamingHttpClient client, MemoryStream output) CreateClientWithMemoryStream() + { + var client = new RawStreamingHttpClient("localhost:9001"); + var output = new MemoryStream(); + client._networkStream = output; + return (client, output); + } + + [Fact] + public async Task WriteTerminator_StartsWithZeroChunk() + { + var (client, output) = CreateClientWithMemoryStream(); + + await client.WriteTerminatorWithTrailersAsync( + new Exception("test"), CancellationToken.None); + + var written = Encoding.UTF8.GetString(output.ToArray()); + Assert.StartsWith("0\r\n", written); + } + + [Fact] + public async Task WriteTerminator_ContainsErrorTypeTrailer() + { + var (client, output) = CreateClientWithMemoryStream(); + + await client.WriteTerminatorWithTrailersAsync( + new InvalidOperationException("bad op"), CancellationToken.None); + + var written = Encoding.UTF8.GetString(output.ToArray()); + Assert.Contains($"{StreamingConstants.ErrorTypeTrailer}: InvalidOperationException\r\n", written); + } + + [Fact] + public async Task WriteTerminator_ContainsErrorBodyTrailerHeader() + { + var (client, output) = CreateClientWithMemoryStream(); + + await client.WriteTerminatorWithTrailersAsync( + new Exception("some error"), CancellationToken.None); + + var written = Encoding.UTF8.GetString(output.ToArray()); + Assert.Contains($"{StreamingConstants.ErrorBodyTrailer}: ", written); + } + + [Fact] + public async Task WriteTerminator_ErrorBodyIsBase64Encoded() + { + var (client, output) = CreateClientWithMemoryStream(); + const string errorMessage = "something broke"; + + await client.WriteTerminatorWithTrailersAsync( + new Exception(errorMessage), CancellationToken.None); + + var written = Encoding.UTF8.GetString(output.ToArray()); + + // Extract the Base64 value from the error body trailer + var prefix = $"{StreamingConstants.ErrorBodyTrailer}: "; + var start = written.IndexOf(prefix, StringComparison.Ordinal) + prefix.Length; + var end = written.IndexOf("\r\n", start, StringComparison.Ordinal); + var base64Value = written.Substring(start, end - start); + + // Should be valid Base64 + var decoded = Encoding.UTF8.GetString(Convert.FromBase64String(base64Value)); + Assert.Contains(errorMessage, decoded); + } + + [Fact] + public async Task WriteTerminator_ErrorBodyBase64ContainsNoNewlines() + { + var (client, output) = CreateClientWithMemoryStream(); + + // Use an exception with a stack trace that would produce multi-line JSON + Exception caughtException; + try { throw new InvalidOperationException("multi\nline\nerror"); } + catch (Exception ex) { caughtException = ex; } + + await client.WriteTerminatorWithTrailersAsync( + caughtException, CancellationToken.None); + + var written = Encoding.UTF8.GetString(output.ToArray()); + + // Extract just the error body trailer line + var prefix = $"{StreamingConstants.ErrorBodyTrailer}: "; + var start = written.IndexOf(prefix, StringComparison.Ordinal) + prefix.Length; + var end = written.IndexOf("\r\n", start, StringComparison.Ordinal); + var base64Value = written.Substring(start, end - start); + + // The Base64 value itself must not contain any newlines + Assert.DoesNotContain("\n", base64Value); + Assert.DoesNotContain("\r", base64Value); + } + + [Fact] + public async Task WriteTerminator_EndsWithEmptyLine() + { + var (client, output) = CreateClientWithMemoryStream(); + + await client.WriteTerminatorWithTrailersAsync( + new Exception("test"), CancellationToken.None); + + var written = Encoding.UTF8.GetString(output.ToArray()); + // Must end with \r\n\r\n — the last trailer line's \r\n plus the empty terminator line + Assert.EndsWith("\r\n\r\n", written); + } + + [Fact] + public async Task WriteTerminator_CorrectWireFormat() + { + var (client, output) = CreateClientWithMemoryStream(); + + await client.WriteTerminatorWithTrailersAsync( + new ArgumentException("bad arg"), CancellationToken.None); + + var written = Encoding.UTF8.GetString(output.ToArray()); + var lines = written.Split("\r\n"); + + // Line 0: "0" (zero-length chunk) + Assert.Equal("0", lines[0]); + // Line 1: error type trailer + Assert.StartsWith($"{StreamingConstants.ErrorTypeTrailer}: ", lines[1]); + // Line 2: error body trailer (Base64) + Assert.StartsWith($"{StreamingConstants.ErrorBodyTrailer}: ", lines[2]); + // Line 3: empty (end of trailers) + Assert.Equal("", lines[3]); + } + } + + // ───────────────────────────────────────────────────────────────────────────── + // ReadAndDiscardResponseAsync tests + // ───────────────────────────────────────────────────────────────────────────── + + public class ReadAndDiscardResponseAsyncTests + { + private static (RawStreamingHttpClient client, MemoryStream input) CreateClientWithResponse(string httpResponse) + { + var client = new RawStreamingHttpClient("localhost:9001"); + var input = new MemoryStream(Encoding.ASCII.GetBytes(httpResponse)); + client._networkStream = input; + return (client, input); + } + + [Fact] + public async Task ReadAndDiscard_HeadersOnly_CompletesSuccessfully() + { + var (client, _) = CreateClientWithResponse( + "HTTP/1.1 202 Accepted\r\nContent-Length: 0\r\n\r\n"); + + await client.ReadAndDiscardResponseAsync(CancellationToken.None); + // Should complete without error + } + + [Fact] + public async Task ReadAndDiscard_WithBody_ReadsFullBody() + { + var body = "OK"; + var (client, _) = CreateClientWithResponse( + $"HTTP/1.1 200 OK\r\nContent-Length: {body.Length}\r\n\r\n{body}"); + + await client.ReadAndDiscardResponseAsync(CancellationToken.None); + } + + [Fact] + public async Task ReadAndDiscard_NoContentLength_CompletesAfterHeaders() + { + var (client, _) = CreateClientWithResponse( + "HTTP/1.1 202 Accepted\r\n\r\n"); + + await client.ReadAndDiscardResponseAsync(CancellationToken.None); + } + + [Fact] + public async Task ReadAndDiscard_EmptyStream_CompletesSuccessfully() + { + var client = new RawStreamingHttpClient("localhost:9001"); + client._networkStream = new MemoryStream(Array.Empty()); + + await client.ReadAndDiscardResponseAsync(CancellationToken.None); + } + + [Fact] + public async Task ReadAndDiscard_PartialBody_WaitsForFullBody() + { + // Content-Length says 10 but we provide all 10 bytes + var body = "0123456789"; + var (client, _) = CreateClientWithResponse( + $"HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\n{body}"); + + await client.ReadAndDiscardResponseAsync(CancellationToken.None); + } + + [Fact] + public async Task ReadAndDiscard_CancellationToken_Respected() + { + // Use a stream that blocks on read to test cancellation + var cts = new CancellationTokenSource(); + cts.Cancel(); + + var client = new RawStreamingHttpClient("localhost:9001"); + client._networkStream = new MemoryStream(Encoding.ASCII.GetBytes( + "HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\n")); + + // Should not throw — ReadAndDiscardResponseAsync catches exceptions + await client.ReadAndDiscardResponseAsync(cts.Token); + } + } + + // ───────────────────────────────────────────────────────────────────────────── + // ChunkedStreamWriter tests + // ───────────────────────────────────────────────────────────────────────────── + + public class ChunkedStreamWriterTests + { + [Fact] + public void CanWrite_IsTrue() + { + using var inner = new MemoryStream(); + using var writer = new ChunkedStreamWriter(inner); + Assert.True(writer.CanWrite); + } + + [Fact] + public void CanRead_IsFalse() + { + using var inner = new MemoryStream(); + using var writer = new ChunkedStreamWriter(inner); + Assert.False(writer.CanRead); + } + + [Fact] + public void CanSeek_IsFalse() + { + using var inner = new MemoryStream(); + using var writer = new ChunkedStreamWriter(inner); + Assert.False(writer.CanSeek); + } + + [Fact] + public void Constructor_NullStream_ThrowsArgumentNullException() + { + Assert.Throws(() => new ChunkedStreamWriter(null)); + } + + [Fact] + public void Length_ThrowsNotSupportedException() + { + using var inner = new MemoryStream(); + using var writer = new ChunkedStreamWriter(inner); + Assert.Throws(() => writer.Length); + } + + [Fact] + public void Position_Get_ThrowsNotSupportedException() + { + using var inner = new MemoryStream(); + using var writer = new ChunkedStreamWriter(inner); + Assert.Throws(() => writer.Position); + } + + [Fact] + public void Position_Set_ThrowsNotSupportedException() + { + using var inner = new MemoryStream(); + using var writer = new ChunkedStreamWriter(inner); + Assert.Throws(() => writer.Position = 0); + } + + [Fact] + public void Read_ThrowsNotSupportedException() + { + using var inner = new MemoryStream(); + using var writer = new ChunkedStreamWriter(inner); + Assert.Throws(() => writer.Read(new byte[1], 0, 1)); + } + + [Fact] + public void Seek_ThrowsNotSupportedException() + { + using var inner = new MemoryStream(); + using var writer = new ChunkedStreamWriter(inner); + Assert.Throws(() => writer.Seek(0, SeekOrigin.Begin)); + } + + [Fact] + public void SetLength_ThrowsNotSupportedException() + { + using var inner = new MemoryStream(); + using var writer = new ChunkedStreamWriter(inner); + Assert.Throws(() => writer.SetLength(0)); + } + + [Fact] + public async Task WriteAsync_ByteArray_ProducesCorrectChunkFormat() + { + using var inner = new MemoryStream(); + using var writer = new ChunkedStreamWriter(inner); + + var data = Encoding.UTF8.GetBytes("Hello"); + await writer.WriteAsync(data, 0, data.Length); + + var output = Encoding.ASCII.GetString(inner.ToArray()); + // "Hello" is 5 bytes = 0x5 + Assert.Equal("5\r\nHello\r\n", output); + } + + [Fact] + public async Task WriteAsync_ReadOnlyMemory_ProducesCorrectChunkFormat() + { + using var inner = new MemoryStream(); + using var writer = new ChunkedStreamWriter(inner); + + var data = Encoding.UTF8.GetBytes("Hi"); + await writer.WriteAsync(new ReadOnlyMemory(data)); + + var output = Encoding.ASCII.GetString(inner.ToArray()); + Assert.Equal("2\r\nHi\r\n", output); + } + + [Fact] + public async Task WriteAsync_ZeroBytes_WritesNothing() + { + using var inner = new MemoryStream(); + using var writer = new ChunkedStreamWriter(inner); + + await writer.WriteAsync(Array.Empty(), 0, 0); + + Assert.Equal(0, inner.Length); + } + + [Fact] + public async Task WriteAsync_ReadOnlyMemory_ZeroBytes_WritesNothing() + { + using var inner = new MemoryStream(); + using var writer = new ChunkedStreamWriter(inner); + + await writer.WriteAsync(ReadOnlyMemory.Empty); + + Assert.Equal(0, inner.Length); + } + + [Fact] + public async Task WriteAsync_MultipleChunks_EachCorrectlyFormatted() + { + using var inner = new MemoryStream(); + using var writer = new ChunkedStreamWriter(inner); + + await writer.WriteAsync(Encoding.UTF8.GetBytes("AB"), 0, 2); + await writer.WriteAsync(Encoding.UTF8.GetBytes("CDE"), 0, 3); + + var output = Encoding.ASCII.GetString(inner.ToArray()); + Assert.Equal("2\r\nAB\r\n3\r\nCDE\r\n", output); + } + + [Fact] + public async Task WriteAsync_LargeChunk_HexSizeCorrect() + { + using var inner = new MemoryStream(); + using var writer = new ChunkedStreamWriter(inner); + + var data = new byte[256]; + Array.Fill(data, (byte)'X'); + await writer.WriteAsync(data, 0, data.Length); + + var output = Encoding.ASCII.GetString(inner.ToArray()); + // 256 = 0x100 + Assert.StartsWith("100\r\n", output); + Assert.EndsWith("\r\n", output); + } + + [Fact] + public async Task WriteAsync_WithOffset_WritesCorrectSlice() + { + using var inner = new MemoryStream(); + using var writer = new ChunkedStreamWriter(inner); + + var data = Encoding.UTF8.GetBytes("ABCDE"); + await writer.WriteAsync(data, 1, 3); // "BCD" + + var output = Encoding.ASCII.GetString(inner.ToArray()); + Assert.Equal("3\r\nBCD\r\n", output); + } + + [Fact] + public void Write_Sync_ProducesCorrectChunkFormat() + { + using var inner = new MemoryStream(); + using var writer = new ChunkedStreamWriter(inner); + + var data = Encoding.UTF8.GetBytes("OK"); + writer.Write(data, 0, data.Length); + + var output = Encoding.ASCII.GetString(inner.ToArray()); + Assert.Equal("2\r\nOK\r\n", output); + } + + [Fact] + public async Task FlushAsync_DelegatesToInnerStream() + { + var flushCalled = false; + var inner = new FlushTrackingStream(() => flushCalled = true); + using var writer = new ChunkedStreamWriter(inner); + + await writer.FlushAsync(CancellationToken.None); + + Assert.True(flushCalled); + } + + [Fact] + public void Flush_DelegatesToInnerStream() + { + var flushCalled = false; + var inner = new FlushTrackingStream(() => flushCalled = true); + using var writer = new ChunkedStreamWriter(inner); + + writer.Flush(); + + Assert.True(flushCalled); + } + + /// + /// A minimal writable stream that tracks Flush calls. + /// + private class FlushTrackingStream : MemoryStream + { + private readonly Action _onFlush; + public FlushTrackingStream(Action onFlush) => _onFlush = onFlush; + public override void Flush() { _onFlush(); base.Flush(); } + public override Task FlushAsync(CancellationToken cancellationToken) + { + _onFlush(); + return base.FlushAsync(cancellationToken); + } + } + } +} diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamFactoryTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamFactoryTests.cs new file mode 100644 index 000000000..0b49eb27a --- /dev/null +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamFactoryTests.cs @@ -0,0 +1,284 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System; +using System.Threading; +using System.Threading.Tasks; +using Amazon.Lambda.RuntimeSupport.Client.ResponseStreaming; +using Xunit; + +namespace Amazon.Lambda.RuntimeSupport.UnitTests +{ + [Collection("RuntimeSupportStateCheck")] + public class ResponseStreamFactoryTests : IDisposable + { + private const long MaxResponseSize = 20 * 1024 * 1024; + + public void Dispose() + { + // Clean up both modes to avoid test pollution + ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: false); + ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: true); + } + + /// + /// A minimal RuntimeApiClient subclass for testing that overrides StartStreamingResponseAsync + /// to avoid real HTTP calls while tracking invocations. + /// + private class MockStreamingRuntimeApiClient : RuntimeApiClient + { + public bool StartStreamingCalled { get; private set; } + public string LastAwsRequestId { get; private set; } + public ResponseStream LastResponseStream { get; private set; } + public TaskCompletionSource SendTaskCompletion { get; } = new TaskCompletionSource(); + + public MockStreamingRuntimeApiClient() + : base(new TestEnvironmentVariables(), new TestHelpers.NoOpInternalRuntimeApiClient()) + { + } + + internal override async Task StartStreamingResponseAsync( + string awsRequestId, ResponseStream responseStream, CancellationToken cancellationToken = default) + { + StartStreamingCalled = true; + LastAwsRequestId = awsRequestId; + LastResponseStream = responseStream; + await SendTaskCompletion.Task; + return new NoOpDisposable(); + } + } + + private void InitializeWithMock(string requestId, bool isMultiConcurrency, MockStreamingRuntimeApiClient mockClient) + { + ResponseStreamFactory.InitializeInvocation( + requestId, isMultiConcurrency, + mockClient, CancellationToken.None); + } + + // --- Property 1: CreateStream Returns Valid Stream --- + + /// + /// Property 1: CreateStream Returns Valid Stream - on-demand mode. + /// Validates: Requirements 1.3, 2.2, 2.3 + /// + [Fact] + public void CreateStream_OnDemandMode_ReturnsValidStream() + { + var mock = new MockStreamingRuntimeApiClient(); + InitializeWithMock("req-1", isMultiConcurrency: false, mock); + + var stream = ResponseStreamFactory.CreateStream(Array.Empty()); + + Assert.NotNull(stream); + Assert.IsAssignableFrom(stream); + } + + /// + /// Property 1: CreateStream Returns Valid Stream - multi-concurrency mode. + /// Validates: Requirements 1.3, 2.2, 2.3 + /// + [Fact] + public void CreateStream_MultiConcurrencyMode_ReturnsValidStream() + { + var mock = new MockStreamingRuntimeApiClient(); + InitializeWithMock("req-2", isMultiConcurrency: true, mock); + + var stream = ResponseStreamFactory.CreateStream(Array.Empty()); + + Assert.NotNull(stream); + Assert.IsAssignableFrom(stream); + } + + // --- Property 4: Single Stream Per Invocation --- + + /// + /// Property 4: Single Stream Per Invocation - calling CreateStream twice throws. + /// Validates: Requirements 2.5, 2.6 + /// + [Fact] + public void CreateStream_CalledTwice_ThrowsInvalidOperationException() + { + var mock = new MockStreamingRuntimeApiClient(); + InitializeWithMock("req-3", isMultiConcurrency: false, mock); + ResponseStreamFactory.CreateStream(Array.Empty()); + + Assert.Throws(() => ResponseStreamFactory.CreateStream(Array.Empty())); + } + + [Fact] + public void CreateStream_OutsideInvocationContext_ThrowsInvalidOperationException() + { + // No InitializeInvocation called + Assert.Throws(() => ResponseStreamFactory.CreateStream(Array.Empty())); + } + + // --- CreateStream starts HTTP POST --- + + /// + /// Validates that CreateStream calls StartStreamingResponseAsync on the RuntimeApiClient. + /// Validates: Requirements 1.3, 1.4, 2.2, 2.3, 2.4 + /// + [Fact] + public void CreateStream_CallsStartStreamingResponseAsync() + { + var mock = new MockStreamingRuntimeApiClient(); + InitializeWithMock("req-start", isMultiConcurrency: false, mock); + + ResponseStreamFactory.CreateStream(Array.Empty()); + + Assert.True(mock.StartStreamingCalled); + Assert.Equal("req-start", mock.LastAwsRequestId); + Assert.NotNull(mock.LastResponseStream); + } + + // --- GetSendTask --- + + /// + /// Validates that GetSendTask returns the task from the HTTP POST. + /// Validates: Requirements 5.1, 7.3 + /// + [Fact] + public void GetSendTask_AfterCreateStream_ReturnsNonNullTask() + { + var mock = new MockStreamingRuntimeApiClient(); + InitializeWithMock("req-send", isMultiConcurrency: false, mock); + + ResponseStreamFactory.CreateStream(Array.Empty()); + + var sendTask = ResponseStreamFactory.GetSendTask(isMultiConcurrency: false); + Assert.NotNull(sendTask); + } + + [Fact] + public void GetSendTask_BeforeCreateStream_ReturnsNull() + { + var mock = new MockStreamingRuntimeApiClient(); + InitializeWithMock("req-nosend", isMultiConcurrency: false, mock); + + var sendTask = ResponseStreamFactory.GetSendTask(isMultiConcurrency: false); + Assert.Null(sendTask); + } + + [Fact] + public void GetSendTask_NoContext_ReturnsNull() + { + Assert.Null(ResponseStreamFactory.GetSendTask(isMultiConcurrency: false)); + } + + // --- Internal methods --- + + [Fact] + public void InitializeInvocation_OnDemand_SetsUpContext() + { + var mock = new MockStreamingRuntimeApiClient(); + InitializeWithMock("req-4", isMultiConcurrency: false, mock); + + Assert.Null(ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: false)); + + var stream = ResponseStreamFactory.CreateStream(Array.Empty()); + Assert.NotNull(stream); + } + + [Fact] + public void InitializeInvocation_MultiConcurrency_SetsUpContext() + { + var mock = new MockStreamingRuntimeApiClient(); + InitializeWithMock("req-5", isMultiConcurrency: true, mock); + + Assert.Null(ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: true)); + + var stream = ResponseStreamFactory.CreateStream(Array.Empty()); + Assert.NotNull(stream); + } + + [Fact] + public void GetStreamIfCreated_AfterCreateStream_ReturnsStream() + { + var mock = new MockStreamingRuntimeApiClient(); + InitializeWithMock("req-6", isMultiConcurrency: false, mock); + ResponseStreamFactory.CreateStream(Array.Empty()); + + var retrieved = ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: false); + Assert.NotNull(retrieved); + } + + [Fact] + public void GetStreamIfCreated_NoContext_ReturnsNull() + { + Assert.Null(ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: false)); + } + + [Fact] + public void CleanupInvocation_ClearsState() + { + var mock = new MockStreamingRuntimeApiClient(); + InitializeWithMock("req-7", isMultiConcurrency: false, mock); + ResponseStreamFactory.CreateStream(Array.Empty()); + + ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: false); + + Assert.Null(ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: false)); + Assert.Throws(() => ResponseStreamFactory.CreateStream(Array.Empty())); + } + + // --- Property 16: State Isolation Between Invocations --- + + /// + /// Property 16: State Isolation Between Invocations - state from one invocation doesn't leak to the next. + /// Validates: Requirements 6.5, 8.9 + /// + [Fact] + public void StateIsolation_SequentialInvocations_NoLeakage() + { + var mock = new MockStreamingRuntimeApiClient(); + + // First invocation - streaming + InitializeWithMock("req-8a", isMultiConcurrency: false, mock); + var stream1 = ResponseStreamFactory.CreateStream(Array.Empty()); + Assert.NotNull(stream1); + ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: false); + + // Second invocation - should start fresh + InitializeWithMock("req-8b", isMultiConcurrency: false, mock); + Assert.Null(ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: false)); + + var stream2 = ResponseStreamFactory.CreateStream(Array.Empty()); + Assert.NotNull(stream2); + ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: false); + } + + /// + /// Property 16: State Isolation - multi-concurrency mode uses AsyncLocal. + /// Validates: Requirements 2.9, 2.10 + /// + [Fact] + public async Task StateIsolation_MultiConcurrency_UsesAsyncLocal() + { + var mock = new MockStreamingRuntimeApiClient(); + InitializeWithMock("req-9", isMultiConcurrency: true, mock); + var stream = ResponseStreamFactory.CreateStream(Array.Empty()); + Assert.NotNull(stream); + + bool childSawNull = false; + await Task.Run(() => + { + ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: true); + childSawNull = ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: true) == null; + }); + + Assert.True(childSawNull); + } + } +} diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamTests.cs new file mode 100644 index 000000000..cd2c00fd2 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/ResponseStreamTests.cs @@ -0,0 +1,447 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Amazon.Lambda.RuntimeSupport.Client.ResponseStreaming; +using Xunit; + +namespace Amazon.Lambda.RuntimeSupport.UnitTests +{ + public class ResponseStreamTests + { + /// + /// Helper: creates a ResponseStream and wires up a MemoryStream as the HTTP output stream. + /// Returns both so tests can inspect what was written. + /// + private static async Task<(ResponseStream stream, MemoryStream httpOutput)> CreateWiredStream() + { + var rs = new ResponseStream(Array.Empty()); + var output = new MemoryStream(); + await rs.SetHttpOutputStreamAsync(output); + return (rs, output); + } + + // ---- Basic state tests ---- + + [Fact] + public void Constructor_InitializesStateCorrectly() + { + var stream = new ResponseStream(Array.Empty()); + + Assert.Equal(0, stream.BytesWritten); + Assert.False(stream.HasError); + Assert.Null(stream.ReportedError); + } + + [Fact] + public async Task WriteAsync_WithOffset_WritesCorrectSlice() + { + var (stream, httpOutput) = await CreateWiredStream(); + var data = new byte[] { 0, 1, 2, 3, 0 }; + + await stream.WriteAsync(data, 1, 3); + + // Raw bytes {1,2,3} written directly — no chunked encoding + var expected = new byte[] { 1, 2, 3 }; + Assert.Equal(expected, httpOutput.ToArray()); + } + + [Fact] + public async Task WriteAsync_MultipleWrites_EachAppearsImmediately() + { + var (stream, httpOutput) = await CreateWiredStream(); + + var data = new byte[] { 0xAA }; + await stream.WriteAsync(data, 0, data.Length); + var afterFirst = httpOutput.ToArray().Length; + Assert.True(afterFirst > 0, "First chunk should be on the HTTP stream immediately after WriteAsync returns"); + + await stream.WriteAsync(new byte[] { 0xBB, 0xCC }, 0, 2); + var afterSecond = httpOutput.ToArray().Length; + Assert.True(afterSecond > afterFirst, "Second chunk should appear on the HTTP stream immediately"); + + Assert.Equal(3, stream.BytesWritten); + } + + [Fact] + public async Task WriteAsync_BlocksUntilSetHttpOutputStream() + { + var rs = new ResponseStream(Array.Empty()); + var httpOutput = new MemoryStream(); + var writeStarted = new ManualResetEventSlim(false); + var writeCompleted = new ManualResetEventSlim(false); + + // Start a write on a background thread — it should block + var writeTask = Task.Run(async () => + { + writeStarted.Set(); + await rs.WriteAsync(new byte[] { 1, 2, 3 }, 0, 3); + writeCompleted.Set(); + }); + + // Wait for the write to start, then verify it hasn't completed + writeStarted.Wait(TimeSpan.FromSeconds(2)); + await Task.Delay(100); // give it a moment + Assert.False(writeCompleted.IsSet, "WriteAsync should block until SetHttpOutputStream is called"); + + // Now provide the HTTP stream — the write should complete + await rs.SetHttpOutputStreamAsync(httpOutput); + await writeTask; + + Assert.True(writeCompleted.IsSet); + Assert.True(httpOutput.ToArray().Length > 0); + } + + [Fact] + public async Task MarkCompleted_ReleasesCompletionSignal() + { + var (stream, _) = await CreateWiredStream(); + + var waitTask = stream.WaitForCompletionAsync(); + Assert.False(waitTask.IsCompleted, "WaitForCompletionAsync should block before MarkCompleted"); + + stream.MarkCompleted(); + + // Should complete within a reasonable time + var completed = await Task.WhenAny(waitTask, Task.Delay(TimeSpan.FromSeconds(2))); + Assert.Same(waitTask, completed); + } + + [Fact] + public async Task ReportErrorAsync_ReleasesCompletionSignal() + { + var (stream, _) = await CreateWiredStream(); + + var waitTask = stream.WaitForCompletionAsync(); + Assert.False(waitTask.IsCompleted, "WaitForCompletionAsync should block before ReportErrorAsync"); + + stream.ReportError(new Exception("test error")); + + var completed = await Task.WhenAny(waitTask, Task.Delay(TimeSpan.FromSeconds(2))); + Assert.Same(waitTask, completed); + Assert.True(stream.HasError); + } + + [Fact] + public async Task WriteAsync_AfterMarkCompleted_StillSucceeds() + { + var (stream, output) = await CreateWiredStream(); + await stream.WriteAsync(new byte[] { 1 }, 0, 1); + stream.MarkCompleted(); + + // Writes after MarkCompleted are allowed — buffered ASP.NET Core responses + // (e.g. Results.Json) may flush pre-start buffer data after the pipeline + // completes and LambdaBootstrap calls MarkCompleted. + await stream.WriteAsync(new byte[] { 2 }, 0, 1); + + Assert.Equal(new byte[] { 1, 2 }, output.ToArray()); + } + + [Fact] + public async Task WriteAsync_AfterReportError_Throws() + { + var (stream, _) = await CreateWiredStream(); + await stream.WriteAsync(new byte[] { 1 }, 0, 1); + stream.ReportError(new Exception("test")); + + await Assert.ThrowsAsync( + () => stream.WriteAsync(new byte[] { 2 }, 0, 1)); + } + + [Fact] + public async Task ReportErrorAsync_SetsErrorState() + { + var stream = new ResponseStream(Array.Empty()); + var exception = new InvalidOperationException("something broke"); + + stream.ReportError(exception); + + Assert.True(stream.HasError); + Assert.Same(exception, stream.ReportedError); + } + + [Fact] + public async Task ReportErrorAsync_AfterCompleted_Throws() + { + var stream = new ResponseStream(Array.Empty()); + stream.MarkCompleted(); + + Assert.Throws( + () => stream.ReportError(new Exception("test"))); + } + + [Fact] + public async Task ReportErrorAsync_CalledTwice_Throws() + { + var stream = new ResponseStream(Array.Empty()); + stream.ReportError(new Exception("first")); + + Assert.Throws( + () => stream.ReportError(new Exception("second"))); + } + + [Fact] + public async Task WriteAsync_NullBuffer_ThrowsArgumentNull() + { + var (stream, _) = await CreateWiredStream(); + + await Assert.ThrowsAsync(() => stream.WriteAsync((byte[])null, 0, 0)); + } + + [Fact] + public async Task WriteAsync_NullBufferWithOffset_ThrowsArgumentNull() + { + var (stream, _) = await CreateWiredStream(); + + await Assert.ThrowsAsync(() => stream.WriteAsync(null, 0, 0)); + } + + [Fact] + public async Task ReportErrorAsync_NullException_ThrowsArgumentNull() + { + var stream = new ResponseStream(Array.Empty()); + + Assert.Throws(() => stream.ReportError(null)); + } + + [Fact] + public async Task Dispose_CalledTwice_DoesNotThrow() + { + var stream = new ResponseStream(Array.Empty()); + stream.Dispose(); + // Second dispose should be a no-op + stream.Dispose(); + } + + // ---- Prelude tests ---- + + [Fact] + public async Task SetHttpOutputStreamAsync_WithPrelude_WritesPreludeBeforeHandlerData() + { + var prelude = new byte[] { 0x01, 0x02, 0x03 }; + var rs = new ResponseStream(prelude); + var output = new MemoryStream(); + + await rs.SetHttpOutputStreamAsync(output); + + // Prelude bytes + 8-byte null delimiter should be written before any handler data + var written = output.ToArray(); + Assert.True(written.Length >= prelude.Length + 8, "Prelude + delimiter should be written"); + Assert.Equal(prelude, written[..prelude.Length]); + Assert.Equal(new byte[8], written[prelude.Length..(prelude.Length + 8)]); + } + + [Fact] + public async Task SetHttpOutputStreamAsync_WithEmptyPrelude_WritesNoPreludeBytes() + { + var rs = new ResponseStream(Array.Empty()); + var output = new MemoryStream(); + + await rs.SetHttpOutputStreamAsync(output); + + // Empty prelude — nothing written yet (handler hasn't written anything) + Assert.Empty(output.ToArray()); + } + + [Fact] + public async Task SetHttpOutputStreamAsync_WithPrelude_HandlerDataAppendsAfterDelimiter() + { + var prelude = new byte[] { 0xAA, 0xBB }; + var rs = new ResponseStream(prelude); + var output = new MemoryStream(); + + await rs.SetHttpOutputStreamAsync(output); + await rs.WriteAsync(new byte[] { 0xFF }, 0, 1); + + var written = output.ToArray(); + // Layout: [prelude][8 null bytes][handler data] + int expectedMinLength = prelude.Length + 8 + 1; + Assert.Equal(expectedMinLength, written.Length); + Assert.Equal(new byte[] { 0xFF }, written[^1..]); + } + + [Fact] + public async Task SetHttpOutputStreamAsync_NullPrelude_WritesNoPreludeBytes() + { + var rs = new ResponseStream(null); + var output = new MemoryStream(); + + await rs.SetHttpOutputStreamAsync(output); + + Assert.Empty(output.ToArray()); + } + + // ---- Prelude + delimiter single-chunk tests (via ChunkedStreamWriter) ---- + + [Fact] + public async Task SetHttpOutputStreamAsync_WithPrelude_ViaChunkedWriter_ProducesSingleChunk() + { + var preludeJson = Encoding.UTF8.GetBytes("{\"statusCode\":200}"); + var rs = new ResponseStream(preludeJson); + var rawOutput = new MemoryStream(); + var chunkedWriter = new ChunkedStreamWriter(rawOutput); + + await rs.SetHttpOutputStreamAsync(chunkedWriter); + + var wireBytes = Encoding.ASCII.GetString(rawOutput.ToArray()); + + // The prelude (18 bytes) + delimiter (8 bytes) = 26 bytes = 0x1A + // Should be exactly one chunk: "1A\r\n{prelude}{8 null bytes}\r\n" + var expectedDataLength = preludeJson.Length + 8; // 26 + var expectedHex = expectedDataLength.ToString("X"); + Assert.StartsWith($"{expectedHex}\r\n", wireBytes); + + // Verify there is only one chunk header (only one hex size prefix) + var chunkCount = 0; + var remaining = wireBytes; + while (remaining.Length > 0) + { + var crlfIndex = remaining.IndexOf("\r\n", StringComparison.Ordinal); + if (crlfIndex < 0) break; + var sizeStr = remaining.Substring(0, crlfIndex); + if (int.TryParse(sizeStr, System.Globalization.NumberStyles.HexNumber, null, out var chunkSize) && chunkSize >= 0) + { + chunkCount++; + // Skip past: hex\r\n{data}\r\n + remaining = remaining.Substring(crlfIndex + 2 + chunkSize + 2); + } + else + { + break; + } + } + Assert.Equal(1, chunkCount); + } + + [Fact] + public async Task SetHttpOutputStreamAsync_WithPrelude_ViaChunkedWriter_DelimiterImmediatelyFollowsPrelude() + { + var preludeJson = Encoding.UTF8.GetBytes("{\"statusCode\":201}"); + var rs = new ResponseStream(preludeJson); + var rawOutput = new MemoryStream(); + var chunkedWriter = new ChunkedStreamWriter(rawOutput); + + await rs.SetHttpOutputStreamAsync(chunkedWriter); + + // Parse the chunk to get the raw data payload + var wireBytes = rawOutput.ToArray(); + var wireStr = Encoding.ASCII.GetString(wireBytes); + var firstCrlf = wireStr.IndexOf("\r\n", StringComparison.Ordinal); + var dataStart = firstCrlf + 2; + var dataLength = preludeJson.Length + 8; + var chunkData = new byte[dataLength]; + Array.Copy(wireBytes, dataStart, chunkData, 0, dataLength); + + // First part should be the prelude JSON + Assert.Equal(preludeJson, chunkData[..preludeJson.Length]); + // Immediately followed by 8 null bytes (delimiter) + Assert.Equal(new byte[8], chunkData[preludeJson.Length..]); + } + + [Fact] + public async Task SetHttpOutputStreamAsync_WithPrelude_ViaChunkedWriter_HandlerDataInSeparateChunk() + { + var preludeJson = Encoding.UTF8.GetBytes("{\"statusCode\":200}"); + var rs = new ResponseStream(preludeJson); + var rawOutput = new MemoryStream(); + var chunkedWriter = new ChunkedStreamWriter(rawOutput); + + await rs.SetHttpOutputStreamAsync(chunkedWriter); + await rs.WriteAsync(Encoding.UTF8.GetBytes("body data"), 0, 9); + + var wireStr = Encoding.ASCII.GetString(rawOutput.ToArray()); + + // Should have exactly 2 chunks: one for prelude+delimiter, one for body + var chunkCount = 0; + var remaining = wireStr; + while (remaining.Length > 0) + { + var crlfIndex = remaining.IndexOf("\r\n", StringComparison.Ordinal); + if (crlfIndex < 0) break; + var sizeStr = remaining.Substring(0, crlfIndex); + if (int.TryParse(sizeStr, System.Globalization.NumberStyles.HexNumber, null, out var chunkSize) && chunkSize >= 0) + { + chunkCount++; + remaining = remaining.Substring(crlfIndex + 2 + chunkSize + 2); + } + else + { + break; + } + } + Assert.Equal(2, chunkCount); + } + + // ---- MarkCompleted idempotency ---- + + [Fact] + public async Task MarkCompleted_CalledTwice_DoesNotThrowOrDoubleRelease() + { + var (stream, _) = await CreateWiredStream(); + + stream.MarkCompleted(); + // Second call should be a no-op — semaphore should not be double-released + stream.MarkCompleted(); + + // WaitForCompletionAsync should complete exactly once without hanging + var waitTask = stream.WaitForCompletionAsync(); + var completed = await Task.WhenAny(waitTask, Task.Delay(TimeSpan.FromSeconds(2))); + Assert.Same(waitTask, completed); + } + + [Fact] + public async Task ReportError_ThenMarkCompleted_MarkCompletedIsNoOp() + { + var stream = new ResponseStream(Array.Empty()); + stream.ReportError(new Exception("error")); + + // MarkCompleted after ReportError should not throw and not double-release + stream.MarkCompleted(); + + // WaitForCompletionAsync should complete (released by ReportError) + var waitTask = stream.WaitForCompletionAsync(); + var completed = await Task.WhenAny(waitTask, Task.Delay(TimeSpan.FromSeconds(2))); + Assert.Same(waitTask, completed); + } + + // ---- BytesWritten tracking ---- + + [Fact] + public async Task BytesWritten_TracksAcrossMultipleWrites() + { + var (stream, _) = await CreateWiredStream(); + + await stream.WriteAsync(new byte[10], 0, 10); + await stream.WriteAsync(new byte[5], 0, 5); + + Assert.Equal(15, stream.BytesWritten); + } + + [Fact] + public async Task BytesWritten_ReflectsOffsetAndCount() + { + var (stream, _) = await CreateWiredStream(); + + await stream.WriteAsync(new byte[10], 2, 6); // only 6 bytes + + Assert.Equal(6, stream.BytesWritten); + } + } +} diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/RuntimeApiClientTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/RuntimeApiClientTests.cs new file mode 100644 index 000000000..a6ce7d892 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/RuntimeApiClientTests.cs @@ -0,0 +1,209 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Amazon.Lambda.RuntimeSupport.Client.ResponseStreaming; +using Xunit; + +namespace Amazon.Lambda.RuntimeSupport.UnitTests +{ + /// + /// Tests for RuntimeApiClient streaming and buffered behavior. + /// Validates Properties 7, 8, 10, 13, 18. + /// + public class RuntimeApiClientTests + { + private const long MaxResponseSize = 20 * 1024 * 1024; + + /// + /// Mock HttpMessageHandler that captures the request for header inspection. + /// It completes the ResponseStream and returns immediately without reading + /// the content body, avoiding the SerializeToStreamAsync blocking issue. + /// + private class MockHttpMessageHandler : HttpMessageHandler + { + public HttpRequestMessage CapturedRequest { get; private set; } + private readonly ResponseStream _responseStream; + + public MockHttpMessageHandler(ResponseStream responseStream) + { + _responseStream = responseStream; + } + + protected override Task SendAsync( + HttpRequestMessage request, CancellationToken cancellationToken) + { + CapturedRequest = request; + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)); + } + } + + private static RuntimeApiClient CreateClientWithMockHandler( + ResponseStream stream, out MockHttpMessageHandler handler) + { + handler = new MockHttpMessageHandler(stream); + var httpClient = new HttpClient(handler); + var envVars = new TestEnvironmentVariables(); + envVars.SetEnvironmentVariable("AWS_LAMBDA_RUNTIME_API", "localhost:9001"); + return new RuntimeApiClient(envVars, httpClient); + } + + // --- Property 7: Streaming Response Mode Header --- + // Note: Properties 7, 8, 13 test the HttpClient-based streaming path which is only used on pre-NET8 targets. + // On NET8+, StartStreamingResponseAsync uses RawStreamingHttpClient (raw TCP) which doesn't go through HttpClient. + +#if !NET8_0_OR_GREATER + /// + /// Property 7: Streaming Response Mode Header + /// For any streaming response, the HTTP request should include + /// "Lambda-Runtime-Function-Response-Mode: streaming". + /// **Validates: Requirements 4.1** + /// + [Fact] + public async Task StartStreamingResponseAsync_IncludesStreamingResponseModeHeader() + { + var stream = new ResponseStream(Array.Empty()); + var client = CreateClientWithMockHandler(stream, out var handler); + + await client.StartStreamingResponseAsync("req-1", stream, CancellationToken.None); + + Assert.NotNull(handler.CapturedRequest); + Assert.True(handler.CapturedRequest.Headers.Contains(StreamingConstants.ResponseModeHeader)); + var values = handler.CapturedRequest.Headers.GetValues(StreamingConstants.ResponseModeHeader).ToList(); + Assert.Single(values); + Assert.Equal(StreamingConstants.StreamingResponseMode, values[0]); + } + + // --- Property 8: Chunked Transfer Encoding Header --- + + /// + /// Property 8: Chunked Transfer Encoding Header + /// For any streaming response, the HTTP request should include + /// "Transfer-Encoding: chunked". + /// **Validates: Requirements 4.2** + /// + [Fact] + public async Task StartStreamingResponseAsync_IncludesChunkedTransferEncodingHeader() + { + var stream = new ResponseStream(Array.Empty()); + var client = CreateClientWithMockHandler(stream, out var handler); + + await client.StartStreamingResponseAsync("req-2", stream, CancellationToken.None); + + Assert.NotNull(handler.CapturedRequest); + Assert.True(handler.CapturedRequest.Headers.TransferEncodingChunked); + } + + // --- Property 13: Trailer Declaration Header --- + + /// + /// Property 13: Trailer Declaration Header + /// For any streaming response, the HTTP request should include a "Trailer" header + /// declaring the error trailer headers upfront (since we cannot know at request + /// start whether an error will occur). + /// **Validates: Requirements 5.4** + /// + [Fact] + public async Task StartStreamingResponseAsync_DeclaresTrailerHeaderUpfront() + { + var stream = new ResponseStream(Array.Empty()); + var client = CreateClientWithMockHandler(stream, out var handler); + + await client.StartStreamingResponseAsync("req-3", stream, CancellationToken.None); + + Assert.NotNull(handler.CapturedRequest); + Assert.True(handler.CapturedRequest.Headers.Contains("Trailer")); + var trailerValue = string.Join(", ", handler.CapturedRequest.Headers.GetValues("Trailer")); + Assert.Contains(StreamingConstants.ErrorTypeTrailer, trailerValue); + Assert.Contains(StreamingConstants.ErrorBodyTrailer, trailerValue); + } +#endif + + // --- Property 10: Buffered Responses Exclude Streaming Headers --- + + /// + /// Mock HttpMessageHandler that captures the request for buffered response header inspection. + /// Returns an Accepted (202) response since that's what the InternalRuntimeApiClient expects. + /// + private class BufferedMockHttpMessageHandler : HttpMessageHandler + { + public HttpRequestMessage CapturedRequest { get; private set; } + + protected override Task SendAsync( + HttpRequestMessage request, CancellationToken cancellationToken) + { + CapturedRequest = request; + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.Accepted)); + } + } + + /// + /// Property 10: Buffered Responses Exclude Streaming Headers + /// For any buffered response (where CreateStream was not called), the HTTP request + /// should not include "Lambda-Runtime-Function-Response-Mode" or + /// "Transfer-Encoding: chunked" or "Trailer" headers. + /// **Validates: Requirements 4.6** + /// + [Fact] + public async Task SendResponseAsync_BufferedResponse_ExcludesStreamingHeaders() + { + var bufferedHandler = new BufferedMockHttpMessageHandler(); + var httpClient = new HttpClient(bufferedHandler); + var envVars = new TestEnvironmentVariables(); + envVars.SetEnvironmentVariable("AWS_LAMBDA_RUNTIME_API", "localhost:9001"); + var client = new RuntimeApiClient(envVars, httpClient); + + var outputStream = new MemoryStream(new byte[] { 1, 2, 3 }); + await client.SendResponseAsync("req-buffered", outputStream, CancellationToken.None); + + Assert.NotNull(bufferedHandler.CapturedRequest); + // Buffered responses must not include streaming-specific headers + Assert.False(bufferedHandler.CapturedRequest.Headers.Contains(StreamingConstants.ResponseModeHeader), + "Buffered response should not include Lambda-Runtime-Function-Response-Mode header"); + Assert.NotEqual(true, bufferedHandler.CapturedRequest.Headers.TransferEncodingChunked); + Assert.False(bufferedHandler.CapturedRequest.Headers.Contains("Trailer"), + "Buffered response should not include Trailer header"); + } + + // --- Argument validation --- + + [Fact] + public async Task StartStreamingResponseAsync_NullRequestId_ThrowsArgumentNullException() + { + var stream = new ResponseStream(Array.Empty()); + var client = CreateClientWithMockHandler(stream, out _); + + await Assert.ThrowsAsync( + () => client.StartStreamingResponseAsync(null, stream, CancellationToken.None)); + } + + [Fact] + public async Task StartStreamingResponseAsync_NullResponseStream_ThrowsArgumentNullException() + { + var stream = new ResponseStream(Array.Empty()); + var client = CreateClientWithMockHandler(stream, out _); + + await Assert.ThrowsAsync( + () => client.StartStreamingResponseAsync("req-5", null, CancellationToken.None)); + } + } +} diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/SnapstartTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/SnapstartTests.cs index aaedf943a..a10f8229c 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/SnapstartTests.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/SnapstartTests.cs @@ -7,10 +7,10 @@ namespace Amazon.Lambda.RuntimeSupport.UnitTests; public class SnapstartTests { - TestHandler _testFunction; - TestInitializer _testInitializer; - TestRuntimeApiClient _testRuntimeApiClient; - TestEnvironmentVariables _environmentVariables; + readonly TestHandler _testFunction; + readonly TestInitializer _testInitializer; + readonly TestRuntimeApiClient _testRuntimeApiClient; + readonly TestEnvironmentVariables _environmentVariables; public SnapstartTests() { @@ -30,7 +30,7 @@ public SnapstartTests() } [Fact] - public async void VerifyRestoreNextIsCalledWhenSnapstartIsEnabled() + public async Task VerifyRestoreNextIsCalledWhenSnapstartIsEnabled() { using var bootstrap = new LambdaBootstrap(_testFunction.BaseHandlerAsync, _testInitializer.InitializeTrueAsync, configuration: new LambdaBootstrapConfiguration(false, true)); @@ -40,7 +40,7 @@ public async void VerifyRestoreNextIsCalledWhenSnapstartIsEnabled() } [Fact] - public async void VerifyRestoreNextIsNotCalledWhenSnapstartIsDisabled() + public async Task VerifyRestoreNextIsNotCalledWhenSnapstartIsDisabled() { using var bootstrap = new LambdaBootstrap(_testFunction.BaseHandlerAsync, _testInitializer.InitializeTrueAsync, configuration: new LambdaBootstrapConfiguration(false, false)); @@ -52,7 +52,7 @@ public async void VerifyRestoreNextIsNotCalledWhenSnapstartIsDisabled() [Fact] - public async void VerifyInitializeErrorIsCalledWhenExceptionInBeforeSnapshotCallables() + public async Task VerifyInitializeErrorIsCalledWhenExceptionInBeforeSnapshotCallables() { using var bootstrap = new LambdaBootstrap(_testFunction.BaseHandlerAsync, _testInitializer.InitializeTrueAsync, configuration: new LambdaBootstrapConfiguration(false, true)); @@ -65,7 +65,7 @@ public async void VerifyInitializeErrorIsCalledWhenExceptionInBeforeSnapshotCall } [Fact] - public async void VerifyRestoreErrorIsCalledWhenExceptionInAfterRestoreCallables() + public async Task VerifyRestoreErrorIsCalledWhenExceptionInAfterRestoreCallables() { using (var bootstrap = new LambdaBootstrap(_testFunction.BaseHandlerAsync, _testInitializer.InitializeTrueAsync, new LambdaBootstrapConfiguration(false, true))) @@ -77,4 +77,4 @@ public async void VerifyRestoreErrorIsCalledWhenExceptionInAfterRestoreCallables Assert.True(_testRuntimeApiClient.ReportRestoreErrorAsyncCalled); } } -} \ No newline at end of file +} diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/StreamingE2EWithMoq.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/StreamingE2EWithMoq.cs new file mode 100644 index 000000000..6f6d6492c --- /dev/null +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/StreamingE2EWithMoq.cs @@ -0,0 +1,495 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Amazon.Lambda.RuntimeSupport.Client.ResponseStreaming; +using Amazon.Lambda.RuntimeSupport.UnitTests.TestHelpers; +using Xunit; + +namespace Amazon.Lambda.RuntimeSupport.UnitTests +{ + [CollectionDefinition("RuntimeSupportStateCheck")] + public class RuntimeSupportStateCheckCollection { } + + /// + /// End-to-end integration tests for the true-streaming architecture. + /// These tests exercise the full pipeline: LambdaBootstrap → ResponseStreamFactory → + /// ResponseStream → captured HTTP output stream. + /// + [Collection("RuntimeSupportStateCheck")] + public class StreamingE2EWithMoq : IDisposable + { + public void Dispose() + { + ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: false); + ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: true); + } + + private static Dictionary> MakeHeaders(string requestId = "test-request-id") + => new Dictionary> + { + { RuntimeApiHeaders.HeaderAwsRequestId, new List { requestId } }, + { RuntimeApiHeaders.HeaderInvokedFunctionArn, new List { "arn:aws:lambda:us-east-1:123456789012:function:test" } }, + { RuntimeApiHeaders.HeaderAwsTenantId, new List { "tenant-id" } }, + { RuntimeApiHeaders.HeaderTraceId, new List { "trace-id" } }, + { RuntimeApiHeaders.HeaderDeadlineMs, new List { "9999999999999" } }, + }; + + /// + /// A capturing RuntimeApiClient that records the raw bytes written to the HTTP output stream + /// by SerializeToStreamAsync. + /// + private class CapturingStreamingRuntimeApiClient : RuntimeApiClient, IRuntimeApiClient + { + private readonly IEnvironmentVariables _envVars; + private readonly Dictionary> _headers; + + public bool StartStreamingCalled { get; private set; } + public bool SendResponseCalled { get; private set; } + public bool ReportInvocationErrorCalled { get; private set; } + public byte[] CapturedHttpBytes { get; private set; } + public ResponseStream LastResponseStream { get; private set; } + public Stream LastBufferedOutputStream { get; private set; } + public Action OnStreamingReady { get; set; } + public MemoryStream CapturedOutputStream { get; private set; } + + public new Amazon.Lambda.RuntimeSupport.Helpers.IConsoleLoggerWriter ConsoleLogger { get; } = new Helpers.LogLevelLoggerWriter(new SystemEnvironmentVariables()); + + public CapturingStreamingRuntimeApiClient( + IEnvironmentVariables envVars, + Dictionary> headers) + : base(envVars, new NoOpInternalRuntimeApiClient()) + { + _envVars = envVars; + _headers = headers; + } + + public new async Task GetNextInvocationAsync(CancellationToken cancellationToken = default) + { + _headers[RuntimeApiHeaders.HeaderTraceId] = new List { Guid.NewGuid().ToString() }; + var inputStream = new MemoryStream(new byte[0]); + return new InvocationRequest + { + InputStream = inputStream, + LambdaContext = new LambdaContext( + new RuntimeApiHeaders(_headers), + new LambdaEnvironment(_envVars), + new TestDateTimeHelper(), + new Helpers.LogLevelLoggerWriter(_envVars)) + }; + } + + internal override async Task StartStreamingResponseAsync( + string awsRequestId, ResponseStream responseStream, CancellationToken cancellationToken = default) + { + StartStreamingCalled = true; + LastResponseStream = responseStream; + + // Use a real MemoryStream as the HTTP output stream so we capture actual bytes + var captureStream = new MemoryStream(); + CapturedOutputStream = captureStream; + await responseStream.SetHttpOutputStreamAsync(captureStream, cancellationToken); + + // Wait for the handler to finish writing (mirrors real RawStreamingHttpClient behavior) + OnStreamingReady?.Invoke(); + await responseStream.WaitForCompletionAsync(cancellationToken); + CapturedHttpBytes = captureStream.ToArray(); + return new NoOpDisposable(); + } + + public new async Task SendResponseAsync(string awsRequestId, Stream outputStream, CancellationToken cancellationToken = default) + { + SendResponseCalled = true; + if (outputStream != null) + { + var ms = new MemoryStream(); + await outputStream.CopyToAsync(ms); + ms.Position = 0; + LastBufferedOutputStream = ms; + } + } + + public new Task ReportInvocationErrorAsync(string awsRequestId, Exception exception, CancellationToken cancellationToken = default) + { + ReportInvocationErrorCalled = true; + return Task.CompletedTask; + } + + public new Task ReportInitializationErrorAsync(Exception exception, string errorType = null, CancellationToken cancellationToken = default) + => Task.CompletedTask; + + public new Task ReportInitializationErrorAsync(string errorType, CancellationToken cancellationToken = default) + => Task.CompletedTask; + + public new Task RestoreNextInvocationAsync(CancellationToken cancellationToken = default) => Task.CompletedTask; + public new Task ReportRestoreErrorAsync(Exception exception, string errorType = null, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + private static CapturingStreamingRuntimeApiClient CreateClient(string requestId = "test-request-id") + => new CapturingStreamingRuntimeApiClient(new TestEnvironmentVariables(), MakeHeaders(requestId)); + + /// + /// End-to-end: all data is transmitted correctly (content round-trip). + /// + [Fact] + public async Task Streaming_AllDataTransmitted_ContentRoundTrip() + { + var client = CreateClient(); + var payload = Encoding.UTF8.GetBytes("integration test payload"); + + LambdaBootstrapHandler handler = async (invocation) => + { + var stream = ResponseStreamFactory.CreateStream(Array.Empty()); + await stream.WriteAsync(payload); + return new InvocationResponse(Stream.Null, false); + }; + + using var bootstrap = new LambdaBootstrap(handler, null, null, new TestEnvironmentVariables()); + bootstrap.Client = client; + await bootstrap.InvokeOnceAsync(); + + var output = client.CapturedHttpBytes; + Assert.NotNull(output); + + var outputStr = Encoding.UTF8.GetString(output); + Assert.Contains("integration test payload", outputStr); + } + + /// + /// End-to-end: stream is finalized (final chunk written, BytesWritten matches). + /// + [Fact] + public async Task Streaming_StreamFinalized_BytesWrittenMatchesPayload() + { + var client = CreateClient(); + var data = Encoding.UTF8.GetBytes("finalization check"); + + LambdaBootstrapHandler handler = async (invocation) => + { + var stream = ResponseStreamFactory.CreateStream(Array.Empty()); + await stream.WriteAsync(data); + return new InvocationResponse(Stream.Null, false); + }; + + using var bootstrap = new LambdaBootstrap(handler, null, null, new TestEnvironmentVariables()); + bootstrap.Client = client; + await bootstrap.InvokeOnceAsync(); + + Assert.NotNull(client.LastResponseStream); + Assert.Equal(data.Length, client.LastResponseStream.BytesWritten); + } + + /// + /// End-to-end: handler does NOT call CreateStream — response goes via buffered path. + /// Verifies SendResponseAsync is called and streaming headers are absent. + /// + [Fact] + public async Task Buffered_HandlerDoesNotCallCreateStream_UsesSendResponsePath() + { + var client = CreateClient(); + var responseBody = Encoding.UTF8.GetBytes("buffered response body"); + + LambdaBootstrapHandler handler = async (invocation) => + { + await Task.Yield(); + return new InvocationResponse(new MemoryStream(responseBody)); + }; + + using var bootstrap = new LambdaBootstrap(handler, null, null, new TestEnvironmentVariables()); + bootstrap.Client = client; + await bootstrap.InvokeOnceAsync(); + + Assert.False(client.StartStreamingCalled, "StartStreamingResponseAsync should NOT be called for buffered mode"); + Assert.True(client.SendResponseCalled, "SendResponseAsync should be called for buffered mode"); + Assert.Null(client.CapturedHttpBytes); + } + + /// + /// End-to-end: buffered response body is transmitted correctly. + /// + [Fact] + public async Task Buffered_ResponseBodyTransmittedCorrectly() + { + var client = CreateClient(); + var responseBody = Encoding.UTF8.GetBytes("hello buffered world"); + + LambdaBootstrapHandler handler = async (invocation) => + { + await Task.Yield(); + return new InvocationResponse(new MemoryStream(responseBody)); + }; + + using var bootstrap = new LambdaBootstrap(handler, null, null, new TestEnvironmentVariables()); + bootstrap.Client = client; + await bootstrap.InvokeOnceAsync(); + + Assert.True(client.SendResponseCalled); + Assert.NotNull(client.LastBufferedOutputStream); + var received = new MemoryStream(); + await client.LastBufferedOutputStream.CopyToAsync(received); + Assert.Equal(responseBody, received.ToArray()); + } + + /// + /// Multi-concurrency: concurrent invocations use AsyncLocal for state isolation. + /// Each invocation independently uses streaming or buffered mode without interference. + /// + [Fact] + public async Task MultiConcurrency_ConcurrentInvocations_StateIsolated() + { + const int concurrency = 3; + var results = new ConcurrentDictionary(); + var barrier = new SemaphoreSlim(0, concurrency); + var allStarted = new SemaphoreSlim(0, concurrency); + + // Simulate concurrent invocations using AsyncLocal directly + var tasks = new List(); + for (int i = 0; i < concurrency; i++) + { + var requestId = $"req-{i}"; + var payload = $"payload-{i}"; + tasks.Add(Task.Run(async () => + { + var mockClient = new MockMultiConcurrencyStreamingClient(); + ResponseStreamFactory.InitializeInvocation( + requestId, + isMultiConcurrency: true, + mockClient, + CancellationToken.None); + + var stream = ResponseStreamFactory.CreateStream(Array.Empty()); + allStarted.Release(); + + // Wait until all tasks have started (to ensure true concurrency) + await barrier.WaitAsync(); + + await stream.WriteAsync(Encoding.UTF8.GetBytes(payload)); + stream.MarkCompleted(); + + // Verify this invocation's stream is still accessible + var retrieved = ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: true); + results[requestId] = retrieved != null ? payload : "MISSING"; + + ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: true); + })); + } + + // Wait for all tasks to start, then release the barrier + for (int i = 0; i < concurrency; i++) + await allStarted.WaitAsync(); + barrier.Release(concurrency); + + await Task.WhenAll(tasks); + + // Each invocation should have seen its own stream + Assert.Equal(concurrency, results.Count); + for (int i = 0; i < concurrency; i++) + Assert.Equal($"payload-{i}", results[$"req-{i}"]); + } + + /// + /// Multi-concurrency: streaming and buffered invocations can run concurrently without interference. + /// + [Fact] + public async Task MultiConcurrency_StreamingAndBufferedMixedConcurrently_NoInterference() + { + var streamingResults = new ConcurrentBag(); + var bufferedResults = new ConcurrentBag(); + var barrier = new SemaphoreSlim(0, 4); + var allStarted = new SemaphoreSlim(0, 4); + + var tasks = new List(); + + // 2 streaming invocations + for (int i = 0; i < 2; i++) + { + var requestId = $"stream-{i}"; + tasks.Add(Task.Run(async () => + { + var mockClient = new MockMultiConcurrencyStreamingClient(); + ResponseStreamFactory.InitializeInvocation( + requestId, + isMultiConcurrency: true, mockClient, CancellationToken.None); + + var stream = ResponseStreamFactory.CreateStream(Array.Empty()); + allStarted.Release(); + await barrier.WaitAsync(); + + await stream.WriteAsync(Encoding.UTF8.GetBytes("streaming data")); + stream.MarkCompleted(); + + var retrieved = ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: true); + streamingResults.Add(retrieved != null); + ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: true); + })); + } + + // 2 buffered invocations (no CreateStream) + for (int i = 0; i < 2; i++) + { + var requestId = $"buffered-{i}"; + tasks.Add(Task.Run(async () => + { + var mockClient = new MockMultiConcurrencyStreamingClient(); + ResponseStreamFactory.InitializeInvocation( + requestId, + isMultiConcurrency: true, mockClient, CancellationToken.None); + + allStarted.Release(); + await barrier.WaitAsync(); + + // No CreateStream — buffered mode + var retrieved = ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: true); + bufferedResults.Add(retrieved == null); // should be null (no stream created) + ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: true); + })); + } + + for (int i = 0; i < 4; i++) + await allStarted.WaitAsync(); + barrier.Release(4); + + await Task.WhenAll(tasks); + + Assert.Equal(2, streamingResults.Count); + Assert.All(streamingResults, r => Assert.True(r, "Streaming invocation should have a stream")); + + Assert.Equal(2, bufferedResults.Count); + Assert.All(bufferedResults, r => Assert.True(r, "Buffered invocation should have no stream")); + } + + /// + /// Minimal mock RuntimeApiClient for multi-concurrency tests. + /// Accepts StartStreamingResponseAsync calls without real HTTP. + /// + private class MockMultiConcurrencyStreamingClient : RuntimeApiClient + { + public MockMultiConcurrencyStreamingClient() + : base(new TestEnvironmentVariables(), new NoOpInternalRuntimeApiClient()) { } + + internal override async Task StartStreamingResponseAsync( + string awsRequestId, ResponseStream responseStream, CancellationToken cancellationToken = default) + { + // Provide the HTTP output stream so writes don't block + await responseStream.SetHttpOutputStreamAsync(new MemoryStream()); + await responseStream.WaitForCompletionAsync(); + return new NoOpDisposable(); + } + } + + /// + /// Backward compatibility: existing handler signatures (event + ILambdaContext) work without modification. + /// + [Fact] + public async Task BackwardCompat_ExistingHandlerSignature_WorksUnchanged() + { + var client = CreateClient(); + bool handlerCalled = false; + + // Simulate a classic handler that returns a buffered response + LambdaBootstrapHandler handler = async (invocation) => + { + handlerCalled = true; + await Task.Yield(); + return new InvocationResponse(new MemoryStream(Encoding.UTF8.GetBytes("classic response"))); + }; + + using var bootstrap = new LambdaBootstrap(handler, null, null, new TestEnvironmentVariables()); + bootstrap.Client = client; + await bootstrap.InvokeOnceAsync(); + + Assert.True(handlerCalled); + Assert.True(client.SendResponseCalled); + Assert.False(client.StartStreamingCalled); + } + + /// + /// Backward compatibility: no regression in buffered response behavior — response body is correct. + /// + [Fact] + public async Task BackwardCompat_BufferedResponse_NoRegression() + { + var client = CreateClient(); + var expected = Encoding.UTF8.GetBytes("no regression here"); + + LambdaBootstrapHandler handler = async (invocation) => + { + await Task.Yield(); + return new InvocationResponse(new MemoryStream(expected)); + }; + + using var bootstrap = new LambdaBootstrap(handler, null, null, new TestEnvironmentVariables()); + bootstrap.Client = client; + await bootstrap.InvokeOnceAsync(); + + Assert.True(client.SendResponseCalled); + Assert.NotNull(client.LastBufferedOutputStream); + var received = new MemoryStream(); + await client.LastBufferedOutputStream.CopyToAsync(received); + Assert.Equal(expected, received.ToArray()); + } + + /// + /// Backward compatibility: handler that returns null OutputStream still works. + /// + [Fact] + public async Task BackwardCompat_NullOutputStream_HandledGracefully() + { + var client = CreateClient(); + + LambdaBootstrapHandler handler = async (invocation) => + { + await Task.Yield(); + return new InvocationResponse(Stream.Null, false); + }; + + using var bootstrap = new LambdaBootstrap(handler, null, null, new TestEnvironmentVariables()); + bootstrap.Client = client; + + // Should not throw + await bootstrap.InvokeOnceAsync(); + + Assert.True(client.SendResponseCalled); + } + + /// + /// Backward compatibility: handler that throws before CreateStream uses standard error path. + /// + [Fact] + public async Task BackwardCompat_HandlerThrows_StandardErrorReportingUsed() + { + var client = CreateClient(); + + LambdaBootstrapHandler handler = async (invocation) => + { + await Task.Yield(); + throw new Exception("classic handler error"); + }; + + using var bootstrap = new LambdaBootstrap(handler, null, null, new TestEnvironmentVariables()); + bootstrap.Client = client; + await bootstrap.InvokeOnceAsync(); + + Assert.True(client.ReportInvocationErrorCalled); + Assert.False(client.StartStreamingCalled); + } + } +} diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/NoOpInternalRuntimeApiClient.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/NoOpInternalRuntimeApiClient.cs new file mode 100644 index 000000000..c73a0382c --- /dev/null +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/NoOpInternalRuntimeApiClient.cs @@ -0,0 +1,58 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Amazon.Lambda.RuntimeSupport.UnitTests.TestHelpers +{ + /// + /// A no-op implementation of IInternalRuntimeApiClient for unit tests + /// that need to construct a RuntimeApiClient without real HTTP calls. + /// + internal class NoOpInternalRuntimeApiClient : IInternalRuntimeApiClient + { + private static readonly SwaggerResponse EmptyStatusResponse = + new SwaggerResponse(200, new Dictionary>(), new StatusResponse()); + + public Task> ErrorAsync( + string lambda_Runtime_Function_Error_Type, string errorJson, CancellationToken cancellationToken) + => Task.FromResult(EmptyStatusResponse); + + public Task> NextAsync(CancellationToken cancellationToken) + => Task.FromResult(new SwaggerResponse(200, new Dictionary>(), Stream.Null)); + + public Task> ResponseAsync(string awsRequestId, Stream outputStream) + => Task.FromResult(EmptyStatusResponse); + + public Task> ResponseAsync( + string awsRequestId, Stream outputStream, CancellationToken cancellationToken) + => Task.FromResult(EmptyStatusResponse); + + public Task> ErrorWithXRayCauseAsync( + string awsRequestId, string lambda_Runtime_Function_Error_Type, + string errorJson, string xrayCause, CancellationToken cancellationToken) + => Task.FromResult(EmptyStatusResponse); + + public Task> RestoreNextAsync(CancellationToken cancellationToken) + => Task.FromResult(new SwaggerResponse(200, new Dictionary>(), Stream.Null)); + + public Task> RestoreErrorAsync( + string lambda_Runtime_Function_Error_Type, string errorJson, CancellationToken cancellationToken) + => Task.FromResult(EmptyStatusResponse); + } +} diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/TestMultiConcurrencyRuntimeApiClient.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/TestMultiConcurrencyRuntimeApiClient.cs index f7c85dd15..754bc1536 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/TestMultiConcurrencyRuntimeApiClient.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/TestMultiConcurrencyRuntimeApiClient.cs @@ -3,6 +3,7 @@ using Amazon.Lambda.RuntimeSupport.Helpers; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; @@ -18,7 +19,7 @@ internal class TestMultiConcurrencyRuntimeApiClient : IRuntimeApiClient private readonly IEnvironmentVariables _environmentVariables; public Queue InvocationEvents { get; } = new Queue(); - public Dictionary ProcessInvocationEvents { get; } = new Dictionary(); + public ConcurrentDictionary ProcessInvocationEvents { get; } = new ConcurrentDictionary(); public TestMultiConcurrencyRuntimeApiClient(IEnvironmentVariables environmentVariables, params InvocationEvent[] invocationEvents) { @@ -58,15 +59,30 @@ public string AwsRequestId public async Task GetNextInvocationAsync(CancellationToken cancellationToken = default) { - // If InvocationEvents is empty then all of the test events have been processed. - // At this point we just need to wait for the test verification to run and then the - // cancellationToken will be triggered to end delay. - if (InvocationEvents.Count == 0) + InvocationEvent data; + lock (InvocationEvents) + { + // If InvocationEvents is empty then all of the test events have been processed. + // At this point we just need to wait for the test verification to run and then the + // cancellationToken will be triggered to end delay. + if (InvocationEvents.Count == 0) + { + // Release the lock before awaiting + data = null; + } + else + { + data = InvocationEvents.Dequeue(); + } + } + + if (data == null) { await Task.Delay(TimeSpan.FromMinutes(10), cancellationToken); + // This line won't be reached in normal test flow since cancellation will throw + return null; } - var data = InvocationEvents.Dequeue(); ProcessInvocationEvents[data.AwsRequestId] = data; var inputStream = new MemoryStream(data.FunctionInput == null ? new byte[0] : data.FunctionInput); @@ -78,20 +94,22 @@ public async Task GetNextInvocationAsync(CancellationToken ca LambdaContext = new LambdaContext( new RuntimeApiHeaders(data.Headers), new LambdaEnvironment(_environmentVariables), - new TestDateTimeHelper(), new Helpers.SimpleLoggerWriter(_environmentVariables)) + new TestDateTimeHelper(), new Helpers.LogLevelLoggerWriter(_environmentVariables)) }; } public Task SendResponseAsync(string awsRequestId, Stream outputStream, CancellationToken cancellationToken = default) { - var data = ProcessInvocationEvents[awsRequestId]; - data.Complete = true; - if (outputStream != null) + if (ProcessInvocationEvents.TryGetValue(awsRequestId, out var data)) { - // copy the stream because it gets disposed by the bootstrap - data.OutputStream = new MemoryStream((int)outputStream.Length); - outputStream.CopyTo(data.OutputStream); - data.OutputStream.Position = 0; + data.Complete = true; + if (outputStream != null) + { + // copy the stream because it gets disposed by the bootstrap + data.OutputStream = new MemoryStream((int)outputStream.Length); + outputStream.CopyTo(data.OutputStream); + data.OutputStream.Position = 0; + } } return Task.Run(() => { }); diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/TestRuntimeApiClient.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/TestRuntimeApiClient.cs index 50bfa7254..b2ef549bc 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/TestRuntimeApiClient.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/TestRuntimeApiClient.cs @@ -50,7 +50,7 @@ public TestRuntimeApiClient(IEnvironmentVariables environmentVariables, Dictiona public string LastTraceId { get; private set; } public byte[] FunctionInput { get; set; } - public Stream LastOutputStream { get; private set; } + public Stream LastOutputStream { get; internal set; } public Exception LastRecordedException { get; private set; } public void VerifyOutput(string expectedOutput) @@ -99,7 +99,7 @@ public Task GetNextInvocationAsync(CancellationToken cancella LambdaContext = new LambdaContext( new RuntimeApiHeaders(_headers), new LambdaEnvironment(_environmentVariables), - new TestDateTimeHelper(), new Helpers.SimpleLoggerWriter(_environmentVariables)) + new TestDateTimeHelper(), ConsoleLogger) }); } diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/TestStreamingRuntimeApiClient.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/TestStreamingRuntimeApiClient.cs new file mode 100644 index 000000000..7b70c8071 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/TestStreamingRuntimeApiClient.cs @@ -0,0 +1,140 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using Amazon.Lambda.RuntimeSupport.Client.ResponseStreaming; +using Amazon.Lambda.RuntimeSupport.Helpers; +using Amazon.Lambda.RuntimeSupport.UnitTests.TestHelpers; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Amazon.Lambda.RuntimeSupport.UnitTests +{ + /// + /// A RuntimeApiClient subclass for testing LambdaBootstrap streaming integration. + /// Extends RuntimeApiClient so the (RuntimeApiClient)Client cast in LambdaBootstrap works. + /// Overrides StartStreamingResponseAsync to avoid real HTTP calls. + /// + internal class TestStreamingRuntimeApiClient : RuntimeApiClient, IRuntimeApiClient + { + private readonly IEnvironmentVariables _environmentVariables; + private readonly Dictionary> _headers; + + public new IConsoleLoggerWriter ConsoleLogger { get; } = new LogLevelLoggerWriter(new SystemEnvironmentVariables()); + + public TestStreamingRuntimeApiClient(IEnvironmentVariables environmentVariables, Dictionary> headers) + : base(environmentVariables, new NoOpInternalRuntimeApiClient()) + { + _environmentVariables = environmentVariables; + _headers = headers; + } + + // Tracking flags + public bool GetNextInvocationAsyncCalled { get; private set; } + public bool ReportInitializationErrorAsyncExceptionCalled { get; private set; } + public bool ReportInvocationErrorAsyncExceptionCalled { get; private set; } + public bool SendResponseAsyncCalled { get; private set; } + public bool StartStreamingResponseAsyncCalled { get; private set; } + + public string LastTraceId { get; private set; } + public byte[] FunctionInput { get; set; } + public Stream LastOutputStream { get; private set; } + public Exception LastRecordedException { get; private set; } + public ResponseStream LastStreamingResponseStream { get; private set; } + + public new async Task GetNextInvocationAsync(CancellationToken cancellationToken = default) + { + GetNextInvocationAsyncCalled = true; + + LastTraceId = Guid.NewGuid().ToString(); + _headers[RuntimeApiHeaders.HeaderTraceId] = new List() { LastTraceId }; + + var inputStream = new MemoryStream(FunctionInput == null ? new byte[0] : FunctionInput); + inputStream.Position = 0; + + return new InvocationRequest() + { + InputStream = inputStream, + LambdaContext = new LambdaContext( + new RuntimeApiHeaders(_headers), + new LambdaEnvironment(_environmentVariables), + new TestDateTimeHelper(), ConsoleLogger) + }; + } + + public new Task ReportInitializationErrorAsync(Exception exception, String errorType = null, CancellationToken cancellationToken = default) + { + LastRecordedException = exception; + ReportInitializationErrorAsyncExceptionCalled = true; + return Task.CompletedTask; + } + + public new Task ReportInitializationErrorAsync(string errorType, CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + + public new Task ReportInvocationErrorAsync(string awsRequestId, Exception exception, CancellationToken cancellationToken = default) + { + LastRecordedException = exception; + ReportInvocationErrorAsyncExceptionCalled = true; + return Task.CompletedTask; + } + + public new async Task SendResponseAsync(string awsRequestId, Stream outputStream, CancellationToken cancellationToken = default) + { + if (outputStream != null) + { + LastOutputStream = new MemoryStream((int)outputStream.Length); + outputStream.CopyTo(LastOutputStream); + LastOutputStream.Position = 0; + } + + SendResponseAsyncCalled = true; + } + + internal override async Task StartStreamingResponseAsync( + string awsRequestId, ResponseStream responseStream, CancellationToken cancellationToken = default) + { + StartStreamingResponseAsyncCalled = true; + LastStreamingResponseStream = responseStream; + + // Simulate the HTTP stream being available + await responseStream.SetHttpOutputStreamAsync(new MemoryStream(), cancellationToken); + + // Wait for the handler to finish writing (mirrors real SerializeToStreamAsync behavior) + await responseStream.WaitForCompletionAsync(); + + return new NoOpDisposable(); + } + + public new Task RestoreNextInvocationAsync(CancellationToken cancellationToken = default) + => Task.CompletedTask; + + public new Task ReportRestoreErrorAsync(Exception exception, String errorType = null, CancellationToken cancellationToken = default) + => Task.CompletedTask; + } + + /// + /// A no-op IDisposable for test overrides of StartStreamingResponseAsync. + /// + internal class NoOpDisposable : IDisposable + { + public void Dispose() { } + } +} diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/UtilsTest.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/UtilsTest.cs index 8068920be..a41e809ac 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/UtilsTest.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/UtilsTest.cs @@ -29,8 +29,11 @@ public void IsUsingMultiConcurrency(string concurrency, bool isMultiConcurrency) [Theory] [InlineData(null, 4, 1)] - [InlineData("5", 4, 4)] - [InlineData("5", 1, 2)] + [InlineData("5", 4, 5)] + [InlineData("5", 1, 5)] + [InlineData("10", 2, 10)] + [InlineData("enabled", 4, 4)] + [InlineData("enabled", 1, 2)] public void DetermineProcessingTaskCount(string concurrency, int processCount, int expected) { var envVars = new TestEnvironmentVariables(); diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/AspNetCoreStreamingApiGatewayTest/AspNetCoreStreamingApiGatewayTest.csproj b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/AspNetCoreStreamingApiGatewayTest/AspNetCoreStreamingApiGatewayTest.csproj new file mode 100644 index 000000000..6c72dc80c --- /dev/null +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/AspNetCoreStreamingApiGatewayTest/AspNetCoreStreamingApiGatewayTest.csproj @@ -0,0 +1,15 @@ + + + Exe + net10.0 + enable + enable + true + Lambda + true + true + + + + + diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/AspNetCoreStreamingApiGatewayTest/Program.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/AspNetCoreStreamingApiGatewayTest/Program.cs new file mode 100644 index 000000000..43f23a628 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/AspNetCoreStreamingApiGatewayTest/Program.cs @@ -0,0 +1,92 @@ +#pragma warning disable CA2252 + +using Amazon.Lambda.AspNetCoreServer.Hosting; + +var builder = WebApplication.CreateBuilder(args); + +// Determine the event source from environment variable. +// RestApi for API Gateway REST API, HttpApi for Lambda Function URL +// (Function URL uses the same payload format as HTTP API v2). +var eventSourceType = Environment.GetEnvironmentVariable("LAMBDA_EVENT_SOURCE") ?? "RestApi"; +var eventSource = eventSourceType.Equals("HttpApi", StringComparison.OrdinalIgnoreCase) + ? LambdaEventSource.HttpApi + : LambdaEventSource.RestApi; + +builder.Services.AddAWSLambdaHosting(eventSource, options => +{ + options.EnableResponseStreaming = true; +}); + +var app = builder.Build(); + +app.MapGet("/", () => "Welcome to ASP.NET Core streaming on Lambda"); + +app.MapGet("/streaming-test", async (HttpContext context) => +{ + context.Response.ContentType = "text/plain"; + context.Response.StatusCode = 200; + + var stream = context.Response.BodyWriter.AsStream(); + using var writer = new StreamWriter(stream, leaveOpen: true); + + for (var i = 1; i <= 100; i++) + { + await writer.WriteLineAsync($"Line {i}"); + if (i % 10 == 0) + { + await writer.FlushAsync(); + } + } +}); + +app.MapGet("/streaming-error", async (HttpContext context) => +{ + context.Response.ContentType = "text/plain"; + context.Response.StatusCode = 200; + + var stream = context.Response.BodyWriter.AsStream(); + using var writer = new StreamWriter(stream, leaveOpen: true); + + for (var i = 1; i <= 10; i++) + { + await writer.WriteLineAsync($"Line {i}"); + } + await writer.FlushAsync(); + + throw new InvalidOperationException("Midstream error for testing"); +}); + +app.MapGet("/json-response", (HttpContext context) => +{ + return Results.Json(new { message = "Hello from streaming Lambda", timestamp = DateTime.UtcNow.ToString("o") }); +}); + +app.MapGet("/custom-headers", (HttpContext context) => +{ + context.Response.StatusCode = 201; + context.Response.ContentType = "text/plain"; + context.Response.Headers["X-Custom-Header"] = "custom-value"; + context.Response.Headers["X-Another-Header"] = "another-value"; + return Results.Text("Custom headers response", "text/plain", statusCode: 201); +}); + +app.MapGet("/set-cookie", (HttpContext context) => +{ + context.Response.Cookies.Append("session", "abc123", new CookieOptions + { + Path = "/", + HttpOnly = true + }); + context.Response.Cookies.Append("theme", "dark"); + return Results.Text("Cookies set"); +}); + +app.MapPost("/echo-body", async (HttpContext context) => +{ + using var reader = new StreamReader(context.Request.Body); + var body = await reader.ReadToEndAsync(); + return Results.Text($"Echo: {body}"); +}); + +app.Run(); + diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/AspNetCoreStreamingApiGatewayTest/aws-lambda-tools-defaults.json b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/AspNetCoreStreamingApiGatewayTest/aws-lambda-tools-defaults.json new file mode 100644 index 000000000..99fead300 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/AspNetCoreStreamingApiGatewayTest/aws-lambda-tools-defaults.json @@ -0,0 +1,17 @@ +{ + "Information": [ + "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.", + "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.", + "dotnet lambda help" + ], + "profile": "", + "region": "", + "configuration": "Release", + "function-runtime": "dotnet10", + "function-memory-size": 512, + "function-timeout": 30, + "function-handler": "AspNetCoreStreamingApiGatewayTest", + "template": "serverless-restapi.template", + "template-parameters": "", + "s3-prefix": "AspNetCoreStreamingApiGatewayTest/" +} diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/AspNetCoreStreamingApiGatewayTest/serverless-functionurl.template b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/AspNetCoreStreamingApiGatewayTest/serverless-functionurl.template new file mode 100644 index 000000000..496398dd1 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/AspNetCoreStreamingApiGatewayTest/serverless-functionurl.template @@ -0,0 +1,38 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Transform": "AWS::Serverless-2016-10-31", + "Description": "Integration test for ASP.NET Core response streaming through Lambda Function URL.", + "Resources": { + "AspNetCoreFunction": { + "Type": "AWS::Serverless::Function", + "Properties": { + "Handler": "AspNetCoreStreamingApiGatewayTest", + "Runtime": "dotnet10", + "CodeUri": "", + "MemorySize": 512, + "Timeout": 30, + "Role": null, + "Policies": [ + "AWSLambda_FullAccess" + ], + "Environment": { + "Variables": { + "LAMBDA_EVENT_SOURCE": "HttpApi", + "LAMBDA_NET_SERIALIZER_DEBUG": "true", + "LAMBDA_RUNTIMESUPPORT_DEBUG": "true" + } + }, + "FunctionUrlConfig": { + "AuthType": "NONE", + "InvokeMode": "RESPONSE_STREAM" + } + } + } + }, + "Outputs": { + "ApiURL": { + "Description": "Lambda Function URL endpoint", + "Value": { "Fn::GetAtt": ["AspNetCoreFunctionUrl", "FunctionUrl"] } + } + } +} diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/AspNetCoreStreamingApiGatewayTest/serverless-restapi.template b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/AspNetCoreStreamingApiGatewayTest/serverless-restapi.template new file mode 100644 index 000000000..f0c338b79 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/AspNetCoreStreamingApiGatewayTest/serverless-restapi.template @@ -0,0 +1,87 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Transform": "AWS::Serverless-2016-10-31", + "Description": "Integration test for ASP.NET Core response streaming through API Gateway.", + "Resources": { + "StreamingApi": { + "Type": "AWS::Serverless::Api", + "Properties": { + "StageName": "prod", + "MethodSettings": [ + { + "ResourcePath": "/*", + "HttpMethod": "*" + } + ], + "DefinitionBody": { + "openapi": "3.0.1", + "info": { + "title": "ASP.NET Core Streaming Integration Test", + "version": "1.0" + }, + "paths": { + "/": { + "x-amazon-apigateway-any-method": { + "x-amazon-apigateway-integration": { + "type": "aws_proxy", + "httpMethod": "POST", + "payloadFormatVersion": "1.0", + "uri": { "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2021-11-15/functions/${AspNetCoreFunction.Arn}/response-streaming-invocations" }, + "responseTransferMode": "STREAM", + "timeoutInMillis": 29000 + } + } + }, + "/{proxy+}": { + "x-amazon-apigateway-any-method": { + "x-amazon-apigateway-integration": { + "type": "aws_proxy", + "httpMethod": "POST", + "payloadFormatVersion": "1.0", + "uri": { "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2021-11-15/functions/${AspNetCoreFunction.Arn}/response-streaming-invocations" }, + "responseTransferMode": "STREAM", + "timeoutInMillis": 29000 + } + } + } + } + } + } + }, + "AspNetCoreFunction": { + "Type": "AWS::Serverless::Function", + "Properties": { + "Handler": "AspNetCoreStreamingApiGatewayTest", + "Runtime": "dotnet10", + "CodeUri": "", + "MemorySize": 512, + "Timeout": 30, + "Role": null, + "Policies": [ + "AWSLambda_FullAccess" + ], + "Environment" : { + "Variables" : { + "LAMBDA_NET_SERIALIZER_DEBUG": "true", + "LAMBDA_RUNTIMESUPPORT_DEBUG": "true" + } + } + } + }, + "ApiPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { "Ref": "AspNetCoreFunction" }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { "Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${StreamingApi}/*/*/*" } + } + } + }, + "Outputs": { + "ApiURL": { + "Description": "API endpoint URL for Prod environment", + "Value": { "Fn::Sub": "https://${StreamingApi}.execute-api.${AWS::Region}.amazonaws.com/prod/" } + } + } +} diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest/Controllers/LoggerTestController.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest/Controllers/LoggerTestController.cs index 86b482fff..5e8e6f7b2 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest/Controllers/LoggerTestController.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest/Controllers/LoggerTestController.cs @@ -1,4 +1,4 @@ -using Amazon.Lambda.Core; +using Amazon.Lambda.Core; using Microsoft.AspNetCore.Mvc; namespace CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest.Controllers @@ -10,7 +10,7 @@ public class LoggerTestController : ControllerBase [HttpGet()] public long Get() { - var lambdaContext = this.HttpContext.Items["LambdaContext"] as ILambdaContext; + var lambdaContext = HttpContext.Items["LambdaContext"] as ILambdaContext; const int maxLogs = 10000; long actualLogsWritten = 0; diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest/CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest.csproj b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest/CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest.csproj index 849e21461..ab7cc9865 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest/CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest.csproj +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest/CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest.csproj @@ -1,10 +1,11 @@  - net8.0 + net10.0 enable enable - bootstrap + bootstrap + true @@ -16,7 +17,7 @@ - + diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest/aws-lambda-tools-defaults.json b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest/aws-lambda-tools-defaults.json index 1502f4b16..7aebdc2ad 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest/aws-lambda-tools-defaults.json +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeAspNetCoreMinimalApiCustomSerializerTest/aws-lambda-tools-defaults.json @@ -1,9 +1,9 @@ -{ +{ "profile": "default", "region": "us-west-2", "configuration": "Release", "msbuild-parameters": "--self-contained true", - "function-runtime": "provided.al2", + "function-runtime": "provided.al2023", "function-memory-size": 256, "function-timeout": 30, "function-handler": "not-used", diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeAspNetCoreMinimalApiTest/Controllers/LoggerTestController.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeAspNetCoreMinimalApiTest/Controllers/LoggerTestController.cs index 558a25e7f..57378eaa9 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeAspNetCoreMinimalApiTest/Controllers/LoggerTestController.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeAspNetCoreMinimalApiTest/Controllers/LoggerTestController.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Amazon.Lambda.Core; namespace CustomRuntimeAspNetCoreMinimalApiTest.Controllers @@ -10,7 +10,7 @@ public class LoggerTestController : ControllerBase [HttpGet()] public long Get() { - var lambdaContext = this.HttpContext.Items["LambdaContext"] as ILambdaContext; + var lambdaContext = HttpContext.Items["LambdaContext"] as ILambdaContext; const int maxLogs = 10000; long actualLogsWritten = 0; diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeAspNetCoreMinimalApiTest/CustomRuntimeAspNetCoreMinimalApiTest.csproj b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeAspNetCoreMinimalApiTest/CustomRuntimeAspNetCoreMinimalApiTest.csproj index 849e21461..67af2cee7 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeAspNetCoreMinimalApiTest/CustomRuntimeAspNetCoreMinimalApiTest.csproj +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeAspNetCoreMinimalApiTest/CustomRuntimeAspNetCoreMinimalApiTest.csproj @@ -1,10 +1,11 @@  - net8.0 + net10.0 enable enable - bootstrap + bootstrap + true @@ -16,7 +17,7 @@ - + diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeAspNetCoreMinimalApiTest/aws-lambda-tools-defaults.json b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeAspNetCoreMinimalApiTest/aws-lambda-tools-defaults.json index ba42343b7..b1b12f46f 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeAspNetCoreMinimalApiTest/aws-lambda-tools-defaults.json +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeAspNetCoreMinimalApiTest/aws-lambda-tools-defaults.json @@ -1,11 +1,11 @@ -{ - "profile": "default", - "region": "us-west-2", - "configuration": "Release", - "msbuild-parameters": "--self-contained true", - "function-runtime": "provided.al2", - "function-memory-size": 256, - "function-timeout": 30, - "function-handler": "not-used", - "function-name": "CustomRuntimeAspNetCoreMinimalApiTest" +{ + "profile": "default", + "region": "us-west-2", + "configuration": "Release", + "msbuild-parameters": "--self-contained true", + "function-runtime": "provided.al2023", + "function-memory-size": 256, + "function-timeout": 30, + "function-handler": "not-used", + "function-name": "CustomRuntimeAspNetCoreMinimalApiTest" } diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeFunctionTest/CustomRuntimeFunctionTest.csproj b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeFunctionTest/CustomRuntimeFunctionTest.csproj index e38db03d4..91deb26de 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeFunctionTest/CustomRuntimeFunctionTest.csproj +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeFunctionTest/CustomRuntimeFunctionTest.csproj @@ -2,9 +2,10 @@ Exe - net6.0;net8.0 + net10.0 IDE0060 + true @@ -20,8 +21,8 @@ - - - + + + diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeFunctionTest/aws-lambda-tools-defaults.json b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeFunctionTest/aws-lambda-tools-defaults.json index 1e44f44ba..e48ac315f 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeFunctionTest/aws-lambda-tools-defaults.json +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/CustomRuntimeFunctionTest/aws-lambda-tools-defaults.json @@ -1,11 +1,11 @@ -{ - "profile": "default", - "region": "us-west-2", - "configuration": "Release", - "msbuild-parameters": "--self-contained true", - "function-runtime": "provided", - "function-memory-size": 256, - "function-timeout": 30, - "function-handler": "PingAsync", - "function-name": "CustomRuntimeFunctionTest" +{ + "profile": "default", + "region": "us-west-2", + "configuration": "Release", + "msbuild-parameters": "--self-contained true", + "function-runtime": "provided.al2023", + "function-memory-size": 256, + "function-timeout": 30, + "function-handler": "PingAsync", + "function-name": "CustomRuntimeFunctionTest" } diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/ResponseStreamingFunctionHandlers/Function.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/ResponseStreamingFunctionHandlers/Function.cs new file mode 100644 index 000000000..8c645ff5b --- /dev/null +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/ResponseStreamingFunctionHandlers/Function.cs @@ -0,0 +1,56 @@ +#pragma warning disable CA2252 + +using Amazon.Lambda.Core; +using Amazon.Lambda.Core.ResponseStreaming; +using Amazon.Lambda.RuntimeSupport; +using Amazon.Lambda.Serialization.SystemTextJson; + +// The function handler that will be called for each Lambda event +var handler = async (string input, ILambdaContext context) => +{ + using var stream = LambdaResponseStreamFactory.CreateStream(); + + switch(input) + { + case $"{nameof(SimpleFunctionHandler)}": + await SimpleFunctionHandler(stream, context); + break; + case $"{nameof(StreamContentHandler)}": + await StreamContentHandler(stream, context); + break; + case $"{nameof(UnhandledExceptionHandler)}": + await UnhandledExceptionHandler(stream, context); + break; + default: + throw new ArgumentException($"Unknown handler scenario {input}"); + } +}; + +async Task SimpleFunctionHandler(Stream stream, ILambdaContext context) +{ + using var writer = new StreamWriter(stream); + await writer.WriteAsync("Hello, World!"); +} + +async Task StreamContentHandler(Stream stream, ILambdaContext context) +{ + using var writer = new StreamWriter(stream); + + await writer.WriteLineAsync("Starting stream content..."); + for(var i = 0; i < 10000; i++) + { + await writer.WriteLineAsync($"Line {i}"); + } + await writer.WriteLineAsync("Finish stream content"); +} + +async Task UnhandledExceptionHandler(Stream stream, ILambdaContext context) +{ + using var writer = new StreamWriter(stream); + await writer.WriteAsync("This method will fail"); + throw new InvalidOperationException("This is an unhandled exception"); +} + +await LambdaBootstrapBuilder.Create(handler, new DefaultLambdaJsonSerializer()) + .Build() + .RunAsync(); diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/ResponseStreamingFunctionHandlers/ResponseStreamingFunctionHandlers.csproj b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/ResponseStreamingFunctionHandlers/ResponseStreamingFunctionHandlers.csproj new file mode 100644 index 000000000..fa81eaa17 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/ResponseStreamingFunctionHandlers/ResponseStreamingFunctionHandlers.csproj @@ -0,0 +1,19 @@ + + + Exe + net10.0 + enable + enable + true + Lambda + + true + + true + + + + + + + \ No newline at end of file diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/ResponseStreamingFunctionHandlers/aws-lambda-tools-defaults.json b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/ResponseStreamingFunctionHandlers/aws-lambda-tools-defaults.json new file mode 100644 index 000000000..3042c3978 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/ResponseStreamingFunctionHandlers/aws-lambda-tools-defaults.json @@ -0,0 +1,15 @@ +{ + "Information": [ + "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.", + "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.", + "dotnet lambda help", + "All the command line options for the Lambda command can be specified in this file." + ], + "profile": "default", + "region": "us-west-2", + "configuration": "Release", + "function-runtime": "dotnet10", + "function-memory-size": 512, + "function-timeout": 30, + "function-handler": "ResponseStreamingFunctionHandlers" +} \ No newline at end of file diff --git a/Libraries/test/EventsTests.NET6/EventsTests.NET6.csproj b/Libraries/test/EventsTests.NET6/EventsTests.NET6.csproj deleted file mode 100644 index afae3cb55..000000000 --- a/Libraries/test/EventsTests.NET6/EventsTests.NET6.csproj +++ /dev/null @@ -1,74 +0,0 @@ - - - - net6.0 - EventsTests31 - true - EventsTests.NET6 - latest - - - - - - - PreserveNewest - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - diff --git a/Libraries/test/EventsTests.NET6/SourceGeneratorSerializerTests.cs b/Libraries/test/EventsTests.NET6/SourceGeneratorSerializerTests.cs deleted file mode 100644 index 548556933..000000000 --- a/Libraries/test/EventsTests.NET6/SourceGeneratorSerializerTests.cs +++ /dev/null @@ -1,342 +0,0 @@ -using System.IO; -using System.Text.Json.Serialization; - -using Xunit; - -using Amazon.Lambda.Serialization.SystemTextJson; -using Amazon.Lambda.APIGatewayEvents; -using System; -using Amazon.Lambda.Core; -using System.Text; -using System.Text.Json; -using Amazon.Lambda.S3Events; -using Amazon.Lambda.CloudWatchEvents.BatchEvents; - -namespace EventsTests.NET6 -{ - [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest))] - [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyResponse))] - internal partial class HttpApiJsonSerializationContext : JsonSerializerContext - { - - } - - [JsonSerializable(typeof(S3ObjectLambdaEvent))] - internal partial class S3ObjectLambdaSerializationContext : JsonSerializerContext - { - - } - - [JsonSerializable(typeof(BatchJobStateChangeEvent))] - internal partial class BatchJobStateChangeEventSerializationContext : JsonSerializerContext - { - - } - - public class SourceGeneratorSerializerTests - { - [Theory] - [InlineData(typeof(SourceGeneratorLambdaJsonSerializer))] - public void HttpApiV2Format(Type serializerType) - { - var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; - using (var fileStream = LoadJsonTestFile("http-api-v2-request.json")) - { - var request = serializer.Deserialize(fileStream); - - Assert.Equal("2.0", request.Version); - Assert.Equal("$default", request.RouteKey); - Assert.Equal("/my/path", request.RawPath); - Assert.Equal("parameter1=value1¶meter1=value2¶meter2=value", request.RawQueryString); - - Assert.Equal(2, request.Cookies.Length); - Assert.Equal("cookie1", request.Cookies[0]); - Assert.Equal("cookie2", request.Cookies[1]); - - Assert.Equal(2, request.QueryStringParameters.Count); - Assert.Equal("value1,value2", request.QueryStringParameters["parameter1"]); - Assert.Equal("value", request.QueryStringParameters["parameter2"]); - - Assert.Equal("Hello from Lambda", request.Body); - Assert.True(request.IsBase64Encoded); - - Assert.Equal(2, request.StageVariables.Count); - Assert.Equal("value1", request.StageVariables["stageVariable1"]); - Assert.Equal("value2", request.StageVariables["stageVariable2"]); - - Assert.Equal(1, request.PathParameters.Count); - Assert.Equal("value1", request.PathParameters["parameter1"]); - - var rc = request.RequestContext; - Assert.NotNull(rc); - Assert.Equal("123456789012", rc.AccountId); - Assert.Equal("api-id", rc.ApiId); - Assert.Equal("id.execute-api.us-east-1.amazonaws.com", rc.DomainName); - Assert.Equal("domain-id", rc.DomainPrefix); - Assert.Equal("request-id", rc.RequestId); - Assert.Equal("route-id", rc.RouteId); - Assert.Equal("$default-route", rc.RouteKey); - Assert.Equal("$default-stage", rc.Stage); - Assert.Equal("12/Mar/2020:19:03:58 +0000", rc.Time); - Assert.Equal(1583348638390, rc.TimeEpoch); - - var clientCert = request.RequestContext.Authentication.ClientCert; - Assert.Equal("CERT_CONTENT", clientCert.ClientCertPem); - Assert.Equal("www.example.com", clientCert.SubjectDN); - Assert.Equal("Example issuer", clientCert.IssuerDN); - Assert.Equal("a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1", clientCert.SerialNumber); - - Assert.Equal("May 28 12:30:02 2019 GMT", clientCert.Validity.NotBefore); - Assert.Equal("Aug 5 09:36:04 2021 GMT", clientCert.Validity.NotAfter); - - var auth = rc.Authorizer; - Assert.NotNull(auth); - Assert.Equal(2, auth.Jwt.Claims.Count); - Assert.Equal("value1", auth.Jwt.Claims["claim1"]); - Assert.Equal("value2", auth.Jwt.Claims["claim2"]); - Assert.Equal(2, auth.Jwt.Scopes.Length); - Assert.Equal("scope1", auth.Jwt.Scopes[0]); - Assert.Equal("scope2", auth.Jwt.Scopes[1]); - - var http = rc.Http; - Assert.NotNull(http); - Assert.Equal("POST", http.Method); - Assert.Equal("/my/path", http.Path); - Assert.Equal("HTTP/1.1", http.Protocol); - Assert.Equal("IP", http.SourceIp); - Assert.Equal("agent", http.UserAgent); - } - } - - [Theory] - [InlineData(typeof(SourceGeneratorLambdaJsonSerializer))] - public void S3ObjectLambdaEventTest(Type serializerType) - { - var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; - using (var fileStream = LoadJsonTestFile("s3-object-lambda-event.json")) - { - var s3Event = serializer.Deserialize(fileStream); - - Assert.Equal("requestId", s3Event.XAmzRequestId); - Assert.Equal("https://my-s3-ap-111122223333.s3-accesspoint.us-east-1.amazonaws.com/example?X-Amz-Security-Token=", s3Event.GetObjectContext.InputS3Url); - Assert.Equal("io-use1-001", s3Event.GetObjectContext.OutputRoute); - Assert.Equal("OutputToken", s3Event.GetObjectContext.OutputToken); - - Assert.Equal("arn:aws:s3-object-lambda:us-east-1:111122223333:accesspoint/example-object-lambda-ap", s3Event.Configuration.AccessPointArn); - Assert.Equal("arn:aws:s3:us-east-1:111122223333:accesspoint/example-ap", s3Event.Configuration.SupportingAccessPointArn); - Assert.Equal("{}", s3Event.Configuration.Payload); - - Assert.Equal("https://object-lambda-111122223333.s3-object-lambda.us-east-1.amazonaws.com/example", s3Event.UserRequest.Url); - Assert.Equal("object-lambda-111122223333.s3-object-lambda.us-east-1.amazonaws.com", s3Event.UserRequest.Headers["Host"]); - - Assert.Equal("AssumedRole", s3Event.UserIdentity.Type); - Assert.Equal("principalId", s3Event.UserIdentity.PrincipalId); - Assert.Equal("arn:aws:sts::111122223333:assumed-role/Admin/example", s3Event.UserIdentity.Arn); - Assert.Equal("111122223333", s3Event.UserIdentity.AccountId); - Assert.Equal("accessKeyId", s3Event.UserIdentity.AccessKeyId); - - Assert.Equal("false", s3Event.UserIdentity.SessionContext.Attributes.MfaAuthenticated); - Assert.Equal("Wed Mar 10 23:41:52 UTC 2021", s3Event.UserIdentity.SessionContext.Attributes.CreationDate); - - Assert.Equal("Role", s3Event.UserIdentity.SessionContext.SessionIssuer.Type); - Assert.Equal("principalId", s3Event.UserIdentity.SessionContext.SessionIssuer.PrincipalId); - Assert.Equal("arn:aws:iam::111122223333:role/Admin", s3Event.UserIdentity.SessionContext.SessionIssuer.Arn); - Assert.Equal("111122223333", s3Event.UserIdentity.SessionContext.SessionIssuer.AccountId); - Assert.Equal("Admin", s3Event.UserIdentity.SessionContext.SessionIssuer.UserName); - - Assert.Equal("1.00", s3Event.ProtocolVersion); - } - } - - [Theory] - [InlineData(typeof(SourceGeneratorLambdaJsonSerializer))] - - public void BatchJobStateChangeEventTest(Type serializerType) - { - var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; - using (var fileStream = LoadJsonTestFile("batch-job-state-change-event.json")) - { - var jobStateChangeEvent = serializer.Deserialize(fileStream); - - Assert.Equal(jobStateChangeEvent.Version, "0"); - Assert.Equal(jobStateChangeEvent.Id, "c8f9c4b5-76e5-d76a-f980-7011e206042b"); - Assert.Equal(jobStateChangeEvent.DetailType, "Batch Job State Change"); - Assert.Equal(jobStateChangeEvent.Source, "aws.batch"); - Assert.Equal(jobStateChangeEvent.Account, "aws_account_id"); - Assert.Equal(jobStateChangeEvent.Time.ToUniversalTime(), DateTime.Parse("2017-10-23T17:56:03Z").ToUniversalTime()); - Assert.Equal(jobStateChangeEvent.Region, "us-east-1"); - Assert.Equal(jobStateChangeEvent.Resources.Count, 1); - Assert.Equal(jobStateChangeEvent.Resources[0], "arn:aws:batch:us-east-1:aws_account_id:job/4c7599ae-0a82-49aa-ba5a-4727fcce14a8"); - Assert.IsType(typeof(Job), jobStateChangeEvent.Detail); - Assert.Equal(jobStateChangeEvent.Detail.JobName, "event-test"); - Assert.Equal(jobStateChangeEvent.Detail.JobId, "4c7599ae-0a82-49aa-ba5a-4727fcce14a8"); - Assert.Equal(jobStateChangeEvent.Detail.JobQueue, "arn:aws:batch:us-east-1:aws_account_id:job-queue/HighPriority"); - Assert.Equal(jobStateChangeEvent.Detail.Status, "RUNNABLE"); - Assert.Equal(jobStateChangeEvent.Detail.Attempts.Count, 0); - Assert.Equal(jobStateChangeEvent.Detail.CreatedAt, 1508781340401); - Assert.Equal(jobStateChangeEvent.Detail.RetryStrategy.Attempts, 1); - Assert.Equal(jobStateChangeEvent.Detail.RetryStrategy.EvaluateOnExit.Count, 1); - Assert.Equal(jobStateChangeEvent.Detail.RetryStrategy.EvaluateOnExit[0].Action, "EXIT"); - Assert.Equal(jobStateChangeEvent.Detail.RetryStrategy.EvaluateOnExit[0].OnExitCode, "*"); - Assert.Equal(jobStateChangeEvent.Detail.RetryStrategy.EvaluateOnExit[0].OnReason, "*"); - Assert.Equal(jobStateChangeEvent.Detail.RetryStrategy.EvaluateOnExit[0].OnStatusReason, "*"); - Assert.Equal(jobStateChangeEvent.Detail.DependsOn.Count, 0); - Assert.Equal(jobStateChangeEvent.Detail.JobDefinition, "arn:aws:batch:us-east-1:aws_account_id:job-definition/first-run-job-definition:1"); - Assert.Equal(jobStateChangeEvent.Detail.Parameters.Count, 1); - Assert.Equal(jobStateChangeEvent.Detail.Parameters["test"], "abc"); - Assert.Equal(jobStateChangeEvent.Detail.Container.Image, "busybox"); - Assert.NotNull(jobStateChangeEvent.Detail.Container.ResourceRequirements); - Assert.Equal(jobStateChangeEvent.Detail.Container.ResourceRequirements.Count, 2); - Assert.Equal(jobStateChangeEvent.Detail.Container.ResourceRequirements[0].Type, "MEMORY"); - Assert.Equal(jobStateChangeEvent.Detail.Container.ResourceRequirements[0].Value, "2000"); - Assert.Equal(jobStateChangeEvent.Detail.Container.ResourceRequirements[1].Type, "VCPU"); - Assert.Equal(jobStateChangeEvent.Detail.Container.ResourceRequirements[1].Value, "2"); - Assert.Equal(jobStateChangeEvent.Detail.Container.Vcpus, 2); - Assert.Equal(jobStateChangeEvent.Detail.Container.Memory, 2000); - Assert.Equal(jobStateChangeEvent.Detail.Container.Command.Count, 2); - Assert.Equal(jobStateChangeEvent.Detail.Container.Command[0], "echo"); - Assert.Equal(jobStateChangeEvent.Detail.Container.Command[1], "'hello world'"); - Assert.Equal(jobStateChangeEvent.Detail.Container.Volumes.Count, 2); - Assert.Equal(jobStateChangeEvent.Detail.Container.Volumes[0].Name, "myhostsource"); - Assert.Equal(jobStateChangeEvent.Detail.Container.Volumes[0].Host.SourcePath, "/data"); - Assert.Equal(jobStateChangeEvent.Detail.Container.Volumes[1].Name, "efs"); - Assert.Equal(jobStateChangeEvent.Detail.Container.Volumes[1].EfsVolumeConfiguration.AuthorizationConfig.AccessPointId, "fsap-XXXXXXXXXXXXXXXXX"); - Assert.Equal(jobStateChangeEvent.Detail.Container.Volumes[1].EfsVolumeConfiguration.AuthorizationConfig.Iam, "ENABLED"); - Assert.Equal(jobStateChangeEvent.Detail.Container.Volumes[1].EfsVolumeConfiguration.FileSystemId, "fs-XXXXXXXXX"); - Assert.Equal(jobStateChangeEvent.Detail.Container.Volumes[1].EfsVolumeConfiguration.RootDirectory, "/"); - Assert.Equal(jobStateChangeEvent.Detail.Container.Volumes[1].EfsVolumeConfiguration.TransitEncryption, "ENABLED"); - Assert.Equal(jobStateChangeEvent.Detail.Container.Volumes[1].EfsVolumeConfiguration.TransitEncryptionPort, 12345); - Assert.NotNull(jobStateChangeEvent.Detail.Container.Environment); - Assert.Equal(jobStateChangeEvent.Detail.Container.Environment.Count, 1); - Assert.Equal(jobStateChangeEvent.Detail.Container.Environment[0].Name, "MANAGED_BY_AWS"); - Assert.Equal(jobStateChangeEvent.Detail.Container.Environment[0].Value, "STARTED_BY_STEP_FUNCTIONS"); - Assert.Equal(jobStateChangeEvent.Detail.Container.MountPoints.Count, 2); - Assert.Equal(jobStateChangeEvent.Detail.Container.MountPoints[0].ContainerPath, "/data"); - Assert.Equal(jobStateChangeEvent.Detail.Container.MountPoints[0].ReadOnly, true); - Assert.Equal(jobStateChangeEvent.Detail.Container.MountPoints[0].SourceVolume, "myhostsource"); - Assert.Equal(jobStateChangeEvent.Detail.Container.MountPoints[1].ContainerPath, "/mount/efs"); - Assert.Equal(jobStateChangeEvent.Detail.Container.MountPoints[1].SourceVolume, "efs"); - Assert.Equal(jobStateChangeEvent.Detail.Container.Ulimits.Count, 1); - Assert.Equal(jobStateChangeEvent.Detail.Container.Ulimits[0].HardLimit, 2048); - Assert.Equal(jobStateChangeEvent.Detail.Container.Ulimits[0].Name, "nofile"); - Assert.Equal(jobStateChangeEvent.Detail.Container.Ulimits[0].SoftLimit, 2048); - Assert.NotNull(jobStateChangeEvent.Detail.Container.LinuxParameters); - Assert.Equal(jobStateChangeEvent.Detail.Container.LinuxParameters.Devices.Count, 1); - Assert.Equal(jobStateChangeEvent.Detail.Container.LinuxParameters.Devices[0].ContainerPath, "/dev/sda"); - Assert.Equal(jobStateChangeEvent.Detail.Container.LinuxParameters.Devices[0].HostPath, "/dev/xvdc"); - Assert.Equal(jobStateChangeEvent.Detail.Container.LinuxParameters.Devices[0].Permissions.Count, 1); - Assert.Equal(jobStateChangeEvent.Detail.Container.LinuxParameters.Devices[0].Permissions[0], "MKNOD"); - Assert.Equal(jobStateChangeEvent.Detail.Container.LinuxParameters.InitProcessEnabled, true); - Assert.Equal(jobStateChangeEvent.Detail.Container.LinuxParameters.SharedMemorySize, 64); - Assert.Equal(jobStateChangeEvent.Detail.Container.LinuxParameters.MaxSwap, 1024); - Assert.Equal(jobStateChangeEvent.Detail.Container.LinuxParameters.Swappiness, 55); - Assert.Equal(jobStateChangeEvent.Detail.Container.LinuxParameters.Tmpfs.Count, 1); - Assert.Equal(jobStateChangeEvent.Detail.Container.LinuxParameters.Tmpfs[0].ContainerPath, "/run"); - Assert.Equal(jobStateChangeEvent.Detail.Container.LinuxParameters.Tmpfs[0].Size, 65536); - Assert.Equal(jobStateChangeEvent.Detail.Container.LinuxParameters.Tmpfs[0].MountOptions.Count, 2); - Assert.Equal(jobStateChangeEvent.Detail.Container.LinuxParameters.Tmpfs[0].MountOptions[0], "noexec"); - Assert.Equal(jobStateChangeEvent.Detail.Container.LinuxParameters.Tmpfs[0].MountOptions[1], "nosuid"); - Assert.NotNull(jobStateChangeEvent.Detail.Container.LogConfiguration); - Assert.Equal(jobStateChangeEvent.Detail.Container.LogConfiguration.LogDriver, "json-file"); - Assert.Equal(jobStateChangeEvent.Detail.Container.LogConfiguration.Options.Count, 2); - Assert.Equal(jobStateChangeEvent.Detail.Container.LogConfiguration.Options["max-size"], "10m"); - Assert.Equal(jobStateChangeEvent.Detail.Container.LogConfiguration.Options["max-file"], "3"); - Assert.Equal(jobStateChangeEvent.Detail.Container.LogConfiguration.SecretOptions.Count, 1); - Assert.Equal(jobStateChangeEvent.Detail.Container.LogConfiguration.SecretOptions[0].Name, "apikey"); - Assert.Equal(jobStateChangeEvent.Detail.Container.LogConfiguration.SecretOptions[0].ValueFrom, "ddApiKey"); - Assert.Equal(jobStateChangeEvent.Detail.Container.Secrets.Count, 1); - Assert.Equal(jobStateChangeEvent.Detail.Container.Secrets[0].Name, "DATABASE_PASSWORD"); - Assert.Equal(jobStateChangeEvent.Detail.Container.Secrets[0].ValueFrom, "arn:aws:ssm:us-east-1:awsExampleAccountID:parameter/awsExampleParameter"); - Assert.NotNull(jobStateChangeEvent.Detail.Container.NetworkConfiguration); - Assert.Equal(jobStateChangeEvent.Detail.Container.NetworkConfiguration.AssignPublicIp, "ENABLED"); - Assert.NotNull(jobStateChangeEvent.Detail.Container.FargatePlatformConfiguration); - Assert.Equal(jobStateChangeEvent.Detail.Container.FargatePlatformConfiguration.PlatformVersion, "LATEST"); - Assert.NotNull(jobStateChangeEvent.Detail.NodeProperties); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.MainNode, 0); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NumNodes, 0); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties.Count, 1); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].TargetNodes, "0:1"); - Assert.NotNull(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Image, "busybox"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.ResourceRequirements.Count, 2); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.ResourceRequirements[0].Type, "MEMORY"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.ResourceRequirements[0].Value, "2000"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.ResourceRequirements[1].Type, "VCPU"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.ResourceRequirements[1].Value, "2"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Vcpus, 2); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Memory, 2000); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Command.Count, 2); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Command[0], "echo"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Command[1], "'hello world'"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes.Count, 2); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes[0].Name, "myhostsource"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes[0].Host.SourcePath, "/data"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes[1].Name, "efs"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes[1].EfsVolumeConfiguration.AuthorizationConfig.AccessPointId, "fsap-XXXXXXXXXXXXXXXXX"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes[1].EfsVolumeConfiguration.AuthorizationConfig.Iam, "ENABLED"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes[1].EfsVolumeConfiguration.FileSystemId, "fs-XXXXXXXXX"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes[1].EfsVolumeConfiguration.RootDirectory, "/"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes[1].EfsVolumeConfiguration.TransitEncryption, "ENABLED"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes[1].EfsVolumeConfiguration.TransitEncryptionPort, 12345); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Environment.Count, 1); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Environment[0].Name, "MANAGED_BY_AWS"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Environment[0].Value, "STARTED_BY_STEP_FUNCTIONS"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.MountPoints.Count, 2); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.MountPoints[0].ContainerPath, "/data"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.MountPoints[0].ReadOnly, true); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.MountPoints[0].SourceVolume, "myhostsource"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.MountPoints[1].ContainerPath, "/mount/efs"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.MountPoints[1].SourceVolume, "efs"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Ulimits.Count, 1); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Ulimits[0].HardLimit, 2048); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Ulimits[0].Name, "nofile"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Ulimits[0].SoftLimit, 2048); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.ExecutionRoleArn, "arn:aws:iam::awsExampleAccountID:role/awsExampleRoleName"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.InstanceType, "p3.2xlarge"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.User, "testuser"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.JobRoleArn, "arn:aws:iam::awsExampleAccountID:role/awsExampleRoleName"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Devices.Count, 1); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Devices[0].HostPath, "/dev/xvdc"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Devices[0].ContainerPath, "/dev/sda"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Devices[0].Permissions.Count, 1); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Devices[0].Permissions[0], "MKNOD"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.InitProcessEnabled, true); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.SharedMemorySize, 64); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.MaxSwap, 1024); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Swappiness, 55); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Tmpfs.Count, 1); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Tmpfs[0].ContainerPath, "/run"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Tmpfs[0].Size, 65536); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Tmpfs[0].MountOptions.Count, 2); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Tmpfs[0].MountOptions[0], "noexec"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Tmpfs[0].MountOptions[1], "nosuid"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LogConfiguration.LogDriver, "awslogs"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LogConfiguration.Options["awslogs-group"], "awslogs-wordpress"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LogConfiguration.Options["awslogs-stream-prefix"], "awslogs-example"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LogConfiguration.SecretOptions.Count, 1); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LogConfiguration.SecretOptions[0].Name, "apikey"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LogConfiguration.SecretOptions[0].ValueFrom, "ddApiKey"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Secrets.Count, 1); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Secrets[0].Name, "DATABASE_PASSWORD"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Secrets[0].ValueFrom, "arn:aws:ssm:us-east-1:awsExampleAccountID:parameter/awsExampleParameter"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.NetworkConfiguration.AssignPublicIp, "DISABLED"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.FargatePlatformConfiguration.PlatformVersion, "LATEST"); - Assert.Equal(jobStateChangeEvent.Detail.PropagateTags, true); - Assert.Equal(jobStateChangeEvent.Detail.Timeout.AttemptDurationSeconds, 90); - Assert.Equal(jobStateChangeEvent.Detail.Tags.Count, 3); - Assert.Equal(jobStateChangeEvent.Detail.Tags["Service"], "Batch"); - Assert.Equal(jobStateChangeEvent.Detail.Tags["Name"], "JobDefinitionTag"); - Assert.Equal(jobStateChangeEvent.Detail.Tags["Expected"], "MergeTag"); - Assert.Equal(jobStateChangeEvent.Detail.PlatformCapabilities.Count, 1); - Assert.Equal(jobStateChangeEvent.Detail.PlatformCapabilities[0], "FARGATE"); - } - } - - - public MemoryStream LoadJsonTestFile(string filename) - { - var json = File.ReadAllText(filename); - return new MemoryStream(UTF8Encoding.UTF8.GetBytes(json)); - } - } -} diff --git a/Libraries/test/EventsTests.NET8/EventsTests.NET8.csproj b/Libraries/test/EventsTests.NET8/EventsTests.NET8.csproj index a004ee0e9..b6f09235b 100644 --- a/Libraries/test/EventsTests.NET8/EventsTests.NET8.csproj +++ b/Libraries/test/EventsTests.NET8/EventsTests.NET8.csproj @@ -14,7 +14,6 @@ PreserveNewest - diff --git a/Libraries/test/EventsTests.NET8/SourceGeneratorSerializerTests.cs b/Libraries/test/EventsTests.NET8/SourceGeneratorSerializerTests.cs new file mode 100644 index 000000000..21f268eb8 --- /dev/null +++ b/Libraries/test/EventsTests.NET8/SourceGeneratorSerializerTests.cs @@ -0,0 +1,342 @@ +using System.IO; +using System.Text.Json.Serialization; + +using Xunit; + +using Amazon.Lambda.Serialization.SystemTextJson; +using Amazon.Lambda.APIGatewayEvents; +using System; +using Amazon.Lambda.Core; +using System.Text; +using System.Text.Json; +using Amazon.Lambda.S3Events; +using Amazon.Lambda.CloudWatchEvents.BatchEvents; + +namespace EventsTests.NET8 +{ + [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest))] + [JsonSerializable(typeof(APIGatewayHttpApiV2ProxyResponse))] + internal partial class HttpApiJsonSerializationContext : JsonSerializerContext + { + + } + + [JsonSerializable(typeof(S3ObjectLambdaEvent))] + internal partial class S3ObjectLambdaSerializationContext : JsonSerializerContext + { + + } + + [JsonSerializable(typeof(BatchJobStateChangeEvent))] + internal partial class BatchJobStateChangeEventSerializationContext : JsonSerializerContext + { + + } + + public class SourceGeneratorSerializerTests + { + [Theory] + [InlineData(typeof(SourceGeneratorLambdaJsonSerializer))] + public void HttpApiV2Format(Type serializerType) + { + var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; + using (var fileStream = LoadJsonTestFile("http-api-v2-request.json")) + { + var request = serializer.Deserialize(fileStream); + + Assert.Equal("2.0", request.Version); + Assert.Equal("$default", request.RouteKey); + Assert.Equal("/my/path", request.RawPath); + Assert.Equal("parameter1=value1¶meter1=value2¶meter2=value", request.RawQueryString); + + Assert.Equal(2, request.Cookies.Length); + Assert.Equal("cookie1", request.Cookies[0]); + Assert.Equal("cookie2", request.Cookies[1]); + + Assert.Equal(2, request.QueryStringParameters.Count); + Assert.Equal("value1,value2", request.QueryStringParameters["parameter1"]); + Assert.Equal("value", request.QueryStringParameters["parameter2"]); + + Assert.Equal("Hello from Lambda", request.Body); + Assert.True(request.IsBase64Encoded); + + Assert.Equal(2, request.StageVariables.Count); + Assert.Equal("value1", request.StageVariables["stageVariable1"]); + Assert.Equal("value2", request.StageVariables["stageVariable2"]); + + Assert.Single(request.PathParameters); + Assert.Equal("value1", request.PathParameters["parameter1"]); + + var rc = request.RequestContext; + Assert.NotNull(rc); + Assert.Equal("123456789012", rc.AccountId); + Assert.Equal("api-id", rc.ApiId); + Assert.Equal("id.execute-api.us-east-1.amazonaws.com", rc.DomainName); + Assert.Equal("domain-id", rc.DomainPrefix); + Assert.Equal("request-id", rc.RequestId); + Assert.Equal("route-id", rc.RouteId); + Assert.Equal("$default-route", rc.RouteKey); + Assert.Equal("$default-stage", rc.Stage); + Assert.Equal("12/Mar/2020:19:03:58 +0000", rc.Time); + Assert.Equal(1583348638390, rc.TimeEpoch); + + var clientCert = request.RequestContext.Authentication.ClientCert; + Assert.Equal("CERT_CONTENT", clientCert.ClientCertPem); + Assert.Equal("www.example.com", clientCert.SubjectDN); + Assert.Equal("Example issuer", clientCert.IssuerDN); + Assert.Equal("a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1", clientCert.SerialNumber); + + Assert.Equal("May 28 12:30:02 2019 GMT", clientCert.Validity.NotBefore); + Assert.Equal("Aug 5 09:36:04 2021 GMT", clientCert.Validity.NotAfter); + + var auth = rc.Authorizer; + Assert.NotNull(auth); + Assert.Equal(2, auth.Jwt.Claims.Count); + Assert.Equal("value1", auth.Jwt.Claims["claim1"]); + Assert.Equal("value2", auth.Jwt.Claims["claim2"]); + Assert.Equal(2, auth.Jwt.Scopes.Length); + Assert.Equal("scope1", auth.Jwt.Scopes[0]); + Assert.Equal("scope2", auth.Jwt.Scopes[1]); + + var http = rc.Http; + Assert.NotNull(http); + Assert.Equal("POST", http.Method); + Assert.Equal("/my/path", http.Path); + Assert.Equal("HTTP/1.1", http.Protocol); + Assert.Equal("IP", http.SourceIp); + Assert.Equal("agent", http.UserAgent); + } + } + + [Theory] + [InlineData(typeof(SourceGeneratorLambdaJsonSerializer))] + public void S3ObjectLambdaEventTest(Type serializerType) + { + var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; + using (var fileStream = LoadJsonTestFile("s3-object-lambda-event.json")) + { + var s3Event = serializer.Deserialize(fileStream); + + Assert.Equal("requestId", s3Event.XAmzRequestId); + Assert.Equal("https://my-s3-ap-111122223333.s3-accesspoint.us-east-1.amazonaws.com/example?X-Amz-Security-Token=", s3Event.GetObjectContext.InputS3Url); + Assert.Equal("io-use1-001", s3Event.GetObjectContext.OutputRoute); + Assert.Equal("OutputToken", s3Event.GetObjectContext.OutputToken); + + Assert.Equal("arn:aws:s3-object-lambda:us-east-1:111122223333:accesspoint/example-object-lambda-ap", s3Event.Configuration.AccessPointArn); + Assert.Equal("arn:aws:s3:us-east-1:111122223333:accesspoint/example-ap", s3Event.Configuration.SupportingAccessPointArn); + Assert.Equal("{}", s3Event.Configuration.Payload); + + Assert.Equal("https://object-lambda-111122223333.s3-object-lambda.us-east-1.amazonaws.com/example", s3Event.UserRequest.Url); + Assert.Equal("object-lambda-111122223333.s3-object-lambda.us-east-1.amazonaws.com", s3Event.UserRequest.Headers["Host"]); + + Assert.Equal("AssumedRole", s3Event.UserIdentity.Type); + Assert.Equal("principalId", s3Event.UserIdentity.PrincipalId); + Assert.Equal("arn:aws:sts::111122223333:assumed-role/Admin/example", s3Event.UserIdentity.Arn); + Assert.Equal("111122223333", s3Event.UserIdentity.AccountId); + Assert.Equal("accessKeyId", s3Event.UserIdentity.AccessKeyId); + + Assert.Equal("false", s3Event.UserIdentity.SessionContext.Attributes.MfaAuthenticated); + Assert.Equal("Wed Mar 10 23:41:52 UTC 2021", s3Event.UserIdentity.SessionContext.Attributes.CreationDate); + + Assert.Equal("Role", s3Event.UserIdentity.SessionContext.SessionIssuer.Type); + Assert.Equal("principalId", s3Event.UserIdentity.SessionContext.SessionIssuer.PrincipalId); + Assert.Equal("arn:aws:iam::111122223333:role/Admin", s3Event.UserIdentity.SessionContext.SessionIssuer.Arn); + Assert.Equal("111122223333", s3Event.UserIdentity.SessionContext.SessionIssuer.AccountId); + Assert.Equal("Admin", s3Event.UserIdentity.SessionContext.SessionIssuer.UserName); + + Assert.Equal("1.00", s3Event.ProtocolVersion); + } + } + + [Theory] + [InlineData(typeof(SourceGeneratorLambdaJsonSerializer))] + + public void BatchJobStateChangeEventTest(Type serializerType) + { + var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; + using (var fileStream = LoadJsonTestFile("batch-job-state-change-event.json")) + { + var jobStateChangeEvent = serializer.Deserialize(fileStream); + + Assert.Equal("0", jobStateChangeEvent.Version); + Assert.Equal("c8f9c4b5-76e5-d76a-f980-7011e206042b", jobStateChangeEvent.Id); + Assert.Equal("Batch Job State Change", jobStateChangeEvent.DetailType); + Assert.Equal("aws.batch", jobStateChangeEvent.Source); + Assert.Equal("aws_account_id", jobStateChangeEvent.Account); + Assert.Equal(DateTime.Parse("2017-10-23T17:56:03Z").ToUniversalTime(), jobStateChangeEvent.Time.ToUniversalTime()); + Assert.Equal("us-east-1", jobStateChangeEvent.Region); + Assert.Single(jobStateChangeEvent.Resources); + Assert.Equal("arn:aws:batch:us-east-1:aws_account_id:job/4c7599ae-0a82-49aa-ba5a-4727fcce14a8", jobStateChangeEvent.Resources[0]); + Assert.IsType(jobStateChangeEvent.Detail); + Assert.Equal("event-test", jobStateChangeEvent.Detail.JobName); + Assert.Equal("4c7599ae-0a82-49aa-ba5a-4727fcce14a8", jobStateChangeEvent.Detail.JobId); + Assert.Equal("arn:aws:batch:us-east-1:aws_account_id:job-queue/HighPriority", jobStateChangeEvent.Detail.JobQueue); + Assert.Equal("RUNNABLE", jobStateChangeEvent.Detail.Status); + Assert.Empty(jobStateChangeEvent.Detail.Attempts); + Assert.Equal(1508781340401, jobStateChangeEvent.Detail.CreatedAt); + Assert.Equal(1, jobStateChangeEvent.Detail.RetryStrategy.Attempts); + Assert.Single(jobStateChangeEvent.Detail.RetryStrategy.EvaluateOnExit); + Assert.Equal("EXIT", jobStateChangeEvent.Detail.RetryStrategy.EvaluateOnExit[0].Action); + Assert.Equal("*", jobStateChangeEvent.Detail.RetryStrategy.EvaluateOnExit[0].OnExitCode); + Assert.Equal("*", jobStateChangeEvent.Detail.RetryStrategy.EvaluateOnExit[0].OnReason); + Assert.Equal("*", jobStateChangeEvent.Detail.RetryStrategy.EvaluateOnExit[0].OnStatusReason); + Assert.Empty(jobStateChangeEvent.Detail.DependsOn); + Assert.Equal("arn:aws:batch:us-east-1:aws_account_id:job-definition/first-run-job-definition:1", jobStateChangeEvent.Detail.JobDefinition); + Assert.Single(jobStateChangeEvent.Detail.Parameters); + Assert.Equal("abc", jobStateChangeEvent.Detail.Parameters["test"]); + Assert.Equal("busybox", jobStateChangeEvent.Detail.Container.Image); + Assert.NotNull(jobStateChangeEvent.Detail.Container.ResourceRequirements); + Assert.Equal(2, jobStateChangeEvent.Detail.Container.ResourceRequirements.Count); + Assert.Equal("MEMORY", jobStateChangeEvent.Detail.Container.ResourceRequirements[0].Type); + Assert.Equal("2000", jobStateChangeEvent.Detail.Container.ResourceRequirements[0].Value); + Assert.Equal("VCPU", jobStateChangeEvent.Detail.Container.ResourceRequirements[1].Type); + Assert.Equal("2", jobStateChangeEvent.Detail.Container.ResourceRequirements[1].Value); + Assert.Equal(2, jobStateChangeEvent.Detail.Container.Vcpus); + Assert.Equal(2000, jobStateChangeEvent.Detail.Container.Memory); + Assert.Equal(2, jobStateChangeEvent.Detail.Container.Command.Count); + Assert.Equal("echo", jobStateChangeEvent.Detail.Container.Command[0]); + Assert.Equal("'hello world'", jobStateChangeEvent.Detail.Container.Command[1]); + Assert.Equal(2, jobStateChangeEvent.Detail.Container.Volumes.Count); + Assert.Equal("myhostsource", jobStateChangeEvent.Detail.Container.Volumes[0].Name); + Assert.Equal("/data", jobStateChangeEvent.Detail.Container.Volumes[0].Host.SourcePath); + Assert.Equal("efs", jobStateChangeEvent.Detail.Container.Volumes[1].Name); + Assert.Equal("fsap-XXXXXXXXXXXXXXXXX", jobStateChangeEvent.Detail.Container.Volumes[1].EfsVolumeConfiguration.AuthorizationConfig.AccessPointId); + Assert.Equal("ENABLED", jobStateChangeEvent.Detail.Container.Volumes[1].EfsVolumeConfiguration.AuthorizationConfig.Iam); + Assert.Equal("fs-XXXXXXXXX", jobStateChangeEvent.Detail.Container.Volumes[1].EfsVolumeConfiguration.FileSystemId); + Assert.Equal("/", jobStateChangeEvent.Detail.Container.Volumes[1].EfsVolumeConfiguration.RootDirectory); + Assert.Equal("ENABLED", jobStateChangeEvent.Detail.Container.Volumes[1].EfsVolumeConfiguration.TransitEncryption); + Assert.Equal(12345, jobStateChangeEvent.Detail.Container.Volumes[1].EfsVolumeConfiguration.TransitEncryptionPort); + Assert.NotNull(jobStateChangeEvent.Detail.Container.Environment); + Assert.Single(jobStateChangeEvent.Detail.Container.Environment); + Assert.Equal("MANAGED_BY_AWS", jobStateChangeEvent.Detail.Container.Environment[0].Name); + Assert.Equal("STARTED_BY_STEP_FUNCTIONS", jobStateChangeEvent.Detail.Container.Environment[0].Value); + Assert.Equal(2, jobStateChangeEvent.Detail.Container.MountPoints.Count); + Assert.Equal("/data", jobStateChangeEvent.Detail.Container.MountPoints[0].ContainerPath); + Assert.True(jobStateChangeEvent.Detail.Container.MountPoints[0].ReadOnly); + Assert.Equal("myhostsource", jobStateChangeEvent.Detail.Container.MountPoints[0].SourceVolume); + Assert.Equal("/mount/efs", jobStateChangeEvent.Detail.Container.MountPoints[1].ContainerPath); + Assert.Equal("efs", jobStateChangeEvent.Detail.Container.MountPoints[1].SourceVolume); + Assert.Single(jobStateChangeEvent.Detail.Container.Ulimits); + Assert.Equal(2048, jobStateChangeEvent.Detail.Container.Ulimits[0].HardLimit); + Assert.Equal("nofile", jobStateChangeEvent.Detail.Container.Ulimits[0].Name); + Assert.Equal(2048, jobStateChangeEvent.Detail.Container.Ulimits[0].SoftLimit); + Assert.NotNull(jobStateChangeEvent.Detail.Container.LinuxParameters); + Assert.Single(jobStateChangeEvent.Detail.Container.LinuxParameters.Devices); + Assert.Equal("/dev/sda", jobStateChangeEvent.Detail.Container.LinuxParameters.Devices[0].ContainerPath); + Assert.Equal("/dev/xvdc", jobStateChangeEvent.Detail.Container.LinuxParameters.Devices[0].HostPath); + Assert.Single(jobStateChangeEvent.Detail.Container.LinuxParameters.Devices[0].Permissions); + Assert.Equal("MKNOD", jobStateChangeEvent.Detail.Container.LinuxParameters.Devices[0].Permissions[0]); + Assert.True(jobStateChangeEvent.Detail.Container.LinuxParameters.InitProcessEnabled); + Assert.Equal(64, jobStateChangeEvent.Detail.Container.LinuxParameters.SharedMemorySize); + Assert.Equal(1024, jobStateChangeEvent.Detail.Container.LinuxParameters.MaxSwap); + Assert.Equal(55, jobStateChangeEvent.Detail.Container.LinuxParameters.Swappiness); + Assert.Single(jobStateChangeEvent.Detail.Container.LinuxParameters.Tmpfs); + Assert.Equal("/run", jobStateChangeEvent.Detail.Container.LinuxParameters.Tmpfs[0].ContainerPath); + Assert.Equal(65536, jobStateChangeEvent.Detail.Container.LinuxParameters.Tmpfs[0].Size); + Assert.Equal(2, jobStateChangeEvent.Detail.Container.LinuxParameters.Tmpfs[0].MountOptions.Count); + Assert.Equal("noexec", jobStateChangeEvent.Detail.Container.LinuxParameters.Tmpfs[0].MountOptions[0]); + Assert.Equal("nosuid", jobStateChangeEvent.Detail.Container.LinuxParameters.Tmpfs[0].MountOptions[1]); + Assert.NotNull(jobStateChangeEvent.Detail.Container.LogConfiguration); + Assert.Equal("json-file", jobStateChangeEvent.Detail.Container.LogConfiguration.LogDriver); + Assert.Equal(2, jobStateChangeEvent.Detail.Container.LogConfiguration.Options.Count); + Assert.Equal("10m", jobStateChangeEvent.Detail.Container.LogConfiguration.Options["max-size"]); + Assert.Equal("3", jobStateChangeEvent.Detail.Container.LogConfiguration.Options["max-file"]); + Assert.Single(jobStateChangeEvent.Detail.Container.LogConfiguration.SecretOptions); + Assert.Equal("apikey", jobStateChangeEvent.Detail.Container.LogConfiguration.SecretOptions[0].Name); + Assert.Equal("ddApiKey", jobStateChangeEvent.Detail.Container.LogConfiguration.SecretOptions[0].ValueFrom); + Assert.Single(jobStateChangeEvent.Detail.Container.Secrets); + Assert.Equal("DATABASE_PASSWORD", jobStateChangeEvent.Detail.Container.Secrets[0].Name); + Assert.Equal("arn:aws:ssm:us-east-1:awsExampleAccountID:parameter/awsExampleParameter", jobStateChangeEvent.Detail.Container.Secrets[0].ValueFrom); + Assert.NotNull(jobStateChangeEvent.Detail.Container.NetworkConfiguration); + Assert.Equal("ENABLED", jobStateChangeEvent.Detail.Container.NetworkConfiguration.AssignPublicIp); + Assert.NotNull(jobStateChangeEvent.Detail.Container.FargatePlatformConfiguration); + Assert.Equal("LATEST", jobStateChangeEvent.Detail.Container.FargatePlatformConfiguration.PlatformVersion); + Assert.NotNull(jobStateChangeEvent.Detail.NodeProperties); + Assert.Equal(0, jobStateChangeEvent.Detail.NodeProperties.MainNode); + Assert.Equal(0, jobStateChangeEvent.Detail.NodeProperties.NumNodes); + Assert.Single(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties); + Assert.Equal("0:1", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].TargetNodes); + Assert.NotNull(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container); + Assert.Equal("busybox", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Image); + Assert.Equal(2, jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.ResourceRequirements.Count); + Assert.Equal("MEMORY", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.ResourceRequirements[0].Type); + Assert.Equal("2000", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.ResourceRequirements[0].Value); + Assert.Equal("VCPU", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.ResourceRequirements[1].Type); + Assert.Equal("2", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.ResourceRequirements[1].Value); + Assert.Equal(2, jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Vcpus); + Assert.Equal(2000, jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Memory); + Assert.Equal(2, jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Command.Count); + Assert.Equal("echo", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Command[0]); + Assert.Equal("'hello world'", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Command[1]); + Assert.Equal(2, jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes.Count); + Assert.Equal("myhostsource", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes[0].Name); + Assert.Equal("/data", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes[0].Host.SourcePath); + Assert.Equal("efs", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes[1].Name); + Assert.Equal("fsap-XXXXXXXXXXXXXXXXX", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes[1].EfsVolumeConfiguration.AuthorizationConfig.AccessPointId); + Assert.Equal("ENABLED", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes[1].EfsVolumeConfiguration.AuthorizationConfig.Iam); + Assert.Equal("fs-XXXXXXXXX", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes[1].EfsVolumeConfiguration.FileSystemId); + Assert.Equal("/", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes[1].EfsVolumeConfiguration.RootDirectory); + Assert.Equal("ENABLED", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes[1].EfsVolumeConfiguration.TransitEncryption); + Assert.Equal(12345, jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes[1].EfsVolumeConfiguration.TransitEncryptionPort); + Assert.Single(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Environment); + Assert.Equal("MANAGED_BY_AWS", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Environment[0].Name); + Assert.Equal("STARTED_BY_STEP_FUNCTIONS", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Environment[0].Value); + Assert.Equal(2, jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.MountPoints.Count); + Assert.Equal("/data", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.MountPoints[0].ContainerPath); + Assert.True(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.MountPoints[0].ReadOnly); + Assert.Equal("myhostsource", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.MountPoints[0].SourceVolume); + Assert.Equal("/mount/efs", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.MountPoints[1].ContainerPath); + Assert.Equal("efs", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.MountPoints[1].SourceVolume); + Assert.Single(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Ulimits); + Assert.Equal(2048, jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Ulimits[0].HardLimit); + Assert.Equal("nofile", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Ulimits[0].Name); + Assert.Equal(2048, jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Ulimits[0].SoftLimit); + Assert.Equal("arn:aws:iam::awsExampleAccountID:role/awsExampleRoleName", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.ExecutionRoleArn); + Assert.Equal("p3.2xlarge", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.InstanceType); + Assert.Equal("testuser", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.User); + Assert.Equal("arn:aws:iam::awsExampleAccountID:role/awsExampleRoleName", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.JobRoleArn); + Assert.Single(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Devices); + Assert.Equal("/dev/xvdc", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Devices[0].HostPath); + Assert.Equal("/dev/sda", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Devices[0].ContainerPath); + Assert.Single(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Devices[0].Permissions); + Assert.Equal("MKNOD", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Devices[0].Permissions[0]); + Assert.True(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.InitProcessEnabled); + Assert.Equal(64, jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.SharedMemorySize); + Assert.Equal(1024, jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.MaxSwap); + Assert.Equal(55, jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Swappiness); + Assert.Single(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Tmpfs); + Assert.Equal("/run", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Tmpfs[0].ContainerPath); + Assert.Equal(65536, jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Tmpfs[0].Size); + Assert.Equal(2, jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Tmpfs[0].MountOptions.Count); + Assert.Equal("noexec", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Tmpfs[0].MountOptions[0]); + Assert.Equal("nosuid", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Tmpfs[0].MountOptions[1]); + Assert.Equal("awslogs", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LogConfiguration.LogDriver); + Assert.Equal("awslogs-wordpress", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LogConfiguration.Options["awslogs-group"]); + Assert.Equal("awslogs-example", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LogConfiguration.Options["awslogs-stream-prefix"]); + Assert.Single(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LogConfiguration.SecretOptions); + Assert.Equal("apikey", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LogConfiguration.SecretOptions[0].Name); + Assert.Equal("ddApiKey", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LogConfiguration.SecretOptions[0].ValueFrom); + Assert.Single(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Secrets); + Assert.Equal("DATABASE_PASSWORD", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Secrets[0].Name); + Assert.Equal("arn:aws:ssm:us-east-1:awsExampleAccountID:parameter/awsExampleParameter", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Secrets[0].ValueFrom); + Assert.Equal("DISABLED", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.NetworkConfiguration.AssignPublicIp); + Assert.Equal("LATEST", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.FargatePlatformConfiguration.PlatformVersion); + Assert.True(jobStateChangeEvent.Detail.PropagateTags); + Assert.Equal(90, jobStateChangeEvent.Detail.Timeout.AttemptDurationSeconds); + Assert.Equal(3, jobStateChangeEvent.Detail.Tags.Count); + Assert.Equal("Batch", jobStateChangeEvent.Detail.Tags["Service"]); + Assert.Equal("JobDefinitionTag", jobStateChangeEvent.Detail.Tags["Name"]); + Assert.Equal("MergeTag", jobStateChangeEvent.Detail.Tags["Expected"]); + Assert.Single(jobStateChangeEvent.Detail.PlatformCapabilities); + Assert.Equal("FARGATE", jobStateChangeEvent.Detail.PlatformCapabilities[0]); + } + } + + + public MemoryStream LoadJsonTestFile(string filename) + { + var json = File.ReadAllText(filename); + return new MemoryStream(UTF8Encoding.UTF8.GetBytes(json)); + } + } +} diff --git a/Libraries/test/EventsTests.NETCore31/TestResponseCasing.cs b/Libraries/test/EventsTests.NET8/TestResponseCasing.cs similarity index 98% rename from Libraries/test/EventsTests.NETCore31/TestResponseCasing.cs rename to Libraries/test/EventsTests.NET8/TestResponseCasing.cs index f6385bc86..d14b5ec47 100644 --- a/Libraries/test/EventsTests.NETCore31/TestResponseCasing.cs +++ b/Libraries/test/EventsTests.NET8/TestResponseCasing.cs @@ -5,7 +5,7 @@ using Amazon.Lambda.Serialization.SystemTextJson; using Newtonsoft.Json.Linq; -namespace EventsTests31 +namespace EventsTests.NET8 { public class TestResponseCasing { @@ -52,4 +52,4 @@ public class DummyResponse public string BingBong { get; set; } } } -} \ No newline at end of file +} diff --git a/Libraries/test/EventsTests.NETCore31/EventsTests.NETCore31.csproj b/Libraries/test/EventsTests.NETCore31/EventsTests.NETCore31.csproj deleted file mode 100644 index 25fedbb49..000000000 --- a/Libraries/test/EventsTests.NETCore31/EventsTests.NETCore31.csproj +++ /dev/null @@ -1,71 +0,0 @@ - - - - netcoreapp3.1 - EventsTests31 - true - win7-x64;win7-x86 - EventsTests31 - latest - - - - - - PreserveNewest - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Libraries/test/EventsTests.Shared/DynamoDBEventJsonTests.cs b/Libraries/test/EventsTests.Shared/DynamoDBEventJsonTests.cs index 39338fe17..ee3d251ac 100644 --- a/Libraries/test/EventsTests.Shared/DynamoDBEventJsonTests.cs +++ b/Libraries/test/EventsTests.Shared/DynamoDBEventJsonTests.cs @@ -1,4 +1,4 @@ -using Amazon.DynamoDBv2.DocumentModel; +using Amazon.DynamoDBv2.DocumentModel; using Amazon.Lambda.DynamoDBEvents; using System.Collections.Generic; using System.IO; @@ -223,7 +223,7 @@ public void Map_ToDocument() Assert.NotNull(document["Map"].AsDocument()); Assert.Equal("string", document["Map"].AsDocument()["string"].AsString()); Assert.Equal(123.45, document["Map"].AsDocument()["number"].AsDouble()); - Assert.Equal(false, document["Map"].AsDocument()["boolean"].AsBoolean()); + Assert.False(document["Map"].AsDocument()["boolean"].AsBoolean()); } [Fact] @@ -357,9 +357,9 @@ public void StringSet_ToDocument() var hashSet = document["StringSet"].AsHashSetOfString(); Assert.NotNull(hashSet); Assert.Equal(3, hashSet.Count); - Assert.True(hashSet.Contains("Black")); - Assert.True(hashSet.Contains("Green")); - Assert.True(hashSet.Contains("Red")); + Assert.Contains("Black", hashSet); + Assert.Contains("Green", hashSet); + Assert.Contains("Red", hashSet); } [Fact] @@ -492,7 +492,7 @@ public void NoAttributes_ToDocument() var json = evnt.Records[0].Dynamodb.NewImage.ToJson(); var document = Document.FromJson(json); - Assert.Equal(0, document.Count); + Assert.Empty(document); } } } diff --git a/Libraries/test/EventsTests.Shared/EventTests.cs b/Libraries/test/EventsTests.Shared/EventTests.cs index 8fa6a6a54..4ca0d39c9 100644 --- a/Libraries/test/EventsTests.Shared/EventTests.cs +++ b/Libraries/test/EventsTests.Shared/EventTests.cs @@ -1,43 +1,43 @@ #pragma warning disable 618 +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.ApplicationLoadBalancerEvents; +using Amazon.Lambda.CloudWatchEvents.BatchEvents; +using Amazon.Lambda.CloudWatchEvents.ECSEvents; +using Amazon.Lambda.CloudWatchEvents.S3Events; +using Amazon.Lambda.CloudWatchEvents.ScheduledEvents; +using Amazon.Lambda.CloudWatchEvents.TranscribeEvents; +using Amazon.Lambda.CloudWatchEvents.TranslateEvents; +using Amazon.Lambda.CloudWatchLogsEvents; +using Amazon.Lambda.CognitoEvents; +using Amazon.Lambda.ConfigEvents; +using Amazon.Lambda.ConnectEvents; +using Amazon.Lambda.Core; +using Amazon.Lambda.DynamoDBEvents; +using Amazon.Lambda.KafkaEvents; +using Amazon.Lambda.KinesisAnalyticsEvents; +using Amazon.Lambda.KinesisEvents; +using Amazon.Lambda.KinesisFirehoseEvents; +using Amazon.Lambda.LexEvents; +using Amazon.Lambda.LexV2Events; +using Amazon.Lambda.MQEvents; +using Amazon.Lambda.S3Events; +using Amazon.Lambda.Serialization.Json; +using Amazon.Lambda.SimpleEmailEvents; +using Amazon.Lambda.SNSEvents; +using Amazon.Lambda.SQSEvents; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Xunit; +using JsonSerializer = Amazon.Lambda.Serialization.Json.JsonSerializer; + namespace Amazon.Lambda.Tests { - using Amazon.Lambda.APIGatewayEvents; - using Amazon.Lambda.ApplicationLoadBalancerEvents; - using Amazon.Lambda.CloudWatchEvents.BatchEvents; - using Amazon.Lambda.CloudWatchEvents.ECSEvents; - using Amazon.Lambda.CloudWatchEvents.S3Events; - using Amazon.Lambda.CloudWatchEvents.ScheduledEvents; - using Amazon.Lambda.CloudWatchEvents.TranscribeEvents; - using Amazon.Lambda.CloudWatchEvents.TranslateEvents; - using Amazon.Lambda.CloudWatchLogsEvents; - using Amazon.Lambda.CognitoEvents; - using Amazon.Lambda.ConfigEvents; - using Amazon.Lambda.ConnectEvents; - using Amazon.Lambda.Core; - using Amazon.Lambda.DynamoDBEvents; - using Amazon.Lambda.KafkaEvents; - using Amazon.Lambda.KinesisAnalyticsEvents; - using Amazon.Lambda.KinesisEvents; - using Amazon.Lambda.KinesisFirehoseEvents; - using Amazon.Lambda.LexEvents; - using Amazon.Lambda.LexV2Events; - using Amazon.Lambda.MQEvents; - using Amazon.Lambda.S3Events; - using Amazon.Lambda.Serialization.Json; - using Amazon.Lambda.SimpleEmailEvents; - using Amazon.Lambda.SNSEvents; - using Amazon.Lambda.SQSEvents; - using Amazon.Runtime.Internal.Transform; - using Newtonsoft.Json; - using Newtonsoft.Json.Linq; - using Newtonsoft.Json.Serialization; - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Text; - using Xunit; - using JsonSerializer = Amazon.Lambda.Serialization.Json.JsonSerializer; public class EventTest { @@ -52,7 +52,7 @@ public MemoryStream LoadJsonTestFile(string filename) public string SerializeJson(ILambdaSerializer serializer, T response) { string serializedJson; - using (MemoryStream stream = new MemoryStream()) + using (var stream = new MemoryStream()) { serializer.Serialize(response, stream); @@ -64,10 +64,8 @@ public string SerializeJson(ILambdaSerializer serializer, T response) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void HttpApiV2Format(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -95,7 +93,7 @@ public void HttpApiV2Format(Type serializerType) Assert.Equal("value1", request.StageVariables["stageVariable1"]); Assert.Equal("value2", request.StageVariables["stageVariable2"]); - Assert.Equal(1, request.PathParameters.Count); + Assert.Single(request.PathParameters); Assert.Equal("value1", request.PathParameters["parameter1"]); var rc = request.RequestContext; @@ -141,10 +139,8 @@ public void HttpApiV2Format(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void HttpApiV2FormatLambdaAuthorizer(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -157,10 +153,8 @@ public void HttpApiV2FormatLambdaAuthorizer(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void HttpApiV2FormatIAMAuthorizer(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -201,10 +195,8 @@ public void SetHeadersToHttpApiV2Response() [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void S3ObjectLambdaEventTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -245,10 +237,8 @@ public void S3ObjectLambdaEventTest(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void S3PutTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -256,25 +246,25 @@ public void S3PutTest(Type serializerType) { var s3Event = serializer.Deserialize(fileStream); - Assert.Equal(s3Event.Records.Count, 2); + Assert.Equal(2, s3Event.Records.Count); var record = s3Event.Records[0]; - Assert.Equal(record.EventVersion, "2.0"); - Assert.Equal(record.EventTime.ToUniversalTime(), DateTime.Parse("1970-01-01T00:00:00.000Z").ToUniversalTime()); - Assert.Equal(record.RequestParameters.SourceIPAddress, "127.0.0.1"); - Assert.Equal(record.S3.ConfigurationId, "testConfigRule"); - Assert.Equal(record.S3.Object.ETag, "0123456789abcdef0123456789abcdef"); - Assert.Equal(record.S3.Object.Key, "HappyFace.jpg"); - Assert.Equal(record.S3.Object.Size, 1024); - Assert.Equal(record.S3.Bucket.Arn, "arn:aws:s3:::mybucket"); - Assert.Equal(record.S3.Bucket.Name, "sourcebucket"); - Assert.Equal(record.S3.Bucket.OwnerIdentity.PrincipalId, "EXAMPLE"); - Assert.Equal(record.S3.S3SchemaVersion, "1.0"); - Assert.Equal(record.ResponseElements.XAmzId2, "EXAMPLE123/5678abcdefghijklambdaisawesome/mnopqrstuvwxyzABCDEFGH"); - Assert.Equal(record.ResponseElements.XAmzRequestId, "EXAMPLE123456789"); - Assert.Equal(record.AwsRegion, "us-east-1"); - Assert.Equal(record.EventName, "ObjectCreated:Put"); - Assert.Equal(record.UserIdentity.PrincipalId, "EXAMPLE"); - Assert.Equal(record.EventSource, "aws:s3"); + Assert.Equal("2.0", record.EventVersion); + Assert.Equal(DateTime.Parse("1970-01-01T00:00:00.000Z").ToUniversalTime(), record.EventTime.ToUniversalTime()); + Assert.Equal("127.0.0.1", record.RequestParameters.SourceIPAddress); + Assert.Equal("testConfigRule", record.S3.ConfigurationId); + Assert.Equal("0123456789abcdef0123456789abcdef", record.S3.Object.ETag); + Assert.Equal("HappyFace.jpg", record.S3.Object.Key); + Assert.Equal(1024, record.S3.Object.Size); + Assert.Equal("arn:aws:s3:::mybucket", record.S3.Bucket.Arn); + Assert.Equal("sourcebucket", record.S3.Bucket.Name); + Assert.Equal("EXAMPLE", record.S3.Bucket.OwnerIdentity.PrincipalId); + Assert.Equal("1.0", record.S3.S3SchemaVersion); + Assert.Equal("EXAMPLE123/5678abcdefghijklambdaisawesome/mnopqrstuvwxyzABCDEFGH", record.ResponseElements.XAmzId2); + Assert.Equal("EXAMPLE123456789", record.ResponseElements.XAmzRequestId); + Assert.Equal("us-east-1", record.AwsRegion); + Assert.Equal("ObjectCreated:Put", record.EventName); + Assert.Equal("EXAMPLE", record.UserIdentity.PrincipalId); + Assert.Equal("aws:s3", record.EventSource); // In the events file the key is New+File.jpg simulating the key being url encoded. Assert.Equal("New File.jpg", s3Event.Records[1].S3.Object.KeyDecoded); @@ -294,38 +284,32 @@ private void Handle(S3Event s3Event) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void KinesisTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; using (var fileStream = LoadJsonTestFile("kinesis-event.json")) { var kinesisEvent = serializer.Deserialize(fileStream); - Assert.Equal(kinesisEvent.Records.Count, 2); + Assert.Equal(2, kinesisEvent.Records.Count); var record = kinesisEvent.Records[0]; - Assert.Equal(record.EventId, "shardId-000000000000:49568167373333333333333333333333333333333333333333333333"); - Assert.Equal(record.EventVersion, "1.0"); - Assert.Equal(record.Kinesis.PartitionKey, "s1"); + Assert.Equal("shardId-000000000000:49568167373333333333333333333333333333333333333333333333", record.EventId); + Assert.Equal("1.0", record.EventVersion); + Assert.Equal("s1", record.Kinesis.PartitionKey); var dataBytes = record.Kinesis.Data.ToArray(); - Assert.Equal(Convert.ToBase64String(dataBytes), "SGVsbG8gV29ybGQ="); - Assert.Equal(Encoding.UTF8.GetString(dataBytes), "Hello World"); - Assert.Equal(record.Kinesis.KinesisSchemaVersion, "1.0"); - Assert.Equal(record.Kinesis.SequenceNumber, "49568167373333333333333333333333333333333333333333333333"); - Assert.Equal(record.InvokeIdentityArn, "arn:aws:iam::123456789012:role/LambdaRole"); - Assert.Equal(record.EventName, "aws:kinesis:record"); - Assert.Equal(record.EventSourceARN, "arn:aws:kinesis:us-east-1:123456789012:stream/simple-stream"); - Assert.Equal(record.EventSource, "aws:kinesis"); - Assert.Equal(record.AwsRegion, "us-east-1"); -#if NET8_0_OR_GREATER + Assert.Equal("SGVsbG8gV29ybGQ=", Convert.ToBase64String(dataBytes)); + Assert.Equal("Hello World", Encoding.UTF8.GetString(dataBytes)); + Assert.Equal("1.0", record.Kinesis.KinesisSchemaVersion); + Assert.Equal("49568167373333333333333333333333333333333333333333333333", record.Kinesis.SequenceNumber); + Assert.Equal("arn:aws:iam::123456789012:role/LambdaRole", record.InvokeIdentityArn); + Assert.Equal("aws:kinesis:record", record.EventName); + Assert.Equal("arn:aws:kinesis:us-east-1:123456789012:stream/simple-stream", record.EventSourceARN); + Assert.Equal("aws:kinesis", record.EventSource); + Assert.Equal("us-east-1", record.AwsRegion); // Starting with .NET 7 the precision of the underlying AddSeconds method was changed. // https://learn.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/7.0/datetime-add-precision Assert.Equal(636162383234769999, record.Kinesis.ApproximateArrivalTimestamp.Value.ToUniversalTime().Ticks); -#else - Assert.Equal(636162383234770000, record.Kinesis.ApproximateArrivalTimestamp.Value.ToUniversalTime().Ticks); -#endif Handle(kinesisEvent); } @@ -345,10 +329,8 @@ private void Handle(KinesisEvent kinesisEvent) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void KinesisBatchItemFailuresTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -356,10 +338,10 @@ public void KinesisBatchItemFailuresTest(Type serializerType) { var kinesisStreamsEventResponse = serializer.Deserialize(fileStream); - Assert.Equal(1, kinesisStreamsEventResponse.BatchItemFailures.Count); + Assert.Single(kinesisStreamsEventResponse.BatchItemFailures); Assert.Equal("1405400000000002063282832", kinesisStreamsEventResponse.BatchItemFailures[0].ItemIdentifier); - MemoryStream ms = new MemoryStream(); + var ms = new MemoryStream(); serializer.Serialize(kinesisStreamsEventResponse, ms); ms.Position = 0; var json = new StreamReader(ms).ReadToEnd(); @@ -372,10 +354,8 @@ public void KinesisBatchItemFailuresTest(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void KinesisTimeWindowTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -383,35 +363,35 @@ public void KinesisTimeWindowTest(Type serializerType) { var kinesisTimeWindowEvent = serializer.Deserialize(fileStream); - Assert.Equal(kinesisTimeWindowEvent.ShardId, "shardId-000000000006"); - Assert.Equal(kinesisTimeWindowEvent.EventSourceARN, "arn:aws:kinesis:us-east-1:123456789012:stream/lambda-stream"); + Assert.Equal("shardId-000000000006", kinesisTimeWindowEvent.ShardId); + Assert.Equal("arn:aws:kinesis:us-east-1:123456789012:stream/lambda-stream", kinesisTimeWindowEvent.EventSourceARN); Assert.False(kinesisTimeWindowEvent.IsFinalInvokeForWindow); Assert.False(kinesisTimeWindowEvent.IsWindowTerminatedEarly); - Assert.Equal(kinesisTimeWindowEvent.State.Count, 2); + Assert.Equal(2, kinesisTimeWindowEvent.State.Count); Assert.True(kinesisTimeWindowEvent.State.ContainsKey("1")); - Assert.Equal(kinesisTimeWindowEvent.State["1"], "282"); + Assert.Equal("282", kinesisTimeWindowEvent.State["1"]); Assert.True(kinesisTimeWindowEvent.State.ContainsKey("2")); - Assert.Equal(kinesisTimeWindowEvent.State["2"], "715"); + Assert.Equal("715", kinesisTimeWindowEvent.State["2"]); Assert.NotNull(kinesisTimeWindowEvent.Window); Assert.Equal(637430942400000000, kinesisTimeWindowEvent.Window.Start.Ticks); Assert.Equal(637430943600000000, kinesisTimeWindowEvent.Window.End.Ticks); - Assert.Equal(kinesisTimeWindowEvent.Records.Count, 1); + Assert.Single(kinesisTimeWindowEvent.Records); var record = kinesisTimeWindowEvent.Records[0]; - Assert.Equal(record.EventId, "shardId-000000000006:49590338271490256608559692538361571095921575989136588898"); - Assert.Equal(record.EventName, "aws:kinesis:record"); - Assert.Equal(record.EventVersion, "1.0"); - Assert.Equal(record.EventSource, "aws:kinesis"); - Assert.Equal(record.InvokeIdentityArn, "arn:aws:iam::123456789012:role/lambda-kinesis-role"); - Assert.Equal(record.AwsRegion, "us-east-1"); - Assert.Equal(record.EventSourceARN, "arn:aws:kinesis:us-east-1:123456789012:stream/lambda-stream"); - - Assert.Equal(record.Kinesis.KinesisSchemaVersion, "1.0"); - Assert.Equal(record.Kinesis.PartitionKey, "1"); - Assert.Equal(record.Kinesis.SequenceNumber, "49590338271490256608559692538361571095921575989136588898"); + Assert.Equal("shardId-000000000006:49590338271490256608559692538361571095921575989136588898", record.EventId); + Assert.Equal("aws:kinesis:record", record.EventName); + Assert.Equal("1.0", record.EventVersion); + Assert.Equal("aws:kinesis", record.EventSource); + Assert.Equal("arn:aws:iam::123456789012:role/lambda-kinesis-role", record.InvokeIdentityArn); + Assert.Equal("us-east-1", record.AwsRegion); + Assert.Equal("arn:aws:kinesis:us-east-1:123456789012:stream/lambda-stream", record.EventSourceARN); + + Assert.Equal("1.0", record.Kinesis.KinesisSchemaVersion); + Assert.Equal("1", record.Kinesis.PartitionKey); + Assert.Equal("49590338271490256608559692538361571095921575989136588898", record.Kinesis.SequenceNumber); var dataBytes = record.Kinesis.Data.ToArray(); - Assert.Equal(Convert.ToBase64String(dataBytes), "SGVsbG8sIHRoaXMgaXMgYSB0ZXN0Lg=="); - Assert.Equal(Encoding.UTF8.GetString(dataBytes), "Hello, this is a test."); + Assert.Equal("SGVsbG8sIHRoaXMgaXMgYSB0ZXN0Lg==", Convert.ToBase64String(dataBytes)); + Assert.Equal("Hello, this is a test.", Encoding.UTF8.GetString(dataBytes)); Assert.Equal(637430942750000000, record.Kinesis.ApproximateArrivalTimestamp.Value.ToUniversalTime().Ticks); Handle(kinesisTimeWindowEvent); @@ -432,10 +412,8 @@ private void Handle(KinesisTimeWindowEvent kinesisTimeWindowEvent) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void KinesisTimeWindowResponseTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -443,38 +421,36 @@ public void KinesisTimeWindowResponseTest(Type serializerType) { var kinesisTimeWindowResponse = serializer.Deserialize(fileStream); - Assert.Equal(kinesisTimeWindowResponse.State.Count, 2); + Assert.Equal(2, kinesisTimeWindowResponse.State.Count); Assert.True(kinesisTimeWindowResponse.State.ContainsKey("1")); - Assert.Equal(kinesisTimeWindowResponse.State["1"], "282"); + Assert.Equal("282", kinesisTimeWindowResponse.State["1"]); Assert.True(kinesisTimeWindowResponse.State.ContainsKey("2")); - Assert.Equal(kinesisTimeWindowResponse.State["2"], "715"); - Assert.Equal(kinesisTimeWindowResponse.BatchItemFailures.Count, 0); + Assert.Equal("715", kinesisTimeWindowResponse.State["2"]); + Assert.Empty(kinesisTimeWindowResponse.BatchItemFailures); } } [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void DynamoDbUpdateTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; Stream json = LoadJsonTestFile("dynamodb-event.json"); var dynamodbEvent = serializer.Deserialize(json); - Assert.Equal(dynamodbEvent.Records.Count, 2); + Assert.Equal(2, dynamodbEvent.Records.Count); var record = dynamodbEvent.Records[0]; - Assert.Equal(record.EventID, "f07f8ca4b0b26cb9c4e5e77e69f274ee"); - Assert.Equal(record.EventVersion, "1.1"); - Assert.Equal(record.Dynamodb.Keys.Count, 2); - Assert.Equal(record.Dynamodb.Keys["key"].S, "binary"); - Assert.Equal(record.Dynamodb.Keys["val"].S, "data"); + Assert.Equal("f07f8ca4b0b26cb9c4e5e77e69f274ee", record.EventID); + Assert.Equal("1.1", record.EventVersion); + Assert.Equal(2, record.Dynamodb.Keys.Count); + Assert.Equal("binary", record.Dynamodb.Keys["key"].S); + Assert.Equal("data", record.Dynamodb.Keys["val"].S); Assert.Null(record.UserIdentity); Assert.Null(record.Dynamodb.OldImage); - Assert.Equal(record.Dynamodb.NewImage["val"].S, "data"); - Assert.Equal(record.Dynamodb.NewImage["key"].S, "binary"); + Assert.Equal("data", record.Dynamodb.NewImage["val"].S); + Assert.Equal("binary", record.Dynamodb.NewImage["key"].S); Assert.Null(record.Dynamodb.NewImage["key"].BOOL); Assert.Null(record.Dynamodb.NewImage["key"].L); Assert.Null(record.Dynamodb.NewImage["key"].M); @@ -482,26 +458,26 @@ public void DynamoDbUpdateTest(Type serializerType) Assert.Null(record.Dynamodb.NewImage["key"].NS); Assert.Null(record.Dynamodb.NewImage["key"].NULL); Assert.Null(record.Dynamodb.NewImage["key"].SS); - Assert.Equal(MemoryStreamToBase64String(record.Dynamodb.NewImage["asdf1"].B), "AAEqQQ=="); - Assert.Equal(record.Dynamodb.NewImage["asdf2"].BS.Count, 2); - Assert.Equal(MemoryStreamToBase64String(record.Dynamodb.NewImage["asdf2"].BS[0]), "AAEqQQ=="); - Assert.Equal(MemoryStreamToBase64String(record.Dynamodb.NewImage["asdf2"].BS[1]), "QSoBAA=="); - Assert.Equal(record.Dynamodb.StreamViewType, "NEW_AND_OLD_IMAGES"); - Assert.Equal(record.Dynamodb.SequenceNumber, "1405400000000002063282832"); - Assert.Equal(record.Dynamodb.SizeBytes, 54); - Assert.Equal(record.AwsRegion, "us-east-1"); - Assert.Equal(record.EventName, "INSERT"); - Assert.Equal(record.EventSourceArn, "arn:aws:dynamodb:us-east-1:123456789012:table/Example-Table/stream/2016-12-01T00:00:00.000"); - Assert.Equal(record.EventSource, "aws:dynamodb"); + Assert.Equal("AAEqQQ==", MemoryStreamToBase64String(record.Dynamodb.NewImage["asdf1"].B)); + Assert.Equal(2, record.Dynamodb.NewImage["asdf2"].BS.Count); + Assert.Equal("AAEqQQ==", MemoryStreamToBase64String(record.Dynamodb.NewImage["asdf2"].BS[0])); + Assert.Equal("QSoBAA==", MemoryStreamToBase64String(record.Dynamodb.NewImage["asdf2"].BS[1])); + Assert.Equal("NEW_AND_OLD_IMAGES", record.Dynamodb.StreamViewType); + Assert.Equal("1405400000000002063282832", record.Dynamodb.SequenceNumber); + Assert.Equal(54, record.Dynamodb.SizeBytes); + Assert.Equal("us-east-1", record.AwsRegion); + Assert.Equal("INSERT", record.EventName); + Assert.Equal("arn:aws:dynamodb:us-east-1:123456789012:table/Example-Table/stream/2016-12-01T00:00:00.000", record.EventSourceArn); + Assert.Equal("aws:dynamodb", record.EventSource); var recordDateTime = record.Dynamodb.ApproximateCreationDateTime; - Assert.Equal(recordDateTime.Ticks, 636162388200000000); + Assert.Equal(636162388200000000, recordDateTime.Ticks); var topLevelList = record.Dynamodb.NewImage["misc1"].L; - Assert.Equal(0, topLevelList.Count); + Assert.Empty(topLevelList); var nestedMap = record.Dynamodb.NewImage["misc2"].M; Assert.NotNull(nestedMap); - Assert.Equal(0, nestedMap["ItemsEmpty"].L.Count); + Assert.Empty(nestedMap["ItemsEmpty"].L); Assert.Equal(3, nestedMap["ItemsNonEmpty"].L.Count); Assert.False(nestedMap["ItemBoolean"].BOOL); Assert.True(nestedMap["ItemNull"].NULL); @@ -527,27 +503,25 @@ public void DynamoDbUpdateTest(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void DynamoDbWithMillisecondsTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; Stream json = LoadJsonTestFile("dynamodb-with-ms-event.json"); var dynamodbEvent = serializer.Deserialize(json); - Assert.Equal(dynamodbEvent.Records.Count, 2); + Assert.Equal(2, dynamodbEvent.Records.Count); var record = dynamodbEvent.Records[0]; - Assert.Equal(record.EventID, "f07f8ca4b0b26cb9c4e5e77e69f274ee"); - Assert.Equal(record.EventVersion, "1.1"); - Assert.Equal(record.Dynamodb.Keys.Count, 2); - Assert.Equal(record.Dynamodb.Keys["key"].S, "binary"); - Assert.Equal(record.Dynamodb.Keys["val"].S, "data"); + Assert.Equal("f07f8ca4b0b26cb9c4e5e77e69f274ee", record.EventID); + Assert.Equal("1.1", record.EventVersion); + Assert.Equal(2, record.Dynamodb.Keys.Count); + Assert.Equal("binary", record.Dynamodb.Keys["key"].S); + Assert.Equal("data", record.Dynamodb.Keys["val"].S); Assert.Null(record.UserIdentity); Assert.Null(record.Dynamodb.OldImage); - Assert.Equal(record.Dynamodb.NewImage["val"].S, "data"); - Assert.Equal(record.Dynamodb.NewImage["key"].S, "binary"); + Assert.Equal("data", record.Dynamodb.NewImage["val"].S); + Assert.Equal("binary", record.Dynamodb.NewImage["key"].S); Assert.Null(record.Dynamodb.NewImage["key"].BOOL); Assert.Null(record.Dynamodb.NewImage["key"].L); Assert.Null(record.Dynamodb.NewImage["key"].M); @@ -555,26 +529,26 @@ public void DynamoDbWithMillisecondsTest(Type serializerType) Assert.Null(record.Dynamodb.NewImage["key"].NS); Assert.Null(record.Dynamodb.NewImage["key"].NULL); Assert.Null(record.Dynamodb.NewImage["key"].SS); - Assert.Equal(MemoryStreamToBase64String(record.Dynamodb.NewImage["asdf1"].B), "AAEqQQ=="); - Assert.Equal(record.Dynamodb.NewImage["asdf2"].BS.Count, 2); - Assert.Equal(MemoryStreamToBase64String(record.Dynamodb.NewImage["asdf2"].BS[0]), "AAEqQQ=="); - Assert.Equal(MemoryStreamToBase64String(record.Dynamodb.NewImage["asdf2"].BS[1]), "QSoBAA=="); - Assert.Equal(record.Dynamodb.StreamViewType, "NEW_AND_OLD_IMAGES"); - Assert.Equal(record.Dynamodb.SequenceNumber, "1405400000000002063282832"); - Assert.Equal(record.Dynamodb.SizeBytes, 54); - Assert.Equal(record.AwsRegion, "us-east-1"); - Assert.Equal(record.EventName, "INSERT"); - Assert.Equal(record.EventSourceArn, "arn:aws:dynamodb:us-east-1:123456789012:table/Example-Table/stream/2016-12-01T00:00:00.000"); - Assert.Equal(record.EventSource, "aws:dynamodb"); + Assert.Equal("AAEqQQ==", MemoryStreamToBase64String(record.Dynamodb.NewImage["asdf1"].B)); + Assert.Equal(2, record.Dynamodb.NewImage["asdf2"].BS.Count); + Assert.Equal("AAEqQQ==", MemoryStreamToBase64String(record.Dynamodb.NewImage["asdf2"].BS[0])); + Assert.Equal("QSoBAA==", MemoryStreamToBase64String(record.Dynamodb.NewImage["asdf2"].BS[1])); + Assert.Equal("NEW_AND_OLD_IMAGES", record.Dynamodb.StreamViewType); + Assert.Equal("1405400000000002063282832", record.Dynamodb.SequenceNumber); + Assert.Equal(54, record.Dynamodb.SizeBytes); + Assert.Equal("us-east-1", record.AwsRegion); + Assert.Equal("INSERT", record.EventName); + Assert.Equal("arn:aws:dynamodb:us-east-1:123456789012:table/Example-Table/stream/2016-12-01T00:00:00.000", record.EventSourceArn); + Assert.Equal("aws:dynamodb", record.EventSource); var recordDateTime = record.Dynamodb.ApproximateCreationDateTime; - Assert.Equal(recordDateTime.Ticks, 636162388200000000); + Assert.Equal(636162388200000000, recordDateTime.Ticks); var topLevelList = record.Dynamodb.NewImage["misc1"].L; - Assert.Equal(0, topLevelList.Count); + Assert.Empty(topLevelList); var nestedMap = record.Dynamodb.NewImage["misc2"].M; Assert.NotNull(nestedMap); - Assert.Equal(0, nestedMap["ItemsEmpty"].L.Count); + Assert.Empty(nestedMap["ItemsEmpty"].L); Assert.Equal(3, nestedMap["ItemsNonEmpty"].L.Count); Assert.False(nestedMap["ItemBoolean"].BOOL); Assert.True(nestedMap["ItemNull"].NULL); @@ -600,10 +574,8 @@ public void DynamoDbWithMillisecondsTest(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void DynamoDbBatchItemFailuresTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -611,10 +583,10 @@ public void DynamoDbBatchItemFailuresTest(Type serializerType) { var dynamoDbStreamsEventResponse = serializer.Deserialize(fileStream); - Assert.Equal(1, dynamoDbStreamsEventResponse.BatchItemFailures.Count); + Assert.Single(dynamoDbStreamsEventResponse.BatchItemFailures); Assert.Equal("1405400000000002063282832", dynamoDbStreamsEventResponse.BatchItemFailures[0].ItemIdentifier); - MemoryStream ms = new MemoryStream(); + var ms = new MemoryStream(); serializer.Serialize(dynamoDbStreamsEventResponse, ms); ms.Position = 0; var json = new StreamReader(ms).ReadToEnd(); @@ -638,10 +610,8 @@ private static void Handle(DynamoDBEvent ddbEvent) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void DynamoDBTimeWindowTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -649,80 +619,78 @@ public void DynamoDBTimeWindowTest(Type serializerType) { var dynamoDBTimeWindowEvent = serializer.Deserialize(fileStream); - Assert.Equal(dynamoDBTimeWindowEvent.ShardId, "shard123456789"); - Assert.Equal(dynamoDBTimeWindowEvent.EventSourceArn, "stream-ARN"); + Assert.Equal("shard123456789", dynamoDBTimeWindowEvent.ShardId); + Assert.Equal("stream-ARN", dynamoDBTimeWindowEvent.EventSourceArn); Assert.False(dynamoDBTimeWindowEvent.IsFinalInvokeForWindow); Assert.False(dynamoDBTimeWindowEvent.IsWindowTerminatedEarly); - Assert.Equal(dynamoDBTimeWindowEvent.State.Count, 1); + Assert.Single(dynamoDBTimeWindowEvent.State); Assert.True(dynamoDBTimeWindowEvent.State.ContainsKey("1")); - Assert.Equal(dynamoDBTimeWindowEvent.State["1"], "state1"); + Assert.Equal("state1", dynamoDBTimeWindowEvent.State["1"]); Assert.NotNull(dynamoDBTimeWindowEvent.Window); Assert.Equal(637317252000000000, dynamoDBTimeWindowEvent.Window.Start.Ticks); Assert.Equal(637317255000000000, dynamoDBTimeWindowEvent.Window.End.Ticks); - Assert.Equal(dynamoDBTimeWindowEvent.Records.Count, 3); + Assert.Equal(3, dynamoDBTimeWindowEvent.Records.Count); var record1 = dynamoDBTimeWindowEvent.Records[0]; - Assert.Equal(record1.EventID, "1"); - Assert.Equal(record1.EventName, "INSERT"); - Assert.Equal(record1.EventVersion, "1.0"); - Assert.Equal(record1.EventSource, "aws:dynamodb"); - Assert.Equal(record1.AwsRegion, "us-east-1"); - Assert.Equal(record1.EventSourceArn, "stream-ARN"); - Assert.Equal(record1.Dynamodb.Keys.Count, 1); - Assert.Equal(record1.Dynamodb.Keys["Id"].N, "101"); - Assert.Equal(record1.Dynamodb.SequenceNumber, "111"); - Assert.Equal(record1.Dynamodb.SizeBytes, 26); - Assert.Equal(record1.Dynamodb.StreamViewType, "NEW_IMAGE"); - Assert.Equal(record1.Dynamodb.NewImage.Count, 2); - Assert.Equal(record1.Dynamodb.NewImage["Message"].S, "New item!"); - Assert.Equal(record1.Dynamodb.NewImage["Id"].N, "101"); + Assert.Equal("1", record1.EventID); + Assert.Equal("INSERT", record1.EventName); + Assert.Equal("1.0", record1.EventVersion); + Assert.Equal("aws:dynamodb", record1.EventSource); + Assert.Equal("us-east-1", record1.AwsRegion); + Assert.Equal("stream-ARN", record1.EventSourceArn); + Assert.Single(record1.Dynamodb.Keys); + Assert.Equal("101", record1.Dynamodb.Keys["Id"].N); + Assert.Equal("111", record1.Dynamodb.SequenceNumber); + Assert.Equal(26, record1.Dynamodb.SizeBytes); + Assert.Equal("NEW_IMAGE", record1.Dynamodb.StreamViewType); + Assert.Equal(2, record1.Dynamodb.NewImage.Count); + Assert.Equal("New item!", record1.Dynamodb.NewImage["Message"].S); + Assert.Equal("101", record1.Dynamodb.NewImage["Id"].N); Assert.Null(record1.Dynamodb.OldImage); var record2 = dynamoDBTimeWindowEvent.Records[1]; - Assert.Equal(record2.EventID, "2"); - Assert.Equal(record2.EventName, "MODIFY"); - Assert.Equal(record2.EventVersion, "1.0"); - Assert.Equal(record2.EventSource, "aws:dynamodb"); - Assert.Equal(record2.AwsRegion, "us-east-1"); - Assert.Equal(record2.EventSourceArn, "stream-ARN"); - Assert.Equal(record2.Dynamodb.Keys.Count, 1); - Assert.Equal(record2.Dynamodb.Keys["Id"].N, "101"); - Assert.Equal(record2.Dynamodb.SequenceNumber, "222"); - Assert.Equal(record2.Dynamodb.SizeBytes, 59); - Assert.Equal(record2.Dynamodb.StreamViewType, "NEW_AND_OLD_IMAGES"); - Assert.Equal(record2.Dynamodb.NewImage.Count, 2); - Assert.Equal(record2.Dynamodb.NewImage["Message"].S, "This item has changed"); - Assert.Equal(record2.Dynamodb.NewImage["Id"].N, "101"); - Assert.Equal(record2.Dynamodb.OldImage.Count, 2); - Assert.Equal(record2.Dynamodb.OldImage["Message"].S, "New item!"); - Assert.Equal(record2.Dynamodb.OldImage["Id"].N, "101"); + Assert.Equal("2", record2.EventID); + Assert.Equal("MODIFY", record2.EventName); + Assert.Equal("1.0", record2.EventVersion); + Assert.Equal("aws:dynamodb", record2.EventSource); + Assert.Equal("us-east-1", record2.AwsRegion); + Assert.Equal("stream-ARN", record2.EventSourceArn); + Assert.Single(record2.Dynamodb.Keys); + Assert.Equal("101", record2.Dynamodb.Keys["Id"].N); + Assert.Equal("222", record2.Dynamodb.SequenceNumber); + Assert.Equal(59, record2.Dynamodb.SizeBytes); + Assert.Equal("NEW_AND_OLD_IMAGES", record2.Dynamodb.StreamViewType); + Assert.Equal(2, record2.Dynamodb.NewImage.Count); + Assert.Equal("This item has changed", record2.Dynamodb.NewImage["Message"].S); + Assert.Equal("101", record2.Dynamodb.NewImage["Id"].N); + Assert.Equal(2, record2.Dynamodb.OldImage.Count); + Assert.Equal("New item!", record2.Dynamodb.OldImage["Message"].S); + Assert.Equal("101", record2.Dynamodb.OldImage["Id"].N); var record3 = dynamoDBTimeWindowEvent.Records[2]; - Assert.Equal(record3.EventID, "3"); - Assert.Equal(record3.EventName, "REMOVE"); - Assert.Equal(record3.EventVersion, "1.0"); - Assert.Equal(record3.EventSource, "aws:dynamodb"); - Assert.Equal(record3.AwsRegion, "us-east-1"); - Assert.Equal(record3.EventSourceArn, "stream-ARN"); - Assert.Equal(record3.Dynamodb.Keys.Count, 1); - Assert.Equal(record3.Dynamodb.Keys["Id"].N, "101"); - Assert.Equal(record3.Dynamodb.SequenceNumber, "333"); - Assert.Equal(record3.Dynamodb.SizeBytes, 38); - Assert.Equal(record3.Dynamodb.StreamViewType, "NEW_AND_OLD_IMAGES"); + Assert.Equal("3", record3.EventID); + Assert.Equal("REMOVE", record3.EventName); + Assert.Equal("1.0", record3.EventVersion); + Assert.Equal("aws:dynamodb", record3.EventSource); + Assert.Equal("us-east-1", record3.AwsRegion); + Assert.Equal("stream-ARN", record3.EventSourceArn); + Assert.Single(record3.Dynamodb.Keys); + Assert.Equal("101", record3.Dynamodb.Keys["Id"].N); + Assert.Equal("333", record3.Dynamodb.SequenceNumber); + Assert.Equal(38, record3.Dynamodb.SizeBytes); + Assert.Equal("NEW_AND_OLD_IMAGES", record3.Dynamodb.StreamViewType); Assert.Null(record3.Dynamodb.NewImage); - Assert.Equal(record3.Dynamodb.OldImage.Count, 2); - Assert.Equal(record3.Dynamodb.OldImage["Message"].S, "This item has changed"); - Assert.Equal(record3.Dynamodb.OldImage["Id"].N, "101"); + Assert.Equal(2, record3.Dynamodb.OldImage.Count); + Assert.Equal("This item has changed", record3.Dynamodb.OldImage["Message"].S); + Assert.Equal("101", record3.Dynamodb.OldImage["Id"].N); } } [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void DynamoDBTimeWindowResponseTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -730,38 +698,36 @@ public void DynamoDBTimeWindowResponseTest(Type serializerType) { var dynamoDBTimeWindowResponse = serializer.Deserialize(fileStream); - Assert.Equal(dynamoDBTimeWindowResponse.State.Count, 2); + Assert.Equal(2, dynamoDBTimeWindowResponse.State.Count); Assert.True(dynamoDBTimeWindowResponse.State.ContainsKey("1")); - Assert.Equal(dynamoDBTimeWindowResponse.State["1"], "282"); + Assert.Equal("282", dynamoDBTimeWindowResponse.State["1"]); Assert.True(dynamoDBTimeWindowResponse.State.ContainsKey("2")); - Assert.Equal(dynamoDBTimeWindowResponse.State["2"], "715"); - Assert.Equal(dynamoDBTimeWindowResponse.BatchItemFailures.Count, 0); + Assert.Equal("715", dynamoDBTimeWindowResponse.State["2"]); + Assert.Empty(dynamoDBTimeWindowResponse.BatchItemFailures); } } [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void CognitoTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; using (var fileStream = LoadJsonTestFile("cognito-event.json")) { var cognitoEvent = serializer.Deserialize(fileStream); - Assert.Equal(cognitoEvent.Version, 2); - Assert.Equal(cognitoEvent.EventType, "SyncTrigger"); - Assert.Equal(cognitoEvent.Region, "us-east-1"); - Assert.Equal(cognitoEvent.DatasetName, "datasetName"); - Assert.Equal(cognitoEvent.IdentityPoolId, "identityPoolId"); - Assert.Equal(cognitoEvent.IdentityId, "identityId"); - Assert.Equal(cognitoEvent.DatasetRecords.Count, 1); + Assert.Equal(2, cognitoEvent.Version); + Assert.Equal("SyncTrigger", cognitoEvent.EventType); + Assert.Equal("us-east-1", cognitoEvent.Region); + Assert.Equal("datasetName", cognitoEvent.DatasetName); + Assert.Equal("identityPoolId", cognitoEvent.IdentityPoolId); + Assert.Equal("identityId", cognitoEvent.IdentityId); + Assert.Single(cognitoEvent.DatasetRecords); Assert.True(cognitoEvent.DatasetRecords.ContainsKey("SampleKey1")); - Assert.Equal(cognitoEvent.DatasetRecords["SampleKey1"].NewValue, "newValue1"); - Assert.Equal(cognitoEvent.DatasetRecords["SampleKey1"].OldValue, "oldValue1"); - Assert.Equal(cognitoEvent.DatasetRecords["SampleKey1"].Op, "replace"); + Assert.Equal("newValue1", cognitoEvent.DatasetRecords["SampleKey1"].NewValue); + Assert.Equal("oldValue1", cognitoEvent.DatasetRecords["SampleKey1"].OldValue); + Assert.Equal("replace", cognitoEvent.DatasetRecords["SampleKey1"].Op); Handle(cognitoEvent); } @@ -780,10 +746,8 @@ private static void Handle(CognitoEvent cognitoEvent) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void CognitoPreSignUpEventTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -809,7 +773,7 @@ public void CognitoPreSignUpEventTest(Type serializerType) Assert.True(cognitoPreSignupEvent.Response.AutoVerifyPhone); Assert.True(cognitoPreSignupEvent.Response.AutoVerifyEmail); - MemoryStream ms = new MemoryStream(); + var ms = new MemoryStream(); serializer.Serialize(cognitoPreSignupEvent, ms); ms.Position = 0; var json = new StreamReader(ms).ReadToEnd(); @@ -822,10 +786,8 @@ public void CognitoPreSignUpEventTest(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void CognitoPostConfirmationEventTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -841,7 +803,7 @@ public void CognitoPostConfirmationEventTest(Type serializerType) Assert.Equal("metadata_2", cognitoPostConfirmationEvent.Request.ClientMetadata.ToArray()[1].Key); Assert.Equal("metadata_value_2", cognitoPostConfirmationEvent.Request.ClientMetadata.ToArray()[1].Value); - MemoryStream ms = new MemoryStream(); + var ms = new MemoryStream(); serializer.Serialize(cognitoPostConfirmationEvent, ms); ms.Position = 0; var json = new StreamReader(ms).ReadToEnd(); @@ -854,10 +816,8 @@ public void CognitoPostConfirmationEventTest(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void CognitoPreAuthenticationEventTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -875,7 +835,7 @@ public void CognitoPreAuthenticationEventTest(Type serializerType) Assert.True(cognitoPreAuthenticationEvent.Request.UserNotFound); - MemoryStream ms = new MemoryStream(); + var ms = new MemoryStream(); serializer.Serialize(cognitoPreAuthenticationEvent, ms); ms.Position = 0; var json = new StreamReader(ms).ReadToEnd(); @@ -888,10 +848,8 @@ public void CognitoPreAuthenticationEventTest(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void CognitoPostAuthenticationEventTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -928,10 +886,8 @@ public void CognitoPostAuthenticationEventTest(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void CognitoDefineAuthChallengeEventTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -966,7 +922,7 @@ public void CognitoDefineAuthChallengeEventTest(Type serializerType) Assert.True(cognitoDefineAuthChallengeEvent.Response.IssueTokens); Assert.True(cognitoDefineAuthChallengeEvent.Response.FailAuthentication); - MemoryStream ms = new MemoryStream(); + var ms = new MemoryStream(); serializer.Serialize(cognitoDefineAuthChallengeEvent, ms); ms.Position = 0; var json = new StreamReader(ms).ReadToEnd(); @@ -979,10 +935,8 @@ public void CognitoDefineAuthChallengeEventTest(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void CognitoDefineAuthChallengeEventWithNullValuesTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -1020,10 +974,8 @@ public void CognitoDefineAuthChallengeEventWithNullValuesTest(Type serializerTyp [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void CognitoCreateAuthChallengeEventTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -1070,7 +1022,7 @@ public void CognitoCreateAuthChallengeEventTest(Type serializerType) Assert.Equal("challenge", cognitoCreateAuthChallengeEvent.Response.ChallengeMetadata); - MemoryStream ms = new MemoryStream(); + var ms = new MemoryStream(); serializer.Serialize(cognitoCreateAuthChallengeEvent, ms); ms.Position = 0; var json = new StreamReader(ms).ReadToEnd(); @@ -1083,10 +1035,8 @@ public void CognitoCreateAuthChallengeEventTest(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void CognitoVerifyAuthChallengeEventTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -1115,7 +1065,7 @@ public void CognitoVerifyAuthChallengeEventTest(Type serializerType) Assert.True(cognitoVerifyAuthChallengeEvent.Response.AnswerCorrect); - MemoryStream ms = new MemoryStream(); + var ms = new MemoryStream(); serializer.Serialize(cognitoVerifyAuthChallengeEvent, ms); ms.Position = 0; var json = new StreamReader(ms).ReadToEnd(); @@ -1129,10 +1079,8 @@ public void CognitoVerifyAuthChallengeEventTest(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void CognitoVerifyAuthChallengeEventWithNullValuesTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -1160,10 +1108,8 @@ public void CognitoVerifyAuthChallengeEventWithNullValuesTest(Type serializerTyp [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void CognitoPreTokenGenerationEventTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -1209,7 +1155,7 @@ public void CognitoPreTokenGenerationEventTest(Type serializerType) Assert.Equal("role", cognitoPreTokenGenerationEvent.Response.ClaimsOverrideDetails.GroupOverrideDetails.PreferredRole); - MemoryStream ms = new MemoryStream(); + var ms = new MemoryStream(); serializer.Serialize(cognitoPreTokenGenerationEvent, ms); ms.Position = 0; var json = new StreamReader(ms).ReadToEnd(); @@ -1222,10 +1168,8 @@ public void CognitoPreTokenGenerationEventTest(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void CognitoPreTokenGenerationV2EventTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -1301,7 +1245,7 @@ public void CognitoPreTokenGenerationV2EventTest(Type serializerType) Assert.Equal("role", cognitoPreTokenGenerationV2Event.Response.ClaimsAndScopeOverrideDetails.GroupOverrideDetails.PreferredRole); - MemoryStream ms = new MemoryStream(); + var ms = new MemoryStream(); serializer.Serialize(cognitoPreTokenGenerationV2Event, ms); ms.Position = 0; var json = new StreamReader(ms).ReadToEnd(); @@ -1314,10 +1258,8 @@ public void CognitoPreTokenGenerationV2EventTest(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void CognitoMigrateUserEventTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -1358,7 +1300,7 @@ public void CognitoMigrateUserEventTest(Type serializerType) Assert.True(cognitoMigrateUserEvent.Response.ForceAliasCreation); - MemoryStream ms = new MemoryStream(); + var ms = new MemoryStream(); serializer.Serialize(cognitoMigrateUserEvent, ms); ms.Position = 0; var json = new StreamReader(ms).ReadToEnd(); @@ -1371,10 +1313,8 @@ public void CognitoMigrateUserEventTest(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void CognitoCustomMessageEventTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -1397,7 +1337,7 @@ public void CognitoCustomMessageEventTest(Type serializerType) Assert.Equal("email", cognitoCustomMessageEvent.Response.EmailMessage); Assert.Equal("subject", cognitoCustomMessageEvent.Response.EmailSubject); - MemoryStream ms = new MemoryStream(); + var ms = new MemoryStream(); serializer.Serialize(cognitoCustomMessageEvent, ms); ms.Position = 0; var json = new StreamReader(ms).ReadToEnd(); @@ -1410,10 +1350,8 @@ public void CognitoCustomMessageEventTest(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void CognitoCustomEmailSenderEventTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -1426,7 +1364,7 @@ public void CognitoCustomEmailSenderEventTest(Type serializerType) Assert.Equal("code", cognitoCustomEmailSenderEvent.Request.Code); Assert.Equal("type", cognitoCustomEmailSenderEvent.Request.Type); - MemoryStream ms = new MemoryStream(); + var ms = new MemoryStream(); serializer.Serialize(cognitoCustomEmailSenderEvent, ms); ms.Position = 0; var json = new StreamReader(ms).ReadToEnd(); @@ -1439,10 +1377,8 @@ public void CognitoCustomEmailSenderEventTest(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void CognitoCustomSmsSenderEventTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -1489,30 +1425,28 @@ private static void AssertBaseClass(CognitoTriggerEvent(fileStream); - Assert.Equal(configEvent.ConfigRuleId, "config-rule-0123456"); - Assert.Equal(configEvent.Version, "1.0"); - Assert.Equal(configEvent.ConfigRuleName, "periodic-config-rule"); - Assert.Equal(configEvent.ConfigRuleArn, "arn:aws:config:us-east-1:012345678912:config-rule/config-rule-0123456"); - Assert.Equal(configEvent.InvokingEvent, ConfigInvokingEvent); - Assert.Equal(configEvent.ResultToken, "myResultToken"); - Assert.Equal(configEvent.EventLeftScope, false); - Assert.Equal(configEvent.RuleParameters, "{\"\":\"\"}"); - Assert.Equal(configEvent.ExecutionRoleArn, "arn:aws:iam::012345678912:role/config-role"); - Assert.Equal(configEvent.AccountId, "012345678912"); + Assert.Equal("config-rule-0123456", configEvent.ConfigRuleId); + Assert.Equal("1.0", configEvent.Version); + Assert.Equal("periodic-config-rule", configEvent.ConfigRuleName); + Assert.Equal("arn:aws:config:us-east-1:012345678912:config-rule/config-rule-0123456", configEvent.ConfigRuleArn); + Assert.Equal(ConfigInvokingEvent, configEvent.InvokingEvent); + Assert.Equal("myResultToken", configEvent.ResultToken); + Assert.False(configEvent.EventLeftScope); + Assert.Equal("{\"\":\"\"}", configEvent.RuleParameters); + Assert.Equal("arn:aws:iam::012345678912:role/config-role", configEvent.ExecutionRoleArn); + Assert.Equal("012345678912", configEvent.AccountId); Handle(configEvent); } @@ -1527,50 +1461,46 @@ private static void Handle(ConfigEvent configEvent) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void ConnectContactFlowTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; using (var fileStream = LoadJsonTestFile("connect-contactflow-event.json")) { var contactFlowEvent = serializer.Deserialize(fileStream); - Assert.Equal(contactFlowEvent.Name, "ContactFlowEvent"); + Assert.Equal("ContactFlowEvent", contactFlowEvent.Name); Assert.NotNull(contactFlowEvent.Details); Assert.NotNull(contactFlowEvent.Details.ContactData); Assert.NotNull(contactFlowEvent.Details.ContactData.Attributes); - Assert.Equal(contactFlowEvent.Details.ContactData.Attributes.Count, 0); - Assert.Equal(contactFlowEvent.Details.ContactData.Channel, "VOICE"); - Assert.Equal(contactFlowEvent.Details.ContactData.ContactId, "4a573372-1f28-4e26-b97b-XXXXXXXXXXX"); + Assert.Empty(contactFlowEvent.Details.ContactData.Attributes); + Assert.Equal("VOICE", contactFlowEvent.Details.ContactData.Channel); + Assert.Equal("4a573372-1f28-4e26-b97b-XXXXXXXXXXX", contactFlowEvent.Details.ContactData.ContactId); Assert.NotNull(contactFlowEvent.Details.ContactData.CustomerEndpoint); - Assert.Equal(contactFlowEvent.Details.ContactData.CustomerEndpoint.Address, "+1234567890"); - Assert.Equal(contactFlowEvent.Details.ContactData.CustomerEndpoint.Type, "TELEPHONE_NUMBER"); - Assert.Equal(contactFlowEvent.Details.ContactData.InitialContactId, "4a573372-1f28-4e26-b97b-XXXXXXXXXXX"); - Assert.Equal(contactFlowEvent.Details.ContactData.InitiationMethod, "INBOUND | OUTBOUND | TRANSFER | CALLBACK"); - Assert.Equal(contactFlowEvent.Details.ContactData.InstanceARN, "arn:aws:connect:aws-region:1234567890:instance/c8c0e68d-2200-4265-82c0-XXXXXXXXXX"); - Assert.Equal(contactFlowEvent.Details.ContactData.PreviousContactId, "4a573372-1f28-4e26-b97b-XXXXXXXXXXX"); + Assert.Equal("+1234567890", contactFlowEvent.Details.ContactData.CustomerEndpoint.Address); + Assert.Equal("TELEPHONE_NUMBER", contactFlowEvent.Details.ContactData.CustomerEndpoint.Type); + Assert.Equal("4a573372-1f28-4e26-b97b-XXXXXXXXXXX", contactFlowEvent.Details.ContactData.InitialContactId); + Assert.Equal("INBOUND | OUTBOUND | TRANSFER | CALLBACK", contactFlowEvent.Details.ContactData.InitiationMethod); + Assert.Equal("arn:aws:connect:aws-region:1234567890:instance/c8c0e68d-2200-4265-82c0-XXXXXXXXXX", contactFlowEvent.Details.ContactData.InstanceARN); + Assert.Equal("4a573372-1f28-4e26-b97b-XXXXXXXXXXX", contactFlowEvent.Details.ContactData.PreviousContactId); Assert.NotNull(contactFlowEvent.Details.ContactData.Queue); - Assert.Equal(contactFlowEvent.Details.ContactData.Queue.Arn, "arn:aws:connect:eu-west-2:111111111111:instance/cccccccc-bbbb-dddd-eeee-ffffffffffff/queue/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"); - Assert.Equal(contactFlowEvent.Details.ContactData.Queue.Name, "PasswordReset"); + Assert.Equal("arn:aws:connect:eu-west-2:111111111111:instance/cccccccc-bbbb-dddd-eeee-ffffffffffff/queue/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", contactFlowEvent.Details.ContactData.Queue.Arn); + Assert.Equal("PasswordReset", contactFlowEvent.Details.ContactData.Queue.Name); Assert.NotNull(contactFlowEvent.Details.ContactData.SystemEndpoint); - Assert.Equal(contactFlowEvent.Details.ContactData.SystemEndpoint.Address, "+1234567890"); - Assert.Equal(contactFlowEvent.Details.ContactData.SystemEndpoint.Type, "TELEPHONE_NUMBER"); + Assert.Equal("+1234567890", contactFlowEvent.Details.ContactData.SystemEndpoint.Address); + Assert.Equal("TELEPHONE_NUMBER", contactFlowEvent.Details.ContactData.SystemEndpoint.Type); Assert.NotNull(contactFlowEvent.Details.Parameters); - Assert.Equal(contactFlowEvent.Details.Parameters.Count, 1); - Assert.Equal(contactFlowEvent.Details.Parameters["sentAttributeKey"], "sentAttributeValue"); + Assert.Single(contactFlowEvent.Details.Parameters); + Assert.Equal("sentAttributeValue", contactFlowEvent.Details.Parameters["sentAttributeKey"]); } } [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void SimpleEmailTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -1578,67 +1508,65 @@ public void SimpleEmailTest(Type serializerType) { var sesEvent = serializer.Deserialize>(fileStream); - Assert.Equal(sesEvent.Records.Count, 1); + Assert.Single(sesEvent.Records); var record = sesEvent.Records[0]; - Assert.Equal(record.EventVersion, "1.0"); - Assert.Equal(record.EventSource, "aws:ses"); - - Assert.Equal(record.Ses.Mail.CommonHeaders.From.Count, 1); - Assert.Equal(record.Ses.Mail.CommonHeaders.From[0], "Amazon Web Services "); - Assert.Equal(record.Ses.Mail.CommonHeaders.To.Count, 1); - Assert.Equal(record.Ses.Mail.CommonHeaders.To[0], "lambda@amazon.com"); - Assert.Equal(record.Ses.Mail.CommonHeaders.ReturnPath, "aws@amazon.com"); - Assert.Equal(record.Ses.Mail.CommonHeaders.MessageId, ""); - Assert.Equal(record.Ses.Mail.CommonHeaders.Date, "Mon, 5 Dec 2016 18:40:08 -0800"); - Assert.Equal(record.Ses.Mail.CommonHeaders.Subject, "Test Subject"); - Assert.Equal(record.Ses.Mail.Source, "aws@amazon.com"); - Assert.Equal(record.Ses.Mail.Timestamp.ToUniversalTime(), DateTime.Parse("2016-12-06T02:40:08.000Z").ToUniversalTime()); - Assert.Equal(record.Ses.Mail.Destination.Count, 1); - Assert.Equal(record.Ses.Mail.Destination[0], "lambda@amazon.com"); - Assert.Equal(record.Ses.Mail.Headers.Count, 10); - Assert.Equal(record.Ses.Mail.Headers[0].Name, "Return-Path"); - Assert.Equal(record.Ses.Mail.Headers[0].Value, ""); - Assert.Equal(record.Ses.Mail.Headers[1].Name, "Received"); - Assert.Equal(record.Ses.Mail.Headers[1].Value, "from mx.amazon.com (mx.amazon.com [127.0.0.1]) by inbound-smtp.us-east-1.amazonaws.com with SMTP id 6n4thuhcbhpfiuf25gshf70rss364fuejrvmqko1 for lambda@amazon.com; Tue, 06 Dec 2016 02:40:10 +0000 (UTC)"); - Assert.Equal(record.Ses.Mail.Headers[2].Name, "DKIM-Signature"); - Assert.Equal(record.Ses.Mail.Headers[2].Value, "v=1; a=rsa-sha256; c=relaxed/relaxed; d=iatn.net; s=amazon; h=mime-version:from:date:message-id:subject:to; bh=chlJxa/vZ11+0O9lf4tKDM/CcPjup2nhhdITm+hSf3c=; b=SsoNPK0wX7umtWnw8pln3YSib+E09XO99d704QdSc1TR1HxM0OTti/UaFxVD4e5b0+okBqo3rgVeWgNZ0sWZEUhBaZwSL3kTd/nHkcPexeV0XZqEgms1vmbg75F6vlz9igWflO3GbXyTRBNMM0gUXKU/686hpVW6aryEIfM/rLY="); - Assert.Equal(record.Ses.Mail.Headers[3].Name, "MIME-Version"); - Assert.Equal(record.Ses.Mail.Headers[3].Value, "1.0"); - Assert.Equal(record.Ses.Mail.Headers[4].Name, "From"); - Assert.Equal(record.Ses.Mail.Headers[4].Value, "Amazon Web Services "); - Assert.Equal(record.Ses.Mail.Headers[5].Name, "Date"); - Assert.Equal(record.Ses.Mail.Headers[5].Value, "Mon, 5 Dec 2016 18:40:08 -0800"); - Assert.Equal(record.Ses.Mail.Headers[6].Name, "Message-ID"); - Assert.Equal(record.Ses.Mail.Headers[6].Value, ""); - Assert.Equal(record.Ses.Mail.Headers[7].Name, "Subject"); - Assert.Equal(record.Ses.Mail.Headers[7].Value, "Test Subject"); - Assert.Equal(record.Ses.Mail.Headers[8].Name, "To"); - Assert.Equal(record.Ses.Mail.Headers[8].Value, "lambda@amazon.com"); - Assert.Equal(record.Ses.Mail.Headers[9].Name, "Content-Type"); - Assert.Equal(record.Ses.Mail.Headers[9].Value, "multipart/alternative; boundary=94eb2c0742269658b10542f452a9"); - Assert.Equal(record.Ses.Mail.HeadersTruncated, false); - Assert.Equal(record.Ses.Mail.MessageId, "6n4thuhcbhpfiuf25gshf70rss364fuejrvmqko1"); - - Assert.Equal(record.Ses.Receipt.Recipients.Count, 1); - Assert.Equal(record.Ses.Receipt.Recipients[0], "lambda@amazon.com"); - Assert.Equal(record.Ses.Receipt.Timestamp.ToUniversalTime(), DateTime.Parse("2016-12-06T02:40:08.000Z").ToUniversalTime()); - Assert.Equal(record.Ses.Receipt.SpamVerdict.Status, "PASS"); - Assert.Equal(record.Ses.Receipt.DKIMVerdict.Status, "PASS"); - Assert.Equal(record.Ses.Receipt.SPFVerdict.Status, "PASS"); - Assert.Equal(record.Ses.Receipt.VirusVerdict.Status, "PASS"); - Assert.Equal(record.Ses.Receipt.DMARCVerdict.Status, "PASS"); - Assert.Equal(record.Ses.Receipt.ProcessingTimeMillis, 574); + Assert.Equal("1.0", record.EventVersion); + Assert.Equal("aws:ses", record.EventSource); + + Assert.Single(record.Ses.Mail.CommonHeaders.From); + Assert.Equal("Amazon Web Services ", record.Ses.Mail.CommonHeaders.From[0]); + Assert.Single(record.Ses.Mail.CommonHeaders.To); + Assert.Equal("lambda@amazon.com", record.Ses.Mail.CommonHeaders.To[0]); + Assert.Equal("aws@amazon.com", record.Ses.Mail.CommonHeaders.ReturnPath); + Assert.Equal("", record.Ses.Mail.CommonHeaders.MessageId); + Assert.Equal("Mon, 5 Dec 2016 18:40:08 -0800", record.Ses.Mail.CommonHeaders.Date); + Assert.Equal("Test Subject", record.Ses.Mail.CommonHeaders.Subject); + Assert.Equal("aws@amazon.com", record.Ses.Mail.Source); + Assert.Equal(DateTime.Parse("2016-12-06T02:40:08.000Z").ToUniversalTime(), record.Ses.Mail.Timestamp.ToUniversalTime()); + Assert.Single(record.Ses.Mail.Destination); + Assert.Equal("lambda@amazon.com", record.Ses.Mail.Destination[0]); + Assert.Equal(10, record.Ses.Mail.Headers.Count); + Assert.Equal("Return-Path", record.Ses.Mail.Headers[0].Name); + Assert.Equal("", record.Ses.Mail.Headers[0].Value); + Assert.Equal("Received", record.Ses.Mail.Headers[1].Name); + Assert.Equal("from mx.amazon.com (mx.amazon.com [127.0.0.1]) by inbound-smtp.us-east-1.amazonaws.com with SMTP id 6n4thuhcbhpfiuf25gshf70rss364fuejrvmqko1 for lambda@amazon.com; Tue, 06 Dec 2016 02:40:10 +0000 (UTC)", record.Ses.Mail.Headers[1].Value); + Assert.Equal("DKIM-Signature", record.Ses.Mail.Headers[2].Name); + Assert.Equal("v=1; a=rsa-sha256; c=relaxed/relaxed; d=iatn.net; s=amazon; h=mime-version:from:date:message-id:subject:to; bh=chlJxa/vZ11+0O9lf4tKDM/CcPjup2nhhdITm+hSf3c=; b=SsoNPK0wX7umtWnw8pln3YSib+E09XO99d704QdSc1TR1HxM0OTti/UaFxVD4e5b0+okBqo3rgVeWgNZ0sWZEUhBaZwSL3kTd/nHkcPexeV0XZqEgms1vmbg75F6vlz9igWflO3GbXyTRBNMM0gUXKU/686hpVW6aryEIfM/rLY=", record.Ses.Mail.Headers[2].Value); + Assert.Equal("MIME-Version", record.Ses.Mail.Headers[3].Name); + Assert.Equal("1.0", record.Ses.Mail.Headers[3].Value); + Assert.Equal("From", record.Ses.Mail.Headers[4].Name); + Assert.Equal("Amazon Web Services ", record.Ses.Mail.Headers[4].Value); + Assert.Equal("Date", record.Ses.Mail.Headers[5].Name); + Assert.Equal("Mon, 5 Dec 2016 18:40:08 -0800", record.Ses.Mail.Headers[5].Value); + Assert.Equal("Message-ID", record.Ses.Mail.Headers[6].Name); + Assert.Equal("", record.Ses.Mail.Headers[6].Value); + Assert.Equal("Subject", record.Ses.Mail.Headers[7].Name); + Assert.Equal("Test Subject", record.Ses.Mail.Headers[7].Value); + Assert.Equal("To", record.Ses.Mail.Headers[8].Name); + Assert.Equal("lambda@amazon.com", record.Ses.Mail.Headers[8].Value); + Assert.Equal("Content-Type", record.Ses.Mail.Headers[9].Name); + Assert.Equal("multipart/alternative; boundary=94eb2c0742269658b10542f452a9", record.Ses.Mail.Headers[9].Value); + Assert.False(record.Ses.Mail.HeadersTruncated); + Assert.Equal("6n4thuhcbhpfiuf25gshf70rss364fuejrvmqko1", record.Ses.Mail.MessageId); + + Assert.Single(record.Ses.Receipt.Recipients); + Assert.Equal("lambda@amazon.com", record.Ses.Receipt.Recipients[0]); + Assert.Equal(DateTime.Parse("2016-12-06T02:40:08.000Z").ToUniversalTime(), record.Ses.Receipt.Timestamp.ToUniversalTime()); + Assert.Equal("PASS", record.Ses.Receipt.SpamVerdict.Status); + Assert.Equal("PASS", record.Ses.Receipt.DKIMVerdict.Status); + Assert.Equal("PASS", record.Ses.Receipt.SPFVerdict.Status); + Assert.Equal("PASS", record.Ses.Receipt.VirusVerdict.Status); + Assert.Equal("PASS", record.Ses.Receipt.DMARCVerdict.Status); + Assert.Equal(574, record.Ses.Receipt.ProcessingTimeMillis); Handle(sesEvent); } } [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void SimpleEmailLambdaActionTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -1646,22 +1574,20 @@ public void SimpleEmailLambdaActionTest(Type serializerType) { var sesEvent = serializer.Deserialize>(fileStream); - Assert.Equal(sesEvent.Records.Count, 1); + Assert.Single(sesEvent.Records); var record = sesEvent.Records[0]; - Assert.Equal(record.Ses.Receipt.Action.Type, "Lambda"); - Assert.Equal(record.Ses.Receipt.Action.InvocationType, "Event"); - Assert.Equal(record.Ses.Receipt.Action.FunctionArn, "arn:aws:lambda:us-east-1:000000000000:function:my-ses-lambda-function"); + Assert.Equal("Lambda", record.Ses.Receipt.Action.Type); + Assert.Equal("Event", record.Ses.Receipt.Action.InvocationType); + Assert.Equal("arn:aws:lambda:us-east-1:000000000000:function:my-ses-lambda-function", record.Ses.Receipt.Action.FunctionArn); Handle(sesEvent); } } [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void SimpleEmailS3ActionTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -1669,14 +1595,14 @@ public void SimpleEmailS3ActionTest(Type serializerType) { var sesEvent = serializer.Deserialize>(fileStream); - Assert.Equal(sesEvent.Records.Count, 1); + Assert.Single(sesEvent.Records); var record = sesEvent.Records[0]; - Assert.Equal(record.Ses.Receipt.Action.Type, "S3"); - Assert.Equal(record.Ses.Receipt.Action.TopicArn, "arn:aws:sns:eu-west-1:123456789:ses-email-received"); - Assert.Equal(record.Ses.Receipt.Action.BucketName, "my-ses-inbox"); - Assert.Equal(record.Ses.Receipt.Action.ObjectKeyPrefix, "important"); - Assert.Equal(record.Ses.Receipt.Action.ObjectKey, "important/fiddlyfaddlyhiddlyhoodly"); + Assert.Equal("S3", record.Ses.Receipt.Action.Type); + Assert.Equal("arn:aws:sns:eu-west-1:123456789:ses-email-received", record.Ses.Receipt.Action.TopicArn); + Assert.Equal("my-ses-inbox", record.Ses.Receipt.Action.BucketName); + Assert.Equal("important", record.Ses.Receipt.Action.ObjectKeyPrefix); + Assert.Equal("important/fiddlyfaddlyhiddlyhoodly", record.Ses.Receipt.Action.ObjectKey); Handle(sesEvent); } @@ -1693,10 +1619,8 @@ private static void Handle(SimpleEmailEvent sesE [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void SNSTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -1704,27 +1628,27 @@ public void SNSTest(Type serializerType) { var snsEvent = serializer.Deserialize(fileStream); - Assert.Equal(snsEvent.Records.Count, 1); + Assert.Single(snsEvent.Records); var record = snsEvent.Records[0]; - Assert.Equal(record.EventVersion, "1.0"); - Assert.Equal(record.EventSubscriptionArn, "arn:aws:sns:EXAMPLE"); - Assert.Equal(record.EventSource, "aws:sns"); - Assert.Equal(record.Sns.SignatureVersion, "1"); - Assert.Equal(record.Sns.Timestamp.ToUniversalTime(), DateTime.Parse("1970-01-01T00:00:00.000Z").ToUniversalTime()); - Assert.Equal(record.Sns.Signature, "EXAMPLE"); - Assert.Equal(record.Sns.SigningCertUrl, "EXAMPLE"); - Assert.Equal(record.Sns.MessageId, "95df01b4-ee98-5cb9-9903-4c221d41eb5e"); - Assert.Equal(record.Sns.Message, "Hello from SNS!"); + Assert.Equal("1.0", record.EventVersion); + Assert.Equal("arn:aws:sns:EXAMPLE", record.EventSubscriptionArn); + Assert.Equal("aws:sns", record.EventSource); + Assert.Equal("1", record.Sns.SignatureVersion); + Assert.Equal(DateTime.Parse("1970-01-01T00:00:00.000Z").ToUniversalTime(), record.Sns.Timestamp.ToUniversalTime()); + Assert.Equal("EXAMPLE", record.Sns.Signature); + Assert.Equal("EXAMPLE", record.Sns.SigningCertUrl); + Assert.Equal("95df01b4-ee98-5cb9-9903-4c221d41eb5e", record.Sns.MessageId); + Assert.Equal("Hello from SNS!", record.Sns.Message); Assert.True(record.Sns.MessageAttributes.ContainsKey("Test")); - Assert.Equal(record.Sns.MessageAttributes["Test"].Type, "String"); - Assert.Equal(record.Sns.MessageAttributes["Test"].Value, "TestString"); + Assert.Equal("String", record.Sns.MessageAttributes["Test"].Type); + Assert.Equal("TestString", record.Sns.MessageAttributes["Test"].Value); Assert.True(record.Sns.MessageAttributes.ContainsKey("TestBinary")); - Assert.Equal(record.Sns.MessageAttributes["TestBinary"].Type, "Binary"); - Assert.Equal(record.Sns.MessageAttributes["TestBinary"].Value, "TestBinary"); - Assert.Equal(record.Sns.Type, "Notification"); - Assert.Equal(record.Sns.UnsubscribeUrl, "EXAMPLE"); - Assert.Equal(record.Sns.TopicArn, "arn:aws:sns:EXAMPLE"); - Assert.Equal(record.Sns.Subject, "TestInvoke"); + Assert.Equal("Binary", record.Sns.MessageAttributes["TestBinary"].Type); + Assert.Equal("TestBinary", record.Sns.MessageAttributes["TestBinary"].Value); + Assert.Equal("Notification", record.Sns.Type); + Assert.Equal("EXAMPLE", record.Sns.UnsubscribeUrl); + Assert.Equal("arn:aws:sns:EXAMPLE", record.Sns.TopicArn); + Assert.Equal("TestInvoke", record.Sns.Subject); Handle(snsEvent); } @@ -1741,10 +1665,8 @@ private static void Handle(SNSEvent snsEvent) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void SQSTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -1752,7 +1674,7 @@ public void SQSTest(Type serializerType) { var sqsEvent = serializer.Deserialize(fileStream); - Assert.Equal(sqsEvent.Records.Count, 1); + Assert.Single(sqsEvent.Records); var record = sqsEvent.Records[0]; Assert.Equal("MessageID", record.MessageId); Assert.Equal("MessageReceiptHandle", record.ReceiptHandle); @@ -1811,10 +1733,8 @@ private static void Handle(SQSEvent sqsEvent) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void SQSBatchResponseTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -1822,7 +1742,7 @@ public void SQSBatchResponseTest(Type serializerType) { var sqsBatchResponse = serializer.Deserialize(fileStream); - Assert.Equal(sqsBatchResponse.BatchItemFailures.Count, 2); + Assert.Equal(2, sqsBatchResponse.BatchItemFailures.Count); { var item1 = sqsBatchResponse.BatchItemFailures[0]; Assert.NotNull(item1); @@ -1848,10 +1768,8 @@ public void SQSBatchResponseTest(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void APIGatewayProxyRequestTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -1859,48 +1777,48 @@ public void APIGatewayProxyRequestTest(Type serializerType) { var proxyEvent = serializer.Deserialize(fileStream); - Assert.Equal(proxyEvent.Resource, "/{proxy+}"); - Assert.Equal(proxyEvent.Path, "/hello/world"); - Assert.Equal(proxyEvent.HttpMethod, "POST"); - Assert.Equal(proxyEvent.Body, "{\r\n\t\"a\": 1\r\n}"); + Assert.Equal("/{proxy+}", proxyEvent.Resource); + Assert.Equal("/hello/world", proxyEvent.Path); + Assert.Equal("POST", proxyEvent.HttpMethod); + Assert.Equal("{\r\n\t\"a\": 1\r\n}", proxyEvent.Body); var headers = proxyEvent.Headers; - Assert.Equal(headers["Accept"], "*/*"); - Assert.Equal(headers["Accept-Encoding"], "gzip, deflate"); - Assert.Equal(headers["cache-control"], "no-cache"); - Assert.Equal(headers["CloudFront-Forwarded-Proto"], "https"); + Assert.Equal("*/*", headers["Accept"]); + Assert.Equal("gzip, deflate", headers["Accept-Encoding"]); + Assert.Equal("no-cache", headers["cache-control"]); + Assert.Equal("https", headers["CloudFront-Forwarded-Proto"]); var queryStringParameters = proxyEvent.QueryStringParameters; - Assert.Equal(queryStringParameters["name"], "me"); + Assert.Equal("me", queryStringParameters["name"]); var pathParameters = proxyEvent.PathParameters; - Assert.Equal(pathParameters["proxy"], "hello/world"); + Assert.Equal("hello/world", pathParameters["proxy"]); var stageVariables = proxyEvent.StageVariables; - Assert.Equal(stageVariables["stageVariableName"], "stageVariableValue"); + Assert.Equal("stageVariableValue", stageVariables["stageVariableName"]); var requestContext = proxyEvent.RequestContext; - Assert.Equal(requestContext.AccountId, "12345678912"); - Assert.Equal(requestContext.ResourceId, "roq9wj"); - Assert.Equal(requestContext.Stage, "testStage"); - Assert.Equal(requestContext.RequestId, "deef4878-7910-11e6-8f14-25afc3e9ae33"); - Assert.Equal(requestContext.ConnectionId, "d034bc98-beed-4fdf-9e85-11bfc15bf734"); - Assert.Equal(requestContext.DomainName, "somerandomdomain.net"); + Assert.Equal("12345678912", requestContext.AccountId); + Assert.Equal("roq9wj", requestContext.ResourceId); + Assert.Equal("testStage", requestContext.Stage); + Assert.Equal("deef4878-7910-11e6-8f14-25afc3e9ae33", requestContext.RequestId); + Assert.Equal("d034bc98-beed-4fdf-9e85-11bfc15bf734", requestContext.ConnectionId); + Assert.Equal("somerandomdomain.net", requestContext.DomainName); Assert.Equal(1519166937665, requestContext.RequestTimeEpoch); Assert.Equal("20/Feb/2018:22:48:57 +0000", requestContext.RequestTime); var identity = requestContext.Identity; - Assert.Equal(identity.CognitoIdentityPoolId, "theCognitoIdentityPoolId"); - Assert.Equal(identity.AccountId, "theAccountId"); - Assert.Equal(identity.CognitoIdentityId, "theCognitoIdentityId"); - Assert.Equal(identity.Caller, "theCaller"); - Assert.Equal(identity.ApiKey, "theApiKey"); - Assert.Equal(identity.SourceIp, "192.168.196.186"); - Assert.Equal(identity.CognitoAuthenticationType, "theCognitoAuthenticationType"); - Assert.Equal(identity.CognitoAuthenticationProvider, "theCognitoAuthenticationProvider"); - Assert.Equal(identity.UserArn, "theUserArn"); - Assert.Equal(identity.UserAgent, "PostmanRuntime/2.4.5"); - Assert.Equal(identity.User, "theUser"); + Assert.Equal("theCognitoIdentityPoolId", identity.CognitoIdentityPoolId); + Assert.Equal("theAccountId", identity.AccountId); + Assert.Equal("theCognitoIdentityId", identity.CognitoIdentityId); + Assert.Equal("theCaller", identity.Caller); + Assert.Equal("theApiKey", identity.ApiKey); + Assert.Equal("192.168.196.186", identity.SourceIp); + Assert.Equal("theCognitoAuthenticationType", identity.CognitoAuthenticationType); + Assert.Equal("theCognitoAuthenticationProvider", identity.CognitoAuthenticationProvider); + Assert.Equal("theUserArn", identity.UserArn); + Assert.Equal("PostmanRuntime/2.4.5", identity.UserAgent); + Assert.Equal("theUser", identity.User); Assert.Equal("IAM_user_access_key", identity.AccessKey); var clientCert = identity.ClientCert; @@ -1932,10 +1850,8 @@ private static APIGatewayProxyResponse Handle(APIGatewayProxyRequest apigProxyEv [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void APIGatewayProxyResponseTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -1955,28 +1871,28 @@ public void APIGatewayProxyResponseTest(Type serializerType) serializedJson = Encoding.UTF8.GetString(stream.ToArray()); } - JObject root = Newtonsoft.Json.JsonConvert.DeserializeObject(serializedJson) as JObject; - Assert.Equal(root["statusCode"], 200); - Assert.Equal(root["body"], "theBody"); + var root = Newtonsoft.Json.JsonConvert.DeserializeObject(serializedJson) as JObject; + Assert.Equal(200, root["statusCode"]); + Assert.Equal("theBody", root["body"]); Assert.NotNull(root["headers"]); var headers = root["headers"] as JObject; - Assert.Equal(headers["Header1"], "Value1"); - Assert.Equal(headers["Header2"], "Value2"); + Assert.Equal("Value1", headers["Header1"]); + Assert.Equal("Value2", headers["Header2"]); } [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void APIGatewayAuthorizerResponseTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; - var context = new APIGatewayCustomAuthorizerContextOutput(); - context["field1"] = "value1"; - context["field2"] = "value2"; + var context = new APIGatewayCustomAuthorizerContextOutput + { + ["field1"] = "value1", + ["field2"] = "value2" + }; var response = new APIGatewayCustomAuthorizerResponse { @@ -1990,7 +1906,7 @@ public void APIGatewayAuthorizerResponseTest(Type serializerType) { new APIGatewayCustomAuthorizerPolicy.IAMPolicyStatement { - Action = new HashSet{ "execute-api:Invoke" }, + Action = ["execute-api:Invoke"], Effect = "Allow", Resource = new HashSet{ "*" } } @@ -2007,7 +1923,7 @@ public void APIGatewayAuthorizerResponseTest(Type serializerType) serializedJson = Encoding.UTF8.GetString(stream.ToArray()); } - JObject root = Newtonsoft.Json.JsonConvert.DeserializeObject(serializedJson) as JObject; + var root = Newtonsoft.Json.JsonConvert.DeserializeObject(serializedJson) as JObject; Assert.Equal("prin1", root["principalId"]); Assert.Equal("usageKey", root["usageIdentifierKey"]); @@ -2023,16 +1939,16 @@ public void APIGatewayAuthorizerResponseTest(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void APIGatewayAuthorizerWithSimpleIAMConditionResponseTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; - var context = new APIGatewayCustomAuthorizerContextOutput(); - context["field1"] = "value1"; - context["field2"] = "value2"; + var context = new APIGatewayCustomAuthorizerContextOutput + { + ["field1"] = "value1", + ["field2"] = "value2" + }; var response = new APIGatewayCustomAuthorizerResponse { @@ -2042,8 +1958,8 @@ public void APIGatewayAuthorizerWithSimpleIAMConditionResponseTest(Type serializ PolicyDocument = new APIGatewayCustomAuthorizerPolicy { Version = "2012-10-17", - Statement = new List - { + Statement = + [ new APIGatewayCustomAuthorizerPolicy.IAMPolicyStatement { Action = new HashSet{ "execute-api:Invoke" }, @@ -2058,7 +1974,7 @@ public void APIGatewayAuthorizerWithSimpleIAMConditionResponseTest(Type serializ } } } - } + ] } }; @@ -2071,7 +1987,7 @@ public void APIGatewayAuthorizerWithSimpleIAMConditionResponseTest(Type serializ serializedJson = Encoding.UTF8.GetString(stream.ToArray()); } - JObject root = Newtonsoft.Json.JsonConvert.DeserializeObject(serializedJson) as JObject; + var root = Newtonsoft.Json.JsonConvert.DeserializeObject(serializedJson) as JObject; Assert.Equal("prin1", root["principalId"]); Assert.Equal("usageKey", root["usageIdentifierKey"]); @@ -2087,16 +2003,16 @@ public void APIGatewayAuthorizerWithSimpleIAMConditionResponseTest(Type serializ [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void APIGatewayAuthorizerWithMultiValueIAMConditionResponseTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; - var context = new APIGatewayCustomAuthorizerContextOutput(); - context["field1"] = "value1"; - context["field2"] = "value2"; + var context = new APIGatewayCustomAuthorizerContextOutput + { + ["field1"] = "value1", + ["field2"] = "value2" + }; var response = new APIGatewayCustomAuthorizerResponse { @@ -2106,8 +2022,8 @@ public void APIGatewayAuthorizerWithMultiValueIAMConditionResponseTest(Type seri PolicyDocument = new APIGatewayCustomAuthorizerPolicy { Version = "2012-10-17", - Statement = new List - { + Statement = + [ new APIGatewayCustomAuthorizerPolicy.IAMPolicyStatement { Action = new HashSet{ "execute-api:Invoke" }, @@ -2132,7 +2048,7 @@ public void APIGatewayAuthorizerWithMultiValueIAMConditionResponseTest(Type seri } } } - } + ] } }; @@ -2145,7 +2061,7 @@ public void APIGatewayAuthorizerWithMultiValueIAMConditionResponseTest(Type seri serializedJson = Encoding.UTF8.GetString(stream.ToArray()); } - JObject root = Newtonsoft.Json.JsonConvert.DeserializeObject(serializedJson) as JObject; + var root = Newtonsoft.Json.JsonConvert.DeserializeObject(serializedJson) as JObject; Assert.Equal("prin1", root["principalId"]); Assert.Equal("usageKey", root["usageIdentifierKey"]); @@ -2170,16 +2086,16 @@ public void APIGatewayAuthorizerWithMultiValueIAMConditionResponseTest(Type seri [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void APIGatewayAuthorizerResponseNotResourceTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; - var context = new APIGatewayCustomAuthorizerContextOutput(); - context["field1"] = "value1"; - context["field2"] = "value2"; + var context = new APIGatewayCustomAuthorizerContextOutput + { + ["field1"] = "value1", + ["field2"] = "value2" + }; var response = new APIGatewayCustomAuthorizerResponse { @@ -2189,19 +2105,19 @@ public void APIGatewayAuthorizerResponseNotResourceTest(Type serializerType) PolicyDocument = new APIGatewayCustomAuthorizerPolicy { Version = "2012-10-17", - Statement = new List - { + Statement = + [ new APIGatewayCustomAuthorizerPolicy.IAMPolicyStatement { Action = new HashSet{ "execute-api:Invoke" }, Effect = "Deny", - NotResource = new HashSet - { + NotResource = + [ "arn:aws:execute-api:us-east-1:1234567890:abcdef1234/Prod/GET/resource1", "arn:aws:execute-api:us-east-1:1234567890:abcdef1234/Prod/GET/resource2" - } + ] } - } + ] } }; @@ -2214,7 +2130,7 @@ public void APIGatewayAuthorizerResponseNotResourceTest(Type serializerType) serializedJson = Encoding.UTF8.GetString(stream.ToArray()); } - JObject root = Newtonsoft.Json.JsonConvert.DeserializeObject(serializedJson) as JObject; + var root = Newtonsoft.Json.JsonConvert.DeserializeObject(serializedJson) as JObject; Assert.Equal("prin1", root["principalId"]); Assert.Equal("usageKey", root["usageIdentifierKey"]); @@ -2236,10 +2152,8 @@ public void APIGatewayAuthorizerResponseNotResourceTest(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void WebSocketApiConnectTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -2253,63 +2167,61 @@ public void WebSocketApiConnectTest(Type serializerType) Assert.Null(proxyEvent.Body); var headers = proxyEvent.Headers; - Assert.Equal(headers["HeaderAuth1"], "headerValue1"); - Assert.Equal(headers["Host"], "lg10ltpf4f.execute-api.us-east-2.amazonaws.com"); - Assert.Equal(headers["Sec-WebSocket-Extensions"], "permessage-deflate; client_max_window_bits"); - Assert.Equal(headers["Sec-WebSocket-Key"], "BvlrrFKoKAPDYOlwBcGKWw=="); - Assert.Equal(headers["Sec-WebSocket-Version"], "13"); - Assert.Equal(headers["X-Amzn-Trace-Id"], "Root=1-625d9ad1-37a5d33a61dd9be33ae3a247"); - Assert.Equal(headers["X-Forwarded-For"], "52.95.4.0"); - Assert.Equal(headers["X-Forwarded-Port"], "443"); - Assert.Equal(headers["X-Forwarded-Proto"], "https"); + Assert.Equal("headerValue1", headers["HeaderAuth1"]); + Assert.Equal("lg10ltpf4f.execute-api.us-east-2.amazonaws.com", headers["Host"]); + Assert.Equal("permessage-deflate; client_max_window_bits", headers["Sec-WebSocket-Extensions"]); + Assert.Equal("BvlrrFKoKAPDYOlwBcGKWw==", headers["Sec-WebSocket-Key"]); + Assert.Equal("13", headers["Sec-WebSocket-Version"]); + Assert.Equal("Root=1-625d9ad1-37a5d33a61dd9be33ae3a247", headers["X-Amzn-Trace-Id"]); + Assert.Equal("52.95.4.0", headers["X-Forwarded-For"]); + Assert.Equal("443", headers["X-Forwarded-Port"]); + Assert.Equal("https", headers["X-Forwarded-Proto"]); var multiValueHeaders = proxyEvent.MultiValueHeaders; - Assert.Equal(multiValueHeaders["HeaderAuth1"].Count, 1); - Assert.Equal(multiValueHeaders["HeaderAuth1"][0], "headerValue1"); - Assert.Equal(multiValueHeaders["Host"].Count, 1); - Assert.Equal(multiValueHeaders["Host"][0], "lg10ltpf4f.execute-api.us-east-2.amazonaws.com"); - Assert.Equal(multiValueHeaders["Sec-WebSocket-Extensions"].Count, 1); - Assert.Equal(multiValueHeaders["Sec-WebSocket-Extensions"][0], "permessage-deflate; client_max_window_bits"); - Assert.Equal(multiValueHeaders["Sec-WebSocket-Key"].Count, 1); - Assert.Equal(multiValueHeaders["Sec-WebSocket-Key"][0], "BvlrrFKoKAPDYOlwBcGKWw=="); - Assert.Equal(multiValueHeaders["Sec-WebSocket-Version"].Count, 1); - Assert.Equal(multiValueHeaders["Sec-WebSocket-Version"][0], "13"); - Assert.Equal(multiValueHeaders["X-Amzn-Trace-Id"].Count, 1); - Assert.Equal(multiValueHeaders["X-Amzn-Trace-Id"][0], "Root=1-625d9ad1-37a5d33a61dd9be33ae3a247"); - Assert.Equal(multiValueHeaders["X-Forwarded-For"].Count, 1); - Assert.Equal(multiValueHeaders["X-Forwarded-For"][0], "52.95.4.0"); - Assert.Equal(multiValueHeaders["X-Forwarded-Port"].Count, 1); - Assert.Equal(multiValueHeaders["X-Forwarded-Port"][0], "443"); - Assert.Equal(multiValueHeaders["X-Forwarded-Proto"].Count, 1); - Assert.Equal(multiValueHeaders["X-Forwarded-Proto"][0], "https"); + Assert.Single(multiValueHeaders["HeaderAuth1"]); + Assert.Equal("headerValue1", multiValueHeaders["HeaderAuth1"][0]); + Assert.Single(multiValueHeaders["Host"]); + Assert.Equal("lg10ltpf4f.execute-api.us-east-2.amazonaws.com", multiValueHeaders["Host"][0]); + Assert.Single(multiValueHeaders["Sec-WebSocket-Extensions"]); + Assert.Equal("permessage-deflate; client_max_window_bits", multiValueHeaders["Sec-WebSocket-Extensions"][0]); + Assert.Single(multiValueHeaders["Sec-WebSocket-Key"]); + Assert.Equal("BvlrrFKoKAPDYOlwBcGKWw==", multiValueHeaders["Sec-WebSocket-Key"][0]); + Assert.Single(multiValueHeaders["Sec-WebSocket-Version"]); + Assert.Equal("13", multiValueHeaders["Sec-WebSocket-Version"][0]); + Assert.Single(multiValueHeaders["X-Amzn-Trace-Id"]); + Assert.Equal("Root=1-625d9ad1-37a5d33a61dd9be33ae3a247", multiValueHeaders["X-Amzn-Trace-Id"][0]); + Assert.Single(multiValueHeaders["X-Forwarded-For"]); + Assert.Equal("52.95.4.0", multiValueHeaders["X-Forwarded-For"][0]); + Assert.Single(multiValueHeaders["X-Forwarded-Port"]); + Assert.Equal("443", multiValueHeaders["X-Forwarded-Port"][0]); + Assert.Single(multiValueHeaders["X-Forwarded-Proto"]); + Assert.Equal("https", multiValueHeaders["X-Forwarded-Proto"][0]); var requestContext = proxyEvent.RequestContext; - Assert.Equal(requestContext.RouteKey, "$connect"); - Assert.Equal(requestContext.EventType, "CONNECT"); - Assert.Equal(requestContext.ExtendedRequestId, "QyUg1HJgCYcFvbw="); - Assert.Equal(requestContext.RequestTime, "18/Apr/2022:17:07:29 +0000"); - Assert.Equal(requestContext.MessageDirection, "IN"); - Assert.Equal(requestContext.Stage, "production"); - Assert.Equal(requestContext.ConnectedAt, 1650301649973); - Assert.Equal(requestContext.RequestTimeEpoch, 1650301649973); - Assert.Equal(requestContext.RequestId, "QyUg1HJgCYcFvbw="); - Assert.Equal(requestContext.DomainName, "lg10ltpf4f.execute-api.us-east-2.amazonaws.com"); - Assert.Equal(requestContext.ConnectionId, "QyUg1czHCYcCHXw="); - Assert.Equal(requestContext.ApiId, "lg10ltpf4f"); + Assert.Equal("$connect", requestContext.RouteKey); + Assert.Equal("CONNECT", requestContext.EventType); + Assert.Equal("QyUg1HJgCYcFvbw=", requestContext.ExtendedRequestId); + Assert.Equal("18/Apr/2022:17:07:29 +0000", requestContext.RequestTime); + Assert.Equal("IN", requestContext.MessageDirection); + Assert.Equal("production", requestContext.Stage); + Assert.Equal(1650301649973, requestContext.ConnectedAt); + Assert.Equal(1650301649973, requestContext.RequestTimeEpoch); + Assert.Equal("QyUg1HJgCYcFvbw=", requestContext.RequestId); + Assert.Equal("lg10ltpf4f.execute-api.us-east-2.amazonaws.com", requestContext.DomainName); + Assert.Equal("QyUg1czHCYcCHXw=", requestContext.ConnectionId); + Assert.Equal("lg10ltpf4f", requestContext.ApiId); Assert.False(proxyEvent.IsBase64Encoded); var identity = requestContext.Identity; - Assert.Equal(identity.SourceIp, "52.95.4.0"); + Assert.Equal("52.95.4.0", identity.SourceIp); } } [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void ApplicationLoadBalancerRequestSingleValueTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -2317,9 +2229,9 @@ public void ApplicationLoadBalancerRequestSingleValueTest(Type serializerType) { var evnt = serializer.Deserialize(fileStream); - Assert.Equal(evnt.Path, "/"); - Assert.Equal(evnt.HttpMethod, "GET"); - Assert.Equal(evnt.Body, "not really base64"); + Assert.Equal("/", evnt.Path); + Assert.Equal("GET", evnt.HttpMethod); + Assert.Equal("not really base64", evnt.Body); Assert.True(evnt.IsBase64Encoded); Assert.Equal(2, evnt.QueryStringParameters.Count); @@ -2331,17 +2243,15 @@ public void ApplicationLoadBalancerRequestSingleValueTest(Type serializerType) var requestContext = evnt.RequestContext; - Assert.Equal(requestContext.Elb.TargetGroupArn, "arn:aws:elasticloadbalancing:region:123456789012:targetgroup/my-target-group/6d0ecf831eec9f09"); + Assert.Equal("arn:aws:elasticloadbalancing:region:123456789012:targetgroup/my-target-group/6d0ecf831eec9f09", requestContext.Elb.TargetGroupArn); } } [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void ApplicationLoadBalancerRequestMultiValueTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -2349,9 +2259,9 @@ public void ApplicationLoadBalancerRequestMultiValueTest(Type serializerType) { var evnt = serializer.Deserialize(fileStream); - Assert.Equal(evnt.Path, "/"); - Assert.Equal(evnt.HttpMethod, "GET"); - Assert.Equal(evnt.Body, "not really base64"); + Assert.Equal("/", evnt.Path); + Assert.Equal("GET", evnt.HttpMethod); + Assert.Equal("not really base64", evnt.Body); Assert.True(evnt.IsBase64Encoded); Assert.Equal(2, evnt.MultiValueQueryStringParameters.Count); @@ -2372,17 +2282,15 @@ public void ApplicationLoadBalancerRequestMultiValueTest(Type serializerType) var requestContext = evnt.RequestContext; - Assert.Equal(requestContext.Elb.TargetGroupArn, "arn:aws:elasticloadbalancing:region:123456789012:targetgroup/my-target-group/6d0ecf831eec9f09"); + Assert.Equal("arn:aws:elasticloadbalancing:region:123456789012:targetgroup/my-target-group/6d0ecf831eec9f09", requestContext.Elb.TargetGroupArn); } } [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void ApplicationLoadBalancerSingleHeaderResponseTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -2409,7 +2317,7 @@ public void ApplicationLoadBalancerSingleHeaderResponseTest(Type serializerType) serializedJson = Encoding.UTF8.GetString(stream.ToArray()); } - JObject root = Newtonsoft.Json.JsonConvert.DeserializeObject(serializedJson) as JObject; + var root = Newtonsoft.Json.JsonConvert.DeserializeObject(serializedJson) as JObject; Assert.Equal("h1-value1", root["headers"]["Head1"]); Assert.Equal("h2-value1", root["headers"]["Head2"]); @@ -2422,10 +2330,8 @@ public void ApplicationLoadBalancerSingleHeaderResponseTest(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void ApplicationLoadBalancerMultiHeaderResponseTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -2453,7 +2359,7 @@ public void ApplicationLoadBalancerMultiHeaderResponseTest(Type serializerType) JObject root = Newtonsoft.Json.JsonConvert.DeserializeObject(serializedJson) as JObject; - Assert.Equal(1, root["multiValueHeaders"]["Head1"].Count()); + Assert.Single(root["multiValueHeaders"]["Head1"]); Assert.Equal("h1-value1", root["multiValueHeaders"]["Head1"].First()); Assert.Equal(2, root["multiValueHeaders"]["Head2"].Count()); @@ -2469,10 +2375,8 @@ public void ApplicationLoadBalancerMultiHeaderResponseTest(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void LexEvent(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -2536,10 +2440,8 @@ public void LexEvent(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void LexResponse(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -2562,12 +2464,12 @@ public void LexResponse(Type serializerType) Assert.Equal("slot-name", lexResponse.DialogAction.SlotToElicit); Assert.Equal(3, lexResponse.DialogAction.ResponseCard.Version); Assert.Equal("application/vnd.amazonaws.card.generic", lexResponse.DialogAction.ResponseCard.ContentType); - Assert.Equal(1, lexResponse.DialogAction.ResponseCard.GenericAttachments.Count); + Assert.Single(lexResponse.DialogAction.ResponseCard.GenericAttachments); Assert.Equal("card-title", lexResponse.DialogAction.ResponseCard.GenericAttachments[0].Title); Assert.Equal("card-sub-title", lexResponse.DialogAction.ResponseCard.GenericAttachments[0].SubTitle); Assert.Equal("URL of the image to be shown", lexResponse.DialogAction.ResponseCard.GenericAttachments[0].ImageUrl); Assert.Equal("URL of the attachment to be associated with the card", lexResponse.DialogAction.ResponseCard.GenericAttachments[0].AttachmentLinkUrl); - Assert.Equal(1, lexResponse.DialogAction.ResponseCard.GenericAttachments[0].Buttons.Count); + Assert.Single(lexResponse.DialogAction.ResponseCard.GenericAttachments[0].Buttons); Assert.Equal("button-text", lexResponse.DialogAction.ResponseCard.GenericAttachments[0].Buttons[0].Text); Assert.Equal("value sent to server on button click", lexResponse.DialogAction.ResponseCard.GenericAttachments[0].Buttons[0].Value); @@ -2624,13 +2526,13 @@ public void LexV2Event(Type serializerType) Assert.Equal("List", lexV2Event.Interpretations[0].Intent.Slots["ActionType"].Shape); Assert.Equal("Action Value", lexV2Event.Interpretations[0].Intent.Slots["ActionType"].Value.OriginalValue); Assert.Equal("Action Value", lexV2Event.Interpretations[0].Intent.Slots["ActionType"].Value.InterpretedValue); - Assert.Equal(1, lexV2Event.Interpretations[0].Intent.Slots["ActionType"].Value.ResolvedValues.Count); + Assert.Single(lexV2Event.Interpretations[0].Intent.Slots["ActionType"].Value.ResolvedValues); Assert.Equal("action value", lexV2Event.Interpretations[0].Intent.Slots["ActionType"].Value.ResolvedValues[0]); - Assert.Equal(1, lexV2Event.Interpretations[0].Intent.Slots["ActionType"].Values.Count); + Assert.Single(lexV2Event.Interpretations[0].Intent.Slots["ActionType"].Values); Assert.Equal("Scalar", lexV2Event.Interpretations[0].Intent.Slots["ActionType"].Values[0].Shape); Assert.Equal("Action Value", lexV2Event.Interpretations[0].Intent.Slots["ActionType"].Values[0].Value.OriginalValue); Assert.Equal("Action Value", lexV2Event.Interpretations[0].Intent.Slots["ActionType"].Values[0].Value.InterpretedValue); - Assert.Equal(1, lexV2Event.Interpretations[0].Intent.Slots["ActionType"].Values[0].Value.ResolvedValues.Count); + Assert.Single(lexV2Event.Interpretations[0].Intent.Slots["ActionType"].Values[0].Value.ResolvedValues); Assert.Equal("action value", lexV2Event.Interpretations[0].Intent.Slots["ActionType"].Values[0].Value.ResolvedValues[0]); Assert.Null(lexV2Event.Interpretations[0].Intent.Slots["ActionType"].Values[0].Values); Assert.Null(lexV2Event.Interpretations[0].Intent.Slots["ActionDate"]); @@ -2644,20 +2546,20 @@ public void LexV2Event(Type serializerType) Assert.Equal(0.5, lexV2Event.Interpretations[0].SentimentResponse.SentimentScore.Neutral); Assert.Equal(0.9, lexV2Event.Interpretations[0].SentimentResponse.SentimentScore.Positive); Assert.Equal("FallbackIntent", lexV2Event.Interpretations[1].Intent.Name); - Assert.Equal(0, lexV2Event.Interpretations[1].Intent.Slots.Count); + Assert.Empty(lexV2Event.Interpretations[1].Intent.Slots); Assert.Equal("ActionDate", lexV2Event.ProposedNextState.DialogAction.SlotToElicit); Assert.Equal("ConfirmIntent", lexV2Event.ProposedNextState.DialogAction.Type); Assert.Equal("NextIntent", lexV2Event.ProposedNextState.Intent.Name); Assert.Equal("None", lexV2Event.ProposedNextState.Intent.ConfirmationState); - Assert.Equal(0, lexV2Event.ProposedNextState.Intent.Slots.Count); + Assert.Empty(lexV2Event.ProposedNextState.Intent.Slots); Assert.Equal("Waiting", lexV2Event.ProposedNextState.Intent.State); Assert.Equal(2, lexV2Event.RequestAttributes.Count); Assert.Equal("value1", lexV2Event.RequestAttributes["key1"]); Assert.Equal("value2", lexV2Event.RequestAttributes["key2"]); - Assert.Equal(1, lexV2Event.SessionState.ActiveContexts.Count); + Assert.Single(lexV2Event.SessionState.ActiveContexts); Assert.Equal(2, lexV2Event.SessionState.ActiveContexts[0].ContextAttributes.Count); Assert.Equal("contextattributevalue1", lexV2Event.SessionState.ActiveContexts[0].ContextAttributes["contextattribute1"]); Assert.Equal("contextattributevalue2", lexV2Event.SessionState.ActiveContexts[0].ContextAttributes["contextattribute2"]); @@ -2671,13 +2573,13 @@ public void LexV2Event(Type serializerType) Assert.Equal("List", lexV2Event.SessionState.Intent.Slots["ActionType"].Shape); Assert.Equal("Action Value", lexV2Event.SessionState.Intent.Slots["ActionType"].Value.OriginalValue); Assert.Equal("Action Value", lexV2Event.SessionState.Intent.Slots["ActionType"].Value.InterpretedValue); - Assert.Equal(1, lexV2Event.SessionState.Intent.Slots["ActionType"].Value.ResolvedValues.Count); + Assert.Single(lexV2Event.SessionState.Intent.Slots["ActionType"].Value.ResolvedValues); Assert.Equal("action value", lexV2Event.SessionState.Intent.Slots["ActionType"].Value.ResolvedValues[0]); - Assert.Equal(1, lexV2Event.SessionState.Intent.Slots["ActionType"].Values.Count); + Assert.Single(lexV2Event.SessionState.Intent.Slots["ActionType"].Values); Assert.Equal("Scalar", lexV2Event.SessionState.Intent.Slots["ActionType"].Values[0].Shape); Assert.Equal("Action Value", lexV2Event.SessionState.Intent.Slots["ActionType"].Values[0].Value.OriginalValue); Assert.Equal("Action Value", lexV2Event.SessionState.Intent.Slots["ActionType"].Values[0].Value.InterpretedValue); - Assert.Equal(1, lexV2Event.SessionState.Intent.Slots["ActionType"].Values[0].Value.ResolvedValues.Count); + Assert.Single(lexV2Event.SessionState.Intent.Slots["ActionType"].Values[0].Value.ResolvedValues); Assert.Equal("action value", lexV2Event.SessionState.Intent.Slots["ActionType"].Values[0].Value.ResolvedValues[0]); Assert.Null(lexV2Event.SessionState.Intent.Slots["ActionType"].Values[0].Values); Assert.Null(lexV2Event.SessionState.Intent.Slots["ActionDate"]); @@ -2685,8 +2587,8 @@ public void LexV2Event(Type serializerType) Assert.Equal("InProgress", lexV2Event.SessionState.Intent.State); Assert.Equal("None", lexV2Event.SessionState.Intent.ConfirmationState); Assert.Equal("85f22c97-b5d3-4a74-9e3d-95446768ecaa", lexV2Event.SessionState.OriginatingRequestId); - Assert.Equal(1, lexV2Event.SessionState.RuntimeHints.SlotHints.Count); - Assert.Equal(1, lexV2Event.SessionState.RuntimeHints.SlotHints["hint1"].Count); + Assert.Single(lexV2Event.SessionState.RuntimeHints.SlotHints); + Assert.Single(lexV2Event.SessionState.RuntimeHints.SlotHints["hint1"]); Assert.Equal(2, lexV2Event.SessionState.RuntimeHints.SlotHints["hint1"]["detail1"].RuntimeHintValues.Count); Assert.Equal("hintvalue1_1", lexV2Event.SessionState.RuntimeHints.SlotHints["hint1"]["detail1"].RuntimeHintValues[0].Phrase); Assert.Equal("hintvalue1_2", lexV2Event.SessionState.RuntimeHints.SlotHints["hint1"]["detail1"].RuntimeHintValues[1].Phrase); @@ -2694,21 +2596,21 @@ public void LexV2Event(Type serializerType) Assert.Equal("sessionvalue1", lexV2Event.SessionState.SessionAttributes["sessionattribute1"]); Assert.Equal("sessionvalue2", lexV2Event.SessionState.SessionAttributes["sessionattribute2"]); - Assert.Equal(1, lexV2Event.Transcriptions.Count); + Assert.Single(lexV2Event.Transcriptions); Assert.Equal("testtranscription", lexV2Event.Transcriptions[0].Transcription); Assert.Equal(0.8, lexV2Event.Transcriptions[0].TranscriptionConfidence); Assert.Equal("TestAction", lexV2Event.Transcriptions[0].ResolvedContext.Intent); - Assert.Equal(1, lexV2Event.Transcriptions[0].ResolvedSlots.Count); + Assert.Single(lexV2Event.Transcriptions[0].ResolvedSlots); Assert.Equal("List", lexV2Event.Transcriptions[0].ResolvedSlots["ActionType"].Shape); Assert.Equal("Action Value", lexV2Event.Transcriptions[0].ResolvedSlots["ActionType"].Value.OriginalValue); Assert.Equal("Action Value", lexV2Event.Transcriptions[0].ResolvedSlots["ActionType"].Value.InterpretedValue); - Assert.Equal(1, lexV2Event.Transcriptions[0].ResolvedSlots["ActionType"].Value.ResolvedValues.Count); + Assert.Single(lexV2Event.Transcriptions[0].ResolvedSlots["ActionType"].Value.ResolvedValues); Assert.Equal("action value", lexV2Event.Transcriptions[0].ResolvedSlots["ActionType"].Value.ResolvedValues[0]); - Assert.Equal(1, lexV2Event.Transcriptions[0].ResolvedSlots["ActionType"].Values.Count); + Assert.Single(lexV2Event.Transcriptions[0].ResolvedSlots["ActionType"].Values); Assert.Equal("Scalar", lexV2Event.Transcriptions[0].ResolvedSlots["ActionType"].Values[0].Shape); Assert.Equal("Action Value", lexV2Event.Transcriptions[0].ResolvedSlots["ActionType"].Values[0].Value.OriginalValue); Assert.Equal("Action Value", lexV2Event.Transcriptions[0].ResolvedSlots["ActionType"].Values[0].Value.InterpretedValue); - Assert.Equal(1, lexV2Event.Transcriptions[0].ResolvedSlots["ActionType"].Values[0].Value.ResolvedValues.Count); + Assert.Single(lexV2Event.Transcriptions[0].ResolvedSlots["ActionType"].Values[0].Value.ResolvedValues); Assert.Equal("action value", lexV2Event.Transcriptions[0].ResolvedSlots["ActionType"].Values[0].Value.ResolvedValues[0]); Assert.Null(lexV2Event.Transcriptions[0].ResolvedSlots["ActionType"].Values[0].Values); } @@ -2725,16 +2627,16 @@ public void LexV2Response(Type serializerType) { var lexV2Response = serializer.Deserialize(fileStream); - Assert.Equal(1, lexV2Response.Messages.Count); + Assert.Single(lexV2Response.Messages); Assert.Equal("Test Content", lexV2Response.Messages[0].Content); Assert.Equal("ImageResponseCard", lexV2Response.Messages[0].ContentType); - Assert.Equal(1, lexV2Response.Messages[0].ImageResponseCard.Buttons.Count); + Assert.Single(lexV2Response.Messages[0].ImageResponseCard.Buttons); Assert.Equal("Take Action", lexV2Response.Messages[0].ImageResponseCard.Buttons[0].Text); Assert.Equal("takeaction", lexV2Response.Messages[0].ImageResponseCard.Buttons[0].Value); Assert.Equal("http://somedomain.com/testimage.png", lexV2Response.Messages[0].ImageResponseCard.ImageUrl); Assert.Equal("Click button to take action", lexV2Response.Messages[0].ImageResponseCard.Subtitle); Assert.Equal("Take Action", lexV2Response.Messages[0].ImageResponseCard.Title); - Assert.Equal(1, lexV2Response.SessionState.ActiveContexts.Count); + Assert.Single(lexV2Response.SessionState.ActiveContexts); Assert.Equal(2, lexV2Response.SessionState.ActiveContexts[0].ContextAttributes.Count); Assert.Equal("contextattributevalue1", lexV2Response.SessionState.ActiveContexts[0].ContextAttributes["contextattribute1"]); Assert.Equal("contextattributevalue2", lexV2Response.SessionState.ActiveContexts[0].ContextAttributes["contextattribute2"]); @@ -2748,13 +2650,13 @@ public void LexV2Response(Type serializerType) Assert.Equal("List", lexV2Response.SessionState.Intent.Slots["ActionType"].Shape); Assert.Equal("Action Value", lexV2Response.SessionState.Intent.Slots["ActionType"].Value.OriginalValue); Assert.Equal("Action Value", lexV2Response.SessionState.Intent.Slots["ActionType"].Value.InterpretedValue); - Assert.Equal(1, lexV2Response.SessionState.Intent.Slots["ActionType"].Value.ResolvedValues.Count); + Assert.Single(lexV2Response.SessionState.Intent.Slots["ActionType"].Value.ResolvedValues); Assert.Equal("action value", lexV2Response.SessionState.Intent.Slots["ActionType"].Value.ResolvedValues[0]); - Assert.Equal(1, lexV2Response.SessionState.Intent.Slots["ActionType"].Values.Count); + Assert.Single(lexV2Response.SessionState.Intent.Slots["ActionType"].Values); Assert.Equal("Scalar", lexV2Response.SessionState.Intent.Slots["ActionType"].Values[0].Shape); Assert.Equal("Action Value", lexV2Response.SessionState.Intent.Slots["ActionType"].Values[0].Value.OriginalValue); Assert.Equal("Action Value", lexV2Response.SessionState.Intent.Slots["ActionType"].Values[0].Value.InterpretedValue); - Assert.Equal(1, lexV2Response.SessionState.Intent.Slots["ActionType"].Values[0].Value.ResolvedValues.Count); + Assert.Single(lexV2Response.SessionState.Intent.Slots["ActionType"].Values[0].Value.ResolvedValues); Assert.Equal("action value", lexV2Response.SessionState.Intent.Slots["ActionType"].Values[0].Value.ResolvedValues[0]); Assert.Null(lexV2Response.SessionState.Intent.Slots["ActionType"].Values[0].Values); Assert.Null(lexV2Response.SessionState.Intent.Slots["ActionDate"]); @@ -2762,8 +2664,8 @@ public void LexV2Response(Type serializerType) Assert.Equal("InProgress", lexV2Response.SessionState.Intent.State); Assert.Equal("None", lexV2Response.SessionState.Intent.ConfirmationState); Assert.Equal("85f22c97-b5d3-4a74-9e3d-95446768ecaa", lexV2Response.SessionState.OriginatingRequestId); - Assert.Equal(1, lexV2Response.SessionState.RuntimeHints.SlotHints.Count); - Assert.Equal(1, lexV2Response.SessionState.RuntimeHints.SlotHints["hint1"].Count); + Assert.Single(lexV2Response.SessionState.RuntimeHints.SlotHints); + Assert.Single(lexV2Response.SessionState.RuntimeHints.SlotHints["hint1"]); Assert.Equal(2, lexV2Response.SessionState.RuntimeHints.SlotHints["hint1"]["detail1"].RuntimeHintValues.Count); Assert.Equal("hintvalue1_1", lexV2Response.SessionState.RuntimeHints.SlotHints["hint1"]["detail1"].RuntimeHintValues[0].Phrase); Assert.Equal("hintvalue1_2", lexV2Response.SessionState.RuntimeHints.SlotHints["hint1"]["detail1"].RuntimeHintValues[1].Phrase); @@ -2774,7 +2676,7 @@ public void LexV2Response(Type serializerType) Assert.Equal("value1", lexV2Response.RequestAttributes["key1"]); Assert.Equal("value2", lexV2Response.RequestAttributes["key2"]); - MemoryStream ms = new MemoryStream(); + var ms = new MemoryStream(); serializer.Serialize(lexV2Response, ms); ms.Position = 0; var json = new StreamReader(ms).ReadToEnd(); @@ -2785,14 +2687,10 @@ public void LexV2Response(Type serializerType) } } - // Test is temporary disabled due to a bug in .NET 8 RC2 - // https://github.com/dotnet/runtime/issues/93903 -#if !NET8_0 [Theory] [InlineData(typeof(JsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void KinesisFirehoseEvent(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -2802,7 +2700,7 @@ public void KinesisFirehoseEvent(Type serializerType) Assert.Equal("00540a87-5050-496a-84e4-e7d92bbaf5e2", kinesisEvent.InvocationId); Assert.Equal("arn:aws:firehose:us-east-1:AAAAAAAAAAAA:deliverystream/lambda-test", kinesisEvent.DeliveryStreamArn); Assert.Equal("us-east-1", kinesisEvent.Region); - Assert.Equal(1, kinesisEvent.Records.Count); + Assert.Single(kinesisEvent.Records); Assert.Equal("49572672223665514422805246926656954630972486059535892482", kinesisEvent.Records[0].RecordId); Assert.Equal("aGVsbG8gd29ybGQ=", kinesisEvent.Records[0].Base64EncodedData); @@ -2813,10 +2711,8 @@ public void KinesisFirehoseEvent(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void KinesisFirehoseResponseTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -2824,7 +2720,7 @@ public void KinesisFirehoseResponseTest(Type serializerType) { var kinesisResponse = serializer.Deserialize(fileStream); - Assert.Equal(1, kinesisResponse.Records.Count); + Assert.Single(kinesisResponse.Records); Assert.Equal("49572672223665514422805246926656954630972486059535892482", kinesisResponse.Records[0].RecordId); Assert.Equal(KinesisFirehoseResponse.TRANSFORMED_STATE_OK, kinesisResponse.Records[0].Result); Assert.Equal("SEVMTE8gV09STEQ=", kinesisResponse.Records[0].Base64EncodedData); @@ -2846,10 +2742,8 @@ public void KinesisFirehoseResponseTest(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void KinesisAnalyticsOutputDeliveryEvent(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -2866,10 +2760,8 @@ public void KinesisAnalyticsOutputDeliveryEvent(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void KinesisAnalyticsOutputDeliveryResponseTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -2877,7 +2769,7 @@ public void KinesisAnalyticsOutputDeliveryResponseTest(Type serializerType) { var kinesisAnalyticsResponse = serializer.Deserialize(fileStream); - Assert.Equal(1, kinesisAnalyticsResponse.Records.Count); + Assert.Single(kinesisAnalyticsResponse.Records); Assert.Equal("49572672223665514422805246926656954630972486059535892482", kinesisAnalyticsResponse.Records[0].RecordId); Assert.Equal(KinesisAnalyticsOutputDeliveryResponse.OK, kinesisAnalyticsResponse.Records[0].Result); @@ -2892,14 +2784,10 @@ public void KinesisAnalyticsOutputDeliveryResponseTest(Type serializerType) } } - // Test is temporary disabled due to a bug in .NET 8 RC2 - // https://github.com/dotnet/runtime/issues/93903 -#if !NET8_0 [Theory] [InlineData(typeof(JsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void KinesisAnalyticsInputProcessingEventTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -2909,7 +2797,7 @@ public void KinesisAnalyticsInputProcessingEventTest(Type serializerType) Assert.Equal("00540a87-5050-496a-84e4-e7d92bbaf5e2", kinesisAnalyticsEvent.InvocationId); Assert.Equal("arn:aws:kinesis:us-east-1:AAAAAAAAAAAA:stream/lambda-test", kinesisAnalyticsEvent.StreamArn); Assert.Equal("arn:aws:kinesisanalytics:us-east-1:12345678911:application/lambda-test", kinesisAnalyticsEvent.ApplicationArn); - Assert.Equal(1, kinesisAnalyticsEvent.Records.Count); + Assert.Single(kinesisAnalyticsEvent.Records); Assert.Equal("49572672223665514422805246926656954630972486059535892482", kinesisAnalyticsEvent.Records[0].RecordId); Assert.Equal("aGVsbG8gd29ybGQ=", kinesisAnalyticsEvent.Records[0].Base64EncodedData); @@ -2918,10 +2806,8 @@ public void KinesisAnalyticsInputProcessingEventTest(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void KinesisAnalyticsInputProcessingResponseTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -2929,7 +2815,7 @@ public void KinesisAnalyticsInputProcessingResponseTest(Type serializerType) { var kinesisAnalyticsResponse = serializer.Deserialize(fileStream); - Assert.Equal(1, kinesisAnalyticsResponse.Records.Count); + Assert.Single(kinesisAnalyticsResponse.Records); Assert.Equal("49572672223665514422805246926656954630972486059535892482", kinesisAnalyticsResponse.Records[0].RecordId); Assert.Equal(KinesisAnalyticsInputPreprocessingResponse.OK, kinesisAnalyticsResponse.Records[0].Result); Assert.Equal("SEVMTE8gV09STEQ=", kinesisAnalyticsResponse.Records[0].Base64EncodedData); @@ -2948,14 +2834,10 @@ public void KinesisAnalyticsInputProcessingResponseTest(Type serializerType) } } - // Test is temporary disabled due to a bug in .NET 8 RC2 - // https://github.com/dotnet/runtime/issues/93903 -#if !NET8_0 [Theory] [InlineData(typeof(JsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void KinesisAnalyticsStreamsInputProcessingEventTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -2965,7 +2847,7 @@ public void KinesisAnalyticsStreamsInputProcessingEventTest(Type serializerType) Assert.Equal("00540a87-5050-496a-84e4-e7d92bbaf5e2", kinesisAnalyticsEvent.InvocationId); Assert.Equal("arn:aws:kinesis:us-east-1:AAAAAAAAAAAA:stream/lambda-test", kinesisAnalyticsEvent.StreamArn); Assert.Equal("arn:aws:kinesisanalytics:us-east-1:12345678911:application/lambda-test", kinesisAnalyticsEvent.ApplicationArn); - Assert.Equal(1, kinesisAnalyticsEvent.Records.Count); + Assert.Single(kinesisAnalyticsEvent.Records); Assert.Equal("49572672223665514422805246926656954630972486059535892482", kinesisAnalyticsEvent.Records[0].RecordId); Assert.Equal("aGVsbG8gd29ybGQ=", kinesisAnalyticsEvent.Records[0].Base64EncodedData); @@ -2978,14 +2860,10 @@ public void KinesisAnalyticsStreamsInputProcessingEventTest(Type serializerType) } } - // Test is temporary disabled due to a bug in .NET 8 RC2 - // https://github.com/dotnet/runtime/issues/93903 -#if !NET8_0 [Theory] [InlineData(typeof(JsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void KinesisAnalyticsFirehoseInputProcessingEventTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -2995,7 +2873,7 @@ public void KinesisAnalyticsFirehoseInputProcessingEventTest(Type serializerType Assert.Equal("00540a87-5050-496a-84e4-e7d92bbaf5e2", kinesisAnalyticsEvent.InvocationId); Assert.Equal("arn:aws:firehose:us-east-1:AAAAAAAAAAAA:deliverystream/lambda-test", kinesisAnalyticsEvent.StreamArn); Assert.Equal("arn:aws:kinesisanalytics:us-east-1:12345678911:application/lambda-test", kinesisAnalyticsEvent.ApplicationArn); - Assert.Equal(1, kinesisAnalyticsEvent.Records.Count); + Assert.Single(kinesisAnalyticsEvent.Records); Assert.Equal("49572672223665514422805246926656954630972486059535892482", kinesisAnalyticsEvent.Records[0].RecordId); Assert.Equal("aGVsbG8gd29ybGQ=", kinesisAnalyticsEvent.Records[0].Base64EncodedData); @@ -3007,10 +2885,8 @@ public void KinesisAnalyticsFirehoseInputProcessingEventTest(Type serializerType [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void CloudWatchLogEvent(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -3037,10 +2913,8 @@ private string MemoryStreamToBase64String(MemoryStream ms) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void BatchJobStateChangeEventTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -3048,177 +2922,177 @@ public void BatchJobStateChangeEventTest(Type serializerType) { var jobStateChangeEvent = serializer.Deserialize(fileStream); - Assert.Equal(jobStateChangeEvent.Version, "0"); - Assert.Equal(jobStateChangeEvent.Id, "c8f9c4b5-76e5-d76a-f980-7011e206042b"); - Assert.Equal(jobStateChangeEvent.DetailType, "Batch Job State Change"); - Assert.Equal(jobStateChangeEvent.Source, "aws.batch"); - Assert.Equal(jobStateChangeEvent.Account, "aws_account_id"); - Assert.Equal(jobStateChangeEvent.Time.ToUniversalTime(), DateTime.Parse("2017-10-23T17:56:03Z").ToUniversalTime()); - Assert.Equal(jobStateChangeEvent.Region, "us-east-1"); - Assert.Equal(jobStateChangeEvent.Resources.Count, 1); - Assert.Equal(jobStateChangeEvent.Resources[0], "arn:aws:batch:us-east-1:aws_account_id:job/4c7599ae-0a82-49aa-ba5a-4727fcce14a8"); - Assert.IsType(typeof(Job), jobStateChangeEvent.Detail); - Assert.Equal(jobStateChangeEvent.Detail.JobName, "event-test"); - Assert.Equal(jobStateChangeEvent.Detail.JobId, "4c7599ae-0a82-49aa-ba5a-4727fcce14a8"); - Assert.Equal(jobStateChangeEvent.Detail.JobQueue, "arn:aws:batch:us-east-1:aws_account_id:job-queue/HighPriority"); - Assert.Equal(jobStateChangeEvent.Detail.Status, "RUNNABLE"); - Assert.Equal(jobStateChangeEvent.Detail.Attempts.Count, 0); - Assert.Equal(jobStateChangeEvent.Detail.CreatedAt, 1508781340401); - Assert.Equal(jobStateChangeEvent.Detail.RetryStrategy.Attempts, 1); - Assert.Equal(jobStateChangeEvent.Detail.RetryStrategy.EvaluateOnExit.Count, 1); - Assert.Equal(jobStateChangeEvent.Detail.RetryStrategy.EvaluateOnExit[0].Action, "EXIT"); - Assert.Equal(jobStateChangeEvent.Detail.RetryStrategy.EvaluateOnExit[0].OnExitCode, "*"); - Assert.Equal(jobStateChangeEvent.Detail.RetryStrategy.EvaluateOnExit[0].OnReason, "*"); - Assert.Equal(jobStateChangeEvent.Detail.RetryStrategy.EvaluateOnExit[0].OnStatusReason, "*"); - Assert.Equal(jobStateChangeEvent.Detail.DependsOn.Count, 0); - Assert.Equal(jobStateChangeEvent.Detail.JobDefinition, "arn:aws:batch:us-east-1:aws_account_id:job-definition/first-run-job-definition:1"); - Assert.Equal(jobStateChangeEvent.Detail.Parameters.Count, 1); - Assert.Equal(jobStateChangeEvent.Detail.Parameters["test"], "abc"); - Assert.Equal(jobStateChangeEvent.Detail.Container.Image, "busybox"); + Assert.Equal("0", jobStateChangeEvent.Version); + Assert.Equal("c8f9c4b5-76e5-d76a-f980-7011e206042b", jobStateChangeEvent.Id); + Assert.Equal("Batch Job State Change", jobStateChangeEvent.DetailType); + Assert.Equal("aws.batch", jobStateChangeEvent.Source); + Assert.Equal("aws_account_id", jobStateChangeEvent.Account); + Assert.Equal(DateTime.Parse("2017-10-23T17:56:03Z").ToUniversalTime(), jobStateChangeEvent.Time.ToUniversalTime()); + Assert.Equal("us-east-1", jobStateChangeEvent.Region); + Assert.Single(jobStateChangeEvent.Resources); + Assert.Equal("arn:aws:batch:us-east-1:aws_account_id:job/4c7599ae-0a82-49aa-ba5a-4727fcce14a8", jobStateChangeEvent.Resources[0]); + Assert.IsType(jobStateChangeEvent.Detail); + Assert.Equal("event-test", jobStateChangeEvent.Detail.JobName); + Assert.Equal("4c7599ae-0a82-49aa-ba5a-4727fcce14a8", jobStateChangeEvent.Detail.JobId); + Assert.Equal("arn:aws:batch:us-east-1:aws_account_id:job-queue/HighPriority", jobStateChangeEvent.Detail.JobQueue); + Assert.Equal("RUNNABLE", jobStateChangeEvent.Detail.Status); + Assert.Empty(jobStateChangeEvent.Detail.Attempts); + Assert.Equal(1508781340401, jobStateChangeEvent.Detail.CreatedAt); + Assert.Equal(1, jobStateChangeEvent.Detail.RetryStrategy.Attempts); + Assert.Single(jobStateChangeEvent.Detail.RetryStrategy.EvaluateOnExit); + Assert.Equal("EXIT", jobStateChangeEvent.Detail.RetryStrategy.EvaluateOnExit[0].Action); + Assert.Equal("*", jobStateChangeEvent.Detail.RetryStrategy.EvaluateOnExit[0].OnExitCode); + Assert.Equal("*", jobStateChangeEvent.Detail.RetryStrategy.EvaluateOnExit[0].OnReason); + Assert.Equal("*", jobStateChangeEvent.Detail.RetryStrategy.EvaluateOnExit[0].OnStatusReason); + Assert.Empty(jobStateChangeEvent.Detail.DependsOn); + Assert.Equal("arn:aws:batch:us-east-1:aws_account_id:job-definition/first-run-job-definition:1", jobStateChangeEvent.Detail.JobDefinition); + Assert.Single(jobStateChangeEvent.Detail.Parameters); + Assert.Equal("abc", jobStateChangeEvent.Detail.Parameters["test"]); + Assert.Equal("busybox", jobStateChangeEvent.Detail.Container.Image); Assert.NotNull(jobStateChangeEvent.Detail.Container.ResourceRequirements); - Assert.Equal(jobStateChangeEvent.Detail.Container.ResourceRequirements.Count, 2); - Assert.Equal(jobStateChangeEvent.Detail.Container.ResourceRequirements[0].Type, "MEMORY"); - Assert.Equal(jobStateChangeEvent.Detail.Container.ResourceRequirements[0].Value, "2000"); - Assert.Equal(jobStateChangeEvent.Detail.Container.ResourceRequirements[1].Type, "VCPU"); - Assert.Equal(jobStateChangeEvent.Detail.Container.ResourceRequirements[1].Value, "2"); - Assert.Equal(jobStateChangeEvent.Detail.Container.Vcpus, 2); - Assert.Equal(jobStateChangeEvent.Detail.Container.Memory, 2000); - Assert.Equal(jobStateChangeEvent.Detail.Container.Command.Count, 2); - Assert.Equal(jobStateChangeEvent.Detail.Container.Command[0], "echo"); - Assert.Equal(jobStateChangeEvent.Detail.Container.Command[1], "'hello world'"); - Assert.Equal(jobStateChangeEvent.Detail.Container.Volumes.Count, 2); - Assert.Equal(jobStateChangeEvent.Detail.Container.Volumes[0].Name, "myhostsource"); - Assert.Equal(jobStateChangeEvent.Detail.Container.Volumes[0].Host.SourcePath, "/data"); - Assert.Equal(jobStateChangeEvent.Detail.Container.Volumes[1].Name, "efs"); - Assert.Equal(jobStateChangeEvent.Detail.Container.Volumes[1].EfsVolumeConfiguration.AuthorizationConfig.AccessPointId, "fsap-XXXXXXXXXXXXXXXXX"); - Assert.Equal(jobStateChangeEvent.Detail.Container.Volumes[1].EfsVolumeConfiguration.AuthorizationConfig.Iam, "ENABLED"); - Assert.Equal(jobStateChangeEvent.Detail.Container.Volumes[1].EfsVolumeConfiguration.FileSystemId, "fs-XXXXXXXXX"); - Assert.Equal(jobStateChangeEvent.Detail.Container.Volumes[1].EfsVolumeConfiguration.RootDirectory, "/"); - Assert.Equal(jobStateChangeEvent.Detail.Container.Volumes[1].EfsVolumeConfiguration.TransitEncryption, "ENABLED"); - Assert.Equal(jobStateChangeEvent.Detail.Container.Volumes[1].EfsVolumeConfiguration.TransitEncryptionPort, 12345); + Assert.Equal(2, jobStateChangeEvent.Detail.Container.ResourceRequirements.Count); + Assert.Equal("MEMORY", jobStateChangeEvent.Detail.Container.ResourceRequirements[0].Type); + Assert.Equal("2000", jobStateChangeEvent.Detail.Container.ResourceRequirements[0].Value); + Assert.Equal("VCPU", jobStateChangeEvent.Detail.Container.ResourceRequirements[1].Type); + Assert.Equal("2", jobStateChangeEvent.Detail.Container.ResourceRequirements[1].Value); + Assert.Equal(2, jobStateChangeEvent.Detail.Container.Vcpus); + Assert.Equal(2000, jobStateChangeEvent.Detail.Container.Memory); + Assert.Equal(2, jobStateChangeEvent.Detail.Container.Command.Count); + Assert.Equal("echo", jobStateChangeEvent.Detail.Container.Command[0]); + Assert.Equal("'hello world'", jobStateChangeEvent.Detail.Container.Command[1]); + Assert.Equal(2, jobStateChangeEvent.Detail.Container.Volumes.Count); + Assert.Equal("myhostsource", jobStateChangeEvent.Detail.Container.Volumes[0].Name); + Assert.Equal("/data", jobStateChangeEvent.Detail.Container.Volumes[0].Host.SourcePath); + Assert.Equal("efs", jobStateChangeEvent.Detail.Container.Volumes[1].Name); + Assert.Equal("fsap-XXXXXXXXXXXXXXXXX", jobStateChangeEvent.Detail.Container.Volumes[1].EfsVolumeConfiguration.AuthorizationConfig.AccessPointId); + Assert.Equal("ENABLED", jobStateChangeEvent.Detail.Container.Volumes[1].EfsVolumeConfiguration.AuthorizationConfig.Iam); + Assert.Equal("fs-XXXXXXXXX", jobStateChangeEvent.Detail.Container.Volumes[1].EfsVolumeConfiguration.FileSystemId); + Assert.Equal("/", jobStateChangeEvent.Detail.Container.Volumes[1].EfsVolumeConfiguration.RootDirectory); + Assert.Equal("ENABLED", jobStateChangeEvent.Detail.Container.Volumes[1].EfsVolumeConfiguration.TransitEncryption); + Assert.Equal(12345, jobStateChangeEvent.Detail.Container.Volumes[1].EfsVolumeConfiguration.TransitEncryptionPort); Assert.NotNull(jobStateChangeEvent.Detail.Container.Environment); - Assert.Equal(jobStateChangeEvent.Detail.Container.Environment.Count, 1); - Assert.Equal(jobStateChangeEvent.Detail.Container.Environment[0].Name, "MANAGED_BY_AWS"); - Assert.Equal(jobStateChangeEvent.Detail.Container.Environment[0].Value, "STARTED_BY_STEP_FUNCTIONS"); - Assert.Equal(jobStateChangeEvent.Detail.Container.MountPoints.Count, 2); - Assert.Equal(jobStateChangeEvent.Detail.Container.MountPoints[0].ContainerPath, "/data"); - Assert.Equal(jobStateChangeEvent.Detail.Container.MountPoints[0].ReadOnly, true); - Assert.Equal(jobStateChangeEvent.Detail.Container.MountPoints[0].SourceVolume, "myhostsource"); - Assert.Equal(jobStateChangeEvent.Detail.Container.MountPoints[1].ContainerPath, "/mount/efs"); - Assert.Equal(jobStateChangeEvent.Detail.Container.MountPoints[1].SourceVolume, "efs"); - Assert.Equal(jobStateChangeEvent.Detail.Container.Ulimits.Count, 1); - Assert.Equal(jobStateChangeEvent.Detail.Container.Ulimits[0].HardLimit, 2048); - Assert.Equal(jobStateChangeEvent.Detail.Container.Ulimits[0].Name, "nofile"); - Assert.Equal(jobStateChangeEvent.Detail.Container.Ulimits[0].SoftLimit, 2048); + Assert.Single(jobStateChangeEvent.Detail.Container.Environment); + Assert.Equal("MANAGED_BY_AWS", jobStateChangeEvent.Detail.Container.Environment[0].Name); + Assert.Equal("STARTED_BY_STEP_FUNCTIONS", jobStateChangeEvent.Detail.Container.Environment[0].Value); + Assert.Equal(2, jobStateChangeEvent.Detail.Container.MountPoints.Count); + Assert.Equal("/data", jobStateChangeEvent.Detail.Container.MountPoints[0].ContainerPath); + Assert.True(jobStateChangeEvent.Detail.Container.MountPoints[0].ReadOnly); + Assert.Equal("myhostsource", jobStateChangeEvent.Detail.Container.MountPoints[0].SourceVolume); + Assert.Equal("/mount/efs", jobStateChangeEvent.Detail.Container.MountPoints[1].ContainerPath); + Assert.Equal("efs", jobStateChangeEvent.Detail.Container.MountPoints[1].SourceVolume); + Assert.Single(jobStateChangeEvent.Detail.Container.Ulimits); + Assert.Equal(2048, jobStateChangeEvent.Detail.Container.Ulimits[0].HardLimit); + Assert.Equal("nofile", jobStateChangeEvent.Detail.Container.Ulimits[0].Name); + Assert.Equal(2048, jobStateChangeEvent.Detail.Container.Ulimits[0].SoftLimit); Assert.NotNull(jobStateChangeEvent.Detail.Container.LinuxParameters); - Assert.Equal(jobStateChangeEvent.Detail.Container.LinuxParameters.Devices.Count, 1); - Assert.Equal(jobStateChangeEvent.Detail.Container.LinuxParameters.Devices[0].ContainerPath, "/dev/sda"); - Assert.Equal(jobStateChangeEvent.Detail.Container.LinuxParameters.Devices[0].HostPath, "/dev/xvdc"); - Assert.Equal(jobStateChangeEvent.Detail.Container.LinuxParameters.Devices[0].Permissions.Count, 1); - Assert.Equal(jobStateChangeEvent.Detail.Container.LinuxParameters.Devices[0].Permissions[0], "MKNOD"); - Assert.Equal(jobStateChangeEvent.Detail.Container.LinuxParameters.InitProcessEnabled, true); - Assert.Equal(jobStateChangeEvent.Detail.Container.LinuxParameters.SharedMemorySize, 64); - Assert.Equal(jobStateChangeEvent.Detail.Container.LinuxParameters.MaxSwap, 1024); - Assert.Equal(jobStateChangeEvent.Detail.Container.LinuxParameters.Swappiness, 55); - Assert.Equal(jobStateChangeEvent.Detail.Container.LinuxParameters.Tmpfs.Count, 1); - Assert.Equal(jobStateChangeEvent.Detail.Container.LinuxParameters.Tmpfs[0].ContainerPath, "/run"); - Assert.Equal(jobStateChangeEvent.Detail.Container.LinuxParameters.Tmpfs[0].Size, 65536); - Assert.Equal(jobStateChangeEvent.Detail.Container.LinuxParameters.Tmpfs[0].MountOptions.Count, 2); - Assert.Equal(jobStateChangeEvent.Detail.Container.LinuxParameters.Tmpfs[0].MountOptions[0], "noexec"); - Assert.Equal(jobStateChangeEvent.Detail.Container.LinuxParameters.Tmpfs[0].MountOptions[1], "nosuid"); + Assert.Single(jobStateChangeEvent.Detail.Container.LinuxParameters.Devices); + Assert.Equal("/dev/sda", jobStateChangeEvent.Detail.Container.LinuxParameters.Devices[0].ContainerPath); + Assert.Equal("/dev/xvdc", jobStateChangeEvent.Detail.Container.LinuxParameters.Devices[0].HostPath); + Assert.Single(jobStateChangeEvent.Detail.Container.LinuxParameters.Devices[0].Permissions); + Assert.Equal("MKNOD", jobStateChangeEvent.Detail.Container.LinuxParameters.Devices[0].Permissions[0]); + Assert.True(jobStateChangeEvent.Detail.Container.LinuxParameters.InitProcessEnabled); + Assert.Equal(64, jobStateChangeEvent.Detail.Container.LinuxParameters.SharedMemorySize); + Assert.Equal(1024, jobStateChangeEvent.Detail.Container.LinuxParameters.MaxSwap); + Assert.Equal(55, jobStateChangeEvent.Detail.Container.LinuxParameters.Swappiness); + Assert.Single(jobStateChangeEvent.Detail.Container.LinuxParameters.Tmpfs); + Assert.Equal("/run", jobStateChangeEvent.Detail.Container.LinuxParameters.Tmpfs[0].ContainerPath); + Assert.Equal(65536, jobStateChangeEvent.Detail.Container.LinuxParameters.Tmpfs[0].Size); + Assert.Equal(2, jobStateChangeEvent.Detail.Container.LinuxParameters.Tmpfs[0].MountOptions.Count); + Assert.Equal("noexec", jobStateChangeEvent.Detail.Container.LinuxParameters.Tmpfs[0].MountOptions[0]); + Assert.Equal("nosuid", jobStateChangeEvent.Detail.Container.LinuxParameters.Tmpfs[0].MountOptions[1]); Assert.NotNull(jobStateChangeEvent.Detail.Container.LogConfiguration); - Assert.Equal(jobStateChangeEvent.Detail.Container.LogConfiguration.LogDriver, "json-file"); - Assert.Equal(jobStateChangeEvent.Detail.Container.LogConfiguration.Options.Count, 2); - Assert.Equal(jobStateChangeEvent.Detail.Container.LogConfiguration.Options["max-size"], "10m"); - Assert.Equal(jobStateChangeEvent.Detail.Container.LogConfiguration.Options["max-file"], "3"); - Assert.Equal(jobStateChangeEvent.Detail.Container.LogConfiguration.SecretOptions.Count, 1); - Assert.Equal(jobStateChangeEvent.Detail.Container.LogConfiguration.SecretOptions[0].Name, "apikey"); - Assert.Equal(jobStateChangeEvent.Detail.Container.LogConfiguration.SecretOptions[0].ValueFrom, "ddApiKey"); - Assert.Equal(jobStateChangeEvent.Detail.Container.Secrets.Count, 1); - Assert.Equal(jobStateChangeEvent.Detail.Container.Secrets[0].Name, "DATABASE_PASSWORD"); - Assert.Equal(jobStateChangeEvent.Detail.Container.Secrets[0].ValueFrom, "arn:aws:ssm:us-east-1:awsExampleAccountID:parameter/awsExampleParameter"); + Assert.Equal("json-file", jobStateChangeEvent.Detail.Container.LogConfiguration.LogDriver); + Assert.Equal(2, jobStateChangeEvent.Detail.Container.LogConfiguration.Options.Count); + Assert.Equal("10m", jobStateChangeEvent.Detail.Container.LogConfiguration.Options["max-size"]); + Assert.Equal("3", jobStateChangeEvent.Detail.Container.LogConfiguration.Options["max-file"]); + Assert.Single(jobStateChangeEvent.Detail.Container.LogConfiguration.SecretOptions); + Assert.Equal("apikey", jobStateChangeEvent.Detail.Container.LogConfiguration.SecretOptions[0].Name); + Assert.Equal("ddApiKey", jobStateChangeEvent.Detail.Container.LogConfiguration.SecretOptions[0].ValueFrom); + Assert.Single(jobStateChangeEvent.Detail.Container.Secrets); + Assert.Equal("DATABASE_PASSWORD", jobStateChangeEvent.Detail.Container.Secrets[0].Name); + Assert.Equal("arn:aws:ssm:us-east-1:awsExampleAccountID:parameter/awsExampleParameter", jobStateChangeEvent.Detail.Container.Secrets[0].ValueFrom); Assert.NotNull(jobStateChangeEvent.Detail.Container.NetworkConfiguration); - Assert.Equal(jobStateChangeEvent.Detail.Container.NetworkConfiguration.AssignPublicIp, "ENABLED"); + Assert.Equal("ENABLED", jobStateChangeEvent.Detail.Container.NetworkConfiguration.AssignPublicIp); Assert.NotNull(jobStateChangeEvent.Detail.Container.FargatePlatformConfiguration); - Assert.Equal(jobStateChangeEvent.Detail.Container.FargatePlatformConfiguration.PlatformVersion, "LATEST"); + Assert.Equal("LATEST", jobStateChangeEvent.Detail.Container.FargatePlatformConfiguration.PlatformVersion); Assert.NotNull(jobStateChangeEvent.Detail.NodeProperties); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.MainNode, 0); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NumNodes, 0); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties.Count, 1); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].TargetNodes, "0:1"); + Assert.Equal(0, jobStateChangeEvent.Detail.NodeProperties.MainNode); + Assert.Equal(0, jobStateChangeEvent.Detail.NodeProperties.NumNodes); + Assert.Single(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties); + Assert.Equal("0:1", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].TargetNodes); Assert.NotNull(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Image, "busybox"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.ResourceRequirements.Count, 2); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.ResourceRequirements[0].Type, "MEMORY"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.ResourceRequirements[0].Value, "2000"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.ResourceRequirements[1].Type, "VCPU"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.ResourceRequirements[1].Value, "2"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Vcpus, 2); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Memory, 2000); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Command.Count, 2); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Command[0], "echo"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Command[1], "'hello world'"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes.Count, 2); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes[0].Name, "myhostsource"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes[0].Host.SourcePath, "/data"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes[1].Name, "efs"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes[1].EfsVolumeConfiguration.AuthorizationConfig.AccessPointId, "fsap-XXXXXXXXXXXXXXXXX"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes[1].EfsVolumeConfiguration.AuthorizationConfig.Iam, "ENABLED"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes[1].EfsVolumeConfiguration.FileSystemId, "fs-XXXXXXXXX"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes[1].EfsVolumeConfiguration.RootDirectory, "/"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes[1].EfsVolumeConfiguration.TransitEncryption, "ENABLED"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes[1].EfsVolumeConfiguration.TransitEncryptionPort, 12345); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Environment.Count, 1); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Environment[0].Name, "MANAGED_BY_AWS"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Environment[0].Value, "STARTED_BY_STEP_FUNCTIONS"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.MountPoints.Count, 2); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.MountPoints[0].ContainerPath, "/data"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.MountPoints[0].ReadOnly, true); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.MountPoints[0].SourceVolume, "myhostsource"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.MountPoints[1].ContainerPath, "/mount/efs"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.MountPoints[1].SourceVolume, "efs"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Ulimits.Count, 1); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Ulimits[0].HardLimit, 2048); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Ulimits[0].Name, "nofile"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Ulimits[0].SoftLimit, 2048); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.ExecutionRoleArn, "arn:aws:iam::awsExampleAccountID:role/awsExampleRoleName"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.InstanceType, "p3.2xlarge"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.User, "testuser"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.JobRoleArn, "arn:aws:iam::awsExampleAccountID:role/awsExampleRoleName"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Devices.Count, 1); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Devices[0].HostPath, "/dev/xvdc"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Devices[0].ContainerPath, "/dev/sda"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Devices[0].Permissions.Count, 1); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Devices[0].Permissions[0], "MKNOD"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.InitProcessEnabled, true); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.SharedMemorySize, 64); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.MaxSwap, 1024); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Swappiness, 55); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Tmpfs.Count, 1); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Tmpfs[0].ContainerPath, "/run"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Tmpfs[0].Size, 65536); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Tmpfs[0].MountOptions.Count, 2); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Tmpfs[0].MountOptions[0], "noexec"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Tmpfs[0].MountOptions[1], "nosuid"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LogConfiguration.LogDriver, "awslogs"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LogConfiguration.Options["awslogs-group"], "awslogs-wordpress"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LogConfiguration.Options["awslogs-stream-prefix"], "awslogs-example"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LogConfiguration.SecretOptions.Count, 1); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LogConfiguration.SecretOptions[0].Name, "apikey"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LogConfiguration.SecretOptions[0].ValueFrom, "ddApiKey"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Secrets.Count, 1); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Secrets[0].Name, "DATABASE_PASSWORD"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Secrets[0].ValueFrom, "arn:aws:ssm:us-east-1:awsExampleAccountID:parameter/awsExampleParameter"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.NetworkConfiguration.AssignPublicIp, "DISABLED"); - Assert.Equal(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.FargatePlatformConfiguration.PlatformVersion, "LATEST"); - Assert.Equal(jobStateChangeEvent.Detail.PropagateTags, true); - Assert.Equal(jobStateChangeEvent.Detail.Timeout.AttemptDurationSeconds, 90); - Assert.Equal(jobStateChangeEvent.Detail.Tags.Count, 3); - Assert.Equal(jobStateChangeEvent.Detail.Tags["Service"], "Batch"); - Assert.Equal(jobStateChangeEvent.Detail.Tags["Name"], "JobDefinitionTag"); - Assert.Equal(jobStateChangeEvent.Detail.Tags["Expected"], "MergeTag"); - Assert.Equal(jobStateChangeEvent.Detail.PlatformCapabilities.Count, 1); - Assert.Equal(jobStateChangeEvent.Detail.PlatformCapabilities[0], "FARGATE"); + Assert.Equal("busybox", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Image); + Assert.Equal(2, jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.ResourceRequirements.Count); + Assert.Equal("MEMORY", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.ResourceRequirements[0].Type); + Assert.Equal("2000", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.ResourceRequirements[0].Value); + Assert.Equal("VCPU", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.ResourceRequirements[1].Type); + Assert.Equal("2", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.ResourceRequirements[1].Value); + Assert.Equal(2, jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Vcpus); + Assert.Equal(2000, jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Memory); + Assert.Equal(2, jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Command.Count); + Assert.Equal("echo", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Command[0]); + Assert.Equal("'hello world'", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Command[1]); + Assert.Equal(2, jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes.Count); + Assert.Equal("myhostsource", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes[0].Name); + Assert.Equal("/data", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes[0].Host.SourcePath); + Assert.Equal("efs", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes[1].Name); + Assert.Equal("fsap-XXXXXXXXXXXXXXXXX", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes[1].EfsVolumeConfiguration.AuthorizationConfig.AccessPointId); + Assert.Equal("ENABLED", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes[1].EfsVolumeConfiguration.AuthorizationConfig.Iam); + Assert.Equal("fs-XXXXXXXXX", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes[1].EfsVolumeConfiguration.FileSystemId); + Assert.Equal("/", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes[1].EfsVolumeConfiguration.RootDirectory); + Assert.Equal("ENABLED", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes[1].EfsVolumeConfiguration.TransitEncryption); + Assert.Equal(12345, jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Volumes[1].EfsVolumeConfiguration.TransitEncryptionPort); + Assert.Single(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Environment); + Assert.Equal("MANAGED_BY_AWS", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Environment[0].Name); + Assert.Equal("STARTED_BY_STEP_FUNCTIONS", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Environment[0].Value); + Assert.Equal(2, jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.MountPoints.Count); + Assert.Equal("/data", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.MountPoints[0].ContainerPath); + Assert.True(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.MountPoints[0].ReadOnly); + Assert.Equal("myhostsource", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.MountPoints[0].SourceVolume); + Assert.Equal("/mount/efs", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.MountPoints[1].ContainerPath); + Assert.Equal("efs", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.MountPoints[1].SourceVolume); + Assert.Single(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Ulimits); + Assert.Equal(2048, jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Ulimits[0].HardLimit); + Assert.Equal("nofile", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Ulimits[0].Name); + Assert.Equal(2048, jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Ulimits[0].SoftLimit); + Assert.Equal("arn:aws:iam::awsExampleAccountID:role/awsExampleRoleName", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.ExecutionRoleArn); + Assert.Equal("p3.2xlarge", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.InstanceType); + Assert.Equal("testuser", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.User); + Assert.Equal("arn:aws:iam::awsExampleAccountID:role/awsExampleRoleName", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.JobRoleArn); + Assert.Single(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Devices); + Assert.Equal("/dev/xvdc", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Devices[0].HostPath); + Assert.Equal("/dev/sda", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Devices[0].ContainerPath); + Assert.Single(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Devices[0].Permissions); + Assert.Equal("MKNOD", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Devices[0].Permissions[0]); + Assert.True(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.InitProcessEnabled); + Assert.Equal(64, jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.SharedMemorySize); + Assert.Equal(1024, jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.MaxSwap); + Assert.Equal(55, jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Swappiness); + Assert.Single(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Tmpfs); + Assert.Equal("/run", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Tmpfs[0].ContainerPath); + Assert.Equal(65536, jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Tmpfs[0].Size); + Assert.Equal(2, jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Tmpfs[0].MountOptions.Count); + Assert.Equal("noexec", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Tmpfs[0].MountOptions[0]); + Assert.Equal("nosuid", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LinuxParameters.Tmpfs[0].MountOptions[1]); + Assert.Equal("awslogs", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LogConfiguration.LogDriver); + Assert.Equal("awslogs-wordpress", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LogConfiguration.Options["awslogs-group"]); + Assert.Equal("awslogs-example", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LogConfiguration.Options["awslogs-stream-prefix"]); + Assert.Single(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LogConfiguration.SecretOptions); + Assert.Equal("apikey", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LogConfiguration.SecretOptions[0].Name); + Assert.Equal("ddApiKey", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.LogConfiguration.SecretOptions[0].ValueFrom); + Assert.Single(jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Secrets); + Assert.Equal("DATABASE_PASSWORD", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Secrets[0].Name); + Assert.Equal("arn:aws:ssm:us-east-1:awsExampleAccountID:parameter/awsExampleParameter", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.Secrets[0].ValueFrom); + Assert.Equal("DISABLED", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.NetworkConfiguration.AssignPublicIp); + Assert.Equal("LATEST", jobStateChangeEvent.Detail.NodeProperties.NodeRangeProperties[0].Container.FargatePlatformConfiguration.PlatformVersion); + Assert.True(jobStateChangeEvent.Detail.PropagateTags); + Assert.Equal(90, jobStateChangeEvent.Detail.Timeout.AttemptDurationSeconds); + Assert.Equal(3, jobStateChangeEvent.Detail.Tags.Count); + Assert.Equal("Batch", jobStateChangeEvent.Detail.Tags["Service"]); + Assert.Equal("JobDefinitionTag", jobStateChangeEvent.Detail.Tags["Name"]); + Assert.Equal("MergeTag", jobStateChangeEvent.Detail.Tags["Expected"]); + Assert.Single(jobStateChangeEvent.Detail.PlatformCapabilities); + Assert.Equal("FARGATE", jobStateChangeEvent.Detail.PlatformCapabilities[0]); Handle(jobStateChangeEvent); } @@ -3231,26 +3105,24 @@ private void Handle(BatchJobStateChangeEvent jobStateChangeEvent) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void ScheduledEventTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; using (var fileStream = LoadJsonTestFile("scheduled-event.json")) { var scheduledEvent = serializer.Deserialize(fileStream); - Assert.Equal(scheduledEvent.Version, "0"); - Assert.Equal(scheduledEvent.Id, "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c"); - Assert.Equal(scheduledEvent.DetailType, "Scheduled Event"); - Assert.Equal(scheduledEvent.Source, "aws.events"); - Assert.Equal(scheduledEvent.Account, "123456789012"); - Assert.Equal(scheduledEvent.Time.ToUniversalTime(), DateTime.Parse("1970-01-01T00:00:00Z").ToUniversalTime()); - Assert.Equal(scheduledEvent.Region, "us-east-1"); - Assert.Equal(scheduledEvent.Resources.Count, 1); - Assert.Equal(scheduledEvent.Resources[0], "arn:aws:events:us-east-1:123456789012:rule/my-schedule"); - Assert.IsType(typeof(Detail), scheduledEvent.Detail); + Assert.Equal("0", scheduledEvent.Version); + Assert.Equal("cdc73f9d-aea9-11e3-9d5a-835b769c0d9c", scheduledEvent.Id); + Assert.Equal("Scheduled Event", scheduledEvent.DetailType); + Assert.Equal("aws.events", scheduledEvent.Source); + Assert.Equal("123456789012", scheduledEvent.Account); + Assert.Equal(DateTime.Parse("1970-01-01T00:00:00Z").ToUniversalTime(), scheduledEvent.Time.ToUniversalTime()); + Assert.Equal("us-east-1", scheduledEvent.Region); + Assert.Single(scheduledEvent.Resources); + Assert.Equal("arn:aws:events:us-east-1:123456789012:rule/my-schedule", scheduledEvent.Resources[0]); + Assert.IsType(scheduledEvent.Detail); Handle(scheduledEvent); } @@ -3263,10 +3135,8 @@ private void Handle(ScheduledEvent scheduledEvent) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void ECSContainerInstanceStateChangeEventTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -3274,38 +3144,38 @@ public void ECSContainerInstanceStateChangeEventTest(Type serializerType) { var ecsEvent = serializer.Deserialize(fileStream); - Assert.Equal(ecsEvent.Version, "0"); - Assert.Equal(ecsEvent.Id, "8952ba83-7be2-4ab5-9c32-6687532d15a2"); - Assert.Equal(ecsEvent.DetailType, "ECS Container Instance State Change"); - Assert.Equal(ecsEvent.Source, "aws.ecs"); - Assert.Equal(ecsEvent.Account, "111122223333"); - Assert.Equal(ecsEvent.Time.ToUniversalTime(), DateTime.Parse("2016-12-06T16:41:06Z").ToUniversalTime()); - Assert.Equal(ecsEvent.Region, "us-east-1"); - Assert.Equal(ecsEvent.Resources.Count, 1); - Assert.Equal(ecsEvent.Resources[0], "arn:aws:ecs:us-east-1:111122223333:container-instance/b54a2a04-046f-4331-9d74-3f6d7f6ca315"); - Assert.IsType(typeof(ContainerInstance), ecsEvent.Detail); - Assert.Equal(ecsEvent.Detail.AgentConnected, true); - Assert.Equal(ecsEvent.Detail.Attributes.Count, 14); - Assert.Equal(ecsEvent.Detail.Attributes[0].Name, "com.amazonaws.ecs.capability.logging-driver.syslog"); - Assert.Equal(ecsEvent.Detail.ClusterArn, "arn:aws:ecs:us-east-1:111122223333:cluster/default"); - Assert.Equal(ecsEvent.Detail.ContainerInstanceArn, "arn:aws:ecs:us-east-1:111122223333:container-instance/b54a2a04-046f-4331-9d74-3f6d7f6ca315"); - Assert.Equal(ecsEvent.Detail.Ec2InstanceId, "i-f3a8506b"); - Assert.Equal(ecsEvent.Detail.RegisteredResources.Count, 4); - Assert.Equal(ecsEvent.Detail.RegisteredResources[0].Name, "CPU"); - Assert.Equal(ecsEvent.Detail.RegisteredResources[0].Type, "INTEGER"); - Assert.Equal(ecsEvent.Detail.RegisteredResources[0].IntegerValue, 2048); - Assert.Equal(ecsEvent.Detail.RegisteredResources[2].StringSetValue[0], "22"); - Assert.Equal(ecsEvent.Detail.RemainingResources.Count, 4); - Assert.Equal(ecsEvent.Detail.RemainingResources[0].Name, "CPU"); - Assert.Equal(ecsEvent.Detail.RemainingResources[0].Type, "INTEGER"); - Assert.Equal(ecsEvent.Detail.RemainingResources[0].IntegerValue, 1988); - Assert.Equal(ecsEvent.Detail.RemainingResources[2].StringSetValue[0], "22"); - Assert.Equal(ecsEvent.Detail.Status, "ACTIVE"); - Assert.Equal(ecsEvent.Detail.Version, 14801); - Assert.Equal(ecsEvent.Detail.VersionInfo.AgentHash, "aebcbca"); - Assert.Equal(ecsEvent.Detail.VersionInfo.AgentVersion, "1.13.0"); - Assert.Equal(ecsEvent.Detail.VersionInfo.DockerVersion, "DockerVersion: 1.11.2"); - Assert.Equal(ecsEvent.Detail.UpdatedAt.ToUniversalTime(), DateTime.Parse("2016-12-06T16:41:06.991Z").ToUniversalTime()); + Assert.Equal("0", ecsEvent.Version); + Assert.Equal("8952ba83-7be2-4ab5-9c32-6687532d15a2", ecsEvent.Id); + Assert.Equal("ECS Container Instance State Change", ecsEvent.DetailType); + Assert.Equal("aws.ecs", ecsEvent.Source); + Assert.Equal("111122223333", ecsEvent.Account); + Assert.Equal(DateTime.Parse("2016-12-06T16:41:06Z").ToUniversalTime(), ecsEvent.Time.ToUniversalTime()); + Assert.Equal("us-east-1", ecsEvent.Region); + Assert.Single(ecsEvent.Resources); + Assert.Equal("arn:aws:ecs:us-east-1:111122223333:container-instance/b54a2a04-046f-4331-9d74-3f6d7f6ca315", ecsEvent.Resources[0]); + Assert.IsType(ecsEvent.Detail); + Assert.True(ecsEvent.Detail.AgentConnected); + Assert.Equal(14, ecsEvent.Detail.Attributes.Count); + Assert.Equal("com.amazonaws.ecs.capability.logging-driver.syslog", ecsEvent.Detail.Attributes[0].Name); + Assert.Equal("arn:aws:ecs:us-east-1:111122223333:cluster/default", ecsEvent.Detail.ClusterArn); + Assert.Equal("arn:aws:ecs:us-east-1:111122223333:container-instance/b54a2a04-046f-4331-9d74-3f6d7f6ca315", ecsEvent.Detail.ContainerInstanceArn); + Assert.Equal("i-f3a8506b", ecsEvent.Detail.Ec2InstanceId); + Assert.Equal(4, ecsEvent.Detail.RegisteredResources.Count); + Assert.Equal("CPU", ecsEvent.Detail.RegisteredResources[0].Name); + Assert.Equal("INTEGER", ecsEvent.Detail.RegisteredResources[0].Type); + Assert.Equal(2048, ecsEvent.Detail.RegisteredResources[0].IntegerValue); + Assert.Equal("22", ecsEvent.Detail.RegisteredResources[2].StringSetValue[0]); + Assert.Equal(4, ecsEvent.Detail.RemainingResources.Count); + Assert.Equal("CPU", ecsEvent.Detail.RemainingResources[0].Name); + Assert.Equal("INTEGER", ecsEvent.Detail.RemainingResources[0].Type); + Assert.Equal(1988, ecsEvent.Detail.RemainingResources[0].IntegerValue); + Assert.Equal("22", ecsEvent.Detail.RemainingResources[2].StringSetValue[0]); + Assert.Equal("ACTIVE", ecsEvent.Detail.Status); + Assert.Equal(14801, ecsEvent.Detail.Version); + Assert.Equal("aebcbca", ecsEvent.Detail.VersionInfo.AgentHash); + Assert.Equal("1.13.0", ecsEvent.Detail.VersionInfo.AgentVersion); + Assert.Equal("DockerVersion: 1.11.2", ecsEvent.Detail.VersionInfo.DockerVersion); + Assert.Equal(DateTime.Parse("2016-12-06T16:41:06.991Z").ToUniversalTime(), ecsEvent.Detail.UpdatedAt.ToUniversalTime()); Handle(ecsEvent); } @@ -3314,10 +3184,8 @@ public void ECSContainerInstanceStateChangeEventTest(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void ECSTaskStateChangeEventTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -3325,78 +3193,78 @@ public void ECSTaskStateChangeEventTest(Type serializerType) { var ecsEvent = serializer.Deserialize(fileStream); - Assert.Equal(ecsEvent.Version, "0"); - Assert.Equal(ecsEvent.Id, "3317b2af-7005-947d-b652-f55e762e571a"); - Assert.Equal(ecsEvent.DetailType, "ECS Task State Change"); - Assert.Equal(ecsEvent.Source, "aws.ecs"); - Assert.Equal(ecsEvent.Account, "111122223333"); - Assert.Equal(ecsEvent.Time.ToUniversalTime(), DateTime.Parse("2020-01-23T17:57:58Z").ToUniversalTime()); - Assert.Equal(ecsEvent.Region, "us-west-2"); + Assert.Equal("0", ecsEvent.Version); + Assert.Equal("3317b2af-7005-947d-b652-f55e762e571a", ecsEvent.Id); + Assert.Equal("ECS Task State Change", ecsEvent.DetailType); + Assert.Equal("aws.ecs", ecsEvent.Source); + Assert.Equal("111122223333", ecsEvent.Account); + Assert.Equal(DateTime.Parse("2020-01-23T17:57:58Z").ToUniversalTime(), ecsEvent.Time.ToUniversalTime()); + Assert.Equal("us-west-2", ecsEvent.Region); Assert.NotNull(ecsEvent.Resources); - Assert.Equal(ecsEvent.Resources.Count, 1); - Assert.Equal(ecsEvent.Resources[0], "arn:aws:ecs:us-west-2:111122223333:task/FargateCluster/c13b4cb40f1f4fe4a2971f76ae5a47ad"); + Assert.Single(ecsEvent.Resources); + Assert.Equal("arn:aws:ecs:us-west-2:111122223333:task/FargateCluster/c13b4cb40f1f4fe4a2971f76ae5a47ad", ecsEvent.Resources[0]); Assert.NotNull(ecsEvent.Detail); - Assert.IsType(typeof(Task), ecsEvent.Detail); + Assert.IsType(ecsEvent.Detail); Assert.NotNull(ecsEvent.Detail.Attachments); - Assert.Equal(ecsEvent.Detail.Attachments.Count, 1); - Assert.Equal(ecsEvent.Detail.Attachments[0].Id, "1789bcae-ddfb-4d10-8ebe-8ac87ddba5b8"); - Assert.Equal(ecsEvent.Detail.Attachments[0].Type, "eni"); - Assert.Equal(ecsEvent.Detail.Attachments[0].Status, "ATTACHED"); + Assert.Single(ecsEvent.Detail.Attachments); + Assert.Equal("1789bcae-ddfb-4d10-8ebe-8ac87ddba5b8", ecsEvent.Detail.Attachments[0].Id); + Assert.Equal("eni", ecsEvent.Detail.Attachments[0].Type); + Assert.Equal("ATTACHED", ecsEvent.Detail.Attachments[0].Status); Assert.NotNull(ecsEvent.Detail.Attachments[0].Details); - Assert.Equal(ecsEvent.Detail.Attachments[0].Details.Count, 4); - Assert.Equal(ecsEvent.Detail.Attachments[0].Details[0].Name, "subnetId"); - Assert.Equal(ecsEvent.Detail.Attachments[0].Details[0].Value, "subnet-abcd1234"); - Assert.Equal(ecsEvent.Detail.Attachments[0].Details[1].Name, "networkInterfaceId"); - Assert.Equal(ecsEvent.Detail.Attachments[0].Details[1].Value, "eni-abcd1234"); - Assert.Equal(ecsEvent.Detail.Attachments[0].Details[2].Name, "macAddress"); - Assert.Equal(ecsEvent.Detail.Attachments[0].Details[2].Value, "0a:98:eb:a7:29:ba"); - Assert.Equal(ecsEvent.Detail.Attachments[0].Details[3].Name, "privateIPv4Address"); - Assert.Equal(ecsEvent.Detail.Attachments[0].Details[3].Value, "10.0.0.139"); - - Assert.Equal(ecsEvent.Detail.AvailabilityZone, "us-west-2c"); - Assert.Equal(ecsEvent.Detail.ClusterArn, "arn:aws:ecs:us-west-2:111122223333:cluster/FargateCluster"); + Assert.Equal(4, ecsEvent.Detail.Attachments[0].Details.Count); + Assert.Equal("subnetId", ecsEvent.Detail.Attachments[0].Details[0].Name); + Assert.Equal("subnet-abcd1234", ecsEvent.Detail.Attachments[0].Details[0].Value); + Assert.Equal("networkInterfaceId", ecsEvent.Detail.Attachments[0].Details[1].Name); + Assert.Equal("eni-abcd1234", ecsEvent.Detail.Attachments[0].Details[1].Value); + Assert.Equal("macAddress", ecsEvent.Detail.Attachments[0].Details[2].Name); + Assert.Equal("0a:98:eb:a7:29:ba", ecsEvent.Detail.Attachments[0].Details[2].Value); + Assert.Equal("privateIPv4Address", ecsEvent.Detail.Attachments[0].Details[3].Name); + Assert.Equal("10.0.0.139", ecsEvent.Detail.Attachments[0].Details[3].Value); + + Assert.Equal("us-west-2c", ecsEvent.Detail.AvailabilityZone); + Assert.Equal("arn:aws:ecs:us-west-2:111122223333:cluster/FargateCluster", ecsEvent.Detail.ClusterArn); Assert.NotNull(ecsEvent.Detail.Containers); - Assert.Equal(ecsEvent.Detail.Containers.Count, 1); - Assert.Equal(ecsEvent.Detail.Containers[0].ContainerArn, "arn:aws:ecs:us-west-2:111122223333:container/cf159fd6-3e3f-4a9e-84f9-66cbe726af01"); - Assert.Equal(ecsEvent.Detail.Containers[0].ExitCode, 0); - Assert.Equal(ecsEvent.Detail.Containers[0].LastStatus, "RUNNING"); - Assert.Equal(ecsEvent.Detail.Containers[0].Name, "FargateApp"); - Assert.Equal(ecsEvent.Detail.Containers[0].Image, "111122223333.dkr.ecr.us-west-2.amazonaws.com/hello-repository:latest"); - Assert.Equal(ecsEvent.Detail.Containers[0].ImageDigest, "sha256:74b2c688c700ec95a93e478cdb959737c148df3fbf5ea706abe0318726e885e6"); - Assert.Equal(ecsEvent.Detail.Containers[0].RuntimeId, "ad64cbc71c7fb31c55507ec24c9f77947132b03d48d9961115cf24f3b7307e1e"); - Assert.Equal(ecsEvent.Detail.Containers[0].TaskArn, "arn:aws:ecs:us-west-2:111122223333:task/FargateCluster/c13b4cb40f1f4fe4a2971f76ae5a47ad"); + Assert.Single(ecsEvent.Detail.Containers); + Assert.Equal("arn:aws:ecs:us-west-2:111122223333:container/cf159fd6-3e3f-4a9e-84f9-66cbe726af01", ecsEvent.Detail.Containers[0].ContainerArn); + Assert.Equal(0, ecsEvent.Detail.Containers[0].ExitCode); + Assert.Equal("RUNNING", ecsEvent.Detail.Containers[0].LastStatus); + Assert.Equal("FargateApp", ecsEvent.Detail.Containers[0].Name); + Assert.Equal("111122223333.dkr.ecr.us-west-2.amazonaws.com/hello-repository:latest", ecsEvent.Detail.Containers[0].Image); + Assert.Equal("sha256:74b2c688c700ec95a93e478cdb959737c148df3fbf5ea706abe0318726e885e6", ecsEvent.Detail.Containers[0].ImageDigest); + Assert.Equal("ad64cbc71c7fb31c55507ec24c9f77947132b03d48d9961115cf24f3b7307e1e", ecsEvent.Detail.Containers[0].RuntimeId); + Assert.Equal("arn:aws:ecs:us-west-2:111122223333:task/FargateCluster/c13b4cb40f1f4fe4a2971f76ae5a47ad", ecsEvent.Detail.Containers[0].TaskArn); Assert.NotNull(ecsEvent.Detail.Containers[0].NetworkInterfaces); - Assert.Equal(ecsEvent.Detail.Containers[0].NetworkInterfaces.Count, 1); - Assert.Equal(ecsEvent.Detail.Containers[0].NetworkInterfaces[0].AttachmentId, "1789bcae-ddfb-4d10-8ebe-8ac87ddba5b8"); - Assert.Equal(ecsEvent.Detail.Containers[0].NetworkInterfaces[0].PrivateIpv4Address, "10.0.0.139"); - Assert.Equal(ecsEvent.Detail.Containers[0].Cpu, "0"); - - Assert.Equal(ecsEvent.Detail.CreatedAt.ToUniversalTime(), DateTime.Parse("2020-01-23T17:57:34.402Z").ToUniversalTime()); - Assert.Equal(ecsEvent.Detail.LaunchType, "FARGATE"); - Assert.Equal(ecsEvent.Detail.Cpu, "256"); - Assert.Equal(ecsEvent.Detail.Memory, "512"); - Assert.Equal(ecsEvent.Detail.DesiredStatus, "RUNNING"); - Assert.Equal(ecsEvent.Detail.Group, "family:sample-fargate"); - Assert.Equal(ecsEvent.Detail.LastStatus, "RUNNING"); - - Assert.Equal(ecsEvent.Detail.Overrides.ContainerOverrides.Count, 1); - Assert.Equal(ecsEvent.Detail.Overrides.ContainerOverrides[0].Name, "FargateApp"); - Assert.Equal(ecsEvent.Detail.Overrides.ContainerOverrides[0].Environment.Count, 1); - Assert.Equal(ecsEvent.Detail.Overrides.ContainerOverrides[0].Environment[0].Name, "testname"); - Assert.Equal(ecsEvent.Detail.Overrides.ContainerOverrides[0].Environment[0].Value, "testvalue"); - - Assert.Equal(ecsEvent.Detail.Connectivity, "CONNECTED"); - Assert.Equal(ecsEvent.Detail.ConnectivityAt.ToUniversalTime(), DateTime.Parse("2020-01-23T17:57:38.453Z").ToUniversalTime()); - Assert.Equal(ecsEvent.Detail.PullStartedAt.ToUniversalTime(), DateTime.Parse("2020-01-23T17:57:52.103Z").ToUniversalTime()); - Assert.Equal(ecsEvent.Detail.StartedAt.ToUniversalTime(), DateTime.Parse("2020-01-23T17:57:58.103Z").ToUniversalTime()); - Assert.Equal(ecsEvent.Detail.PullStoppedAt.ToUniversalTime(), DateTime.Parse("2020-01-23T17:57:55.103Z").ToUniversalTime()); - Assert.Equal(ecsEvent.Detail.UpdatedAt.ToUniversalTime(), DateTime.Parse("2020-01-23T17:57:58.103Z").ToUniversalTime()); - Assert.Equal(ecsEvent.Detail.TaskArn, "arn:aws:ecs:us-west-2:111122223333:task/FargateCluster/c13b4cb40f1f4fe4a2971f76ae5a47ad"); - Assert.Equal(ecsEvent.Detail.TaskDefinitionArn, "arn:aws:ecs:us-west-2:111122223333:task-definition/sample-fargate:1"); - Assert.Equal(ecsEvent.Detail.Version, 4); - Assert.Equal(ecsEvent.Detail.PlatformVersion, "1.3.0"); + Assert.Single(ecsEvent.Detail.Containers[0].NetworkInterfaces); + Assert.Equal("1789bcae-ddfb-4d10-8ebe-8ac87ddba5b8", ecsEvent.Detail.Containers[0].NetworkInterfaces[0].AttachmentId); + Assert.Equal("10.0.0.139", ecsEvent.Detail.Containers[0].NetworkInterfaces[0].PrivateIpv4Address); + Assert.Equal("0", ecsEvent.Detail.Containers[0].Cpu); + + Assert.Equal(DateTime.Parse("2020-01-23T17:57:34.402Z").ToUniversalTime(), ecsEvent.Detail.CreatedAt.ToUniversalTime()); + Assert.Equal("FARGATE", ecsEvent.Detail.LaunchType); + Assert.Equal("256", ecsEvent.Detail.Cpu); + Assert.Equal("512", ecsEvent.Detail.Memory); + Assert.Equal("RUNNING", ecsEvent.Detail.DesiredStatus); + Assert.Equal("family:sample-fargate", ecsEvent.Detail.Group); + Assert.Equal("RUNNING", ecsEvent.Detail.LastStatus); + + Assert.Single(ecsEvent.Detail.Overrides.ContainerOverrides); + Assert.Equal("FargateApp", ecsEvent.Detail.Overrides.ContainerOverrides[0].Name); + Assert.Single(ecsEvent.Detail.Overrides.ContainerOverrides[0].Environment); + Assert.Equal("testname", ecsEvent.Detail.Overrides.ContainerOverrides[0].Environment[0].Name); + Assert.Equal("testvalue", ecsEvent.Detail.Overrides.ContainerOverrides[0].Environment[0].Value); + + Assert.Equal("CONNECTED", ecsEvent.Detail.Connectivity); + Assert.Equal(DateTime.Parse("2020-01-23T17:57:38.453Z").ToUniversalTime(), ecsEvent.Detail.ConnectivityAt.ToUniversalTime()); + Assert.Equal(DateTime.Parse("2020-01-23T17:57:52.103Z").ToUniversalTime(), ecsEvent.Detail.PullStartedAt.ToUniversalTime()); + Assert.Equal(DateTime.Parse("2020-01-23T17:57:58.103Z").ToUniversalTime(), ecsEvent.Detail.StartedAt.ToUniversalTime()); + Assert.Equal(DateTime.Parse("2020-01-23T17:57:55.103Z").ToUniversalTime(), ecsEvent.Detail.PullStoppedAt.ToUniversalTime()); + Assert.Equal(DateTime.Parse("2020-01-23T17:57:58.103Z").ToUniversalTime(), ecsEvent.Detail.UpdatedAt.ToUniversalTime()); + Assert.Equal("arn:aws:ecs:us-west-2:111122223333:task/FargateCluster/c13b4cb40f1f4fe4a2971f76ae5a47ad", ecsEvent.Detail.TaskArn); + Assert.Equal("arn:aws:ecs:us-west-2:111122223333:task-definition/sample-fargate:1", ecsEvent.Detail.TaskDefinitionArn); + Assert.Equal(4, ecsEvent.Detail.Version); + Assert.Equal("1.3.0", ecsEvent.Detail.PlatformVersion); Handle(ecsEvent); } @@ -3414,10 +3282,8 @@ private void Handle(ECSTaskStateChangeEvent ecsEvent) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void KafkaEventTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -3425,40 +3291,38 @@ public void KafkaEventTest(Type serializerType) { var kafkaEvent = serializer.Deserialize(fileStream); Assert.NotNull(kafkaEvent); - Assert.Equal(kafkaEvent.EventSource, "aws:kafka"); - Assert.Equal(kafkaEvent.EventSourceArn, "arn:aws:kafka:us-east-1:123456789012:cluster/vpc-3432434/4834-3547-3455-9872-7929"); - Assert.Equal(kafkaEvent.BootstrapServers, "b-2.demo-cluster-1.a1bcde.c1.kafka.us-east-1.amazonaws.com:9092,b-1.demo-cluster-1.a1bcde.c1.kafka.us-east-1.amazonaws.com:9092"); + Assert.Equal("aws:kafka", kafkaEvent.EventSource); + Assert.Equal("arn:aws:kafka:us-east-1:123456789012:cluster/vpc-3432434/4834-3547-3455-9872-7929", kafkaEvent.EventSourceArn); + Assert.Equal("b-2.demo-cluster-1.a1bcde.c1.kafka.us-east-1.amazonaws.com:9092,b-1.demo-cluster-1.a1bcde.c1.kafka.us-east-1.amazonaws.com:9092", kafkaEvent.BootstrapServers); Assert.NotNull(kafkaEvent.Records); - Assert.Equal(kafkaEvent.Records.Count, 1); + Assert.Single(kafkaEvent.Records); var record = kafkaEvent.Records.FirstOrDefault(); - Assert.NotNull(record); - Assert.Equal(record.Key, "mytopic-0"); + Assert.Equal("mytopic-0", record.Key); - Assert.Equal(record.Value.Count, 1); + Assert.Single(record.Value); var eventRecord = record.Value.FirstOrDefault(); - Assert.Equal(eventRecord.Topic, "mytopic"); - Assert.Equal(eventRecord.Partition, 12); - Assert.Equal(eventRecord.Offset, 3043205); - Assert.Equal(eventRecord.Timestamp, 1545084650987); - Assert.Equal(eventRecord.TimestampType, "CREATE_TIME"); + Assert.Equal("mytopic", eventRecord.Topic); + Assert.Equal(12, eventRecord.Partition); + Assert.Equal(3043205, eventRecord.Offset); + Assert.Equal(1545084650987, eventRecord.Timestamp); + Assert.Equal("CREATE_TIME", eventRecord.TimestampType); - Assert.Equal(new StreamReader(eventRecord.Value).ReadToEnd(), "Hello, this is a test."); + Assert.Equal("Hello, this is a test.", new StreamReader(eventRecord.Value).ReadToEnd()); - Assert.Equal(eventRecord.Headers.Count, 8); + Assert.Equal(8, eventRecord.Headers.Count); var eventRecordHeader = eventRecord.Headers.FirstOrDefault(); Assert.NotNull(eventRecordHeader); - Assert.Equal(eventRecordHeader.Count, 1); + Assert.Single(eventRecordHeader); var eventRecordHeaderValue = eventRecordHeader.FirstOrDefault(); - Assert.NotNull(eventRecordHeaderValue); - Assert.Equal(eventRecordHeaderValue.Key, "headerKey"); + Assert.Equal("headerKey", eventRecordHeaderValue.Key); // Convert sbyte[] to byte[] array. var tempHeaderValueByteArray = new byte[eventRecordHeaderValue.Value.Length]; Buffer.BlockCopy(eventRecordHeaderValue.Value, 0, tempHeaderValueByteArray, 0, tempHeaderValueByteArray.Length); - Assert.Equal(Encoding.UTF8.GetString(tempHeaderValueByteArray), "headerValue"); + Assert.Equal("headerValue", Encoding.UTF8.GetString(tempHeaderValueByteArray)); Handle(kafkaEvent); } @@ -3479,10 +3343,8 @@ private void Handle(KafkaEvent kafkaEvent) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void ActiveMQEventTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -3522,10 +3384,8 @@ public void ActiveMQEventTest(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void RabbitMQEventTest(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -3536,7 +3396,7 @@ public void RabbitMQEventTest(Type serializerType) Assert.Equal("aws:rmq", rabbitmqEvent.EventSource); Assert.Equal("arn:aws:mq:us-west-2:112556298976:broker:pizzaBroker:b-9bcfa592-423a-4942-879d-eb284b418fc8", rabbitmqEvent.EventSourceArn); - Assert.Equal(1, rabbitmqEvent.RmqMessagesByQueue.Count); + Assert.Single(rabbitmqEvent.RmqMessagesByQueue); Assert.Equal(2, rabbitmqEvent.RmqMessagesByQueue["pizzaQueue::/"].Count); var firstMessage = rabbitmqEvent.RmqMessagesByQueue["pizzaQueue::/"][0]; @@ -3563,7 +3423,7 @@ public void RabbitMQEventTest(Type serializerType) Assert.NotNull(secondMessage.BasicProperties); Assert.Null(secondMessage.BasicProperties.ContentType); Assert.Null(secondMessage.BasicProperties.ContentEncoding); - Assert.Equal(0, secondMessage.BasicProperties.Headers.Count); + Assert.Empty(secondMessage.BasicProperties.Headers); Assert.Equal(1, secondMessage.BasicProperties.DeliveryMode); Assert.Null(secondMessage.BasicProperties.Priority); Assert.Null(secondMessage.BasicProperties.CorrelationId); @@ -3583,10 +3443,8 @@ public void RabbitMQEventTest(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void APIGatewayCustomAuthorizerV2Request(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -3655,10 +3513,8 @@ public void APIGatewayCustomAuthorizerV2Request(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void APIGatewayCustomAuthorizerV2SimpleResponse(Type serializerType) { var response = new APIGatewayCustomAuthorizerV2SimpleResponse @@ -3680,10 +3536,8 @@ public void APIGatewayCustomAuthorizerV2SimpleResponse(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void APIGatewayCustomAuthorizerV2IamResponse(Type serializerType) { var response = new APIGatewayCustomAuthorizerV2IamResponse @@ -3695,7 +3549,7 @@ public void APIGatewayCustomAuthorizerV2IamResponse(Type serializerType) { new APIGatewayCustomAuthorizerPolicy.IAMPolicyStatement { - Action = new HashSet { "execute-api:Invoke" }, + Action = ["execute-api:Invoke"], Effect = "Allow", Resource = new HashSet{ "arn:aws:execute-api:{regionId}:{accountId}:{apiId}/{stage}/{httpVerb}/[{resource}/[{child-resources}]]" } } @@ -3757,10 +3611,8 @@ public void SerializeWithCamelCaseNamingStrategyCanDeserializeBothCamelAndPascal [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void CloudWatchEventsS3ObjectCreate(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -3787,10 +3639,8 @@ public void CloudWatchEventsS3ObjectCreate(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void CloudWatchEventsS3ObjectDelete(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -3817,10 +3667,8 @@ public void CloudWatchEventsS3ObjectDelete(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void CloudWatchEventsS3ObjectRestore(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -3846,10 +3694,8 @@ public void CloudWatchEventsS3ObjectRestore(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void CloudWatchTranscribeJobStateChangeCompleted(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -3868,10 +3714,8 @@ public void CloudWatchTranscribeJobStateChangeCompleted(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void CloudWatchTranscribeJobStateChangeFailed(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -3890,10 +3734,8 @@ public void CloudWatchTranscribeJobStateChangeFailed(Type serializerType) [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void CloudWatchTranslateTextTranslationJobStateChange(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -3911,10 +3753,8 @@ public void CloudWatchTranslateTextTranslationJobStateChange(Type serializerType [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void CloudWatchTranslateParallelDataStateChangeCreate(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -3933,10 +3773,8 @@ public void CloudWatchTranslateParallelDataStateChangeCreate(Type serializerType [Theory] [InlineData(typeof(JsonSerializer))] -#if NETCOREAPP3_1_OR_GREATER [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.LambdaJsonSerializer))] [InlineData(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] -#endif public void CloudWatchTranslateParallelDataStateChangeUpdate(Type serializerType) { var serializer = Activator.CreateInstance(serializerType) as ILambdaSerializer; @@ -3967,7 +3805,7 @@ public void TestJsonIncludeNullValueSerializer() SomeOtherValue = null }; - MemoryStream ms = new MemoryStream(); + var ms = new MemoryStream(); serializer.Serialize(response, ms); ms.Position = 0; var json = new StreamReader(ms).ReadToEnd(); diff --git a/Libraries/test/IntegrationTests.Helpers/CloudFormationHelper.cs b/Libraries/test/IntegrationTests.Helpers/CloudFormationHelper.cs index 43f4005c1..00d5f5773 100644 --- a/Libraries/test/IntegrationTests.Helpers/CloudFormationHelper.cs +++ b/Libraries/test/IntegrationTests.Helpers/CloudFormationHelper.cs @@ -18,7 +18,7 @@ public CloudFormationHelper(IAmazonCloudFormation cloudFormationClient) public async Task GetStackStatusAsync(string stackName) { var stack = await GetStackAsync(stackName); - return stack?.StackStatus; + return stack?.StackStatus ?? StackStatus.CREATE_FAILED; } public async Task IsDeletedAsync(string stackName) @@ -60,15 +60,19 @@ public async Task DeleteStackAsync(string stackName) { 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; + // Use DescribeStackResource (singular) to query a specific resource by logical ID. + // DescribeStackResources (plural) returns at most 100 resources without pagination, + // which causes resources to be silently missed in large stacks (>100 resources). + var response = await _cloudFormationClient.DescribeStackResourceAsync( + new DescribeStackResourceRequest + { + StackName = stackName, + LogicalResourceId = logicalResourceId + }); + + var physicalId = response.StackResourceDetail?.PhysicalResourceId; + + Console.WriteLine($"[CloudFormationHelper] Resource '{logicalResourceId}' in stack '{stackName}': PhysicalId={physicalId}, Status={response.StackResourceDetail?.ResourceStatus}"); if (physicalId == null) { diff --git a/Libraries/test/IntegrationTests.Helpers/IntegrationTests.Helpers.csproj b/Libraries/test/IntegrationTests.Helpers/IntegrationTests.Helpers.csproj index c1f9c663b..6ac30f29c 100644 --- a/Libraries/test/IntegrationTests.Helpers/IntegrationTests.Helpers.csproj +++ b/Libraries/test/IntegrationTests.Helpers/IntegrationTests.Helpers.csproj @@ -1,16 +1,16 @@ - net6.0 + net8.0 disable enable - - - - + + + + diff --git a/Libraries/test/IntegrationTests.Helpers/LambdaHelper.cs b/Libraries/test/IntegrationTests.Helpers/LambdaHelper.cs index 6436c7c7b..2a6d70f6c 100644 --- a/Libraries/test/IntegrationTests.Helpers/LambdaHelper.cs +++ b/Libraries/test/IntegrationTests.Helpers/LambdaHelper.cs @@ -1,3 +1,6 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + using System.Collections.Generic; using System.Threading.Tasks; using Amazon.Lambda; @@ -55,20 +58,35 @@ public async Task ListEventSourceMappingsAsync( }); } + public async Task GetFunctionUrlConfigAsync(string functionName) + { + return await _lambdaClient.GetFunctionUrlConfigAsync(new GetFunctionUrlConfigRequest + { + FunctionName = functionName + }); + } + public async Task WaitTillNotPending(List functions) { foreach (var function in functions) { while (true) { - var response = await _lambdaClient.GetFunctionConfigurationAsync(new GetFunctionConfigurationRequest { FunctionName = function }); - if (response.State == State.Pending) + try { - await Task.Delay(1000); + var response = await _lambdaClient.GetFunctionConfigurationAsync(new GetFunctionConfigurationRequest { FunctionName = function }); + if (response.State == State.Pending) + { + await Task.Delay(1000); + } + else + { + break; + } } - else + catch(TooManyRequestsException) { - break; + await Task.Delay(10000); } } } diff --git a/Libraries/test/PowerShellTests/ExceptionHandlingTests.cs b/Libraries/test/PowerShellTests/ExceptionHandlingTests.cs index 2adc059a3..b4e7221c8 100644 --- a/Libraries/test/PowerShellTests/ExceptionHandlingTests.cs +++ b/Libraries/test/PowerShellTests/ExceptionHandlingTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Text; @@ -115,7 +115,7 @@ private void ExceptionValidator(PowerShellScriptsAsFunctions.Function function, } Assert.NotNull(foundException); - Assert.True(foundException.GetType().Name.EndsWith(exceptionType)); + Assert.EndsWith(exceptionType, foundException.GetType().Name); if(message != null) { diff --git a/Libraries/test/PowerShellTests/PowerShellTests.csproj b/Libraries/test/PowerShellTests/PowerShellTests.csproj index c97458cf5..8d38a5470 100644 --- a/Libraries/test/PowerShellTests/PowerShellTests.csproj +++ b/Libraries/test/PowerShellTests/PowerShellTests.csproj @@ -13,12 +13,12 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Libraries/test/PowerShellTests/ScriptInvokeTests.cs b/Libraries/test/PowerShellTests/ScriptInvokeTests.cs index 2fa5e0e09..4e65f2cbf 100644 --- a/Libraries/test/PowerShellTests/ScriptInvokeTests.cs +++ b/Libraries/test/PowerShellTests/ScriptInvokeTests.cs @@ -123,7 +123,7 @@ public void UseAWSPowerShellCmdLetTest() Assert.Contains("AWS Lambda", resultString); } -#if NETCOREAPP3_1_OR_GREATER +#if NET8_0_OR_GREATER [Fact] public void ForObjectParallelTest() { diff --git a/Libraries/test/SnapshotRestore.Registry.Tests/SnapshotRestore.Registry.Tests.csproj b/Libraries/test/SnapshotRestore.Registry.Tests/SnapshotRestore.Registry.Tests.csproj index dd41e38a4..9d3345666 100644 --- a/Libraries/test/SnapshotRestore.Registry.Tests/SnapshotRestore.Registry.Tests.csproj +++ b/Libraries/test/SnapshotRestore.Registry.Tests/SnapshotRestore.Registry.Tests.csproj @@ -7,12 +7,12 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Libraries/test/TestCustomAuthorizerApp.IntegrationTests/IntegrationTestContextFixtureCollection.cs b/Libraries/test/TestCustomAuthorizerApp.IntegrationTests/IntegrationTestContextFixtureCollection.cs index f90a2401b..dd673e7b9 100644 --- a/Libraries/test/TestCustomAuthorizerApp.IntegrationTests/IntegrationTestContextFixtureCollection.cs +++ b/Libraries/test/TestCustomAuthorizerApp.IntegrationTests/IntegrationTestContextFixtureCollection.cs @@ -2,7 +2,7 @@ namespace TestCustomAuthorizerApp.IntegrationTests; -[CollectionDefinition("Integration Tests")] +[CollectionDefinition("Integration Tests", DisableParallelization = true)] public class IntegrationTestContextFixtureCollection : ICollectionFixture { // This class has no code, and is never created. Its purpose is simply diff --git a/Libraries/test/TestCustomAuthorizerApp.IntegrationTests/NonStringAuthorizerTests.cs b/Libraries/test/TestCustomAuthorizerApp.IntegrationTests/NonStringAuthorizerTests.cs index 7b58cfd64..0d25145dd 100644 --- a/Libraries/test/TestCustomAuthorizerApp.IntegrationTests/NonStringAuthorizerTests.cs +++ b/Libraries/test/TestCustomAuthorizerApp.IntegrationTests/NonStringAuthorizerTests.cs @@ -105,7 +105,7 @@ public async Task NonStringUserInfo_IntValueIsCorrectType() // Verify TenantId is returned as a number, not a string var tenantIdToken = json["TenantId"]; Assert.NotNull(tenantIdToken); - Assert.Equal(JTokenType.Integer, tenantIdToken.Type); + Assert.Equal(JTokenType.Integer, tenantIdToken!.Type); } /// @@ -129,6 +129,6 @@ public async Task NonStringUserInfo_BoolValueIsCorrectType() // Verify IsAdmin is returned as a boolean, not a string var isAdminToken = json["IsAdmin"]; Assert.NotNull(isAdminToken); - Assert.Equal(JTokenType.Boolean, isAdminToken.Type); + Assert.Equal(JTokenType.Boolean, isAdminToken!.Type); } } diff --git a/Libraries/test/TestCustomAuthorizerApp.IntegrationTests/TestCustomAuthorizerApp.IntegrationTests.csproj b/Libraries/test/TestCustomAuthorizerApp.IntegrationTests/TestCustomAuthorizerApp.IntegrationTests.csproj index 9271cfe61..02540483e 100644 --- a/Libraries/test/TestCustomAuthorizerApp.IntegrationTests/TestCustomAuthorizerApp.IntegrationTests.csproj +++ b/Libraries/test/TestCustomAuthorizerApp.IntegrationTests/TestCustomAuthorizerApp.IntegrationTests.csproj @@ -1,7 +1,7 @@ - + - net8.0 + net8.0;net10.0 enable enable Library @@ -9,11 +9,11 @@ - - - - - + + + + + all runtime; build; native; contentfiles; analyzers diff --git a/Libraries/test/TestCustomAuthorizerApp/TestCustomAuthorizerApp.csproj b/Libraries/test/TestCustomAuthorizerApp/TestCustomAuthorizerApp.csproj index 6abb25b6c..1b49e1c0e 100644 --- a/Libraries/test/TestCustomAuthorizerApp/TestCustomAuthorizerApp.csproj +++ b/Libraries/test/TestCustomAuthorizerApp/TestCustomAuthorizerApp.csproj @@ -1,6 +1,6 @@ - net6.0 + net10.0 enable enable true diff --git a/Libraries/test/TestCustomAuthorizerApp/serverless.template b/Libraries/test/TestCustomAuthorizerApp/serverless.template index a9bd4fc47..7b31f165f 100644 --- a/Libraries/test/TestCustomAuthorizerApp/serverless.template +++ b/Libraries/test/TestCustomAuthorizerApp/serverless.template @@ -1,7 +1,7 @@ { "AWSTemplateFormatVersion": "2010-09-09", "Transform": "AWS::Serverless-2016-10-31", - "Description": "This template is partially managed by Amazon.Lambda.Annotations (v1.12.0.0).", + "Description": "This template is partially managed by Amazon.Lambda.Annotations (v2.0.1.0).", "Resources": { "AnnotationsHttpApi": { "Type": "AWS::Serverless::HttpApi", @@ -111,7 +111,7 @@ "Tool": "Amazon.Lambda.Annotations" }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -128,7 +128,7 @@ "Tool": "Amazon.Lambda.Annotations" }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -145,7 +145,7 @@ "Tool": "Amazon.Lambda.Annotations" }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -162,7 +162,7 @@ "Tool": "Amazon.Lambda.Annotations" }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -179,7 +179,7 @@ "Tool": "Amazon.Lambda.Annotations" }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -207,7 +207,7 @@ } }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -250,7 +250,7 @@ } }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -292,7 +292,7 @@ } }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -332,7 +332,7 @@ } }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -376,7 +376,7 @@ } }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -420,7 +420,7 @@ } }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -463,7 +463,7 @@ } }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -506,7 +506,7 @@ } }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -549,7 +549,7 @@ } }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, diff --git a/Libraries/test/TestCustomAuthorizerApp/src/Function/serverless.template b/Libraries/test/TestCustomAuthorizerApp/src/Function/serverless.template index 59a8256fb..e4c7c9b2a 100644 --- a/Libraries/test/TestCustomAuthorizerApp/src/Function/serverless.template +++ b/Libraries/test/TestCustomAuthorizerApp/src/Function/serverless.template @@ -1,7 +1,7 @@ { "AWSTemplateFormatVersion": "2010-09-09", "Transform": "AWS::Serverless-2016-10-31", - "Description": "This template is partially managed by Amazon.Lambda.Annotations (v1.9.0.0).", + "Description": "This template is partially managed by Amazon.Lambda.Annotations (v1.14.0.0).", "Resources": { "AnnotationsHttpApi": { "Type": "AWS::Serverless::HttpApi", @@ -11,7 +11,7 @@ "Properties": { "Auth": { "Authorizers": { - "HttpApiAuthorize": { + "CustomAuthorizer": { "FunctionArn": { "Fn::GetAtt": [ "CustomAuthorizer", @@ -28,7 +28,7 @@ "EnableFunctionDefaultPermissions": true, "AuthorizerResultTtlInSeconds": 0 }, - "HttpApiAuthorizeV1": { + "CustomAuthorizerV1": { "FunctionArn": { "Fn::GetAtt": [ "CustomAuthorizerV1", @@ -45,7 +45,7 @@ "EnableFunctionDefaultPermissions": true, "AuthorizerResultTtlInSeconds": 0 }, - "SimpleHttpApiAuthorize": { + "SimpleAuthorizer": { "FunctionArn": { "Fn::GetAtt": [ "SimpleAuthorizer", @@ -72,7 +72,7 @@ "StageName": "Prod", "Auth": { "Authorizers": { - "RestApiAuthorize": { + "RestApiAuthorizer": { "FunctionArn": { "Fn::GetAtt": [ "RestApiAuthorizer", @@ -85,7 +85,7 @@ "FunctionPayloadType": "TOKEN", "AuthorizerResultTtlInSeconds": 0 }, - "SimpleRestApiAuthorize": { + "SimpleRestAuthorizer": { "FunctionArn": { "Fn::GetAtt": [ "SimpleRestAuthorizer", @@ -111,7 +111,7 @@ "Tool": "Amazon.Lambda.Annotations" }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -128,7 +128,7 @@ "Tool": "Amazon.Lambda.Annotations" }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -145,7 +145,7 @@ "Tool": "Amazon.Lambda.Annotations" }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -162,7 +162,7 @@ "Tool": "Amazon.Lambda.Annotations" }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -179,7 +179,7 @@ "Tool": "Amazon.Lambda.Annotations" }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -207,7 +207,7 @@ } }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -223,7 +223,7 @@ "Path": "/api/protected", "Method": "GET", "Auth": { - "Authorizer": "HttpApiAuthorize" + "Authorizer": "CustomAuthorizer" }, "ApiId": { "Ref": "AnnotationsHttpApi" @@ -250,7 +250,7 @@ } }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -266,7 +266,7 @@ "Path": "/api/user-info", "Method": "GET", "Auth": { - "Authorizer": "HttpApiAuthorize" + "Authorizer": "CustomAuthorizer" }, "ApiId": { "Ref": "AnnotationsHttpApi" @@ -292,7 +292,7 @@ } }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -332,7 +332,7 @@ } }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -348,7 +348,7 @@ "Path": "/api/rest-user-info", "Method": "GET", "Auth": { - "Authorizer": "RestApiAuthorize" + "Authorizer": "RestApiAuthorizer" }, "RestApiId": { "Ref": "AnnotationsRestApi" @@ -376,7 +376,7 @@ } }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -393,7 +393,7 @@ "Method": "GET", "PayloadFormatVersion": "1.0", "Auth": { - "Authorizer": "HttpApiAuthorizeV1" + "Authorizer": "CustomAuthorizerV1" }, "ApiId": { "Ref": "AnnotationsHttpApi" @@ -420,7 +420,7 @@ } }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -436,7 +436,7 @@ "Path": "/api/ihttpresult-user-info", "Method": "GET", "Auth": { - "Authorizer": "HttpApiAuthorize" + "Authorizer": "CustomAuthorizer" }, "ApiId": { "Ref": "AnnotationsHttpApi" @@ -463,7 +463,7 @@ } }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -479,7 +479,7 @@ "Path": "/api/simple-httpapi-user-info", "Method": "GET", "Auth": { - "Authorizer": "SimpleHttpApiAuthorize" + "Authorizer": "SimpleAuthorizer" }, "ApiId": { "Ref": "AnnotationsHttpApi" @@ -506,7 +506,7 @@ } }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -522,7 +522,7 @@ "Path": "/api/simple-restapi-user-info", "Method": "GET", "Auth": { - "Authorizer": "SimpleRestApiAuthorize" + "Authorizer": "SimpleRestAuthorizer" }, "RestApiId": { "Ref": "AnnotationsRestApi" @@ -549,7 +549,7 @@ } }, "Properties": { - "Runtime": "dotnet6", + "Runtime": "dotnet10", "CodeUri": ".", "MemorySize": 512, "Timeout": 30, @@ -565,7 +565,7 @@ "Path": "/api/nonstring-user-info", "Method": "GET", "Auth": { - "Authorizer": "HttpApiAuthorize" + "Authorizer": "CustomAuthorizer" }, "ApiId": { "Ref": "AnnotationsHttpApi" diff --git a/Libraries/test/TestExecutableServerlessApp/NullableReferenceTypeExample.cs b/Libraries/test/TestExecutableServerlessApp/NullableReferenceTypeExample.cs index 9b930176d..a50a6c57a 100644 --- a/Libraries/test/TestExecutableServerlessApp/NullableReferenceTypeExample.cs +++ b/Libraries/test/TestExecutableServerlessApp/NullableReferenceTypeExample.cs @@ -1,4 +1,4 @@ -using Amazon.Lambda.Annotations; +using Amazon.Lambda.Annotations; using Amazon.Lambda.Annotations.APIGateway; using Amazon.Lambda.Core; @@ -8,7 +8,7 @@ public class NullableReferenceTypeExample { [LambdaFunction(PackageType = LambdaPackageType.Image)] [HttpApi(LambdaHttpMethod.Get, "/nullableheaderhttpapi")] - public void NullableHeaderHttpApi([FromHeader(Name = "MyHeader")] string? text, ILambdaContext context) + public void NullableHeaderHttpApi([FromHeader(Name = "MyHeader")] string text, ILambdaContext context) { context.Logger.LogLine(text); } diff --git a/Libraries/test/TestExecutableServerlessApp/ParameterlessTaskMethods.cs b/Libraries/test/TestExecutableServerlessApp/ParameterlessTaskMethods.cs new file mode 100644 index 000000000..10a31cb1c --- /dev/null +++ b/Libraries/test/TestExecutableServerlessApp/ParameterlessTaskMethods.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using Amazon.Lambda.Annotations; + +namespace TestServerlessApp +{ + public class ParameterlessTaskMethods + { + [LambdaFunction] + public async Task NoParameterTask() + { + await Task.Delay(0); + } + } +} diff --git a/Libraries/test/TestExecutableServerlessApp/TestExecutableServerlessApp.csproj b/Libraries/test/TestExecutableServerlessApp/TestExecutableServerlessApp.csproj index 0fa9708b4..ba6eba8e9 100644 --- a/Libraries/test/TestExecutableServerlessApp/TestExecutableServerlessApp.csproj +++ b/Libraries/test/TestExecutableServerlessApp/TestExecutableServerlessApp.csproj @@ -1,7 +1,7 @@  exe - net6.0 + net10.0 true Lambda @@ -19,7 +19,7 @@ - + diff --git a/Libraries/test/TestExecutableServerlessApp/serverless.template b/Libraries/test/TestExecutableServerlessApp/serverless.template index 65ce726fb..5623bc11f 100644 --- a/Libraries/test/TestExecutableServerlessApp/serverless.template +++ b/Libraries/test/TestExecutableServerlessApp/serverless.template @@ -1,7 +1,7 @@ { "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.12.0.0).", + "Description": "An AWS Serverless Application. This template is partially managed by Amazon.Lambda.Annotations (v2.0.1.0).", "Parameters": { "ArchitectureTypeParameter": { "Type": "String", @@ -22,19 +22,10 @@ } }, "Resources": { - "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderGenerated": { + "TestServerlessAppTaskExampleTaskReturnGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } + "Tool": "Amazon.Lambda.Annotations" }, "Properties": { "MemorySize": 512, @@ -51,21 +42,56 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "OkResponseWithHeader" + "ANNOTATIONS_HANDLER": "TaskReturn" } - }, - "Events": { - "RootGet": { - "Type": "Api", - "Properties": { - "Path": "/okresponsewithheader/{x}", - "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" } } } }, - "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderAsyncGenerated": { + "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" + } + } + } + }, + "TestServerlessAppNullableReferenceTypeExampleNullableHeaderHttpApiGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -94,33 +120,49 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "OkResponseWithHeaderAsync" + "ANNOTATIONS_HANDLER": "NullableHeaderHttpApi" } }, "Events": { "RootGet": { - "Type": "Api", + "Type": "HttpApi", "Properties": { - "Path": "/okresponsewithheaderasync/{x}", + "Path": "/nullableheaderhttpapi", "Method": "GET" } } } } }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2Generated": { + "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": [ + "TestExecutableServerlessApp" ] + }, + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "DynamicReturn" + } } + } + }, + "TestServerlessAppDynamicExampleDynamicInputGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" }, "Properties": { "MemorySize": 512, @@ -137,21 +179,37 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV2" + "ANNOTATIONS_HANDLER": "DynamicInput" } + } + } + }, + "ToUpper": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestExecutableServerlessApp" + ] }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/notfoundwithheaderv2/{x}", - "Method": "GET" - } + "Environment": { + "Variables": { + "ANNOTATIONS_HANDLER": "ToUpper" } } } }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2AsyncGenerated": { + "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -180,21 +238,21 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV2Async" + "ANNOTATIONS_HANDLER": "OkResponseWithHeader" } }, "Events": { "RootGet": { - "Type": "HttpApi", + "Type": "Api", "Properties": { - "Path": "/notfoundwithheaderv2async/{x}", + "Path": "/okresponsewithheader/{x}", "Method": "GET" } } } } }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1Generated": { + "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderAsyncGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -204,8 +262,7 @@ "SyncedEventProperties": { "RootGet": [ "Path", - "Method", - "PayloadFormatVersion" + "Method" ] } }, @@ -224,22 +281,21 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV1" + "ANNOTATIONS_HANDLER": "OkResponseWithHeaderAsync" } }, "Events": { "RootGet": { - "Type": "HttpApi", + "Type": "Api", "Properties": { - "Path": "/notfoundwithheaderv1/{x}", - "Method": "GET", - "PayloadFormatVersion": "1.0" + "Path": "/okresponsewithheaderasync/{x}", + "Method": "GET" } } } } }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1AsyncGenerated": { + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2Generated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -249,8 +305,7 @@ "SyncedEventProperties": { "RootGet": [ "Path", - "Method", - "PayloadFormatVersion" + "Method" ] } }, @@ -269,50 +324,33 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV1Async" + "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV2" } }, "Events": { "RootGet": { "Type": "HttpApi", "Properties": { - "Path": "/notfoundwithheaderv1async/{x}", - "Method": "GET", - "PayloadFormatVersion": "1.0" + "Path": "/notfoundwithheaderv2/{x}", + "Method": "GET" } } } } }, - "TestServerlessAppDynamicExampleDynamicReturnGenerated": { + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2AsyncGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestExecutableServerlessApp" + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" ] - }, - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "DynamicReturn" - } } - } - }, - "TestServerlessAppDynamicExampleDynamicInputGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" }, "Properties": { "MemorySize": 512, @@ -329,12 +367,21 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "DynamicInput" + "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV2Async" + } + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/notfoundwithheaderv2async/{x}", + "Method": "GET" + } } } } }, - "GreeterSayHello": { + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1Generated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -350,7 +397,7 @@ } }, "Properties": { - "MemorySize": 1024, + "MemorySize": 512, "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" @@ -364,14 +411,14 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "SayHello" + "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV1" } }, "Events": { "RootGet": { "Type": "HttpApi", "Properties": { - "Path": "/Greeter/SayHello", + "Path": "/notfoundwithheaderv1/{x}", "Method": "GET", "PayloadFormatVersion": "1.0" } @@ -379,7 +426,7 @@ } } }, - "GreeterSayHelloAsync": { + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1AsyncGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -396,7 +443,7 @@ }, "Properties": { "MemorySize": 512, - "Timeout": 50, + "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" ], @@ -409,14 +456,14 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "SayHelloAsync" + "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV1Async" } }, "Events": { "RootGet": { "Type": "HttpApi", "Properties": { - "Path": "/Greeter/SayHelloAsync", + "Path": "/notfoundwithheaderv1async/{x}", "Method": "GET", "PayloadFormatVersion": "1.0" } @@ -424,13 +471,23 @@ } } }, - "TestServerlessAppIntrinsicExampleHasIntrinsicGenerated": { + "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" @@ -444,12 +501,22 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "HasIntrinsic" + "ANNOTATIONS_HANDLER": "SayHello" + } + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/Greeter/SayHello", + "Method": "GET", + "PayloadFormatVersion": "1.0" + } } } } }, - "TestServerlessAppNullableReferenceTypeExampleNullableHeaderHttpApiGenerated": { + "GreeterSayHelloAsync": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -459,13 +526,14 @@ "SyncedEventProperties": { "RootGet": [ "Path", - "Method" + "Method", + "PayloadFormatVersion" ] } }, "Properties": { "MemorySize": 512, - "Timeout": 30, + "Timeout": 50, "Policies": [ "AWSLambdaBasicExecutionRole" ], @@ -478,42 +546,21 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "NullableHeaderHttpApi" + "ANNOTATIONS_HANDLER": "SayHelloAsync" } }, "Events": { "RootGet": { "Type": "HttpApi", "Properties": { - "Path": "/nullableheaderhttpapi", - "Method": "GET" + "Path": "/Greeter/SayHelloAsync", + "Method": "GET", + "PayloadFormatVersion": "1.0" } } } } }, - "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": { @@ -536,19 +583,10 @@ } } }, - "TestExecutableServerlessAppSourceGenerationSerializationExampleGetPersonGenerated": { + "TestServerlessAppVoidExampleVoidReturnGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } + "Tool": "Amazon.Lambda.Annotations" }, "Properties": { "MemorySize": 512, @@ -565,21 +603,12 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "GetPerson" - } - }, - "Events": { - "RootGet": { - "Type": "Api", - "Properties": { - "Path": "/", - "Method": "GET" - } + "ANNOTATIONS_HANDLER": "VoidReturn" } } } }, - "ToUpper": { + "TestServerlessAppIntrinsicExampleHasIntrinsicGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations" @@ -599,37 +628,24 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "ToUpper" + "ANNOTATIONS_HANDLER": "HasIntrinsic" } } } }, - "ToLower": { + "TestExecutableServerlessAppSourceGenerationSerializationExampleGetPersonGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "Runtime": "provided.al2", - "CodeUri": ".", - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" ], - "PackageType": "Zip", - "Handler": "TestExecutableServerlessApp", - "Environment": { - "Variables": { - "ANNOTATIONS_HANDLER": "ToLower" - } + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] } - } - }, - "TestServerlessAppTaskExampleTaskReturnGenerated": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" }, "Properties": { "MemorySize": 512, @@ -646,32 +662,38 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "TaskReturn" + "ANNOTATIONS_HANDLER": "GetPerson" + } + }, + "Events": { + "RootGet": { + "Type": "Api", + "Properties": { + "Path": "/", + "Method": "GET" + } } } } }, - "TestServerlessAppVoidExampleVoidReturnGenerated": { + "TestServerlessAppParameterlessTaskMethodsNoParameterTaskGenerated": { "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": "VoidReturn" + "ANNOTATIONS_HANDLER": "NoParameterTask" } } } diff --git a/Libraries/test/TestFunction/TestFunction.csproj b/Libraries/test/TestFunction/TestFunction.csproj index 98b0093d3..d15b5ec17 100644 --- a/Libraries/test/TestFunction/TestFunction.csproj +++ b/Libraries/test/TestFunction/TestFunction.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net10.0 TestFunction Library TestFunction diff --git a/Libraries/test/TestFunctionFSharp/FSharpJsonSerializer/FSharpJsonSerializer.csproj b/Libraries/test/TestFunctionFSharp/FSharpJsonSerializer/FSharpJsonSerializer.csproj index 2f2a4d5c5..3371f242e 100644 --- a/Libraries/test/TestFunctionFSharp/FSharpJsonSerializer/FSharpJsonSerializer.csproj +++ b/Libraries/test/TestFunctionFSharp/FSharpJsonSerializer/FSharpJsonSerializer.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + net8.0;net10.0 FSharpJsonSerializer Library FSharpJsonSerializer diff --git a/Libraries/test/TestFunctionFSharp/TestFunctionFSharp/TestFunctionFSharp.fsproj b/Libraries/test/TestFunctionFSharp/TestFunctionFSharp/TestFunctionFSharp.fsproj index 094d04fd7..693b53bd5 100644 --- a/Libraries/test/TestFunctionFSharp/TestFunctionFSharp/TestFunctionFSharp.fsproj +++ b/Libraries/test/TestFunctionFSharp/TestFunctionFSharp/TestFunctionFSharp.fsproj @@ -18,7 +18,6 @@ - diff --git a/Libraries/test/TestMinimalAPIApp/TestMinimalAPIApp.csproj b/Libraries/test/TestMinimalAPIApp/TestMinimalAPIApp.csproj index 60080ae84..94bb22bb2 100644 --- a/Libraries/test/TestMinimalAPIApp/TestMinimalAPIApp.csproj +++ b/Libraries/test/TestMinimalAPIApp/TestMinimalAPIApp.csproj @@ -1,7 +1,7 @@  - net6.0 + net10.0 enable enable diff --git a/Libraries/test/TestServerlessApp.ALB.IntegrationTests/TestServerlessApp.ALB.IntegrationTests.csproj b/Libraries/test/TestServerlessApp.ALB.IntegrationTests/TestServerlessApp.ALB.IntegrationTests.csproj index ceb0564e5..b9537f341 100644 --- a/Libraries/test/TestServerlessApp.ALB.IntegrationTests/TestServerlessApp.ALB.IntegrationTests.csproj +++ b/Libraries/test/TestServerlessApp.ALB.IntegrationTests/TestServerlessApp.ALB.IntegrationTests.csproj @@ -1,18 +1,19 @@ - net6.0 + net10.0 false - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + diff --git a/Libraries/test/TestServerlessApp.ALB/TestServerlessApp.ALB.csproj b/Libraries/test/TestServerlessApp.ALB/TestServerlessApp.ALB.csproj index 9f9e5630c..cbbc51785 100644 --- a/Libraries/test/TestServerlessApp.ALB/TestServerlessApp.ALB.csproj +++ b/Libraries/test/TestServerlessApp.ALB/TestServerlessApp.ALB.csproj @@ -1,6 +1,6 @@ - net6.0 + net10.0 true Lambda true diff --git a/Libraries/test/TestServerlessApp.ALB/serverless.template b/Libraries/test/TestServerlessApp.ALB/serverless.template index 71b1349ae..df0a3279a 100644 --- a/Libraries/test/TestServerlessApp.ALB/serverless.template +++ b/Libraries/test/TestServerlessApp.ALB/serverless.template @@ -1,7 +1,7 @@ { "AWSTemplateFormatVersion": "2010-09-09", "Transform": "AWS::Serverless-2016-10-31", - "Description": "ALB Integration Test Stack - VPC and ALB infrastructure for testing Lambda ALB annotations This template is partially managed by Amazon.Lambda.Annotations (v1.12.0.0).", + "Description": "ALB Integration Test Stack - VPC and ALB infrastructure for testing Lambda ALB annotations This template is partially managed by Amazon.Lambda.Annotations (v2.0.1.0).", "Resources": { "ALBTestVPC": { "Type": "AWS::EC2::VPC", @@ -167,28 +167,6 @@ ] } }, - "ALBHello": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedAlbResources": [ - "ALBHelloALBPermission", - "ALBHelloALBTargetGroup", - "ALBHelloALBListenerRule" - ] - }, - "Properties": { - "Runtime": "dotnet6", - "CodeUri": ".", - "MemorySize": 256, - "Timeout": 15, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Zip", - "Handler": "TestServerlessApp.ALB::TestServerlessApp.ALB.ALBFunctions_Hello_Generated::Hello" - } - }, "ALBHelloALBPermission": { "Type": "AWS::Lambda::Permission", "Metadata": { @@ -255,28 +233,6 @@ } } }, - "ALBHealth": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedAlbResources": [ - "ALBHealthALBPermission", - "ALBHealthALBTargetGroup", - "ALBHealthALBListenerRule" - ] - }, - "Properties": { - "Runtime": "dotnet6", - "CodeUri": ".", - "MemorySize": 128, - "Timeout": 5, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Zip", - "Handler": "TestServerlessApp.ALB::TestServerlessApp.ALB.ALBFunctions_Health_Generated::Health" - } - }, "ALBHealthALBPermission": { "Type": "AWS::Lambda::Permission", "Metadata": { @@ -343,28 +299,6 @@ } } }, - "ALBGreeting": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedAlbResources": [ - "ALBGreetingALBPermission", - "ALBGreetingALBTargetGroup", - "ALBGreetingALBListenerRule" - ] - }, - "Properties": { - "Runtime": "dotnet6", - "CodeUri": ".", - "MemorySize": 256, - "Timeout": 15, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Zip", - "Handler": "TestServerlessApp.ALB::TestServerlessApp.ALB.ALBFunctions_Greeting_Generated::Greeting" - } - }, "ALBGreetingALBPermission": { "Type": "AWS::Lambda::Permission", "Metadata": { @@ -431,28 +365,6 @@ } } }, - "ALBCreateItem": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedAlbResources": [ - "ALBCreateItemALBPermission", - "ALBCreateItemALBTargetGroup", - "ALBCreateItemALBListenerRule" - ] - }, - "Properties": { - "Runtime": "dotnet6", - "CodeUri": ".", - "MemorySize": 256, - "Timeout": 15, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Zip", - "Handler": "TestServerlessApp.ALB::TestServerlessApp.ALB.ALBFunctions_CreateItem_Generated::CreateItem" - } - }, "ALBCreateItemALBPermission": { "Type": "AWS::Lambda::Permission", "Metadata": { @@ -526,6 +438,94 @@ "Ref": "ALBTestListener" } } + }, + "ALBHello": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedAlbResources": [ + "ALBHelloALBPermission", + "ALBHelloALBTargetGroup", + "ALBHelloALBListenerRule" + ] + }, + "Properties": { + "Runtime": "dotnet10", + "CodeUri": ".", + "MemorySize": 256, + "Timeout": 15, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestServerlessApp.ALB::TestServerlessApp.ALB.ALBFunctions_Hello_Generated::Hello" + } + }, + "ALBHealth": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedAlbResources": [ + "ALBHealthALBPermission", + "ALBHealthALBTargetGroup", + "ALBHealthALBListenerRule" + ] + }, + "Properties": { + "Runtime": "dotnet10", + "CodeUri": ".", + "MemorySize": 128, + "Timeout": 5, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestServerlessApp.ALB::TestServerlessApp.ALB.ALBFunctions_Health_Generated::Health" + } + }, + "ALBGreeting": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedAlbResources": [ + "ALBGreetingALBPermission", + "ALBGreetingALBTargetGroup", + "ALBGreetingALBListenerRule" + ] + }, + "Properties": { + "Runtime": "dotnet10", + "CodeUri": ".", + "MemorySize": 256, + "Timeout": 15, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestServerlessApp.ALB::TestServerlessApp.ALB.ALBFunctions_Greeting_Generated::Greeting" + } + }, + "ALBCreateItem": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedAlbResources": [ + "ALBCreateItemALBPermission", + "ALBCreateItemALBTargetGroup", + "ALBCreateItemALBListenerRule" + ] + }, + "Properties": { + "Runtime": "dotnet10", + "CodeUri": ".", + "MemorySize": 256, + "Timeout": 15, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestServerlessApp.ALB::TestServerlessApp.ALB.ALBFunctions_CreateItem_Generated::CreateItem" + } } }, "Outputs": { diff --git a/Libraries/test/TestServerlessApp.IntegrationTests/DeploymentScript.ps1 b/Libraries/test/TestServerlessApp.IntegrationTests/DeploymentScript.ps1 index 5bc3b87fc..bbff35b47 100644 --- a/Libraries/test/TestServerlessApp.IntegrationTests/DeploymentScript.ps1 +++ b/Libraries/test/TestServerlessApp.IntegrationTests/DeploymentScript.ps1 @@ -77,6 +77,45 @@ try Write-Host "Added TestS3Bucket resource to serverless.template" } + # Add TestTopic resource to serverless.template for SNS event integration testing + # The source generator creates a Ref to TestTopic but doesn't define the resource itself + $template = Get-Content $templatePath | Out-String | ConvertFrom-Json + if (-not $template.Resources.PSObject.Properties['TestTopic']) { + $template.Resources | Add-Member -NotePropertyName "TestTopic" -NotePropertyValue @{ Type = "AWS::SNS::Topic" } -Force + $template | ConvertTo-Json -Depth 100 | Set-Content $templatePath + Write-Host "Added TestTopic resource to serverless.template" + } + + # Add TestTable resource to serverless.template for DynamoDB event integration testing + # The source generator creates a Fn::GetAtt reference to TestTable for StreamArn but doesn't define the resource itself + $template = Get-Content $templatePath | Out-String | ConvertFrom-Json + if (-not $template.Resources.PSObject.Properties['TestTable']) { + $testTableResource = @{ + Type = "AWS::DynamoDB::Table" + Properties = @{ + BillingMode = "PAY_PER_REQUEST" + AttributeDefinitions = @( + @{ + AttributeName = "Id" + AttributeType = "S" + } + ) + KeySchema = @( + @{ + AttributeName = "Id" + KeyType = "HASH" + } + ) + StreamSpecification = @{ + StreamViewType = "NEW_AND_OLD_IMAGES" + } + } + } + $template.Resources | Add-Member -NotePropertyName "TestTable" -NotePropertyValue $testTableResource -Force + $template | ConvertTo-Json -Depth 100 | Set-Content $templatePath + Write-Host "Added TestTable 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/DynamoDBEventSourceMapping.cs b/Libraries/test/TestServerlessApp.IntegrationTests/DynamoDBEventSourceMapping.cs new file mode 100644 index 000000000..cef6c76e4 --- /dev/null +++ b/Libraries/test/TestServerlessApp.IntegrationTests/DynamoDBEventSourceMapping.cs @@ -0,0 +1,41 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace TestServerlessApp.IntegrationTests +{ + [Collection("Integration Tests")] + public class DynamoDBEventSourceMapping + { + private readonly IntegrationTestContextFixture _fixture; + + public DynamoDBEventSourceMapping(IntegrationTestContextFixture fixture) + { + _fixture = fixture; + } + + [Fact] + public async Task VerifyDynamoDBEventSourceMappingConfiguration() + { + var lambdaFunctionName = _fixture.LambdaFunctions.FirstOrDefault(x => string.Equals(x.LogicalId, "DynamoDBStreamHandler"))?.Name; + Assert.NotNull(lambdaFunctionName); + + var testTableStreamArn = _fixture.TestTableStreamARN; + Assert.False(string.IsNullOrEmpty(testTableStreamArn), "TestTable stream ARN should not be empty"); + + var listEventSourceMappingResponse = await _fixture.LambdaHelper.ListEventSourceMappingsAsync(lambdaFunctionName, testTableStreamArn); + var eventSourceMappings = listEventSourceMappingResponse.EventSourceMappings; + + Assert.Single(eventSourceMappings); + + var dynamoDbEventSourceMapping = eventSourceMappings.First(); + + Assert.Equal(testTableStreamArn, dynamoDbEventSourceMapping.EventSourceArn); + Assert.Equal(100, dynamoDbEventSourceMapping.BatchSize); + Assert.Equal("TRIM_HORIZON", dynamoDbEventSourceMapping.StartingPosition); + } + } +} diff --git a/Libraries/test/TestServerlessApp.IntegrationTests/FunctionUrlExample.cs b/Libraries/test/TestServerlessApp.IntegrationTests/FunctionUrlExample.cs new file mode 100644 index 000000000..b3f97929b --- /dev/null +++ b/Libraries/test/TestServerlessApp.IntegrationTests/FunctionUrlExample.cs @@ -0,0 +1,103 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace TestServerlessApp.IntegrationTests +{ + [Collection("Integration Tests")] + public class FunctionUrlExample + { + private readonly IntegrationTestContextFixture _fixture; + + public FunctionUrlExample(IntegrationTestContextFixture fixture) + { + _fixture = fixture; + } + + [Fact] + public async Task GetItems_WithCategory_ReturnsOkWithItems() + { + Assert.False(string.IsNullOrEmpty(_fixture.FunctionUrlPrefix), "FunctionUrlPrefix should not be empty. The Function URL was not discovered during setup."); + + var response = await GetWithRetryAsync($"{_fixture.FunctionUrlPrefix}?category=electronics"); + response.EnsureSuccessStatusCode(); + + var content = await response.Content.ReadAsStringAsync(); + var json = JObject.Parse(content); + + Assert.Equal("electronics", json["category"]?.ToString()); + Assert.NotNull(json["items"]); + var items = json["items"].ToObject(); + Assert.Equal(2, items.Length); + Assert.Contains("item1", items); + Assert.Contains("item2", items); + } + + [Fact] + public async Task GetItems_LogsToCloudWatch() + { + Assert.False(string.IsNullOrEmpty(_fixture.FunctionUrlPrefix), "FunctionUrlPrefix should not be empty. The Function URL was not discovered during setup."); + + var response = await GetWithRetryAsync($"{_fixture.FunctionUrlPrefix}?category=books"); + response.EnsureSuccessStatusCode(); + + var lambdaFunctionName = _fixture.LambdaFunctions + .FirstOrDefault(x => string.Equals(x.LogicalId, "TestServerlessAppFunctionUrlExampleGetItemsGenerated"))?.Name; + Assert.False(string.IsNullOrEmpty(lambdaFunctionName)); + + var logGroupName = _fixture.CloudWatchHelper.GetLogGroupName(lambdaFunctionName); + Assert.True( + await _fixture.CloudWatchHelper.MessageExistsInRecentLogEventsAsync("Getting items for category: books", logGroupName, logGroupName), + "Expected log message not found in CloudWatch logs"); + } + + [Fact] + public async Task VerifyFunctionUrlConfig_HasNoneAuthType() + { + var lambdaFunctionName = _fixture.LambdaFunctions + .FirstOrDefault(x => string.Equals(x.LogicalId, "TestServerlessAppFunctionUrlExampleGetItemsGenerated"))?.Name; + Assert.False(string.IsNullOrEmpty(lambdaFunctionName)); + + var functionUrlConfig = await _fixture.LambdaHelper.GetFunctionUrlConfigAsync(lambdaFunctionName); + Assert.NotNull(functionUrlConfig); + Assert.Equal("NONE", functionUrlConfig.AuthType.Value); + Assert.False(string.IsNullOrEmpty(functionUrlConfig.FunctionUrl), "Function URL should not be empty"); + Assert.Contains(".lambda-url.", functionUrlConfig.FunctionUrl); + } + + private async Task GetWithRetryAsync(string url) + { + const int maxAttempts = 10; + HttpResponseMessage response = null; + + for (var attempt = 0; attempt < maxAttempts; attempt++) + { + await Task.Delay(attempt * 1000); + try + { + response = await _fixture.HttpClient.GetAsync(url); + + // If we get a 403 Forbidden, it may be an eventual consistency issue + // with the Function URL permissions propagating. + if (response.StatusCode == System.Net.HttpStatusCode.Forbidden) + continue; + + break; + } + catch + { + if (attempt + 1 == maxAttempts) + throw; + } + } + + return response; + } + } +} diff --git a/Libraries/test/TestServerlessApp.IntegrationTests/Greeter.cs b/Libraries/test/TestServerlessApp.IntegrationTests/Greeter.cs index 8f114d80c..395ebfc29 100644 --- a/Libraries/test/TestServerlessApp.IntegrationTests/Greeter.cs +++ b/Libraries/test/TestServerlessApp.IntegrationTests/Greeter.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; @@ -38,7 +38,7 @@ public async Task SayHelloAsync_FromHeader_LogsToCloudWatch() RequestUri = new Uri($"{_fixture.HttpApiUrlPrefix}/Greeter/SayHelloAsync"), Headers = {{ "names", new List{"Alice", "Bob"}}} }; - var response = _fixture.HttpClient.SendAsync(httpRequestMessage).Result; + var response = await _fixture.HttpClient.SendAsync(httpRequestMessage); response.EnsureSuccessStatusCode(); var lambdaFunctionName = _fixture.LambdaFunctions.FirstOrDefault(x => string.Equals(x.LogicalId, "GreeterSayHelloAsync"))?.Name; Assert.False(string.IsNullOrEmpty(lambdaFunctionName)); @@ -46,4 +46,4 @@ public async Task SayHelloAsync_FromHeader_LogsToCloudWatch() Assert.True(await _fixture.CloudWatchHelper.MessageExistsInRecentLogEventsAsync("Hello Alice, Bob", logGroupName, logGroupName)); } } -} \ No newline at end of file +} diff --git a/Libraries/test/TestServerlessApp.IntegrationTests/IntegrationTestContextFixture.cs b/Libraries/test/TestServerlessApp.IntegrationTests/IntegrationTestContextFixture.cs index 0102424a8..864b72058 100644 --- a/Libraries/test/TestServerlessApp.IntegrationTests/IntegrationTestContextFixture.cs +++ b/Libraries/test/TestServerlessApp.IntegrationTests/IntegrationTestContextFixture.cs @@ -1,3 +1,6 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + using System; using System.Collections.Generic; using System.IO; @@ -30,6 +33,9 @@ public class IntegrationTestContextFixture : IAsyncLifetime public string RestApiUrlPrefix; public string HttpApiUrlPrefix; + public string FunctionUrlPrefix; + public string TestTopicARN; + public string TestTableStreamARN; public string TestQueueARN; public string TestS3BucketName; public List LambdaFunctions; @@ -81,6 +87,21 @@ public async Task InitializeAsync() Assert.False(string.IsNullOrEmpty(queueUrl), $"CloudFormation resource 'TestQueue' was not found in stack '{_stackName}'."); TestQueueARN = ConvertSqsUrlToArn(queueUrl); + // Get the SNS test topic ARN (physical ID is the ARN for SNS topics) + TestTopicARN = await _cloudFormationHelper.GetResourcePhysicalIdAsync(_stackName, "TestTopic"); + Console.WriteLine($"[IntegrationTest] TestTopic ARN: {TestTopicARN}"); + + // Get the DynamoDB table stream ARN + var testTableName = await _cloudFormationHelper.GetResourcePhysicalIdAsync(_stackName, "TestTable"); + Console.WriteLine($"[IntegrationTest] TestTable: {testTableName}"); + if (!string.IsNullOrEmpty(testTableName)) + { + using var dynamoDbClient = new Amazon.DynamoDBv2.AmazonDynamoDBClient(Amazon.RegionEndpoint.USWest2); + var describeTableResponse = await dynamoDbClient.DescribeTableAsync(testTableName); + TestTableStreamARN = describeTableResponse.Table.LatestStreamArn; + Console.WriteLine($"[IntegrationTest] TestTable Stream ARN: {TestTableStreamARN}"); + } + // Get the S3 bucket name from the physical resource ID TestS3BucketName = await _cloudFormationHelper.GetResourcePhysicalIdAsync(_stackName, "TestS3Bucket"); Console.WriteLine($"[IntegrationTest] TestS3Bucket: {TestS3BucketName}"); @@ -90,12 +111,22 @@ public async Task InitializeAsync() Console.WriteLine($"[IntegrationTest] Found {LambdaFunctions.Count} Lambda functions: {string.Join(", ", LambdaFunctions.Select(f => f.Name ?? "(null)"))}"); Assert.True(await _s3Helper.BucketExistsAsync(_bucketName), $"S3 bucket {_bucketName} should exist"); - Assert.Equal(37, LambdaFunctions.Count); + Assert.Equal(41, LambdaFunctions.Count); Assert.False(string.IsNullOrEmpty(RestApiUrlPrefix), "RestApiUrlPrefix should not be empty"); Assert.False(string.IsNullOrEmpty(HttpApiUrlPrefix), "HttpApiUrlPrefix should not be empty"); await LambdaHelper.WaitTillNotPending(LambdaFunctions.Where(x => x.Name != null).Select(x => x.Name).ToList()); + // Discover the Function URL for the FunctionUrlExample function + var functionUrlLambdaName = LambdaFunctions + .FirstOrDefault(x => string.Equals(x.LogicalId, "TestServerlessAppFunctionUrlExampleGetItemsGenerated"))?.Name; + if (!string.IsNullOrEmpty(functionUrlLambdaName)) + { + var functionUrlConfig = await LambdaHelper.GetFunctionUrlConfigAsync(functionUrlLambdaName); + FunctionUrlPrefix = functionUrlConfig.FunctionUrl.TrimEnd('/'); + Console.WriteLine($"[IntegrationTest] FunctionUrlPrefix: {FunctionUrlPrefix}"); + } + // Wait an additional 10 seconds for any other eventually consistency state to finish up. await Task.Delay(10000); } diff --git a/Libraries/test/TestServerlessApp.IntegrationTests/SNSEventSubscription.cs b/Libraries/test/TestServerlessApp.IntegrationTests/SNSEventSubscription.cs new file mode 100644 index 000000000..075a5162b --- /dev/null +++ b/Libraries/test/TestServerlessApp.IntegrationTests/SNSEventSubscription.cs @@ -0,0 +1,44 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Linq; +using System.Threading.Tasks; +using Amazon.SimpleNotificationService; +using Xunit; + +namespace TestServerlessApp.IntegrationTests +{ + [Collection("Integration Tests")] + public class SNSEventSubscription + { + private readonly IntegrationTestContextFixture _fixture; + + public SNSEventSubscription(IntegrationTestContextFixture fixture) + { + _fixture = fixture; + } + + [Fact] + public async Task VerifySNSSubscriptionConfiguration() + { + var lambdaFunctionName = _fixture.LambdaFunctions.FirstOrDefault(x => string.Equals(x.LogicalId, "SNSMessageHandler"))?.Name; + Assert.NotNull(lambdaFunctionName); + + var testTopicArn = _fixture.TestTopicARN; + Assert.False(string.IsNullOrEmpty(testTopicArn), "TestTopic ARN should not be empty"); + + var snsClient = new AmazonSimpleNotificationServiceClient(Amazon.RegionEndpoint.USWest2); + var subscriptions = await snsClient.ListSubscriptionsByTopicAsync(testTopicArn); + + // Find the Lambda subscription + var lambdaSub = subscriptions.Subscriptions.FirstOrDefault(s => + s.Protocol == "lambda" && s.Endpoint.Contains(lambdaFunctionName)); + Assert.NotNull(lambdaSub); + + // Verify filter policy + var attrs = await snsClient.GetSubscriptionAttributesAsync(lambdaSub.SubscriptionArn); + Assert.True(attrs.Attributes.ContainsKey("FilterPolicy")); + Assert.Contains("example_corp", attrs.Attributes["FilterPolicy"]); + } + } +} diff --git a/Libraries/test/TestServerlessApp.IntegrationTests/SQSEventSourceMapping.cs b/Libraries/test/TestServerlessApp.IntegrationTests/SQSEventSourceMapping.cs index e24169f0d..02f803074 100644 --- a/Libraries/test/TestServerlessApp.IntegrationTests/SQSEventSourceMapping.cs +++ b/Libraries/test/TestServerlessApp.IntegrationTests/SQSEventSourceMapping.cs @@ -1,4 +1,7 @@ -using System.Linq; +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Linq; using System.Threading.Tasks; using Xunit; diff --git a/Libraries/test/TestServerlessApp.IntegrationTests/ScheduleEventRule.cs b/Libraries/test/TestServerlessApp.IntegrationTests/ScheduleEventRule.cs new file mode 100644 index 000000000..19ef9e9da --- /dev/null +++ b/Libraries/test/TestServerlessApp.IntegrationTests/ScheduleEventRule.cs @@ -0,0 +1,71 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Linq; +using System.Threading.Tasks; +using Amazon.CloudWatchEvents; +using Amazon.CloudWatchEvents.Model; +using Xunit; + +namespace TestServerlessApp.IntegrationTests +{ + [Collection("Integration Tests")] + public class ScheduleEventRule + { + private readonly IntegrationTestContextFixture _fixture; + + public ScheduleEventRule(IntegrationTestContextFixture fixture) + { + _fixture = fixture; + } + + [Fact] + public async Task VerifyScheduleEventRuleConfiguration() + { + var lambdaFunctionName = _fixture.LambdaFunctions.FirstOrDefault(x => string.Equals(x.LogicalId, "ScheduledHandler"))?.Name; + Assert.NotNull(lambdaFunctionName); + + var eventsClient = new AmazonCloudWatchEventsClient(Amazon.RegionEndpoint.USWest2); + + // Paginate through all rules and verify the rule targets the correct Lambda function + Rule matchingRule = null; + string nextToken = null; + + do + { + var rulesResponse = await eventsClient.ListRulesAsync(new ListRulesRequest + { + NextToken = nextToken + }); + + foreach (var rule in rulesResponse.Rules.Where(r => + string.Equals(r.ScheduleExpression, "rate(5 minutes)") && + string.Equals(r.Description, "Runs every 5 minutes"))) + { + var targetsResponse = await eventsClient.ListTargetsByRuleAsync(new ListTargetsByRuleRequest + { + Rule = rule.Name + }); + + if (targetsResponse.Targets.Any(t => t.Arn != null && t.Arn.Contains($":function:{lambdaFunctionName}"))) + { + matchingRule = rule; + break; + } + } + + if (matchingRule != null) + { + break; + } + + nextToken = rulesResponse.NextToken; + } + while (!string.IsNullOrEmpty(nextToken)); + + Assert.NotNull(matchingRule); + Assert.Equal("rate(5 minutes)", matchingRule.ScheduleExpression); + Assert.Equal("Runs every 5 minutes", matchingRule.Description); + } + } +} diff --git a/Libraries/test/TestServerlessApp.IntegrationTests/TestServerlessApp.IntegrationTests.csproj b/Libraries/test/TestServerlessApp.IntegrationTests/TestServerlessApp.IntegrationTests.csproj index 5f0728358..36bce5b4c 100644 --- a/Libraries/test/TestServerlessApp.IntegrationTests/TestServerlessApp.IntegrationTests.csproj +++ b/Libraries/test/TestServerlessApp.IntegrationTests/TestServerlessApp.IntegrationTests.csproj @@ -1,17 +1,20 @@ - net6.0 + net10.0 Library - - - - - + + + + + + + + all runtime; build; native; contentfiles; analyzers diff --git a/Libraries/test/TestServerlessApp.NET8/serverless.template b/Libraries/test/TestServerlessApp.NET8/serverless.template index b8c707f54..d62ac8f91 100644 --- a/Libraries/test/TestServerlessApp.NET8/serverless.template +++ b/Libraries/test/TestServerlessApp.NET8/serverless.template @@ -1,7 +1,7 @@ { "AWSTemplateFormatVersion": "2010-09-09", "Transform": "AWS::Serverless-2016-10-31", - "Description": "This template is partially managed by Amazon.Lambda.Annotations (v1.12.0.0).", + "Description": "This template is partially managed by Amazon.Lambda.Annotations (v2.0.1.0).", "Resources": { "TestServerlessAppNET8FunctionsToUpperGenerated": { "Type": "AWS::Serverless::Function", diff --git a/Libraries/test/TestServerlessApp/Dockerfile b/Libraries/test/TestServerlessApp/Dockerfile index 3f1c870e6..775bedb52 100644 --- a/Libraries/test/TestServerlessApp/Dockerfile +++ b/Libraries/test/TestServerlessApp/Dockerfile @@ -1,4 +1,4 @@ -FROM public.ecr.aws/lambda/dotnet:6 +FROM public.ecr.aws/lambda/dotnet:10 WORKDIR /var/task diff --git a/Libraries/test/TestServerlessApp/DynamoDBEventExamples/InvalidDynamoDBEvents.cs.error b/Libraries/test/TestServerlessApp/DynamoDBEventExamples/InvalidDynamoDBEvents.cs.error new file mode 100644 index 000000000..dda434652 --- /dev/null +++ b/Libraries/test/TestServerlessApp/DynamoDBEventExamples/InvalidDynamoDBEvents.cs.error @@ -0,0 +1,68 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Lambda.Annotations; +using Amazon.Lambda.Annotations.APIGateway; +using Amazon.Lambda.Annotations.DynamoDB; +using Amazon.Lambda.DynamoDBEvents; +using System; + +namespace TestServerlessApp.DynamoDBEventExamples +{ + // This file represents invalid usage of the DynamoDBEventAttribute. + // This file is sent as input to the source generator unit tests and we assert that compilation errors are thrown with the appropriate diagnostic message. + + public class InvalidDynamoDBEvents + { + [LambdaFunction(PackageType = LambdaPackageType.Image)] + [DynamoDBEvent("@testTable", BatchSize = 10001, MaximumBatchingWindowInSeconds = 301)] + public void ProcessMessageWithInvalidDynamoDBEventAttributes(DynamoDBEvent evnt) + { + Console.WriteLine($"Event processed: {evnt}"); + } + + [LambdaFunction(PackageType = LambdaPackageType.Image)] + [DynamoDBEvent("@testTable")] + public void ProcessMessageWithInvalidParameters(DynamoDBEvent evnt, bool invalidParameter1, int invalidParameter2) + { + Console.WriteLine($"Event processed: {evnt}"); + } + + [LambdaFunction(PackageType = LambdaPackageType.Image)] + [DynamoDBEvent("@testTable")] + public bool ProcessMessageWithInvalidReturnType(DynamoDBEvent evnt) + { + Console.WriteLine($"Event processed: {evnt}"); + return true; + } + + [LambdaFunction(PackageType = LambdaPackageType.Image)] + [RestApi(LambdaHttpMethod.Get, "/")] + [DynamoDBEvent("@testTable")] + public void ProcessMessageWithMultipleEventTypes(DynamoDBEvent evnt) + { + Console.WriteLine($"Event processed: {evnt}"); + } + + [LambdaFunction(PackageType = LambdaPackageType.Image)] + [DynamoDBEvent("not-a-valid-arn")] + public void ProcessMessageWithInvalidStreamArn(DynamoDBEvent evnt) + { + Console.WriteLine($"Event processed: {evnt}"); + } + + [LambdaFunction(PackageType = LambdaPackageType.Image)] + [DynamoDBEvent("@testTable", ResourceName = "dynamo-event-source")] + public void ProcessMessageWithInvalidResourceName(DynamoDBEvent evnt) + { + Console.WriteLine($"Event processed: {evnt}"); + } + + [LambdaFunction(PackageType = LambdaPackageType.Image)] + [DynamoDBEvent("@testTable", ResourceName = "")] + public void ProcessMessageWithEmptyResourceName(DynamoDBEvent evnt) + { + Console.WriteLine($"Event processed: {evnt}"); + } + } +} diff --git a/Libraries/test/TestServerlessApp/DynamoDBEventExamples/ValidDynamoDBEvents.cs.txt b/Libraries/test/TestServerlessApp/DynamoDBEventExamples/ValidDynamoDBEvents.cs.txt new file mode 100644 index 000000000..90bec559f --- /dev/null +++ b/Libraries/test/TestServerlessApp/DynamoDBEventExamples/ValidDynamoDBEvents.cs.txt @@ -0,0 +1,33 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Lambda.Annotations; +using Amazon.Lambda.Annotations.DynamoDB; +using Amazon.Lambda.DynamoDBEvents; +using System; +using System.Threading.Tasks; + +namespace TestServerlessApp.DynamoDBEventExamples +{ + // This file represents valid usage of the DynamoDBEventAttribute. This is added as .txt file since we do not want to deploy these functions during our integration tests. + // This file is only sent as input to the source generator unit tests. + + public class ValidDynamoDBEvents + { + [LambdaFunction(PackageType = LambdaPackageType.Image)] + [DynamoDBEvent("arn:aws:dynamodb:us-east-2:444455556666:table/MyTable/stream/2024-01-01T00:00:00", BatchSize = 50, MaximumBatchingWindowInSeconds = 2, Filters = "My-Filter-1; My-Filter-2")] + [DynamoDBEvent("arn:aws:dynamodb:us-east-2:444455556666:table/MyTable2/stream/2024-01-01T00:00:00", StartingPosition = StartingPosition.TRIM_HORIZON, Enabled = false)] + [DynamoDBEvent("@testTable", ResourceName = "testTableEvent")] + public void ProcessMessages(DynamoDBEvent evnt) + { + Console.WriteLine($"Event processed: {evnt}"); + } + + [LambdaFunction(PackageType = LambdaPackageType.Image)] + [DynamoDBEvent("arn:aws:dynamodb:us-east-2:444455556666:table/MyTable/stream/2024-01-01T00:00:00")] + public async Task ProcessMessagesAsync(DynamoDBEvent evnt) + { + await Console.Out.WriteLineAsync($"Event processed: {evnt}"); + } + } +} diff --git a/Libraries/test/TestServerlessApp/DynamoDbStreamProcessing.cs b/Libraries/test/TestServerlessApp/DynamoDbStreamProcessing.cs new file mode 100644 index 000000000..cd865c0f6 --- /dev/null +++ b/Libraries/test/TestServerlessApp/DynamoDbStreamProcessing.cs @@ -0,0 +1,17 @@ +using Amazon.Lambda.Annotations; +using Amazon.Lambda.Annotations.DynamoDB; +using Amazon.Lambda.Core; +using Amazon.Lambda.DynamoDBEvents; + +namespace TestServerlessApp +{ + public class DynamoDbStreamProcessing + { + [LambdaFunction(ResourceName = "DynamoDBStreamHandler", Policies = "AWSLambdaDynamoDBExecutionRole", PackageType = LambdaPackageType.Image)] + [DynamoDBEvent("@TestTable", ResourceName = "TestTableStream", BatchSize = 100, StartingPosition = StartingPosition.TRIM_HORIZON)] + public void HandleStream(DynamoDBEvent evnt, ILambdaContext lambdaContext) + { + lambdaContext.Logger.Log($"Received {evnt.Records.Count} records"); + } + } +} diff --git a/Libraries/test/TestServerlessApp/FunctionUrlExample.cs b/Libraries/test/TestServerlessApp/FunctionUrlExample.cs new file mode 100644 index 000000000..4909c768e --- /dev/null +++ b/Libraries/test/TestServerlessApp/FunctionUrlExample.cs @@ -0,0 +1,20 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Lambda.Annotations; +using Amazon.Lambda.Annotations.APIGateway; +using Amazon.Lambda.Core; + +namespace TestServerlessApp +{ + public class FunctionUrlExample + { + [LambdaFunction(PackageType = LambdaPackageType.Image)] + [FunctionUrl(AuthType = FunctionUrlAuthType.NONE)] + public IHttpResult GetItems([FromQuery] string category, ILambdaContext context) + { + context.Logger.LogLine($"Getting items for category: {category}"); + return HttpResults.Ok(new { items = new[] { "item1", "item2" }, category }); + } + } +} diff --git a/Libraries/test/TestServerlessApp/NullableReferenceTypeExample.cs b/Libraries/test/TestServerlessApp/NullableReferenceTypeExample.cs index 9b930176d..ad165ce86 100644 --- a/Libraries/test/TestServerlessApp/NullableReferenceTypeExample.cs +++ b/Libraries/test/TestServerlessApp/NullableReferenceTypeExample.cs @@ -1,4 +1,4 @@ -using Amazon.Lambda.Annotations; +using Amazon.Lambda.Annotations; using Amazon.Lambda.Annotations.APIGateway; using Amazon.Lambda.Core; diff --git a/Libraries/test/TestServerlessApp/SNSEventExamples/InvalidSNSEvents.cs.error b/Libraries/test/TestServerlessApp/SNSEventExamples/InvalidSNSEvents.cs.error new file mode 100644 index 000000000..639c31441 --- /dev/null +++ b/Libraries/test/TestServerlessApp/SNSEventExamples/InvalidSNSEvents.cs.error @@ -0,0 +1,61 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Lambda.Annotations; +using Amazon.Lambda.Annotations.APIGateway; +using Amazon.Lambda.Annotations.SNS; +using Amazon.Lambda.SNSEvents; +using System; + +namespace TestServerlessApp.SNSEventExamples +{ + // This file represents invalid usage of the SNSEventAttribute. + // This file is sent as input to the source generator unit tests and we assert that compilation errors are thrown with the appropriate diagnostic message. + + public class InvalidSNSEvents + { + [LambdaFunction(PackageType = LambdaPackageType.Image)] + [SNSEvent("not-a-valid-arn")] + public void ProcessMessageWithInvalidTopicArn(SNSEvent evnt) + { + Console.WriteLine($"Event processed: {evnt}"); + } + + [LambdaFunction(PackageType = LambdaPackageType.Image)] + [SNSEvent("@testTopic")] + public void ProcessMessageWithInvalidParameters(SNSEvent evnt, bool invalidParameter1, int invalidParameter2) + { + Console.WriteLine($"Event processed: {evnt}"); + } + + [LambdaFunction(PackageType = LambdaPackageType.Image)] + [SNSEvent("@testTopic")] + public bool ProcessMessageWithInvalidReturnType(SNSEvent evnt) + { + Console.WriteLine($"Event processed: {evnt}"); + return true; + } + + [LambdaFunction(PackageType = LambdaPackageType.Image)] + [RestApi(LambdaHttpMethod.Get, "/")] + [SNSEvent("@testTopic")] + public void ProcessMessageWithMultipleEventTypes(SNSEvent evnt) + { + Console.WriteLine($"Event processed: {evnt}"); + } + + [LambdaFunction(PackageType = LambdaPackageType.Image)] + [SNSEvent("@testTopic", ResourceName = "sns-event-source")] + public void ProcessMessageWithInvalidResourceName(SNSEvent evnt) + { + Console.WriteLine($"Event processed: {evnt}"); + } + + [LambdaFunction(PackageType = LambdaPackageType.Image)] + [SNSEvent("@testTopic", ResourceName = "")] + public void ProcessMessageWithEmptyResourceName(SNSEvent evnt) + { + Console.WriteLine($"Event processed: {evnt}"); + } + } +} diff --git a/Libraries/test/TestServerlessApp/SNSEventExamples/ValidSNSEvents.cs.txt b/Libraries/test/TestServerlessApp/SNSEventExamples/ValidSNSEvents.cs.txt new file mode 100644 index 000000000..9c42ebc70 --- /dev/null +++ b/Libraries/test/TestServerlessApp/SNSEventExamples/ValidSNSEvents.cs.txt @@ -0,0 +1,32 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Lambda.Annotations; +using Amazon.Lambda.Annotations.SNS; +using Amazon.Lambda.SNSEvents; +using System; +using System.Threading.Tasks; + +namespace TestServerlessApp.SNSEventExamples +{ + // This file represents valid usage of the SNSEventAttribute. This is added as .txt file since we do not want to deploy these functions during our integration tests. + // This file is only sent as input to the source generator unit tests. + + public class ValidSNSEvents + { + [LambdaFunction(PackageType = LambdaPackageType.Image)] + [SNSEvent("arn:aws:sns:us-east-2:444455556666:MyTopic", FilterPolicy = "{ \"store\": [\"example_corp\"] }")] + [SNSEvent("@testTopic", ResourceName = "testTopicEvent")] + public void ProcessMessages(SNSEvent evnt) + { + Console.WriteLine($"Event processed: {evnt}"); + } + + [LambdaFunction(PackageType = LambdaPackageType.Image)] + [SNSEvent("arn:aws:sns:us-east-2:444455556666:MyTopic")] + public async Task ProcessMessagesAsync(SNSEvent evnt) + { + await Console.Out.WriteLineAsync($"Event processed: {evnt}"); + } + } +} diff --git a/Libraries/test/TestServerlessApp/SQSEventExamples/InvalidSQSEvents.cs.error b/Libraries/test/TestServerlessApp/SQSEventExamples/InvalidSQSEvents.cs.error index 1603f121d..617c758f7 100644 --- a/Libraries/test/TestServerlessApp/SQSEventExamples/InvalidSQSEvents.cs.error +++ b/Libraries/test/TestServerlessApp/SQSEventExamples/InvalidSQSEvents.cs.error @@ -1,4 +1,7 @@ -using Amazon.Lambda.Annotations; +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Lambda.Annotations; using Amazon.Lambda.Annotations.APIGateway; using Amazon.Lambda.Annotations.SQS; using Amazon.Lambda.SQSEvents; diff --git a/Libraries/test/TestServerlessApp/SQSEventExamples/ValidSQSEvents.cs.txt b/Libraries/test/TestServerlessApp/SQSEventExamples/ValidSQSEvents.cs.txt index 8bd12d68e..5c177b178 100644 --- a/Libraries/test/TestServerlessApp/SQSEventExamples/ValidSQSEvents.cs.txt +++ b/Libraries/test/TestServerlessApp/SQSEventExamples/ValidSQSEvents.cs.txt @@ -1,3 +1,6 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + using Amazon.Lambda.Annotations; using Amazon.Lambda.Annotations.SQS; using Amazon.Lambda.SQSEvents; diff --git a/Libraries/test/TestServerlessApp/ScheduleEventExamples/InvalidScheduleEvents.cs.error b/Libraries/test/TestServerlessApp/ScheduleEventExamples/InvalidScheduleEvents.cs.error new file mode 100644 index 000000000..4ed8897cc --- /dev/null +++ b/Libraries/test/TestServerlessApp/ScheduleEventExamples/InvalidScheduleEvents.cs.error @@ -0,0 +1,61 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Lambda.Annotations; +using Amazon.Lambda.Annotations.APIGateway; +using Amazon.Lambda.Annotations.Schedule; +using Amazon.Lambda.CloudWatchEvents.ScheduledEvents; +using System; + +namespace TestServerlessApp.ScheduleEventExamples +{ + // This file represents invalid usage of the ScheduleEventAttribute. + // This file is sent as input to the source generator unit tests and we assert that compilation errors are thrown with the appropriate diagnostic message. + + public class InvalidScheduleEvents + { + [LambdaFunction(PackageType = LambdaPackageType.Image)] + [ScheduleEvent("every 5 minutes")] + public void ProcessMessageWithInvalidScheduleExpression(ScheduledEvent evnt) + { + Console.WriteLine($"Event processed: {evnt}"); + } + + [LambdaFunction(PackageType = LambdaPackageType.Image)] + [ScheduleEvent("rate(5 minutes)")] + public void ProcessMessageWithInvalidParameters(ScheduledEvent evnt, bool invalidParameter1, int invalidParameter2) + { + Console.WriteLine($"Event processed: {evnt}"); + } + + [LambdaFunction(PackageType = LambdaPackageType.Image)] + [ScheduleEvent("rate(5 minutes)")] + public bool ProcessMessageWithInvalidReturnType(ScheduledEvent evnt) + { + Console.WriteLine($"Event processed: {evnt}"); + return true; + } + + [LambdaFunction(PackageType = LambdaPackageType.Image)] + [RestApi(LambdaHttpMethod.Get, "/")] + [ScheduleEvent("rate(5 minutes)")] + public void ProcessMessageWithMultipleEventTypes(ScheduledEvent evnt) + { + Console.WriteLine($"Event processed: {evnt}"); + } + + [LambdaFunction(PackageType = LambdaPackageType.Image)] + [ScheduleEvent("rate(5 minutes)", ResourceName = "invalid-name!")] + public void ProcessMessageWithInvalidResourceName(ScheduledEvent evnt) + { + Console.WriteLine($"Event processed: {evnt}"); + } + + [LambdaFunction(PackageType = LambdaPackageType.Image)] + [ScheduleEvent("rate(5 minutes)", ResourceName = "")] + public void ProcessMessageWithEmptyResourceName(ScheduledEvent evnt) + { + Console.WriteLine($"Event processed: {evnt}"); + } + } +} diff --git a/Libraries/test/TestServerlessApp/ScheduleEventExamples/ValidScheduleEvents.cs.txt b/Libraries/test/TestServerlessApp/ScheduleEventExamples/ValidScheduleEvents.cs.txt new file mode 100644 index 000000000..179417209 --- /dev/null +++ b/Libraries/test/TestServerlessApp/ScheduleEventExamples/ValidScheduleEvents.cs.txt @@ -0,0 +1,28 @@ +using Amazon.Lambda.Annotations; +using Amazon.Lambda.Annotations.Schedule; +using Amazon.Lambda.CloudWatchEvents.ScheduledEvents; +using System; +using System.Threading.Tasks; + +namespace TestServerlessApp.ScheduleEventExamples +{ + // This file represents valid usage of the ScheduleEventAttribute. This is added as .txt file since we do not want to deploy these functions during our integration tests. + // This file is only sent as input to the source generator unit tests. + + public class ValidScheduleEvents + { + [LambdaFunction(PackageType = LambdaPackageType.Image)] + [ScheduleEvent("rate(5 minutes)", Description = "Runs every 5 minutes", Input = "{ \"key\": \"value\" }")] + public void ProcessScheduledEvent(ScheduledEvent evnt) + { + Console.WriteLine($"Event processed: {evnt}"); + } + + [LambdaFunction(PackageType = LambdaPackageType.Image)] + [ScheduleEvent("cron(0 12 * * ? *)", ResourceName = "DailyNoonSchedule")] + public async Task ProcessScheduledEventAsync(ScheduledEvent evnt) + { + await Console.Out.WriteLineAsync($"Event processed: {evnt}"); + } + } +} diff --git a/Libraries/test/TestServerlessApp/ScheduledProcessing.cs b/Libraries/test/TestServerlessApp/ScheduledProcessing.cs new file mode 100644 index 000000000..fd1079948 --- /dev/null +++ b/Libraries/test/TestServerlessApp/ScheduledProcessing.cs @@ -0,0 +1,20 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Lambda.Annotations; +using Amazon.Lambda.Annotations.Schedule; +using Amazon.Lambda.CloudWatchEvents.ScheduledEvents; +using Amazon.Lambda.Core; + +namespace TestServerlessApp +{ + public class ScheduledProcessing + { + [LambdaFunction(ResourceName = "ScheduledHandler", Policies = "AWSLambdaBasicExecutionRole", PackageType = LambdaPackageType.Image)] + [ScheduleEvent("rate(5 minutes)", ResourceName = "FiveMinuteSchedule", Description = "Runs every 5 minutes")] + public void HandleSchedule(ScheduledEvent evnt, ILambdaContext lambdaContext) + { + lambdaContext.Logger.Log($"Scheduled event received at {evnt.Time}"); + } + } +} diff --git a/Libraries/test/TestServerlessApp/SimpleCalculator.cs b/Libraries/test/TestServerlessApp/SimpleCalculator.cs index 8dabd1f49..0c703ab47 100644 --- a/Libraries/test/TestServerlessApp/SimpleCalculator.cs +++ b/Libraries/test/TestServerlessApp/SimpleCalculator.cs @@ -19,7 +19,7 @@ public class SimpleCalculator /// public SimpleCalculator(ISimpleCalculatorService simpleCalculatorService) { - this._simpleCalculatorService = simpleCalculatorService; + _simpleCalculatorService = simpleCalculatorService; } [LambdaFunction(ResourceName = "SimpleCalculatorAdd", PackageType = LambdaPackageType.Image)] @@ -90,4 +90,4 @@ public class RandomsInput public int MaxValue { get; set; } } } -} \ No newline at end of file +} diff --git a/Libraries/test/TestServerlessApp/SnsMessageProcessing.cs b/Libraries/test/TestServerlessApp/SnsMessageProcessing.cs new file mode 100644 index 000000000..91004a5b9 --- /dev/null +++ b/Libraries/test/TestServerlessApp/SnsMessageProcessing.cs @@ -0,0 +1,20 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.Lambda.Annotations; +using Amazon.Lambda.Annotations.SNS; +using Amazon.Lambda.Core; +using Amazon.Lambda.SNSEvents; + +namespace TestServerlessApp +{ + public class SnsMessageProcessing + { + [LambdaFunction(ResourceName = "SNSMessageHandler", Policies = "AWSLambdaBasicExecutionRole", PackageType = LambdaPackageType.Image)] + [SNSEvent("@TestTopic", ResourceName = "TestTopicEvent", FilterPolicy = "{ \"store\": [\"example_corp\"] }")] + public void HandleMessage(SNSEvent evnt, ILambdaContext lambdaContext) + { + lambdaContext.Logger.Log($"Received {evnt.Records.Count} messages"); + } + } +} diff --git a/Libraries/test/TestServerlessApp/TestServerlessApp.csproj b/Libraries/test/TestServerlessApp/TestServerlessApp.csproj index 83d7cf89e..4efe4a451 100644 --- a/Libraries/test/TestServerlessApp/TestServerlessApp.csproj +++ b/Libraries/test/TestServerlessApp/TestServerlessApp.csproj @@ -1,10 +1,11 @@  - net6.0 + net10.0 true Lambda true + CS8632;CS8669 @@ -19,14 +20,16 @@ - - + + + + diff --git a/Libraries/test/TestServerlessApp/aws-lambda-tools-defaults.json b/Libraries/test/TestServerlessApp/aws-lambda-tools-defaults.json index 0b96350ff..eca7ad5fb 100644 --- a/Libraries/test/TestServerlessApp/aws-lambda-tools-defaults.json +++ b/Libraries/test/TestServerlessApp/aws-lambda-tools-defaults.json @@ -8,7 +8,7 @@ "profile": "default", "region": "us-west-2", "configuration": "Release", - "framework": "net6.0", + "framework": "net10.0", "s3-prefix": "TestServerlessApp/", "template": "serverless.template", "template-parameters": "", diff --git a/Libraries/test/TestServerlessApp/serverless.template b/Libraries/test/TestServerlessApp/serverless.template index 82ed9f7d4..65220b6ae 100644 --- a/Libraries/test/TestServerlessApp/serverless.template +++ b/Libraries/test/TestServerlessApp/serverless.template @@ -1,11 +1,8 @@ { "AWSTemplateFormatVersion": "2010-09-09", "Transform": "AWS::Serverless-2016-10-31", - "Description": "This template is partially managed by Amazon.Lambda.Annotations (v1.12.0.0).", + "Description": "This template is partially managed by Amazon.Lambda.Annotations (v2.0.1.0).", "Resources": { - "TestQueue": { - "Type": "AWS::SQS::Queue" - }, "AnnotationsHttpApi": { "Type": "AWS::Serverless::HttpApi", "Metadata": { @@ -61,7 +58,7 @@ "Tool": "Amazon.Lambda.Annotations" } }, - "AuthNameFallbackTest": { + "TestServerlessAppCustomAuthorizerWithIHttpResultsExampleAuthorizerWithIHttpResultsGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -86,14 +83,14 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.AuthNameFallback_GetUserId_Generated::GetUserId" + "TestServerlessApp::TestServerlessApp.CustomAuthorizerWithIHttpResultsExample_AuthorizerWithIHttpResults_Generated::AuthorizerWithIHttpResults" ] }, "Events": { "RootGet": { "Type": "HttpApi", "Properties": { - "Path": "/api/authorizer-fallback", + "Path": "/authorizerihttpresults", "Method": "GET", "ApiId": { "Ref": "AnnotationsHttpApi" @@ -103,20 +100,11 @@ } } }, - "TestServerlessAppComplexCalculatorAddGenerated": { + "TestServerlessAppFunctionUrlExampleGetItemsGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootPost" - ], - "SyncedEventProperties": { - "RootPost": [ - "Path", - "Method", - "ApiId.Ref" - ] - } + "SyncedFunctionUrlConfig": true }, "Properties": { "MemorySize": 512, @@ -128,32 +116,23 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.ComplexCalculator_Add_Generated::Add" + "TestServerlessApp::TestServerlessApp.FunctionUrlExample_GetItems_Generated::GetItems" ] }, - "Events": { - "RootPost": { - "Type": "HttpApi", - "Properties": { - "Path": "/ComplexCalculator/Add", - "Method": "POST", - "ApiId": { - "Ref": "AnnotationsHttpApi" - } - } - } + "FunctionUrlConfig": { + "AuthType": "NONE" } } }, - "TestServerlessAppComplexCalculatorSubtractGenerated": { + "HttpApiAuthorizerTest": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", "SyncedEvents": [ - "RootPost" + "RootGet" ], "SyncedEventProperties": { - "RootPost": [ + "RootGet": [ "Path", "Method", "ApiId.Ref" @@ -170,15 +149,15 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.ComplexCalculator_Subtract_Generated::Subtract" + "TestServerlessApp::TestServerlessApp.CustomAuthorizerHttpApiExample_HttpApiAuthorizer_Generated::HttpApiAuthorizer" ] }, "Events": { - "RootPost": { + "RootGet": { "Type": "HttpApi", "Properties": { - "Path": "/ComplexCalculator/Subtract", - "Method": "POST", + "Path": "/api/authorizer", + "Method": "GET", "ApiId": { "Ref": "AnnotationsHttpApi" } @@ -187,18 +166,21 @@ } } }, - "HttpApiAuthorizerTest": { + "SQSMessageHandler": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", "SyncedEvents": [ - "RootGet" + "TestQueueEvent" ], "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method", - "ApiId.Ref" + "TestQueueEvent": [ + "Queue.Fn::GetAtt", + "BatchSize", + "FilterCriteria.Filters", + "FunctionResponseTypes", + "MaximumBatchingWindowInSeconds", + "ScalingConfig.MaximumConcurrency" ] } }, @@ -206,29 +188,65 @@ "MemorySize": 512, "Timeout": 30, "Policies": [ - "AWSLambdaBasicExecutionRole" + "AWSLambdaSQSQueueExecutionRole" ], "PackageType": "Image", "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.CustomAuthorizerHttpApiExample_HttpApiAuthorizer_Generated::HttpApiAuthorizer" + "TestServerlessApp::TestServerlessApp.SqsMessageProcessing_HandleMessage_Generated::HandleMessage" ] }, "Events": { - "RootGet": { - "Type": "HttpApi", + "TestQueueEvent": { + "Type": "SQS", "Properties": { - "Path": "/api/authorizer", - "Method": "GET", - "ApiId": { - "Ref": "AnnotationsHttpApi" + "BatchSize": 50, + "FilterCriteria": { + "Filters": [ + { + "Pattern": "{ \"body\" : { \"RequestCode\" : [ \"BBBB\" ] } }" + } + ] + }, + "FunctionResponseTypes": [ + "ReportBatchItemFailures" + ], + "MaximumBatchingWindowInSeconds": 5, + "ScalingConfig": { + "MaximumConcurrency": 5 + }, + "Queue": { + "Fn::GetAtt": [ + "TestQueue", + "Arn" + ] } } } } } }, + "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" + ] + } + } + }, "HttpApiV1AuthorizerTest": { "Type": "AWS::Serverless::Function", "Metadata": { @@ -273,18 +291,17 @@ } } }, - "HttpApiNonString": { + "SNSMessageHandler": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", "SyncedEvents": [ - "RootGet" + "TestTopicEvent" ], "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method", - "ApiId.Ref" + "TestTopicEvent": [ + "Topic.Ref", + "FilterPolicy" ] } }, @@ -298,37 +315,26 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.CustomAuthorizerNonStringExample_HttpApiWithNonString_Generated::HttpApiWithNonString" + "TestServerlessApp::TestServerlessApp.SnsMessageProcessing_HandleMessage_Generated::HandleMessage" ] }, "Events": { - "RootGet": { - "Type": "HttpApi", + "TestTopicEvent": { + "Type": "SNS", "Properties": { - "Path": "/api/authorizer-non-string", - "Method": "GET", - "ApiId": { - "Ref": "AnnotationsHttpApi" + "FilterPolicy": "{ \"store\": [\"example_corp\"] }", + "Topic": { + "Ref": "TestTopic" } } } } } }, - "RestAuthorizerTest": { + "TestServerlessAppVoidExampleVoidReturnGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method", - "RestApiId.Ref" - ] - } + "Tool": "Amazon.Lambda.Annotations" }, "Properties": { "MemorySize": 512, @@ -340,24 +346,12 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.CustomAuthorizerRestExample_RestAuthorizer_Generated::RestAuthorizer" + "TestServerlessApp::TestServerlessApp.VoidExample_VoidReturn_Generated::VoidReturn" ] - }, - "Events": { - "RootGet": { - "Type": "Api", - "Properties": { - "Path": "/rest/authorizer", - "Method": "GET", - "RestApiId": { - "Ref": "AnnotationsRestApi" - } - } - } } } }, - "TestServerlessAppCustomAuthorizerWithIHttpResultsExampleAuthorizerWithIHttpResultsGenerated": { + "RestAuthorizerTest": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -368,7 +362,7 @@ "RootGet": [ "Path", "Method", - "ApiId.Ref" + "RestApiId.Ref" ] } }, @@ -382,17 +376,17 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.CustomAuthorizerWithIHttpResultsExample_AuthorizerWithIHttpResults_Generated::AuthorizerWithIHttpResults" + "TestServerlessApp::TestServerlessApp.CustomAuthorizerRestExample_RestAuthorizer_Generated::RestAuthorizer" ] }, "Events": { "RootGet": { - "Type": "HttpApi", + "Type": "Api", "Properties": { - "Path": "/authorizerihttpresults", + "Path": "/rest/authorizer", "Method": "GET", - "ApiId": { - "Ref": "AnnotationsHttpApi" + "RestApiId": { + "Ref": "AnnotationsRestApi" } } } @@ -699,10 +693,20 @@ } } }, - "TestServerlessAppDynamicExampleDynamicReturnGenerated": { + "SimpleCalculatorAdd": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations" + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "RestApiId.Ref" + ] + } }, "Properties": { "MemorySize": 512, @@ -714,15 +718,37 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.DynamicExample_DynamicReturn_Generated::DynamicReturn" + "TestServerlessApp::TestServerlessApp.SimpleCalculator_Add_Generated::Add" ] + }, + "Events": { + "RootGet": { + "Type": "Api", + "Properties": { + "Path": "/SimpleCalculator/Add", + "Method": "GET", + "RestApiId": { + "Ref": "AnnotationsRestApi" + } + } + } } } }, - "TestServerlessAppDynamicExampleDynamicInputGenerated": { + "SimpleCalculatorSubtract": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations" + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "RestApiId.Ref" + ] + } }, "Properties": { "MemorySize": 512, @@ -734,12 +760,24 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.DynamicExample_DynamicInput_Generated::DynamicInput" + "TestServerlessApp::TestServerlessApp.SimpleCalculator_Subtract_Generated::Subtract" ] + }, + "Events": { + "RootGet": { + "Type": "Api", + "Properties": { + "Path": "/SimpleCalculator/Subtract", + "Method": "GET", + "RestApiId": { + "Ref": "AnnotationsRestApi" + } + } + } } } }, - "TestServerlessAppFromScratchNoApiGatewayEventsReferenceToUpperGenerated": { + "SimpleCalculatorMultiply": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -750,7 +788,7 @@ "RootGet": [ "Path", "Method", - "ApiId.Ref" + "RestApiId.Ref" ] } }, @@ -764,27 +802,37 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.FromScratch.NoApiGatewayEventsReference_ToUpper_Generated::ToUpper" + "TestServerlessApp::TestServerlessApp.SimpleCalculator_Multiply_Generated::Multiply" ] }, "Events": { "RootGet": { - "Type": "HttpApi", + "Type": "Api", "Properties": { - "Path": "/{text}", + "Path": "/SimpleCalculator/Multiply/{x}/{y}", "Method": "GET", - "ApiId": { - "Ref": "AnnotationsHttpApi" + "RestApiId": { + "Ref": "AnnotationsRestApi" } } } } } }, - "TestServerlessAppFromScratchNoSerializerAttributeReferenceToUpperGenerated": { + "SimpleCalculatorDivideAsync": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations" + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "RestApiId.Ref" + ] + } }, "Properties": { "MemorySize": 512, @@ -796,29 +844,50 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.FromScratch.NoSerializerAttributeReference_ToUpper_Generated::ToUpper" + "TestServerlessApp::TestServerlessApp.SimpleCalculator_DivideAsync_Generated::DivideAsync" ] - } - } + }, + "Events": { + "RootGet": { + "Type": "Api", + "Properties": { + "Path": "/SimpleCalculator/DivideAsync/{x}/{y}", + "Method": "GET", + "RestApiId": { + "Ref": "AnnotationsRestApi" + } + } + } + } + } }, - "GreeterSayHello": { + "PI": { "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", - "PayloadFormatVersion", - "ApiId.Ref" + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.SimpleCalculator_Pi_Generated::Pi" ] } + } + }, + "Random": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" }, "Properties": { - "MemorySize": 1024, + "MemorySize": 512, "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" @@ -827,43 +896,39 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.Greeter_SayHello_Generated::SayHello" + "TestServerlessApp::TestServerlessApp.SimpleCalculator_Random_Generated::Random" ] - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/Greeter/SayHello", - "Method": "GET", - "PayloadFormatVersion": "1.0", - "ApiId": { - "Ref": "AnnotationsHttpApi" - } - } - } } } }, - "GreeterSayHelloAsync": { + "Randoms": { "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", - "PayloadFormatVersion", - "ApiId.Ref" + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.SimpleCalculator_Randoms_Generated::Randoms" ] } + } + }, + "TestServerlessAppIntrinsicExampleHasIntrinsicGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" }, "Properties": { "MemorySize": 512, - "Timeout": 50, + "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" ], @@ -871,21 +936,8 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.Greeter_SayHelloAsync_Generated::SayHelloAsync" + "TestServerlessApp::TestServerlessApp.IntrinsicExample_HasIntrinsic_Generated::HasIntrinsic" ] - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/Greeter/SayHelloAsync", - "Method": "GET", - "PayloadFormatVersion": "1.0", - "ApiId": { - "Ref": "AnnotationsHttpApi" - } - } - } } } }, @@ -929,38 +981,63 @@ } } }, - "TestServerlessAppIntrinsicExampleHasIntrinsicGenerated": { + "DynamoDBStreamHandler": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations" + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "TestTableStream" + ], + "SyncedEventProperties": { + "TestTableStream": [ + "Stream.Fn::GetAtt", + "StartingPosition", + "BatchSize" + ] + } }, "Properties": { "MemorySize": 512, "Timeout": 30, "Policies": [ - "AWSLambdaBasicExecutionRole" + "AWSLambdaDynamoDBExecutionRole" ], "PackageType": "Image", "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.IntrinsicExample_HasIntrinsic_Generated::HasIntrinsic" + "TestServerlessApp::TestServerlessApp.DynamoDbStreamProcessing_HandleStream_Generated::HandleStream" ] + }, + "Events": { + "TestTableStream": { + "Type": "DynamoDB", + "Properties": { + "StartingPosition": "TRIM_HORIZON", + "BatchSize": 100, + "Stream": { + "Fn::GetAtt": [ + "TestTable", + "StreamArn" + ] + } + } + } } } }, - "TestServerlessAppNullableReferenceTypeExampleNullableHeaderHttpApiGenerated": { + "S3EventHandler": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", "SyncedEvents": [ - "RootGet" + "TestS3Bucket" ], "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method", - "ApiId.Ref" + "TestS3Bucket": [ + "Bucket.Ref", + "Events", + "Filter.S3Key.Rules" ] } }, @@ -968,30 +1045,42 @@ "MemorySize": 512, "Timeout": 30, "Policies": [ - "AWSLambdaBasicExecutionRole" + "AWSLambdaBasicExecutionRole", + "AmazonS3ReadOnlyAccess" ], "PackageType": "Image", "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.NullableReferenceTypeExample_NullableHeaderHttpApi_Generated::NullableHeaderHttpApi" + "TestServerlessApp::TestServerlessApp.S3EventExamples.S3EventProcessing_ProcessS3Event_Generated::ProcessS3Event" ] }, "Events": { - "RootGet": { - "Type": "HttpApi", + "TestS3Bucket": { + "Type": "S3", "Properties": { - "Path": "/nullableheaderhttpapi", - "Method": "GET", - "ApiId": { - "Ref": "AnnotationsHttpApi" + "Events": [ + "s3:ObjectCreated:*" + ], + "Filter": { + "S3Key": { + "Rules": [ + { + "Name": "suffix", + "Value": ".json" + } + ] + } + }, + "Bucket": { + "Ref": "TestS3Bucket" } } } } } }, - "SimpleCalculatorAdd": { + "GreeterSayHello": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -1002,12 +1091,13 @@ "RootGet": [ "Path", "Method", - "RestApiId.Ref" + "PayloadFormatVersion", + "ApiId.Ref" ] } }, "Properties": { - "MemorySize": 512, + "MemorySize": 1024, "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" @@ -1016,24 +1106,25 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.SimpleCalculator_Add_Generated::Add" + "TestServerlessApp::TestServerlessApp.Greeter_SayHello_Generated::SayHello" ] }, "Events": { "RootGet": { - "Type": "Api", + "Type": "HttpApi", "Properties": { - "Path": "/SimpleCalculator/Add", + "Path": "/Greeter/SayHello", "Method": "GET", - "RestApiId": { - "Ref": "AnnotationsRestApi" + "PayloadFormatVersion": "1.0", + "ApiId": { + "Ref": "AnnotationsHttpApi" } } } } } }, - "SimpleCalculatorSubtract": { + "GreeterSayHelloAsync": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -1044,13 +1135,14 @@ "RootGet": [ "Path", "Method", - "RestApiId.Ref" + "PayloadFormatVersion", + "ApiId.Ref" ] } }, "Properties": { "MemorySize": 512, - "Timeout": 30, + "Timeout": 50, "Policies": [ "AWSLambdaBasicExecutionRole" ], @@ -1058,24 +1150,25 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.SimpleCalculator_Subtract_Generated::Subtract" + "TestServerlessApp::TestServerlessApp.Greeter_SayHelloAsync_Generated::SayHelloAsync" ] }, "Events": { "RootGet": { - "Type": "Api", + "Type": "HttpApi", "Properties": { - "Path": "/SimpleCalculator/Subtract", + "Path": "/Greeter/SayHelloAsync", "Method": "GET", - "RestApiId": { - "Ref": "AnnotationsRestApi" + "PayloadFormatVersion": "1.0", + "ApiId": { + "Ref": "AnnotationsHttpApi" } } } } } }, - "SimpleCalculatorMultiply": { + "HttpApiNonString": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -1086,7 +1179,7 @@ "RootGet": [ "Path", "Method", - "RestApiId.Ref" + "ApiId.Ref" ] } }, @@ -1100,24 +1193,44 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.SimpleCalculator_Multiply_Generated::Multiply" + "TestServerlessApp::TestServerlessApp.CustomAuthorizerNonStringExample_HttpApiWithNonString_Generated::HttpApiWithNonString" ] }, "Events": { "RootGet": { - "Type": "Api", + "Type": "HttpApi", "Properties": { - "Path": "/SimpleCalculator/Multiply/{x}/{y}", + "Path": "/api/authorizer-non-string", "Method": "GET", - "RestApiId": { - "Ref": "AnnotationsRestApi" + "ApiId": { + "Ref": "AnnotationsHttpApi" } } } } } }, - "SimpleCalculatorDivideAsync": { + "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" + ] + } + } + }, + "AuthNameFallbackTest": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -1128,7 +1241,7 @@ "RootGet": [ "Path", "Method", - "RestApiId.Ref" + "ApiId.Ref" ] } }, @@ -1142,24 +1255,24 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.SimpleCalculator_DivideAsync_Generated::DivideAsync" + "TestServerlessApp::TestServerlessApp.AuthNameFallback_GetUserId_Generated::GetUserId" ] }, "Events": { "RootGet": { - "Type": "Api", + "Type": "HttpApi", "Properties": { - "Path": "/SimpleCalculator/DivideAsync/{x}/{y}", + "Path": "/api/authorizer-fallback", "Method": "GET", - "RestApiId": { - "Ref": "AnnotationsRestApi" + "ApiId": { + "Ref": "AnnotationsHttpApi" } } } } } }, - "PI": { + "TestServerlessAppDynamicExampleDynamicReturnGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations" @@ -1174,12 +1287,12 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.SimpleCalculator_Pi_Generated::Pi" + "TestServerlessApp::TestServerlessApp.DynamicExample_DynamicReturn_Generated::DynamicReturn" ] } } }, - "Random": { + "TestServerlessAppDynamicExampleDynamicInputGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations" @@ -1194,12 +1307,12 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.SimpleCalculator_Random_Generated::Random" + "TestServerlessApp::TestServerlessApp.DynamicExample_DynamicInput_Generated::DynamicInput" ] } } }, - "Randoms": { + "ToUpper": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations" @@ -1214,26 +1327,22 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.SimpleCalculator_Randoms_Generated::Randoms" + "TestServerlessApp::TestServerlessApp.Sub1.Functions_ToUpper_Generated::ToUpper" ] } } }, - "SQSMessageHandler": { + "ScheduledHandler": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", "SyncedEvents": [ - "TestQueueEvent" + "FiveMinuteSchedule" ], "SyncedEventProperties": { - "TestQueueEvent": [ - "Queue.Fn::GetAtt", - "BatchSize", - "FilterCriteria.Filters", - "FunctionResponseTypes", - "MaximumBatchingWindowInSeconds", - "ScalingConfig.MaximumConcurrency" + "FiveMinuteSchedule": [ + "Schedule", + "Description" ] } }, @@ -1241,49 +1350,40 @@ "MemorySize": 512, "Timeout": 30, "Policies": [ - "AWSLambdaSQSQueueExecutionRole" + "AWSLambdaBasicExecutionRole" ], "PackageType": "Image", "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.SqsMessageProcessing_HandleMessage_Generated::HandleMessage" + "TestServerlessApp::TestServerlessApp.ScheduledProcessing_HandleSchedule_Generated::HandleSchedule" ] }, "Events": { - "TestQueueEvent": { - "Type": "SQS", + "FiveMinuteSchedule": { + "Type": "Schedule", "Properties": { - "BatchSize": 50, - "FilterCriteria": { - "Filters": [ - { - "Pattern": "{ \"body\" : { \"RequestCode\" : [ \"BBBB\" ] } }" - } - ] - }, - "FunctionResponseTypes": [ - "ReportBatchItemFailures" - ], - "MaximumBatchingWindowInSeconds": 5, - "ScalingConfig": { - "MaximumConcurrency": 5 - }, - "Queue": { - "Fn::GetAtt": [ - "TestQueue", - "Arn" - ] - } + "Schedule": "rate(5 minutes)", + "Description": "Runs every 5 minutes" } } } } }, - "ToUpper": { + "TestServerlessAppNullableReferenceTypeExampleNullableHeaderHttpApiGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations" + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "ApiId.Ref" + ] + } }, "Properties": { "MemorySize": 512, @@ -1295,15 +1395,37 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.Sub1.Functions_ToUpper_Generated::ToUpper" + "TestServerlessApp::TestServerlessApp.NullableReferenceTypeExample_NullableHeaderHttpApi_Generated::NullableHeaderHttpApi" ] + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/nullableheaderhttpapi", + "Method": "GET", + "ApiId": { + "Ref": "AnnotationsHttpApi" + } + } + } } } }, - "TestServerlessAppTaskExampleTaskReturnGenerated": { + "TestServerlessAppComplexCalculatorAddGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations" + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootPost" + ], + "SyncedEventProperties": { + "RootPost": [ + "Path", + "Method", + "ApiId.Ref" + ] + } }, "Properties": { "MemorySize": 512, @@ -1315,15 +1437,37 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.TaskExample_TaskReturn_Generated::TaskReturn" + "TestServerlessApp::TestServerlessApp.ComplexCalculator_Add_Generated::Add" ] + }, + "Events": { + "RootPost": { + "Type": "HttpApi", + "Properties": { + "Path": "/ComplexCalculator/Add", + "Method": "POST", + "ApiId": { + "Ref": "AnnotationsHttpApi" + } + } + } } } }, - "TestServerlessAppVoidExampleVoidReturnGenerated": { + "TestServerlessAppComplexCalculatorSubtractGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations" + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootPost" + ], + "SyncedEventProperties": { + "RootPost": [ + "Path", + "Method", + "ApiId.Ref" + ] + } }, "Properties": { "MemorySize": 512, @@ -1335,23 +1479,35 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.VoidExample_VoidReturn_Generated::VoidReturn" + "TestServerlessApp::TestServerlessApp.ComplexCalculator_Subtract_Generated::Subtract" ] + }, + "Events": { + "RootPost": { + "Type": "HttpApi", + "Properties": { + "Path": "/ComplexCalculator/Subtract", + "Method": "POST", + "ApiId": { + "Ref": "AnnotationsHttpApi" + } + } + } } } }, - "S3EventHandler": { + "TestServerlessAppFromScratchNoApiGatewayEventsReferenceToUpperGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", "SyncedEvents": [ - "TestS3Bucket" + "RootGet" ], "SyncedEventProperties": { - "TestS3Bucket": [ - "Bucket.Ref", - "Events", - "Filter.S3Key.Rules" + "RootGet": [ + "Path", + "Method", + "ApiId.Ref" ] } }, @@ -1359,43 +1515,58 @@ "MemorySize": 512, "Timeout": 30, "Policies": [ - "AWSLambdaBasicExecutionRole", - "AmazonS3ReadOnlyAccess" + "AWSLambdaBasicExecutionRole" ], "PackageType": "Image", "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.S3EventExamples.S3EventProcessing_ProcessS3Event_Generated::ProcessS3Event" + "TestServerlessApp::TestServerlessApp.FromScratch.NoApiGatewayEventsReference_ToUpper_Generated::ToUpper" ] }, "Events": { - "TestS3Bucket": { - "Type": "S3", + "RootGet": { + "Type": "HttpApi", "Properties": { - "Events": [ - "s3:ObjectCreated:*" - ], - "Filter": { - "S3Key": { - "Rules": [ - { - "Name": "suffix", - "Value": ".json" - } - ] - } - }, - "Bucket": { - "Ref": "TestS3Bucket" + "Path": "/{text}", + "Method": "GET", + "ApiId": { + "Ref": "AnnotationsHttpApi" } } } } } }, + "TestQueue": { + "Type": "AWS::SQS::Queue" + }, "TestS3Bucket": { "Type": "AWS::S3::Bucket" + }, + "TestTopic": { + "Type": "AWS::SNS::Topic" + }, + "TestTable": { + "Properties": { + "StreamSpecification": { + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "AttributeDefinitions": [ + { + "AttributeName": "Id", + "AttributeType": "S" + } + ], + "BillingMode": "PAY_PER_REQUEST", + "KeySchema": [ + { + "AttributeName": "Id", + "KeyType": "HASH" + } + ] + }, + "Type": "AWS::DynamoDB::Table" } } } \ No newline at end of file diff --git a/Libraries/test/TestWebApp/Controllers/BinaryContentController.cs b/Libraries/test/TestWebApp/Controllers/BinaryContentController.cs index 6b3c3a195..5aab5d9e2 100644 --- a/Libraries/test/TestWebApp/Controllers/BinaryContentController.cs +++ b/Libraries/test/TestWebApp/Controllers/BinaryContentController.cs @@ -21,7 +21,7 @@ public IActionResult Get([FromQuery] string firstName, [FromQuery] string lastNa [HttpPut] public string Put() { - using (var reader = new StreamReader(this.HttpContext.Request.Body)) + using (var reader = new StreamReader(HttpContext.Request.Body)) { return reader.ReadToEnd(); } diff --git a/Libraries/test/TestWebApp/Controllers/RawQueryStringController.cs b/Libraries/test/TestWebApp/Controllers/RawQueryStringController.cs index 52e083bf9..456006851 100644 --- a/Libraries/test/TestWebApp/Controllers/RawQueryStringController.cs +++ b/Libraries/test/TestWebApp/Controllers/RawQueryStringController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -12,7 +12,7 @@ public class RawQueryStringController : Controller [HttpGet] public string Get() { - return this.Request.QueryString.ToString(); + return Request.QueryString.ToString(); } [HttpGet] diff --git a/Libraries/test/TestWebApp/Controllers/RedirectTestController.cs b/Libraries/test/TestWebApp/Controllers/RedirectTestController.cs index 3a5f4b0ae..ca5239eb7 100644 --- a/Libraries/test/TestWebApp/Controllers/RedirectTestController.cs +++ b/Libraries/test/TestWebApp/Controllers/RedirectTestController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -14,7 +14,7 @@ public class RedirectTestController : Controller [HttpGet] public ActionResult Get() { - return this.Redirect("redirecttarget"); + return Redirect("redirecttarget"); } } diff --git a/Libraries/test/TestWebApp/Controllers/RequestServicesExampleController.cs b/Libraries/test/TestWebApp/Controllers/RequestServicesExampleController.cs index e193aec5d..7412cf97a 100644 --- a/Libraries/test/TestWebApp/Controllers/RequestServicesExampleController.cs +++ b/Libraries/test/TestWebApp/Controllers/RequestServicesExampleController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -12,7 +12,7 @@ public class RequestServicesExampleController : ControllerBase [HttpGet] public string Get() { - return this.HttpContext.RequestServices?.GetType().FullName; + return HttpContext.RequestServices?.GetType().FullName; } } } diff --git a/Libraries/test/TestWebApp/Controllers/ResourcePathController.cs b/Libraries/test/TestWebApp/Controllers/ResourcePathController.cs index 082f14863..0fc682a15 100644 --- a/Libraries/test/TestWebApp/Controllers/ResourcePathController.cs +++ b/Libraries/test/TestWebApp/Controllers/ResourcePathController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -27,7 +27,7 @@ public string Get(string id) [HttpGet] public string GetString(string value) { - var path = this.HttpContext.Request.Path; + var path = HttpContext.Request.Path; return "value=" + value; } diff --git a/Libraries/test/TestWebApp/Controllers/RouteKeyController.cs b/Libraries/test/TestWebApp/Controllers/RouteKeyController.cs new file mode 100644 index 000000000..3328efa9b --- /dev/null +++ b/Libraries/test/TestWebApp/Controllers/RouteKeyController.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + + +namespace TestWebApp.Controllers +{ + [Route("/")] + public class RouteKeyController : Controller + { + [HttpPost("$default")] + public string PostBody([FromBody] Person body) + { + return $"{body.LastName}, {body.FirstName}"; + } + + public class Person + { + public string FirstName { get; set; } + public string LastName { get; set; } + } + } +} diff --git a/Libraries/test/TestWebApp/Controllers/TraceTestsController.cs b/Libraries/test/TestWebApp/Controllers/TraceTestsController.cs index 970d484a9..268d57ddd 100644 --- a/Libraries/test/TestWebApp/Controllers/TraceTestsController.cs +++ b/Libraries/test/TestWebApp/Controllers/TraceTestsController.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; namespace TestWebApp.Controllers { @@ -8,7 +8,7 @@ public class TraceTestsController : Controller [HttpGet] public string GetTraceId() { - return this.HttpContext.TraceIdentifier; + return HttpContext.TraceIdentifier; } } } diff --git a/Libraries/test/TestWebApp/HttpApiV2LambdaFunction.cs b/Libraries/test/TestWebApp/HttpApiV2LambdaFunction.cs index fa525b113..b8d88ccb1 100644 --- a/Libraries/test/TestWebApp/HttpApiV2LambdaFunction.cs +++ b/Libraries/test/TestWebApp/HttpApiV2LambdaFunction.cs @@ -6,11 +6,9 @@ namespace TestWebApp { public class HttpV2LambdaFunction : APIGatewayHttpApiV2ProxyFunction { -#if NET8_0_OR_GREATER protected override IEnumerable GetBeforeSnapshotRequests() => [ new HttpRequestMessage(HttpMethod.Get, "api/SnapStart") ]; -#endif } } diff --git a/Libraries/test/TestWebApp/Program.cs b/Libraries/test/TestWebApp/Program.cs index b728f4386..9c6421e35 100644 --- a/Libraries/test/TestWebApp/Program.cs +++ b/Libraries/test/TestWebApp/Program.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -12,13 +12,14 @@ public class Program { public static void Main(string[] args) { +#pragma warning disable ASPDEPR008,ASPDEPR004 var host = new WebHostBuilder() .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) .UseIISIntegration() .UseStartup() .Build(); - +#pragma warning restore ASPDEPR008,ASPDEPR004 host.Run(); } } diff --git a/Libraries/test/TestWebApp/Startup.cs b/Libraries/test/TestWebApp/Startup.cs index 055fbb7a9..1d0422c30 100644 --- a/Libraries/test/TestWebApp/Startup.cs +++ b/Libraries/test/TestWebApp/Startup.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -8,22 +8,15 @@ using Microsoft.AspNetCore.ResponseCompression; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using System.Runtime.Serialization.Json; using System.IO; using Microsoft.AspNetCore.Http.Features; - -#if NETCOREAPP_2_1 -using Newtonsoft.Json.Linq; -using Swashbuckle.AspNetCore.Swagger; -#else using System.Text.Json; -#endif namespace TestWebApp { public class Startup { +#pragma warning disable CS0618 public Startup(IHostingEnvironment env) { var builder = new ConfigurationBuilder() @@ -34,6 +27,7 @@ public Startup(IHostingEnvironment env) builder.AddEnvironmentVariables(); Configuration = builder.Build(); } +#pragma warning restore CS0618 public IConfigurationRoot Configuration { get; } @@ -52,28 +46,16 @@ public void ConfigureServices(IServiceCollection services) options.MimeTypes = new string[] { "application/json-compress" }; }); -#if NETCOREAPP_2_1 - services.AddSwaggerGen(c => - { - c.SwaggerDoc("v1", new Info { Title = "My API", Version = "v1" }); - }); - - // Add framework services. - services.AddApplicationInsightsTelemetry(Configuration); - - services.AddMvc(); -#elif NETCOREAPP3_1_OR_GREATER services.AddControllers(); -#endif } +#pragma warning disable CS0618 // This method gets called by the runtime. Use this method to configure the HTTP request pipeline public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseMiddleware(); app.UseResponseCompression(); -#if NETCOREAPP3_1_OR_GREATER app.UseRouting(); app.UseAuthorization(); @@ -82,35 +64,10 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env) { endpoints.MapControllers(); }); -#else - app.UseSwagger(); - - app.UseMvc(); -#endif app.Run(async (context) => { var rawTarget = context.Features.Get()?.RawTarget; -#if NETCOREAPP_2_1 - var root = new JObject(); - root["Path"] = new JValue(context.Request.Path); - root["PathBase"] = new JValue(context.Request.PathBase); - root["RawTarget"] = new JValue(rawTarget); - - var query = new JObject(); - foreach(var queryKey in context.Request.Query.Keys) - { - var variables = new JArray(); - foreach(var v in context.Request.Query[queryKey]) - { - variables.Add(new JValue(v)); - } - query[queryKey] = variables; - } - root["QueryVariables"] = query; - - var body = root.ToString(); -#else var stream = new MemoryStream(); var writer = new Utf8JsonWriter(stream); writer.WriteStartObject(); @@ -132,10 +89,11 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env) writer.Dispose(); stream.Position = 0; var body = new StreamReader(stream).ReadToEnd(); -#endif + context.Response.Headers["Content-Type"] = "application/json"; await context.Response.WriteAsync(body); }); } +#pragma warning restore CS0618 } } diff --git a/Libraries/test/TestWebApp/TestWebApp.csproj b/Libraries/test/TestWebApp/TestWebApp.csproj index e5607beb2..ebc1b93db 100644 --- a/Libraries/test/TestWebApp/TestWebApp.csproj +++ b/Libraries/test/TestWebApp/TestWebApp.csproj @@ -1,7 +1,7 @@  - net6.0;net8.0 + net8.0;net10.0 true TestWebApp Exe @@ -11,9 +11,5 @@ - - - - diff --git a/Libraries/test/TestWebApp/WebsocketApiLambdaFunction.cs b/Libraries/test/TestWebApp/WebsocketApiLambdaFunction.cs new file mode 100644 index 000000000..105302c7f --- /dev/null +++ b/Libraries/test/TestWebApp/WebsocketApiLambdaFunction.cs @@ -0,0 +1,7 @@ +using Amazon.Lambda.AspNetCoreServer; +namespace TestWebApp +{ + public class WebsocketLambdaFunction : APIGatewayWebsocketApiProxyFunction + { + } +} \ No newline at end of file diff --git a/Libraries/test/TestWebApp/serverless.template b/Libraries/test/TestWebApp/serverless.template index 1b9693422..b753ebf38 100644 --- a/Libraries/test/TestWebApp/serverless.template +++ b/Libraries/test/TestWebApp/serverless.template @@ -9,7 +9,7 @@ "Type" : "AWS::Serverless::Function", "Properties": { "Handler": "TestWebApp::TestWebApp.LambdaFunction::FunctionHandlerAsync", - "Runtime": "dotnetcore2.0", + "Runtime": "dotnet10", "CodeUri": "", "Description": "Default function", "MemorySize": 256, @@ -30,4 +30,4 @@ }, "Outputs" : { } -} \ No newline at end of file +} diff --git a/PowerShell/Module/AWSLambdaPSCore.psd1 b/PowerShell/Module/AWSLambdaPSCore.psd1 index 64d14463e..e43306b39 100644 --- a/PowerShell/Module/AWSLambdaPSCore.psd1 +++ b/PowerShell/Module/AWSLambdaPSCore.psd1 @@ -12,7 +12,7 @@ RootModule = 'AWSLambdaPSCore.psm1' # Version number of this module. -ModuleVersion = '5.0.0.0' +ModuleVersion = '5.0.2.0' # Supported PSEditions CompatiblePSEditions = 'Core' diff --git a/PowerShell/Module/Private/_Constants.ps1 b/PowerShell/Module/Private/_Constants.ps1 index a97e1e383..8ccbe2ffb 100644 --- a/PowerShell/Module/Private/_Constants.ps1 +++ b/PowerShell/Module/Private/_Constants.ps1 @@ -20,7 +20,7 @@ if (!($AwsPowerShellFunctionEnvName)) if (!($AwsPowerShellDefaultSdkVersion)) { - New-Variable -Name AwsPowerShellDefaultSdkVersion -Value '7.5.4' -Option Constant + New-Variable -Name AwsPowerShellDefaultSdkVersion -Value '7.6.0' -Option Constant } if (!($AwsPowerShellTargetFramework)) @@ -31,4 +31,33 @@ if (!($AwsPowerShellTargetFramework)) if (!($AwsPowerShellLambdaRuntime)) { New-Variable -Name AwsPowerShellLambdaRuntime -Value 'dotnet10' -Option Constant +} + +if (!($AwsModuleStripFilters)) +{ + # File patterns inside AWS-authored PowerShell modules that have no purpose at + # Lambda runtime (no interactive shell, no Get-Help, no debugger). Stripping + # them reduces package size and INIT (cold-start) duration. LICENSE / NOTICE + # files are intentionally retained. + # + # The '*.xml' pattern matches case-insensitively, covering: + # - .dll-Help.xml — PowerShell MAML help + # - .XML — .NET XMLDoc compiler output (IntelliSense data) + # - PSGetModuleInfo.xml — PowerShellGet install metadata + # Format.ps1xml / Types.ps1xml are NOT matched because their extension is + # .ps1xml (not .xml) and the wildcard requires a literal '.xml' suffix. + New-Variable -Name AwsModuleStripFilters -Value @( + '*.xml', + '*.pdb' + ) -Option Constant +} + +if (!($AwsAuthoredModuleNamePatterns)) +{ + # Only AWS-authored modules under Modules/ are stripped; third-party / community + # modules are left untouched. + New-Variable -Name AwsAuthoredModuleNamePatterns -Value @( + 'AWSPowerShell.NetCore', + 'AWS.Tools.*' + ) -Option Constant } \ No newline at end of file diff --git a/PowerShell/Module/Private/_DeploymentFunctions.ps1 b/PowerShell/Module/Private/_DeploymentFunctions.ps1 index 96c81bc7b..3ac8c2f2c 100644 --- a/PowerShell/Module/Private/_DeploymentFunctions.ps1 +++ b/PowerShell/Module/Private/_DeploymentFunctions.ps1 @@ -479,6 +479,57 @@ function _formatArray return $sb.ToString() } +function _stripAwsModuleFiles +{ + param + ( + [Parameter(Mandatory = $true)] + [string]$ModulesRoot, + + [Parameter(Mandatory = $true)] + [string[]]$Filters, + + [Parameter(Mandatory = $true)] + [string[]]$ModuleNamePatterns + ) + + if (!(Test-Path -Path $ModulesRoot)) + { + return + } + + foreach ($moduleDir in (Get-ChildItem -Path $ModulesRoot -Directory)) + { + $matchesAws = $false + foreach ($pattern in $ModuleNamePatterns) + { + if ($moduleDir.Name -like $pattern) + { + $matchesAws = $true + break + } + } + + if (-not $matchesAws) + { + continue + } + + $removed = Get-ChildItem -Path $moduleDir.FullName -Recurse -File -Include $Filters -ErrorAction SilentlyContinue + foreach ($file in $removed) + { + Write-Verbose ('Removing AWS module file: {0}' -f $file.FullName) + Remove-Item -LiteralPath $file.FullName -Force -ErrorAction SilentlyContinue + } + + $count = ($removed | Measure-Object).Count + if ($count -gt 0) + { + Write-Verbose ('Stripped {0} unwanted file(s) from AWS module {1}' -f $count, $moduleDir.Name) + } + } +} + function _prepareDependentPowerShellModules { param @@ -567,6 +618,11 @@ function _prepareDependentPowerShellModules } ## Add verbosity that no RequiredModules found else {Write-Verbose "No RequiredModules found for script '$Script'"} + + _stripAwsModuleFiles ` + -ModulesRoot $SavedModulesDirectory ` + -Filters $AwsModuleStripFilters ` + -ModuleNamePatterns $AwsAuthoredModuleNamePatterns } function _findLocalModule diff --git a/PowerShell/Module/Templates/Blueprints/aws-lambda-tools-defaults.txt b/PowerShell/Module/Templates/Blueprints/aws-lambda-tools-defaults.txt index a53b061c4..5225a0bf5 100644 --- a/PowerShell/Module/Templates/Blueprints/aws-lambda-tools-defaults.txt +++ b/PowerShell/Module/Templates/Blueprints/aws-lambda-tools-defaults.txt @@ -2,7 +2,7 @@ "profile" : "CONFIGURED_PROFILE", "region" : "CONFIGURED_REGION", "configuration" : "Release", - "function-runtime" : "dotnet6", + "function-runtime" : "", "function-memory-size" : DEFAULT_MEMORY, "function-timeout" : DEFAULT_TIMEOUT, "function-handler" : "PROJECT_NAME::PROJECT_NAME.Bootstrap::ExecuteFunction", diff --git a/PowerShell/Module/Templates/Blueprints/projectfile.csproj.txt b/PowerShell/Module/Templates/Blueprints/projectfile.csproj.txt index 11b68f1aa..5f2a5b1f9 100644 --- a/PowerShell/Module/Templates/Blueprints/projectfile.csproj.txt +++ b/PowerShell/Module/Templates/Blueprints/projectfile.csproj.txt @@ -16,7 +16,7 @@ - - + + \ No newline at end of file diff --git a/PowerShell/Tests/Get-AWSPowerShelLambdaTemplate.Tests.ps1 b/PowerShell/Tests/Get-AWSPowerShelLambdaTemplate.Tests.ps1 index dd5fffe83..54169af12 100644 --- a/PowerShell/Tests/Get-AWSPowerShelLambdaTemplate.Tests.ps1 +++ b/PowerShell/Tests/Get-AWSPowerShelLambdaTemplate.Tests.ps1 @@ -7,31 +7,33 @@ Import-Module $moduleManifestPath InModuleScope -ModuleName $module -ScriptBlock { Describe -Name 'Get-AWSPowerShellLambdaTemplate' -Fixture { - function LoadFakeData - { - ConvertTo-Json -InputObject @{ - manifestVersion = 1 - blueprints = @( - @{ - name = 'Basic' - description = 'Bare bones script' - content = @( - @{ - source = 'basic.ps1.txt' - output = '{basename}.ps1' - filetype = 'lambdaFunction' - }, - @{ - source = 'readme.txt' - output = 'readme.txt' - } - ) - } - ) + BeforeAll { + function LoadFakeData + { + ConvertTo-Json -InputObject @{ + manifestVersion = 1 + blueprints = @( + @{ + name = 'Basic' + description = 'Bare bones script' + content = @( + @{ + source = 'basic.ps1.txt' + output = '{basename}.ps1' + filetype = 'lambdaFunction' + }, + @{ + source = 'readme.txt' + output = 'readme.txt' + } + ) + } + ) + } } + Mock -CommandName '_getHostedBlueprintsContent' -MockWith {LoadFakeData} + Mock -CommandName '_getLocalBlueprintsContent' -MockWith {LoadFakeData} } - Mock -CommandName '_getHostedBlueprintsContent' -MockWith {LoadFakeData} - Mock -CommandName '_getLocalBlueprintsContent' -MockWith {LoadFakeData} Context -Name 'Online Templates' -Fixture { It -Name 'Retrieves Blueprints from online sources by default' -Test { diff --git a/PowerShell/Tests/_stripAwsModuleFiles.Tests.ps1 b/PowerShell/Tests/_stripAwsModuleFiles.Tests.ps1 new file mode 100644 index 000000000..25de6131c --- /dev/null +++ b/PowerShell/Tests/_stripAwsModuleFiles.Tests.ps1 @@ -0,0 +1,251 @@ +$module = 'AWSLambdaPSCore' +$moduleManifestPath = [System.IO.Path]::Combine('..', 'Module', "$module.psd1") + +if (Get-Module -Name $module) {Remove-Module -Name $module} +Import-Module $moduleManifestPath + + InModuleScope -ModuleName $module -ScriptBlock { + Describe -Name '_stripAwsModuleFiles' -Fixture { + + BeforeAll { + function New-FakeModuleDir + { + param + ( + [Parameter(Mandatory = $true)][string]$Root, + [Parameter(Mandatory = $true)][string]$Name, + [Parameter()][string[]]$ExtraFiles = @() + ) + + $dir = Join-Path -Path $Root -ChildPath $Name + New-Item -ItemType Directory -Path $dir -Force | Out-Null + + $defaults = @( + 'AWS.Tools.S3.dll-Help.xml', + 'AWS.Tools.S3-Help.xml', + 'LICENSE', + 'LICENSE.txt', + 'NOTICE', + 'NOTICE.txt', + 'AWS.Tools.S3.pdb', + 'AWS.Tools.S3.psm1', + 'AWS.Tools.S3.psd1' + ) + foreach ($f in ($defaults + $ExtraFiles)) + { + Set-Content -Path (Join-Path -Path $dir -ChildPath $f) -Value 'x' -Force + } + return $dir + } + } + + Context -Name 'AWS-authored module directories' -Fixture { + + It -Name 'Strips unwanted files from AWS.Tools.* modules' -Test { + $root = Join-Path -Path $TestDrive -ChildPath 'aws-tools' + New-Item -ItemType Directory -Path $root -Force | Out-Null + $moduleDir = New-FakeModuleDir -Root $root -Name 'AWS.Tools.S3' + + _stripAwsModuleFiles ` + -ModulesRoot $root ` + -Filters $AwsModuleStripFilters ` + -ModuleNamePatterns $AwsAuthoredModuleNamePatterns + + Test-Path -Path (Join-Path $moduleDir 'AWS.Tools.S3.dll-Help.xml') | Should -BeFalse + Test-Path -Path (Join-Path $moduleDir 'AWS.Tools.S3-Help.xml') | Should -BeFalse + Test-Path -Path (Join-Path $moduleDir 'AWS.Tools.S3.pdb') | Should -BeFalse + Test-Path -Path (Join-Path $moduleDir 'LICENSE') | Should -BeTrue + Test-Path -Path (Join-Path $moduleDir 'LICENSE.txt') | Should -BeTrue + Test-Path -Path (Join-Path $moduleDir 'NOTICE') | Should -BeTrue + Test-Path -Path (Join-Path $moduleDir 'NOTICE.txt') | Should -BeTrue + Test-Path -Path (Join-Path $moduleDir 'AWS.Tools.S3.psm1') | Should -BeTrue + Test-Path -Path (Join-Path $moduleDir 'AWS.Tools.S3.psd1') | Should -BeTrue + } + + It -Name 'Strips unwanted files from AWSPowerShell.NetCore (exact-name match)' -Test { + $root = Join-Path -Path $TestDrive -ChildPath 'monolithic' + New-Item -ItemType Directory -Path $root -Force | Out-Null + $moduleDir = New-FakeModuleDir -Root $root -Name 'AWSPowerShell.NetCore' + + _stripAwsModuleFiles ` + -ModulesRoot $root ` + -Filters $AwsModuleStripFilters ` + -ModuleNamePatterns $AwsAuthoredModuleNamePatterns + + Test-Path -Path (Join-Path $moduleDir 'AWS.Tools.S3.pdb') | Should -BeFalse + Test-Path -Path (Join-Path $moduleDir 'LICENSE') | Should -BeTrue + Test-Path -Path (Join-Path $moduleDir 'AWS.Tools.S3.psm1') | Should -BeTrue + } + + It -Name 'Recurses into nested version subdirectories' -Test { + $root = Join-Path -Path $TestDrive -ChildPath 'versioned' + New-Item -ItemType Directory -Path $root -Force | Out-Null + $versionDir = Join-Path -Path $root -ChildPath 'AWS.Tools.EC2\1.2.3' + New-Item -ItemType Directory -Path $versionDir -Force | Out-Null + Set-Content -Path (Join-Path $versionDir 'AWS.Tools.EC2.dll-Help.xml') -Value 'x' + Set-Content -Path (Join-Path $versionDir 'AWS.Tools.EC2.pdb') -Value 'x' + Set-Content -Path (Join-Path $versionDir 'AWS.Tools.EC2.psm1') -Value 'x' + + _stripAwsModuleFiles ` + -ModulesRoot $root ` + -Filters $AwsModuleStripFilters ` + -ModuleNamePatterns $AwsAuthoredModuleNamePatterns + + Test-Path -Path (Join-Path $versionDir 'AWS.Tools.EC2.dll-Help.xml') | Should -BeFalse + Test-Path -Path (Join-Path $versionDir 'AWS.Tools.EC2.pdb') | Should -BeFalse + Test-Path -Path (Join-Path $versionDir 'AWS.Tools.EC2.psm1') | Should -BeTrue + } + } + + Context -Name 'Non-AWS module directories' -Fixture { + + It -Name 'Leaves third-party modules untouched' -Test { + $root = Join-Path -Path $TestDrive -ChildPath 'third-party' + New-Item -ItemType Directory -Path $root -Force | Out-Null + $moduleDir = New-FakeModuleDir -Root $root -Name 'Pester' + + _stripAwsModuleFiles ` + -ModulesRoot $root ` + -Filters $AwsModuleStripFilters ` + -ModuleNamePatterns $AwsAuthoredModuleNamePatterns + + Test-Path -Path (Join-Path $moduleDir 'AWS.Tools.S3.dll-Help.xml') | Should -BeTrue + Test-Path -Path (Join-Path $moduleDir 'LICENSE') | Should -BeTrue + Test-Path -Path (Join-Path $moduleDir 'NOTICE') | Should -BeTrue + Test-Path -Path (Join-Path $moduleDir 'AWS.Tools.S3.pdb') | Should -BeTrue + Test-Path -Path (Join-Path $moduleDir 'AWS.Tools.S3.psm1') | Should -BeTrue + } + + It -Name 'Strips AWS module while leaving co-resident third-party module untouched' -Test { + $root = Join-Path -Path $TestDrive -ChildPath 'mixed' + New-Item -ItemType Directory -Path $root -Force | Out-Null + $awsDir = New-FakeModuleDir -Root $root -Name 'AWS.Tools.Lambda' + $otherDir = New-FakeModuleDir -Root $root -Name 'PSReadLine' + + _stripAwsModuleFiles ` + -ModulesRoot $root ` + -Filters $AwsModuleStripFilters ` + -ModuleNamePatterns $AwsAuthoredModuleNamePatterns + + # AWS module: .pdb stripped, LICENSE retained + Test-Path -Path (Join-Path $awsDir 'AWS.Tools.S3.pdb') | Should -BeFalse + Test-Path -Path (Join-Path $awsDir 'LICENSE') | Should -BeTrue + + # Third-party module: nothing touched + Test-Path -Path (Join-Path $otherDir 'AWS.Tools.S3.pdb') | Should -BeTrue + Test-Path -Path (Join-Path $otherDir 'LICENSE') | Should -BeTrue + } + } + + Context -Name 'Files intentionally retained' -Fixture { + + It -Name 'Does not remove LICENSE or NOTICE files (Apache 2.0 retention)' -Test { + $root = Join-Path -Path $TestDrive -ChildPath 'license-retained' + New-Item -ItemType Directory -Path $root -Force | Out-Null + $moduleDir = New-FakeModuleDir -Root $root -Name 'AWS.Tools.S3' + + _stripAwsModuleFiles ` + -ModulesRoot $root ` + -Filters $AwsModuleStripFilters ` + -ModuleNamePatterns $AwsAuthoredModuleNamePatterns + + Test-Path -Path (Join-Path $moduleDir 'LICENSE') | Should -BeTrue + Test-Path -Path (Join-Path $moduleDir 'LICENSE.txt') | Should -BeTrue + Test-Path -Path (Join-Path $moduleDir 'NOTICE') | Should -BeTrue + Test-Path -Path (Join-Path $moduleDir 'NOTICE.txt') | Should -BeTrue + } + + It -Name 'Does not remove Format.ps1xml or Types.ps1xml files' -Test { + $root = Join-Path -Path $TestDrive -ChildPath 'narrow-xml' + New-Item -ItemType Directory -Path $root -Force | Out-Null + $moduleDir = New-FakeModuleDir -Root $root -Name 'AWS.Tools.S3' -ExtraFiles @( + 'AWS.Tools.S3.Format.ps1xml', + 'AWS.Tools.S3.Types.ps1xml' + ) + + _stripAwsModuleFiles ` + -ModulesRoot $root ` + -Filters $AwsModuleStripFilters ` + -ModuleNamePatterns $AwsAuthoredModuleNamePatterns + + Test-Path -Path (Join-Path $moduleDir 'AWS.Tools.S3.dll-Help.xml') | Should -BeFalse + Test-Path -Path (Join-Path $moduleDir 'AWS.Tools.S3.Format.ps1xml') | Should -BeTrue + Test-Path -Path (Join-Path $moduleDir 'AWS.Tools.S3.Types.ps1xml') | Should -BeTrue + } + + It -Name 'Strips XMLDoc (.XML) and PSGetModuleInfo.xml' -Test { + $root = Join-Path -Path $TestDrive -ChildPath 'xmldoc' + New-Item -ItemType Directory -Path $root -Force | Out-Null + $moduleDir = New-FakeModuleDir -Root $root -Name 'AWS.Tools.S3' -ExtraFiles @( + 'AWS.Tools.S3.XML', + 'PSGetModuleInfo.xml' + ) + + _stripAwsModuleFiles ` + -ModulesRoot $root ` + -Filters $AwsModuleStripFilters ` + -ModuleNamePatterns $AwsAuthoredModuleNamePatterns + + Test-Path -Path (Join-Path $moduleDir 'AWS.Tools.S3.XML') | Should -BeFalse + Test-Path -Path (Join-Path $moduleDir 'PSGetModuleInfo.xml') | Should -BeFalse + } + + It -Name 'Does not remove Aliases.psm1 or Completers.psm1' -Test { + $root = Join-Path -Path $TestDrive -ChildPath 'preserve-nested' + New-Item -ItemType Directory -Path $root -Force | Out-Null + $moduleDir = New-FakeModuleDir -Root $root -Name 'AWS.Tools.S3' -ExtraFiles @( + 'AWS.Tools.S3.Aliases.psm1', + 'AWS.Tools.S3.Completers.psm1' + ) + + _stripAwsModuleFiles ` + -ModulesRoot $root ` + -Filters $AwsModuleStripFilters ` + -ModuleNamePatterns $AwsAuthoredModuleNamePatterns + + Test-Path -Path (Join-Path $moduleDir 'AWS.Tools.S3.Aliases.psm1') | Should -BeTrue + Test-Path -Path (Join-Path $moduleDir 'AWS.Tools.S3.Completers.psm1') | Should -BeTrue + } + } + + Context -Name 'Robustness' -Fixture { + + It -Name 'Is idempotent (second run is a clean no-op)' -Test { + $root = Join-Path -Path $TestDrive -ChildPath 'idempotent' + New-Item -ItemType Directory -Path $root -Force | Out-Null + $moduleDir = New-FakeModuleDir -Root $root -Name 'AWS.Tools.S3' + + _stripAwsModuleFiles ` + -ModulesRoot $root ` + -Filters $AwsModuleStripFilters ` + -ModuleNamePatterns $AwsAuthoredModuleNamePatterns + + { _stripAwsModuleFiles ` + -ModulesRoot $root ` + -Filters $AwsModuleStripFilters ` + -ModuleNamePatterns $AwsAuthoredModuleNamePatterns } | Should -Not -Throw + + Test-Path -Path (Join-Path $moduleDir 'AWS.Tools.S3.psm1') | Should -BeTrue + } + + It -Name 'No-ops gracefully when ModulesRoot does not exist' -Test { + $missing = Join-Path -Path $TestDrive -ChildPath 'does-not-exist' + + { _stripAwsModuleFiles ` + -ModulesRoot $missing ` + -Filters $AwsModuleStripFilters ` + -ModuleNamePatterns $AwsAuthoredModuleNamePatterns } | Should -Not -Throw + } + + It -Name 'No-ops on empty ModulesRoot' -Test { + $root = Join-Path -Path $TestDrive -ChildPath 'empty-root' + New-Item -ItemType Directory -Path $root -Force | Out-Null + + { _stripAwsModuleFiles ` + -ModulesRoot $root ` + -Filters $AwsModuleStripFilters ` + -ModuleNamePatterns $AwsAuthoredModuleNamePatterns } | Should -Not -Throw + } + } + } # End Describe +} # End InModuleScope diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Amazon.Lambda.TestTool.csproj b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Amazon.Lambda.TestTool.csproj index 9711bce5d..beb981862 100644 --- a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Amazon.Lambda.TestTool.csproj +++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Amazon.Lambda.TestTool.csproj @@ -15,8 +15,8 @@ true Amazon.Lambda.TestTool dotnet-lambda-test-tool - 0.13.0 - NU5100 + 0.14.1 + NU5100;NU5048;CS1591 Major README.md @@ -26,6 +26,10 @@ + + + + @@ -75,4 +79,13 @@ + + + + + + PreserveNewest + + + diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Commands/RunCommand.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Commands/RunCommand.cs index 036611cd5..d4397b301 100644 --- a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Commands/RunCommand.cs +++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Commands/RunCommand.cs @@ -8,6 +8,7 @@ using Amazon.Lambda.TestTool.Models; using Amazon.Lambda.TestTool.Processes; using Amazon.Lambda.TestTool.Processes.SQSEventSource; +using Amazon.Lambda.TestTool.Processes.DynamoDBStreamsEventSource; using Amazon.Lambda.TestTool.Services; using Amazon.Lambda.TestTool.Services.IO; using Spectre.Console.Cli; @@ -28,7 +29,7 @@ public sealed class RunCommand( /// /// Task for the Lambda Runtime API. /// - public Task LambdRuntimeApiTask { get; private set; } + public Task? LambdRuntimeApiTask { get; private set; } /// /// The method responsible for executing the . @@ -39,10 +40,10 @@ public override async Task ExecuteAsync(CommandContext context, RunCommandS { EvaluateEnvironmentVariables(settings); - if (!settings.LambdaEmulatorPort.HasValue && !settings.ApiGatewayEmulatorPort.HasValue && !settings.ApiGatewayEmulatorHttpsPort.HasValue && string.IsNullOrEmpty(settings.SQSEventSourceConfig)) + if (!settings.LambdaEmulatorPort.HasValue && !settings.ApiGatewayEmulatorPort.HasValue && !settings.ApiGatewayEmulatorHttpsPort.HasValue && string.IsNullOrEmpty(settings.SQSEventSourceConfig) && string.IsNullOrEmpty(settings.DynamoDBStreamsEventSourceConfig)) { throw new ArgumentException("At least one of the following parameters must be set: " + - "--lambda-emulator-port, --api-gateway-emulator-port, --api-gateway-emulator-https-port or --sqs-eventsource-config"); + "--lambda-emulator-port, --api-gateway-emulator-port, --api-gateway-emulator-https-port, --sqs-eventsource-config or --dynamodbstreams-eventsource-config"); } var tasks = new List(); @@ -87,6 +88,12 @@ public override async Task ExecuteAsync(CommandContext context, RunCommandS { var sqsEventSourceProcess = SQSEventSourceProcess.Startup(settings, cancellationTokenSource.Token); tasks.Add(sqsEventSourceProcess.RunningTask); + } + + if (!string.IsNullOrEmpty(settings.DynamoDBStreamsEventSourceConfig)) + { + var dynamoDBStreamsProcess = DynamoDBStreamsEventSourceProcess.Startup(settings, cancellationTokenSource.Token); + tasks.Add(dynamoDBStreamsProcess.RunningTask); } await Task.Run(() => Task.WaitAny(tasks.ToArray(), cancellationTokenSource.Token)); @@ -184,6 +191,16 @@ private void EvaluateEnvironmentVariables(RunCommandSettings settings) throw new InvalidOperationException($"Environment variable {envVariable} for the SQS event source config was empty"); } settings.SQSEventSourceConfig = environmentVariables[envVariable]?.ToString(); + } + + if (settings.DynamoDBStreamsEventSourceConfig != null && settings.DynamoDBStreamsEventSourceConfig.StartsWith(Constants.ArgumentEnvironmentVariablePrefix, StringComparison.CurrentCultureIgnoreCase)) + { + var envVariable = settings.DynamoDBStreamsEventSourceConfig.Substring(Constants.ArgumentEnvironmentVariablePrefix.Length); + if (!environmentVariables.Contains(envVariable)) + { + throw new InvalidOperationException($"Environment variable {envVariable} for the DynamoDB Streams event source config was empty"); + } + settings.DynamoDBStreamsEventSourceConfig = environmentVariables[envVariable]?.ToString(); } } } diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Commands/Settings/RunCommandSettings.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Commands/Settings/RunCommandSettings.cs index 0584eaa30..f4ab3d57f 100644 --- a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Commands/Settings/RunCommandSettings.cs +++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Commands/Settings/RunCommandSettings.cs @@ -82,6 +82,14 @@ public sealed class RunCommandSettings : CommandSettings [Description("The configuration for the SQS event source. The format of the config is a comma delimited key pairs. For example \"QueueUrl=,FunctionName=,VisibilityTimeout=100\". Possible keys are: BatchSize, DisableMessageDelete, FunctionName, LambdaRuntimeApi, Profile, QueueUrl, Region, VisibilityTimeout")] public string? SQSEventSourceConfig { get; set; } + + /// + /// The configuration for the DynamoDB Streams event source. The config can be provided as comma delimited key pairs, a JSON object, a JSON array, or a file path to a JSON configuration file. For example "TableName=my-table,FunctionName=function-name,BatchSize=100". + /// Possible keys are: BatchSize, FunctionName, LambdaRuntimeApi, PollingIntervalMs, Profile, Region, TableName + /// + [CommandOption("--dynamodbstreams-eventsource-config ")] + [Description("The configuration for the DynamoDB Streams event source. The config can be provided as comma delimited key pairs, a JSON object, a JSON array, or a file path to a JSON configuration file. For example \"TableName=,FunctionName=,BatchSize=100\". Possible keys are: BatchSize, FunctionName, LambdaRuntimeApi, PollingIntervalMs, Profile, Region, TableName")] + public string? DynamoDBStreamsEventSourceConfig { get; set; } /// /// The absolute path used to save global settings and saved requests. You will need to specify a path in order to enable saving global settings and requests. /// diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/HttpContextExtensions.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/HttpContextExtensions.cs index b0e585f7d..f7e95614f 100644 --- a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/HttpContextExtensions.cs +++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/HttpContextExtensions.cs @@ -124,6 +124,7 @@ public static async Task ToApiGatewayHttpV2Requ /// /// The to be translated. /// The configuration of the API Gateway route, including the HTTP method, path, and other metadata. + /// /// An object representing the translated request. public static async Task ToApiGatewayRequest( this HttpContext context, diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Models/EventContainer.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Models/EventContainer.cs index 366cb2a17..694f134cb 100644 --- a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Models/EventContainer.cs +++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Models/EventContainer.cs @@ -68,7 +68,7 @@ public void ReportSuccessResponse(string response) _dataStore.RaiseStateChanged(); } - public void ReportErrorResponse(string errorType, string errorBody) + public void ReportErrorResponse(string errorType, string? errorBody) { LastUpdated = DateTime.Now; ErrorType = errorType; diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/DynamoDBStreamsEventSource/DynamoDBStreamsEventSourceBackgroundService.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/DynamoDBStreamsEventSource/DynamoDBStreamsEventSourceBackgroundService.cs new file mode 100644 index 000000000..7b7647ddb --- /dev/null +++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/DynamoDBStreamsEventSource/DynamoDBStreamsEventSourceBackgroundService.cs @@ -0,0 +1,435 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.DynamoDBv2; +using Amazon.DynamoDBStreams; +using Amazon.DynamoDBStreams.Model; +using Amazon.Lambda.DynamoDBEvents; +using Amazon.Lambda.Model; +using Amazon.Lambda.TestTool.Services; +using Amazon.Runtime; +using System.Text.Json; + +namespace Amazon.Lambda.TestTool.Processes.DynamoDBStreamsEventSource; + +/// +/// IHostedService that will run continually polling a DynamoDB Stream for records and invoking the connected +/// Lambda function with the polled records. +/// +public class DynamoDBStreamsEventSourceBackgroundService : BackgroundService +{ + private static readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + private readonly ILogger _logger; + private readonly IAmazonDynamoDB _ddbClient; + private readonly IAmazonDynamoDBStreams _streamsClient; + private readonly ILambdaClient _lambdaClient; + private readonly DynamoDBStreamsEventSourceBackgroundServiceConfig _config; + + /// + /// Constructs instance of . + /// + public DynamoDBStreamsEventSourceBackgroundService( + ILogger logger, + IAmazonDynamoDB ddbClient, + IAmazonDynamoDBStreams streamsClient, + DynamoDBStreamsEventSourceBackgroundServiceConfig config, + ILambdaClient lambdaClient) + { + _logger = logger; + _ddbClient = ddbClient; + _streamsClient = streamsClient; + _config = config; + _lambdaClient = lambdaClient; + } + + /// + /// Execute the DynamoDBStreamsEventSourceBackgroundService. + /// + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("Starting DynamoDB Streams poller for table: {tableName}", _config.TableName); + + while (!stoppingToken.IsCancellationRequested) + { + try + { + var streamArn = await GetStreamArnForTable(stoppingToken); + if (streamArn == null) + { + _logger.LogWarning("No stream found for table {tableName}. Retrying in 5 seconds.", _config.TableName); + await Task.Delay(5000, stoppingToken); + continue; + } + + await PollStream(streamArn, stoppingToken); + } + catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) + { + return; + } + catch (TaskCanceledException) when (stoppingToken.IsCancellationRequested) + { + return; + } + catch (Exception e) + { + _logger.LogWarning(e, "Exception occurred in DynamoDB Streams poller for {tableName}: {message}", _config.TableName, e.Message); + await Task.Delay(3000, stoppingToken); + } + } + } + + private async Task GetStreamArnForTable(CancellationToken stoppingToken) + { + // If the configured value is already a stream ARN, use it directly + if (_config.TableName.StartsWith("arn:") && _config.TableName.Contains("/stream/")) + { + _logger.LogInformation("Using provided stream ARN directly: {streamArn}", _config.TableName); + return _config.TableName; + } + + _logger.LogInformation("Looking up latest stream ARN for table {tableName}", _config.TableName); + var response = await _ddbClient.DescribeTableAsync(_config.TableName, stoppingToken); + _logger.LogInformation("Resolved stream ARN: {streamArn}", response.Table.LatestStreamArn); + return response.Table.LatestStreamArn; + } + + private async Task PollStream(string streamArn, CancellationToken stoppingToken) + { + // Shard polling strategy: + // + // Goal: Only deliver records to Lambda that were written AFTER the test tool started. + // + // 1. At startup, discover all shards. Open shards get a LATEST iterator (future records only). + // Closed shards are recorded in a "closed at startup" set and never polled — they contain + // only historical data from before the tool started. + // + // 2. Every 30 seconds (or immediately when a shard is exhausted), re-discover shards: + // - Shards already being polled: leave their iterator alone (preserves position). + // - Shards in the "closed at startup" set: skip (pre-existing historical data). + // - Any other shard (new since startup): poll with TRIM_HORIZON to read all its records, + // since the shard was created after the tool started and all its data is relevant. + + var closedAtStartup = new HashSet(); + var shardIterators = await DiscoverInitialShards(streamArn, closedAtStartup, stoppingToken); + + _logger.LogInformation("Initial discovery: {openCount} open shard(s), {closedCount} closed shard(s) at startup", + shardIterators.Count, closedAtStartup.Count); + + var lastDiscoveryTime = DateTime.UtcNow; + const int ShardRediscoveryIntervalSeconds = 30; + + while (!stoppingToken.IsCancellationRequested) + { + // Poll all active shards concurrently + var tasks = new List>(); + foreach (var (shardId, iterator) in shardIterators) + { + if (iterator == null) + continue; + tasks.Add(PollShard(shardId, iterator, stoppingToken)); + } + + var activeCount = tasks.Count; + _logger.LogDebug("Polling {activeShardCount} active shard(s)", activeCount); + + if (activeCount == 0) + { + // No active shards — re-discover + shardIterators = await DiscoverNewShards(streamArn, shardIterators, closedAtStartup, stoppingToken); + lastDiscoveryTime = DateTime.UtcNow; + if (shardIterators.Count == 0) + { + await Task.Delay(1000, stoppingToken); + } + continue; + } + + var results = await Task.WhenAll(tasks); + + var hasRecords = false; + var shardExhausted = false; + foreach (var (shardId, response) in results) + { + if (response == null) + continue; + + if (response.NextShardIterator == null) + { + _logger.LogInformation("Shard {shardId} exhausted (closed), records in final batch: {count}", + shardId, response.Records?.Count); + shardIterators.Remove(shardId); + shardExhausted = true; + } + else + { + shardIterators[shardId] = response.NextShardIterator; + } + + if (response.Records == null || response.Records.Count == 0) + continue; + + hasRecords = true; + _logger.LogInformation("Retrieved {recordCount} record(s) from shard {shardId}", response.Records.Count, shardId); + var lambdaRecords = ConvertToLambdaRecords(response.Records, streamArn); + + var lambdaPayload = new DynamoDBEvent { Records = lambdaRecords }; + var invokeRequest = new InvokeRequest + { + InvocationType = InvocationType.RequestResponse, + FunctionName = _config.FunctionName, + Payload = JsonSerializer.Serialize(lambdaPayload, _jsonOptions) + }; + + _logger.LogInformation("Invoking Lambda function {functionName} with {recordCount} DynamoDB stream records", + _config.FunctionName, lambdaRecords.Count); + + var lambdaResponse = await _lambdaClient.InvokeAsync(invokeRequest, _config.LambdaRuntimeApi); + + if (lambdaResponse.FunctionError != null) + { + _logger.LogError("Invoking Lambda {function} with {recordCount} DynamoDB stream records failed with error {errorMessage}", + _config.FunctionName, lambdaRecords.Count, lambdaResponse.FunctionError); + } + } + + // Re-discover if a shard was exhausted or 30 seconds have elapsed + var timeSinceDiscovery = (DateTime.UtcNow - lastDiscoveryTime).TotalSeconds; + if (shardExhausted || timeSinceDiscovery >= ShardRediscoveryIntervalSeconds) + { + _logger.LogInformation("Re-discovering shards (exhausted={shardExhausted}, elapsed={elapsed}s)", + shardExhausted, (int)timeSinceDiscovery); + shardIterators = await DiscoverNewShards(streamArn, shardIterators, closedAtStartup, stoppingToken); + lastDiscoveryTime = DateTime.UtcNow; + continue; + } + + if (!hasRecords) + { + await Task.Delay(_config.PollingIntervalMs, stoppingToken); + } + } + } + + private async Task<(string ShardId, GetRecordsResponse? Response)> PollShard(string shardId, string iterator, CancellationToken stoppingToken) + { + try + { + var response = await _streamsClient.GetRecordsAsync(new GetRecordsRequest + { + ShardIterator = iterator, + Limit = _config.BatchSize + }, stoppingToken); + + return (shardId, response); + } + catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) + { + throw; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to poll records for shard {shardId}", shardId); + return (shardId, null); + } + } + + /// + /// Initial shard discovery at startup. Uses LATEST for open shards and records closed shard IDs. + /// + private async Task> DiscoverInitialShards(string streamArn, HashSet closedAtStartup, CancellationToken stoppingToken) + { + var shards = await GetAllShards(streamArn, stoppingToken); + var iterators = new Dictionary(); + + foreach (var shard in shards) + { + var isClosed = shard.SequenceNumberRange?.EndingSequenceNumber != null; + if (isClosed) + { + closedAtStartup.Add(shard.ShardId); + continue; + } + + // Open shard — use LATEST to only get records created after startup + var iteratorResponse = await _streamsClient.GetShardIteratorAsync(new GetShardIteratorRequest + { + StreamArn = streamArn, + ShardId = shard.ShardId, + ShardIteratorType = ShardIteratorType.LATEST + }, stoppingToken); + + _logger.LogInformation("Got LATEST iterator for startup shard {shardId}", shard.ShardId); + iterators[shard.ShardId] = iteratorResponse.ShardIterator; + } + _logger.LogInformation("Initial shard discovery complete: {openCount} open shard(s), {closedCount} closed shard(s) at startup", + iterators.Count, closedAtStartup.Count); + + return iterators; + } + + /// + /// Ongoing shard discovery. Preserves existing iterators, skips shards closed at startup, + /// and starts TRIM_HORIZON pollers for any new shards (even if closed). + /// + private async Task> DiscoverNewShards(string streamArn, Dictionary existingIterators, HashSet closedAtStartup, CancellationToken stoppingToken) + { + var shards = await GetAllShards(streamArn, stoppingToken); + var iterators = new Dictionary(existingIterators); + + foreach (var shard in shards) + { + // Already being polled — leave iterator alone + if (iterators.ContainsKey(shard.ShardId)) + continue; + + // Was closed at startup — skip + if (closedAtStartup.Contains(shard.ShardId)) + continue; + + // New shard discovered after startup — use TRIM_HORIZON to read all its records + var iteratorResponse = await _streamsClient.GetShardIteratorAsync(new GetShardIteratorRequest + { + StreamArn = streamArn, + ShardId = shard.ShardId, + ShardIteratorType = ShardIteratorType.TRIM_HORIZON + }, stoppingToken); + + _logger.LogInformation("Got TRIM_HORIZON iterator for new shard {shardId}", shard.ShardId); + iterators[shard.ShardId] = iteratorResponse.ShardIterator; + } + + return iterators; + } + + private async Task> GetAllShards(string streamArn, CancellationToken stoppingToken) + { + _logger.LogDebug("Discovering shards for stream {streamArn}", streamArn); + var shards = new List(); + string? lastEvaluatedShardId = null; + + do + { + var describeResponse = await _streamsClient.DescribeStreamAsync(new DescribeStreamRequest + { + StreamArn = streamArn, + ExclusiveStartShardId = lastEvaluatedShardId + }, stoppingToken); + + shards.AddRange(describeResponse.StreamDescription.Shards); + lastEvaluatedShardId = describeResponse.StreamDescription.LastEvaluatedShardId; + } while (lastEvaluatedShardId != null); + + _logger.LogDebug("There were {shardCount} shard(s) returned from DescribeStream", shards.Count); + return shards; + } + + /// + /// Convert from the SDK's DynamoDB Streams records to the Lambda event's DynamoDB record type. + /// + internal static IList ConvertToLambdaRecords(List records, string streamArn) + { + return records.Select(r => ConvertToLambdaRecord(r, streamArn)).ToList(); + } + + /// + /// Convert a single SDK stream record to the Lambda event record type. + /// + internal static DynamoDBEvent.DynamodbStreamRecord ConvertToLambdaRecord(Record record, string streamArn) + { + var lambdaRecord = new DynamoDBEvent.DynamodbStreamRecord + { + EventID = record.EventID, + EventName = record.EventName?.Value, + EventSource = "aws:dynamodb", + EventSourceArn = streamArn, + EventVersion = record.EventVersion, + AwsRegion = Arn.Parse(streamArn).Region + }; + + if (record.Dynamodb != null) + { + lambdaRecord.Dynamodb = new DynamoDBEvent.StreamRecord + { + ApproximateCreationDateTime = record.Dynamodb.ApproximateCreationDateTime ?? DateTime.MinValue, + SequenceNumber = record.Dynamodb.SequenceNumber, + SizeBytes = record.Dynamodb.SizeBytes ?? 0, + StreamViewType = record.Dynamodb.StreamViewType?.Value + }; + + if (record.Dynamodb.Keys != null) + { + lambdaRecord.Dynamodb.Keys = ConvertAttributeMap(record.Dynamodb.Keys); + } + + if (record.Dynamodb.NewImage != null) + { + lambdaRecord.Dynamodb.NewImage = ConvertAttributeMap(record.Dynamodb.NewImage); + } + + if (record.Dynamodb.OldImage != null) + { + lambdaRecord.Dynamodb.OldImage = ConvertAttributeMap(record.Dynamodb.OldImage); + } + } + + if (record.UserIdentity != null) + { + lambdaRecord.UserIdentity = new DynamoDBEvent.Identity + { + PrincipalId = record.UserIdentity.PrincipalId, + Type = record.UserIdentity.Type + }; + } + + return lambdaRecord; + } + + /// + /// Convert SDK AttributeValue dictionary to Lambda event AttributeValue dictionary. + /// + internal static Dictionary ConvertAttributeMap(Dictionary sdkMap) + { + var result = new Dictionary(); + foreach (var kvp in sdkMap) + { + result[kvp.Key] = ConvertAttributeValue(kvp.Value); + } + return result; + } + + /// + /// Convert a single SDK AttributeValue to the Lambda event AttributeValue. + /// + internal static DynamoDBEvent.AttributeValue ConvertAttributeValue(AttributeValue sdkValue) + { + var lambdaValue = new DynamoDBEvent.AttributeValue(); + + if (sdkValue.S != null) + lambdaValue.S = sdkValue.S; + if (sdkValue.N != null) + lambdaValue.N = sdkValue.N; + if (sdkValue.B != null) + lambdaValue.B = sdkValue.B; + if (sdkValue.BOOL != null) + lambdaValue.BOOL = sdkValue.BOOL; + if (sdkValue.NULL != null) + lambdaValue.NULL = sdkValue.NULL; + if (sdkValue.SS != null) + lambdaValue.SS = sdkValue.SS; + if (sdkValue.NS != null) + lambdaValue.NS = sdkValue.NS; + if (sdkValue.BS != null) + lambdaValue.BS = sdkValue.BS; + if (sdkValue.L != null) + lambdaValue.L = sdkValue.L.Select(ConvertAttributeValue).ToList(); + if (sdkValue.M != null) + lambdaValue.M = ConvertAttributeMap(sdkValue.M); + + return lambdaValue; + } +} diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/DynamoDBStreamsEventSource/DynamoDBStreamsEventSourceBackgroundServiceConfig.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/DynamoDBStreamsEventSource/DynamoDBStreamsEventSourceBackgroundServiceConfig.cs new file mode 100644 index 000000000..07e4a4b58 --- /dev/null +++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/DynamoDBStreamsEventSource/DynamoDBStreamsEventSourceBackgroundServiceConfig.cs @@ -0,0 +1,35 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace Amazon.Lambda.TestTool.Processes.DynamoDBStreamsEventSource; + +/// +/// Configuration for the service. +/// +public class DynamoDBStreamsEventSourceBackgroundServiceConfig +{ + /// + /// The batch size to read from the stream and send to the Lambda function. + /// + public required int BatchSize { get; init; } = DynamoDBStreamsEventSourceProcess.DefaultBatchSize; + + /// + /// The Lambda function to send the DynamoDB stream records to. + /// + public required string FunctionName { get; init; } + + /// + /// The endpoint where the emulated Lambda runtime API is running. + /// + public required string LambdaRuntimeApi { get; init; } + + /// + /// The DynamoDB table name to read streams from. + /// + public required string TableName { get; init; } + + /// + /// The polling interval in milliseconds between stream reads when no records are found. + /// + public required int PollingIntervalMs { get; init; } = DynamoDBStreamsEventSourceProcess.DefaultPollingIntervalMs; +} diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/DynamoDBStreamsEventSource/DynamoDBStreamsEventSourceConfig.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/DynamoDBStreamsEventSource/DynamoDBStreamsEventSourceConfig.cs new file mode 100644 index 000000000..4a1524e52 --- /dev/null +++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/DynamoDBStreamsEventSource/DynamoDBStreamsEventSourceConfig.cs @@ -0,0 +1,48 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace Amazon.Lambda.TestTool.Processes.DynamoDBStreamsEventSource; + +/// +/// This class represents the input values from the user for DynamoDB Streams event source configuration. +/// +internal class DynamoDBStreamsEventSourceConfig +{ + /// + /// The batch size to read from the stream and send to the Lambda function. + /// + public int? BatchSize { get; set; } + + /// + /// The Lambda function to send the DynamoDB stream records to. + /// If not set the default function will be used. + /// + public string? FunctionName { get; set; } + + /// + /// The endpoint where the emulated Lambda runtime API is running. + /// If not set the current Test Tool instance will be used. + /// + public string? LambdaRuntimeApi { get; set; } + + /// + /// The AWS profile to use for credentials. + /// + public string? Profile { get; set; } + + /// + /// The AWS region the DynamoDB table is in. + /// + public string? Region { get; set; } + + /// + /// The DynamoDB table name to read streams from. + /// + public string? TableName { get; set; } + + /// + /// The polling interval in milliseconds between stream reads when no records are found. + /// Default is 1000. + /// + public int? PollingIntervalMs { get; set; } +} diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/DynamoDBStreamsEventSource/DynamoDBStreamsEventSourceProcess.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/DynamoDBStreamsEventSource/DynamoDBStreamsEventSourceProcess.cs new file mode 100644 index 000000000..cbf7bfd4e --- /dev/null +++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/DynamoDBStreamsEventSource/DynamoDBStreamsEventSourceProcess.cs @@ -0,0 +1,218 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Amazon.DynamoDBv2; +using Amazon.DynamoDBStreams; +using Amazon.Lambda.TestTool.Commands.Settings; +using Amazon.Lambda.TestTool.Services; +using System.Text.Json; + +namespace Amazon.Lambda.TestTool.Processes.DynamoDBStreamsEventSource; + +/// +/// Process for handling DynamoDB Streams event source for Lambda functions. +/// +public class DynamoDBStreamsEventSourceProcess +{ + internal const int DefaultBatchSize = 100; + internal const int DefaultPollingIntervalMs = 1000; + + /// + /// The Parent task for all the tasks started for each DynamoDB Streams event source. + /// + public required Task RunningTask { get; init; } + + /// + /// Startup DynamoDB Streams event sources + /// + public static DynamoDBStreamsEventSourceProcess Startup(RunCommandSettings settings, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(settings.DynamoDBStreamsEventSourceConfig)) + { + throw new InvalidOperationException($"The {nameof(RunCommandSettings.DynamoDBStreamsEventSourceConfig)} can not be null when starting the DynamoDB Streams event source process"); + } + + var configs = LoadDynamoDBStreamsEventSourceConfig(settings.DynamoDBStreamsEventSourceConfig); + + var tasks = new List(); + + foreach (var config in configs) + { + var builder = Host.CreateApplicationBuilder(); + + var ddbConfig = new AmazonDynamoDBStreamsConfig(); + if (!string.IsNullOrEmpty(config.Profile)) + { + ddbConfig.Profile = new Profile(config.Profile); + } + + if (!string.IsNullOrEmpty(config.Region)) + { + ddbConfig.RegionEndpoint = RegionEndpoint.GetBySystemName(config.Region); + } + + var streamsClient = new AmazonDynamoDBStreamsClient(ddbConfig); + builder.Services.AddSingleton(streamsClient); + + var ddbClientConfig = new AmazonDynamoDBConfig(); + if (!string.IsNullOrEmpty(config.Profile)) + { + ddbClientConfig.Profile = new Profile(config.Profile); + } + if (!string.IsNullOrEmpty(config.Region)) + { + ddbClientConfig.RegionEndpoint = RegionEndpoint.GetBySystemName(config.Region); + } + var ddbClient = new AmazonDynamoDBClient(ddbClientConfig); + builder.Services.AddSingleton(ddbClient); + + builder.Services.AddSingleton(); + + var tableName = config.TableName; + if (string.IsNullOrEmpty(tableName)) + { + throw new InvalidOperationException("TableName is a required property for DynamoDB Streams event source config"); + } + + var lambdaRuntimeApi = config.LambdaRuntimeApi; + if (string.IsNullOrEmpty(lambdaRuntimeApi)) + { + if (!settings.LambdaEmulatorPort.HasValue) + { + throw new InvalidOperationException("No Lambda runtime api endpoint was given as part of the DynamoDB Streams event source config and the current " + + "instance of the test tool is not running the Lambda runtime api. Either provide a Lambda runtime api endpoint or set a port for " + + "the lambda runtime api when starting the test tool."); + } + lambdaRuntimeApi = $"http://{settings.LambdaEmulatorHost}:{settings.LambdaEmulatorPort}/"; + } + + var backgroundServiceConfig = new DynamoDBStreamsEventSourceBackgroundServiceConfig + { + BatchSize = config.BatchSize ?? DefaultBatchSize, + FunctionName = config.FunctionName ?? LambdaRuntimeApi.DefaultFunctionName, + LambdaRuntimeApi = lambdaRuntimeApi, + TableName = tableName, + PollingIntervalMs = config.PollingIntervalMs ?? DefaultPollingIntervalMs + }; + + builder.Services.AddSingleton(backgroundServiceConfig); + builder.Services.AddHostedService(); + + var app = builder.Build(); + var task = app.RunAsync(cancellationToken); + tasks.Add(task); + } + + return new DynamoDBStreamsEventSourceProcess + { + RunningTask = Task.WhenAll(tasks) + }; + } + + /// + /// Load the DynamoDB Streams event source configs. Supports JSON or comma-delimited key-value pair format. + /// If the value points to a file that exists, the file contents will be read. + /// + internal static List LoadDynamoDBStreamsEventSourceConfig(string configString) + { + if (File.Exists(configString)) + { + configString = File.ReadAllText(configString); + } + + configString = configString.Trim(); + + List? configs = null; + + var jsonOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + + if (configString.StartsWith('[')) + { + try + { + configs = JsonSerializer.Deserialize>(configString, jsonOptions); + if (configs == null) + { + throw new InvalidOperationException("Failed to parse DynamoDB Streams event source JSON config: " + configString); + } + } + catch (JsonException e) + { + throw new InvalidOperationException("Failed to parse DynamoDB Streams event source JSON config: " + configString, e); + } + } + else if (configString.StartsWith('{')) + { + try + { + var config = JsonSerializer.Deserialize(configString, jsonOptions); + if (config == null) + { + throw new InvalidOperationException("Failed to parse DynamoDB Streams event source JSON config: " + configString); + } + + configs = new List { config }; + } + catch (JsonException e) + { + throw new InvalidOperationException("Failed to parse DynamoDB Streams event source JSON config: " + configString, e); + } + } + else + { + var config = new DynamoDBStreamsEventSourceConfig(); + var tokens = configString.Split(','); + foreach (var token in tokens) + { + if (string.IsNullOrWhiteSpace(token)) + continue; + + var keyValuePair = token.Split('='); + if (keyValuePair.Length != 2) + { + throw new InvalidOperationException("Failed to parse DynamoDB Streams event source config. Format should be \"TableName=,FunctionName=,...\""); + } + + switch (keyValuePair[0].ToLower().Trim()) + { + case "batchsize": + if (!int.TryParse(keyValuePair[1].Trim(), out var batchSize)) + { + throw new InvalidOperationException("Value for batch size is not a formatted integer"); + } + config.BatchSize = batchSize; + break; + case "functionname": + config.FunctionName = keyValuePair[1].Trim(); + break; + case "lambdaruntimeapi": + config.LambdaRuntimeApi = keyValuePair[1].Trim(); + break; + case "profile": + config.Profile = keyValuePair[1].Trim(); + break; + case "region": + config.Region = keyValuePair[1].Trim(); + break; + case "tablename": + config.TableName = keyValuePair[1].Trim(); + break; + case "pollingintervalms": + if (!int.TryParse(keyValuePair[1].Trim(), out var pollingInterval)) + { + throw new InvalidOperationException("Value for polling interval is not a formatted integer"); + } + config.PollingIntervalMs = pollingInterval; + break; + } + } + + configs = new List { config }; + } + + return configs; + } +} diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/TestToolProcess.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/TestToolProcess.cs index 61a0a8aa5..5da9d349b 100644 --- a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/TestToolProcess.cs +++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/TestToolProcess.cs @@ -60,10 +60,8 @@ public static TestToolProcess Startup(RunCommandSettings settings, CancellationT builder.Services.AddHttpContextAccessor(); var wwwrootPath = Path.Combine(AppContext.BaseDirectory, "wwwroot"); - if (builder.Environment.IsProduction()) - { - builder.Services.AddSingleton(new PhysicalFileProvider(wwwrootPath)); - } + var wwwrootFileProvider = new PhysicalFileProvider(wwwrootPath); + builder.Services.AddSingleton(wwwrootFileProvider); builder.Services.AddSingleton(); var serviceHttp = $"http://{settings.LambdaEmulatorHost}:{settings.LambdaEmulatorPort}"; @@ -87,20 +85,21 @@ public static TestToolProcess Startup(RunCommandSettings settings, CancellationT var app = builder.Build(); - if (app.Environment.IsProduction()) - { - app.UseStaticFiles(new StaticFileOptions - { - FileProvider = new PhysicalFileProvider(wwwrootPath) - }); - } - else + if (!app.Environment.IsProduction()) { // nosemgrep: csharp.lang.security.stacktrace-disclosure.stacktrace-disclosure app.UseDeveloperExceptionPage(); - app.UseStaticFiles(); } + // Always use the explicit file provider to serve static files from the tool's install + // directory. Without this, non-Production environments attempt to use the static web + // assets manifest which contains absolute paths from the build machine and will fail + // when running as an installed global tool on a different machine. + app.UseStaticFiles(new StaticFileOptions + { + FileProvider = wwwrootFileProvider + }); + app.UseAntiforgery(); app.MapRazorComponents() diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Services/RuntimeApiDataStore.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Services/RuntimeApiDataStore.cs index 1329c6e35..7a41f039b 100644 --- a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Services/RuntimeApiDataStore.cs +++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Services/RuntimeApiDataStore.cs @@ -72,7 +72,8 @@ public interface IRuntimeApiDataStore /// notification from the Lambda function. /// /// - /// + /// + /// void ReportError(string awsRequestId, string errorType, string errorBody); } diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Utilities/HttpRequestUtility.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Utilities/HttpRequestUtility.cs index fce9494cc..2d1f9b261 100644 --- a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Utilities/HttpRequestUtility.cs +++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Utilities/HttpRequestUtility.cs @@ -145,6 +145,7 @@ public static (IDictionary, IDictionary>) /// A string representing a random request ID in the format used by API Gateway for HTTP APIs. /// /// The generated ID is a 145character string consisting of lowercase letters and numbers, followed by an equals sign. + /// public static string GenerateRequestId() { return $"{Guid.NewGuid().ToString("N").Substring(0, 8)}{Guid.NewGuid().ToString("N").Substring(0, 7)}="; diff --git a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/Amazon.Lambda.TestTool.IntegrationTests.csproj b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/Amazon.Lambda.TestTool.IntegrationTests.csproj index c72ab6e1d..7ac170c61 100644 --- a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/Amazon.Lambda.TestTool.IntegrationTests.csproj +++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.IntegrationTests/Amazon.Lambda.TestTool.IntegrationTests.csproj @@ -20,7 +20,7 @@ - + diff --git a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Amazon.Lambda.TestTool.UnitTests.csproj b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Amazon.Lambda.TestTool.UnitTests.csproj index 524f55fcd..7a799ca1e 100644 --- a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Amazon.Lambda.TestTool.UnitTests.csproj +++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Amazon.Lambda.TestTool.UnitTests.csproj @@ -15,6 +15,9 @@ + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/DynamoDBStreamsEventSource/ConvertDynamoDBStreamsRecordTests.cs b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/DynamoDBStreamsEventSource/ConvertDynamoDBStreamsRecordTests.cs new file mode 100644 index 000000000..d47483ac2 --- /dev/null +++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/DynamoDBStreamsEventSource/ConvertDynamoDBStreamsRecordTests.cs @@ -0,0 +1,256 @@ +using Amazon.DynamoDBStreams; +using Amazon.DynamoDBStreams.Model; +using Amazon.Lambda.TestTool.Processes.DynamoDBStreamsEventSource; +using Xunit; +using Record = Amazon.DynamoDBStreams.Model.Record; + +namespace Amazon.Lambda.TestTool.UnitTests.DynamoDBStreamsEventSource; + +public class ConvertDynamoDBStreamsRecordTests +{ + private const string TestStreamArn = "arn:aws:dynamodb:us-west-2:123456789012:table/my-table/stream/2024-01-01T00:00:00.000"; + + [Fact] + public void ConvertBasicRecord() + { + var record = new Record + { + EventID = "event-123", + EventName = new Amazon.DynamoDBStreams.OperationType("INSERT"), + EventVersion = "1.1", + EventSource = "aws:dynamodb", + Dynamodb = new StreamRecord + { + ApproximateCreationDateTime = new DateTime(2024, 1, 15, 10, 30, 0, DateTimeKind.Utc), + SequenceNumber = "111111111111111111111", + SizeBytes = 256, + StreamViewType = new StreamViewType("NEW_AND_OLD_IMAGES"), + Keys = new Dictionary + { + ["Id"] = new AttributeValue { S = "key-1" } + }, + NewImage = new Dictionary + { + ["Id"] = new AttributeValue { S = "key-1" }, + ["Name"] = new AttributeValue { S = "Test Item" } + } + } + }; + + var result = DynamoDBStreamsEventSourceBackgroundService.ConvertToLambdaRecord(record, TestStreamArn); + + Assert.Equal("event-123", result.EventID); + Assert.Equal("INSERT", result.EventName); + Assert.Equal("aws:dynamodb", result.EventSource); + Assert.Equal(TestStreamArn, result.EventSourceArn); + Assert.Equal("1.1", result.EventVersion); + Assert.Equal("us-west-2", result.AwsRegion); + + Assert.NotNull(result.Dynamodb); + Assert.Equal("111111111111111111111", result.Dynamodb.SequenceNumber); + Assert.Equal(256, result.Dynamodb.SizeBytes); + Assert.Equal("NEW_AND_OLD_IMAGES", result.Dynamodb.StreamViewType); + + Assert.Single(result.Dynamodb.Keys); + Assert.Equal("key-1", result.Dynamodb.Keys["Id"].S); + + Assert.Equal(2, result.Dynamodb.NewImage.Count); + Assert.Equal("key-1", result.Dynamodb.NewImage["Id"].S); + Assert.Equal("Test Item", result.Dynamodb.NewImage["Name"].S); + } + + [Fact] + public void ConvertRecordWithAllAttributeTypes() + { + var record = new Record + { + EventID = "event-456", + EventName = new Amazon.DynamoDBStreams.OperationType("MODIFY"), + EventVersion = "1.1", + Dynamodb = new StreamRecord + { + Keys = new Dictionary + { + ["Id"] = new AttributeValue { S = "key-1" } + }, + NewImage = new Dictionary + { + ["StringAttr"] = new AttributeValue { S = "hello" }, + ["NumberAttr"] = new AttributeValue { N = "42" }, + ["BoolAttr"] = new AttributeValue { BOOL = true }, + ["NullAttr"] = new AttributeValue { NULL = true }, + ["ListAttr"] = new AttributeValue + { + L = new List + { + new AttributeValue { S = "item1" }, + new AttributeValue { N = "2" } + } + }, + ["MapAttr"] = new AttributeValue + { + M = new Dictionary + { + ["nested"] = new AttributeValue { S = "value" } + } + }, + ["StringSetAttr"] = new AttributeValue { SS = new List { "a", "b" } }, + ["NumberSetAttr"] = new AttributeValue { NS = new List { "1", "2" } } + } + } + }; + + var result = DynamoDBStreamsEventSourceBackgroundService.ConvertToLambdaRecord(record, TestStreamArn); + + var newImage = result.Dynamodb.NewImage; + Assert.Equal("hello", newImage["StringAttr"].S); + Assert.Equal("42", newImage["NumberAttr"].N); + Assert.True(newImage["BoolAttr"].BOOL); + Assert.True(newImage["NullAttr"].NULL); + Assert.Equal(2, newImage["ListAttr"].L.Count); + Assert.Equal("item1", newImage["ListAttr"].L[0].S); + Assert.Equal("value", newImage["MapAttr"].M["nested"].S); + Assert.Equal(new List { "a", "b" }, newImage["StringSetAttr"].SS); + Assert.Equal(new List { "1", "2" }, newImage["NumberSetAttr"].NS); + } + + [Fact] + public void ConvertRecordWithUserIdentity() + { + var record = new Record + { + EventID = "event-789", + EventName = new Amazon.DynamoDBStreams.OperationType("REMOVE"), + EventVersion = "1.1", + UserIdentity = new Identity + { + PrincipalId = "dynamodb.amazonaws.com", + Type = "Service" + }, + Dynamodb = new StreamRecord + { + Keys = new Dictionary + { + ["Id"] = new AttributeValue { S = "expired-item" } + } + } + }; + + var result = DynamoDBStreamsEventSourceBackgroundService.ConvertToLambdaRecord(record, TestStreamArn); + + Assert.NotNull(result.UserIdentity); + Assert.Equal("dynamodb.amazonaws.com", result.UserIdentity.PrincipalId); + Assert.Equal("Service", result.UserIdentity.Type); + } + + [Fact] + public void ConvertRecordWithBinaryAttributes() + { + var binaryData = new byte[] { 0x01, 0x02, 0x03, 0xFF }; + var binarySet = new List + { + new MemoryStream(new byte[] { 0xAA, 0xBB }), + new MemoryStream(new byte[] { 0xCC, 0xDD }) + }; + + var record = new Record + { + EventID = "event-bin", + EventName = new Amazon.DynamoDBStreams.OperationType("INSERT"), + EventVersion = "1.1", + Dynamodb = new StreamRecord + { + Keys = new Dictionary + { + ["Id"] = new AttributeValue { S = "key-1" } + }, + NewImage = new Dictionary + { + ["BinaryAttr"] = new AttributeValue { B = new MemoryStream(binaryData) }, + ["BinarySetAttr"] = new AttributeValue { BS = binarySet } + } + } + }; + + var result = DynamoDBStreamsEventSourceBackgroundService.ConvertToLambdaRecord(record, TestStreamArn); + + var newImage = result.Dynamodb.NewImage; + Assert.NotNull(newImage["BinaryAttr"].B); + Assert.Equal(binaryData, newImage["BinaryAttr"].B.ToArray()); + Assert.NotNull(newImage["BinarySetAttr"].BS); + Assert.Equal(2, newImage["BinarySetAttr"].BS.Count); + Assert.Equal(new byte[] { 0xAA, 0xBB }, newImage["BinarySetAttr"].BS[0].ToArray()); + Assert.Equal(new byte[] { 0xCC, 0xDD }, newImage["BinarySetAttr"].BS[1].ToArray()); + } + + [Fact] + public void ConvertRecordWithEmptyListAndMap() + { + var record = new Record + { + EventID = "event-empty", + EventName = new Amazon.DynamoDBStreams.OperationType("INSERT"), + EventVersion = "1.1", + Dynamodb = new StreamRecord + { + Keys = new Dictionary + { + ["Id"] = new AttributeValue { S = "key-1" } + }, + NewImage = new Dictionary + { + ["EmptyList"] = new AttributeValue { L = new List() }, + ["EmptyMap"] = new AttributeValue { M = new Dictionary() } + } + } + }; + + var result = DynamoDBStreamsEventSourceBackgroundService.ConvertToLambdaRecord(record, TestStreamArn); + + var newImage = result.Dynamodb.NewImage; + Assert.NotNull(newImage["EmptyList"].L); + Assert.Empty(newImage["EmptyList"].L); + Assert.NotNull(newImage["EmptyMap"].M); + Assert.Empty(newImage["EmptyMap"].M); + } + + [Fact] + public void ConvertMultipleRecords() + { + var records = new List + { + new Record + { + EventID = "event-1", + EventName = new Amazon.DynamoDBStreams.OperationType("INSERT"), + EventVersion = "1.1", + Dynamodb = new StreamRecord + { + Keys = new Dictionary + { + ["Id"] = new AttributeValue { S = "key-1" } + } + } + }, + new Record + { + EventID = "event-2", + EventName = new Amazon.DynamoDBStreams.OperationType("MODIFY"), + EventVersion = "1.1", + Dynamodb = new StreamRecord + { + Keys = new Dictionary + { + ["Id"] = new AttributeValue { S = "key-2" } + } + } + } + }; + + var result = DynamoDBStreamsEventSourceBackgroundService.ConvertToLambdaRecords(records, TestStreamArn); + + Assert.Equal(2, result.Count); + Assert.Equal("event-1", result[0].EventID); + Assert.Equal("event-2", result[1].EventID); + } +} diff --git a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/DynamoDBStreamsEventSource/ParseDynamoDBStreamsEventSourceConfigTests.cs b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/DynamoDBStreamsEventSource/ParseDynamoDBStreamsEventSourceConfigTests.cs new file mode 100644 index 000000000..836d36c22 --- /dev/null +++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/DynamoDBStreamsEventSource/ParseDynamoDBStreamsEventSourceConfigTests.cs @@ -0,0 +1,107 @@ +using Amazon.Lambda.TestTool.Processes.DynamoDBStreamsEventSource; +using Xunit; + +namespace Amazon.Lambda.TestTool.UnitTests.DynamoDBStreamsEventSource; + +public class ParseDynamoDBStreamsEventSourceConfigTests +{ + [Fact] + public void ParseValidJsonObject() + { + string json = """ +{ + "TableName" : "my-table", + "FunctionName" : "LambdaFunction", + "BatchSize" : 50, + "LambdaRuntimeApi" : "http://localhost:7777/", + "Profile" : "beta", + "Region" : "us-east-23" +} +"""; + + var configs = DynamoDBStreamsEventSourceProcess.LoadDynamoDBStreamsEventSourceConfig(json); + Assert.Single(configs); + Assert.Equal("my-table", configs[0].TableName); + Assert.Equal("LambdaFunction", configs[0].FunctionName); + Assert.Equal(50, configs[0].BatchSize); + Assert.Equal("http://localhost:7777/", configs[0].LambdaRuntimeApi); + Assert.Equal("beta", configs[0].Profile); + Assert.Equal("us-east-23", configs[0].Region); + } + + [Fact] + public void ParseInvalidJsonObject() + { + string json = """ +{ + "aaa" +} +"""; + + Assert.Throws(() => DynamoDBStreamsEventSourceProcess.LoadDynamoDBStreamsEventSourceConfig(json)); + } + + [Fact] + public void ParseValidJsonArray() + { + string json = """ +[ + { + "TableName" : "table-1", + "FunctionName" : "Function1", + "BatchSize" : 25 + }, + { + "TableName" : "table-2", + "FunctionName" : "Function2", + "BatchSize" : 75 + } +] +"""; + + var configs = DynamoDBStreamsEventSourceProcess.LoadDynamoDBStreamsEventSourceConfig(json); + Assert.Equal(2, configs.Count); + Assert.Equal("table-1", configs[0].TableName); + Assert.Equal("Function1", configs[0].FunctionName); + Assert.Equal(25, configs[0].BatchSize); + Assert.Equal("table-2", configs[1].TableName); + Assert.Equal("Function2", configs[1].FunctionName); + Assert.Equal(75, configs[1].BatchSize); + } + + [Fact] + public void ParseInvalidJsonArray() + { + string json = """ +[ + {"aaa"} +] +"""; + + Assert.Throws(() => DynamoDBStreamsEventSourceProcess.LoadDynamoDBStreamsEventSourceConfig(json)); + } + + [Fact] + public void ParseKeyPairs() + { + var configs = DynamoDBStreamsEventSourceProcess.LoadDynamoDBStreamsEventSourceConfig( + "TableName=my-table ,functionName =LambdaFunction, batchSize=50," + + "LambdaRuntimeApi=http://localhost:7777/ ,Profile=beta,Region=us-east-23"); + + Assert.Single(configs); + Assert.Equal("my-table", configs[0].TableName); + Assert.Equal("LambdaFunction", configs[0].FunctionName); + Assert.Equal(50, configs[0].BatchSize); + Assert.Equal("http://localhost:7777/", configs[0].LambdaRuntimeApi); + Assert.Equal("beta", configs[0].Profile); + Assert.Equal("us-east-23", configs[0].Region); + } + + [Theory] + [InlineData("novalue")] + [InlineData("BatchSize=noint")] + public void InvalidKeyPairString(string keyPairConfig) + { + Assert.Throws(() => DynamoDBStreamsEventSourceProcess.LoadDynamoDBStreamsEventSourceConfig(keyPairConfig)); + } +} diff --git a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Extensions/InvokeResponseExtensionsTests.cs b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Extensions/InvokeResponseExtensionsTests.cs index f07f70a47..24d75acf3 100644 --- a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Extensions/InvokeResponseExtensionsTests.cs +++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Extensions/InvokeResponseExtensionsTests.cs @@ -124,6 +124,8 @@ await _helper.VerifyApiGatewayResponseAsync( /// This test ensures that our ToApiGatewayHttpApiV2ProxyResponse method /// correctly replicates this observed behavior, rather than the documented behavior. /// + /// + /// [Theory] [InlineData("Invalid_JSON_Partial_Object", "{\"name\": \"John Doe\", \"age\":", "{\"name\": \"John Doe\", \"age\":")] // Invalid JSON (partial object) [InlineData("Valid_JSON_Object", "{\"name\": \"John Doe\", \"age\": 30}", "{\"name\": \"John Doe\", \"age\": 30}")] // Valid JSON object without statusCode diff --git a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/PackagingTests.cs b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/PackagingTests.cs index d2051615e..0b3dc7cdf 100644 --- a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/PackagingTests.cs +++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/PackagingTests.cs @@ -89,7 +89,7 @@ public void VerifyPackageContentsHasStaticAssets() public void VerifyPackageContentsHasRuntimeSupport() { var projectPath = Path.Combine(_workingDirectory, "Tools", "LambdaTestTool-v2", "src", "Amazon.Lambda.TestTool", "Amazon.Lambda.TestTool.csproj"); - var expectedFrameworks = new string[] { "net6.0", "net8.0", "net9.0", "net10.0" }; + var expectedFrameworks = new string[] { "net8.0", "net9.0", "net10.0" }; _output.WriteLine("Packing TestTool..."); var packProcess = new Process { diff --git a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Processes/ApiGatewayEmulatorProcessTests.cs b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Processes/ApiGatewayEmulatorProcessTests.cs index fe6a6d790..4d7b03094 100644 --- a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Processes/ApiGatewayEmulatorProcessTests.cs +++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Processes/ApiGatewayEmulatorProcessTests.cs @@ -12,7 +12,7 @@ namespace Amazon.Lambda.TestTool.UnitTests.Processes; -public class ApiGatewayEmulatorProcessTests(ITestOutputHelper testOutputHelper) +public class ApiGatewayEmulatorProcessTests { [Theory] [InlineData(ApiGatewayEmulatorMode.Rest, HttpStatusCode.Forbidden, "{\"message\":\"Missing Authentication Token\"}")] diff --git a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/RuntimeApiTests.cs b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/RuntimeApiTests.cs index cb2afa01a..2b6bc20a8 100644 --- a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/RuntimeApiTests.cs +++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/RuntimeApiTests.cs @@ -19,7 +19,7 @@ namespace Amazon.Lambda.TestTool.UnitTests; -public class RuntimeApiTests(ITestOutputHelper testOutputHelper) +public class RuntimeApiTests { #if DEBUG [Fact] diff --git a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Utilities/DirectoryHelpers.cs b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Utilities/DirectoryHelpers.cs index 9303cd590..1c22bc4ce 100644 --- a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Utilities/DirectoryHelpers.cs +++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Utilities/DirectoryHelpers.cs @@ -40,7 +40,7 @@ public static void CleanUp(string directory) } /// - /// + /// https://docs.microsoft.com/en-us/dotnet/standard/io/how-to-copy-directories /// private static void CopyDirectory(DirectoryInfo dir, string destDirName) { diff --git a/Tools/LambdaTestTool/src/Amazon.Lambda.TestTool.BlazorTester/Amazon.Lambda.TestTool.BlazorTester.csproj b/Tools/LambdaTestTool/src/Amazon.Lambda.TestTool.BlazorTester/Amazon.Lambda.TestTool.BlazorTester.csproj index 878c49507..ea15d3e35 100644 --- a/Tools/LambdaTestTool/src/Amazon.Lambda.TestTool.BlazorTester/Amazon.Lambda.TestTool.BlazorTester.csproj +++ b/Tools/LambdaTestTool/src/Amazon.Lambda.TestTool.BlazorTester/Amazon.Lambda.TestTool.BlazorTester.csproj @@ -1,20 +1,21 @@ - + Exe A tool to help debug and test your .NET Core AWS Lambda functions locally. Latest - 0.17.0 + 0.17.2 AWS .NET Lambda Test Tool Apache 2 AWS;Amazon;Lambda 1701;1702;1591;1587;3021;NU5100;CS1591 true net8.0;net9.0;net10.0 - - + false + + diff --git a/Tools/LambdaTestTool/src/Amazon.Lambda.TestTool.BlazorTester/Amazon.Lambda.TestTool.BlazorTester10_0-pack.csproj b/Tools/LambdaTestTool/src/Amazon.Lambda.TestTool.BlazorTester/Amazon.Lambda.TestTool.BlazorTester10_0-pack.csproj index f18756bce..7de8090e1 100644 --- a/Tools/LambdaTestTool/src/Amazon.Lambda.TestTool.BlazorTester/Amazon.Lambda.TestTool.BlazorTester10_0-pack.csproj +++ b/Tools/LambdaTestTool/src/Amazon.Lambda.TestTool.BlazorTester/Amazon.Lambda.TestTool.BlazorTester10_0-pack.csproj @@ -5,7 +5,7 @@ Exe A tool to help debug and test your .NET 10.0 AWS Lambda functions locally. - 0.17.0 + 0.17.2 AWS .NET Lambda Test Tool Apache 2 AWS;Amazon;Lambda @@ -14,6 +14,7 @@ true true Amazon.Lambda.TestTool-10.0 + false Amazon.Lambda.TestTool.BlazorTester Amazon.Lambda.TestTool.BlazorTester true diff --git a/Tools/LambdaTestTool/src/Amazon.Lambda.TestTool.BlazorTester/Amazon.Lambda.TestTool.BlazorTester80-pack.csproj b/Tools/LambdaTestTool/src/Amazon.Lambda.TestTool.BlazorTester/Amazon.Lambda.TestTool.BlazorTester80-pack.csproj index 45afdfeb1..50ae9d0ba 100644 --- a/Tools/LambdaTestTool/src/Amazon.Lambda.TestTool.BlazorTester/Amazon.Lambda.TestTool.BlazorTester80-pack.csproj +++ b/Tools/LambdaTestTool/src/Amazon.Lambda.TestTool.BlazorTester/Amazon.Lambda.TestTool.BlazorTester80-pack.csproj @@ -5,7 +5,7 @@ Exe A tool to help debug and test your .NET 8.0 AWS Lambda functions locally. - 0.17.0 + 0.17.2 AWS .NET Lambda Test Tool Apache 2 AWS;Amazon;Lambda @@ -14,6 +14,7 @@ true true Amazon.Lambda.TestTool-8.0 + false Amazon.Lambda.TestTool.BlazorTester Amazon.Lambda.TestTool.BlazorTester true diff --git a/Tools/LambdaTestTool/src/Amazon.Lambda.TestTool.BlazorTester/Amazon.Lambda.TestTool.BlazorTester90-pack.csproj b/Tools/LambdaTestTool/src/Amazon.Lambda.TestTool.BlazorTester/Amazon.Lambda.TestTool.BlazorTester90-pack.csproj index e4a7cc450..0b381a707 100644 --- a/Tools/LambdaTestTool/src/Amazon.Lambda.TestTool.BlazorTester/Amazon.Lambda.TestTool.BlazorTester90-pack.csproj +++ b/Tools/LambdaTestTool/src/Amazon.Lambda.TestTool.BlazorTester/Amazon.Lambda.TestTool.BlazorTester90-pack.csproj @@ -5,7 +5,7 @@ Exe A tool to help debug and test your .NET 9.0 AWS Lambda functions locally. - 0.17.0 + 0.17.2 AWS .NET Lambda Test Tool Apache 2 AWS;Amazon;Lambda @@ -14,6 +14,7 @@ true true Amazon.Lambda.TestTool-9.0 + false Amazon.Lambda.TestTool.BlazorTester Amazon.Lambda.TestTool.BlazorTester true diff --git a/Tools/LambdaTestTool/src/Amazon.Lambda.TestTool/Amazon.Lambda.TestTool.csproj b/Tools/LambdaTestTool/src/Amazon.Lambda.TestTool/Amazon.Lambda.TestTool.csproj index 7467b5896..2b1d75130 100644 --- a/Tools/LambdaTestTool/src/Amazon.Lambda.TestTool/Amazon.Lambda.TestTool.csproj +++ b/Tools/LambdaTestTool/src/Amazon.Lambda.TestTool/Amazon.Lambda.TestTool.csproj @@ -1,11 +1,12 @@  - + net8.0;net9.0;net10.0 Common code for the AWS .NET Core Lambda Mock Test Tool. 1701;1702;1591;1587;3021;NU5100;CS1591 + false @@ -19,10 +20,10 @@ - + net8.0 - + net9.0 @@ -35,7 +36,7 @@ - + @@ -46,6 +47,6 @@ - - + + diff --git a/buildtools/common.props b/buildtools/common.props index 70fbc9abb..67829840f 100644 --- a/buildtools/common.props +++ b/buildtools/common.props @@ -1,20 +1,22 @@ - $(MSBuildThisFileDirectory)/public.snk true - Amazon Web Services - + + net8.0;net10.0 + Library - false + $(NoWarn);CA1822;NU5048 + true true - + https://sdk-for-net.amazonwebservices.com/images/AWSLogo128x128.png https://github.com/aws/aws-lambda-dotnet Apache-2.0 - + https://github.com/aws/aws-lambda-dotnet + git false false @@ -23,6 +25,5 @@ false false false - - \ No newline at end of file +