From cf4418dd346ac853d9bdcbb64c0056161c9c543f Mon Sep 17 00:00:00 2001 From: Erwin van der Valk Date: Wed, 29 Jan 2025 15:03:00 +0100 Subject: [PATCH] Several improvements to to the Tests - nullability - no more .wait() - general cleanups --- .../Hosts.Tests/TestInfra/AppHostFixture.cs | 4 + .../Duende.Bff.Blazor.Client.UnitTests.csproj | 3 +- .../Duende.Bff.Blazor.UnitTests.csproj | 3 +- .../Duende.Bff.EntityFramework.Tests.csproj | 4 +- .../Duende.Bff.Tests/Duende.Bff.Tests.csproj | 54 +- .../Endpoints/LocalEndpointTests.cs | 132 ++--- .../Management/LoginEndpointTests.cs | 32 +- .../Management/LogoutEndpointTests.cs | 27 +- .../Endpoints/Management/UserEndpointTests.cs | 4 +- .../Endpoints/RemoteEndpointTests.cs | 257 ++++----- .../Endpoints/YarpRemoteEndpointTests.cs | 173 +++--- .../Headers/ApiAndBffUseForwardedHeaders.cs | 15 +- .../Headers/ApiUseForwardedHeaders.cs | 12 +- bff/test/Duende.Bff.Tests/Headers/General.cs | 6 +- .../SessionManagement/CookieSlidingTests.cs | 5 +- .../ServerSideTicketStoreTests.cs | 3 +- .../TestFramework/ApiResponse.cs | 17 +- .../TestFramework/GenericHost.cs | 84 +-- .../MockExternalAuthenticationHandler.cs | 86 ++- .../MockSessionRevocationService.cs | 4 +- .../Duende.Bff.Tests/TestFramework/Records.cs | 8 +- .../TestFramework/TestBrowserClient.cs | 161 ++++-- .../TestFramework/TestLoggerProvider.cs | 6 +- .../TestFramework/TestSerializerOptions.cs | 15 + .../Duende.Bff.Tests/TestHosts/ApiHost.cs | 28 +- .../Duende.Bff.Tests/TestHosts/BffHost.cs | 67 +-- .../BffHostUsingResourceNamedTokens.cs | 42 +- .../TestHosts/BffIntegrationTestBase.cs | 38 +- .../TestHosts/IdentityServerHost.cs | 10 +- .../Duende.Bff.Tests/TestHosts/YarpBffHost.cs | 512 +++++++++--------- .../TestHosts/YarpBffIntegrationTestBase.cs | 28 +- 31 files changed, 899 insertions(+), 941 deletions(-) create mode 100644 bff/test/Duende.Bff.Tests/TestFramework/TestSerializerOptions.cs diff --git a/bff/samples/Hosts.Tests/TestInfra/AppHostFixture.cs b/bff/samples/Hosts.Tests/TestInfra/AppHostFixture.cs index 11d196201..9216ad94c 100644 --- a/bff/samples/Hosts.Tests/TestInfra/AppHostFixture.cs +++ b/bff/samples/Hosts.Tests/TestInfra/AppHostFixture.cs @@ -1,6 +1,10 @@ using Aspire.Hosting; using Microsoft.Extensions.Logging; + +#if !DEBUG_NCRUNCH using Projects; +#endif + using Serilog; using Serilog.Core; using Serilog.Extensions.Logging; diff --git a/bff/test/Duende.Bff.Blazor.Client.UnitTests/Duende.Bff.Blazor.Client.UnitTests.csproj b/bff/test/Duende.Bff.Blazor.Client.UnitTests/Duende.Bff.Blazor.Client.UnitTests.csproj index 655aba15b..cabd2e4b1 100644 --- a/bff/test/Duende.Bff.Blazor.Client.UnitTests/Duende.Bff.Blazor.Client.UnitTests.csproj +++ b/bff/test/Duende.Bff.Blazor.Client.UnitTests/Duende.Bff.Blazor.Client.UnitTests.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -7,6 +7,7 @@ false true + True diff --git a/bff/test/Duende.Bff.Blazor.UnitTests/Duende.Bff.Blazor.UnitTests.csproj b/bff/test/Duende.Bff.Blazor.UnitTests/Duende.Bff.Blazor.UnitTests.csproj index a3931e328..f158ad223 100644 --- a/bff/test/Duende.Bff.Blazor.UnitTests/Duende.Bff.Blazor.UnitTests.csproj +++ b/bff/test/Duende.Bff.Blazor.UnitTests/Duende.Bff.Blazor.UnitTests.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -7,6 +7,7 @@ false true + True diff --git a/bff/test/Duende.Bff.EntityFramework.Tests/Duende.Bff.EntityFramework.Tests.csproj b/bff/test/Duende.Bff.EntityFramework.Tests/Duende.Bff.EntityFramework.Tests.csproj index 8c5527268..c31d5220d 100644 --- a/bff/test/Duende.Bff.EntityFramework.Tests/Duende.Bff.EntityFramework.Tests.csproj +++ b/bff/test/Duende.Bff.EntityFramework.Tests/Duende.Bff.EntityFramework.Tests.csproj @@ -1,6 +1,8 @@ - + net8.0;net9.0 + enable + True diff --git a/bff/test/Duende.Bff.Tests/Duende.Bff.Tests.csproj b/bff/test/Duende.Bff.Tests/Duende.Bff.Tests.csproj index edecbe2b8..580618fbd 100644 --- a/bff/test/Duende.Bff.Tests/Duende.Bff.Tests.csproj +++ b/bff/test/Duende.Bff.Tests/Duende.Bff.Tests.csproj @@ -1,31 +1,33 @@ - + - - net8.0;net9.0 - + + net8.0;net9.0 + enable + True + - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + - - - - + + + + + + + + + + diff --git a/bff/test/Duende.Bff.Tests/Endpoints/LocalEndpointTests.cs b/bff/test/Duende.Bff.Tests/Endpoints/LocalEndpointTests.cs index b236faa56..61d4188c5 100644 --- a/bff/test/Duende.Bff.Tests/Endpoints/LocalEndpointTests.cs +++ b/bff/test/Duende.Bff.Tests/Endpoints/LocalEndpointTests.cs @@ -6,8 +6,7 @@ using Microsoft.Extensions.DependencyInjection; using System.Net; using System.Net.Http; -using System.Text; -using System.Text.Json; +using System.Net.Http.Json; using System.Threading.Tasks; using Shouldly; using Xunit; @@ -22,44 +21,36 @@ public async Task calls_to_authorized_local_endpoint_should_succeed() { await BffHost.BffLoginAsync("alice"); - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/local_authz")); - req.Headers.Add("x-csrf", "1"); - var response = await BffHost.BrowserClient.SendAsync(req); + var apiResult = await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/local_authz") + ); - response.IsSuccessStatusCode.ShouldBeTrue(); - response.Content.Headers.ContentType.MediaType.ShouldBe("application/json"); - var json = await response.Content.ReadAsStringAsync(); - var apiResult = JsonSerializer.Deserialize(json); apiResult.Method.ShouldBe("GET"); apiResult.Path.ShouldBe("/local_authz"); apiResult.Sub.ShouldBe("alice"); } - + [Fact] public async Task calls_to_authorized_local_endpoint_without_csrf_should_succeed_without_antiforgery_header() { await BffHost.BffLoginAsync("alice"); - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/local_authz_no_csrf")); - var response = await BffHost.BrowserClient.SendAsync(req); + var apiResult = await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/local_authz_no_csrf") + ); - response.IsSuccessStatusCode.ShouldBeTrue(); - response.Content.Headers.ContentType.MediaType.ShouldBe("application/json"); - var json = await response.Content.ReadAsStringAsync(); - var apiResult = JsonSerializer.Deserialize(json); apiResult.Method.ShouldBe("GET"); apiResult.Path.ShouldBe("/local_authz_no_csrf"); apiResult.Sub.ShouldBe("alice"); } - + [Fact] public async Task unauthenticated_calls_to_authorized_local_endpoint_should_fail() { - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/local_authz")); - req.Headers.Add("x-csrf", "1"); - var response = await BffHost.BrowserClient.SendAsync(req); - - response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); + var response = await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/local_authz"), + expectedStatusCode: HttpStatusCode.Unauthorized + ); } [Fact] @@ -70,27 +61,24 @@ public async Task calls_to_local_endpoint_should_require_antiforgery_header() response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); } - + + [Fact] public async Task calls_to_local_endpoint_without_csrf_should_not_require_antiforgery_header() { - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/local_anon_no_csrf")); - var response = await BffHost.BrowserClient.SendAsync(req); - - response.StatusCode.ShouldBe(HttpStatusCode.OK); + var response = await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/local_anon_no_csrf"), + expectedStatusCode: HttpStatusCode.OK + ); } [Fact] public async Task calls_to_anon_endpoint_should_allow_anonymous() { - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/local_anon")); - req.Headers.Add("x-csrf", "1"); - var response = await BffHost.BrowserClient.SendAsync(req); + var apiResult = await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/local_anon") + ); - response.IsSuccessStatusCode.ShouldBeTrue(); - response.Content.Headers.ContentType.MediaType.ShouldBe("application/json"); - var json = await response.Content.ReadAsStringAsync(); - var apiResult = JsonSerializer.Deserialize(json); apiResult.Method.ShouldBe("GET"); apiResult.Path.ShouldBe("/local_anon"); apiResult.Sub.ShouldBeNull(); @@ -101,20 +89,17 @@ public async Task put_to_local_endpoint_should_succeed() { await BffHost.BffLoginAsync("alice"); - var req = new HttpRequestMessage(HttpMethod.Put, BffHost.Url("/local_authz")); - req.Headers.Add("x-csrf", "1"); - req.Content = new StringContent(JsonSerializer.Serialize(new TestPayload("hello test api")), Encoding.UTF8, "application/json"); - var response = await BffHost.BrowserClient.SendAsync(req); + var apiResult = await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/local_authz"), + method: HttpMethod.Put, + content: JsonContent.Create(new TestPayload("hello test api")) + ); - response.IsSuccessStatusCode.ShouldBeTrue(); - response.Content.Headers.ContentType.MediaType.ShouldBe("application/json"); - var json = await response.Content.ReadAsStringAsync(); - var apiResult = JsonSerializer.Deserialize(json); apiResult.Method.ShouldBe("PUT"); apiResult.Path.ShouldBe("/local_authz"); apiResult.Sub.ShouldBe("alice"); - var body = JsonSerializer.Deserialize(apiResult.Body); - body.Message.ShouldBe("hello test api"); + var body = apiResult.BodyAs(); + body.Message.ShouldBe("hello test api", apiResult.Body); } [Fact] @@ -125,29 +110,31 @@ public async Task unauthenticated_non_bff_endpoint_should_return_302_for_login() var response = await BffHost.BrowserClient.SendAsync(req); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); - response.Headers.Location.ToString().ToLowerInvariant().ShouldStartWith(IdentityServerHost.Url("/connect/authorize")); + response.Headers.Location + .ShouldNotBeNull() + .ToString() + .ToLowerInvariant() + .ShouldStartWith(IdentityServerHost.Url("/connect/authorize")); } [Fact] public async Task unauthenticated_api_call_should_return_401() { - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/always_fail_authz")); - req.Headers.Add("x-csrf", "1"); - var response = await BffHost.BrowserClient.SendAsync(req); - - response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); + var response = await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/always_fail_authz"), + expectedStatusCode: HttpStatusCode.Unauthorized + ); } - + [Fact] public async Task forbidden_api_call_should_return_403() { await BffHost.BffLoginAsync("alice"); - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/always_fail_authz")); - req.Headers.Add("x-csrf", "1"); - var response = await BffHost.BrowserClient.SendAsync(req); - - response.StatusCode.ShouldBe(HttpStatusCode.Forbidden); + var response = await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/always_fail_authz"), + expectedStatusCode: HttpStatusCode.Forbidden + ); } [Fact] @@ -156,11 +143,10 @@ public async Task challenge_response_should_return_401() await BffHost.BffLoginAsync("alice"); BffHost.LocalApiResponseStatus = BffHost.ResponseStatus.Challenge; - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/local_authz")); - req.Headers.Add("x-csrf", "1"); - var response = await BffHost.BrowserClient.SendAsync(req); - - response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); + var response = await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/local_authz"), + expectedStatusCode: HttpStatusCode.Unauthorized + ); } [Fact] @@ -169,11 +155,10 @@ public async Task forbid_response_should_return_403() await BffHost.BffLoginAsync("alice"); BffHost.LocalApiResponseStatus = BffHost.ResponseStatus.Forbid; - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/local_authz")); - req.Headers.Add("x-csrf", "1"); - var response = await BffHost.BrowserClient.SendAsync(req); - - response.StatusCode.ShouldBe(HttpStatusCode.Forbidden); + var response = await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/local_authz"), + expectedStatusCode: HttpStatusCode.Forbidden + ); } [Fact] @@ -182,21 +167,20 @@ public async Task challenge_response_when_response_handling_skipped_should_trigg await BffHost.BffLoginAsync("alice"); BffHost.LocalApiResponseStatus = BffHost.ResponseStatus.Challenge; - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/local_anon_no_csrf_no_response_handling")); - var response = await BffHost.BrowserClient.SendAsync(req); - - response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + var response = await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/local_anon_no_csrf_no_response_handling"), + expectedStatusCode: HttpStatusCode.Redirect + ); } - [Fact] public async Task fallback_policy_should_not_fail() { BffHost.OnConfigureServices += svcs => { svcs.AddAuthorization(opts => - { - opts.FallbackPolicy = + { + opts.FallbackPolicy = new Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .Build(); @@ -208,4 +192,4 @@ public async Task fallback_policy_should_not_fail() response.StatusCode.ShouldNotBe(HttpStatusCode.InternalServerError); } } -} +} \ No newline at end of file diff --git a/bff/test/Duende.Bff.Tests/Endpoints/Management/LoginEndpointTests.cs b/bff/test/Duende.Bff.Tests/Endpoints/Management/LoginEndpointTests.cs index 95410f557..2a4d46dd1 100644 --- a/bff/test/Duende.Bff.Tests/Endpoints/Management/LoginEndpointTests.cs +++ b/bff/test/Duende.Bff.Tests/Endpoints/Management/LoginEndpointTests.cs @@ -38,16 +38,16 @@ public async Task login_endpoint_should_challenge_and_redirect_to_root() { var response = await BffHost.BrowserClient.GetAsync(BffHost.Url("/bff/login")); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); - response.Headers.Location.ToString().ShouldStartWith(IdentityServerHost.Url("/connect/authorize")); + response.Headers.Location!.ToString().ShouldStartWith(IdentityServerHost.Url("/connect/authorize")); await IdentityServerHost.IssueSessionCookieAsync("alice"); response = await IdentityServerHost.BrowserClient.GetAsync(response.Headers.Location.ToString()); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); - response.Headers.Location.ToString().ShouldStartWith(BffHost.Url("/signin-oidc")); + response.Headers.Location!.ToString().ShouldStartWith(BffHost.Url("/signin-oidc")); response = await BffHost.BrowserClient.GetAsync(response.Headers.Location.ToString()); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); - response.Headers.Location.ToString().ShouldBe("/"); + response.Headers.Location!.ToString().ShouldBe("/"); } [Fact] @@ -62,16 +62,16 @@ public async Task login_endpoint_should_challenge_and_redirect_to_root_with_cust var response = await BffHost.BrowserClient.GetAsync(BffHost.Url("/custom/bff/login")); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); - response.Headers.Location.ToString().ShouldStartWith(IdentityServerHost.Url("/connect/authorize")); + response.Headers.Location!.ToString().ShouldStartWith(IdentityServerHost.Url("/connect/authorize")); await IdentityServerHost.IssueSessionCookieAsync("alice"); response = await IdentityServerHost.BrowserClient.GetAsync(response.Headers.Location.ToString()); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); - response.Headers.Location.ToString().ShouldStartWith(BffHost.Url("/signin-oidc")); + response.Headers.Location!.ToString().ShouldStartWith(BffHost.Url("/signin-oidc")); response = await BffHost.BrowserClient.GetAsync(response.Headers.Location.ToString()); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); - response.Headers.Location.ToString().ShouldBe("/"); + response.Headers.Location!.ToString().ShouldBe("/"); } [Fact] @@ -86,16 +86,16 @@ public async Task login_endpoint_should_challenge_and_redirect_to_root_with_cust var response = await BffHost.BrowserClient.GetAsync(BffHost.Url("/custom/bff/login")); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); - response.Headers.Location.ToString().ShouldStartWith(IdentityServerHost.Url("/connect/authorize")); + response.Headers.Location!.ToString().ShouldStartWith(IdentityServerHost.Url("/connect/authorize")); await IdentityServerHost.IssueSessionCookieAsync("alice"); response = await IdentityServerHost.BrowserClient.GetAsync(response.Headers.Location.ToString()); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); - response.Headers.Location.ToString().ShouldStartWith(BffHost.Url("/signin-oidc")); + response.Headers.Location!.ToString().ShouldStartWith(BffHost.Url("/signin-oidc")); response = await BffHost.BrowserClient.GetAsync(response.Headers.Location.ToString()); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); - response.Headers.Location.ToString().ShouldBe("/"); + response.Headers.Location!.ToString().ShouldBe("/"); } [Fact] @@ -110,16 +110,16 @@ public async Task login_endpoint_should_challenge_and_redirect_to_root_with_root var response = await BffHost.BrowserClient.GetAsync(BffHost.Url("/login")); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); - response.Headers.Location.ToString().ShouldStartWith(IdentityServerHost.Url("/connect/authorize")); + response.Headers.Location!.ToString().ShouldStartWith(IdentityServerHost.Url("/connect/authorize")); await IdentityServerHost.IssueSessionCookieAsync("alice"); response = await IdentityServerHost.BrowserClient.GetAsync(response.Headers.Location.ToString()); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); - response.Headers.Location.ToString().ShouldStartWith(BffHost.Url("/signin-oidc")); + response.Headers.Location!.ToString().ShouldStartWith(BffHost.Url("/signin-oidc")); response = await BffHost.BrowserClient.GetAsync(response.Headers.Location.ToString()); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); - response.Headers.Location.ToString().ShouldBe("/"); + response.Headers.Location!.ToString().ShouldBe("/"); } [Fact] @@ -129,7 +129,7 @@ public async Task login_endpoint_with_existing_session_should_challenge() var response = await BffHost.BrowserClient.GetAsync(BffHost.Url("/bff/login")); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); - response.Headers.Location.ToString().ShouldStartWith(IdentityServerHost.Url("/connect/authorize")); + response.Headers.Location!.ToString().ShouldStartWith(IdentityServerHost.Url("/connect/authorize")); } [Fact] @@ -137,16 +137,16 @@ public async Task login_endpoint_should_accept_returnUrl() { var response = await BffHost.BrowserClient.GetAsync(BffHost.Url("/bff/login") + "?returnUrl=/foo"); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); - response.Headers.Location.ToString().ShouldStartWith(IdentityServerHost.Url("/connect/authorize")); + response.Headers.Location!.ToString().ShouldStartWith(IdentityServerHost.Url("/connect/authorize")); await IdentityServerHost.IssueSessionCookieAsync("alice"); response = await IdentityServerHost.BrowserClient.GetAsync(response.Headers.Location.ToString()); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); - response.Headers.Location.ToString().ShouldStartWith(BffHost.Url("/signin-oidc")); + response.Headers.Location!.ToString().ShouldStartWith(BffHost.Url("/signin-oidc")); response = await BffHost.BrowserClient.GetAsync(response.Headers.Location.ToString()); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); - response.Headers.Location.ToString().ShouldBe("/foo"); + response.Headers.Location!.ToString().ShouldBe("/foo"); } [Fact] diff --git a/bff/test/Duende.Bff.Tests/Endpoints/Management/LogoutEndpointTests.cs b/bff/test/Duende.Bff.Tests/Endpoints/Management/LogoutEndpointTests.cs index e895542f9..0a08182c2 100644 --- a/bff/test/Duende.Bff.Tests/Endpoints/Management/LogoutEndpointTests.cs +++ b/bff/test/Duende.Bff.Tests/Endpoints/Management/LogoutEndpointTests.cs @@ -63,7 +63,7 @@ public async Task logout_endpoint_for_authenticated_when_require_option_is_false var response = await BffHost.BrowserClient.GetAsync(BffHost.Url("/bff/logout")); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // endsession - response.Headers.Location.ToString().ToLowerInvariant().ShouldStartWith(IdentityServerHost.Url("/connect/endsession")); + response.Headers.Location!.ToString().ToLowerInvariant().ShouldStartWith(IdentityServerHost.Url("/connect/endsession")); } [Fact] @@ -81,7 +81,7 @@ public async Task logout_endpoint_for_authenticated_user_without_sid_should_succ var response = await BffHost.BrowserClient.GetAsync(BffHost.Url("/bff/logout")); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // endsession - response.Headers.Location.ToString().ToLowerInvariant().ShouldStartWith(IdentityServerHost.Url("/connect/endsession")); + response.Headers.Location!.ToString().ToLowerInvariant().ShouldStartWith(IdentityServerHost.Url("/connect/endsession")); } [Fact] @@ -89,7 +89,7 @@ public async Task logout_endpoint_for_anonymous_user_without_sid_should_succeed( { var response = await BffHost.BrowserClient.GetAsync(BffHost.Url("/bff/logout")); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // endsession - response.Headers.Location.ToString().ToLowerInvariant().ShouldStartWith(IdentityServerHost.Url("/connect/endsession")); + response.Headers.Location!.ToString().ToLowerInvariant().ShouldStartWith(IdentityServerHost.Url("/connect/endsession")); } [Fact] @@ -99,7 +99,12 @@ public async Task logout_endpoint_should_redirect_to_external_signout_and_return await BffHost.BffLogoutAsync("sid123"); - BffHost.BrowserClient.CurrentUri.ToString().ToLowerInvariant().ShouldBe(BffHost.Url("/")); + BffHost.BrowserClient.CurrentUri + .ShouldNotBeNull() + .ToString() + .ToLowerInvariant() + .ShouldBe(BffHost.Url("/")); + (await BffHost.GetIsUserLoggedInAsync()).ShouldBeFalse(); } @@ -110,19 +115,19 @@ public async Task logout_endpoint_should_accept_returnUrl() var response = await BffHost.BrowserClient.GetAsync(BffHost.Url("/bff/logout") + "?sid=sid123&returnUrl=/foo"); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // endsession - response.Headers.Location.ToString().ToLowerInvariant().ShouldStartWith(IdentityServerHost.Url("/connect/endsession")); + response.Headers.Location!.ToString().ToLowerInvariant().ShouldStartWith(IdentityServerHost.Url("/connect/endsession")); - response = await IdentityServerHost.BrowserClient.GetAsync(response.Headers.Location.ToString()); + response = await IdentityServerHost.BrowserClient.GetAsync(response.Headers.Location!.ToString()); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // logout - response.Headers.Location.ToString().ToLowerInvariant().ShouldStartWith(IdentityServerHost.Url("/account/logout")); + response.Headers.Location!.ToString().ToLowerInvariant().ShouldStartWith(IdentityServerHost.Url("/account/logout")); - response = await IdentityServerHost.BrowserClient.GetAsync(response.Headers.Location.ToString()); + response = await IdentityServerHost.BrowserClient.GetAsync(response.Headers.Location!.ToString()); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // post logout redirect uri - response.Headers.Location.ToString().ToLowerInvariant().ShouldStartWith(BffHost.Url("/signout-callback-oidc")); + response.Headers.Location!.ToString().ToLowerInvariant().ShouldStartWith(BffHost.Url("/signout-callback-oidc")); - response = await BffHost.BrowserClient.GetAsync(response.Headers.Location.ToString()); + response = await BffHost.BrowserClient.GetAsync(response.Headers.Location!.ToString()); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // root - response.Headers.Location.ToString().ToLowerInvariant().ShouldBe("/foo"); + response.Headers.Location!.ToString().ToLowerInvariant().ShouldBe("/foo"); } [Fact] diff --git a/bff/test/Duende.Bff.Tests/Endpoints/Management/UserEndpointTests.cs b/bff/test/Duende.Bff.Tests/Endpoints/Management/UserEndpointTests.cs index 01ddf1d50..a023de29a 100644 --- a/bff/test/Duende.Bff.Tests/Endpoints/Management/UserEndpointTests.cs +++ b/bff/test/Duende.Bff.Tests/Endpoints/Management/UserEndpointTests.cs @@ -74,12 +74,12 @@ public async Task user_endpoint_for_unauthenticated_user_should_fail() } [Fact] - public async Task when_configured_user_endpoint_for_unauthenticated_user_should_return_200_and_null() + public async Task when_configured_user_endpoint_for_unauthenticated_user_should_return_200_and_empty() { BffHost.BffOptions.AnonymousSessionResponse = AnonymousSessionResponse.Response200; var data = await BffHost.CallUserEndpointAsync(); - data.ShouldBeNull(); + data.ShouldBeEmpty(); } } } diff --git a/bff/test/Duende.Bff.Tests/Endpoints/RemoteEndpointTests.cs b/bff/test/Duende.Bff.Tests/Endpoints/RemoteEndpointTests.cs index 844521945..e3cb5f2b6 100644 --- a/bff/test/Duende.Bff.Tests/Endpoints/RemoteEndpointTests.cs +++ b/bff/test/Duende.Bff.Tests/Endpoints/RemoteEndpointTests.cs @@ -9,8 +9,8 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Http.Json; using System.Security.Cryptography; -using System.Text; using System.Text.Json; using System.Threading.Tasks; using Shouldly; @@ -24,11 +24,10 @@ public class RemoteEndpointTests(ITestOutputHelper output) : BffIntegrationTestB [Fact] public async Task unauthenticated_calls_to_remote_endpoint_should_return_401() { - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/api_user/test")); - req.Headers.Add("x-csrf", "1"); - var response = await BffHost.BrowserClient.SendAsync(req); - - response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); + await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/api_user/test"), + expectedStatusCode: HttpStatusCode.Unauthorized + ); } [Fact] @@ -36,14 +35,10 @@ public async Task calls_to_remote_endpoint_should_forward_user_to_api() { await BffHost.BffLoginAsync("alice"); - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/api_user/test")); - req.Headers.Add("x-csrf", "1"); - var response = await BffHost.BrowserClient.SendAsync(req); + var apiResult = await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/api_user/test") + ); - response.IsSuccessStatusCode.ShouldBeTrue(); - response.Content.Headers.ContentType.MediaType.ShouldBe("application/json"); - var json = await response.Content.ReadAsStringAsync(); - var apiResult = JsonSerializer.Deserialize(json); apiResult.Method.ShouldBe("GET"); apiResult.Path.ShouldBe("/test"); apiResult.Sub.ShouldBe("alice"); @@ -53,16 +48,12 @@ public async Task calls_to_remote_endpoint_should_forward_user_to_api() [Fact] public async Task calls_to_remote_endpoint_with_useraccesstokenparameters_having_stored_named_token_should_forward_user_to_api() { - var loginResponse = await BffHostWithNamedTokens.BffLoginAsync("alice"); + await BffHostWithNamedTokens.BffLoginAsync("alice"); - var req = new HttpRequestMessage(HttpMethod.Get, BffHostWithNamedTokens.Url("/api_user_with_useraccesstokenparameters_having_stored_named_token/test")); - req.Headers.Add("x-csrf", "1"); - var response = await BffHostWithNamedTokens.BrowserClient.SendAsync(req); + var apiResult = await BffHostWithNamedTokens.BrowserClient.CallBffHostApi( + url: BffHostWithNamedTokens.Url("/api_user_with_useraccesstokenparameters_having_stored_named_token/test") + ); - response.IsSuccessStatusCode.ShouldBeTrue(); - response.Content.Headers.ContentType.MediaType.ShouldBe("application/json"); - var json = await response.Content.ReadAsStringAsync(); - var apiResult = JsonSerializer.Deserialize(json); apiResult.Method.ShouldBe("GET"); apiResult.Path.ShouldBe("/test"); apiResult.Sub.ShouldBe("alice"); @@ -70,15 +61,14 @@ public async Task calls_to_remote_endpoint_with_useraccesstokenparameters_having } [Fact] - public async Task calls_to_remote_endpoint_with_useraccesstokenparameters_having_not_stored_corresponding_named_token_finds_no_matching_token_should_fail() + public async Task calls_to_remote_endpoint_with_useraccesstokenparameters_having_not_stored_corresponding_named_token_finds_no_matching_token_should_fail() { - var loginResponse = await BffHostWithNamedTokens.BffLoginAsync("alice"); - - var req = new HttpRequestMessage(HttpMethod.Get, BffHostWithNamedTokens.Url("/api_user_with_useraccesstokenparameters_having_not_stored_named_token/test")); - req.Headers.Add("x-csrf", "1"); + await BffHostWithNamedTokens.BffLoginAsync("alice"); - var response = await BffHostWithNamedTokens.BrowserClient.SendAsync(req); - response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); + await BffHostWithNamedTokens.BrowserClient.CallBffHostApi( + url: BffHostWithNamedTokens.Url("/api_user_with_useraccesstokenparameters_having_not_stored_named_token/test"), + expectedStatusCode: HttpStatusCode.Unauthorized + ); } [Fact] @@ -86,59 +76,47 @@ public async Task put_to_remote_endpoint_should_forward_user_to_api() { await BffHost.BffLoginAsync("alice"); - var req = new HttpRequestMessage(HttpMethod.Put, BffHost.Url("/api_user/test")); - req.Headers.Add("x-csrf", "1"); - req.Content = new StringContent(JsonSerializer.Serialize(new TestPayload("hello test api")), Encoding.UTF8, "application/json"); - var response = await BffHost.BrowserClient.SendAsync(req); + var apiResult = await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/api_user/test"), + method: HttpMethod.Put, + content: JsonContent.Create(new TestPayload("hello test api")) + ); - response.IsSuccessStatusCode.ShouldBeTrue(); - response.Content.Headers.ContentType.MediaType.ShouldBe("application/json"); - var json = await response.Content.ReadAsStringAsync(); - var apiResult = JsonSerializer.Deserialize(json); apiResult.Method.ShouldBe("PUT"); apiResult.Path.ShouldBe("/test"); apiResult.Sub.ShouldBe("alice"); apiResult.ClientId.ShouldBe("spa"); - var body = JsonSerializer.Deserialize(apiResult.Body); - body.Message.ShouldBe("hello test api"); + var body = apiResult.BodyAs(); + body.Message.ShouldBe("hello test api", apiResult.Body); } - + [Fact] public async Task post_to_remote_endpoint_should_forward_user_to_api() { await BffHost.BffLoginAsync("alice"); - var req = new HttpRequestMessage(HttpMethod.Post, BffHost.Url("/api_user/test")); - req.Headers.Add("x-csrf", "1"); - req.Content = new StringContent(JsonSerializer.Serialize(new TestPayload("hello test api")), Encoding.UTF8, "application/json"); - var response = await BffHost.BrowserClient.SendAsync(req); + var apiResult = await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/api_user/test"), + method: HttpMethod.Post, + content: JsonContent.Create(new TestPayload("hello test api")) + ); - response.IsSuccessStatusCode.ShouldBeTrue(); - response.Content.Headers.ContentType.MediaType.ShouldBe("application/json"); - var json = await response.Content.ReadAsStringAsync(); - var apiResult = JsonSerializer.Deserialize(json); apiResult.Method.ShouldBe("POST"); apiResult.Path.ShouldBe("/test"); apiResult.Sub.ShouldBe("alice"); apiResult.ClientId.ShouldBe("spa"); - var body = JsonSerializer.Deserialize(apiResult.Body); - body.Message.ShouldBe("hello test api"); + var body = apiResult.BodyAs(); + body.Message.ShouldBe("hello test api", apiResult.Body); } - - [Fact] public async Task calls_to_remote_endpoint_should_forward_user_or_anonymous_to_api() { { - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/api_user_or_anon/test")); - req.Headers.Add("x-csrf", "1"); - var response = await BffHost.BrowserClient.SendAsync(req); - - response.IsSuccessStatusCode.ShouldBeTrue(); - response.Content.Headers.ContentType.MediaType.ShouldBe("application/json"); - var json = await response.Content.ReadAsStringAsync(); - var apiResult = JsonSerializer.Deserialize(json); + var apiResult = await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/api_user_or_anon/test") + ); + apiResult.Method.ShouldBe("GET"); apiResult.Path.ShouldBe("/test"); apiResult.Sub.ShouldBeNull(); @@ -148,14 +126,10 @@ public async Task calls_to_remote_endpoint_should_forward_user_or_anonymous_to_a { await BffHost.BffLoginAsync("alice"); - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/api_user_or_anon/test")); - req.Headers.Add("x-csrf", "1"); - var response = await BffHost.BrowserClient.SendAsync(req); + var apiResult = await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/api_user_or_anon/test") + ); - response.IsSuccessStatusCode.ShouldBeTrue(); - response.Content.Headers.ContentType.MediaType.ShouldBe("application/json"); - var json = await response.Content.ReadAsStringAsync(); - var apiResult = JsonSerializer.Deserialize(json); apiResult.Method.ShouldBe("GET"); apiResult.Path.ShouldBe("/test"); apiResult.Sub.ShouldBe("alice"); @@ -168,14 +142,10 @@ public async Task calls_to_remote_endpoint_should_forward_client_token_to_api() { await BffHost.BffLoginAsync("alice"); - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/api_client/test")); - req.Headers.Add("x-csrf", "1"); - var response = await BffHost.BrowserClient.SendAsync(req); + var apiResult = await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/api_client/test") + ); - response.IsSuccessStatusCode.ShouldBeTrue(); - response.Content.Headers.ContentType.MediaType.ShouldBe("application/json"); - var json = await response.Content.ReadAsStringAsync(); - var apiResult = JsonSerializer.Deserialize(json); apiResult.Method.ShouldBe("GET"); apiResult.Path.ShouldBe("/test"); apiResult.Sub.ShouldBeNull(); @@ -187,11 +157,10 @@ public async Task calls_to_remote_endpoint_should_fail_when_token_retrieval_fail { await BffHost.BffLoginAsync("alice"); - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/api_with_access_token_retrieval_that_fails")); - req.Headers.Add("x-csrf", "1"); - var response = await BffHost.BrowserClient.SendAsync(req); - - response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); + await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/api_with_access_token_retrieval_that_fails"), + expectedStatusCode: HttpStatusCode.Unauthorized + ); } [Fact] @@ -199,12 +168,10 @@ public async Task calls_to_remote_endpoint_should_send_token_from_token_retrieve { await BffHost.BffLoginAsync("alice"); - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/api_with_access_token_retriever")); - req.Headers.Add("x-csrf", "1"); - var response = await BffHost.BrowserClient.SendAsync(req); + var apiResult = await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/api_with_access_token_retriever") + ); - var json = await response.Content.ReadAsStringAsync(); - var apiResult = JsonSerializer.Deserialize(json); apiResult.Sub.ShouldBe("123"); apiResult.ClientId.ShouldBe("fake-client"); } @@ -213,14 +180,10 @@ public async Task calls_to_remote_endpoint_should_send_token_from_token_retrieve public async Task calls_to_remote_endpoint_should_forward_user_or_client_to_api() { { - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/api_user_or_client/test")); - req.Headers.Add("x-csrf", "1"); - var response = await BffHost.BrowserClient.SendAsync(req); - - response.IsSuccessStatusCode.ShouldBeTrue(); - response.Content.Headers.ContentType.MediaType.ShouldBe("application/json"); - var json = await response.Content.ReadAsStringAsync(); - var apiResult = JsonSerializer.Deserialize(json); + var apiResult = await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/api_user_or_client/test") + ); + apiResult.Method.ShouldBe("GET"); apiResult.Path.ShouldBe("/test"); apiResult.Sub.ShouldBeNull(); @@ -230,14 +193,10 @@ public async Task calls_to_remote_endpoint_should_forward_user_or_client_to_api( { await BffHost.BffLoginAsync("alice"); - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/api_user_or_client/test")); - req.Headers.Add("x-csrf", "1"); - var response = await BffHost.BrowserClient.SendAsync(req); + var apiResult = await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/api_user_or_client/test") + ); - response.IsSuccessStatusCode.ShouldBeTrue(); - response.Content.Headers.ContentType.MediaType.ShouldBe("application/json"); - var json = await response.Content.ReadAsStringAsync(); - var apiResult = JsonSerializer.Deserialize(json); apiResult.Method.ShouldBe("GET"); apiResult.Path.ShouldBe("/test"); apiResult.Sub.ShouldBe("alice"); @@ -249,14 +208,10 @@ public async Task calls_to_remote_endpoint_should_forward_user_or_client_to_api( public async Task calls_to_remote_endpoint_with_anon_should_be_anon() { { - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/api_anon_only/test")); - req.Headers.Add("x-csrf", "1"); - var response = await BffHost.BrowserClient.SendAsync(req); - - response.IsSuccessStatusCode.ShouldBeTrue(); - response.Content.Headers.ContentType.MediaType.ShouldBe("application/json"); - var json = await response.Content.ReadAsStringAsync(); - var apiResult = JsonSerializer.Deserialize(json); + var apiResult = await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/api_anon_only/test") + ); + apiResult.Method.ShouldBe("GET"); apiResult.Path.ShouldBe("/test"); apiResult.Sub.ShouldBeNull(); @@ -266,14 +221,10 @@ public async Task calls_to_remote_endpoint_with_anon_should_be_anon() { await BffHost.BffLoginAsync("alice"); - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/api_anon_only/test")); - req.Headers.Add("x-csrf", "1"); - var response = await BffHost.BrowserClient.SendAsync(req); + var apiResult = await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/api_anon_only/test") + ); - response.IsSuccessStatusCode.ShouldBeTrue(); - response.Content.Headers.ContentType.MediaType.ShouldBe("application/json"); - var json = await response.Content.ReadAsStringAsync(); - var apiResult = JsonSerializer.Deserialize(json); apiResult.Method.ShouldBe("GET"); apiResult.Path.ShouldBe("/test"); apiResult.Sub.ShouldBeNull(); @@ -281,42 +232,33 @@ public async Task calls_to_remote_endpoint_with_anon_should_be_anon() } } - [Fact] public async Task calls_to_remote_endpoint_expecting_token_but_without_token_should_fail() { var client = IdentityServerHost.Clients.Single(x => x.ClientId == "spa"); client.Enabled = false; - { - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/api_user_or_client/test")); - req.Headers.Add("x-csrf", "1"); - var response = await BffHost.BrowserClient.SendAsync(req); - - response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); - } - - { - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/api_client/test")); - req.Headers.Add("x-csrf", "1"); - var response = await BffHost.BrowserClient.SendAsync(req); + await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/api_user_or_client/test"), + expectedStatusCode: HttpStatusCode.Unauthorized + ); - response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); - } + await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/api_client/test"), + expectedStatusCode: HttpStatusCode.Unauthorized + ); } - [Fact] public async Task response_status_401_from_remote_endpoint_should_return_401_from_bff() { await BffHost.BffLoginAsync("alice"); ApiHost.ApiStatusCodeToReturn = 401; - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/api_user/test")); - req.Headers.Add("x-csrf", "1"); - var response = await BffHost.BrowserClient.SendAsync(req); - - response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); + await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/api_user/test"), + expectedStatusCode: HttpStatusCode.Unauthorized + ); } [Fact] @@ -325,15 +267,11 @@ public async Task response_status_403_from_remote_endpoint_should_return_403_fro await BffHost.BffLoginAsync("alice"); ApiHost.ApiStatusCodeToReturn = 403; - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/api_user/test")); - req.Headers.Add("x-csrf", "1"); - var response = await BffHost.BrowserClient.SendAsync(req); - - response.StatusCode.ShouldBe(HttpStatusCode.Forbidden); + await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/api_user/test"), + expectedStatusCode: HttpStatusCode.Forbidden + ); } - - - [Fact] public async Task calls_to_remote_endpoint_should_require_csrf() { @@ -348,13 +286,10 @@ public async Task endpoints_that_disable_csrf_should_not_require_csrf_header() { await BffHost.BffLoginAsync("alice"); - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/api_user_no_csrf/test")); - var response = await BffHost.BrowserClient.SendAsync(req); + var apiResult = await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/api_user_no_csrf/test") + ); - response.IsSuccessStatusCode.ShouldBeTrue(); - response.Content.Headers.ContentType.MediaType.ShouldBe("application/json"); - var json = await response.Content.ReadAsStringAsync(); - var apiResult = JsonSerializer.Deserialize(json); apiResult.Method.ShouldBe("GET"); apiResult.Path.ShouldBe("/test"); apiResult.Sub.ShouldBe("alice"); @@ -364,18 +299,18 @@ public async Task endpoints_that_disable_csrf_should_not_require_csrf_header() [Fact] public async Task calls_to_endpoint_without_bff_metadata_should_fail() { - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/not_bff_endpoint")); - - Func f = () => BffHost.BrowserClient.SendAsync(req); + Func f = () => BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/not_bff_endpoint") + ); await f.ShouldThrowAsync(); } - + [Fact] public async Task calls_to_bff_not_in_endpoint_routing_should_fail() { - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/invalid_endpoint/test")); - - Func f = () => BffHost.BrowserClient.SendAsync(req); + Func f = () => BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/invalid_endpoint/test") + ); await f.ShouldThrowAsync(); } @@ -396,16 +331,12 @@ public async Task test_dpop() }; await BffHost.InitializeAsync(); - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/api_client/test")); - req.Headers.Add("x-csrf", "1"); - var response = await BffHost.BrowserClient.SendAsync(req); + var apiResult = await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/api_client/test") + ); - response.IsSuccessStatusCode.ShouldBeTrue(); - response.Content.Headers.ContentType.MediaType.ShouldBe("application/json"); - var json = await response.Content.ReadAsStringAsync(); - var apiResult = JsonSerializer.Deserialize(json); apiResult.RequestHeaders["DPoP"].First().ShouldNotBeNullOrEmpty(); apiResult.RequestHeaders["Authorization"].First().StartsWith("DPoP ").ShouldBeTrue(); } } -} +} \ No newline at end of file diff --git a/bff/test/Duende.Bff.Tests/Endpoints/YarpRemoteEndpointTests.cs b/bff/test/Duende.Bff.Tests/Endpoints/YarpRemoteEndpointTests.cs index 697dc5d13..148208e2b 100644 --- a/bff/test/Duende.Bff.Tests/Endpoints/YarpRemoteEndpointTests.cs +++ b/bff/test/Duende.Bff.Tests/Endpoints/YarpRemoteEndpointTests.cs @@ -4,9 +4,7 @@ using Duende.Bff.Tests.TestHosts; using System.Net; using System.Net.Http; -using System.Text.Json; using System.Threading.Tasks; -using Duende.Bff.Tests.TestFramework; using Shouldly; using Xunit; using Xunit.Abstractions; @@ -18,12 +16,12 @@ public class YarpRemoteEndpointTests(ITestOutputHelper output) : YarpBffIntegrat [Fact] public async Task anonymous_call_with_no_csrf_header_to_no_token_requirement_no_csrf_route_should_succeed() { - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/api_anon_no_csrf/test")); - var response = await BffHost.BrowserClient.SendAsync(req); - - response.StatusCode.ShouldBe(HttpStatusCode.OK); + await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/api_anon_no_csrf/test"), + expectedStatusCode: HttpStatusCode.OK + ); } - + [Fact] public async Task anonymous_call_with_no_csrf_header_to_csrf_route_should_fail() { @@ -32,38 +30,33 @@ public async Task anonymous_call_with_no_csrf_header_to_csrf_route_should_fail() response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); } - + + [Fact] public async Task anonymous_call_to_no_token_requirement_route_should_succeed() { - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/api_anon/test")); - req.Headers.Add("x-csrf", "1"); - var response = await BffHost.BrowserClient.SendAsync(req); - - response.StatusCode.ShouldBe(HttpStatusCode.OK); + await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/api_anon/test"), + expectedStatusCode: HttpStatusCode.OK + ); } - + [Fact] public async Task anonymous_call_to_user_token_requirement_route_should_fail() { - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/api_user/test")); - req.Headers.Add("x-csrf", "1"); - var response = await BffHost.BrowserClient.SendAsync(req); - - response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); + await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/api_user/test"), + expectedStatusCode: HttpStatusCode.Unauthorized + ); } [Fact] public async Task anonymous_call_to_optional_user_token_route_should_succeed() { - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/api_optional_user/test")); - req.Headers.Add("x-csrf", "1"); - var response = await BffHost.BrowserClient.SendAsync(req); + var apiResult = await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/api_optional_user/test") + ); - response.IsSuccessStatusCode.ShouldBeTrue(); - response.Content.Headers.ContentType.MediaType.ShouldBe("application/json"); - var json = await response.Content.ReadAsStringAsync(); - var apiResult = JsonSerializer.Deserialize(json); apiResult.Method.ShouldBe("GET"); apiResult.Path.ShouldBe("/api_optional_user/test"); apiResult.Sub.ShouldBeNull(); @@ -76,36 +69,29 @@ public async Task anonymous_call_to_optional_user_token_route_should_succeed() public async Task authenticated_GET_should_forward_user_to_api(string route) { await BffHost.BffLoginAsync("alice"); - - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url(route)); - req.Headers.Add("x-csrf", "1"); - var response = await BffHost.BrowserClient.SendAsync(req); - - response.IsSuccessStatusCode.ShouldBeTrue(); - response.Content.Headers.ContentType.MediaType.ShouldBe("application/json"); - var json = await response.Content.ReadAsStringAsync(); - var apiResult = JsonSerializer.Deserialize(json); + + var apiResult = await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url(route) + ); + apiResult.Method.ShouldBe("GET"); apiResult.Path.ShouldBe(route); apiResult.Sub.ShouldBe("alice"); apiResult.ClientId.ShouldBe("spa"); } - + [Theory] [InlineData("/api_user/test")] [InlineData("/api_optional_user/test")] public async Task authenticated_PUT_should_forward_user_to_api(string route) { await BffHost.BffLoginAsync("alice"); - - var req = new HttpRequestMessage(HttpMethod.Put, BffHost.Url(route)); - req.Headers.Add("x-csrf", "1"); - var response = await BffHost.BrowserClient.SendAsync(req); - - response.IsSuccessStatusCode.ShouldBeTrue(); - response.Content.Headers.ContentType.MediaType.ShouldBe("application/json"); - var json = await response.Content.ReadAsStringAsync(); - var apiResult = JsonSerializer.Deserialize(json); + + var apiResult = await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url(route), + method: HttpMethod.Put + ); + apiResult.Method.ShouldBe("PUT"); apiResult.Path.ShouldBe(route); apiResult.Sub.ShouldBe("alice"); @@ -118,109 +104,92 @@ public async Task authenticated_PUT_should_forward_user_to_api(string route) public async Task authenticated_POST_should_forward_user_to_api(string route) { await BffHost.BffLoginAsync("alice"); - - var req = new HttpRequestMessage(HttpMethod.Post, BffHost.Url(route)); - req.Headers.Add("x-csrf", "1"); - var response = await BffHost.BrowserClient.SendAsync(req); - - response.IsSuccessStatusCode.ShouldBeTrue(); - response.Content.Headers.ContentType.MediaType.ShouldBe("application/json"); - var json = await response.Content.ReadAsStringAsync(); - var apiResult = JsonSerializer.Deserialize(json); + + var apiResult = await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url(route), + method: HttpMethod.Post + ); + apiResult.Method.ShouldBe("POST"); apiResult.Path.ShouldBe(route); apiResult.Sub.ShouldBe("alice"); apiResult.ClientId.ShouldBe("spa"); } - + [Fact] public async Task call_to_client_token_route_should_forward_client_token_to_api() { await BffHost.BffLoginAsync("alice"); - - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/api_client/test")); - req.Headers.Add("x-csrf", "1"); - var response = await BffHost.BrowserClient.SendAsync(req); - - response.IsSuccessStatusCode.ShouldBeTrue(); - response.Content.Headers.ContentType.MediaType.ShouldBe("application/json"); - var json = await response.Content.ReadAsStringAsync(); - var apiResult = JsonSerializer.Deserialize(json); + + var apiResult = await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/api_client/test") + ); + apiResult.Method.ShouldBe("GET"); apiResult.Path.ShouldBe("/api_client/test"); apiResult.Sub.ShouldBeNull(); apiResult.ClientId.ShouldBe("spa"); } - + [Fact] public async Task call_to_user_or_client_token_route_should_forward_user_or_client_token_to_api() { { - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/api_user_or_client/test")); - req.Headers.Add("x-csrf", "1"); - var response = await BffHost.BrowserClient.SendAsync(req); - - response.IsSuccessStatusCode.ShouldBeTrue(); - response.Content.Headers.ContentType.MediaType.ShouldBe("application/json"); - var json = await response.Content.ReadAsStringAsync(); - var apiResult = JsonSerializer.Deserialize(json); + var apiResult = await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/api_user_or_client/test") + ); + apiResult.Method.ShouldBe("GET"); apiResult.Path.ShouldBe("/api_user_or_client/test"); apiResult.Sub.ShouldBeNull(); apiResult.ClientId.ShouldBe("spa"); } - + { await BffHost.BffLoginAsync("alice"); - - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/api_user_or_client/test")); - req.Headers.Add("x-csrf", "1"); - var response = await BffHost.BrowserClient.SendAsync(req); - - response.IsSuccessStatusCode.ShouldBeTrue(); - response.Content.Headers.ContentType.MediaType.ShouldBe("application/json"); - var json = await response.Content.ReadAsStringAsync(); - var apiResult = JsonSerializer.Deserialize(json); + + var apiResult = await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/api_user_or_client/test") + ); + apiResult.Method.ShouldBe("GET"); apiResult.Path.ShouldBe("/api_user_or_client/test"); apiResult.Sub.ShouldBe("alice"); apiResult.ClientId.ShouldBe("spa"); } } - + [Fact] public async Task response_status_401_from_remote_endpoint_should_return_401_from_bff() { await BffHost.BffLoginAsync("alice"); ApiHost.ApiStatusCodeToReturn = 401; - - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/api_user/test")); - req.Headers.Add("x-csrf", "1"); - var response = await BffHost.BrowserClient.SendAsync(req); - - response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); + + var response = await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/api_user/test"), + expectedStatusCode: HttpStatusCode.Unauthorized + ); } - + [Fact] public async Task response_status_403_from_remote_endpoint_should_return_403_from_bff() { await BffHost.BffLoginAsync("alice"); ApiHost.ApiStatusCodeToReturn = 403; - - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/api_user/test")); - req.Headers.Add("x-csrf", "1"); - var response = await BffHost.BrowserClient.SendAsync(req); - - response.StatusCode.ShouldBe(HttpStatusCode.Forbidden); + + var response = await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/api_user/test"), + expectedStatusCode: HttpStatusCode.Forbidden + ); } [Fact] public async Task invalid_configuration_of_routes_should_return_500() { - var req = new HttpRequestMessage(HttpMethod.Get, BffHost.Url("/api_invalid/test")); - var response = await BffHost.BrowserClient.SendAsync(req); - - response.StatusCode.ShouldBe(HttpStatusCode.InternalServerError); + var response = await BffHost.BrowserClient.CallBffHostApi( + url: BffHost.Url("/api_invalid/test"), + expectedStatusCode: HttpStatusCode.InternalServerError + ); } } -} +} \ No newline at end of file diff --git a/bff/test/Duende.Bff.Tests/Headers/ApiAndBffUseForwardedHeaders.cs b/bff/test/Duende.Bff.Tests/Headers/ApiAndBffUseForwardedHeaders.cs index 8236df6af..ed88be0ad 100644 --- a/bff/test/Duende.Bff.Tests/Headers/ApiAndBffUseForwardedHeaders.cs +++ b/bff/test/Duende.Bff.Tests/Headers/ApiAndBffUseForwardedHeaders.cs @@ -14,13 +14,10 @@ public class ApiAndBffUseForwardedHeaders : BffIntegrationTestBase { public ApiAndBffUseForwardedHeaders(ITestOutputHelper output) : base(output) { - ApiHost = new ApiHost(output.WriteLine, IdentityServerHost, "scope1", useForwardedHeaders: true); - ApiHost.InitializeAsync().Wait(); - - BffHost = new BffHost(output.WriteLine, IdentityServerHost, ApiHost, "spa", useForwardedHeaders: true); - BffHost.InitializeAsync().Wait(); + BffHost.UseForwardedHeaders = true; + ApiHost.UseForwardedHeaders = true; } - + [Fact] public async Task bff_host_name_should_propagate_to_api() { @@ -30,7 +27,7 @@ public async Task bff_host_name_should_propagate_to_api() response.IsSuccessStatusCode.ShouldBeTrue(); var json = await response.Content.ReadAsStringAsync(); - var apiResult = JsonSerializer.Deserialize(json); + var apiResult = JsonSerializer.Deserialize(json).ShouldNotBeNull(); var host = apiResult.RequestHeaders["Host"].Single(); host.ShouldBe("app"); @@ -48,7 +45,7 @@ public async Task forwarded_host_name_without_header_forwarding_propagate_to_api response.IsSuccessStatusCode.ShouldBeTrue(); var json = await response.Content.ReadAsStringAsync(); - var apiResult = JsonSerializer.Deserialize(json); + var apiResult = JsonSerializer.Deserialize(json).ShouldNotBeNull(); var host = apiResult.RequestHeaders["Host"].Single(); host.ShouldBe("external"); @@ -66,7 +63,7 @@ public async Task forwarded_host_name_with_header_forwarding_should_propagate_to response.IsSuccessStatusCode.ShouldBeTrue(); var json = await response.Content.ReadAsStringAsync(); - var apiResult = JsonSerializer.Deserialize(json); + var apiResult = JsonSerializer.Deserialize(json).ShouldNotBeNull(); var host = apiResult.RequestHeaders["Host"].Single(); host.ShouldBe("external"); diff --git a/bff/test/Duende.Bff.Tests/Headers/ApiUseForwardedHeaders.cs b/bff/test/Duende.Bff.Tests/Headers/ApiUseForwardedHeaders.cs index 5e3d3d54e..b72afdb9c 100644 --- a/bff/test/Duende.Bff.Tests/Headers/ApiUseForwardedHeaders.cs +++ b/bff/test/Duende.Bff.Tests/Headers/ApiUseForwardedHeaders.cs @@ -14,13 +14,9 @@ public class ApiUseForwardedHeaders : BffIntegrationTestBase { public ApiUseForwardedHeaders(ITestOutputHelper output) : base(output) { - ApiHost = new ApiHost(output.WriteLine, IdentityServerHost, "scope1", useForwardedHeaders: true); - ApiHost.InitializeAsync().Wait(); - - BffHost = new BffHost(output.WriteLine, IdentityServerHost, ApiHost, "spa", useForwardedHeaders: false); - BffHost.InitializeAsync().Wait(); + ApiHost.UseForwardedHeaders = true; } - + [Fact] public async Task bff_host_name_should_propagate_to_api() { @@ -30,7 +26,7 @@ public async Task bff_host_name_should_propagate_to_api() response.IsSuccessStatusCode.ShouldBeTrue(); var json = await response.Content.ReadAsStringAsync(); - var apiResult = JsonSerializer.Deserialize(json); + var apiResult = JsonSerializer.Deserialize(json).ShouldNotBeNull(); var host = apiResult.RequestHeaders["Host"].Single(); host.ShouldBe("app"); @@ -46,7 +42,7 @@ public async Task forwarded_host_name_should_not_propagate_to_api() response.IsSuccessStatusCode.ShouldBeTrue(); var json = await response.Content.ReadAsStringAsync(); - var apiResult = JsonSerializer.Deserialize(json); + var apiResult = JsonSerializer.Deserialize(json).ShouldNotBeNull(); var host = apiResult.RequestHeaders["Host"].Single(); host.ShouldBe("app"); diff --git a/bff/test/Duende.Bff.Tests/Headers/General.cs b/bff/test/Duende.Bff.Tests/Headers/General.cs index 000ebfda4..e86cf3d4a 100644 --- a/bff/test/Duende.Bff.Tests/Headers/General.cs +++ b/bff/test/Duende.Bff.Tests/Headers/General.cs @@ -21,7 +21,7 @@ public async Task local_endpoint_should_receive_standard_headers() response.IsSuccessStatusCode.ShouldBeTrue(); var json = await response.Content.ReadAsStringAsync(); - var apiResult = JsonSerializer.Deserialize(json); + var apiResult = JsonSerializer.Deserialize(json).ShouldNotBeNull(); apiResult.RequestHeaders.Count.ShouldBe(2); apiResult.RequestHeaders["Host"].Single().ShouldBe("app"); @@ -40,7 +40,7 @@ public async Task custom_header_should_be_forwarded() response.IsSuccessStatusCode.ShouldBeTrue(); var json = await response.Content.ReadAsStringAsync(); - var apiResult = JsonSerializer.Deserialize(json); + var apiResult = JsonSerializer.Deserialize(json).ShouldNotBeNull(); apiResult.RequestHeaders["Host"].Single().ShouldBe("api"); apiResult.RequestHeaders["x-custom"].Single().ShouldBe("custom"); @@ -58,7 +58,7 @@ public async Task custom_header_should_be_forwarded_and_xforwarded_headers_shoul response.IsSuccessStatusCode.ShouldBeTrue(); var json = await response.Content.ReadAsStringAsync(); - var apiResult = JsonSerializer.Deserialize(json); + var apiResult = JsonSerializer.Deserialize(json).ShouldNotBeNull(); apiResult.RequestHeaders["X-Forwarded-Host"].Single().ShouldBe("app"); apiResult.RequestHeaders["X-Forwarded-Proto"].Single().ShouldBe("https"); diff --git a/bff/test/Duende.Bff.Tests/SessionManagement/CookieSlidingTests.cs b/bff/test/Duende.Bff.Tests/SessionManagement/CookieSlidingTests.cs index 8e0e0289f..e56749149 100644 --- a/bff/test/Duende.Bff.Tests/SessionManagement/CookieSlidingTests.cs +++ b/bff/test/Duende.Bff.Tests/SessionManagement/CookieSlidingTests.cs @@ -17,8 +17,8 @@ namespace Duende.Bff.Tests.SessionManagement { public class CookieSlidingTests : BffIntegrationTestBase { - InMemoryUserSessionStore _sessionStore = new InMemoryUserSessionStore(); - FakeTimeProvider _clock = new(DateTime.UtcNow); + readonly InMemoryUserSessionStore _sessionStore = new(); + readonly FakeTimeProvider _clock = new(DateTime.UtcNow); public CookieSlidingTests(ITestOutputHelper output) : base(output) { @@ -32,7 +32,6 @@ public CookieSlidingTests(ITestOutputHelper output) : base(output) }); services.AddSingleton(_clock); }; - BffHost.InitializeAsync().Wait(); } private void SetClock(TimeSpan t) diff --git a/bff/test/Duende.Bff.Tests/SessionManagement/ServerSideTicketStoreTests.cs b/bff/test/Duende.Bff.Tests/SessionManagement/ServerSideTicketStoreTests.cs index e0030a51b..109a6a8aa 100644 --- a/bff/test/Duende.Bff.Tests/SessionManagement/ServerSideTicketStoreTests.cs +++ b/bff/test/Duende.Bff.Tests/SessionManagement/ServerSideTicketStoreTests.cs @@ -13,7 +13,7 @@ namespace Duende.Bff.Tests.SessionManagement { public class ServerSideTicketStoreTests : BffIntegrationTestBase { - InMemoryUserSessionStore _sessionStore = new InMemoryUserSessionStore(); + readonly InMemoryUserSessionStore _sessionStore = new(); public ServerSideTicketStoreTests(ITestOutputHelper output) : base(output) { @@ -21,7 +21,6 @@ public ServerSideTicketStoreTests(ITestOutputHelper output) : base(output) { services.AddSingleton(_sessionStore); }; - BffHost.InitializeAsync().Wait(); } [Fact] diff --git a/bff/test/Duende.Bff.Tests/TestFramework/ApiResponse.cs b/bff/test/Duende.Bff.Tests/TestFramework/ApiResponse.cs index d25434a19..23128b47d 100644 --- a/bff/test/Duende.Bff.Tests/TestFramework/ApiResponse.cs +++ b/bff/test/Duende.Bff.Tests/TestFramework/ApiResponse.cs @@ -1,14 +1,23 @@ -// Copyright (c) Duende Software. All rights reserved. +// Copyright (c) Duende Software. All rights reserved. // See LICENSE in the project root for license information. +using System; using System.Collections.Generic; +using System.Text.Json; +using Shouldly; namespace Duende.Bff.Tests.TestFramework { - public record ApiResponse(string Method, string Path, string Sub, string ClientId, IEnumerable Claims) + public record ApiResponse(string Method, string Path, string? Sub, string? ClientId, IEnumerable Claims) { - public string Body { get; init; } + public required string? Body { get; init; } - public Dictionary> RequestHeaders { get; init; } + public Dictionary> RequestHeaders { get; init; } = new(); + + public T BodyAs() + { + Body.ShouldNotBeNull(); + return JsonSerializer.Deserialize(Body, TestSerializerOptions.Default) ?? throw new NullReferenceException($"result {Body} could not be deserialized to {typeof(T).Name}"); + } } } diff --git a/bff/test/Duende.Bff.Tests/TestFramework/GenericHost.cs b/bff/test/Duende.Bff.Tests/TestFramework/GenericHost.cs index dc035b5cf..edb92d423 100644 --- a/bff/test/Duende.Bff.Tests/TestFramework/GenericHost.cs +++ b/bff/test/Duende.Bff.Tests/TestFramework/GenericHost.cs @@ -13,39 +13,38 @@ using System.Linq; using System.Net; using System.Net.Http; -using System.Reflection; using System.Security.Claims; using System.Threading.Tasks; +using Microsoft.AspNetCore.HttpOverrides; namespace Duende.Bff.Tests.TestFramework { - public class GenericHost(WriteTestOutput writeOutput, string baseAddress = "https://server") + public class GenericHost(WriteTestOutput writeOutput, string baseAddress = "https://server"): IAsyncDisposable { private readonly string _baseAddress = baseAddress.EndsWith("/") ? baseAddress.Substring(0, baseAddress.Length - 1) : baseAddress; - IServiceProvider _appServices; + IServiceProvider _appServices = null!; - public Assembly HostAssembly { get; set; } - public bool IsDevelopment { get; set; } + public bool UseForwardedHeaders { get; set; } - public TestServer Server { get; private set; } - public TestBrowserClient BrowserClient { get; set; } - public HttpClient HttpClient { get; set; } + public TestServer Server { get; private set; } = null!; + public TestBrowserClient BrowserClient { get; private set; } = null!; + public HttpClient HttpClient { get; private set; } = null!; - public TestLoggerProvider Logger { get; set; } = new TestLoggerProvider(writeOutput, baseAddress + " - "); + private TestLoggerProvider Logger { get; } = new(writeOutput, baseAddress + " - "); - public T Resolve() + public T Resolve() where T:notnull { // not calling dispose on scope on purpose return _appServices.GetRequiredService().CreateScope().ServiceProvider.GetRequiredService(); } - public string Url(string path = null) + public string Url(string? path = null) { - path = path ?? String.Empty; + path ??= String.Empty; if (!path.StartsWith("/")) path = "/" + path; return _baseAddress + path; } @@ -57,25 +56,21 @@ public async Task InitializeAsync() { builder.UseTestServer(); - builder.ConfigureAppConfiguration((context, b) => + builder.ConfigureServices(ConfigureServices); + builder.Configure(app => { - if (HostAssembly is not null) + if (UseForwardedHeaders) { - context.HostingEnvironment.ApplicationName = HostAssembly.GetName().Name; + app.UseForwardedHeaders(new ForwardedHeadersOptions + { + ForwardedHeaders = ForwardedHeaders.XForwardedFor | + ForwardedHeaders.XForwardedProto | + ForwardedHeaders.XForwardedHost + }); } - }); - - if (IsDevelopment) - { - builder.UseSetting("Environment", "Development"); - } - else - { - builder.UseSetting("Environment", "Production"); - } - builder.ConfigureServices(ConfigureServices); - builder.Configure(ConfigureApp); + ConfigureApp(app); + }); }); // Build and start the IHost @@ -86,8 +81,8 @@ public async Task InitializeAsync() HttpClient = Server.CreateClient(); } - public event Action OnConfigureServices = services => { }; - public event Action OnConfigure = app => { }; + public event Action OnConfigureServices = _ => { }; + public event Action OnConfigure = _ => { }; void ConfigureServices(IServiceCollection services) { @@ -137,7 +132,7 @@ void ConfigureSignin(IApplicationBuilder app) { if (ctx.Request.Path == "/__signin") { - if (_userToSignIn is not object) + if (_userToSignIn is null) { throw new Exception("No User Configured for SignIn"); } @@ -155,15 +150,16 @@ void ConfigureSignin(IApplicationBuilder app) await next(); }); } - ClaimsPrincipal _userToSignIn; - AuthenticationProperties _propsToSignIn; + ClaimsPrincipal? _userToSignIn; + AuthenticationProperties? _propsToSignIn; public async Task IssueSessionCookieAsync(params Claim[] claims) { _userToSignIn = new ClaimsPrincipal(new ClaimsIdentity(claims, "test", "name", "role")); var response = await BrowserClient.GetAsync(Url("__signin")); response.StatusCode.ShouldBe(HttpStatusCode.NoContent); } - public Task IssueSessionCookieAsync(AuthenticationProperties props, params Claim[] claims) + + protected Task IssueSessionCookieAsync(AuthenticationProperties props, params Claim[] claims) { _propsToSignIn = props; return IssueSessionCookieAsync(claims); @@ -172,5 +168,27 @@ public Task IssueSessionCookieAsync(string sub, params Claim[] claims) { return IssueSessionCookieAsync(claims.Append(new Claim("sub", sub)).ToArray()); } + + public async ValueTask DisposeAsync() + { + await CastAndDispose(Server); + await CastAndDispose(BrowserClient); + await CastAndDispose(HttpClient); + await CastAndDispose(Logger); + + return; + + static async ValueTask CastAndDispose(IDisposable resource) + { + if (resource is IAsyncDisposable resourceAsyncDisposable) + { + await resourceAsyncDisposable.DisposeAsync(); + } + else + { + resource.Dispose(); + } + } + } } } diff --git a/bff/test/Duende.Bff.Tests/TestFramework/MockExternalAuthenticationHandler.cs b/bff/test/Duende.Bff.Tests/TestFramework/MockExternalAuthenticationHandler.cs index bcd504d1e..c8f36c4c3 100644 --- a/bff/test/Duende.Bff.Tests/TestFramework/MockExternalAuthenticationHandler.cs +++ b/bff/test/Duende.Bff.Tests/TestFramework/MockExternalAuthenticationHandler.cs @@ -1,70 +1,56 @@ -// Copyright (c) Duende Software. All rights reserved. +// Copyright (c) Duende Software. All rights reserved. // See LICENSE in the project root for license information. -using Duende.Bff.Tests.TestFramework; +using System.Text.Encodings.Web; +using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using System; -using System.Text.Encodings.Web; -using System.Threading.Tasks; -namespace Duende.Bff.Tests.TestFramework +namespace Duende.Bff.Tests.TestFramework; + +public class MockExternalAuthenticationHandler : RemoteAuthenticationHandler, + IAuthenticationSignOutHandler { - public class MockExternalAuthenticationHandler : RemoteAuthenticationHandler, IAuthenticationSignOutHandler + public MockExternalAuthenticationHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder) + : base(options, logger, encoder) { - public MockExternalAuthenticationHandler( - IOptionsMonitor options, - ILoggerFactory logger, - UrlEncoder encoder) - : base(options, logger, encoder) - { - } + } - public bool ChallengeWasCalled { get; set; } - public AuthenticationProperties ChallengeAuthenticationProperties { get; set; } - protected override Task HandleChallengeAsync(AuthenticationProperties properties) - { - ChallengeWasCalled = true; - ChallengeAuthenticationProperties = properties; - return Task.CompletedTask; - } + public bool ChallengeWasCalled { get; set; } = false; + public AuthenticationProperties? ChallengeAuthenticationProperties { get; set; } - protected override Task HandleRemoteAuthenticateAsync() - { - var result = HandleRequestResult.NoResult(); - return Task.FromResult(result); - } + protected override Task HandleChallengeAsync(AuthenticationProperties properties) + { + ChallengeWasCalled = true; + ChallengeAuthenticationProperties = properties; + return Task.CompletedTask; + } - public bool SignOutWasCalled { get; set; } - public AuthenticationProperties SignOutAuthenticationProperties { get; set; } - public Task SignOutAsync(AuthenticationProperties properties) - { - SignOutWasCalled = true; - SignOutAuthenticationProperties = properties; - return Task.CompletedTask; - } + protected override Task HandleRemoteAuthenticateAsync() + { + var result = HandleRequestResult.NoResult(); + return Task.FromResult(result); } - public class MockExternalAuthenticationOptions : RemoteAuthenticationOptions + public bool SignOutWasCalled { get; set; } + public AuthenticationProperties? SignOutAuthenticationProperties { get; set; } + + public Task SignOutAsync(AuthenticationProperties? properties) { - public MockExternalAuthenticationOptions() - { - CallbackPath = "/external-callback"; - } + SignOutWasCalled = true; + SignOutAuthenticationProperties = properties; + return Task.CompletedTask; } } -namespace Microsoft.Extensions.DependencyInjection +public class MockExternalAuthenticationOptions : RemoteAuthenticationOptions { - public static class MockExternalAuthenticationExtensions + public MockExternalAuthenticationOptions() { - public static AuthenticationBuilder AddMockExternalAuthentication(this AuthenticationBuilder builder, - string authenticationScheme = "external", - string displayName = "external", - Action configureOptions = null) - { - return builder.AddRemoteScheme(authenticationScheme, displayName, configureOptions); - } + CallbackPath = "/external-callback"; } -} +} \ No newline at end of file diff --git a/bff/test/Duende.Bff.Tests/TestFramework/MockSessionRevocationService.cs b/bff/test/Duende.Bff.Tests/TestFramework/MockSessionRevocationService.cs index 7c716a763..ae638819a 100644 --- a/bff/test/Duende.Bff.Tests/TestFramework/MockSessionRevocationService.cs +++ b/bff/test/Duende.Bff.Tests/TestFramework/MockSessionRevocationService.cs @@ -1,4 +1,4 @@ -// Copyright (c) Duende Software. All rights reserved. +// Copyright (c) Duende Software. All rights reserved. // See LICENSE in the project root for license information. using System.Threading; @@ -9,7 +9,7 @@ namespace Duende.Bff.Tests.TestFramework public class MockSessionRevocationService : ISessionRevocationService { public bool DeleteUserSessionsWasCalled { get; set; } - public UserSessionsFilter DeleteUserSessionsFilter { get; set; } + public UserSessionsFilter? DeleteUserSessionsFilter { get; set; } public Task RevokeSessionsAsync(UserSessionsFilter filter, CancellationToken cancellationToken) { DeleteUserSessionsWasCalled = true; diff --git a/bff/test/Duende.Bff.Tests/TestFramework/Records.cs b/bff/test/Duende.Bff.Tests/TestFramework/Records.cs index 49ffe0bb8..be74df303 100644 --- a/bff/test/Duende.Bff.Tests/TestFramework/Records.cs +++ b/bff/test/Duende.Bff.Tests/TestFramework/Records.cs @@ -9,18 +9,18 @@ namespace Duende.Bff.Tests.TestFramework public record JsonRecord(string Type, JsonElement Value) { [JsonPropertyName("type")] - public string Type { get; init; } = Type; + public string Type { get; } = Type; [JsonPropertyName("value")] - public JsonElement Value { get; init; } = Value; + public JsonElement Value { get; } = Value; } public record ClaimRecord(string Type, string Value) { [JsonPropertyName("type")] - public string Type { get; init; } = Type; + public string Type { get; } = Type; [JsonPropertyName("value")] - public string Value { get; init; } = Value; + public string Value { get; } = Value; } } diff --git a/bff/test/Duende.Bff.Tests/TestFramework/TestBrowserClient.cs b/bff/test/Duende.Bff.Tests/TestFramework/TestBrowserClient.cs index 4a64c20d1..ddd89e6fc 100644 --- a/bff/test/Duende.Bff.Tests/TestFramework/TestBrowserClient.cs +++ b/bff/test/Duende.Bff.Tests/TestFramework/TestBrowserClient.cs @@ -1,79 +1,150 @@ -// Copyright (c) Duende Software. All rights reserved. +// Copyright (c) Duende Software. All rights reserved. // See LICENSE in the project root for license information. using System; using System.Linq; using System.Net; using System.Net.Http; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Shouldly; -namespace Duende.Bff.Tests.TestFramework +namespace Duende.Bff.Tests.TestFramework; + +public class TestBrowserClient : HttpClient { - public class TestBrowserClient : HttpClient + private class CookieHandler(HttpMessageHandler next) : DelegatingHandler(next) { - class CookieHandler : DelegatingHandler - { - public CookieContainer CookieContainer { get; } = new CookieContainer(); - public Uri CurrentUri { get; private set; } - public HttpResponseMessage LastResponse { get; private set; } + public CookieContainer CookieContainer { get; } = new(); + public Uri? CurrentUri { get; private set; } + public HttpResponseMessage? LastResponse { get; private set; } - public CookieHandler(HttpMessageHandler next) - : base(next) + protected override async Task SendAsync(HttpRequestMessage request, + CancellationToken cancellationToken) + { + CurrentUri = request.RequestUri ?? throw new NullReferenceException("RequestUri is not set"); + var cookieHeader = CookieContainer.GetCookieHeader(request.RequestUri); + if (!string.IsNullOrEmpty(cookieHeader)) { + request.Headers.Add("Cookie", cookieHeader); } - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + var response = await base.SendAsync(request, cancellationToken); + + if (response.Headers.Contains("Set-Cookie")) { - CurrentUri = request.RequestUri; - string cookieHeader = CookieContainer.GetCookieHeader(request.RequestUri); - if (!string.IsNullOrEmpty(cookieHeader)) - { - request.Headers.Add("Cookie", cookieHeader); - } + var responseCookieHeader = string.Join(",", response.Headers.GetValues("Set-Cookie")); + CookieContainer.SetCookies(request.RequestUri, responseCookieHeader); + } - var response = await base.SendAsync(request, cancellationToken); + LastResponse = response; - if (response.Headers.Contains("Set-Cookie")) - { - var responseCookieHeader = string.Join(",", response.Headers.GetValues("Set-Cookie")); - CookieContainer.SetCookies(request.RequestUri, responseCookieHeader); - } + return response; + } + } - LastResponse = response; + private readonly CookieHandler _handler; - return response; - } - } + private CookieContainer CookieContainer => _handler.CookieContainer; + public Uri? CurrentUri => _handler.CurrentUri; + public HttpResponseMessage? LastResponse => _handler.LastResponse; - private CookieHandler _handler; - - public CookieContainer CookieContainer => _handler.CookieContainer; - public Uri CurrentUri => _handler.CurrentUri; - public HttpResponseMessage LastResponse => _handler.LastResponse; + public TestBrowserClient(HttpMessageHandler handler) + : this(new CookieHandler(handler)) + { + } - public TestBrowserClient(HttpMessageHandler handler) - : this(new CookieHandler(handler)) + private TestBrowserClient(CookieHandler handler) + : base(handler) + { + _handler = handler; + } + + public void RemoveCookie(string name) + { + if (CurrentUri == null) throw new NullReferenceException("CurrentUri is null"); + + RemoveCookie(CurrentUri.ToString(), name); + } + + private void RemoveCookie(string uri, string name) + { + var cookie = CookieContainer.GetCookies(new Uri(uri)).FirstOrDefault(x => x.Name == name); + if (cookie != null) { + cookie.Expired = true; } + } + + + /// + /// Calls the specified BFF api and verifies that the response is successful + /// and returns the ApiResponse object. + /// + /// The url to call + /// If specified, the system will verify that this reponse code was given + /// Cancellation token + /// The specified api response + public async Task CallBffHostApi( + string url, + HttpStatusCode? expectedStatusCode = null, + CancellationToken ct = default) + { + var req = new HttpRequestMessage(HttpMethod.Get, url); + req.Headers.Add("x-csrf", "1"); + var response = await SendAsync(req, ct); + + if (expectedStatusCode == null) + { + response.IsSuccessStatusCode.ShouldBeTrue(); + response.Content.Headers.ContentType?.MediaType.ShouldBe("application/json"); + var json = await response.Content.ReadAsStringAsync(ct); + var apiResult = JsonSerializer.Deserialize(json).ShouldNotBeNull(); + + apiResult.Method.ShouldBe("GET", StringCompareShould.IgnoreCase); - private TestBrowserClient(CookieHandler handler) - : base(handler) + return apiResult; + } + else { - _handler = handler; + response.StatusCode.ToString().ShouldBe(expectedStatusCode.ToString()); + return null!; } - public void RemoveCookie(string name) + } + + public async Task CallBffHostApi( + string url, + HttpMethod method, + HttpContent? content = null, + HttpStatusCode? expectedStatusCode = null, + CancellationToken ct = default) + { + var req = new HttpRequestMessage(method, url); + if (req.Content == null) { - RemoveCookie(CurrentUri.ToString(), name); + req.Content = content; } - public void RemoveCookie(string uri, string name) + req.Headers.Add("x-csrf", "1"); + var response = await SendAsync(req, ct); + if (expectedStatusCode == null) { - var cookie = CookieContainer.GetCookies(new Uri(uri)).FirstOrDefault(x => x.Name == name); - if (cookie != null) - { - cookie.Expired = true; - } + response.IsSuccessStatusCode.ShouldBeTrue(); + response.Content.Headers.ContentType?.MediaType.ShouldBe("application/json"); + var json = await response.Content.ReadAsStringAsync(ct); + var apiResult = JsonSerializer.Deserialize(json).ShouldNotBeNull(); + + apiResult.Method.ShouldBe(method.ToString(), StringCompareShould.IgnoreCase); + return apiResult; + } + else + { + response.StatusCode.ToString().ShouldBe(expectedStatusCode.ToString()); + return null!; } } + + + } \ No newline at end of file diff --git a/bff/test/Duende.Bff.Tests/TestFramework/TestLoggerProvider.cs b/bff/test/Duende.Bff.Tests/TestFramework/TestLoggerProvider.cs index 8d6210996..98ed2f417 100644 --- a/bff/test/Duende.Bff.Tests/TestFramework/TestLoggerProvider.cs +++ b/bff/test/Duende.Bff.Tests/TestFramework/TestLoggerProvider.cs @@ -12,7 +12,7 @@ public class TestLoggerProvider(WriteTestOutput writeOutput, string name) : ILog private readonly WriteTestOutput _writeOutput = writeOutput ?? throw new ArgumentNullException(nameof(writeOutput)); private readonly string _name = name ?? throw new ArgumentNullException(nameof(name)); - public class DebugLogger : ILogger, IDisposable + private class DebugLogger : ILogger, IDisposable { private readonly TestLoggerProvider _parent; private readonly string _category; @@ -27,7 +27,7 @@ public void Dispose() { } - public IDisposable BeginScope(TState state) + public IDisposable BeginScope(TState state) where TState : notnull { return this; } @@ -37,7 +37,7 @@ public bool IsEnabled(LogLevel logLevel) return true; } - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { var msg = $"[{logLevel}] {_category} : {formatter(state, exception)}"; _parent.Log(msg); diff --git a/bff/test/Duende.Bff.Tests/TestFramework/TestSerializerOptions.cs b/bff/test/Duende.Bff.Tests/TestFramework/TestSerializerOptions.cs new file mode 100644 index 000000000..7d6d11ddb --- /dev/null +++ b/bff/test/Duende.Bff.Tests/TestFramework/TestSerializerOptions.cs @@ -0,0 +1,15 @@ +// // Copyright (c) Duende Software. All rights reserved. +// // See LICENSE in the project root for license information. + +using System.Text.Json; + +namespace Duende.Bff.Tests.TestFramework; + +public static class TestSerializerOptions +{ + public static JsonSerializerOptions Default => new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true + }; +} diff --git a/bff/test/Duende.Bff.Tests/TestHosts/ApiHost.cs b/bff/test/Duende.Bff.Tests/TestHosts/ApiHost.cs index 35d08fc81..66a469fa1 100644 --- a/bff/test/Duende.Bff.Tests/TestHosts/ApiHost.cs +++ b/bff/test/Duende.Bff.Tests/TestHosts/ApiHost.cs @@ -20,18 +20,15 @@ public class ApiHost : GenericHost public int? ApiStatusCodeToReturn { get; set; } private readonly IdentityServerHost _identityServerHost; - private readonly bool _useForwardedHeaders; public ApiHost( WriteTestOutput output, IdentityServerHost identityServerHost, string scope, - string baseAddress = "https://api", - bool useForwardedHeaders = false) + string baseAddress = "https://api") : base(output, baseAddress) { _identityServerHost = identityServerHost; - _useForwardedHeaders = useForwardedHeaders; _identityServerHost.ApiScopes.Add(new ApiScope(scope)); @@ -56,17 +53,6 @@ private void ConfigureServices(IServiceCollection services) private void Configure(IApplicationBuilder app) { - if (_useForwardedHeaders) - { - app.UseForwardedHeaders(new ForwardedHeadersOptions - { - ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost, - - // allow double-hop scenarios - ForwardLimit = null - }); - } - app.UseRouting(); app.UseAuthentication(); @@ -90,16 +76,16 @@ private void Configure(IApplicationBuilder app) var requestHeaders = new Dictionary>(); foreach (var header in context.Request.Headers) { - var values = new List(header.Value.Select(v => v)); + var values = new List(header.Value.Select(v => v ?? string.Empty)); requestHeaders.Add(header.Key, values); } var response = new ApiResponse( - context.Request.Method, - context.Request.Path.Value, - context.User.FindFirst("sub")?.Value, - context.User.FindFirst("client_id")?.Value, - context.User.Claims.Select(x => new ClaimRecord(x.Type, x.Value)).ToArray()) + Method: context.Request.Method, + Path: context.Request.Path.Value ?? "/", + Sub: context.User.FindFirst("sub")?.Value, + ClientId: context.User.FindFirst("client_id")?.Value, + Claims: context.User.Claims.Select(x => new ClaimRecord(x.Type, x.Value)).ToArray()) { Body = body, RequestHeaders = requestHeaders diff --git a/bff/test/Duende.Bff.Tests/TestHosts/BffHost.cs b/bff/test/Duende.Bff.Tests/TestHosts/BffHost.cs index 735a23e9c..30f26690e 100644 --- a/bff/test/Duende.Bff.Tests/TestHosts/BffHost.cs +++ b/bff/test/Duende.Bff.Tests/TestHosts/BffHost.cs @@ -31,9 +31,7 @@ public enum ResponseStatus private readonly IdentityServerHost _identityServerHost; private readonly ApiHost _apiHost; private readonly string _clientId; - private readonly bool _useForwardedHeaders; - - public BffOptions BffOptions { get; private set; } + public BffOptions BffOptions { get; private set; } = null!; public BffHost( WriteTestOutput output, @@ -47,7 +45,7 @@ public BffHost( _identityServerHost = identityServerHost; _apiHost = apiHost; _clientId = clientId; - _useForwardedHeaders = useForwardedHeaders; + UseForwardedHeaders = useForwardedHeaders; OnConfigureServices += ConfigureServices; OnConfigure += Configure; @@ -122,16 +120,6 @@ private void ConfigureServices(IServiceCollection services) private void Configure(IApplicationBuilder app) { - if (_useForwardedHeaders) - { - app.UseForwardedHeaders(new ForwardedHeadersOptions - { - ForwardedHeaders = ForwardedHeaders.XForwardedFor | - ForwardedHeaders.XForwardedProto | - ForwardedHeaders.XForwardedHost - }); - } - app.UseAuthentication(); app.UseRouting(); @@ -159,13 +147,13 @@ private void Configure(IApplicationBuilder app) var requestHeaders = new Dictionary>(); foreach (var header in context.Request.Headers) { - var values = new List(header.Value.Select(v => v)); + var values = new List(header.Value.Select(v => v!)); requestHeaders.Add(header.Key, values); } var response = new ApiResponse( context.Request.Method, - context.Request.Path.Value, + context.Request.Path.Value ?? "/", context.User.FindFirst("sub")?.Value, context.User.FindFirst("client_id")?.Value, context.User.Claims.Select(x => new ClaimRecord(x.Type, x.Value)).ToArray()) @@ -212,13 +200,13 @@ private void Configure(IApplicationBuilder app) var requestHeaders = new Dictionary>(); foreach (var header in context.Request.Headers) { - var values = new List(header.Value.Select(v => v)); + var values = new List(header.Value.Select(v => v)!); requestHeaders.Add(header.Key, values); } var response = new ApiResponse( context.Request.Method, - context.Request.Path.Value, + context.Request.Path.Value ?? "/", context.User.FindFirst("sub")?.Value, context.User.FindFirst("client_id")?.Value, context.User.Claims.Select(x => new ClaimRecord(x.Type, x.Value)).ToArray()) @@ -266,13 +254,13 @@ private void Configure(IApplicationBuilder app) var requestHeaders = new Dictionary>(); foreach (var header in context.Request.Headers) { - var values = new List(header.Value.Select(v => v)); + var values = new List(header.Value.Select(v => v!)); requestHeaders.Add(header.Key, values); } var response = new ApiResponse( context.Request.Method, - context.Request.Path.Value, + context.Request.Path.Value ?? "/", context.User.FindFirst("sub")?.Value, context.User.FindFirst("client_id")?.Value, context.User.Claims.Select(x => new ClaimRecord(x.Type, x.Value)).ToArray()) @@ -322,7 +310,7 @@ private void Configure(IApplicationBuilder app) var response = new ApiResponse( context.Request.Method, - context.Request.Path.Value, + context.Request.Path.Value ?? "/", sub, context.User.FindFirst("client_id")?.Value, context.User.Claims.Select(x => new ClaimRecord(x.Type, x.Value)).ToArray()) @@ -369,7 +357,7 @@ private void Configure(IApplicationBuilder app) var response = new ApiResponse( context.Request.Method, - context.Request.Path.Value, + context.Request.Path.Value ?? "/", sub, context.User.FindFirst("client_id")?.Value, context.User.Claims.Select(x => new ClaimRecord(x.Type, x.Value)).ToArray()) @@ -452,7 +440,7 @@ private void Configure(IApplicationBuilder app) invalid => invalid.Use(next => RemoteApiEndpoint.Map("/invalid_endpoint", _apiHost.Url()))); } - public async Task GetIsUserLoggedInAsync(string userQuery = null) + public async Task GetIsUserLoggedInAsync(string? userQuery = null) { if (userQuery != null) userQuery = "?" + userQuery; @@ -474,13 +462,13 @@ public async Task> CallUserEndpointAsync() var response = await BrowserClient.SendAsync(req); response.StatusCode.ShouldBe(HttpStatusCode.OK); - response.Content.Headers.ContentType.MediaType.ShouldBe("application/json"); + response.Content.Headers.ContentType?.MediaType.ShouldBe("application/json"); var json = await response.Content.ReadAsStringAsync(); - return JsonSerializer.Deserialize>(json); + return JsonSerializer.Deserialize>(json, TestSerializerOptions.Default) ?? []; } - public async Task BffLoginAsync(string sub, string sid = null) + public async Task BffLoginAsync(string sub, string? sid = null) { await _identityServerHost.CreateIdentityServerSessionCookieAsync(sub, sid); return await BffOidcLoginAsync(); @@ -490,16 +478,16 @@ public async Task BffOidcLoginAsync() { var response = await BrowserClient.GetAsync(Url("/bff/login")); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // authorize - response.Headers.Location.ToString().ToLowerInvariant() + response.Headers.Location!.ToString().ToLowerInvariant() .ShouldStartWith(_identityServerHost.Url("/connect/authorize")); response = await _identityServerHost.BrowserClient.GetAsync(response.Headers.Location.ToString()); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // client callback - response.Headers.Location.ToString().ToLowerInvariant().ShouldStartWith(Url("/signin-oidc")); + response.Headers.Location!.ToString().ToLowerInvariant().ShouldStartWith(Url("/signin-oidc")); response = await BrowserClient.GetAsync(response.Headers.Location.ToString()); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // root - response.Headers.Location.ToString().ToLowerInvariant().ShouldBe("/"); + response.Headers.Location!.ToString().ToLowerInvariant().ShouldBe("/"); (await GetIsUserLoggedInAsync()).ShouldBeTrue(); @@ -507,23 +495,23 @@ public async Task BffOidcLoginAsync() return response; } - public async Task BffLogoutAsync(string sid = null) + public async Task BffLogoutAsync(string? sid = null) { var response = await BrowserClient.GetAsync(Url("/bff/logout") + "?sid=" + sid); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // endsession - response.Headers.Location.ToString().ToLowerInvariant().ShouldStartWith(_identityServerHost.Url("/connect/endsession")); + response.Headers.Location!.ToString().ToLowerInvariant().ShouldStartWith(_identityServerHost.Url("/connect/endsession")); response = await _identityServerHost.BrowserClient.GetAsync(response.Headers.Location.ToString()); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // logout - response.Headers.Location.ToString().ToLowerInvariant().ShouldStartWith(_identityServerHost.Url("/account/logout")); + response.Headers.Location!.ToString().ToLowerInvariant().ShouldStartWith(_identityServerHost.Url("/account/logout")); response = await _identityServerHost.BrowserClient.GetAsync(response.Headers.Location.ToString()); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // post logout redirect uri - response.Headers.Location.ToString().ToLowerInvariant().ShouldStartWith(Url("/signout-callback-oidc")); + response.Headers.Location!.ToString().ToLowerInvariant().ShouldStartWith(Url("/signout-callback-oidc")); response = await BrowserClient.GetAsync(response.Headers.Location.ToString()); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // root - response.Headers.Location.ToString().ToLowerInvariant().ShouldBe("/"); + response.Headers.Location!.ToString().ToLowerInvariant().ShouldBe("/"); (await GetIsUserLoggedInAsync()).ShouldBeFalse(); @@ -531,18 +519,13 @@ public async Task BffLogoutAsync(string sid = null) return response; } - public class CallbackHttpMessageInvokerFactory : IHttpMessageInvokerFactory + private class CallbackHttpMessageInvokerFactory(Func callback) + : IHttpMessageInvokerFactory { - public CallbackHttpMessageInvokerFactory(Func callback) - { - CreateInvoker = callback; - } - - public Func CreateInvoker { get; set; } public HttpMessageInvoker CreateClient(string localPath) { - return CreateInvoker.Invoke(localPath); + return callback.Invoke(localPath); } } } \ No newline at end of file diff --git a/bff/test/Duende.Bff.Tests/TestHosts/BffHostUsingResourceNamedTokens.cs b/bff/test/Duende.Bff.Tests/TestHosts/BffHostUsingResourceNamedTokens.cs index e980fa3f0..25ce6cc8c 100644 --- a/bff/test/Duende.Bff.Tests/TestHosts/BffHostUsingResourceNamedTokens.cs +++ b/bff/test/Duende.Bff.Tests/TestHosts/BffHostUsingResourceNamedTokens.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Http.Json; using System.Text.Json; using System.Threading.Tasks; using Duende.Bff.Yarp; @@ -29,7 +30,7 @@ public class BffHostUsingResourceNamedTokens : GenericHost private readonly string _clientId; private readonly bool _useForwardedHeaders; - public BffOptions BffOptions { get; private set; } + public BffOptions BffOptions { get; private set; } = null!; public BffHostUsingResourceNamedTokens( WriteTestOutput output, @@ -80,7 +81,8 @@ private void ConfigureServices(IServiceCollection services) { options.Events.OnUserInformationReceived = context => { - StoreNamedTokens((context.ProtocolMessage.AccessToken, context.ProtocolMessage.RefreshToken), context.Properties, context.ProtocolMessage.IdToken); + StoreNamedTokens((context.ProtocolMessage.AccessToken, context.ProtocolMessage.RefreshToken), context.Properties + ?? throw new NullReferenceException("AuthenticationProperties are not set")); return Task.CompletedTask; }; @@ -116,7 +118,7 @@ private void ConfigureServices(IServiceCollection services) }); } - public static void StoreNamedTokens((string accessToken, string refreshToken) userTokens, AuthenticationProperties authenticationProperties, string identityToken = null) + public static void StoreNamedTokens((string accessToken, string refreshToken) userTokens, AuthenticationProperties authenticationProperties) { var tokens = new List(); tokens.Add(new AuthenticationToken { Name = $"{OpenIdConnectParameterNames.AccessToken}::named_token_stored", Value = userTokens.accessToken, }); @@ -162,7 +164,7 @@ private void Configure(IApplicationBuilder app) invalid => invalid.Use(next => RemoteApiEndpoint.Map("/invalid_endpoint", _apiHost.Url()))); } - public async Task GetIsUserLoggedInAsync(string userQuery = null) + public async Task GetIsUserLoggedInAsync(string? userQuery = null) { if (userQuery != null) userQuery = "?" + userQuery; @@ -175,21 +177,7 @@ public async Task GetIsUserLoggedInAsync(string userQuery = null) return response.StatusCode == HttpStatusCode.OK; } - public async Task> CallUserEndpointAsync() - { - var req = new HttpRequestMessage(HttpMethod.Get, Url("/bff/user")); - req.Headers.Add("x-csrf", "1"); - - var response = await BrowserClient.SendAsync(req); - - response.StatusCode.ShouldBe(HttpStatusCode.OK); - response.Content.Headers.ContentType.MediaType.ShouldBe("application/json"); - - var json = await response.Content.ReadAsStringAsync(); - return JsonSerializer.Deserialize>(json); - } - - public async Task BffLoginAsync(string sub, string sid = null) + public async Task BffLoginAsync(string sub, string? sid = null) { await _identityServerHost.CreateIdentityServerSessionCookieAsync(sub, sid); return await BffOidcLoginAsync(); @@ -199,15 +187,15 @@ public async Task BffOidcLoginAsync() { var response = await BrowserClient.GetAsync(Url("/bff/login")); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // authorize - response.Headers.Location.ToString().ToLowerInvariant().ShouldStartWith(_identityServerHost.Url("/connect/authorize")); + response.Headers.Location!.ToString().ToLowerInvariant().ShouldStartWith(_identityServerHost.Url("/connect/authorize")); response = await _identityServerHost.BrowserClient.GetAsync(response.Headers.Location.ToString()); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // client callback - response.Headers.Location.ToString().ToLowerInvariant().ShouldStartWith(Url("/signin-oidc")); + response.Headers.Location!.ToString().ToLowerInvariant().ShouldStartWith(Url("/signin-oidc")); response = await BrowserClient.GetAsync(response.Headers.Location.ToString()); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // root - response.Headers.Location.ToString().ToLowerInvariant().ShouldBe("/"); + response.Headers.Location!.ToString().ToLowerInvariant().ShouldBe("/"); (await GetIsUserLoggedInAsync()).ShouldBeTrue(); @@ -215,23 +203,23 @@ public async Task BffOidcLoginAsync() return response; } - public async Task BffLogoutAsync(string sid = null) + public async Task BffLogoutAsync(string? sid = null) { var response = await BrowserClient.GetAsync(Url("/bff/logout") + "?sid=" + sid); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // endsession - response.Headers.Location.ToString().ToLowerInvariant().ShouldStartWith(_identityServerHost.Url("/connect/endsession")); + response.Headers.Location!.ToString().ToLowerInvariant().ShouldStartWith(_identityServerHost.Url("/connect/endsession")); response = await _identityServerHost.BrowserClient.GetAsync(response.Headers.Location.ToString()); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // logout - response.Headers.Location.ToString().ToLowerInvariant().ShouldStartWith(_identityServerHost.Url("/account/logout")); + response.Headers.Location!.ToString().ToLowerInvariant().ShouldStartWith(_identityServerHost.Url("/account/logout")); response = await _identityServerHost.BrowserClient.GetAsync(response.Headers.Location.ToString()); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // post logout redirect uri - response.Headers.Location.ToString().ToLowerInvariant().ShouldStartWith(Url("/signout-callback-oidc")); + response.Headers.Location!.ToString().ToLowerInvariant().ShouldStartWith(Url("/signout-callback-oidc")); response = await BrowserClient.GetAsync(response.Headers.Location.ToString()); response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // root - response.Headers.Location.ToString().ToLowerInvariant().ShouldBe("/"); + response.Headers.Location!.ToString().ToLowerInvariant().ShouldBe("/"); (await GetIsUserLoggedInAsync()).ShouldBeFalse(); diff --git a/bff/test/Duende.Bff.Tests/TestHosts/BffIntegrationTestBase.cs b/bff/test/Duende.Bff.Tests/TestHosts/BffIntegrationTestBase.cs index 2e296602e..db49a7759 100644 --- a/bff/test/Duende.Bff.Tests/TestHosts/BffIntegrationTestBase.cs +++ b/bff/test/Duende.Bff.Tests/TestHosts/BffIntegrationTestBase.cs @@ -1,18 +1,21 @@ // Copyright (c) Duende Software. All rights reserved. // See LICENSE in the project root for license information. +using System; using Duende.IdentityServer.Models; using Duende.IdentityServer.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System.Security.Claims; using System.Threading.Tasks; +using Xunit; using Xunit.Abstractions; namespace Duende.Bff.Tests.TestHosts { - public class BffIntegrationTestBase + public class BffIntegrationTestBase : IAsyncLifetime { + private readonly ITestOutputHelper _output; protected readonly IdentityServerHost IdentityServerHost; protected ApiHost ApiHost; protected BffHost BffHost; @@ -20,8 +23,12 @@ public class BffIntegrationTestBase public BffIntegrationTestBase(ITestOutputHelper output) { + _output = output; IdentityServerHost = new IdentityServerHost(output.WriteLine); - + ApiHost = new ApiHost(_output.WriteLine, IdentityServerHost, "scope1"); + BffHost = new BffHost(_output.WriteLine, IdentityServerHost, ApiHost, "spa"); + BffHostWithNamedTokens = new BffHostUsingResourceNamedTokens(_output.WriteLine, IdentityServerHost, ApiHost, "spa"); + IdentityServerHost.Clients.Add(new Client { ClientId = "spa", @@ -38,28 +45,35 @@ public BffIntegrationTestBase(ITestOutputHelper output) IdentityServerHost.OnConfigureServices += services => { services.AddTransient(provider => new DefaultBackChannelLogoutHttpClient( - BffHost.HttpClient, + BffHost!.HttpClient, provider.GetRequiredService(), provider.GetRequiredService())); services.AddSingleton(); }; - IdentityServerHost.InitializeAsync().Wait(); + } - ApiHost = new ApiHost(output.WriteLine, IdentityServerHost, "scope1"); - ApiHost.InitializeAsync().Wait(); + public async Task Login(string sub) + { + await IdentityServerHost.IssueSessionCookieAsync(new Claim("sub", sub)); + } - BffHost = new BffHost(output.WriteLine, IdentityServerHost, ApiHost, "spa"); - BffHost.InitializeAsync().Wait(); + public async Task InitializeAsync() + { + await IdentityServerHost.InitializeAsync(); + await ApiHost.InitializeAsync(); + await BffHost.InitializeAsync(); + await BffHostWithNamedTokens.InitializeAsync(); - BffHostWithNamedTokens = new BffHostUsingResourceNamedTokens(output.WriteLine, IdentityServerHost, ApiHost, "spa"); - BffHostWithNamedTokens.InitializeAsync().Wait(); } - public async Task Login(string sub) + public async Task DisposeAsync() { - await IdentityServerHost.IssueSessionCookieAsync(new Claim("sub", sub)); + await ApiHost.DisposeAsync(); + await BffHost.DisposeAsync(); + await BffHostWithNamedTokens.DisposeAsync(); + await IdentityServerHost.DisposeAsync(); } } } diff --git a/bff/test/Duende.Bff.Tests/TestHosts/IdentityServerHost.cs b/bff/test/Duende.Bff.Tests/TestHosts/IdentityServerHost.cs index 37365f670..682c308ae 100644 --- a/bff/test/Duende.Bff.Tests/TestHosts/IdentityServerHost.cs +++ b/bff/test/Duende.Bff.Tests/TestHosts/IdentityServerHost.cs @@ -25,14 +25,14 @@ public IdentityServerHost(WriteTestOutput output, string baseAddress = "https:// OnConfigure += Configure; } - public List Clients { get; set; } = new List(); - public List IdentityResources { get; set; } = new List() + public List Clients { get; set; } = new(); + public List IdentityResources { get; set; } = new() { new IdentityResources.OpenId(), new IdentityResources.Profile(), new IdentityResources.Email(), }; - public List ApiScopes { get; set; } = new List(); + public List ApiScopes { get; set; } = new(); private void ConfigureServices(IServiceCollection services) { @@ -76,7 +76,7 @@ private void Configure(IApplicationBuilder app) var signOutContext = await interaction.GetLogoutContextAsync(logoutId); - context.Response.Redirect(signOutContext.PostLogoutRedirectUri); + context.Response.Redirect(signOutContext.PostLogoutRedirectUri ?? "/"); }); endpoints.MapGet("/__token", async (ITokenService tokens) => { @@ -103,7 +103,7 @@ private void Configure(IApplicationBuilder app) }); } - public async Task CreateIdentityServerSessionCookieAsync(string sub, string sid = null) + public async Task CreateIdentityServerSessionCookieAsync(string sub, string? sid = null) { var props = new AuthenticationProperties(); diff --git a/bff/test/Duende.Bff.Tests/TestHosts/YarpBffHost.cs b/bff/test/Duende.Bff.Tests/TestHosts/YarpBffHost.cs index a6a2cd197..a4e082475 100644 --- a/bff/test/Duende.Bff.Tests/TestHosts/YarpBffHost.cs +++ b/bff/test/Duende.Bff.Tests/TestHosts/YarpBffHost.cs @@ -1,10 +1,6 @@ // Copyright (c) Duende Software. All rights reserved. // See LICENSE in the project root for license information. -using Duende.Bff.Tests.TestFramework; -using Shouldly; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; using System.Linq; @@ -12,185 +8,186 @@ using System.Net.Http; using System.Text.Json; using System.Threading.Tasks; +using Duende.Bff.Tests.TestFramework; using Duende.Bff.Yarp; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.Forwarder; -namespace Duende.Bff.Tests.TestHosts +namespace Duende.Bff.Tests.TestHosts; + +public class YarpBffHost : GenericHost { - public class YarpBffHost : GenericHost + public enum ResponseStatus { - public enum ResponseStatus - { - Ok, Challenge, Forbid - } - public ResponseStatus LocalApiResponseStatus { get; set; } = ResponseStatus.Ok; - - private readonly IdentityServerHost _identityServerHost; - private readonly ApiHost _apiHost; - private readonly string _clientId; - private readonly bool _useForwardedHeaders; - - public BffOptions BffOptions { get; private set; } - - public YarpBffHost( - WriteTestOutput output, - IdentityServerHost identityServerHost, - ApiHost apiHost, - string clientId, - string baseAddress = "https://app", - bool useForwardedHeaders = false) - : base(output, baseAddress) - { - _identityServerHost = identityServerHost; - _apiHost = apiHost; - _clientId = clientId; - _useForwardedHeaders = useForwardedHeaders; + Ok, + Challenge, + Forbid + } - OnConfigureServices += ConfigureServices; - OnConfigure += Configure; - } + public ResponseStatus LocalApiResponseStatus { get; set; } = ResponseStatus.Ok; - private void ConfigureServices(IServiceCollection services) - { - services.AddRouting(); - services.AddAuthorization(); + private readonly IdentityServerHost _identityServerHost; + private readonly ApiHost _apiHost; + private readonly string _clientId; + private readonly bool _useForwardedHeaders; + + public BffOptions BffOptions { get; private set; } = null!; + + public YarpBffHost( + WriteTestOutput output, + IdentityServerHost identityServerHost, + ApiHost apiHost, + string clientId, + string baseAddress = "https://app", + bool useForwardedHeaders = false) + : base(output, baseAddress) + { + _identityServerHost = identityServerHost; + _apiHost = apiHost; + _clientId = clientId; + _useForwardedHeaders = useForwardedHeaders; - var bff = services.AddBff(options => + OnConfigureServices += ConfigureServices; + OnConfigure += Configure; + } + + private void ConfigureServices(IServiceCollection services) + { + services.AddRouting(); + services.AddAuthorization(); + + var bff = services.AddBff(options => { BffOptions = options; }); + + services.AddSingleton( + new CallbackForwarderHttpClientFactory( + context => new HttpMessageInvoker(_apiHost.Server.CreateHandler()))); + + var yarpBuilder = services.AddReverseProxy() + .AddBffExtensions(); + + yarpBuilder.LoadFromMemory( + new[] { - BffOptions = options; - }); - - services.AddSingleton( - new CallbackForwarderHttpClientFactory( - context => new HttpMessageInvoker(_apiHost.Server.CreateHandler()))); - - var yarpBuilder = services.AddReverseProxy() - .AddBffExtensions(); - - yarpBuilder.LoadFromMemory( - new[] + new RouteConfig + { + RouteId = "api_anon_no_csrf", + ClusterId = "cluster1", + + Match = new RouteMatch + { + Path = "/api_anon_no_csrf/{**catch-all}" + } + }, + + new RouteConfig { - new RouteConfig() + RouteId = "api_anon", + ClusterId = "cluster1", + + Match = new RouteMatch + { + Path = "/api_anon/{**catch-all}" + } + }.WithAntiforgeryCheck(), + + new RouteConfig { - RouteId = "api_anon_no_csrf", + RouteId = "api_user", ClusterId = "cluster1", - Match = new() + Match = new RouteMatch { - Path = "/api_anon_no_csrf/{**catch-all}" + Path = "/api_user/{**catch-all}" } - }, - - new RouteConfig() + }.WithAntiforgeryCheck() + .WithAccessToken(TokenType.User), + + new RouteConfig { - RouteId = "api_anon", + RouteId = "api_optional_user", ClusterId = "cluster1", - Match = new() + Match = new RouteMatch { - Path = "/api_anon/{**catch-all}" + Path = "/api_optional_user/{**catch-all}" } - }.WithAntiforgeryCheck(), - - new RouteConfig() + }.WithAntiforgeryCheck() + .WithOptionalUserAccessToken(), + + new RouteConfig { - RouteId = "api_user", + RouteId = "api_client", ClusterId = "cluster1", - Match = new() + Match = new RouteMatch { - Path = "/api_user/{**catch-all}" + Path = "/api_client/{**catch-all}" } }.WithAntiforgeryCheck() - .WithAccessToken(TokenType.User), - - new RouteConfig() + .WithAccessToken(TokenType.Client), + + new RouteConfig { - RouteId = "api_optional_user", + RouteId = "api_user_or_client", ClusterId = "cluster1", - Match = new() + Match = new RouteMatch { - Path = "/api_optional_user/{**catch-all}" + Path = "/api_user_or_client/{**catch-all}" } }.WithAntiforgeryCheck() - .WithOptionalUserAccessToken(), - - new RouteConfig() - { - RouteId = "api_client", - ClusterId = "cluster1", - - Match = new() - { - Path = "/api_client/{**catch-all}" - } - }.WithAntiforgeryCheck() - .WithAccessToken(TokenType.Client), - - new RouteConfig() - { - RouteId = "api_user_or_client", - ClusterId = "cluster1", - - Match = new() - { - Path = "/api_user_or_client/{**catch-all}" - } - }.WithAntiforgeryCheck() - .WithAccessToken(TokenType.UserOrClient), - - // This route configuration is invalid. WithAccessToken says - // that the access token is required, while - // WithOptionalUserAccessToken says that it is optional. - // Calling this endpoint results in a run time error. - new RouteConfig() - { - RouteId = "api_invalid", - ClusterId = "cluster1", - - Match = new() - { - Path = "/api_invalid/{**catch-all}" - } - }.WithOptionalUserAccessToken() - .WithAccessToken(TokenType.User), - }, - - new[] - { - new ClusterConfig + .WithAccessToken(TokenType.UserOrClient), + + // This route configuration is invalid. WithAccessToken says + // that the access token is required, while + // WithOptionalUserAccessToken says that it is optional. + // Calling this endpoint results in a run time error. + new RouteConfig { + RouteId = "api_invalid", ClusterId = "cluster1", - Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) + Match = new RouteMatch { - { "destination1", new() { Address = _apiHost.Url() } }, + Path = "/api_invalid/{**catch-all}" } + }.WithOptionalUserAccessToken() + .WithAccessToken(TokenType.User) + }, + new[] + { + new ClusterConfig + { + ClusterId = "cluster1", + + Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "destination1", new DestinationConfig { Address = _apiHost.Url() } } } - }); + } + }); - // todo: need YARP equivalent - // services.AddSingleton( - // new CallbackHttpMessageInvokerFactory( - // path => new HttpMessageInvoker(_apiHost.Server.CreateHandler()))); + // todo: need YARP equivalent + // services.AddSingleton( + // new CallbackHttpMessageInvokerFactory( + // path => new HttpMessageInvoker(_apiHost.Server.CreateHandler()))); - services.AddAuthentication("cookie") - .AddCookie("cookie", options => - { - options.Cookie.Name = "bff"; - }); + services.AddAuthentication("cookie") + .AddCookie("cookie", options => { options.Cookie.Name = "bff"; }); - bff.AddServerSideSessions(); - - services.AddAuthentication(options => - { - options.DefaultChallengeScheme = "oidc"; - options.DefaultSignOutScheme = "oidc"; - }) - .AddOpenIdConnect("oidc", options => + bff.AddServerSideSessions(); + + services.AddAuthentication(options => + { + options.DefaultChallengeScheme = "oidc"; + options.DefaultSignOutScheme = "oidc"; + }) + .AddOpenIdConnect("oidc", + options => { options.Authority = _identityServerHost.Url(); @@ -218,159 +215,146 @@ private void ConfigureServices(IServiceCollection services) options.BackchannelHttpHandler = _identityServerHost.Server.CreateHandler(); }); - services.AddAuthorization(options => - { - options.AddPolicy("AlwaysFail", policy => { policy.RequireAssertion(ctx => false); }); - }); - } - - private void Configure(IApplicationBuilder app) + services.AddAuthorization(options => { - if (_useForwardedHeaders) - { - app.UseForwardedHeaders(new ForwardedHeadersOptions - { - ForwardedHeaders = ForwardedHeaders.XForwardedFor | - ForwardedHeaders.XForwardedProto | - ForwardedHeaders.XForwardedHost - }); - } - - app.UseAuthentication(); - - app.UseRouting(); - - app.UseBff(); - app.UseAuthorization(); + options.AddPolicy("AlwaysFail", policy => { policy.RequireAssertion(ctx => false); }); + }); + } - app.UseEndpoints(endpoints => + private void Configure(IApplicationBuilder app) + { + if (_useForwardedHeaders) + { + app.UseForwardedHeaders(new ForwardedHeadersOptions { - endpoints.MapBffManagementEndpoints(); - - endpoints.MapReverseProxy(proxyApp => - { - proxyApp.UseAntiforgeryCheck(); - }); - - // replace with YARP endpoints - // endpoints.MapRemoteBffApiEndpoint( - // "/api_user", _apiHost.Url()) - // .RequireAccessToken(); - // - // endpoints.MapRemoteBffApiEndpoint( - // "/api_user_no_csrf", _apiHost.Url()) - // .SkipAntiforgery() - // .RequireAccessToken(); - // - // endpoints.MapRemoteBffApiEndpoint( - // "/api_client", _apiHost.Url()) - // .RequireAccessToken(TokenType.Client); - // - // endpoints.MapRemoteBffApiEndpoint( - // "/api_user_or_client", _apiHost.Url()) - // .RequireAccessToken(TokenType.UserOrClient); - // - // endpoints.MapRemoteBffApiEndpoint( - // "/api_user_or_anon", _apiHost.Url()) - // .WithOptionalUserAccessToken(); - // - // endpoints.MapRemoteBffApiEndpoint( - // "/api_anon_only", _apiHost.Url()); + ForwardedHeaders = ForwardedHeaders.XForwardedFor | + ForwardedHeaders.XForwardedProto | + ForwardedHeaders.XForwardedHost }); } - public async Task GetIsUserLoggedInAsync(string userQuery = null) - { - if (userQuery != null) userQuery = "?" + userQuery; + app.UseAuthentication(); - var req = new HttpRequestMessage(HttpMethod.Get, Url("/bff/user") + userQuery); - req.Headers.Add("x-csrf", "1"); - var response = await BrowserClient.SendAsync(req); + app.UseRouting(); - (response.StatusCode == HttpStatusCode.OK || response.StatusCode == HttpStatusCode.Unauthorized) - .ShouldBeTrue(); + app.UseBff(); + app.UseAuthorization(); - return response.StatusCode == HttpStatusCode.OK; - } + app.UseEndpoints(endpoints => + { + endpoints.MapBffManagementEndpoints(); + + endpoints.MapReverseProxy(proxyApp => { proxyApp.UseAntiforgeryCheck(); }); + + // replace with YARP endpoints + // endpoints.MapRemoteBffApiEndpoint( + // "/api_user", _apiHost.Url()) + // .RequireAccessToken(); + // + // endpoints.MapRemoteBffApiEndpoint( + // "/api_user_no_csrf", _apiHost.Url()) + // .SkipAntiforgery() + // .RequireAccessToken(); + // + // endpoints.MapRemoteBffApiEndpoint( + // "/api_client", _apiHost.Url()) + // .RequireAccessToken(TokenType.Client); + // + // endpoints.MapRemoteBffApiEndpoint( + // "/api_user_or_client", _apiHost.Url()) + // .RequireAccessToken(TokenType.UserOrClient); + // + // endpoints.MapRemoteBffApiEndpoint( + // "/api_user_or_anon", _apiHost.Url()) + // .WithOptionalUserAccessToken(); + // + // endpoints.MapRemoteBffApiEndpoint( + // "/api_anon_only", _apiHost.Url()); + }); + } - public async Task> CallUserEndpointAsync() + public async Task GetIsUserLoggedInAsync(string? userQuery = null) + { + if (userQuery != null) { - var req = new HttpRequestMessage(HttpMethod.Get, Url("/bff/user")); - req.Headers.Add("x-csrf", "1"); + userQuery = "?" + userQuery; + } - var response = await BrowserClient.SendAsync(req); + var req = new HttpRequestMessage(HttpMethod.Get, Url("/bff/user") + userQuery); + req.Headers.Add("x-csrf", "1"); + var response = await BrowserClient.SendAsync(req); - response.StatusCode.ShouldBe(HttpStatusCode.OK); - response.Content.Headers.ContentType.MediaType.ShouldBe("application/json"); + (response.StatusCode == HttpStatusCode.OK || response.StatusCode == HttpStatusCode.Unauthorized) + .ShouldBeTrue(); - var json = await response.Content.ReadAsStringAsync(); - return JsonSerializer.Deserialize>(json); - } + return response.StatusCode == HttpStatusCode.OK; + } - public async Task BffLoginAsync(string sub, string sid = null) - { - await _identityServerHost.CreateIdentityServerSessionCookieAsync(sub, sid); - return await BffOidcLoginAsync(); - } + public async Task BffLoginAsync(string sub, string? sid = null) + { + await _identityServerHost.CreateIdentityServerSessionCookieAsync(sub, sid); + return await BffOidcLoginAsync(); + } - public async Task BffOidcLoginAsync() - { - var response = await BrowserClient.GetAsync(Url("/bff/login")); - response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // authorize - response.Headers.Location.ToString().ToLowerInvariant(). - ShouldStartWith(_identityServerHost.Url("/connect/authorize")); + public async Task BffOidcLoginAsync() + { + var response = await BrowserClient.GetAsync(Url("/bff/login")); + response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // authorize + response.Headers.Location!.ToString().ToLowerInvariant() + .ShouldStartWith(_identityServerHost.Url("/connect/authorize")); - response = await _identityServerHost.BrowserClient.GetAsync(response.Headers.Location.ToString()); - response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // client callback - response.Headers.Location.ToString().ToLowerInvariant().ShouldStartWith(Url("/signin-oidc")); + response = await _identityServerHost.BrowserClient.GetAsync(response.Headers.Location.ToString()); + response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // client callback + response.Headers.Location!.ToString().ToLowerInvariant().ShouldStartWith(Url("/signin-oidc")); - response = await BrowserClient.GetAsync(response.Headers.Location.ToString()); - response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // root - response.Headers.Location.ToString().ToLowerInvariant().ShouldBe("/"); + response = await BrowserClient.GetAsync(response.Headers.Location.ToString()); + response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // root + response.Headers.Location!.ToString().ToLowerInvariant().ShouldBe("/"); - (await GetIsUserLoggedInAsync()).ShouldBeTrue(); + (await GetIsUserLoggedInAsync()).ShouldBeTrue(); - response = await BrowserClient.GetAsync(Url(response.Headers.Location.ToString())); - return response; - } + response = await BrowserClient.GetAsync(Url(response.Headers.Location.ToString())); + return response; + } - public async Task BffLogoutAsync(string sid = null) - { - var response = await BrowserClient.GetAsync(Url("/bff/logout") + "?sid=" + sid); - response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // endsession - response.Headers.Location.ToString().ToLowerInvariant().ShouldStartWith(_identityServerHost.Url("/connect/endsession")); + public async Task BffLogoutAsync(string? sid = null) + { + var response = await BrowserClient.GetAsync(Url("/bff/logout") + "?sid=" + sid); + response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // endsession + response.Headers.Location!.ToString().ToLowerInvariant() + .ShouldStartWith(_identityServerHost.Url("/connect/endsession")); - response = await _identityServerHost.BrowserClient.GetAsync(response.Headers.Location.ToString()); - response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // logout - response.Headers.Location.ToString().ToLowerInvariant().ShouldStartWith(_identityServerHost.Url("/account/logout")); + response = await _identityServerHost.BrowserClient.GetAsync(response.Headers.Location.ToString()); + response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // logout + response.Headers.Location!.ToString().ToLowerInvariant() + .ShouldStartWith(_identityServerHost.Url("/account/logout")); - response = await _identityServerHost.BrowserClient.GetAsync(response.Headers.Location.ToString()); - response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // post logout redirect uri - response.Headers.Location.ToString().ToLowerInvariant().ShouldStartWith(Url("/signout-callback-oidc")); + response = await _identityServerHost.BrowserClient.GetAsync(response.Headers.Location.ToString()); + response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // post logout redirect uri + response.Headers.Location!.ToString().ToLowerInvariant().ShouldStartWith(Url("/signout-callback-oidc")); - response = await BrowserClient.GetAsync(response.Headers.Location.ToString()); - response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // root - response.Headers.Location.ToString().ToLowerInvariant().ShouldBe("/"); + response = await BrowserClient.GetAsync(response.Headers.Location.ToString()); + response.StatusCode.ShouldBe(HttpStatusCode.Redirect); // root + response.Headers.Location!.ToString().ToLowerInvariant().ShouldBe("/"); - (await GetIsUserLoggedInAsync()).ShouldBeFalse(); + (await GetIsUserLoggedInAsync()).ShouldBeFalse(); - response = await BrowserClient.GetAsync(Url(response.Headers.Location.ToString())); - return response; + response = await BrowserClient.GetAsync(Url(response.Headers.Location.ToString())); + return response; + } + + public class CallbackForwarderHttpClientFactory : IForwarderHttpClientFactory + { + public Func CreateInvoker { get; set; } + + public CallbackForwarderHttpClientFactory(Func callback) + { + CreateInvoker = callback; } - public class CallbackForwarderHttpClientFactory : IForwarderHttpClientFactory + public HttpMessageInvoker CreateClient(ForwarderHttpClientContext context) { - public Func CreateInvoker { get; set; } - - public CallbackForwarderHttpClientFactory(Func callback) - { - CreateInvoker = callback; - } - - public HttpMessageInvoker CreateClient(ForwarderHttpClientContext context) - { - return CreateInvoker.Invoke(context); - } + return CreateInvoker.Invoke(context); } } } \ No newline at end of file diff --git a/bff/test/Duende.Bff.Tests/TestHosts/YarpBffIntegrationTestBase.cs b/bff/test/Duende.Bff.Tests/TestHosts/YarpBffIntegrationTestBase.cs index 36b23fda7..a0954d2f8 100644 --- a/bff/test/Duende.Bff.Tests/TestHosts/YarpBffIntegrationTestBase.cs +++ b/bff/test/Duende.Bff.Tests/TestHosts/YarpBffIntegrationTestBase.cs @@ -1,17 +1,19 @@ // Copyright (c) Duende Software. All rights reserved. // See LICENSE in the project root for license information. +using System; using Duende.IdentityServer.Models; using Duende.IdentityServer.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System.Security.Claims; using System.Threading.Tasks; +using Xunit; using Xunit.Abstractions; namespace Duende.Bff.Tests.TestHosts { - public class YarpBffIntegrationTestBase + public class YarpBffIntegrationTestBase : IAsyncLifetime { private readonly IdentityServerHost _identityServerHost; protected readonly ApiHost ApiHost; @@ -38,26 +40,38 @@ protected YarpBffIntegrationTestBase(ITestOutputHelper output) _identityServerHost.OnConfigureServices += services => { services.AddTransient(provider => new DefaultBackChannelLogoutHttpClient( - BffHost.HttpClient, + BffHost!.HttpClient, provider.GetRequiredService(), provider.GetRequiredService())); }; - _identityServerHost.InitializeAsync().Wait(); - ApiHost = new ApiHost(output.WriteLine, _identityServerHost, "scope1"); - ApiHost.InitializeAsync().Wait(); BffHost = new YarpBffHost(output.WriteLine, _identityServerHost, ApiHost, "spa"); - BffHost.InitializeAsync().Wait(); _bffHostWithNamedTokens = new BffHostUsingResourceNamedTokens(output.WriteLine, _identityServerHost, ApiHost, "spa"); - _bffHostWithNamedTokens.InitializeAsync().Wait(); } public async Task Login(string sub) { await _identityServerHost.IssueSessionCookieAsync(new Claim("sub", sub)); } + + public async Task InitializeAsync() + { + await _identityServerHost.InitializeAsync(); + await ApiHost.InitializeAsync(); + await BffHost.InitializeAsync(); + await _bffHostWithNamedTokens.InitializeAsync(); + + } + + public async Task DisposeAsync() + { + await _identityServerHost.DisposeAsync(); + await ApiHost.DisposeAsync(); + await BffHost.DisposeAsync(); + await _bffHostWithNamedTokens.DisposeAsync(); + } } }