// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Aspire.Cli.Interaction;
using Aspire.Cli.NuGet;
using Aspire.Cli.Tests.TestServices;
using Aspire.Cli.Utils;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Semver;
using NuGetPackage = Aspire.Shared.NuGetPackageCli;
using Microsoft.AspNetCore.InternalTesting;
namespace Aspire.Cli.Tests.Utils;
public class CliUpdateNotificationServiceTests(ITestOutputHelper outputHelper)
{
[Fact]
public async Task PrereleaseWillRecommendUpgradeToPrereleaseOnSameVersionFamily()
{
var currentVersion = VersionHelper.GetDefaultTemplateVersion();
TaskCompletionSource<string> suggestedVersionTcs = new();
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, configure =>
{
configure.NuGetPackageCacheFactory = (sp) =>
{
var cache = new FakeNuGetPackageCache { GetCliPackagesAsyncCallback = (_, _, _, _) => Task.FromResult<IEnumerable<NuGetPackage>>([
// Should be ignored because it's lower than current prerelease version.
new NuGetPackage { Id = "Aspire.Cli", Version = "9.3.1", Source = "nuget.org" },
// Should be selected because it is higher than 9.4.0-dev (dev and preview sort using alphabetical sort).
new NuGetPackage { Id = "Aspire.Cli", Version = "9.4.0-preview", Source = "nuget.org" },
// Should be ignored because it is lower than 9.4.0-dev (dev and preview sort using alpha).
new NuGetPackage { Id = "Aspire.Cli", Version = "9.4.0-beta", Source = "nuget.org" }
]) }; return cache;
};
configure.InteractionServiceFactory = (sp) =>
{
var interactionService = new TestInteractionService();
interactionService.DisplayVersionUpdateNotificationCallback = (newerVersion) =>
{
suggestedVersionTcs.SetResult(newerVersion);
};
return interactionService;
};
configure.CliUpdateNotifierFactory = sp => CreateCliUpdateNotifier(sp, "9.4.0-dev");
});
using var provider = services.BuildServiceProvider();
var notifier = provider.GetRequiredService<ICliUpdateNotifier>();
await notifier.CheckForCliUpdatesAsync(workspace.WorkspaceRoot, CancellationToken.None).DefaultTimeout();
notifier.NotifyIfUpdateAvailable();
var suggestedVersion = await suggestedVersionTcs.Task.DefaultTimeout();
Assert.Equal("9.4.0-preview", suggestedVersion);
}
[Fact]
public async Task PrereleaseWillRecommendUpgradeToStableInCurrentVersionFamily()
{
var currentVersion = VersionHelper.GetDefaultTemplateVersion();
TaskCompletionSource<string> suggestedVersionTcs = new();
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, configure =>
{
configure.NuGetPackageCacheFactory = (sp) =>
{
var cache = new FakeNuGetPackageCache { GetCliPackagesAsyncCallback = (_, _, _, _) => Task.FromResult<IEnumerable<NuGetPackage>>([
// Should be selected because stable sorts higher than preview.
new NuGetPackage { Id = "Aspire.Cli", Version = "9.4.0", Source = "nuget.org" },
// Should be ignored because its prerelease but in a higher version family.
new NuGetPackage { Id = "Aspire.Cli", Version = "9.5.0-preview", Source = "nuget.org" },
]) }; return cache;
};
configure.InteractionServiceFactory = (sp) =>
{
var interactionService = new TestInteractionService();
interactionService.DisplayVersionUpdateNotificationCallback = (newerVersion) =>
{
suggestedVersionTcs.SetResult(newerVersion);
};
return interactionService;
};
configure.CliUpdateNotifierFactory = sp => CreateCliUpdateNotifier(sp, "9.4.0-dev");
});
using var provider = services.BuildServiceProvider();
var notifier = provider.GetRequiredService<ICliUpdateNotifier>();
await notifier.CheckForCliUpdatesAsync(workspace.WorkspaceRoot, CancellationToken.None).DefaultTimeout();
notifier.NotifyIfUpdateAvailable();
var suggestedVersion = await suggestedVersionTcs.Task.DefaultTimeout();
Assert.Equal("9.4.0", suggestedVersion);
}
[Fact]
public async Task StableWillOnlyRecommendGoingToNewerStable()
{
var currentVersion = VersionHelper.GetDefaultTemplateVersion();
TaskCompletionSource<string> suggestedVersionTcs = new();
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, configure =>
{
configure.NuGetPackageCacheFactory = (sp) =>
{
var cache = new FakeNuGetPackageCache { GetCliPackagesAsyncCallback = (_, _, _, _) => Task.FromResult<IEnumerable<NuGetPackage>>([
// Should be ignored because its stable in a higher version family.
new NuGetPackage { Id = "Aspire.Cli", Version = "9.5.0", Source = "nuget.org" },
// Should be ignored because its prerelease but in a (even) higher version family.
new NuGetPackage { Id = "Aspire.Cli", Version = "9.6.0-preview", Source = "nuget.org" },
]) }; return cache;
};
configure.InteractionServiceFactory = (sp) =>
{
var interactionService = new TestInteractionService();
interactionService.DisplayVersionUpdateNotificationCallback = (newerVersion) =>
{
suggestedVersionTcs.SetResult(newerVersion);
};
return interactionService;
};
configure.CliUpdateNotifierFactory = sp => CreateCliUpdateNotifier(sp, "9.4.0");
});
using var provider = services.BuildServiceProvider();
var notifier = provider.GetRequiredService<ICliUpdateNotifier>();
await notifier.CheckForCliUpdatesAsync(workspace.WorkspaceRoot, CancellationToken.None).DefaultTimeout();
notifier.NotifyIfUpdateAvailable();
var suggestedVersion = await suggestedVersionTcs.Task.DefaultTimeout();
Assert.Equal("9.5.0", suggestedVersion);
}
[Fact]
public void NotifyIfUpdateAvailable_WithoutCachedPackages_DoesNotNotify()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, configure =>
{
configure.InteractionServiceFactory = sp =>
{
var interactionService = new TestInteractionService();
interactionService.DisplayVersionUpdateNotificationCallback = _ =>
{
Assert.Fail("Should not notify before package metadata has been cached.");
};
return interactionService;
};
configure.CliUpdateNotifierFactory = sp => CreateCliUpdateNotifier(sp, "9.4.0");
});
using var provider = services.BuildServiceProvider();
var notifier = provider.GetRequiredService<ICliUpdateNotifier>();
notifier.NotifyIfUpdateAvailable();
}
[Fact]
public async Task NotifyIfUpdateAvailable_UsesDotnetToolCommandForNativeAotToolStorePath()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
TestInteractionService? interactionService = null;
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, configure =>
{
UseProcessPath(configure, "/home/test/.dotnet/tools/.store/aspire.cli/9.4.0/aspire.cli.linux-x64/9.4.0/tools/any/linux-x64/aspire");
configure.NuGetPackageCacheFactory = _ => new FakeNuGetPackageCache
{
GetCliPackagesAsyncCallback = (_, _, _, _) => Task.FromResult<IEnumerable<NuGetPackage>>([
new NuGetPackage { Id = "Aspire.Cli", Version = "9.5.0", Source = "nuget.org" }
])
};
configure.InteractionServiceFactory = _ =>
{
interactionService = new TestInteractionService();
return interactionService;
};
configure.CliUpdateNotifierFactory = sp => CreateCliUpdateNotifier(sp, "9.4.0");
});
using var provider = services.BuildServiceProvider();
var notifier = provider.GetRequiredService<ICliUpdateNotifier>();
await notifier.CheckForCliUpdatesAsync(workspace.WorkspaceRoot, CancellationToken.None).DefaultTimeout();
notifier.NotifyIfUpdateAvailable();
Assert.NotNull(interactionService);
Assert.Equal("dotnet tool update -g Aspire.Cli", interactionService.LastVersionUpdateCommand);
}
[Fact]
public async Task NotifyIfUpdateAvailable_UsesToolPathCommandForCustomToolPath()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var toolPath = Path.Combine(workspace.CreateDirectory("install").FullName, "custom tool path");
var processPath = CreateCustomToolPathInstall(toolPath);
TestInteractionService? interactionService = null;
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, configure =>
{
UseProcessPath(configure, processPath);
configure.NuGetPackageCacheFactory = _ => new FakeNuGetPackageCache
{
GetCliPackagesAsyncCallback = (_, _, _, _) => Task.FromResult<IEnumerable<NuGetPackage>>([
new NuGetPackage { Id = "Aspire.Cli", Version = "9.5.0", Source = "nuget.org" }
])
};
configure.InteractionServiceFactory = _ =>
{
interactionService = new TestInteractionService();
return interactionService;
};
configure.CliUpdateNotifierFactory = sp => CreateCliUpdateNotifier(sp, "9.4.0");
});
using var provider = services.BuildServiceProvider();
var notifier = provider.GetRequiredService<ICliUpdateNotifier>();
await notifier.CheckForCliUpdatesAsync(workspace.WorkspaceRoot, CancellationToken.None).DefaultTimeout();
notifier.NotifyIfUpdateAvailable();
Assert.NotNull(interactionService);
Assert.Equal($"dotnet tool update --tool-path \"{toolPath}\" Aspire.Cli", interactionService.LastVersionUpdateCommand);
}
[Fact]
public async Task NotifyIfUpdateAvailable_UsesAspireUpdateCommandForStandaloneArchivePath()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
TestInteractionService? interactionService = null;
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, configure =>
{
UseProcessPath(configure, "/home/test/.aspire/bin/aspire");
configure.NuGetPackageCacheFactory = _ => new FakeNuGetPackageCache
{
GetCliPackagesAsyncCallback = (_, _, _, _) => Task.FromResult<IEnumerable<NuGetPackage>>([
new NuGetPackage { Id = "Aspire.Cli", Version = "9.5.0", Source = "nuget.org" }
])
};
configure.InteractionServiceFactory = _ =>
{
interactionService = new TestInteractionService();
return interactionService;
};
configure.CliUpdateNotifierFactory = sp => CreateCliUpdateNotifier(sp, "9.4.0");
});
using var provider = services.BuildServiceProvider();
var notifier = provider.GetRequiredService<ICliUpdateNotifier>();
await notifier.CheckForCliUpdatesAsync(workspace.WorkspaceRoot, CancellationToken.None).DefaultTimeout();
notifier.NotifyIfUpdateAvailable();
Assert.NotNull(interactionService);
Assert.Equal("aspire update", interactionService.LastVersionUpdateCommand);
}
[Fact]
public async Task NotifyIfUpdateAvailable_UsesNpmCommandForNpmInstall()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
using var npmScope = NpmInstallDetection.UseEnvironmentForTesting(CreateNpmInstallEnvironment());
TestInteractionService? interactionService = null;
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, configure =>
{
UseProcessPath(configure, "/home/test/.aspire/bin/aspire");
configure.NuGetPackageCacheFactory = _ => new FakeNuGetPackageCache
{
GetCliPackagesAsyncCallback = (_, _, _, _) => Task.FromResult<IEnumerable<NuGetPackage>>([
new NuGetPackage { Id = "Aspire.Cli", Version = "9.5.0", Source = "nuget.org" }
])
};
configure.InteractionServiceFactory = _ =>
{
interactionService = new TestInteractionService();
return interactionService;
};
configure.CliUpdateNotifierFactory = sp => CreateCliUpdateNotifier(sp, "9.4.0");
});
using var provider = services.BuildServiceProvider();
var notifier = provider.GetRequiredService<ICliUpdateNotifier>();
await notifier.CheckForCliUpdatesAsync(workspace.WorkspaceRoot, CancellationToken.None).DefaultTimeout();
notifier.NotifyIfUpdateAvailable();
Assert.NotNull(interactionService);
Assert.Equal("npm install -g @microsoft/aspire-cli@latest", interactionService.LastVersionUpdateCommand);
}
[Fact]
public async Task StableWillNotRecommendUpdatingToPreview()
{
var currentVersion = VersionHelper.GetDefaultTemplateVersion();
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, configure =>
{
configure.NuGetPackageCacheFactory = (sp) =>
{
var cache = new FakeNuGetPackageCache { GetCliPackagesAsyncCallback = (_, _, _, _) => Task.FromResult<IEnumerable<NuGetPackage>>([
new NuGetPackage { Id = "Aspire.Cli", Version = "9.4.0-preview", Source = "nuget.org" },
new NuGetPackage { Id = "Aspire.Cli", Version = "9.5.0-preview", Source = "nuget.org" },
]) }; return cache;
};
configure.InteractionServiceFactory = (sp) =>
{
var interactionService = new TestInteractionService();
interactionService.DisplayVersionUpdateNotificationCallback = (newerVersion) =>
{
Assert.Fail("Should not suggest a preview version when current version is stable.");
};
return interactionService;
};
configure.CliUpdateNotifierFactory = sp => CreateCliUpdateNotifier(sp, "9.4.0");
});
using var provider = services.BuildServiceProvider();
var notifier = provider.GetRequiredService<ICliUpdateNotifier>();
await notifier.CheckForCliUpdatesAsync(workspace.WorkspaceRoot, CancellationToken.None).DefaultTimeout();
notifier.NotifyIfUpdateAvailable();
}
[Fact]
public async Task NotifyIfUpdateAvailableAsync_WithNewerStableVersion_DoesNotThrow()
{
// Arrange
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
// Replace the NuGetPackageCache with our test implementation
var nugetCache = new FakeNuGetPackageCache
{
GetCliPackagesAsyncCallback = (_, _, _, _) => Task.FromResult<IEnumerable<NuGetPackage>>([
new NuGetPackage { Id = "Aspire.Cli", Version = "9.0.0", Source = "nuget.org" }
])
};
services.AddSingleton<INuGetPackageCache>(nugetCache);
services.AddSingleton<ICliUpdateNotifier, CliUpdateNotifier>();
using var provider = services.BuildServiceProvider();
var service = provider.GetRequiredService<ICliUpdateNotifier>();
// Act & Assert (should not throw)
await service.CheckForCliUpdatesAsync(workspace.WorkspaceRoot, CancellationToken.None).DefaultTimeout();
service.NotifyIfUpdateAvailable();
}
[Fact]
public async Task NotifyIfUpdateAvailableAsync_WithEmptyPackages_DoesNotThrow()
{
// Arrange
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
// Replace the NuGetPackageCache with our test implementation
services.AddSingleton<INuGetPackageCache>(new FakeNuGetPackageCache());
services.AddSingleton<ICliUpdateNotifier, CliUpdateNotifier>();
using var provider = services.BuildServiceProvider();
var service = provider.GetRequiredService<ICliUpdateNotifier>();
// Act & Assert (should not throw)
await service.CheckForCliUpdatesAsync(workspace.WorkspaceRoot, CancellationToken.None).DefaultTimeout();
service.NotifyIfUpdateAvailable();
}
private static string CreateCustomToolPathInstall(string toolPath)
{
var processPath = Path.Combine(toolPath, GetAspireExecutableName());
var storeExecutablePath = Path.Combine(
toolPath,
".store",
"aspire.cli",
"9.4.0",
"aspire.cli.linux-x64",
"9.4.0",
"tools",
"net10.0",
"linux-x64",
GetAspireExecutableName());
Directory.CreateDirectory(toolPath);
Directory.CreateDirectory(Path.GetDirectoryName(storeExecutablePath)!);
File.WriteAllText(processPath, string.Empty);
File.WriteAllText(storeExecutablePath, string.Empty);
return processPath;
}
private static string GetAspireExecutableName()
{
return OperatingSystem.IsWindows() ? "aspire.exe" : "aspire";
}
private static IReadOnlyDictionary<string, string?> CreateNpmInstallEnvironment()
{
return new Dictionary<string, string?>
{
[NpmInstallDetection.PackageEnvironmentVariableName] = NpmInstallDetection.ExpectedPackageName,
[NpmInstallDetection.PackageVersionEnvironmentVariableName] = "9.4.0",
[NpmInstallDetection.PackageRidEnvironmentVariableName] = "linux-x64"
};
}
private static void UseProcessPath(CliServiceCollectionTestOptions options, string? processPath)
{
options.ProcessPathProviderFactory = _ => new TestProcessPathProvider(processPath);
}
private static CliUpdateNotifierWithPackageVersionOverride CreateCliUpdateNotifier(IServiceProvider serviceProvider, string currentVersion)
{
return new CliUpdateNotifierWithPackageVersionOverride(
currentVersion,
serviceProvider.GetRequiredService<ILogger<CliUpdateNotifier>>(),
serviceProvider.GetRequiredService<INuGetPackageCache>(),
serviceProvider.GetRequiredService<IInteractionService>(),
serviceProvider.GetRequiredService<IProcessPathProvider>(),
serviceProvider.GetRequiredService<CliExecutionContext>());
}
}
internal sealed class CliUpdateNotifierWithPackageVersionOverride(
string currentVersion,
ILogger<CliUpdateNotifier> logger,
INuGetPackageCache nuGetPackageCache,
IInteractionService interactionService,
IProcessPathProvider processPathProvider,
CliExecutionContext executionContext) : CliUpdateNotifier(logger, nuGetPackageCache, interactionService, processPathProvider, executionContext)
{
protected override SemVersion? GetCurrentVersion()
{
return SemVersion.Parse(currentVersion, SemVersionStyles.Strict);
}
}