├── .editorconfig ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ ├── Build_&_Test.yml │ ├── Manual_Nuget_Push.yml │ └── docs.yml ├── .gitignore ├── .nuke ├── build.schema.json └── parameters.json ├── LICENSE ├── README.md ├── build.cmd ├── build.ps1 ├── build.sh ├── build ├── .editorconfig ├── Directory.Build.props ├── Directory.Build.targets ├── _build.csproj ├── _build.csproj.DotSettings └── build.cs ├── docs ├── .vitepress │ ├── config.ts │ └── theme │ │ ├── custom.css │ │ └── index.js ├── ClassDiagram.drawio ├── guide │ ├── bootstrapping.md │ ├── extensions.md │ ├── gettingstarted.md │ ├── history.md │ ├── index.md │ ├── nunit.md │ ├── opentelemetry.md │ ├── security.md │ ├── snapshot.md │ ├── tunit.md │ └── xunit.md ├── index.md ├── public │ ├── ClassDiagram.drawio.png │ ├── alba.jpg │ └── tracing.png ├── scenarios │ ├── assertions.md │ ├── formdata.md │ ├── headers.md │ ├── index.md │ ├── json.md │ ├── redirects.md │ ├── setup.md │ ├── statuscode.md │ ├── text.md │ ├── urls.md │ ├── writingscenarios.md │ └── xml.md └── vite.config.js ├── img └── icon.png ├── mdsnippets.json ├── package-lock.json ├── package.json └── src ├── Alba.Testing ├── Acceptance │ ├── asserting_against_status_code.cs │ ├── asserting_against_the_response_body_text.cs │ ├── assertions_against_redirects.cs │ ├── assertions_against_response_headers.cs │ ├── assertions_against_the_querystring.cs │ ├── assertions_against_the_request_headers.cs │ ├── content_type_verifications.cs │ ├── customize_before_each_and_after_each.cs │ ├── data_binding_in_mvc_app.cs │ ├── specs_against_aspnet_core_app.cs │ ├── using_custom_service_registrations.cs │ ├── web_application_factory_usage.cs │ └── write_out_the_body_anytime_the_status_code_is_in_the_500s.cs ├── ActivityTests.cs ├── Alba.Testing.csproj ├── Assertions │ ├── AssertionRunner.cs │ ├── BodyContainsAssertionTests.cs │ ├── BodyDoesNotContainAssertionTests.cs │ ├── BodyTextAssertionTests.cs │ ├── HasSingleHeaderValueAssertionTests.cs │ ├── HeaderMatchAssertionTests.cs │ ├── HeaderMultiValueAssertionTests.cs │ ├── HeaderValueAssertionTests.cs │ ├── NoHeaderValueAssertionTests.cs │ ├── RedirectAssertionTests.cs │ ├── StatusCodeAssertionTests.cs │ ├── StatusCodeSuccessAssertionTests.cs │ └── sending_and_receiving_json.cs ├── CrudeRouter.cs ├── FormDataExtensionsTests.cs ├── MimeTypeTests.cs ├── MimimalApi │ └── end_to_end_with_json_serialization.cs ├── Properties │ └── launchSettings.json ├── Samples │ ├── ContractTestWithAlba.cs │ ├── Extensions.cs │ ├── FormData.cs │ ├── Headers.cs │ ├── JsonAndXml.cs │ ├── MinimalApiUsage.cs │ ├── Quickstart.cs │ ├── Quickstart3.cs │ ├── Redirects.cs │ ├── SnapshotTesting.SnapshotTest.verified.txt │ ├── SnapshotTesting.cs │ ├── StatusCodes.cs │ └── Urls.cs ├── ScenarioAssertionExceptionTests.cs ├── ScenarioContext.cs ├── ScenarioTests.cs ├── Security │ ├── IdentityServerFixture.cs │ ├── JwtSecurityStubTests.cs │ ├── OpenConnectClientCredentialsTests.cs │ ├── OpenConnectUserPasswordTests.cs │ ├── web_api_authentication_with_individual_stub.cs │ ├── web_api_authentication_with_jwt.cs │ └── web_api_authentication_with_stub.cs ├── SpecificationExtensions.cs ├── TestImage.jpg ├── TestTextFile.txt ├── before_and_after_actions.cs ├── reading_and_writing_xml_to_context.cs ├── using_extensions_with_sync_builder.cs ├── using_json_helpers.cs └── xunit.runner.json ├── Alba.sln ├── Alba ├── Alba.csproj ├── AlbaHost.cs ├── AlbaHostExtensions.cs ├── AlbaJsonFormatterException.cs ├── AlbaWebApplicationFactory.cs ├── AssemblyInfo.cs ├── AssertionContext.cs ├── Assertions │ ├── BodyContainsAssertion.cs │ ├── BodyDoesNotContainAssertion.cs │ ├── BodyTextAssertion.cs │ ├── HasSingleHeaderValueAssertion.cs │ ├── HeaderExistsAssertion.cs │ ├── HeaderMatchAssertion.cs │ ├── HeaderMultiValueAssertion.cs │ ├── HeaderValueAssertion.cs │ ├── NoHeaderValueAssertion.cs │ ├── RedirectAssertion.cs │ ├── StatusCodeAssertion.cs │ └── StatusCodeSuccessAssertion.cs ├── ConfigurationOverride.cs ├── EmptyResponseException.cs ├── FormDataExtensions.cs ├── HeaderExpectations.cs ├── HeaderExtensions.cs ├── HttpContextExtensions.cs ├── HttpRequestBody.cs ├── IAlbaExtension.cs ├── IAlbaHost.cs ├── IAlbaWebApplicationFactory.cs ├── IScenarioAssertion.cs ├── IScenarioResult.cs ├── IUrlExpression.cs ├── Internal │ ├── AlbaTracing.cs │ ├── LightweightCache.cs │ ├── StreamExtensions.cs │ └── StringExtensions.cs ├── MimeType.cs ├── Scenario.cs ├── ScenarioAssertionException.cs ├── ScenarioExpectationsExtensions.cs ├── ScenarioResult.cs ├── Security │ ├── AuthenticationExtensionBase.cs │ ├── AuthenticationStub.cs │ ├── ClaimsExtensions.cs │ ├── IHasClaims.cs │ ├── JwtSecurityStub.cs │ ├── OpenConnectClientCredentials.cs │ ├── OpenConnectExtension.cs │ └── OpenConnectUserPassword.cs ├── SendExpression.cs └── Serialization │ ├── FormatterSerializer.cs │ ├── IJsonStrategy.cs │ └── SystemTextJsonSerializer.cs ├── Directory.Build.props ├── IdentityServer.New ├── Config.cs ├── HostingExtensions.cs ├── IdentityServer.New.csproj ├── Pages │ ├── Account │ │ ├── AccessDenied.cshtml │ │ ├── AccessDenied.cshtml.cs │ │ ├── Create │ │ │ ├── Index.cshtml │ │ │ ├── Index.cshtml.cs │ │ │ └── InputModel.cs │ │ ├── Login │ │ │ ├── Index.cshtml │ │ │ ├── Index.cshtml.cs │ │ │ ├── InputModel.cs │ │ │ ├── LoginOptions.cs │ │ │ └── ViewModel.cs │ │ └── Logout │ │ │ ├── Index.cshtml │ │ │ ├── Index.cshtml.cs │ │ │ ├── LoggedOut.cshtml │ │ │ ├── LoggedOut.cshtml.cs │ │ │ ├── LoggedOutViewModel.cs │ │ │ └── LogoutOptions.cs │ ├── Ciba │ │ ├── All.cshtml │ │ ├── All.cshtml.cs │ │ ├── Consent.cshtml │ │ ├── Consent.cshtml.cs │ │ ├── ConsentOptions.cs │ │ ├── Index.cshtml │ │ ├── Index.cshtml.cs │ │ ├── InputModel.cs │ │ ├── ViewModel.cs │ │ └── _ScopeListItem.cshtml │ ├── Consent │ │ ├── ConsentOptions.cs │ │ ├── Index.cshtml │ │ ├── Index.cshtml.cs │ │ ├── InputModel.cs │ │ ├── ViewModel.cs │ │ └── _ScopeListItem.cshtml │ ├── Device │ │ ├── DeviceOptions.cs │ │ ├── Index.cshtml │ │ ├── Index.cshtml.cs │ │ ├── InputModel.cs │ │ ├── Success.cshtml │ │ ├── Success.cshtml.cs │ │ ├── ViewModel.cs │ │ └── _ScopeListItem.cshtml │ ├── Diagnostics │ │ ├── Index.cshtml │ │ ├── Index.cshtml.cs │ │ └── ViewModel.cs │ ├── Extensions.cs │ ├── ExternalLogin │ │ ├── Callback.cshtml │ │ ├── Callback.cshtml.cs │ │ ├── Challenge.cshtml │ │ └── Challenge.cshtml.cs │ ├── Grants │ │ ├── Index.cshtml │ │ ├── Index.cshtml.cs │ │ └── ViewModel.cs │ ├── Home │ │ └── Error │ │ │ ├── Index.cshtml │ │ │ ├── Index.cshtml.cs │ │ │ └── ViewModel.cs │ ├── Index.cshtml │ ├── Index.cshtml.cs │ ├── Redirect │ │ ├── Index.cshtml │ │ └── Index.cshtml.cs │ ├── SecurityHeadersAttribute.cs │ ├── ServerSideSessions │ │ ├── Index.cshtml │ │ └── Index.cshtml.cs │ ├── Shared │ │ ├── _Layout.cshtml │ │ ├── _Nav.cshtml │ │ └── _ValidationSummary.cshtml │ ├── TestUsers.cs │ ├── _ViewImports.cshtml │ └── _ViewStart.cshtml ├── Program.cs ├── Properties │ └── launchSettings.json ├── keys │ └── is-signing-key-BAF757129D4DA200C5FD1BF53156790E.json └── wwwroot │ ├── css │ ├── site.css │ ├── site.min.css │ └── site.scss │ ├── duende-logo.svg │ ├── favicon.ico │ └── js │ ├── signin-redirect.js │ └── signout-redirect.js ├── MinimalApiWithOakton ├── MinimalApiWithOakton.csproj ├── Program.cs ├── Properties │ └── launchSettings.json ├── appsettings.Development.json └── appsettings.json ├── NUnitSamples ├── NUnitSamples.csproj └── UnitTest1.cs ├── TUnitSamples ├── Program.cs └── TUnitSamples.csproj ├── WebApiAspNetCore3 ├── Controllers │ └── WeatherForecastController.cs ├── HomeController.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Startup.cs ├── WeatherForecast.cs ├── WebApiStartupHostingModel.csproj ├── appsettings.Development.json └── appsettings.json ├── WebApiNet6 ├── Program.cs ├── Properties │ └── launchSettings.json ├── WebApiNet6.csproj ├── appsettings.Development.json └── appsettings.json ├── WebApp ├── Controllers │ ├── AuthController.cs │ ├── FakeController.cs │ ├── FilesController.cs │ ├── GatewayController.cs │ ├── JsonController.cs │ ├── MathController.cs │ ├── QueryStringContoller.cs │ └── ValuesController.cs ├── IWidget.cs ├── Program.cs ├── Project_Readme.html ├── Properties │ └── launchSettings.json ├── Startup.cs ├── WebApp.csproj ├── appsettings.json ├── hello.txt └── web.config └── WebAppSecuredWithJwt ├── ArithmeticController.cs ├── IdentityController.cs ├── Program.cs ├── Properties └── launchSettings.json ├── Startup.cs ├── WebAppSecuredWithJwt.csproj ├── appsettings.Development.json └── appsettings.json /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | indent_style = space 3 | indent_size = 4 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.verified.txt text eol=lf working-tree-encoding=UTF-8 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [JasperFx] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # eventsourcingnetcore 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/Build_&_Test.yml: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # 3 | # 4 | # This code was generated. 5 | # 6 | # - To turn off auto-generation set: 7 | # 8 | # [GitHubActions (AutoGenerate = false)] 9 | # 10 | # - To trigger manual generation invoke: 11 | # 12 | # nuke --generate-configuration GitHubActions_Build_&_Test --host GitHubActions 13 | # 14 | # 15 | # ------------------------------------------------------------------------------ 16 | 17 | name: Build_&_Test 18 | 19 | on: 20 | push: 21 | branches: 22 | - master 23 | pull_request: 24 | branches: 25 | - master 26 | 27 | jobs: 28 | ubuntu-latest: 29 | name: ubuntu-latest 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v4 33 | - uses: actions/setup-dotnet@v4 34 | with: 35 | dotnet-version: '9' 36 | - name: 'Run: Test' 37 | run: ./build.cmd Test 38 | -------------------------------------------------------------------------------- /.github/workflows/Manual_Nuget_Push.yml: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # 3 | # 4 | # This code was generated. 5 | # 6 | # - To turn off auto-generation set: 7 | # 8 | # [GitHubActions (AutoGenerate = false)] 9 | # 10 | # - To trigger manual generation invoke: 11 | # 12 | # nuke --generate-configuration GitHubActions_Manual_Nuget_Push --host GitHubActions 13 | # 14 | # 15 | # ------------------------------------------------------------------------------ 16 | 17 | name: Manual_Nuget_Push 18 | 19 | on: [workflow_dispatch] 20 | 21 | jobs: 22 | ubuntu-latest: 23 | name: ubuntu-latest 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v4 27 | - uses: actions/setup-dotnet@v4 28 | with: 29 | dotnet-version: '9' 30 | 31 | - name: 'Run: NugetPush' 32 | run: ./build.cmd NugetPush 33 | env: 34 | NugetApiKey: ${{ secrets.NUGET_API_KEY }} 35 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Build & Deploy Docs 2 | 3 | on: [workflow_dispatch] 4 | 5 | permissions: 6 | contents: read 7 | pages: write 8 | id-token: write 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Install .NET Core 8.0.x 18 | uses: actions/setup-dotnet@v4 19 | with: 20 | dotnet-version: 8.0.x 21 | 22 | - name: Install Node.js 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: 20 26 | 27 | - name: Setup Pages 28 | uses: actions/configure-pages@v5 29 | 30 | - name: Install dependencies 31 | run: npm ci 32 | 33 | - name: Build docs 34 | run: | 35 | dotnet tool install -g MarkdownSnippets.Tool 36 | npm run docs-build 37 | touch docs/.vitepress/dist/.nojekyll 38 | 39 | - name: Upload artifact 40 | uses: actions/upload-pages-artifact@v3 41 | with: 42 | path: docs/.vitepress/dist 43 | 44 | deploy: 45 | environment: 46 | name: github-pages 47 | url: ${{ steps.deployment.outputs.page_url }} 48 | needs: build 49 | runs-on: ubuntu-latest 50 | name: Deploy 51 | steps: 52 | - name: Deploy to GitHub Pages 53 | id: deployment 54 | uses: actions/deploy-pages@v4 55 | -------------------------------------------------------------------------------- /.nuke/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./build.schema.json", 3 | "Solution": "src/Alba.sln" 4 | } 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Alba 2 | 3 | [![Discord](https://img.shields.io/discord/1074998995086225460?color=blue&label=Chat%20on%20Discord)](https://discord.gg/WMxrvegf8H) 4 | [![Nuget](https://img.shields.io/nuget/v/alba)](https://www.nuget.org/packages/Alba/) 5 | ![Nuget](https://img.shields.io/nuget/dt/alba) 6 | 7 | Tooling for better integration testing against ASP.Net Core applications. Check out [the documentation](https://jasperfx.github.io/alba) for examples. 8 | 9 | To work with the code, just open the solution at `src/Alba.sln` and go. No other setup necessary. 10 | 11 | To run the documentation locally, use `build docs` on Windows or `./build.sh docs` on Linux or OSX. The documentation website will require 12 | a recent installation of NPM. The documentation is built with [VitePress](https://vitepress.vuejs.org/). 13 | 14 | ## Support Plans 15 | 16 |
17 | JasperFx logo 18 |
19 | 20 | While Alba is open source, [JasperFx Software offers paid support and consulting contracts](https://bit.ly/3szhwT2) for Alba. 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /build.cmd: -------------------------------------------------------------------------------- 1 | :; set -eo pipefail 2 | :; SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) 3 | :; ${SCRIPT_DIR}/build.sh "$@" 4 | :; exit $? 5 | 6 | @ECHO OFF 7 | powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0build.ps1" %* 8 | -------------------------------------------------------------------------------- /build/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | dotnet_style_qualification_for_field = false:warning 3 | dotnet_style_qualification_for_property = false:warning 4 | dotnet_style_qualification_for_method = false:warning 5 | dotnet_style_qualification_for_event = false:warning 6 | dotnet_style_require_accessibility_modifiers = never:warning 7 | 8 | csharp_style_expression_bodied_methods = true:silent 9 | csharp_style_expression_bodied_properties = true:warning 10 | csharp_style_expression_bodied_indexers = true:warning 11 | csharp_style_expression_bodied_accessors = true:warning 12 | -------------------------------------------------------------------------------- /build/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /build/Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /build/_build.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0 6 | 7 | CS0649;CS0169;CA1050;CA1822;CA2211;IDE1006 8 | .. 9 | .. 10 | 1 11 | false 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/custom.css: -------------------------------------------------------------------------------- 1 | 2 | :root { 3 | --alba-green-light: #3cbaad; 4 | --alba-green-dark: #00857c; 5 | --alba-green: #4db6ac; 6 | 7 | --vp-c-brand-1: var(--alba-green); 8 | --vp-c-brand-2: var(--alba-green-dark); 9 | --vp-c-brand-3: var(--alba-green-light); 10 | --vp-c-brand-soft: var(--vp-c-green-soft); 11 | 12 | } 13 | 14 | 15 | .dark { 16 | 17 | --alba-green-light: #3eaca1; 18 | --alba-green-dark: #03726b; 19 | --alba-green: #4db6ac; 20 | } -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.js: -------------------------------------------------------------------------------- 1 | import DefaultTheme from 'vitepress/theme' 2 | import './custom.css' 3 | 4 | export default { 5 | ...DefaultTheme 6 | } -------------------------------------------------------------------------------- /docs/ClassDiagram.drawio: -------------------------------------------------------------------------------- 1 | zVnfd6I6EP5rOOfeh/YQEMRH0e5ud+2e3rX3dvsYIUpOI3FDrNq//k4gAUGtP3a1fZJ8TCZk5puPCVpub7r8LPAsueMxYZZjx0vL7VuOg1Dgw49CVgXS9t0CmAgaa6MKGNJXokFbo3Mak6xmKDlnks7qYMTTlESyhmEh+KJuNuasvuoMT8gGMIww20QfaSyTAg2cdoV/IXSSmJWR3ynuTLEx1jvJEhzzxRrk3lhuT3Aui6vpskeYCp6Jy+Pt6pENnv3PX//JfuF/w28P3/+7Kpx9OmZKuQVBUnmy69XrwE+w/NYO+lxmfnA3jn09xX7BbK7jZTk+g0XCMYe1YNNypSPp/5pzc+Mqy/PcBQPkzZbVTbia5L89x+qGNJVEjHFE1DAMjeuRKM0MYoDbLzyTBoW9jJqWgBVPZmCn9pBOIqcMrlB574UISZYNLuwJJCqzC2VB+JRIsYJ52otj66jpiiipvqj4hQyWrHHL0xjWlJ6Urqu0wYXO3BFZRBtZ7LIRLkLZiE+2oFOGU6ITOdR3VLwwo5MUriOi0gaACh2FSurqG5LPAI0SyuIBXvG52lYmcfRsRmHCBX0Ft9jkAG4LqUXB8WsWQzUTYBtQQRSh7k1eUAO6w8ua4QBnUgMRZwzPMjoqtzHFYkLTkEvJp9oo32nxEMg7IzNct84M19tkhtvawgz/XMxwt9R3gxGM5mzIpODPpUSqQI4pYz3OOFChn/LcyFCEkbHcQpApjWOWO5vhiKaTB0WY/hWqkEE+se9WyA8dBQUJLrHERSpV3hgeEXbPMyopV/5FYRvOOChLHiovtLx+jgjZ4ylsAtM8gQQosiCZPDTbu+tqM9um7v3DkhucKbetLbmFzdpDKGAsKP+rG+Vx08ppYCOdfxf6/YCzZ2NyO4ySwugHyeZst8pCJGXJmQZHjqdNoSt1hrQ2GaIgDnPHLH8LJ0A2km5hTZ0dIWSkZ197iidOD8aoGr8bddzWYdQxFPvj3PH36wJJ467qv2A0YlxJdRjjLCGxlge4/4mqVfOgw8gorKNkvnoNCz5P43xWbhdDd6YXgcAnfMJTzG4qFDzBBn8q82vPDJ/KNWHQX5ZrqtHKjJZU/ly7fqpcwKiapAZmTrFp9UwnvAYgWnwuIrK/ROElOCFvscXezpb1hsK8IgRhWNKX+gNv44d2d6+qoXpFocYrqtVsSoo96Vnr7WTDkdPZ46jY9IajnK7lHk9ncHCmzvUBKn9IBNT8B2o5Wy2vHuzgvVvOzlECwme5UleSgf6sZJjSL8v9ab3at5Z+KTNoXWTWNGeHzJxbMswZ+eNIhuftOO8cKxlttMfRmSUDneu0e5umh8uFos5ANbZ1zu89fZXNdX4G2tnv9N+SIf2RRU+2yk8be9uZzpvyZF+3/KBTz+3vUc+Y8PE4I+chw+ah+WwK1jx/nt4E2edVp72qE1xOdRpfWVqnqo7fUB2/fWHVcTaJdurXMdWYUNi04C80rknO6JD25GLfQby6GCB7S7sSoAu2K+iADyHvWO2/07/Y1ofqX+wDlaQoi8uceRpSgk6VkvI73S5HJ0sJDKs/Egrz6u8Y9+Z/ -------------------------------------------------------------------------------- /docs/guide/index.md: -------------------------------------------------------------------------------- 1 | # Guide 2 | 3 | Just here for Algolia 4 | -------------------------------------------------------------------------------- /docs/guide/nunit.md: -------------------------------------------------------------------------------- 1 | # Integrating with NUnit 2 | 3 | When using Alba within NUnit testing projects, you probably want to reuse the `IAlbaHost` across tests and test fixtures because 4 | `AlbaHost` is relatively expensive to create (it's bootstrapping your whole application more than Alba itself is slow). To do that with NUnit, you could 5 | track a single `AlbaHost` on a static class like this one: 6 | 7 | 8 | 9 | ```cs 10 | [SetUpFixture] 11 | public class Application 12 | { 13 | [OneTimeSetUp] 14 | public async Task Init() 15 | { 16 | Host = await AlbaHost.For(); 17 | } 18 | 19 | public static IAlbaHost Host { get; private set; } 20 | 21 | // Make sure that NUnit will shut down the AlbaHost when 22 | // all the projects are finished 23 | [OneTimeTearDown] 24 | public void Teardown() 25 | { 26 | Host.Dispose(); 27 | } 28 | } 29 | ``` 30 | snippet source | anchor 31 | 32 | 33 | Then reference the `AlbaHost` in tests like this sample: 34 | 35 | 36 | 37 | ```cs 38 | public class sample_integration_fixture 39 | { 40 | [Test] 41 | public async Task happy_path() 42 | { 43 | await Application.Host.Scenario(_ => 44 | { 45 | _.Get.Url("/fake/okay"); 46 | _.StatusCodeShouldBeOk(); 47 | }); 48 | } 49 | } 50 | ``` 51 | snippet source | anchor 52 | 53 | -------------------------------------------------------------------------------- /docs/guide/opentelemetry.md: -------------------------------------------------------------------------------- 1 | # Tracing & Open Telemetry 2 | 3 | Performing distributed tracing within your CI pipline is a relatively new yet powerful concept that can improve your teams response to broken, flaky or slow-performing tests. 4 | Alba has support for Open Telemetry tracing within `Scenario` calls, permitting tracing within your test pipeline with any compatible OpenTelemetry integration. 5 | If you believe there's value in tracing additional areas of Alba, please let us know! 6 | 7 | ## Automated Instrumentation 8 | 9 | ### Datadog CI Visibility 10 | 11 | ![Datadog Tracing](/tracing.png) 12 | 13 | Datadog's CI Visibility feature is compatible with Alba, however you must be using DD .NET Tracer 2.24+ and have `DD_TRACE_OTEL_ENABLED` set to `true`. See the [documentation](https://docs.datadoghq.com/continuous_integration/tests/dotnet/) for setup information. 14 | 15 | 16 | ## Manual Instrumentation 17 | 18 | ### xUnit 19 | 20 | Manually instrumenting your tests requires a moderate amount of supporting code to work correctly. See this [repository](https://github.com/martinjt/unittest-with-otel) and related [guide](https://www.honeycomb.io/blog/monitoring-unit-tests-opentelemetry) by the team at Honeycomb.io as a starting point. 21 | -------------------------------------------------------------------------------- /docs/guide/snapshot.md: -------------------------------------------------------------------------------- 1 | # Snapshot Testing 2 | 3 | Although Alba does not ship built-in snapshotting support, it does easily integrate with the popular [Verify](https://github.com/VerifyTests/Verify) framework. 4 | Given Verify and [Verify.AspNetCore](https://github.com/VerifyTests/Verify.AspNetCore) has been added and initialized for your test framework of choice, you can pass the `scenario` to `Verify()`, and response data will be captured for a complete picture of your network request. 5 | 6 | Given a test with the shape of: 7 | 8 | 9 | ```cs 10 | await using var host = await AlbaHost.For(); 11 | 12 | var scenario = await host.Scenario(s => 13 | { 14 | s.Post.Json(new MyEntity(Guid.NewGuid(), "SomeValue")).ToUrl("/json"); 15 | }); 16 | 17 | var body = scenario.ReadAsJson(); 18 | 19 | await Verify(new { 20 | scenario.Context, 21 | ResponseBody = body, 22 | }); 23 | ``` 24 | snippet source | anchor 25 | 26 | 27 | Will result in a snapshot of: 28 | 29 | <<< ../../src/Alba.Testing/Samples/SnapshotTesting.SnapshotTest.verified.txt{json} 30 | -------------------------------------------------------------------------------- /docs/guide/tunit.md: -------------------------------------------------------------------------------- 1 | # Integrating with TUnit 2 | 3 | Like other testing frameworks, you'll want to reuse the `IAlbaHost` across tests and test fixtures because 4 | `AlbaHost` is relatively expensive to create. To do that with TUnit, you should start by writing a bootstrapping class 5 | that inherits from `IAsyncInitializer` and `IAsyncDisposable`: 6 | 7 | 8 | 9 | ```cs 10 | public sealed class AlbaBootstrap : IAsyncInitializer, IAsyncDisposable 11 | { 12 | public IAlbaHost Host { get; private set; } = null!; 13 | 14 | public async Task InitializeAsync() 15 | { 16 | Host = await AlbaHost.For(); 17 | } 18 | 19 | public async ValueTask DisposeAsync() 20 | { 21 | await Host.DisposeAsync(); 22 | } 23 | } 24 | ``` 25 | snippet source | anchor 26 | 27 | 28 | Then inject the instance by adding `[ClassDataSource(Shared = SharedType.PerTestSession)]` to your test class. We recommend creating a base class to allow easier access of the host and any other dependencies. 29 | 30 | 31 | 32 | ```cs 33 | public abstract class AlbaTestBase(AlbaBootstrap albaBootstrap) 34 | { 35 | protected IAlbaHost Host => albaBootstrap.Host; 36 | } 37 | 38 | [ClassDataSource(Shared = SharedType.PerTestSession)] 39 | public class MyTestClass(AlbaBootstrap albaBootstrap) : AlbaTestBase(albaBootstrap) 40 | { 41 | [Test] 42 | public async Task happy_path() 43 | { 44 | await Host.Scenario(_ => 45 | { 46 | _.Get.Url("/fake/okay"); 47 | _.StatusCodeShouldBeOk(); 48 | }); 49 | } 50 | } 51 | ``` 52 | snippet source | anchor 53 | 54 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | 4 | hero: 5 | name: Alba 6 | text: Easy Integration Testing for ASP.NET Core 7 | actions: 8 | - theme: brand 9 | text: Get Started 10 | link: /guide/gettingstarted 11 | - theme: alt 12 | text: View on GitHub 13 | link: https://github.com/JasperFx/Alba 14 | 15 | features: 16 | - icon: 👀 17 | title: Declarative Syntax 18 | details: Write readable tests your whole team can understand. 19 | - icon: 🤝 20 | title: Classic & Minimal API Support 21 | details: Use MVC controllers, minimal APIs or a mix of both. 22 | - icon: 🔓 23 | title: Authorization Stubbing 24 | details: Stop fighting with your authorization system. Modify the shape of your user at the test level. 25 | footer: MIT Licensed | Copyright © Jeremy D. Miller and contributors. 26 | --- 27 | -------------------------------------------------------------------------------- /docs/public/ClassDiagram.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasperFx/alba/72219db976bdced2997fbed14e8e6f99a2369623/docs/public/ClassDiagram.drawio.png -------------------------------------------------------------------------------- /docs/public/alba.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasperFx/alba/72219db976bdced2997fbed14e8e6f99a2369623/docs/public/alba.jpg -------------------------------------------------------------------------------- /docs/public/tracing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasperFx/alba/72219db976bdced2997fbed14e8e6f99a2369623/docs/public/tracing.png -------------------------------------------------------------------------------- /docs/scenarios/index.md: -------------------------------------------------------------------------------- 1 | # Scenarios 2 | 3 | Just here for Algolia 4 | -------------------------------------------------------------------------------- /docs/scenarios/redirects.md: -------------------------------------------------------------------------------- 1 | # Working with Redirects 2 | 3 | ## Asserting on Expected Redirect Responses 4 | 5 | Alba comes with some out of the box assertions to declaratively check expected redirects. 6 | 7 | 8 | 9 | ```cs 10 | public async Task asserting_redirects(IAlbaHost system) 11 | { 12 | await system.Scenario(_ => 13 | { 14 | // should redirect to the url 15 | _.RedirectShouldBe("/redirect"); 16 | 17 | // should redirect permanently to the url 18 | _.RedirectPermanentShouldBe("/redirect"); 19 | }); 20 | } 21 | ``` 22 | snippet source | anchor 23 | 24 | -------------------------------------------------------------------------------- /docs/scenarios/setup.md: -------------------------------------------------------------------------------- 1 | # Before and After actions 2 | 3 | ::: warning 4 | The Before/After actions are **not** additive. The last one specified is the only one executed. 5 | ::: 6 | 7 | Alba allows you to specify actions that run immediately before or after an HTTP request is executed for common setup or teardown 8 | work like setting up authentication credentials or tracing or whatever. 9 | 10 | Here's a sample: 11 | 12 | 13 | 14 | ```cs 15 | // Synchronously 16 | system.BeforeEach(context => 17 | { 18 | // Modify the HttpContext immediately before each 19 | // Scenario()/HTTP request is executed 20 | context.Request.Headers.Append("trace", "something"); 21 | }); 22 | 23 | system.AfterEach(context => 24 | { 25 | // perform an action immediately after the scenario/HTTP request 26 | // is executed 27 | }); 28 | 29 | // Asynchronously 30 | system.BeforeEachAsync(context => 31 | { 32 | // do something asynchronous here 33 | return Task.CompletedTask; 34 | }); 35 | 36 | system.AfterEachAsync(context => 37 | { 38 | // do something asynchronous here 39 | return Task.CompletedTask; 40 | }); 41 | ``` 42 | snippet source | anchor 43 | 44 | -------------------------------------------------------------------------------- /docs/scenarios/statuscode.md: -------------------------------------------------------------------------------- 1 | # Http Status Codes 2 | 3 | You can declaratively check the status code with this syntax: 4 | 5 | 6 | 7 | ```cs 8 | public async Task check_the_status(IAlbaHost system) 9 | { 10 | await system.Scenario(_ => 11 | { 12 | // Shorthand for saying that the StatusCode should be 200 13 | _.StatusCodeShouldBeOk(); 14 | 15 | // Or a specific status code 16 | _.StatusCodeShouldBe(403); 17 | 18 | // Ignore the status code altogether 19 | _.IgnoreStatusCode(); 20 | }); 21 | } 22 | ``` 23 | snippet source | anchor 24 | 25 | 26 | Do note that by default, if you do not specify the expected status code, Alba assumes that 27 | the request should return 200 (OK) and will fail the scenario if a different status code is found. You 28 | can ignore that check with the `Scenario.IgnoreStatusCode()` method. 29 | -------------------------------------------------------------------------------- /docs/scenarios/urls.md: -------------------------------------------------------------------------------- 1 | # Working with Urls 2 | 3 | The simplest way to specify the url for the request is to use one of these calls shown below, 4 | depending upon the HTTP method: 5 | 6 | 7 | 8 | ```cs 9 | public async Task specify_url(AlbaHost system) 10 | { 11 | await system.Scenario(_ => 12 | { 13 | // Directly specify the Url against a given 14 | // HTTP method 15 | _.Get.Url("/"); 16 | _.Put.Url("/"); 17 | _.Post.Url("/"); 18 | _.Delete.Url("/"); 19 | _.Head.Url("/"); 20 | }); 21 | } 22 | ``` 23 | snippet source | anchor 24 | 25 | 26 | -------------------------------------------------------------------------------- /docs/vite.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | server: { 3 | fsServe: { 4 | root: '../' 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasperFx/alba/72219db976bdced2997fbed14e8e6f99a2369623/img/icon.png -------------------------------------------------------------------------------- /mdsnippets.json: -------------------------------------------------------------------------------- 1 | { 2 | "Convention": "InPlaceOverwrite", 3 | "LinkFormat": "GitHub", 4 | "UrlPrefix": "https://github.com/JasperFx/alba/blob/master", 5 | "ExcludeMarkdownDirectories": ["documentation"], 6 | "ExcludeSnippetDirectories": [], 7 | "TreatMissingAsWarning": false 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "vitepress-dev": "vitepress dev docs --port 5050", 6 | "vitepress-build": "vitepress build docs", 7 | "mdsnippets": "mdsnippets", 8 | "docs": "npm-run-all -s mdsnippets vitepress-dev", 9 | "docs-build": "npm-run-all -s mdsnippets vitepress-build" 10 | }, 11 | "dependencies": { 12 | "vitepress": "1.6.3" 13 | }, 14 | "devDependencies": { 15 | "npm-run-all": "^4.1.5" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Alba.Testing/Acceptance/assertions_against_the_querystring.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Shouldly; 3 | 4 | namespace Alba.Testing.Acceptance 5 | { 6 | public class assertions_against_the_querystring : ScenarioContext 7 | { 8 | [Fact] 9 | public Task using_scenario_with_QueryString_should_set_query_string_parameter() 10 | { 11 | router.Handlers["/one"] = c => 12 | { 13 | c.Response.StatusCode = 200; 14 | return c.Response.WriteAsync(c.Request.Query["test"]); 15 | }; 16 | 17 | return host.Scenario(_ => 18 | { 19 | _.Get.Url("/one").QueryString("test", "value"); 20 | 21 | _.ConfigureHttpContext(c => 22 | { 23 | c.Request.Query["test"].ToString().ShouldBe("value"); 24 | }); 25 | }); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/Alba.Testing/Acceptance/content_type_verifications.cs: -------------------------------------------------------------------------------- 1 | using Shouldly; 2 | 3 | namespace Alba.Testing 4 | { 5 | public class content_type_verifications : ScenarioContext 6 | { 7 | 8 | [Fact] 9 | public Task content_type_should_be_happy_path() 10 | { 11 | router.Handlers["/memory/hello"] = c => 12 | { 13 | c.Response.ContentType("text/plain"); 14 | c.Response.Write("some text"); 15 | 16 | return Task.CompletedTask; 17 | }; 18 | 19 | return host.Scenario(_ => 20 | { 21 | _.Get.Url("/memory/hello"); 22 | 23 | _.ContentTypeShouldBe(MimeType.Text); 24 | }); 25 | } 26 | 27 | 28 | [Fact] 29 | public async Task content_type_sad_path() 30 | { 31 | router.RegisterRoute(x => x.get_memory_hello(), "GET", "/memory/hello"); 32 | router.Handlers["/memory/hello"] = c => 33 | { 34 | c.Response.ContentType("text/plain"); 35 | c.Response.Write("Some text"); 36 | 37 | return Task.CompletedTask; 38 | }; 39 | 40 | var ex = await fails(_ => 41 | { 42 | _.Get.Url("/memory/hello"); 43 | 44 | _.ContentTypeShouldBe("text/json"); 45 | }); 46 | 47 | ex.Message.ShouldContain( 48 | "Expected a single header value of 'Content-Type'='text/json', but the actual value was 'text/plain'"); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /src/Alba.Testing/Acceptance/customize_before_each_and_after_each.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Shouldly; 3 | using WebApp; 4 | 5 | namespace Alba.Testing.Acceptance 6 | { 7 | public class customize_before_each_and_after_each 8 | { 9 | [Fact] 10 | public async Task before_each_and_after_each_is_called() 11 | { 12 | await using var host = await AlbaHost.For(); 13 | host.BeforeEach(c => 14 | { 15 | BeforeContext = c; 16 | }) 17 | .AfterEach(c => AfterContext = c); 18 | 19 | BeforeContext = AfterContext = null; 20 | 21 | await host.Scenario(_ => 22 | { 23 | _.Get.Url("/api/values"); 24 | }); 25 | 26 | AfterContext.ShouldNotBeNull(); 27 | BeforeContext.ShouldNotBeNull(); 28 | } 29 | 30 | public HttpContext BeforeContext { get; set; } 31 | 32 | public HttpContext AfterContext { get; set; } 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /src/Alba.Testing/Acceptance/data_binding_in_mvc_app.cs: -------------------------------------------------------------------------------- 1 | using Shouldly; 2 | using WebApp; 3 | using WebApp.Controllers; 4 | 5 | namespace Alba.Testing.Acceptance 6 | { 7 | public class data_binding_in_mvc_app 8 | { 9 | #region sample_binding_against_a_model 10 | [Fact] 11 | public async Task can_bind_to_form_data() 12 | { 13 | await using var system = await AlbaHost.For(); 14 | 15 | var input = new InputModel { 16 | One = "one", 17 | Two = "two", 18 | Three = "three" 19 | }; 20 | 21 | await system.Scenario(_ => 22 | { 23 | _.Post.FormData(input) 24 | .ToUrl("/gateway/insert"); 25 | }); 26 | 27 | 28 | GatewayController.LastInput.ShouldNotBeNull(); 29 | 30 | GatewayController.LastInput.One.ShouldBe("one"); 31 | GatewayController.LastInput.Two.ShouldBe("two"); 32 | GatewayController.LastInput.Three.ShouldBe("three"); 33 | } 34 | 35 | #endregion 36 | 37 | [Fact] 38 | public async Task can_bind_to_form_data_as_dictionary() 39 | { 40 | await using var system = await AlbaHost.For(); 41 | 42 | var dict = new Dictionary {{"One", "one"}, {"Two", "two"}, {"Three", "three"}}; 43 | 44 | 45 | await system.Scenario(_ => 46 | { 47 | _.Post.FormData(dict) 48 | .ToUrl("/gateway/insert"); 49 | }); 50 | 51 | GatewayController.LastInput.ShouldNotBeNull(); 52 | 53 | GatewayController.LastInput.One.ShouldBe("one"); 54 | GatewayController.LastInput.Two.ShouldBe("two"); 55 | GatewayController.LastInput.Three.ShouldBe("three"); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Alba.Testing/Acceptance/using_custom_service_registrations.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Shouldly; 4 | using WebApp; 5 | using WebApp.Controllers; 6 | 7 | namespace Alba.Testing.Acceptance 8 | { 9 | public class using_custom_service_registrations 10 | { 11 | [Fact] 12 | public async Task override_service_registration_in_bootstrapping() 13 | { 14 | ValuesController.LastWidget = Array.Empty(); 15 | 16 | using var system = await AlbaHost.For(builder => 17 | { 18 | builder.ConfigureServices((c, _) => 19 | { 20 | _.AddTransient(); 21 | }); 22 | }); 23 | 24 | ValuesController.LastWidget = null; 25 | 26 | // The default registration is a GreenWidget 27 | 28 | await system.Scenario(_ => 29 | { 30 | 31 | _.Put.Url("/api/values/foo").ContentType("application/json"); 32 | }); 33 | 34 | ValuesController.LastWidget.Length.ShouldBe(2); 35 | } 36 | 37 | [Fact] 38 | public async Task can_request_services() 39 | { 40 | using var system = await AlbaHost.For(builder => 41 | { 42 | builder.ConfigureServices((c, _) => { _.AddHttpContextAccessor(); }); 43 | }); 44 | 45 | var accessor1 = system.Services.GetService(); 46 | Assert.NotNull(accessor1); 47 | 48 | var accessor2 = system.Server.Services.GetService(); 49 | Assert.NotNull(accessor2); 50 | 51 | var accessor3 = ((IAlbaHost)system).Services.GetService(); 52 | Assert.NotNull(accessor3); 53 | } 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /src/Alba.Testing/Acceptance/write_out_the_body_anytime_the_status_code_is_in_the_500s.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Shouldly; 3 | 4 | namespace Alba.Testing.Acceptance 5 | { 6 | public class write_out_the_body_anytime_the_status_code_is_in_the_500s : ScenarioContext 7 | { 8 | [Fact] 9 | public async Task will_write_out_the_body_on_500() 10 | { 11 | router.Handlers["/one"] = c => 12 | { 13 | c.Response.StatusCode = 500; 14 | return c.Response.WriteAsync(new NotSupportedException().ToString()); 15 | }; 16 | 17 | var ex = await fails(_ => 18 | { 19 | _.Get.Url("/one"); 20 | _.StatusCodeShouldBeOk(); 21 | }); 22 | 23 | ex.Message.ShouldContain("NotSupportedException"); 24 | } 25 | 26 | [Fact] 27 | public async Task will_write_out_the_body_on_501() 28 | { 29 | router.Handlers["/one"] = c => 30 | { 31 | c.Response.StatusCode = 501; 32 | return c.Response.WriteAsync(new NotSupportedException().ToString()); 33 | }; 34 | 35 | var ex = await fails(_ => 36 | { 37 | _.Get.Url("/one"); 38 | _.StatusCodeShouldBeOk(); 39 | }); 40 | 41 | ex.Message.ShouldContain("NotSupportedException"); 42 | } 43 | 44 | [Fact] 45 | public async Task will_write_out_the_body_on_502() 46 | { 47 | router.Handlers["/one"] = c => 48 | { 49 | c.Response.StatusCode = 501; 50 | return c.Response.WriteAsync(new DivideByZeroException().ToString()); 51 | }; 52 | 53 | var ex = await fails(_ => 54 | { 55 | _.Get.Url("/one"); 56 | _.StatusCodeShouldBeOk(); 57 | }); 58 | 59 | ex.Message.ShouldContain("DivideByZeroException"); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /src/Alba.Testing/ActivityTests.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Alba.Internal; 3 | 4 | namespace Alba.Testing; 5 | 6 | // activity listener cannot be tested in parallel with other tests 7 | [CollectionDefinition(nameof(ActivityCollection), DisableParallelization = true)] 8 | public class ActivityCollection 9 | { 10 | 11 | } 12 | 13 | [Collection(nameof(ActivityCollection))] 14 | public class ActivityTests 15 | { 16 | [Fact] 17 | public async Task ActivityTagged_AsExpected() 18 | { 19 | await using var host = await AlbaHost.For(); 20 | var startCalled = false; 21 | var endCalled = false; 22 | var expectedTags = new [] 23 | { 24 | AlbaTracing.HttpMethod, 25 | AlbaTracing.HttpUrl, 26 | AlbaTracing.HttpStatusCode, 27 | AlbaTracing.NetPeerName, 28 | AlbaTracing.HttpResponseContentLength, 29 | AlbaTracing.HttpRequestContentLength 30 | }; 31 | using var listener = new ActivityListener 32 | { 33 | ShouldListenTo = _ => _.Name == "Alba", 34 | Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData, 35 | ActivityStarted = activity => 36 | { 37 | startCalled = true; 38 | Assert.NotNull(activity); 39 | Assert.Equal($"POST /json", activity.DisplayName); 40 | }, 41 | ActivityStopped = activity => 42 | { 43 | endCalled = true; 44 | Assert.NotNull(activity); 45 | Assert.Contains(activity.Tags, x => expectedTags.Contains(x.Key)); 46 | 47 | } 48 | }; 49 | 50 | ActivitySource.AddActivityListener(listener); 51 | 52 | await host.Scenario(_ => 53 | { 54 | _.Post.Json(new MyEntity(Guid.NewGuid(), "SomeValue")).ToUrl("/json"); 55 | }); 56 | 57 | Assert.True(startCalled); 58 | Assert.True(endCalled); 59 | } 60 | } -------------------------------------------------------------------------------- /src/Alba.Testing/Alba.Testing.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0 4 | false 5 | Exe 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | all 27 | runtime; build; native; contentfiles; analyzers; buildtransitive 28 | 29 | 30 | 31 | 32 | 33 | PreserveNewest 34 | 35 | 36 | 37 | 38 | 39 | PreserveNewest 40 | 41 | 42 | PreserveNewest 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/Alba.Testing/Assertions/AssertionRunner.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | 3 | namespace Alba.Testing.Assertions 4 | { 5 | public static class AssertionRunner 6 | { 7 | public static ScenarioAssertionException Run(IScenarioAssertion assertion, 8 | Action configuration) 9 | { 10 | var ex = new ScenarioAssertionException(); 11 | 12 | var context = new DefaultHttpContext(); 13 | context.Response.Body = new MemoryStream(); 14 | context.Request.Body = new MemoryStream(); 15 | 16 | 17 | configuration(context); 18 | 19 | var stream = context.Response.Body; 20 | stream.Position = 0; 21 | 22 | assertion.Assert(null, new AssertionContext(context, ex)); 23 | 24 | return ex; 25 | } 26 | 27 | public static void SingleMessageShouldBe(this ScenarioAssertionException ex, string message) 28 | { 29 | ex.Messages.ShouldHaveTheSameElementsAs(message); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/Alba.Testing/Assertions/BodyContainsAssertionTests.cs: -------------------------------------------------------------------------------- 1 | using Alba.Assertions; 2 | 3 | namespace Alba.Testing.Assertions 4 | { 5 | public class BodyContainsAssertionTests 6 | { 7 | [Fact] 8 | public void happy_path() 9 | { 10 | AssertionRunner.Run(new BodyContainsAssertion("Hey!"), env => env.Response.Write("Hey! You!")) 11 | .AssertAll(); 12 | } 13 | 14 | [Fact] 15 | public void sad_path() 16 | { 17 | var assertion = new BodyContainsAssertion("Hey!"); 18 | AssertionRunner.Run(assertion, env => env.Response.Write("Not the droids you are looking for")) 19 | .SingleMessageShouldBe("Expected text 'Hey!' was not found in the response body"); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/Alba.Testing/Assertions/BodyDoesNotContainAssertionTests.cs: -------------------------------------------------------------------------------- 1 | using Alba.Assertions; 2 | 3 | namespace Alba.Testing.Assertions 4 | { 5 | public class BodyDoesNotContainAssertionTests 6 | { 7 | [Fact] 8 | public void happy_path() 9 | { 10 | AssertionRunner.Run(new BodyDoesNotContainAssertion("Hey!"), env => env.Response.Write("You!")) 11 | .AssertAll(); 12 | } 13 | 14 | [Fact] 15 | public void sad_path() 16 | { 17 | var assertion = new BodyDoesNotContainAssertion("Hey!"); 18 | AssertionRunner.Run(assertion, env => env.Response.Write("Hey! You!")) 19 | .SingleMessageShouldBe("Text 'Hey!' should not be found in the response body"); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/Alba.Testing/Assertions/BodyTextAssertionTests.cs: -------------------------------------------------------------------------------- 1 | using Alba.Assertions; 2 | using Shouldly; 3 | 4 | namespace Alba.Testing.Assertions 5 | { 6 | public class BodyTextAssertionTests 7 | { 8 | [Fact] 9 | public void happy_path() 10 | { 11 | AssertionRunner.Run(new BodyTextAssertion("Hey!"), env => env.Response.Write("Hey!")) 12 | .AssertAll(); 13 | } 14 | 15 | [Fact] 16 | public void sad_path() 17 | { 18 | var assertion = new BodyTextAssertion("Hey!"); 19 | AssertionRunner.Run(assertion, env => env.Response.Write("Hey! You!")) 20 | .Messages.Single().ShouldContain("Expected the content to be 'Hey!'"); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/Alba.Testing/Assertions/HasSingleHeaderValueAssertionTests.cs: -------------------------------------------------------------------------------- 1 | using Alba.Assertions; 2 | using Microsoft.AspNetCore.Http; 3 | 4 | namespace Alba.Testing.Assertions 5 | { 6 | public class HasSingleHeaderValueAssertionTests 7 | { 8 | [Fact] 9 | public void happy_path() 10 | { 11 | var assertion = new HasSingleHeaderValueAssertion("foo"); 12 | AssertionRunner.Run(assertion, x => x.Response.Headers["foo"] = "bar") 13 | .AssertAll(); 14 | } 15 | 16 | [Fact] 17 | public void sad_path_no_values() 18 | { 19 | var assertion = new HasSingleHeaderValueAssertion("foo"); 20 | AssertionRunner.Run(assertion, e => { }) 21 | .SingleMessageShouldBe("Expected a single header value of 'foo', but no values were found on the response"); 22 | } 23 | 24 | [Fact] 25 | public void sad_path_too_many_values() 26 | { 27 | var assertion = new HasSingleHeaderValueAssertion("foo"); 28 | AssertionRunner.Run(assertion, x => 29 | { 30 | x.Response.Headers.Append("foo", "baz"); 31 | x.Response.Headers.Append("foo", "bar"); 32 | }) 33 | .SingleMessageShouldBe("Expected a single header value of 'foo', but found multiple values on the response: 'baz', 'bar'"); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/Alba.Testing/Assertions/HeaderMatchAssertionTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using Alba.Assertions; 3 | using Microsoft.AspNetCore.Http; 4 | 5 | namespace Alba.Testing.Assertions 6 | { 7 | public class HeaderMatchAssertionTests 8 | { 9 | private readonly HeaderMatchAssertion _assertion; 10 | 11 | public HeaderMatchAssertionTests() 12 | { 13 | _assertion = new HeaderMatchAssertion("foo", new Regex("^b.?r$")); 14 | } 15 | 16 | [Fact] 17 | public void happy_path() 18 | { 19 | AssertionRunner.Run(_assertion, x => x.Response.Headers["foo"] = "bar") 20 | .AssertAll(); 21 | } 22 | 23 | [Fact] 24 | public void sad_path_no_values() 25 | { 26 | AssertionRunner.Run(_assertion, e => { }) 27 | .SingleMessageShouldBe("Expected a single header value of 'foo' matching '^b.?r$', but no values were found on the response"); 28 | } 29 | 30 | [Fact] 31 | public void sad_path_wrong_value() 32 | { 33 | AssertionRunner.Run(_assertion, x => x.Response.Headers["foo"] = "baz") 34 | .SingleMessageShouldBe("Expected a single header value of 'foo' matching '^b.?r$', but the actual value was 'baz'"); 35 | } 36 | 37 | [Fact] 38 | public void sad_path_too_many_values() 39 | { 40 | AssertionRunner.Run(_assertion, x => 41 | { 42 | x.Response.Headers.Append("foo", "baz"); 43 | x.Response.Headers.Append("foo", "bar"); 44 | }) 45 | .SingleMessageShouldBe("Expected a single header value of 'foo' matching '^b.?r$', but the actual values were 'baz', 'bar'"); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Alba.Testing/Assertions/HeaderValueAssertionTests.cs: -------------------------------------------------------------------------------- 1 | using Alba.Assertions; 2 | using Microsoft.AspNetCore.Http; 3 | 4 | namespace Alba.Testing.Assertions 5 | { 6 | public class HeaderValueAssertionTests 7 | { 8 | [Fact] 9 | public void happy_path() 10 | { 11 | var assertion = new HeaderValueAssertion("foo", "bar"); 12 | AssertionRunner.Run(assertion, x => x.Response.Headers["foo"] = "bar") 13 | .AssertAll(); 14 | } 15 | 16 | [Fact] 17 | public void sad_path_no_values() 18 | { 19 | var assertion = new HeaderValueAssertion("foo", "bar"); 20 | AssertionRunner.Run(assertion, e => {}) 21 | .SingleMessageShouldBe("Expected a single header value of 'foo'='bar', but no values were found on the response"); 22 | } 23 | 24 | [Fact] 25 | public void sad_path_wrong_value() 26 | { 27 | var assertion = new HeaderValueAssertion("foo", "bar"); 28 | AssertionRunner.Run(assertion, x => x.Response.Headers["foo"] = "baz") 29 | .SingleMessageShouldBe("Expected a single header value of 'foo'='bar', but the actual value was 'baz'"); 30 | } 31 | 32 | [Fact] 33 | public void sad_path_too_many_values() 34 | { 35 | var assertion = new HeaderValueAssertion("foo", "bar"); 36 | AssertionRunner.Run(assertion, x => 37 | { 38 | x.Response.Headers.Append("foo", "baz"); 39 | x.Response.Headers.Append("foo", "bar"); 40 | }) 41 | .SingleMessageShouldBe("Expected a single header value of 'foo'='bar', but the actual values were 'baz', 'bar'"); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/Alba.Testing/Assertions/NoHeaderValueAssertionTests.cs: -------------------------------------------------------------------------------- 1 | using Alba.Assertions; 2 | using Microsoft.AspNetCore.Http; 3 | 4 | namespace Alba.Testing.Assertions 5 | { 6 | public class NoHeaderValueAssertionTests 7 | { 8 | [Fact] 9 | public void happy_path() 10 | { 11 | var assertion = new NoHeaderValueAssertion("foo"); 12 | AssertionRunner.Run(assertion, e => {}).AssertAll(); 13 | } 14 | 15 | [Fact] 16 | public void sad_path_any_values() 17 | { 18 | var assertion = new NoHeaderValueAssertion("foo"); 19 | AssertionRunner.Run(assertion, x => 20 | { 21 | x.Response.Headers.Append("foo", "baz"); 22 | x.Response.Headers.Append("foo", "bar"); 23 | }) 24 | .SingleMessageShouldBe("Expected no value for header 'foo', but found values 'baz', 'bar'"); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/Alba.Testing/Assertions/RedirectAssertionTests.cs: -------------------------------------------------------------------------------- 1 | using Alba.Assertions; 2 | using Shouldly; 3 | 4 | namespace Alba.Testing.Assertions 5 | { 6 | public class RedirectAssertionTests 7 | { 8 | [Fact] 9 | public void happy_path() 10 | { 11 | var assertion = new RedirectAssertion("/to", false); 12 | AssertionRunner 13 | .Run(assertion, x => x.Response.Redirect("/to")) 14 | .AssertAll(); 15 | } 16 | 17 | [Fact] 18 | public void sad_path_no_value() 19 | { 20 | var assertion = new RedirectAssertion("/to", false); 21 | AssertionRunner 22 | .Run(assertion, x => { }) 23 | .Messages.FirstOrDefault() 24 | .ShouldBe("Expected to be redirected to '/to' but was ''."); 25 | } 26 | 27 | [Fact] 28 | public void sad_path_wrong_value() 29 | { 30 | var assertion = new RedirectAssertion("/to", false); 31 | AssertionRunner 32 | .Run(assertion, x => x.Response.Redirect("/wrong")) 33 | .SingleMessageShouldBe("Expected to be redirected to '/to' but was '/wrong'."); 34 | } 35 | 36 | [Fact] 37 | public void happy_path_permanent() 38 | { 39 | var assertion = new RedirectAssertion("/to", true); 40 | AssertionRunner 41 | .Run(assertion, x => x.Response.Redirect("/to", true)) 42 | .AssertAll(); 43 | } 44 | 45 | [Fact] 46 | public void sad_path_permanent_wrong_value() 47 | { 48 | var assertion = new RedirectAssertion("/to", false); 49 | AssertionRunner 50 | .Run(assertion, x => x.Response.Redirect("/to", true)) 51 | .SingleMessageShouldBe("Expected status code 302, but was 301"); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Alba.Testing/Assertions/StatusCodeAssertionTests.cs: -------------------------------------------------------------------------------- 1 | using Alba.Assertions; 2 | 3 | namespace Alba.Testing.Assertions 4 | { 5 | public class StatusCodeAssertionTests 6 | { 7 | [Fact] 8 | public void happy_path() 9 | { 10 | var assertion = new StatusCodeAssertion(304); 11 | 12 | AssertionRunner.Run(assertion, _ => _.StatusCode(304)) 13 | .AssertAll(); 14 | } 15 | 16 | [Fact] 17 | public void sad_path() 18 | { 19 | var assertion = new StatusCodeAssertion(200); 20 | 21 | AssertionRunner.Run(assertion, _ => _.StatusCode(304)) 22 | .SingleMessageShouldBe("Expected status code 200, but was 304"); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/Alba.Testing/Assertions/StatusCodeSuccessAssertionTests.cs: -------------------------------------------------------------------------------- 1 | using Alba.Assertions; 2 | using System.Collections; 3 | 4 | namespace Alba.Testing.Assertions 5 | { 6 | public class StatusCodeSuccessAssertionTests 7 | { 8 | [Theory] 9 | [ClassData(typeof(SuccessStatusCodes))] 10 | public void HappyPath(int statusCode) 11 | { 12 | var assertion = new StatusCodeSuccessAssertion(); 13 | 14 | AssertionRunner.Run(assertion, _ => _.StatusCode(statusCode)) 15 | .AssertAll(); 16 | } 17 | 18 | [Theory] 19 | [ClassData(typeof(FailureStatusCodes))] 20 | public void SadPath(int statusCode) 21 | { 22 | var assertion = new StatusCodeSuccessAssertion(); 23 | 24 | AssertionRunner.Run(assertion, _ => _.StatusCode(statusCode)) 25 | .SingleMessageShouldBe($"Expected a status code between 200 and 299, but was {statusCode}"); 26 | } 27 | } 28 | 29 | 30 | 31 | } 32 | 33 | public class SuccessStatusCodes : IEnumerable 34 | { 35 | public IEnumerator GetEnumerator() 36 | { 37 | foreach (var code in Enumerable.Range(200, 99)) 38 | { 39 | yield return new object[] { code }; 40 | } 41 | } 42 | 43 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 44 | 45 | } 46 | 47 | public class FailureStatusCodes : IEnumerable 48 | { 49 | public IEnumerator GetEnumerator() 50 | { 51 | foreach (var code in Enumerable.Range(100, 99)) 52 | { 53 | yield return new object[] { code }; 54 | } 55 | foreach (var code in Enumerable.Range(300, 200)) 56 | { 57 | yield return new object[] { code }; 58 | } 59 | } 60 | 61 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 62 | 63 | } -------------------------------------------------------------------------------- /src/Alba.Testing/Assertions/sending_and_receiving_json.cs: -------------------------------------------------------------------------------- 1 | namespace Alba.Testing.Assertions 2 | { 3 | public class sending_and_receiving_json : ScenarioContext 4 | { 5 | 6 | 7 | 8 | 9 | 10 | } 11 | } -------------------------------------------------------------------------------- /src/Alba.Testing/CrudeRouter.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | using System.Reflection; 3 | using JasperFx.Core; 4 | using JasperFx.Core.Reflection; 5 | using Microsoft.AspNetCore.Http; 6 | 7 | namespace Alba.Testing 8 | { 9 | public class CrudeRouter 10 | { 11 | public readonly LightweightCache Handlers = new LightweightCache( 12 | path => throw new NotImplementedException()); 13 | 14 | 15 | public Task Invoke(HttpContext context) 16 | { 17 | return Handlers[context.Request.Path](context); 18 | } 19 | 20 | internal class Route 21 | { 22 | public Type HandlerType; 23 | public MethodInfo Method; 24 | public string Url; 25 | public string HttpMethod; 26 | } 27 | 28 | private readonly IList _routes = new List(); 29 | 30 | public void RegisterRoute(Expression> expression, string method, string route) 31 | { 32 | _routes.Add(new Route 33 | { 34 | HttpMethod = method, 35 | HandlerType = typeof(T), 36 | Url = route, 37 | Method = ReflectionHelper.GetMethod(expression) 38 | }); 39 | } 40 | 41 | public string UrlFor(Expression> expression, string httpMethod) 42 | { 43 | var method = ReflectionHelper.GetMethod(expression); 44 | 45 | var route = 46 | _routes.Single(x => x.HttpMethod == httpMethod && x.HandlerType == typeof(T) && x.Method.Name == method.Name); 47 | 48 | return route.Url; 49 | } 50 | 51 | public string UrlFor(string method) 52 | { 53 | throw new NotImplementedException(); 54 | } 55 | 56 | public string UrlFor(T input, string httpMethod) 57 | { 58 | return null; 59 | } 60 | 61 | } 62 | } -------------------------------------------------------------------------------- /src/Alba.Testing/FormDataExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using JasperFx.Core; 2 | using Microsoft.AspNetCore.Http; 3 | using Shouldly; 4 | 5 | namespace Alba.Testing 6 | { 7 | public class FormDataExtensionsTests 8 | { 9 | 10 | [Fact] 11 | public void round_trip_writing_and_parsing() 12 | { 13 | var form1 = new Dictionary 14 | { 15 | ["a"] = "what?", 16 | ["b"] = "now?", 17 | ["c"] = "really?" 18 | }; 19 | 20 | var context = new DefaultHttpContext(); 21 | using var stream = new MemoryStream(); 22 | context.Request.Body = stream; 23 | 24 | context.WriteFormData(form1); 25 | 26 | context.Request.Body.Position = 0; 27 | 28 | context.Request.Body.ReadAllText() 29 | .ShouldBe("a=what%3F&b=now%3F&c=really%3F"); 30 | 31 | } 32 | 33 | 34 | } 35 | } -------------------------------------------------------------------------------- /src/Alba.Testing/MimeTypeTests.cs: -------------------------------------------------------------------------------- 1 | using Shouldly; 2 | 3 | namespace Alba.Testing 4 | { 5 | public class MimeTypeTests 6 | { 7 | [Fact] 8 | public void find_default_extension_for_javascript() 9 | { 10 | MimeType.Javascript.DefaultExtension().ShouldBe(".js"); 11 | } 12 | 13 | [Fact] 14 | public void find_default_extension_for_css() 15 | { 16 | MimeType.Css.DefaultExtension().ShouldBe(".css"); 17 | } 18 | 19 | [Fact] 20 | public void find_default_extension_for_truetype_font() 21 | { 22 | MimeType.TrueTypeFont.DefaultExtension().ShouldBe(".ttf"); 23 | } 24 | 25 | [Fact] 26 | public void determine_mime_type_from_name_for_js() 27 | { 28 | MimeType.MimeTypeByFileName("file.coffee.js") 29 | .ShouldBe(MimeType.Javascript); 30 | } 31 | 32 | [Fact] 33 | public void determine_mime_type_from_name_for_css() 34 | { 35 | MimeType.MimeTypeByFileName("style.css") 36 | .ShouldBe(MimeType.Css); 37 | } 38 | 39 | [Fact] 40 | public void determine_mime_type_from_name_for_truetype_font() 41 | { 42 | MimeType.MimeTypeByFileName("somefont.ttf") 43 | .ShouldBe(MimeType.TrueTypeFont); 44 | } 45 | 46 | [Fact] 47 | public void determine_mime_type_for_an_extension_that_has_been_added() 48 | { 49 | MimeType.Javascript.AddExtension(".coffee"); 50 | MimeType.Css.AddExtension(".scss"); 51 | 52 | MimeType.MimeTypeByFileName("file.coffee").ShouldBe(MimeType.Javascript); 53 | MimeType.MimeTypeByFileName("file.scss").ShouldBe(MimeType.Css); 54 | } 55 | 56 | [Fact] 57 | public void can_return_null_for_a_totally_unrecognized_extension() 58 | { 59 | MimeType.MimeTypeByFileName("foo.657878XXXXXX") 60 | .ShouldBeNull(); 61 | } 62 | 63 | [Fact] 64 | public void mimetype_from_extended_extension_set() 65 | { 66 | MimeType.MimeTypeByFileName("foo.323") 67 | .Value.ShouldBe("text/h323"); 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /src/Alba.Testing/MimimalApi/end_to_end_with_json_serialization.cs: -------------------------------------------------------------------------------- 1 | using Lamar; 2 | using MinimalApiWithOakton; 3 | using Oakton; 4 | using Shouldly; 5 | 6 | namespace Alba.Testing.MimimalApi; 7 | 8 | public class end_to_end_with_json_serialization : IAsyncLifetime 9 | { 10 | private readonly ITestOutputHelper _output; 11 | private IAlbaHost _host; 12 | 13 | public end_to_end_with_json_serialization(ITestOutputHelper output) 14 | { 15 | _output = output; 16 | } 17 | 18 | public async ValueTask InitializeAsync() 19 | { 20 | OaktonEnvironment.AutoStartHost = true; 21 | _host = await AlbaHost.For(); 22 | 23 | var container = (IContainer)_host.Services; 24 | _output.WriteLine(container.WhatDoIHave()); 25 | } 26 | 27 | public async ValueTask DisposeAsync() 28 | { 29 | await _host.StopAsync(); 30 | } 31 | 32 | [Fact] 33 | public async Task automatic_json_serialization() 34 | { 35 | var guid = Guid.NewGuid(); 36 | 37 | var result = await _host.PostJson(new PostedMessage(guid), "/go") 38 | .Receive(); 39 | 40 | result.Id.ShouldBe(guid); 41 | } 42 | 43 | [Fact] 44 | public async Task automatic_json_serialization_2() 45 | { 46 | var guid = Guid.NewGuid(); 47 | 48 | var result = await _host.PostJson(new PostedMessage(guid), "/go", JsonStyle.MinimalApi) 49 | .Receive(); 50 | 51 | result.Id.ShouldBe(guid); 52 | } 53 | } -------------------------------------------------------------------------------- /src/Alba.Testing/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:14622/", 7 | "sslPort": 44321 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "Alba.Testing": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "https://localhost:5001;http://localhost:5000" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/Alba.Testing/Samples/Extensions.cs: -------------------------------------------------------------------------------- 1 | namespace Alba.Testing.Samples; 2 | 3 | public class Extensions 4 | { 5 | public async Task ConfigurationExtension() 6 | { 7 | #nullable enable 8 | #region sample_configuration_extension 9 | 10 | var configValues = new Dictionary() 11 | { 12 | { "ConnectionStrings:Postgres", "MyOverriddenValue" } 13 | }; 14 | 15 | var host = await AlbaHost.For(builder => 16 | { 17 | builder.ConfigureServices(c => 18 | { 19 | // services config 20 | }); 21 | }, ConfigurationOverride.Create(configValues)); 22 | #endregion 23 | } 24 | } -------------------------------------------------------------------------------- /src/Alba.Testing/Samples/Quickstart3.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.AspNetCore.Http; 5 | 6 | namespace Alba.Testing.Samples 7 | { 8 | public static class Program 9 | { 10 | public static IHostBuilder CreateHostBuilder(string[] args) => 11 | Host.CreateDefaultBuilder(args) 12 | .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); 13 | } 14 | 15 | public class Startup 16 | { 17 | public void Configure(IApplicationBuilder builder) 18 | { 19 | builder.Run(context => 20 | { 21 | context.Response.Headers.ContentType = "text/plain"; 22 | return context.Response.WriteAsync("Hello, World!"); 23 | }); 24 | } 25 | } 26 | 27 | public class Quickstart3 28 | { 29 | #region sample_Quickstart3 30 | [Fact] 31 | public async Task build_host_from_Program() 32 | { 33 | // Bootstrap your application just as your real application does 34 | var hostBuilder = Program.CreateHostBuilder(Array.Empty()); 35 | 36 | await using var host = new AlbaHost(hostBuilder); 37 | 38 | // Just as a sample, I'll run a scenario against 39 | // a "hello, world" application's root url 40 | await host.Scenario(s => 41 | { 42 | s.Get.Url("/"); 43 | s.ContentShouldBe("Hello, World!"); 44 | }); 45 | } 46 | #endregion 47 | 48 | 49 | #region sample_shorthand_bootstrapping 50 | [Fact] 51 | public async Task fluent_interface_bootstrapping() 52 | { 53 | await using var host = await Program 54 | .CreateHostBuilder(Array.Empty()) 55 | .StartAlbaAsync(); 56 | 57 | // Just as a sample, I'll run a scenario against 58 | // a "hello, world" application's root url 59 | await host.Scenario(s => 60 | { 61 | s.Get.Url("/"); 62 | s.ContentShouldBe("Hello, World!"); 63 | }); 64 | } 65 | #endregion 66 | 67 | } 68 | 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/Alba.Testing/Samples/Redirects.cs: -------------------------------------------------------------------------------- 1 | namespace Alba.Testing.Samples 2 | { 3 | public class Redirects 4 | { 5 | #region sample_asserting_redirects 6 | public async Task asserting_redirects(IAlbaHost system) 7 | { 8 | await system.Scenario(_ => 9 | { 10 | // should redirect to the url 11 | _.RedirectShouldBe("/redirect"); 12 | 13 | // should redirect permanently to the url 14 | _.RedirectPermanentShouldBe("/redirect"); 15 | }); 16 | } 17 | #endregion 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Alba.Testing/Samples/SnapshotTesting.SnapshotTest.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | Context: { 3 | Request: { 4 | Headers: { 5 | Accept: application/json, 6 | Content-Length: 67, 7 | Content-Type: application/json, 8 | Host: localhost 9 | } 10 | }, 11 | Response: { 12 | StatusCode: OK, 13 | Headers: { 14 | Content-Type: application/json; charset=utf-8 15 | }, 16 | Value: { 17 | id: Guid_1, 18 | myValue: SomeValue 19 | } 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/Alba.Testing/Samples/SnapshotTesting.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace Alba.Testing.Samples; 4 | 5 | public static class ModuleInitializer 6 | { 7 | [ModuleInitializer] 8 | public static void Initialize() => 9 | VerifierSettings.InitializePlugins(); 10 | } 11 | 12 | public class SnapshotTesting 13 | { 14 | [Fact] 15 | public async Task SnapshotTest() 16 | { 17 | #region sample_snapshot_testing 18 | await using var host = await AlbaHost.For(); 19 | 20 | var scenario = await host.Scenario(s => 21 | { 22 | s.Post.Json(new MyEntity(Guid.NewGuid(), "SomeValue")).ToUrl("/json"); 23 | }); 24 | 25 | await Verify(scenario); 26 | #endregion 27 | } 28 | } -------------------------------------------------------------------------------- /src/Alba.Testing/Samples/StatusCodes.cs: -------------------------------------------------------------------------------- 1 | namespace Alba.Testing.Samples 2 | { 3 | public class StatusCodes 4 | { 5 | #region sample_check_the_status_code 6 | public async Task check_the_status(IAlbaHost system) 7 | { 8 | await system.Scenario(_ => 9 | { 10 | // Shorthand for saying that the StatusCode should be 200 11 | _.StatusCodeShouldBeOk(); 12 | 13 | // Or a specific status code 14 | _.StatusCodeShouldBe(403); 15 | 16 | // Ignore the status code altogether 17 | _.IgnoreStatusCode(); 18 | }); 19 | } 20 | #endregion 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Alba.Testing/Samples/Urls.cs: -------------------------------------------------------------------------------- 1 | namespace Alba.Testing.Samples 2 | { 3 | public class Urls 4 | { 5 | #region sample_specify_the_url_directly 6 | public async Task specify_url(AlbaHost system) 7 | { 8 | await system.Scenario(_ => 9 | { 10 | // Directly specify the Url against a given 11 | // HTTP method 12 | _.Get.Url("/"); 13 | _.Put.Url("/"); 14 | _.Post.Url("/"); 15 | _.Delete.Url("/"); 16 | _.Head.Url("/"); 17 | }); 18 | } 19 | #endregion 20 | 21 | 22 | } 23 | 24 | public class InputModel 25 | { 26 | public string Id; 27 | } 28 | 29 | public class MyController 30 | { 31 | public string Get() 32 | { 33 | return "something"; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Alba.Testing/ScenarioAssertionExceptionTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Shouldly; 3 | 4 | namespace Alba.Testing 5 | { 6 | public class ScenarioAssertionExceptionTests 7 | { 8 | [Fact] 9 | public void assert_does_nothing_with_no_messages() 10 | { 11 | var ex = new ScenarioAssertionException(); 12 | 13 | ex.AssertAll(); // all good! 14 | } 15 | 16 | [Fact] 17 | public void assert_with_any_messages_blows_up() 18 | { 19 | var ex = new ScenarioAssertionException(); 20 | ex.Add("You stink!"); 21 | 22 | Exception.ShouldBeThrownBy(() => ex.AssertAll()); 23 | } 24 | 25 | [Fact] 26 | public void all_messages_are_in_the_ex_message() 27 | { 28 | var ex = new ScenarioAssertionException(); 29 | ex.Add("You stink!"); 30 | ex.Add("You missed a header!"); 31 | 32 | ex.Message.ShouldContain("You stink!"); 33 | ex.Message.ShouldContain("You missed a header!"); 34 | } 35 | 36 | [Fact] 37 | public void show_the_body_in_the_message_if_set() 38 | { 39 | var ex = new ScenarioAssertionException(); 40 | var ctx = new DefaultHttpContext(); 41 | ctx.Response.Body = new MemoryStream(); 42 | var body = ""; 43 | using var sw = new StreamWriter(ctx.Response.Body); 44 | sw.Write(body); 45 | sw.Flush(); 46 | 47 | var context = new AssertionContext(ctx, ex); 48 | ex.Add("You stink!"); 49 | 50 | ex.Message.ShouldNotContain("Actual body text was:"); 51 | 52 | context.ReadBodyAsString(); 53 | ex.Message.ShouldContain("Actual body text was:"); 54 | ex.Message.ShouldContain(body); 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /src/Alba.Testing/ScenarioContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Hosting; 5 | 6 | namespace Alba.Testing 7 | { 8 | public class ScenarioContext : IDisposable 9 | { 10 | protected CrudeRouter router = new CrudeRouter(); 11 | protected readonly IAlbaHost host; 12 | 13 | public ScenarioContext() 14 | { 15 | host = new AlbaHost(Host.CreateDefaultBuilder() 16 | .ConfigureServices((s) => s.AddMvcCore()) 17 | .ConfigureWebHostDefaults(c => 18 | c.Configure(app => 19 | { 20 | app.Run(router.Invoke); 21 | }))); 22 | } 23 | 24 | 25 | protected Task fails(Action configuration) 26 | { 27 | return Exception.ShouldBeThrownBy(() => host.Scenario(configuration)); 28 | } 29 | 30 | public void Dispose() 31 | { 32 | host?.Dispose(); 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/Alba.Testing/Security/IdentityServerFixture.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.Testing; 2 | using Microsoft.AspNetCore.TestHost; 3 | 4 | namespace Alba.Testing.Security 5 | { 6 | [CollectionDefinition("OIDC")] 7 | public class IdentityServerCollection : ICollectionFixture 8 | { 9 | 10 | } 11 | 12 | public class IdentityServerFixture : IAsyncLifetime 13 | { 14 | public TestServer IdentityServer { get; set; } 15 | public ValueTask InitializeAsync() 16 | { 17 | IdentityServer = new WebApplicationFactory().Server; 18 | return ValueTask.CompletedTask; 19 | } 20 | 21 | public ValueTask DisposeAsync() 22 | { 23 | IdentityServer.Dispose(); 24 | return ValueTask.CompletedTask; 25 | } 26 | 27 | } 28 | } -------------------------------------------------------------------------------- /src/Alba.Testing/TestImage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasperFx/alba/72219db976bdced2997fbed14e8e6f99a2369623/src/Alba.Testing/TestImage.jpg -------------------------------------------------------------------------------- /src/Alba.Testing/TestTextFile.txt: -------------------------------------------------------------------------------- 1 | Hello there! -------------------------------------------------------------------------------- /src/Alba.Testing/using_json_helpers.cs: -------------------------------------------------------------------------------- 1 | using Shouldly; 2 | using WebApp.Controllers; 3 | 4 | namespace Alba.Testing 5 | { 6 | public class using_json_helpers 7 | { 8 | #region sample_get_json 9 | [Fact] 10 | public async Task get_happy_path() 11 | { 12 | await using var system = await AlbaHost.For(); 13 | 14 | // Issue a request, and check the results 15 | var result = await system.GetAsJson("/math/add/3/4"); 16 | 17 | result.Answer.ShouldBe(7); 18 | } 19 | #endregion 20 | 21 | #region sample_post_json_get_json 22 | [Fact] 23 | public async Task post_and_expect_response() 24 | { 25 | await using var system = await AlbaHost.For(); 26 | var request = new OperationRequest 27 | { 28 | Type = OperationType.Multiply, 29 | One = 3, 30 | Two = 4 31 | }; 32 | 33 | var result = await system.PostJson(request, "/math") 34 | .Receive(); 35 | 36 | result.Answer.ShouldBe(12); 37 | result.Method.ShouldBe("POST"); 38 | } 39 | #endregion 40 | 41 | [Fact] 42 | public async Task put_and_expect_response() 43 | { 44 | await using var system = await AlbaHost.For(); 45 | var request = new OperationRequest 46 | { 47 | Type = OperationType.Subtract, 48 | One = 3, 49 | Two = 4 50 | }; 51 | 52 | var result = await system.PutJson(request, "/math") 53 | .Receive(); 54 | 55 | result.Answer.ShouldBe(-1); 56 | result.Method.ShouldBe("PUT"); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Alba.Testing/xunit.runner.json: -------------------------------------------------------------------------------- 1 | { 2 | "parallelizeTestCollections": false 3 | } -------------------------------------------------------------------------------- /src/Alba/Alba.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Supercharged integration testing for ASP.NET Core HTTP endpoints 5 | 8.2.0 6 | net8.0;net9.0 7 | http://jasperfx.github.io/alba/ 8 | Apache-2.0 9 | git 10 | git://github.com/JasperFx/alba 11 | library 12 | icon.png 13 | enable 14 | enable 15 | true 16 | CS1591; 17 | README.md 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 | -------------------------------------------------------------------------------- /src/Alba/AlbaJsonFormatterException.cs: -------------------------------------------------------------------------------- 1 | namespace Alba; 2 | 3 | public sealed class AlbaJsonFormatterException : Exception 4 | { 5 | public AlbaJsonFormatterException(string json) : base($"The JSON formatter was unable to process the raw JSON:\n{json}") 6 | { 7 | } 8 | } -------------------------------------------------------------------------------- /src/Alba/AlbaWebApplicationFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.AspNetCore.Mvc.Testing; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Hosting; 5 | 6 | namespace Alba; 7 | 8 | /// 9 | internal sealed class AlbaWebApplicationFactory : WebApplicationFactory, IAlbaWebApplicationFactory where TEntryPoint : class 10 | { 11 | private readonly Action _configuration; 12 | private readonly IAlbaExtension[] _extensions; 13 | public AlbaWebApplicationFactory(Action configuration, IAlbaExtension[] extensions) 14 | { 15 | _configuration = configuration; 16 | _extensions = extensions; 17 | } 18 | 19 | protected override void ConfigureWebHost(IWebHostBuilder builder) 20 | { 21 | builder.ConfigureServices(services => 22 | { 23 | services.AddHttpContextAccessor(); 24 | }); 25 | 26 | _configuration(builder); 27 | 28 | base.ConfigureWebHost(builder); 29 | } 30 | 31 | protected override IHost CreateHost(IHostBuilder builder) 32 | { 33 | foreach (var extension in _extensions) 34 | { 35 | extension.Configure(builder); 36 | } 37 | 38 | return base.CreateHost(builder); 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /src/Alba/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Alba.Testing")] 2 | 3 | -------------------------------------------------------------------------------- /src/Alba/AssertionContext.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Text; 3 | using Alba.Internal; 4 | using Microsoft.AspNetCore.Http; 5 | 6 | namespace Alba; 7 | 8 | public sealed class AssertionContext 9 | { 10 | private readonly ScenarioAssertionException _assertionException; 11 | public HttpContext HttpContext { get; } 12 | public AssertionContext(HttpContext httpContext, ScenarioAssertionException assertionException) 13 | { 14 | HttpContext = httpContext; 15 | _assertionException = assertionException; 16 | } 17 | 18 | /// 19 | /// Add an assertion failure message 20 | /// 21 | /// 22 | public void AddFailure(string message) => _assertionException.Add(message); 23 | 24 | private string? _body; 25 | 26 | /// 27 | /// Reads the response body and returns it as a string 28 | /// 29 | /// A string with the content of the body 30 | [MemberNotNull(nameof(_body))] 31 | public string ReadBodyAsString() 32 | { 33 | // Hardening for GH-95 34 | try 35 | { 36 | var stream = HttpContext.Response.Body; 37 | if (_body == null) 38 | { 39 | if (stream.CanSeek) 40 | { 41 | stream.Position = 0; 42 | } 43 | 44 | _body = stream.ReadAllText(); 45 | 46 | // reset the position so users can do follow up activities without tripping up. 47 | if (stream.CanSeek) 48 | { 49 | stream.Position = 0; 50 | } 51 | 52 | _assertionException.AddBody(_body); 53 | } 54 | } 55 | catch (Exception) 56 | { 57 | _body = string.Empty; 58 | } 59 | 60 | return _body; 61 | } 62 | 63 | 64 | } -------------------------------------------------------------------------------- /src/Alba/Assertions/BodyContainsAssertion.cs: -------------------------------------------------------------------------------- 1 | namespace Alba.Assertions; 2 | 3 | #region sample_BodyContainsAssertion 4 | internal sealed class BodyContainsAssertion : IScenarioAssertion 5 | { 6 | public string Text { get; set; } 7 | 8 | public BodyContainsAssertion(string text) 9 | { 10 | Text = text; 11 | } 12 | 13 | public void Assert(Scenario scenario, AssertionContext context) 14 | { 15 | // Context has this useful extension to read the body as a string. 16 | // This will bake the body contents into the exception message to make debugging easier. 17 | var body = context.ReadBodyAsString(); 18 | if (!body.Contains(Text)) 19 | { 20 | // Add the failure message to the exception. This exception only 21 | // gets thrown if there are failures. 22 | context.AddFailure($"Expected text '{Text}' was not found in the response body"); 23 | } 24 | } 25 | } 26 | #endregion -------------------------------------------------------------------------------- /src/Alba/Assertions/BodyDoesNotContainAssertion.cs: -------------------------------------------------------------------------------- 1 | namespace Alba.Assertions; 2 | 3 | internal sealed class BodyDoesNotContainAssertion : IScenarioAssertion 4 | { 5 | public string Text { get; set; } 6 | 7 | public BodyDoesNotContainAssertion(string text) 8 | { 9 | Text = text; 10 | } 11 | 12 | public void Assert(Scenario scenario, AssertionContext context) 13 | { 14 | var body = context.ReadBodyAsString(); 15 | if (body.Contains(Text)) 16 | { 17 | context.AddFailure($"Text '{Text}' should not be found in the response body"); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/Alba/Assertions/BodyTextAssertion.cs: -------------------------------------------------------------------------------- 1 | namespace Alba.Assertions; 2 | 3 | internal sealed class BodyTextAssertion : IScenarioAssertion 4 | { 5 | public string Text { get; set; } 6 | 7 | public BodyTextAssertion(string text) 8 | { 9 | Text = text; 10 | } 11 | 12 | public void Assert(Scenario scenario, AssertionContext context) 13 | { 14 | var body = context.ReadBodyAsString(); 15 | if (!body.Equals(Text)) 16 | { 17 | context.AddFailure($"Expected the content to be '{Text}'"); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/Alba/Assertions/HasSingleHeaderValueAssertion.cs: -------------------------------------------------------------------------------- 1 | namespace Alba.Assertions; 2 | 3 | internal sealed class HasSingleHeaderValueAssertion : IScenarioAssertion 4 | { 5 | private readonly string _headerKey; 6 | 7 | public HasSingleHeaderValueAssertion(string headerKey) 8 | { 9 | _headerKey = headerKey; 10 | } 11 | 12 | public void Assert(Scenario scenario, AssertionContext context) 13 | { 14 | var values = context.HttpContext.Response.Headers[_headerKey]; 15 | 16 | switch (values.Count) 17 | { 18 | case 0: 19 | context.AddFailure( 20 | $"Expected a single header value of '{_headerKey}', but no values were found on the response"); 21 | break; 22 | case 1: 23 | // nothing, thats' good;) 24 | break; 25 | 26 | default: 27 | var valueText = values.Select(x => "'" + x + "'").Aggregate((s1, s2) => $"{s1}, {s2}"); 28 | context.AddFailure($"Expected a single header value of '{_headerKey}', but found multiple values on the response: {valueText}"); 29 | break; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/Alba/Assertions/HeaderExistsAssertion.cs: -------------------------------------------------------------------------------- 1 | namespace Alba.Assertions; 2 | 3 | internal sealed class HeaderExistsAssertion : IScenarioAssertion 4 | { 5 | private readonly string _headerKey; 6 | 7 | public HeaderExistsAssertion(string headerKey) 8 | { 9 | _headerKey = headerKey; 10 | } 11 | 12 | public void Assert(Scenario scenario, AssertionContext context) 13 | { 14 | var values = context.HttpContext.Response.Headers[_headerKey]; 15 | 16 | if (values.Count == 0) 17 | { 18 | context.AddFailure($"Expected header '{_headerKey}' to be present but no values were found on the response."); 19 | } 20 | 21 | } 22 | } -------------------------------------------------------------------------------- /src/Alba/Assertions/HeaderMatchAssertion.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace Alba.Assertions; 4 | 5 | internal sealed class HeaderMatchAssertion : IScenarioAssertion 6 | { 7 | private readonly string _headerKey; 8 | private readonly Regex _regex; 9 | 10 | public HeaderMatchAssertion(string headerKey, Regex regex) 11 | { 12 | _headerKey = headerKey; 13 | _regex = regex; 14 | } 15 | 16 | public void Assert(Scenario scenario, AssertionContext context) 17 | { 18 | var values = context.HttpContext.Response.Headers[_headerKey]; 19 | 20 | switch (values.Count) 21 | { 22 | case 0: 23 | context.AddFailure($"Expected a single header value of '{_headerKey}' matching '{_regex}', but no values were found on the response"); 24 | break; 25 | 26 | case 1: 27 | var actual = values.Single(); 28 | if (_regex.IsMatch(actual) == false) 29 | { 30 | context.AddFailure($"Expected a single header value of '{_headerKey}' matching '{_regex}', but the actual value was '{actual}'"); 31 | } 32 | break; 33 | 34 | default: 35 | var valueText = values.Select(x => "'" + x + "'").Aggregate((s1, s2) => $"{s1}, {s2}"); 36 | context.AddFailure($"Expected a single header value of '{_headerKey}' matching '{_regex}', but the actual values were {valueText}"); 37 | break; 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/Alba/Assertions/HeaderMultiValueAssertion.cs: -------------------------------------------------------------------------------- 1 | namespace Alba.Assertions; 2 | 3 | internal sealed class HeaderMultiValueAssertion : IScenarioAssertion 4 | { 5 | private readonly string _headerKey; 6 | private readonly List _expected; 7 | 8 | public HeaderMultiValueAssertion(string headerKey, IEnumerable expected) 9 | { 10 | _headerKey = headerKey; 11 | _expected = expected.ToList(); 12 | } 13 | 14 | public void Assert(Scenario scenario, AssertionContext context) 15 | { 16 | var values = context.HttpContext.Response.Headers[_headerKey]; 17 | var expectedText = _expected.Select(x => "'" + x + "'").Aggregate((s1, s2) => $"{s1}, {s2}"); 18 | 19 | switch (values.Count) 20 | { 21 | case 0: 22 | context.AddFailure($"Expected header values of '{_headerKey}'={expectedText}, but no values were found on the response."); 23 | break; 24 | 25 | default: 26 | if (!_expected.All(x => values.Contains(x))) 27 | { 28 | var valueText = values.Select(x => "'" + x + "'").Aggregate((s1, s2) => $"{s1}, {s2}"); 29 | context.AddFailure($"Expected header values of '{_headerKey}'={expectedText}, but the actual values were {valueText}."); 30 | } 31 | break; 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/Alba/Assertions/HeaderValueAssertion.cs: -------------------------------------------------------------------------------- 1 | namespace Alba.Assertions; 2 | 3 | internal sealed class HeaderValueAssertion : IScenarioAssertion 4 | { 5 | private readonly string _headerKey; 6 | private readonly string _expected; 7 | 8 | public HeaderValueAssertion(string headerKey, string expected) 9 | { 10 | _headerKey = headerKey; 11 | _expected = expected; 12 | } 13 | 14 | public void Assert(Scenario scenario, AssertionContext context) 15 | { 16 | var values = context.HttpContext.Response.Headers[_headerKey]; 17 | 18 | switch (values.Count) 19 | { 20 | case 0: 21 | context.AddFailure($"Expected a single header value of '{_headerKey}'='{_expected}', but no values were found on the response"); 22 | break; 23 | 24 | case 1: 25 | var actual = values.Single(); 26 | if (actual != _expected) 27 | { 28 | context.AddFailure($"Expected a single header value of '{_headerKey}'='{_expected}', but the actual value was '{actual}'"); 29 | } 30 | break; 31 | 32 | default: 33 | var valueText = values.Select(x => "'" + x + "'").Aggregate((s1, s2) => $"{s1}, {s2}"); 34 | context.AddFailure($"Expected a single header value of '{_headerKey}'='{_expected}', but the actual values were {valueText}"); 35 | break; 36 | } 37 | 38 | } 39 | } -------------------------------------------------------------------------------- /src/Alba/Assertions/NoHeaderValueAssertion.cs: -------------------------------------------------------------------------------- 1 | namespace Alba.Assertions; 2 | 3 | internal sealed class NoHeaderValueAssertion : IScenarioAssertion 4 | { 5 | private readonly string _headerKey; 6 | 7 | public NoHeaderValueAssertion(string headerKey) 8 | { 9 | _headerKey = headerKey; 10 | } 11 | 12 | public void Assert(Scenario scenario, AssertionContext context) 13 | { 14 | var headers = context.HttpContext.Response.Headers; 15 | if (headers.TryGetValue(_headerKey, out var values)) 16 | { 17 | var valueText = values.Select(x => "'" + x + "'").Aggregate((s1, s2) => $"{s1}, {s2}"); 18 | context.AddFailure($"Expected no value for header '{_headerKey}', but found values {valueText}"); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/Alba/Assertions/RedirectAssertion.cs: -------------------------------------------------------------------------------- 1 | namespace Alba.Assertions; 2 | 3 | internal sealed class RedirectAssertion : IScenarioAssertion 4 | { 5 | public RedirectAssertion(string expected, bool permanent) 6 | { 7 | Expected = expected; 8 | Permanent = permanent; 9 | } 10 | 11 | public string Expected { get; } 12 | public bool Permanent { get; } 13 | 14 | public void Assert(Scenario scenario, AssertionContext context) 15 | { 16 | var location = context.HttpContext.Response.Headers.Location; 17 | if (!string.Equals(location, Expected, StringComparison.OrdinalIgnoreCase)) 18 | { 19 | context.AddFailure($"Expected to be redirected to '{Expected}' but was '{location}'."); 20 | } 21 | 22 | new StatusCodeAssertion(Permanent ? 301 : 302).Assert(scenario, context); 23 | } 24 | } -------------------------------------------------------------------------------- /src/Alba/Assertions/StatusCodeAssertion.cs: -------------------------------------------------------------------------------- 1 | namespace Alba.Assertions; 2 | 3 | #region sample_StatusCodeAssertion 4 | internal sealed class StatusCodeAssertion : IScenarioAssertion 5 | { 6 | public int Expected { get; set; } 7 | 8 | public StatusCodeAssertion(int expected) 9 | { 10 | Expected = expected; 11 | } 12 | 13 | public void Assert(Scenario scenario, AssertionContext context) 14 | { 15 | var statusCode = context.HttpContext.Response.StatusCode; 16 | if (statusCode != Expected) 17 | { 18 | context.AddFailure($"Expected status code {Expected}, but was {statusCode}"); 19 | 20 | context.ReadBodyAsString(); 21 | } 22 | } 23 | } 24 | #endregion -------------------------------------------------------------------------------- /src/Alba/Assertions/StatusCodeSuccessAssertion.cs: -------------------------------------------------------------------------------- 1 | namespace Alba.Assertions; 2 | 3 | public sealed class StatusCodeSuccessAssertion : IScenarioAssertion 4 | { 5 | public void Assert(Scenario scenario, AssertionContext context) 6 | { 7 | var statusCode = context.HttpContext.Response.StatusCode; 8 | if(statusCode < 200 || statusCode >= 300) 9 | { 10 | context.AddFailure($"Expected a status code between 200 and 299, but was {statusCode}"); 11 | context.ReadBodyAsString(); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/Alba/ConfigurationOverride.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.Hosting; 3 | 4 | namespace Alba; 5 | 6 | /// 7 | /// Used to override configuration values for tests. Required due to https://github.com/dotnet/aspnetcore/issues/37680 8 | /// 9 | public sealed class ConfigurationOverride : IAlbaExtension 10 | { 11 | private readonly IEnumerable> _configDictionary; 12 | 13 | internal ConfigurationOverride(IEnumerable> configDictionary) 14 | { 15 | _configDictionary = configDictionary; 16 | } 17 | 18 | public static ConfigurationOverride Create(IEnumerable> configDictionary) 19 | { 20 | return new ConfigurationOverride(configDictionary); 21 | } 22 | 23 | public void Dispose() 24 | { 25 | 26 | } 27 | 28 | public ValueTask DisposeAsync() 29 | { 30 | return ValueTask.CompletedTask; 31 | } 32 | 33 | public Task Start(IAlbaHost host) 34 | { 35 | return Task.CompletedTask; 36 | } 37 | 38 | public IHostBuilder Configure(IHostBuilder builder) 39 | { 40 | return builder.ConfigureHostConfiguration(config => 41 | { 42 | config.AddInMemoryCollection(_configDictionary); 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Alba/EmptyResponseException.cs: -------------------------------------------------------------------------------- 1 | namespace Alba; 2 | 3 | public class EmptyResponseException : Exception 4 | { 5 | public EmptyResponseException() : base("There is no content in the Response.Body") 6 | { 7 | } 8 | } -------------------------------------------------------------------------------- /src/Alba/FormDataExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | 3 | namespace Alba; 4 | 5 | public static class FormDataExtensions 6 | { 7 | /// 8 | /// Write the dictionary values to the HttpContext.Request.Body. 9 | /// Also sets content-length and content-type header to 10 | /// application/x-www-form-urlencoded 11 | /// 12 | /// 13 | /// 14 | public static void WriteFormData(this HttpContext context, Dictionary values) 15 | { 16 | using var form = new FormUrlEncodedContent(values); 17 | 18 | form.CopyTo(context.Request.Body, null, CancellationToken.None); 19 | 20 | context.Request.Headers.ContentType = form.Headers.ContentType!.ToString(); 21 | context.Request.Headers.ContentLength = form.Headers.ContentLength; 22 | 23 | } 24 | 25 | 26 | /// 27 | /// Writes the to the provided HttpContext, along with the 28 | /// required headers. 29 | /// 30 | /// 31 | /// 32 | public static void WriteMultipartFormData(this HttpContext context, MultipartFormDataContent content) 33 | { 34 | content.CopyTo(context.Request.Body, null, CancellationToken.None); 35 | foreach (var kv in content.Headers) 36 | { 37 | context.Request.Headers.Append(kv.Key, kv.Value.ToArray()); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/Alba/HeaderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.Net.Http.Headers; 3 | 4 | namespace Alba; 5 | 6 | public static class HeaderExtensions 7 | { 8 | /// 9 | /// Get the content-length header value 10 | /// 11 | /// 12 | /// 13 | public static long? ContentLength(this IHeaderDictionary headers) 14 | { 15 | return headers.ContentLength; 16 | } 17 | 18 | /// 19 | /// Set the content-length header value 20 | /// 21 | /// 22 | /// 23 | public static void ContentLength(this IHeaderDictionary headers, long? value) 24 | { 25 | if (value.HasValue) 26 | { 27 | headers[HeaderNames.ContentLength] = FormatInt64(value.Value); 28 | } 29 | else 30 | { 31 | headers.Remove(HeaderNames.ContentLength); 32 | } 33 | } 34 | 35 | private static bool TryParseInt64(string input, out long value) { 36 | return HeaderUtilities.TryParseNonNegativeInt64(input, out value); 37 | } 38 | private static string FormatInt64(long input) { 39 | return HeaderUtilities.FormatNonNegativeInt64(input); 40 | } 41 | } -------------------------------------------------------------------------------- /src/Alba/HttpContextExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Text; 3 | using Alba.Internal; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.WebUtilities; 6 | 7 | namespace Alba; 8 | 9 | public static class HttpContextExtensions 10 | { 11 | public static void ContentType(this HttpRequest request, string mimeType) 12 | { 13 | request.ContentType = mimeType; 14 | } 15 | 16 | public static void ContentType(this HttpResponse response, string mimeType) 17 | { 18 | response.Headers.ContentType = mimeType; 19 | } 20 | 21 | public static void RelativeUrl(this HttpContext context, [StringSyntax(StringSyntaxAttribute.Uri)]string? relativeUrl) 22 | { 23 | if (relativeUrl != null && relativeUrl.Contains("?")) 24 | { 25 | var parts = relativeUrl.Trim().Split('?'); 26 | context.Request.Path = parts[0]; 27 | 28 | if (parts[1].IsNotEmpty()) 29 | { 30 | context.Request.QueryString = QueryString.Create(QueryHelpers.ParseQuery(parts[1])); 31 | } 32 | } 33 | else 34 | { 35 | context.Request.Path = relativeUrl; 36 | } 37 | } 38 | 39 | /// 40 | /// Set the Authorization header value to "Bearer [jwt]" 41 | /// 42 | /// 43 | /// 44 | public static void SetBearerToken(this HttpContext context, string jwt) 45 | { 46 | context.Request.Headers.Authorization = $"Bearer {jwt}"; 47 | } 48 | 49 | public static void Accepts(this HttpContext context, string mimeType) 50 | { 51 | context.Request.Headers.Accept = mimeType; 52 | } 53 | 54 | public static void HttpMethod(this HttpContext context, string method) 55 | { 56 | context.Request.Method = method; 57 | } 58 | 59 | public static void StatusCode(this HttpContext context, int statusCode) 60 | { 61 | context.Response.StatusCode = statusCode; 62 | } 63 | 64 | public static void Write(this HttpResponse response, string content) 65 | { 66 | var bytes = Encoding.UTF8.GetBytes(content); 67 | response.Body.Write(bytes, 0, bytes.Length); 68 | response.Body.Flush(); 69 | } 70 | 71 | } -------------------------------------------------------------------------------- /src/Alba/HttpRequestBody.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Xml.Serialization; 3 | using Microsoft.AspNetCore.Http; 4 | 5 | namespace Alba; 6 | 7 | public class HttpRequestBody 8 | { 9 | private readonly Scenario _parent; 10 | 11 | internal HttpRequestBody(Scenario parent) 12 | { 13 | _parent = parent; 14 | } 15 | 16 | public void XmlInputIs(object target) 17 | { 18 | using var writer = new StringWriter(); 19 | 20 | var serializer = new XmlSerializer(target.GetType()); 21 | serializer.Serialize(writer, target); 22 | var xml = writer.ToString(); 23 | var bytes = Encoding.UTF8.GetBytes(xml); 24 | 25 | _parent.ConfigureHttpContext(context => 26 | { 27 | var stream = context.Request.Body; 28 | stream.Write(bytes, 0, bytes.Length); 29 | stream.Position = 0; 30 | 31 | context.Request.ContentType = MimeType.Xml.Value; 32 | context.Accepts(MimeType.Xml.Value); 33 | context.Request.ContentLength = xml.Length; 34 | }); 35 | } 36 | 37 | private static void WriteTextToBody(string json, HttpContext context) 38 | { 39 | var stream = context.Request.Body; 40 | 41 | var writer = new StreamWriter(stream); 42 | writer.Write(json); 43 | writer.Flush(); 44 | 45 | stream.Position = 0; 46 | 47 | context.Request.ContentLength = stream.Length; 48 | } 49 | 50 | public void WriteFormData(Dictionary input) 51 | { 52 | _parent.ConfigureHttpContext(context => 53 | { 54 | context.WriteFormData(input); 55 | }); 56 | } 57 | 58 | public void WriteMultipartFormData(MultipartFormDataContent content) 59 | { 60 | _parent.ConfigureHttpContext(context => 61 | { 62 | context.WriteMultipartFormData(content); 63 | }); 64 | } 65 | 66 | public void TextIs(string body) 67 | { 68 | _parent.ConfigureHttpContext(context => 69 | { 70 | WriteTextToBody(body, context); 71 | context.Request.ContentType = MimeType.Text.Value; 72 | context.Request.ContentLength = body.Length; 73 | }); 74 | } 75 | } -------------------------------------------------------------------------------- /src/Alba/IAlbaExtension.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Hosting; 2 | 3 | namespace Alba; 4 | 5 | #region sample_IAlbaExtension 6 | 7 | /// 8 | /// Models an extension to an AlbaHost 9 | /// 10 | public interface IAlbaExtension : IDisposable, IAsyncDisposable 11 | { 12 | /// 13 | /// Called during the initialization of an AlbaHost after the application is started, 14 | /// so the application DI container is available. Useful for registering setup or teardown 15 | /// actions on an AlbaHOst 16 | /// 17 | /// 18 | /// 19 | Task Start(IAlbaHost host); 20 | 21 | /// 22 | /// Allow an extension to alter the application's 23 | /// IHostBuilder prior to starting the application 24 | /// 25 | /// 26 | /// 27 | IHostBuilder Configure(IHostBuilder builder); 28 | } 29 | 30 | #endregion -------------------------------------------------------------------------------- /src/Alba/IAlbaHost.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.AspNetCore.TestHost; 3 | using Microsoft.Extensions.Hosting; 4 | 5 | namespace Alba; 6 | 7 | public interface IAlbaHost : IHost, IAsyncDisposable 8 | { 9 | /// 10 | /// Define and execute an integration test by running an Http request through 11 | /// your ASP.Net Core system 12 | /// 13 | /// 14 | /// 15 | /// 16 | Task Scenario(Action configure); 17 | 18 | /// 19 | /// Execute some kind of action before each scenario. This is NOT additive 20 | /// 21 | /// 22 | /// 23 | IAlbaHost BeforeEach(Action beforeEach); 24 | 25 | /// 26 | /// Execute some clean up action immediately after executing each HTTP execution. This is NOT additive 27 | /// 28 | /// 29 | /// 30 | IAlbaHost AfterEach(Action afterEach); 31 | 32 | /// 33 | /// Run some kind of set up action immediately before executing an HTTP request 34 | /// 35 | /// 36 | /// 37 | IAlbaHost BeforeEachAsync(Func beforeEach); 38 | 39 | /// 40 | /// Execute some clean up action immediately after executing each HTTP execution. This is NOT additive 41 | /// 42 | /// 43 | /// 44 | IAlbaHost AfterEachAsync(Func afterEach); 45 | 46 | /// 47 | /// The underlying TestServer for additional functionality 48 | /// 49 | TestServer Server { get; } 50 | 51 | 52 | } -------------------------------------------------------------------------------- /src/Alba/IAlbaWebApplicationFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.TestHost; 2 | 3 | namespace Alba; 4 | 5 | internal interface IAlbaWebApplicationFactory : IDisposable, IAsyncDisposable 6 | { 7 | public TestServer Server { get; } 8 | public IServiceProvider Services { get; } 9 | } -------------------------------------------------------------------------------- /src/Alba/IScenarioAssertion.cs: -------------------------------------------------------------------------------- 1 | namespace Alba; 2 | 3 | #region sample_IScenarioAssertion 4 | public interface IScenarioAssertion 5 | { 6 | void Assert(Scenario scenario, AssertionContext context); 7 | } 8 | #endregion -------------------------------------------------------------------------------- /src/Alba/IScenarioResult.cs: -------------------------------------------------------------------------------- 1 | using System.Xml; 2 | using Microsoft.AspNetCore.Http; 3 | 4 | namespace Alba; 5 | 6 | #region sample_IScenarioResult 7 | public interface IScenarioResult 8 | { 9 | /// 10 | /// The raw HttpContext used during the scenario 11 | /// 12 | HttpContext Context { get; } 13 | 14 | /// 15 | /// Read the contents of the HttpResponse.Body as text 16 | /// 17 | /// 18 | string ReadAsText(); 19 | 20 | /// 21 | /// Read the contents of the HttpResponse.Body as text 22 | /// 23 | /// 24 | Task ReadAsTextAsync(); 25 | 26 | /// 27 | /// Read the contents of the HttpResponse.Body into an XmlDocument object 28 | /// 29 | /// 30 | XmlDocument? ReadAsXml(); 31 | 32 | /// 33 | /// Read the contents of the HttpResponse.Body into an XmlDocument object 34 | /// 35 | /// 36 | Task ReadAsXmlAsync(); 37 | 38 | /// 39 | /// Deserialize the contents of the HttpResponse.Body into an object 40 | /// of type T using the built in XmlSerializer 41 | /// 42 | /// 43 | /// 44 | T? ReadAsXml() where T : class; 45 | 46 | /// 47 | /// Deserialize the contents of the HttpResponse.Body into an object 48 | /// of type T using the configured Json serializer 49 | /// 50 | /// Throws if the response cannot be deserialized. 51 | /// Throws if the response is empty. 52 | T ReadAsJson(); 53 | 54 | /// 55 | /// Deserialize the contents of the HttpResponse.Body into an object 56 | /// of type T using the configured Json serializer 57 | /// 58 | /// Throws if the response cannot be deserialized. 59 | /// Throws if the response is empty. 60 | Task ReadAsJsonAsync(); 61 | 62 | } 63 | #endregion -------------------------------------------------------------------------------- /src/Alba/Internal/AlbaTracing.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore.Http.Extensions; 4 | 5 | namespace Alba.Internal; 6 | 7 | internal static class AlbaTracing 8 | { 9 | private static readonly ActivitySource Source = new("Alba"); 10 | 11 | public const string HttpUrl = "http.url"; 12 | public const string HttpMethod = "http.method"; 13 | public const string HttpRequestContentLength = "http.request_content_length"; 14 | public const string NetPeerName = "net.peer.name"; 15 | 16 | public const string HttpStatusCode = "http.status_code"; 17 | public const string HttpResponseContentLength = "http.response_content_length"; 18 | 19 | public static Activity? StartRequestActivity(HttpRequest request) 20 | { 21 | var activity = Source.StartActivity($"{request.Method} {request.Path}", ActivityKind.Client); 22 | activity?.SetRequestTags(request); 23 | return activity; 24 | } 25 | 26 | public static void SetRequestTags(this Activity activity, HttpRequest request) 27 | { 28 | activity.SetTag(HttpUrl, request.GetDisplayUrl()); 29 | activity.SetTag(HttpMethod, request.Method); 30 | activity.SetTag(HttpRequestContentLength, request.ContentLength); 31 | activity.SetTag(NetPeerName, request.Host.Host); 32 | 33 | DistributedContextPropagator.Current.Inject(activity, request, static (carrier, name, value) => 34 | { 35 | if (carrier is HttpRequest r) 36 | { 37 | r.Headers.TryAdd(name, value); 38 | } 39 | }); 40 | 41 | } 42 | 43 | public static void SetResponseTags(this Activity activity, HttpResponse response) 44 | { 45 | activity.SetTag(HttpStatusCode, response.StatusCode); 46 | activity.SetTag(HttpResponseContentLength, response.ContentLength); 47 | } 48 | } -------------------------------------------------------------------------------- /src/Alba/Internal/StreamExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Alba.Internal; 2 | 3 | internal static class StreamExtensions 4 | { 5 | public static string ReadAllText(this Stream stream) 6 | { 7 | using var sr = new StreamReader(stream, leaveOpen: true); 8 | return sr.ReadToEnd(); 9 | } 10 | 11 | public static byte[] ReadAllBytes(this Stream stream) 12 | { 13 | using var content = new MemoryStream(); 14 | stream.CopyTo(content); 15 | return content.ToArray(); 16 | } 17 | 18 | public static async Task ReadAllTextAsync(this Stream stream) 19 | { 20 | using var sr = new StreamReader(stream, leaveOpen: true); 21 | return await sr.ReadToEndAsync(); 22 | } 23 | 24 | public static async Task ReadAllBytesAsync(this Stream stream) 25 | { 26 | using var content = new MemoryStream(); 27 | await stream.CopyToAsync(content); 28 | return content.ToArray(); 29 | } 30 | } -------------------------------------------------------------------------------- /src/Alba/Internal/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace Alba.Internal; 4 | 5 | internal static class StringExtensions 6 | { 7 | public static bool IsEmpty([NotNullWhen(false)] this string? stringValue) => string.IsNullOrEmpty(stringValue); 8 | 9 | public static bool IsNotEmpty([NotNullWhen(true)] this string? stringValue) => !string.IsNullOrEmpty(stringValue); 10 | } -------------------------------------------------------------------------------- /src/Alba/ScenarioAssertionException.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using Alba.Internal; 3 | 4 | 5 | namespace Alba; 6 | 7 | public sealed class ScenarioAssertionException : Exception 8 | { 9 | private readonly IList _messages = new List(); 10 | 11 | /// 12 | /// Add an assertion failure message 13 | /// 14 | /// 15 | public void Add(string message) 16 | { 17 | _messages.Add(message); 18 | } 19 | 20 | private string? _body; 21 | 22 | public void AddBody(string body) => _body = body; 23 | 24 | internal void AssertAll() 25 | { 26 | if (_messages.Any()) 27 | { 28 | throw this; 29 | } 30 | } 31 | 32 | /// 33 | /// All the assertion failure messages 34 | /// 35 | public IEnumerable Messages => _messages; 36 | 37 | public override string Message 38 | { 39 | get 40 | { 41 | var writer = new StringBuilder(); 42 | 43 | foreach (var message in _messages) 44 | { 45 | writer.AppendLine(message); 46 | } 47 | 48 | if (_body.IsNotEmpty()) 49 | { 50 | writer.AppendLine(); 51 | writer.AppendLine(); 52 | writer.AppendLine("Actual body text was:"); 53 | writer.AppendLine(); 54 | writer.AppendLine(_body); 55 | } 56 | 57 | return writer.ToString(); 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /src/Alba/Security/ClaimsExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | 3 | namespace Alba.Security; 4 | 5 | public static class ClaimsExtensions 6 | { 7 | /// 8 | /// Append a baseline Claim to be placed on all requests 9 | /// 10 | /// 11 | /// 12 | /// 13 | /// 14 | public static T With(this T claims, Claim claim) where T : IHasClaims 15 | { 16 | claims.AddClaim(claim); 17 | return claims; 18 | } 19 | 20 | /// 21 | /// Add a baseline Claim to be placed on all requests 22 | /// 23 | /// 24 | /// 25 | /// 26 | /// 27 | /// 28 | public static T With(this T claims, string type, string value) where T : IHasClaims 29 | { 30 | var claim = new Claim(type, value); 31 | return claims.With(claim); 32 | } 33 | 34 | /// 35 | /// Add a baseline "name" claim to each request 36 | /// 37 | /// 38 | /// 39 | /// 40 | /// 41 | public static T WithName(this T claims, string name) where T : IHasClaims 42 | { 43 | var claim = new Claim(ClaimTypes.Name, name); 44 | return claims.With(claim); 45 | } 46 | } -------------------------------------------------------------------------------- /src/Alba/Security/IHasClaims.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | 3 | namespace Alba.Security; 4 | 5 | /// 6 | /// Implemented by types that model user permissions by claims 7 | /// 8 | public interface IHasClaims 9 | { 10 | /// 11 | /// Add a baseline claim that will be added to the ClaimsPrincipal 12 | /// for any scenario executed by the current AlbaHost 13 | /// 14 | /// 15 | void AddClaim(Claim claim); 16 | } -------------------------------------------------------------------------------- /src/Alba/Security/OpenConnectClientCredentials.cs: -------------------------------------------------------------------------------- 1 | using Alba.Internal; 2 | using IdentityModel.Client; 3 | 4 | namespace Alba.Security; 5 | 6 | /// 7 | /// Apply OIDC security workflow to outgoing Alba scenario requests using the 8 | /// Client Credentials workflow 9 | /// 10 | public class OpenConnectClientCredentials : OpenConnectExtension 11 | { 12 | public override void AssertValid() 13 | { 14 | if (ClientId.IsEmpty()) throw new Exception($"{nameof(ClientId)} cannot be null"); 15 | if (ClientSecret.IsEmpty()) throw new Exception($"{nameof(ClientSecret)} cannot be null"); 16 | if (Scope.IsEmpty()) throw new Exception($"{nameof(Scope)} cannot be null"); 17 | } 18 | 19 | /// 20 | /// User supplied value for the Open Id Connect "Scope". This is required. 21 | /// 22 | public string? Scope { get; set; } 23 | 24 | public override Task FetchToken(HttpClient client, DiscoveryDocumentResponse? disco, 25 | object? tokenCustomization) 26 | { 27 | if (disco == null) throw new ArgumentNullException(nameof(disco), "Unable to load the token discovery document"); 28 | 29 | return client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest 30 | { 31 | Address = disco.TokenEndpoint, 32 | ClientId = ClientId, 33 | ClientSecret = ClientSecret, 34 | Scope = Scope 35 | }); 36 | } 37 | } -------------------------------------------------------------------------------- /src/Alba/Serialization/IJsonStrategy.cs: -------------------------------------------------------------------------------- 1 | namespace Alba.Serialization; 2 | 3 | public interface IJsonStrategy 4 | { 5 | Stream Write(T body); 6 | T Read(ScenarioResult response); 7 | Task ReadAsync(ScenarioResult scenarioResult); 8 | } -------------------------------------------------------------------------------- /src/Alba/Serialization/SystemTextJsonSerializer.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using Alba.Internal; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Options; 5 | 6 | namespace Alba.Serialization; 7 | 8 | public class SystemTextJsonSerializer : IJsonStrategy 9 | { 10 | private readonly JsonSerializerOptions _options; 11 | 12 | public SystemTextJsonSerializer(IAlbaHost host) 13 | { 14 | var options = host.Services.GetService> (); 15 | 16 | _options = options?.Value.SerializerOptions ?? new JsonSerializerOptions(JsonSerializerDefaults.Web); 17 | } 18 | 19 | public Stream Write(T body) 20 | { 21 | var stream = new MemoryStream(); 22 | JsonSerializer.Serialize(new Utf8JsonWriter(stream), body, _options); 23 | return stream; 24 | } 25 | 26 | public T Read(ScenarioResult response) 27 | { 28 | var json = response.Context.Response.Body.ReadAllText(); 29 | var res = JsonSerializer.Deserialize(json, _options); 30 | 31 | if (res is not null) return res; 32 | 33 | throw new AlbaJsonFormatterException(json); 34 | } 35 | 36 | public async Task ReadAsync(ScenarioResult response) 37 | { 38 | var json = await response.Context.Response.Body.ReadAllTextAsync(); 39 | var res = JsonSerializer.Deserialize(json, _options); 40 | 41 | if (res is not null) return res; 42 | 43 | throw new AlbaJsonFormatterException(json); 44 | } 45 | } -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | NU1901;NU1902;NU1904 5 | all 6 | low 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/IdentityServer.New/Config.cs: -------------------------------------------------------------------------------- 1 | using Duende.IdentityServer.Models; 2 | 3 | namespace IdentityServer.New 4 | { 5 | public static class Config 6 | { 7 | 8 | public static IEnumerable IdentityResources => 9 | new IdentityResource[] 10 | { 11 | new IdentityResources.OpenId(), 12 | new IdentityResources.Profile(), 13 | }; 14 | public const string ClientId = "spa"; 15 | public const string ApiScope = "api"; 16 | public const string ClientSecret = "secret"; 17 | 18 | public static IEnumerable ApiScopes => 19 | new ApiScope[] 20 | { 21 | new ApiScope(ApiScope, new[] { "name" }), 22 | }; 23 | 24 | 25 | public static IEnumerable Clients => 26 | new Client[] 27 | { 28 | // m2m client credentials flow client 29 | new Client 30 | { 31 | ClientId = ClientId, 32 | ClientName = "Client Credentials Client", 33 | 34 | AllowedGrantTypes = GrantTypes.ResourceOwnerPasswordAndClientCredentials, 35 | ClientSecrets = { new Secret(ClientSecret.Sha256()) }, 36 | 37 | AllowedScopes = { "openid", "profile", ApiScope } 38 | }, 39 | }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/IdentityServer.New/HostingExtensions.cs: -------------------------------------------------------------------------------- 1 | using Duende.IdentityServer; 2 | using IdentityServer.New; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | using Serilog; 5 | 6 | namespace IdentityServer.New 7 | { 8 | internal static class HostingExtensions 9 | { 10 | public static WebApplication ConfigureServices(this WebApplicationBuilder builder) 11 | { 12 | builder.Services.AddRazorPages(); 13 | 14 | var isBuilder = builder.Services.AddIdentityServer(options => 15 | { 16 | options.Events.RaiseErrorEvents = true; 17 | options.Events.RaiseInformationEvents = true; 18 | options.Events.RaiseFailureEvents = true; 19 | options.Events.RaiseSuccessEvents = true; 20 | 21 | // see https://docs.duendesoftware.com/identityserver/v6/fundamentals/resources/ 22 | options.EmitStaticAudienceClaim = true; 23 | }) 24 | .AddTestUsers(TestUsers.Users); 25 | 26 | isBuilder.AddInMemoryIdentityResources(Config.IdentityResources); 27 | isBuilder.AddInMemoryApiScopes(Config.ApiScopes); 28 | isBuilder.AddInMemoryClients(Config.Clients); 29 | 30 | builder.Services.AddAuthentication(); 31 | 32 | return builder.Build(); 33 | } 34 | 35 | public static WebApplication ConfigurePipeline(this WebApplication app) 36 | { 37 | app.UseSerilogRequestLogging(); 38 | 39 | if (app.Environment.IsDevelopment()) 40 | { 41 | app.UseDeveloperExceptionPage(); 42 | } 43 | 44 | app.UseStaticFiles(); 45 | app.UseRouting(); 46 | app.UseIdentityServer(); 47 | app.UseAuthorization(); 48 | 49 | app.MapRazorPages() 50 | .RequireAuthorization(); 51 | 52 | return app; 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Account/AccessDenied.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model IdentityServer.New.Pages.Account.AccessDeniedModel 3 | @{ 4 | } 5 |
6 |
7 |

Access Denied

8 |

You do not have permission to access that resource.

9 |
10 |
-------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Account/AccessDenied.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.RazorPages; 2 | 3 | namespace IdentityServer.New.Pages.Account 4 | { 5 | public class AccessDeniedModel : PageModel 6 | { 7 | public void OnGet() 8 | { 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Account/Create/Index.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model IdentityServer.New.Pages.Create.Index 3 | 4 | -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Account/Create/InputModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Duende Software. All rights reserved. 2 | // See LICENSE in the project root for license information. 3 | 4 | 5 | using System.ComponentModel.DataAnnotations; 6 | 7 | namespace IdentityServer.New.Pages.Create 8 | { 9 | public class InputModel 10 | { 11 | [Required] 12 | public string Username { get; set; } 13 | 14 | [Required] 15 | public string Password { get; set; } 16 | 17 | public string Name { get; set; } 18 | public string Email { get; set; } 19 | 20 | public string ReturnUrl { get; set; } 21 | 22 | public string Button { get; set; } 23 | } 24 | } -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Account/Login/InputModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Duende Software. All rights reserved. 2 | // See LICENSE in the project root for license information. 3 | 4 | 5 | using System.ComponentModel.DataAnnotations; 6 | 7 | namespace IdentityServer.New.Pages.Login 8 | { 9 | public class InputModel 10 | { 11 | [Required] 12 | public string Username { get; set; } 13 | 14 | [Required] 15 | public string Password { get; set; } 16 | 17 | public bool RememberLogin { get; set; } 18 | 19 | public string ReturnUrl { get; set; } 20 | 21 | public string Button { get; set; } 22 | } 23 | } -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Account/Login/LoginOptions.cs: -------------------------------------------------------------------------------- 1 | namespace IdentityServer.New.Pages.Login 2 | { 3 | public class LoginOptions 4 | { 5 | public static bool AllowLocalLogin = true; 6 | public static bool AllowRememberLogin = true; 7 | public static TimeSpan RememberMeLoginDuration = TimeSpan.FromDays(30); 8 | public static string InvalidCredentialsErrorMessage = "Invalid username or password"; 9 | } 10 | } -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Account/Login/ViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Duende Software. All rights reserved. 2 | // See LICENSE in the project root for license information. 3 | 4 | namespace IdentityServer.New.Pages.Login 5 | { 6 | public class ViewModel 7 | { 8 | public bool AllowRememberLogin { get; set; } = true; 9 | public bool EnableLocalLogin { get; set; } = true; 10 | 11 | public IEnumerable ExternalProviders { get; set; } = Enumerable.Empty(); 12 | public IEnumerable VisibleExternalProviders => ExternalProviders.Where(x => !String.IsNullOrWhiteSpace(x.DisplayName)); 13 | 14 | public bool IsExternalLoginOnly => EnableLocalLogin == false && ExternalProviders?.Count() == 1; 15 | public string ExternalLoginScheme => IsExternalLoginOnly ? ExternalProviders?.SingleOrDefault()?.AuthenticationScheme : null; 16 | 17 | public class ExternalProvider 18 | { 19 | public string DisplayName { get; set; } 20 | public string AuthenticationScheme { get; set; } 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Account/Logout/Index.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model IdentityServer.New.Pages.Logout.Index 3 | 4 |
5 |
6 |

Logout

7 |

Would you like to logout of IdentityServer?

8 |
9 | 10 |
11 | 12 | 13 |
14 | 15 |
16 |
17 |
-------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Account/Logout/LoggedOut.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model IdentityServer.New.Pages.Logout.LoggedOut 3 | 4 |
5 |

6 | Logout 7 | You are now logged out 8 |

9 | 10 | @if (Model.View.PostLogoutRedirectUri != null) 11 | { 12 |
13 | Click here to return to the 14 | @Model.View.ClientName application. 15 |
16 | } 17 | 18 | @if (Model.View.SignOutIframeUrl != null) 19 | { 20 | 21 | } 22 |
23 | 24 | @section scripts 25 | { 26 | @if (Model.View.AutomaticRedirectAfterSignOut) 27 | { 28 | 29 | } 30 | } -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Account/Logout/LoggedOut.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Duende.IdentityServer.Services; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | 5 | namespace IdentityServer.New.Pages.Logout 6 | { 7 | [SecurityHeaders] 8 | [AllowAnonymous] 9 | public class LoggedOut : PageModel 10 | { 11 | private readonly IIdentityServerInteractionService _interactionService; 12 | 13 | public LoggedOutViewModel View { get; set; } 14 | 15 | public LoggedOut(IIdentityServerInteractionService interactionService) 16 | { 17 | _interactionService = interactionService; 18 | } 19 | 20 | public async Task OnGet(string logoutId) 21 | { 22 | // get context information (client name, post logout redirect URI and iframe for federated signout) 23 | var logout = await _interactionService.GetLogoutContextAsync(logoutId); 24 | 25 | View = new LoggedOutViewModel 26 | { 27 | AutomaticRedirectAfterSignOut = LogoutOptions.AutomaticRedirectAfterSignOut, 28 | PostLogoutRedirectUri = logout?.PostLogoutRedirectUri, 29 | ClientName = String.IsNullOrEmpty(logout?.ClientName) ? logout?.ClientId : logout?.ClientName, 30 | SignOutIframeUrl = logout?.SignOutIFrameUrl 31 | }; 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Account/Logout/LoggedOutViewModel.cs: -------------------------------------------------------------------------------- 1 | 2 | // Copyright (c) Duende Software. All rights reserved. 3 | // See LICENSE in the project root for license information. 4 | 5 | 6 | namespace IdentityServer.New.Pages.Logout 7 | { 8 | public class LoggedOutViewModel 9 | { 10 | public string PostLogoutRedirectUri { get; set; } 11 | public string ClientName { get; set; } 12 | public string SignOutIframeUrl { get; set; } 13 | public bool AutomaticRedirectAfterSignOut { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Account/Logout/LogoutOptions.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace IdentityServer.New.Pages.Logout 3 | { 4 | public class LogoutOptions 5 | { 6 | public static bool ShowLogoutPrompt = true; 7 | public static bool AutomaticRedirectAfterSignOut = false; 8 | } 9 | } -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Ciba/All.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model IdentityServer.New.Pages.Ciba.AllModel 3 | @{ 4 | } 5 | 6 |
7 |
8 |
9 |
10 |
11 |

Pending Backchannel Login Requests

12 |
13 |
14 | @if (Model.Logins?.Any() == true) 15 | { 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | @foreach (var login in Model.Logins) 27 | { 28 | 29 | 30 | 31 | 32 | 35 | 36 | } 37 | 38 |
IdClient IdBinding Message
@login.InternalId@login.Client.ClientId@login.BindingMessage 33 | Process 34 |
39 | } 40 | else 41 | { 42 |
No Pending Login Requests
43 | } 44 |
45 |
46 |
47 |
48 |
-------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Ciba/All.cshtml.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Duende Software. All rights reserved. 2 | // See LICENSE in the project root for license information. 3 | 4 | using Duende.IdentityServer.Models; 5 | using Duende.IdentityServer.Services; 6 | using Microsoft.AspNetCore.Authorization; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.AspNetCore.Mvc.RazorPages; 9 | using System.ComponentModel.DataAnnotations; 10 | 11 | namespace IdentityServer.New.Pages.Ciba 12 | { 13 | [SecurityHeaders] 14 | [Authorize] 15 | public class AllModel : PageModel 16 | { 17 | public IEnumerable Logins { get; set; } 18 | 19 | [BindProperty, Required] 20 | public string Id { get; set; } 21 | [BindProperty, Required] 22 | public string Button { get; set; } 23 | 24 | private readonly IBackchannelAuthenticationInteractionService _backchannelAuthenticationInteraction; 25 | 26 | public AllModel(IBackchannelAuthenticationInteractionService backchannelAuthenticationInteractionService) 27 | { 28 | _backchannelAuthenticationInteraction = backchannelAuthenticationInteractionService; 29 | } 30 | 31 | public async Task OnGet() 32 | { 33 | Logins = await _backchannelAuthenticationInteraction.GetPendingLoginRequestsForCurrentUserAsync(); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Ciba/ConsentOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Duende Software. All rights reserved. 2 | // See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServer.New.Pages.Ciba 6 | { 7 | public class ConsentOptions 8 | { 9 | public static bool EnableOfflineAccess = true; 10 | public static string OfflineAccessDisplayName = "Offline Access"; 11 | public static string OfflineAccessDescription = "Access to your applications and resources, even when you are offline"; 12 | 13 | public static readonly string MustChooseOneErrorMessage = "You must pick at least one permission"; 14 | public static readonly string InvalidSelectionErrorMessage = "Invalid selection"; 15 | } 16 | } -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Ciba/Index.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model IdentityServer.New.Pages.Ciba.IndexModel 3 | @{ 4 | } 5 | 6 |
7 |
8 | @if (Model.LoginRequest.Client.LogoUri != null) 9 | { 10 | 11 | } 12 |

13 | @Model.LoginRequest.Client.ClientName 14 | is requesting your permission 15 |

16 | 17 |

18 | Verify that this identifier matches what the client is displaying: 19 | @Model.LoginRequest.BindingMessage 20 |

21 | 22 |

23 | Do you wish to continue? 24 |

25 |
26 | Yes, Continue 27 |
28 | 29 |
30 |
31 | -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Ciba/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Duende Software. All rights reserved. 2 | // See LICENSE in the project root for license information. 3 | 4 | using Duende.IdentityServer.Models; 5 | using Duende.IdentityServer.Services; 6 | using Microsoft.AspNetCore.Authorization; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.AspNetCore.Mvc.RazorPages; 9 | 10 | namespace IdentityServer.New.Pages.Ciba 11 | { 12 | [AllowAnonymous] 13 | [SecurityHeaders] 14 | public class IndexModel : PageModel 15 | { 16 | public BackchannelUserLoginRequest LoginRequest { get; set; } 17 | 18 | private readonly IBackchannelAuthenticationInteractionService _backchannelAuthenticationInteraction; 19 | private readonly ILogger _logger; 20 | 21 | public IndexModel(IBackchannelAuthenticationInteractionService backchannelAuthenticationInteractionService, ILogger logger) 22 | { 23 | _backchannelAuthenticationInteraction = backchannelAuthenticationInteractionService; 24 | _logger = logger; 25 | } 26 | 27 | public async Task OnGet(string id) 28 | { 29 | LoginRequest = await _backchannelAuthenticationInteraction.GetLoginRequestByInternalIdAsync(id); 30 | if (LoginRequest == null) 31 | { 32 | _logger.LogWarning("Invalid backchannel login id {id}", id); 33 | return RedirectToPage("/Home/Error/Index"); 34 | } 35 | 36 | return Page(); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Ciba/InputModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Duende Software. All rights reserved. 2 | // See LICENSE in the project root for license information. 3 | 4 | namespace IdentityServer.New.Pages.Ciba 5 | { 6 | public class InputModel 7 | { 8 | public string Button { get; set; } 9 | public IEnumerable ScopesConsented { get; set; } 10 | public string Id { get; set; } 11 | public string Description { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Ciba/ViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Duende Software. All rights reserved. 2 | // See LICENSE in the project root for license information. 3 | 4 | namespace IdentityServer.New.Pages.Ciba 5 | { 6 | public class ViewModel 7 | { 8 | public string ClientName { get; set; } 9 | public string ClientUrl { get; set; } 10 | public string ClientLogoUrl { get; set; } 11 | 12 | public string BindingMessage { get; set; } 13 | 14 | public IEnumerable IdentityScopes { get; set; } 15 | public IEnumerable ApiScopes { get; set; } 16 | } 17 | 18 | public class ScopeViewModel 19 | { 20 | public string Name { get; set; } 21 | public string Value { get; set; } 22 | public string DisplayName { get; set; } 23 | public string Description { get; set; } 24 | public bool Emphasize { get; set; } 25 | public bool Required { get; set; } 26 | public bool Checked { get; set; } 27 | public IEnumerable Resources { get; set; } 28 | } 29 | 30 | public class ResourceViewModel 31 | { 32 | public string Name { get; set; } 33 | public string DisplayName { get; set; } 34 | } 35 | } -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Ciba/_ScopeListItem.cshtml: -------------------------------------------------------------------------------- 1 | @using IdentityServer.New.Pages.Ciba 2 | @model ScopeViewModel 3 | 4 |
  • 5 | 25 | @if (Model.Required) 26 | { 27 | (required) 28 | } 29 | @if (Model.Description != null) 30 | { 31 | 34 | } 35 | @if (Model.Resources?.Any() == true) 36 | { 37 | 46 | } 47 |
  • -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Consent/ConsentOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Duende Software. All rights reserved. 2 | // See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServer.New.Pages.Consent 6 | { 7 | public class ConsentOptions 8 | { 9 | public static bool EnableOfflineAccess = true; 10 | public static string OfflineAccessDisplayName = "Offline Access"; 11 | public static string OfflineAccessDescription = "Access to your applications and resources, even when you are offline"; 12 | 13 | public static readonly string MustChooseOneErrorMessage = "You must pick at least one permission"; 14 | public static readonly string InvalidSelectionErrorMessage = "Invalid selection"; 15 | } 16 | } -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Consent/InputModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Duende Software. All rights reserved. 2 | // See LICENSE in the project root for license information. 3 | 4 | namespace IdentityServer.New.Pages.Consent 5 | { 6 | public class InputModel 7 | { 8 | public string Button { get; set; } 9 | public IEnumerable ScopesConsented { get; set; } 10 | public bool RememberConsent { get; set; } = true; 11 | public string ReturnUrl { get; set; } 12 | public string Description { get; set; } 13 | } 14 | } -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Consent/ViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Duende Software. All rights reserved. 2 | // See LICENSE in the project root for license information. 3 | 4 | namespace IdentityServer.New.Pages.Consent 5 | { 6 | public class ViewModel 7 | { 8 | public string ClientName { get; set; } 9 | public string ClientUrl { get; set; } 10 | public string ClientLogoUrl { get; set; } 11 | public bool AllowRememberConsent { get; set; } 12 | 13 | public IEnumerable IdentityScopes { get; set; } 14 | public IEnumerable ApiScopes { get; set; } 15 | } 16 | 17 | public class ScopeViewModel 18 | { 19 | public string Name { get; set; } 20 | public string Value { get; set; } 21 | public string DisplayName { get; set; } 22 | public string Description { get; set; } 23 | public bool Emphasize { get; set; } 24 | public bool Required { get; set; } 25 | public bool Checked { get; set; } 26 | public IEnumerable Resources { get; set; } 27 | } 28 | 29 | public class ResourceViewModel 30 | { 31 | public string Name { get; set; } 32 | public string DisplayName { get; set; } 33 | } 34 | } -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Consent/_ScopeListItem.cshtml: -------------------------------------------------------------------------------- 1 | @using IdentityServer.New.Pages.Consent 2 | @model ScopeViewModel 3 | 4 |
  • 5 | 25 | @if (Model.Required) 26 | { 27 | (required) 28 | } 29 | @if (Model.Description != null) 30 | { 31 | 34 | } 35 | @if (Model.Resources?.Any() == true) 36 | { 37 | 46 | } 47 |
  • -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Device/DeviceOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Duende Software. All rights reserved. 2 | // See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServer.New.Pages.Device 6 | { 7 | public class DeviceOptions 8 | { 9 | public static bool EnableOfflineAccess = true; 10 | public static string OfflineAccessDisplayName = "Offline Access"; 11 | public static string OfflineAccessDescription = "Access to your applications and resources, even when you are offline"; 12 | 13 | public static readonly string InvalidUserCode = "Invalid user code"; 14 | public static readonly string MustChooseOneErrorMessage = "You must pick at least one permission"; 15 | public static readonly string InvalidSelectionErrorMessage = "Invalid selection"; 16 | } 17 | } -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Device/InputModel.cs: -------------------------------------------------------------------------------- 1 | namespace IdentityServer.New.Pages.Device 2 | { 3 | public class InputModel 4 | { 5 | public string Button { get; set; } 6 | public IEnumerable ScopesConsented { get; set; } 7 | public bool RememberConsent { get; set; } = true; 8 | public string ReturnUrl { get; set; } 9 | public string Description { get; set; } 10 | public string UserCode { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Device/Success.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model IdentityServer.New.Pages.Device.SuccessModel 3 | @{ 4 | } 5 | 6 | 7 |
    8 |
    9 |

    Success

    10 |

    You have successfully authorized the device

    11 |
    12 |
    13 | -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Device/Success.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc.RazorPages; 3 | 4 | namespace IdentityServer.New.Pages.Device 5 | { 6 | [SecurityHeaders] 7 | [Authorize] 8 | public class SuccessModel : PageModel 9 | { 10 | public void OnGet() 11 | { 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Device/ViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace IdentityServer.New.Pages.Device 2 | { 3 | public class ViewModel 4 | { 5 | public string ClientName { get; set; } 6 | public string ClientUrl { get; set; } 7 | public string ClientLogoUrl { get; set; } 8 | public bool AllowRememberConsent { get; set; } 9 | 10 | public IEnumerable IdentityScopes { get; set; } 11 | public IEnumerable ApiScopes { get; set; } 12 | } 13 | 14 | public class ScopeViewModel 15 | { 16 | public string Value { get; set; } 17 | public string DisplayName { get; set; } 18 | public string Description { get; set; } 19 | public bool Emphasize { get; set; } 20 | public bool Required { get; set; } 21 | public bool Checked { get; set; } 22 | } 23 | } -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Device/_ScopeListItem.cshtml: -------------------------------------------------------------------------------- 1 | @using IdentityServer.New.Pages.Device 2 | @model ScopeViewModel 3 | 4 |
  • 5 | 25 | @if (Model.Required) 26 | { 27 | (required) 28 | } 29 | @if (Model.Description != null) 30 | { 31 | 34 | } 35 |
  • -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Diagnostics/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.AspNetCore.Mvc.RazorPages; 5 | 6 | namespace IdentityServer.New.Pages.Diagnostics 7 | { 8 | [SecurityHeaders] 9 | [Authorize] 10 | public class Index : PageModel 11 | { 12 | public ViewModel View { get; set; } 13 | 14 | public async Task OnGet() 15 | { 16 | var localAddresses = new string[] { "127.0.0.1", "::1", HttpContext.Connection.LocalIpAddress.ToString() }; 17 | if (!localAddresses.Contains(HttpContext.Connection.RemoteIpAddress.ToString())) 18 | { 19 | return NotFound(); 20 | } 21 | 22 | View = new ViewModel(await HttpContext.AuthenticateAsync()); 23 | 24 | return Page(); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Diagnostics/ViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Duende Software. All rights reserved. 2 | // See LICENSE in the project root for license information. 3 | 4 | 5 | using IdentityModel; 6 | using Microsoft.AspNetCore.Authentication; 7 | using System.Text; 8 | using System.Text.Json; 9 | 10 | namespace IdentityServer.New.Pages.Diagnostics 11 | { 12 | public class ViewModel 13 | { 14 | public ViewModel(AuthenticateResult result) 15 | { 16 | AuthenticateResult = result; 17 | 18 | if (result.Properties.Items.TryGetValue("client_list", out var encoded)) 19 | { 20 | var bytes = Base64Url.Decode(encoded); 21 | var value = Encoding.UTF8.GetString(bytes); 22 | 23 | Clients = JsonSerializer.Deserialize(value); 24 | } 25 | } 26 | 27 | public AuthenticateResult AuthenticateResult { get; } 28 | public IEnumerable Clients { get; } = new List(); 29 | } 30 | } -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Extensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Duende Software. All rights reserved. 2 | // See LICENSE in the project root for license information. 3 | 4 | 5 | using Duende.IdentityServer.Models; 6 | using Microsoft.AspNetCore.Authentication; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.AspNetCore.Mvc.RazorPages; 9 | 10 | namespace IdentityServer.New.Pages 11 | { 12 | public static class Extensions 13 | { 14 | /// 15 | /// Determines if the authentication scheme support signout. 16 | /// 17 | public static async Task GetSchemeSupportsSignOutAsync(this HttpContext context, string scheme) 18 | { 19 | var provider = context.RequestServices.GetRequiredService(); 20 | var handler = await provider.GetHandlerAsync(context, scheme); 21 | return (handler is IAuthenticationSignOutHandler); 22 | } 23 | 24 | /// 25 | /// Checks if the redirect URI is for a native client. 26 | /// 27 | public static bool IsNativeClient(this AuthorizationRequest context) 28 | { 29 | return !context.RedirectUri.StartsWith("https", StringComparison.Ordinal) 30 | && !context.RedirectUri.StartsWith("http", StringComparison.Ordinal); 31 | } 32 | 33 | /// 34 | /// Renders a loading page that is used to redirect back to the redirectUri. 35 | /// 36 | public static IActionResult LoadingPage(this PageModel page, string redirectUri) 37 | { 38 | page.HttpContext.Response.StatusCode = 200; 39 | page.HttpContext.Response.Headers.Location = ""; 40 | 41 | return page.RedirectToPage("/Redirect/Index", new { RedirectUri = redirectUri }); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/ExternalLogin/Callback.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model IdentityServer.New.Pages.ExternalLogin.Callback 3 | 4 | @{ 5 | Layout = null; 6 | } 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
    16 | 17 |
    18 | 19 | -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/ExternalLogin/Challenge.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model IdentityServer.New.Pages.ExternalLogin.Challenge 3 | 4 | @{ 5 | Layout = null; 6 | } 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
    16 | 17 |
    18 | 19 | -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/ExternalLogin/Challenge.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Duende.IdentityServer.Services; 2 | using Microsoft.AspNetCore.Authentication; 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.AspNetCore.Mvc.RazorPages; 6 | 7 | namespace IdentityServer.New.Pages.ExternalLogin 8 | { 9 | [AllowAnonymous] 10 | [SecurityHeaders] 11 | public class Challenge : PageModel 12 | { 13 | private readonly IIdentityServerInteractionService _interactionService; 14 | 15 | public Challenge(IIdentityServerInteractionService interactionService) 16 | { 17 | _interactionService = interactionService; 18 | } 19 | 20 | public IActionResult OnGet(string scheme, string returnUrl) 21 | { 22 | if (string.IsNullOrEmpty(returnUrl)) returnUrl = "~/"; 23 | 24 | // validate returnUrl - either it is a valid OIDC URL or back to a local page 25 | if (Url.IsLocalUrl(returnUrl) == false && _interactionService.IsValidReturnUrl(returnUrl) == false) 26 | { 27 | // user might have clicked on a malicious link - should be logged 28 | throw new Exception("invalid return URL"); 29 | } 30 | 31 | // start challenge and roundtrip the return URL and scheme 32 | var props = new AuthenticationProperties 33 | { 34 | RedirectUri = Url.Page("/externallogin/callback"), 35 | 36 | Items = 37 | { 38 | { "returnUrl", returnUrl }, 39 | { "scheme", scheme }, 40 | } 41 | }; 42 | 43 | return Challenge(props, scheme); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Grants/ViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace IdentityServer.New.Pages.Grants 2 | { 3 | public class ViewModel 4 | { 5 | public IEnumerable Grants { get; set; } 6 | } 7 | 8 | public class GrantViewModel 9 | { 10 | public string ClientId { get; set; } 11 | public string ClientName { get; set; } 12 | public string ClientUrl { get; set; } 13 | public string ClientLogoUrl { get; set; } 14 | public string Description { get; set; } 15 | public DateTime Created { get; set; } 16 | public DateTime? Expires { get; set; } 17 | public IEnumerable IdentityGrantNames { get; set; } 18 | public IEnumerable ApiGrantNames { get; set; } 19 | } 20 | } -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Home/Error/Index.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model IdentityServer.New.Pages.Error.Index 3 | 4 |
    5 |
    6 |

    Error

    7 |
    8 | 9 |
    10 |
    11 |
    12 | Sorry, there was an error 13 | 14 | @if (Model.View.Error != null) 15 | { 16 | 17 | 18 | : @Model.View.Error.Error 19 | 20 | 21 | 22 | if (Model.View.Error.ErrorDescription != null) 23 | { 24 |
    @Model.View.Error.ErrorDescription
    25 | } 26 | } 27 |
    28 | 29 | @if (Model?.View?.Error?.RequestId != null) 30 | { 31 |
    Request Id: @Model.View.Error.RequestId
    32 | } 33 |
    34 |
    35 |
    36 | -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Home/Error/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Duende.IdentityServer.Services; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | 5 | namespace IdentityServer.New.Pages.Error 6 | { 7 | [AllowAnonymous] 8 | [SecurityHeaders] 9 | public class Index : PageModel 10 | { 11 | private readonly IIdentityServerInteractionService _interaction; 12 | private readonly IWebHostEnvironment _environment; 13 | 14 | public ViewModel View { get; set; } 15 | 16 | public Index(IIdentityServerInteractionService interaction, IWebHostEnvironment environment) 17 | { 18 | _interaction = interaction; 19 | _environment = environment; 20 | } 21 | 22 | public async Task OnGet(string errorId) 23 | { 24 | View = new ViewModel(); 25 | 26 | // retrieve error details from identityserver 27 | var message = await _interaction.GetErrorContextAsync(errorId); 28 | if (message != null) 29 | { 30 | View.Error = message; 31 | 32 | if (!_environment.IsDevelopment()) 33 | { 34 | // only show in development 35 | message.ErrorDescription = null; 36 | } 37 | } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Home/Error/ViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Duende Software. All rights reserved. 2 | // See LICENSE in the project root for license information. 3 | 4 | using Duende.IdentityServer.Models; 5 | 6 | namespace IdentityServer.New.Pages.Error 7 | { 8 | public class ViewModel 9 | { 10 | public ViewModel() 11 | { 12 | } 13 | 14 | public ViewModel(string error) 15 | { 16 | Error = new ErrorMessage { Error = error }; 17 | } 18 | 19 | public ErrorMessage Error { get; set; } 20 | } 21 | } -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Index.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model IdentityServer.New.Pages.Home.Index 3 | 4 |
    5 |

    6 | 7 | Welcome to Duende IdentityServer 8 | (version @Model.Version) 9 |

    10 | 11 |
      12 |
    • 13 | IdentityServer publishes a 14 | discovery document 15 | where you can find metadata and links to all the endpoints, key material, etc. 16 |
    • 17 |
    • 18 | Click here to see the claims for your current session. 19 |
    • 20 |
    • 21 | Click here to manage your stored grants. 22 |
    • 23 |
    • 24 | Click here to view the server side sessions. 25 |
    • 26 |
    • 27 | Click here to view your pending CIBA login requests. 28 |
    • 29 |
    • 30 | Here are links to the 31 | source code repository, 32 | and ready to use samples. 33 |
    • 34 |
    35 |
    36 | -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc.RazorPages; 3 | using System.Reflection; 4 | 5 | namespace IdentityServer.New.Pages.Home 6 | { 7 | [AllowAnonymous] 8 | public class Index : PageModel 9 | { 10 | public string Version; 11 | 12 | public void OnGet() 13 | { 14 | Version = typeof(Duende.IdentityServer.Hosting.IdentityServerMiddleware).Assembly.GetCustomAttribute()?.InformationalVersion.Split('+').First(); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Redirect/Index.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model IdentityServer.New.Pages.Redirect.IndexModel 3 | @{ 4 | } 5 | 6 |
    7 |
    8 |

    You are now being returned to the application

    9 |

    Once complete, you may close this tab.

    10 |
    11 |
    12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Redirect/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | 5 | namespace IdentityServer.New.Pages.Redirect 6 | { 7 | [AllowAnonymous] 8 | public class IndexModel : PageModel 9 | { 10 | public string RedirectUri { get; set; } 11 | 12 | public IActionResult OnGet(string redirectUri) 13 | { 14 | if (!Url.IsLocalUrl(redirectUri)) 15 | { 16 | return RedirectToPage("/Home/Error/Index"); 17 | } 18 | 19 | RedirectUri = redirectUri; 20 | return Page(); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/ServerSideSessions/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Duende.IdentityServer.Models; 2 | using Duende.IdentityServer.Services; 3 | using Duende.IdentityServer.Stores; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.AspNetCore.Mvc.RazorPages; 6 | 7 | namespace IdentityServer.New.Pages.ServerSideSessions 8 | { 9 | public class IndexModel : PageModel 10 | { 11 | private readonly ISessionManagementService _sessionManagementService; 12 | 13 | public IndexModel(ISessionManagementService sessionManagementService = null) 14 | { 15 | _sessionManagementService = sessionManagementService; 16 | } 17 | 18 | public QueryResult UserSessions { get; set; } 19 | 20 | [BindProperty(SupportsGet = true)] 21 | public string DisplayNameFilter { get; set; } 22 | 23 | [BindProperty(SupportsGet = true)] 24 | public string SessionIdFilter { get; set; } 25 | 26 | [BindProperty(SupportsGet = true)] 27 | public string SubjectIdFilter { get; set; } 28 | 29 | [BindProperty(SupportsGet = true)] 30 | public string Token { get; set; } 31 | 32 | [BindProperty(SupportsGet = true)] 33 | public string Prev { get; set; } 34 | 35 | public async Task OnGet() 36 | { 37 | if (_sessionManagementService != null) 38 | { 39 | UserSessions = await _sessionManagementService.QuerySessionsAsync(new SessionQuery 40 | { 41 | ResultsToken = Token, 42 | RequestPriorResults = Prev == "true", 43 | DisplayName = DisplayNameFilter, 44 | SessionId = SessionIdFilter, 45 | SubjectId = SubjectIdFilter 46 | }); 47 | } 48 | } 49 | 50 | [BindProperty] 51 | public string SessionId { get; set; } 52 | 53 | public async Task OnPost() 54 | { 55 | await _sessionManagementService.RemoveSessionsAsync(new RemoveSessionsContext 56 | { 57 | SessionId = SessionId, 58 | }); 59 | return RedirectToPage("/ServerSideSessions/Index", new { Token, DisplayNameFilter, SessionIdFilter, SubjectIdFilter, Prev }); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Duende IdentityServer 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
    21 | @RenderBody() 22 |
    23 | 24 | 25 | 26 | 27 | @RenderSection("scripts", required: false) 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Shared/_Nav.cshtml: -------------------------------------------------------------------------------- 1 | @using Duende.IdentityServer.Extensions 2 | @{ 3 | string name = null; 4 | if (!true.Equals(ViewData["signed-out"])) 5 | { 6 | name = Context.User?.GetDisplayName(); 7 | } 8 | } 9 | 10 | 33 | -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/Shared/_ValidationSummary.cshtml: -------------------------------------------------------------------------------- 1 | @if (ViewContext.ModelState.IsValid == false) 2 | { 3 |
    4 | Error 5 |
    6 |
    7 | } -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using IdentityServer.New.Pages 2 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 3 | -------------------------------------------------------------------------------- /src/IdentityServer.New/Pages/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /src/IdentityServer.New/Program.cs: -------------------------------------------------------------------------------- 1 | using IdentityServer.New; 2 | using Serilog; 3 | 4 | namespace IdentityServer.New; 5 | 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | 11 | Log.Logger = new LoggerConfiguration() 12 | .WriteTo.Console() 13 | .CreateBootstrapLogger(); 14 | 15 | Log.Information("Starting up"); 16 | 17 | try 18 | { 19 | var builder = WebApplication.CreateBuilder(args); 20 | 21 | builder.Host.UseSerilog((ctx, lc) => lc 22 | .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}") 23 | .Enrich.FromLogContext() 24 | .ReadFrom.Configuration(ctx.Configuration)); 25 | 26 | var app = builder 27 | .ConfigureServices() 28 | .ConfigurePipeline(); 29 | 30 | app.Run(); 31 | } 32 | catch (Exception ex) 33 | { 34 | Log.Fatal(ex, "Unhandled exception"); 35 | } 36 | finally 37 | { 38 | Log.Information("Shut down complete"); 39 | Log.CloseAndFlush(); 40 | } 41 | } 42 | } 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/IdentityServer.New/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "SelfHost": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | }, 9 | "applicationUrl": "https://localhost:5001" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/IdentityServer.New/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | .welcome-page .logo { 2 | width: 64px; 3 | } 4 | 5 | .icon-banner { 6 | width: 32px; 7 | } 8 | 9 | .body-container { 10 | margin-top: 60px; 11 | padding-bottom: 40px; 12 | } 13 | 14 | .welcome-page li { 15 | list-style: none; 16 | padding: 4px; 17 | } 18 | 19 | .logged-out-page iframe { 20 | display: none; 21 | width: 0; 22 | height: 0; 23 | } 24 | 25 | .grants-page .card { 26 | margin-top: 20px; 27 | border-bottom: 1px solid lightgray; 28 | } 29 | .grants-page .card .card-title { 30 | font-size: 120%; 31 | font-weight: bold; 32 | } 33 | .grants-page .card .card-title img { 34 | width: 100px; 35 | height: 100px; 36 | } 37 | .grants-page .card label { 38 | font-weight: bold; 39 | } 40 | -------------------------------------------------------------------------------- /src/IdentityServer.New/wwwroot/css/site.min.css: -------------------------------------------------------------------------------- 1 | .welcome-page .logo{width:64px;}.icon-banner{width:32px;}.body-container{margin-top:60px;padding-bottom:40px;}.welcome-page li{list-style:none;padding:4px;}.logged-out-page iframe{display:none;width:0;height:0;}.grants-page .card{margin-top:20px;border-bottom:1px solid #d3d3d3;}.grants-page .card .card-title{font-size:120%;font-weight:bold;}.grants-page .card .card-title img{width:100px;height:100px;}.grants-page .card label{font-weight:bold;} -------------------------------------------------------------------------------- /src/IdentityServer.New/wwwroot/css/site.scss: -------------------------------------------------------------------------------- 1 | .welcome-page { 2 | .logo { 3 | width: 64px; 4 | } 5 | } 6 | 7 | .icon-banner { 8 | width: 32px; 9 | } 10 | 11 | .body-container { 12 | margin-top: 60px; 13 | padding-bottom: 40px; 14 | } 15 | 16 | .welcome-page { 17 | li { 18 | list-style: none; 19 | padding: 4px; 20 | } 21 | } 22 | 23 | .logged-out-page { 24 | iframe { 25 | display: none; 26 | width: 0; 27 | height: 0; 28 | } 29 | } 30 | 31 | .grants-page { 32 | .card { 33 | margin-top: 20px; 34 | border-bottom: 1px solid lightgray; 35 | 36 | .card-title { 37 | img { 38 | width: 100px; 39 | height: 100px; 40 | } 41 | 42 | font-size: 120%; 43 | font-weight: bold; 44 | } 45 | 46 | label { 47 | font-weight: bold; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/IdentityServer.New/wwwroot/duende-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/IdentityServer.New/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasperFx/alba/72219db976bdced2997fbed14e8e6f99a2369623/src/IdentityServer.New/wwwroot/favicon.ico -------------------------------------------------------------------------------- /src/IdentityServer.New/wwwroot/js/signin-redirect.js: -------------------------------------------------------------------------------- 1 | window.location.href = document.querySelector("meta[http-equiv=refresh]").getAttribute("data-url"); 2 | -------------------------------------------------------------------------------- /src/IdentityServer.New/wwwroot/js/signout-redirect.js: -------------------------------------------------------------------------------- 1 | window.addEventListener("load", function () { 2 | var a = document.querySelector("a.PostLogoutRedirectUri"); 3 | if (a) { 4 | window.location = a.href; 5 | } 6 | }); 7 | -------------------------------------------------------------------------------- /src/MinimalApiWithOakton/MinimalApiWithOakton.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/MinimalApiWithOakton/Program.cs: -------------------------------------------------------------------------------- 1 | using Lamar.Microsoft.DependencyInjection; 2 | using Microsoft.AspNetCore.Http.Json; 3 | using Oakton; 4 | 5 | namespace MinimalApiWithOakton 6 | { 7 | public class Program 8 | { 9 | public static Task Main(string[] args) 10 | { 11 | var builder = WebApplication.CreateBuilder(args); 12 | builder.Host.UseLamar(); 13 | 14 | // Configure JSON options. 15 | builder.Services.Configure(options => 16 | { 17 | options.SerializerOptions.IncludeFields = true; 18 | }); 19 | 20 | var app = builder.Build(); 21 | app.MapGet("/", () => "Hello World!"); 22 | 23 | app.MapPost("/go", (PostedMessage input) => new OutputMessage(input.Id)); 24 | 25 | return app.RunOaktonCommands(args); 26 | } 27 | } 28 | 29 | public record PostedMessage(Guid Id); 30 | public record OutputMessage(Guid Id); 31 | } 32 | 33 | -------------------------------------------------------------------------------- /src/MinimalApiWithOakton/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:35694", 7 | "sslPort": 44353 8 | } 9 | }, 10 | "profiles": { 11 | "MinimalApiWithOakton": { 12 | "commandName": "Project", 13 | "dotnetRunMessages": true, 14 | "launchBrowser": true, 15 | "applicationUrl": "https://localhost:7161;http://localhost:5161", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "IIS Express": { 21 | "commandName": "IISExpress", 22 | "launchBrowser": true, 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/MinimalApiWithOakton/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/MinimalApiWithOakton/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /src/NUnitSamples/NUnitSamples.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/NUnitSamples/UnitTest1.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Alba; 3 | using NUnit.Framework; 4 | 5 | namespace NUnitSamples; 6 | 7 | #region sample_NUnit_Application 8 | 9 | [SetUpFixture] 10 | public class Application 11 | { 12 | [OneTimeSetUp] 13 | public async Task Init() 14 | { 15 | Host = await AlbaHost.For(); 16 | } 17 | 18 | public static IAlbaHost Host { get; private set; } 19 | 20 | // Make sure that NUnit will shut down the AlbaHost when 21 | // all the projects are finished 22 | [OneTimeTearDown] 23 | public void Teardown() 24 | { 25 | Host.Dispose(); 26 | } 27 | } 28 | 29 | #endregion 30 | 31 | #region sample_NUnit_scenario_test 32 | public class sample_integration_fixture 33 | { 34 | [Test] 35 | public async Task happy_path() 36 | { 37 | await Application.Host.Scenario(_ => 38 | { 39 | _.Get.Url("/fake/okay"); 40 | _.StatusCodeShouldBeOk(); 41 | }); 42 | } 43 | } 44 | #endregion -------------------------------------------------------------------------------- /src/TUnitSamples/Program.cs: -------------------------------------------------------------------------------- 1 | using Alba; 2 | using TUnit.Core.Interfaces; 3 | 4 | namespace TUnitSamples; 5 | 6 | #region sample_TUnit_Application 7 | public sealed class AlbaBootstrap : IAsyncInitializer, IAsyncDisposable 8 | { 9 | public IAlbaHost Host { get; private set; } = null!; 10 | 11 | public async Task InitializeAsync() 12 | { 13 | Host = await AlbaHost.For(); 14 | } 15 | 16 | public async ValueTask DisposeAsync() 17 | { 18 | await Host.DisposeAsync(); 19 | } 20 | } 21 | #endregion 22 | 23 | #region sample_TUnit_scenario_test 24 | public abstract class AlbaTestBase(AlbaBootstrap albaBootstrap) 25 | { 26 | protected IAlbaHost Host => albaBootstrap.Host; 27 | } 28 | 29 | [ClassDataSource(Shared = SharedType.PerTestSession)] 30 | public class MyTestClass(AlbaBootstrap albaBootstrap) : AlbaTestBase(albaBootstrap) 31 | { 32 | [Test] 33 | public async Task happy_path() 34 | { 35 | await Host.Scenario(_ => 36 | { 37 | _.Get.Url("/fake/okay"); 38 | _.StatusCodeShouldBeOk(); 39 | }); 40 | } 41 | } 42 | #endregion -------------------------------------------------------------------------------- /src/TUnitSamples/TUnitSamples.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/WebApiAspNetCore3/Controllers/WeatherForecastController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace WebApiStartupHostingModel.Controllers 8 | { 9 | [ApiController] 10 | [Route("[controller]")] 11 | public class WeatherForecastController : ControllerBase 12 | { 13 | private static readonly string[] Summaries = new[] 14 | { 15 | "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" 16 | }; 17 | 18 | private readonly ILogger _logger; 19 | 20 | public WeatherForecastController(ILogger logger) 21 | { 22 | _logger = logger; 23 | } 24 | 25 | [HttpGet] 26 | public IEnumerable Get() 27 | { 28 | var rng = new Random(); 29 | return Enumerable.Range(1, 5).Select(index => new WeatherForecast 30 | { 31 | Date = DateTime.Now.AddDays(index), 32 | TemperatureC = rng.Next(-20, 55), 33 | Summary = Summaries[rng.Next(Summaries.Length)] 34 | }) 35 | .ToArray(); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/WebApiAspNetCore3/HomeController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace WebApiStartupHostingModel 4 | { 5 | public class HomeController : Controller 6 | { 7 | [HttpGet("/")] 8 | public string Index() 9 | { 10 | return "Hello world."; 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/WebApiAspNetCore3/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | 4 | namespace WebApiStartupHostingModel 5 | { 6 | #region sample_WebApi3StandardTemplate 7 | public class Program 8 | { 9 | public static void Main(string[] args) 10 | { 11 | CreateHostBuilder(args).Build().Run(); 12 | } 13 | 14 | public static IHostBuilder CreateHostBuilder(string[] args) => 15 | Host.CreateDefaultBuilder(args) 16 | .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup()); 17 | } 18 | #endregion 19 | } 20 | -------------------------------------------------------------------------------- /src/WebApiAspNetCore3/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:45715", 8 | "sslPort": 44368 9 | } 10 | }, 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "weatherforecast", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "WebApiAspNetCore3": { 21 | "commandName": "Project", 22 | "launchBrowser": true, 23 | "launchUrl": "weatherforecast", 24 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/WebApiAspNetCore3/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | 7 | namespace WebApiStartupHostingModel 8 | { 9 | public class Startup 10 | { 11 | public Startup(IConfiguration configuration) 12 | { 13 | Configuration = configuration; 14 | } 15 | 16 | public IConfiguration Configuration { get; } 17 | 18 | // This method gets called by the runtime. Use this method to add services to the container. 19 | public void ConfigureServices(IServiceCollection services) 20 | { 21 | services.AddControllers(); 22 | } 23 | 24 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 25 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 26 | { 27 | if (env.IsDevelopment()) 28 | { 29 | app.UseDeveloperExceptionPage(); 30 | } 31 | 32 | app.UseHttpsRedirection(); 33 | 34 | app.UseRouting(); 35 | 36 | app.UseAuthorization(); 37 | 38 | app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/WebApiAspNetCore3/WeatherForecast.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WebApiStartupHostingModel 4 | { 5 | public class WeatherForecast 6 | { 7 | public DateTime Date { get; set; } 8 | 9 | public int TemperatureC { get; set; } 10 | 11 | public int TemperatureF => 32 + (int) (TemperatureC / 0.5556); 12 | 13 | public string Summary { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /src/WebApiAspNetCore3/WebApiStartupHostingModel.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/WebApiAspNetCore3/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/WebApiAspNetCore3/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /src/WebApiNet6/Program.cs: -------------------------------------------------------------------------------- 1 | #region sample_minimal_web_api 2 | 3 | var builder = WebApplication.CreateBuilder(args); 4 | 5 | // Add services to the container. 6 | 7 | var app = builder.Build(); 8 | 9 | // Configure the HTTP request pipeline. 10 | 11 | app.UseHttpsRedirection(); 12 | 13 | 14 | app.MapGet("/", () => "Hello World!"); 15 | app.MapGet("/blowup", context => throw new Exception("Boo!")); 16 | app.MapPost("/json", (MyEntity entity) => entity); 17 | 18 | 19 | app.Run(); 20 | 21 | public record MyEntity(Guid Id, string MyValue); 22 | 23 | #endregion 24 | 25 | -------------------------------------------------------------------------------- /src/WebApiNet6/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:30886", 8 | "sslPort": 44345 9 | } 10 | }, 11 | "profiles": { 12 | "WebApiNet6": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "weatherforecast", 17 | "applicationUrl": "https://localhost:7138;http://localhost:5138", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "IIS Express": { 23 | "commandName": "IISExpress", 24 | "launchBrowser": true, 25 | "launchUrl": "weatherforecast", 26 | "environmentVariables": { 27 | "ASPNETCORE_ENVIRONMENT": "Development" 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/WebApiNet6/WebApiNet6.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0;net9.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/WebApiNet6/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/WebApiNet6/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /src/WebApp/Controllers/AuthController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace WebApp.Controllers 5 | { 6 | [ApiController] 7 | [Route("[controller]/[action]")] 8 | public class AuthController : Controller 9 | { 10 | #region sample_windows_challenge_endpoint 11 | public IActionResult WindowsChallenge() 12 | { 13 | return new ChallengeResult(new List {"NTLM", "Negotiate"}); 14 | } 15 | #endregion 16 | 17 | public IActionResult Redirect() 18 | { 19 | return RedirectToAction("Get", "Values"); 20 | } 21 | 22 | public IActionResult RedirectPermanent() 23 | { 24 | return RedirectToActionPermanent("Get", "Values"); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/WebApp/Controllers/FakeController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace WebApp.Controllers 6 | { 7 | [ApiController] 8 | [Route("[controller]")] 9 | public class FakeController : ControllerBase 10 | { 11 | [HttpGet("{status}")] 12 | public IResult Fake(string status) 13 | { 14 | switch (status) 15 | { 16 | case "bad": 17 | throw new DivideByZeroException("Boom!"); 18 | 19 | case "invalid": 20 | return Results.Problem("It's all wrong", title: "This stinks!"); 21 | 22 | default: 23 | 24 | return Results.Ok("it's all good"); 25 | } 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/WebApp/Controllers/FilesController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Net.Mime; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Http; 8 | using Microsoft.AspNetCore.Mvc; 9 | 10 | namespace WebApp.Controllers 11 | { 12 | [Route("api/[controller]")] 13 | [ApiController] 14 | public class FilesController : ControllerBase 15 | { 16 | [HttpPost("upload")] 17 | public async Task>> UploadTextFile([FromForm] UploadRequest request) 18 | { 19 | if (request.Files.Count > 0) 20 | { 21 | var res = new List(); 22 | foreach (var formFile in request.Files) 23 | { 24 | if (formFile.ContentType == MediaTypeNames.Text.Plain) 25 | { 26 | var content = await ReadAsStringAsync(formFile); 27 | res.Add(new UploadResponse(formFile.FileName, formFile.Length, content.TrimEnd(), request.AdditionalContent)); 28 | } 29 | 30 | if (formFile.ContentType == MediaTypeNames.Image.Jpeg) 31 | { 32 | res.Add(new UploadResponse(formFile.FileName, formFile.Length, "image", request.AdditionalContent)); 33 | } 34 | } 35 | 36 | 37 | return res; 38 | } 39 | 40 | return BadRequest(); 41 | } 42 | 43 | public record UploadRequest(string AdditionalContent, List Files); 44 | 45 | public record UploadResponse(string Name, long Length, string Content, string AdditionalContent); 46 | 47 | public static async Task ReadAsStringAsync(IFormFile file) 48 | { 49 | var result = new StringBuilder(); 50 | using (var reader = new StreamReader(file.OpenReadStream())) 51 | { 52 | while (reader.Peek() >= 0) 53 | result.AppendLine(await reader.ReadLineAsync()); 54 | } 55 | return result.ToString(); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/WebApp/Controllers/GatewayController.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace WebApp.Controllers 5 | { 6 | [ApiController] 7 | [Route("[controller]/[action]")] 8 | public class GatewayController : Controller 9 | { 10 | [HttpPost] 11 | public IActionResult Insert([FromForm]InputModel callInfo) 12 | { 13 | LastInput = callInfo; 14 | 15 | return Ok(); 16 | } 17 | 18 | public static InputModel LastInput { get; set; } 19 | } 20 | 21 | public class InputModel 22 | { 23 | public string One { get; set; } 24 | public string Two { get; set; } 25 | public string Three { get; set; } 26 | 27 | } 28 | } -------------------------------------------------------------------------------- /src/WebApp/Controllers/JsonController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using JasperFx.Core; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.AspNetCore.Mvc.Formatters; 8 | using Newtonsoft.Json; 9 | 10 | namespace WebApp.Controllers 11 | { 12 | [ApiController] 13 | [Route("api/[controller]")] 14 | public class JsonController : Controller 15 | { 16 | [HttpGet] 17 | public IActionResult Get() 18 | { 19 | return Json(new Person()); 20 | } 21 | 22 | [HttpPost] 23 | public IActionResult Post([FromBody]Person person) 24 | { 25 | return Json(person); 26 | } 27 | 28 | [HttpPut] 29 | public IActionResult Put([FromBody]Person person) 30 | { 31 | return Json(person); 32 | } 33 | } 34 | 35 | public class Person 36 | { 37 | public string FirstName = "Jeremy"; 38 | public string LastName = "Miller"; 39 | } 40 | 41 | public class TextInputFormatter : InputFormatter 42 | { 43 | public TextInputFormatter() 44 | { 45 | SupportedMediaTypes.Add("text/plain"); 46 | } 47 | public override bool CanRead(InputFormatterContext context) 48 | { 49 | return context.HttpContext.Request.ContentType == "text/plain"; 50 | } 51 | 52 | public override async Task ReadRequestBodyAsync(InputFormatterContext context) 53 | { 54 | var text = await context.HttpContext.Request.Body.ReadAllTextAsync(); 55 | return await InputFormatterResult.SuccessAsync(text); 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /src/WebApp/Controllers/QueryStringContoller.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using JasperFx.Core; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace WebApp.Controllers 6 | { 7 | [ApiController] 8 | public class QueryStringContoller : Controller 9 | { 10 | [HttpGet("querystring")] 11 | public string QueryString([FromQuery] string test) 12 | { 13 | return test; 14 | } 15 | 16 | [HttpGet("querystringarray")] 17 | public string QueryStringList([FromQuery] string[] tests) 18 | { 19 | return string.Join(',', tests); 20 | } 21 | 22 | [HttpPost("sendform")] 23 | public string SendForm([FromForm] string test) 24 | { 25 | return test; 26 | } 27 | 28 | [HttpPost("sendbody")] 29 | public string SendBody([FromBody] string text) 30 | { 31 | return text; 32 | } 33 | 34 | [HttpGet("querystring2")] 35 | public string ReadQueryString() 36 | { 37 | var queryString = HttpContext.Request.Query; 38 | 39 | if (queryString.Count == 0) 40 | { 41 | return "No query string parameters"; 42 | } 43 | 44 | return queryString.Select(x => $"{x.Key}={x.Value}").Join(";"); 45 | } 46 | 47 | 48 | } 49 | } -------------------------------------------------------------------------------- /src/WebApp/Controllers/ValuesController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Mvc; 8 | 9 | namespace WebApp.Controllers 10 | { 11 | [ApiController] 12 | [Route("api/[controller]")] 13 | public class ValuesController : Controller 14 | { 15 | private readonly IEnumerable _lastWidget; 16 | public static IWidget[] LastWidget { get; set; } 17 | 18 | public ValuesController(IEnumerable lastWidget) 19 | { 20 | _lastWidget = lastWidget; 21 | 22 | } 23 | 24 | [HttpGet("/empty")] 25 | public string Empty() 26 | { 27 | return string.Empty; 28 | } 29 | 30 | // GET api/values 31 | [HttpGet] 32 | public string Get() 33 | { 34 | HttpContext.Response.Headers.Append("content-type", "text/plain"); 35 | 36 | return "value1, value2"; 37 | } 38 | 39 | // GET api/values/5 40 | [HttpGet("{id}")] 41 | public string Get(int id) 42 | { 43 | return "value"; 44 | } 45 | 46 | // POST api/values 47 | [HttpPost] 48 | public Task Post() 49 | { 50 | var reader = new StreamReader(HttpContext.Request.Body); 51 | var value = reader.ReadToEnd(); 52 | return HttpContext.Response.WriteAsync("I ran a POST with value " + value); 53 | } 54 | 55 | // PATCH api/values 56 | [HttpPatch()] 57 | public string Patch([FromBody] string value) 58 | { 59 | return "I ran a PATCH with value " + value; 60 | } 61 | 62 | // PUT api/values 63 | [HttpPut("{value}")] 64 | public void Put([FromQuery] string value) 65 | { 66 | LastWidget = _lastWidget.ToArray(); 67 | } 68 | 69 | // DELETE api/values/5 70 | [HttpDelete("{id}")] 71 | public void Delete(int id) 72 | { 73 | } 74 | } 75 | 76 | 77 | } -------------------------------------------------------------------------------- /src/WebApp/IWidget.cs: -------------------------------------------------------------------------------- 1 | namespace WebApp 2 | { 3 | public interface IWidget 4 | { 5 | 6 | } 7 | 8 | public class GreenWidget : IWidget { } 9 | public class RedWidget : IWidget { } 10 | } -------------------------------------------------------------------------------- /src/WebApp/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | 4 | namespace WebApp 5 | { 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | CreateHostBuilder(args).Build().Run(); 11 | } 12 | 13 | public static IHostBuilder CreateHostBuilder(string[] args) => 14 | Host.CreateDefaultBuilder(args) 15 | .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/WebApp/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:61043/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "launchUrl": "api/values", 15 | "environmentVariables": { 16 | "ASPNETCORE_ENVIRONMENT": "Development" 17 | } 18 | }, 19 | "WebApp": { 20 | "commandName": "Project", 21 | "launchBrowser": true, 22 | "launchUrl": "http://localhost:5000/api/values", 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | } 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/WebApp/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.FileProviders; 6 | using WebApp.Controllers; 7 | 8 | namespace WebApp 9 | { 10 | public class Startup 11 | { 12 | public IConfiguration Configuration { get; } 13 | 14 | // This method gets called by the runtime. Use this method to add services to the container 15 | public void ConfigureServices(IServiceCollection services) 16 | { 17 | services.AddTransient(); 18 | 19 | services.AddControllers(config => 20 | { 21 | config.RespectBrowserAcceptHeader = true; 22 | config.InputFormatters.Insert(0,new TextInputFormatter()); 23 | }).AddNewtonsoftJson(); 24 | 25 | services.AddProblemDetails(); 26 | } 27 | 28 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline 29 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 30 | { 31 | app.UseStaticFiles(new StaticFileOptions 32 | { 33 | ServeUnknownFileTypes = true, 34 | FileProvider = new PhysicalFileProvider(env.ContentRootPath) 35 | }); 36 | 37 | app.UseExceptionHandler(); 38 | 39 | app.UseRouting(); 40 | 41 | app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/WebApp/WebApp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | true 6 | WebApp 7 | Exe 8 | WebApp 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/WebApp/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Debug", 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/WebApp/hello.txt: -------------------------------------------------------------------------------- 1 | Hello from ASP.Net Core app! -------------------------------------------------------------------------------- /src/WebApp/web.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/WebAppSecuredWithJwt/ArithmeticController.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace WebApi 6 | { 7 | public class Result 8 | { 9 | public int Sum { get; set; } 10 | public int Product { get; set; } 11 | } 12 | 13 | public class Numbers 14 | { 15 | public int[] Values { get; set; } 16 | } 17 | 18 | public class ArithmeticController : ControllerBase 19 | { 20 | [Authorize] 21 | [HttpPost("/math")] 22 | public Result DoMath([FromBody] Numbers input) 23 | { 24 | var product = 1; 25 | foreach (var value in input.Values) 26 | { 27 | product *= value; 28 | } 29 | 30 | return new Result 31 | { 32 | Sum = input.Values.Sum(), 33 | Product = product 34 | }; 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/WebAppSecuredWithJwt/IdentityController.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace WebApi 6 | { 7 | [Route("identity")] 8 | [Authorize] 9 | public class IdentityController : ControllerBase 10 | { 11 | [HttpGet] 12 | public IActionResult Get() 13 | { 14 | return new JsonResult(from c in User.Claims select new { c.Type, c.Value }); 15 | } 16 | } 17 | 18 | [Route("identity2")] 19 | [Authorize(AuthenticationSchemes = "custom")] 20 | public class Identity2Controller : ControllerBase 21 | { 22 | [HttpGet] 23 | public IActionResult Get() 24 | { 25 | return new JsonResult(from c in User.Claims select new { c.Type, c.Value }); 26 | } 27 | } 28 | 29 | [Route("identity3")] 30 | [Authorize(AuthenticationSchemes = "AzureAuthentication")] 31 | public class Identity3Controller : ControllerBase 32 | { 33 | [HttpGet] 34 | public IActionResult Get() 35 | { 36 | return new JsonResult(from c in User.Claims select new { c.Type, c.Value }); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/WebAppSecuredWithJwt/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace WebAppSecuredWithJwt 11 | { 12 | public class Program 13 | { 14 | public static void Main(string[] args) 15 | { 16 | CreateHostBuilder(args).Build().Run(); 17 | } 18 | 19 | public static IHostBuilder CreateHostBuilder(string[] args) => 20 | Host.CreateDefaultBuilder(args) 21 | .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); 22 | } 23 | } -------------------------------------------------------------------------------- /src/WebAppSecuredWithJwt/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:14999", 8 | "sslPort": 44321 9 | } 10 | }, 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "swagger", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "WebAppSecuredWithJwt": { 21 | "commandName": "Project", 22 | "dotnetRunMessages": "true", 23 | "launchBrowser": true, 24 | "launchUrl": "swagger", 25 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 26 | "environmentVariables": { 27 | "ASPNETCORE_ENVIRONMENT": "Development" 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/WebAppSecuredWithJwt/WebAppSecuredWithJwt.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/WebAppSecuredWithJwt/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AzureAdB2C": { 10 | "Instance": "https://dummy.b2clogin.com", 11 | "Domain": "dummy.onmicrosoft.com", 12 | "TenantId": "sometenantid", 13 | "Audience": "someaudience", 14 | "ClientId": "someclientid", 15 | "SignedOutCallbackPath": "/signout-oidc", 16 | "SignUpSignInPolicyId": "B2C_1A_SIGNUP_SIGNIN" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/WebAppSecuredWithJwt/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | --------------------------------------------------------------------------------