diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests/AuthEndpointsIntegrationTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests/AuthEndpointsIntegrationTests.cs index 6ab0018c..45ca5c6c 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests/AuthEndpointsIntegrationTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests/AuthEndpointsIntegrationTests.cs @@ -3,6 +3,7 @@ using System.Net.Http.Json; using System.Text.Json; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.TestHost; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; @@ -63,7 +64,13 @@ public sealed class AuthEndpointsIntegrationTests : IAsyncLifetime app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); - app.UseEndpoints(e => e.MapOtOpcUaAuth()); + app.UseEndpoints(e => + { + e.MapOtOpcUaAuth(); + // Protected root used by AuthChallengeTests below — exercises the cookie + // scheme's challenge heuristic without depending on the full Razor host. + e.MapGet("/", () => Results.Ok("authenticated")).RequireAuthorization(); + }); }); }) .Build(); @@ -81,6 +88,13 @@ public sealed class AuthEndpointsIntegrationTests : IAsyncLifetime private HttpClient NewClient() => _server.CreateClient(); + /// Creates a TestServer-backed HttpClient that does NOT auto-follow redirects. + /// Used by challenge tests so we can assert on the 302 + Location directly. + private HttpClient NewClientNoRedirect() => new(_server.CreateHandler()) + { + BaseAddress = _server.BaseAddress, + }; + /// Tests that login with valid credentials returns 204 and sets cookie. [Fact] public async Task Login_with_valid_credentials_returns_204_and_sets_cookie() @@ -178,6 +192,46 @@ public sealed class AuthEndpointsIntegrationTests : IAsyncLifetime .ShouldContain(c => c.StartsWith("OtOpcUa.Auth=") && c.Contains("expires=", StringComparison.OrdinalIgnoreCase)); } + /// Anonymous browser GET of a protected route redirects to /login with a ReturnUrl. + [Fact] + public async Task Root_anonymous_browser_GET_redirects_to_login() + { + var client = NewClientNoRedirect(); + var req = new HttpRequestMessage(HttpMethod.Get, "/"); + req.Headers.Accept.ParseAdd("text/html"); + var resp = await client.SendAsync(req, Ct); + + resp.StatusCode.ShouldBe(HttpStatusCode.Found); + resp.Headers.Location.ShouldNotBeNull(); + resp.Headers.Location!.OriginalString.ShouldContain("/login"); + resp.Headers.Location.OriginalString.ShouldContain("ReturnUrl"); + } + + /// Anonymous AJAX GET of a protected route returns 401 with no Location. + [Fact] + public async Task Root_anonymous_ajax_GET_returns_401() + { + var client = NewClientNoRedirect(); + var req = new HttpRequestMessage(HttpMethod.Get, "/"); + req.Headers.Add("X-Requested-With", "XMLHttpRequest"); + var resp = await client.SendAsync(req, Ct); + + resp.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); + resp.Headers.Location.ShouldBeNull(); + } + + /// Anonymous JSON GET of a protected route returns 401. + [Fact] + public async Task Root_anonymous_json_GET_returns_401() + { + var client = NewClientNoRedirect(); + var req = new HttpRequestMessage(HttpMethod.Get, "/"); + req.Headers.Accept.ParseAdd("application/json"); + var resp = await client.SendAsync(req, Ct); + + resp.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); + } + private static void AttachCookies(HttpRequestMessage request, HttpResponseMessage prior) { if (!prior.Headers.TryGetValues("Set-Cookie", out var setCookies)) return;