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;