using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using PolyTraderSharp.Models; using Xunit; namespace PolyTrader.Tests.WebUi { /// /// Geheimnisse verlassen den Server nie ueber das WebUI (#562). /// /// Entscheidung Richard, 13.09.2026: Geheimnisse werden ausschliesslich im Desktop verwaltet. /// Dieser Test haelt das fest, ohne sich auf eine Liste von Routen zu verlassen: Er zaehlt alle /// GET-Routen selbst auf und prueft jede Antwort gegen eindeutige Kanarienwerte. Eine neue Route, /// die versehentlich ein Konto oder die Server-Einstellungen serialisiert, faellt damit auf, ohne /// dass jemand an diesen Test denken muss. /// public class WebUiSecretsTests { private const string ApiKeyCanary = "CANARY-APIKEY-4b1f"; private const string ApiSecretCanary = "CANARY-APISECRET-8c2e"; private const string ApiPassphraseCanary = "CANARY-PASSPHRASE-1d7a"; private const string PrivateKeyCanary = "CANARY-PRIVATEKEY-0x9f1e2d3c"; private const string MullvadCanary = "CANARY-MULLVAD-5566"; private const string AlchemyCanary = "CANARY-ALCHEMYKEY-7a7a"; private const string WatchdogCanary = "CANARY-WATCHDOGTOKEN-3e3e"; private const string LicenseCanary = "CANARY-LICENSE-2f2f"; private static readonly string[] Secrets = { ApiKeyCanary, ApiSecretCanary, ApiPassphraseCanary, PrivateKeyCanary, MullvadCanary, AlchemyCanary, WatchdogCanary, LicenseCanary, WebUiTestHost.Token }; /// /// Ein Live-Konto mit allen Schluesseln und Server-Einstellungen mit allen Geheimnissen. Die /// Modul-Ablagen sind registriert, damit die Routen echten Code durchlaufen statt frueh /// mit "nicht verfuegbar" zu antworten. /// private static async Task StartWithSecretsAsync() { var settings = new ServerSettings { MullvadAccount = MullvadCanary, PolygonRpcUrl = "wss://polygon-mainnet.g.alchemy.com/v2/" + AlchemyCanary, WatchdogToken = WatchdogCanary, LicenseKey = LicenseCanary }; var web = await WebUiTestHost.StartAsync(s => { WebUiTestHost.AddCopyTradingStore(s); WebUiTestHost.AddResolutionFarmingStore(s); s.AddSingleton(settings); }); web.State.Accounts[1] = new AccountState { AccountId = 1, Name = "Live-Konto", WalletAddress = "0x00000000000000000000000000000000000000a1", ApiKey = ApiKeyCanary, ApiSecret = ApiSecretCanary, ApiPassphrase = ApiPassphraseCanary, PrivateKey = PrivateKeyCanary, TotalBalance = 1000m, AvailableBalance = 800m }; return web; } /// /// Alle GET-Routen unter /api, mit eingesetzten Parametern. Ausgenommen ist nur der Live-Log-Stream: /// Server-Sent Events enden nie. Er liefert dieselben Eintraege wie GET /api/logs, und die /// Route ist abgedeckt. /// private static List ApiGetPaths(WebUiTestHost web) { var paths = new List(); foreach (var endpoint in web.Get().Endpoints.OfType()) { string raw = endpoint.RoutePattern.RawText ?? string.Empty; if (!raw.StartsWith("/api/", StringComparison.OrdinalIgnoreCase)) continue; if (raw.Contains('*')) continue; if (raw.EndsWith("/stream", StringComparison.OrdinalIgnoreCase)) continue; var methods = endpoint.Metadata.GetMetadata()?.HttpMethods; if (methods == null || !methods.Contains("GET")) continue; paths.Add(Regex.Replace(raw, @"\{([^}:]+)(:([^}]+))?\}", m => m.Groups[3].Value is "int" or "long" ? "1" : "x")); } return paths; } [Fact] public async Task Keine_GET_Route_liefert_ein_Geheimnis_aus() { await using var web = await StartWithSecretsAsync(); var paths = ApiGetPaths(web); foreach (var path in paths) { using var res = await web.Client.GetAsync(path); string body = await res.Content.ReadAsStringAsync(); foreach (var secret in Secrets) { Assert.False(body.Contains(secret, StringComparison.Ordinal), $"GET {path} ({(int)res.StatusCode}) liefert ein Geheimnis aus: {secret}"); } } // Ohne Untergrenze bestuende der Test auch dann, wenn die Aufzaehlung leer bliebe. Assert.True(paths.Count >= 25, $"Nur {paths.Count} GET-Routen gefunden: {string.Join(", ", paths)}"); } [Fact] public async Task Zugangseinstellungen_zeigen_vom_Token_nur_ob_es_gesetzt_ist() { await using var web = await WebUiTestHost.StartAsync(); var options = await web.GetJsonAsync("/api/settings/webui"); Assert.True(options.GetProperty("hasToken").GetBoolean()); Assert.False(options.TryGetProperty("token", out _)); Assert.Equal("desktop", options.GetProperty("managedIn").GetString()); } [Fact] public async Task Zugangseinstellungen_lassen_sich_ueber_das_WebUI_nicht_aendern() { await using var web = await WebUiTestHost.StartAsync(); var (saveStatus, saveBody) = await web.PostJsonAsync("/api/settings/webui", new { enabled = true, port = 7000, localOnly = false, authToken = "selbst-gewaehlt" }); var (generateStatus, generateBody) = await web.PostJsonAsync("/api/settings/webui/generate-token"); Assert.Equal(403, saveStatus); Assert.Equal(403, generateStatus); Assert.Contains("Desktop", saveBody.GetProperty("error").GetString()); Assert.False(generateBody.TryGetProperty("token", out _)); // Nichts davon darf den laufenden Zustand beruehrt haben. Assert.Equal(WebUiTestHost.Token, web.WebOptions.AuthToken); Assert.True(web.WebOptions.LocalOnly); Assert.Equal(6001, web.WebOptions.Port); } [Fact] public async Task Polymarket_Konten_sind_im_WebUI_nur_lesbar() { await using var web = await WebUiTestHost.StartAsync(); var accountRoutes = web.Get().Endpoints.OfType() .Where(e => (e.RoutePattern.RawText ?? string.Empty) .StartsWith("/api/accounts", StringComparison.OrdinalIgnoreCase)) .ToList(); Assert.NotEmpty(accountRoutes); Assert.All(accountRoutes, e => Assert.Equal(new[] { "GET" }, e.Metadata.GetMetadata()!.HttpMethods)); } } }