using FluentAssertions;
using IBKRTrader.Core.Settings;
using IBKRTrader.Hosting.Web;
namespace IBKRTrader.Tests.Web;
[Trait("cat", "unit")]
public class AuthServiceTests
{
[Fact]
public void ValidateToken_ReturnsTrueForCorrectToken()
{
var settings = new SettingsService();
settings.Settings.Webserver.AuthToken = "secret-token-123";
var sut = new AuthService(settings);
sut.IsConfigured.Should().BeTrue();
sut.ValidateToken("secret-token-123").Should().BeTrue();
sut.ValidateToken("wrong-token").Should().BeFalse();
sut.ValidateToken("").Should().BeFalse();
}
[Fact]
public void ValidateToken_ReturnsFalseWhenNotConfigured()
{
var settings = new SettingsService();
settings.Settings.Webserver.AuthToken = "";
var sut = new AuthService(settings);
sut.IsConfigured.Should().BeFalse();
sut.ValidateToken("any-token").Should().BeFalse();
}
[Fact]
public void SessionToken_RoundTrip_ValidatesSuccessfully()
{
var settings = new SettingsService();
settings.Settings.Webserver.AuthToken = "my-secret-key";
var sut = new AuthService(settings);
var sessionToken = sut.CreateSessionToken(TimeSpan.FromHours(1));
sessionToken.Should().NotBeNullOrWhiteSpace();
sut.ValidateSessionToken(sessionToken).Should().BeTrue();
}
[Fact]
public void SessionToken_TamperedToken_FailsValidation()
{
var settings = new SettingsService();
settings.Settings.Webserver.AuthToken = "my-secret-key";
var sut = new AuthService(settings);
var sessionToken = sut.CreateSessionToken(TimeSpan.FromHours(1));
var tampered = sessionToken + "tamper";
sut.ValidateSessionToken(tampered).Should().BeFalse();
}
[Fact]
public void SessionToken_ExpiredToken_FailsValidation()
{
var settings = new SettingsService();
settings.Settings.Webserver.AuthToken = "my-secret-key";
var sut = new AuthService(settings);
// Abgelaufenes Token (minus 1 Sekunde)
var sessionToken = sut.CreateSessionToken(TimeSpan.FromSeconds(-10));
sut.ValidateSessionToken(sessionToken).Should().BeFalse();
}
[Fact]
public void IsAuthenticated_RecognizesBearerHeader()
{
var settings = new SettingsService();
settings.Settings.Webserver.AuthToken = "my-secret-key";
var sut = new AuthService(settings);
var ctx = new Microsoft.AspNetCore.Http.DefaultHttpContext();
ctx.Request.Headers.Authorization = "Bearer my-secret-key";
sut.IsAuthenticated(ctx).Should().BeTrue();
}
///
/// Der Token im Query-String gilt nur für den SSE-Strom. EventSource kann keine
/// Kopfzeilen setzen – das ist der einzige Grund, aus dem es diesen Weg gibt.
///
[Fact]
public void IsAuthenticated_AcceptsQueryToken_OnTheLogStream()
{
var sut = SutWithToken("my-secret-key");
var ctx = new Microsoft.AspNetCore.Http.DefaultHttpContext();
ctx.Request.Path = "/api/logs/stream";
ctx.Request.QueryString = new Microsoft.AspNetCore.Http.QueryString("?token=my-secret-key");
sut.IsAuthenticated(ctx).Should().BeTrue();
}
///
/// Überall sonst zählt er nicht. Ein Query-String landet in Server-Logs, im Browserverlauf und
/// in Referer-Kopfzeilen; ein Geheimnis hat dort nichts zu suchen, wo ein anderer Weg offensteht.
///
[Fact]
public void IsAuthenticated_IgnoresQueryToken_EverywhereElse()
{
var sut = SutWithToken("my-secret-key");
var ctx = new Microsoft.AspNetCore.Http.DefaultHttpContext();
ctx.Request.Path = "/api/positions";
ctx.Request.QueryString = new Microsoft.AspNetCore.Http.QueryString("?token=my-secret-key");
sut.IsAuthenticated(ctx).Should().BeFalse();
}
private static AuthService SutWithToken(string token)
{
var settings = new SettingsService();
settings.Settings.Webserver.AuthToken = token;
return new AuthService(settings);
}
[Fact]
public void IsAuthenticated_RejectsUnauthenticatedRequest()
{
var settings = new SettingsService();
settings.Settings.Webserver.AuthToken = "my-secret-key";
var sut = new AuthService(settings);
var ctx = new Microsoft.AspNetCore.Http.DefaultHttpContext();
sut.IsAuthenticated(ctx).Should().BeFalse();
}
}