File: LaunchSettingsPortHelpersTests.cs
Web Access
Project: src\tests\Aspire.Cli.EndToEnd.Tests\Aspire.Cli.EndToEnd.Tests.csproj (Aspire.Cli.EndToEnd.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.EndToEnd.Tests.Helpers;
using Xunit;
 
namespace Aspire.Cli.EndToEnd.Tests;
 
public sealed class LaunchSettingsPortHelpersTests : IDisposable
{
    // Shape of the launch settings generated by `aspire new` for a starter AppHost.
    private const string AppHostLaunchSettings = """
        {
          "$schema": "https://json.schemastore.org/launchsettings.json",
          "profiles": {
            "https": {
              "commandName": "Project",
              "dotnetRunMessages": true,
              "launchBrowser": true,
              "applicationUrl": "https://localhost:17179;http://localhost:15203",
              "environmentVariables": {
                "ASPNETCORE_ENVIRONMENT": "Development",
                "DOTNET_ENVIRONMENT": "Development",
                "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21151",
                "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22001"
              }
            },
            "http": {
              "commandName": "Project",
              "dotnetRunMessages": true,
              "launchBrowser": true,
              "applicationUrl": "http://localhost:15203",
              "environmentVariables": {
                "ASPNETCORE_ENVIRONMENT": "Development",
                "DOTNET_ENVIRONMENT": "Development",
                "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19021",
                "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20187"
              }
            }
          }
        }
        """;
 
    private const string ApiServiceLaunchSettings = """
        {
          "profiles": {
            "https": {
              "commandName": "Project",
              "applicationUrl": "https://app.dev.localhost:7411;http://app.dev.localhost:5402"
            }
          }
        }
        """;
 
    private readonly DirectoryInfo _root = Directory.CreateTempSubdirectory("aspire-launch-ports-");
 
    public void Dispose() => _root.Delete(recursive: true);
 
    [Fact]
    public void MakePortsDistinct_RewritesPortsSharedWithOtherProject()
    {
        // Worst case of the template's random port choice: both projects got identical ports.
        var app1 = CreateProject("App1", AppHostLaunchSettings, ApiServiceLaunchSettings);
        var app2 = CreateProject("App2", AppHostLaunchSettings, ApiServiceLaunchSettings);
        Assert.Contains(22001, GetPorts(app2));
 
        var mapping = LaunchSettingsPortHelpers.MakePortsDistinct(app2, app1);
 
        var app1Ports = GetPorts(app1);
        var app2Ports = GetPorts(app2);
        Assert.Empty(app1Ports.Intersect(app2Ports));
        Assert.Equal(app1Ports.Count, app2Ports.Count);
        Assert.Equal(app1Ports.Order(), mapping.Keys.Order());
 
        // App1 is untouched; App2 keeps its content apart from the remapped ports, and a port that is
        // repeated across profiles maps to the same new port everywhere.
        Assert.Equal(AppHostLaunchSettings, File.ReadAllText(GetAppHostFile(app1)));
        var expectedAppHost = AppHostLaunchSettings;
        foreach (var (oldPort, newPort) in mapping.OrderByDescending(m => m.Key))
        {
            expectedAppHost = expectedAppHost.Replace($":{oldPort}", $":{newPort}", StringComparison.Ordinal);
        }
        Assert.Equal(expectedAppHost, File.ReadAllText(GetAppHostFile(app2)));
        foreach (var file in LaunchSettingsPortHelpers.GetLaunchSettingsFiles(app2))
        {
            using var _ = JsonDocument.Parse(File.ReadAllText(file));
        }
    }
 
    [Fact]
    public void MakePortsDistinct_SkipsPortsUsedByEitherProject()
    {
        var app1 = CreateProject("App1", """{ "a": "https://localhost:22001", "b": "https://localhost:22002" }""");
        var app2 = CreateProject("App2", """{ "a": "https://localhost:22001", "b": "https://localhost:22003" }""");
 
        var mapping = LaunchSettingsPortHelpers.MakePortsDistinct(app2, app1);
 
        Assert.Equal(new Dictionary<int, int> { [22001] = 22004 }, mapping);
        Assert.Equal("""{ "a": "https://localhost:22004", "b": "https://localhost:22003" }""", File.ReadAllText(GetAppHostFile(app2)));
    }
 
    [Fact]
    public void MakePortsDistinct_LeavesProjectWithoutSharedPortsUnchanged()
    {
        var app1 = CreateProject("App1", AppHostLaunchSettings);
        var distinct = """{ "a": "https://localhost:17000", "b": "https://localhost:22000" }""";
        var app2 = CreateProject("App2", distinct);
 
        var mapping = LaunchSettingsPortHelpers.MakePortsDistinct(app2, app1);
 
        Assert.Empty(mapping);
        Assert.Equal(distinct, File.ReadAllText(GetAppHostFile(app2)));
    }
 
    private DirectoryInfo CreateProject(string name, string appHostLaunchSettings, string? apiServiceLaunchSettings = null)
    {
        var project = _root.CreateSubdirectory(name);
        WriteLaunchSettings(project, $"{name}.AppHost", appHostLaunchSettings);
        if (apiServiceLaunchSettings is not null)
        {
            WriteLaunchSettings(project, $"{name}.ApiService", apiServiceLaunchSettings);
        }
        return project;
    }
 
    private static void WriteLaunchSettings(DirectoryInfo project, string projectName, string content)
    {
        var properties = project.CreateSubdirectory(Path.Combine(projectName, "Properties"));
        File.WriteAllText(Path.Combine(properties.FullName, "launchSettings.json"), content);
    }
 
    private static string GetAppHostFile(DirectoryInfo project) =>
        Path.Combine(project.FullName, $"{project.Name}.AppHost", "Properties", "launchSettings.json");
 
    private static HashSet<int> GetPorts(DirectoryInfo project) =>
        LaunchSettingsPortHelpers.GetLaunchSettingsFiles(project)
            .SelectMany(f => LaunchSettingsPortHelpers.GetPorts(File.ReadAllText(f)))
            .ToHashSet();
}