File: DistributedApplicationBuilderTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Tests\Aspire.Hosting.Tests.csproj (Aspire.Hosting.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#pragma warning disable ASPIREPIPELINES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
 
using Aspire.Hosting.Dashboard;
using Aspire.Hosting.Dcp;
using Aspire.Hosting.Devcontainers;
using Aspire.Hosting.Lifecycle;
using Aspire.Hosting.Pipelines;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
 
namespace Aspire.Hosting.Tests;
 
public class DistributedApplicationBuilderTests
{
    [Theory]
    [InlineData(new string[0], DistributedApplicationOperation.Run)]
    [InlineData(new string[] { "--publisher", "manifest" }, DistributedApplicationOperation.Publish)]
    public void BuilderExecutionContextExposesCorrectOperation(string[] args, DistributedApplicationOperation operation)
    {
        var builder = DistributedApplication.CreateBuilder(args);
        Assert.Equal(operation, builder.ExecutionContext.Operation);
    }
 
    [Fact]
    public void BuilderAddsDefaultServices()
    {
        var appBuilder = DistributedApplication.CreateBuilder();
 
        appBuilder.Services.Configure<DcpOptions>(o =>
        {
            o.DashboardPath = "dashboard";
            o.CliPath = "dcp";
        });
 
        using var app = appBuilder.Build();
 
        var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
        Assert.Empty(appModel.Resources);
 
        var eventingSubscribers = app.Services.GetServices<IDistributedApplicationEventingSubscriber>();
        Assert.Collection(
            eventingSubscribers,
            s => Assert.IsType<DashboardEventHandlers>(s),
            s => Assert.IsType<DevcontainerPortForwardingLifecycleHook>(s)
        );
 
        var options = app.Services.GetRequiredService<IOptions<PipelineOptions>>();
        Assert.Null(options.Value.OutputPath);
    }
 
    [Fact]
    public void BuilderAddsResourceToAddModel()
    {
        var appBuilder = DistributedApplication.CreateBuilder();
        appBuilder.AddResource(new TestResource());
        using var app = appBuilder.Build();
 
        var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
        var resource = Assert.Single(appModel.Resources);
        Assert.IsType<TestResource>(resource);
    }
 
    [Fact]
    public void BuilderConfiguresPublishingOptionsFromCommandLine()
    {
        var appBuilder = DistributedApplication.CreateBuilder(["--publisher", "manifest", "--output-path", "/tmp/"]);
        using var app = appBuilder.Build();
 
        var pipelineOptions = app.Services.GetRequiredService<IOptions<PipelineOptions>>();
        Assert.Equal("/tmp/", pipelineOptions.Value.OutputPath);
    }
 
    [Fact]
    public void BuilderConfiguresPublishingOptionsFromConfig()
    {
        var appBuilder = DistributedApplication.CreateBuilder(["--publisher", "manifest", "--output-path", "/tmp/"]);
        appBuilder.Configuration["Pipeline:OutputPath"] = "/path/";
        using var app = appBuilder.Build();
 
        var pipelineOptions = app.Services.GetRequiredService<IOptions<PipelineOptions>>();
        Assert.Equal("/path/", pipelineOptions.Value.OutputPath);
    }
 
    [Fact]
    public void AppHostDirectoryAvailableViaConfig()
    {
        var appBuilder = DistributedApplication.CreateBuilder();
        var appHostDirectory = appBuilder.AppHostDirectory;
        using var app = appBuilder.Build();
 
        var config = app.Services.GetRequiredService<IConfiguration>();
        Assert.Equal(appHostDirectory, config["AppHost:Directory"]);
    }
 
    [Fact]
    public void ResourceServiceConfig_Secured()
    {
        var appBuilder = DistributedApplication.CreateBuilder();
        using var app = appBuilder.Build();
 
        var config = app.Services.GetRequiredService<IConfiguration>();
        Assert.Equal(nameof(ResourceServiceAuthMode.ApiKey), config["AppHost:ResourceService:AuthMode"]);
        Assert.False(string.IsNullOrEmpty(config["AppHost:ResourceService:ApiKey"]));
    }
 
    [Theory]
    [InlineData(KnownConfigNames.DashboardUnsecuredAllowAnonymous)]
    [InlineData(KnownConfigNames.Legacy.DashboardUnsecuredAllowAnonymous)]
    public void ResourceServiceConfig_Unsecured(string dashboardUnsecuredAllowAnonymousKey)
    {
        var appBuilder = DistributedApplication.CreateBuilder(args: [$"{dashboardUnsecuredAllowAnonymousKey}=true"]);
        using var app = appBuilder.Build();
 
        var config = app.Services.GetRequiredService<IConfiguration>();
        Assert.Equal(nameof(ResourceServiceAuthMode.Unsecured), config["AppHost:ResourceService:AuthMode"]);
        Assert.True(string.IsNullOrEmpty(config["AppHost:ResourceService:ApiKey"]));
    }
 
    [Fact]
    public void AddResource_DuplicateResourceNames_SameCasing_Error()
    {
        var appBuilder = DistributedApplication.CreateBuilder();
 
        appBuilder.AddResource(new ContainerResource("Test"));
 
        var ex = Assert.Throws<DistributedApplicationException>(() => appBuilder.AddResource(new ContainerResource("Test")));
        Assert.Equal("Cannot add resource of type 'Aspire.Hosting.ApplicationModel.ContainerResource' with name 'Test' because resource of type 'Aspire.Hosting.ApplicationModel.ContainerResource' with that name already exists. Resource names are case-insensitive.", ex.Message);
    }
 
    [Fact]
    public void AddResource_DuplicateResourceNames_MixedCasing_Error()
    {
        var appBuilder = DistributedApplication.CreateBuilder();
 
        appBuilder.AddResource(new ContainerResource("Test"));
 
        var ex = Assert.Throws<DistributedApplicationException>(() => appBuilder.AddResource(new ContainerResource("TEST")));
        Assert.Equal("Cannot add resource of type 'Aspire.Hosting.ApplicationModel.ContainerResource' with name 'TEST' because resource of type 'Aspire.Hosting.ApplicationModel.ContainerResource' with that name already exists. Resource names are case-insensitive.", ex.Message);
    }
 
    [Fact]
    public void Build_DuplicateResourceNames_MixedCasing_Error()
    {
        var appBuilder = DistributedApplication.CreateBuilder();
 
        appBuilder.Resources.Add(new ContainerResource("Test"));
        appBuilder.Resources.Add(new ContainerResource("Test"));
 
        var ex = Assert.Throws<DistributedApplicationException>(appBuilder.Build);
        Assert.Equal("Multiple resources with the name 'Test'. Resource names are case-insensitive.", ex.Message);
    }
 
    [Fact]
    public void Build_DuplicateResourceNames_SameCasing_Error()
    {
        var appBuilder = DistributedApplication.CreateBuilder();
 
        appBuilder.Resources.Add(new ContainerResource("Test"));
        appBuilder.Resources.Add(new ContainerResource("TEST"));
 
        var ex = Assert.Throws<DistributedApplicationException>(appBuilder.Build);
        Assert.Equal("Multiple resources with the name 'Test'. Resource names are case-insensitive.", ex.Message);
    }
 
    [Fact]
    public void PathShaAndProjectNameShaBothAvailable()
    {
        var appBuilder = DistributedApplication.CreateBuilder();
 
        var pathSha = appBuilder.Configuration["AppHost:PathSha256"];
        var projectNameSha = appBuilder.Configuration["AppHost:ProjectNameSha256"];
        var legacySha = appBuilder.Configuration["AppHost:Sha256"];
 
        // Verify all three SHA values are available
        Assert.NotNull(pathSha);
        Assert.NotNull(projectNameSha);
        Assert.NotNull(legacySha);
 
        // In run mode, legacy SHA should equal PathSha
        Assert.False(appBuilder.ExecutionContext.IsPublishMode);
        Assert.Equal(pathSha, legacySha);
    }
 
    [Fact]
    public void PathShaDiffersForDifferentPaths()
    {
        var options1 = new DistributedApplicationOptions
        {
            ProjectDirectory = "/home/user/project1",
            ProjectName = "TestApp",
            Args = []
        };
 
        var options2 = new DistributedApplicationOptions
        {
            ProjectDirectory = "/home/user/project2",
            ProjectName = "TestApp", // Same name, different path
            Args = []
        };
 
        var builder1 = (DistributedApplicationBuilder)DistributedApplication.CreateBuilder(options1);
        var builder2 = (DistributedApplicationBuilder)DistributedApplication.CreateBuilder(options2);
 
        var pathSha1 = builder1.Configuration["AppHost:PathSha256"];
        var pathSha2 = builder2.Configuration["AppHost:PathSha256"];
        var projectNameSha1 = builder1.Configuration["AppHost:ProjectNameSha256"];
        var projectNameSha2 = builder2.Configuration["AppHost:ProjectNameSha256"];
 
        // PathSha should differ for different paths
        Assert.NotEqual(pathSha1, pathSha2);
 
        // ProjectNameSha should be the same for same project name
        Assert.Equal(projectNameSha1, projectNameSha2);
    }
 
    [Fact]
    public void LegacyShaUsesProjectNameShaInPublishMode()
    {
        var appBuilder = DistributedApplication.CreateBuilder(["--publisher", "manifest"]);
 
        var pathSha = appBuilder.Configuration["AppHost:PathSha256"];
        var projectNameSha = appBuilder.Configuration["AppHost:ProjectNameSha256"];
        var legacySha = appBuilder.Configuration["AppHost:Sha256"];
 
        // Verify all three SHA values are available
        Assert.NotNull(pathSha);
        Assert.NotNull(projectNameSha);
        Assert.NotNull(legacySha);
 
        // In publish mode, legacy SHA should equal ProjectNameSha
        Assert.True(appBuilder.ExecutionContext.IsPublishMode);
        Assert.Equal(projectNameSha, legacySha);
    }
 
    private sealed class TestResource : IResource
    {
        public string Name => nameof(TestResource);
 
        public ResourceAnnotationCollection Annotations => throw new NotImplementedException();
    }
}