|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Text.Json;
using System.Text.Json.Nodes;
using Aspire.Cli.DotNet;
using Aspire.Cli.Interaction;
using Aspire.Cli.Packaging;
using Aspire.Cli.Projects;
using Aspire.Cli.Tests.TestServices;
using Aspire.Cli.Tests.Utils;
using Aspire.Shared;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Aspire.Cli.Tests.Projects;
public class ProjectUpdaterTests(ITestOutputHelper outputHelper)
{
[Fact]
public async Task UpdateProjectFileAsync_DoesAttemptToUpdateIfNoUpdatesRequired()
{
using var workspace = TemporaryWorkspace.Create(outputHelper);
var srcFolder = workspace.CreateDirectory("src");
var serviceDefaultsFolder = workspace.CreateDirectory("UpdateTester.ServiceDefaults");
var serviceDefaultsProjectFile = new FileInfo(Path.Combine(serviceDefaultsFolder.FullName, "UpdateTester.ServiceDefaults.csproj"));
var webAppFolder = workspace.CreateDirectory("UpdateTester.WebApp");
var webAppProjectFile = new FileInfo(Path.Combine(webAppFolder.FullName, "UpdateTester.WebApp.csproj"));
var appHostFolder = workspace.CreateDirectory("UpdateTester.AppHost");
var appHostProjectFile = new FileInfo(Path.Combine(appHostFolder.FullName, "UpdateTester.AppHost.csproj"));
await File.WriteAllTextAsync(
appHostProjectFile.FullName,
$$"""
<Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="Aspire.AppHost.Sdk" Version="9.5.1-preview.1" />
</Project>
""");
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, config =>
{
config.DotNetCliRunnerFactory = (sp) =>
{
return new TestDotNetCliRunner()
{
SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _) =>
{
var packages = new List<NuGetPackageCli>();
packages.Add(query switch
{
"Aspire.AppHost.Sdk" => new NuGetPackageCli { Id = "Aspire.AppHost.Sdk", Version = "9.4.1", Source = "nuget.org" },
"Aspire.Hosting.AppHost" => new NuGetPackageCli { Id = "Aspire.Hosting.AppHost", Version = "9.4.1", Source = "nuget.org" },
"Aspire.Hosting.Redis" => new NuGetPackageCli { Id = "Aspire.Hosting.Redis", Version = "9.4.1", Source = "nuget.org" },
"Aspire.StackExchange.Redis.OutputCaching" => new NuGetPackageCli { Id = "Aspire.StackExchange.Redis.OutputCaching", Version = "9.4.1", Source = "nuget.org" },
"Microsoft.Extensions.ServiceDiscovery" => new NuGetPackageCli { Id = "Microsoft.Extensions.ServiceDiscovery", Version = "9.4.1", Source = "nuget.org" },
_ => throw new InvalidOperationException("Unexpected package query."),
});
return (0, packages.ToArray());
},
GetProjectItemsAndPropertiesAsyncCallback = (projectFile, _, _, _, _) =>
{
var itemsAndProperties = new JsonObject();
if (projectFile.FullName == appHostProjectFile.FullName)
{
itemsAndProperties.WithSdkVersion("9.4.1");
itemsAndProperties.WithPackageReference("Aspire.Hosting.AppHost", "9.4.1");
itemsAndProperties.WithPackageReference("Aspire.Hosting.Redis", "9.4.1");
itemsAndProperties.WithProjectReference(webAppProjectFile.FullName);
}
else if (projectFile.FullName == webAppProjectFile.FullName)
{
itemsAndProperties.WithPackageReference("Aspire.StackExchange.Redis.OutputCaching", "9.4.1");
itemsAndProperties.WithProjectReference(serviceDefaultsProjectFile.FullName);
}
else if (projectFile.FullName == serviceDefaultsProjectFile.FullName)
{
itemsAndProperties.WithPackageReference("Microsoft.ServiceDiscovery.Extensions", "9.4.1");
}
else
{
throw new InvalidOperationException("Unexpected project file.");
}
var json = itemsAndProperties.ToJsonString();
var document = JsonDocument.Parse(json);
return (0, document);
}
};
};
config.InteractionServiceFactory = (sp) =>
{
var interactionService = new TestConsoleInteractionService();
interactionService.ConfirmCallback = (promptText, defaultValue) =>
{
throw new InvalidOperationException("Should not prompt when no work required.");
};
return interactionService;
};
});
var provider = services.BuildServiceProvider();
// Services we need for project updater.
var logger = provider.GetRequiredService<ILogger<ProjectUpdater>>();
var runner = provider.GetRequiredService<IDotNetCliRunner>();
var interactionService = provider.GetRequiredService<IInteractionService>();
var cache = provider.GetRequiredService<IMemoryCache>();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var packagingService = provider.GetRequiredService<IPackagingService>();
var channels = await packagingService.GetChannelsAsync();
var selectedChannel = channels.Single(c => c.Name == "default");
// If this throws then it means that the updater prompted
// for confirmation to do an update when no update was required!
var projectUpdater = new ProjectUpdater(logger, runner, interactionService, cache, executionContext);
var updateResult = await projectUpdater.UpdateProjectAsync(appHostProjectFile, selectedChannel).WaitAsync(CliTestConstants.DefaultTimeout);
Assert.False(updateResult.UpdatedApplied);
}
[Fact]
public async Task UpdateProjectFileAsync_CanUpdateFromStableToDaily()
{
using var workspace = TemporaryWorkspace.Create(outputHelper);
var srcFolder = workspace.CreateDirectory("src");
var serviceDefaultsFolder = workspace.CreateDirectory("UpdateTester.ServiceDefaults");
var serviceDefaultsProjectFile = new FileInfo(Path.Combine(serviceDefaultsFolder.FullName, "UpdateTester.ServiceDefaults.csproj"));
var webAppFolder = workspace.CreateDirectory("UpdateTester.WebApp");
var webAppProjectFile = new FileInfo(Path.Combine(webAppFolder.FullName, "UpdateTester.WebApp.csproj"));
var appHostFolder = workspace.CreateDirectory("UpdateTester.AppHost");
var appHostProjectFile = new FileInfo(Path.Combine(appHostFolder.FullName, "UpdateTester.AppHost.csproj"));
await File.WriteAllTextAsync(
appHostProjectFile.FullName,
$$"""
<Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="Aspire.AppHost.Sdk" Version="9.4.1" />
</Project>
""");
var packagesAddsExecuted = new List<(FileInfo ProjectFile, string PackageId, string PackageVersion, string? PackageSource)>();
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, config =>
{
config.DotNetCliRunnerFactory = (sp) =>
{
return new TestDotNetCliRunner()
{
SearchPackagesAsyncCallback = (_, query, _, _, _, _, _, _) =>
{
var packages = new List<NuGetPackageCli>();
packages.Add(query switch
{
"Aspire.AppHost.Sdk" => new NuGetPackageCli { Id = "Aspire.AppHost.Sdk", Version = "9.5.0-preview.1", Source = "daily" },
"Aspire.Hosting.AppHost" => new NuGetPackageCli { Id = "Aspire.Hosting.AppHost", Version = "9.5.0-preview.1", Source = "daily" },
"Aspire.Hosting.Redis" => new NuGetPackageCli { Id = "Aspire.Hosting.Redis", Version = "9.5.0-preview.1", Source = "daily" },
"Aspire.StackExchange.Redis.OutputCaching" => new NuGetPackageCli { Id = "Aspire.StackExchange.Redis.OutputCaching", Version = "9.5.0-preview.1", Source = "daily" },
"Microsoft.Extensions.ServiceDiscovery" => new NuGetPackageCli { Id = "Microsoft.Extensions.ServiceDiscovery", Version = "9.5.0-preview.1", Source = "daily" },
_ => throw new InvalidOperationException("Unexpected package query."),
});
return (0, packages.ToArray());
},
GetProjectItemsAndPropertiesAsyncCallback = (projectFile, _, _, _, _) =>
{
var itemsAndProperties = new JsonObject();
if (projectFile.FullName == appHostProjectFile.FullName)
{
itemsAndProperties.WithSdkVersion("9.4.1");
itemsAndProperties.WithPackageReference("Aspire.Hosting.AppHost", "9.4.1");
itemsAndProperties.WithPackageReference("Aspire.Hosting.Redis", "9.4.1");
itemsAndProperties.WithProjectReference(webAppProjectFile.FullName);
}
else if (projectFile.FullName == webAppProjectFile.FullName)
{
itemsAndProperties.WithPackageReference("Aspire.StackExchange.Redis.OutputCaching", "9.4.1");
itemsAndProperties.WithProjectReference(serviceDefaultsProjectFile.FullName);
}
else if (projectFile.FullName == serviceDefaultsProjectFile.FullName)
{
itemsAndProperties.WithPackageReference("Microsoft.Extensions.ServiceDiscovery", "9.4.1");
}
else
{
throw new InvalidOperationException("Unexpected project file.");
}
var json = itemsAndProperties.ToJsonString();
var document = JsonDocument.Parse(json);
return (0, document);
},
// FileInfo, string, string, string?, DotNetCliRunnerInvocationOptions, CancellationToken, int
AddPackageAsyncCallback = (projectFile, packageId, packageVersion, source, _, _) =>
{
packagesAddsExecuted.Add((projectFile, packageId, packageVersion, source!));
return 0;
}
};
};
config.InteractionServiceFactory = (s) =>
{
var interactionService = new TestConsoleInteractionService();
return interactionService;
};
});
var provider = services.BuildServiceProvider();
// Services we need for project updater.
var logger = provider.GetRequiredService<ILogger<ProjectUpdater>>();
var runner = provider.GetRequiredService<IDotNetCliRunner>();
var interactionService = provider.GetRequiredService<IInteractionService>();
var cache = provider.GetRequiredService<IMemoryCache>();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var packagingService = provider.GetRequiredService<IPackagingService>();
var channels = await packagingService.GetChannelsAsync();
var selectedChannel = channels.Single(c => c.Name == "daily");
// If this throws then it means that the updater prompted
// for confirmation to do an update when no update was required!
var projectUpdater = new ProjectUpdater(logger, runner, interactionService, cache, executionContext);
var updateResult = await projectUpdater.UpdateProjectAsync(appHostProjectFile, selectedChannel).WaitAsync(CliTestConstants.DefaultTimeout);
Assert.True(updateResult.UpdatedApplied);
Assert.Collection(
packagesAddsExecuted,
item =>
{
Assert.Equal("Aspire.Hosting.AppHost", item.PackageId);
Assert.Equal("9.5.0-preview.1", item.PackageVersion);
Assert.Null(item.PackageSource); // Should be null because of --no-restore behavior.
Assert.Equal(appHostProjectFile.FullName, item.ProjectFile.FullName);
},
item =>
{
Assert.Equal("Aspire.Hosting.Redis", item.PackageId);
Assert.Equal("9.5.0-preview.1", item.PackageVersion);
Assert.Null(item.PackageSource); // Should be null because of --no-restore behavior.
Assert.Equal(appHostProjectFile.FullName, item.ProjectFile.FullName);
},
item =>
{
Assert.Equal("Aspire.StackExchange.Redis.OutputCaching", item.PackageId);
Assert.Equal("9.5.0-preview.1", item.PackageVersion);
Assert.Null(item.PackageSource); // Should be null because of --no-restore behavior.
Assert.Equal(webAppProjectFile.FullName, item.ProjectFile.FullName);
},
item =>
{
Assert.Equal("Microsoft.Extensions.ServiceDiscovery", item.PackageId);
Assert.Equal("9.5.0-preview.1", item.PackageVersion);
Assert.Null(item.PackageSource); // Should be null because of --no-restore behavior.
Assert.Equal(serviceDefaultsProjectFile.FullName, item.ProjectFile.FullName);
}
);
}
[Fact]
public async Task UpdateProjectFileAsync_CanUpdateFromDailyToStableWhereOnePackageIsUnstableOnly()
{
using var workspace = TemporaryWorkspace.Create(outputHelper);
var srcFolder = workspace.CreateDirectory("src");
var serviceDefaultsFolder = workspace.CreateDirectory("UpdateTester.ServiceDefaults");
var serviceDefaultsProjectFile = new FileInfo(Path.Combine(serviceDefaultsFolder.FullName, "UpdateTester.ServiceDefaults.csproj"));
var webAppFolder = workspace.CreateDirectory("UpdateTester.WebApp");
var webAppProjectFile = new FileInfo(Path.Combine(webAppFolder.FullName, "UpdateTester.WebApp.csproj"));
var appHostFolder = workspace.CreateDirectory("UpdateTester.AppHost");
var appHostProjectFile = new FileInfo(Path.Combine(appHostFolder.FullName, "UpdateTester.AppHost.csproj"));
await File.WriteAllTextAsync(
appHostProjectFile.FullName,
$$"""
<Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="Aspire.AppHost.Sdk" Version="9.4.1" />
</Project>
""");
var packagesAddsExecuted = new List<(FileInfo ProjectFile, string PackageId, string PackageVersion, string? PackageSource)>();
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, config =>
{
config.DotNetCliRunnerFactory = (sp) =>
{
return new TestDotNetCliRunner()
{
SearchPackagesAsyncCallback = (_, query, prerelease, _, _, _, _, _) =>
{
var packages = new List<NuGetPackageCli>();
var matchedPackage = (query, prerelease) switch
{
{ query: "Aspire.AppHost.Sdk", prerelease: false } => new NuGetPackageCli { Id = "Aspire.AppHost.Sdk", Version = "9.4.1", Source = "nuget" },
{ query: "Aspire.Hosting.AppHost", prerelease: false } => new NuGetPackageCli { Id = "Aspire.Hosting.AppHost", Version = "9.4.1", Source = "nuget" },
{ query: "Aspire.Hosting.Redis", prerelease: false } => new NuGetPackageCli { Id = "Aspire.Hosting.Redis", Version = "9.4.1", Source = "nuget" },
{ query: "Aspire.Hosting.Docker", prerelease: true } => new NuGetPackageCli { Id = "Aspire.Hosting.Docker", Version = "9.4.1-preview.1", Source = "nuget" },
{ query: "Aspire.Hosting.Docker", prerelease: false } => null, // Not in feed.
{ query: "Aspire.StackExchange.Redis.OutputCaching", prerelease: false } => new NuGetPackageCli { Id = "Aspire.StackExchange.Redis.OutputCaching", Version = "9.4.1", Source = "nuget" },
{ query: "Microsoft.Extensions.ServiceDiscovery", prerelease: false } => new NuGetPackageCli { Id = "Microsoft.Extensions.ServiceDiscovery", Version = "9.4.1", Source = "nuget" },
_ => throw new InvalidOperationException("Unexpected package query."),
};
if (matchedPackage != null)
{
packages.Add(matchedPackage);
}
return (0, packages.ToArray());
},
GetProjectItemsAndPropertiesAsyncCallback = (projectFile, _, _, _, _) =>
{
var itemsAndProperties = new JsonObject();
if (projectFile.FullName == appHostProjectFile.FullName)
{
itemsAndProperties.WithSdkVersion("9.5.1-preview.1");
itemsAndProperties.WithPackageReference("Aspire.Hosting.AppHost", "9.5.1-preview.1");
itemsAndProperties.WithPackageReference("Aspire.Hosting.Redis", "9.5.1-preview.1");
itemsAndProperties.WithPackageReference("Aspire.Hosting.Docker", "9.5.1-preview.1");
itemsAndProperties.WithProjectReference(webAppProjectFile.FullName);
}
else if (projectFile.FullName == webAppProjectFile.FullName)
{
itemsAndProperties.WithPackageReference("Aspire.StackExchange.Redis.OutputCaching", "9.5.1-preview.1");
itemsAndProperties.WithProjectReference(serviceDefaultsProjectFile.FullName);
}
else if (projectFile.FullName == serviceDefaultsProjectFile.FullName)
{
itemsAndProperties.WithPackageReference("Microsoft.Extensions.ServiceDiscovery", "9.5.1-preview.1");
}
else
{
throw new InvalidOperationException("Unexpected project file.");
}
var json = itemsAndProperties.ToJsonString();
var document = JsonDocument.Parse(json);
return (0, document);
},
// FileInfo, string, string, string?, DotNetCliRunnerInvocationOptions, CancellationToken, int
AddPackageAsyncCallback = (projectFile, packageId, packageVersion, source, _, _) =>
{
packagesAddsExecuted.Add((projectFile, packageId, packageVersion, source!));
return 0;
}
};
};
config.InteractionServiceFactory = (s) =>
{
var interactionService = new TestConsoleInteractionService();
return interactionService;
};
});
var provider = services.BuildServiceProvider();
// Services we need for project updater.
var logger = provider.GetRequiredService<ILogger<ProjectUpdater>>();
var runner = provider.GetRequiredService<IDotNetCliRunner>();
var interactionService = provider.GetRequiredService<IInteractionService>();
var cache = provider.GetRequiredService<IMemoryCache>();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var packagingService = provider.GetRequiredService<IPackagingService>();
var channels = await packagingService.GetChannelsAsync();
var selectedChannel = channels.Single(c => c.Name == "stable");
// If this throws then it means that the updater prompted
// for confirmation to do an update when no update was required!
var projectUpdater = new ProjectUpdater(logger, runner, interactionService, cache, executionContext);
var updateResult = await projectUpdater.UpdateProjectAsync(appHostProjectFile, selectedChannel).WaitAsync(CliTestConstants.DefaultTimeout);
Assert.True(updateResult.UpdatedApplied);
Assert.Collection(
packagesAddsExecuted,
item =>
{
Assert.Equal("Aspire.Hosting.AppHost", item.PackageId);
Assert.Equal("9.4.1", item.PackageVersion);
Assert.Null(item.PackageSource); // Should be null because of --no-restore behavior.
Assert.Equal(appHostProjectFile.FullName, item.ProjectFile.FullName);
},
item =>
{
Assert.Equal("Aspire.Hosting.Redis", item.PackageId);
Assert.Equal("9.4.1", item.PackageVersion);
Assert.Null(item.PackageSource); // Should be null because of --no-restore behavior.
Assert.Equal(appHostProjectFile.FullName, item.ProjectFile.FullName);
},
item =>
{
Assert.Equal("Aspire.Hosting.Docker", item.PackageId);
Assert.Equal("9.4.1-preview.1", item.PackageVersion);
Assert.Null(item.PackageSource); // Should be null because of --no-restore behavior.
Assert.Equal(appHostProjectFile.FullName, item.ProjectFile.FullName);
},
item =>
{
Assert.Equal("Aspire.StackExchange.Redis.OutputCaching", item.PackageId);
Assert.Equal("9.4.1", item.PackageVersion);
Assert.Null(item.PackageSource); // Should be null because of --no-restore behavior.
Assert.Equal(webAppProjectFile.FullName, item.ProjectFile.FullName);
},
item =>
{
Assert.Equal("Microsoft.Extensions.ServiceDiscovery", item.PackageId);
Assert.Equal("9.4.1", item.PackageVersion);
Assert.Null(item.PackageSource); // Should be null because of --no-restore behavior.
Assert.Equal(serviceDefaultsProjectFile.FullName, item.ProjectFile.FullName);
}
);
}
private static Aspire.Cli.CliExecutionContext CreateExecutionContext(DirectoryInfo workingDirectory)
{
// NOTE: This would normally be in the users home directory, but for tests we create
// it in the temporary workspace directory.
var settingsDirectory = workingDirectory.CreateSubdirectory(".aspire");
var hivesDirectory = settingsDirectory.CreateSubdirectory("hives");
return new CliExecutionContext(workingDirectory, hivesDirectory);
}
}
internal static class MSBuildJsonDocumentExtensions
{
public static JsonObject WithSdkVersion(this JsonObject root, string sdkVersion)
{
JsonObject properties = new JsonObject();
if (!root.TryAdd("Properties", properties))
{
properties = root["Properties"]!.AsObject();
}
properties.Add("AspireHostingSDKVersion", JsonValue.Create<string>(sdkVersion));
return root;
}
public static JsonObject WithPackageReference(this JsonObject root, string packageId, string packageVersion)
{
JsonObject items = new JsonObject();
items.Add("ProjectReference", new JsonArray());
items.Add("PackageReference", new JsonArray());
if (!root.TryAdd("Items", items))
{
items = root["Items"]!.AsObject();
}
JsonArray packageReferences = new JsonArray();
if (!items.TryAdd("PackageReference", packageReferences))
{
packageReferences = items["PackageReference"]!.AsArray();
}
JsonObject newPackageReference = new JsonObject
{
{ "Identity", JsonValue.Create<string>(packageId) },
{ "Version", JsonValue.Create<string>(packageVersion) }
};
packageReferences.Add(newPackageReference);
return root;
}
public static JsonObject WithProjectReference(this JsonObject root, string fullPath)
{
JsonObject items = new JsonObject();
if (!root.TryAdd("Items", items))
{
items = root["Items"]!.AsObject();
}
JsonArray projectReferences = new JsonArray();
if (!items.TryAdd("ProjectReference", projectReferences))
{
projectReferences = items["ProjectReference"]!.AsArray();
}
JsonObject newProjectReference = new JsonObject
{
{ "FullPath", JsonValue.Create<string>(fullPath) }
};
projectReferences.Add(newProjectReference);
return root;
}
} |