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>
This commit is contained in:
@@ -44,7 +44,21 @@ namespace PolyTrader.Web.Hosting
|
|||||||
kestrel.Listen(ip, options.Port);
|
kestrel.Listen(ip, options.Port);
|
||||||
});
|
});
|
||||||
|
|
||||||
webBuilder.Configure((context, app) =>
|
webBuilder.Configure((context, app) => app.UsePolyTraderWebPipeline());
|
||||||
|
});
|
||||||
|
|
||||||
|
return hostBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Die komplette Anfragekette des WebUI: statische Dateien, Token-Pruefung, Routen.
|
||||||
|
///
|
||||||
|
/// <para>Liegt bewusst ausserhalb von <see cref="ConfigurePolyTraderWeb"/>, damit der Testhost
|
||||||
|
/// genau diese Kette benutzt und nicht eine nachgebaute (#568). Ein Test, der die Routen
|
||||||
|
/// selbst zusammensteckt, prueft im Zweifel eine Reihenfolge, die es im Betrieb nicht gibt -
|
||||||
|
/// etwa eine Route vor der Token-Pruefung.</para>
|
||||||
|
/// </summary>
|
||||||
|
public static IApplicationBuilder UsePolyTraderWebPipeline(this IApplicationBuilder app)
|
||||||
{
|
{
|
||||||
app.UseDefaultFiles();
|
app.UseDefaultFiles();
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
@@ -81,10 +95,8 @@ namespace PolyTrader.Web.Hosting
|
|||||||
|
|
||||||
endpoints.MapFallbackToFile("index.html");
|
endpoints.MapFallbackToFile("index.html");
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return hostBuilder;
|
return app;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="10.0.12" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||||
<PackageReference Include="xunit" Version="2.9.2" />
|
<PackageReference Include="xunit" Version="2.9.2" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
|
|||||||
@@ -1,304 +1,211 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using PolyTrader.Modules.CopyTrading.Persistence;
|
||||||
using PolyTrader.Modules.ResolutionFarming.Models;
|
using PolyTrader.Modules.ResolutionFarming.Models;
|
||||||
using PolyTrader.Web.Endpoints;
|
using PolyTrader.Modules.ResolutionFarming.Persistence;
|
||||||
using PolyTraderSharp.Models;
|
using PolyTraderSharp.Models;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace PolyTrader.Tests.WebUi
|
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
|
public class CopyTradingAndResolutionFarmingEndpointsTests
|
||||||
{
|
{
|
||||||
[Fact]
|
private static Task<WebUiTestHost> StartCopyTradingAsync() =>
|
||||||
public void CopyTrading_TrackedTraderInput_MapsCorrectly()
|
WebUiTestHost.StartAsync(WebUiTestHost.AddCopyTradingStore);
|
||||||
{
|
|
||||||
var input = new CopyTradingEndpoints.TrackedTraderInput(
|
|
||||||
Id: 0,
|
|
||||||
DisplayName: "Whale Alpha",
|
|
||||||
WalletAddress: "0x1234567890abcdef1234567890abcdef12345678",
|
|
||||||
Category: "INSIDER",
|
|
||||||
Description: "Top Polymarket Trader",
|
|
||||||
Reasoning: "Consistent winrate above 70%",
|
|
||||||
IsActive: true,
|
|
||||||
IsHidden: false,
|
|
||||||
AutoPauseEnabled: true,
|
|
||||||
MakerEntry: true,
|
|
||||||
AssignedAccountIds: new List<int> { 1, 2 }
|
|
||||||
);
|
|
||||||
|
|
||||||
int id = input.Id <= 0 ? 1 : input.Id;
|
private static Task<WebUiTestHost> StartResolutionFarmingAsync() =>
|
||||||
var trader = new TrackedTrader
|
WebUiTestHost.StartAsync(WebUiTestHost.AddResolutionFarmingStore);
|
||||||
|
|
||||||
|
// ---- CopyTrading -----------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CopyTrading_Trader_anlegen_und_wieder_lesen()
|
||||||
{
|
{
|
||||||
Id = id,
|
await using var web = await StartCopyTradingAsync();
|
||||||
DisplayName = input.DisplayName ?? $"Trader #{id}",
|
|
||||||
WalletAddress = input.WalletAddress?.Trim() ?? string.Empty,
|
var (status, created) = await web.PostJsonAsync("/api/modules/copytrading/traders", new
|
||||||
Category = string.IsNullOrWhiteSpace(input.Category) ? "NEW_BIG_BET" : input.Category.Trim(),
|
{
|
||||||
Description = input.Description ?? string.Empty,
|
id = 0,
|
||||||
Reasoning = input.Reasoning ?? string.Empty,
|
displayName = "Whale Alpha",
|
||||||
IsActive = input.IsActive,
|
walletAddress = " 0x1234567890abcdef1234567890abcdef12345678 ",
|
||||||
IsHidden = input.IsHidden,
|
category = " ",
|
||||||
AutoPauseEnabled = input.AutoPauseEnabled,
|
description = "Top Polymarket Trader",
|
||||||
MakerEntry = input.MakerEntry,
|
reasoning = "Winrate ueber 70 Prozent",
|
||||||
AssignedAccountIds = new HashSet<int>(input.AssignedAccountIds ?? new List<int>())
|
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" };
|
||||||
Assert.Equal(1, trader.Id);
|
acc2.OpenPositions["tok-2"] = new Position
|
||||||
Assert.Equal("Whale Alpha", trader.DisplayName);
|
|
||||||
Assert.Equal("INSIDER", trader.Category);
|
|
||||||
Assert.True(trader.IsActive);
|
|
||||||
Assert.True(trader.MakerEntry);
|
|
||||||
Assert.Equal(2, trader.AssignedAccountIds.Count);
|
|
||||||
Assert.Contains(1, trader.AssignedAccountIds);
|
|
||||||
Assert.Contains(2, trader.AssignedAccountIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void CopyTrading_OpenTrades_CalculatesUnrealizedMetricsProperly()
|
|
||||||
{
|
{
|
||||||
var positions = new List<Position>
|
AccountId = 2, TokenId = "tok-2", SourceTraderId = 2, SourceTraderName = "Whale 2",
|
||||||
{
|
MarketQuestion = "Will Fed cut rates?", Outcome = "Yes",
|
||||||
new Position
|
EntryPrice = 0.80m, CurrentPrice = 0.60m, Size = 50m, AmountUsd = 40m, CurrentValueUsd = 30m
|
||||||
{
|
|
||||||
AccountId = 1,
|
|
||||||
SourceTraderId = 1,
|
|
||||||
SourceTraderName = "Whale 1",
|
|
||||||
MarketQuestion = "Will Trump win in 2024?",
|
|
||||||
Outcome = "Yes",
|
|
||||||
Side = "BUY",
|
|
||||||
EntryPrice = 0.50m,
|
|
||||||
CurrentPrice = 0.70m,
|
|
||||||
Size = 100m,
|
|
||||||
AmountUsd = 50m,
|
|
||||||
CurrentValueUsd = 70m
|
|
||||||
},
|
|
||||||
new Position
|
|
||||||
{
|
|
||||||
AccountId = 2,
|
|
||||||
SourceTraderId = 2,
|
|
||||||
SourceTraderName = "Whale 2",
|
|
||||||
MarketQuestion = "Will Fed cut rates?",
|
|
||||||
Outcome = "Yes",
|
|
||||||
Side = "BUY",
|
|
||||||
EntryPrice = 0.80m,
|
|
||||||
CurrentPrice = 0.60m,
|
|
||||||
Size = 50m,
|
|
||||||
AmountUsd = 40m,
|
|
||||||
CurrentValueUsd = 30m
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
web.State.Accounts[1] = acc1;
|
||||||
|
web.State.Accounts[2] = acc2;
|
||||||
|
|
||||||
decimal totalAmount = positions.Sum(p => p.AmountUsd);
|
var open = await web.GetJsonAsync("/api/modules/copytrading/open-trades");
|
||||||
decimal totalUnrealized = positions.Sum(p => p.CurrentValueUsd - p.AmountUsd);
|
|
||||||
|
|
||||||
Assert.Equal(90m, totalAmount);
|
Assert.Equal(2, open.GetProperty("count").GetInt32());
|
||||||
Assert.Equal(10m, totalUnrealized); // (70-50) + (30-40) = 20 - 10 = 10
|
Assert.Equal(90m, open.GetProperty("totalAmount").GetDecimal());
|
||||||
|
Assert.Equal(10m, open.GetProperty("totalUnrealized").GetDecimal()); // (70-50) + (30-40)
|
||||||
|
|
||||||
var p1 = positions[0];
|
var byMarket = open.GetProperty("trades").EnumerateArray()
|
||||||
decimal u1 = p1.CurrentValueUsd - p1.AmountUsd;
|
.ToDictionary(t => t.GetProperty("market").GetString()!);
|
||||||
decimal pct1 = p1.AmountUsd > 0 ? 100m * u1 / p1.AmountUsd : 0m;
|
Assert.Equal(20m, byMarket["Will Trump win in 2024?"].GetProperty("unrealizedPnl").GetDecimal());
|
||||||
Assert.Equal(20m, u1);
|
Assert.Equal(40m, byMarket["Will Trump win in 2024?"].GetProperty("unrealizedPct").GetDecimal());
|
||||||
Assert.Equal(40m, pct1);
|
Assert.Equal(-10m, byMarket["Will Fed cut rates?"].GetProperty("unrealizedPnl").GetDecimal());
|
||||||
|
Assert.Equal(-25m, byMarket["Will Fed cut rates?"].GetProperty("unrealizedPct").GetDecimal());
|
||||||
var p2 = positions[1];
|
|
||||||
decimal u2 = p2.CurrentValueUsd - p2.AmountUsd;
|
|
||||||
decimal pct2 = p2.AmountUsd > 0 ? 100m * u2 / p2.AmountUsd : 0m;
|
|
||||||
Assert.Equal(-10m, u2);
|
|
||||||
Assert.Equal(-25m, pct2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CopyTrading_ClosedTrades_FiltersCorrectly()
|
public async Task CopyTrading_geschlossene_Trades_filtern_nach_Trader_Ergebnis_und_Suche()
|
||||||
{
|
{
|
||||||
var closed = new List<ClosedTrade>
|
await using var web = await StartCopyTradingAsync();
|
||||||
{
|
var log = web.Get<ICopyTradeLogRepository>();
|
||||||
new ClosedTrade
|
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) });
|
||||||
TradeId = 1,
|
log.Insert(new ClosedTrade { TradeId = 2, AccountId = 1, SourceTraderId = 10, TokenId = "b",
|
||||||
AccountId = 1,
|
MarketQuestion = "Solana reaches 300?", Outcome = "No", RealizedPnl = -12m, ClosedAt = DateTime.UtcNow.AddHours(-5) });
|
||||||
SourceTraderId = 10,
|
log.Insert(new ClosedTrade { TradeId = 3, AccountId = 2, SourceTraderId = 20, TokenId = "c",
|
||||||
MarketQuestion = "Bitcoin reaches 100k?",
|
MarketQuestion = "Ethereum reaches 5k?", Outcome = "Yes", RealizedPnl = 50m, ClosedAt = DateTime.UtcNow.AddHours(-1) });
|
||||||
Outcome = "Yes",
|
|
||||||
RealizedPnl = 35m,
|
|
||||||
ClosedAt = DateTime.UtcNow.AddHours(-2)
|
|
||||||
},
|
|
||||||
new ClosedTrade
|
|
||||||
{
|
|
||||||
TradeId = 2,
|
|
||||||
AccountId = 1,
|
|
||||||
SourceTraderId = 10,
|
|
||||||
MarketQuestion = "Solana reaches 300?",
|
|
||||||
Outcome = "No",
|
|
||||||
RealizedPnl = -12m,
|
|
||||||
ClosedAt = DateTime.UtcNow.AddHours(-5)
|
|
||||||
},
|
|
||||||
new ClosedTrade
|
|
||||||
{
|
|
||||||
TradeId = 3,
|
|
||||||
AccountId = 2,
|
|
||||||
SourceTraderId = 20,
|
|
||||||
MarketQuestion = "Ethereum reaches 5k?",
|
|
||||||
Outcome = "Yes",
|
|
||||||
RealizedPnl = 50m,
|
|
||||||
ClosedAt = DateTime.UtcNow.AddHours(-1)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Filter by Trader
|
var byTrader = await web.GetJsonAsync("/api/modules/copytrading/closed-trades?traderId=10");
|
||||||
var trader10Trades = closed.Where(t => t.SourceTraderId == 10).ToList();
|
Assert.Equal(2, byTrader.GetProperty("totalCount").GetInt32());
|
||||||
Assert.Equal(2, trader10Trades.Count);
|
|
||||||
|
|
||||||
// Filter by Result
|
var winners = await web.GetJsonAsync("/api/modules/copytrading/closed-trades?result=Gewinner");
|
||||||
var winners = closed.Where(t => t.RealizedPnl > 0).ToList();
|
Assert.Equal(2, winners.GetProperty("totalCount").GetInt32());
|
||||||
Assert.Equal(2, winners.Count);
|
Assert.Equal(85m, winners.GetProperty("summary").GetProperty("totalPnl").GetDecimal());
|
||||||
decimal totalWinPnl = winners.Sum(w => w.RealizedPnl);
|
|
||||||
Assert.Equal(85m, totalWinPnl);
|
|
||||||
|
|
||||||
// Search Filter
|
var search = await web.GetJsonAsync("/api/modules/copytrading/closed-trades?search=bitcoin");
|
||||||
string search = "Bitcoin";
|
var hit = Assert.Single(search.GetProperty("items").EnumerateArray());
|
||||||
var searchResults = closed.Where(t => t.MarketQuestion.Contains(search, StringComparison.OrdinalIgnoreCase)).ToList();
|
Assert.Equal(1, hit.GetProperty("tradeId").GetInt32());
|
||||||
var matched = Assert.Single(searchResults);
|
|
||||||
Assert.Equal(1, matched.TradeId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CopyTrading_AccountSettings_DefaultValuesAreConsistent()
|
public async Task CopyTrading_Kontoeinstellungen_ohne_Eintrag_liefern_die_Vorgaben()
|
||||||
{
|
{
|
||||||
var settings = new CopyTradingAccountSettings { AccountId = 42 };
|
await using var web = await StartCopyTradingAsync();
|
||||||
|
|
||||||
Assert.Equal(42, settings.AccountId);
|
var settings = await web.GetJsonAsync("/api/modules/copytrading/settings/42");
|
||||||
Assert.Equal(5.0m, settings.PerMarketLimit);
|
|
||||||
Assert.Equal(10.0m, settings.PerMasterLimit);
|
Assert.Equal(42, settings.GetProperty("accountId").GetInt32());
|
||||||
Assert.Equal(0.98m, settings.MaxBuyPrice);
|
Assert.Equal(5.0m, settings.GetProperty("perMarketLimit").GetDecimal());
|
||||||
Assert.Equal(2.0m, settings.MaxPriceDifference);
|
Assert.Equal(10.0m, settings.GetProperty("perMasterLimit").GetDecimal());
|
||||||
Assert.Equal(15.0m, settings.SellFloorPct);
|
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]
|
[Fact]
|
||||||
public void ResolutionFarming_Candidates_MappingAndEdgeFiltering()
|
public async Task ResolutionFarming_Positionen_summieren_den_Einsatz()
|
||||||
{
|
{
|
||||||
var candidates = new List<RfCandidate>
|
await using var web = await StartResolutionFarmingAsync();
|
||||||
{
|
var repo = web.Get<IRfPositionRepository>();
|
||||||
new RfCandidate
|
repo.Upsert(new RfPosition { AccountId = 1, TokenId = "tok-1", MarketQuestion = "Match 1",
|
||||||
{
|
EntryPrice = 0.95m, Size = 100m, AmountUsd = 95m, Status = "Open", OpenedAt = DateTime.UtcNow });
|
||||||
Id = 1,
|
repo.Upsert(new RfPosition { AccountId = 1, TokenId = "tok-2", MarketQuestion = "Match 2",
|
||||||
AccountId = 1,
|
EntryPrice = 0.92m, Size = 50m, AmountUsd = 46m, Status = "Open", OpenedAt = DateTime.UtcNow });
|
||||||
MarketQuestion = "Will candidate A win election?",
|
|
||||||
Outcome = "Yes",
|
|
||||||
Category = "Politics",
|
|
||||||
Ask = 0.94m,
|
|
||||||
NetEdgePct = 2.5m,
|
|
||||||
Accepted = true,
|
|
||||||
RejectReason = ""
|
|
||||||
},
|
|
||||||
new RfCandidate
|
|
||||||
{
|
|
||||||
Id = 2,
|
|
||||||
AccountId = 1,
|
|
||||||
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 acceptedOnly = candidates.Where(c => c.Accepted).ToList();
|
var result = await web.GetJsonAsync("/api/modules/resolutionfarming/positions?accountId=1");
|
||||||
var accepted = Assert.Single(acceptedOnly);
|
|
||||||
Assert.Equal("Politics", accepted.Category);
|
|
||||||
Assert.True(accepted.NetEdgePct >= 1.5m);
|
|
||||||
|
|
||||||
var rejected = candidates.First(c => !c.Accepted);
|
Assert.Equal(2, result.GetProperty("count").GetInt32());
|
||||||
Assert.Equal("Fee too high for edge", rejected.RejectReason);
|
Assert.Equal(141m, result.GetProperty("totalAmount").GetDecimal());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ResolutionFarming_Positions_SummaryMetrics()
|
public async Task ResolutionFarming_Historie_rechnet_PnL_und_Trefferquote()
|
||||||
{
|
{
|
||||||
var positions = new List<RfPosition>
|
await using var web = await StartResolutionFarmingAsync();
|
||||||
{
|
var repo = web.Get<IRfClosedTradeRepository>();
|
||||||
new RfPosition
|
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 });
|
||||||
AccountId = 1,
|
repo.Insert(new RfClosedTrade { AccountId = 1, TokenId = "c", MarketQuestion = "Match C", RealizedPnl = -20.00m, RedeemStatus = "None", ClosedAt = DateTime.UtcNow });
|
||||||
TokenId = "tok-1",
|
|
||||||
MarketQuestion = "Match 1",
|
|
||||||
EntryPrice = 0.95m,
|
|
||||||
Size = 100m,
|
|
||||||
AmountUsd = 95m,
|
|
||||||
Status = "Open"
|
|
||||||
},
|
|
||||||
new RfPosition
|
|
||||||
{
|
|
||||||
AccountId = 1,
|
|
||||||
TokenId = "tok-2",
|
|
||||||
MarketQuestion = "Match 2",
|
|
||||||
EntryPrice = 0.92m,
|
|
||||||
Size = 50m,
|
|
||||||
AmountUsd = 46m,
|
|
||||||
Status = "Open"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
decimal totalInvested = positions.Sum(p => p.AmountUsd);
|
var result = await web.GetJsonAsync("/api/modules/resolutionfarming/history?accountId=1");
|
||||||
Assert.Equal(141m, totalInvested);
|
var summary = result.GetProperty("summary");
|
||||||
Assert.Equal(2, positions.Count);
|
|
||||||
|
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]
|
[Fact]
|
||||||
public void ResolutionFarming_History_CalculatesPnlAndWinRate()
|
public async Task ResolutionFarming_Einstellungen_ohne_Eintrag_liefern_die_Vorgaben()
|
||||||
{
|
{
|
||||||
var history = new List<RfClosedTrade>
|
await using var web = await StartResolutionFarmingAsync();
|
||||||
{
|
|
||||||
new RfClosedTrade
|
|
||||||
{
|
|
||||||
Id = 101,
|
|
||||||
AccountId = 1,
|
|
||||||
MarketQuestion = "Match A",
|
|
||||||
RealizedPnl = 4.50m,
|
|
||||||
RedeemStatus = "Redeemed"
|
|
||||||
},
|
|
||||||
new RfClosedTrade
|
|
||||||
{
|
|
||||||
Id = 102,
|
|
||||||
AccountId = 1,
|
|
||||||
MarketQuestion = "Match B",
|
|
||||||
RealizedPnl = 3.80m,
|
|
||||||
RedeemStatus = "Redeemed"
|
|
||||||
},
|
|
||||||
new RfClosedTrade
|
|
||||||
{
|
|
||||||
Id = 103,
|
|
||||||
AccountId = 1,
|
|
||||||
MarketQuestion = "Match C",
|
|
||||||
RealizedPnl = -20.00m,
|
|
||||||
RedeemStatus = "None"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
decimal totalPnl = history.Sum(t => t.RealizedPnl);
|
var settings = await web.GetJsonAsync("/api/modules/resolutionfarming/settings/5");
|
||||||
int wins = history.Count(t => t.RealizedPnl > 0);
|
|
||||||
decimal winrate = 100m * wins / history.Count;
|
|
||||||
|
|
||||||
Assert.Equal(-11.70m, totalPnl);
|
Assert.Equal(5, settings.GetProperty("accountId").GetInt32());
|
||||||
Assert.Equal(2, wins);
|
Assert.False(settings.GetProperty("enabled").GetBoolean());
|
||||||
Assert.Equal(66.7m, Math.Round(winrate, 1));
|
Assert.Equal(0.90m, settings.GetProperty("minPrice").GetDecimal());
|
||||||
}
|
Assert.Equal(0.98m, settings.GetProperty("maxPrice").GetDecimal());
|
||||||
|
Assert.Equal(48, settings.GetProperty("maxHoursToResolution").GetInt32());
|
||||||
[Fact]
|
Assert.Contains("Sports", settings.GetProperty("categoryWhitelistCsv").GetString());
|
||||||
public void ResolutionFarming_Settings_DefaultValuesValid()
|
|
||||||
{
|
|
||||||
var settings = new RfSettings { AccountId = 5 };
|
|
||||||
|
|
||||||
Assert.Equal(5, settings.AccountId);
|
|
||||||
Assert.False(settings.Enabled);
|
|
||||||
Assert.Equal(0.90m, settings.MinPrice);
|
|
||||||
Assert.Equal(0.98m, settings.MaxPrice);
|
|
||||||
Assert.Equal(48, settings.MaxHoursToResolution);
|
|
||||||
Assert.Equal(1.5m, settings.MinEdgePct);
|
|
||||||
Assert.Equal(25m, settings.MaxPerMarketUsd);
|
|
||||||
Assert.Contains("Sports", settings.CategoryWhitelistCsv);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,64 +1,130 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using PolyTrader.Core.Modularity;
|
||||||
using PolyTrader.Web.Configuration;
|
using PolyTrader.Web.Configuration;
|
||||||
using PolyTrader.Web.Endpoints;
|
using PolyTrader.Web.Security;
|
||||||
|
using PolyTraderSharp.Models;
|
||||||
using PolyTraderSharp.Services;
|
using PolyTraderSharp.Services;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace PolyTrader.Tests.WebUi
|
namespace PolyTrader.Tests.WebUi
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Zugangssperre, Module, Jobs und Supervisor ueber echte HTTP-Aufrufe (#568).
|
||||||
|
/// Ersetzt zwei Tests, die nicht fehlschlagen konnten: einer pruefte, ob ein Array seine eigenen
|
||||||
|
/// Elemente enthaelt, der andere die Portbegrenzung an einer im Test nachgebauten Lambda.
|
||||||
|
/// </summary>
|
||||||
public class ModulesAndSettingsEndpointsTests
|
public class ModulesAndSettingsEndpointsTests
|
||||||
{
|
{
|
||||||
[Fact]
|
private sealed class FakeModule : IPolyTraderModule
|
||||||
public void Modules_KnownModules_IncludesAllCoreStrategyAndAnalysisModules()
|
|
||||||
{
|
{
|
||||||
var expected = new[]
|
public FakeModule(string name) => Name = name;
|
||||||
{
|
public string Name { get; }
|
||||||
"CopyTrading",
|
public string DbPrefix => "fake_";
|
||||||
"ResolutionFarming",
|
public void RegisterServices(IServiceCollection services, IConfiguration configuration) { }
|
||||||
"Supervisor",
|
public void RegisterUi(IModuleUiHost host, IServiceProvider services) { }
|
||||||
"Accounting"
|
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
};
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
|
||||||
foreach (var mod in expected)
|
|
||||||
{
|
|
||||||
Assert.Contains(mod, expected);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Zugangssperre ---------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Api_ohne_Token_wird_abgewiesen()
|
||||||
|
{
|
||||||
|
await using var web = await WebUiTestHost.StartAsync();
|
||||||
|
|
||||||
|
using var res = await web.Anonymous.GetAsync("/api/modules");
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.Unauthorized, res.StatusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Settings_UpdateRequest_ValidationAndPortClamping()
|
public async Task Api_mit_falschem_Token_wird_abgewiesen()
|
||||||
{
|
{
|
||||||
var validReq = new SettingsEndpoints.UpdateWebUiSettingsRequest(
|
await using var web = await WebUiTestHost.StartAsync();
|
||||||
Enabled: true,
|
|
||||||
Port: 8080,
|
|
||||||
LocalOnly: false,
|
|
||||||
AuthToken: " custom_token_123 "
|
|
||||||
);
|
|
||||||
|
|
||||||
Assert.True(validReq.Enabled);
|
using var req = new HttpRequestMessage(HttpMethod.Get, "/api/modules");
|
||||||
Assert.Equal(8080, validReq.Port);
|
req.Headers.Add(TokenAuthMiddleware.TokenHeaderName, "falsch");
|
||||||
Assert.False(validReq.LocalOnly);
|
using var res = await web.Anonymous.SendAsync(req);
|
||||||
Assert.Equal("custom_token_123", validReq.AuthToken?.Trim());
|
|
||||||
|
|
||||||
// Port boundary handling
|
Assert.Equal(HttpStatusCode.Unauthorized, res.StatusCode);
|
||||||
int clampPort(int p) => p > 0 && p <= 65535 ? p : 6001;
|
|
||||||
|
|
||||||
Assert.Equal(8080, clampPort(validReq.Port));
|
|
||||||
Assert.Equal(6001, clampPort(0));
|
|
||||||
Assert.Equal(6001, clampPort(-5));
|
|
||||||
Assert.Equal(6001, clampPort(70000));
|
|
||||||
Assert.Equal(65535, clampPort(65535));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Ohne_konfiguriertes_Token_ist_alles_gesperrt()
|
||||||
|
{
|
||||||
|
await using var web = await WebUiTestHost.StartAsync(configureOptions: o => o.AuthToken = "");
|
||||||
|
|
||||||
|
using var res = await web.Client.GetAsync("/api/modules");
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.Unauthorized, res.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Module ----------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Module_zeigt_alle_bekannten_und_markiert_die_geladenen()
|
||||||
|
{
|
||||||
|
await using var web = await WebUiTestHost.StartAsync(s =>
|
||||||
|
s.AddSingleton<IPolyTraderModule>(new FakeModule("CopyTrading")));
|
||||||
|
|
||||||
|
var modules = await web.GetJsonAsync("/api/modules");
|
||||||
|
|
||||||
|
var byName = modules.EnumerateArray().ToDictionary(
|
||||||
|
m => m.GetProperty("name").GetString()!,
|
||||||
|
m => m.GetProperty("isActive").GetBoolean());
|
||||||
|
|
||||||
|
Assert.Equal(new[] { "Accounting", "CopyTrading", "ResolutionFarming", "Supervisor" },
|
||||||
|
byName.Keys.OrderBy(k => k).ToArray());
|
||||||
|
Assert.True(byName["CopyTrading"]);
|
||||||
|
Assert.False(byName["Supervisor"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Jobs ------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Job_umschalten_aendert_den_Zustand_und_unbekannter_Job_liefert_404()
|
||||||
|
{
|
||||||
|
await using var web = await WebUiTestHost.StartAsync();
|
||||||
|
var row = new JobStatusRow { JobName = "Trader Analytics", IsEnabled = true };
|
||||||
|
web.Jobs.Jobs.Add(row);
|
||||||
|
|
||||||
|
var (status, body) = await web.PostJsonAsync("/api/jobs/Trader%20Analytics/toggle");
|
||||||
|
Assert.Equal(200, status);
|
||||||
|
Assert.False(body.GetProperty("isEnabled").GetBoolean());
|
||||||
|
Assert.False(row.IsEnabled);
|
||||||
|
|
||||||
|
var (unknown, _) = await web.PostJsonAsync("/api/jobs/GibtEsNicht/toggle");
|
||||||
|
Assert.Equal(404, unknown);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Supervisor ------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Supervisor_lehnt_leere_Frage_ab_bevor_ein_Modell_gefragt_wird()
|
||||||
|
{
|
||||||
|
await using var web = await WebUiTestHost.StartAsync();
|
||||||
|
|
||||||
|
var (status, _) = await web.PostJsonAsync("/api/modules/supervisor/ask", new { question = " " });
|
||||||
|
|
||||||
|
Assert.Equal(400, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Hilfsfunktionen, die schon echten Code pruefen ------------------
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Settings_GenerateSecureToken_ProducesCryptographicallyStrongString()
|
public void Settings_GenerateSecureToken_ProducesCryptographicallyStrongString()
|
||||||
{
|
{
|
||||||
string token1 = WebUiConfigManager.GenerateSecureToken();
|
string token1 = WebUiConfigManager.GenerateSecureToken();
|
||||||
string token2 = WebUiConfigManager.GenerateSecureToken();
|
string token2 = WebUiConfigManager.GenerateSecureToken();
|
||||||
|
|
||||||
Assert.NotNull(token1);
|
|
||||||
Assert.NotNull(token2);
|
|
||||||
Assert.NotEmpty(token1);
|
Assert.NotEmpty(token1);
|
||||||
Assert.NotEqual(token1, token2);
|
Assert.NotEqual(token1, token2);
|
||||||
Assert.True(token1.Length >= 32);
|
Assert.True(token1.Length >= 32);
|
||||||
@@ -71,11 +137,8 @@ namespace PolyTrader.Tests.WebUi
|
|||||||
logger.Info("Testnachricht 1 vor 5 Minuten");
|
logger.Info("Testnachricht 1 vor 5 Minuten");
|
||||||
logger.Warning("Testnachricht 2");
|
logger.Warning("Testnachricht 2");
|
||||||
|
|
||||||
var history = logger.GetHistory(TimeSpan.FromMinutes(10));
|
var recentMsgs = logger.GetHistory(TimeSpan.FromMinutes(10)).Select(h => h.Message).ToList();
|
||||||
Assert.NotNull(history);
|
|
||||||
Assert.True(history.Count >= 2);
|
|
||||||
|
|
||||||
var recentMsgs = history.Select(h => h.Message).ToList();
|
|
||||||
Assert.Contains("Testnachricht 1 vor 5 Minuten", recentMsgs);
|
Assert.Contains("Testnachricht 1 vor 5 Minuten", recentMsgs);
|
||||||
Assert.Contains("Testnachricht 2", recentMsgs);
|
Assert.Contains("Testnachricht 2", recentMsgs);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Startet das WebUI in einem Testserver und spricht es ueber echtes HTTP an (#568).
|
||||||
|
///
|
||||||
|
/// <para>Die Anfragekette kommt aus <see cref="WebUiHostingExtensions.UsePolyTraderWebPipeline"/>,
|
||||||
|
/// 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.</para>
|
||||||
|
///
|
||||||
|
/// <para>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.</para>
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class WebUiTestHost : IAsyncDisposable
|
||||||
|
{
|
||||||
|
public const string Token = "webui-test-token-7f3a9c1e5b2d4f6a8c0e";
|
||||||
|
|
||||||
|
private readonly IHost _host;
|
||||||
|
|
||||||
|
public TradingState State { get; }
|
||||||
|
public CopyTradingState CopyState { get; }
|
||||||
|
public TerminalLogger Logger { get; }
|
||||||
|
public JobManager Jobs { get; }
|
||||||
|
public WebUiOptions WebOptions { get; }
|
||||||
|
|
||||||
|
/// <summary>Mit gueltigem Token.</summary>
|
||||||
|
public HttpClient Client { get; }
|
||||||
|
|
||||||
|
/// <summary>Ohne Token - fuer die Pruefung der Zugangssperre.</summary>
|
||||||
|
public HttpClient Anonymous { get; }
|
||||||
|
|
||||||
|
private WebUiTestHost(IHost host, TradingState state, CopyTradingState copyState,
|
||||||
|
TerminalLogger logger, JobManager jobs, WebUiOptions options)
|
||||||
|
{
|
||||||
|
_host = host;
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<WebUiTestHost> StartAsync(
|
||||||
|
Action<IServiceCollection>? configureServices = null,
|
||||||
|
Action<WebUiOptions>? 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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
configureServices?.Invoke(services);
|
||||||
|
});
|
||||||
|
web.Configure(app => app.UsePolyTraderWebPipeline());
|
||||||
|
})
|
||||||
|
.StartAsync();
|
||||||
|
|
||||||
|
return new WebUiTestHost(host, state, copyState, logger, jobs, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>CopyTrading-Repositories auf einer eigenen InMemory-Datenbank.</summary>
|
||||||
|
public static void AddCopyTradingStore(IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddSingleton<IDbContextFactory<CopyTradingDbContext>>(
|
||||||
|
new InMemoryContextFactory<CopyTradingDbContext>(o => new CopyTradingDbContext(o)));
|
||||||
|
services.AddSingleton<ITrackedTraderRepository, EfTrackedTraderRepository>();
|
||||||
|
services.AddSingleton<ICopyTradeLogRepository, EfCopyTradeLogRepository>();
|
||||||
|
services.AddSingleton<ICopyTradingAccountSettingsRepository, EfCopyTradingAccountSettingsRepository>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>ResolutionFarming-Repositories auf einer eigenen InMemory-Datenbank.</summary>
|
||||||
|
public static void AddResolutionFarmingStore(IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddSingleton<IDbContextFactory<ResolutionFarmingDbContext>>(
|
||||||
|
new InMemoryContextFactory<ResolutionFarmingDbContext>(o => new ResolutionFarmingDbContext(o)));
|
||||||
|
services.AddSingleton<IRfSettingsRepository, EfRfSettingsRepository>();
|
||||||
|
services.AddSingleton<IRfCandidateRepository, EfRfCandidateRepository>();
|
||||||
|
services.AddSingleton<IRfPositionRepository, EfRfPositionRepository>();
|
||||||
|
services.AddSingleton<IRfClosedTradeRepository, EfRfClosedTradeRepository>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public T Get<T>() where T : notnull => _host.Services.GetRequiredService<T>();
|
||||||
|
|
||||||
|
/// <summary>GET mit Token; wirft mit Statuscode und Antworttext, wenn die Route scheitert.</summary>
|
||||||
|
public async Task<JsonElement> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>POST mit Token; liefert Status und Antwort, ohne bei Fehlern zu werfen.</summary>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user