using Xunit; using StellaOps.TestKit; namespace StellaOps.Scanner.Analyzers.OS.Windows.Msi.Tests; public class MsiDatabaseParserTests { private readonly MsiDatabaseParser _parser = new(); [Trait("Category", TestCategories.Unit)] [Fact] public void Parse_WithValidMsiFile_ExtractsMetadata() { // Arrange - Create a minimal valid OLE compound document var tempFile = CreateMinimalMsiFile("TestProduct-1.2.3.msi"); try { // Act var result = _parser.Parse(tempFile, CancellationToken.None); // Assert Assert.NotNull(result); Assert.Equal("TestProduct", result.ProductName); Assert.Equal("1.2.3", result.ProductVersion); Assert.Equal(tempFile, result.FilePath); Assert.True(result.FileSize > 0); Assert.StartsWith("sha256:", result.FileHash); } finally { if (File.Exists(tempFile)) { File.Delete(tempFile); } } } [Trait("Category", TestCategories.Unit)] [Fact] public void Parse_WithVersionedFilename_ExtractsVersionFromName() { // Arrange var tempFile = CreateMinimalMsiFile("SomeProduct_v2.0.1.msi"); try { // Act var result = _parser.Parse(tempFile, CancellationToken.None); // Assert Assert.NotNull(result); Assert.Equal("SomeProduct", result.ProductName); Assert.Equal("2.0.1", result.ProductVersion); } finally { if (File.Exists(tempFile)) { File.Delete(tempFile); } } } [Trait("Category", TestCategories.Unit)] [Fact] public void Parse_WithSpaceVersionedFilename_ExtractsVersionFromName() { // Arrange var tempFile = CreateMinimalMsiFile("Application Setup 3.0.0.msi"); try { // Act var result = _parser.Parse(tempFile, CancellationToken.None); // Assert Assert.NotNull(result); Assert.Equal("Application Setup", result.ProductName); Assert.Equal("3.0.0", result.ProductVersion); } finally { if (File.Exists(tempFile)) { File.Delete(tempFile); } } } [Trait("Category", TestCategories.Unit)] [Fact] public void Parse_WithUnversionedFilename_UsesDefaultVersion() { // Arrange var tempFile = CreateMinimalMsiFile("SimpleInstaller.msi"); try { // Act var result = _parser.Parse(tempFile, CancellationToken.None); // Assert Assert.NotNull(result); Assert.Equal("SimpleInstaller", result.ProductName); Assert.Equal("0.0.0", result.ProductVersion); } finally { if (File.Exists(tempFile)) { File.Delete(tempFile); } } } [Trait("Category", TestCategories.Unit)] [Fact] public void Parse_WithNonExistentFile_ReturnsNull() { // Act var result = _parser.Parse("/nonexistent/path/file.msi", CancellationToken.None); // Assert Assert.Null(result); } [Trait("Category", TestCategories.Unit)] [Fact] public void Parse_WithInvalidMsiFile_ReturnsNull() { // Arrange - Create a file with invalid content var tempFile = Path.GetTempFileName(); File.WriteAllText(tempFile, "Not an MSI file"); try { // Act var result = _parser.Parse(tempFile, CancellationToken.None); // Assert Assert.Null(result); } finally { File.Delete(tempFile); } } [Trait("Category", TestCategories.Unit)] [Fact] public void Parse_WithEmptyPath_ReturnsNull() { // Act var result = _parser.Parse(string.Empty, CancellationToken.None); // Assert Assert.Null(result); } [Trait("Category", TestCategories.Unit)] [Fact] public void Parse_WithNullPath_ReturnsNull() { // Act var result = _parser.Parse(null!, CancellationToken.None); // Assert Assert.Null(result); } [Trait("Category", TestCategories.Unit)] [Theory] [InlineData("Product-1.0.msi", "Product", "1.0")] [InlineData("Product-1.0.0.msi", "Product", "1.0.0")] [InlineData("Product-1.0.0.1.msi", "Product", "1.0.0.1")] [InlineData("My-Product_v5.2.msi", "My-Product", "5.2")] [InlineData("App 10.0.msi", "App", "10.0")] public void Parse_WithVariousFilenamePatterns_ExtractsCorrectNameAndVersion( string filename, string expectedName, string expectedVersion) { // Arrange var tempFile = CreateMinimalMsiFile(filename); try { // Act var result = _parser.Parse(tempFile, CancellationToken.None); // Assert Assert.NotNull(result); Assert.Equal(expectedName, result.ProductName); Assert.Equal(expectedVersion, result.ProductVersion); } finally { if (File.Exists(tempFile)) { File.Delete(tempFile); } } } /// /// Creates a minimal valid OLE compound document (MSI) file for testing. /// private static string CreateMinimalMsiFile(string filename) { var tempDir = Path.Combine(Path.GetTempPath(), "msi-test-" + Guid.NewGuid().ToString("N")[..8]); Directory.CreateDirectory(tempDir); var filePath = Path.Combine(tempDir, filename); // OLE compound document header (512 bytes minimum) // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-cfb/ var header = new byte[512]; // Magic number: D0 CF 11 E0 A1 B1 1A E1 header[0] = 0xD0; header[1] = 0xCF; header[2] = 0x11; header[3] = 0xE0; header[4] = 0xA1; header[5] = 0xB1; header[6] = 0x1A; header[7] = 0xE1; // Minor version (typically 0x003E) header[0x18] = 0x3E; header[0x19] = 0x00; // Major version (3 for sector size 512, 4 for sector size 4096) header[0x1A] = 0x03; header[0x1B] = 0x00; // Byte order (0xFFFE = little endian) header[0x1C] = 0xFE; header[0x1D] = 0xFF; // Sector size power (9 = 512 bytes) header[0x1E] = 0x09; header[0x1F] = 0x00; // Mini sector size power (6 = 64 bytes) header[0x20] = 0x06; header[0x21] = 0x00; File.WriteAllBytes(filePath, header); return filePath; } }