Files
PolyTraderSharp/tests/PolyTrader.Tests/WebUi/CopyTradingAndResolutionFarmingEndpointsTests.cs
T
RichardandClaude Opus 5 b70d5e5f00 WebUI-Tests rufen jetzt echte Routen auf
Behebt #568.

Keine der Testdateien mit "EndpointsTests" im Namen hat je einen Endpunkt
aufgerufen. CopyTradingAndResolutionFarmingEndpointsTests beruehrte gar keinen
Produktionscode: Die Tests summierten Listen im Test und verglichen das Ergebnis
mit sich selbst. Modules_KnownModules pruefte, ob ein Array seine eigenen
Elemente enthaelt. Settings_UpdateRequest pruefte die Portbegrenzung an einer im
Test nachgebauten Lambda. Solche Tests bleiben gruen, egal was der Endpunkt tut.

Neu startet WebUiTestHost das WebUI in einem Testserver
(Microsoft.AspNetCore.TestHost) und spricht es ueber HTTP an. Damit der Test
die Kette aus dem Betrieb prueft und keine nachgebaute, liegt sie jetzt in
UsePolyTraderWebPipeline(): statische Dateien, Token-Pruefung, alle Routen.
ConfigurePolyTraderWeb ruft genau diese Methode. Ersetzt wird nur, was
ausserhalb des Prozesses liegt - Kestrel durch den Testserver, MySQL durch den
InMemory-Provider.

Die acht CopyTrading/ResolutionFarming-Tests pruefen dieselben Absichten wie
vorher, jetzt an der Antwort der Route. Neu hinzugekommen sind Tests fuer:
- Zugangssperre: ohne Token, mit falschem Token, ohne konfiguriertes Token
- die Modulliste
- Job-Umschalten samt 404
- die Ablehnung einer leeren Supervisor-Frage

Die beiden Tests, die schon echten Code prueften, bleiben.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 13:38:19 +02:00

212 lines
12 KiB
C#

using System;
using System.Linq;
using System.Threading.Tasks;
using PolyTrader.Modules.CopyTrading.Persistence;
using PolyTrader.Modules.ResolutionFarming.Models;
using PolyTrader.Modules.ResolutionFarming.Persistence;
using PolyTraderSharp.Models;
using Xunit;
namespace PolyTrader.Tests.WebUi
{
/// <summary>
/// CopyTrading- und ResolutionFarming-Endpunkte ueber echte HTTP-Aufrufe (#568).
///
/// Die Datei gleichen Namens enthielt vorher acht Tests, die keine einzige Zeile Produktionscode
/// aufriefen: Sie summierten Listen im Test und verglichen das Ergebnis mit sich selbst. Die
/// Absichten sind dieselben geblieben, geprueft wird jetzt die Antwort der Route.
/// </summary>
public class CopyTradingAndResolutionFarmingEndpointsTests
{
private static Task<WebUiTestHost> StartCopyTradingAsync() =>
WebUiTestHost.StartAsync(WebUiTestHost.AddCopyTradingStore);
private static Task<WebUiTestHost> StartResolutionFarmingAsync() =>
WebUiTestHost.StartAsync(WebUiTestHost.AddResolutionFarmingStore);
// ---- CopyTrading -----------------------------------------------------
[Fact]
public async Task CopyTrading_Trader_anlegen_und_wieder_lesen()
{
await using var web = await StartCopyTradingAsync();
var (status, created) = await web.PostJsonAsync("/api/modules/copytrading/traders", new
{
id = 0,
displayName = "Whale Alpha",
walletAddress = " 0x1234567890abcdef1234567890abcdef12345678 ",
category = " ",
description = "Top Polymarket Trader",
reasoning = "Winrate ueber 70 Prozent",
isActive = true,
isHidden = false,
autoPauseEnabled = true,
makerEntry = true,
assignedAccountIds = new[] { 1, 2 }
});
Assert.Equal(200, status);
var trader = created.GetProperty("trader");
Assert.Equal(1, trader.GetProperty("id").GetInt32());
Assert.Equal("NEW_BIG_BET", trader.GetProperty("category").GetString()); // leere Kategorie -> Vorgabe
Assert.Equal("0x1234567890abcdef1234567890abcdef12345678", trader.GetProperty("walletAddress").GetString());
var all = await web.GetJsonAsync("/api/modules/copytrading/traders");
var stored = Assert.Single(all.EnumerateArray());
Assert.Equal("Whale Alpha", stored.GetProperty("displayName").GetString());
Assert.True(stored.GetProperty("makerEntry").GetBoolean());
Assert.Equal(new[] { 1, 2 },
stored.GetProperty("assignedAccountIds").EnumerateArray().Select(e => e.GetInt32()).OrderBy(x => x).ToArray());
}
[Fact]
public async Task CopyTrading_offene_Trades_rechnen_Buchgewinn_je_Position_und_gesamt()
{
await using var web = await StartCopyTradingAsync();
var acc1 = new AccountState { AccountId = 1, Name = "Konto 1" };
acc1.OpenPositions["tok-1"] = new Position
{
AccountId = 1, TokenId = "tok-1", SourceTraderId = 1, SourceTraderName = "Whale 1",
MarketQuestion = "Will Trump win in 2024?", Outcome = "Yes",
EntryPrice = 0.50m, CurrentPrice = 0.70m, Size = 100m, AmountUsd = 50m, CurrentValueUsd = 70m
};
var acc2 = new AccountState { AccountId = 2, Name = "Konto 2" };
acc2.OpenPositions["tok-2"] = new Position
{
AccountId = 2, TokenId = "tok-2", SourceTraderId = 2, SourceTraderName = "Whale 2",
MarketQuestion = "Will Fed cut rates?", Outcome = "Yes",
EntryPrice = 0.80m, CurrentPrice = 0.60m, Size = 50m, AmountUsd = 40m, CurrentValueUsd = 30m
};
web.State.Accounts[1] = acc1;
web.State.Accounts[2] = acc2;
var open = await web.GetJsonAsync("/api/modules/copytrading/open-trades");
Assert.Equal(2, open.GetProperty("count").GetInt32());
Assert.Equal(90m, open.GetProperty("totalAmount").GetDecimal());
Assert.Equal(10m, open.GetProperty("totalUnrealized").GetDecimal()); // (70-50) + (30-40)
var byMarket = open.GetProperty("trades").EnumerateArray()
.ToDictionary(t => t.GetProperty("market").GetString()!);
Assert.Equal(20m, byMarket["Will Trump win in 2024?"].GetProperty("unrealizedPnl").GetDecimal());
Assert.Equal(40m, byMarket["Will Trump win in 2024?"].GetProperty("unrealizedPct").GetDecimal());
Assert.Equal(-10m, byMarket["Will Fed cut rates?"].GetProperty("unrealizedPnl").GetDecimal());
Assert.Equal(-25m, byMarket["Will Fed cut rates?"].GetProperty("unrealizedPct").GetDecimal());
}
[Fact]
public async Task CopyTrading_geschlossene_Trades_filtern_nach_Trader_Ergebnis_und_Suche()
{
await using var web = await StartCopyTradingAsync();
var log = web.Get<ICopyTradeLogRepository>();
log.Insert(new ClosedTrade { TradeId = 1, AccountId = 1, SourceTraderId = 10, TokenId = "a",
MarketQuestion = "Bitcoin reaches 100k?", Outcome = "Yes", RealizedPnl = 35m, ClosedAt = DateTime.UtcNow.AddHours(-2) });
log.Insert(new ClosedTrade { TradeId = 2, AccountId = 1, SourceTraderId = 10, TokenId = "b",
MarketQuestion = "Solana reaches 300?", Outcome = "No", RealizedPnl = -12m, ClosedAt = DateTime.UtcNow.AddHours(-5) });
log.Insert(new ClosedTrade { TradeId = 3, AccountId = 2, SourceTraderId = 20, TokenId = "c",
MarketQuestion = "Ethereum reaches 5k?", Outcome = "Yes", RealizedPnl = 50m, ClosedAt = DateTime.UtcNow.AddHours(-1) });
var byTrader = await web.GetJsonAsync("/api/modules/copytrading/closed-trades?traderId=10");
Assert.Equal(2, byTrader.GetProperty("totalCount").GetInt32());
var winners = await web.GetJsonAsync("/api/modules/copytrading/closed-trades?result=Gewinner");
Assert.Equal(2, winners.GetProperty("totalCount").GetInt32());
Assert.Equal(85m, winners.GetProperty("summary").GetProperty("totalPnl").GetDecimal());
var search = await web.GetJsonAsync("/api/modules/copytrading/closed-trades?search=bitcoin");
var hit = Assert.Single(search.GetProperty("items").EnumerateArray());
Assert.Equal(1, hit.GetProperty("tradeId").GetInt32());
}
[Fact]
public async Task CopyTrading_Kontoeinstellungen_ohne_Eintrag_liefern_die_Vorgaben()
{
await using var web = await StartCopyTradingAsync();
var settings = await web.GetJsonAsync("/api/modules/copytrading/settings/42");
Assert.Equal(42, settings.GetProperty("accountId").GetInt32());
Assert.Equal(5.0m, settings.GetProperty("perMarketLimit").GetDecimal());
Assert.Equal(10.0m, settings.GetProperty("perMasterLimit").GetDecimal());
Assert.Equal(0.98m, settings.GetProperty("maxBuyPrice").GetDecimal());
Assert.Equal(15.0m, settings.GetProperty("sellFloorPct").GetDecimal());
}
// ---- ResolutionFarming -----------------------------------------------
[Fact]
public async Task ResolutionFarming_Kandidaten_liefern_Annahme_und_Ablehnungsgrund()
{
await using var web = await StartResolutionFarmingAsync();
web.State.Accounts[1] = new AccountState { AccountId = 1, Name = "Demo", IsDemo = true };
var repo = web.Get<IRfCandidateRepository>();
repo.Insert(new RfCandidate { AccountId = 1, TokenId = "t1", ScannedAt = DateTime.UtcNow,
MarketQuestion = "Will candidate A win election?", Outcome = "Yes", Category = "Politics",
Ask = 0.94m, NetEdgePct = 2.5m, Accepted = true, RejectReason = "" });
repo.Insert(new RfCandidate { AccountId = 1, TokenId = "t2", ScannedAt = DateTime.UtcNow,
MarketQuestion = "Will crypto token reach 100?", Outcome = "Yes", Category = "Crypto",
Ask = 0.91m, NetEdgePct = 0.5m, Accepted = false, RejectReason = "Fee too high for edge" });
var result = await web.GetJsonAsync("/api/modules/resolutionfarming/candidates?accountId=1");
Assert.Equal(2, result.GetProperty("count").GetInt32());
var items = result.GetProperty("candidates").EnumerateArray().ToList();
var accepted = Assert.Single(items, c => c.GetProperty("accepted").GetBoolean());
Assert.Equal("Politics", accepted.GetProperty("category").GetString());
var rejected = Assert.Single(items, c => !c.GetProperty("accepted").GetBoolean());
Assert.Equal("Fee too high for edge", rejected.GetProperty("rejectReason").GetString());
}
[Fact]
public async Task ResolutionFarming_Positionen_summieren_den_Einsatz()
{
await using var web = await StartResolutionFarmingAsync();
var repo = web.Get<IRfPositionRepository>();
repo.Upsert(new RfPosition { AccountId = 1, TokenId = "tok-1", MarketQuestion = "Match 1",
EntryPrice = 0.95m, Size = 100m, AmountUsd = 95m, Status = "Open", OpenedAt = DateTime.UtcNow });
repo.Upsert(new RfPosition { AccountId = 1, TokenId = "tok-2", MarketQuestion = "Match 2",
EntryPrice = 0.92m, Size = 50m, AmountUsd = 46m, Status = "Open", OpenedAt = DateTime.UtcNow });
var result = await web.GetJsonAsync("/api/modules/resolutionfarming/positions?accountId=1");
Assert.Equal(2, result.GetProperty("count").GetInt32());
Assert.Equal(141m, result.GetProperty("totalAmount").GetDecimal());
}
[Fact]
public async Task ResolutionFarming_Historie_rechnet_PnL_und_Trefferquote()
{
await using var web = await StartResolutionFarmingAsync();
var repo = web.Get<IRfClosedTradeRepository>();
repo.Insert(new RfClosedTrade { AccountId = 1, TokenId = "a", MarketQuestion = "Match A", RealizedPnl = 4.50m, RedeemStatus = "Redeemed", ClosedAt = DateTime.UtcNow });
repo.Insert(new RfClosedTrade { AccountId = 1, TokenId = "b", MarketQuestion = "Match B", RealizedPnl = 3.80m, RedeemStatus = "Redeemed", ClosedAt = DateTime.UtcNow });
repo.Insert(new RfClosedTrade { AccountId = 1, TokenId = "c", MarketQuestion = "Match C", RealizedPnl = -20.00m, RedeemStatus = "None", ClosedAt = DateTime.UtcNow });
var result = await web.GetJsonAsync("/api/modules/resolutionfarming/history?accountId=1");
var summary = result.GetProperty("summary");
Assert.Equal(3, summary.GetProperty("count").GetInt32());
Assert.Equal(-11.70m, summary.GetProperty("pnl").GetDecimal());
Assert.Equal(2, summary.GetProperty("wins").GetInt32());
Assert.Equal(66.7m, Math.Round(summary.GetProperty("winRate").GetDecimal(), 1));
}
[Fact]
public async Task ResolutionFarming_Einstellungen_ohne_Eintrag_liefern_die_Vorgaben()
{
await using var web = await StartResolutionFarmingAsync();
var settings = await web.GetJsonAsync("/api/modules/resolutionfarming/settings/5");
Assert.Equal(5, settings.GetProperty("accountId").GetInt32());
Assert.False(settings.GetProperty("enabled").GetBoolean());
Assert.Equal(0.90m, settings.GetProperty("minPrice").GetDecimal());
Assert.Equal(0.98m, settings.GetProperty("maxPrice").GetDecimal());
Assert.Equal(48, settings.GetProperty("maxHoursToResolution").GetInt32());
Assert.Contains("Sports", settings.GetProperty("categoryWhitelistCsv").GetString());
}
}
}