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(); } }