feat: Web-UI als zweiter Ausgabeweg neben Avalonia-Shell (#490)
- Einheitlicher WebserverWorker in IBKRTrader.Hosting auf Kestrel-Basis - Token-Authentifizierung (AuthService) mit zeitkonstantem Vergleich und HMAC-Session-Tokens - OrderManager im Core fuer Bestands- und Reservierungsabgleich offener Broker-Orders - SettingsSchema fuer reflexionsbasiertes Auslesen und sichere Aktualisierung ohne Klartext-Passwoerter - SSE-Streaming (LogStreamService) fuer Echtzeit-Logs ueber isolierte Channels - Statische Single-Page-Application in wwwroot (Avalonia Fluent / Polytrader Theme) fuer alle 9 Ansichten - Unit-Tests fuer OrderManager, AuthService, SettingsSchema, LogStreamService und WebserverWorker (498 Tests gruen)
This commit is contained in:
@@ -28,6 +28,7 @@
|
||||
<ProjectReference Include="..\..\src\IBKRTrader.Modules.Accounting\IBKRTrader.Modules.Accounting.csproj" />
|
||||
<ProjectReference Include="..\..\src\IBKRTrader.Modules.Supervisor\IBKRTrader.Modules.Supervisor.csproj" />
|
||||
<ProjectReference Include="..\..\src\IBKRTrader.Modules.DebugTrading\IBKRTrader.Modules.DebugTrading.csproj" />
|
||||
<ProjectReference Include="..\..\src\IBKRTrader.Hosting\IBKRTrader.Hosting.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
using FluentAssertions;
|
||||
using IBKRTrader.Core.Logging;
|
||||
using IBKRTrader.Core.Orders;
|
||||
using IBKRTrader.Core.Persistence.Ef;
|
||||
using IBKRTrader.Core.Persistence.Entities;
|
||||
using IBKRTrader.Core.Settings;
|
||||
using IBKRTrader.Core.Trading;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NSubstitute;
|
||||
|
||||
namespace IBKRTrader.Tests.Orders;
|
||||
|
||||
[Trait("cat", "unit")]
|
||||
public class OrderManagerTests
|
||||
{
|
||||
private sealed class InMemoryFactory : IDbContextFactory<CoreDbContext>
|
||||
{
|
||||
private readonly DbContextOptions<CoreDbContext> _options;
|
||||
public InMemoryFactory(DbContextOptions<CoreDbContext> options) => _options = options;
|
||||
public CoreDbContext CreateDbContext() => new(_options);
|
||||
}
|
||||
|
||||
private static (OrderManager Sut, IBrokerOrderBook OrderBook, IDbContextFactory<CoreDbContext> Dbf, SettingsService Settings) CreateSut()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<CoreDbContext>()
|
||||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
var factory = new InMemoryFactory(options);
|
||||
var orderBook = Substitute.For<IBrokerOrderBook>();
|
||||
var settings = new SettingsService();
|
||||
var logger = new LoggingService();
|
||||
|
||||
var sut = new OrderManager(orderBook, factory, settings, logger);
|
||||
return (sut, orderBook, factory, settings);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetOrderOverviewAsync_IdentifiesBackedAndUnbackedOrders()
|
||||
{
|
||||
var (sut, orderBook, dbf, _) = CreateSut();
|
||||
|
||||
// 1. Broker-Orders vorbereiten
|
||||
var bo1 = new BrokerOrder(
|
||||
OrderId: 101, PermId: 1001, Instrument: InstrumentRef.ForStock("AAPL"),
|
||||
Side: TradeSide.Buy, Quantity: 10, Filled: 0, Remaining: 10,
|
||||
OrderType: "LMT", LimitPrice: 150m, Status: "Submitted", Account: "U123");
|
||||
|
||||
var bo2 = new BrokerOrder(
|
||||
OrderId: 102, PermId: 1002, Instrument: InstrumentRef.ForStock("MSFT"),
|
||||
Side: TradeSide.Buy, Quantity: 5, Filled: 0, Remaining: 5,
|
||||
OrderType: "LMT", LimitPrice: 400m, Status: "Submitted", Account: "U123");
|
||||
|
||||
orderBook.GetOpenOrdersAsync(Arg.Any<CancellationToken>())
|
||||
.Returns([bo1, bo2]);
|
||||
|
||||
// 2. Eigene Kapitalbindung nur für bo1 anlegen
|
||||
await using (var db = dbf.CreateDbContext())
|
||||
{
|
||||
db.Reservations.Add(new CoreReservation
|
||||
{
|
||||
Id = 1,
|
||||
SignalId = "sig-1",
|
||||
Book = "CT",
|
||||
Side = "BUY",
|
||||
Quantity = 10,
|
||||
ContractKey = "AAPL",
|
||||
Amount = 1500m,
|
||||
Currency = "USD",
|
||||
State = ReservationState.Submitted,
|
||||
BrokerOrderId = "101",
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
// Act
|
||||
var overview = await sut.GetOrderOverviewAsync();
|
||||
|
||||
// Assert
|
||||
overview.BrokerReachable.Should().BeTrue();
|
||||
overview.Orders.Should().HaveCount(2);
|
||||
|
||||
var managed1 = overview.Orders.First(o => o.BrokerOrder.OrderId == 101);
|
||||
managed1.IsBackedByReservation.Should().BeTrue();
|
||||
managed1.ReservationId.Should().Be(1);
|
||||
managed1.Book.Should().Be("CT");
|
||||
|
||||
var managed2 = overview.Orders.First(o => o.BrokerOrder.OrderId == 102);
|
||||
managed2.IsBackedByReservation.Should().BeFalse();
|
||||
managed2.ReservationId.Should().BeNull();
|
||||
|
||||
overview.UnbackedOrders.Should().HaveCount(1);
|
||||
overview.UnbackedOrders.Single().BrokerOrder.OrderId.Should().Be(102);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CancelOrderAsync_DelegatesToBrokerOrderBook()
|
||||
{
|
||||
var (sut, orderBook, _, _) = CreateSut();
|
||||
orderBook.CancelOrderAsync(101, Arg.Any<CancellationToken>()).Returns(true);
|
||||
|
||||
var result = await sut.CancelOrderAsync(101);
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
result.Message.Should().Contain("101");
|
||||
await orderBook.Received(1).CancelOrderAsync(101, Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CancelAllOrdersAsync_RejectsWhenNotPaperMode()
|
||||
{
|
||||
var (sut, orderBook, _, settings) = CreateSut();
|
||||
settings.Settings.Trading.Mode = "Live";
|
||||
|
||||
var result = await sut.CancelAllOrdersAsync();
|
||||
|
||||
result.Success.Should().BeFalse();
|
||||
result.Message.Should().Contain("Handelsmodus");
|
||||
await orderBook.DidNotReceive().GetOpenOrdersAsync(Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CancelAllOrdersAsync_CancelsAllOrdersInPaperMode()
|
||||
{
|
||||
var (sut, orderBook, _, settings) = CreateSut();
|
||||
settings.Settings.Trading.Mode = "Paper";
|
||||
|
||||
var bo1 = new BrokerOrder(
|
||||
OrderId: 201, PermId: 2001, Instrument: InstrumentRef.ForStock("AAPL"),
|
||||
Side: TradeSide.Buy, Quantity: 10, Filled: 0, Remaining: 10,
|
||||
OrderType: "LMT", LimitPrice: 150m, Status: "Submitted", Account: "U123");
|
||||
|
||||
var bo2 = new BrokerOrder(
|
||||
OrderId: 202, PermId: 2002, Instrument: InstrumentRef.ForStock("MSFT"),
|
||||
Side: TradeSide.Buy, Quantity: 5, Filled: 0, Remaining: 5,
|
||||
OrderType: "LMT", LimitPrice: 400m, Status: "Submitted", Account: "U123");
|
||||
|
||||
orderBook.GetOpenOrdersAsync(Arg.Any<CancellationToken>()).Returns([bo1, bo2]);
|
||||
orderBook.CancelOrderAsync(Arg.Any<int>(), Arg.Any<CancellationToken>()).Returns(true);
|
||||
|
||||
var result = await sut.CancelAllOrdersAsync();
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
result.Cancelled.Should().Be(2);
|
||||
result.Total.Should().Be(2);
|
||||
await orderBook.Received(1).CancelOrderAsync(201, Arg.Any<CancellationToken>());
|
||||
await orderBook.Received(1).CancelOrderAsync(202, Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetOrderOverviewAsync_WhenReservationHasNoBrokerOrderId_TreatedAsUnbacked()
|
||||
{
|
||||
var (sut, orderBook, dbf, _) = CreateSut();
|
||||
|
||||
var bo = new BrokerOrder(
|
||||
OrderId: 301, PermId: 3001, Instrument: InstrumentRef.ForStock("NVDA"),
|
||||
Side: TradeSide.Buy, Quantity: 20, Filled: 0, Remaining: 20,
|
||||
OrderType: "LMT", LimitPrice: 120m, Status: "Submitted", Account: "U123");
|
||||
|
||||
orderBook.GetOpenOrdersAsync(Arg.Any<CancellationToken>()).Returns([bo]);
|
||||
|
||||
await using (var db = dbf.CreateDbContext())
|
||||
{
|
||||
db.Reservations.Add(new CoreReservation
|
||||
{
|
||||
Id = 10,
|
||||
SignalId = "sig-nvda",
|
||||
Book = "CT",
|
||||
Side = "BUY",
|
||||
Quantity = 20,
|
||||
ContractKey = "NVDA",
|
||||
Amount = 2400m,
|
||||
Currency = "USD",
|
||||
State = ReservationState.Reserved,
|
||||
BrokerOrderId = null, // noch keine Order-ID zugewiesen
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var overview = await sut.GetOrderOverviewAsync();
|
||||
|
||||
overview.BrokerReachable.Should().BeTrue();
|
||||
overview.Orders.Should().HaveCount(1);
|
||||
var mo = overview.Orders.Single();
|
||||
mo.IsBackedByReservation.Should().BeFalse();
|
||||
overview.UnbackedOrders.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetOrderOverviewAsync_HandlesBrokerException_Gracefully()
|
||||
{
|
||||
var (sut, orderBook, _, _) = CreateSut();
|
||||
orderBook.GetOpenOrdersAsync(Arg.Any<CancellationToken>())
|
||||
.Returns<IReadOnlyList<BrokerOrder>>(_ => throw new InvalidOperationException("Broker offline"));
|
||||
|
||||
var overview = await sut.GetOrderOverviewAsync();
|
||||
|
||||
overview.BrokerReachable.Should().BeFalse();
|
||||
overview.Orders.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CancelOrderAsync_WhenBrokerReturnsFalse_ReportsFailure()
|
||||
{
|
||||
var (sut, orderBook, _, _) = CreateSut();
|
||||
orderBook.CancelOrderAsync(999, Arg.Any<CancellationToken>()).Returns(false);
|
||||
|
||||
var result = await sut.CancelOrderAsync(999);
|
||||
|
||||
result.Success.Should().BeFalse();
|
||||
result.Message.Should().Contain("NICHT bestätigt");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
using FluentAssertions;
|
||||
using IBKRTrader.Core.Settings;
|
||||
|
||||
namespace IBKRTrader.Tests.Settings;
|
||||
|
||||
[Trait("cat", "unit")]
|
||||
public class SettingsSchemaTests
|
||||
{
|
||||
[Fact]
|
||||
public void Build_ExtractsSectionsAndFieldsFromAppSettings()
|
||||
{
|
||||
var settings = new AppSettings();
|
||||
var sections = SettingsSchema.Build(settings);
|
||||
|
||||
sections.Should().NotBeEmpty();
|
||||
sections.SelectMany(s => s.Fields).Should().NotBeEmpty();
|
||||
|
||||
// Webserver-Abschnitt prüfen
|
||||
var ws = sections.FirstOrDefault(s => s.Fields.Any(f => f.Path == "Webserver.Port"));
|
||||
ws.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToDto_MasksPasswordsAndTokens()
|
||||
{
|
||||
var settings = new AppSettings();
|
||||
settings.Webserver.AuthToken = "super-secret-1234";
|
||||
settings.Database.Password = "db-pass-secret";
|
||||
|
||||
var dtos = SettingsSchema.ToDto(settings);
|
||||
var allFields = dtos.SelectMany(s => s.Fields).ToList();
|
||||
|
||||
// AuthToken prüfen
|
||||
var tokenField = allFields.FirstOrDefault(f => f.Path == "Webserver.AuthToken");
|
||||
tokenField.Should().NotBeNull();
|
||||
tokenField!.IsPassword.Should().BeTrue();
|
||||
tokenField.HasValue.Should().BeTrue();
|
||||
tokenField.Value.Should().BeNull("Passwörter und Tokens dürfen niemals im Klartext im DTO stehen!");
|
||||
|
||||
// DB Password prüfen
|
||||
var passField = allFields.FirstOrDefault(f => f.Path == "Database.Password");
|
||||
passField.Should().NotBeNull();
|
||||
passField!.IsPassword.Should().BeTrue();
|
||||
passField.Value.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrySetValue_UpdatesValueByPath()
|
||||
{
|
||||
var settings = new AppSettings();
|
||||
settings.Webserver.Port = 5001;
|
||||
|
||||
var success = SettingsSchema.TrySetValue(settings, "Webserver.Port", 6002);
|
||||
|
||||
success.Should().BeTrue();
|
||||
settings.Webserver.Port.Should().Be(6002);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrySetValue_PreservesPasswordWhenEmptyPassed()
|
||||
{
|
||||
var settings = new AppSettings();
|
||||
settings.Webserver.AuthToken = "keep-this-token";
|
||||
|
||||
var success = SettingsSchema.TrySetValue(settings, "Webserver.AuthToken", "");
|
||||
|
||||
success.Should().BeTrue();
|
||||
settings.Webserver.AuthToken.Should().Be("keep-this-token");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrySetValue_ReturnsFalseForUnknownPath()
|
||||
{
|
||||
var settings = new AppSettings();
|
||||
|
||||
var success = SettingsSchema.TrySetValue(settings, "NonExistent.Field", "val");
|
||||
|
||||
success.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrySetValue_ParsesStringRepresentationOfTypes()
|
||||
{
|
||||
var settings = new AppSettings();
|
||||
|
||||
// int als string
|
||||
SettingsSchema.TrySetValue(settings, "Webserver.Port", "7001").Should().BeTrue();
|
||||
settings.Webserver.Port.Should().Be(7001);
|
||||
|
||||
// bool als string
|
||||
SettingsSchema.TrySetValue(settings, "Webserver.Enabled", "true").Should().BeTrue();
|
||||
settings.Webserver.Enabled.Should().BeTrue();
|
||||
|
||||
// bool als false
|
||||
SettingsSchema.TrySetValue(settings, "Webserver.Enabled", "false").Should().BeTrue();
|
||||
settings.Webserver.Enabled.Should().BeFalse();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
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();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsAuthenticated_RecognizesQueryToken()
|
||||
{
|
||||
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.QueryString = new Microsoft.AspNetCore.Http.QueryString("?token=my-secret-key");
|
||||
|
||||
sut.IsAuthenticated(ctx).Should().BeTrue();
|
||||
}
|
||||
|
||||
[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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
using FluentAssertions;
|
||||
using IBKRTrader.Core.Logging;
|
||||
using IBKRTrader.Hosting.Web;
|
||||
|
||||
namespace IBKRTrader.Tests.Web;
|
||||
|
||||
[Trait("cat", "unit")]
|
||||
public class LogStreamServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetRecent_CapturesLogEntries_AndCapsAtRingBufferSize()
|
||||
{
|
||||
var logger = new LoggingService();
|
||||
using var sut = new LogStreamService(logger);
|
||||
|
||||
for (int i = 0; i < 150; i++)
|
||||
{
|
||||
logger.Info("TestModule", $"Message {i}");
|
||||
}
|
||||
|
||||
var recent = sut.GetRecent();
|
||||
recent.Count.Should().Be(100);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_UnsubscribesFromLogger()
|
||||
{
|
||||
var logger = new LoggingService();
|
||||
var sut = new LogStreamService(logger);
|
||||
|
||||
logger.Info("TestModule", "Before dispose");
|
||||
sut.GetRecent().Count.Should().Be(1);
|
||||
|
||||
sut.Dispose();
|
||||
|
||||
logger.Info("TestModule", "After dispose");
|
||||
sut.GetRecent().Count.Should().Be(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using FluentAssertions;
|
||||
using IBKRTrader.Core.Logging;
|
||||
using IBKRTrader.Core.Persistence.Ef;
|
||||
using IBKRTrader.Core.Settings;
|
||||
using IBKRTrader.Core.Workers;
|
||||
using IBKRTrader.Hosting.Web;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NSubstitute;
|
||||
|
||||
namespace IBKRTrader.Tests.Web;
|
||||
|
||||
[Trait("cat", "unit")]
|
||||
public class WebserverWorkerTests
|
||||
{
|
||||
private sealed class InMemoryFactory : IDbContextFactory<CoreDbContext>
|
||||
{
|
||||
private readonly DbContextOptions<CoreDbContext> _options;
|
||||
public InMemoryFactory(DbContextOptions<CoreDbContext> options) => _options = options;
|
||||
public CoreDbContext CreateDbContext() => new(_options);
|
||||
}
|
||||
|
||||
private static (WebserverWorker Sut, SettingsService Settings) CreateSut(bool enabled = true, int port = 5001)
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<CoreDbContext>()
|
||||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
var dbf = new InMemoryFactory(options);
|
||||
var settings = new SettingsService();
|
||||
settings.Settings.Webserver.Enabled = enabled;
|
||||
settings.Settings.Webserver.Port = port;
|
||||
|
||||
var logger = new LoggingService();
|
||||
var sp = Substitute.For<IServiceProvider>();
|
||||
|
||||
var sut = new WebserverWorker(logger, dbf, settings, sp);
|
||||
return (sut, settings);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Worker_Properties_AreConfiguredCorrectly()
|
||||
{
|
||||
var (sut, _) = CreateSut(enabled: true, port: 5001);
|
||||
|
||||
sut.Name.Should().Be("WebserverService");
|
||||
sut.Module.Should().Be("Core");
|
||||
sut.Type.Should().Be(WorkerType.Service);
|
||||
sut.Info.Active.Should().BeTrue();
|
||||
sut.Info.RunEvery.Should().Be("Service");
|
||||
sut.Info.Info.Should().Be("Port 5001");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Worker_WhenDisabled_ReportsInactive()
|
||||
{
|
||||
var (sut, _) = CreateSut(enabled: false, port: 8080);
|
||||
|
||||
sut.Info.Active.Should().BeFalse();
|
||||
sut.Info.Info.Should().Be("Deaktiviert");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user