diff --git a/.copilot/skills/apex-migration/SKILL.md b/.copilot/skills/apex-migration/SKILL.md index f7cd6b15c49..d850b9c2f97 100644 --- a/.copilot/skills/apex-migration/SKILL.md +++ b/.copilot/skills/apex-migration/SKILL.md @@ -274,6 +274,7 @@ using var testContext = new ApexTestContext(VisualStudio, ProjectTemplate.NetCor - Get PMC console via `GetConsole(testContext.Project)` helper method in the test class. - Don't use the method-delegates-to-async-helper pattern unless the helper is actually called from multiple classes. Inline the test logic directly in the test method. +- Do NOT add "Migrated from" comments in the test code. The PR description tracks the mapping — the code should stand on its own without referencing the old PS test. ## Tests that should NOT be migrated diff --git a/test/EndToEnd/tests/FindPackageTest.ps1 b/test/EndToEnd/tests/FindPackageTest.ps1 index 335bcc685d6..a66544d149d 100644 --- a/test/EndToEnd/tests/FindPackageTest.ps1 +++ b/test/EndToEnd/tests/FindPackageTest.ps1 @@ -1,27 +1,3 @@ -function Test-FindPackageByIdjQuery { - # Act - $packages = Find-Package jQuery - - # Assert - Assert-True $packages.Count -gt 0 "Find-Package cmdlet does not returns any package" -} - -function Test-FindPackageByIdMVC { - # Act - $packages = Find-Package microsoft.aspnet.mvc - - # Assert - Assert-True $packages.Count -gt 0 "Find-Package cmdlet does not returns any package" -} - -function Test-FindPackageByIdaspnet { - # Act - $packages = Find-Package aspnet - - # Assert - Assert-True $packages.Count -gt 0 "Find-Package cmdlet does not returns any package" -} - # As of now Find-Package does not suport wildcard yet. # TODO: Uncomment the test when the feature is implemented. function FindPackageByIdWildcard { @@ -33,46 +9,6 @@ function FindPackageByIdWildcard { Assert-True $packages.Count -gt 0 "Find-Package cmdlet does not returns any package" } -function Test-FindPackageByIdAndVersion { - # Act - $packages = Find-Package entityframework -version 6.1.2 - $version = [NuGet.Versioning.NuGetVersion]::Parse("6.1.2") - - # Assert - Assert-True $packages[0].Versions[0] -eq $version - Assert-True $packages[0].Id -eq "EntityFramework" -} - -function Test-FindPackageByIdAndPrereleaseVersion { - [SkipTest('https://github.com/NuGet/Home/issues/8496')] - param() - - # Act 1 - $packages = Find-Package TestPackage.AlwaysPrerelease - - # Assert 1 - Assert-Null $packages - - # Act 2 - $packages = Find-Package TestPackage.AlwaysPrerelease -Pre - - # Assert 2 - Assert-True $packages[0].Count -ne 0 - Assert-True $packages[0].Id -eq TestPackage.AlwaysPrerelease - Assert-True $packages[0].Versions[0].ToString() -eq "5.0.0-beta" -} - - -function Test-FindPackageByIdWithAllVersions { - # Act - $packages = Find-Package elmah.io -allversions - - # Assert - Assert-True $packages[0].Count -gt 1 - Assert-True $packages[0].Id -eq elmah.io - Assert-True $packages[0].Versions.Count -gt 4 -} - function Test-FindPackageByIdWithFirstAndSkip { [SkipTest('https://github.com/NuGet/Home/issues/8496')] param() diff --git a/test/EndToEnd/tests/GetPackageTest.ps1 b/test/EndToEnd/tests/GetPackageTest.ps1 index 7db31949c9c..ba419c43c16 100644 --- a/test/EndToEnd/tests/GetPackageTest.ps1 +++ b/test/EndToEnd/tests/GetPackageTest.ps1 @@ -8,23 +8,6 @@ function Test-GetPackageRetunsMoreThanServerPagingLimit { } -function Test-GetPackageWithoutOpenSolutionThrows { - Assert-Throws { Get-Package } "The current environment doesn't have a solution open." -} - -function Test-GetPackageWithUpdatesListsUpdates { - # Arrange - $p = New-ConsoleApplication - - # Act - Install-Package NuGet.Core -Version 1.6.0 -Project $p.Name - Install-Package NuGet.CommandLine -Version 1.6.0 -Project $p.Name - $packages = Get-Package -Updates - - # Assert - Assert-AreEqual 2 $packages.Count -} - function Test-GetPackageCollapsesPackageVersionsForListAvailable { [SkipTest('https://github.com/NuGet/Home/issues/8849')] param() @@ -68,25 +51,6 @@ function GetPackageAcceptsAllAsSourceName { Assert-True (1 -le $p.Count) } -function Test-GetPackagesWithNoUpdatesReturnPackagesWithIsUpdateNotSet { - # Arrange & Act - $package = Get-Package -ListAvailable -First 1 - - # Assert - Assert-NotNull $package - Assert-False $package.IsUpdate -} - -function Test-GetPackageDoesNotThrowIfSolutionIsTemporary { - param($context) - - # Arrange - New-TextFile - - # Act and Assert - Assert-Throws { Get-Package } "Solution is not saved. Please save your solution before managing NuGet packages." -} - function Test-GetPackageUpdatesAfterSwitchToSourceThatDoesNotContainInstalledPackageId { [SkipTest('https://github.com/NuGet/Home/issues/10254')] diff --git a/test/NuGet.Clients.Tests/NuGetConsole.Host.PowerShell.Test/Cmdlets/FindPackageCommandTests.cs b/test/NuGet.Clients.Tests/NuGetConsole.Host.PowerShell.Test/Cmdlets/FindPackageCommandTests.cs new file mode 100644 index 00000000000..e5639cd6db9 --- /dev/null +++ b/test/NuGet.Clients.Tests/NuGetConsole.Host.PowerShell.Test/Cmdlets/FindPackageCommandTests.cs @@ -0,0 +1,334 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Management.Automation; +using System.Management.Automation.Host; +using System.Management.Automation.Runspaces; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.VisualStudio.ComponentModelHost; +using Microsoft.VisualStudio.Sdk.TestFramework; +using Microsoft.VisualStudio.Shell; +using Moq; +using NuGet.Commands; +using NuGet.Configuration; +using NuGet.PackageManagement; +using NuGet.PackageManagement.PowerShellCmdlets; +using NuGet.PackageManagement.VisualStudio; +using NuGet.ProjectManagement; +using NuGet.Protocol.Core.Types; +using NuGet.Test.Utility; +using NuGet.VisualStudio; +using Test.Utility; +using Xunit; +using PSCommand = System.Management.Automation.Runspaces.Command; + +namespace NuGetConsole.Host.PowerShell.Test +{ + [Collection(MockedVS.Collection)] + public class FindPackageCommandTests : IAsyncServiceProvider + { + private readonly Dictionary> _services = new Dictionary>(); + private readonly Mock _componentModel; + private readonly Mock _solutionManager; + + public FindPackageCommandTests(GlobalServiceProvider globalServiceProvider) + { + globalServiceProvider.Reset(); + + _solutionManager = new Mock(); + _solutionManager.SetupGet(x => x.SolutionDirectory).Returns(@"C:\test"); + _solutionManager.SetupGet(x => x.IsSolutionOpen).Returns(true); + _solutionManager.Setup(x => x.IsSolutionAvailableAsync()).ReturnsAsync(true); + + // FindPackageCommand does not call GetNuGetProjectAsync, but the base command constructor + // resolves IVsSolutionManager, so we still need a valid mock. + var defaultProject = new Mock( + new Dictionary { { NuGetProjectMetadataKeys.Name, "TestProject" } }); + _solutionManager.Setup(x => x.GetDefaultNuGetProjectAsync()) + .ReturnsAsync(defaultProject.Object); + + _componentModel = new Mock(); + _componentModel.Setup(x => x.GetService()).Returns(Mock.Of()); + _componentModel.Setup(x => x.GetService()).Returns(_solutionManager.Object); + _componentModel.Setup(x => x.GetService()).Returns(Mock.Of()); + _componentModel.Setup(x => x.GetService()).Returns(Mock.Of()); + _componentModel.Setup(x => x.GetService()).Returns(Mock.Of()); + _componentModel.Setup(x => x.GetService()).Returns(Mock.Of()); + _componentModel.Setup(x => x.GetService()).Returns(Mock.Of()); + + globalServiceProvider.AddService(typeof(SComponentModel), _componentModel.Object); + + ServiceLocator.InitializePackageServiceProvider(this); + } + + /// + /// Verifies that Find-Package returns packages matching an ID keyword. + /// + [Fact] + public async Task FindPackage_ById_ReturnsMatchingPackagesAsync() + { + // Arrange + using var pathContext = new SimpleTestPathContext(); + + await SimpleTestPackageUtility.CreatePackagesAsync( + pathContext.PackageSource, + new SimpleTestPackageContext("FindByIdPackageA", "1.0.0"), + new SimpleTestPackageContext("FindByIdPackageB", "2.0.0"), + new SimpleTestPackageContext("OtherPackage", "1.0.0")); + + SetupSourceRepositoryProvider(pathContext.PackageSource); + + using var fixture = new CmdletRunspaceFixture(activeSource: pathContext.PackageSource); + + // Act — search for packages whose ID contains "FindById" + var results = fixture.Invoke( + "Find-Package", + new Dictionary + { + { "Id", "FindById" }, + { "Source", pathContext.PackageSource }, + }); + + // Assert — at least the two matching packages are returned + results.Should().HaveCountGreaterThanOrEqualTo(2, because: "two packages whose ID contains 'FindById' exist in the source"); + var ids = results.Select(r => ((PowerShellPackage)r.BaseObject).Id).ToList(); + ids.Should().Contain("FindByIdPackageA"); + ids.Should().Contain("FindByIdPackageB"); + ids.Should().NotContain("OtherPackage"); + } + + /// + /// Verifies that Find-Package returns a result with the expected version. + /// + [Fact] + public async Task FindPackage_ByExactId_ReturnsPackageWithVersionAsync() + { + // Arrange + using var pathContext = new SimpleTestPathContext(); + + await SimpleTestPackageUtility.CreatePackagesAsync( + pathContext.PackageSource, + new SimpleTestPackageContext("VersionTestPackage", "1.2.3")); + + SetupSourceRepositoryProvider(pathContext.PackageSource); + + using var fixture = new CmdletRunspaceFixture(activeSource: pathContext.PackageSource); + + // Act + var results = fixture.Invoke( + "Find-Package", + new Dictionary + { + { "Id", "VersionTestPackage" }, + { "Source", pathContext.PackageSource }, + }); + + // Assert + results.Should().ContainSingle(); + var package = (PowerShellPackage)results[0].BaseObject; + package.Id.Should().Be("VersionTestPackage"); + package.Version.ToString().Should().Be("1.2.3"); + } + + /// + /// Verifies that Find-Package -AllVersions returns multiple versions for a package. + /// + [Fact] + public async Task FindPackage_WithAllVersions_ReturnsMultipleVersionsAsync() + { + // Arrange + using var pathContext = new SimpleTestPathContext(); + + await SimpleTestPackageUtility.CreatePackagesAsync( + pathContext.PackageSource, + new SimpleTestPackageContext("AllVersionsPackage", "1.0.0"), + new SimpleTestPackageContext("AllVersionsPackage", "2.0.0"), + new SimpleTestPackageContext("AllVersionsPackage", "3.0.0")); + + SetupSourceRepositoryProvider(pathContext.PackageSource); + + using var fixture = new CmdletRunspaceFixture(activeSource: pathContext.PackageSource); + + // Act + var results = fixture.Invoke( + "Find-Package", + new Dictionary + { + { "Id", "AllVersionsPackage" }, + { "AllVersions", true }, + { "Source", pathContext.PackageSource }, + }); + + // Assert + results.Should().ContainSingle(); + var package = (PowerShellPackage)results[0].BaseObject; + package.Id.Should().Be("AllVersionsPackage"); + var versions = package.Versions.Select(v => v.ToNormalizedString()).ToList(); + versions.Should().HaveCountGreaterThanOrEqualTo(3, because: "three versions were published"); + versions.Should().Contain("1.0.0").And.Contain("2.0.0").And.Contain("3.0.0"); + } + + /// + /// Verifies that Find-Package without -IncludePrerelease hides prerelease-only packages. + /// + [Fact] + public async Task FindPackage_WithoutPrerelease_HidesPrereleaseOnlyPackagesAsync() + { + // Arrange + using var pathContext = new SimpleTestPathContext(); + + // Only a prerelease version exists + await SimpleTestPackageUtility.CreatePackagesAsync( + pathContext.PackageSource, + new SimpleTestPackageContext("PrereleaseOnlyPackage", "1.0.0-beta")); + + SetupSourceRepositoryProvider(pathContext.PackageSource); + + using var fixture = new CmdletRunspaceFixture(activeSource: pathContext.PackageSource); + + // Act — no -IncludePrerelease flag + var results = fixture.Invoke( + "Find-Package", + new Dictionary + { + { "Id", "PrereleaseOnlyPackage" }, + { "Source", pathContext.PackageSource }, + }); + + // Assert — prerelease-only package should not appear + results.Should().BeEmpty(because: "the package only has a prerelease version and -IncludePrerelease was not specified"); + } + + /// + /// Verifies that Find-Package -IncludePrerelease returns prerelease packages. + /// + [Fact] + public async Task FindPackage_WithPrerelease_IncludesPrereleasePackagesAsync() + { + // Arrange + using var pathContext = new SimpleTestPathContext(); + + await SimpleTestPackageUtility.CreatePackagesAsync( + pathContext.PackageSource, + new SimpleTestPackageContext("PrereleaseOnlyPackage", "1.0.0-beta")); + + SetupSourceRepositoryProvider(pathContext.PackageSource); + + using var fixture = new CmdletRunspaceFixture(activeSource: pathContext.PackageSource); + + // Act — with -IncludePrerelease + var results = fixture.Invoke( + "Find-Package", + new Dictionary + { + { "Id", "PrereleaseOnlyPackage" }, + { "IncludePrerelease", true }, + { "Source", pathContext.PackageSource }, + }); + + // Assert — prerelease version should be returned + results.Should().ContainSingle(); + var package = (PowerShellPackage)results[0].BaseObject; + package.Id.Should().Be("PrereleaseOnlyPackage"); + package.Version.ToString().Should().Be("1.0.0-beta"); + } + + #region Helpers + + private void SetupSourceRepositoryProvider(string localSourcePath) + { + var localSource = new PackageSource(localSourcePath); + var sourceRepositoryProvider = TestSourceRepositoryUtility.CreateSourceRepositoryProvider(localSource); + _componentModel.Setup(x => x.GetService()).Returns(sourceRepositoryProvider); + } + + public Task GetServiceAsync(Type serviceType) + { + if (_services.TryGetValue(serviceType, out Task? task)) + { + return task!; + } + + return Task.FromResult(null); + } + + #endregion + + #region Test Infrastructure + + /// + /// Encapsulates runspace and host setup for invoking the Find-Package cmdlet in tests. + /// + private sealed class CmdletRunspaceFixture : IDisposable + { + private readonly Runspace _runspace; + + public CmdletRunspaceFixture(string activeSource = "https://contoso.com/v3/index.json") + { + var host = new TestPSHost(activeSource); + var initialSessionState = InitialSessionState.CreateDefault(); + initialSessionState.Commands.Add( + new SessionStateCmdletEntry("Find-Package", typeof(FindPackageCommand), null)); + + _runspace = RunspaceFactory.CreateRunspace(host, initialSessionState); + _runspace.Open(); + } + + public IList Invoke(string cmdletName, Dictionary parameters) + { + using var pipeline = _runspace.CreatePipeline(); + var cmd = new PSCommand(cmdletName); + foreach (var kvp in parameters) + { + cmd.Parameters.Add(kvp.Key, kvp.Value); + } + pipeline.Commands.Add(cmd); + return pipeline.Invoke().ToList(); + } + + public void Dispose() + { + _runspace.Close(); + _runspace.Dispose(); + } + } + + /// + /// Minimal PSHost that provides PrivateData with properties expected by NuGet cmdlets. + /// + private sealed class TestPSHost : PSHost + { + private readonly Guid _instanceId = Guid.NewGuid(); + private readonly PSObject _privateData; + + public TestPSHost(string activeSource) + { + _privateData = new PSObject(); + _privateData.Properties.Add(new PSNoteProperty("activePackageSource", activeSource)); + _privateData.Properties.Add(new PSNoteProperty("CancellationTokenKey", CancellationToken.None)); + } + + public override CultureInfo CurrentCulture => CultureInfo.InvariantCulture; + public override CultureInfo CurrentUICulture => CultureInfo.InvariantCulture; + public override Guid InstanceId => _instanceId; + public override string Name => "TestNuGetHost"; + public override PSObject PrivateData => _privateData; + public override PSHostUserInterface? UI => null; + public override Version Version => new Version(1, 0); + + public override void EnterNestedPrompt() { } + public override void ExitNestedPrompt() { } + public override void NotifyBeginApplication() { } + public override void NotifyEndApplication() { } + public override void SetShouldExit(int exitCode) { } + } + + #endregion + } +} diff --git a/test/NuGet.Clients.Tests/NuGetConsole.Host.PowerShell.Test/Cmdlets/GetPackageCommandTests.cs b/test/NuGet.Clients.Tests/NuGetConsole.Host.PowerShell.Test/Cmdlets/GetPackageCommandTests.cs index fc5c76f778c..47336cf1450 100644 --- a/test/NuGet.Clients.Tests/NuGetConsole.Host.PowerShell.Test/Cmdlets/GetPackageCommandTests.cs +++ b/test/NuGet.Clients.Tests/NuGetConsole.Host.PowerShell.Test/Cmdlets/GetPackageCommandTests.cs @@ -587,6 +587,48 @@ await SimpleTestPackageUtility.CreatePackagesAsync( package.Id.Should().Be("ReleaseNotesPackage"); } + /// + /// Verifies that Get-Package (installed) throws when no solution is open. + /// + [Fact] + public void GetPackage_WithNoOpenSolution_Throws() + { + // Arrange — override the default IsSolutionOpen = true to simulate no solution + _solutionManager.SetupGet(x => x.IsSolutionOpen).Returns(false); + + SetupSourceRepositoryProvider("https://contoso.com/v3/index.json"); + + using var fixture = new CmdletRunspaceFixture(activeSource: "https://contoso.com/v3/index.json"); + + // Act + Assert — terminating error is surfaced as a RuntimeException + var act = () => fixture.Invoke("Get-Package", new Dictionary()); + act.Should().Throw() + .Which.ErrorRecord.Exception.Message.Should() + .Contain("The current environment doesn't have a solution open."); + } + + /// + /// Verifies that Get-Package (installed) throws when the solution exists but is not yet saved + /// (e.g., a text file was opened without saving the solution). + /// + [Fact] + public void GetPackage_WithUnsavedSolution_Throws() + { + // Arrange — solution is open but not saved (IsSolutionAvailableAsync returns false) + _solutionManager.SetupGet(x => x.IsSolutionOpen).Returns(true); + _solutionManager.Setup(x => x.IsSolutionAvailableAsync()).ReturnsAsync(false); + + SetupSourceRepositoryProvider("https://contoso.com/v3/index.json"); + + using var fixture = new CmdletRunspaceFixture(activeSource: "https://contoso.com/v3/index.json"); + + // Act + Assert — terminating error is surfaced as a RuntimeException + var act = () => fixture.Invoke("Get-Package", new Dictionary()); + act.Should().Throw() + .Which.ErrorRecord.Exception.Message.Should() + .Contain("Solution is not saved."); + } + #region Helpers private void SetupSourceRepositoryProvider(string localSourcePath) @@ -658,7 +700,7 @@ private sealed class CmdletRunspaceFixture : IDisposable { private readonly Runspace _runspace; - public CmdletRunspaceFixture(string activeSource = "https://api.nuget.org/v3/index.json") + public CmdletRunspaceFixture(string activeSource = "https://contoso.com/v3/index.json") { var host = new TestPSHost(activeSource); var initialSessionState = InitialSessionState.CreateDefault(); diff --git a/test/NuGet.Tests.Apex/NuGet.Tests.Apex/NuGetEndToEndTests/GetPackageTestCase.cs b/test/NuGet.Tests.Apex/NuGet.Tests.Apex/NuGetEndToEndTests/GetPackageTestCase.cs index 9eacefefcab..58bef601347 100644 --- a/test/NuGet.Tests.Apex/NuGet.Tests.Apex/NuGetEndToEndTests/GetPackageTestCase.cs +++ b/test/NuGet.Tests.Apex/NuGet.Tests.Apex/NuGetEndToEndTests/GetPackageTestCase.cs @@ -66,5 +66,64 @@ public async Task GetPackage_InstallListAndUpdateLifecycleAsync() prereleaseUpdatesText.Should().Contain(packageName, because: "prerelease update exists"); prereleaseUpdatesText.Should().Contain("2.0.0-beta", because: "-Prerelease switch enables prerelease updates"); } + + /// + /// Installs two packages at old versions and verifies Get-Package -Updates lists both as having updates. + /// + [TestMethod] + [Timeout(DefaultTimeout)] + public async Task GetPackage_WithUpdates_ListsMultipleUpdatesAsync() + { + using var testContext = new ApexTestContext(VisualStudio, ProjectTemplate.ConsoleApplication, Logger); + + var packageA = "UpdateTestPackageA"; + var packageB = "UpdateTestPackageB"; + + // Create old and new versions for both packages + await CommonUtility.CreatePackageInSourceAsync(testContext.PackageSource, packageA, "1.0.0"); + await CommonUtility.CreatePackageInSourceAsync(testContext.PackageSource, packageA, "2.0.0"); + await CommonUtility.CreatePackageInSourceAsync(testContext.PackageSource, packageB, "1.0.0"); + await CommonUtility.CreatePackageInSourceAsync(testContext.PackageSource, packageB, "2.0.0"); + + var nugetConsole = GetConsole(testContext.Project); + + // Install old versions + nugetConsole.InstallPackageFromPMC(packageA, "1.0.0"); + nugetConsole.InstallPackageFromPMC(packageB, "1.0.0"); + + // Act — Get-Package -Updates + string escapedSource = testContext.PackageSource.Replace("'", "''"); + nugetConsole.Clear(); + nugetConsole.Execute($"Get-Package -Updates -Source '{escapedSource}'"); + + // Assert — both packages should appear in updates output + string updatesText = nugetConsole.GetText(); + updatesText.Should().Contain(packageA, because: $"'{packageA}' has a newer version available. PMC output: {updatesText}"); + updatesText.Should().Contain(packageB, because: $"'{packageB}' has a newer version available. PMC output: {updatesText}"); + } + + /// + /// Verifies that Get-Package -ListAvailable returns a package whose IsUpdate property + /// is not set (falsy). The original E2E test (Test-GetPackagesWithNoUpdatesReturnPackagesWithIsUpdateNotSet) + /// called Assert-False on $package.IsUpdate — which succeeds because PowerShellRemotePackage + /// does not have an IsUpdate property, so PowerShell returns $null (falsy). + /// + [TestMethod] + [Timeout(DefaultTimeout)] + public async Task GetPackage_ListAvailable_IsUpdateNotSetAsync() + { + using var testContext = new ApexTestContext(VisualStudio, ProjectTemplate.ConsoleApplication, Logger); + + var nugetConsole = GetConsole(testContext.Project); + + // Act — get a package from -ListAvailable (default source) and evaluate IsUpdate. + // PowerShellRemotePackage has no IsUpdate property, so $pkg.IsUpdate is $null (falsy). + nugetConsole.Clear(); + nugetConsole.Execute("$pkg = Get-Package -ListAvailable -First 1; Write-Host \"IsUpdate=$([bool]$pkg.IsUpdate)\""); + + // Assert — IsUpdate should be False (property doesn't exist on remote packages) + string pmcText = nugetConsole.GetText(); + pmcText.Should().Contain("IsUpdate=False", because: $"package from -ListAvailable should not have IsUpdate set. PMC output: {pmcText}"); + } } }