using System.Security.Cryptography; using Autofac; using Moq; using NUnit.Framework; namespace e_suite.DependencyInjection.UnitTests; [TestFixture] public class ModuleDiscoveryUnitTests { [Test] public void LoadOrCreateModuleCache_UsesExistingCache_WhenHashMatches() { // Arrange var fileSystem = new Mock(); var assemblyLoader = new Mock(); var identity = new Mock(); var basePath = AppContext.BaseDirectory; var cacheDirectory = Path.Combine(basePath, ".cache"); var cachePath = Path.Combine(cacheDirectory, "module-cache.json"); // The hash we expect the discovery code to compute var fakeBytes = new byte[] { 1, 2, 3, 4 }; var expectedHash = Convert.ToHexString(SHA256.HashData(fakeBytes)); // Existing cache with the SAME hash var existingCacheJson = $$""" { "AssemblyHash": "{{expectedHash}}", "Modules": [ "TestModule.dll" ] } """; // Mock filesystem behaviour fileSystem.Setup(fs => fs.CreateDirectory(cacheDirectory)); fileSystem.Setup(fs => fs.FileExists(cachePath)).Returns(true); fileSystem.Setup(fs => fs.FileExists(Path.Combine(basePath, "TestModule.dll"))).Returns(true); fileSystem.Setup(fs => fs.ReadAllText(cachePath)).Returns(existingCacheJson); // Mock assembly identity → returns an assembly whose hash we control var assembly = typeof(ModuleDiscoveryUnitTests).Assembly; identity.Setup(x => x.GetAssemblyForHashing()).Returns(assembly); // Mock hashing stream // The bytes here don't matter — the test only cares that the hash matches fileSystem.Setup(fs => fs.OpenRead(assembly.Location)) .Returns(new MemoryStream(fakeBytes)); // Mock assembly loader for the module listed in the cache assemblyLoader.Setup(l => l.LoadFrom(Path.Combine(basePath, "TestModule.dll"))) .Returns(assembly); var discovery = new ModuleDiscovery(fileSystem.Object, assemblyLoader.Object, identity.Object); // Act discovery.RegisterAllModuleIocRegistrations(new ContainerBuilder()); // Assert: cache should NOT be rewritten fileSystem.Verify(fs => fs.WriteAllText(It.IsAny(), It.IsAny()), Times.Never); // Assert: existing cache was read fileSystem.Verify(fs => fs.ReadAllText(cachePath), Times.Once); } [Test] public void LoadOrCreateModuleCache_RegeneratesCache_WhenHashDoesNotMatch() { // Arrange var fileSystem = new Mock(); var assemblyLoader = new Mock(); var identity = new Mock(); var basePath = AppContext.BaseDirectory; var cacheDirectory = Path.Combine(basePath, ".cache"); var cachePath = Path.Combine(cacheDirectory, "module-cache.json"); var existingCacheJson = """ { "AssemblyHash": "OLDHASH", "Modules": [ "OldModule.dll" ] } """; fileSystem.Setup(fs => fs.CreateDirectory(cacheDirectory)); fileSystem.Setup(fs => fs.FileExists(cachePath)).Returns(true); fileSystem.Setup(fs => fs.ReadAllText(cachePath)).Returns(existingCacheJson); string? writtenJson = null; fileSystem.Setup(fs => fs.WriteAllText(cachePath, It.IsAny())) .Callback((_, json) => writtenJson = json); var assembly = typeof(TestRegistration).Assembly; identity.Setup(x => x.GetAssemblyForHashing()).Returns(assembly); var fakeStream = new MemoryStream(new byte[] { 1, 2, 3, 4 }); fileSystem.Setup(fs => fs.OpenRead(assembly.Location)).Returns(fakeStream); var newModulePath = Path.Combine(basePath, "NewModule.dll"); // Regeneration scan fileSystem.Setup(fs => fs.GetFiles(basePath, "*.dll")) .Returns(new[] { newModulePath }); // After regeneration, RegisterAllModuleIocRegistrations will check FileExists on fullPath fileSystem.Setup(fs => fs.FileExists(newModulePath)).Returns(true); assemblyLoader.Setup(l => l.LoadFrom(newModulePath)) .Returns(typeof(TestRegistration).Assembly); var discovery = new ModuleDiscovery(fileSystem.Object, assemblyLoader.Object, identity.Object); // Act discovery.RegisterAllModuleIocRegistrations(new ContainerBuilder()); // Assert fileSystem.Verify(fs => fs.WriteAllText(cachePath, It.IsAny()), Times.Once); fileSystem.Verify(fs => fs.GetFiles(basePath, "*.dll"), Times.Once); Assert.That(writtenJson, Is.Not.Null); Assert.That(writtenJson, Does.Contain("NewModule.dll")); } [Test] public void RegisterAllModuleIocRegistrations_Throws_WhenCachedModuleMissing() { // Arrange var fileSystem = new Mock(); var assemblyLoader = new Mock(); var identity = new Mock(); var basePath = AppContext.BaseDirectory; var cacheDirectory = Path.Combine(basePath, ".cache"); var cachePath = Path.Combine(cacheDirectory, "module-cache.json"); // Fake bytes for hashing var fakeBytes = new byte[] { 1, 2, 3, 4 }; var expectedHash = Convert.ToHexString(SHA256.HashData(fakeBytes)); // Cache contains a module that does NOT exist var existingCacheJson = $$""" { "AssemblyHash": "{{expectedHash}}", "Modules": [ "MissingModule.dll" ] } """; // Mock filesystem fileSystem.Setup(fs => fs.CreateDirectory(cacheDirectory)); fileSystem.Setup(fs => fs.FileExists(cachePath)).Returns(true); fileSystem.Setup(fs => fs.ReadAllText(cachePath)).Returns(existingCacheJson); // Hashing stream var assembly = typeof(ModuleDiscoveryUnitTests).Assembly; identity.Setup(x => x.GetAssemblyForHashing()).Returns(assembly); fileSystem.Setup(fs => fs.OpenRead(assembly.Location)) .Returns(new MemoryStream(fakeBytes)); // Critical: Missing module file → FileExists returns false var missingModulePath = Path.Combine(basePath, "MissingModule.dll"); fileSystem.Setup(fs => fs.FileExists(missingModulePath)).Returns(false); var discovery = new ModuleDiscovery(fileSystem.Object, assemblyLoader.Object, identity.Object); // Act + Assert var ex = Assert.Throws(() => discovery.RegisterAllModuleIocRegistrations(new ContainerBuilder())); Assert.That(ex!.Message, Does.Contain("MissingModule.dll")); } [Test] public void RegenerateModuleCache_IgnoresAssemblyLoadFailures() { // Arrange var fileSystem = new Mock(); var assemblyLoader = new Mock(); var identity = new Mock(); var basePath = AppContext.BaseDirectory; var cacheDirectory = Path.Combine(basePath, ".cache"); var cachePath = Path.Combine(cacheDirectory, "module-cache.json"); // Existing cache with WRONG hash → forces regeneration var existingCacheJson = """ { "AssemblyHash": "OLDHASH", "Modules": [ "OldModule.dll" ] } """; fileSystem.Setup(fs => fs.CreateDirectory(cacheDirectory)); fileSystem.Setup(fs => fs.FileExists(cachePath)).Returns(true); fileSystem.Setup(fs => fs.ReadAllText(cachePath)).Returns(existingCacheJson); // Capture regenerated JSON string? writtenJson = null; fileSystem.Setup(fs => fs.WriteAllText(cachePath, It.IsAny())) .Callback((_, json) => writtenJson = json); // Hashing setup var assembly = typeof(TestRegistration).Assembly; identity.Setup(x => x.GetAssemblyForHashing()).Returns(assembly); var fakeStream = new MemoryStream(new byte[] { 1, 2, 3, 4 }); fileSystem.Setup(fs => fs.OpenRead(assembly.Location)).Returns(fakeStream); // Regeneration scan finds a DLL var badDllPath = Path.Combine(basePath, "BadModule.dll"); fileSystem.Setup(fs => fs.GetFiles(basePath, "*.dll")) .Returns(new[] { badDllPath }); // Critical: assembly load fails assemblyLoader.Setup(l => l.LoadFrom(badDllPath)) .Throws(new BadImageFormatException("Not a .NET assembly")); // After regeneration, RegisterAllModuleIocRegistrations will check FileExists // but since no modules are added, this path is never hit. // Still safe to mock it as false. fileSystem.Setup(fs => fs.FileExists(It.IsAny())).Returns(false); var discovery = new ModuleDiscovery(fileSystem.Object, assemblyLoader.Object, identity.Object); // Act discovery.RegisterAllModuleIocRegistrations(new ContainerBuilder()); // Assert: regeneration happened fileSystem.Verify(fs => fs.GetFiles(basePath, "*.dll"), Times.Once); // Assert: cache was rewritten fileSystem.Verify(fs => fs.WriteAllText(cachePath, It.IsAny()), Times.Once); // Assert: regenerated cache contains NO modules Assert.That(writtenJson, Is.Not.Null); Assert.That(writtenJson, Does.Not.Contain("BadModule.dll")); } [Test] public void RegisterAllModuleIocRegistrations_InvokesRegistrationTypes() { // Arrange TestRegistration.WasCalled = false; var fileSystem = new Mock(); var assemblyLoader = new Mock(); var identity = new Mock(); var basePath = AppContext.BaseDirectory; var cacheDirectory = Path.Combine(basePath, ".cache"); var cachePath = Path.Combine(cacheDirectory, "module-cache.json"); // Fake hash bytes var fakeBytes = new byte[] { 1, 2, 3, 4 }; var expectedHash = Convert.ToHexString(SHA256.HashData(fakeBytes)); // Cache contains our test module var existingCacheJson = $$""" { "AssemblyHash": "{{expectedHash}}", "Modules": [ "TestModule.dll" ] } """; // Filesystem mocks fileSystem.Setup(fs => fs.CreateDirectory(cacheDirectory)); fileSystem.Setup(fs => fs.FileExists(cachePath)).Returns(true); fileSystem.Setup(fs => fs.ReadAllText(cachePath)).Returns(existingCacheJson); // Hashing stream var assembly = typeof(TestRegistration).Assembly; identity.Setup(x => x.GetAssemblyForHashing()).Returns(assembly); fileSystem.Setup(fs => fs.OpenRead(assembly.Location)) .Returns(new MemoryStream(fakeBytes)); // Module file exists var modulePath = Path.Combine(basePath, "TestModule.dll"); fileSystem.Setup(fs => fs.FileExists(modulePath)).Returns(true); // Assembly loader returns assembly containing TestRegistration assemblyLoader.Setup(l => l.LoadFrom(modulePath)) .Returns(typeof(TestRegistration).Assembly); var discovery = new ModuleDiscovery(fileSystem.Object, assemblyLoader.Object, identity.Object); var builder = new ContainerBuilder(); // Act discovery.RegisterAllModuleIocRegistrations(builder); // Assert Assert.That(TestRegistration.WasCalled, Is.True); } }