├── .config
└── dotnet-tools.json
├── .devcontainer
└── devcontainer.json
├── .editorconfig
├── .gitattributes
├── .github
├── CODEOWNERS
├── CONTRIBUTING.md
├── FUNDING.yml
├── ISSUE_TEMPLATE.md
├── ISSUE_TEMPLATE
│ ├── 10_bug_report.yml
│ ├── 20_feature_request.yml
│ ├── 30_question.yml
│ └── config.yml
├── PULL_REQUEST_TEMPLATE.md
├── actionlint-matcher.json
├── dependabot.yml
├── update-dotnet-sdk.json
└── workflows
│ ├── build.yml
│ ├── bump-version.yml
│ ├── codeql.yml
│ ├── dependency-review.yml
│ ├── lint.yml
│ ├── ossf-scorecard.yml
│ └── release.yml
├── .gitignore
├── .markdownlint.json
├── .vscode
├── extensions.json
├── launch.json
└── tasks.json
├── .vsconfig
├── AwsLambdaTestServer.ruleset
├── AwsLambdaTestServer.slnx
├── AwsLambdaTestServer.snk
├── CODE_OF_CONDUCT.md
├── Directory.Build.props
├── Directory.Packages.props
├── LICENSE
├── NuGet.config
├── README.md
├── SECURITY.md
├── build.ps1
├── global.json
├── package-icon.png
├── package-readme.md
├── samples
├── MathsFunctions.Tests
│ ├── MathsFunctionTests.cs
│ └── MathsFunctions.Tests.csproj
├── MathsFunctions
│ ├── MathsFunction.cs
│ ├── MathsFunctions.csproj
│ ├── MathsRequest.cs
│ └── MathsResponse.cs
├── MinimalApi.Tests
│ ├── ApiTests.cs
│ ├── HttpLambdaTestServer.cs
│ └── MinimalApi.Tests.csproj
└── MinimalApi
│ ├── HashRequest.cs
│ ├── HashResponse.cs
│ ├── MinimalApi.csproj
│ ├── Program.cs
│ ├── Properties
│ └── launchSettings.json
│ ├── appsettings.Development.json
│ └── appsettings.json
├── src
└── AwsLambdaTestServer
│ ├── LambdaTestContext.cs
│ ├── LambdaTestRequest.cs
│ ├── LambdaTestResponse.cs
│ ├── LambdaTestResponseExtensions.cs
│ ├── LambdaTestServer.cs
│ ├── LambdaTestServerExtensions.cs
│ ├── LambdaTestServerOptions.cs
│ ├── MartinCostello.Testing.AwsLambdaTestServer.csproj
│ ├── PublicAPI
│ ├── PublicAPI.Shipped.txt
│ └── PublicAPI.Unshipped.txt
│ └── RuntimeHandler.cs
├── stylecop.json
└── tests
└── AwsLambdaTestServer.Tests
├── AssemblyFixture.cs
├── AwsIntegrationTests.cs
├── Examples.cs
├── FunctionRunner.cs
├── HttpLambdaTestServer.cs
├── HttpLambdaTestServerTests.cs
├── LambdaTestRequestTests.cs
├── LambdaTestResponseExtensionsTests.cs
├── LambdaTestServerCollection.cs
├── LambdaTestServerExtensionsTests.cs
├── LambdaTestServerOptionsTests.cs
├── LambdaTestServerTests.cs
├── MartinCostello.Testing.AwsLambdaTestServer.Tests.csproj
├── MyFunctionEntrypoint.cs
├── MyHandler.cs
├── MyRequest.cs
├── MyResponse.cs
├── ParallelismTests.cs
├── ReverseFunction.cs
├── ReverseFunctionTests.cs
├── ReverseFunctionWithCustomOptionsTests.cs
├── ReverseFunctionWithLoggingTests.cs
├── ReverseFunctionWithMobileSdkTests.cs
└── TestExtensions.cs
/.config/dotnet-tools.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 1,
3 | "isRoot": true,
4 | "tools": {
5 | "dotnet-validate": {
6 | "version": "0.0.1-preview.537",
7 | "commands": [
8 | "dotnet-validate"
9 | ]
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "C# (.NET)",
3 | "customizations": {
4 | "vscode": {
5 | "extensions": [
6 | "editorconfig.editorconfig",
7 | "ms-dotnettools.csharp",
8 | "ms-vscode.PowerShell"
9 | ]
10 | }
11 | },
12 | "postCreateCommand": "./build.ps1 -SkipTests",
13 | "remoteEnv": {
14 | "PATH": "/root/.dotnet/tools:${containerWorkspaceFolder}/.dotnet:${containerEnv:PATH}"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = crlf
6 | indent_size = 4
7 | indent_style = space
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.{config,csproj,json,props,ruleset,targets,vsconfig,yml}]
12 | indent_size = 2
13 |
14 | [*.{sh}]
15 | end_of_line = lf
16 |
17 | # Code files
18 | [*.{cs,csx,vb,vbx}]
19 | file_header_template = Copyright (c) Martin Costello, 2019. All rights reserved.\nLicensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
20 | indent_size = 4
21 | insert_final_newline = true
22 | charset = utf-8-bom
23 |
24 | ###############################
25 | # .NET Coding Conventions #
26 | ###############################
27 | [*.{cs,vb}]
28 |
29 | # Enable style analyzers
30 | dotnet_analyzer_diagnostic.category-Style.severity = warning
31 |
32 | dotnet_diagnostic.IDE0005.severity = silent
33 | dotnet_diagnostic.IDE0045.severity = silent
34 | dotnet_diagnostic.IDE0046.severity = silent
35 | dotnet_diagnostic.IDE0058.severity = silent
36 | dotnet_diagnostic.IDE0072.severity = silent
37 | dotnet_diagnostic.IDE0079.severity = silent
38 |
39 | # Organize usings
40 | dotnet_sort_system_directives_first = true
41 |
42 | # this. preferences
43 | dotnet_style_qualification_for_field = false:none
44 | dotnet_style_qualification_for_property = false:none
45 | dotnet_style_qualification_for_method = false:none
46 | dotnet_style_qualification_for_event = false:none
47 |
48 | # Language keywords vs BCL types preferences
49 | dotnet_style_predefined_type_for_locals_parameters_members = true:none
50 | dotnet_style_predefined_type_for_member_access = true:none
51 |
52 | # Modifier preferences
53 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:none
54 | dotnet_style_readonly_field = true:suggestion
55 |
56 | # Expression-level preferences
57 | dotnet_style_object_initializer = true:suggestion
58 | dotnet_style_collection_initializer = true:suggestion
59 | dotnet_style_explicit_tuple_names = true:suggestion
60 | dotnet_style_null_propagation = true:suggestion
61 | dotnet_style_coalesce_expression = true:suggestion
62 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:none
63 | dotnet_style_prefer_inferred_tuple_names = true:suggestion
64 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
65 | dotnet_style_prefer_auto_properties = true:none
66 |
67 | # Public API analyzer preferences
68 | dotnet_public_api_analyzer.require_api_files = true
69 |
70 | ###############################
71 | # Naming Conventions #
72 | ###############################
73 |
74 | # Style Definitions
75 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case
76 |
77 | # Use PascalCase for constant fields
78 | dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
79 | dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
80 | dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
81 | dotnet_naming_symbols.constant_fields.applicable_kinds = field
82 | dotnet_naming_symbols.constant_fields.applicable_accessibilities = *
83 | dotnet_naming_symbols.constant_fields.required_modifiers = const
84 |
85 | ###############################
86 | # C# Coding Conventions #
87 | ###############################
88 | [*.cs]
89 | # var preferences
90 | csharp_style_var_for_built_in_types = true:none
91 | csharp_style_var_when_type_is_apparent = true:none
92 | csharp_style_var_elsewhere = true:none
93 |
94 | # Expression-bodied members
95 | csharp_style_expression_bodied_methods = false:none
96 | csharp_style_expression_bodied_constructors = false:none
97 | csharp_style_expression_bodied_operators = false:none
98 | csharp_style_expression_bodied_properties = true:none
99 | csharp_style_expression_bodied_indexers = true:none
100 | csharp_style_expression_bodied_accessors = true:none
101 | csharp_style_expression_bodied_local_functions = when_on_single_line
102 |
103 | # Pattern matching preferences
104 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
105 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
106 |
107 | # Null-checking preferences
108 | csharp_style_throw_expression = true:suggestion
109 | csharp_style_conditional_delegate_call = true:suggestion
110 |
111 | # Modifier preferences
112 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion
113 |
114 | # Expression-level preferences
115 | csharp_prefer_braces = true:none
116 | csharp_style_deconstructed_variable_declaration = true:suggestion
117 | csharp_prefer_simple_default_expression = true:suggestion
118 | csharp_style_pattern_local_over_anonymous_function = true:suggestion
119 | csharp_style_inlined_variable_declaration = true:suggestion
120 |
121 | # Namespace preferences
122 | csharp_style_namespace_declarations = file_scoped
123 |
124 | ###############################
125 | # C# Formatting Rules #
126 | ###############################
127 | # New line preferences
128 | csharp_new_line_before_open_brace = all
129 | csharp_new_line_before_else = true
130 | csharp_new_line_before_catch = true
131 | csharp_new_line_before_finally = true
132 | csharp_new_line_before_members_in_object_initializers = true
133 | csharp_new_line_before_members_in_anonymous_types = true
134 | csharp_new_line_between_query_expression_clauses = true
135 |
136 | # Indentation preferences
137 | csharp_indent_case_contents = true
138 | csharp_indent_switch_labels = true
139 | csharp_indent_labels = flush_left
140 |
141 | # Space preferences
142 | csharp_space_after_cast = false
143 | csharp_space_after_keywords_in_control_flow_statements = true
144 | csharp_space_between_method_call_parameter_list_parentheses = false
145 | csharp_space_between_method_declaration_parameter_list_parentheses = false
146 | csharp_space_between_parentheses = false
147 | csharp_space_before_colon_in_inheritance_clause = true
148 | csharp_space_after_colon_in_inheritance_clause = true
149 | csharp_space_around_binary_operators = before_and_after
150 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
151 | csharp_space_between_method_call_name_and_opening_parenthesis = false
152 | csharp_space_between_method_call_empty_parameter_list_parentheses = false
153 |
154 | # Wrapping preferences
155 | csharp_preserve_single_line_statements = true
156 | csharp_preserve_single_line_blocks = true
157 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 | *.sh eol=lf
3 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @martincostello
2 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing Guide
2 |
3 | To contribute changes (source code, scripts, configuration) to this repository please follow the steps below. These steps are a guideline for contributing and do not necessarily need to be followed for all changes.
4 |
5 | 1. If you intend to fix a bug please create an issue before forking the repository.
6 | 1. Fork the `main` branch of this repository from the latest commit.
7 | 1. Create a branch from your fork's `main` branch to help isolate your changes from any further work on `main`. If fixing an issue try to reference its name in your branch name (e.g. `issue-42`) to make changes easier to track the changes.
8 | 1. Work on your proposed changes on your fork. If you are fixing an issue include at least one unit test that reproduces it if the code changes to fix it have not been applied; if you are adding new functionality please include unit tests appropriate to the changes you are making. The [code coverage figure](https://codecov.io/gh/martincostello/lambda-test-server) should be maintained where possible.
9 | 1. When you think your changes are complete, test that the code builds cleanly using `build.ps1`. There should be no compiler warnings and all tests should pass.
10 | 1. Once your changes build cleanly locally submit a Pull Request back to the `main` branch from your fork's branch. Ideally commits to your branch should be squashed before creating the Pull Request. If the Pull Request fixes an issue please reference it in the title and/or description. Please keep changes focused around a specific topic rather than include multiple types of changes in a single Pull Request.
11 | 1. After your Pull Request is created it will build against the repository's continuous integrations.
12 | 1. Once the Pull Request has been reviewed by the project's [contributors](https://github.com/martincostello/lambda-test-server/graphs/contributors) and the status checks pass your Pull Request will be merged back to the `main` branch, assuming that the changes are deemed appropriate.
13 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [martincostello]
2 | buy_me_a_coffee: martincostello
3 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: ''
3 | ---
4 |
5 | ### Expected behaviour
6 |
7 |
8 |
9 | ### Actual behaviour
10 |
11 |
12 |
13 | ### Steps to reproduce
14 |
15 |
16 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/10_bug_report.yml:
--------------------------------------------------------------------------------
1 | name: 🐛 Bug report
2 | description: Something not behaving as expected?
3 | title: '[Bug]: '
4 | labels: ['bug']
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | Please check for an existing issue and the [README](https://github.com/martincostello/lambda-test-server/blob/main/README.md) before submitting a bug report.
10 |
11 | If you're not using the latest release, please try upgrading to the latest version first to see if the issue resolves itself.
12 | - type: input
13 | attributes:
14 | label: Version
15 | description: Which version of the library are you experiencing the issue with?
16 | placeholder: 0.9.0
17 | validations:
18 | required: true
19 | - type: textarea
20 | attributes:
21 | label: Describe the bug
22 | description: A clear and concise description of what the bug is.
23 | validations:
24 | required: true
25 | - type: textarea
26 | attributes:
27 | label: Expected behaviour
28 | description: A clear and concise description of what you expected to happen.
29 | validations:
30 | required: false
31 | - type: textarea
32 | attributes:
33 | label: Actual behaviour
34 | description: What actually happens.
35 | validations:
36 | required: false
37 | - type: textarea
38 | attributes:
39 | label: Steps to reproduce
40 | description: |
41 | Provide a link to a [minimalistic project which reproduces this issue (repro)](https://stackoverflow.com/help/mcve) hosted in a **public** GitHub repository.
42 | Code snippets, such as a failing unit test or small console app, which demonstrate the issue wrapped in a [fenced code block](https://docs.github.com/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks) are also acceptable.
43 |
44 | This issue will be closed if:
45 | - The behaviour you're reporting cannot be easily reproduced.
46 | - The issue is a duplicate of an existing issue.
47 | - The behaviour you're reporting is by design.
48 | validations:
49 | required: false
50 | - type: textarea
51 | attributes:
52 | label: Exception(s) (if any)
53 | description: Include any exception(s) and/or stack trace(s) you get when facing this issue.
54 | render: text
55 | validations:
56 | required: false
57 | - type: input
58 | attributes:
59 | label: .NET Version
60 | description: |
61 | Run `dotnet --version` to get the .NET SDK version you're using.
62 | Alternatively, which target framework(s) (e.g. `net8.0`) does the project you're using the package with target?
63 | placeholder: 9.0.100
64 | validations:
65 | required: false
66 | - type: textarea
67 | attributes:
68 | label: Anything else?
69 | description: |
70 | Links? References? Anything that will give us more context about the issue you are encountering is useful.
71 |
72 | 💡Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
73 | validations:
74 | required: false
75 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/20_feature_request.yml:
--------------------------------------------------------------------------------
1 | name: 💡 Feature request
2 | description: Suggest a feature request or improvement
3 | title: '[Feature request]: '
4 | labels: ['feature-request']
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | Please check for an existing issue and the [README](https://github.com/martincostello/lambda-test-server/blob/main/README.md) before submitting a feature request.
10 | - type: textarea
11 | attributes:
12 | label: Is your feature request related to a specific problem? Or an existing feature?
13 | description: A clear and concise description of what the problem is. Motivating examples help prioritise things.
14 | placeholder: I am trying to [...] but [...]
15 | validations:
16 | required: true
17 | - type: textarea
18 | attributes:
19 | label: Describe the solution you'd like
20 | description: |
21 | A clear and concise description of what you want to happen. Include any alternative solutions you've considered.
22 | validations:
23 | required: true
24 | - type: textarea
25 | attributes:
26 | label: Additional context
27 | description: |
28 | Add any other context or screenshots about the feature request here.
29 | validations:
30 | required: false
31 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/30_question.yml:
--------------------------------------------------------------------------------
1 | name: 🤔 Question?
2 | description: You have something specific to achieve and the existing documentation hasn't covered how.
3 | title: '[Question]: '
4 | labels: ['question']
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | Please check for an existing issue and the [README](https://github.com/martincostello/lambda-test-server/blob/main/README.md) before asking a question.
10 | - type: textarea
11 | attributes:
12 | label: What do you want to achieve?
13 | description: A clear and concise description of what you're trying to do.
14 | placeholder: I am trying to [...] but [...]
15 | validations:
16 | required: true
17 | - type: textarea
18 | attributes:
19 | label: What code or approach do you have so far?
20 | description: |
21 | Provide a [minimalistic project which shows what you have so far](https://stackoverflow.com/help/mcve) hosted in a **public** GitHub repository.
22 | Code snippets wrapped in a [fenced code block](https://docs.github.com/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks) are also acceptable.
23 | validations:
24 | required: true
25 | - type: textarea
26 | attributes:
27 | label: Additional context
28 | description: |
29 | Add any other context or screenshots related to your question here.
30 | validations:
31 | required: false
32 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: true
2 | contact_links:
3 | - name: Contact me
4 | url: https://martincostello.com/bluesky
5 | about: You can also contact me on Bluesky.
6 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/.github/actionlint-matcher.json:
--------------------------------------------------------------------------------
1 | {
2 | "problemMatcher": [
3 | {
4 | "owner": "actionlint",
5 | "pattern": [
6 | {
7 | "regexp": "^(?:\\x1b\\[\\d+m)?(.+?)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*: (?:\\x1b\\[\\d+m)*(.+?)(?:\\x1b\\[\\d+m)* \\[(.+?)\\]$",
8 | "file": 1,
9 | "line": 2,
10 | "column": 3,
11 | "message": 4,
12 | "code": 5
13 | }
14 | ]
15 | }
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | time: "05:30"
8 | timezone: Europe/London
9 | - package-ecosystem: nuget
10 | directory: "/"
11 | groups:
12 | xunit:
13 | patterns:
14 | - xunit*
15 | schedule:
16 | interval: daily
17 | time: "05:30"
18 | timezone: Europe/London
19 | open-pull-requests-limit: 99
20 | ignore:
21 | - dependency-name: "AWSSDK.SQS"
22 | update-types: ["version-update:semver-patch"]
23 | - dependency-name: "Microsoft.AspNetCore.TestHost"
24 |
--------------------------------------------------------------------------------
/.github/update-dotnet-sdk.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://raw.githubusercontent.com/martincostello/github-automation/main/.github/update-dotnet-sdk-schema.json",
3 | "update-nuget-packages": false
4 | }
5 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: build
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | tags: [ v* ]
7 | paths-ignore:
8 | - '**/*.gitattributes'
9 | - '**/*.gitignore'
10 | - '**/*.md'
11 | pull_request:
12 | branches:
13 | - main
14 | - dotnet-vnext
15 | - dotnet-nightly
16 | workflow_dispatch:
17 |
18 | env:
19 | DOTNET_CLI_TELEMETRY_OPTOUT: true
20 | DOTNET_NOLOGO: true
21 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
22 | DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION: 1
23 | NUGET_XMLDOC_MODE: skip
24 | TERM: xterm
25 |
26 | permissions:
27 | contents: read
28 |
29 | jobs:
30 | build:
31 | name: ${{ matrix.os-name }}
32 | runs-on: ${{ matrix.runner }}
33 | timeout-minutes: 20
34 |
35 | outputs:
36 | dotnet-sdk-version: ${{ steps.setup-dotnet.outputs.dotnet-version }}
37 | dotnet-validate-version: ${{ steps.get-dotnet-validate-version.outputs.dotnet-validate-version }}
38 | package-names: ${{ steps.build.outputs.package-names }}
39 | package-version: ${{ steps.build.outputs.package-version }}
40 |
41 | permissions:
42 | attestations: write
43 | contents: write
44 | id-token: write
45 |
46 | strategy:
47 | fail-fast: false
48 | matrix:
49 | include:
50 | - os-name: macos
51 | runner: macos-latest
52 | - os-name: linux
53 | runner: ubuntu-latest
54 | - os-name: windows
55 | runner: windows-latest
56 |
57 | steps:
58 |
59 | - name: Checkout code
60 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
61 | with:
62 | filter: 'tree:0'
63 | show-progress: false
64 |
65 | - name: Setup .NET SDK
66 | uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
67 | id: setup-dotnet
68 |
69 | - name: Setup NuGet cache
70 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
71 | with:
72 | path: ~/.nuget/packages
73 | key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/*.props') }}
74 | restore-keys: ${{ runner.os }}-nuget-
75 |
76 | - name: Configure AWS credentials
77 | uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b330d9fbac3d116df # v4.2.1
78 | if: |
79 | github.event.repository.fork == false &&
80 | !contains('["costellobot[bot]", "dependabot[bot]", "github-actions[bot]"]', github.actor)
81 | with:
82 | role-to-assume: arn:aws:iam::492538393790:role/github-actions-martincostello-lambda-test-server
83 | role-session-name: ${{ github.event.repository.name }}-${{ github.run_id }}-build
84 | aws-region: eu-west-2
85 |
86 | - name: Build, Test and Package
87 | id: build
88 | shell: pwsh
89 | run: ./build.ps1
90 |
91 | - name: Upload coverage to Codecov
92 | uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
93 | with:
94 | flags: ${{ matrix.os-name }}
95 | token: ${{ secrets.CODECOV_TOKEN }}
96 |
97 | - name: Upload test results to Codecov
98 | uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f # v1.1.1
99 | if: ${{ !cancelled() }}
100 | with:
101 | flags: ${{ matrix.os-name }}
102 | token: ${{ secrets.CODECOV_TOKEN }}
103 |
104 | - name: Generate SBOM
105 | uses: anchore/sbom-action@e11c554f704a0b820cbf8c51673f6945e0731532 # v0.20.0
106 | with:
107 | artifact-name: build-${{ matrix.os-name }}.spdx.json
108 | output-file: ./artifacts/build.spdx.json
109 | path: ./artifacts/bin
110 | upload-release-assets: ${{ runner.os == 'Windows' }}
111 |
112 | - name: Attest artifacts
113 | uses: actions/attest-build-provenance@db473fddc028af60658334401dc6fa3ffd8669fd # v2.3.0
114 | if: |
115 | runner.os == 'Windows' &&
116 | github.event.repository.fork == false &&
117 | (github.ref_name == github.event.repository.default_branch || startsWith(github.ref, 'refs/tags/v'))
118 | with:
119 | subject-path: |
120 | ./artifacts/bin/MartinCostello.Testing.AwsLambdaTestServer/release*/*.dll
121 | ./artifacts/package/release/*
122 |
123 | - name: Publish artifacts
124 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
125 | with:
126 | name: artifacts-${{ matrix.os-name }}
127 | path: ./artifacts
128 |
129 | - name: Publish NuGet packages
130 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
131 | with:
132 | name: packages-${{ matrix.os-name }}
133 | path: ./artifacts/package/release
134 | if-no-files-found: error
135 |
136 | - name: Get dotnet-validate version
137 | id: get-dotnet-validate-version
138 | shell: pwsh
139 | run: |
140 | $dotnetValidateVersion = (Get-Content "./.config/dotnet-tools.json" | Out-String | ConvertFrom-Json).tools.'dotnet-validate'.version
141 | "dotnet-validate-version=${dotnetValidateVersion}" >> $env:GITHUB_OUTPUT
142 |
143 | validate-packages:
144 | needs: build
145 | runs-on: ubuntu-latest
146 | steps:
147 |
148 | - name: Download packages
149 | uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
150 | with:
151 | name: packages-windows
152 |
153 | - name: Setup .NET SDK
154 | uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
155 | with:
156 | dotnet-version: ${{ needs.build.outputs.dotnet-sdk-version }}
157 |
158 | - name: Validate NuGet packages
159 | shell: pwsh
160 | env:
161 | DOTNET_VALIDATE_VERSION: ${{ needs.build.outputs.dotnet-validate-version }}
162 | run: |
163 | dotnet tool install --global dotnet-validate --version ${env:DOTNET_VALIDATE_VERSION} --allow-roll-forward
164 | $packages = Get-ChildItem -Filter "*.nupkg" | ForEach-Object { $_.FullName }
165 | $invalidPackages = 0
166 | foreach ($package in $packages) {
167 | dotnet validate package local $package
168 | if ($LASTEXITCODE -ne 0) {
169 | $invalidPackages++
170 | }
171 | }
172 | if ($invalidPackages -gt 0) {
173 | Write-Output "::error::$invalidPackages NuGet package(s) failed validation."
174 | exit 1
175 | }
176 |
177 | publish-feedz-io:
178 | needs: [ build, validate-packages ]
179 | runs-on: ubuntu-latest
180 | if: |
181 | github.event.repository.fork == false &&
182 | (github.ref_name == github.event.repository.default_branch ||
183 | startsWith(github.ref, 'refs/tags/v'))
184 | environment:
185 | name: feedz.io
186 | url: https://feedz.io/org/${{ github.repository_owner }}/repository/lambda-test-server/packages/MartinCostello.Testing.AwsLambdaTestServer
187 |
188 | steps:
189 |
190 | - name: Download packages
191 | uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
192 | with:
193 | name: packages-windows
194 |
195 | - name: Setup .NET SDK
196 | uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
197 | with:
198 | dotnet-version: ${{ needs.build.outputs.dotnet-sdk-version }}
199 |
200 | - name: Push NuGet packages to feedz.io
201 | shell: bash
202 | env:
203 | API_KEY: ${{ secrets.FEEDZ_IO_TOKEN }}
204 | PACKAGE_VERSION: ${{ needs.build.outputs.package-version }}
205 | SOURCE: "https://f.feedz.io/${{ github.repository }}/nuget/index.json"
206 | run: dotnet nuget push "*.nupkg" --api-key "${API_KEY}" --skip-duplicate --source "${SOURCE}" && echo "::notice title=feedz.io::Published version ${PACKAGE_VERSION} to feedz.io."
207 |
208 | publish-nuget:
209 | needs: [ build, validate-packages ]
210 | runs-on: ubuntu-latest
211 | if: |
212 | github.event.repository.fork == false &&
213 | startsWith(github.ref, 'refs/tags/v')
214 |
215 | environment:
216 | name: NuGet.org
217 | url: https://www.nuget.org/packages/MartinCostello.Testing.AwsLambdaTestServer
218 |
219 | steps:
220 |
221 | - name: Download packages
222 | uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
223 | with:
224 | name: packages-windows
225 |
226 | - name: Setup .NET SDK
227 | uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
228 | with:
229 | dotnet-version: ${{ needs.build.outputs.dotnet-sdk-version }}
230 |
231 | - name: Push NuGet packages to NuGet.org
232 | shell: bash
233 | env:
234 | API_KEY: ${{ secrets.NUGET_TOKEN }}
235 | PACKAGE_VERSION: ${{ needs.build.outputs.package-version }}
236 | SOURCE: https://api.nuget.org/v3/index.json
237 | run: dotnet nuget push "*.nupkg" --api-key "${API_KEY}" --skip-duplicate --source "${SOURCE}" && echo "::notice title=nuget.org::Published version ${PACKAGE_VERSION} to NuGet.org."
238 |
239 | - name: Publish nuget_packages_published
240 | uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3.0.0
241 | with:
242 | event-type: nuget_packages_published
243 | repository: ${{ github.repository_owner }}/github-automation
244 | token: ${{ secrets.COSTELLOBOT_TOKEN }}
245 | client-payload: |-
246 | {
247 | "repository": "${{ github.repository }}",
248 | "packages": "${{ needs.build.outputs.package-names }}",
249 | "version": "${{ needs.build.outputs.package-version }}"
250 | }
251 |
--------------------------------------------------------------------------------
/.github/workflows/bump-version.yml:
--------------------------------------------------------------------------------
1 | name: bump-version
2 |
3 | on:
4 | release:
5 | types: [ published ]
6 | workflow_dispatch:
7 | inputs:
8 | version:
9 | description: 'The optional version string for the next release.'
10 | required: false
11 | type: string
12 | default: ''
13 |
14 | permissions: {}
15 |
16 | jobs:
17 | bump-version:
18 | runs-on: [ ubuntu-latest ]
19 |
20 | concurrency:
21 | group: ${{ github.workflow }}
22 | cancel-in-progress: false
23 |
24 | steps:
25 |
26 | - name: Checkout code
27 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
28 | with:
29 | filter: 'tree:0'
30 | show-progress: false
31 | token: ${{ secrets.COSTELLOBOT_TOKEN }}
32 |
33 | - name: Bump version
34 | id: bump-version
35 | shell: pwsh
36 | env:
37 | NEXT_VERSION: ${{ inputs.version }}
38 | run: |
39 | $properties = Join-Path "." "Directory.Build.props"
40 |
41 | $xml = [xml](Get-Content $properties)
42 | $versionPrefix = $xml.SelectSingleNode('Project/PropertyGroup/VersionPrefix')
43 | $publishedVersion = $versionPrefix.InnerText
44 |
45 | if (-Not [string]::IsNullOrEmpty(${env:NEXT_VERSION})) {
46 | $version = [System.Version]::new(${env:NEXT_VERSION})
47 | $assemblyVersionProperty = $xml.SelectSingleNode('Project/PropertyGroup/AssemblyVersion')
48 | $assemblyVersion = [System.Version]::new($version.Major, ($version.Major -eq 0 ? $version.Minor : 0), 0, 0)
49 | $assemblyVersionProperty.InnerText = $assemblyVersion.ToString()
50 | } else {
51 | $version = [System.Version]::new($publishedVersion)
52 | $version = [System.Version]::new($version.Major, $version.Minor, $version.Build + 1)
53 | }
54 |
55 | $updatedVersion = $version.ToString()
56 | $versionPrefix.InnerText = $updatedVersion
57 |
58 | $packageValidationBaselineVersion = $xml.SelectSingleNode('Project/PropertyGroup/PackageValidationBaselineVersion')
59 | $packageValidationBaselineVersion.InnerText = $publishedVersion
60 |
61 | $settings = New-Object System.Xml.XmlWriterSettings
62 | $settings.Encoding = New-Object System.Text.UTF8Encoding($false)
63 | $settings.Indent = $true
64 | $settings.OmitXmlDeclaration = $true
65 |
66 | $writer = [System.Xml.XmlWriter]::Create($properties, $settings)
67 |
68 | $xml.Save($writer)
69 |
70 | $writer.Flush()
71 | $writer.Close()
72 | $writer = $null
73 |
74 | "" >> $properties
75 |
76 | "version=${updatedVersion}" >> $env:GITHUB_OUTPUT
77 |
78 | - name: Push changes to GitHub
79 | id: push-changes
80 | shell: pwsh
81 | env:
82 | GIT_COMMIT_USER_EMAIL: ${{ vars.GIT_COMMIT_USER_EMAIL }}
83 | GIT_COMMIT_USER_NAME: ${{ vars.GIT_COMMIT_USER_NAME }}
84 | NEXT_VERSION: ${{ steps.bump-version.outputs.version }}
85 | run: |
86 | $gitStatus = (git status --porcelain)
87 |
88 | if ([string]::IsNullOrEmpty($gitStatus)) {
89 | throw "No changes to commit."
90 | }
91 |
92 | git config color.diff always
93 | git --no-pager diff
94 |
95 | $branchName = "bump-version-${env:NEXT_VERSION}"
96 |
97 | git config user.email ${env:GIT_COMMIT_USER_EMAIL} | Out-Null
98 | git config user.name ${env:GIT_COMMIT_USER_NAME} | Out-Null
99 | git fetch origin --no-tags | Out-Null
100 | git rev-parse --verify --quiet "remotes/origin/${branchName}" | Out-Null
101 |
102 | if ($LASTEXITCODE -eq 0) {
103 | Write-Output "Branch ${branchName} already exists."
104 | exit 0
105 | }
106 |
107 | git checkout -b $branchName
108 | git add .
109 | git commit -m "Bump version`n`nBump version to ${env:NEXT_VERSION} for the next release."
110 | git push -u origin $branchName
111 |
112 | "branch-name=${branchName}" >> $env:GITHUB_OUTPUT
113 | "updated-version=true" >> $env:GITHUB_OUTPUT
114 | "version=${env:NEXT_VERSION}" >> $env:GITHUB_OUTPUT
115 |
116 | - name: Create pull request
117 | if: steps.push-changes.outputs.updated-version == 'true'
118 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
119 | env:
120 | BASE_BRANCH: ${{ github.event.repository.default_branch }}
121 | HEAD_BRANCH: ${{ steps.push-changes.outputs.branch-name }}
122 | NEXT_VERSION: ${{ steps.push-changes.outputs.version }}
123 | with:
124 | github-token: ${{ secrets.COSTELLOBOT_TOKEN }}
125 | script: |
126 | const nextVersion = process.env.NEXT_VERSION;
127 | const { repo, owner } = context.repo;
128 | const workflowUrl = `${process.env.GITHUB_SERVER_URL}/${owner}/${repo}/actions/runs/${process.env.GITHUB_RUN_ID}`;
129 |
130 | const { data: pr } = await github.rest.pulls.create({
131 | title: 'Bump version',
132 | owner,
133 | repo,
134 | head: process.env.HEAD_BRANCH,
135 | base: process.env.BASE_BRANCH,
136 | draft: true,
137 | body: [
138 | `Bump version to \`${nextVersion}\` for the next release.`,
139 | '',
140 | `This pull request was generated by [GitHub Actions](${workflowUrl}).`
141 | ].join('\n')
142 | });
143 |
144 | core.notice(`Created pull request ${owner}/${repo}#${pr.number}: ${pr.html_url}`);
145 |
146 | try {
147 | const { data: milestones } = await github.rest.issues.listMilestones({
148 | owner,
149 | repo,
150 | state: 'open',
151 | });
152 |
153 | const title = `v${nextVersion}`;
154 | let milestone = milestones.find((p) => p.title === title);
155 |
156 | if (!milestone) {
157 | const created = await github.rest.issues.createMilestone({
158 | owner,
159 | repo,
160 | title,
161 | });
162 | milestone = created.data;
163 | }
164 |
165 | await github.rest.issues.update({
166 | owner,
167 | repo,
168 | issue_number: pr.number,
169 | milestone: milestone.number
170 | });
171 | } catch (error) {
172 | // Ignore
173 | }
174 |
175 | close-milestone:
176 | runs-on: [ ubuntu-latest ]
177 | if: github.event_name == 'release'
178 |
179 | concurrency:
180 | group: ${{ github.workflow }}
181 | cancel-in-progress: false
182 |
183 | steps:
184 |
185 | - name: Close milestone
186 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
187 | env:
188 | RELEASE_DATE: ${{ github.event.release.published_at }}
189 | RELEASE_VERSION: ${{ github.event.release.tag_name }}
190 | with:
191 | github-token: ${{ secrets.COSTELLOBOT_TOKEN }}
192 | script: |
193 | const { repo, owner } = context.repo;
194 |
195 | const { data: milestones } = await github.rest.issues.listMilestones({
196 | owner,
197 | repo,
198 | state: 'open',
199 | });
200 |
201 | const milestone = milestones.find((p) => p.title === process.env.RELEASE_VERSION);
202 |
203 | if (!milestone) {
204 | return;
205 | }
206 |
207 | try {
208 | await github.rest.issues.updateMilestone({
209 | owner,
210 | repo,
211 | milestone_number: milestone.number,
212 | state: 'closed',
213 | due_on: process.env.RELEASE_DATE,
214 | });
215 | } catch (error) {
216 | // Ignore
217 | }
218 |
--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | name: codeql
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches:
8 | - main
9 | - dotnet-vnext
10 | - dotnet-nightly
11 | schedule:
12 | - cron: '0 6 * * MON'
13 | workflow_dispatch:
14 |
15 | permissions: {}
16 |
17 | jobs:
18 | analysis:
19 | runs-on: ubuntu-latest
20 |
21 | permissions:
22 | actions: read
23 | contents: read
24 | security-events: write
25 |
26 | strategy:
27 | fail-fast: false
28 | matrix:
29 | language: [ 'actions', 'csharp' ]
30 |
31 | steps:
32 | - name: Checkout repository
33 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
34 | with:
35 | filter: 'tree:0'
36 | show-progress: false
37 |
38 | - name: Initialize CodeQL
39 | uses: github/codeql-action/init@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19
40 | with:
41 | build-mode: none
42 | languages: ${{ matrix.language }}
43 | queries: security-and-quality
44 |
45 | - name: Perform CodeQL Analysis
46 | uses: github/codeql-action/analyze@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19
47 | with:
48 | category: '/language:${{ matrix.language }}'
49 |
50 | codeql:
51 | if: ${{ !cancelled() }}
52 | needs: [ analysis ]
53 | runs-on: ubuntu-latest
54 |
55 | steps:
56 | - name: Report status
57 | shell: bash
58 | env:
59 | SCAN_SUCCESS: ${{ !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }}
60 | run: |
61 | if [ "${SCAN_SUCCESS}" == "true" ]
62 | then
63 | echo 'CodeQL analysis successful ✅'
64 | else
65 | echo 'CodeQL analysis failed ❌'
66 | exit 1
67 | fi
68 |
--------------------------------------------------------------------------------
/.github/workflows/dependency-review.yml:
--------------------------------------------------------------------------------
1 | name: dependency-review
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 | - dotnet-vnext
8 | - dotnet-nightly
9 |
10 | permissions:
11 | contents: read
12 |
13 | jobs:
14 | dependency-review:
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 |
19 | - name: Checkout code
20 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
21 | with:
22 | filter: 'tree:0'
23 | show-progress: false
24 |
25 | - name: Review dependencies
26 | uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1
27 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: lint
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | paths-ignore:
7 | - '**/*.gitattributes'
8 | - '**/*.gitignore'
9 | - '**/*.md'
10 | pull_request:
11 | branches:
12 | - main
13 | - dotnet-vnext
14 | - dotnet-nightly
15 | workflow_dispatch:
16 |
17 | permissions:
18 | contents: read
19 |
20 | env:
21 | FORCE_COLOR: 3
22 | POWERSHELL_YAML_VERSION: '0.4.12'
23 | PSSCRIPTANALYZER_VERSION: '1.24.0'
24 | TERM: xterm
25 |
26 | jobs:
27 | lint:
28 | runs-on: ubuntu-latest
29 |
30 | steps:
31 |
32 | - name: Checkout code
33 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
34 | with:
35 | filter: 'tree:0'
36 | show-progress: false
37 |
38 | - name: Add actionlint problem matcher
39 | run: echo "::add-matcher::.github/actionlint-matcher.json"
40 |
41 | - name: Lint workflows
42 | uses: docker://rhysd/actionlint@sha256:887a259a5a534f3c4f36cb02dca341673c6089431057242cdc931e9f133147e9 # v1.7.7
43 | with:
44 | args: -color
45 |
46 | - name: Lint markdown
47 | uses: DavidAnson/markdownlint-cli2-action@992badcdf24e3b8eb7e87ff9287fe931bcb00c6e # v20.0.0
48 | with:
49 | config: '.markdownlint.json'
50 | globs: |
51 | **/*.md
52 |
53 | - name: Lint PowerShell in workflows
54 | uses: martincostello/lint-actions-powershell@5942e3350ee5bd8f8933cec4e1185d13f0ea688f # v1.0.0
55 | with:
56 | powershell-yaml-version: ${{ env.POWERSHELL_YAML_VERSION }}
57 | psscriptanalyzer-version: ${{ env.PSSCRIPTANALYZER_VERSION }}
58 | treat-warnings-as-errors: true
59 |
60 | - name: Lint PowerShell scripts
61 | shell: pwsh
62 | run: |
63 | $settings = @{
64 | IncludeDefaultRules = $true
65 | Severity = @("Error", "Warning")
66 | }
67 | $issues = Invoke-ScriptAnalyzer -Path ${env:GITHUB_WORKSPACE} -Recurse -ReportSummary -Settings $settings
68 | foreach ($issue in $issues) {
69 | $severity = $issue.Severity.ToString()
70 | $level = $severity.Contains("Error") ? "error" : $severity.Contains("Warning") ? "warning" : "notice"
71 | Write-Output "::${level} file=$($issue.ScriptName),line=$($issue.Line),title=PSScriptAnalyzer::$($issue.Message)"
72 | }
73 | if ($issues.Count -gt 0) {
74 | exit 1
75 | }
76 |
--------------------------------------------------------------------------------
/.github/workflows/ossf-scorecard.yml:
--------------------------------------------------------------------------------
1 | name: ossf-scorecard
2 |
3 | on:
4 | branch_protection_rule:
5 | push:
6 | branches: [ main ]
7 | schedule:
8 | - cron: '0 5 * * MON'
9 | workflow_dispatch:
10 |
11 | permissions: read-all
12 |
13 | jobs:
14 | analysis:
15 | name: analysis
16 | runs-on: ubuntu-latest
17 |
18 | permissions:
19 | id-token: write
20 | security-events: write
21 |
22 | steps:
23 | - name: Checkout code
24 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
25 | with:
26 | filter: 'tree:0'
27 | persist-credentials: false
28 | show-progress: false
29 |
30 | - name: Run analysis
31 | uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2
32 | with:
33 | publish_results: true
34 | results_file: results.sarif
35 | results_format: sarif
36 |
37 | - name: Upload artifact
38 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
39 | with:
40 | name: SARIF
41 | path: results.sarif
42 | retention-days: 5
43 |
44 | - name: Upload to code-scanning
45 | uses: github/codeql-action/upload-sarif@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19
46 | with:
47 | sarif_file: results.sarif
48 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: release
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | publish:
7 | description: 'If true, does not create the release as a draft.'
8 | required: false
9 | type: boolean
10 | default: false
11 |
12 | permissions: {}
13 |
14 | jobs:
15 | release:
16 | runs-on: [ ubuntu-latest ]
17 |
18 | concurrency:
19 | group: ${{ github.workflow }}
20 | cancel-in-progress: false
21 |
22 | steps:
23 |
24 | - name: Checkout code
25 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
26 | with:
27 | filter: 'tree:0'
28 | show-progress: false
29 | token: ${{ secrets.COSTELLOBOT_TOKEN }}
30 |
31 | - name: Get version
32 | id: get-version
33 | shell: pwsh
34 | run: |
35 | $properties = Join-Path "." "Directory.Build.props"
36 | $xml = [xml](Get-Content $properties)
37 | $version = $xml.SelectSingleNode('Project/PropertyGroup/VersionPrefix').InnerText
38 | "version=${version}" >> $env:GITHUB_OUTPUT
39 |
40 | - name: Create release
41 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
42 | env:
43 | DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
44 | DRAFT: ${{ inputs.publish != true }}
45 | VERSION: ${{ steps.get-version.outputs.version }}
46 | with:
47 | github-token: ${{ secrets.COSTELLOBOT_TOKEN }}
48 | script: |
49 | const { repo, owner } = context.repo;
50 | const draft = process.env.DRAFT === 'true';
51 | const version = process.env.VERSION;
52 | const tag_name = `v${version}`;
53 | const name = tag_name;
54 |
55 | const { data: notes } = await github.rest.repos.generateReleaseNotes({
56 | owner,
57 | repo,
58 | tag_name,
59 | target_commitish: process.env.DEFAULT_BRANCH,
60 | });
61 |
62 | const body = notes.body
63 | .split('\n')
64 | .filter((line) => !line.includes(' @costellobot '))
65 | .filter((line) => !line.includes(' @dependabot '))
66 | .filter((line) => !line.includes(' @github-actions '))
67 | .join('\n');
68 |
69 | const { data: release } = await github.rest.repos.createRelease({
70 | owner,
71 | repo,
72 | tag_name,
73 | name,
74 | body,
75 | draft,
76 | });
77 |
78 | core.notice(`Created release ${release.name}: ${release.html_url}`);
79 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .dotnet
3 | .idea
4 | .metadata
5 | .settings
6 | .vs
7 | _ReSharper*
8 | _reports
9 | _UpgradeReport_Files/
10 | artifacts/
11 | Backup*/
12 | bin
13 | Bin
14 | coverage
15 | coverage.*
16 | junit.xml
17 | MSBuild_Logs/
18 | obj
19 | packages
20 | TestResults
21 | UpgradeLog*.htm
22 | UpgradeLog*.XML
23 | *.binlog
24 | *.coverage
25 | *.DotSettings
26 | *.log
27 | *.nupkg
28 | *.opensdf
29 | *.[Pp]ublish.xml
30 | *.publishproj
31 | *.pubxml
32 | *.sdf
33 | *.sln.cache
34 | *.sln.docstates
35 | *.sln.ide
36 | *.suo
37 | *.user
38 |
--------------------------------------------------------------------------------
/.markdownlint.json:
--------------------------------------------------------------------------------
1 | {
2 | "MD013": false,
3 | "MD040": false
4 | }
5 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "editorconfig.editorconfig",
4 | "ms-dotnettools.csharp",
5 | "ms-vscode.PowerShell"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": "Run tests",
6 | "type": "coreclr",
7 | "request": "launch",
8 | "preLaunchTask": "build",
9 | "program": "dotnet",
10 | "args": [
11 | "test"
12 | ],
13 | "cwd": "${workspaceFolder}/tests/AwsLambdaTestServer.Tests",
14 | "console": "internalConsole",
15 | "stopAtEntry": false,
16 | "internalConsoleOptions": "openOnSessionStart"
17 | }
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "label": "build",
6 | "command": "dotnet build",
7 | "type": "shell",
8 | "group": "build",
9 | "presentation": {
10 | "reveal": "silent"
11 | },
12 | "problemMatcher": "$msCompile"
13 | }
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/.vsconfig:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.0",
3 | "components": [
4 | "Microsoft.VisualStudio.Component.CoreEditor",
5 | "Microsoft.VisualStudio.Workload.CoreEditor",
6 | "Microsoft.NetCore.Component.Runtime.9.0",
7 | "Microsoft.NetCore.Component.SDK",
8 | "Microsoft.VisualStudio.Component.Roslyn.Compiler",
9 | "Microsoft.VisualStudio.Component.Roslyn.LanguageServices"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/AwsLambdaTestServer.ruleset:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/AwsLambdaTestServer.slnx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/AwsLambdaTestServer.snk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martincostello/lambda-test-server/f886c3c2582a893cbba5c363167b594f74ad30cd/AwsLambdaTestServer.snk
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team [through a GitHub issue](https://github.com/martincostello/lambda-test-server/issues). All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at [https://contributor-covenant.org/version/1/4][version]
72 |
73 | [homepage]: https://contributor-covenant.org
74 | [version]: https://contributor-covenant.org/version/1/4/
75 |
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | false
4 | $(MSBuildThisFileDirectory)AwsLambdaTestServer.snk
5 | $(MSBuildThisFileDirectory)AwsLambdaTestServer.ruleset
6 | true
7 | aws;lambda;testserver;testing
8 | true
9 | false
10 | 0.9.0.0
11 | 0.9.0
12 | 0.9.1
13 |
14 |
15 | true
16 | $(NoWarn);419;1570;1573;1574;1584;1591;SA0001;SA1602
17 |
18 |
19 |
--------------------------------------------------------------------------------
/Directory.Packages.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | https://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | https://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/NuGet.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AWS Lambda Test Server for .NET
2 |
3 | [](https://www.nuget.org/packages/MartinCostello.Testing.AwsLambdaTestServer "Download MartinCostello.Testing.AwsLambdaTestServer from NuGet")
4 | [](https://www.nuget.org/packages/MartinCostello.Testing.AwsLambdaTestServer "Download MartinCostello.Testing.AwsLambdaTestServer from NuGet")
5 |
6 | [](https://github.com/martincostello/lambda-test-server/actions/workflows/build.yml?query=branch%3Amain+event%3Apush)
7 | [](https://codecov.io/gh/martincostello/lambda-test-server)
8 | [](https://securityscorecards.dev/viewer/?uri=github.com/martincostello/lambda-test-server)
9 |
10 | ## Introduction
11 |
12 | A NuGet package that builds on top of the `TestServer` class in the [Microsoft.AspNetCore.TestHost](https://www.nuget.org/packages/Microsoft.AspNetCore.TestHost) NuGet package to provide infrastructure to use with end-to-end/integration tests of .NET 6 AWS Lambda Functions using a custom runtime with the `LambdaBootstrap` class from the [Amazon.Lambda.RuntimeSupport](https://www.nuget.org/packages/Amazon.Lambda.RuntimeSupport/) NuGet package.
13 |
14 | [_.NET Core 3.0 on Lambda with AWS Lambda's Custom Runtime_](https://aws.amazon.com/blogs/developer/net-core-3-0-on-lambda-with-aws-lambdas-custom-runtime/ ".NET Core 3.0 on Lambda with AWS Lambda's Custom Runtime on the AWS Developer Blog")
15 |
16 | ### Installation
17 |
18 | To install the library from [NuGet](https://www.nuget.org/packages/MartinCostello.Testing.AwsLambdaTestServer/ "MartinCostello.Testing.AwsLambdaTestServer on NuGet.org") using the .NET SDK run:
19 |
20 | ```sh
21 | dotnet add package MartinCostello.Testing.AwsLambdaTestServer
22 | ```
23 |
24 | ### Usage
25 |
26 | Before you can use the Lambda test server to test your function, you need to factor your function entry-point
27 | in such a way that you can supply both a `HttpClient` and `CancellationToken` to it from your tests. This is to allow you to both plug in the `HttpClient` for the test server into `LambdaBootstrap`, and to stop the Lambda function running at a time of your choosing by signalling the `CancellationToken`.
28 |
29 | Here's an example of how to do this with a simple Lambda function that takes an array of integers and returns them in reverse order:
30 |
31 | ```csharp
32 | using System.Linq;
33 | using System.Net.Http;
34 | using System.Threading;
35 | using System.Threading.Tasks;
36 | using Amazon.Lambda.RuntimeSupport;
37 | using Amazon.Lambda.Serialization.Json;
38 |
39 | namespace MyFunctions;
40 |
41 | public static class ReverseFunction
42 | {
43 | public static async Task Main()
44 | => await RunAsync();
45 |
46 | public static async Task RunAsync(
47 | HttpClient httpClient = null,
48 | CancellationToken cancellationToken = default)
49 | {
50 | var serializer = new JsonSerializer();
51 |
52 | using var handlerWrapper = HandlerWrapper.GetHandlerWrapper(ReverseAsync, serializer);
53 | using var bootstrap = new LambdaBootstrap(httpClient ?? new HttpClient(), handlerWrapper);
54 |
55 | await bootstrap.RunAsync(cancellationToken);
56 | }
57 |
58 | public static Task ReverseAsync(int[] values)
59 | => Task.FromResult(values.Reverse().ToArray());
60 | }
61 | ```
62 |
63 | Once you've done that, you can use `LambdaTestServer` in your tests with your function to verify how it processes requests.
64 |
65 | Here's an example using xunit to verify that `ReverseFunction` works as intended:
66 |
67 | ```csharp
68 | using System;
69 | using System.Text.Json;
70 | using System.Threading;
71 | using System.Threading.Tasks;
72 | using MartinCostello.Testing.AwsLambdaTestServer;
73 | using Xunit;
74 |
75 | namespace MyFunctions;
76 |
77 | public static class ReverseFunctionTests
78 | {
79 | [Fact]
80 | public static async Task Function_Reverses_Numbers()
81 | {
82 | // Arrange
83 | using var server = new LambdaTestServer();
84 | using var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(1));
85 |
86 | await server.StartAsync(cancellationTokenSource.Token);
87 |
88 | int[] value = [1, 2, 3];
89 | string json = JsonSerializer.Serialize(value);
90 |
91 | LambdaTestContext context = await server.EnqueueAsync(json);
92 |
93 | using var httpClient = server.CreateClient();
94 |
95 | // Act
96 | await ReverseFunction.RunAsync(httpClient, cancellationTokenSource.Token);
97 |
98 | // Assert
99 | Assert.True(context.Response.TryRead(out LambdaTestResponse response));
100 | Assert.True(response.IsSuccessful);
101 |
102 | json = await response.ReadAsStringAsync();
103 | int[] actual = JsonSerializer.Deserialize(json);
104 |
105 | Assert.Equal([3, 2, 1], actual);
106 | }
107 | }
108 | ```
109 |
110 | The key parts to call out here are:
111 |
112 | 1. An instance of `LambdaTestServer` is created and then the `StartAsync()` method called with a `CancellationToken` that allows the test to stop the function. In the example here the token is signalled with a timeout, but you could also write code to stop the processing based on arbitrary criteria.
113 | 1. The request that the Lambda function should be invoked with is passed to `EnqueueAsync()`. This can be specified with an instance of `LambdaTestRequest` for fine-grained control, but there are overloads that accept `byte[]` and `string`. You could also make your own extensions to serialize objects to JSON using the serializer of your choice.
114 | 1. `EnqueueAsync()` returns a `LambdaTestContext`. This contains a reference to the `LambdaTestRequest` and a `ChannelReader`. This channel reader can be used to await the request being processed by the function under test.
115 | 1. Once the request is enqueued, an `HttpClient` is obtained from the test server and passed to the function to test with the cancellation token and run by calling `RunAsync()`.
116 | 1. Once the function processing completes after the `CancellationToken` is signalled, the channel reader is read to obtain the `LambdaTestResponse` for the request that was enqueued.
117 | 1. Once this is returned from the channel reader, the response is checked for success using `IsSuccessful` and then the `Content` (which is a `byte[]`) is deserialized into the expected response to be asserted on. Again, you could make your own extensions to deserialize the response content into `string` or objects from JSON.
118 |
119 | The library itself targets `net8.0` and `net9.0` so requires your test project to target at least .NET 8.
120 |
121 | #### Sequence Diagram
122 |
123 | The sequence diagram below illustrates the flow of events for a test using the test server for the above example.
124 |
125 | ```mermaid
126 | sequenceDiagram
127 | autonumber
128 |
129 | participant T as Test Method
130 | participant S as Lambda Test Server
131 | participant F as Lambda Function
132 | participant H as Handler
133 |
134 | title How AWS Lambda Test Server Works
135 |
136 | note over T:Arrange
137 |
138 | T->>+S: Start test server
139 |
140 | S->>S:Start HTTP server
141 |
142 | S-->>T:
143 |
144 | T->>S:Queue request
145 |
146 | note over S:Request is queued
147 |
148 | S-->>T:LambdaTestContext
149 |
150 | T->>+F:Create function with HttpClient for Test Server
151 |
152 | note over T:Act
153 |
154 | note over T:Wait for request(s)
to be handled
155 |
156 | loop Poll for Lambda invocations
157 |
158 | F->>S:GET /{LambdaVersion}/runtime/invocation/next
159 |
160 | note over S:Request is dequeued
161 |
162 | S-->>F:HTTP 200
163 |
164 | F->>+H:Invoke Handler
165 |
166 | note over H:System Under Test
167 |
168 | H-->>-F:Response
169 |
170 | alt Invocation is handled successfully
171 |
172 | F->>S:POST /{LambdaVersion}/runtime/invocation/{AwsRequestId}/response
173 |
174 | else Invocation throws an exception
175 |
176 | F->>S:POST /{LambdaVersion}/runtime/invocation/{AwsRequestId}/error
177 |
178 | end
179 |
180 | note over S:Associate response with
LambdaTestContext
181 |
182 | S-)T:Signal request handled
on LambdaTestContext
183 |
184 | S-->>F:HTTP 204
185 |
186 | T->>F:Stop Lambda function
187 |
188 | note over F:Terminate client
listen loop
189 |
190 | deactivate F
191 |
192 | end
193 |
194 | T->>S:Stop server
195 |
196 | S->>S:Stop HTTP server
197 |
198 | S-->> T:
199 |
200 | deactivate S
201 |
202 | note over T:Assert
203 | ```
204 |
205 |
208 |
209 | ### Examples
210 |
211 | You can find examples of how to factor your Lambda function and how to test it:
212 |
213 | 1. In the [samples](https://github.com/martincostello/lambda-test-server/tree/main/samples "Sample functions and tests");
214 | 1. In the [unit tests](https://github.com/martincostello/lambda-test-server/blob/main/tests/AwsLambdaTestServer.Tests/Examples.cs "Unit test examples") for this project;
215 | 1. How I use the library in the tests for my own [Alexa skill](https://github.com/martincostello/alexa-london-travel/blob/e363ff77a1368e9da694c37fff33a1102ea6accf/test/LondonTravel.Skill.Tests/EndToEndTests.cs#L22 "Alexa London Travel's end-to-end tests").
216 |
217 | ### Advanced Usage
218 |
219 | #### AWS Mobile SDK with Cognito
220 |
221 | If you use either the `ClientContext` or `Identity` properties on `ILambdaContext` in your function, you can specify the serialized JSON for either property as a `string` when enqueueing a request to the test server to be made available to the function invocation.
222 |
223 | An example of providing these values from an xunit test is shown below:
224 |
225 | ```csharp
226 | using System;
227 | using System.Text.Json;
228 | using System.Threading;
229 | using System.Threading.Tasks;
230 | using MartinCostello.Testing.AwsLambdaTestServer;
231 | using Xunit;
232 |
233 | namespace MyFunctions;
234 |
235 | public static class ReverseFunctionWithMobileSdkTests
236 | {
237 | [Fact]
238 | public static async Task Function_Reverses_Numbers_With_Mobile_Sdk()
239 | {
240 | // Arrange
241 | using var server = new LambdaTestServer();
242 | using var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(1));
243 |
244 | await server.StartAsync(cancellationTokenSource.Token);
245 |
246 | int[] value = [1, 2, 3];
247 | string json = JsonSerializer.Serialize(value);
248 | byte[] content = Encoding.UTF8.GetBytes(json);
249 |
250 | var request = new LambdaTestRequest(content)
251 | {
252 | ClientContext = """{ "client": { "app_title": "my-app" } }""",
253 | CognitoIdentity = """{ "identityId": "my-identity" }""",
254 | };
255 |
256 | LambdaTestContext context = await server.EnqueueAsync(json);
257 |
258 | using var httpClient = server.CreateClient();
259 |
260 | // Act
261 | await ReverseFunction.RunAsync(httpClient, cancellationTokenSource.Token);
262 |
263 | // Assert
264 | Assert.True(context.Response.TryRead(out LambdaTestResponse response));
265 | Assert.True(response.IsSuccessful);
266 |
267 | json = await response.ReadAsStringAsync();
268 | int[] actual = JsonSerializer.Deserialize(json);
269 |
270 | Assert.Equal([3, 2, 1], actual);
271 | }
272 | }
273 | ```
274 |
275 | #### Lambda Runtime Options
276 |
277 | If your function makes use of the various other properties in the `ILambdaContext` passed to the function, you can pass an instance of `LambdaTestServerOptions` to the constructor of `LambdaTestServer` to change the values the server provides to `LambdaBootstrap` before it invokes your function.
278 |
279 | Options you can specify include the function memory size, timeout and ARN.
280 |
281 | > The test server does not enforce these values at runtime, unlike the production AWS Lambda environment. They are provided for you to drive the usage of such properties in the code you are testing and should not be relied on to ensure that your function does not take too long to execute or uses too much memory during execution or any other constraints, as appropriate.
282 |
283 | An example of this customisation for an xunit test is shown below:
284 |
285 | ```csharp
286 | using System;
287 | using System.Text.Json;
288 | using System.Threading;
289 | using System.Threading.Tasks;
290 | using MartinCostello.Testing.AwsLambdaTestServer;
291 | using Xunit;
292 |
293 | namespace MyFunctions;
294 |
295 | public static class ReverseFunctionWithCustomOptionsTests
296 | {
297 | [Fact]
298 | public static async Task Function_Reverses_Numbers_With_Custom_Options()
299 | {
300 | // Arrange
301 | var options = new LambdaTestServerOptions()
302 | {
303 | FunctionMemorySize = 256,
304 | FunctionTimeout = TimeSpan.FromSeconds(30),
305 | FunctionVersion = 42,
306 | };
307 |
308 | using var server = new LambdaTestServer(options);
309 | using var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(1));
310 |
311 | await server.StartAsync(cancellationTokenSource.Token);
312 |
313 | int[] value = [1, 2, 3];
314 | string json = JsonSerializer.Serialize(value);
315 |
316 | LambdaTestContext context = await server.EnqueueAsync(json);
317 |
318 | using var httpClient = server.CreateClient();
319 |
320 | // Act
321 | await ReverseFunction.RunAsync(httpClient, cancellationTokenSource.Token);
322 |
323 | // Assert
324 | Assert.True(context.Response.TryRead(out LambdaTestResponse response));
325 | Assert.True(response.IsSuccessful);
326 |
327 | json = await response.ReadAsStringAsync();
328 | int[] actual = JsonSerializer.Deserialize(json);
329 |
330 | Assert.Equal([3, 2, 1], actual);
331 | }
332 | }
333 | ```
334 |
335 | #### Logging from the Test Server
336 |
337 | To help diagnose failing tests, the `LambdaTestServer` outputs logs of the requests it receives to the emulated AWS Lambda Runtime it provides. To route the logging output to a location of your choosing, you can use the configuration callbacks, such as the constructor overload that accepts an `Action` or the `Configure` property on the `LambdaTestServerOptions` class.
338 |
339 | Here's an example of configuring the test server to route its logs to xunit using the [xunit-logging](https://www.nuget.org/packages/MartinCostello.Logging.XUnit) library:
340 |
341 | ```csharp
342 | using System;
343 | using System.Text.Json;
344 | using System.Threading;
345 | using System.Threading.Tasks;
346 | using MartinCostello.Logging.XUnit;
347 | using MartinCostello.Testing.AwsLambdaTestServer;
348 | using Microsoft.Extensions.DependencyInjection;
349 | using Microsoft.Extensions.Logging;
350 | using Xunit;
351 | using Xunit.Abstractions;
352 |
353 | namespace MartinCostello.Testing.AwsLambdaTestServer;
354 |
355 | public class ReverseFunctionWithLoggingTests : ITestOutputHelperAccessor
356 | {
357 | public ReverseFunctionWithLoggingTests(ITestOutputHelper outputHelper)
358 | {
359 | OutputHelper = outputHelper;
360 | }
361 |
362 | public ITestOutputHelper OutputHelper { get; set; }
363 |
364 | [Fact]
365 | public async Task Function_Reverses_Numbers_With_Logging()
366 | {
367 | // Arrange
368 | using var server = new LambdaTestServer(
369 | (services) => services.AddLogging(
370 | (builder) => builder.AddXUnit(this)));
371 |
372 | using var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(1));
373 |
374 | await server.StartAsync(cancellationTokenSource.Token);
375 |
376 | int[] value = [1, 2, 3];
377 | string json = JsonSerializer.Serialize(value);
378 |
379 | LambdaTestContext context = await server.EnqueueAsync(json);
380 |
381 | using var httpClient = server.CreateClient();
382 |
383 | // Act
384 | await ReverseFunction.RunAsync(httpClient, cancellationTokenSource.Token);
385 |
386 | // Assert
387 | Assert.True(context.Response.TryRead(out LambdaTestResponse response));
388 | Assert.True(response.IsSuccessful);
389 |
390 | json = await response.ReadAsStringAsync();
391 | int[] actual = JsonSerializer.Deserialize(json);
392 |
393 | Assert.Equal([3, 2, 1], actual);
394 | }
395 | }
396 | ```
397 |
398 | This then outputs logs similar to the below into the xunit test results:
399 |
400 | ```sh
401 | Test Name: Function_Reverses_Numbers_With_Logging
402 | Test Outcome: Passed
403 | Result StandardOutput:
404 | [2019-11-04 15:21:06Z] info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
405 | Request starting HTTP/1.1 GET http://localhost/2018-06-01/runtime/invocation/next
406 | [2019-11-04 15:21:06Z] info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
407 | Executing endpoint '/{LambdaVersion}/runtime/invocation/next HTTP: GET'
408 | [2019-11-04 15:21:06Z] info: MartinCostello.Testing.AwsLambdaTestServer.RuntimeHandler[0]
409 | Waiting for new request for Lambda function with ARN arn:aws:lambda:eu-west-1:123456789012:function:test-function.
410 | [2019-11-04 15:21:06Z] info: MartinCostello.Testing.AwsLambdaTestServer.RuntimeHandler[0]
411 | Invoking Lambda function with ARN arn:aws:lambda:eu-west-1:123456789012:function:test-function for request Id 7e1a283d-6268-4401-921c-0d0d67da1da4 and trace Id 51792f7f-2c1e-4934-bfd9-f5f7c6f0d628.
412 | [2019-11-04 15:21:06Z] info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
413 | Executed endpoint '/{LambdaVersion}/runtime/invocation/next HTTP: GET'
414 | [2019-11-04 15:21:06Z] info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
415 | Request finished in 71.9334ms 200 application/json
416 | [2019-11-04 15:21:06Z] info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
417 | Request starting HTTP/1.1 POST http://localhost/2018-06-01/runtime/invocation/7e1a283d-6268-4401-921c-0d0d67da1da4/response application/json
418 | [2019-11-04 15:21:06Z] info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
419 | Executing endpoint '/{LambdaVersion}/runtime/invocation/{AwsRequestId}/response HTTP: POST'
420 | [2019-11-04 15:21:06Z] info: MartinCostello.Testing.AwsLambdaTestServer.RuntimeHandler[0]
421 | Invoked Lambda function with ARN arn:aws:lambda:eu-west-1:123456789012:function:test-function for request Id 7e1a283d-6268-4401-921c-0d0d67da1da4: [3,2,1].
422 | [2019-11-04 15:21:06Z] info: MartinCostello.Testing.AwsLambdaTestServer.RuntimeHandler[0]
423 | Completed processing AWS request Id 7e1a283d-6268-4401-921c-0d0d67da1da4 for Lambda function with ARN arn:aws:lambda:eu-west-1:123456789012:function:test-function in 107 milliseconds.
424 | [2019-11-04 15:21:06Z] info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
425 | Executed endpoint '/{LambdaVersion}/runtime/invocation/{AwsRequestId}/response HTTP: POST'
426 | [2019-11-04 15:21:06Z] info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
427 | Request finished in 26.6306ms 204
428 | ```
429 |
430 | #### Custom Lambda Server
431 |
432 | It is also possible to use `LambdaTestServer` with a custom [`IServer`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.hosting.server.iserver "IServer Interface on docs.microsoft.com") implementation by overriding the [`CreateServer()`](https://github.com/martincostello/lambda-test-server/blob/cd5e038660d6e607d06833c03a4a0e8740d643a2/src/AwsLambdaTestServer/LambdaTestServer.cs#L209-L217 "LambdaTestServer.CreateServer() method") method in a derived class.
433 |
434 | This can be used, for example, to host the Lambda test server in a real HTTP server that can be accessed remotely instead of being hosted in-memory with the [`TestServer`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.testhost.testserver "TestServer Class on docs.microsoft.com") class.
435 |
436 | For examples of this use case, see the `MinimalApi` example project and its test project in the [samples](https://github.com/martincostello/lambda-test-server/tree/main/samples "Sample functions and tests").
437 |
438 | ## Feedback
439 |
440 | Any feedback or issues can be added to the issues for this project in [GitHub](https://github.com/martincostello/lambda-test-server/issues "Issues for this project on GitHub.com").
441 |
442 | ## Repository
443 |
444 | The repository is hosted in [GitHub](https://github.com/martincostello/lambda-test-server "This project on GitHub.com"):
445 |
446 | ## License
447 |
448 | This project is licensed under the [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt "The Apache 2.0 license") license.
449 |
450 | ## Building and Testing
451 |
452 | Compiling the library yourself requires Git and the [.NET SDK](https://dotnet.microsoft.com/download "Download the .NET SDK") to be installed.
453 |
454 | To build and test the library locally from a terminal/command-line, run one of the following set of commands:
455 |
456 | ```powershell
457 | git clone https://github.com/martincostello/lambda-test-server.git
458 | cd lambda-test-server
459 | ./build.ps1
460 | ```
461 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Reporting a Vulnerability
4 |
5 | To privately report a security vulnerability, please create a security advisory in the [repository's Security tab](https://github.com/martincostello/lambda-test-server/security/advisories).
6 |
7 | Further details can be found in the [GitHub documentation](https://docs.github.com/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability).
8 |
--------------------------------------------------------------------------------
/build.ps1:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env pwsh
2 | param(
3 | [Parameter(Mandatory = $false)][switch] $SkipTests
4 | )
5 |
6 | $ErrorActionPreference = "Stop"
7 | $InformationPreference = "Continue"
8 | $ProgressPreference = "SilentlyContinue"
9 |
10 | $solutionPath = $PSScriptRoot
11 | $sdkFile = Join-Path $solutionPath "global.json"
12 |
13 | $libraryProject = Join-Path $solutionPath "src" "AwsLambdaTestServer" "MartinCostello.Testing.AwsLambdaTestServer.csproj"
14 |
15 | $testProjects = @(
16 | (Join-Path $solutionPath "tests" "AwsLambdaTestServer.Tests" "MartinCostello.Testing.AwsLambdaTestServer.Tests.csproj"),
17 | (Join-Path $solutionPath "samples" "MathsFunctions.Tests" "MathsFunctions.Tests.csproj"),
18 | (Join-Path $solutionPath "samples" "MinimalApi.Tests" "MinimalApi.Tests.csproj")
19 | )
20 |
21 | $dotnetVersion = (Get-Content $sdkFile | Out-String | ConvertFrom-Json).sdk.version
22 |
23 | $installDotNetSdk = $false;
24 |
25 | if (($null -eq (Get-Command "dotnet" -ErrorAction SilentlyContinue)) -and ($null -eq (Get-Command "dotnet.exe" -ErrorAction SilentlyContinue))) {
26 | Write-Information "The .NET SDK is not installed."
27 | $installDotNetSdk = $true
28 | }
29 | else {
30 | Try {
31 | $installedDotNetVersion = (dotnet --version 2>&1 | Out-String).Trim()
32 | }
33 | Catch {
34 | $installedDotNetVersion = "?"
35 | }
36 |
37 | if ($installedDotNetVersion -ne $dotnetVersion) {
38 | Write-Information "The required version of the .NET SDK is not installed. Expected $dotnetVersion."
39 | $installDotNetSdk = $true
40 | }
41 | }
42 |
43 | if ($installDotNetSdk) {
44 |
45 | ${env:DOTNET_INSTALL_DIR} = Join-Path $solutionPath ".dotnet"
46 | $sdkPath = Join-Path ${env:DOTNET_INSTALL_DIR} "sdk" $dotnetVersion
47 |
48 | if (!(Test-Path $sdkPath)) {
49 | if (!(Test-Path ${env:DOTNET_INSTALL_DIR})) {
50 | mkdir ${env:DOTNET_INSTALL_DIR} | Out-Null
51 | }
52 | [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor "Tls12"
53 | if (($PSVersionTable.PSVersion.Major -ge 6) -And !$IsWindows) {
54 | $installScript = Join-Path ${env:DOTNET_INSTALL_DIR} "install.sh"
55 | Invoke-WebRequest "https://dot.net/v1/dotnet-install.sh" -OutFile $installScript -UseBasicParsing
56 | chmod +x $installScript
57 | & $installScript --jsonfile $sdkFile --install-dir ${env:DOTNET_INSTALL_DIR} --no-path --skip-non-versioned-files
58 | }
59 | else {
60 | $installScript = Join-Path ${env:DOTNET_INSTALL_DIR} "install.ps1"
61 | Invoke-WebRequest "https://dot.net/v1/dotnet-install.ps1" -OutFile $installScript -UseBasicParsing
62 | & $installScript -JsonFile $sdkFile -InstallDir ${env:DOTNET_INSTALL_DIR} -NoPath -SkipNonVersionedFiles
63 | }
64 | }
65 | }
66 | else {
67 | ${env:DOTNET_INSTALL_DIR} = Split-Path -Path (Get-Command dotnet).Path
68 | }
69 |
70 | $dotnet = Join-Path ${env:DOTNET_INSTALL_DIR} "dotnet"
71 |
72 | if ($installDotNetSdk) {
73 | ${env:PATH} = "${env:DOTNET_INSTALL_DIR};${env:PATH}"
74 | }
75 |
76 | function DotNetPack {
77 | param([string]$Project)
78 |
79 | & $dotnet pack $Project --include-symbols --include-source
80 |
81 | if ($LASTEXITCODE -ne 0) {
82 | throw "dotnet pack failed with exit code $LASTEXITCODE"
83 | }
84 | }
85 |
86 | function DotNetTest {
87 | param([string]$Project)
88 |
89 | $additionalArgs = @()
90 |
91 | if (-Not [string]::IsNullOrEmpty($env:GITHUB_SHA)) {
92 | $additionalArgs += "--logger:GitHubActions;report-warnings=false"
93 | $additionalArgs += "--logger:junit;LogFilePath=junit.xml"
94 | }
95 |
96 | & $dotnet test $Project --configuration "Release" $additionalArgs -- RunConfiguration.TestSessionTimeout=300000
97 |
98 | if ($LASTEXITCODE -ne 0) {
99 | throw "dotnet test failed with exit code $LASTEXITCODE"
100 | }
101 | }
102 |
103 | Write-Information "Packaging library..."
104 | DotNetPack $libraryProject
105 |
106 | if (-Not $SkipTests) {
107 | Write-Information "Running tests..."
108 | ForEach ($testProject in $testProjects) {
109 | DotNetTest $testProject
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/global.json:
--------------------------------------------------------------------------------
1 | {
2 | "sdk": {
3 | "version": "9.0.300",
4 | "allowPrerelease": false,
5 | "paths": [ ".dotnet", "$host$" ],
6 | "errorMessage": "The required version of the .NET SDK could not be found. Please run ./build.ps1 to bootstrap the .NET SDK."
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/package-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/martincostello/lambda-test-server/f886c3c2582a893cbba5c363167b594f74ad30cd/package-icon.png
--------------------------------------------------------------------------------
/package-readme.md:
--------------------------------------------------------------------------------
1 | # AWS Lambda Test Server for .NET
2 |
3 | [](https://www.nuget.org/packages/MartinCostello.Testing.AwsLambdaTestServer "Download MartinCostello.Testing.AwsLambdaTestServer from NuGet")
4 | [](https://www.nuget.org/packages/MartinCostello.Testing.AwsLambdaTestServer "Download MartinCostello.Testing.AwsLambdaTestServer from NuGet")
5 |
6 | [](https://github.com/martincostello/lambda-test-server/actions/workflows/build.yml?query=branch%3Amain+event%3Apush)
7 | [](https://codecov.io/gh/martincostello/lambda-test-server)
8 |
9 | ## Introduction
10 |
11 | A NuGet package that builds on top of the `TestServer` class in the [Microsoft.AspNetCore.TestHost](https://www.nuget.org/packages/Microsoft.AspNetCore.TestHost) NuGet package to provide infrastructure to use with end-to-end/integration tests of .NET 6 AWS Lambda Functions using a custom runtime with the `LambdaBootstrap` class from the [Amazon.Lambda.RuntimeSupport](https://www.nuget.org/packages/Amazon.Lambda.RuntimeSupport/) NuGet package.
12 |
13 | [_.NET Core 3.0 on Lambda with AWS Lambda's Custom Runtime_](https://aws.amazon.com/blogs/developer/net-core-3-0-on-lambda-with-aws-lambdas-custom-runtime/ ".NET Core 3.0 on Lambda with AWS Lambda's Custom Runtime on the AWS Developer Blog")
14 |
15 | ## Feedback
16 |
17 | Any feedback or issues for this package can be added to the issues in [GitHub](https://github.com/martincostello/lambda-test-server/issues "Issues for this package on GitHub.com").
18 |
19 | ## License
20 |
21 | This package is licensed under the [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt "The Apache 2.0 license") license.
22 |
--------------------------------------------------------------------------------
/samples/MathsFunctions.Tests/MathsFunctionTests.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using System.Text.Json;
5 | using MartinCostello.Testing.AwsLambdaTestServer;
6 |
7 | namespace MathsFunctions;
8 |
9 | public static class MathsFunctionTests
10 | {
11 | [Theory]
12 | [InlineData(1, "+", 1, 2)]
13 | [InlineData(9, "-", 6, 3)]
14 | [InlineData(2, "*", 2, 4)]
15 | [InlineData(9, "/", 3, 3)]
16 | [InlineData(7, "%", 2, 1)]
17 | [InlineData(3, "^", 3, 27)]
18 | public static async Task Function_Computes_Results(double left, string op, double right, double expected)
19 | {
20 | // Arrange
21 | using var server = new LambdaTestServer();
22 | using var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(2));
23 |
24 | await server.StartAsync(cancellationTokenSource.Token);
25 |
26 | var value = new MathsRequest() { Left = left, Operator = op, Right = right };
27 | string json = JsonSerializer.Serialize(value);
28 |
29 | var context = await server.EnqueueAsync(json);
30 |
31 | using var httpClient = server.CreateClient();
32 |
33 | // Act
34 | await MathsFunction.RunAsync(httpClient, cancellationTokenSource.Token);
35 |
36 | // Assert
37 | Assert.True(context.Response.TryRead(out var response));
38 | Assert.NotNull(response);
39 | Assert.True(response!.IsSuccessful);
40 |
41 | json = await response.ReadAsStringAsync();
42 | var actual = JsonSerializer.Deserialize(json);
43 |
44 | Assert.NotNull(actual);
45 | Assert.Equal(expected, actual!.Result);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/samples/MathsFunctions.Tests/MathsFunctions.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | Exe
4 | MathsFunctions
5 | net9.0
6 | true
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/samples/MathsFunctions/MathsFunction.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using Amazon.Lambda.RuntimeSupport;
5 | using Amazon.Lambda.Serialization.Json;
6 |
7 | namespace MathsFunctions;
8 |
9 | public static class MathsFunction
10 | {
11 | public static async Task Main()
12 | => await RunAsync();
13 |
14 | public static async Task RunAsync(HttpClient? httpClient = null, CancellationToken cancellationToken = default)
15 | {
16 | var serializer = new JsonSerializer();
17 |
18 | using var handlerWrapper = HandlerWrapper.GetHandlerWrapper(Evaluate, serializer);
19 | using var bootstrap = new LambdaBootstrap(httpClient ?? new HttpClient(), handlerWrapper);
20 |
21 | await bootstrap.RunAsync(cancellationToken);
22 | }
23 |
24 | private static MathsResponse Evaluate(MathsRequest request)
25 | {
26 | double result = request.Operator switch
27 | {
28 | "+" => request.Left + request.Right,
29 | "-" => request.Left - request.Right,
30 | "*" => request.Left * request.Right,
31 | "/" => request.Left / request.Right,
32 | "%" => request.Left % request.Right,
33 | "^" => Math.Pow(request.Left, request.Right),
34 | _ => throw new NotSupportedException($"The '{request.Operator}' operator is not supported."),
35 | };
36 |
37 | return new() { Result = result };
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/samples/MathsFunctions/MathsFunctions.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | false
4 | $(NoWarn);CA2000;CA2007;SA1600
5 | Library
6 | MathsFunctions
7 | net8.0;net9.0
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/samples/MathsFunctions/MathsRequest.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace MathsFunctions;
5 |
6 | public class MathsRequest
7 | {
8 | public double Left { get; set; }
9 |
10 | public double Right { get; set; }
11 |
12 | public string? Operator { get; set; }
13 | }
14 |
--------------------------------------------------------------------------------
/samples/MathsFunctions/MathsResponse.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace MathsFunctions;
5 |
6 | public class MathsResponse
7 | {
8 | public double Result { get; set; }
9 | }
10 |
--------------------------------------------------------------------------------
/samples/MinimalApi.Tests/ApiTests.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using System.Net.Sockets;
5 | using System.Reflection;
6 | using System.Text.Json;
7 | using Amazon.Lambda.APIGatewayEvents;
8 | using MartinCostello.Testing.AwsLambdaTestServer;
9 | using Microsoft.AspNetCore.Http;
10 |
11 | namespace MinimalApi;
12 |
13 | public sealed class ApiTests : IAsyncLifetime, IDisposable
14 | {
15 | private readonly HttpLambdaTestServer _server;
16 |
17 | public ApiTests(ITestOutputHelper outputHelper)
18 | {
19 | _server = new() { OutputHelper = outputHelper };
20 | }
21 |
22 | public void Dispose()
23 | => _server.Dispose();
24 |
25 | public async ValueTask DisposeAsync()
26 | => await _server.DisposeAsync();
27 |
28 | public async ValueTask InitializeAsync()
29 | => await _server.InitializeAsync();
30 |
31 | [Fact(Timeout = 5_000)]
32 | public async Task Can_Hash_String()
33 | {
34 | // Arrange
35 | var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
36 |
37 | var body = new
38 | {
39 | algorithm = "sha256",
40 | format = "base64",
41 | plaintext = "ASP.NET Core",
42 | };
43 |
44 | var request = new APIGatewayProxyRequest()
45 | {
46 | Body = JsonSerializer.Serialize(body),
47 | Headers = new Dictionary()
48 | {
49 | ["content-type"] = "application/json",
50 | },
51 | HttpMethod = HttpMethods.Post,
52 | Path = "/hash",
53 | };
54 |
55 | // Arrange
56 | string json = JsonSerializer.Serialize(request, options);
57 |
58 | LambdaTestContext context = await _server.EnqueueAsync(json);
59 |
60 | using var cts = GetCancellationTokenSourceForResponseAvailable(context);
61 |
62 | // Act
63 | _ = Task.Run(
64 | () =>
65 | {
66 | try
67 | {
68 | typeof(HashRequest).Assembly.EntryPoint!.Invoke(null, [Array.Empty()]);
69 | }
70 | catch (Exception ex) when (LambdaServerWasShutDown(ex))
71 | {
72 | // The Lambda runtime server was shut down
73 | }
74 | },
75 | cts.Token);
76 |
77 | // Assert
78 | await context.Response.WaitToReadAsync(cts.IsCancellationRequested ? default : cts.Token);
79 |
80 | context.Response.TryRead(out LambdaTestResponse? response).ShouldBeTrue();
81 | response.IsSuccessful.ShouldBeTrue($"Failed to process request: {await response.ReadAsStringAsync()}");
82 | response.Duration.ShouldBeInRange(TimeSpan.Zero, TimeSpan.FromSeconds(2));
83 | response.Content.ShouldNotBeEmpty();
84 |
85 | // Assert
86 | var actual = JsonSerializer.Deserialize(response.Content, options);
87 |
88 | actual.ShouldNotBeNull();
89 |
90 | actual.ShouldNotBeNull();
91 | actual.StatusCode.ShouldBe(StatusCodes.Status200OK);
92 | actual.MultiValueHeaders.ShouldContainKey("Content-Type");
93 | actual.MultiValueHeaders["Content-Type"].ShouldBe(["application/json; charset=utf-8"]);
94 |
95 | var hash = JsonSerializer.Deserialize(actual.Body, options);
96 |
97 | hash.ShouldNotBeNull();
98 | hash.Hash.ShouldBe("XXE/IcKhlw/yjLTH7cCWPSr7JfOw5LuYXeBuE5skNfA=");
99 | }
100 |
101 | private static CancellationTokenSource GetCancellationTokenSourceForResponseAvailable(
102 | LambdaTestContext context,
103 | TimeSpan? timeout = null)
104 | {
105 | if (timeout == null)
106 | {
107 | timeout = System.Diagnostics.Debugger.IsAttached ? Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(3);
108 | }
109 |
110 | var cts = new CancellationTokenSource(timeout.Value);
111 |
112 | // Queue a task to stop the test server from listening as soon as the response is available
113 | _ = Task.Run(
114 | async () =>
115 | {
116 | await context.Response.WaitToReadAsync(cts.Token);
117 |
118 | if (!cts.IsCancellationRequested)
119 | {
120 | await cts.CancelAsync();
121 | }
122 | },
123 | cts.Token);
124 |
125 | return cts;
126 | }
127 |
128 | private static bool LambdaServerWasShutDown(Exception exception)
129 | {
130 | if (exception is not TargetInvocationException targetException ||
131 | targetException.InnerException is not HttpRequestException httpException ||
132 | httpException.InnerException is not SocketException socketException)
133 | {
134 | return false;
135 | }
136 |
137 | return socketException.SocketErrorCode == SocketError.ConnectionRefused;
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/samples/MinimalApi.Tests/HttpLambdaTestServer.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using MartinCostello.Logging.XUnit;
5 | using MartinCostello.Testing.AwsLambdaTestServer;
6 | using Microsoft.AspNetCore.Hosting;
7 | using Microsoft.AspNetCore.Hosting.Server;
8 | using Microsoft.Extensions.DependencyInjection;
9 | using Microsoft.Extensions.Logging;
10 |
11 | namespace MinimalApi;
12 |
13 | internal sealed class HttpLambdaTestServer : LambdaTestServer, IAsyncLifetime, ITestOutputHelperAccessor
14 | {
15 | private readonly CancellationTokenSource _cts = new();
16 | private bool _disposed;
17 | private IWebHost? _webHost;
18 |
19 | public ITestOutputHelper? OutputHelper { get; set; }
20 |
21 | public async ValueTask DisposeAsync()
22 | {
23 | if (_webHost is not null)
24 | {
25 | await _webHost.StopAsync();
26 | }
27 |
28 | Dispose();
29 | }
30 |
31 | public async ValueTask InitializeAsync()
32 | => await StartAsync(_cts.Token);
33 |
34 | protected override IServer CreateServer(WebHostBuilder builder)
35 | {
36 | _webHost = builder
37 | .UseKestrel()
38 | .ConfigureServices((services) => services.AddLogging((builder) => builder.AddXUnit(this)))
39 | .UseUrls("http://127.0.0.1:0")
40 | .Build();
41 |
42 | _webHost.Start();
43 |
44 | return _webHost.Services.GetRequiredService();
45 | }
46 |
47 | protected override void Dispose(bool disposing)
48 | {
49 | if (!_disposed)
50 | {
51 | if (disposing)
52 | {
53 | _webHost?.Dispose();
54 |
55 | _cts.Cancel();
56 | _cts.Dispose();
57 | }
58 |
59 | _disposed = true;
60 | }
61 |
62 | base.Dispose(disposing);
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/samples/MinimalApi.Tests/MinimalApi.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | Exe
4 | MinimalApi
5 | net9.0
6 | true
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/samples/MinimalApi/HashRequest.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace MinimalApi;
5 |
6 | public class HashRequest
7 | {
8 | public string Algorithm { get; set; } = string.Empty;
9 |
10 | public string Format { get; set; } = string.Empty;
11 |
12 | public string Plaintext { get; set; } = string.Empty;
13 | }
14 |
--------------------------------------------------------------------------------
/samples/MinimalApi/HashResponse.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace MinimalApi;
5 |
6 | public class HashResponse
7 | {
8 | public string Hash { get; set; } = string.Empty;
9 | }
10 |
--------------------------------------------------------------------------------
/samples/MinimalApi/MinimalApi.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | $(NoWarn);CA1050;CA1812;CA2007;CA5350;CA5351;SA1600
4 | MinimalApi
5 | net9.0
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/samples/MinimalApi/Program.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using System.Security.Cryptography;
5 | using System.Text;
6 | using MinimalApi;
7 |
8 | var builder = WebApplication.CreateBuilder(args);
9 |
10 | builder.Services.AddAWSLambdaHosting(LambdaEventSource.RestApi);
11 |
12 | var app = builder.Build();
13 |
14 | app.MapGet("/", () => "Hello World!");
15 |
16 | app.MapPost("/hash", async (HttpRequest httpRequest) =>
17 | {
18 | var request = await httpRequest.ReadFromJsonAsync();
19 |
20 | if (string.IsNullOrWhiteSpace(request?.Algorithm))
21 | {
22 | return Results.Problem(
23 | "No hash algorithm name specified.",
24 | statusCode: StatusCodes.Status400BadRequest);
25 | }
26 |
27 | if (string.IsNullOrWhiteSpace(request.Format))
28 | {
29 | return Results.Problem(
30 | "No hash output format specified.",
31 | statusCode: StatusCodes.Status400BadRequest);
32 | }
33 |
34 | bool? formatAsBase64 = request.Format.ToUpperInvariant() switch
35 | {
36 | "BASE64" => true,
37 | "HEXADECIMAL" => false,
38 | _ => null,
39 | };
40 |
41 | if (formatAsBase64 is null)
42 | {
43 | return Results.Problem(
44 | $"The specified hash format '{request.Format}' is invalid.",
45 | statusCode: StatusCodes.Status400BadRequest);
46 | }
47 |
48 | const int MaxPlaintextLength = 4096;
49 |
50 | if (request.Plaintext?.Length > MaxPlaintextLength)
51 | {
52 | return Results.Problem(
53 | $"The plaintext to hash cannot be more than {MaxPlaintextLength} characters in length.",
54 | statusCode: StatusCodes.Status400BadRequest);
55 | }
56 |
57 | byte[] buffer = Encoding.UTF8.GetBytes(request.Plaintext ?? string.Empty);
58 | HashAlgorithmName? hashAlgorithm = request.Algorithm.ToUpperInvariant() switch
59 | {
60 | "MD5" => HashAlgorithmName.MD5,
61 | "SHA1" => HashAlgorithmName.SHA1,
62 | "SHA256" => HashAlgorithmName.SHA256,
63 | "SHA384" => HashAlgorithmName.SHA384,
64 | "SHA512" => HashAlgorithmName.SHA512,
65 | _ => null,
66 | };
67 |
68 | if (hashAlgorithm is not { } algorithm)
69 | {
70 | return Results.Problem(
71 | $"The specified hash algorithm '{request.Algorithm}' is not supported.",
72 | statusCode: StatusCodes.Status400BadRequest);
73 | }
74 |
75 | byte[] hash = CryptographicOperations.HashData(algorithm, buffer);
76 |
77 | var result = new HashResponse()
78 | {
79 | Hash = formatAsBase64 == true ? Convert.ToBase64String(hash) : Convert.ToHexString(hash),
80 | };
81 |
82 | return Results.Json(result);
83 | });
84 |
85 | app.Run();
86 |
--------------------------------------------------------------------------------
/samples/MinimalApi/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "MinimalApi": {
4 | "commandName": "Project",
5 | "dotnetRunMessages": true,
6 | "launchBrowser": true,
7 | "applicationUrl": "https://localhost:5001;http://localhost:5000",
8 | "environmentVariables": {
9 | "ASPNETCORE_ENVIRONMENT": "Development"
10 | }
11 | },
12 | "MinimalApi.Lambda": {
13 | "commandName": "Project",
14 | "commandLineArgs": "",
15 | "launchBrowser": true,
16 | "environmentVariables": {
17 | "ASPNETCORE_ENVIRONMENT": "Development",
18 | "AWS_LAMBDA_FUNCTION_NAME": "MinimalApi",
19 | "AWS_LAMBDA_RUNTIME_API": "localhost:5050",
20 | "AWS_PROFILE": "default",
21 | "AWS_REGION": "eu-west-1"
22 | },
23 | "applicationUrl": "http://localhost:5050/runtime"
24 | },
25 | "Lambda Test Tool": {
26 | "commandName": "Executable",
27 | "commandLineArgs": "--port 5050",
28 | "workingDirectory": ".\\bin\\$(Configuration)\\net9.0",
29 | "executablePath": "%USERPROFILE%\\.dotnet\\tools\\dotnet-lambda-test-tool-9.0.exe"
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/samples/MinimalApi/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/samples/MinimalApi/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | },
8 | "AllowedHosts": "*"
9 | }
10 |
--------------------------------------------------------------------------------
/src/AwsLambdaTestServer/LambdaTestContext.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using System.Threading.Channels;
5 |
6 | namespace MartinCostello.Testing.AwsLambdaTestServer;
7 |
8 | ///
9 | /// A class representing the context for an AWS request enqueued to be processed by an AWS Lambda function. This class cannot be inherited.
10 | ///
11 | public sealed class LambdaTestContext
12 | {
13 | ///
14 | /// Initializes a new instance of the class.
15 | ///
16 | /// The request to invoke the Lambda function with.
17 | /// The channel reader associated with the response.
18 | internal LambdaTestContext(LambdaTestRequest request, ChannelReader reader)
19 | {
20 | Request = request;
21 | Response = reader;
22 | }
23 |
24 | ///
25 | /// Gets the request to invoke the Lambda function with.
26 | ///
27 | public LambdaTestRequest Request { get; }
28 |
29 | ///
30 | /// Gets the channel reader which completes once the request is processed by the function.
31 | ///
32 | public ChannelReader Response { get; }
33 | }
34 |
--------------------------------------------------------------------------------
/src/AwsLambdaTestServer/LambdaTestRequest.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace MartinCostello.Testing.AwsLambdaTestServer;
5 |
6 | #pragma warning disable CA1819
7 |
8 | ///
9 | /// A class representing a test request to an AWS Lambda function.
10 | ///
11 | public class LambdaTestRequest
12 | {
13 | ///
14 | /// Initializes a new instance of the class.
15 | ///
16 | /// The raw content of the request to invoke the Lambda function with.
17 | /// The optional AWS request Id to invoke the Lambda function with.
18 | ///
19 | /// is .
20 | ///
21 | public LambdaTestRequest(byte[] content, string? awsRequestId = null)
22 | {
23 | ArgumentNullException.ThrowIfNull(content);
24 | Content = content;
25 | AwsRequestId = awsRequestId ?? Guid.NewGuid().ToString();
26 | }
27 |
28 | ///
29 | /// Gets the AWS request Id for the request to the function.
30 | ///
31 | public string AwsRequestId { get; }
32 |
33 | ///
34 | /// Gets the raw byte content of the request to the function.
35 | ///
36 | public byte[] Content { get; }
37 |
38 | ///
39 | /// Gets or sets an optional string containing the serialized JSON
40 | /// for the client context when invoked through the AWS Mobile SDK.
41 | ///
42 | public string? ClientContext { get; set; }
43 |
44 | ///
45 | /// Gets or sets an optional string containing the serialized JSON for the
46 | /// Amazon Cognito identity provider when invoked through the AWS Mobile SDK.
47 | ///
48 | public string? CognitoIdentity { get; set; }
49 | }
50 |
--------------------------------------------------------------------------------
/src/AwsLambdaTestServer/LambdaTestResponse.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace MartinCostello.Testing.AwsLambdaTestServer;
5 |
6 | #pragma warning disable CA1819
7 |
8 | ///
9 | /// A class representing a test response from an AWS Lambda function. This class cannot be inherited.
10 | ///
11 | public sealed class LambdaTestResponse
12 | {
13 | ///
14 | /// Initializes a new instance of the class.
15 | ///
16 | /// The raw content of the response from the Lambda function.
17 | /// Whether the response indicates the request was successfully handled.
18 | /// The duration of the Lambda function invocation.
19 | internal LambdaTestResponse(byte[] content, bool isSuccessful, TimeSpan duration)
20 | {
21 | Content = content;
22 | Duration = duration;
23 | IsSuccessful = isSuccessful;
24 | }
25 |
26 | ///
27 | /// Gets the raw byte content of the response from the function.
28 | ///
29 | public byte[] Content { get; }
30 |
31 | ///
32 | /// Gets the approximate duration of the Lambda function invocation.
33 | ///
34 | public TimeSpan Duration { get; }
35 |
36 | ///
37 | /// Gets a value indicating whether the response indicates the request was successfully handled.
38 | ///
39 | public bool IsSuccessful { get; }
40 | }
41 |
--------------------------------------------------------------------------------
/src/AwsLambdaTestServer/LambdaTestResponseExtensions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using System.ComponentModel;
5 |
6 | namespace MartinCostello.Testing.AwsLambdaTestServer;
7 |
8 | ///
9 | /// A class containing extension methods for the class. This class cannot be inherited.
10 | ///
11 | [EditorBrowsable(EditorBrowsableState.Never)]
12 | public static class LambdaTestResponseExtensions
13 | {
14 | ///
15 | /// Reads the content of the specified response as a string as an asynchronous operation.
16 | ///
17 | /// The response to read as a string.
18 | ///
19 | /// A representing the asynchronous operation to
20 | /// read the content of the specified response as a .
21 | ///
22 | ///
23 | /// is .
24 | ///
25 | public static async Task ReadAsStringAsync(this LambdaTestResponse response)
26 | {
27 | ArgumentNullException.ThrowIfNull(response);
28 |
29 | using var stream = new MemoryStream(response.Content);
30 | using var reader = new StreamReader(stream);
31 |
32 | return await reader.ReadToEndAsync().ConfigureAwait(false);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/AwsLambdaTestServer/LambdaTestServer.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using System.Threading.Channels;
5 | using Microsoft.AspNetCore.Builder;
6 | using Microsoft.AspNetCore.Hosting;
7 | using Microsoft.AspNetCore.Hosting.Server;
8 | using Microsoft.AspNetCore.Hosting.Server.Features;
9 | using Microsoft.AspNetCore.TestHost;
10 | using Microsoft.Extensions.DependencyInjection;
11 | using Microsoft.Extensions.Hosting;
12 | using Microsoft.Extensions.Logging;
13 |
14 | namespace MartinCostello.Testing.AwsLambdaTestServer;
15 |
16 | ///
17 | /// A class representing a test AWS Lambda runtime HTTP server for an AWS Lambda function.
18 | ///
19 | public class LambdaTestServer : IDisposable
20 | {
21 | private readonly CancellationTokenSource _onDisposed;
22 |
23 | private bool _disposed;
24 | private RuntimeHandler? _handler;
25 | private bool _isStarted;
26 | private IServer? _server;
27 | private CancellationTokenSource? _onStopped;
28 |
29 | ///
30 | /// Initializes a new instance of the class.
31 | ///
32 | public LambdaTestServer()
33 | : this(new LambdaTestServerOptions())
34 | {
35 | }
36 |
37 | ///
38 | /// Initializes a new instance of the class.
39 | ///
40 | /// An optional delegate to invoke when configuring the test Lambda runtime server.
41 | public LambdaTestServer(Action configure)
42 | : this(new LambdaTestServerOptions() { Configure = configure })
43 | {
44 | }
45 |
46 | ///
47 | /// Initializes a new instance of the class.
48 | ///
49 | /// The options to use to configure the test Lambda runtime server.
50 | ///
51 | /// is .
52 | ///
53 | public LambdaTestServer(LambdaTestServerOptions options)
54 | {
55 | ArgumentNullException.ThrowIfNull(options);
56 | Options = options;
57 | _onDisposed = new CancellationTokenSource();
58 | }
59 |
60 | ///
61 | /// Finalizes an instance of the class.
62 | ///
63 | ~LambdaTestServer()
64 | {
65 | Dispose(false);
66 | }
67 |
68 | ///
69 | /// Gets a value indicating whether the test Lambda runtime server has been started.
70 | ///
71 | public bool IsStarted => _isStarted;
72 |
73 | ///
74 | /// Gets the options in use by the test Lambda runtime server.
75 | ///
76 | public LambdaTestServerOptions Options { get; }
77 |
78 | ///
79 | /// Clears any AWS Lambda environment variables set by instances of .
80 | ///
81 | public static void ClearLambdaEnvironmentVariables()
82 | {
83 | Environment.SetEnvironmentVariable("AWS_LAMBDA_DOTNET_DISABLE_MEMORY_LIMIT_CHECK", null);
84 | Environment.SetEnvironmentVariable("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", null);
85 | Environment.SetEnvironmentVariable("AWS_LAMBDA_FUNCTION_NAME", null);
86 | Environment.SetEnvironmentVariable("AWS_LAMBDA_FUNCTION_VERSION", null);
87 | Environment.SetEnvironmentVariable("AWS_LAMBDA_LOG_GROUP_NAME", null);
88 | Environment.SetEnvironmentVariable("AWS_LAMBDA_LOG_STREAM_NAME", null);
89 | Environment.SetEnvironmentVariable("AWS_LAMBDA_RUNTIME_API", null);
90 | Environment.SetEnvironmentVariable("_HANDLER", null);
91 | }
92 |
93 | ///
94 | public void Dispose()
95 | {
96 | Dispose(true);
97 | GC.SuppressFinalize(this);
98 | }
99 |
100 | ///
101 | /// Creates an to use to interact with the test Lambda runtime server.
102 | ///
103 | ///
104 | /// An that can be used to process Lambda runtime HTTP requests.
105 | ///
106 | ///
107 | /// The test server has not been started.
108 | ///
109 | ///
110 | /// The instance has been disposed.
111 | ///
112 | public virtual HttpClient CreateClient()
113 | {
114 | ThrowIfDisposed();
115 | ThrowIfNotStarted();
116 |
117 | if (_server is TestServer testServer)
118 | {
119 | return testServer.CreateClient();
120 | }
121 |
122 | var baseAddress = GetServerBaseAddress();
123 |
124 | return new() { BaseAddress = baseAddress };
125 | }
126 |
127 | ///
128 | /// Enqueues a request for the Lambda function to process as an asynchronous operation.
129 | ///
130 | /// The request to invoke the function with.
131 | ///
132 | /// A representing the asynchronous operation to
133 | /// enqueue the request which returns a context containing a
134 | /// which completes once the request is processed by the function.
135 | ///
136 | ///
137 | /// is .
138 | ///
139 | ///
140 | /// A request with the Id specified by is currently in-flight or the test server has not been started.
141 | ///
142 | ///
143 | /// The instance has been disposed.
144 | ///
145 | public async Task EnqueueAsync(LambdaTestRequest request)
146 | {
147 | ArgumentNullException.ThrowIfNull(request);
148 |
149 | ThrowIfDisposed();
150 | ThrowIfNotStarted();
151 |
152 | var reader = await _handler!.EnqueueAsync(request, _onStopped!.Token).ConfigureAwait(false);
153 |
154 | return new LambdaTestContext(request, reader);
155 | }
156 |
157 | ///
158 | /// Starts the test Lambda runtime server as an asynchronous operation.
159 | ///
160 | ///
161 | /// The optional cancellation token to use to signal the server should stop listening to invocation requests.
162 | ///
163 | ///
164 | /// A representing the asynchronous operation to start the test Lambda runtime server.
165 | ///
166 | ///
167 | /// The test server has already been started.
168 | ///
169 | ///
170 | /// The instance has been disposed.
171 | ///
172 | public virtual Task StartAsync(CancellationToken cancellationToken = default)
173 | {
174 | ThrowIfDisposed();
175 |
176 | if (_server != null)
177 | {
178 | throw new InvalidOperationException("The test server has already been started.");
179 | }
180 |
181 | _onStopped = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _onDisposed.Token);
182 | _handler = new RuntimeHandler(Options, _onStopped.Token);
183 |
184 | var builder = new WebHostBuilder();
185 |
186 | ConfigureWebHost(builder);
187 |
188 | _server = CreateServer(builder) ?? throw new InvalidOperationException($"No {nameof(IServer)} was returned by the {nameof(CreateServer)}() method.");
189 |
190 | Uri baseAddress;
191 |
192 | if (_server is TestServer testServer)
193 | {
194 | _handler.Logger = testServer.Services.GetRequiredService>();
195 | baseAddress = testServer.BaseAddress;
196 | }
197 | else
198 | {
199 | baseAddress = GetServerBaseAddress();
200 | }
201 |
202 | SetLambdaEnvironmentVariables(baseAddress);
203 |
204 | _isStarted = true;
205 |
206 | return Task.CompletedTask;
207 | }
208 |
209 | ///
210 | /// Creates the server to use for the Lambda runtime.
211 | ///
212 | /// The to use to create the server.
213 | ///
214 | /// The to use.
215 | ///
216 | protected virtual IServer CreateServer(WebHostBuilder builder)
217 | => new TestServer(builder);
218 |
219 | ///
220 | /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
221 | ///
222 | ///
223 | /// to release both managed and unmanaged resources;
224 | /// to release only unmanaged resources.
225 | ///
226 | protected virtual void Dispose(bool disposing)
227 | {
228 | if (!_disposed)
229 | {
230 | if (disposing)
231 | {
232 | if (_onDisposed != null)
233 | {
234 | // The token for _onStopped is linked to this token, so this will cancel both
235 | if (!_onDisposed.IsCancellationRequested)
236 | {
237 | _onDisposed.Cancel();
238 | }
239 |
240 | _onDisposed.Dispose();
241 | _onStopped?.Dispose();
242 | }
243 |
244 | _server?.Dispose();
245 | _handler?.Dispose();
246 | }
247 |
248 | _disposed = true;
249 | }
250 | }
251 |
252 | ///
253 | /// Configures the application for the test Lambda runtime server.
254 | ///
255 | /// The to configure.
256 | protected virtual void Configure(IApplicationBuilder app)
257 | {
258 | app.UseRouting();
259 | app.UseEndpoints((endpoints) =>
260 | {
261 | #pragma warning disable ASP0018
262 | // See https://github.com/aws/aws-lambda-dotnet/blob/4f9142b95b376bd238bce6be43f4e1ec1f983592/Libraries/src/Amazon.Lambda.RuntimeSupport/Client/InternalClientAdapted.cs#L75
263 | endpoints.MapGet("/{LambdaVersion}/runtime/invocation/next", _handler!.HandleNextAsync);
264 | endpoints.MapPost("/{LambdaVersion}/runtime/init/error", _handler.HandleInitializationErrorAsync);
265 | endpoints.MapPost("/{LambdaVersion}/runtime/invocation/{AwsRequestId}/error", _handler.HandleInvocationErrorAsync);
266 | endpoints.MapPost("/{LambdaVersion}/runtime/invocation/{AwsRequestId}/response", _handler.HandleResponseAsync);
267 | #pragma warning restore ASP0018
268 | });
269 | }
270 |
271 | ///
272 | /// Configures the services for the test Lambda runtime server application.
273 | ///
274 | /// The to use.
275 | protected virtual void ConfigureServices(IServiceCollection services)
276 | {
277 | services.AddRouting();
278 |
279 | Options.Configure?.Invoke(services);
280 | }
281 |
282 | ///
283 | /// Configures the web host builder for the test Lambda runtime server.
284 | ///
285 | /// The to configure.
286 | ///
287 | /// is .
288 | ///
289 | protected virtual void ConfigureWebHost(IWebHostBuilder builder)
290 | {
291 | ArgumentNullException.ThrowIfNull(builder);
292 |
293 | builder.UseContentRoot(Environment.CurrentDirectory);
294 | builder.UseShutdownTimeout(TimeSpan.Zero);
295 |
296 | builder.ConfigureServices(ConfigureServices);
297 | builder.Configure(Configure);
298 | }
299 |
300 | private void SetLambdaEnvironmentVariables(Uri baseAddress)
301 | {
302 | var provider = CultureInfo.InvariantCulture;
303 |
304 | // See https://github.com/aws/aws-lambda-dotnet/blob/4f9142b95b376bd238bce6be43f4e1ec1f983592/Libraries/src/Amazon.Lambda.RuntimeSupport/Context/LambdaEnvironment.cs#L46-L52
305 | Environment.SetEnvironmentVariable("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", Options.FunctionMemorySize.ToString(provider));
306 | Environment.SetEnvironmentVariable("AWS_LAMBDA_FUNCTION_NAME", Options.FunctionName);
307 | Environment.SetEnvironmentVariable("AWS_LAMBDA_FUNCTION_VERSION", Options.FunctionVersion.ToString(provider));
308 | Environment.SetEnvironmentVariable("AWS_LAMBDA_LOG_GROUP_NAME", Options.LogGroupName);
309 | Environment.SetEnvironmentVariable("AWS_LAMBDA_LOG_STREAM_NAME", Options.LogStreamName);
310 | Environment.SetEnvironmentVariable("AWS_LAMBDA_RUNTIME_API", $"{baseAddress.Host}:{baseAddress.Port}");
311 | Environment.SetEnvironmentVariable("_HANDLER", Options.FunctionHandler);
312 |
313 | // See https://github.com/aws/aws-lambda-dotnet/pull/1595
314 | if (Options.DisableMemoryLimitCheck)
315 | {
316 | Environment.SetEnvironmentVariable("AWS_LAMBDA_DOTNET_DISABLE_MEMORY_LIMIT_CHECK", bool.TrueString);
317 | }
318 | }
319 |
320 | private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(_disposed, this);
321 |
322 | [System.Diagnostics.CodeAnalysis.MemberNotNull(nameof(_server))]
323 | private void ThrowIfNotStarted()
324 | {
325 | if (_server is null)
326 | {
327 | throw new InvalidOperationException("The test server has not been started.");
328 | }
329 | }
330 |
331 | private Uri GetServerBaseAddress()
332 | {
333 | var serverAddresses = _server!.Features.Get();
334 | var serverUrl = serverAddresses?.Addresses?.FirstOrDefault();
335 |
336 | return serverUrl is null
337 | ? throw new InvalidOperationException("No server addresses are available.")
338 | : new Uri(serverUrl, UriKind.Absolute);
339 | }
340 | }
341 |
--------------------------------------------------------------------------------
/src/AwsLambdaTestServer/LambdaTestServerExtensions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using System.ComponentModel;
5 | using System.Text;
6 | using System.Threading.Channels;
7 |
8 | namespace MartinCostello.Testing.AwsLambdaTestServer;
9 |
10 | ///
11 | /// A class containing extension methods for the class. This class cannot be inherited.
12 | ///
13 | [EditorBrowsable(EditorBrowsableState.Never)]
14 | public static class LambdaTestServerExtensions
15 | {
16 | ///
17 | /// Enqueues a request for the Lambda function to process as an asynchronous operation.
18 | ///
19 | /// The server to enqueue the request with.
20 | /// The request content to process.
21 | ///
22 | /// A representing the asynchronous operation to
23 | /// enqueue the request which returns a context containing a
24 | /// which completes once the request is processed by the function.
25 | ///
26 | ///
27 | /// or is .
28 | ///
29 | ///
30 | /// The instance has been disposed.
31 | ///
32 | public static async Task EnqueueAsync(
33 | this LambdaTestServer server,
34 | string value)
35 | {
36 | ArgumentNullException.ThrowIfNull(server);
37 | ArgumentNullException.ThrowIfNull(value);
38 |
39 | byte[] content = Encoding.UTF8.GetBytes(value);
40 |
41 | return await server.EnqueueAsync(content).ConfigureAwait(false);
42 | }
43 |
44 | ///
45 | /// Enqueues a request for the Lambda function to process as an asynchronous operation.
46 | ///
47 | /// The server to enqueue the request with.
48 | /// The request content to process.
49 | ///
50 | /// A representing the asynchronous operation to
51 | /// enqueue the request which returns a context containing a
52 | /// which completes once the request is processed by the function.
53 | ///
54 | ///
55 | /// or is .
56 | ///
57 | ///
58 | /// The instance has been disposed.
59 | ///
60 | public static async Task EnqueueAsync(
61 | this LambdaTestServer server,
62 | byte[] content)
63 | {
64 | ArgumentNullException.ThrowIfNull(server);
65 |
66 | var request = new LambdaTestRequest(content);
67 |
68 | return await server.EnqueueAsync(request).ConfigureAwait(false);
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/AwsLambdaTestServer/LambdaTestServerOptions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using Microsoft.Extensions.DependencyInjection;
5 |
6 | namespace MartinCostello.Testing.AwsLambdaTestServer;
7 |
8 | ///
9 | /// A class representing options for the class. This class cannot be inherited.
10 | ///
11 | public sealed class LambdaTestServerOptions
12 | {
13 | ///
14 | /// Initializes a new instance of the class.
15 | ///
16 | public LambdaTestServerOptions()
17 | {
18 | FunctionName = "test-function";
19 | FunctionArn = $"arn:aws:lambda:eu-west-1:123456789012:function:{FunctionName}";
20 | LogGroupName = "test-function-log-group";
21 | LogStreamName = "test-function-log-stream";
22 |
23 | FunctionHandler = string.Empty;
24 | FunctionMemorySize = 128;
25 | FunctionTimeout = TimeSpan.FromSeconds(3);
26 | FunctionVersion = 1;
27 | }
28 |
29 | ///
30 | /// Gets or sets an optional delegate to invoke when configuring the test Lambda runtime server.
31 | ///
32 | public Action? Configure { get; set; }
33 |
34 | ///
35 | /// Gets or sets a value indicating whether to disable the memory limit check for the Lambda function.
36 | ///
37 | public bool DisableMemoryLimitCheck { get; set; }
38 |
39 | ///
40 | /// Gets or sets the ARN of the Lambda function being tested.
41 | ///
42 | public string FunctionArn { get; set; }
43 |
44 | ///
45 | /// Gets or sets the optional handler for the Lambda function being tested.
46 | ///
47 | public string FunctionHandler { get; set; }
48 |
49 | ///
50 | /// Gets or sets the amount of memory available to the function in megabytes during execution.
51 | ///
52 | ///
53 | /// To disable enforcement of this limit by the AWS Lambda runtime, set to .
54 | ///
55 | public int FunctionMemorySize { get; set; }
56 |
57 | ///
58 | /// Gets or sets the name of the Lambda function being tested.
59 | ///
60 | public string FunctionName { get; set; }
61 |
62 | ///
63 | /// Gets or sets the function's timeout. The default value is 3 seconds.
64 | ///
65 | ///
66 | /// This limit is not enforced and is only used for reporting into the Lambda context.
67 | ///
68 | public TimeSpan FunctionTimeout { get; set; }
69 |
70 | ///
71 | /// Gets or sets the version of the Lambda function being tested.
72 | ///
73 | public int FunctionVersion { get; set; }
74 |
75 | ///
76 | /// Gets or sets the name of the log group for the Lambda function being tested.
77 | ///
78 | public string LogGroupName { get; set; }
79 |
80 | ///
81 | /// Gets or sets the name of the log stream for the Lambda function being tested.
82 | ///
83 | public string LogStreamName { get; set; }
84 | }
85 |
--------------------------------------------------------------------------------
/src/AwsLambdaTestServer/MartinCostello.Testing.AwsLambdaTestServer.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | Test server for AWS Lambda
4 | Provides an in-memory test server for testing AWS Lambda functions.
5 | true
6 | true
7 | true
8 | Library
9 | MartinCostello.Testing.AwsLambdaTestServer
10 | MartinCostello.Testing.AwsLambdaTestServer
11 | net8.0;net9.0
12 | AWS Lambda Test Server
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/AwsLambdaTestServer/PublicAPI/PublicAPI.Shipped.txt:
--------------------------------------------------------------------------------
1 | #nullable enable
2 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestContext
3 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestContext.Request.get -> MartinCostello.Testing.AwsLambdaTestServer.LambdaTestRequest!
4 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestContext.Response.get -> System.Threading.Channels.ChannelReader!
5 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestRequest
6 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestRequest.AwsRequestId.get -> string!
7 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestRequest.ClientContext.get -> string?
8 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestRequest.ClientContext.set -> void
9 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestRequest.CognitoIdentity.get -> string?
10 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestRequest.CognitoIdentity.set -> void
11 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestRequest.Content.get -> byte[]!
12 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestRequest.LambdaTestRequest(byte[]! content, string? awsRequestId = null) -> void
13 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestResponse
14 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestResponse.Content.get -> byte[]!
15 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestResponse.Duration.get -> System.TimeSpan
16 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestResponse.IsSuccessful.get -> bool
17 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestResponseExtensions
18 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServer
19 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServer.Dispose() -> void
20 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServer.EnqueueAsync(MartinCostello.Testing.AwsLambdaTestServer.LambdaTestRequest! request) -> System.Threading.Tasks.Task!
21 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServer.IsStarted.get -> bool
22 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServer.LambdaTestServer() -> void
23 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServer.LambdaTestServer(MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServerOptions! options) -> void
24 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServer.LambdaTestServer(System.Action! configure) -> void
25 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServer.Options.get -> MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServerOptions!
26 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServer.~LambdaTestServer() -> void
27 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServerExtensions
28 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServerOptions
29 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServerOptions.Configure.get -> System.Action?
30 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServerOptions.Configure.set -> void
31 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServerOptions.DisableMemoryLimitCheck.get -> bool
32 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServerOptions.DisableMemoryLimitCheck.set -> void
33 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServerOptions.FunctionArn.get -> string!
34 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServerOptions.FunctionArn.set -> void
35 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServerOptions.FunctionHandler.get -> string!
36 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServerOptions.FunctionHandler.set -> void
37 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServerOptions.FunctionMemorySize.get -> int
38 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServerOptions.FunctionMemorySize.set -> void
39 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServerOptions.FunctionName.get -> string!
40 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServerOptions.FunctionName.set -> void
41 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServerOptions.FunctionTimeout.get -> System.TimeSpan
42 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServerOptions.FunctionTimeout.set -> void
43 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServerOptions.FunctionVersion.get -> int
44 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServerOptions.FunctionVersion.set -> void
45 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServerOptions.LambdaTestServerOptions() -> void
46 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServerOptions.LogGroupName.get -> string!
47 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServerOptions.LogGroupName.set -> void
48 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServerOptions.LogStreamName.get -> string!
49 | MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServerOptions.LogStreamName.set -> void
50 | static MartinCostello.Testing.AwsLambdaTestServer.LambdaTestResponseExtensions.ReadAsStringAsync(this MartinCostello.Testing.AwsLambdaTestServer.LambdaTestResponse! response) -> System.Threading.Tasks.Task!
51 | static MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServer.ClearLambdaEnvironmentVariables() -> void
52 | static MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServerExtensions.EnqueueAsync(this MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServer! server, byte[]! content) -> System.Threading.Tasks.Task!
53 | static MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServerExtensions.EnqueueAsync(this MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServer! server, string! value) -> System.Threading.Tasks.Task!
54 | virtual MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServer.Configure(Microsoft.AspNetCore.Builder.IApplicationBuilder! app) -> void
55 | virtual MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServer.ConfigureServices(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> void
56 | virtual MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServer.ConfigureWebHost(Microsoft.AspNetCore.Hosting.IWebHostBuilder! builder) -> void
57 | virtual MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServer.CreateClient() -> System.Net.Http.HttpClient!
58 | virtual MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServer.CreateServer(Microsoft.AspNetCore.Hosting.WebHostBuilder! builder) -> Microsoft.AspNetCore.Hosting.Server.IServer!
59 | virtual MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServer.Dispose(bool disposing) -> void
60 | virtual MartinCostello.Testing.AwsLambdaTestServer.LambdaTestServer.StartAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
61 |
--------------------------------------------------------------------------------
/src/AwsLambdaTestServer/PublicAPI/PublicAPI.Unshipped.txt:
--------------------------------------------------------------------------------
1 | #nullable enable
2 |
--------------------------------------------------------------------------------
/src/AwsLambdaTestServer/RuntimeHandler.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using System.Collections.Concurrent;
5 | using System.Diagnostics;
6 | using System.Net.Mime;
7 | using System.Security.Cryptography;
8 | using System.Text;
9 | using System.Threading.Channels;
10 | using Microsoft.AspNetCore.Http;
11 | using Microsoft.Extensions.Logging;
12 |
13 | namespace MartinCostello.Testing.AwsLambdaTestServer;
14 |
15 | ///
16 | /// A class representing a handler for AWS Lambda runtime HTTP requests. This class cannot be inherited.
17 | ///
18 | internal sealed class RuntimeHandler : IDisposable
19 | {
20 | ///
21 | /// The cancellation token that is signalled when request listening should stop. This field is read-only.
22 | ///
23 | private readonly CancellationToken _cancellationToken;
24 |
25 | ///
26 | /// The test server's options. This field is read-only.
27 | ///
28 | private readonly LambdaTestServerOptions _options;
29 |
30 | ///
31 | /// The channel of function requests to process. This field is read-only.
32 | ///
33 | private readonly Channel _requests;
34 |
35 | ///
36 | /// A dictionary containing channels for the responses for enqueued requests. This field is read-only.
37 | ///
38 | private readonly ConcurrentDictionary _responses;
39 |
40 | ///
41 | /// Whether the instance has been disposed.
42 | ///
43 | private bool _disposed;
44 |
45 | ///
46 | /// Initializes a new instance of the class.
47 | ///
48 | /// The test server's options.
49 | /// The cancellation token that is signalled when request listening should stop.
50 | internal RuntimeHandler(LambdaTestServerOptions options, CancellationToken cancellationToken)
51 | {
52 | _cancellationToken = cancellationToken;
53 | _options = options;
54 |
55 | // Support multi-threaded access to the request queue, although the default
56 | // usage scenario would be a single reader and writer from a test method.
57 | var channelOptions = new UnboundedChannelOptions()
58 | {
59 | SingleReader = false,
60 | SingleWriter = false,
61 | };
62 |
63 | _requests = Channel.CreateUnbounded(channelOptions);
64 | _responses = new ConcurrentDictionary(StringComparer.Ordinal);
65 | }
66 |
67 | ///
68 | /// Finalizes an instance of the class.
69 | ///
70 | ~RuntimeHandler() => Dispose(false);
71 |
72 | ///
73 | /// Gets or sets the logger to use.
74 | ///
75 | internal ILogger? Logger { get; set; }
76 |
77 | ///
78 | public void Dispose()
79 | {
80 | Dispose(true);
81 | GC.SuppressFinalize(this);
82 | }
83 |
84 | ///
85 | /// Enqueues a request for the Lambda function to process as an asynchronous operation.
86 | ///
87 | /// The request to invoke the function with.
88 | /// The cancellation token to use when enqueuing the item.
89 | ///
90 | /// A representing the asynchronous operation to enqueue the request
91 | /// which returns a channel reader which completes once the request is processed by the function.
92 | ///
93 | ///
94 | /// A request with the Id specified by is currently in-flight.
95 | ///
96 | internal async Task> EnqueueAsync(
97 | LambdaTestRequest request,
98 | CancellationToken cancellationToken)
99 | {
100 | // There is only one response per request, so the channel is bounded to one item
101 | var channel = Channel.CreateBounded(1);
102 | var context = new ResponseContext(channel);
103 |
104 | if (!_responses.TryAdd(request.AwsRequestId, context))
105 | {
106 | throw new InvalidOperationException($"A request with AWS request Id '{request.AwsRequestId}' is currently in-flight.");
107 | }
108 |
109 | // Enqueue the request for the Lambda runtime to process
110 | await _requests.Writer.WriteAsync(request, cancellationToken).ConfigureAwait(false);
111 |
112 | // Return the reader to the caller to await the function being handled
113 | return channel.Reader;
114 | }
115 |
116 | ///
117 | /// Handles a request for the next invocation for the Lambda function.
118 | ///
119 | /// The HTTP context.
120 | ///
121 | /// A representing the asynchronous operation to get the next invocation request.
122 | ///
123 | internal async Task HandleNextAsync(HttpContext httpContext)
124 | {
125 | Logger?.LogInformation(
126 | "Waiting for new request for Lambda function with ARN {FunctionArn}.",
127 | _options.FunctionArn);
128 |
129 | LambdaTestRequest request;
130 |
131 | try
132 | {
133 | // Additionally cancel the listen loop if the processing is stopped
134 | using var cts = CancellationTokenSource.CreateLinkedTokenSource(httpContext.RequestAborted, _cancellationToken);
135 |
136 | // Wait until there is a request to process
137 | if (!await _requests.Reader.WaitToReadAsync(cts.Token).ConfigureAwait(false))
138 | {
139 | cts.Token.ThrowIfCancellationRequested();
140 | }
141 |
142 | request = await _requests.Reader.ReadAsync(cts.Token).ConfigureAwait(false);
143 | }
144 | catch (Exception ex) when (ex is OperationCanceledException or ChannelClosedException)
145 | {
146 | Logger?.LogInformation(
147 | ex,
148 | "Stopped listening for additional requests for Lambda function with ARN {FunctionArn}.",
149 | _options.FunctionArn);
150 |
151 | // Throw back into LambdaBootstrap, which will then stop processing.
152 | // See https://github.com/aws/aws-lambda-dotnet/pull/540 for details of the change.
153 | throw;
154 | }
155 |
156 | // Write the response for the Lambda runtime to pass to the function to invoke
157 | string traceId = GenerateTraceId();
158 |
159 | Logger?.LogInformation(
160 | "Invoking Lambda function with ARN {FunctionArn} for request Id {AwsRequestId} and trace Id {AwsTraceId}.",
161 | _options.FunctionArn,
162 | request.AwsRequestId,
163 | traceId);
164 |
165 | // These headers are required, as otherwise an exception is thrown
166 | httpContext.Response.Headers["Lambda-Runtime-Aws-Request-Id"] = request.AwsRequestId;
167 | httpContext.Response.Headers["Lambda-Runtime-Invoked-Function-Arn"] = _options.FunctionArn;
168 |
169 | // These headers are optional
170 | httpContext.Response.Headers["Lambda-Runtime-Trace-Id"] = traceId;
171 |
172 | if (request.ClientContext != null)
173 | {
174 | httpContext.Response.Headers["Lambda-Runtime-Client-Context"] = request.ClientContext;
175 | }
176 |
177 | if (request.CognitoIdentity != null)
178 | {
179 | httpContext.Response.Headers["Lambda-Runtime-Cognito-Identity"] = request.CognitoIdentity;
180 | }
181 |
182 | var deadline = DateTimeOffset.UtcNow.Add(_options.FunctionTimeout).ToUnixTimeMilliseconds();
183 |
184 | // Record the current time for the response to have the duration measured
185 | _responses[request.AwsRequestId].DurationTimer = Stopwatch.StartNew();
186 |
187 | httpContext.Response.Headers["Lambda-Runtime-Deadline-Ms"] = deadline.ToString("F0", CultureInfo.InvariantCulture);
188 |
189 | httpContext.Response.ContentType = MediaTypeNames.Application.Json;
190 | httpContext.Response.StatusCode = StatusCodes.Status200OK;
191 |
192 | await httpContext.Response.BodyWriter.WriteAsync(request.Content, httpContext.RequestAborted).ConfigureAwait(false);
193 | }
194 |
195 | ///
196 | /// Handles an successful response for an invocation of the Lambda function.
197 | ///
198 | /// The HTTP context.
199 | ///
200 | /// A representing the asynchronous operation to handle the response.
201 | ///
202 | internal async Task HandleResponseAsync(HttpContext httpContext)
203 | {
204 | string? awsRequestId = httpContext.Request.RouteValues["AwsRequestId"] as string;
205 |
206 | byte[] content = await ReadContentAsync(httpContext, httpContext.RequestAborted).ConfigureAwait(false);
207 |
208 | Logger?.LogInformation(
209 | "Invoked Lambda function with ARN {FunctionArn} for request Id {AwsRequestId}: {ResponseContent}.",
210 | _options.FunctionArn,
211 | awsRequestId,
212 | ToString(content));
213 |
214 | await CompleteRequestChannelAsync(
215 | awsRequestId!,
216 | content,
217 | isSuccessful: true,
218 | httpContext.RequestAborted).ConfigureAwait(false);
219 |
220 | httpContext.Response.StatusCode = StatusCodes.Status204NoContent;
221 | }
222 |
223 | ///
224 | /// Handles an error response for an invocation of the Lambda function.
225 | ///
226 | /// The HTTP context.
227 | ///
228 | /// A representing the asynchronous operation to handle the response.
229 | ///
230 | internal async Task HandleInvocationErrorAsync(HttpContext httpContext)
231 | {
232 | string? awsRequestId = httpContext.Request.RouteValues["AwsRequestId"] as string;
233 |
234 | byte[] content = await ReadContentAsync(httpContext, httpContext.RequestAborted).ConfigureAwait(false);
235 |
236 | Logger?.LogError(
237 | "Error invoking Lambda function with ARN {FunctionArn} for request Id {AwsRequestId}: {ErrorContent}",
238 | _options.FunctionArn,
239 | awsRequestId,
240 | ToString(content));
241 |
242 | await CompleteRequestChannelAsync(
243 | awsRequestId!,
244 | content,
245 | isSuccessful: false,
246 | httpContext.RequestAborted).ConfigureAwait(false);
247 |
248 | httpContext.Response.StatusCode = StatusCodes.Status204NoContent;
249 | }
250 |
251 | ///
252 | /// Handles an error response for the failed initialization of the Lambda function.
253 | ///
254 | /// The HTTP context.
255 | ///
256 | /// A representing the asynchronous operation to handle the response.
257 | ///
258 | internal async Task HandleInitializationErrorAsync(HttpContext httpContext)
259 | {
260 | byte[] content = await ReadContentAsync(httpContext, httpContext.RequestAborted).ConfigureAwait(false);
261 |
262 | Logger?.LogError(
263 | "Error initializing Lambda function with ARN {FunctionArn}: {ErrorContent}",
264 | _options.FunctionArn,
265 | ToString(content));
266 |
267 | httpContext.Response.StatusCode = StatusCodes.Status204NoContent;
268 | }
269 |
270 | ///
271 | /// Generates a valid trace identifier for AWS X-Ray.
272 | ///
273 | ///
274 | /// The generated trace identifier.
275 | ///
276 | ///
277 | /// See https://docs.aws.amazon.com/xray/latest/devguide/xray-api-sendingdata.html#xray-api-traceids.
278 | ///
279 | private static string GenerateTraceId()
280 | {
281 | var epoch = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
282 | var buffer = RandomNumberGenerator.GetBytes(96 / 8);
283 | #if NET9_0_OR_GREATER
284 | var identifier = Convert.ToHexStringLower(buffer);
285 | #else
286 | #pragma warning disable CA1308
287 | var identifier = Convert.ToHexString(buffer).ToLowerInvariant();
288 | #pragma warning restore CA1308
289 | #endif
290 |
291 | return FormattableString.Invariant($"Root=1-{epoch:x8}-{identifier}");
292 | }
293 |
294 | ///
295 | /// Reads the HTTP request body as an asynchronous operation.
296 | ///
297 | /// The HTTP request to read the body from.
298 | /// The cancellation token to use.
299 | ///
300 | /// A representing the asynchronous operation to read the
301 | /// request body that returns a byte array containing the request content.
302 | ///
303 | private static async Task ReadContentAsync(HttpContext httpContext, CancellationToken cancellationToken)
304 | {
305 | using var stream = new MemoryStream();
306 |
307 | await httpContext.Request.BodyReader.CopyToAsync(stream, cancellationToken).ConfigureAwait(false);
308 |
309 | return stream.ToArray();
310 | }
311 |
312 | ///
313 | /// Converts the specified byte array to a string.
314 | ///
315 | /// The array to convert to a string.
316 | ///
317 | /// The UTF-8 representation of .
318 | ///
319 | private static string ToString(byte[] content) => Encoding.UTF8.GetString(content);
320 |
321 | ///
322 | /// Completes the request channel for the specified request.
323 | ///
324 | /// The AWS request Id to complete the response for.
325 | /// The raw content associated with the request's response.
326 | /// Whether the response indicates the request was successfully handled.
327 | /// The cancellation token to use.
328 | ///
329 | /// A representing the asynchronous operation.
330 | ///
331 | private async Task CompleteRequestChannelAsync(
332 | string awsRequestId,
333 | byte[] content,
334 | bool isSuccessful,
335 | CancellationToken cancellationToken)
336 | {
337 | if (!_responses.TryRemove(awsRequestId, out var context))
338 | {
339 | throw new InvalidOperationException($"Failed to complete channel for AWS request Id {awsRequestId}.");
340 | }
341 |
342 | context.DurationTimer!.Stop();
343 |
344 | Logger?.LogInformation(
345 | "Completed processing AWS request Id {AwsRequestId} for Lambda function with ARN {FunctionArn} in {FunctionDuration} milliseconds.",
346 | awsRequestId,
347 | _options.FunctionArn,
348 | context.DurationTimer.ElapsedMilliseconds);
349 |
350 | // Make the response available to read by the enqueuer
351 | var response = new LambdaTestResponse(content, isSuccessful, context.DurationTimer.Elapsed);
352 | await context.Channel.Writer.WriteAsync(response, cancellationToken).ConfigureAwait(false);
353 |
354 | // Mark the channel as complete as there will be no more responses written
355 | context.Channel.Writer.Complete();
356 | }
357 |
358 | private void Dispose(bool disposing)
359 | {
360 | if (!_disposed)
361 | {
362 | if (disposing)
363 | {
364 | // Complete the channels so that any callers who do not know
365 | // the server has been disposed do not wait indefinitely for a
366 | // channel to complete that will now never be written to again.
367 | _requests.Writer.TryComplete();
368 |
369 | var contexts = _responses.Values;
370 | _responses.Clear();
371 |
372 | foreach (var context in contexts)
373 | {
374 | context.Channel.Writer.TryComplete();
375 | }
376 | }
377 |
378 | _disposed = true;
379 | }
380 | }
381 |
382 | private sealed class ResponseContext
383 | {
384 | internal ResponseContext(Channel channel)
385 | {
386 | Channel = channel;
387 | }
388 |
389 | internal Channel Channel { get; }
390 |
391 | internal Stopwatch? DurationTimer { get; set; }
392 | }
393 | }
394 |
--------------------------------------------------------------------------------
/stylecop.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
3 | "settings": {
4 | "documentationRules": {
5 | "companyName": "https://github.com/martincostello/lambda-test-server",
6 | "copyrightText": "Copyright (c) {ownerName}, {year}. All rights reserved.\nLicensed under the {licenseName} license. See the {licenseFile} file in the project root for full license information.",
7 | "documentExposedElements": true,
8 | "documentInterfaces": true,
9 | "documentInternalElements": true,
10 | "documentPrivateElements": false,
11 | "documentPrivateFields": false,
12 | "fileNamingConvention": "metadata",
13 | "xmlHeader": false,
14 | "variables": {
15 | "licenseFile": "LICENSE",
16 | "licenseName": "Apache 2.0",
17 | "ownerName": "Martin Costello",
18 | "year": "2019"
19 | }
20 | },
21 | "layoutRules": {
22 | "newlineAtEndOfFile": "require"
23 | },
24 | "maintainabilityRules": {},
25 | "namingRules": {
26 | "allowCommonHungarianPrefixes": true,
27 | "allowedHungarianPrefixes": []
28 | },
29 | "orderingRules": {
30 | "elementOrder": [
31 | "kind",
32 | "accessibility",
33 | "constant",
34 | "static",
35 | "readonly"
36 | ],
37 | "systemUsingDirectivesFirst": true,
38 | "usingDirectivesPlacement": "outsideNamespace"
39 | },
40 | "readabilityRules": {},
41 | "spacingRules": {}
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/tests/AwsLambdaTestServer.Tests/AssemblyFixture.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using System.Diagnostics;
5 | using MartinCostello.Testing.AwsLambdaTestServer;
6 |
7 | [assembly: AssemblyFixture(typeof(AssemblyFixture))]
8 |
9 | namespace MartinCostello.Testing.AwsLambdaTestServer;
10 |
11 | public sealed class AssemblyFixture
12 | {
13 | // Read the default memory limits before any of the tests execute any code that may change it.
14 | // The cast to ulong is required for the setting to be respected by the runtime.
15 | // See https://github.com/aws/aws-lambda-dotnet/pull/1595#issuecomment-1771747410.
16 | private static readonly ulong DefaultMemory = (ulong)GC.GetGCMemoryInfo().TotalAvailableMemoryBytes;
17 |
18 | public AssemblyFixture()
19 | {
20 | ResetMemoryLimits();
21 | }
22 |
23 | public static void ResetMemoryLimits()
24 | {
25 | Debug.Assert(DefaultMemory != 134217728, "The default value of TotalAvailableMemoryBytes should not be 128MB.");
26 |
27 | // Undo any changes that Amazon.Lambda.RuntimeSupport makes internally
28 | AppContext.SetData("GCHeapHardLimit", DefaultMemory);
29 | GC.RefreshMemoryLimit();
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/tests/AwsLambdaTestServer.Tests/AwsIntegrationTests.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using Amazon;
5 | using Amazon.Lambda.Core;
6 | using Amazon.Lambda.RuntimeSupport;
7 | using Amazon.Runtime;
8 | using Amazon.SQS;
9 | using Amazon.SQS.Model;
10 |
11 | namespace MartinCostello.Testing.AwsLambdaTestServer;
12 |
13 | public static class AwsIntegrationTests
14 | {
15 | [Fact]
16 | public static async Task Runtime_Generates_Valid_Aws_Trace_Id()
17 | {
18 | // Arrange
19 | Assert.SkipWhen(GetAwsCredentials() is null, "No AWS credentials are configured.");
20 |
21 | using var server = new LambdaTestServer();
22 | using var cancellationTokenSource = new CancellationTokenSource();
23 |
24 | await server.StartAsync(cancellationTokenSource.Token);
25 |
26 | cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(5));
27 |
28 | var request = new QueueExistsRequest()
29 | {
30 | QueueName = Guid.NewGuid().ToString(),
31 | };
32 |
33 | var json = System.Text.Json.JsonSerializer.Serialize(request);
34 | var context = await server.EnqueueAsync(json);
35 |
36 | _ = Task.Run(
37 | async () =>
38 | {
39 | await context.Response.WaitToReadAsync(cancellationTokenSource.Token);
40 |
41 | if (!cancellationTokenSource.IsCancellationRequested)
42 | {
43 | await cancellationTokenSource.CancelAsync();
44 | }
45 | },
46 | cancellationTokenSource.Token);
47 |
48 | using var httpClient = server.CreateClient();
49 |
50 | // Act
51 | await RunAsync(httpClient, cancellationTokenSource.Token);
52 |
53 | // Assert
54 | context.Response.TryRead(out var response).ShouldBeTrue();
55 | response.ShouldNotBeNull();
56 | response.IsSuccessful.ShouldBeTrue();
57 | }
58 |
59 | private static async Task RunAsync(HttpClient? httpClient, CancellationToken cancellationToken)
60 | {
61 | var serializer = new Amazon.Lambda.Serialization.Json.JsonSerializer();
62 |
63 | using var handlerWrapper = HandlerWrapper.GetHandlerWrapper(SqsFunction.QueueExistsAsync, serializer);
64 | using var bootstrap = new LambdaBootstrap(httpClient, handlerWrapper, SqsFunction.InitializeAsync);
65 |
66 | await bootstrap.RunAsync(cancellationToken);
67 | }
68 |
69 | private static AWSCredentials? GetAwsCredentials()
70 | {
71 | try
72 | {
73 | return new EnvironmentVariablesAWSCredentials();
74 | }
75 | catch (InvalidOperationException)
76 | {
77 | // Not configured
78 | }
79 |
80 | try
81 | {
82 | return AssumeRoleWithWebIdentityCredentials.FromEnvironmentVariables();
83 | }
84 | catch (ArgumentException)
85 | {
86 | return null;
87 | }
88 | }
89 |
90 | private static class SqsFunction
91 | {
92 | public static Task InitializeAsync() => Task.FromResult(true);
93 |
94 | public static async Task QueueExistsAsync(QueueExistsRequest request, ILambdaContext context)
95 | {
96 | context.Logger.LogLine($"Handling AWS request Id {context.AwsRequestId} to check if SQS queue ${request.QueueName} exists.");
97 |
98 | var credentials = GetAwsCredentials();
99 | var region = RegionEndpoint.EUWest2;
100 |
101 | bool exists;
102 |
103 | try
104 | {
105 | using var client = new AmazonSQSClient(credentials, region);
106 | _ = await client.GetQueueUrlAsync(request.QueueName);
107 |
108 | exists = true;
109 | }
110 | catch (QueueDoesNotExistException)
111 | {
112 | exists = false;
113 | }
114 |
115 | context.Logger.LogLine($"SQS queue ${request.QueueName} {(exists ? "exists" : "does not exist")}.");
116 |
117 | return exists;
118 | }
119 | }
120 |
121 | private sealed class QueueExistsRequest
122 | {
123 | public string QueueName { get; set; } = string.Empty;
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/tests/AwsLambdaTestServer.Tests/Examples.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using System.Text.Json;
5 |
6 | namespace MartinCostello.Testing.AwsLambdaTestServer;
7 |
8 | ///
9 | /// Examples for using MartinCostello.Testing.AwsLambdaTestServer.
10 | ///
11 | public static class Examples
12 | {
13 | [Fact]
14 | public static async Task Function_Can_Process_Request()
15 | {
16 | // Arrange - Create a test server for the Lambda runtime to use
17 | using var server = new LambdaTestServer();
18 |
19 | // Create a cancellation token that stops the server listening for new requests.
20 | // Auto-cancel the server after 2 seconds in case something goes wrong and the request is not handled.
21 | using var cancellationTokenSource = new CancellationTokenSource();
22 |
23 | // Start the test server so it is ready to listen for requests from the Lambda runtime
24 | await server.StartAsync(cancellationTokenSource.Token);
25 |
26 | // Now that the server has started, cancel it after 2 seconds if no requests are processed
27 | cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(2));
28 |
29 | // Create a test request for the Lambda function being tested
30 | var value = new MyRequest()
31 | {
32 | Values = [1, 2, 3], // The function returns the sum of the specified numbers
33 | };
34 |
35 | string requestJson = JsonSerializer.Serialize(value);
36 |
37 | // Queue the request with the server to invoke the Lambda function and
38 | // store the ChannelReader into a variable to use to read the response.
39 | LambdaTestContext context = await server.EnqueueAsync(requestJson);
40 |
41 | // Queue a task to stop the test server from listening as soon as the response is available
42 | _ = Task.Run(
43 | async () =>
44 | {
45 | await context.Response.WaitToReadAsync(cancellationTokenSource.Token);
46 |
47 | if (!cancellationTokenSource.IsCancellationRequested)
48 | {
49 | await cancellationTokenSource.CancelAsync();
50 | }
51 | },
52 | cancellationTokenSource.Token);
53 |
54 | // Create an HttpClient for the Lambda to use with LambdaBootstrap
55 | using var httpClient = server.CreateClient();
56 |
57 | // Act - Start the Lambda runtime and run until the cancellation token is signalled
58 | await MyFunctionEntrypoint.RunAsync(httpClient, cancellationTokenSource.Token);
59 |
60 | // Assert - The channel reader should have the response available
61 | context.Response.TryRead(out LambdaTestResponse? response).ShouldBeTrue("No Lambda response is available.");
62 |
63 | response!.IsSuccessful.ShouldBeTrue("The Lambda function failed to handle the request.");
64 | response.Content.ShouldNotBeEmpty("The Lambda function did not return any content.");
65 |
66 | string responseJson = await response.ReadAsStringAsync();
67 | var actual = JsonSerializer.Deserialize(responseJson);
68 |
69 | actual.ShouldNotBeNull();
70 | actual.Sum.ShouldBe(6, "The Lambda function returned an incorrect response.");
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/tests/AwsLambdaTestServer.Tests/FunctionRunner.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using Amazon.Lambda.RuntimeSupport;
5 | using Amazon.Lambda.Serialization.Json;
6 |
7 | namespace MartinCostello.Testing.AwsLambdaTestServer;
8 |
9 | internal static class FunctionRunner
10 | {
11 | internal static async Task RunAsync(HttpClient? httpClient, CancellationToken cancellationToken)
12 | where T : MyHandler, new()
13 | {
14 | var handler = new T();
15 | var serializer = new JsonSerializer();
16 |
17 | using var handlerWrapper = HandlerWrapper.GetHandlerWrapper(handler.SumAsync, serializer);
18 | using var bootstrap = new LambdaBootstrap(httpClient, handlerWrapper, handler.InitializeAsync);
19 |
20 | await bootstrap.RunAsync(cancellationToken);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/tests/AwsLambdaTestServer.Tests/HttpLambdaTestServer.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using MartinCostello.Logging.XUnit;
5 | using Microsoft.AspNetCore.Hosting;
6 | using Microsoft.AspNetCore.Hosting.Server;
7 | using Microsoft.Extensions.DependencyInjection;
8 | using Microsoft.Extensions.Logging;
9 |
10 | namespace MartinCostello.Testing.AwsLambdaTestServer;
11 |
12 | internal sealed class HttpLambdaTestServer : LambdaTestServer, IAsyncLifetime, ITestOutputHelperAccessor
13 | {
14 | private readonly CancellationTokenSource _cts = new();
15 | private bool _disposed;
16 | private IWebHost? _webHost;
17 |
18 | public HttpLambdaTestServer()
19 | : base()
20 | {
21 | }
22 |
23 | public HttpLambdaTestServer(Action configure)
24 | : base(configure)
25 | {
26 | }
27 |
28 | public ITestOutputHelper? OutputHelper { get; set; }
29 |
30 | public async ValueTask DisposeAsync()
31 | {
32 | if (_webHost is not null)
33 | {
34 | await _webHost.StopAsync();
35 | }
36 |
37 | Dispose();
38 | }
39 |
40 | public async ValueTask InitializeAsync()
41 | {
42 | Options.Configure = (services) =>
43 | services.AddLogging((builder) => builder.AddXUnit(this));
44 |
45 | await StartAsync(_cts.Token);
46 | }
47 |
48 | protected override IServer CreateServer(WebHostBuilder builder)
49 | {
50 | _webHost = builder
51 | .UseKestrel()
52 | .ConfigureServices((services) => services.AddLogging((builder) => builder.AddXUnit(this)))
53 | .UseUrls("http://127.0.0.1:0")
54 | .Build();
55 |
56 | _webHost.Start();
57 |
58 | return _webHost.Services.GetRequiredService();
59 | }
60 |
61 | protected override void Dispose(bool disposing)
62 | {
63 | if (!_disposed)
64 | {
65 | if (disposing)
66 | {
67 | _webHost?.Dispose();
68 |
69 | _cts.Cancel();
70 | _cts.Dispose();
71 | }
72 |
73 | _disposed = true;
74 | }
75 |
76 | base.Dispose(disposing);
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/tests/AwsLambdaTestServer.Tests/HttpLambdaTestServerTests.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using System.Text;
5 | using MartinCostello.Logging.XUnit;
6 | using Microsoft.Extensions.DependencyInjection;
7 | using Microsoft.Extensions.Logging;
8 |
9 | namespace MartinCostello.Testing.AwsLambdaTestServer;
10 |
11 | #pragma warning disable JSON002
12 |
13 | [Collection]
14 | public class HttpLambdaTestServerTests(ITestOutputHelper outputHelper) : ITestOutputHelperAccessor
15 | {
16 | public ITestOutputHelper? OutputHelper { get; set; } = outputHelper;
17 |
18 | [Fact]
19 | public async Task Function_Can_Process_Request()
20 | {
21 | // Arrange
22 | void Configure(IServiceCollection services)
23 | => services.AddLogging((builder) => builder.AddXUnit(this));
24 |
25 | using var server = new HttpLambdaTestServer(Configure);
26 | using var cts = new CancellationTokenSource();
27 |
28 | await server.StartAsync(cts.Token);
29 |
30 | cts.CancelAfter(TimeSpan.FromSeconds(2));
31 |
32 | var context = await server.EnqueueAsync("""{"Values": [ 1, 2, 3 ]}""");
33 |
34 | _ = Task.Run(
35 | async () =>
36 | {
37 | await context.Response.WaitToReadAsync(cts.Token);
38 |
39 | if (!cts.IsCancellationRequested)
40 | {
41 | await cts.CancelAsync();
42 | }
43 | },
44 | cts.Token);
45 |
46 | using var httpClient = server.CreateClient();
47 |
48 | // Act
49 | await MyFunctionEntrypoint.RunAsync(httpClient, cts.Token);
50 |
51 | // Assert
52 | context.Response.TryRead(out var response).ShouldBeTrue();
53 |
54 | response.ShouldNotBeNull();
55 | response!.IsSuccessful.ShouldBeTrue();
56 | response.Content.ShouldNotBeNull();
57 | response.Duration.ShouldBeGreaterThan(TimeSpan.Zero);
58 | Encoding.UTF8.GetString(response.Content).ShouldBe("""{"Sum":6}""");
59 | }
60 |
61 | [Fact]
62 | public async Task Function_Can_Handle_Failed_Request()
63 | {
64 | // Arrange
65 | void Configure(IServiceCollection services)
66 | => services.AddLogging((builder) => builder.AddXUnit(this));
67 |
68 | using var server = new LambdaTestServer(Configure);
69 | using var cts = new CancellationTokenSource();
70 |
71 | await server.StartAsync(cts.Token);
72 |
73 | cts.CancelAfter(TimeSpan.FromSeconds(2));
74 |
75 | var context = await server.EnqueueAsync("""{"Values": null}""");
76 |
77 | _ = Task.Run(
78 | async () =>
79 | {
80 | await context.Response.WaitToReadAsync(cts.Token);
81 |
82 | if (!cts.IsCancellationRequested)
83 | {
84 | await cts.CancelAsync();
85 | }
86 | },
87 | cts.Token);
88 |
89 | using var httpClient = server.CreateClient();
90 |
91 | // Act
92 | await MyFunctionEntrypoint.RunAsync(httpClient, cts.Token);
93 |
94 | // Assert
95 | context.Response.TryRead(out var response).ShouldBeTrue();
96 |
97 | response.ShouldNotBeNull();
98 | response!.IsSuccessful.ShouldBeFalse();
99 | response.Content.ShouldNotBeNull();
100 | }
101 |
102 | [Fact]
103 | public async Task Function_Can_Process_Multiple_Requests()
104 | {
105 | // Arrange
106 | void Configure(IServiceCollection services)
107 | => services.AddLogging((builder) => builder.AddXUnit(this));
108 |
109 | using var server = new LambdaTestServer(Configure);
110 | using var cts = new CancellationTokenSource();
111 |
112 | await server.StartAsync(cts.Token);
113 |
114 | cts.CancelAfter(TimeSpan.FromSeconds(2));
115 |
116 | var channels = new List<(int Expected, LambdaTestContext Context)>();
117 |
118 | for (int i = 0; i < 10; i++)
119 | {
120 | var request = new MyRequest()
121 | {
122 | Values = [.. Enumerable.Range(1, i + 1)],
123 | };
124 |
125 | channels.Add((request.Values.Sum(), await server.EnqueueAsync(request)));
126 | }
127 |
128 | _ = Task.Run(
129 | async () =>
130 | {
131 | foreach ((var _, var context) in channels)
132 | {
133 | await context.Response.WaitToReadAsync(cts.Token);
134 | }
135 |
136 | if (!cts.IsCancellationRequested)
137 | {
138 | await cts.CancelAsync();
139 | }
140 | },
141 | cts.Token);
142 |
143 | using var httpClient = server.CreateClient();
144 |
145 | // Act
146 | await MyFunctionEntrypoint.RunAsync(httpClient, cts.Token);
147 |
148 | // Assert
149 | foreach ((int expected, var context) in channels)
150 | {
151 | context.Response.TryRead(out var response).ShouldBeTrue();
152 |
153 | response.ShouldNotBeNull();
154 | response!.IsSuccessful.ShouldBeTrue();
155 | response.Content.ShouldNotBeNull();
156 |
157 | var deserialized = response.ReadAs();
158 | deserialized.Sum.ShouldBe(expected);
159 | }
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/tests/AwsLambdaTestServer.Tests/LambdaTestRequestTests.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace MartinCostello.Testing.AwsLambdaTestServer;
5 |
6 | public static class LambdaTestRequestTests
7 | {
8 | [Fact]
9 | public static void Constructor_Throws_If_Content_Null()
10 | {
11 | // Arrange
12 | byte[] content = null!;
13 |
14 | // Act and Assert
15 | Assert.Throws("content", () => new LambdaTestRequest(content));
16 | }
17 |
18 | [Fact]
19 | public static void Constructor_Sets_Properties()
20 | {
21 | // Arrange
22 | byte[] content = [1];
23 |
24 | // Act
25 | var actual = new LambdaTestRequest(content);
26 |
27 | // Assert
28 | actual.Content.ShouldBeSameAs(content);
29 | actual.AwsRequestId.ShouldNotBeNullOrEmpty();
30 | Guid.TryParse(actual.AwsRequestId, out var requestId).ShouldBeTrue();
31 | requestId.ShouldNotBe(Guid.Empty);
32 |
33 | // Arrange
34 | string awsRequestId = "my-request-id";
35 |
36 | // Act
37 | actual = new LambdaTestRequest(content, awsRequestId);
38 |
39 | // Assert
40 | actual.Content.ShouldBeSameAs(content);
41 | actual.AwsRequestId.ShouldBe(awsRequestId);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/tests/AwsLambdaTestServer.Tests/LambdaTestResponseExtensionsTests.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace MartinCostello.Testing.AwsLambdaTestServer;
5 |
6 | public static class LambdaTestResponseExtensionsTests
7 | {
8 | [Fact]
9 | public static async Task EnqueueAsync_Validates_Parameters()
10 | {
11 | // Arrange
12 | LambdaTestResponse response = null!;
13 |
14 | // Act
15 | await Assert.ThrowsAsync("response", response.ReadAsStringAsync);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/tests/AwsLambdaTestServer.Tests/LambdaTestServerCollection.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace MartinCostello.Testing.AwsLambdaTestServer;
5 |
6 | [CollectionDefinition(nameof(LambdaTestServerCollection), DisableParallelization = true)]
7 | public class LambdaTestServerCollection;
8 |
--------------------------------------------------------------------------------
/tests/AwsLambdaTestServer.Tests/LambdaTestServerExtensionsTests.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace MartinCostello.Testing.AwsLambdaTestServer;
5 |
6 | public static class LambdaTestServerExtensionsTests
7 | {
8 | [Fact]
9 | public static async Task EnqueueAsync_Validates_Parameters()
10 | {
11 | // Arrange
12 | using var server = new LambdaTestServer();
13 | using LambdaTestServer nullServer = null!;
14 | byte[] content = null!;
15 | string value = null!;
16 |
17 | byte[] emptyBytes = [];
18 |
19 | // Act
20 | await Assert.ThrowsAsync("content", () => server.EnqueueAsync(content));
21 | await Assert.ThrowsAsync("value", () => server.EnqueueAsync(value));
22 | await Assert.ThrowsAsync("server", () => nullServer.EnqueueAsync(emptyBytes));
23 | await Assert.ThrowsAsync("server", () => nullServer.EnqueueAsync(string.Empty));
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/tests/AwsLambdaTestServer.Tests/LambdaTestServerOptionsTests.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace MartinCostello.Testing.AwsLambdaTestServer;
5 |
6 | public static class LambdaTestServerOptionsTests
7 | {
8 | [Fact]
9 | public static void Constructor_Initializes_Defaults()
10 | {
11 | // Act
12 | var actual = new LambdaTestServerOptions();
13 |
14 | // Assert
15 | actual.Configure.ShouldBeNull();
16 | actual.FunctionArn.ShouldNotBeNullOrEmpty();
17 | actual.FunctionHandler.ShouldBe(string.Empty);
18 | actual.FunctionMemorySize.ShouldBe(128);
19 | actual.FunctionTimeout.ShouldBe(TimeSpan.FromSeconds(3));
20 | actual.FunctionVersion.ShouldBe(1);
21 | actual.LogGroupName.ShouldNotBeNullOrEmpty();
22 | actual.LogStreamName.ShouldNotBeNullOrEmpty();
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tests/AwsLambdaTestServer.Tests/MartinCostello.Testing.AwsLambdaTestServer.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | Tests for MartinCostello.Testing.AwsLambdaTestServer.
4 | Exe
5 | MartinCostello.Testing.AwsLambdaTestServer
6 | net9.0
7 | true
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | true
29 | 84
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/tests/AwsLambdaTestServer.Tests/MyFunctionEntrypoint.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace MartinCostello.Testing.AwsLambdaTestServer;
5 |
6 | internal static class MyFunctionEntrypoint
7 | {
8 | internal static async Task RunAsync(
9 | HttpClient? httpClient = null,
10 | CancellationToken cancellationToken = default)
11 | {
12 | await FunctionRunner.RunAsync(httpClient, cancellationToken);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/tests/AwsLambdaTestServer.Tests/MyHandler.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using Amazon.Lambda.Core;
5 |
6 | namespace MartinCostello.Testing.AwsLambdaTestServer;
7 |
8 | internal class MyHandler
9 | {
10 | public virtual Task InitializeAsync()
11 | => Task.FromResult(true);
12 |
13 | public virtual Task SumAsync(MyRequest request, ILambdaContext context)
14 | {
15 | context.Logger.LogLine($"Handling AWS request Id {context.AwsRequestId}.");
16 |
17 | var response = new MyResponse()
18 | {
19 | Sum = request.Values!.Sum(),
20 | };
21 |
22 | context.Logger.LogLine($"The sum of the {request.Values?.Count} values is {response.Sum}.");
23 |
24 | return Task.FromResult(response);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/tests/AwsLambdaTestServer.Tests/MyRequest.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace MartinCostello.Testing.AwsLambdaTestServer;
5 |
6 | internal sealed class MyRequest
7 | {
8 | public ICollection? Values { get; set; }
9 | }
10 |
--------------------------------------------------------------------------------
/tests/AwsLambdaTestServer.Tests/MyResponse.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | namespace MartinCostello.Testing.AwsLambdaTestServer;
5 |
6 | internal sealed class MyResponse
7 | {
8 | public int Sum { get; set; }
9 | }
10 |
--------------------------------------------------------------------------------
/tests/AwsLambdaTestServer.Tests/ParallelismTests.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using System.Collections.Concurrent;
5 | using MyFunctions;
6 |
7 | namespace MartinCostello.Testing.AwsLambdaTestServer;
8 |
9 | public static class ParallelismTests
10 | {
11 | [Fact(Timeout = 30_000)]
12 | public static async Task Function_Can_Process_Multiple_Requests_On_Different_Threads()
13 | {
14 | // Arrange
15 | int messageCount = 1_000;
16 | int expected = Enumerable.Range(0, messageCount).Sum();
17 |
18 | using var server = new LambdaTestServer();
19 | using var cts = new CancellationTokenSource();
20 |
21 | await server.StartAsync(cts.Token);
22 |
23 | using var httpClient = server.CreateClient();
24 |
25 | // Enqueue the requests to process in the background
26 | var addTask = EnqueueInParallel(messageCount, server);
27 |
28 | // Start a task to consume the responses in the background
29 | var processTask = Assert(addTask, messageCount, cts);
30 |
31 | // Act - Start the function processing
32 | await ReverseFunction.RunAsync(httpClient, cts.Token);
33 |
34 | // Assert
35 | int actual = await processTask.Task;
36 | actual.ShouldBe(expected);
37 | }
38 |
39 | private static async Task> EnqueueInParallel(
40 | int count,
41 | LambdaTestServer server)
42 | {
43 | var collection = new ConcurrentBag();
44 |
45 | await Task.Yield();
46 | await Parallel.ForAsync(0, count, async (i, _) => collection.Add(await server.EnqueueAsync(new[] { i })));
47 |
48 | return collection;
49 | }
50 |
51 | private static TaskCompletionSource Assert(
52 | Task> addTask,
53 | int messages,
54 | CancellationTokenSource cts)
55 | {
56 | var completionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
57 |
58 | _ = Task.Run(async () =>
59 | {
60 | var collection = await addTask;
61 | collection.Count.ShouldBe(messages);
62 |
63 | int actual = 0;
64 |
65 | foreach (var context in collection)
66 | {
67 | await context.Response.WaitToReadAsync();
68 |
69 | var result = await context.Response.ReadAsync();
70 |
71 | var response = result.ReadAs();
72 |
73 | actual += response[0];
74 | }
75 |
76 | completionSource.SetResult(actual);
77 | await cts.CancelAsync();
78 | });
79 |
80 | return completionSource;
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/tests/AwsLambdaTestServer.Tests/ReverseFunction.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using Amazon.Lambda.RuntimeSupport;
5 | using Amazon.Lambda.Serialization.Json;
6 |
7 | #pragma warning disable IDE0130
8 | namespace MyFunctions;
9 |
10 | public static class ReverseFunction
11 | {
12 | public static async Task RunAsync(
13 | HttpClient? httpClient = null,
14 | CancellationToken cancellationToken = default)
15 | {
16 | var serializer = new JsonSerializer();
17 |
18 | #pragma warning disable CA2000
19 | using var handlerWrapper = HandlerWrapper.GetHandlerWrapper(ReverseAsync, serializer);
20 | using var bootstrap = new LambdaBootstrap(httpClient ?? new HttpClient(), handlerWrapper);
21 | #pragma warning restore CA2000
22 |
23 | await bootstrap.RunAsync(cancellationToken);
24 | }
25 |
26 | public static Task ReverseAsync(int[] values)
27 | {
28 | return Task.FromResult(values.Reverse().ToArray());
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/tests/AwsLambdaTestServer.Tests/ReverseFunctionTests.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using System.Text.Json;
5 | using MartinCostello.Testing.AwsLambdaTestServer;
6 |
7 | #pragma warning disable IDE0130
8 | namespace MyFunctions;
9 |
10 | public static class ReverseFunctionTests
11 | {
12 | [Fact]
13 | public static async Task Function_Reverses_Numbers()
14 | {
15 | // Arrange
16 | using var server = new LambdaTestServer();
17 | using var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(2));
18 |
19 | await server.StartAsync(cancellationTokenSource.Token);
20 |
21 | int[] value = [1, 2, 3];
22 | byte[] json = JsonSerializer.SerializeToUtf8Bytes(value);
23 |
24 | LambdaTestContext context = await server.EnqueueAsync(json);
25 |
26 | using var httpClient = server.CreateClient();
27 |
28 | // Act
29 | await ReverseFunction.RunAsync(httpClient, cancellationTokenSource.Token);
30 |
31 | // Assert
32 | Assert.True(context.Response.TryRead(out LambdaTestResponse? response));
33 | Assert.True(response!.IsSuccessful);
34 | Assert.NotNull(response.Content);
35 |
36 | var actual = JsonSerializer.Deserialize(response.Content);
37 |
38 | Assert.NotNull(actual);
39 | int[] expected = [3, 2, 1];
40 | Assert.Equal(expected, actual);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/tests/AwsLambdaTestServer.Tests/ReverseFunctionWithCustomOptionsTests.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using System.Text.Json;
5 | using MartinCostello.Testing.AwsLambdaTestServer;
6 |
7 | #pragma warning disable IDE0130
8 | namespace MyFunctions;
9 |
10 | public static class ReverseFunctionWithCustomOptionsTests
11 | {
12 | [Fact]
13 | public static async Task Function_Reverses_Numbers_With_Custom_Options()
14 | {
15 | // Arrange
16 | var options = new LambdaTestServerOptions()
17 | {
18 | FunctionMemorySize = 256,
19 | FunctionTimeout = TimeSpan.FromSeconds(30),
20 | FunctionVersion = 42,
21 | };
22 |
23 | using var server = new LambdaTestServer(options);
24 | using var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(2));
25 |
26 | await server.StartAsync(cancellationTokenSource.Token);
27 |
28 | int[] value = [1, 2, 3];
29 | byte[] json = JsonSerializer.SerializeToUtf8Bytes(value);
30 |
31 | LambdaTestContext context = await server.EnqueueAsync(json);
32 |
33 | using var httpClient = server.CreateClient();
34 |
35 | // Act
36 | await ReverseFunction.RunAsync(httpClient, cancellationTokenSource.Token);
37 |
38 | // Assert
39 | Assert.True(context.Response.TryRead(out LambdaTestResponse? response));
40 | Assert.True(response!.IsSuccessful);
41 |
42 | var actual = JsonSerializer.Deserialize(response.Content);
43 |
44 | Assert.NotNull(actual);
45 | int[] expected = [3, 2, 1];
46 | Assert.Equal(expected, actual);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/tests/AwsLambdaTestServer.Tests/ReverseFunctionWithLoggingTests.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using System.Text.Json;
5 | using MartinCostello.Logging.XUnit;
6 | using MartinCostello.Testing.AwsLambdaTestServer;
7 | using Microsoft.Extensions.DependencyInjection;
8 | using Microsoft.Extensions.Logging;
9 |
10 | #pragma warning disable IDE0130
11 | namespace MyFunctions;
12 |
13 | public class ReverseFunctionWithLoggingTests(ITestOutputHelper outputHelper) : ITestOutputHelperAccessor
14 | {
15 | public ITestOutputHelper? OutputHelper { get; set; } = outputHelper;
16 |
17 | [Fact]
18 | public async Task Function_Reverses_Numbers_With_Logging()
19 | {
20 | // Arrange
21 | using var server = new LambdaTestServer(
22 | (services) => services.AddLogging(
23 | (builder) => builder.AddXUnit(this)));
24 |
25 | using var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(2));
26 |
27 | await server.StartAsync(cancellationTokenSource.Token);
28 |
29 | int[] value = [1, 2, 3];
30 | byte[] json = JsonSerializer.SerializeToUtf8Bytes(value);
31 |
32 | LambdaTestContext context = await server.EnqueueAsync(json);
33 |
34 | using var httpClient = server.CreateClient();
35 |
36 | // Act
37 | await ReverseFunction.RunAsync(httpClient, cancellationTokenSource.Token);
38 |
39 | // Assert
40 | Assert.True(context.Response.TryRead(out LambdaTestResponse? response));
41 | Assert.NotNull(response);
42 | Assert.True(response!.IsSuccessful);
43 |
44 | var actual = JsonSerializer.Deserialize(response.Content);
45 |
46 | Assert.NotNull(actual);
47 | int[] expected = [3, 2, 1];
48 | Assert.Equal(expected, actual);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/tests/AwsLambdaTestServer.Tests/ReverseFunctionWithMobileSdkTests.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using System.Text.Json;
5 | using MartinCostello.Testing.AwsLambdaTestServer;
6 |
7 | #pragma warning disable IDE0130
8 | namespace MyFunctions;
9 |
10 | public static class ReverseFunctionWithMobileSdkTests
11 | {
12 | [Fact]
13 | public static async Task Function_Reverses_Numbers_With_Mobile_Sdk()
14 | {
15 | // Arrange
16 | using var server = new LambdaTestServer();
17 | using var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(2));
18 |
19 | await server.StartAsync(cancellationTokenSource.Token);
20 |
21 | int[] value = [1, 2, 3];
22 | byte[] content = JsonSerializer.SerializeToUtf8Bytes(value);
23 |
24 | LambdaTestContext context = await server.EnqueueAsync(content);
25 |
26 | using var httpClient = server.CreateClient();
27 |
28 | // Act
29 | await ReverseFunction.RunAsync(httpClient, cancellationTokenSource.Token);
30 |
31 | // Assert
32 | Assert.True(context.Response.TryRead(out LambdaTestResponse? response));
33 | Assert.NotNull(response);
34 | Assert.True(response!.IsSuccessful);
35 |
36 | var actual = JsonSerializer.Deserialize(response.Content);
37 |
38 | Assert.NotNull(actual);
39 | int[] expected = [3, 2, 1];
40 | Assert.Equal(expected, actual);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/tests/AwsLambdaTestServer.Tests/TestExtensions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Martin Costello, 2019. All rights reserved.
2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3 |
4 | using System.Text.Json;
5 |
6 | namespace MartinCostello.Testing.AwsLambdaTestServer;
7 |
8 | internal static class TestExtensions
9 | {
10 | internal static async Task EnqueueAsync(this LambdaTestServer server, T value)
11 | where T : class
12 | {
13 | byte[] json = JsonSerializer.SerializeToUtf8Bytes(value);
14 | return await server.EnqueueAsync(json);
15 | }
16 |
17 | internal static T ReadAs(this LambdaTestResponse response)
18 | where T : class
19 | {
20 | return JsonSerializer.Deserialize(response.Content)!;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------