File: InnerLoop\GracefulDegradationTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Radius.Tests\Aspire.Hosting.Radius.Tests.csproj (Aspire.Hosting.Radius.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.Runtime.CompilerServices;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.DependencyInjection;
 
namespace Aspire.Hosting.Radius.Tests.InnerLoop;
 
public class GracefulDegradationTests
{
    [Fact]
    public async Task AspireRun_Completes_WhenKubernetesUnavailable()
    {
        // In Run mode the Radius integration short-circuits — it doesn't register itself
        // with the app builder, doesn't add pipeline steps, and never attempts to talk to
        // Kubernetes or the rad CLI. So `aspire run` against an AppHost using AddRadiusEnvironment
        // must succeed regardless of whether a Kubernetes cluster is reachable.
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
        builder.AddRadiusEnvironment("radius");
        builder.AddContainer("api", "myapp/api:latest");
 
        using var app = builder.Build();
 
        await ExecuteBeforeStartHooksAsync(app, default);
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        // Radius env is intentionally NOT registered in Run mode (see A3 in RadiusExtensions).
        Assert.Empty(model.Resources.OfType<RadiusEnvironmentResource>());
 
        // The container is still in the model and not annotated for Radius — it stays a
        // regular Aspire container for inner-loop execution.
        var apiResource = model.Resources.First(r => r.Name == "api");
        Assert.Empty(apiResource.Annotations.OfType<DeploymentTargetAnnotation>());
    }
 
    [Fact]
    public async Task AspireRun_Completes_WhenRadCliNotOnPath()
    {
        // Run mode never invokes the rad CLI — the deploy pipeline step is publish-only.
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
        builder.AddRadiusEnvironment("radius");
        builder.AddContainer("api", "myapp/api:latest");
 
        using var app = builder.Build();
        await ExecuteBeforeStartHooksAsync(app, default);
    }
 
    [Fact]
    public async Task PublishMode_AttachesAnnotationsToAllComputeResources()
    {
        // In Publish mode the prepare step attaches DeploymentTargetAnnotation to every
        // compute resource, independent of whether infrastructure is reachable. The actual
        // deploy step is what requires rad/Kubernetes — annotation attachment is pure
        // in-process model manipulation.
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        builder.AddRadiusEnvironment("radius");
        builder.AddContainer("frontend", "myapp/frontend:latest");
        builder.AddContainer("backend", "myapp/backend:latest");
 
        using var app = builder.Build();
        await ExecuteBeforeStartHooksAsync(app, default);
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        foreach (var resource in model.GetComputeResources())
        {
            var annotations = resource.Annotations.OfType<DeploymentTargetAnnotation>().ToArray();
            Assert.NotEmpty(annotations);
        }
    }
 
    [Fact]
    public async Task PublishMode_LogsEnvironmentConfiguration()
    {
        // RadiusInfrastructure.PrepareDeploymentTargetsAsync emits an Information-level log
        // line describing the environment / namespace each time it runs. We don't capture
        // the log here, just verify the prepare step ran (annotations attached) and the
        // environment carries the requested namespace.
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        builder.AddRadiusEnvironment("testenv").WithNamespace("test-ns");
        builder.AddContainer("api", "myapp/api:latest");
 
        using var app = builder.Build();
        await ExecuteBeforeStartHooksAsync(app, default);
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var apiResource = model.Resources.First(r => r.Name == "api");
        var annotations = apiResource.Annotations.OfType<DeploymentTargetAnnotation>().ToArray();
        Assert.Single(annotations);
 
        var env = model.Resources.OfType<RadiusEnvironmentResource>().First();
        Assert.Equal("test-ns", env.Namespace);
    }
 
    [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "ExecuteBeforeStartHooksAsync")]
    private static extern Task ExecuteBeforeStartHooksAsync(DistributedApplication app, CancellationToken cancellationToken);
}