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 */ }
}
}
}