using System.Reflection;
using FluentAssertions;
using IBKRTrader.Core.Trading;
using IBKRTrader.Modules.Accounting;
using IBKRTrader.Modules.CongressTrading;
using IBKRTrader.Modules.OptionsWheel;
using IBKRTrader.Modules.Supervisor;
namespace IBKRTrader.Tests.Modules;
///
/// Die Grenze zwischen Modul und Core (Kapital- und Buchmodell §2, Regel 4): Ein Modul handelt
/// ausschließlich über seinen gebundenen -Zugang. Weder der Broker
/// noch die gemeinsame Ausführungs-Pipeline gehören in ein Modul.
///
/// Warum als Test und nicht als Container-Trennung. Getrennte DI-Container je Modul
/// wären der harte Weg, aber sie ändern die Modularchitektur und kosten bei jedem Modul Aufwand.
/// Der Fall, um den es wirklich geht, ist ein neues Modul, das den Core versehentlich
/// umgeht – und genau den fängt dieser Test ab, beim Übersetzen statt im Betrieb.
///
/// Heute hält sich jedes Modul daran. Ohne Prüfung fiele der erste Bruch niemandem auf.
///
[Trait("cat", "unit")]
public class ModuleBoundaryTests
{
/// Was ein Modul nicht anfassen darf.
private static readonly Type[] Forbidden =
[
typeof(IBrokerClient),
typeof(IBrokerPortfolioReader),
typeof(IExecutionService)
];
private static Assembly[] ModuleAssemblies =>
[
typeof(CongressTradingModule).Assembly,
typeof(AccountingModule).Assembly,
typeof(SupervisorModule).Assembly,
typeof(OptionsWheelModule).Assembly
];
[Fact]
public void NoModuleTakesTheBrokerOrTheSharedPipelineThroughDependencyInjection()
{
var offences = new List();
foreach (var assembly in ModuleAssemblies)
foreach (var type in assembly.GetTypes())
{
foreach (var ctor in type.GetConstructors(
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
foreach (var parameter in ctor.GetParameters().Where(p => Forbidden.Contains(p.ParameterType)))
offences.Add($"{type.FullName}: Konstruktorparameter '{parameter.Name}' " +
$"vom Typ {parameter.ParameterType.Name}");
foreach (var field in type.GetFields(
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static)
.Where(f => Forbidden.Contains(f.FieldType)))
offences.Add($"{type.FullName}: Feld '{field.Name}' vom Typ {field.FieldType.Name}");
foreach (var property in type.GetProperties(
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static)
.Where(p => Forbidden.Contains(p.PropertyType)))
offences.Add($"{type.FullName}: Eigenschaft '{property.Name}' " +
$"vom Typ {property.PropertyType.Name}");
}
offences.Should().BeEmpty(
"ein Modul handelt ausschließlich über seinen gebundenen IModuleExecution-Zugang. " +
"Wer den Broker oder die gemeinsame Pipeline nimmt, umgeht Buchführung, Kapitalbindung " +
"und Limits des Cores");
}
///
/// Der zweite Weg an der Grenze vorbei: sich den verbotenen Dienst zur Laufzeit aus dem
/// Container holen. Das sieht die Reflection oben nicht – dafür steht es im Quelltext.
///
/// Findet der Test die Quellen nicht (etwa in einer Auslieferung ohne Repository), prüft
/// er nichts und schlägt auch nicht fehl; im Repository und in der CI greift er.
///
[Fact]
public void NoModuleResolvesTheBrokerOrTheSharedPipelineFromTheContainer()
{
var moduleSources = FindModuleSourceDirectories();
if (moduleSources.Count == 0) return;
var offences = new List();
foreach (var directory in moduleSources)
foreach (var file in Directory.EnumerateFiles(directory, "*.cs", SearchOption.AllDirectories))
{
var text = File.ReadAllText(file);
foreach (var type in Forbidden.Select(t => t.Name))
if (text.Contains($"<{type}>", StringComparison.Ordinal) ||
text.Contains($"({type} ", StringComparison.Ordinal))
offences.Add($"{Path.GetFileName(file)}: nennt {type}");
}
offences.Should().BeEmpty(
"ein Modul darf sich den Broker oder die gemeinsame Pipeline auch nicht aus dem " +
"Container holen – der gebundene Zugang ist der einzige Weg");
}
/// Sucht die Modul-Quellverzeichnisse ausgehend vom Testverzeichnis.
private static List FindModuleSourceDirectories()
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory is not null)
{
var src = Path.Combine(directory.FullName, "src");
if (Directory.Exists(src))
return [.. Directory.GetDirectories(src, "IBKRTrader.Modules.*")];
directory = directory.Parent;
}
return [];
}
}