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 []; } }