Phase 7: Mapping-Randfälle + Parser-/Repo-Randfälle (78 Tests)
- DbContextModelTests: feinkörnige Modell-Invarianten - Dezimal-Präzision (18,6) auf Geldfeldern (Core + Settings), MaxLength auf Strings, manuell gesetzte Keys (ValueGeneratedNever), AssignedAccountIds hat Value-Converter + text-Spalte, MarketData JSON-Spalten = text, Indizes (TradeRecord.ClosedAt, MasterTraderHistory TraderId/ClosedAt). (GetColumnType() wirft unter InMemory -> Spaltentyp via Relational:ColumnType- Annotation ausgelesen.) - MongoExportParserTests erweitert: numerische statt String-Dezimale, unbekannte Felder ignoriert, mehrere Dokumente in Reihenfolge, AssignedAccountIds als String-Zahlen, IsActive-Default. - RepositoryEdgeCaseTests: GetAll leer != null, Update-Alias, Factory-Isolation, History-Exists/GetByTraderSince inklusive an den Grenzen. Alle 78 gruen. (Copytrading-Service-Logik bewusst noch nicht getestet - wird parallel ueberarbeitet.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
b8d2c8094b
commit
92a88e8be8
@@ -0,0 +1,134 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using PolyTrader.Core.Persistence.Ef;
|
||||
using PolyTrader.Modules.CopyTrading.Persistence.Ef;
|
||||
using PolyTrader.Tests.TestSupport;
|
||||
using PolyTraderSharp.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace PolyTrader.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Feinkörnige Modell-Invarianten (Randfälle des Mappings): Dezimal-Präzision auf
|
||||
/// Geldfeldern, MaxLength auf Strings, manuell gesetzte Keys (ValueGeneratedNever),
|
||||
/// Value-Converter/Spaltentyp der JSON-Felder, Indizes.
|
||||
/// </summary>
|
||||
public class DbContextModelTests
|
||||
{
|
||||
private static CoreDbContext CoreCtx() =>
|
||||
new InMemoryContextFactory<CoreDbContext>(o => new CoreDbContext(o)).CreateDbContext();
|
||||
|
||||
private static CopyTradingDbContext CtCtx() =>
|
||||
new InMemoryContextFactory<CopyTradingDbContext>(o => new CopyTradingDbContext(o)).CreateDbContext();
|
||||
|
||||
private static IProperty Prop(DbContext ctx, Type clr, string name) =>
|
||||
ctx.Model.FindEntityType(clr)!.FindProperty(name)!;
|
||||
|
||||
// GetColumnType() wirft unter dem InMemory-Provider (kein RelationalTypeMapping);
|
||||
// die HasColumnType-Annotation lässt sich aber providerunabhängig auslesen.
|
||||
private static string? ColumnType(IProperty p) =>
|
||||
p.FindAnnotation("Relational:ColumnType")?.Value as string;
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(AccountState), nameof(AccountState.TotalBalance))]
|
||||
[InlineData(typeof(AccountState), nameof(AccountState.AvailableBalance))]
|
||||
[InlineData(typeof(Position), nameof(Position.EntryPrice))]
|
||||
[InlineData(typeof(TradeRecord), nameof(TradeRecord.RealizedPnl))]
|
||||
public void Core_money_fields_have_precision_18_6(Type clr, string name)
|
||||
{
|
||||
using var ctx = CoreCtx();
|
||||
var p = Prop(ctx, clr, name);
|
||||
Assert.Equal(18, p.GetPrecision());
|
||||
Assert.Equal(6, p.GetScale());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(nameof(CopyTradingAccountSettings.PerMarketLimit))]
|
||||
[InlineData(nameof(CopyTradingAccountSettings.PerMasterLimit))]
|
||||
[InlineData(nameof(CopyTradingAccountSettings.perMaxTimeNone))]
|
||||
public void Settings_money_fields_have_precision_18_6(string name)
|
||||
{
|
||||
using var ctx = CtCtx();
|
||||
var p = Prop(ctx, typeof(CopyTradingAccountSettings), name);
|
||||
Assert.Equal(18, p.GetPrecision());
|
||||
Assert.Equal(6, p.GetScale());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(AccountState), nameof(AccountState.WalletAddress), 128)]
|
||||
[InlineData(typeof(AccountState), nameof(AccountState.Name), 200)]
|
||||
[InlineData(typeof(MarketData), nameof(MarketData.Id), 120)]
|
||||
public void Core_string_fields_have_expected_max_length(Type clr, string name, int expected)
|
||||
{
|
||||
using var ctx = CoreCtx();
|
||||
Assert.Equal(expected, Prop(ctx, clr, name).GetMaxLength());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrackedTrader_category_has_max_length_64()
|
||||
{
|
||||
using var ctx = CtCtx();
|
||||
Assert.Equal(64, Prop(ctx, typeof(TrackedTrader), nameof(TrackedTrader.Category)).GetMaxLength());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(AccountState), nameof(AccountState.AccountId))]
|
||||
public void Core_manual_keys_are_not_store_generated(Type clr, string name)
|
||||
{
|
||||
using var ctx = CoreCtx();
|
||||
Assert.Equal(ValueGenerated.Never, Prop(ctx, clr, name).ValueGenerated);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(TrackedTrader), nameof(TrackedTrader.Id))]
|
||||
[InlineData(typeof(ClosedTrade), nameof(ClosedTrade.TradeId))]
|
||||
[InlineData(typeof(CopyTradingAccountSettings), nameof(CopyTradingAccountSettings.AccountId))]
|
||||
public void Module_manual_keys_are_not_store_generated(Type clr, string name)
|
||||
{
|
||||
using var ctx = CtCtx();
|
||||
Assert.Equal(ValueGenerated.Never, Prop(ctx, clr, name).ValueGenerated);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AssignedAccountIds_has_value_converter_and_text_column()
|
||||
{
|
||||
using var ctx = CtCtx();
|
||||
var p = Prop(ctx, typeof(TrackedTrader), nameof(TrackedTrader.AssignedAccountIds));
|
||||
Assert.NotNull(p.GetValueConverter());
|
||||
Assert.Equal("text", ColumnType(p));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(nameof(MarketData.ClobTokenIds))]
|
||||
[InlineData(nameof(MarketData.Outcomes))]
|
||||
public void Market_json_columns_are_text(string name)
|
||||
{
|
||||
using var ctx = CoreCtx();
|
||||
Assert.Equal("text", ColumnType(Prop(ctx, typeof(MarketData), name)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TradeRecord_is_indexed_on_closed_at()
|
||||
{
|
||||
using var ctx = CoreCtx();
|
||||
var indexed = ctx.Model.FindEntityType(typeof(TradeRecord))!
|
||||
.GetIndexes()
|
||||
.SelectMany(i => i.Properties.Select(p => p.Name));
|
||||
Assert.Contains(nameof(TradeRecord.ClosedAt), indexed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MasterTraderHistory_is_indexed_on_trader_and_closed_at()
|
||||
{
|
||||
using var ctx = CtCtx();
|
||||
var indexed = ctx.Model.FindEntityType(typeof(MasterTraderHistoryRecord))!
|
||||
.GetIndexes()
|
||||
.SelectMany(i => i.Properties.Select(p => p.Name))
|
||||
.ToList();
|
||||
Assert.Contains(nameof(MasterTraderHistoryRecord.TraderId), indexed);
|
||||
Assert.Contains(nameof(MasterTraderHistoryRecord.ClosedAt), indexed);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,5 +116,51 @@ namespace PolyTrader.Tests
|
||||
Assert.Empty(MongoExportParser.ParseAccounts("[]"));
|
||||
Assert.Empty(MongoExportParser.ParseTraders("[]"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseAccounts_accepts_numeric_decimals_not_only_strings()
|
||||
{
|
||||
const string json = """[{ "_id": 1, "TotalBalance": 12.5, "PerMarketLimit": 3 }]""";
|
||||
|
||||
var imp = Assert.Single(MongoExportParser.ParseAccounts(json));
|
||||
Assert.Equal(12.5m, imp.Account.TotalBalance);
|
||||
Assert.Equal(3m, imp.Settings.PerMarketLimit);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseAccounts_ignores_unknown_extra_fields()
|
||||
{
|
||||
const string json = """[{ "_id": 1, "Name": "X", "SomethingUnknown": {"a":1}, "Extra": [1,2,3] }]""";
|
||||
|
||||
var imp = Assert.Single(MongoExportParser.ParseAccounts(json));
|
||||
Assert.Equal("X", imp.Account.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseAccounts_reads_multiple_documents_in_order()
|
||||
{
|
||||
const string json = """[{ "_id": 1 }, { "_id": 2 }, { "_id": 3 }]""";
|
||||
|
||||
var ids = MongoExportParser.ParseAccounts(json).Select(x => x.Account.AccountId).ToArray();
|
||||
Assert.Equal(new[] { 1, 2, 3 }, ids);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseTraders_accepts_assigned_account_ids_as_string_numbers()
|
||||
{
|
||||
const string json = """[{ "_id": 9, "AssignedAccountIds": ["1", "3"] }]""";
|
||||
|
||||
var t = Assert.Single(MongoExportParser.ParseTraders(json));
|
||||
Assert.Equal(new HashSet<int> { 1, 3 }, t.AssignedAccountIds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseTraders_missing_isactive_defaults_true()
|
||||
{
|
||||
const string json = """[{ "_id": 9 }]""";
|
||||
|
||||
var t = Assert.Single(MongoExportParser.ParseTraders(json));
|
||||
Assert.True(t.IsActive);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
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 RepositoryEdgeCaseTests
|
||||
{
|
||||
private static EfTrackedTraderRepository TraderRepo() =>
|
||||
new(new InMemoryContextFactory<CopyTradingDbContext>(o => new CopyTradingDbContext(o)));
|
||||
|
||||
private static EfMasterTraderHistoryRepository HistoryRepo() =>
|
||||
new(new InMemoryContextFactory<CopyTradingDbContext>(o => new CopyTradingDbContext(o)));
|
||||
|
||||
[Fact]
|
||||
public void GetAll_on_empty_repo_returns_empty_not_null()
|
||||
{
|
||||
var repo = TraderRepo();
|
||||
var all = repo.GetAll();
|
||||
Assert.NotNull(all);
|
||||
Assert.Empty(all);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_alias_behaves_like_upsert_for_existing()
|
||||
{
|
||||
var repo = TraderRepo();
|
||||
repo.Upsert(new TrackedTrader { Id = 1, DisplayName = "A" });
|
||||
repo.Update(new TrackedTrader { Id = 1, DisplayName = "B" });
|
||||
|
||||
Assert.Equal("B", Assert.Single(repo.GetAll()).DisplayName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Two_factories_are_isolated_from_each_other()
|
||||
{
|
||||
var a = TraderRepo();
|
||||
var b = TraderRepo();
|
||||
a.Upsert(new TrackedTrader { Id = 1 });
|
||||
|
||||
Assert.Single(a.GetAll());
|
||||
Assert.Empty(b.GetAll());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void History_Exists_is_inclusive_on_window_boundaries()
|
||||
{
|
||||
var repo = HistoryRepo();
|
||||
var t = new DateTime(2026, 7, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||
repo.Insert(new MasterTraderHistoryRecord { TraderId = 1, TokenId = "tok", ClosedAt = t });
|
||||
|
||||
// Fenster endet exakt auf dem Zeitpunkt (untere Grenze)
|
||||
Assert.True(repo.Exists(1, "tok", t, t.AddSeconds(2)));
|
||||
// Fenster beginnt exakt auf dem Zeitpunkt (obere Grenze)
|
||||
Assert.True(repo.Exists(1, "tok", t.AddSeconds(-2), t));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetByTraderSince_is_inclusive_on_cutoff()
|
||||
{
|
||||
var repo = HistoryRepo();
|
||||
var cutoff = new DateTime(2026, 7, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
repo.Insert(new MasterTraderHistoryRecord { TraderId = 1, ClosedAt = cutoff }); // genau am Cutoff -> drin
|
||||
repo.Insert(new MasterTraderHistoryRecord { TraderId = 1, ClosedAt = cutoff.AddTicks(-1) }); // knapp davor -> raus
|
||||
|
||||
Assert.Single(repo.GetByTraderSince(1, cutoff));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user