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