| File: Projects\FallbackProjectParserTests.cs | Web Access |
| Project: src\tests\Aspire.Cli.Tests\Aspire.Cli.Tests.csproj (Aspire.Cli.Tests) |
// 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 Aspire.Cli.Projects; using Aspire.Cli.Tests.Utils; using Microsoft.Extensions.Logging.Abstractions; namespace Aspire.Cli.Tests.Projects; public class FallbackProjectParserTests(ITestOutputHelper output) { private static readonly JsonSerializerOptions s_indentedOptions = new() { WriteIndented = true }; private static string FormatJson(JsonDocument document) { return JsonSerializer.Serialize(document.RootElement, s_indentedOptions); } [Fact] public async Task ParseProject_ExtractsAspireAppHostSdk_OldFormat() { // Arrange using var workspace = TemporaryWorkspace.Create(output); var projectFile = Path.Combine(workspace.WorkspaceRoot.FullName, "Test.csproj"); var projectContent = """ <Project Sdk="Microsoft.NET.Sdk"> <Sdk Name="Aspire.AppHost.Sdk" Version="9.5.0-test" /> </Project> """; await File.WriteAllTextAsync(projectFile, projectContent); var parser = new FallbackProjectParser(NullLogger<FallbackProjectParser>.Instance); // Act var result = parser.ParseProject(new FileInfo(projectFile)); // Assert await Verify(FormatJson(result), extension: "json"); } [Fact] public async Task ParseProject_ExtractsAspireAppHostSdk_NewFormat() { // Arrange - tests the new <Project Sdk="Aspire.AppHost.Sdk/version"> format using var workspace = TemporaryWorkspace.Create(output); var projectFile = Path.Combine(workspace.WorkspaceRoot.FullName, "Test.csproj"); var projectContent = """ <Project Sdk="Aspire.AppHost.Sdk/13.0.1"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net10.0</TargetFramework> </PropertyGroup> </Project> """; await File.WriteAllTextAsync(projectFile, projectContent); var parser = new FallbackProjectParser(NullLogger<FallbackProjectParser>.Instance); // Act var result = parser.ParseProject(new FileInfo(projectFile)); // Assert await Verify(FormatJson(result), extension: "json"); } [Fact] public async Task ParseProject_ExtractsAspireAppHostSdk_NewFormat_WithMultipleSdks() { // Arrange - tests parsing when multiple SDKs are in the attribute using var workspace = TemporaryWorkspace.Create(output); var projectFile = Path.Combine(workspace.WorkspaceRoot.FullName, "Test.csproj"); var projectContent = """ <Project Sdk="Aspire.AppHost.Sdk/13.0.1;Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net10.0</TargetFramework> </PropertyGroup> </Project> """; await File.WriteAllTextAsync(projectFile, projectContent); var parser = new FallbackProjectParser(NullLogger<FallbackProjectParser>.Instance); // Act var result = parser.ParseProject(new FileInfo(projectFile)); // Assert - should extract only the version, not the other SDK await Verify(FormatJson(result), extension: "json"); } [Fact] public async Task ParseProject_DoesNotMatchSimilarSdkName() { // Arrange - tests that Aspire.AppHost.SdkFoo doesn't match using var workspace = TemporaryWorkspace.Create(output); var projectFile = Path.Combine(workspace.WorkspaceRoot.FullName, "Test.csproj"); var projectContent = """ <Project Sdk="Aspire.AppHost.SdkFoo/1.0.0"> <Sdk Name="Aspire.AppHost.Sdk" Version="9.5.0" /> <PropertyGroup> <OutputType>Exe</OutputType> </PropertyGroup> </Project> """; await File.WriteAllTextAsync(projectFile, projectContent); var parser = new FallbackProjectParser(NullLogger<FallbackProjectParser>.Instance); // Act var result = parser.ParseProject(new FileInfo(projectFile)); // Assert - should fall back to old format and get 9.5.0 await Verify(FormatJson(result), extension: "json"); } [Fact] public async Task ParseProject_ExtractsPackageReferences() { // Arrange using var workspace = TemporaryWorkspace.Create(output); var projectFile = Path.Combine(workspace.WorkspaceRoot.FullName, "Test.csproj"); var projectContent = """ <Project Sdk="Microsoft.NET.Sdk"> <Sdk Name="Aspire.AppHost.Sdk" Version="9.5.0-test" /> <ItemGroup> <PackageReference Include="Aspire.Hosting.AppHost" Version="9.5.0-test" /> <PackageReference Include="Aspire.Hosting.Redis" Version="9.4.1" /> </ItemGroup> </Project> """; await File.WriteAllTextAsync(projectFile, projectContent); var parser = new FallbackProjectParser(NullLogger<FallbackProjectParser>.Instance); // Act var result = parser.ParseProject(new FileInfo(projectFile)); // Assert await Verify(FormatJson(result), extension: "json"); } [Fact] public async Task ParseProject_ExtractsProjectReferences() { // Arrange using var workspace = TemporaryWorkspace.Create(output); var projectFile = Path.Combine(workspace.WorkspaceRoot.FullName, "Test.csproj"); var projectContent = """ <Project Sdk="Microsoft.NET.Sdk"> <Sdk Name="Aspire.AppHost.Sdk" Version="9.5.0-test" /> <ItemGroup> <ProjectReference Include="../ServiceDefaults/ServiceDefaults.csproj" /> <ProjectReference Include="../WebApp/WebApp.csproj" /> </ItemGroup> </Project> """; await File.WriteAllTextAsync(projectFile, projectContent); var parser = new FallbackProjectParser(NullLogger<FallbackProjectParser>.Instance); // Act var result = parser.ParseProject(new FileInfo(projectFile)); // Assert - scrub the temp path and normalize path separators for cross-platform consistency // Note: In JSON, backslashes are escaped as \\, so we need to replace \\\\ (which represents \\) // with / to get a single forward slash var tempPath = Path.GetTempPath().Replace("\\", "/"); await Verify(FormatJson(result), extension: "json") .ScrubLinesWithReplace(line => { // First normalize JSON-escaped backslashes (\\) to forward slashes // In JSON, a single backslash is represented as \\, so we replace that with / line = line.Replace("\\\\", "/"); // Then replace the temp path line = line.Replace($"/private{tempPath}", "{TempPath}"); // Handle macOS temp symlinks line = line.Replace(tempPath, "{TempPath}"); return line; }); } [Fact] public async Task ParseProject_InvalidXml_ThrowsProjectUpdaterException() { // Arrange using var workspace = TemporaryWorkspace.Create(output); var projectFile = Path.Combine(workspace.WorkspaceRoot.FullName, "Test.csproj"); var invalidProjectContent = """ <Project Sdk="Microsoft.NET.Sdk"> <Sdk Name="Aspire.AppHost.Sdk" Version="9.5.0-test" /> <!-- Missing closing tag --> <ItemGroup> <PackageReference Include="Test" Version="1.0.0" /> """; await File.WriteAllTextAsync(projectFile, invalidProjectContent); var parser = new FallbackProjectParser(NullLogger<FallbackProjectParser>.Instance); // Act & Assert Assert.Throws<ProjectUpdaterException>(() => parser.ParseProject(new FileInfo(projectFile))); } [Fact] public async Task ParseProject_SingleFileAppHost_ExtractsAspireAppHostSdk() { // Arrange using var workspace = TemporaryWorkspace.Create(output); var projectFile = Path.Combine(workspace.WorkspaceRoot.FullName, "Test.cs"); var projectContent = """ #:sdk Aspire.AppHost.Sdk@13.0.0-preview.1.25519.5 #:package Aspire.Hosting.NodeJs@9.5.1 var builder = DistributedApplication.CreateBuilder(args); builder.Build().Run(); """; await File.WriteAllTextAsync(projectFile, projectContent); var parser = new FallbackProjectParser(NullLogger<FallbackProjectParser>.Instance); // Act var result = parser.ParseProject(new FileInfo(projectFile)); // Assert await Verify(FormatJson(result), extension: "json"); } [Fact] public async Task ParseProject_SingleFileAppHost_ExtractsPackageReferences() { // Arrange using var workspace = TemporaryWorkspace.Create(output); var projectFile = Path.Combine(workspace.WorkspaceRoot.FullName, "Test.cs"); var projectContent = """ #:sdk Aspire.AppHost.Sdk@13.0.0-preview.1.25519.5 #:package Aspire.Hosting.NodeJs@9.5.1 #:package Aspire.Hosting.Python@9.5.1 #:package Aspire.Hosting.Redis@9.5.1 #:package CommunityToolkit.Aspire.Hosting.NodeJS.Extensions@9.8.0 #pragma warning disable ASPIREHOSTINGPYTHON001 var builder = DistributedApplication.CreateBuilder(args); var cache = builder.AddRedis("cache"); builder.Build().Run(); """; await File.WriteAllTextAsync(projectFile, projectContent); var parser = new FallbackProjectParser(NullLogger<FallbackProjectParser>.Instance); // Act var result = parser.ParseProject(new FileInfo(projectFile)); // Assert await Verify(FormatJson(result), extension: "json"); } [Fact] public async Task ParseProject_SingleFileAppHost_NoPackageReferences() { // Arrange using var workspace = TemporaryWorkspace.Create(output); var projectFile = Path.Combine(workspace.WorkspaceRoot.FullName, "Test.cs"); var projectContent = """ #:sdk Aspire.AppHost.Sdk@9.5.0 var builder = DistributedApplication.CreateBuilder(args); builder.Build().Run(); """; await File.WriteAllTextAsync(projectFile, projectContent); var parser = new FallbackProjectParser(NullLogger<FallbackProjectParser>.Instance); // Act var result = parser.ParseProject(new FileInfo(projectFile)); // Assert await Verify(FormatJson(result), extension: "json"); } [Fact] public async Task ParseProject_SingleFileAppHost_WithWildcardVersion() { // Arrange using var workspace = TemporaryWorkspace.Create(output); var projectFile = Path.Combine(workspace.WorkspaceRoot.FullName, "Test.cs"); var projectContent = """ #:sdk Aspire.AppHost.Sdk@* #:package Aspire.Hosting.Redis@* var builder = DistributedApplication.CreateBuilder(args); builder.Build().Run(); """; await File.WriteAllTextAsync(projectFile, projectContent); var parser = new FallbackProjectParser(NullLogger<FallbackProjectParser>.Instance); // Act var result = parser.ParseProject(new FileInfo(projectFile)); // Assert await Verify(FormatJson(result), extension: "json"); } [Fact] public async Task ParseProject_SingleFileAppHost_NoProjectReferences() { // Arrange - single-file apphosts don't support project references using var workspace = TemporaryWorkspace.Create(output); var projectFile = Path.Combine(workspace.WorkspaceRoot.FullName, "Test.cs"); var projectContent = """ #:sdk Aspire.AppHost.Sdk@9.5.0 var builder = DistributedApplication.CreateBuilder(args); builder.Build().Run(); """; await File.WriteAllTextAsync(projectFile, projectContent); var parser = new FallbackProjectParser(NullLogger<FallbackProjectParser>.Instance); // Act var result = parser.ParseProject(new FileInfo(projectFile)); // Assert await Verify(FormatJson(result), extension: "json"); } [Fact] public async Task ParseProject_SingleFileAppHost_NoSdkDirective() { // Arrange using var workspace = TemporaryWorkspace.Create(output); var projectFile = Path.Combine(workspace.WorkspaceRoot.FullName, "Test.cs"); var projectContent = """ // Missing SDK directive var builder = DistributedApplication.CreateBuilder(args); builder.Build().Run(); """; await File.WriteAllTextAsync(projectFile, projectContent); var parser = new FallbackProjectParser(NullLogger<FallbackProjectParser>.Instance); // Act var result = parser.ParseProject(new FileInfo(projectFile)); // Assert await Verify(FormatJson(result), extension: "json"); } [Fact] public async Task ParseProject_UnsupportedFileType_ThrowsProjectUpdaterException() { // Arrange using var workspace = TemporaryWorkspace.Create(output); var projectFile = Path.Combine(workspace.WorkspaceRoot.FullName, "Test.txt"); var projectContent = "Some random content"; await File.WriteAllTextAsync(projectFile, projectContent); var parser = new FallbackProjectParser(NullLogger<FallbackProjectParser>.Instance); // Act & Assert var exception = Assert.Throws<ProjectUpdaterException>(() => parser.ParseProject(new FileInfo(projectFile))); Assert.Contains("Unsupported project file type", exception.Message); } }