From 7d0590333d91e99a7aef0759071abbed1453fd57 Mon Sep 17 00:00:00 2001 From: Raul Portugues del Peno Date: Tue, 26 Nov 2024 17:13:13 +0200 Subject: [PATCH] Add tests --- .../swiftpm/Contracts/SwiftPMResolvedFile.cs | 10 + .../SwiftPMResolvedComponentDetector.cs | 9 +- .../SwiftPMComponentTests.cs | 80 ++++ .../SwiftPMResolvedDetectorTests.cs | 396 ++++++++++++++++++ 4 files changed, 494 insertions(+), 1 deletion(-) create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/SwiftPMComponentTests.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/SwiftPMResolvedDetectorTests.cs diff --git a/src/Microsoft.ComponentDetection.Detectors/swiftpm/Contracts/SwiftPMResolvedFile.cs b/src/Microsoft.ComponentDetection.Detectors/swiftpm/Contracts/SwiftPMResolvedFile.cs index 317e3d5e..601695f2 100644 --- a/src/Microsoft.ComponentDetection.Detectors/swiftpm/Contracts/SwiftPMResolvedFile.cs +++ b/src/Microsoft.ComponentDetection.Detectors/swiftpm/Contracts/SwiftPMResolvedFile.cs @@ -1,39 +1,49 @@ namespace Microsoft.ComponentDetection.Contracts.TypedComponent; using System.Collections.Generic; +using Newtonsoft.Json; /// /// Represents a SwiftPM component. /// public class SwiftPMResolvedFile { + [JsonProperty("pins")] public IList Pins { get; set; } + [JsonProperty("version")] public int Version { get; set; } public class SwiftPMDependency { // The name of the package + [JsonProperty("identity")] public string Identity { get; set; } // How the package is imported. Example: "remoteSourceControl" + [JsonProperty("kind")] public string Kind { get; set; } // The unique path to the repository where the package is located. Example: Git repo URL. + [JsonProperty("location")] public string Location { get; set; } // Data about the package version and commit hash. + [JsonProperty("state")] public SwiftPMState State { get; set; } public class SwiftPMState { // The commit hash of the package. + [JsonProperty("revision")] public string Revision { get; set; } // The version of the package. Might be missing. + [JsonProperty("version")] public string Version { get; set; } // The branch of the package. Might be missing. + [JsonProperty("branch")] public string Branch { get; set; } } } diff --git a/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftPMResolvedComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftPMResolvedComponentDetector.cs index cc481d72..001f9b0e 100644 --- a/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftPMResolvedComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftPMResolvedComponentDetector.cs @@ -43,7 +43,14 @@ protected override Task OnFileFoundAsync( { this.Logger.LogInformation("SwiftPMComponentDetector: Found Package.resolved file: {Location}", processRequest.ComponentStream.Location); - this.ProcessPackageResolvedFile(processRequest.SingleFileComponentRecorder, processRequest.ComponentStream); + try + { + this.ProcessPackageResolvedFile(processRequest.SingleFileComponentRecorder, processRequest.ComponentStream); + } + catch (Exception exception) + { + this.Logger.LogError(exception, "SwiftPMComponentDetector: Error processing Package.resolved file: {Location}", processRequest.ComponentStream.Location); + } return Task.CompletedTask; } diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/SwiftPMComponentTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/SwiftPMComponentTests.cs new file mode 100644 index 00000000..b69ffbec --- /dev/null +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/SwiftPMComponentTests.cs @@ -0,0 +1,80 @@ +namespace Microsoft.ComponentDetection.Detectors.Tests.SwiftPM; + +using System; +using System.Collections.Generic; +using FluentAssertions; +using Microsoft.ComponentDetection.Contracts.TypedComponent; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using PackageUrl; + +[TestClass] +public class SwiftPMComponentTests +{ + [TestMethod] + public void Constructor_ShouldInitializeProperties() + { + var name = "alamofire"; + var version = "5.9.1"; + var packageUrl = "https://github.com/Alamofire/Alamofire"; + var hash = "f455c2975872ccd2d9c81594c658af65716e9b9a"; + + var component = new SwiftPMComponent(name, version, packageUrl, hash); + + component.Name.Should().Be(name); + component.Version.Should().Be(version); + component.Type.Should().Be(ComponentType.SwiftPM); + component.Id.Should().Be($"{name} {version} - {component.Type}"); + } + + [TestMethod] + public void Constructor_ShouldThrowException_WhenNameIsNull() + { + Action action = () => new SwiftPMComponent(null, "5.9.1", "https://github.com/Alamofire/Alamofire", "f455c2975872ccd2d9c81594c658af65716e9b9a"); + action.Should().Throw().WithMessage("*name*"); + } + + [TestMethod] + public void Constructor_ShouldThrowException_WhenVersionIsNull() + { + Action action = () => new SwiftPMComponent("alamofire", null, "https://github.com/Alamofire/Alamofire", "f455c2975872ccd2d9c81594c658af65716e9b9a"); + action.Should().Throw().WithMessage("*version*"); + } + + [TestMethod] + public void Constructor_ShouldThrowException_WhenPackageUrlIsNull() + { + Action action = () => new SwiftPMComponent("alamofire", "5.9.1", null, "f455c2975872ccd2d9c81594c658af65716e9b9a"); + action.Should().Throw().WithMessage("*packageUrl*"); + } + + [TestMethod] + public void Constructor_ShouldThrowException_WhenHashIsNull() + { + Action action = () => new SwiftPMComponent("alamofire", "5.9.1", "https://github.com/Alamofire/Alamofire", null); + action.Should().Throw().WithMessage("*hash*"); + } + + [TestMethod] + public void PackageURL_ShouldReturnCorrectPackageURL() + { + var name = "alamofire"; + var version = "5.9.1"; + var packageUrl = "https://github.com/Alamofire/Alamofire"; + var hash = "f455c2975872ccd2d9c81594c658af65716e9b9a"; + + var component = new SwiftPMComponent(name, version, packageUrl, hash); + + var expectedPackageURL = new PackageURL( + type: "swift", + @namespace: "github.com", + name: name, + version: hash, + qualifiers: new SortedDictionary + { + { "repository_url", packageUrl }, + }, + subpath: null); + + component.PackageURL.Should().BeEquivalentTo(expectedPackageURL); + } +} diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/SwiftPMResolvedDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/SwiftPMResolvedDetectorTests.cs new file mode 100644 index 00000000..7bc10839 --- /dev/null +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/SwiftPMResolvedDetectorTests.cs @@ -0,0 +1,396 @@ +namespace Microsoft.ComponentDetection.Detectors.Tests.SwiftPM; + +using System; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Contracts.TypedComponent; +using Microsoft.ComponentDetection.Detectors.SwiftPM; +using Microsoft.ComponentDetection.TestsUtilities; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public class SwiftPMResolvedDetectorTests : BaseDetectorTest +{ + [TestMethod] + public async Task Test_GivenDetectorWithValidFile_WhenScan_ThenScanIsSuccessfulAndComponentsAreRegistered() + { + var validResolvedPackageFile = """ +{ + "pins" : [ + { + "identity" : "alamofire", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Alamofire/Alamofire", + "state" : { + "revision" : "f455c2975872ccd2d9c81594c658af65716e9b9a", + "version" : "5.9.1" + } + } + ], + "version" : 2 +} +"""; + var (scanResult, componentRecorder) = await this.DetectorTestUtility.WithFile( + "Package.resolved", + validResolvedPackageFile) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + + var detectedComponents = componentRecorder.GetDetectedComponents(); + + // Two components are detected because this detector registers a SwiftPM and Git component. + detectedComponents.Should().HaveCount(2); + + var typedComponents = detectedComponents.Select(c => c.Component).ToList(); + + typedComponents.Should().ContainEquivalentOf( + new SwiftPMComponent( + name: "alamofire", + version: "5.9.1", + packageUrl: "https://github.com/Alamofire/Alamofire", + hash: "f455c2975872ccd2d9c81594c658af65716e9b9a")); + + typedComponents.Should().ContainEquivalentOf( + new GitComponent( + repositoryUrl: new Uri("https://github.com/Alamofire/Alamofire"), + commitHash: "f455c2975872ccd2d9c81594c658af65716e9b9a", + tag: "5.9.1")); + } + + // Test for several packages + [TestMethod] + public async Task Test_GivenDetectorWithValidFileWithMultiplePackages_WhenScan_ThenScanIsSuccessfulAndComponentsAreRegistered() + { + var validLongResolvedPackageFile = this.validLongResolvedPackageFile; + + var (scanResult, componentRecorder) = await this.DetectorTestUtility.WithFile( + "Package.resolved", + validLongResolvedPackageFile) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + + var detectedComponents = componentRecorder.GetDetectedComponents(); + + // Two components are detected because this detector registers a SwiftPM and Git component. + detectedComponents.Should().HaveCount(6); + + var typedComponents = detectedComponents.Select(c => c.Component).ToList(); + + typedComponents.Should().ContainEquivalentOf( + new SwiftPMComponent( + name: "alamofire", + version: "5.6.0", + packageUrl: "https://github.com/Alamofire/Alamofire", + hash: "63dfa86548c4e5d5c6fd6ed42f638e388cbce529")); + + typedComponents.Should().ContainEquivalentOf( + new GitComponent( + repositoryUrl: new Uri("https://github.com/sideeffect-io/AsyncExtensions"), + commitHash: "3442d3d046800f1974bda096faaf0ac510b21154", + tag: "0.5.3")); + + typedComponents.Should().ContainEquivalentOf( + new GitComponent( + repositoryUrl: new Uri("https://github.com/devicekit/DeviceKit.git"), + commitHash: "d37e70cb2646666dcf276d7d3d4a9760a41ff8a6", + tag: "4.9.0")); + } + + [TestMethod] + public async Task Test_GivenInvalidJSONFile_WhenScan_ThenNoComponentRegisteredAndScanIsSuccessful() + { + var invalidJSONResolvedPackageFile = """ +{ + INVALID JSON +} +"""; + var (scanResult, componentRecorder) = await this.DetectorTestUtility.WithFile( + "Package.resolved", + invalidJSONResolvedPackageFile) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + componentRecorder.GetDetectedComponents().Should().BeEmpty(); + } + + [TestMethod] + public async Task Test_GivenEmptyFile_WhenScan_ThenNoComponentRegisteredAndScanIsSuccessful() + { + var emptyResolvedPackageFile = string.Empty; + var (scanResult, componentRecorder) = await this.DetectorTestUtility.WithFile( + "Package.resolved", + emptyResolvedPackageFile) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + componentRecorder.GetDetectedComponents().Should().BeEmpty(); + } + + [TestMethod] + public async Task Test_GivenResvoledPackageWithoutPins_WhenScan_ThenScanIsSuccessfulAndNoComponentsRegistered() + { + var resolvedPackageWithoutPins = """ +{ + "pins" : [ + ], + "version" : 2 +} +"""; + + var (scanResult, componentRecorder) = await this.DetectorTestUtility.WithFile( + "Package.resolved", + resolvedPackageWithoutPins) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + componentRecorder.GetDetectedComponents().Should().BeEmpty(); + } + + [TestMethod] + public async Task Test_GivenResolvedPackageWithoutIdentity_WhenScan_ThenScanIsSuccessfulAndNoComponentsRegistered() + { + var validResolvedPackageFile = """ +{ + "pins" : [ + { + "identity" : "", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Alamofire/Alamofire", + "state" : { + "revision" : "f455c2975872ccd2d9c81594c658af65716e9b9a", + "version" : "5.9.1" + } + }, + { + "kind" : "remoteSourceControl", + "location" : "https://github.com/SimplyDanny/SwiftLintPlugins", + "state" : { + "revision" : "6c3d6c32a37224179dc290f21e03d1238f3d963b", + "version" : "0.56.2" + } + } + ], + "version" : 2 +} +"""; + + var (scanResult, componentRecorder) = await this.DetectorTestUtility.WithFile( + "Package.resolved", + validResolvedPackageFile) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + componentRecorder.GetDetectedComponents().Should().BeEmpty(); + } + + [TestMethod] + public async Task Test_GivenResolvedPackageWithoutKind_WhenScan_ThenScanIsSuccessfulAndNoComponentsRegistered() + { + var resolvedPackageWithoutKind = """ +{ + "pins" : [ + { + "identity" : "alamofire", + "location" : "https://github.com/Alamofire/Alamofire", + "state" : { + "revision" : "f455c2975872ccd2d9c81594c658af65716e9b9a", + "version" : "5.9.1" + } + } + ], + "version" : 2 +} +"""; + + var (scanResult, componentRecorder) = await this.DetectorTestUtility.WithFile( + "Package.resolved", + resolvedPackageWithoutKind) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + componentRecorder.GetDetectedComponents().Should().BeEmpty(); + } + + [TestMethod] + public async Task Test_GivenResolvedPackageWithoutLocation_WhenScan_ThenScanIsSuccessfulAndNoComponentsRegistered() + { + var resolvedPackageWithoutLocation = """ +{ + "pins" : [ + { + "identity" : "alamofire", + "kind" : "remoteSourceControl", + "state" : { + "revision" : "f455c2975872ccd2d9c81594c658af65716e9b9a", + "version" : "5.9.1" + } + } + ], + "version" : 2 +} +"""; + + var (scanResult, componentRecorder) = await this.DetectorTestUtility.WithFile( + "Package.resolved", + resolvedPackageWithoutLocation) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + componentRecorder.GetDetectedComponents().Should().BeEmpty(); + } + + [TestMethod] + public async Task Test_GivenResolvedPackageWithoutState_WhenScan_ThenScanIsSuccessfulAndNoComponentsRegistered() + { + var resolvedPackageWithoutState = """ +{ + "pins" : [ + { + "identity" : "alamofire", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Alamofire/Alamofire" + } + ], + "version" : 2 +} +"""; + + var (scanResult, componentRecorder) = await this.DetectorTestUtility.WithFile( + "Package.resolved", + resolvedPackageWithoutState) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + componentRecorder.GetDetectedComponents().Should().BeEmpty(); + } + + [TestMethod] + public async Task Test_GivenResolvedPackageWithEmptyState_WhenScan_ThenScanIsSuccessfulAndNoComponentsRegistered() + { + var resolvedPackageWithEmptyState = """ +{ + "pins" : [ + { + "identity" : "alamofire", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Alamofire/Alamofire", + "state" : {} + } + ], + "version" : 2 +} +"""; + + var (scanResult, componentRecorder) = await this.DetectorTestUtility.WithFile( + "Package.resolved", + resolvedPackageWithEmptyState) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + componentRecorder.GetDetectedComponents().Should().BeEmpty(); + } + + [TestMethod] + public async Task Test_GivenResolvedPackageWithoutRevision_WhenScan_ThenScanIsSuccessfulAndNoComponentsRegistered() + { + var resolvedPackageWithoutRevision = """ +{ + "pins" : [ + { + "identity" : "alamofire", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Alamofire/Alamofire", + "state" : { + "version" : "5.9.1" + } + } + ], + "version" : 2 +} +"""; + + var (scanResult, componentRecorder) = await this.DetectorTestUtility.WithFile( + "Package.resolved", + resolvedPackageWithoutRevision) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + componentRecorder.GetDetectedComponents().Should().BeEmpty(); + } + + [TestMethod] + public async Task Test_GivenResolvedPackageWithoutVersion_WhenScan_ThenScanIsSuccessfulAndNoComponentsRegistered() + { + var resolvedPackageWithoutVersion = """ +{ + "pins" : [ + { + "identity" : "alamofire", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Alamofire/Alamofire", + "state" : { + "revision" : "f455c2975872ccd2d9c81594c658af65716e9b9a" + } + } + ], + "version" : 2 +} +"""; + + var (scanResult, componentRecorder) = await this.DetectorTestUtility.WithFile( + "Package.resolved", + resolvedPackageWithoutVersion) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + componentRecorder.GetDetectedComponents().Should().BeEmpty(); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "Test data that is better placed at the end of the file.")] + private readonly string validLongResolvedPackageFile = """ +{ + "originHash" : "6ad1e0d3ae43bde33043d3286afc3d98e5be09945ac257218cb6a9dba14466c3", + "pins" : [ + { + "identity" : "alamofire", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Alamofire/Alamofire", + "state" : { + "revision" : "63dfa86548c4e5d5c6fd6ed42f638e388cbce529", + "version" : "5.6.0" + } + }, + { + "identity" : "asyncextensions", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sideeffect-io/AsyncExtensions", + "state" : { + "branch": null, + "revision" : "3442d3d046800f1974bda096faaf0ac510b21154", + "version" : "0.5.3" + } + }, + { + "identity" : "devicekit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/devicekit/DeviceKit.git", + "state" : { + "revision" : "d37e70cb2646666dcf276d7d3d4a9760a41ff8a6", + "version" : "4.9.0" + } + }, + { + "identity" : "localdependency", + "kind" : "localSource", + "location" : "../LocalDependency" + } + ], + "version" : 2 +} +""" +; +}