up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-28 19:23:54 +02:00
parent d1cbb905f8
commit d040c001ac
36 changed files with 4668 additions and 9 deletions

View File

@@ -0,0 +1,80 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey;
using Xunit;
namespace StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey.Tests;
public class ChocolateyAnalyzerPluginTests
{
[Fact]
public void Name_ReturnsCorrectPluginName()
{
// Arrange
var plugin = new ChocolateyAnalyzerPlugin();
// Act
var name = plugin.Name;
// Assert
Assert.Equal("StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey", name);
}
[Fact]
public void IsAvailable_WithValidServiceProvider_ReturnsTrue()
{
// Arrange
var plugin = new ChocolateyAnalyzerPlugin();
var services = new ServiceCollection()
.AddSingleton<ILoggerFactory, NullLoggerFactory>()
.BuildServiceProvider();
// Act
var result = plugin.IsAvailable(services);
// Assert
Assert.True(result);
}
[Fact]
public void IsAvailable_WithNullServiceProvider_ReturnsFalse()
{
// Arrange
var plugin = new ChocolateyAnalyzerPlugin();
// Act
var result = plugin.IsAvailable(null!);
// Assert
Assert.False(result);
}
[Fact]
public void CreateAnalyzer_WithValidServiceProvider_ReturnsAnalyzer()
{
// Arrange
var plugin = new ChocolateyAnalyzerPlugin();
var services = new ServiceCollection()
.AddSingleton<ILoggerFactory, NullLoggerFactory>()
.BuildServiceProvider();
// Act
var analyzer = plugin.CreateAnalyzer(services);
// Assert
Assert.NotNull(analyzer);
Assert.IsType<ChocolateyPackageAnalyzer>(analyzer);
Assert.Equal("windows-chocolatey", analyzer.AnalyzerId);
}
[Fact]
public void CreateAnalyzer_WithNullServiceProvider_ThrowsArgumentNullException()
{
// Arrange
var plugin = new ChocolateyAnalyzerPlugin();
// Act & Assert
Assert.Throws<ArgumentNullException>(() => plugin.CreateAnalyzer(null!));
}
}

View File

@@ -0,0 +1,501 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey;
using Xunit;
namespace StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey.Tests;
public class ChocolateyPackageAnalyzerTests
{
private readonly ChocolateyPackageAnalyzer _analyzer;
private readonly ILogger _logger;
public ChocolateyPackageAnalyzerTests()
{
_logger = NullLoggerFactory.Instance.CreateLogger<ChocolateyPackageAnalyzer>();
_analyzer = new ChocolateyPackageAnalyzer((ILogger<ChocolateyPackageAnalyzer>)_logger);
}
private OSPackageAnalyzerContext CreateContext(string rootPath)
{
return new OSPackageAnalyzerContext(
rootPath,
workspacePath: null,
TimeProvider.System,
_logger);
}
[Fact]
public void AnalyzerId_ReturnsCorrectValue()
{
Assert.Equal("windows-chocolatey", _analyzer.AnalyzerId);
}
[Fact]
public async Task AnalyzeAsync_WithNoChocolateyDirectory_ReturnsEmptyList()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), "choco-analyzer-test-" + Guid.NewGuid().ToString("N")[..8]);
Directory.CreateDirectory(tempDir);
try
{
var context = CreateContext(tempDir);
// Act
var result = await _analyzer.AnalyzeAsync(context, CancellationToken.None);
// Assert
Assert.NotNull(result);
Assert.Empty(result.Packages);
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
[Fact]
public async Task AnalyzeAsync_WithEmptyChocolateyLib_ReturnsEmptyList()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), "choco-analyzer-test-" + Guid.NewGuid().ToString("N")[..8]);
var chocoLib = Path.Combine(tempDir, "ProgramData", "Chocolatey", "lib");
Directory.CreateDirectory(chocoLib);
try
{
var context = CreateContext(tempDir);
// Act
var result = await _analyzer.AnalyzeAsync(context, CancellationToken.None);
// Assert
Assert.NotNull(result);
Assert.Empty(result.Packages);
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
[Fact]
public async Task AnalyzeAsync_WithNuspecFile_ReturnsPackageRecord()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), "choco-analyzer-test-" + Guid.NewGuid().ToString("N")[..8]);
var chocoLib = Path.Combine(tempDir, "ProgramData", "Chocolatey", "lib");
var packageDir = Path.Combine(chocoLib, "git.2.42.0");
Directory.CreateDirectory(packageDir);
CreateNuspecFile(packageDir, "git", "2.42.0", "Git", "Git Authors", "Git for Windows");
try
{
var context = CreateContext(tempDir);
// Act
var result = await _analyzer.AnalyzeAsync(context, CancellationToken.None);
// Assert
Assert.Single(result.Packages);
var record = result.Packages[0];
Assert.Equal("Git", record.Name);
Assert.Equal("2.42.0", record.Version);
Assert.Equal("pkg:chocolatey/git@2.42.0", record.PackageUrl);
Assert.Equal(PackageEvidenceSource.WindowsChocolatey, record.EvidenceSource);
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
[Fact]
public async Task AnalyzeAsync_WithMultiplePackages_ReturnsAllRecords()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), "choco-analyzer-test-" + Guid.NewGuid().ToString("N")[..8]);
var chocoLib = Path.Combine(tempDir, "ProgramData", "Chocolatey", "lib");
var package1Dir = Path.Combine(chocoLib, "git.2.42.0");
var package2Dir = Path.Combine(chocoLib, "nodejs.20.10.0");
var package3Dir = Path.Combine(chocoLib, "7zip.23.01");
Directory.CreateDirectory(package1Dir);
Directory.CreateDirectory(package2Dir);
Directory.CreateDirectory(package3Dir);
CreateNuspecFile(package1Dir, "git", "2.42.0", "Git", "Git Authors", "Git for Windows");
CreateNuspecFile(package2Dir, "nodejs", "20.10.0", "Node.js", "Node.js Foundation", "Node.js runtime");
CreateNuspecFile(package3Dir, "7zip", "23.01", "7-Zip", "Igor Pavlov", "File archiver");
try
{
var context = CreateContext(tempDir);
// Act
var result = await _analyzer.AnalyzeAsync(context, CancellationToken.None);
// Assert
Assert.Equal(3, result.Packages.Count);
var git = result.Packages.FirstOrDefault(r => r.PackageUrl.Contains("git"));
Assert.NotNull(git);
Assert.Equal("2.42.0", git.Version);
var node = result.Packages.FirstOrDefault(r => r.PackageUrl.Contains("nodejs"));
Assert.NotNull(node);
Assert.Equal("20.10.0", node.Version);
var sevenZip = result.Packages.FirstOrDefault(r => r.PackageUrl.Contains("7zip"));
Assert.NotNull(sevenZip);
Assert.Equal("23.01", sevenZip.Version);
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
[Fact]
public async Task AnalyzeAsync_ExtractsVendorMetadata()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), "choco-analyzer-test-" + Guid.NewGuid().ToString("N")[..8]);
var chocoLib = Path.Combine(tempDir, "ProgramData", "Chocolatey", "lib");
var packageDir = Path.Combine(chocoLib, "vscode.1.85.0");
Directory.CreateDirectory(packageDir);
CreateNuspecFile(packageDir, "vscode", "1.85.0", "Visual Studio Code",
"Microsoft", "Visual Studio Code editor",
"https://code.visualstudio.com",
"https://code.visualstudio.com/license");
try
{
var context = CreateContext(tempDir);
// Act
var result = await _analyzer.AnalyzeAsync(context, CancellationToken.None);
// Assert
Assert.Single(result.Packages);
var record = result.Packages[0];
Assert.Equal("vscode", record.VendorMetadata["choco:id"]);
Assert.Equal("1.85.0", record.VendorMetadata["choco:version"]);
Assert.Equal("Visual Studio Code", record.VendorMetadata["choco:title"]);
Assert.Equal("Microsoft", record.VendorMetadata["choco:authors"]);
Assert.Equal("https://code.visualstudio.com", record.VendorMetadata["choco:project_url"]);
Assert.Equal("https://code.visualstudio.com/license", record.VendorMetadata["choco:license_url"]);
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
[Fact]
public async Task AnalyzeAsync_WithInstallScript_ComputesHash()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), "choco-analyzer-test-" + Guid.NewGuid().ToString("N")[..8]);
var chocoLib = Path.Combine(tempDir, "ProgramData", "Chocolatey", "lib");
var packageDir = Path.Combine(chocoLib, "git.2.42.0");
var toolsDir = Path.Combine(packageDir, "tools");
Directory.CreateDirectory(toolsDir);
CreateNuspecFile(packageDir, "git", "2.42.0", "Git", "Git Authors", "Git for Windows");
File.WriteAllText(Path.Combine(toolsDir, "chocolateyinstall.ps1"), "Write-Host 'Installing Git'");
try
{
var context = CreateContext(tempDir);
// Act
var result = await _analyzer.AnalyzeAsync(context, CancellationToken.None);
// Assert
Assert.Single(result.Packages);
var record = result.Packages[0];
Assert.True(record.VendorMetadata.ContainsKey("choco:install_script_hash"));
Assert.StartsWith("sha256:", record.VendorMetadata["choco:install_script_hash"]);
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
[Fact]
public async Task AnalyzeAsync_FallsBackToDirectoryParsing_WhenNoNuspec()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), "choco-analyzer-test-" + Guid.NewGuid().ToString("N")[..8]);
var chocoLib = Path.Combine(tempDir, "ProgramData", "Chocolatey", "lib");
var packageDir = Path.Combine(chocoLib, "python.3.12.0");
Directory.CreateDirectory(packageDir);
// Create a file but no nuspec
File.WriteAllText(Path.Combine(packageDir, "dummy.txt"), "placeholder");
try
{
var context = CreateContext(tempDir);
// Act
var result = await _analyzer.AnalyzeAsync(context, CancellationToken.None);
// Assert
Assert.Single(result.Packages);
var record = result.Packages[0];
Assert.Equal("python", record.Name);
Assert.Equal("3.12.0", record.Version);
Assert.Equal("pkg:chocolatey/python@3.12.0", record.PackageUrl);
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
[Fact]
public async Task AnalyzeAsync_IncludesFileEvidence()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), "choco-analyzer-test-" + Guid.NewGuid().ToString("N")[..8]);
var chocoLib = Path.Combine(tempDir, "ProgramData", "Chocolatey", "lib");
var packageDir = Path.Combine(chocoLib, "git.2.42.0");
var toolsDir = Path.Combine(packageDir, "tools");
Directory.CreateDirectory(toolsDir);
CreateNuspecFile(packageDir, "git", "2.42.0", "Git", "Git Authors", "Git for Windows");
File.WriteAllText(Path.Combine(toolsDir, "chocolateyinstall.ps1"), "Write-Host 'Installing'");
File.WriteAllText(Path.Combine(toolsDir, "helper.bat"), "@echo off");
File.WriteAllText(Path.Combine(packageDir, "config.json"), "{}");
try
{
var context = CreateContext(tempDir);
// Act
var result = await _analyzer.AnalyzeAsync(context, CancellationToken.None);
// Assert
Assert.Single(result.Packages);
var record = result.Packages[0];
// Should include key files (ps1, bat, json, nuspec)
Assert.Contains(record.Files, f => f.Path.EndsWith(".ps1"));
Assert.Contains(record.Files, f => f.Path.EndsWith(".bat"));
Assert.Contains(record.Files, f => f.Path.EndsWith(".json"));
Assert.Contains(record.Files, f => f.Path.EndsWith(".nuspec"));
// Config file should be marked as config
var configFile = record.Files.FirstOrDefault(f => f.Path.EndsWith(".json"));
Assert.NotNull(configFile);
Assert.True(configFile.IsConfigFile);
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
[Fact]
public async Task AnalyzeAsync_ResultsAreSortedDeterministically()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), "choco-analyzer-test-" + Guid.NewGuid().ToString("N")[..8]);
var chocoLib = Path.Combine(tempDir, "ProgramData", "Chocolatey", "lib");
// Create packages in random order
var zetaDir = Path.Combine(chocoLib, "zeta.1.0.0");
var alphaDir = Path.Combine(chocoLib, "alpha.1.0.0");
var midDir = Path.Combine(chocoLib, "mid.1.0.0");
Directory.CreateDirectory(zetaDir);
Directory.CreateDirectory(alphaDir);
Directory.CreateDirectory(midDir);
CreateNuspecFile(zetaDir, "zeta", "1.0.0", "Zeta", "Author", "Zeta package");
CreateNuspecFile(alphaDir, "alpha", "1.0.0", "Alpha", "Author", "Alpha package");
CreateNuspecFile(midDir, "mid", "1.0.0", "Mid", "Author", "Mid package");
try
{
var context = CreateContext(tempDir);
// Act
var result = await _analyzer.AnalyzeAsync(context, CancellationToken.None);
// Assert - Results should be sorted by PURL
Assert.Equal(3, result.Packages.Count);
Assert.Equal("Alpha", result.Packages[0].Name);
Assert.Equal("Mid", result.Packages[1].Name);
Assert.Equal("Zeta", result.Packages[2].Name);
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
[Fact]
public async Task AnalyzeAsync_SkipsHiddenDirectories()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), "choco-analyzer-test-" + Guid.NewGuid().ToString("N")[..8]);
var chocoLib = Path.Combine(tempDir, "ProgramData", "Chocolatey", "lib");
var validDir = Path.Combine(chocoLib, "git.2.42.0");
var hiddenDir = Path.Combine(chocoLib, ".hidden");
Directory.CreateDirectory(validDir);
Directory.CreateDirectory(hiddenDir);
CreateNuspecFile(validDir, "git", "2.42.0", "Git", "Author", "Git");
CreateNuspecFile(hiddenDir, "hidden", "1.0.0", "Hidden", "Author", "Hidden package");
try
{
var context = CreateContext(tempDir);
// Act
var result = await _analyzer.AnalyzeAsync(context, CancellationToken.None);
// Assert - Only valid package should be returned
Assert.Single(result.Packages);
Assert.Equal("Git", result.Packages[0].Name);
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
[Fact]
public async Task AnalyzeAsync_HandlesLowerCaseChocolateyPath()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), "choco-analyzer-test-" + Guid.NewGuid().ToString("N")[..8]);
var chocoLib = Path.Combine(tempDir, "ProgramData", "chocolatey", "lib"); // lowercase
var packageDir = Path.Combine(chocoLib, "git.2.42.0");
Directory.CreateDirectory(packageDir);
CreateNuspecFile(packageDir, "git", "2.42.0", "Git", "Author", "Git");
try
{
var context = CreateContext(tempDir);
// Act
var result = await _analyzer.AnalyzeAsync(context, CancellationToken.None);
// Assert
Assert.Single(result.Packages);
Assert.Equal("Git", result.Packages[0].Name);
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
[Fact]
public async Task AnalyzeAsync_TruncatesLongDescription()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), "choco-analyzer-test-" + Guid.NewGuid().ToString("N")[..8]);
var chocoLib = Path.Combine(tempDir, "ProgramData", "Chocolatey", "lib");
var packageDir = Path.Combine(chocoLib, "longdesc.1.0.0");
Directory.CreateDirectory(packageDir);
var longDescription = new string('A', 500);
CreateNuspecFile(packageDir, "longdesc", "1.0.0", "LongDesc", "Author", longDescription);
try
{
var context = CreateContext(tempDir);
// Act
var result = await _analyzer.AnalyzeAsync(context, CancellationToken.None);
// Assert
Assert.Single(result.Packages);
var record = result.Packages[0];
var description = record.VendorMetadata["choco:description"];
Assert.True(description!.Length <= 200);
Assert.EndsWith("...", description);
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
[Fact]
public async Task AnalyzeAsync_WithCancellation_ThrowsOperationCanceledException()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), "choco-analyzer-test-" + Guid.NewGuid().ToString("N")[..8]);
var chocoLib = Path.Combine(tempDir, "ProgramData", "Chocolatey", "lib");
var packageDir = Path.Combine(chocoLib, "git.2.42.0");
Directory.CreateDirectory(packageDir);
CreateNuspecFile(packageDir, "git", "2.42.0", "Git", "Author", "Git");
using var cts = new CancellationTokenSource();
cts.Cancel();
try
{
var context = CreateContext(tempDir);
// Act & Assert
await Assert.ThrowsAsync<OperationCanceledException>(
async () => await _analyzer.AnalyzeAsync(context, cts.Token));
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
private static void CreateNuspecFile(
string packageDir,
string id,
string version,
string title,
string authors,
string description,
string? projectUrl = null,
string? licenseUrl = null)
{
var nuspecPath = Path.Combine(packageDir, $"{id}.nuspec");
var projectUrlElement = projectUrl is not null ? $"<projectUrl>{projectUrl}</projectUrl>" : "";
var licenseUrlElement = licenseUrl is not null ? $"<licenseUrl>{licenseUrl}</licenseUrl>" : "";
var content = $@"<?xml version=""1.0"" encoding=""utf-8""?>
<package xmlns=""http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd"">
<metadata>
<id>{id}</id>
<version>{version}</version>
<title>{title}</title>
<authors>{authors}</authors>
<description>{description}</description>
{projectUrlElement}
{licenseUrlElement}
</metadata>
</package>";
File.WriteAllText(nuspecPath, content);
}
}

View File

@@ -0,0 +1,408 @@
using StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey;
using Xunit;
namespace StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey.Tests;
public class NuspecParserTests
{
private readonly NuspecParser _parser = new();
[Fact]
public void Parse_WithValidNuspec_ReturnsMetadata()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), "nuspec-parser-test-" + Guid.NewGuid().ToString("N")[..8]);
Directory.CreateDirectory(tempDir);
var nuspecPath = Path.Combine(tempDir, "git.nuspec");
File.WriteAllText(nuspecPath, @"<?xml version=""1.0"" encoding=""utf-8""?>
<package xmlns=""http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd"">
<metadata>
<id>git</id>
<version>2.42.0</version>
<title>Git</title>
<authors>Git Authors</authors>
<description>Git for Windows</description>
<projectUrl>https://git-scm.com</projectUrl>
<licenseUrl>https://opensource.org/licenses/MIT</licenseUrl>
</metadata>
</package>");
try
{
// Act
var result = _parser.Parse(nuspecPath, tempDir);
// Assert
Assert.NotNull(result);
Assert.Equal("git", result.Id);
Assert.Equal("2.42.0", result.Version);
Assert.Equal("Git", result.Title);
Assert.Equal("Git Authors", result.Authors);
Assert.Equal("Git for Windows", result.Description);
Assert.Equal("https://git-scm.com", result.ProjectUrl);
Assert.Equal("https://opensource.org/licenses/MIT", result.LicenseUrl);
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
[Fact]
public void Parse_WithOldNamespace_ReturnsMetadata()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), "nuspec-parser-test-" + Guid.NewGuid().ToString("N")[..8]);
Directory.CreateDirectory(tempDir);
var nuspecPath = Path.Combine(tempDir, "git.nuspec");
File.WriteAllText(nuspecPath, @"<?xml version=""1.0"" encoding=""utf-8""?>
<package xmlns=""http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"">
<metadata>
<id>git</id>
<version>2.42.0</version>
</metadata>
</package>");
try
{
// Act
var result = _parser.Parse(nuspecPath, tempDir);
// Assert
Assert.NotNull(result);
Assert.Equal("git", result.Id);
Assert.Equal("2.42.0", result.Version);
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
[Fact]
public void Parse_WithOld2011Namespace_ReturnsMetadata()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), "nuspec-parser-test-" + Guid.NewGuid().ToString("N")[..8]);
Directory.CreateDirectory(tempDir);
var nuspecPath = Path.Combine(tempDir, "git.nuspec");
File.WriteAllText(nuspecPath, @"<?xml version=""1.0"" encoding=""utf-8""?>
<package xmlns=""http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd"">
<metadata>
<id>git</id>
<version>2.42.0</version>
</metadata>
</package>");
try
{
// Act
var result = _parser.Parse(nuspecPath, tempDir);
// Assert
Assert.NotNull(result);
Assert.Equal("git", result.Id);
Assert.Equal("2.42.0", result.Version);
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
[Fact]
public void Parse_WithNoNamespace_ReturnsMetadata()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), "nuspec-parser-test-" + Guid.NewGuid().ToString("N")[..8]);
Directory.CreateDirectory(tempDir);
var nuspecPath = Path.Combine(tempDir, "git.nuspec");
File.WriteAllText(nuspecPath, @"<?xml version=""1.0"" encoding=""utf-8""?>
<package>
<metadata>
<id>git</id>
<version>2.42.0</version>
</metadata>
</package>");
try
{
// Act
var result = _parser.Parse(nuspecPath, tempDir);
// Assert
Assert.NotNull(result);
Assert.Equal("git", result.Id);
Assert.Equal("2.42.0", result.Version);
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
[Fact]
public void Parse_WithMissingId_ReturnsNull()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), "nuspec-parser-test-" + Guid.NewGuid().ToString("N")[..8]);
Directory.CreateDirectory(tempDir);
var nuspecPath = Path.Combine(tempDir, "invalid.nuspec");
File.WriteAllText(nuspecPath, @"<?xml version=""1.0""?>
<package xmlns=""http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd"">
<metadata>
<version>1.0.0</version>
</metadata>
</package>");
try
{
// Act
var result = _parser.Parse(nuspecPath, tempDir);
// Assert
Assert.Null(result);
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
[Fact]
public void Parse_WithMissingVersion_ReturnsNull()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), "nuspec-parser-test-" + Guid.NewGuid().ToString("N")[..8]);
Directory.CreateDirectory(tempDir);
var nuspecPath = Path.Combine(tempDir, "invalid.nuspec");
File.WriteAllText(nuspecPath, @"<?xml version=""1.0""?>
<package xmlns=""http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd"">
<metadata>
<id>test</id>
</metadata>
</package>");
try
{
// Act
var result = _parser.Parse(nuspecPath, tempDir);
// Assert
Assert.Null(result);
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
[Fact]
public void Parse_WithInvalidXml_ReturnsNull()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), "nuspec-parser-test-" + Guid.NewGuid().ToString("N")[..8]);
Directory.CreateDirectory(tempDir);
var nuspecPath = Path.Combine(tempDir, "invalid.nuspec");
File.WriteAllText(nuspecPath, "not valid xml");
try
{
// Act
var result = _parser.Parse(nuspecPath, tempDir);
// Assert
Assert.Null(result);
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
[Fact]
public void Parse_WithNonExistentFile_ReturnsNull()
{
// Act
var result = _parser.Parse("/nonexistent/path/file.nuspec", "/nonexistent");
// Assert
Assert.Null(result);
}
[Fact]
public void Parse_WithNullPath_ReturnsNull()
{
// Act
var result = _parser.Parse(null!, "/some/path");
// Assert
Assert.Null(result);
}
[Fact]
public void Parse_WithEmptyPath_ReturnsNull()
{
// Act
var result = _parser.Parse("", "/some/path");
// Assert
Assert.Null(result);
}
[Fact]
public void Parse_WithWhitespacePath_ReturnsNull()
{
// Act
var result = _parser.Parse(" ", "/some/path");
// Assert
Assert.Null(result);
}
[Fact]
public void Parse_ComputesInstallScriptHash_FromToolsDirectory()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), "nuspec-parser-test-" + Guid.NewGuid().ToString("N")[..8]);
var toolsDir = Path.Combine(tempDir, "tools");
Directory.CreateDirectory(toolsDir);
var nuspecPath = Path.Combine(tempDir, "git.nuspec");
File.WriteAllText(nuspecPath, @"<?xml version=""1.0""?>
<package xmlns=""http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd"">
<metadata>
<id>git</id>
<version>2.42.0</version>
</metadata>
</package>");
File.WriteAllText(Path.Combine(toolsDir, "chocolateyinstall.ps1"), "Write-Host 'Installing'");
try
{
// Act
var result = _parser.Parse(nuspecPath, tempDir);
// Assert
Assert.NotNull(result);
Assert.NotNull(result.InstallScriptHash);
Assert.StartsWith("sha256:", result.InstallScriptHash);
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
[Fact]
public void Parse_ComputesInstallScriptHash_FromRootDirectory()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), "nuspec-parser-test-" + Guid.NewGuid().ToString("N")[..8]);
Directory.CreateDirectory(tempDir);
var nuspecPath = Path.Combine(tempDir, "git.nuspec");
File.WriteAllText(nuspecPath, @"<?xml version=""1.0""?>
<package xmlns=""http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd"">
<metadata>
<id>git</id>
<version>2.42.0</version>
</metadata>
</package>");
File.WriteAllText(Path.Combine(tempDir, "chocolateyinstall.ps1"), "Write-Host 'Installing'");
try
{
// Act
var result = _parser.Parse(nuspecPath, tempDir);
// Assert
Assert.NotNull(result);
Assert.NotNull(result.InstallScriptHash);
Assert.StartsWith("sha256:", result.InstallScriptHash);
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
[Fact]
public void Parse_EnumeratesInstalledFiles()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), "nuspec-parser-test-" + Guid.NewGuid().ToString("N")[..8]);
var toolsDir = Path.Combine(tempDir, "tools");
Directory.CreateDirectory(toolsDir);
var nuspecPath = Path.Combine(tempDir, "git.nuspec");
File.WriteAllText(nuspecPath, @"<?xml version=""1.0""?>
<package xmlns=""http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd"">
<metadata>
<id>git</id>
<version>2.42.0</version>
</metadata>
</package>");
File.WriteAllText(Path.Combine(toolsDir, "chocolateyinstall.ps1"), "Write-Host 'Installing'");
File.WriteAllText(Path.Combine(tempDir, "config.json"), "{}");
try
{
// Act
var result = _parser.Parse(nuspecPath, tempDir);
// Assert
Assert.NotNull(result);
Assert.Contains(result.InstalledFiles, f => f.Contains("chocolateyinstall.ps1"));
Assert.Contains(result.InstalledFiles, f => f.Contains("config.json"));
Assert.Contains(result.InstalledFiles, f => f.Contains("git.nuspec"));
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
[Theory]
[InlineData("git.2.42.0", "git", "2.42.0")]
[InlineData("nodejs.20.10.0", "nodejs", "20.10.0")]
[InlineData("7zip.23.01", "7zip", "23.01")]
[InlineData("python.3.12.0", "python", "3.12.0")]
[InlineData("dotnet-sdk.8.0.100-rc.1", "dotnet-sdk", "8.0.100-rc.1")]
[InlineData("Microsoft.WindowsTerminal.1.18.3181.0", "Microsoft.WindowsTerminal", "1.18.3181.0")]
public void ParsePackageDirectory_WithValidFormat_ReturnsIdAndVersion(string dirName, string expectedId, string expectedVersion)
{
// Act
var result = NuspecParser.ParsePackageDirectory(dirName);
// Assert
Assert.NotNull(result);
Assert.Equal(expectedId, result.Value.Id);
Assert.Equal(expectedVersion, result.Value.Version);
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
[InlineData("git")]
[InlineData("no-version-here")]
public void ParsePackageDirectory_WithInvalidFormat_ReturnsNull(string? dirName)
{
// Act
var result = NuspecParser.ParsePackageDirectory(dirName!);
// Assert
Assert.Null(result);
}
}

View File

@@ -0,0 +1,27 @@
<?xml version='1.0' encoding='utf-8'?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<UseConcelierTestInfra>false</UseConcelierTestInfra>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-rc.2.25502.107" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageReference Include="coverlet.collector" Version="6.0.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.OS/StellaOps.Scanner.Analyzers.OS.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey/StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>