using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; using PolyTrader.Web.Configuration; using PolyTrader.Web.Security; using Xunit; namespace PolyTrader.Tests.WebUi { public class TokenAuthMiddlewareTests { [Fact] public void ValidateToken_CorrectToken_ReturnsTrue() { Assert.True(TokenAuthMiddleware.ValidateToken("secret-token-123", "secret-token-123")); } [Fact] public void ValidateToken_WrongToken_ReturnsFalse() { Assert.False(TokenAuthMiddleware.ValidateToken("wrong-token", "secret-token-123")); } [Theory] [InlineData("", "secret")] [InlineData("secret", "")] [InlineData(null, "secret")] [InlineData("secret", null)] public void ValidateToken_NullOrEmpty_ReturnsFalse(string? candidate, string? expected) { Assert.False(TokenAuthMiddleware.ValidateToken(candidate!, expected!)); } [Fact] public async Task InvokeAsync_UnconfiguredToken_RedirectsToLoginWithNoTokenError() { bool nextCalled = false; var middleware = new TokenAuthMiddleware(_ => { nextCalled = true; return Task.CompletedTask; }); var context = new DefaultHttpContext(); context.Request.Path = "/"; var options = Options.Create(new WebUiOptions { AuthToken = "" }); await middleware.InvokeAsync(context, options); Assert.False(nextCalled); Assert.Equal(StatusCodes.Status302Found, context.Response.StatusCode); Assert.Equal("/login?error=no_token", context.Response.Headers.Location); } [Fact] public async Task InvokeAsync_UnconfiguredToken_ApiReturns401() { bool nextCalled = false; var middleware = new TokenAuthMiddleware(_ => { nextCalled = true; return Task.CompletedTask; }); var context = new DefaultHttpContext(); context.Request.Path = "/api/dashboard/summary"; context.Response.Body = new MemoryStream(); var options = Options.Create(new WebUiOptions { AuthToken = "" }); await middleware.InvokeAsync(context, options); Assert.False(nextCalled); Assert.Equal(StatusCodes.Status401Unauthorized, context.Response.StatusCode); } [Fact] public async Task InvokeAsync_PublicPathLogin_PassesThroughEvenWithoutToken() { bool nextCalled = false; var middleware = new TokenAuthMiddleware(_ => { nextCalled = true; return Task.CompletedTask; }); var context = new DefaultHttpContext(); context.Request.Path = "/login"; var options = Options.Create(new WebUiOptions { AuthToken = "" }); await middleware.InvokeAsync(context, options); Assert.True(nextCalled); } [Fact] public async Task InvokeAsync_ValidTokenInCookie_PassesThrough() { bool nextCalled = false; var middleware = new TokenAuthMiddleware(_ => { nextCalled = true; return Task.CompletedTask; }); var context = new DefaultHttpContext(); context.Request.Path = "/"; context.Request.Headers.Cookie = $"{TokenAuthMiddleware.CookieName}=valid-secret"; var options = Options.Create(new WebUiOptions { AuthToken = "valid-secret" }); await middleware.InvokeAsync(context, options); Assert.True(nextCalled); } [Fact] public async Task InvokeAsync_ValidTokenInBearerHeader_PassesThrough() { bool nextCalled = false; var middleware = new TokenAuthMiddleware(_ => { nextCalled = true; return Task.CompletedTask; }); var context = new DefaultHttpContext(); context.Request.Path = "/api/accounts"; context.Request.Headers.Authorization = "Bearer valid-secret"; var options = Options.Create(new WebUiOptions { AuthToken = "valid-secret" }); await middleware.InvokeAsync(context, options); Assert.True(nextCalled); } [Fact] public async Task InvokeAsync_MissingAuth_HtmlPageRedirectsToLogin() { bool nextCalled = false; var middleware = new TokenAuthMiddleware(_ => { nextCalled = true; return Task.CompletedTask; }); var context = new DefaultHttpContext(); context.Request.Path = "/jobs"; var options = Options.Create(new WebUiOptions { AuthToken = "valid-secret" }); await middleware.InvokeAsync(context, options); Assert.False(nextCalled); Assert.Equal(StatusCodes.Status302Found, context.Response.StatusCode); Assert.Equal("/login", context.Response.Headers.Location); } [Fact] public async Task InvokeAsync_MissingAuth_ApiReturns401() { bool nextCalled = false; var middleware = new TokenAuthMiddleware(_ => { nextCalled = true; return Task.CompletedTask; }); var context = new DefaultHttpContext(); context.Request.Path = "/api/jobs"; context.Response.Body = new MemoryStream(); var options = Options.Create(new WebUiOptions { AuthToken = "valid-secret" }); await middleware.InvokeAsync(context, options); Assert.False(nextCalled); Assert.Equal(StatusCodes.Status401Unauthorized, context.Response.StatusCode); } } }