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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.Json.Nodes;
|
||||
using PolyTrader.Web.Configuration;
|
||||
using Xunit;
|
||||
|
||||
namespace PolyTrader.Tests.WebUi
|
||||
{
|
||||
public class WebUiConfigManagerTests : IDisposable
|
||||
{
|
||||
private readonly string _tempFile;
|
||||
|
||||
public WebUiConfigManagerTests()
|
||||
{
|
||||
_tempFile = Path.Combine(Path.GetTempPath(), $"appsettings_test_{Guid.NewGuid():N}.json");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (File.Exists(_tempFile))
|
||||
File.Delete(_tempFile);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateSecureToken_Returns64CharHexAndIsRandom()
|
||||
{
|
||||
string t1 = WebUiConfigManager.GenerateSecureToken();
|
||||
string t2 = WebUiConfigManager.GenerateSecureToken();
|
||||
|
||||
Assert.NotNull(t1);
|
||||
Assert.Equal(64, t1.Length);
|
||||
Assert.Matches("^[0-9a-f]{64}$", t1);
|
||||
Assert.NotEqual(t1, t2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SaveToLocalSettings_CreatesFileAndPreservesExistingSections()
|
||||
{
|
||||
// Vorhandene Sektion simulieren
|
||||
File.WriteAllText(_tempFile, "{\"Database\":{\"Provider\":\"MySql\"}}");
|
||||
|
||||
var options = new WebUiOptions
|
||||
{
|
||||
Enabled = true,
|
||||
Port = 5001,
|
||||
LocalOnly = false,
|
||||
AuthToken = "test-token-xyz"
|
||||
};
|
||||
|
||||
WebUiConfigManager.SaveToLocalSettings(options, _tempFile);
|
||||
|
||||
string content = File.ReadAllText(_tempFile);
|
||||
var node = JsonNode.Parse(content)?.AsObject();
|
||||
|
||||
Assert.NotNull(node);
|
||||
Assert.Equal("MySql", node["Database"]?["Provider"]?.GetValue<string>());
|
||||
|
||||
var webNode = node["WebUi"]?.AsObject();
|
||||
Assert.NotNull(webNode);
|
||||
Assert.True(webNode["Enabled"]?.GetValue<bool>());
|
||||
Assert.Equal(5001, webNode["Port"]?.GetValue<int>());
|
||||
Assert.False(webNode["LocalOnly"]?.GetValue<bool>());
|
||||
Assert.Equal("test-token-xyz", webNode["AuthToken"]?.GetValue<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SaveToLocalSettings_WhenFileDoesNotExist_CreatesNewFile()
|
||||
{
|
||||
if (File.Exists(_tempFile)) File.Delete(_tempFile);
|
||||
|
||||
var options = new WebUiOptions
|
||||
{
|
||||
Enabled = true,
|
||||
Port = 5050,
|
||||
LocalOnly = true,
|
||||
AuthToken = "brand-new-token"
|
||||
};
|
||||
|
||||
WebUiConfigManager.SaveToLocalSettings(options, _tempFile);
|
||||
|
||||
Assert.True(File.Exists(_tempFile));
|
||||
string content = File.ReadAllText(_tempFile);
|
||||
var node = JsonNode.Parse(content)?.AsObject();
|
||||
Assert.NotNull(node);
|
||||
Assert.Equal(5050, node["WebUi"]?["Port"]?.GetValue<int>());
|
||||
Assert.Equal("brand-new-token", node["WebUi"]?["AuthToken"]?.GetValue<string>());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user