using System; using System.Net.Http; using System.Text; using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using PolyTrader.Modules.CopyTrading.Persistence; using PolyTrader.Modules.CopyTrading.Persistence.Ef; using PolyTrader.Modules.ResolutionFarming.Persistence; using PolyTrader.Modules.ResolutionFarming.Persistence.Ef; using PolyTrader.Tests.TestSupport; using PolyTrader.Web.Configuration; using PolyTrader.Web.Hosting; using PolyTrader.Web.Security; using PolyTraderSharp; using PolyTraderSharp.Services; namespace PolyTrader.Tests.WebUi { /// /// Startet das WebUI in einem Testserver und spricht es ueber echtes HTTP an (#568). /// /// Die Anfragekette kommt aus , /// also exakt aus dem Betrieb: statische Dateien, Token-Pruefung, Routen. Ersetzt wird nur, was /// ausserhalb des Prozesses liegt - Kestrel durch den Testserver, MySQL durch den /// InMemory-Provider. /// /// Vorher rief keiner der WebUI-Tests eine Route auf. Sie bauten die Logik der Endpunkte im /// Test nach und prueften den Nachbau - ein solcher Test bleibt gruen, egal was der Endpunkt tut. /// internal sealed class WebUiTestHost : IAsyncDisposable { public const string Token = "webui-test-token-7f3a9c1e5b2d4f6a8c0e"; private readonly IHost _host; private readonly string _settingsDirectory; public TradingState State { get; } public CopyTradingState CopyState { get; } public TerminalLogger Logger { get; } public JobManager Jobs { get; } public WebUiOptions WebOptions { get; } /// Eigene server_settings.xml je Testhost in einem temporaeren Ordner - nie die echte. public string SettingsPath => System.IO.Path.Combine(_settingsDirectory, "server_settings.xml"); /// Mit gueltigem Token. public HttpClient Client { get; } /// Ohne Token - fuer die Pruefung der Zugangssperre. public HttpClient Anonymous { get; } private WebUiTestHost(IHost host, TradingState state, CopyTradingState copyState, TerminalLogger logger, JobManager jobs, WebUiOptions options, string settingsDirectory) { _host = host; _settingsDirectory = settingsDirectory; State = state; CopyState = copyState; Logger = logger; Jobs = jobs; WebOptions = options; var server = host.GetTestServer(); Client = server.CreateClient(); Client.DefaultRequestHeaders.Add(TokenAuthMiddleware.TokenHeaderName, Token); Anonymous = server.CreateClient(); } /// /// Startet den Host. Die Typen, die Endpunkte direkt als Parameter nehmen (TradingState, /// CopyTradingState, TerminalLogger, JobManager), sind immer registriert: Minimal APIs werten /// einen nicht registrierten Parametertyp sonst als Anfragekoerper, und dann scheitert jede /// GET-Route schon beim Aufbau. /// public static async Task StartAsync( Action? configureServices = null, Action? configureOptions = null) { var state = new TradingState(); var copyState = new CopyTradingState(); var logger = new TerminalLogger(); var jobs = new JobManager(); var options = new WebUiOptions { Enabled = true, AuthToken = Token }; configureOptions?.Invoke(options); string settingsDirectory = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "polytrader-webui-" + Guid.NewGuid().ToString("N")); System.IO.Directory.CreateDirectory(settingsDirectory); var host = await new HostBuilder() .ConfigureWebHost(web => { web.UseTestServer(); web.ConfigureServices(services => { services.AddRouting(); services.AddSingleton(Microsoft.Extensions.Options.Options.Create(options)); services.AddSingleton(state); services.AddSingleton(copyState); services.AddSingleton(logger); services.AddSingleton(jobs); // Wie im Betrieb (Program): alle Module und die Einstellungsdatei. Tests koennen beides // in configureServices ersetzen - die letzte Registrierung gewinnt. services.AddSingleton(new PolyTrader.Core.Modularity.KnownModules(new PolyTrader.Core.Modularity.IPolyTraderModule[] { new PolyTrader.Modules.CopyTrading.CopyTradingModule(), new PolyTrader.Modules.ResolutionFarming.ResolutionFarmingModule(), new PolyTrader.Modules.Supervisor.SupervisorModule(), new PolyTrader.Modules.Accounting.AccountingModule() })); services.AddSingleton(new PolyTrader.Core.Configuration.ServerSettingsFile( System.IO.Path.Combine(settingsDirectory, "server_settings.xml"))); configureServices?.Invoke(services); }); web.Configure(app => app.UsePolyTraderWebPipeline()); }) .StartAsync(); return new WebUiTestHost(host, state, copyState, logger, jobs, options, settingsDirectory); } /// CopyTrading-Repositories auf einer eigenen InMemory-Datenbank. public static void AddCopyTradingStore(IServiceCollection services) { services.AddSingleton>( new InMemoryContextFactory(o => new CopyTradingDbContext(o))); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); } /// ResolutionFarming-Repositories auf einer eigenen InMemory-Datenbank. public static void AddResolutionFarmingStore(IServiceCollection services) { services.AddSingleton>( new InMemoryContextFactory(o => new ResolutionFarmingDbContext(o))); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); } public T Get() where T : notnull => _host.Services.GetRequiredService(); /// GET mit Token; wirft mit Statuscode und Antworttext, wenn die Route scheitert. public async Task GetJsonAsync(string path) { using var res = await Client.GetAsync(path); string body = await res.Content.ReadAsStringAsync(); if (!res.IsSuccessStatusCode) throw new InvalidOperationException($"GET {path} -> {(int)res.StatusCode}: {body}"); return Parse(body); } /// POST mit Token; liefert Status und Antwort, ohne bei Fehlern zu werfen. public async Task<(int Status, JsonElement Body)> PostJsonAsync(string path, object? payload = null) { using var content = new StringContent( payload == null ? "{}" : JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"); using var res = await Client.PostAsync(path, content); string body = await res.Content.ReadAsStringAsync(); return ((int)res.StatusCode, Parse(body)); } private static JsonElement Parse(string body) => string.IsNullOrWhiteSpace(body) ? default : JsonDocument.Parse(body).RootElement.Clone(); public async ValueTask DisposeAsync() { Client.Dispose(); Anonymous.Dispose(); await _host.StopAsync(); _host.Dispose(); try { System.IO.Directory.Delete(_settingsDirectory, recursive: true); } catch { /* temporaer, darf liegen bleiben */ } } } }