File: PublishingTests.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 ASPIREPUBLISHERS001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
 
using Aspire.Hosting.Publishing;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Xunit;
 
namespace Aspire.Hosting.Tests;
 
public class PublishingTests
{
    [Fact]
    public void PublishCallsPublishingCallback()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default");
 
        var publishedCalled = false;
 
        builder.AddContainer("cache", "redis")
               .WithPublishingCallback(context =>
                {
                    Assert.NotNull(context);
                    Assert.NotNull(context.Services);
                    Assert.True(context.CancellationToken.CanBeCanceled);
                    Assert.Equal(DistributedApplicationOperation.Publish, context.ExecutionContext.Operation);
                    Assert.Equal("default", context.ExecutionContext.PublisherName);
                    Assert.True(Path.IsPathFullyQualified(context.OutputPath));
                    publishedCalled = true;
                    return Task.CompletedTask;
                });
 
        using var app = builder.Build();
        app.Run();
 
        Assert.True(publishedCalled, "Publishing callback was not called.");
    }
 
    [Fact]
    public void PublishWithDeployFalseDoesNotCallDeployingCallback()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default");
 
        // Explicitly set Deploy to false
        builder.Configuration["Publishing:Deploy"] = "false";
 
        var publishingCalled = false;
        var deployingCalled = false;
 
        builder.AddContainer("cache", "redis")
               .WithPublishingCallback(context =>
                {
                    publishingCalled = true;
                    return Task.CompletedTask;
                })
               .WithAnnotation(new DeployingCallbackAnnotation(context =>
                {
                    deployingCalled = true;
                    return Task.CompletedTask;
                }));
 
        using var app = builder.Build();
        app.Run();
 
        Assert.True(publishingCalled, "Publishing callback was not called.");
        Assert.False(deployingCalled, "Deploying callback should not be called when Deploy is false.");
    }
 
    [Fact]
    public void PublishWithDeployTrueCallsDeployingCallback()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default");
 
        // Explicitly set Deploy to true
        builder.Configuration["Publishing:Deploy"] = "true";
 
        var publishingCalled = false;
        var deployingCalled = false;
 
        builder.AddContainer("cache", "redis")
               .WithPublishingCallback(context =>
                {
                    Assert.NotNull(context);
                    Assert.NotNull(context.Services);
                    Assert.True(context.CancellationToken.CanBeCanceled);
                    Assert.Equal(DistributedApplicationOperation.Publish, context.ExecutionContext.Operation);
                    Assert.Equal("default", context.ExecutionContext.PublisherName);
                    Assert.True(Path.IsPathFullyQualified(context.OutputPath));
                    publishingCalled = true;
                    return Task.CompletedTask;
                })
               .WithAnnotation(new DeployingCallbackAnnotation(context =>
                {
                    Assert.NotNull(context);
                    Assert.NotNull(context.Services);
                    Assert.True(context.CancellationToken.CanBeCanceled);
                    Assert.Equal(DistributedApplicationOperation.Publish, context.ExecutionContext.Operation);
                    Assert.Equal("default", context.ExecutionContext.PublisherName);
                    Assert.True(Path.IsPathFullyQualified(context.OutputPath));
                    deployingCalled = true;
                    return Task.CompletedTask;
                }));
 
        using var app = builder.Build();
        app.Run();
 
        Assert.True(publishingCalled, "Publishing callback was not called.");
        Assert.True(deployingCalled, "Deploying callback was not called when Deploy is true.");
    }
 
    [Fact]
    public void DeployingCallbacksAreCalledAfterPublishingCallbacks()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default");
 
        // Explicitly set Deploy to true
        builder.Configuration["Publishing:Deploy"] = "true";
 
        var callOrder = new List<string>();
 
        builder.AddContainer("cache", "redis")
               .WithPublishingCallback(context =>
                {
                    callOrder.Add("publishing");
                    return Task.CompletedTask;
                })
               .WithAnnotation(new DeployingCallbackAnnotation(context =>
                {
                    callOrder.Add("deploying");
                    return Task.CompletedTask;
                }));
 
        using var app = builder.Build();
        app.Run();
 
        Assert.Equal(2, callOrder.Count);
        Assert.Equal("publishing", callOrder[0]);
        Assert.Equal("deploying", callOrder[1]);
    }
 
    [Fact]
    public void MultipleResourcesWithDeployingCallbacks()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default");
 
        // Explicitly set Deploy to true
        builder.Configuration["Publishing:Deploy"] = "true";
 
        var deployingCallbacks = new List<string>();
 
        builder.AddContainer("cache", "redis")
               .WithAnnotation(new DeployingCallbackAnnotation(context =>
                {
                    deployingCallbacks.Add("cache");
                    return Task.CompletedTask;
                }));
 
        builder.AddContainer("db", "postgres")
               .WithAnnotation(new DeployingCallbackAnnotation(context =>
                {
                    deployingCallbacks.Add("db");
                    return Task.CompletedTask;
                }));
 
        using var app = builder.Build();
        app.Run();
 
        Assert.Equal(2, deployingCallbacks.Count);
        Assert.Contains("cache", deployingCallbacks);
        Assert.Contains("db", deployingCallbacks);
    }
 
    [Fact]
    public void DeployingCallbackReceivesCorrectContext()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default");
 
        // Explicitly set Deploy to true
        builder.Configuration["Publishing:Deploy"] = "true";
 
        var contextValidated = false;
 
        builder.AddContainer("cache", "redis")
               .WithAnnotation(new DeployingCallbackAnnotation(context =>
                {
                    // Validate all properties of the DeployingContext
                    Assert.NotNull(context);
                    Assert.NotNull(context.Model);
                    Assert.NotNull(context.Services);
                    Assert.NotNull(context.Logger);
                    Assert.True(context.CancellationToken.CanBeCanceled);
                    Assert.Equal(DistributedApplicationOperation.Publish, context.ExecutionContext.Operation);
                    Assert.Equal("default", context.ExecutionContext.PublisherName);
                    Assert.True(Path.IsPathFullyQualified(context.OutputPath));
 
                    // Verify the model contains our resource
                    Assert.Single(context.Model.Resources);
                    Assert.Equal("cache", context.Model.Resources.Single().Name);
 
                    contextValidated = true;
                    return Task.CompletedTask;
                }));
 
        using var app = builder.Build();
        app.Run();
 
        Assert.True(contextValidated, "DeployingContext validation failed.");
    }
 
    [Fact]
    public void PublishingOptionsDeployPropertyDefaultsToFalse()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default");
        using var app = builder.Build();
 
        var publishingOptions = app.Services.GetRequiredService<IOptions<PublishingOptions>>();
        Assert.False(publishingOptions.Value.Deploy, "Deploy should default to false.");
    }
 
    [Fact]
    public void PublishingOptionsDeployPropertyCanBeSetToTrue()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default");
        builder.Configuration["Publishing:Deploy"] = "true";
        using var app = builder.Build();
 
        var publishingOptions = app.Services.GetRequiredService<IOptions<PublishingOptions>>();
        Assert.True(publishingOptions.Value.Deploy, "Deploy should be true when configured.");
    }
 
    [Fact]
    public void PublishingOptionsDeployPropertyCanBeSetViaCommandLine()
    {
        var args = new[] { "--publisher", "default", "--output-path", "./", "--deploy", "true" };
        using var builder = TestDistributedApplicationBuilder.Create(args);
        using var app = builder.Build();
 
        var publishingOptions = app.Services.GetRequiredService<IOptions<PublishingOptions>>();
        Assert.True(publishingOptions.Value.Deploy, "Deploy should be true when set via command line.");
    }
 
    [Fact]
    public async Task DeployingCallback_Throws_PropagatesException()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default");
        builder.Configuration["Publishing:Deploy"] = "true";
        builder.AddContainer("cache", "redis")
               .WithAnnotation(new DeployingCallbackAnnotation(_ => throw new InvalidOperationException("Deploy failed!")));
        using var app = builder.Build();
        var ex = await Assert.ThrowsAsync<DistributedApplicationException>(() => app.RunAsync());
        Assert.Contains("Deploy failed!", ex.Message);
    }
 
    [Fact]
    public void DeployingCallback_OnlyLastAnnotationIsUsed()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default");
        builder.Configuration["Publishing:Deploy"] = "true";
        var called = string.Empty;
        builder.AddContainer("cache", "redis")
               .WithAnnotation(new DeployingCallbackAnnotation(_ => { called = "first"; return Task.CompletedTask; }))
               .WithAnnotation(new DeployingCallbackAnnotation(_ => { called = "second"; return Task.CompletedTask; }));
        using var app = builder.Build();
        app.Run();
        Assert.Equal("second", called);
    }
 
    [Fact]
    public void DeployingContextProgressReporterProperty()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "default");
 
        // Explicitly set Deploy to true
        builder.Configuration["Publishing:Deploy"] = "true";
 
        var progressReporterAccessed = false;
 
        builder.AddContainer("cache", "redis")
               .WithAnnotation(new DeployingCallbackAnnotation(context =>
                {
                    // Verify that ProgressReporter property is accessible and not null
                    Assert.NotNull(context.ProgressReporter);
                    Assert.IsAssignableFrom<IPublishingActivityProgressReporter>(context.ProgressReporter);
                    
                    // Verify that accessing it multiple times returns the same instance (lazy initialization)
                    var reporter1 = context.ProgressReporter;
                    var reporter2 = context.ProgressReporter;
                    Assert.Same(reporter1, reporter2);
                    
                    progressReporterAccessed = true;
                    return Task.CompletedTask;
                }));
 
        using var app = builder.Build();
        app.Run();
 
        Assert.True(progressReporterAccessed, "ProgressReporter property was not tested.");
    }
}