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:
co-authored by
Claude Opus 4.8
parent
e27a659529
commit
ca16cd8a91
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user