Paralleles WebGUI mit Kestrel, Token-Sicherung und SSE-Logs implementiert
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user