R3 Slice 3: CongressTrading-Modul + Worker-Log + core_settings auf EF Core

- Modul: CongressTradingDbContext (ct_congressMember/ct_trade) + Design-Time-Factory;
  CongressRepository von Dapper auf EF; CongressTrade.DetailsFetched ergaenzt; Dapper entfernt
- Core: CoreSettingsService (core_settings via EF); WorkerBase-Log auf EF (core_worker_log);
  7 Worker-Ctors DatabaseService -> IDbContextFactory<CoreDbContext>
- CoreMigrations + CongressMigrations (Dapper) entfernt; core_-Schema nun rein EF
- EF-Migration InitialCongressTrading; AddCorePersistence-Fallback fuer leeren Connection-String
  (App startet ohne DB); Connection aus appsettings.Local.json (gitignored)
- Tests: +4 CongressRepository (EF-InMemory) -> 39/39 gruen; Build + smoke-ui + App-Start ok

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
This commit is contained in:
Richard
2026-07-28 11:34:42 +02:00
parent 7e3791b82d
commit 6c8e36dc3c
27 changed files with 832 additions and 323 deletions
@@ -0,0 +1,81 @@
using FluentAssertions;
using IBKRTrader.Core.Logging;
using IBKRTrader.Core.Persistence.Ef;
using IBKRTrader.Core.Settings;
using IBKRTrader.Modules.CongressTrading.Database;
using IBKRTrader.Modules.CongressTrading.Models;
using IBKRTrader.Modules.CongressTrading.Persistence.Ef;
using Microsoft.EntityFrameworkCore;
namespace IBKRTrader.Tests.Modules;
/// <summary>EF-Repo des Moduls gegen EF-InMemory (deterministisch, kein externer DB-Zugriff).</summary>
[Trait("cat", "unit")]
public class CongressRepositoryTests
{
private sealed class Factory<T>(DbContextOptions<T> options) : IDbContextFactory<T> where T : DbContext
{
public T CreateDbContext() => (T)Activator.CreateInstance(typeof(T), options)!;
}
private static CongressRepository CreateSut()
{
var ctOpts = new DbContextOptionsBuilder<CongressTradingDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString()).Options;
var coreOpts = new DbContextOptionsBuilder<CoreDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString()).Options;
var settings = new CoreSettingsService(new Factory<CoreDbContext>(coreOpts));
return new CongressRepository(new Factory<CongressTradingDbContext>(ctOpts), settings, new LoggingService());
}
[Fact]
public async Task UpsertMember_IsIdempotent_ByBioId()
{
var repo = CreateSut();
await repo.UpsertMemberAsync(new CongressMember { BioId = "W1", Name = "Alice" });
await repo.UpsertMemberAsync(new CongressMember { BioId = "W1", Name = "Alice B." });
(await repo.MemberExistsAsync("W1")).Should().BeTrue();
(await repo.GetMemberCountAsync()).Should().Be(1);
(await repo.GetAllMemberBioIdsAsync()).Should().Contain("W1");
}
[Fact]
public async Task InsertTrade_IgnoresDuplicateTradeId()
{
var repo = CreateSut();
await repo.InsertTradeAsync(new CongressTrade { TradeId = "T1", MemberBioId = "W1", Ticker = "AAPL", TradeType = "buy" });
await repo.InsertTradeAsync(new CongressTrade { TradeId = "T1", MemberBioId = "W1", Ticker = "AAPL" });
(await repo.TradeExistsAsync("T1")).Should().BeTrue();
(await repo.GetTradeCountAsync()).Should().Be(1);
}
[Fact]
public async Task DetailsFlow_MarksTradeComplete()
{
var repo = CreateSut();
await repo.InsertTradeAsync(new CongressTrade { TradeId = "T2" }, detailsFetched: false);
(await repo.GetTradesWithoutDetailsAsync()).Should().Contain("T2");
await repo.UpdateTradeDetailsAsync(new CongressTrade { TradeId = "T2", Ticker = "MSFT", TradeType = "sell" });
(await repo.GetTradesWithoutDetailsAsync()).Should().NotContain("T2");
}
[Fact]
public async Task Settings_RoundTripThroughCoreSettings()
{
var repo = CreateSut();
await repo.SetSettingAsync("ct.history_import_page", "7");
(await repo.GetSettingAsync("ct.history_import_page")).Should().Be("7");
await repo.DeleteSettingAsync("ct.history_import_page");
(await repo.GetSettingAsync("ct.history_import_page")).Should().BeNull();
}
}
@@ -45,7 +45,6 @@ public class CongressTradingModuleTests
var types = services.Select(d => d.ServiceType).ToList();
types.Should().Contain(new[]
{
typeof(CongressMigrations),
typeof(CongressRepository),
typeof(CapitolTradesScraper),
typeof(CongressHistoryImportWorker),