using System; using System.IO; using System.Linq; using System.Text.Json; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using PolyTraderSharp.Models; using Xunit; namespace PolyTrader.Tests.WebUi { /// /// Server-Einstellungen über das WebUI (#561): eine Freigabeliste ohne Geheimnisse, geprüfte Werte, /// alles oder nichts. /// public class ServerSettingsEndpointTests { private static ServerSettings WithSecrets() => new() { LicenseKey = "enc:v1:LIZENZ", WatchdogToken = "enc:v1:WATCHDOG", MullvadAccount = "1234567890", PolygonRpcUrl = "wss://polygon-mainnet.g.alchemy.com/v2/ALCHEMY", WatchdogUrl = "https://dc.example", VpnLocation = "cz", WatchdogIntervalSeconds = 60 }; private static JsonElement Field(JsonElement settings, string key) => settings.GetProperty("sections").EnumerateArray() .SelectMany(s => s.GetProperty("fields").EnumerateArray()) .Single(f => f.GetProperty("key").GetString() == key); private static bool HasField(JsonElement settings, string key) => settings.GetProperty("sections").EnumerateArray() .SelectMany(s => s.GetProperty("fields").EnumerateArray()) .Any(f => f.GetProperty("key").GetString() == key); [Fact] public async Task Anzeige_enthaelt_die_Freigabeliste_aber_keine_Geheimnisfelder() { await using var web = await WebUiTestHost.StartAsync(); WithSecrets().Save(web.SettingsPath); var settings = await web.GetJsonAsync("/api/settings/server"); Assert.True(Field(settings, "VpnEnabled").GetProperty("editable").GetBoolean()); Assert.False(Field(settings, "WatchdogUrl").GetProperty("editable").GetBoolean()); Assert.False(Field(settings, "MullvadCliPath").GetProperty("editable").GetBoolean()); Assert.True(Field(settings, "ApplicationTimeZoneId").GetProperty("requiresRestart").GetBoolean()); Assert.False(Field(settings, "UsePolymarketWebsockets").GetProperty("requiresRestart").GetBoolean()); foreach (var secret in new[] { "LicenseKey", "WatchdogToken", "MullvadAccount", "PolygonRpcUrl" }) Assert.False(HasField(settings, secret), secret); var secrets = settings.GetProperty("secrets").EnumerateArray() .ToDictionary(x => x.GetProperty("label").GetString()!, x => x.GetProperty("status").GetString()!); Assert.Equal("gesetzt", secrets["Mullvad-Account"]); Assert.Equal("gesetzt (verschlüsselt)", secrets["Lizenzschlüssel"]); } [Fact] public async Task Aendern_schreibt_nur_die_Werte_und_zieht_den_laufenden_Stand_nach() { var live = WithSecrets(); await using var web = await WebUiTestHost.StartAsync(s => s.AddSingleton(live)); WithSecrets().Save(web.SettingsPath); var (status, body) = await web.PostJsonAsync("/api/settings/server", new { values = new { VpnEnabled = true, WatchdogIntervalSeconds = 120, VpnLocation = "cz-prg", Theme = "dunkel" } }); Assert.Equal(200, status); Assert.True(body.GetProperty("restartRequired").GetBoolean()); // Theme wirkt erst nach Neustart var written = ServerSettings.Load(web.SettingsPath); Assert.True(written.VpnEnabled); Assert.Equal(120, written.WatchdogIntervalSeconds); Assert.Equal("cz-prg", written.VpnLocation); Assert.Equal(AppThemeVariant.Dunkel, written.Theme); Assert.Equal("enc:v1:LIZENZ", written.LicenseKey); Assert.Equal("enc:v1:WATCHDOG", written.WatchdogToken); Assert.Equal("1234567890", written.MullvadAccount); Assert.Equal("wss://polygon-mainnet.g.alchemy.com/v2/ALCHEMY", written.PolygonRpcUrl); Assert.True(live.VpnEnabled); // laufender Stand Assert.Equal(120, live.WatchdogIntervalSeconds); } [Theory] [InlineData("WatchdogUrl", "https://angreifer.example")] [InlineData("MullvadCliPath", "/tmp/boese")] [InlineData("LicenseKey", "neu")] [InlineData("PolygonRpcUrl", "wss://x")] public async Task Desktop_Felder_werden_mit_403_abgelehnt_und_nichts_geschrieben(string key, string value) { await using var web = await WebUiTestHost.StartAsync(); WithSecrets().Save(web.SettingsPath); string before = File.ReadAllText(web.SettingsPath); var values = new System.Collections.Generic.Dictionary { [key] = value, ["VpnEnabled"] = true }; var (status, body) = await web.PostJsonAsync("/api/settings/server", new { values }); Assert.Equal(403, status); Assert.Contains("Desktop", body.GetProperty("error").GetString()); Assert.Equal(before, File.ReadAllText(web.SettingsPath)); } [Theory] [InlineData("VpnLocation", "cz account login 999")] // Argumente fuer das Mullvad-Programm einschleusen [InlineData("VpnLocation", "cz;reboot")] [InlineData("ApplicationTimeZoneId", "Mars/Olympus")] [InlineData("WatchdogSource", "polytrader\\\" , \\\"x")] public async Task Ungueltige_Texte_werden_abgelehnt(string key, string value) { await using var web = await WebUiTestHost.StartAsync(); WithSecrets().Save(web.SettingsPath); string before = File.ReadAllText(web.SettingsPath); var values = new System.Collections.Generic.Dictionary { [key] = value }; var (status, _) = await web.PostJsonAsync("/api/settings/server", new { values }); Assert.Equal(400, status); Assert.Equal(before, File.ReadAllText(web.SettingsPath)); } [Fact] public async Task Ein_ungueltiger_Wert_verhindert_auch_die_gueltigen() { await using var web = await WebUiTestHost.StartAsync(); WithSecrets().Save(web.SettingsPath); var (status, body) = await web.PostJsonAsync("/api/settings/server", new { values = new { VpnEnabled = true, WatchdogIntervalSeconds = 5 } }); Assert.Equal(400, status); Assert.Contains("Nichts gespeichert", body.GetProperty("error").GetString()); Assert.False(ServerSettings.Load(web.SettingsPath).VpnEnabled); } [Fact] public async Task Unbekanntes_Feld_leerer_Rumpf_und_gueltige_Zeitzone() { await using var web = await WebUiTestHost.StartAsync(); var (unknown, _) = await web.PostJsonAsync("/api/settings/server", new { values = new { GibtEsNicht = 1 } }); var (empty, _) = await web.PostJsonAsync("/api/settings/server"); Assert.Equal(400, unknown); Assert.Equal(400, empty); Assert.False(File.Exists(web.SettingsPath)); var (ok, _) = await web.PostJsonAsync("/api/settings/server", new { values = new { ApplicationTimeZoneId = "Europe/Berlin" } }); Assert.Equal(200, ok); Assert.Equal("Europe/Berlin", ServerSettings.Load(web.SettingsPath).ApplicationTimeZoneId); } [Fact] public async Task Unlesbare_Datei_wird_weder_angezeigt_noch_ueberschrieben() { await using var web = await WebUiTestHost.StartAsync(); const string kaputt = "enc:v1:LIZENZ