Phase 7: Testprojekt (xUnit) + Repository-Tests

- Neues Projekt tests/PolyTrader.Tests (xUnit, EF Core InMemory-Provider),
  zur Solution hinzugefügt.
- InMemoryContextFactory<TContext>: Test-IDbContextFactory (isolierte In-Memory-DB
  je Test, geteilt über alle Kontexte einer Factory – wie im echten Betrieb).
- 18 Tests über die EF-Repos:
  - TrackedTrader: Upsert insert/update ohne Duplikat, Delete,
    AssignedAccountIds-JSON-Round-Trip (Value-Converter) + Set-Ersetzung.
  - CopyTradingAccountSettings: Upsert/Get (Dezimalwerte), Get-null, Delete.
  - CopyTradeLog: Insert/Find, Exists-Dedup (Account+Token), Predikat-Filter.
  - MasterTraderHistory: Exists-Zeitfenster, GetByTraderSince-Cutoff.
  - Core Accounts: Upsert insert/update, Delete.
- Alle 18 grün.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-07-06 11:24:33 +02:00
co-authored by Claude Opus 4.8
parent e27a659529
commit ca16cd8a91
8 changed files with 357 additions and 0 deletions
+17
View File
@@ -17,6 +17,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PolyTrader.Core", "src\Poly
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PolyTrader.Modules.CopyTrading", "src\PolyTrader.Modules.CopyTrading\PolyTrader.Modules.CopyTrading.csproj", "{FA3FC57B-EA9D-4703-B643-2874C8C0461E}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PolyTrader.Tests", "tests\PolyTrader.Tests\PolyTrader.Tests.csproj", "{E361C601-CC50-409F-8298-FD753DB4F6FF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -75,6 +79,18 @@ Global
{FA3FC57B-EA9D-4703-B643-2874C8C0461E}.Release|x64.Build.0 = Release|Any CPU
{FA3FC57B-EA9D-4703-B643-2874C8C0461E}.Release|x86.ActiveCfg = Release|Any CPU
{FA3FC57B-EA9D-4703-B643-2874C8C0461E}.Release|x86.Build.0 = Release|Any CPU
{E361C601-CC50-409F-8298-FD753DB4F6FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E361C601-CC50-409F-8298-FD753DB4F6FF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E361C601-CC50-409F-8298-FD753DB4F6FF}.Debug|x64.ActiveCfg = Debug|Any CPU
{E361C601-CC50-409F-8298-FD753DB4F6FF}.Debug|x64.Build.0 = Debug|Any CPU
{E361C601-CC50-409F-8298-FD753DB4F6FF}.Debug|x86.ActiveCfg = Debug|Any CPU
{E361C601-CC50-409F-8298-FD753DB4F6FF}.Debug|x86.Build.0 = Debug|Any CPU
{E361C601-CC50-409F-8298-FD753DB4F6FF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E361C601-CC50-409F-8298-FD753DB4F6FF}.Release|Any CPU.Build.0 = Release|Any CPU
{E361C601-CC50-409F-8298-FD753DB4F6FF}.Release|x64.ActiveCfg = Release|Any CPU
{E361C601-CC50-409F-8298-FD753DB4F6FF}.Release|x64.Build.0 = Release|Any CPU
{E361C601-CC50-409F-8298-FD753DB4F6FF}.Release|x86.ActiveCfg = Release|Any CPU
{E361C601-CC50-409F-8298-FD753DB4F6FF}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -84,6 +100,7 @@ Global
{EFAE2853-E52F-4163-8266-3859C7748B85} = {35072DD7-6FE3-A86A-51D9-DD886E81202C}
{3502A702-FA60-456E-BF23-05EAD02465E6} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{FA3FC57B-EA9D-4703-B643-2874C8C0461E} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{E361C601-CC50-409F-8298-FD753DB4F6FF} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {60AA6BCF-B17E-4D52-A290-14154A3E97CF}
@@ -0,0 +1,53 @@
using System.Linq;
using PolyTrader.Core.Persistence.Ef;
using PolyTrader.Tests.TestSupport;
using PolyTraderSharp.Models;
using Xunit;
namespace PolyTrader.Tests
{
public class AccountRepositoryTests
{
private static EfAccountRepository NewRepo() =>
new(new InMemoryContextFactory<CoreDbContext>(o => new CoreDbContext(o)));
[Fact]
public void Upsert_inserts_and_reads_back_account()
{
var repo = NewRepo();
repo.Upsert(new AccountState
{
AccountId = 1,
Name = "Richard Test",
WalletAddress = "0xabc",
IsDemo = false,
TotalBalance = 64.763626m
});
var acc = repo.GetAll().Single();
Assert.Equal("Richard Test", acc.Name);
Assert.Equal(64.763626m, acc.TotalBalance);
}
[Fact]
public void Upsert_updates_existing_without_duplicating()
{
var repo = NewRepo();
repo.Upsert(new AccountState { AccountId = 1, Name = "A" });
repo.Upsert(new AccountState { AccountId = 1, Name = "B" });
var all = repo.GetAll();
Assert.Single(all);
Assert.Equal("B", all[0].Name);
}
[Fact]
public void Delete_removes_account()
{
var repo = NewRepo();
repo.Upsert(new AccountState { AccountId = 1 });
repo.Delete(1);
Assert.Empty(repo.GetAll());
}
}
}
@@ -0,0 +1,61 @@
using PolyTrader.Modules.CopyTrading.Persistence.Ef;
using PolyTrader.Tests.TestSupport;
using PolyTraderSharp.Models;
using Xunit;
namespace PolyTrader.Tests
{
public class AccountSettingsRepositoryTests
{
private static EfCopyTradingAccountSettingsRepository NewRepo() =>
new(new InMemoryContextFactory<CopyTradingDbContext>(o => new CopyTradingDbContext(o)));
[Fact]
public void Upsert_and_Get_persists_decimal_values()
{
var repo = NewRepo();
repo.Upsert(new CopyTradingAccountSettings
{
AccountId = 1,
PerMarketLimit = 0.5m,
PerMasterLimit = 15m,
MaxBuyPrice = 98m,
PreRedeemLimit = 99.5m
});
var s = repo.Get(1);
Assert.NotNull(s);
Assert.Equal(0.5m, s!.PerMarketLimit);
Assert.Equal(15m, s.PerMasterLimit);
Assert.Equal(98m, s.MaxBuyPrice);
Assert.Equal(99.5m, s.PreRedeemLimit);
}
[Fact]
public void Get_returns_null_when_missing()
{
var repo = NewRepo();
Assert.Null(repo.Get(42));
}
[Fact]
public void Upsert_updates_existing_without_duplicating()
{
var repo = NewRepo();
repo.Upsert(new CopyTradingAccountSettings { AccountId = 1, PerMarketLimit = 1m });
repo.Upsert(new CopyTradingAccountSettings { AccountId = 1, PerMarketLimit = 2m });
Assert.Single(repo.GetAll());
Assert.Equal(2m, repo.Get(1)!.PerMarketLimit);
}
[Fact]
public void Delete_removes_settings()
{
var repo = NewRepo();
repo.Upsert(new CopyTradingAccountSettings { AccountId = 1 });
repo.Delete(1);
Assert.Empty(repo.GetAll());
}
}
}
@@ -0,0 +1,47 @@
using System.Linq;
using PolyTrader.Modules.CopyTrading.Persistence.Ef;
using PolyTrader.Tests.TestSupport;
using PolyTraderSharp.Models;
using Xunit;
namespace PolyTrader.Tests
{
public class CopyTradeLogRepositoryTests
{
private static EfCopyTradeLogRepository NewRepo() =>
new(new InMemoryContextFactory<CopyTradingDbContext>(o => new CopyTradingDbContext(o)));
[Fact]
public void Insert_and_Find_returns_all()
{
var repo = NewRepo();
repo.Insert(new ClosedTrade { TradeId = 1, AccountId = 1, TokenId = "a" });
repo.Insert(new ClosedTrade { TradeId = 2, AccountId = 1, TokenId = "b" });
Assert.Equal(2, repo.Find(_ => true).Count);
}
[Fact]
public void Exists_is_true_for_matching_account_and_token()
{
var repo = NewRepo();
repo.Insert(new ClosedTrade { TradeId = 1, AccountId = 5, TokenId = "tok" });
Assert.True(repo.Exists(5, "tok"));
Assert.False(repo.Exists(5, "other"));
Assert.False(repo.Exists(9, "tok"));
}
[Fact]
public void Find_applies_predicate()
{
var repo = NewRepo();
repo.Insert(new ClosedTrade { TradeId = 1, AccountId = 1, RealizedPnl = 10m });
repo.Insert(new ClosedTrade { TradeId = 2, AccountId = 1, RealizedPnl = -3m });
var winners = repo.Find(t => t.RealizedPnl > 0);
Assert.Single(winners);
Assert.Equal(1, winners[0].TradeId);
}
}
}
@@ -0,0 +1,52 @@
using System;
using System.Linq;
using PolyTrader.Modules.CopyTrading.Persistence.Ef;
using PolyTrader.Tests.TestSupport;
using PolyTraderSharp.Models;
using Xunit;
namespace PolyTrader.Tests
{
public class MasterTraderHistoryRepositoryTests
{
private static EfMasterTraderHistoryRepository NewRepo() =>
new(new InMemoryContextFactory<CopyTradingDbContext>(o => new CopyTradingDbContext(o)));
[Fact]
public void Exists_is_true_within_time_window()
{
var repo = NewRepo();
var t = new DateTime(2026, 7, 1, 12, 0, 0, DateTimeKind.Utc);
repo.Insert(new MasterTraderHistoryRecord { TraderId = 1, TokenId = "tok", ClosedAt = t, RealizedPnl = 5m });
Assert.True(repo.Exists(1, "tok", t.AddSeconds(-2), t.AddSeconds(2)));
}
[Fact]
public void Exists_is_false_outside_window_or_wrong_trader()
{
var repo = NewRepo();
var t = new DateTime(2026, 7, 1, 12, 0, 0, DateTimeKind.Utc);
repo.Insert(new MasterTraderHistoryRecord { TraderId = 1, TokenId = "tok", ClosedAt = t });
Assert.False(repo.Exists(1, "tok", t.AddMinutes(1), t.AddMinutes(2)));
Assert.False(repo.Exists(2, "tok", t.AddSeconds(-2), t.AddSeconds(2)));
Assert.False(repo.Exists(1, "nope", t.AddSeconds(-2), t.AddSeconds(2)));
}
[Fact]
public void GetByTraderSince_filters_by_trader_and_cutoff()
{
var repo = NewRepo();
var now = new DateTime(2026, 7, 8, 0, 0, 0, DateTimeKind.Utc);
repo.Insert(new MasterTraderHistoryRecord { TraderId = 1, ClosedAt = now.AddDays(-1) }); // in
repo.Insert(new MasterTraderHistoryRecord { TraderId = 1, ClosedAt = now.AddDays(-10) }); // too old
repo.Insert(new MasterTraderHistoryRecord { TraderId = 2, ClosedAt = now }); // other trader
var result = repo.GetByTraderSince(1, now.AddDays(-7));
Assert.Single(result);
Assert.Equal(1, result[0].TraderId);
}
}
}
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
<!-- Core/Module sind net8.0-windows (WinForms); damit deren Assemblies sauber laden. -->
<UseWindowsForms>true</UseWindowsForms>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.11" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\PolyTrader.Core\PolyTrader.Core.csproj" />
<ProjectReference Include="..\..\src\PolyTrader.Modules.CopyTrading\PolyTrader.Modules.CopyTrading.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,29 @@
using System;
using Microsoft.EntityFrameworkCore;
namespace PolyTrader.Tests.TestSupport
{
/// <summary>
/// Test-Implementierung von <see cref="IDbContextFactory{TContext}"/> auf Basis des
/// EF-Core-InMemory-Providers. Jede Factory-Instanz nutzt eine eigene (per GUID benannte)
/// In-Memory-DB, sodass Tests voneinander isoliert sind; alle vom selben Factory erzeugten
/// Kontexte teilen sich die gleiche Datenbank (wie im echten Betrieb der DbContextFactory).
/// </summary>
public sealed class InMemoryContextFactory<TContext> : IDbContextFactory<TContext>
where TContext : DbContext
{
private readonly Func<DbContextOptions<TContext>, TContext> _ctor;
private readonly DbContextOptions<TContext> _options;
public InMemoryContextFactory(Func<DbContextOptions<TContext>, TContext> ctor)
{
_ctor = ctor;
_options = new DbContextOptionsBuilder<TContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.EnableSensitiveDataLogging()
.Options;
}
public TContext CreateDbContext() => _ctor(_options);
}
}
@@ -0,0 +1,74 @@
using System.Collections.Generic;
using System.Linq;
using PolyTrader.Modules.CopyTrading.Persistence.Ef;
using PolyTrader.Tests.TestSupport;
using PolyTraderSharp.Models;
using Xunit;
namespace PolyTrader.Tests
{
public class TrackedTraderRepositoryTests
{
private static EfTrackedTraderRepository NewRepo() =>
new(new InMemoryContextFactory<CopyTradingDbContext>(o => new CopyTradingDbContext(o)));
[Fact]
public void Upsert_inserts_new_trader()
{
var repo = NewRepo();
repo.Upsert(new TrackedTrader { Id = 1, DisplayName = "Alice" });
var all = repo.GetAll();
Assert.Single(all);
Assert.Equal("Alice", all[0].DisplayName);
}
[Fact]
public void Upsert_updates_existing_without_duplicating()
{
var repo = NewRepo();
repo.Upsert(new TrackedTrader { Id = 1, DisplayName = "Alice", IsActive = true });
repo.Upsert(new TrackedTrader { Id = 1, DisplayName = "Bob", IsActive = false });
var all = repo.GetAll();
Assert.Single(all);
Assert.Equal("Bob", all[0].DisplayName);
Assert.False(all[0].IsActive);
}
[Fact]
public void AssignedAccountIds_round_trips_through_json_conversion()
{
var repo = NewRepo();
repo.Upsert(new TrackedTrader { Id = 7, DisplayName = "X", AssignedAccountIds = new HashSet<int> { 1, 3 } });
var loaded = repo.GetAll().Single();
Assert.Equal(new HashSet<int> { 1, 3 }, loaded.AssignedAccountIds);
}
[Fact]
public void AssignedAccountIds_update_replaces_the_set()
{
var repo = NewRepo();
repo.Upsert(new TrackedTrader { Id = 7, AssignedAccountIds = new HashSet<int> { 1, 3 } });
repo.Upsert(new TrackedTrader { Id = 7, AssignedAccountIds = new HashSet<int> { 2 } });
var loaded = repo.GetAll().Single();
Assert.Equal(new HashSet<int> { 2 }, loaded.AssignedAccountIds);
}
[Fact]
public void Delete_removes_the_trader()
{
var repo = NewRepo();
repo.Upsert(new TrackedTrader { Id = 1 });
repo.Upsert(new TrackedTrader { Id = 2 });
repo.Delete(1);
var all = repo.GetAll();
Assert.Single(all);
Assert.Equal(2, all[0].Id);
}
}
}