Backend/e-suite.DependencyInjection.UnitTests/ModuleDiscoveryUnitTests.cs

294 lines
12 KiB
C#

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<IFileSystem>();
var assemblyLoader = new Mock<IAssemblyLoader>();
var identity = new Mock<IAssemblyIdentity>();
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<string>(), It.IsAny<string>()), 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<IFileSystem>();
var assemblyLoader = new Mock<IAssemblyLoader>();
var identity = new Mock<IAssemblyIdentity>();
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<string>()))
.Callback<string, string>((_, 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<string>()), 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<IFileSystem>();
var assemblyLoader = new Mock<IAssemblyLoader>();
var identity = new Mock<IAssemblyIdentity>();
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<FileNotFoundException>(() =>
discovery.RegisterAllModuleIocRegistrations(new ContainerBuilder()));
Assert.That(ex!.Message, Does.Contain("MissingModule.dll"));
}
[Test]
public void RegenerateModuleCache_IgnoresAssemblyLoadFailures()
{
// Arrange
var fileSystem = new Mock<IFileSystem>();
var assemblyLoader = new Mock<IAssemblyLoader>();
var identity = new Mock<IAssemblyIdentity>();
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<string>()))
.Callback<string, string>((_, 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<string>())).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<string>()), 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<IFileSystem>();
var assemblyLoader = new Mock<IAssemblyLoader>();
var identity = new Mock<IAssemblyIdentity>();
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);
}
}