diff --git a/src/Microsoft.DotNet.Build.Tasks.Feed.Tests/BuildManifestUtilTests.cs b/src/Microsoft.DotNet.Build.Tasks.Feed.Tests/BuildManifestUtilTests.cs index 47cfb6e2878..9e25c484b57 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Feed.Tests/BuildManifestUtilTests.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Feed.Tests/BuildManifestUtilTests.cs @@ -335,7 +335,7 @@ public void RoundTripFromTaskItemsToFileToXml() new TaskItem(localPackagePath, new Dictionary() { { "CertificateName", "IHasACert" }, - { "PublicKeyToken", "BLORG" } + { "PublicKeyToken", "abcdabcdabcdabcd" } }) }; @@ -454,7 +454,7 @@ public void RoundTripFromTaskItemsToFileToXml() { item.Include.Should().Be("test-package-a.1.0.0.nupkg"); item.CertificateName.Should().Be("IHasACert"); - item.PublicKeyToken.Should().Be("BLORG"); + item.PublicKeyToken.Should().Be("abcdabcdabcdabcd"); }); modelFromFile.SigningInformation.FileSignInfo.Should().SatisfyRespectively( item => @@ -466,12 +466,12 @@ public void RoundTripFromTaskItemsToFileToXml() item => { item.Include.Should().Be("MyCert"); - item.DualSigningAllowed.Should().Be("false"); + item.DualSigningAllowed.Should().Be(false); }, item => { item.Include.Should().Be("MyOtherCert"); - item.DualSigningAllowed.Should().Be("true"); + item.DualSigningAllowed.Should().Be(true); }); modelFromFile.SigningInformation.FileExtensionSignInfo.Should().SatisfyRespectively( item => @@ -553,7 +553,7 @@ public void SignInfoIsCorrectlyPopulatedFromItems() new TaskItem(localPackagePath, new Dictionary() { { "CertificateName", "IHasACert" }, - { "PublicKeyToken", "BLORG" } + { "PublicKeyToken", "abcdabcdabcdabcd" } }) }; @@ -607,7 +607,7 @@ public void SignInfoIsCorrectlyPopulatedFromItems() { item.Include.Should().Be("test-package-a.1.0.0.nupkg"); item.CertificateName.Should().Be("IHasACert"); - item.PublicKeyToken.Should().Be("BLORG"); + item.PublicKeyToken.Should().Be("abcdabcdabcdabcd"); }); model.SigningInformation.FileSignInfo.Should().SatisfyRespectively( item => @@ -619,12 +619,12 @@ public void SignInfoIsCorrectlyPopulatedFromItems() item => { item.Include.Should().Be("MyCert"); - item.DualSigningAllowed.Should().Be("false"); + item.DualSigningAllowed.Should().Be(false); }, item => { item.Include.Should().Be("MyOtherCert"); - item.DualSigningAllowed.Should().Be("true"); + item.DualSigningAllowed.Should().Be(true); }); model.SigningInformation.FileExtensionSignInfo.Should().SatisfyRespectively( item => diff --git a/src/Microsoft.DotNet.Build.Tasks.Feed/src/BuildManifestUtil.cs b/src/Microsoft.DotNet.Build.Tasks.Feed/src/BuildManifestUtil.cs index 9af60d330d6..5bb44eaf1e4 100644 --- a/src/Microsoft.DotNet.Build.Tasks.Feed/src/BuildManifestUtil.cs +++ b/src/Microsoft.DotNet.Build.Tasks.Feed/src/BuildManifestUtil.cs @@ -203,7 +203,7 @@ public static void WriteAsXml(this BuildModel buildModel, string filePath, TaskL foreach (var signInfo in certificatesSignInfo) { var attributes = signInfo.CloneCustomMetadata() as IDictionary; - parsedCertificatesSignInfoModel.Add(new CertificatesSignInfoModel { Include = signInfo.ItemSpec, DualSigningAllowed = attributes["DualSigningAllowed"] }); + parsedCertificatesSignInfoModel.Add(new CertificatesSignInfoModel { Include = signInfo.ItemSpec, DualSigningAllowed = bool.Parse(attributes["DualSigningAllowed"]) }); } } diff --git a/src/Microsoft.DotNet.VersionTools/lib/src/BuildManifest/Model/SigningInformationModel.cs b/src/Microsoft.DotNet.VersionTools/lib/src/BuildManifest/Model/SigningInformationModel.cs index 4458c1345a6..176a544a9b2 100644 --- a/src/Microsoft.DotNet.VersionTools/lib/src/BuildManifest/Model/SigningInformationModel.cs +++ b/src/Microsoft.DotNet.VersionTools/lib/src/BuildManifest/Model/SigningInformationModel.cs @@ -36,35 +36,43 @@ public void Add(SigningInformationModel source) "SigningInformation", Enumerable.Concat( FileExtensionSignInfo + .ThrowIfInvalidFileExtensionSignInfo() .OrderBy(fe => fe.Include, StringComparer.OrdinalIgnoreCase) .ThenBy(fe => fe.CertificateName, StringComparer.OrdinalIgnoreCase) .Select(fe => fe.ToXml()), FileSignInfo + .ThrowIfInvalidFileSignInfo() .OrderBy(f => f.Include, StringComparer.OrdinalIgnoreCase) .ThenBy(f => f.CertificateName, StringComparer.OrdinalIgnoreCase) .Select(f => f.ToXml())) .Concat(CertificatesSignInfo + .ThrowIfInvalidCertificateSignInfo() .OrderBy(f => f.Include, StringComparer.OrdinalIgnoreCase) - .ThenBy(f => f.DualSigningAllowed, StringComparer.OrdinalIgnoreCase) + .ThenBy(f => f.DualSigningAllowed) .Select(f => f.ToXml())) .Concat(ItemsToSign .OrderBy(i => i.Include, StringComparer.OrdinalIgnoreCase) .Select(i => i.ToXml())) .Concat(StrongNameSignInfo + .ThrowIfInvalidStrongNameSignInfo() .OrderBy(s => s.Include, StringComparer.OrdinalIgnoreCase) .ThenBy(s => s.PublicKeyToken, StringComparer.OrdinalIgnoreCase) .Select(s => s.ToXml()))); public bool IsEmpty() => !FileExtensionSignInfo.Any() && !FileSignInfo.Any() - && !ItemsToSign.Any() && !StrongNameSignInfo.Any(); + && !ItemsToSign.Any() && !StrongNameSignInfo.Any() && !CertificatesSignInfo.Any(); public static SigningInformationModel Parse(XElement xml) => xml == null ? null : new SigningInformationModel { - FileExtensionSignInfo = xml.Elements("FileExtensionSignInfo").Select(FileExtensionSignInfoModel.Parse).ToList(), - FileSignInfo = xml.Elements("FileSignInfo").Select(FileSignInfoModel.Parse).ToList(), + FileExtensionSignInfo = xml.Elements("FileExtensionSignInfo").Select(FileExtensionSignInfoModel.Parse) + .ThrowIfInvalidFileExtensionSignInfo().ToList(), + FileSignInfo = xml.Elements("FileSignInfo").Select(FileSignInfoModel.Parse) + .ThrowIfInvalidFileSignInfo().ToList(), ItemsToSign = xml.Elements("ItemsToSign").Select(ItemToSignModel.Parse).ToList(), - StrongNameSignInfo = xml.Elements("StrongNameSignInfo").Select(StrongNameSignInfoModel.Parse).ToList(), - CertificatesSignInfo = xml.Elements("CertificatesSignInfo").Select(CertificatesSignInfoModel.Parse).ToList(), + StrongNameSignInfo = xml.Elements("StrongNameSignInfo").Select(StrongNameSignInfoModel.Parse) + .ThrowIfInvalidStrongNameSignInfo().ToList(), + CertificatesSignInfo = xml.Elements("CertificatesSignInfo").Select(CertificatesSignInfoModel.Parse) + .ThrowIfInvalidCertificateSignInfo().ToList(), }; } @@ -124,6 +132,19 @@ public string CertificateName get { return Attributes.GetOrDefault(nameof(CertificateName)); } set { Attributes[nameof(CertificateName)] = value; } } + + public string TargetFramework + { + get { return Attributes.GetOrDefault(nameof(TargetFramework)); } + set { Attributes[nameof(TargetFramework)] = value; } + } + + public string PublicKeyToken + { + get { return Attributes.GetOrDefault(nameof(PublicKeyToken)); } + set { Attributes[nameof(PublicKeyToken)] = value; } + } + public override string ToString() => $"File \"{Include}\" is signed with {CertificateName}"; public XElement ToXml() => new XElement( @@ -155,10 +176,10 @@ public string Include set { Attributes[nameof(Include)] = value; } } - public string DualSigningAllowed + public bool DualSigningAllowed { - get { return Attributes.GetOrDefault(nameof(DualSigningAllowed)); } - set { Attributes[nameof(DualSigningAllowed)] = value; } + get { return bool.Parse(Attributes.GetOrDefault(nameof(DualSigningAllowed))); } + set { Attributes[nameof(DualSigningAllowed)] = value.ToString(); } } public override string ToString() => $"Certificate \"{Include}\" has DualSigningAllowed set to {DualSigningAllowed}"; diff --git a/src/Microsoft.DotNet.VersionTools/lib/src/BuildManifest/Model/SigningInformationParsingExtensions.cs b/src/Microsoft.DotNet.VersionTools/lib/src/BuildManifest/Model/SigningInformationParsingExtensions.cs new file mode 100644 index 00000000000..c54320fc45a --- /dev/null +++ b/src/Microsoft.DotNet.VersionTools/lib/src/BuildManifest/Model/SigningInformationParsingExtensions.cs @@ -0,0 +1,228 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.Versioning; + +namespace Microsoft.DotNet.VersionTools.BuildManifest.Model +{ + public static class SigningInformationParsingExtensions + { + /// + /// Check the file sign extension information + /// + /// - Throw if there are any file extension sign information entries that conflict, meaning + /// the same extension has different certificates. + /// + /// - Throw if certicates are empty strings or Path.GetFileExtension(info.Include) != info.Include. + /// + /// File extension sign infos + /// File extension sign infos + public static IEnumerable ThrowIfInvalidFileExtensionSignInfo( + this IEnumerable fileExtensionSignInfos) + { + Dictionary> extensionToCertMapping = new Dictionary>( + StringComparer.OrdinalIgnoreCase); + foreach (var signInfo in fileExtensionSignInfos) + { + if (string.IsNullOrWhiteSpace(signInfo.CertificateName)) + { + throw new ArgumentException($"Value of FileExtensionSignInfo 'CertificateName' is invalid, must be non-empty."); + } + + if (string.IsNullOrWhiteSpace(signInfo.Include)) + { + throw new ArgumentException($"Value of FileExtensionSignInfo 'Include' is invalid, must be non-empty."); + } + + if (!signInfo.Include.Equals(Path.GetExtension(signInfo.Include))) + { + throw new ArgumentException($"Value of FileExtensionSignInfo Include is invalid: '{signInfo.Include}' is not returned by Path.GetExtension('{signInfo.Include}')"); + } + + if (!extensionToCertMapping.TryGetValue(signInfo.Include, out var hashSet)) + { + hashSet = new HashSet(StringComparer.OrdinalIgnoreCase); + extensionToCertMapping.Add(signInfo.Include, hashSet); + } + hashSet.Add(signInfo.CertificateName); + } + + var conflicts = extensionToCertMapping.Where(kv => kv.Value.Count() > 1); + + if (conflicts.Count() > 0) + { + throw new ArgumentException( + $"Some extensions have conflicting FileExtensionSignInfo: {string.Join(", ", conflicts.Select(s => s.Key))}"); + } + + return fileExtensionSignInfos; + } + + /// + /// Throw if there are any explicit signing information entries that conflict. Explicit + /// entries would conflict if the certificates were different and the following properties + /// are identical: + /// - File name + /// - Target framework + /// - Public key token (case insensitive) + /// + /// File sign info entries + /// File sign info entries + public static IEnumerable ThrowIfInvalidFileSignInfo( + this IEnumerable fileSignInfo) + { + // Create a simple dictionary where the key is "filename/tfm/pkt" + Dictionary> keyToCertMapping = new Dictionary>( + StringComparer.OrdinalIgnoreCase); + foreach (var signInfo in fileSignInfo) + { + if (signInfo.Include.IndexOfAny(new[] { '/', '\\' }) >= 0) + { + throw new ArgumentException($"FileSignInfo should specify file name and extension, not a full path: '{signInfo.Include}'"); + } + + if (!string.IsNullOrWhiteSpace(signInfo.TargetFramework) && !IsValidTargetFrameworkName(signInfo.TargetFramework)) + { + throw new ArgumentException($"TargetFramework metadata of FileSignInfo '{signInfo.Include}' is invalid: '{signInfo.TargetFramework}'"); + } + + if (string.IsNullOrWhiteSpace(signInfo.CertificateName)) + { + throw new ArgumentException($"CertificateName metadata of FileSignInfo '{signInfo.Include}' should be non-empty."); + } + + if (!string.IsNullOrEmpty(signInfo.PublicKeyToken) && !IsValidPublicKeyToken(signInfo.PublicKeyToken)) + { + throw new ArgumentException($"PublicKeyToken metadata of FileSignInfo '{signInfo.Include}' is invalid: '{signInfo.PublicKeyToken}'"); + } + + string key = $"{signInfo.Include}/{signInfo.TargetFramework}/{signInfo.PublicKeyToken?.ToLower()}"; + if (!keyToCertMapping.TryGetValue(key, out var hashSet)) + { + hashSet = new HashSet(StringComparer.OrdinalIgnoreCase); + keyToCertMapping.Add(key, hashSet); + } + hashSet.Add(signInfo.CertificateName); + } + + var conflicts = keyToCertMapping.Where(kv => kv.Value.Count() > 1); + + if (conflicts.Count() > 0) + { + throw new ArgumentException( + $"The following files have conflicting FileSignInfo entries: {string.Join(", ", conflicts.Select(s => s.Key.Substring(0, s.Key.IndexOf("/"))))}"); + } + + return fileSignInfo; + } + + public static bool IsValidTargetFrameworkName(string tfn) + { + try + { + new FrameworkName(tfn); + return true; + } + catch (Exception) + { + return false; + } + } + + public static bool IsValidPublicKeyToken(string pkt) + { + if (pkt == null) return false; + + if (pkt.Length != 16) return false; + + return pkt.ToLower().All(c => (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z')); + } + + /// + /// Throw if there are dual sign info entries that are conflicting. + /// If the cert names are the same, but DualSigningAllowed is different. + /// + /// File sign info entries + /// File sign info entries + public static IEnumerable ThrowIfInvalidCertificateSignInfo( + this IEnumerable certificateSignInfo) + { + Dictionary> extensionToCertMapping = new Dictionary>(); + foreach (var signInfo in certificateSignInfo) + { + if (string.IsNullOrWhiteSpace(signInfo.Include)) + { + throw new ArgumentException($"CertificateName metadata of CertificatesSignInfo is invalid. Must not be empty"); + } + + if (!extensionToCertMapping.TryGetValue(signInfo.Include, out var hashSet)) + { + hashSet = new HashSet(); + extensionToCertMapping.Add(signInfo.Include, hashSet); + } + hashSet.Add(signInfo.DualSigningAllowed); + } + + var conflicts = extensionToCertMapping.Where(kv => kv.Value.Count() > 1); + + if (conflicts.Count() > 0) + { + throw new ArgumentException( + $"Some certificates have conflicting DualSigningAllowed entries: {string.Join(", ", conflicts.Select(s => s.Key))}"); + } + + return certificateSignInfo; + } + + /// + /// Throw if there conflicting strong name entries. A strong name entry uses the public key token + /// as the key, mapping to a strong name and a cert. + /// + /// File sign info entries + /// File sign info entries + public static IEnumerable ThrowIfInvalidStrongNameSignInfo( + this IEnumerable strongNameSignInfo) + { + Dictionary> pktMapping = new Dictionary>(StringComparer.OrdinalIgnoreCase); + foreach (var signInfo in strongNameSignInfo) + { + if (string.IsNullOrWhiteSpace(signInfo.Include)) + { + throw new ArgumentException($"An invalid strong name was specified in StrongNameSignInfo. Must not be empty."); + } + + if (!IsValidPublicKeyToken(signInfo.PublicKeyToken)) + { + throw new ArgumentException($"PublicKeyToken metadata of StrongNameSignInfo is not a valid public key token: '{signInfo.PublicKeyToken}'"); + } + + if (string.IsNullOrWhiteSpace(signInfo.CertificateName)) + { + throw new ArgumentException($"CertificateName metadata of StrongNameSignInfo is invalid. Must not be empty"); + } + + string value = $"{signInfo.Include}/{signInfo.CertificateName}"; + if (!pktMapping.TryGetValue(signInfo.PublicKeyToken, out var hashSet)) + { + hashSet = new HashSet(); + pktMapping.Add(signInfo.PublicKeyToken, hashSet); + } + hashSet.Add(value); + } + + var conflicts = pktMapping.Where(kv => kv.Value.Count() > 1); + + if (conflicts.Count() > 0) + { + throw new ArgumentException( + $"Some public key tokens have conflicting StrongNameSignInfo entries: {string.Join(", ", conflicts.Select(s => s.Key))}"); + } + + return strongNameSignInfo; + } + } +} diff --git a/src/Microsoft.DotNet.VersionTools/tests/BuildManifest/BuildManifestClientTests.cs b/src/Microsoft.DotNet.VersionTools/tests/BuildManifest/BuildManifestClientTests.cs index 248bb475943..4af14ca66d1 100644 --- a/src/Microsoft.DotNet.VersionTools/tests/BuildManifest/BuildManifestClientTests.cs +++ b/src/Microsoft.DotNet.VersionTools/tests/BuildManifest/BuildManifestClientTests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.DotNet.VersionTools.Automation; @@ -13,6 +13,7 @@ using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; +using FluentAssertions; namespace Microsoft.DotNet.VersionTools.Tests.BuildManifest { @@ -183,15 +184,16 @@ public async Task TestPushConflictingChangeAsync() .Setup(c => c.GetGitHubFileContentsAsync(It.IsAny(), proj, fakeCommitHash)) .ReturnsAsync(() => fakeNewExistingBuild.ToXml().ToString()); - await Assert.ThrowsAsync( - async () => await client.PushChangeAsync( + var pushClient = client.PushChangeAsync( new BuildManifestChange( new BuildManifestLocation(proj, @ref, basePath), message, fakeExistingBuild.Identity.BuildId, new[] { addSemaphorePath }, _ => { } - ))); + )); + Func act = async () => { await pushClient; }; + await act.Should().ThrowAsync(); mockGitHub.VerifyAll(); } diff --git a/src/Microsoft.DotNet.VersionTools/tests/BuildManifest/ManifestModelTests.cs b/src/Microsoft.DotNet.VersionTools/tests/BuildManifest/ManifestModelTests.cs index d9b8ff21036..d43fcff003a 100644 --- a/src/Microsoft.DotNet.VersionTools/tests/BuildManifest/ManifestModelTests.cs +++ b/src/Microsoft.DotNet.VersionTools/tests/BuildManifest/ManifestModelTests.cs @@ -1,13 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.Generic; using Microsoft.DotNet.VersionTools.BuildManifest.Model; +using System; +using System.Collections.Generic; +using System.Text; using System.Xml.Linq; using Xunit; using Xunit.Abstractions; -using System.Reflection.Metadata; -using System.Reflection; +using FluentAssertions; namespace Microsoft.DotNet.VersionTools.Tests.BuildManifest { @@ -20,6 +21,346 @@ public ManifestModelTests(ITestOutputHelper output) _output = output; } + /// + /// Given a set of file sign extension infos, + /// ToXml should throw with an appropriate error message if they are invalid + /// + [Theory] + [InlineData(null, ".ps1", "foocert")] + [InlineData(null, ".ps1", "foocert", ".PS1", "foocert")] + [InlineData(null, ".bar", "foocert", ".PS1", "foocert")] + [InlineData(null, ".ps1", "FOOCERT", ".PS1", "foocert")] + [InlineData(typeof(ArgumentException), ".ps1", "FOOCERT", ".ps1", "barcert")] // Conflict + [InlineData(typeof(ArgumentException), ".ps1", "FOOCERT", ".PS1", "barcert")] // Conflict + [InlineData(typeof(ArgumentException), "foo.ps1", "FOOCERT", ".PS1", "barcert")] // Conflict + [InlineData(typeof(ArgumentException), ".ps1", "")] // Can't be empty + [InlineData(typeof(ArgumentException), "", "bar")] // Can't be empty + public void ManifestModelToXmlValidatesFileExtensionSignInfos(Type exceptionType, params string[] infos) + { + if (infos.Length % 2 != 0) + { + throw new ArgumentException(); + } + + List models = new List(); + + // Include is first arg, cert name is second + // InlineData can't pass tuple types so using this instead. + for (int i = 0; i < infos.Length / 2; i++) + { + models.Add(new FileExtensionSignInfoModel() { Include = infos[i * 2], CertificateName = infos[i * 2 + 1] }); + } + + SigningInformationModel signInfo = new SigningInformationModel() + { + FileExtensionSignInfo = models + }; + + VerifyToXml(exceptionType, signInfo); + } + + private static void VerifyToXml(Type expectedExceptionType, SigningInformationModel signInfo) + { + if (expectedExceptionType != null) + { + Action act = () => signInfo.ToXml(); + act.Should().Throw().And.Should().BeOfType(expectedExceptionType); + } + else + { + signInfo.ToXml().Should().NotBeNull(); + } + } + + /// + /// Given a set of file sign extension infos, + /// Parse should throw with an appropriate error message if they are invalid. + /// + [Theory] + [InlineData(null, ".ps1", "foocert")] + [InlineData(null, ".ps1", "foocert", ".PS1", "foocert")] + [InlineData(null, ".bar", "foocert", ".PS1", "foocert")] + [InlineData(null, ".ps1", "FOOCERT", ".PS1", "foocert")] + [InlineData(typeof(ArgumentException), ".ps1", "FOOCERT", ".ps1", "barcert")] // Conflict + [InlineData(typeof(ArgumentException), ".ps1", "FOOCERT", ".PS1", "barcert")] // Conflict + [InlineData(typeof(ArgumentException), "foo.ps1", "FOOCERT", ".PS1", "barcert")] // Conflict + [InlineData(typeof(ArgumentException), ".ps1", "")] // Can't be empty + [InlineData(typeof(ArgumentException), "", "bar")] // Can't be empty + public void ManifestModelFromXmlValidatesFileExtensionSignInfos(Type exceptionType, params string[] infos) + { + if (infos.Length % 2 != 0) + { + throw new ArgumentException(); + } + + StringBuilder builder = new StringBuilder(); + builder.AppendLine(""); + + // Include is first arg, cert name is second + // InlineData can't pass tuple types so using this instead. + for (int i = 0; i < infos.Length / 2; i++) + { + builder.AppendLine($""); + } + + builder.AppendLine(""); + + VerifyFromXml(exceptionType, builder); + } + + private static void VerifyFromXml(Type expectedExceptionType, StringBuilder builder) + { + if (expectedExceptionType != null) + { + Action act = () => SigningInformationModel.Parse(XElement.Parse(builder.ToString())); + act.Should().Throw().And.Should().BeOfType(expectedExceptionType); + } + else + { + SigningInformationModel.Parse(XElement.Parse(builder.ToString())).Should().NotBeNull(); + } + } + + /// + /// Given a set of explicit file sign infos that conflict, + /// ToXml should throw with an appropriate error message. + /// + /// param order is Include CertificateName TargetFramework PublicKeyToken + /// + [Theory] + [InlineData(null, "bar.bat", "foocert", null, null)] + [InlineData(typeof(ArgumentException), "foo/bar.bat", "foocert", null, null)] // Invalid file name + [InlineData(typeof(ArgumentException), "foo\\bar.bat", "foocert", null, null)] // Invalid file name + [InlineData(typeof(ArgumentException), "bar.bat", "", null, null)] // Empty cert + [InlineData(typeof(ArgumentException), "bar.bat", null, null, null)] // Null cert + [InlineData(typeof(ArgumentException), "bar.bat", "foocert", "net5", null)] // Invalid tfm + [InlineData(typeof(ArgumentException), "bar.bat", "foocert", "net5.0", "zzz")] // Invalid pkt + [InlineData(null, "bar.bat", "foocert", null, null, "bar.bat2", "foocert", null, null)] + [InlineData(typeof(ArgumentException), "bar.bat", "foocert2", null, null, "bar.bat", "foocert", null, null)] + [InlineData(null, "bar.bat", "foocert2", ".NETCoreApp,Version=v5.0", null, "bar.bat", "foocert", ".NETCoreApp,Version=v3.1", null)] + [InlineData(null, "bar.bat", "foocert2", ".NETCoreApp,Version=v1.0", "aaaaaaaaaaaaaaaa", "bar.bat", "foocert", ".NETCoreApp,Version=v1.0", "aaaaaaaaaaaaaaab")] + [InlineData(typeof(ArgumentException), "bar.bat", "foocert2", ".NETCoreApp,Version=v1.0", "aaaaaaaaaaaaaaaa", "bar.bat", "foocert", ".NETCoreApp,Version=v1.0", "aaaaaaaaaaaaaaaa")] + public void ManifestModelToXmlValidatesFileSignInfos(Type exceptionType, params string[] infos) + { + if (infos.Length % 4 != 0) + { + throw new ArgumentException(); + } + + List models = new List(); + + for (int i = 0; i < infos.Length / 4; i++) + { + models.Add(new FileSignInfoModel() + { + Include = infos[i * 4], + CertificateName = infos[i * 4 + 1], + TargetFramework = infos[i * 4 + 2], + PublicKeyToken = infos[i * 4 + 3], + }); + } + + SigningInformationModel signInfo = new SigningInformationModel() + { + FileSignInfo = models + }; + + VerifyToXml(exceptionType, signInfo); + } + + /// + /// Given a set of explicit file sign infos that or are invalid, + /// Parse should throw with an appropriate error message if they are invalid. + /// + /// param order is Include CertificateName TargetFramework PublicKeyToken + [Theory] + [InlineData(null, "bar.bat", "foocert", null, null)] + [InlineData(typeof(ArgumentException), "foo/bar.bat", "foocert", null, null)] // Invalid file name + [InlineData(typeof(ArgumentException), "foo\\bar.bat", "foocert", null, null)] // Invalid file name + [InlineData(typeof(ArgumentException), "bar.bat", "", null, null)] // Empty cert + [InlineData(typeof(ArgumentException), "bar.bat", null, null, null)] // Null cert + [InlineData(typeof(ArgumentException), "bar.bat", "foocert", "net5", null)] // Invalid tfm + [InlineData(typeof(ArgumentException), "bar.bat", "foocert", "net5.0", "zzz")] // Invalid pkt + [InlineData(null, "bar.bat", "foocert", null, null, "bar.bat2", "foocert", null, null)] + [InlineData(typeof(ArgumentException), "bar.bat", "foocert2", null, null, "bar.bat", "foocert", null, null)] + [InlineData(null, "bar.bat", "foocert2", ".NETCoreApp,Version=v5.0", null, "bar.bat", "foocert", ".NETCoreApp,Version=v3.1", null)] + [InlineData(null, "bar.bat", "foocert2", ".NETCoreApp,Version=v1.0", "aaaaaaaaaaaaaaaa", "bar.bat", "foocert", ".NETCoreApp,Version=v1.0", "aaaaaaaaaaaaaaab")] + [InlineData(typeof(ArgumentException), "bar.bat", "foocert2", ".NETCoreApp,Version=v1.0", "aaaaaaaaaaaaaaaa", "bar.bat", "foocert", ".NETCoreApp,Version=v1.0", "aaaaaaaaaaaaaaaa")] + public void ManifestModelFromXmlValidatesFileSignInfos(Type exceptionType, params string[] infos) + { + if (infos.Length % 4 != 0) + { + throw new ArgumentException(); + } + + StringBuilder builder = new StringBuilder(); + builder.AppendLine(""); + + List models = new List(); + + for (int i = 0; i < infos.Length / 4; i++) + { + string targetFramework = infos[i * 4 + 2] != null ? $"TargetFramework=\"{infos[i * 4 + 2]}\"" : ""; + string publicKeyToken = infos[i * 4 + 3] != null ? $"PublicKeyToken=\"{infos[i * 4 + 3]}\"" : ""; + builder.AppendLine($""); + } + + builder.AppendLine(""); + + VerifyFromXml(exceptionType, builder); + } + + /// + /// Given a set of certificate sign infos that conflict or are invalid, + /// ToXml should throw with an appropriate error message. + /// + /// param order is CertificateName DualSigningAllowed + /// + [Theory] + [InlineData(null, "foocert", "true")] + [InlineData(null, "foocert", "false")] + [InlineData(typeof(ArgumentException), "foocert", "true", "foocert", "false")] + [InlineData(null, "foocert", "false", "foocert2", "false")] + public void ManifestModelToXmlValidatesCertificateSignInfo(Type exceptionType, params string[] infos) + { + if (infos.Length % 2 != 0) + { + throw new ArgumentException(); + } + + List models = new List(); + + for (int i = 0; i < infos.Length / 2; i++) + { + models.Add(new CertificatesSignInfoModel() + { + Include = infos[i * 2], + DualSigningAllowed = bool.Parse(infos[i * 2 + 1]) + }); + } + + SigningInformationModel signInfo = new SigningInformationModel() + { + CertificatesSignInfo = models + }; + + VerifyToXml(exceptionType, signInfo); + } + + /// + /// Given a set of certificate sign infos that conflict or are invalid, + /// Parse should throw with an appropriate error message if they are invalid. + /// + /// param order is Include DualSigningAllowed + [Theory] + [InlineData(null, "foocert", "true")] + [InlineData(null, "foocert", "false")] + [InlineData(typeof(ArgumentException), "", "true")] // No cert + [InlineData(typeof(ArgumentException), "", "false")] // No cert + [InlineData(typeof(FormatException), "foocert", "FORKS")] // Invalid bool + [InlineData(typeof(FormatException), "foocert", "")] // No dual signing allowed param + [InlineData(typeof(ArgumentException), "foocert", "true", "foocert", "false")] + [InlineData(null, "foocert", "false", "foocert2", "false")] + public void ManifestModelFromXmlValidatesCertificateSignInfo(Type exceptionType, params string[] infos) + { + if (infos.Length % 2 != 0) + { + throw new ArgumentException(); + } + + StringBuilder builder = new StringBuilder(); + builder.AppendLine(""); + + List models = new List(); + + for (int i = 0; i < infos.Length / 2; i++) + { + builder.AppendLine($""); + } + + builder.AppendLine(""); + + VerifyFromXml(exceptionType, builder); + } + + /// + /// Given a set of strong name sign that conflict or are invalid, + /// ToXml should throw with an appropriate error message. + /// + /// param order is Include (strong name) PublicKeyToken CertificateName + /// + [Theory] + [InlineData(null, "MyStrongName", "aaaaaaaaaaaaaaaa", "Mycert")] // Valid + [InlineData(typeof(ArgumentException), "MyStrongName", "", "Mycert")] // Invalid strong name key + [InlineData(typeof(ArgumentException), "MyStrongName", "aaaaaaaaaaaaaaaa", "")] // Invalid cert + [InlineData(typeof(ArgumentException), "MyStrongName", "aaaaaaaaaaaa", "Mycert")] // Invalid strong name key + [InlineData(null, "MyStrongName", "aaaaaaaaaaaaaaaa", "Mycert", "MyStrongName", "aaaaaaaaaaaaaaaa", "Mycert")] // No conflicts + [InlineData(typeof(ArgumentException), "MyStrongName2", "aaaaaaaaaaaaaaaa", "Mycert", "MyStrongName", "aaaaaaaaaaaaaaaa", "Mycert")] // Different strong names + [InlineData(typeof(ArgumentException), "MyStrongName", "aaaaaaaaaaaaaaaa", "Mycert", "MyStrongName", "aaaaaaaaaaaaaaaa", "Mycert2")] // Different certs + [InlineData(null, "MyStrongName", "aaaaaaaaaaaaaaab", "Mycert", "MyStrongName", "aaaaaaaaaaaaaaaa", "Mycert2")] // No conflict + public void ManifestModelToXmlValidatesStrongNameSignInfo(Type exceptionType, params string[] infos) + { + if (infos.Length % 3 != 0) + { + throw new ArgumentException(); + } + + List models = new List(); + + for (int i = 0; i < infos.Length / 3; i++) + { + models.Add(new StrongNameSignInfoModel() + { + Include = infos[i * 3], + PublicKeyToken = infos[i * 3 + 1], + CertificateName = infos[i * 3 + 2] + }); + } + + SigningInformationModel signInfo = new SigningInformationModel() + { + StrongNameSignInfo = models + }; + + VerifyToXml(exceptionType, signInfo); + } + + /// + /// Given a set of strong name sign that conflict or are invalid, + /// Parse should throw with an appropriate error message if they are invalid. + /// + /// param order is Include (strong name) PublicKeyToken CertificateName + [Theory] + [InlineData(null, "MyStrongName", "aaaaaaaaaaaaaaaa", "Mycert")] // Valid + [InlineData(typeof(ArgumentException), "MyStrongName", "", "Mycert")] // Invalid strong name key + [InlineData(typeof(ArgumentException), "MyStrongName", "aaaaaaaaaaaaaaaa", "")] // Invalid cert + [InlineData(typeof(ArgumentException), "MyStrongName", "aaaaaaaaaaaa", "Mycert")] // Invalid strong name key + [InlineData(null, "MyStrongName", "aaaaaaaaaaaaaaaa", "Mycert", "MyStrongName", "aaaaaaaaaaaaaaaa", "Mycert")] // No conflicts + [InlineData(typeof(ArgumentException), "MyStrongName2", "aaaaaaaaaaaaaaaa", "Mycert", "MyStrongName", "aaaaaaaaaaaaaaaa", "Mycert")] // Different strong names + [InlineData(typeof(ArgumentException), "MyStrongName", "aaaaaaaaaaaaaaaa", "Mycert", "MyStrongName", "aaaaaaaaaaaaaaaa", "Mycert2")] // Different certs + [InlineData(null, "MyStrongName", "aaaaaaaaaaaaaaab", "Mycert", "MyStrongName", "aaaaaaaaaaaaaaaa", "Mycert2")] // No conflict + public void ManifestModelFromXmlValidatesStrongNameSignInfo(Type exceptionType, params string[] infos) + { + if (infos.Length % 3 != 0) + { + throw new ArgumentException(); + } + + StringBuilder builder = new StringBuilder(); + builder.AppendLine(""); + + List models = new List(); + + for (int i = 0; i < infos.Length / 3; i++) + { + builder.AppendLine($""); + } + + builder.AppendLine(""); + + VerifyFromXml(exceptionType, builder); + } + [Fact] public void TestExampleBuildManifestRoundtrip() { @@ -27,9 +368,7 @@ public void TestExampleBuildManifestRoundtrip() var model = BuildModel.Parse(xml); XElement modelXml = model.ToXml(); - Assert.True( - XNode.DeepEquals(xml, modelXml), - "Model failed to output the parsed XML."); + XNode.DeepEquals(xml, modelXml).Should().BeTrue("Model failed to output the parsed XML."); } [Fact] @@ -39,9 +378,7 @@ public void TestExampleOrchestratedBuildManifestRoundtrip() var model = OrchestratedBuildModel.Parse(xml); XElement modelXml = model.ToXml(); - Assert.True( - XNode.DeepEquals(xml, modelXml), - "Model failed to output the parsed XML."); + XNode.DeepEquals(xml, modelXml).Should().BeTrue("Model failed to output the parsed XML."); } [Fact] @@ -52,9 +389,7 @@ public void TestExampleCustomBuildIdentityRoundtrip() var model = BuildModel.Parse(xml); XElement modelXml = model.ToXml(); - Assert.True( - XNode.DeepEquals(xml, modelXml), - "Model failed to output the parsed XML."); + XNode.DeepEquals(xml, modelXml).Should().BeTrue("Model failed to output the parsed XML."); } [Fact] @@ -64,7 +399,7 @@ public void TestPackageOnlyBuildManifest() XElement modelXml = model.ToXml(); XElement xml = XElement.Parse(@""); - Assert.True(XNode.DeepEquals(xml, modelXml)); + XNode.DeepEquals(xml, modelXml).Should().BeTrue("Model failed to output the parsed XML."); } [Fact] @@ -96,7 +431,7 @@ public void TestMergeBuildManifests() "); - Assert.True(XNode.DeepEquals(xml, modelXml)); + XNode.DeepEquals(xml, modelXml).Should().BeTrue("Model failed to output the parsed XML."); } [Fact] @@ -107,7 +442,7 @@ public void TestManifestWithSigningInformation() XElement modelXml = buildModel.ToXml(); XElement xml = XElement.Parse(ExampleBuildStringWithSigningInformation); - Assert.True(XNode.DeepEquals(xml, modelXml)); + XNode.DeepEquals(xml, modelXml).Should().BeTrue("Model failed to output the parsed XML."); } private BuildModel CreatePackageOnlyBuildManifestModel() diff --git a/src/Microsoft.DotNet.VersionTools/tests/BuildManifest/VersionIdentiferTests.cs b/src/Microsoft.DotNet.VersionTools/tests/BuildManifest/VersionIdentiferTests.cs index bd8a69d1a59..eaa6ad85c8d 100644 --- a/src/Microsoft.DotNet.VersionTools/tests/BuildManifest/VersionIdentiferTests.cs +++ b/src/Microsoft.DotNet.VersionTools/tests/BuildManifest/VersionIdentiferTests.cs @@ -1,11 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using FluentAssertions; +using Microsoft.DotNet.VersionTools.BuildManifest; using System.Collections.Generic; using System.IO; using Xunit; -using Microsoft.DotNet.VersionTools.BuildManifest; -using NuGet.ContentModel; namespace Microsoft.DotNet.VersionTools.Tests.BuildManifest { @@ -39,7 +39,7 @@ public class VersionTests [InlineData("What-Is-A.FooPackage-2.2.nupkg", null)] public void ValidateSimpleVersions(string assetName, string version) { - Assert.Equal(version, VersionIdentifier.GetVersion(assetName)); + VersionIdentifier.GetVersion(assetName).Should().Be(version); } [Fact] @@ -52,11 +52,11 @@ public void ValidateVersions() // First check whether the original version number can be identified string expectedVersion = testAsset.ExpectedVersion; string actualVersion = VersionIdentifier.GetVersion(testAsset.Name); - Assert.True(expectedVersion == actualVersion, $"Line {testAsset.Line} has incorrect computed version {actualVersion}"); + actualVersion.Should().Be(expectedVersion, $"Line {testAsset.Line} has incorrect computed version {actualVersion}"); // Then check that all versions can be removed from the path of any blob asset string expectedNameWithoutVersions = testAsset.NameWithoutVersions; string actualNameWithoutVersions = VersionIdentifier.RemoveVersions(testAsset.Name); - Assert.True(expectedNameWithoutVersions == actualNameWithoutVersions, $"Line {testAsset.Line} has incorrect asset name without versions {actualNameWithoutVersions}"); + actualNameWithoutVersions.Should().Be(expectedNameWithoutVersions, $"Line {testAsset.Line} has incorrect asset name without versions {actualNameWithoutVersions}"); } } @@ -71,8 +71,8 @@ private List GetTestAssets() if (!string.IsNullOrEmpty(line)) { string[] elements = line.Split(','); - - Assert.True(elements.Length == 3, $"Line {i+1} is missing version or path-without-versions info"); + + elements.Should().HaveCount(3, $"Line {i + 1} is missing version or path-without-versions info"); string name = elements[0]; string expectedVersion = string.IsNullOrEmpty(elements[1]) ? null : elements[1]; diff --git a/src/Microsoft.DotNet.VersionTools/tests/Microsoft.DotNet.VersionTools.Tests.csproj b/src/Microsoft.DotNet.VersionTools/tests/Microsoft.DotNet.VersionTools.Tests.csproj index fd126562b46..764d42703fd 100644 --- a/src/Microsoft.DotNet.VersionTools/tests/Microsoft.DotNet.VersionTools.Tests.csproj +++ b/src/Microsoft.DotNet.VersionTools/tests/Microsoft.DotNet.VersionTools.Tests.csproj @@ -6,6 +6,7 @@ +