Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CycloneDX.E2ETests/Infrastructure/CycloneDxRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,11 @@ private static string BuildArgs(string projectOrSolutionPath, string outputDir,
sb.Append($" --framework {options.Framework}");
}

if (options.Configuration != null)
{
sb.Append($" --configuration {options.Configuration}");
}

if (options.AdditionalArgs != null)
{
sb.Append($" {options.AdditionalArgs}");
Expand Down Expand Up @@ -190,6 +195,7 @@ internal sealed class ToolRunOptions
public string SpecVersion { get; set; }
public string ExcludeFilter { get; set; }
public string Framework { get; set; }
public string Configuration { get; set; }
/// <summary>Appended verbatim to the command line for exotic scenarios.</summary>
public string AdditionalArgs { get; set; }
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<bom xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" serialNumber="urn:uuid:{scrubbed}" version="1" xmlns="http://cyclonedx.org/schema/bom/1.6">
<bom xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" serialNumber="urn:uuid:{scrubbed}" version="1" xmlns="http://cyclonedx.org/schema/bom/1.7">
<metadata>
<timestamp>{scrubbed-timestamp}</timestamp>
<tools>
Expand Down Expand Up @@ -37,7 +37,7 @@
<description>Test package TestPkg.A</description>
<scope>required</scope>
<hashes>
<hash alg="SHA-512">{scrubbed-hash}531C6F999C3D7E54935AF23D43D1F34E34F13C64</hash>
<hash alg="SHA-512">{scrubbed-hash}</hash>
</hashes>
<licenses>
<license>
Expand All @@ -53,4 +53,4 @@
<dependency ref="pkg:nuget/TestPkg.A@1.0.0" />
</dependency>
</dependencies>
</bom>
</bom>
125 changes: 125 additions & 0 deletions CycloneDX.E2ETests/Tests/ConditionalPackageTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// This file is part of CycloneDX Tool for .NET
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) OWASP Foundation. All Rights Reserved.

using System.Threading.Tasks;
using CycloneDX.E2ETests.Builders;
using CycloneDX.E2ETests.Infrastructure;
using Xunit;

namespace CycloneDX.E2ETests.Tests
{
/// <summary>
/// Tests that the <c>--configuration</c> flag correctly filters conditional
/// <c>PackageReference</c> items based on MSBuild configuration conditions.
/// </summary>
[Collection("E2E")]
public sealed class ConditionalPackageTests
{
private readonly E2EFixture _fixture;

// XML injected into the .csproj: TestPkg.A is always present;
// TestPkg.C is only included when Configuration == Debug.
private const string ConditionalPackageXml = """
<ItemGroup>
<PackageReference Include="TestPkg.A" Version="1.0.0" />
<PackageReference Include="TestPkg.C" Version="1.0.0" Condition="'$(Configuration)' == 'Debug'" />
</ItemGroup>
""";

public ConditionalPackageTests(E2EFixture fixture)
{
_fixture = fixture;
}

[Fact]
public async Task NoConfiguration_BothPackagesAppearInBom()
{
// Without --configuration, dotnet restore uses the default configuration
// (Debug), so both the unconditional and the Debug-only package are restored.
using var solution = await new SolutionBuilder("ConditionalPkgNoConfigSln")
.AddProject("MyApp", p => p
.WithTargetFramework("net8.0")
.WithRawXml(ConditionalPackageXml))
.BuildAsync(_fixture.NuGetFeedUrl);

using var outputDir = solution.CreateOutputDir();

var result = await _fixture.Runner.RunAsync(
solution.SolutionFile,
outputDir.Path,
new ToolRunOptions { NuGetFeedUrl = _fixture.NuGetFeedUrl });

Assert.True(result.Success, $"Tool failed:\n{result.StdErr}");
Assert.Contains("TestPkg.A", result.BomContent);
Assert.Contains("TestPkg.C", result.BomContent);
}

[Fact]
public async Task DebugConfiguration_BothPackagesAppearInBom()
{
// With --configuration Debug, the condition evaluates to true,
// so TestPkg.C should be present alongside TestPkg.A.
using var solution = await new SolutionBuilder("ConditionalPkgDebugSln")
.AddProject("MyApp", p => p
.WithTargetFramework("net8.0")
.WithRawXml(ConditionalPackageXml))
.BuildAsync(_fixture.NuGetFeedUrl);

using var outputDir = solution.CreateOutputDir();

var result = await _fixture.Runner.RunAsync(
solution.SolutionFile,
outputDir.Path,
new ToolRunOptions
{
NuGetFeedUrl = _fixture.NuGetFeedUrl,
Configuration = "Debug"
});

Assert.True(result.Success, $"Tool failed:\n{result.StdErr}");
Assert.Contains("TestPkg.A", result.BomContent);
Assert.Contains("TestPkg.C", result.BomContent);
}

[Fact]
public async Task ReleaseConfiguration_OnlyUnconditionalPackageAppearInBom()
{
// With --configuration Release, the Debug-only condition is false,
// so TestPkg.C must NOT appear; TestPkg.A must still be present.
using var solution = await new SolutionBuilder("ConditionalPkgReleaseSln")
.AddProject("MyApp", p => p
.WithTargetFramework("net8.0")
.WithRawXml(ConditionalPackageXml))
.BuildAsync(_fixture.NuGetFeedUrl);

using var outputDir = solution.CreateOutputDir();

var result = await _fixture.Runner.RunAsync(
solution.SolutionFile,
outputDir.Path,
new ToolRunOptions
{
NuGetFeedUrl = _fixture.NuGetFeedUrl,
Configuration = "Release"
});

Assert.True(result.Success, $"Tool failed:\n{result.StdErr}");
Assert.Contains("TestPkg.A", result.BomContent);
Assert.DoesNotContain("TestPkg.C", result.BomContent);
}
}
}
102 changes: 69 additions & 33 deletions CycloneDX.Tests/DotnetUtilsServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,36 +137,72 @@ public void GetPackageCachePaths_ReturnsGlobalCacheAndFallbackCachePaths()
path => Assert.Equal(XFS.Path(@"c:\dotnet\sdk\NuGetFallbackFolder"), path));
}

[Fact]
public void GetPackageCachePaths_ReturnsGlobalCacheWithoutFallbackCachePath()
{
var fileSystem = new MockFileSystem();

var dotnetCommandService = new Mock<IDotnetCommandService>();
dotnetCommandService
.Setup(m => m.Run("--list-sdks"))
.Returns(new DotnetCommandResult
{
ExitCode = 0,
StdOut = @"2.2.402 [" + XFS.Path(@"c:\dotnet\sdk") + "]"
});
dotnetCommandService
.Setup(m => m.Run("nuget locals global-packages --list"))
.Returns(new DotnetCommandResult
{
ExitCode = 0,
StdOut = @"info : global-packages: " + XFS.Path(@"c:\user\.nuget\packages")
});

var dotnetUtilsService = new DotnetUtilsService(
fileSystem, dotnetCommandService.Object);

// act
var cachePaths = dotnetUtilsService.GetPackageCachePaths();

Assert.Collection(
cachePaths.Result,
path => Assert.Equal(XFS.Path(@"c:\user\.nuget\packages"), path));
}
}
}
[Fact]
public void GetPackageCachePaths_ReturnsGlobalCacheWithoutFallbackCachePath()
{
var fileSystem = new MockFileSystem();

var dotnetCommandService = new Mock<IDotnetCommandService>();
dotnetCommandService
.Setup(m => m.Run("--list-sdks"))
.Returns(new DotnetCommandResult
{
ExitCode = 0,
StdOut = @"2.2.402 [" + XFS.Path(@"c:\dotnet\sdk") + "]"
});
dotnetCommandService
.Setup(m => m.Run("nuget locals global-packages --list"))
.Returns(new DotnetCommandResult
{
ExitCode = 0,
StdOut = @"info : global-packages: " + XFS.Path(@"c:\user\.nuget\packages")
});

var dotnetUtilsService = new DotnetUtilsService(
fileSystem, dotnetCommandService.Object);

// act
var cachePaths = dotnetUtilsService.GetPackageCachePaths();

Assert.Collection(
cachePaths.Result,
path => Assert.Equal(XFS.Path(@"c:\user\.nuget\packages"), path));
}

[Fact]
public void Restore_WithoutConfiguration_DoesNotPassConfigurationProperty()
{
string capturedArguments = null;
var dotnetCommandService = new Mock<IDotnetCommandService>();
dotnetCommandService
.Setup(m => m.Run(It.IsAny<string>()))
.Callback<string>(args => capturedArguments = args)
.Returns(new DotnetCommandResult { ExitCode = 0, StdOut = "" });

var dotnetUtilsService = new DotnetUtilsService(
new MockFileSystem(), dotnetCommandService.Object);

dotnetUtilsService.Restore(XFS.Path(@"c:\Project\Project.csproj"), null, null, null);

Assert.DoesNotContain("-p:Configuration=", capturedArguments);
}

[Fact]
public void Restore_WithConfiguration_PassesConfigurationPropertyToDotnetRestore()
{
string capturedArguments = null;
var dotnetCommandService = new Mock<IDotnetCommandService>();
dotnetCommandService
.Setup(m => m.Run(It.IsAny<string>()))
.Callback<string>(args => capturedArguments = args)
.Returns(new DotnetCommandResult { ExitCode = 0, StdOut = "" });

var dotnetUtilsService = new DotnetUtilsService(
new MockFileSystem(), dotnetCommandService.Object);

dotnetUtilsService.Restore(XFS.Path(@"c:\Project\Project.csproj"), null, null, "Release");

Assert.Contains("-p:Configuration=Release", capturedArguments);
}
}
}
52 changes: 45 additions & 7 deletions CycloneDX.Tests/ProjectFileServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ public async Task GetProjectDotnetDependencys_WithProjectAssetsFile_ReturnsDotne
});
var mockDotnetUtilsService = new Mock<IDotnetUtilsService>();
mockDotnetUtilsService
.Setup(s => s.Restore(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
.Setup(s => s.Restore(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
.Returns(new DotnetUtilsResult());
mockDotnetUtilsService
.Setup(s => s.GetAssetsPath(It.IsAny<string>()))
Expand Down Expand Up @@ -145,7 +145,7 @@ public async Task GetProjectDotnetDependencys_WithProjectAssetsFileWithoutRestor
});
var mockDotnetUtilsService = new Mock<IDotnetUtilsService>();
mockDotnetUtilsService
.Setup(s => s.Restore(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
.Setup(s => s.Restore(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
.Throws(new ApplicationException("Restore should not be called"));
mockDotnetUtilsService
.Setup(s => s.GetAssetsPath(It.IsAny<string>()))
Expand Down Expand Up @@ -184,7 +184,7 @@ public async Task GetProjectDotnetDependencys_WithProjectAssetsFile_ReturnsMulti
});
var mockDotnetUtilsService = new Mock<IDotnetUtilsService>();
mockDotnetUtilsService
.Setup(s => s.Restore(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
.Setup(s => s.Restore(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
.Returns(new DotnetUtilsResult());
mockDotnetUtilsService
.Setup(s => s.GetAssetsPath(It.IsAny<string>()))
Expand Down Expand Up @@ -225,7 +225,7 @@ public async Task GetProjectDotnetDependencys_WithPackagesConfig_ReturnsDotnetDe
});
var mockDotnetUtilsService = new Mock<IDotnetUtilsService>();
mockDotnetUtilsService
.Setup(s => s.Restore(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
.Setup(s => s.Restore(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
.Returns(new DotnetUtilsResult());
mockDotnetUtilsService
.Setup(s => s.GetAssetsPath(It.IsAny<string>()))
Expand Down Expand Up @@ -268,7 +268,7 @@ public async Task GetProjectDotnetDependencys_WithPackagesConfig_ReturnsMultiple
});
var mockDotnetUtilsService = new Mock<IDotnetUtilsService>();
mockDotnetUtilsService
.Setup(s => s.Restore(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
.Setup(s => s.Restore(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
.Returns(new DotnetUtilsResult());
mockDotnetUtilsService
.Setup(s => s.GetAssetsPath(It.IsAny<string>()))
Expand Down Expand Up @@ -450,7 +450,7 @@ public async Task RecursivelyGetProjectDotnetDependencys_WithAssetsFile_WritesRe
});
var mockDotnetUtilsService = new Mock<IDotnetUtilsService>();
mockDotnetUtilsService
.Setup(s => s.Restore(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
.Setup(s => s.Restore(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
.Returns(new DotnetUtilsResult());
mockDotnetUtilsService
.Setup(s => s.GetAssetsPath(It.IsAny<string>()))
Expand Down Expand Up @@ -494,7 +494,7 @@ public async Task RecursivelyGetProjectDotnetDependencys_WithoutAssetsFile_DoesN
});
var mockDotnetUtilsService = new Mock<IDotnetUtilsService>();
mockDotnetUtilsService
.Setup(s => s.Restore(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
.Setup(s => s.Restore(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
.Returns(new DotnetUtilsResult());
mockDotnetUtilsService
.Setup(s => s.GetAssetsPath(It.IsAny<string>()))
Expand Down Expand Up @@ -529,6 +529,44 @@ await projectFileService.RecursivelyGetProjectDotnetDependencysAsync(
Assert.DoesNotContain("Consider removing --recursive", capturedError.ToString());
}

[Fact]
public async Task GetProjectDotnetDependencys_WithConfiguration_ForwardsConfigurationToRestore()
{
var mockFileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
{
{ XFS.Path(@"c:\Project\Project.csproj"), "<Project Sdk=\"Microsoft.NET.Sdk\" />" },
{ XFS.Path(@"c:\Project\obj\project.assets.json"), "" },
});
var mockDotnetUtilsService = new Mock<IDotnetUtilsService>();
mockDotnetUtilsService
.Setup(s => s.Restore(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
.Returns(new DotnetUtilsResult());
mockDotnetUtilsService
.Setup(s => s.GetAssetsPath(It.IsAny<string>()))
.Returns(new DotnetUtilsResult<string> { Result = "" });
var mockPackageFileService = new Mock<IPackagesFileService>();
var mockProjectAssetsFileService = new Mock<IProjectAssetsFileService>();
mockProjectAssetsFileService
.Setup(s => s.GetDotnetDependencys(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>()))
.Returns(new HashSet<DotnetDependency>());
var projectFileService = new ProjectFileService(
mockFileSystem,
mockDotnetUtilsService.Object,
mockPackageFileService.Object,
mockProjectAssetsFileService.Object);

await projectFileService.GetProjectDotnetDependencysAsync(
XFS.Path(@"c:\Project\Project.csproj"), "", false, null, null, "Release").ConfigureAwait(true);

mockDotnetUtilsService.Verify(
s => s.Restore(
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
"Release"),
Times.Once);
}

// -----------------------------------------------------------------------
// GetAssemblyNameAndVersion — version property chain
// -----------------------------------------------------------------------
Expand Down
Loading
Loading