From 674ef888a2d147691d19f9e3ff65a1f09e35d82e Mon Sep 17 00:00:00 2001 From: Maksim Ivanyuk Date: Wed, 3 Nov 2021 02:53:36 +0300 Subject: [PATCH] Add `-HttpVersion` parameter to web cmdlets (#15853) --- .../Common/HttpVersionCompletionsAttribute.cs | 51 +++++++ .../Common/WebRequestPSCmdlet.Common.cs | 24 +++- .../resources/WebCmdletStrings.resx | 4 +- ...rgumentToVersionTransformationAttribute.cs | 58 ++++++++ .../engine/InternalCommands.cs | 46 ++----- .../TabCompletion/TabCompletion.Tests.ps1 | 21 +++ .../WebCmdlets.Tests.ps1 | 32 +++++ .../WebListener/Controllers/GetController.cs | 17 +-- test/tools/WebListener/Program.cs | 126 ++++++++++-------- ...rgumentToVersionTransformationAttribute.cs | 51 +++++++ 10 files changed, 329 insertions(+), 101 deletions(-) create mode 100644 src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/HttpVersionCompletionsAttribute.cs create mode 100644 src/System.Management.Automation/engine/ArgumentToVersionTransformationAttribute.cs create mode 100644 test/xUnit/csharp/test_ArgumentToVersionTransformationAttribute.cs diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/HttpVersionCompletionsAttribute.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/HttpVersionCompletionsAttribute.cs new file mode 100644 index 000000000..05eb8ed96 --- /dev/null +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/HttpVersionCompletionsAttribute.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Net; +using System.Reflection; + +namespace Microsoft.PowerShell.Commands +{ + /// + /// A completer for HTTP version names. + /// + internal sealed class HttpVersionCompletionsAttribute : ArgumentCompletionsAttribute + { + public static readonly string[] AllowedVersions; + + static HttpVersionCompletionsAttribute() + { + FieldInfo[] fields = typeof(HttpVersion).GetFields(BindingFlags.Static | BindingFlags.Public); + + var versions = new List(fields.Length - 1); + + for (int i = 0; i < fields.Length; i++) + { + // skip field Unknown and not Version type + if (fields[i].Name == nameof(HttpVersion.Unknown) || fields[i].FieldType != typeof(Version)) + { + continue; + } + + var version = (Version?)fields[i].GetValue(null); + + if (version is not null) + { + versions.Add(version.ToString()); + } + } + + AllowedVersions = versions.ToArray(); + } + + /// + public HttpVersionCompletionsAttribute() : base(AllowedVersions) + { + } + } +} diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs index d0abc8f3c..a5a4060e7 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs @@ -107,6 +107,18 @@ namespace Microsoft.PowerShell.Commands #endregion + #region HTTP Version + + /// + /// Gets or sets the HTTP Version property. + /// + [Parameter] + [ArgumentToVersionTransformation] + [HttpVersionCompletions] + public virtual Version HttpVersion { get; set; } + + #endregion + #region Session /// /// Gets or sets the Session property. @@ -1081,6 +1093,11 @@ namespace Microsoft.PowerShell.Commands // create the base WebRequest object var request = new HttpRequestMessage(httpMethod, requestUri); + if (HttpVersion is not null) + { + request.Version = HttpVersion; + } + // pull in session data if (WebSession.Headers.Count > 0) { @@ -1413,6 +1430,7 @@ namespace Microsoft.PowerShell.Commands string reqVerboseMsg = string.Format( CultureInfo.CurrentCulture, WebCmdletStrings.WebMethodInvocationVerboseMsg, + requestWithoutRange.Version, requestWithoutRange.Method, requestWithoutRange.RequestUri, requestContentLength); @@ -1505,10 +1523,14 @@ namespace Microsoft.PowerShell.Commands if (request.Content != null) requestContentLength = request.Content.Headers.ContentLength.Value; - string reqVerboseMsg = string.Format(CultureInfo.CurrentCulture, + string reqVerboseMsg = string.Format( + CultureInfo.CurrentCulture, WebCmdletStrings.WebMethodInvocationVerboseMsg, + request.Version, request.Method, + request.RequestUri, requestContentLength); + WriteVerbose(reqVerboseMsg); HttpResponseMessage response = GetResponse(client, request, keepAuthorization); diff --git a/src/Microsoft.PowerShell.Commands.Utility/resources/WebCmdletStrings.resx b/src/Microsoft.PowerShell.Commands.Utility/resources/WebCmdletStrings.resx index 3d6635730..8ba25baff 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/resources/WebCmdletStrings.resx +++ b/src/Microsoft.PowerShell.Commands.Utility/resources/WebCmdletStrings.resx @@ -129,7 +129,7 @@ The cmdlet cannot run because the following parameter is not specified: Credential. The supplied Authentication type requires a Credential. Specify Credential, then retry. - + The cmdlet cannot run because the following parameter is not specified: Token. The supplied Authentication type requires a Token. Specify Token, then retry. @@ -247,7 +247,7 @@ Following rel link {0} - {0} with {1}-byte payload + HTTP/{0} {1} {2} with {3}-byte payload The remote server indicated it could not resume downloading. The local file will be overwritten. diff --git a/src/System.Management.Automation/engine/ArgumentToVersionTransformationAttribute.cs b/src/System.Management.Automation/engine/ArgumentToVersionTransformationAttribute.cs new file mode 100644 index 000000000..facaa9e3e --- /dev/null +++ b/src/System.Management.Automation/engine/ArgumentToVersionTransformationAttribute.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace System.Management.Automation +{ + /// + /// To make it easier to specify a version, we add some conversions that wouldn't happen otherwise: + /// * A simple integer, i.e. 2; + /// * A string without a dot, i.e. "2". + /// + internal class ArgumentToVersionTransformationAttribute : ArgumentTransformationAttribute + { + /// + public override object Transform(EngineIntrinsics engineIntrinsics, object inputData) + { + object version = PSObject.Base(inputData); + + if (version is string versionStr) + { + if (TryConvertFromString(versionStr, out var convertedVersion)) + { + return convertedVersion; + } + + if (versionStr.Contains('.')) + { + // If the string contains a '.', let the Version constructor handle the conversion. + return inputData; + } + } + + if (version is double) + { + // The conversion to int below is wrong, but the usual conversions will turn + // the double into a string, so just return the original object. + return inputData; + } + + if (LanguagePrimitives.TryConvertTo(version, out var majorVersion)) + { + return new Version(majorVersion, 0); + } + + return inputData; + } + + protected virtual bool TryConvertFromString(string versionString, [NotNullWhen(true)] out Version? version) + { + version = null; + return false; + } + } +} diff --git a/src/System.Management.Automation/engine/InternalCommands.cs b/src/System.Management.Automation/engine/InternalCommands.cs index 8d628aea6..d9d54539b 100644 --- a/src/System.Management.Automation/engine/InternalCommands.cs +++ b/src/System.Management.Automation/engine/InternalCommands.cs @@ -6,7 +6,6 @@ using System.Collections; using System.Collections.Generic; using System.Dynamic; using System.Globalization; -using System.Linq; using System.Linq.Expressions; using System.Management.Automation; using System.Management.Automation.Internal; @@ -15,8 +14,10 @@ using System.Management.Automation.PSTasks; using System.Runtime.CompilerServices; using System.Text; using System.Threading; + using CommonParamSet = System.Management.Automation.Internal.CommonParameters; using Dbg = System.Management.Automation.Diagnostics; +using NotNullWhen = System.Diagnostics.CodeAnalysis.NotNullWhenAttribute; namespace Microsoft.PowerShell.Commands { @@ -2647,46 +2648,19 @@ namespace Microsoft.PowerShell.Commands private SwitchParameter _off; /// - /// To make it easier to specify a version, we add some conversions that wouldn't happen otherwise: - /// * A simple integer, i.e. 2 - /// * A string without a dot, i.e. "2" - /// * The string 'latest', which we interpret to be the current version of PowerShell. + /// Handle 'latest', which we interpret to be the current version of PowerShell. /// - private sealed class ArgumentToVersionTransformationAttribute : ArgumentTransformationAttribute + private sealed class ArgumentToPSVersionTransformationAttribute : ArgumentToVersionTransformationAttribute { - public override object Transform(EngineIntrinsics engineIntrinsics, object inputData) + protected override bool TryConvertFromString(string versionString, [NotNullWhen(true)] out Version version) { - object version = PSObject.Base(inputData); - - string versionStr = version as string; - if (versionStr != null) + if (string.Equals("latest", versionString, StringComparison.OrdinalIgnoreCase)) { - if (versionStr.Equals("latest", StringComparison.OrdinalIgnoreCase)) - { - return PSVersionInfo.PSVersion; - } - - if (versionStr.Contains('.')) - { - // If the string contains a '.', let the Version constructor handle the conversion. - return inputData; - } + version = PSVersionInfo.PSVersion; + return true; } - if (version is double) - { - // The conversion to int below is wrong, but the usual conversions will turn - // the double into a string, so just return the original object. - return inputData; - } - - int majorVersion; - if (LanguagePrimitives.TryConvertTo(version, out majorVersion)) - { - return new Version(majorVersion, 0); - } - - return inputData; + return base.TryConvertFromString(versionString, out version); } } @@ -2711,7 +2685,7 @@ namespace Microsoft.PowerShell.Commands /// Gets or sets strict mode in the current scope. /// [Parameter(ParameterSetName = "Version", Mandatory = true)] - [ArgumentToVersionTransformation] + [ArgumentToPSVersionTransformation] [ValidateVersion] [Alias("v")] public Version Version diff --git a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 index 2674a74a2..67aa4bb59 100644 --- a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 +++ b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 @@ -1134,6 +1134,27 @@ dir -Recurse ` $res.CompletionMatches | Should -HaveCount 4 [string]::Join(',', ($res.CompletionMatches.completiontext | Sort-Object)) | Should -BeExactly "-Path,-PipelineVariable,-PSPath,-pv" } + + It "Test completion for HttpVersion parameter name" { + $inputStr = 'Invoke-WebRequest -HttpV' + $res = TabExpansion2 -inputScript $inputStr -cursorColumn $inputStr.Length + $res.CompletionMatches | Should -HaveCount 1 + $res.CompletionMatches[0].CompletionText | Should -BeExactly "-HttpVersion" + } + + It "Test completion for HttpVersion parameter" { + $inputStr = 'Invoke-WebRequest -HttpVersion ' + $res = TabExpansion2 -inputScript $inputStr -cursorColumn $inputStr.Length + $res.CompletionMatches | Should -HaveCount 4 + [string]::Join(',', ($res.CompletionMatches.completiontext | Sort-Object)) | Should -BeExactly "1.0,1.1,2.0,3.0" + } + + It "Test completion for HttpVersion parameter with input" { + $inputStr = 'Invoke-WebRequest -HttpVersion 1' + $res = TabExpansion2 -inputScript $inputStr -cursorColumn $inputStr.Length + $res.CompletionMatches | Should -HaveCount 2 + [string]::Join(',', ($res.CompletionMatches.completiontext | Sort-Object)) | Should -BeExactly "1.0,1.1" + } } Context "Module completion for 'using module'" { diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 index e4973f551..0f1f2ca59 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 @@ -469,6 +469,23 @@ Describe "Invoke-WebRequest tests" -Tags "Feature", "RequireAdminOnWindows" { $result.Output.Headers.Connection | Should -Be "Close" } + It "Validate Invoke-WebRequest -HttpVersion ''" -Skip:(!$IsWindows) -TestCases @( + @{ httpVersion = '1.1'}, + @{ httpVersion = '2'} + ) { + param($httpVersion) + # Operation options + $uri = Get-WebListenerUrl -Test 'Get' -Https + $command = "Invoke-WebRequest -Uri $uri -HttpVersion $httpVersion -SkipCertificateCheck" + + $result = ExecuteWebCommand -command $command + ValidateResponse -response $result + + # Validate response content + $jsonContent = $result.Output.Content | ConvertFrom-Json + $jsonContent.protocol | Should -Be "HTTP/$httpVersion" + } + It "Validate Invoke-WebRequest -MaximumRedirection" { $uri = Get-WebListenerUrl -Test 'Redirect' -TestValue '3' $command = "Invoke-WebRequest -Uri '$uri' -MaximumRedirection 4" @@ -2118,6 +2135,21 @@ Describe "Invoke-RestMethod tests" -Tags "Feature", "RequireAdminOnWindows" { $result.Output.Headers.Connection | Should -Be "Close" } + It "Validate Invoke-RestMethod -HttpVersion ''" -Skip:(!$IsWindows) -TestCases @( + @{ httpVersion = '1.1'}, + @{ httpVersion = '2'} + ) { + param($httpVersion) + # Operation options + $uri = Get-WebListenerUrl -Test 'Get' -Https + $command = "Invoke-RestMethod -Uri $uri -HttpVersion $httpVersion -SkipCertificateCheck" + + $result = ExecuteWebCommand -command $command + + # Validate response + $result.Output.protocol | Should -Be "HTTP/$httpVersion" + } + It "Validate Invoke-RestMethod -MaximumRedirection" { $uri = Get-WebListenerUrl -Test 'Redirect' -TestValue '3' $command = "Invoke-RestMethod -Uri '$uri' -MaximumRedirection 4" diff --git a/test/tools/WebListener/Controllers/GetController.cs b/test/tools/WebListener/Controllers/GetController.cs index 03f733f50..631886bb7 100644 --- a/test/tools/WebListener/Controllers/GetController.cs +++ b/test/tools/WebListener/Controllers/GetController.cs @@ -1,13 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.Collections; -using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Http.Extensions; using mvc.Models; @@ -32,12 +28,13 @@ namespace mvc.Controllers Hashtable output = new Hashtable { - {"args", args}, - {"headers", headers}, - {"origin", Request.HttpContext.Connection.RemoteIpAddress.ToString()}, - {"url", UriHelper.GetDisplayUrl(Request)}, - {"query", Request.QueryString.ToUriComponent()}, - {"method", Request.Method} + { "args", args }, + { "headers", headers }, + { "origin", Request.HttpContext.Connection.RemoteIpAddress.ToString() }, + { "url", UriHelper.GetDisplayUrl(Request) }, + { "query", Request.QueryString.ToUriComponent() }, + { "method", Request.Method }, + { "protocol", Request.Protocol } }; if (Request.HasFormContentType) diff --git a/test/tools/WebListener/Program.cs b/test/tools/WebListener/Program.cs index a4b575121..37a62a582 100644 --- a/test/tools/WebListener/Program.cs +++ b/test/tools/WebListener/Program.cs @@ -2,18 +2,13 @@ // Licensed under the MIT License. using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Net; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; -using System.Threading.Tasks; + using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Https; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; namespace mvc { @@ -34,52 +29,79 @@ namespace mvc WebHost.CreateDefaultBuilder() .UseStartup().UseKestrel(options => { - options.AllowSynchronousIO = true; - options.Listen(IPAddress.Loopback, int.Parse(args[2])); - options.Listen(IPAddress.Loopback, int.Parse(args[3]), listenOptions => - { - var certificate = new X509Certificate2(args[0], args[1]); - HttpsConnectionAdapterOptions httpsOption = new HttpsConnectionAdapterOptions(); - httpsOption.SslProtocols = SslProtocols.Tls12; - httpsOption.ClientCertificateMode = ClientCertificateMode.AllowCertificate; - httpsOption.ClientCertificateValidation = (inCertificate, inChain, inPolicy) => { return true; }; - httpsOption.CheckCertificateRevocation = false; - httpsOption.ServerCertificate = certificate; - listenOptions.UseHttps(httpsOption); - }); - options.Listen(IPAddress.Loopback, int.Parse(args[4]), listenOptions => - { - var certificate = new X509Certificate2(args[0], args[1]); - HttpsConnectionAdapterOptions httpsOption = new HttpsConnectionAdapterOptions(); - httpsOption.SslProtocols = SslProtocols.Tls11; - httpsOption.ClientCertificateMode = ClientCertificateMode.AllowCertificate; - httpsOption.ClientCertificateValidation = (inCertificate, inChain, inPolicy) => { return true; }; - httpsOption.CheckCertificateRevocation = false; - httpsOption.ServerCertificate = certificate; - listenOptions.UseHttps(httpsOption); - }); - options.Listen(IPAddress.Loopback, int.Parse(args[5]), listenOptions => - { - var certificate = new X509Certificate2(args[0], args[1]); - HttpsConnectionAdapterOptions httpsOption = new HttpsConnectionAdapterOptions(); - httpsOption.SslProtocols = SslProtocols.Tls; - httpsOption.ClientCertificateMode = ClientCertificateMode.AllowCertificate; - httpsOption.ClientCertificateValidation = (inCertificate, inChain, inPolicy) => { return true; }; - httpsOption.CheckCertificateRevocation = false; - httpsOption.ServerCertificate = certificate; - listenOptions.UseHttps(httpsOption); - }); - options.Listen(IPAddress.Loopback, int.Parse(args[6]), listenOptions => - { - var certificate = new X509Certificate2(args[0], args[1]); - HttpsConnectionAdapterOptions httpsOption = new HttpsConnectionAdapterOptions(); - httpsOption.SslProtocols = SslProtocols.Tls13; - httpsOption.ClientCertificateMode = ClientCertificateMode.AllowCertificate; - httpsOption.ClientCertificateValidation = (inCertificate, inChain, inPolicy) => { return true; }; - httpsOption.CheckCertificateRevocation = false; - httpsOption.ServerCertificate = certificate; - listenOptions.UseHttps(httpsOption); - }); + options.AllowSynchronousIO = true; + + options.Listen( + IPAddress.Loopback, + int.Parse(args[2]), + listenOptions => + { + listenOptions.Protocols = Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols.Http1AndHttp2; + }); + + options.Listen( + IPAddress.Loopback, + int.Parse(args[3]), + listenOptions => + { + var certificate = new X509Certificate2(args[0], args[1]); + HttpsConnectionAdapterOptions httpsOption = new HttpsConnectionAdapterOptions(); + httpsOption.SslProtocols = SslProtocols.Tls12; + httpsOption.ClientCertificateMode = ClientCertificateMode.AllowCertificate; + httpsOption.ClientCertificateValidation = (inCertificate, inChain, inPolicy) => { return true; }; + httpsOption.CheckCertificateRevocation = false; + httpsOption.ServerCertificate = certificate; + listenOptions.UseHttps(httpsOption); + listenOptions.Protocols = Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols.Http1AndHttp2; + }); + + options.Listen( + IPAddress.Loopback, + int.Parse(args[4]), + listenOptions => + { + var certificate = new X509Certificate2(args[0], args[1]); + HttpsConnectionAdapterOptions httpsOption = new HttpsConnectionAdapterOptions(); + httpsOption.SslProtocols = SslProtocols.Tls11; + httpsOption.ClientCertificateMode = ClientCertificateMode.AllowCertificate; + httpsOption.ClientCertificateValidation = (inCertificate, inChain, inPolicy) => { return true; }; + httpsOption.CheckCertificateRevocation = false; + httpsOption.ServerCertificate = certificate; + listenOptions.UseHttps(httpsOption); + listenOptions.Protocols = Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols.Http1AndHttp2; + }); + + options.Listen( + IPAddress.Loopback, + int.Parse(args[5]), + listenOptions => + { + var certificate = new X509Certificate2(args[0], args[1]); + HttpsConnectionAdapterOptions httpsOption = new HttpsConnectionAdapterOptions(); + httpsOption.SslProtocols = SslProtocols.Tls; + httpsOption.ClientCertificateMode = ClientCertificateMode.AllowCertificate; + httpsOption.ClientCertificateValidation = (inCertificate, inChain, inPolicy) => { return true; }; + httpsOption.CheckCertificateRevocation = false; + httpsOption.ServerCertificate = certificate; + listenOptions.UseHttps(httpsOption); + listenOptions.Protocols = Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols.Http1AndHttp2; + }); + + options.Listen( + IPAddress.Loopback, + int.Parse(args[6]), + listenOptions => + { + var certificate = new X509Certificate2(args[0], args[1]); + HttpsConnectionAdapterOptions httpsOption = new HttpsConnectionAdapterOptions(); + httpsOption.SslProtocols = SslProtocols.Tls13; + httpsOption.ClientCertificateMode = ClientCertificateMode.AllowCertificate; + httpsOption.ClientCertificateValidation = (inCertificate, inChain, inPolicy) => { return true; }; + httpsOption.CheckCertificateRevocation = false; + httpsOption.ServerCertificate = certificate; + listenOptions.UseHttps(httpsOption); + listenOptions.Protocols = Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols.Http1AndHttp2; + }); }) .Build(); } diff --git a/test/xUnit/csharp/test_ArgumentToVersionTransformationAttribute.cs b/test/xUnit/csharp/test_ArgumentToVersionTransformationAttribute.cs new file mode 100644 index 000000000..d7ca79367 --- /dev/null +++ b/test/xUnit/csharp/test_ArgumentToVersionTransformationAttribute.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Management.Automation; + +using Xunit; + +namespace PSTests.Parallel +{ + public class ArgumentToVersionTransformationAttributeTests + { + [Theory] + [MemberData(nameof(TestCases))] + public void TestConversion(object inputData, object expected) + { + var transformation = new ArgumentToVersionTransformationAttribute(); + var result = transformation.Transform(default, inputData); + + Assert.Equal(expected, result); + } + + public static IEnumerable TestCases() + { + // strings + yield return new object[] { "1.1", "1.1" }; + yield return new object[] { "1", new Version(1, 0) }; + yield return new object[] { string.Empty, new Version(0, 0) }; + + // doubles + yield return new object[] { 1.0, 1.0 }; + yield return new object[] { 1.1, 1.1 }; + + // ints + yield return new object[] { 1, new Version(1, 0) }; + yield return new object[] { 2, new Version(2, 0) }; + + // PSObjects + yield return new object[] { new PSObject(obj: 0), new Version(0, 0) }; + yield return new object[] { new PSObject(obj: 1), new Version(1, 0) }; + + // unhandled + var obj = new object(); + yield return new object[] { obj, obj }; + + var date = DateTimeOffset.UtcNow; + yield return new object[] { date, date }; + } + } +}