using System; using System.IO; using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using PolyTrader.Core.Configuration; using PolyTrader.Core.Modularity; using PolyTraderSharp.Models; using Xunit; namespace PolyTrader.Tests.WebUi { /// /// Moduleinstellungen über das WebUI (#560): anzeigen, aktivieren, deaktivieren - und die /// Einstellungsdatei dabei nie beschädigen. /// public class ModuleSettingsEndpointTests { private sealed class FakeModule : IPolyTraderModule { private readonly string? _blocker; public FakeModule(string name, string? blocker = null) { Name = name; _blocker = blocker; } public string Name { get; } public string DbPrefix => "fake_"; public void RegisterServices(IServiceCollection services, IConfiguration configuration) { } public void RegisterUi(IModuleUiHost host, IServiceProvider services) { } public string? GetActivationBlocker(IConfiguration configuration) => _blocker; public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; } private static JsonElement ByName(JsonElement modules, string name) => modules.EnumerateArray().Single(m => m.GetProperty("name").GetString() == name); [Fact] public async Task Laufendes_Modul_ohne_Eintrag_ist_aktiv_die_uebrigen_starten_nach_Neustart() { await using var web = await WebUiTestHost.StartAsync(s => s.AddSingleton(new FakeModule("CopyTrading"))); var modules = await web.GetJsonAsync("/api/modules"); var ct = ByName(modules, "CopyTrading"); Assert.Equal("Aktiv", ct.GetProperty("status").GetString()); Assert.Equal("Deaktivieren", ct.GetProperty("action").GetString()); Assert.True(ct.GetProperty("canToggle").GetBoolean()); Assert.Equal("Startet nach Neustart", ByName(modules, "Supervisor").GetProperty("status").GetString()); } [Fact] public async Task Deaktivieren_schreibt_DisabledModules_und_laesst_Schluessel_in_der_Datei_stehen() { await using var web = await WebUiTestHost.StartAsync(s => s.AddSingleton(new FakeModule("CopyTrading"))); new ServerSettings { LicenseKey = "enc:v1:LIZENZ", WatchdogToken = "enc:v1:WATCHDOG", PolygonRpcUrl = "wss://polygon-mainnet.g.alchemy.com/v2/ALCHEMY", VpnLocation = "se" }.Save(web.SettingsPath); var (status, body) = await web.PostJsonAsync("/api/modules/copytrading/enabled", new { enabled = false }); Assert.Equal(200, status); Assert.Equal("Aktiv – stoppt nach Neustart", body.GetProperty("status").GetString()); var written = ServerSettings.Load(web.SettingsPath); Assert.Equal(new[] { "CopyTrading" }, written.DisabledModules); // kanonischer Name, nicht "copytrading" Assert.Equal("enc:v1:LIZENZ", written.LicenseKey); Assert.Equal("enc:v1:WATCHDOG", written.WatchdogToken); Assert.Equal("wss://polygon-mainnet.g.alchemy.com/v2/ALCHEMY", written.PolygonRpcUrl); Assert.Equal("se", written.VpnLocation); } [Fact] public async Task Wiederholter_Aufruf_aendert_nichts_mehr_und_Aktivieren_nimmt_den_Eintrag_zurueck() { await using var web = await WebUiTestHost.StartAsync(); await web.PostJsonAsync("/api/modules/Supervisor/enabled", new { enabled = false }); await web.PostJsonAsync("/api/modules/Supervisor/enabled", new { enabled = false }); Assert.Equal(new[] { "Supervisor" }, ServerSettings.Load(web.SettingsPath).DisabledModules); var (status, body) = await web.PostJsonAsync("/api/modules/Supervisor/enabled", new { enabled = true }); Assert.Equal(200, status); Assert.Empty(ServerSettings.Load(web.SettingsPath).DisabledModules); Assert.True(body.GetProperty("desiredEnabled").GetBoolean()); } [Fact] public async Task Unbekanntes_Modul_404_fehlender_Zustand_400() { await using var web = await WebUiTestHost.StartAsync(); var (unknown, _) = await web.PostJsonAsync("/api/modules/GibtEsNicht/enabled", new { enabled = false }); var (missing, _) = await web.PostJsonAsync("/api/modules/Supervisor/enabled"); Assert.Equal(404, unknown); Assert.Equal(400, missing); Assert.False(File.Exists(web.SettingsPath)); // nichts geschrieben } [Fact] public async Task Modul_mit_Blocker_laesst_sich_nicht_schalten() { await using var web = await WebUiTestHost.StartAsync(s => s.AddSingleton(new KnownModules(new IPolyTraderModule[] { new FakeModule("Supervisor", blocker: "OpenRouter-Key fehlt") }))); var modules = await web.GetJsonAsync("/api/modules"); var sup = ByName(modules, "Supervisor"); Assert.False(sup.GetProperty("canToggle").GetBoolean()); Assert.Equal("OpenRouter-Key fehlt", sup.GetProperty("hint").GetString()); var (status, _) = await web.PostJsonAsync("/api/modules/Supervisor/enabled", new { enabled = true }); Assert.Equal(409, status); } [Fact] public async Task Unlesbare_Einstellungsdatei_wird_nicht_angefasst() { await using var web = await WebUiTestHost.StartAsync(); const string kaputt = "enc:v1:LIZENZ Assert.False(m.GetProperty("canToggle").GetBoolean())); } // ---- ModuleActivationView --------------------------------------------- [Theory] [InlineData(true, true, null, "Aktiv", "Deaktivieren", true)] [InlineData(true, false, null, "Aktiv – stoppt nach Neustart", "Aktivieren", true)] [InlineData(false, true, null, "Startet nach Neustart", "Deaktivieren", true)] [InlineData(false, false, null, "Deaktiviert", "Aktivieren", true)] [InlineData(false, true, "API-Key fehlt", "⚠ Nicht aktivierbar", "—", false)] public void Anzeige_folgt_den_Regeln_des_Desktops(bool running, bool desired, string? blocker, string status, string action, bool canToggle) { var view = ModuleActivationView.Describe(new ModuleActivationInfo("X", running, blocker), desired); Assert.Equal(status, view.Status); Assert.Equal(action, view.Action); Assert.Equal(canToggle, view.CanToggle); } } }