294 lines
12 KiB
C#
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);
|
|
}
|
|
} |