File: TestServices\TestConfigSourceServices.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 System.Text.Json.Nodes;
using System.Text.Json;
using System.Collections.Concurrent;
using System.Text;
using Aspire.Cli.Configuration;
using Aspire.Cli.Git;
using Aspire.Cli.Utils;
using Aspire.Shared.Telemetry;
using Aspire.Cli.Tests.Utils;
using Microsoft.Extensions.Logging.Abstractions;
 
namespace Aspire.Cli.Tests.TestServices;
 
internal static class TestConfigSourceServices
{
    public static async Task SaveBindingAsync(ConfigSource source, DirectoryInfo projectDirectory, CancellationToken cancellationToken)
    {
        var path = Path.Combine(projectDirectory.FullName, AspireConfigFile.FileName);
        var configuration = File.Exists(path)
            ? JsonNode.Parse(await File.ReadAllTextAsync(path, cancellationToken))!.AsObject()
            : new JsonObject();
        configuration["configSource"] = JsonSerializer.SerializeToNode(source, JsonSourceGenerationContext.Default.ConfigSource);
        await ConfigurationHelper.WriteSettingsFileAsync(path, configuration, cancellationToken);
    }
 
    public static ConfigSourceService Create(CliExecutionContext context, IGitClient? git = null,
        TimeProvider? clock = null, IEnvironment? environment = null, TestConfigSourceDetector? detector = null,
        TestInteractionService? interaction = null, bool interactive = false, FileInfo? globalSettingsFile = null)
    {
        var configurationService = new TestConfigurationService
        {
            SettingsFilePath = globalSettingsFile?.FullName ?? Path.Combine(context.WorkingDirectory.FullName, ".aspire", "settings.global.json")
        };
        return new ConfigSourceService(context, configurationService, environment ?? new TestEnvironment(), detector ?? new TestConfigSourceDetector(),
            git ?? new TestConfigSourceRepository(), clock ?? TimeProvider.System, interaction ?? new TestInteractionService(),
            interactive ? TestHelpers.CreateInteractiveHostEnvironment() : TestHelpers.CreateNonInteractiveHostEnvironment(),
            NullLogger<ConfigSourceService>.Instance);
    }
 
    public static async Task<(string Commit, string Version, JsonObject Configuration)> AcquireAsync(
        this IGitClient git, ConfigSource source, string directory, CancellationToken cancellationToken, TimeProvider? clock = null)
    {
        using var service = Create(TestExecutionContextHelper.CreateExecutionContext(new DirectoryInfo(directory)), git: git, clock: clock);
        var snapshot = await service.AcquireAsync(source, force: true, cancellationToken);
        return (snapshot.Metadata.Commit, snapshot.Metadata.Version, snapshot.Configuration);
    }
 
    public static async Task<(string Commit, string Version)> CheckAsync(
        this IGitClient git, ConfigSource source, string directory, CancellationToken cancellationToken, TimeProvider? clock = null)
    {
        var project = new DirectoryInfo(directory);
        await SaveBindingAsync(new ConfigSource
        {
            Repository = source.Repository,
            Path = source.Path,
            Ref = source.Ref,
            AppliedVersion = "previous-version"
        }, project, cancellationToken);
        var interaction = new TestInteractionService();
        using var service = Create(TestExecutionContextHelper.CreateExecutionContext(project), git: git, clock: clock, interaction: interaction);
        await service.CheckProjectAsync(project, force: false, cancellationToken);
        var path = Path.Combine(service.GetCacheDirectory(source), "check.json");
        Assert.True(File.Exists(path), "The service did not persist the Git version check.");
        var metadata = JsonSerializer.Deserialize(await File.ReadAllTextAsync(path, cancellationToken),
            JsonSourceGenerationContext.Default.ConfigSourceCheckMetadata)!;
        return (metadata.Commit, metadata.Version);
    }
}
 
internal sealed class TestConfigSourceDetector : IInternalMicrosoftDetector
{
    public int Calls { get; private set; }
    public bool IsInternal { get; set; }
    public string? Outcome { get; set; }
 
    public Task<InternalMicrosoftDetectionResult> IsInternalMicrosoftMachineAsync(CancellationToken cancellationToken = default)
    {
        Calls++;
        return Task.FromResult(new InternalMicrosoftDetectionResult(IsInternal, null, null, null,
            Outcome ?? (IsInternal ? InternalMicrosoftDetectorOutcome.Detected : InternalMicrosoftDetectorOutcome.NotDetected),
            InternalMicrosoftDetectorCacheStatus.Miss, TimeSpan.Zero, []));
    }
}
 
internal sealed class TestConfigSourceRepository : IGitClient
{
    private readonly ConcurrentDictionary<string, RepositoryState> _repositories = new();
    private int _calls;
    private int _checkCalls;
    public string Commit { get; set; } = "commit-1";
    public int Calls => _calls;
    public int CheckCalls => _checkCalls;
    public Func<ConfigSource, CancellationToken, Task<(string Version, JsonObject Configuration)>> ReadConfiguration { get; set; }
        = (_, _) => Task.FromResult(("version-1", new JsonObject()));
    public Func<ConfigSource, CancellationToken, Task<string>> InspectConfiguration { get; set; }
        = (_, _) => Task.FromResult("version-1");
 
    public Task InitializeAsync(DirectoryInfo workingDirectory, GitRepositoryLocation repository, string? objectFormat,
        GitCommandMode mode, CancellationToken cancellationToken) => Task.CompletedTask;
 
    public Task FetchAsync(GitRepositoryLocation destination, string repository, string reference, GitCommandMode mode, CancellationToken cancellationToken)
    {
        _repositories[destination.Directory.FullName] = new RepositoryState(repository, reference, mode);
        return Task.CompletedTask;
    }
 
    public Task<string> GetCommitAsync(GitRepositoryLocation repository, string reference, GitCommandMode mode, CancellationToken cancellationToken) =>
        Task.FromResult(Commit);
 
    public async Task<GitTreeEntry?> GetFileAsync(GitRepositoryLocation repository, string commit, string path, GitCommandMode mode, CancellationToken cancellationToken)
    {
        var state = _repositories[repository.Directory.FullName];
        var source = new ConfigSource
        {
            Repository = state.Repository,
            Path = path,
            Ref = state.Reference == "HEAD" ? null : state.Reference.StartsWith("refs/heads/", StringComparison.Ordinal)
                ? state.Reference["refs/heads/".Length..] : state.Reference
        };
        string version;
        if (state.Mode == GitCommandMode.Unattended)
        {
            Interlocked.Increment(ref _checkCalls);
            version = await InspectConfiguration(source, cancellationToken);
        }
        else
        {
            Interlocked.Increment(ref _calls);
            var configuration = await ReadConfiguration(source, cancellationToken);
            version = configuration.Version;
            state.Content = configuration.Configuration.ToJsonString();
        }
        return new GitTreeEntry(GitTreeEntry.RegularFileMode, "blob", version);
    }
 
    public Task<long?> GetObjectSizeAsync(GitRepositoryLocation repository, string objectId, GitCommandMode mode, CancellationToken cancellationToken) =>
        Task.FromResult<long?>(Encoding.UTF8.GetByteCount(_repositories[repository.Directory.FullName].Content));
 
    public Task<string> ReadBlobAsync(GitRepositoryLocation repository, string objectId, GitCommandMode mode, CancellationToken cancellationToken) =>
        Task.FromResult(_repositories[repository.Directory.FullName].Content);
 
    public Task<string> ResolveReferenceAsync(string repository, string? reference, DirectoryInfo workingDirectory, GitCommandMode mode, CancellationToken cancellationToken) =>
        Task.FromResult(Commit);
 
    public Task CheckoutAsync(GitRepositoryLocation repository, GitCommandMode mode, CancellationToken cancellationToken) =>
        throw new InvalidOperationException("Configuration sources must not check out a worktree.");
 
    private sealed class RepositoryState(string repository, string reference, GitCommandMode mode)
    {
        public string Repository { get; } = repository;
        public string Reference { get; } = reference;
        public GitCommandMode Mode { get; } = mode;
        public string Content { get; set; } = string.Empty;
    }
}