File: MauiWindowsExtensionsTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Maui.Tests\Aspire.Hosting.Maui.Tests.csproj (Aspire.Hosting.Maui.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Eventing;
using Aspire.Hosting.Maui;
using Aspire.Hosting.Maui.Utilities;
using Microsoft.Extensions.DependencyInjection;
 
namespace Aspire.Hosting.Tests;
 
public class MauiWindowsExtensionsTests
{
    [Fact]
    public void AddWindowsDevice_CreatesResource()
    {
        // Arrange - Create a temporary project file with Windows TFM
        var projectContent = """
            <Project Sdk="Microsoft.NET.Sdk">
                <PropertyGroup>
                    <TargetFrameworks>net10.0-android;net10.0-ios;net10.0-windows10.0.19041.0</TargetFrameworks>
                </PropertyGroup>
            </Project>
            """;
        var tempFile = CreateTempProjectFile(projectContent);
 
        try
        {
            var appBuilder = DistributedApplication.CreateBuilder();
            var maui = appBuilder.AddMauiProject("mauiapp", tempFile);
 
            // Act
            var windows = maui.AddWindowsDevice();
 
            // Assert
            Assert.NotNull(windows);
            Assert.Equal("mauiapp-windows", windows.Resource.Name);
            Assert.Same(maui.Resource, windows.Resource.Parent);
            
            // Verify the resource is in the application model
            var windowsDeviceInModel = appBuilder.Resources
                .OfType<MauiWindowsPlatformResource>()
                .FirstOrDefault(r => r.Name == "mauiapp-windows");
            Assert.NotNull(windowsDeviceInModel);
            Assert.Same(windows.Resource, windowsDeviceInModel);
        }
        finally
        {
            CleanupTempFile(tempFile);
        }
    }
 
    [Fact]
    public void AddWindowsDevice_WithCustomName_UsesProvidedName()
    {
        // Arrange - Create a temporary project file with Windows TFM
        var projectContent = """
            <Project Sdk="Microsoft.NET.Sdk">
                <PropertyGroup>
                    <TargetFrameworks>net10.0-android;net10.0-ios;net10.0-windows10.0.19041.0</TargetFrameworks>
                </PropertyGroup>
            </Project>
            """;
        var tempFile = CreateTempProjectFile(projectContent);
 
        try
        {
            var appBuilder = DistributedApplication.CreateBuilder();
            var maui = appBuilder.AddMauiProject("mauiapp", tempFile);
 
            // Act
            var windows = maui.AddWindowsDevice("custom-windows");
 
            // Assert
            Assert.Equal("custom-windows", windows.Resource.Name);
        }
        finally
        {
            CleanupTempFile(tempFile);
        }
    }
 
    [Fact]
    public void AddWindowsDevice_DuplicateName_ThrowsException()
    {
        // Arrange - Create a temporary project file with Windows TFM
        var projectContent = """
            <Project Sdk="Microsoft.NET.Sdk">
                <PropertyGroup>
                    <TargetFrameworks>net10.0-android;net10.0-ios;net10.0-windows10.0.19041.0</TargetFrameworks>
                </PropertyGroup>
            </Project>
            """;
        var tempFile = CreateTempProjectFile(projectContent);
 
        try
        {
            var appBuilder = DistributedApplication.CreateBuilder();
            var maui = appBuilder.AddMauiProject("mauiapp", tempFile);
            maui.AddWindowsDevice("device1");
 
            // Act & Assert
            var exception = Assert.Throws<DistributedApplicationException>(() => maui.AddWindowsDevice("device1"));
            Assert.Contains("already exists", exception.Message);
        }
        finally
        {
            CleanupTempFile(tempFile);
        }
    }
 
    [Fact]
    public void AddWindowsDevice_MultipleDevices_AllCreated()
    {
        // Arrange - Create a temporary project file with Windows TFM
        var projectContent = """
            <Project Sdk="Microsoft.NET.Sdk">
                <PropertyGroup>
                    <TargetFrameworks>net10.0-android;net10.0-ios;net10.0-windows10.0.19041.0</TargetFrameworks>
                </PropertyGroup>
            </Project>
            """;
        var tempFile = CreateTempProjectFile(projectContent);
 
        try
        {
            var appBuilder = DistributedApplication.CreateBuilder();
            var maui = appBuilder.AddMauiProject("mauiapp", tempFile);
 
            // Act
            var windows1 = maui.AddWindowsDevice("device1");
            var windows2 = maui.AddWindowsDevice("device2");
 
            // Assert
            var windowsDevices = appBuilder.Resources
                .OfType<MauiWindowsPlatformResource>()
                .Where(r => r.Parent == maui.Resource)
                .ToList();
            
            Assert.Equal(2, windowsDevices.Count);
            Assert.Contains(windows1.Resource, windowsDevices);
            Assert.Contains(windows2.Resource, windowsDevices);
        }
        finally
        {
            CleanupTempFile(tempFile);
        }
    }
 
    [Fact]
    public void AddWindowsDevice_HasCorrectConfiguration()
    {
        // Arrange - Create a temporary project file with Windows TFM
        var projectContent = """
            <Project Sdk="Microsoft.NET.Sdk">
                <PropertyGroup>
                    <TargetFrameworks>net10.0-android;net10.0-ios;net10.0-windows10.0.19041.0</TargetFrameworks>
                </PropertyGroup>
            </Project>
            """;
        var tempFile = CreateTempProjectFile(projectContent);
 
        try
        {
            var appBuilder = DistributedApplication.CreateBuilder();
            var maui = appBuilder.AddMauiProject("mauiapp", tempFile);
 
            // Act
            var windows = maui.AddWindowsDevice();
 
            // Assert
            var resource = windows.Resource;
            
            // Check ExecutableAnnotation
            var execAnnotation = resource.Annotations.OfType<ExecutableAnnotation>().FirstOrDefault();
            Assert.NotNull(execAnnotation);
            Assert.Equal("dotnet", execAnnotation.Command);
            
            // Check for MauiProjectMetadata annotation
            var projectMetadata = resource.Annotations.OfType<IProjectMetadata>().FirstOrDefault();
            Assert.NotNull(projectMetadata);
            Assert.Equal(tempFile, projectMetadata.ProjectPath);
            
            // Check for explicit start annotation
            var hasExplicitStart = resource.TryGetAnnotationsOfType<ExplicitStartupAnnotation>(out _);
            Assert.True(hasExplicitStart);
        }
        finally
        {
            CleanupTempFile(tempFile);
        }
    }
 
    [Fact]
    public async Task AddWindowsDevice_WithoutWindowsTfm_ThrowsOnBeforeStartEvent()
    {
        // Arrange - Create a temporary project file without Windows TFM
        var projectContent = """
            <Project Sdk="Microsoft.NET.Sdk">
                <PropertyGroup>
                    <TargetFrameworks>net10.0-android;net10.0-ios;net10.0-maccatalyst</TargetFrameworks>
                </PropertyGroup>
            </Project>
            """;
        var tempFile = CreateTempProjectFile(projectContent);
 
        try
        {
            var appBuilder = DistributedApplication.CreateBuilder();
            var maui = appBuilder.AddMauiProject("mauiapp", tempFile);
 
            // Act - Adding the device should succeed (validation deferred to start)
            var windows = maui.AddWindowsDevice();
            
            // Assert - Resource is created
            Assert.NotNull(windows);
            Assert.Equal("mauiapp-windows", windows.Resource.Name);
            
            // Build the app to get access to eventing
            await using var app = appBuilder.Build();
            
            // Trigger the BeforeResourceStartedEvent which should throw
            var exception = await Assert.ThrowsAsync<DistributedApplicationException>(async () =>
            {
                await app.Services.GetRequiredService<IDistributedApplicationEventing>()
                    .PublishAsync(new BeforeResourceStartedEvent(windows.Resource, app.Services), CancellationToken.None);
            });
            
            Assert.Contains("Unable to detect Windows target framework", exception.Message);
            Assert.Contains(tempFile, exception.Message);
        }
        finally
        {
            CleanupTempFile(tempFile);
        }
    }
 
    private static string CreateTempProjectFile(string content)
    {
        var tempFile = Path.GetTempFileName();
        var tempProjectFile = Path.ChangeExtension(tempFile, ".csproj");
        if (File.Exists(tempFile))
        {
            File.Delete(tempFile);
        }
        File.WriteAllText(tempProjectFile, content);
        return tempProjectFile;
    }
 
    private static void CleanupTempFile(string filePath)
    {
        if (File.Exists(filePath))
        {
            File.Delete(filePath);
        }
    }
 
    [Fact]
    public async Task GetWindowsTargetFramework_WithWindowsTfm_ReturnsCorrectTfm()
    {
        // Arrange
        var projectContent = """
            <Project Sdk="Microsoft.NET.Sdk">
                <PropertyGroup>
                    <TargetFrameworks>net10.0-android;net10.0-ios;net10.0-windows10.0.19041.0</TargetFrameworks>
                </PropertyGroup>
            </Project>
            """;
        var tempFile = CreateTempProjectFile(projectContent);
 
        try
        {
            // Act
            var tfm = ProjectFileReader.GetPlatformTargetFramework(tempFile, "windows");
 
            // Assert
            Assert.Equal("net10.0-windows10.0.19041.0", tfm);
        }
        finally
        {
            CleanupTempFile(tempFile);
        }
    }
 
    [Fact]
    public async Task GetWindowsTargetFramework_WithConditionalWindowsTfm_ReturnsCorrectTfm()
    {
        if (!System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows))
        {
            Assert.Skip("This test requires Windows because MSBuild only evaluates the conditional Windows TFM on Windows platforms.");
        }
 
        // Arrange
        var projectContent = """
            <Project Sdk="Microsoft.NET.Sdk">
                <PropertyGroup>
                    <TargetFrameworks>net10.0-android;net10.0-ios</TargetFrameworks>
                    <TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net10.0-windows10.0.19041.0</TargetFrameworks>
                </PropertyGroup>
            </Project>
            """;
        var tempFile = CreateTempProjectFile(projectContent);
 
        try
        {
            // Act
            var tfm = ProjectFileReader.GetPlatformTargetFramework(tempFile, "windows");
 
            // Assert
            Assert.Equal("net10.0-windows10.0.19041.0", tfm);
        }
        finally
        {
            CleanupTempFile(tempFile);
        }
    }
 
    [Fact]
    public async Task GetWindowsTargetFramework_WithoutWindowsTfm_ReturnsNull()
    {
        // Arrange
        var projectContent = """
            <Project Sdk="Microsoft.NET.Sdk">
                <PropertyGroup>
                    <TargetFrameworks>net10.0-android;net10.0-ios;net10.0-maccatalyst</TargetFrameworks>
                </PropertyGroup>
            </Project>
            """;
        var tempFile = CreateTempProjectFile(projectContent);
 
        try
        {
            // Act
            var tfm = ProjectFileReader.GetPlatformTargetFramework(tempFile, "windows");
 
            // Assert
            Assert.Null(tfm);
        }
        finally
        {
            CleanupTempFile(tempFile);
        }
    }
 
    [Fact]
    public async Task GetWindowsTargetFramework_WithSingleWindowsTfm_ReturnsCorrectTfm()
    {
        // Arrange
        var projectContent = """
            <Project Sdk="Microsoft.NET.Sdk">
                <PropertyGroup>
                    <TargetFramework>net10.0-windows10.0.19041.0</TargetFramework>
                </PropertyGroup>
            </Project>
            """;
        var tempFile = CreateTempProjectFile(projectContent);
 
        try
        {
            // Act
            var tfm = ProjectFileReader.GetPlatformTargetFramework(tempFile, "windows");
 
            // Assert
            Assert.Equal("net10.0-windows10.0.19041.0", tfm);
        }
        finally
        {
            CleanupTempFile(tempFile);
        }
    }
 
    [Fact]
    public async Task GetWindowsTargetFramework_InvalidFile_ReturnsNull()
    {
        // Arrange
        var nonExistentFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".csproj");
 
        // Act
        var tfm = ProjectFileReader.GetPlatformTargetFramework(nonExistentFile, "windows");
 
        // Assert - returns null when file can't be read
        Assert.Null(tfm);
    }
}