diff --git a/PolyTraderSharp.sln b/PolyTraderSharp.sln index 7f53f2e..5a96c73 100644 --- a/PolyTraderSharp.sln +++ b/PolyTraderSharp.sln @@ -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} diff --git a/tests/PolyTrader.Tests/AccountRepositoryTests.cs b/tests/PolyTrader.Tests/AccountRepositoryTests.cs new file mode 100644 index 0000000..fdf1640 --- /dev/null +++ b/tests/PolyTrader.Tests/AccountRepositoryTests.cs @@ -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(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()); + } + } +} diff --git a/tests/PolyTrader.Tests/AccountSettingsRepositoryTests.cs b/tests/PolyTrader.Tests/AccountSettingsRepositoryTests.cs new file mode 100644 index 0000000..69256fc --- /dev/null +++ b/tests/PolyTrader.Tests/AccountSettingsRepositoryTests.cs @@ -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(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()); + } + } +} diff --git a/tests/PolyTrader.Tests/CopyTradeLogRepositoryTests.cs b/tests/PolyTrader.Tests/CopyTradeLogRepositoryTests.cs new file mode 100644 index 0000000..d884c24 --- /dev/null +++ b/tests/PolyTrader.Tests/CopyTradeLogRepositoryTests.cs @@ -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(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); + } + } +} diff --git a/tests/PolyTrader.Tests/MasterTraderHistoryRepositoryTests.cs b/tests/PolyTrader.Tests/MasterTraderHistoryRepositoryTests.cs new file mode 100644 index 0000000..c31742d --- /dev/null +++ b/tests/PolyTrader.Tests/MasterTraderHistoryRepositoryTests.cs @@ -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(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); + } + } +} diff --git a/tests/PolyTrader.Tests/PolyTrader.Tests.csproj b/tests/PolyTrader.Tests/PolyTrader.Tests.csproj new file mode 100644 index 0000000..84d9d17 --- /dev/null +++ b/tests/PolyTrader.Tests/PolyTrader.Tests.csproj @@ -0,0 +1,24 @@ + + + + net8.0-windows + enable + enable + false + + true + + + + + + + + + + + + + + + diff --git a/tests/PolyTrader.Tests/TestSupport/InMemoryContextFactory.cs b/tests/PolyTrader.Tests/TestSupport/InMemoryContextFactory.cs new file mode 100644 index 0000000..2fadf69 --- /dev/null +++ b/tests/PolyTrader.Tests/TestSupport/InMemoryContextFactory.cs @@ -0,0 +1,29 @@ +using System; +using Microsoft.EntityFrameworkCore; + +namespace PolyTrader.Tests.TestSupport +{ + /// + /// Test-Implementierung von 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). + /// + public sealed class InMemoryContextFactory : IDbContextFactory + where TContext : DbContext + { + private readonly Func, TContext> _ctor; + private readonly DbContextOptions _options; + + public InMemoryContextFactory(Func, TContext> ctor) + { + _ctor = ctor; + _options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .EnableSensitiveDataLogging() + .Options; + } + + public TContext CreateDbContext() => _ctor(_options); + } +} diff --git a/tests/PolyTrader.Tests/TrackedTraderRepositoryTests.cs b/tests/PolyTrader.Tests/TrackedTraderRepositoryTests.cs new file mode 100644 index 0000000..fbd1d95 --- /dev/null +++ b/tests/PolyTrader.Tests/TrackedTraderRepositoryTests.cs @@ -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(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 { 1, 3 } }); + + var loaded = repo.GetAll().Single(); + Assert.Equal(new HashSet { 1, 3 }, loaded.AssignedAccountIds); + } + + [Fact] + public void AssignedAccountIds_update_replaces_the_set() + { + var repo = NewRepo(); + repo.Upsert(new TrackedTrader { Id = 7, AssignedAccountIds = new HashSet { 1, 3 } }); + repo.Upsert(new TrackedTrader { Id = 7, AssignedAccountIds = new HashSet { 2 } }); + + var loaded = repo.GetAll().Single(); + Assert.Equal(new HashSet { 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); + } + } +}