feat(api): Implement Console Export Client and Models
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
mock-dev-release / package-mock-release (push) Has been cancelled
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
mock-dev-release / package-mock-release (push) Has been cancelled
- Added ConsoleExportClient for managing export requests and responses. - Introduced ConsoleExportRequest and ConsoleExportResponse models. - Implemented methods for creating and retrieving exports with appropriate headers. feat(crypto): Add Software SM2/SM3 Cryptography Provider - Implemented SmSoftCryptoProvider for software-only SM2/SM3 cryptography. - Added support for signing and verification using SM2 algorithm. - Included hashing functionality with SM3 algorithm. - Configured options for loading keys from files and environment gate checks. test(crypto): Add unit tests for SmSoftCryptoProvider - Created comprehensive tests for signing, verifying, and hashing functionalities. - Ensured correct behavior for key management and error handling. feat(api): Enhance Console Export Models - Expanded ConsoleExport models to include detailed status and event types. - Added support for various export formats and notification options. test(time): Implement TimeAnchorPolicyService tests - Developed tests for TimeAnchorPolicyService to validate time anchors. - Covered scenarios for anchor validation, drift calculation, and policy enforcement.
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.DotNet.Internal.Bundling;
|
||||
using StellaOps.Scanner.Analyzers.Lang.DotNet.Tests.TestUtilities;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.DotNet.Tests.DotNet.Bundling;
|
||||
|
||||
public sealed class ILMergedAssemblyDetectorTests
|
||||
{
|
||||
[Fact]
|
||||
public void DetectsCosturaFody()
|
||||
{
|
||||
var tempDir = DotNetFixtureBuilder.CreateTemporaryDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
var assemblyPath = DotNetFixtureBuilder.CreateMockILMergedAssembly(
|
||||
tempDir, "CosturaApp.exe", BundlingTool.CosturaFody);
|
||||
|
||||
var result = ILMergedAssemblyDetector.Analyze(assemblyPath);
|
||||
|
||||
Assert.True(result.IsMerged);
|
||||
Assert.Equal(BundlingTool.CosturaFody, result.Tool);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DotNetFixtureBuilder.SafeDelete(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetectsILMergeMarker()
|
||||
{
|
||||
var tempDir = DotNetFixtureBuilder.CreateTemporaryDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
var assemblyPath = DotNetFixtureBuilder.CreateMockILMergedAssembly(
|
||||
tempDir, "ILMergedApp.exe", BundlingTool.ILMerge);
|
||||
|
||||
var result = ILMergedAssemblyDetector.Analyze(assemblyPath);
|
||||
|
||||
Assert.True(result.IsMerged);
|
||||
Assert.Equal(BundlingTool.ILMerge, result.Tool);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DotNetFixtureBuilder.SafeDelete(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetectsILRepackMarker()
|
||||
{
|
||||
var tempDir = DotNetFixtureBuilder.CreateTemporaryDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
var assemblyPath = DotNetFixtureBuilder.CreateMockILMergedAssembly(
|
||||
tempDir, "ILRepackApp.exe", BundlingTool.ILRepack);
|
||||
|
||||
var result = ILMergedAssemblyDetector.Analyze(assemblyPath);
|
||||
|
||||
Assert.True(result.IsMerged);
|
||||
Assert.Equal(BundlingTool.ILRepack, result.Tool);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DotNetFixtureBuilder.SafeDelete(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReturnsNotMergedForNormalAssembly()
|
||||
{
|
||||
var tempDir = DotNetFixtureBuilder.CreateTemporaryDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
// Create a minimal PE file without any bundling markers
|
||||
var assemblyPath = Path.Combine(tempDir, "Normal.exe");
|
||||
var content = new byte[1024];
|
||||
content[0] = 0x4D; // 'M'
|
||||
content[1] = 0x5A; // 'Z'
|
||||
File.WriteAllBytes(assemblyPath, content);
|
||||
|
||||
var result = ILMergedAssemblyDetector.Analyze(assemblyPath);
|
||||
|
||||
Assert.False(result.IsMerged);
|
||||
Assert.Equal(BundlingTool.None, result.Tool);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DotNetFixtureBuilder.SafeDelete(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandlesNonExistentFile()
|
||||
{
|
||||
var result = ILMergedAssemblyDetector.Analyze("/nonexistent/assembly.exe");
|
||||
|
||||
Assert.False(result.IsMerged);
|
||||
Assert.Equal(ILMergeDetectionResult.NotMerged, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandlesEmptyPath()
|
||||
{
|
||||
var result = ILMergedAssemblyDetector.Analyze("");
|
||||
|
||||
Assert.False(result.IsMerged);
|
||||
Assert.Equal(ILMergeDetectionResult.NotMerged, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandlesNullPath()
|
||||
{
|
||||
var result = ILMergedAssemblyDetector.Analyze(null!);
|
||||
|
||||
Assert.False(result.IsMerged);
|
||||
Assert.Equal(ILMergeDetectionResult.NotMerged, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnalyzeManyFiltersNonMerged()
|
||||
{
|
||||
var tempDir = DotNetFixtureBuilder.CreateTemporaryDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
var mergedPath = DotNetFixtureBuilder.CreateMockILMergedAssembly(
|
||||
tempDir, "Merged.exe", BundlingTool.CosturaFody);
|
||||
|
||||
// Create a normal file
|
||||
var normalPath = Path.Combine(tempDir, "Normal.exe");
|
||||
var content = new byte[1024];
|
||||
content[0] = 0x4D;
|
||||
content[1] = 0x5A;
|
||||
File.WriteAllBytes(normalPath, content);
|
||||
|
||||
var results = ILMergedAssemblyDetector.AnalyzeMany(
|
||||
[mergedPath, normalPath],
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.Single(results);
|
||||
Assert.True(results[0].IsMerged);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DotNetFixtureBuilder.SafeDelete(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnalyzeManyRespectsCancellation()
|
||||
{
|
||||
var tempDir = DotNetFixtureBuilder.CreateTemporaryDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
var assemblyPath = DotNetFixtureBuilder.CreateMockILMergedAssembly(
|
||||
tempDir, "App.exe", BundlingTool.CosturaFody);
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
cts.Cancel();
|
||||
|
||||
Assert.Throws<OperationCanceledException>(() =>
|
||||
ILMergedAssemblyDetector.AnalyzeMany([assemblyPath], cts.Token));
|
||||
}
|
||||
finally
|
||||
{
|
||||
DotNetFixtureBuilder.SafeDelete(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizesAssemblyPath()
|
||||
{
|
||||
var tempDir = DotNetFixtureBuilder.CreateTemporaryDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
var assemblyPath = DotNetFixtureBuilder.CreateMockILMergedAssembly(
|
||||
tempDir, "App.exe", BundlingTool.CosturaFody);
|
||||
|
||||
var result = ILMergedAssemblyDetector.Analyze(assemblyPath);
|
||||
|
||||
Assert.NotNull(result.AssemblyPath);
|
||||
Assert.DoesNotContain("\\", result.AssemblyPath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DotNetFixtureBuilder.SafeDelete(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetectsEmbeddedDllPatterns()
|
||||
{
|
||||
var tempDir = DotNetFixtureBuilder.CreateTemporaryDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
// Create a file with many .dll patterns (triggers the embedded DLL heuristic)
|
||||
var assemblyPath = Path.Combine(tempDir, "ManyDlls.exe");
|
||||
var content = new byte[10000];
|
||||
content[0] = 0x4D;
|
||||
content[1] = 0x5A;
|
||||
|
||||
var dllPattern = ".dll"u8.ToArray();
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
Array.Copy(dllPattern, 0, content, 100 + i * 100, dllPattern.Length);
|
||||
}
|
||||
|
||||
File.WriteAllBytes(assemblyPath, content);
|
||||
|
||||
var result = ILMergedAssemblyDetector.Analyze(assemblyPath);
|
||||
|
||||
Assert.True(result.IsMerged);
|
||||
Assert.Contains(result.Indicators, i => i.Contains("embedded assembly patterns"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
DotNetFixtureBuilder.SafeDelete(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetectsAssemblyLoaderPattern()
|
||||
{
|
||||
var tempDir = DotNetFixtureBuilder.CreateTemporaryDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
var assemblyPath = Path.Combine(tempDir, "WithLoader.exe");
|
||||
var content = new byte[5000];
|
||||
content[0] = 0x4D;
|
||||
content[1] = 0x5A;
|
||||
|
||||
// Add AssemblyLoader and ResolveAssembly patterns
|
||||
var loaderPattern = "AssemblyLoader"u8.ToArray();
|
||||
var resolvePattern = "ResolveAssembly"u8.ToArray();
|
||||
Array.Copy(loaderPattern, 0, content, 100, loaderPattern.Length);
|
||||
Array.Copy(resolvePattern, 0, content, 200, resolvePattern.Length);
|
||||
|
||||
File.WriteAllBytes(assemblyPath, content);
|
||||
|
||||
var result = ILMergedAssemblyDetector.Analyze(assemblyPath);
|
||||
|
||||
Assert.True(result.IsMerged);
|
||||
Assert.Contains(result.Indicators, i => i.Contains("Assembly loader pattern"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
DotNetFixtureBuilder.SafeDelete(tempDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.DotNet.Internal.Bundling;
|
||||
using StellaOps.Scanner.Analyzers.Lang.DotNet.Tests.TestUtilities;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.DotNet.Tests.DotNet.Bundling;
|
||||
|
||||
public sealed class SingleFileAppDetectorTests
|
||||
{
|
||||
[Fact]
|
||||
public void DetectsBundleSignature()
|
||||
{
|
||||
var tempDir = DotNetFixtureBuilder.CreateTemporaryDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
var bundlePath = DotNetFixtureBuilder.CreateMockSingleFileBundle(
|
||||
tempDir, "SingleFileApp.exe");
|
||||
|
||||
var result = SingleFileAppDetector.Analyze(bundlePath);
|
||||
|
||||
Assert.True(result.IsSingleFile);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DotNetFixtureBuilder.SafeDelete(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RejectsNonMZHeader()
|
||||
{
|
||||
var tempDir = DotNetFixtureBuilder.CreateTemporaryDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
var filePath = Path.Combine(tempDir, "NotPE.exe");
|
||||
var content = new byte[1024];
|
||||
content[0] = 0x00;
|
||||
content[1] = 0x00;
|
||||
File.WriteAllBytes(filePath, content);
|
||||
|
||||
var result = SingleFileAppDetector.Analyze(filePath);
|
||||
|
||||
Assert.False(result.IsSingleFile);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DotNetFixtureBuilder.SafeDelete(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandlesSmallFile()
|
||||
{
|
||||
var tempDir = DotNetFixtureBuilder.CreateTemporaryDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
var filePath = Path.Combine(tempDir, "Small.exe");
|
||||
var content = new byte[50]; // < 100KB
|
||||
content[0] = 0x4D;
|
||||
content[1] = 0x5A;
|
||||
File.WriteAllBytes(filePath, content);
|
||||
|
||||
var result = SingleFileAppDetector.Analyze(filePath);
|
||||
|
||||
Assert.False(result.IsSingleFile);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DotNetFixtureBuilder.SafeDelete(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandlesNonExistentFile()
|
||||
{
|
||||
var result = SingleFileAppDetector.Analyze("/nonexistent/app.exe");
|
||||
|
||||
Assert.False(result.IsSingleFile);
|
||||
Assert.Equal(SingleFileDetectionResult.NotSingleFile, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandlesEmptyPath()
|
||||
{
|
||||
var result = SingleFileAppDetector.Analyze("");
|
||||
|
||||
Assert.False(result.IsSingleFile);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandlesNullPath()
|
||||
{
|
||||
var result = SingleFileAppDetector.Analyze(null!);
|
||||
|
||||
Assert.False(result.IsSingleFile);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnalyzeManyFiltersNonBundled()
|
||||
{
|
||||
var tempDir = DotNetFixtureBuilder.CreateTemporaryDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
var bundlePath = DotNetFixtureBuilder.CreateMockSingleFileBundle(
|
||||
tempDir, "Bundle.exe");
|
||||
|
||||
// Create a normal file
|
||||
var normalPath = Path.Combine(tempDir, "Normal.exe");
|
||||
var content = new byte[1024];
|
||||
content[0] = 0x4D;
|
||||
content[1] = 0x5A;
|
||||
File.WriteAllBytes(normalPath, content);
|
||||
|
||||
var results = SingleFileAppDetector.AnalyzeMany(
|
||||
[bundlePath, normalPath],
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.Single(results);
|
||||
Assert.True(results[0].IsSingleFile);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DotNetFixtureBuilder.SafeDelete(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnalyzeManyRespectsCancellation()
|
||||
{
|
||||
var tempDir = DotNetFixtureBuilder.CreateTemporaryDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
var bundlePath = DotNetFixtureBuilder.CreateMockSingleFileBundle(
|
||||
tempDir, "Bundle.exe");
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
cts.Cancel();
|
||||
|
||||
Assert.Throws<OperationCanceledException>(() =>
|
||||
SingleFileAppDetector.AnalyzeMany([bundlePath], cts.Token));
|
||||
}
|
||||
finally
|
||||
{
|
||||
DotNetFixtureBuilder.SafeDelete(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizesFilePath()
|
||||
{
|
||||
var tempDir = DotNetFixtureBuilder.CreateTemporaryDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
var bundlePath = DotNetFixtureBuilder.CreateMockSingleFileBundle(
|
||||
tempDir, "Bundle.exe");
|
||||
|
||||
var result = SingleFileAppDetector.Analyze(bundlePath);
|
||||
|
||||
Assert.NotNull(result.FilePath);
|
||||
Assert.DoesNotContain("\\", result.FilePath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DotNetFixtureBuilder.SafeDelete(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetectsEmbeddedDllPatterns()
|
||||
{
|
||||
var tempDir = DotNetFixtureBuilder.CreateTemporaryDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
var bundlePath = DotNetFixtureBuilder.CreateMockSingleFileBundle(
|
||||
tempDir, "Bundle.exe");
|
||||
|
||||
var result = SingleFileAppDetector.Analyze(bundlePath);
|
||||
|
||||
Assert.True(result.IsSingleFile);
|
||||
Assert.Contains(result.Indicators, i => i.Contains(".dll"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
DotNetFixtureBuilder.SafeDelete(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EstimatesBundledAssemblyCount()
|
||||
{
|
||||
var tempDir = DotNetFixtureBuilder.CreateTemporaryDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
var bundlePath = DotNetFixtureBuilder.CreateMockSingleFileBundle(
|
||||
tempDir, "Bundle.exe");
|
||||
|
||||
var result = SingleFileAppDetector.Analyze(bundlePath);
|
||||
|
||||
Assert.True(result.IsSingleFile);
|
||||
Assert.True(result.EstimatedBundledAssemblies >= 0);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DotNetFixtureBuilder.SafeDelete(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetectsSystemNamespacePatterns()
|
||||
{
|
||||
var tempDir = DotNetFixtureBuilder.CreateTemporaryDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
var bundlePath = DotNetFixtureBuilder.CreateMockSingleFileBundle(
|
||||
tempDir, "Bundle.exe");
|
||||
|
||||
var result = SingleFileAppDetector.Analyze(bundlePath);
|
||||
|
||||
Assert.True(result.IsSingleFile);
|
||||
Assert.Contains(result.Indicators, i => i.Contains("System."));
|
||||
}
|
||||
finally
|
||||
{
|
||||
DotNetFixtureBuilder.SafeDelete(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifiesMZHeader()
|
||||
{
|
||||
var tempDir = DotNetFixtureBuilder.CreateTemporaryDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
var bundlePath = DotNetFixtureBuilder.CreateMockSingleFileBundle(
|
||||
tempDir, "Bundle.exe");
|
||||
|
||||
// Read the file and verify MZ header
|
||||
var bytes = File.ReadAllBytes(bundlePath);
|
||||
Assert.Equal(0x4D, bytes[0]); // 'M'
|
||||
Assert.Equal(0x5A, bytes[1]); // 'Z'
|
||||
|
||||
var result = SingleFileAppDetector.Analyze(bundlePath);
|
||||
Assert.True(result.IsSingleFile);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DotNetFixtureBuilder.SafeDelete(tempDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.DotNet.Internal.Config;
|
||||
using StellaOps.Scanner.Analyzers.Lang.DotNet.Tests.TestUtilities;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.DotNet.Tests.DotNet.Config;
|
||||
|
||||
public sealed class GlobalJsonParserTests
|
||||
{
|
||||
[Fact]
|
||||
public void ParsesSdkVersion()
|
||||
{
|
||||
var content = """
|
||||
{
|
||||
"sdk": {
|
||||
"version": "8.0.100"
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var result = GlobalJsonParser.Parse(content);
|
||||
|
||||
Assert.Equal("8.0.100", result.SdkVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesRollForward()
|
||||
{
|
||||
var content = """
|
||||
{
|
||||
"sdk": {
|
||||
"version": "8.0.100",
|
||||
"rollForward": "latestMinor"
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var result = GlobalJsonParser.Parse(content);
|
||||
|
||||
Assert.Equal("latestMinor", result.RollForward);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesAllowPrerelease()
|
||||
{
|
||||
var content = """
|
||||
{
|
||||
"sdk": {
|
||||
"version": "9.0.100-preview.1",
|
||||
"allowPrerelease": true
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var result = GlobalJsonParser.Parse(content);
|
||||
|
||||
Assert.True(result.AllowPrerelease);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesMsBuildSdks()
|
||||
{
|
||||
var content = """
|
||||
{
|
||||
"sdk": {
|
||||
"version": "8.0.100"
|
||||
},
|
||||
"msbuild-sdks": {
|
||||
"Microsoft.Build.Traversal": "3.4.0",
|
||||
"Microsoft.Build.CentralPackageVersions": "2.1.3"
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var result = GlobalJsonParser.Parse(content);
|
||||
|
||||
Assert.Equal(2, result.MsBuildSdks.Count);
|
||||
Assert.Equal("3.4.0", result.MsBuildSdks["Microsoft.Build.Traversal"]);
|
||||
Assert.Equal("2.1.3", result.MsBuildSdks["Microsoft.Build.CentralPackageVersions"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandlesMissingSdkSection()
|
||||
{
|
||||
var content = """
|
||||
{
|
||||
"msbuild-sdks": {
|
||||
"Microsoft.Build.Traversal": "3.4.0"
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var result = GlobalJsonParser.Parse(content);
|
||||
|
||||
Assert.Null(result.SdkVersion);
|
||||
Assert.Single(result.MsBuildSdks);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandlesEmptyFile()
|
||||
{
|
||||
var content = "";
|
||||
|
||||
var result = GlobalJsonParser.Parse(content);
|
||||
|
||||
Assert.Equal(GlobalJsonParser.Empty, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandlesMalformedJson()
|
||||
{
|
||||
var content = "{ invalid json";
|
||||
|
||||
var result = GlobalJsonParser.Parse(content);
|
||||
|
||||
Assert.Equal(GlobalJsonParser.Empty, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandlesNonExistentFileAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var result = await GlobalJsonParser.ParseAsync("/nonexistent/global.json", cancellationToken);
|
||||
|
||||
Assert.Equal(GlobalJsonParser.Empty, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindNearestTraversesUp()
|
||||
{
|
||||
var tempDir = DotNetFixtureBuilder.CreateTemporaryDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
var childDir = Path.Combine(tempDir, "src", "project");
|
||||
Directory.CreateDirectory(childDir);
|
||||
DotNetFixtureBuilder.CreateGlobalJson(tempDir, "8.0.100");
|
||||
|
||||
var found = GlobalJsonParser.FindNearest(childDir);
|
||||
|
||||
Assert.NotNull(found);
|
||||
Assert.EndsWith("global.json", found);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DotNetFixtureBuilder.SafeDelete(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindNearestRespectsRoot()
|
||||
{
|
||||
var tempDir = DotNetFixtureBuilder.CreateTemporaryDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
var parentDir = Directory.GetParent(tempDir)!.FullName;
|
||||
var childDir = Path.Combine(tempDir, "src");
|
||||
Directory.CreateDirectory(childDir);
|
||||
|
||||
// Create global.json in parent (outside root boundary)
|
||||
DotNetFixtureBuilder.CreateGlobalJson(parentDir, "8.0.100");
|
||||
|
||||
var found = GlobalJsonParser.FindNearest(childDir, tempDir);
|
||||
|
||||
Assert.Null(found);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DotNetFixtureBuilder.SafeDelete(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindNearestRespectsMaxDepth()
|
||||
{
|
||||
var tempDir = DotNetFixtureBuilder.CreateTemporaryDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
// Create a deeply nested structure (more than 10 levels)
|
||||
var deepDir = tempDir;
|
||||
for (var i = 0; i < 15; i++)
|
||||
{
|
||||
deepDir = Path.Combine(deepDir, $"level{i}");
|
||||
}
|
||||
Directory.CreateDirectory(deepDir);
|
||||
|
||||
// global.json at root
|
||||
DotNetFixtureBuilder.CreateGlobalJson(tempDir, "8.0.100");
|
||||
|
||||
var found = GlobalJsonParser.FindNearest(deepDir);
|
||||
|
||||
// Should not find it because max depth is 10
|
||||
Assert.Null(found);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DotNetFixtureBuilder.SafeDelete(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizesPath()
|
||||
{
|
||||
var content = """
|
||||
{
|
||||
"sdk": {
|
||||
"version": "8.0.100"
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var result = GlobalJsonParser.Parse(content, @"C:\Projects\global.json");
|
||||
|
||||
Assert.Equal("C:/Projects/global.json", result.SourcePath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParsesFileAsyncSuccessfullyAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempDir = DotNetFixtureBuilder.CreateTemporaryDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
var globalJsonPath = DotNetFixtureBuilder.CreateGlobalJson(
|
||||
tempDir, "8.0.100", "latestMinor", true);
|
||||
|
||||
var result = await GlobalJsonParser.ParseAsync(globalJsonPath, cancellationToken);
|
||||
|
||||
Assert.Equal("8.0.100", result.SdkVersion);
|
||||
Assert.Equal("latestMinor", result.RollForward);
|
||||
Assert.True(result.AllowPrerelease);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DotNetFixtureBuilder.SafeDelete(tempDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.DotNet.Internal.Config;
|
||||
using StellaOps.Scanner.Analyzers.Lang.DotNet.Tests.TestUtilities;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.DotNet.Tests.DotNet.Config;
|
||||
|
||||
public sealed class NuGetConfigParserTests
|
||||
{
|
||||
[Fact]
|
||||
public void ParsesPackageSources()
|
||||
{
|
||||
var content = """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
<add key="myget" value="https://myget.org/F/feed/api/v3/index.json" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
""";
|
||||
|
||||
var result = NuGetConfigParser.Parse(content);
|
||||
|
||||
Assert.Equal(2, result.PackageSources.Length);
|
||||
Assert.Contains(result.PackageSources, s => s.Name == "nuget.org");
|
||||
Assert.Contains(result.PackageSources, s => s.Name == "myget");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesProtocolVersion()
|
||||
{
|
||||
var content = """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
""";
|
||||
|
||||
var result = NuGetConfigParser.Parse(content);
|
||||
|
||||
Assert.Single(result.PackageSources);
|
||||
Assert.Equal("3", result.PackageSources[0].ProtocolVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetectsDisabledSources()
|
||||
{
|
||||
var content = """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
<add key="disabled-feed" value="https://disabled.example.com/index.json" />
|
||||
</packageSources>
|
||||
<disabledPackageSources>
|
||||
<add key="disabled-feed" value="true" />
|
||||
</disabledPackageSources>
|
||||
</configuration>
|
||||
""";
|
||||
|
||||
var result = NuGetConfigParser.Parse(content);
|
||||
|
||||
Assert.Equal(2, result.PackageSources.Length);
|
||||
var disabledSource = result.PackageSources.First(s => s.Name == "disabled-feed");
|
||||
Assert.False(disabledSource.IsEnabled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesCredentialsUsername()
|
||||
{
|
||||
var content = """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<add key="private-feed" value="https://private.example.com/index.json" />
|
||||
</packageSources>
|
||||
<packageSourceCredentials>
|
||||
<private-feed>
|
||||
<add key="Username" value="myuser" />
|
||||
<add key="ClearTextPassword" value="secret123" />
|
||||
</private-feed>
|
||||
</packageSourceCredentials>
|
||||
</configuration>
|
||||
""";
|
||||
|
||||
var result = NuGetConfigParser.Parse(content);
|
||||
|
||||
Assert.True(result.HasCredentials);
|
||||
Assert.True(result.Credentials.ContainsKey("private-feed"));
|
||||
Assert.Equal("myuser", result.Credentials["private-feed"].Username);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetectsClearTextPassword()
|
||||
{
|
||||
var content = """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<add key="private-feed" value="https://private.example.com/index.json" />
|
||||
</packageSources>
|
||||
<packageSourceCredentials>
|
||||
<private-feed>
|
||||
<add key="Username" value="myuser" />
|
||||
<add key="ClearTextPassword" value="secret123" />
|
||||
</private-feed>
|
||||
</packageSourceCredentials>
|
||||
</configuration>
|
||||
""";
|
||||
|
||||
var result = NuGetConfigParser.Parse(content);
|
||||
|
||||
Assert.True(result.Credentials["private-feed"].IsClearTextPassword);
|
||||
Assert.True(result.Credentials["private-feed"].HasPassword);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MasksEncryptedPassword()
|
||||
{
|
||||
var content = """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<add key="private-feed" value="https://private.example.com/index.json" />
|
||||
</packageSources>
|
||||
<packageSourceCredentials>
|
||||
<private-feed>
|
||||
<add key="Username" value="myuser" />
|
||||
<add key="Password" value="ENCRYPTED_VALUE" />
|
||||
</private-feed>
|
||||
</packageSourceCredentials>
|
||||
</configuration>
|
||||
""";
|
||||
|
||||
var result = NuGetConfigParser.Parse(content);
|
||||
|
||||
Assert.False(result.Credentials["private-feed"].IsClearTextPassword);
|
||||
Assert.True(result.Credentials["private-feed"].HasPassword);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesConfigSection()
|
||||
{
|
||||
var content = """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
</packageSources>
|
||||
<config>
|
||||
<add key="globalPackagesFolder" value="C:\packages" />
|
||||
<add key="repositoryPath" value=".\packages" />
|
||||
</config>
|
||||
</configuration>
|
||||
""";
|
||||
|
||||
var result = NuGetConfigParser.Parse(content);
|
||||
|
||||
Assert.Equal(@"C:\packages", result.Config["globalPackagesFolder"]);
|
||||
Assert.Equal(@".\packages", result.Config["repositoryPath"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesPackageRestoreSection()
|
||||
{
|
||||
var content = """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
</packageSources>
|
||||
<packageRestore>
|
||||
<add key="enabled" value="True" />
|
||||
<add key="automatic" value="True" />
|
||||
</packageRestore>
|
||||
</configuration>
|
||||
""";
|
||||
|
||||
var result = NuGetConfigParser.Parse(content);
|
||||
|
||||
Assert.Equal("True", result.Config["packageRestore.enabled"]);
|
||||
Assert.Equal("True", result.Config["packageRestore.automatic"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetectsClearElement()
|
||||
{
|
||||
var content = """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="local" value="./packages" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
""";
|
||||
|
||||
var result = NuGetConfigParser.Parse(content);
|
||||
|
||||
Assert.Equal("true", result.Config["packageSources.clear"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnabledSourcesProperty()
|
||||
{
|
||||
var content = """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
<add key="disabled-feed" value="https://disabled.example.com/index.json" />
|
||||
</packageSources>
|
||||
<disabledPackageSources>
|
||||
<add key="disabled-feed" value="true" />
|
||||
</disabledPackageSources>
|
||||
</configuration>
|
||||
""";
|
||||
|
||||
var result = NuGetConfigParser.Parse(content);
|
||||
|
||||
Assert.Single(result.EnabledSources);
|
||||
Assert.Equal("nuget.org", result.EnabledSources[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasCustomSourcesProperty()
|
||||
{
|
||||
var content = """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
<add key="myget" value="https://myget.org/F/feed/api/v3/index.json" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
""";
|
||||
|
||||
var result = NuGetConfigParser.Parse(content);
|
||||
|
||||
Assert.True(result.HasCustomSources);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasCredentialsProperty()
|
||||
{
|
||||
var content = """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<add key="private-feed" value="https://private.example.com/index.json" />
|
||||
</packageSources>
|
||||
<packageSourceCredentials>
|
||||
<private-feed>
|
||||
<add key="Username" value="myuser" />
|
||||
</private-feed>
|
||||
</packageSourceCredentials>
|
||||
</configuration>
|
||||
""";
|
||||
|
||||
var result = NuGetConfigParser.Parse(content);
|
||||
|
||||
Assert.True(result.HasCredentials);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GlobalPackagesFolderProperty()
|
||||
{
|
||||
var content = """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
</packageSources>
|
||||
<config>
|
||||
<add key="globalPackagesFolder" value="D:\NuGetCache" />
|
||||
</config>
|
||||
</configuration>
|
||||
""";
|
||||
|
||||
var result = NuGetConfigParser.Parse(content);
|
||||
|
||||
Assert.Equal(@"D:\NuGetCache", result.GlobalPackagesFolder);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNuGetOrgDetection()
|
||||
{
|
||||
var content = """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
""";
|
||||
|
||||
var result = NuGetConfigParser.Parse(content);
|
||||
|
||||
Assert.True(result.PackageSources[0].IsNuGetOrg);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsLocalPathDetection()
|
||||
{
|
||||
var content = """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<add key="local" value="./packages" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
""";
|
||||
|
||||
var result = NuGetConfigParser.Parse(content);
|
||||
|
||||
Assert.True(result.PackageSources[0].IsLocalPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindNearestTraversesUp()
|
||||
{
|
||||
var tempDir = DotNetFixtureBuilder.CreateTemporaryDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
var childDir = Path.Combine(tempDir, "src", "project");
|
||||
Directory.CreateDirectory(childDir);
|
||||
DotNetFixtureBuilder.CreateNuGetConfig(tempDir, ("nuget.org", "https://api.nuget.org/v3/index.json"));
|
||||
|
||||
var found = NuGetConfigParser.FindNearest(childDir);
|
||||
|
||||
Assert.NotNull(found);
|
||||
Assert.EndsWith("NuGet.config", found);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DotNetFixtureBuilder.SafeDelete(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandlesMalformedXml()
|
||||
{
|
||||
var content = "<configuration><invalid";
|
||||
|
||||
var result = NuGetConfigParser.Parse(content);
|
||||
|
||||
Assert.Equal(NuGetConfigParser.Empty, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParsesFileAsyncSuccessfullyAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempDir = DotNetFixtureBuilder.CreateTemporaryDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
var configPath = DotNetFixtureBuilder.CreateNuGetConfig(
|
||||
tempDir,
|
||||
("nuget.org", "https://api.nuget.org/v3/index.json"),
|
||||
("myget", "https://myget.org/F/feed/api/v3/index.json"));
|
||||
|
||||
var result = await NuGetConfigParser.ParseAsync(configPath, cancellationToken);
|
||||
|
||||
Assert.Equal(2, result.PackageSources.Length);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DotNetFixtureBuilder.SafeDelete(tempDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.DotNet.Internal.BuildMetadata;
|
||||
using StellaOps.Scanner.Analyzers.Lang.DotNet.Internal.Conflicts;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.DotNet.Tests.DotNet.Conflicts;
|
||||
|
||||
public sealed class DotNetVersionConflictDetectorTests
|
||||
{
|
||||
private readonly DotNetVersionConflictDetector _detector = new();
|
||||
|
||||
[Fact]
|
||||
public void DetectsNoConflicts()
|
||||
{
|
||||
var dependencies = new[]
|
||||
{
|
||||
CreateDependency("Newtonsoft.Json", "13.0.3", "Project1.csproj"),
|
||||
CreateDependency("Serilog", "3.1.1", "Project1.csproj"),
|
||||
};
|
||||
|
||||
var result = _detector.Detect(dependencies);
|
||||
|
||||
Assert.False(result.HasConflicts);
|
||||
Assert.Empty(result.Conflicts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetectsVersionConflict()
|
||||
{
|
||||
var dependencies = new[]
|
||||
{
|
||||
CreateDependency("Newtonsoft.Json", "13.0.3", "Project1.csproj"),
|
||||
CreateDependency("Newtonsoft.Json", "12.0.1", "Project2.csproj"),
|
||||
};
|
||||
|
||||
var result = _detector.Detect(dependencies);
|
||||
|
||||
Assert.True(result.HasConflicts);
|
||||
Assert.Single(result.Conflicts);
|
||||
Assert.Equal("Newtonsoft.Json", result.Conflicts[0].PackageId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifiesMajorVersionAsHigh()
|
||||
{
|
||||
var dependencies = new[]
|
||||
{
|
||||
CreateDependency("Newtonsoft.Json", "13.0.0", "Project1.csproj"),
|
||||
CreateDependency("Newtonsoft.Json", "12.0.0", "Project2.csproj"),
|
||||
};
|
||||
|
||||
var result = _detector.Detect(dependencies);
|
||||
|
||||
Assert.True(result.HasConflicts);
|
||||
Assert.Equal(ConflictSeverity.High, result.Conflicts[0].Severity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifiesMinorVersionAsMedium()
|
||||
{
|
||||
var dependencies = new[]
|
||||
{
|
||||
CreateDependency("Newtonsoft.Json", "13.1.0", "Project1.csproj"),
|
||||
CreateDependency("Newtonsoft.Json", "13.2.0", "Project2.csproj"),
|
||||
};
|
||||
|
||||
var result = _detector.Detect(dependencies);
|
||||
|
||||
Assert.True(result.HasConflicts);
|
||||
Assert.Equal(ConflictSeverity.Medium, result.Conflicts[0].Severity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifiesPatchVersionAsLow()
|
||||
{
|
||||
var dependencies = new[]
|
||||
{
|
||||
CreateDependency("Newtonsoft.Json", "13.0.1", "Project1.csproj"),
|
||||
CreateDependency("Newtonsoft.Json", "13.0.2", "Project2.csproj"),
|
||||
};
|
||||
|
||||
var result = _detector.Detect(dependencies);
|
||||
|
||||
Assert.True(result.HasConflicts);
|
||||
Assert.Equal(ConflictSeverity.Low, result.Conflicts[0].Severity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandlesPrereleaseSuffixes()
|
||||
{
|
||||
var dependencies = new[]
|
||||
{
|
||||
CreateDependency("MyPackage", "1.0.0-beta", "Project1.csproj"),
|
||||
CreateDependency("MyPackage", "1.0.0", "Project2.csproj"),
|
||||
};
|
||||
|
||||
var result = _detector.Detect(dependencies);
|
||||
|
||||
Assert.True(result.HasConflicts);
|
||||
// Both parse to 1.0.0, so should be Low severity
|
||||
Assert.Equal(ConflictSeverity.Low, result.Conflicts[0].Severity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandlesUnparseableVersions()
|
||||
{
|
||||
var dependencies = new[]
|
||||
{
|
||||
CreateDependency("MyPackage", "latest", "Project1.csproj"),
|
||||
CreateDependency("MyPackage", "1.0.0", "Project2.csproj"),
|
||||
};
|
||||
|
||||
var result = _detector.Detect(dependencies);
|
||||
|
||||
Assert.True(result.HasConflicts);
|
||||
// Can't parse "latest", so severity should be Low
|
||||
Assert.Equal(ConflictSeverity.Low, result.Conflicts[0].Severity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetConflictsAboveFiltersCorrectly()
|
||||
{
|
||||
var dependencies = new[]
|
||||
{
|
||||
CreateDependency("Major", "1.0.0", "Project1.csproj"),
|
||||
CreateDependency("Major", "2.0.0", "Project2.csproj"),
|
||||
CreateDependency("Minor", "1.0.0", "Project1.csproj"),
|
||||
CreateDependency("Minor", "1.1.0", "Project2.csproj"),
|
||||
CreateDependency("Patch", "1.0.0", "Project1.csproj"),
|
||||
CreateDependency("Patch", "1.0.1", "Project2.csproj"),
|
||||
};
|
||||
|
||||
var result = _detector.Detect(dependencies);
|
||||
|
||||
var highAndAbove = result.GetConflictsAbove(ConflictSeverity.High);
|
||||
var mediumAndAbove = result.GetConflictsAbove(ConflictSeverity.Medium);
|
||||
|
||||
Assert.Single(highAndAbove);
|
||||
Assert.Equal(2, mediumAndAbove.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HighSeverityConflictsProperty()
|
||||
{
|
||||
var dependencies = new[]
|
||||
{
|
||||
CreateDependency("Major", "1.0.0", "Project1.csproj"),
|
||||
CreateDependency("Major", "2.0.0", "Project2.csproj"),
|
||||
CreateDependency("Minor", "1.0.0", "Project1.csproj"),
|
||||
CreateDependency("Minor", "1.1.0", "Project2.csproj"),
|
||||
};
|
||||
|
||||
var result = _detector.Detect(dependencies);
|
||||
|
||||
Assert.Single(result.HighSeverityConflicts);
|
||||
Assert.Equal("Major", result.HighSeverityConflicts[0].PackageId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AffectedPackagesProperty()
|
||||
{
|
||||
var dependencies = new[]
|
||||
{
|
||||
CreateDependency("PackageA", "1.0.0", "Project1.csproj"),
|
||||
CreateDependency("PackageA", "2.0.0", "Project2.csproj"),
|
||||
CreateDependency("PackageB", "1.0.0", "Project1.csproj"),
|
||||
CreateDependency("PackageB", "1.1.0", "Project2.csproj"),
|
||||
};
|
||||
|
||||
var result = _detector.Detect(dependencies);
|
||||
|
||||
Assert.Equal(2, result.AffectedPackages.Length);
|
||||
Assert.Contains("PackageA", result.AffectedPackages);
|
||||
Assert.Contains("PackageB", result.AffectedPackages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MaxSeverityProperty()
|
||||
{
|
||||
var dependencies = new[]
|
||||
{
|
||||
CreateDependency("Major", "1.0.0", "Project1.csproj"),
|
||||
CreateDependency("Major", "2.0.0", "Project2.csproj"),
|
||||
CreateDependency("Minor", "1.0.0", "Project1.csproj"),
|
||||
CreateDependency("Minor", "1.1.0", "Project2.csproj"),
|
||||
};
|
||||
|
||||
var result = _detector.Detect(dependencies);
|
||||
|
||||
Assert.Equal(ConflictSeverity.High, result.MaxSeverity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SortsConflictsBySeverityThenId()
|
||||
{
|
||||
var dependencies = new[]
|
||||
{
|
||||
CreateDependency("Zebra", "1.0.0", "Project1.csproj"),
|
||||
CreateDependency("Zebra", "1.0.1", "Project2.csproj"), // Low
|
||||
CreateDependency("Alpha", "1.0.0", "Project1.csproj"),
|
||||
CreateDependency("Alpha", "2.0.0", "Project2.csproj"), // High
|
||||
CreateDependency("Beta", "1.0.0", "Project1.csproj"),
|
||||
CreateDependency("Beta", "1.1.0", "Project2.csproj"), // Medium
|
||||
};
|
||||
|
||||
var result = _detector.Detect(dependencies);
|
||||
|
||||
Assert.Equal(3, result.Conflicts.Length);
|
||||
// Should be sorted by severity (High first) then alphabetically
|
||||
Assert.Equal("Alpha", result.Conflicts[0].PackageId);
|
||||
Assert.Equal(ConflictSeverity.High, result.Conflicts[0].Severity);
|
||||
Assert.Equal("Beta", result.Conflicts[1].PackageId);
|
||||
Assert.Equal(ConflictSeverity.Medium, result.Conflicts[1].Severity);
|
||||
Assert.Equal("Zebra", result.Conflicts[2].PackageId);
|
||||
Assert.Equal(ConflictSeverity.Low, result.Conflicts[2].Severity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandlesNullDependencies()
|
||||
{
|
||||
var result = _detector.Detect(null!);
|
||||
|
||||
Assert.Equal(ConflictDetectionResult.Empty, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandlesEmptyVersion()
|
||||
{
|
||||
var dependencies = new[]
|
||||
{
|
||||
CreateDependency("Package", "", "Project1.csproj"),
|
||||
CreateDependency("Package", "1.0.0", "Project2.csproj"),
|
||||
};
|
||||
|
||||
var result = _detector.Detect(dependencies);
|
||||
|
||||
// Empty version should be skipped
|
||||
Assert.False(result.HasConflicts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VersionConflictDescriptionProperty()
|
||||
{
|
||||
var dependencies = new[]
|
||||
{
|
||||
CreateDependency("Newtonsoft.Json", "13.0.0", "Project1.csproj"),
|
||||
CreateDependency("Newtonsoft.Json", "12.0.0", "Project2.csproj"),
|
||||
};
|
||||
|
||||
var result = _detector.Detect(dependencies);
|
||||
|
||||
Assert.Contains("Newtonsoft.Json", result.Conflicts[0].Description);
|
||||
Assert.Contains("2 different versions", result.Conflicts[0].Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CaseInsensitivePackageIdMatching()
|
||||
{
|
||||
var dependencies = new[]
|
||||
{
|
||||
CreateDependency("Newtonsoft.Json", "13.0.0", "Project1.csproj"),
|
||||
CreateDependency("newtonsoft.json", "12.0.0", "Project2.csproj"),
|
||||
};
|
||||
|
||||
var result = _detector.Detect(dependencies);
|
||||
|
||||
Assert.True(result.HasConflicts);
|
||||
Assert.Single(result.Conflicts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TracksConflictLocations()
|
||||
{
|
||||
var dependencies = new[]
|
||||
{
|
||||
CreateDependency("Newtonsoft.Json", "13.0.0", "Project1.csproj"),
|
||||
CreateDependency("Newtonsoft.Json", "12.0.0", "Project2.csproj"),
|
||||
};
|
||||
|
||||
var result = _detector.Detect(dependencies);
|
||||
|
||||
Assert.Equal(2, result.Conflicts[0].Locations.Length);
|
||||
}
|
||||
|
||||
private static DotNetDependencyDeclaration CreateDependency(string packageId, string version, string locator)
|
||||
{
|
||||
return new DotNetDependencyDeclaration
|
||||
{
|
||||
PackageId = packageId,
|
||||
Version = version,
|
||||
Locator = locator,
|
||||
Source = "csproj",
|
||||
VersionSource = DotNetVersionSource.Direct
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,537 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.DotNet.Internal.BuildMetadata;
|
||||
using StellaOps.Scanner.Analyzers.Lang.DotNet.Internal.Parsing;
|
||||
using StellaOps.Scanner.Analyzers.Lang.DotNet.Tests.TestUtilities;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.DotNet.Tests.DotNet.Parsing;
|
||||
|
||||
public sealed class MsBuildProjectParserTests
|
||||
{
|
||||
[Fact]
|
||||
public void ParsesEmptyProjectReturnsEmpty()
|
||||
{
|
||||
var content = "";
|
||||
var result = MsBuildProjectParser.Parse(content);
|
||||
|
||||
Assert.Equal(MsBuildProjectParser.Empty, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesSdkStyleProject()
|
||||
{
|
||||
var content = """
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
""";
|
||||
|
||||
var result = MsBuildProjectParser.Parse(content);
|
||||
|
||||
Assert.True(result.IsSdkStyle);
|
||||
Assert.Equal("Microsoft.NET.Sdk", result.Sdk);
|
||||
Assert.Equal(DotNetProjectType.SdkStyle, result.ProjectType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesSdkElementVariant()
|
||||
{
|
||||
var content = """
|
||||
<Project>
|
||||
<Sdk Name="Microsoft.NET.Sdk.Web" />
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
""";
|
||||
|
||||
var result = MsBuildProjectParser.Parse(content);
|
||||
|
||||
Assert.True(result.IsSdkStyle);
|
||||
Assert.Equal("Microsoft.NET.Sdk.Web", result.Sdk);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesLegacyStyleProject()
|
||||
{
|
||||
var content = """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
""";
|
||||
|
||||
var result = MsBuildProjectParser.Parse(content);
|
||||
|
||||
Assert.False(result.IsSdkStyle);
|
||||
Assert.Null(result.Sdk);
|
||||
Assert.Equal(DotNetProjectType.LegacyStyle, result.ProjectType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesSingleTargetFramework()
|
||||
{
|
||||
var content = """
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
""";
|
||||
|
||||
var result = MsBuildProjectParser.Parse(content);
|
||||
|
||||
Assert.Single(result.TargetFrameworks);
|
||||
Assert.Equal("net8.0", result.TargetFrameworks[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesMultipleTargetFrameworks()
|
||||
{
|
||||
var content = """
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net6.0;net8.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
""";
|
||||
|
||||
var result = MsBuildProjectParser.Parse(content);
|
||||
|
||||
Assert.Equal(3, result.TargetFrameworks.Length);
|
||||
Assert.Contains("netstandard2.0", result.TargetFrameworks);
|
||||
Assert.Contains("net6.0", result.TargetFrameworks);
|
||||
Assert.Contains("net8.0", result.TargetFrameworks);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesPackageReferences()
|
||||
{
|
||||
var content = """
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Serilog" Version="3.1.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
""";
|
||||
|
||||
var result = MsBuildProjectParser.Parse(content);
|
||||
|
||||
Assert.Equal(2, result.PackageReferences.Length);
|
||||
Assert.Contains(result.PackageReferences, p => p.PackageId == "Newtonsoft.Json" && p.Version == "13.0.3");
|
||||
Assert.Contains(result.PackageReferences, p => p.PackageId == "Serilog" && p.Version == "3.1.1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesPackageReferenceVersionElement()
|
||||
{
|
||||
var content = """
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json">
|
||||
<Version>13.0.3</Version>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
""";
|
||||
|
||||
var result = MsBuildProjectParser.Parse(content);
|
||||
|
||||
Assert.Single(result.PackageReferences);
|
||||
Assert.Equal("Newtonsoft.Json", result.PackageReferences[0].PackageId);
|
||||
Assert.Equal("13.0.3", result.PackageReferences[0].Version);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesPackageReferenceWithUpdateAttribute()
|
||||
{
|
||||
var content = """
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Update="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
""";
|
||||
|
||||
var result = MsBuildProjectParser.Parse(content);
|
||||
|
||||
Assert.Single(result.PackageReferences);
|
||||
Assert.Equal("Newtonsoft.Json", result.PackageReferences[0].PackageId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesPackageReferenceCondition()
|
||||
{
|
||||
var content = """
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net462'">
|
||||
<PackageReference Include="System.Net.Http" Version="4.3.4" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
""";
|
||||
|
||||
var result = MsBuildProjectParser.Parse(content);
|
||||
|
||||
Assert.Single(result.PackageReferences);
|
||||
Assert.Equal("'$(TargetFramework)' == 'net462'", result.PackageReferences[0].Condition);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesPackageReferencePrivateAssets()
|
||||
{
|
||||
var content = """
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
""";
|
||||
|
||||
var result = MsBuildProjectParser.Parse(content);
|
||||
|
||||
Assert.Single(result.PackageReferences);
|
||||
Assert.True(result.PackageReferences[0].IsDevelopmentDependency);
|
||||
Assert.Equal("all", result.PackageReferences[0].PrivateAssets);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesProjectReferences()
|
||||
{
|
||||
var content = """
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Lib\Lib.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
""";
|
||||
|
||||
var result = MsBuildProjectParser.Parse(content);
|
||||
|
||||
Assert.Single(result.ProjectReferences);
|
||||
Assert.Equal("../Lib/Lib.csproj", result.ProjectReferences[0].ProjectPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesFrameworkReferences()
|
||||
{
|
||||
var content = """
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
""";
|
||||
|
||||
var result = MsBuildProjectParser.Parse(content);
|
||||
|
||||
Assert.Single(result.FrameworkReferences);
|
||||
Assert.Equal("Microsoft.AspNetCore.App", result.FrameworkReferences[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesProperties()
|
||||
{
|
||||
var content = """
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Version>1.0.0</Version>
|
||||
<Authors>Test Author</Authors>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
""";
|
||||
|
||||
var result = MsBuildProjectParser.Parse(content);
|
||||
|
||||
Assert.True(result.Properties.ContainsKey("Version"));
|
||||
Assert.Equal("1.0.0", result.Properties["Version"]);
|
||||
Assert.True(result.Properties.ContainsKey("Authors"));
|
||||
Assert.Equal("Test Author", result.Properties["Authors"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesOutputType()
|
||||
{
|
||||
var content = """
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
""";
|
||||
|
||||
var result = MsBuildProjectParser.Parse(content);
|
||||
|
||||
Assert.Equal("Exe", result.OutputType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesAssemblyName()
|
||||
{
|
||||
var content = """
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<AssemblyName>MyCustomAssembly</AssemblyName>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
""";
|
||||
|
||||
var result = MsBuildProjectParser.Parse(content);
|
||||
|
||||
Assert.Equal("MyCustomAssembly", result.AssemblyName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesLicenseExpression()
|
||||
{
|
||||
var content = """
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
""";
|
||||
|
||||
var result = MsBuildProjectParser.Parse(content);
|
||||
|
||||
Assert.Single(result.Licenses);
|
||||
Assert.Equal("MIT", result.Licenses[0].Expression);
|
||||
Assert.Equal(DotNetProjectLicenseConfidence.High, result.Licenses[0].Confidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesLicenseFile()
|
||||
{
|
||||
var content = """
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<PackageLicenseFile>LICENSE.txt</PackageLicenseFile>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
""";
|
||||
|
||||
var result = MsBuildProjectParser.Parse(content);
|
||||
|
||||
Assert.Single(result.Licenses);
|
||||
Assert.Equal("LICENSE.txt", result.Licenses[0].File);
|
||||
Assert.Equal(DotNetProjectLicenseConfidence.Medium, result.Licenses[0].Confidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesLicenseUrl()
|
||||
{
|
||||
var content = """
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<PackageLicenseUrl>https://opensource.org/licenses/MIT</PackageLicenseUrl>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
""";
|
||||
|
||||
var result = MsBuildProjectParser.Parse(content);
|
||||
|
||||
Assert.Single(result.Licenses);
|
||||
Assert.Equal("https://opensource.org/licenses/MIT", result.Licenses[0].Url);
|
||||
Assert.Equal(DotNetProjectLicenseConfidence.Low, result.Licenses[0].Confidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandlesXmlException()
|
||||
{
|
||||
var content = "<Project><Invalid";
|
||||
|
||||
var result = MsBuildProjectParser.Parse(content);
|
||||
|
||||
Assert.Equal(MsBuildProjectParser.Empty, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandlesFileNotFoundAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var result = await MsBuildProjectParser.ParseAsync("/nonexistent/path.csproj", cancellationToken);
|
||||
|
||||
Assert.Equal(MsBuildProjectParser.Empty, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetEffectiveAssemblyNameReturnsAssemblyName()
|
||||
{
|
||||
var content = """
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<AssemblyName>MyCustomAssembly</AssemblyName>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
""";
|
||||
|
||||
var result = MsBuildProjectParser.Parse(content, "Test.csproj");
|
||||
|
||||
Assert.Equal("MyCustomAssembly", result.GetEffectiveAssemblyName());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetEffectiveAssemblyNameFallsBackToProjectName()
|
||||
{
|
||||
var content = """
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
""";
|
||||
|
||||
var result = MsBuildProjectParser.Parse(content, "MyProject.csproj");
|
||||
|
||||
Assert.Equal("MyProject", result.GetEffectiveAssemblyName());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPrimaryTargetFrameworkReturnsFirstTfm()
|
||||
{
|
||||
var content = """
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net6.0;net8.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
""";
|
||||
|
||||
var result = MsBuildProjectParser.Parse(content);
|
||||
|
||||
Assert.Equal("netstandard2.0", result.GetPrimaryTargetFramework());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizesPathsToForwardSlashes()
|
||||
{
|
||||
var content = """
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Lib\Lib.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
""";
|
||||
|
||||
var result = MsBuildProjectParser.Parse(content, @"C:\Projects\App\App.csproj");
|
||||
|
||||
Assert.Equal("C:/Projects/App/App.csproj", result.SourcePath);
|
||||
Assert.Equal("../Lib/Lib.csproj", result.ProjectReferences[0].ProjectPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParsesFileAsyncSuccessfullyAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempDir = DotNetFixtureBuilder.CreateTemporaryDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
var projectPath = DotNetFixtureBuilder.CreateSdkStyleProject(
|
||||
tempDir,
|
||||
"Test.csproj",
|
||||
"net8.0",
|
||||
("Newtonsoft.Json", "13.0.3"));
|
||||
|
||||
var result = await MsBuildProjectParser.ParseAsync(projectPath, cancellationToken);
|
||||
|
||||
Assert.True(result.IsSdkStyle);
|
||||
Assert.Single(result.PackageReferences);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DotNetFixtureBuilder.SafeDelete(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesManagePackageVersionsCentrally()
|
||||
{
|
||||
var content = """
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
""";
|
||||
|
||||
var result = MsBuildProjectParser.Parse(content);
|
||||
|
||||
Assert.True(result.ManagePackageVersionsCentrally);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesPackageReferenceWithoutVersion()
|
||||
{
|
||||
var content = """
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
""";
|
||||
|
||||
var result = MsBuildProjectParser.Parse(content);
|
||||
|
||||
Assert.Single(result.PackageReferences);
|
||||
Assert.Equal("Newtonsoft.Json", result.PackageReferences[0].PackageId);
|
||||
Assert.Null(result.PackageReferences[0].Version);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FirstPropertyGroupWins()
|
||||
{
|
||||
var content = """
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Version>1.0.0</Version>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<Version>2.0.0</Version>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
""";
|
||||
|
||||
var result = MsBuildProjectParser.Parse(content);
|
||||
|
||||
Assert.Equal("1.0.0", result.Version);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.DotNet.Internal.BuildMetadata;
|
||||
using StellaOps.Scanner.Analyzers.Lang.DotNet.Internal.Parsing;
|
||||
using StellaOps.Scanner.Analyzers.Lang.DotNet.Tests.TestUtilities;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.DotNet.Tests.DotNet.Parsing;
|
||||
|
||||
public sealed class PackagesConfigParserTests
|
||||
{
|
||||
[Fact]
|
||||
public void ParsesBasicPackage()
|
||||
{
|
||||
var content = """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Newtonsoft.Json" version="13.0.3" targetFramework="net472" />
|
||||
</packages>
|
||||
""";
|
||||
|
||||
var result = PackagesConfigParser.Parse(content);
|
||||
|
||||
Assert.Single(result.Packages);
|
||||
Assert.Equal("Newtonsoft.Json", result.Packages[0].PackageId);
|
||||
Assert.Equal("13.0.3", result.Packages[0].Version);
|
||||
Assert.Single(result.Packages[0].TargetFrameworks);
|
||||
Assert.Equal("net472", result.Packages[0].TargetFrameworks[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesDevelopmentDependency()
|
||||
{
|
||||
var content = """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="StyleCop.Analyzers" version="1.2.0" targetFramework="net472" developmentDependency="true" />
|
||||
</packages>
|
||||
""";
|
||||
|
||||
var result = PackagesConfigParser.Parse(content);
|
||||
|
||||
Assert.Single(result.Packages);
|
||||
Assert.True(result.Packages[0].IsDevelopmentDependency);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesAllowedVersions()
|
||||
{
|
||||
var content = """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Newtonsoft.Json" version="13.0.3" targetFramework="net472" allowedVersions="[13.0,14.0)" />
|
||||
</packages>
|
||||
""";
|
||||
|
||||
var result = PackagesConfigParser.Parse(content);
|
||||
|
||||
Assert.Single(result.Packages);
|
||||
Assert.Equal("[13.0,14.0)", result.Packages[0].AllowedVersions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandlesMultiplePackages()
|
||||
{
|
||||
var content = """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Newtonsoft.Json" version="13.0.3" targetFramework="net472" />
|
||||
<package id="Serilog" version="3.1.1" targetFramework="net472" />
|
||||
<package id="Dapper" version="2.1.24" targetFramework="net472" />
|
||||
<package id="FluentValidation" version="11.8.0" targetFramework="net472" />
|
||||
<package id="AutoMapper" version="12.0.1" targetFramework="net472" />
|
||||
</packages>
|
||||
""";
|
||||
|
||||
var result = PackagesConfigParser.Parse(content);
|
||||
|
||||
Assert.Equal(5, result.Packages.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SkipsPackageWithoutId()
|
||||
{
|
||||
var content = """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package version="13.0.3" targetFramework="net472" />
|
||||
<package id="Serilog" version="3.1.1" targetFramework="net472" />
|
||||
</packages>
|
||||
""";
|
||||
|
||||
var result = PackagesConfigParser.Parse(content);
|
||||
|
||||
Assert.Single(result.Packages);
|
||||
Assert.Equal("Serilog", result.Packages[0].PackageId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandlesEmptyFile()
|
||||
{
|
||||
var content = "";
|
||||
|
||||
var result = PackagesConfigParser.Parse(content);
|
||||
|
||||
Assert.Equal(PackagesConfigParser.Empty, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandlesMalformedXml()
|
||||
{
|
||||
var content = "<packages><invalid";
|
||||
|
||||
var result = PackagesConfigParser.Parse(content);
|
||||
|
||||
Assert.Equal(PackagesConfigParser.Empty, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandlesNonExistentFileAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var result = await PackagesConfigParser.ParseAsync("/nonexistent/packages.config", cancellationToken);
|
||||
|
||||
Assert.Equal(PackagesConfigParser.Empty, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizesSourcePath()
|
||||
{
|
||||
var content = """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Newtonsoft.Json" version="13.0.3" targetFramework="net472" />
|
||||
</packages>
|
||||
""";
|
||||
|
||||
var result = PackagesConfigParser.Parse(content, @"C:\Projects\App\packages.config");
|
||||
|
||||
Assert.Equal("C:/Projects/App/packages.config", result.SourcePath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetsVersionSourceToPackagesConfig()
|
||||
{
|
||||
var content = """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Newtonsoft.Json" version="13.0.3" targetFramework="net472" />
|
||||
</packages>
|
||||
""";
|
||||
|
||||
var result = PackagesConfigParser.Parse(content);
|
||||
|
||||
Assert.Single(result.Packages);
|
||||
Assert.Equal(DotNetVersionSource.PackagesConfig, result.Packages[0].VersionSource);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtractsTargetFramework()
|
||||
{
|
||||
var content = """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Newtonsoft.Json" version="13.0.3" targetFramework="net461" />
|
||||
</packages>
|
||||
""";
|
||||
|
||||
var result = PackagesConfigParser.Parse(content);
|
||||
|
||||
Assert.Single(result.Packages);
|
||||
Assert.Single(result.Packages[0].TargetFrameworks);
|
||||
Assert.Equal("net461", result.Packages[0].TargetFrameworks[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllPackagesAreDirect()
|
||||
{
|
||||
var content = """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Newtonsoft.Json" version="13.0.3" targetFramework="net472" />
|
||||
<package id="Serilog" version="3.1.1" targetFramework="net472" />
|
||||
</packages>
|
||||
""";
|
||||
|
||||
var result = PackagesConfigParser.Parse(content);
|
||||
|
||||
Assert.All(result.Packages, p => Assert.Equal("packages.config", p.Source));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParsesFileAsyncSuccessfullyAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var tempDir = DotNetFixtureBuilder.CreateTemporaryDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
var configPath = DotNetFixtureBuilder.CreatePackagesConfig(
|
||||
tempDir,
|
||||
("Newtonsoft.Json", "13.0.3", "net472"),
|
||||
("Serilog", "3.1.1", "net472"));
|
||||
|
||||
var result = await PackagesConfigParser.ParseAsync(configPath, cancellationToken);
|
||||
|
||||
Assert.Equal(2, result.Packages.Length);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DotNetFixtureBuilder.SafeDelete(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandlesEmptyTargetFramework()
|
||||
{
|
||||
var content = """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Newtonsoft.Json" version="13.0.3" />
|
||||
</packages>
|
||||
""";
|
||||
|
||||
var result = PackagesConfigParser.Parse(content);
|
||||
|
||||
Assert.Single(result.Packages);
|
||||
Assert.Empty(result.Packages[0].TargetFrameworks);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<!-- Disable Concelier test infrastructure - this project doesn't need MongoDB -->
|
||||
<UseConcelierTestInfra>false</UseConcelierTestInfra>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Remove inherited packages and files from Directory.Build.props -->
|
||||
<ItemGroup>
|
||||
<PackageReference Remove="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Remove="xunit" />
|
||||
<PackageReference Remove="xunit.runner.visualstudio" />
|
||||
<PackageReference Remove="Microsoft.AspNetCore.Mvc.Testing" />
|
||||
<PackageReference Remove="Mongo2Go" />
|
||||
<PackageReference Remove="coverlet.collector" />
|
||||
<PackageReference Remove="Microsoft.Extensions.TimeProvider.Testing" />
|
||||
<PackageReference Remove="SharpCompress" />
|
||||
<!-- Remove OpenSSL shim files - not needed for this test project -->
|
||||
<Compile Remove="Shared/OpenSslLegacyShim.cs" />
|
||||
<Compile Remove="Shared/OpenSslAutoInit.cs" />
|
||||
<None Remove="native/linux-x64/*.so.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="xunit.v3" Version="3.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.3">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Scanner.Analyzers.Lang.DotNet\StellaOps.Scanner.Analyzers.Lang.DotNet.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,395 @@
|
||||
using System.Text;
|
||||
using StellaOps.Scanner.Analyzers.Lang.DotNet.Internal.Bundling;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.DotNet.Tests.TestUtilities;
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating .NET project fixtures for testing.
|
||||
/// </summary>
|
||||
internal static class DotNetFixtureBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a minimal SDK-style project file.
|
||||
/// </summary>
|
||||
public static string CreateSdkStyleProject(
|
||||
string directory,
|
||||
string projectName,
|
||||
string targetFramework = "net8.0",
|
||||
params (string PackageId, string Version)[] packages)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("""<Project Sdk="Microsoft.NET.Sdk">""");
|
||||
sb.AppendLine(" <PropertyGroup>");
|
||||
sb.AppendLine($" <TargetFramework>{targetFramework}</TargetFramework>");
|
||||
sb.AppendLine(" </PropertyGroup>");
|
||||
|
||||
if (packages.Length > 0)
|
||||
{
|
||||
sb.AppendLine(" <ItemGroup>");
|
||||
foreach (var (packageId, version) in packages)
|
||||
{
|
||||
if (string.IsNullOrEmpty(version))
|
||||
{
|
||||
sb.AppendLine($""" <PackageReference Include="{packageId}" />""");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($""" <PackageReference Include="{packageId}" Version="{version}" />""");
|
||||
}
|
||||
}
|
||||
sb.AppendLine(" </ItemGroup>");
|
||||
}
|
||||
|
||||
sb.AppendLine("</Project>");
|
||||
|
||||
var filePath = Path.Combine(directory, projectName);
|
||||
Directory.CreateDirectory(directory);
|
||||
File.WriteAllText(filePath, sb.ToString());
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a multi-target SDK-style project file.
|
||||
/// </summary>
|
||||
public static string CreateMultiTargetProject(
|
||||
string directory,
|
||||
string projectName,
|
||||
string[] targetFrameworks,
|
||||
params (string PackageId, string Version, string? Condition)[] packages)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("""<Project Sdk="Microsoft.NET.Sdk">""");
|
||||
sb.AppendLine(" <PropertyGroup>");
|
||||
sb.AppendLine($" <TargetFrameworks>{string.Join(';', targetFrameworks)}</TargetFrameworks>");
|
||||
sb.AppendLine(" </PropertyGroup>");
|
||||
|
||||
if (packages.Length > 0)
|
||||
{
|
||||
sb.AppendLine(" <ItemGroup>");
|
||||
foreach (var (packageId, version, condition) in packages)
|
||||
{
|
||||
if (string.IsNullOrEmpty(condition))
|
||||
{
|
||||
sb.AppendLine($""" <PackageReference Include="{packageId}" Version="{version}" />""");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($""" <PackageReference Include="{packageId}" Version="{version}" Condition="{condition}" />""");
|
||||
}
|
||||
}
|
||||
sb.AppendLine(" </ItemGroup>");
|
||||
}
|
||||
|
||||
sb.AppendLine("</Project>");
|
||||
|
||||
var filePath = Path.Combine(directory, projectName);
|
||||
Directory.CreateDirectory(directory);
|
||||
File.WriteAllText(filePath, sb.ToString());
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Directory.Build.props file with properties.
|
||||
/// </summary>
|
||||
public static string CreateDirectoryBuildProps(
|
||||
string directory,
|
||||
IDictionary<string, string> properties)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("<Project>");
|
||||
sb.AppendLine(" <PropertyGroup>");
|
||||
foreach (var (key, value) in properties)
|
||||
{
|
||||
sb.AppendLine($" <{key}>{value}</{key}>");
|
||||
}
|
||||
sb.AppendLine(" </PropertyGroup>");
|
||||
sb.AppendLine("</Project>");
|
||||
|
||||
var filePath = Path.Combine(directory, "Directory.Build.props");
|
||||
Directory.CreateDirectory(directory);
|
||||
File.WriteAllText(filePath, sb.ToString());
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Directory.Packages.props file for CPM.
|
||||
/// </summary>
|
||||
public static string CreateDirectoryPackagesProps(
|
||||
string directory,
|
||||
bool managePackageVersionsCentrally = true,
|
||||
params (string PackageId, string Version)[] packages)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("<Project>");
|
||||
sb.AppendLine(" <PropertyGroup>");
|
||||
sb.AppendLine($" <ManagePackageVersionsCentrally>{managePackageVersionsCentrally.ToString().ToLowerInvariant()}</ManagePackageVersionsCentrally>");
|
||||
sb.AppendLine(" </PropertyGroup>");
|
||||
|
||||
if (packages.Length > 0)
|
||||
{
|
||||
sb.AppendLine(" <ItemGroup>");
|
||||
foreach (var (packageId, version) in packages)
|
||||
{
|
||||
sb.AppendLine($""" <PackageVersion Include="{packageId}" Version="{version}" />""");
|
||||
}
|
||||
sb.AppendLine(" </ItemGroup>");
|
||||
}
|
||||
|
||||
sb.AppendLine("</Project>");
|
||||
|
||||
var filePath = Path.Combine(directory, "Directory.Packages.props");
|
||||
Directory.CreateDirectory(directory);
|
||||
File.WriteAllText(filePath, sb.ToString());
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a packages.lock.json file.
|
||||
/// </summary>
|
||||
public static string CreatePackagesLockJson(
|
||||
string directory,
|
||||
string targetFramework,
|
||||
params (string PackageId, string Version, bool IsDirect)[] packages)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("{");
|
||||
sb.AppendLine(""" "version": 1,""");
|
||||
sb.AppendLine(""" "dependencies": {""");
|
||||
sb.AppendLine($""" "{targetFramework}": {{""");
|
||||
|
||||
for (var i = 0; i < packages.Length; i++)
|
||||
{
|
||||
var (packageId, version, isDirect) = packages[i];
|
||||
var type = isDirect ? "Direct" : "Transitive";
|
||||
var comma = i < packages.Length - 1 ? "," : "";
|
||||
|
||||
sb.AppendLine($""" "{packageId}": {{""");
|
||||
sb.AppendLine($""" "type": "{type}",""");
|
||||
sb.AppendLine($""" "resolved": "{version}",""");
|
||||
sb.AppendLine($""" "contentHash": "sha512-test{i}==""");
|
||||
sb.AppendLine($" }}{comma}");
|
||||
}
|
||||
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine("}");
|
||||
|
||||
var filePath = Path.Combine(directory, "packages.lock.json");
|
||||
Directory.CreateDirectory(directory);
|
||||
File.WriteAllText(filePath, sb.ToString());
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a legacy packages.config file.
|
||||
/// </summary>
|
||||
public static string CreatePackagesConfig(
|
||||
string directory,
|
||||
params (string PackageId, string Version, string TargetFramework)[] packages)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("""<?xml version="1.0" encoding="utf-8"?>""");
|
||||
sb.AppendLine("<packages>");
|
||||
|
||||
foreach (var (packageId, version, targetFramework) in packages)
|
||||
{
|
||||
sb.AppendLine($""" <package id="{packageId}" version="{version}" targetFramework="{targetFramework}" />""");
|
||||
}
|
||||
|
||||
sb.AppendLine("</packages>");
|
||||
|
||||
var filePath = Path.Combine(directory, "packages.config");
|
||||
Directory.CreateDirectory(directory);
|
||||
File.WriteAllText(filePath, sb.ToString());
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a global.json file.
|
||||
/// </summary>
|
||||
public static string CreateGlobalJson(
|
||||
string directory,
|
||||
string sdkVersion,
|
||||
string? rollForward = null,
|
||||
bool? allowPrerelease = null)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("{");
|
||||
sb.AppendLine(""" "sdk": {""");
|
||||
sb.Append($""" "version": "{sdkVersion}"""");
|
||||
|
||||
if (!string.IsNullOrEmpty(rollForward))
|
||||
{
|
||||
sb.AppendLine(",");
|
||||
sb.Append($""" "rollForward": "{rollForward}"""");
|
||||
}
|
||||
|
||||
if (allowPrerelease.HasValue)
|
||||
{
|
||||
sb.AppendLine(",");
|
||||
sb.Append($""" "allowPrerelease": {allowPrerelease.Value.ToString().ToLowerInvariant()}""");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine("}");
|
||||
|
||||
var filePath = Path.Combine(directory, "global.json");
|
||||
Directory.CreateDirectory(directory);
|
||||
File.WriteAllText(filePath, sb.ToString());
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a NuGet.config file.
|
||||
/// </summary>
|
||||
public static string CreateNuGetConfig(
|
||||
string directory,
|
||||
params (string Name, string Url)[] sources)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("""<?xml version="1.0" encoding="utf-8"?>""");
|
||||
sb.AppendLine("<configuration>");
|
||||
sb.AppendLine(" <packageSources>");
|
||||
|
||||
foreach (var (name, url) in sources)
|
||||
{
|
||||
sb.AppendLine($""" <add key="{name}" value="{url}" />""");
|
||||
}
|
||||
|
||||
sb.AppendLine(" </packageSources>");
|
||||
sb.AppendLine("</configuration>");
|
||||
|
||||
var filePath = Path.Combine(directory, "NuGet.config");
|
||||
Directory.CreateDirectory(directory);
|
||||
File.WriteAllText(filePath, sb.ToString());
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a mock ILMerged assembly (binary with markers).
|
||||
/// </summary>
|
||||
public static string CreateMockILMergedAssembly(
|
||||
string directory,
|
||||
string assemblyName,
|
||||
BundlingTool tool)
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
var marker = tool switch
|
||||
{
|
||||
BundlingTool.CosturaFody => "costura.embedded.dll"u8.ToArray(),
|
||||
BundlingTool.ILMerge => "ILMerge.marker"u8.ToArray(),
|
||||
BundlingTool.ILRepack => "ILRepack.marker"u8.ToArray(),
|
||||
_ => Array.Empty<byte>()
|
||||
};
|
||||
|
||||
// Create a file with MZ header and embedded marker
|
||||
var content = new byte[1024 * 100]; // 100KB
|
||||
content[0] = 0x4D; // 'M'
|
||||
content[1] = 0x5A; // 'Z'
|
||||
|
||||
if (marker.Length > 0)
|
||||
{
|
||||
Array.Copy(marker, 0, content, 100, marker.Length);
|
||||
}
|
||||
|
||||
// Add multiple .dll patterns
|
||||
var dllPattern = ".dll"u8.ToArray();
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
Array.Copy(dllPattern, 0, content, 200 + i * 50, dllPattern.Length);
|
||||
}
|
||||
|
||||
var filePath = Path.Combine(directory, assemblyName);
|
||||
File.WriteAllBytes(filePath, content);
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a mock single-file bundle (binary with markers).
|
||||
/// </summary>
|
||||
public static string CreateMockSingleFileBundle(
|
||||
string directory,
|
||||
string bundleName)
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
// .NET Core bundle signature
|
||||
var bundleSignature = ".net core bundle"u8.ToArray();
|
||||
|
||||
// Create a file with MZ header and bundle markers
|
||||
var content = new byte[1024 * 200]; // 200KB
|
||||
content[0] = 0x4D; // 'M'
|
||||
content[1] = 0x5A; // 'Z'
|
||||
|
||||
// Add bundle signature
|
||||
Array.Copy(bundleSignature, 0, content, 500, bundleSignature.Length);
|
||||
|
||||
// Add some System. namespace patterns
|
||||
var systemPattern = "System.Runtime"u8.ToArray();
|
||||
Array.Copy(systemPattern, 0, content, 1000, systemPattern.Length);
|
||||
|
||||
// Add .dll patterns
|
||||
var dllPattern = ".dll"u8.ToArray();
|
||||
for (var i = 0; i < 15; i++)
|
||||
{
|
||||
Array.Copy(dllPattern, 0, content, 2000 + i * 100, dllPattern.Length);
|
||||
}
|
||||
|
||||
var filePath = Path.Combine(directory, bundleName);
|
||||
File.WriteAllBytes(filePath, content);
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a legacy-style project file (with MSBuild namespace).
|
||||
/// </summary>
|
||||
public static string CreateLegacyStyleProject(
|
||||
string directory,
|
||||
string projectName,
|
||||
string targetFramework = "net472")
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("""<?xml version="1.0" encoding="utf-8"?>""");
|
||||
sb.AppendLine("""<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">""");
|
||||
sb.AppendLine(" <PropertyGroup>");
|
||||
sb.AppendLine($" <TargetFrameworkVersion>v{targetFramework.Replace("net", "").Insert(1, ".")}</TargetFrameworkVersion>");
|
||||
sb.AppendLine(" <OutputType>Library</OutputType>");
|
||||
sb.AppendLine(" </PropertyGroup>");
|
||||
sb.AppendLine("</Project>");
|
||||
|
||||
var filePath = Path.Combine(directory, projectName);
|
||||
Directory.CreateDirectory(directory);
|
||||
File.WriteAllText(filePath, sb.ToString());
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a temporary directory for test isolation.
|
||||
/// </summary>
|
||||
public static string CreateTemporaryDirectory()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), "stellaops-tests", Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(path);
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Safely deletes a directory (swallows exceptions).
|
||||
/// </summary>
|
||||
public static void SafeDelete(string directory)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(directory))
|
||||
{
|
||||
Directory.Delete(directory, recursive: true);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user