File: TestServices\FakeAppHostServerSession.cs
Web Access
Project: src\tests\Aspire.Cli.Tests\Aspire.Cli.Tests.csproj (Aspire.Cli.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.Cli.Commands.Sdk;
using Aspire.Cli.Processes;
using Aspire.Cli.Projects;
using Aspire.Cli.Utils;
using Aspire.TypeSystem;
 
namespace Aspire.Cli.Tests.TestServices;
 
/// <summary>
/// Fake <see cref="IAppHostServerSession"/> that returns a <see cref="FakeAppHostRpcClient"/>
/// without starting a real process or connecting to a socket.
/// </summary>
internal sealed class FakeAppHostServerSession : IAppHostServerSession
{
    private readonly IAppHostRpcClient _rpcClient;
    private readonly TaskCompletionSource<int> _exit = new();
 
    public FakeAppHostServerSession(IAppHostRpcClient? rpcClient = null)
    {
        _rpcClient = rpcClient ?? new FakeAppHostRpcClient();
    }
 
    public Func<Task>? StartAsyncCallback { get; init; }
 
    public Func<CancellationToken, Task<IAppHostRpcClient>>? GetRpcClientAsyncCallback { get; init; }
 
    public bool StartAsyncCalled { get; private set; }
 
    public string AuthenticationToken { get; } = "fake-token";
 
    public string? SocketPath { get; } = "fake.sock";
 
    public OutputCollector? Output { get; } = new();
 
    public bool? ServerHasExited { get; set; }
 
    public int? ServerExitCode { get; set; }
 
    public bool? HasServerExited => ServerHasExited ?? _exit.Task.IsCompleted;
 
    public int? TryGetServerExitCode()
    {
        if (ServerHasExited == true)
        {
            return ServerExitCode;
        }
 
        return _exit.Task.IsCompletedSuccessfully ? _exit.Task.Result : null;
    }
 
    public async Task StartAsync()
    {
        StartAsyncCalled = true;
        if (StartAsyncCallback is not null)
        {
            await StartAsyncCallback();
        }
    }
 
    public Task<int> WaitForExitAsync() => _exit.Task;
 
    public Task<IAppHostRpcClient> GetRpcClientAsync(CancellationToken cancellationToken)
        => GetRpcClientAsyncCallback is not null
            ? GetRpcClientAsyncCallback(cancellationToken)
            : Task.FromResult(_rpcClient);
 
    public ValueTask DisposeAsync()
    {
        _exit.TrySetResult(0);
        return ValueTask.CompletedTask;
    }
}
 
/// <summary>
/// Fake <see cref="IAppHostServerSessionFactory"/> that hands back a <see cref="FakeAppHostServerSession"/>
/// without building or launching a real AppHost server.
/// </summary>
internal sealed class FakeAppHostServerSessionFactory : IAppHostServerSessionFactory
{
    public IAppHostServerSession? Session { get; init; }
 
    public Dictionary<string, string>? CapturedEnvironmentVariables { get; private set; }
 
    public IAppHostServerSession Create(
        IAppHostServerProject appHostServerProject,
        Dictionary<string, string>? environmentVariables,
        bool debug,
        IProcessTreeGracefulShutdownSignaler? gracefulShutdownSignaler,
        IGracefulShutdownWindow? shutdownService,
        bool isolateConsole,
        CancellationToken stopRequested)
    {
        CapturedEnvironmentVariables = environmentVariables is null ? null : new Dictionary<string, string>(environmentVariables);
        return Session ?? new FakeAppHostServerSession();
    }
}
 
/// <summary>
/// Fake RPC client that returns empty results for all operations.
/// Used to exercise code paths that run after RPC connection without needing a real server.
/// </summary>
internal sealed class FakeAppHostRpcClient : IAppHostRpcClient
{
    public Task<RuntimeSpec> GetRuntimeSpecAsync(string languageId, CancellationToken cancellationToken)
        => Task.FromResult(new RuntimeSpec
        {
            Language = languageId,
            DisplayName = "Fake",
            CodeGenLanguage = "TypeScript",
            DetectionPatterns = ["apphost.ts"],
            Execute = new CommandSpec { Command = "node", Args = ["apphost.js"] }
        });
 
    public Task<Dictionary<string, string>> ScaffoldAppHostAsync(string languageId, string targetPath, string? projectName, CancellationToken cancellationToken)
        => throw new NotSupportedException();
 
    public Task<Dictionary<string, string>> GenerateCodeAsync(string languageId, CancellationToken cancellationToken)
        => Task.FromResult(new Dictionary<string, string>());
 
    public Task<Dictionary<string, string>> GenerateCodeForAssemblyAsync(string languageId, string assemblyName, CancellationToken cancellationToken)
        => Task.FromResult(new Dictionary<string, string>());
 
    public Task<CapabilitiesInfo> GetCapabilitiesAsync(CancellationToken cancellationToken)
        => throw new NotSupportedException();
 
    public Task<CapabilitiesInfo> GetCapabilitiesForAssembliesAsync(IReadOnlyList<string> assemblyNames, CancellationToken cancellationToken)
        => throw new NotSupportedException();
 
    public Task<T> InvokeAsync<T>(string methodName, object?[] parameters, CancellationToken cancellationToken)
        => throw new NotSupportedException();
 
    public Task InvokeAsync(string methodName, object?[] parameters, CancellationToken cancellationToken)
        => throw new NotSupportedException();
 
    public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}