File: Commands\NewCommandLocalTemplateTests.SourceManagement.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.Globalization;
using System.Text.Json;
using System.Text.Json.Nodes;
using Aspire.Cli.Commands;
using Aspire.Cli.Configuration;
using Aspire.Cli.Resources;
using Aspire.Cli.Templating;
using Aspire.Cli.Tests.TestServices;
using Aspire.Cli.Tests.Utils;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
using RootCommand = Aspire.Cli.Commands.RootCommand;
 
namespace Aspire.Cli.Tests.Commands;
 
public partial class NewCommandLocalTemplateTests
{
    [Theory]
    [InlineData(false, true)]
    [InlineData(false, false)]
    [InlineData(false, null)]
    [InlineData(true, true)]
    [InlineData(true, false)]
    [InlineData(true, null)]
    public async Task TemplateSourceCommands_RequiresTrustBeforeAdding(bool repository, bool? trust)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var interaction = new TestInteractionService();
        if (trust is { } response)
        {
            interaction.SetupBooleanResponse(response);
        }
        using var provider = CreateServices(workspace, interaction, interactive: true).BuildServiceProvider();
        var path = provider.GetRequiredService<IConfigurationService>().GetSettingsFilePath(true);
        const string original = """{ /*keep*/ "unrelated": "preserved", }""";
        await File.WriteAllTextAsync(path, original);
        var location = repository ? "https://example.test/templates" : workspace.Path;
 
        var result = await provider.GetRequiredService<RootCommand>()
            .Parse(["template", "source", "add", "company", repository ? "--repository" : "--path", location])
            .InvokeAsync().DefaultTimeout();
 
        var prompt = Assert.Single(interaction.BooleanPromptCalls);
        Assert.False(prompt.DefaultValue);
        Assert.Equal(string.Format(CultureInfo.CurrentCulture, NewCommandStrings.TemplateSourceTrustPrompt, "company", location),
            prompt.PromptText);
        if (trust == true)
        {
            Assert.Equal(CliExitCodes.Success, result);
            var source = Assert.Single(await provider.GetRequiredService<TemplateSourceStore>().ListAsync(default));
            Assert.Equal("company", source.Key);
            Assert.Equal(location, source.Value![repository ? "repository" : "path"]!.GetValue<string>());
            Assert.Single(interaction.DisplayedSuccess);
        }
        else
        {
            Assert.Equal(CliExitCodes.Cancelled, result);
            Assert.Equal(original, await File.ReadAllTextAsync(path));
            Assert.Empty(interaction.DisplayedSuccess);
            Assert.Single(interaction.DisplayedCancellations);
        }
        Assert.Empty(interaction.DisplayedErrors);
    }
 
    [Theory]
    [InlineData(true, "--yes")]
    [InlineData(true, "-y")]
    [InlineData(false, "--yes")]
    [InlineData(false, "-y")]
    public async Task TemplateSourceCommands_YesSkipsTrustPrompt(bool interactive, string yesOption)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var interaction = new TestInteractionService();
        using var provider = CreateServices(workspace, interaction, interactive).BuildServiceProvider();
        var mode = interactive ? Array.Empty<string>() : ["--non-interactive"];
 
        var result = await provider.GetRequiredService<RootCommand>()
            .Parse(["template", "source", "add", "company", "--repository", "https://example.test/templates", yesOption, .. mode])
            .InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, result);
        Assert.Empty(interaction.BooleanPromptCalls);
        Assert.Equal("company", Assert.Single(await provider.GetRequiredService<TemplateSourceStore>().ListAsync(default)).Key);
    }
 
    [Theory]
    [InlineData("")]
    [InlineData("--yes false")]
    public void TemplateSourceCommands_NonInteractiveRequiresYes(string yesOption)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var interaction = new TestInteractionService();
        using var provider = CreateServices(workspace, interaction, interactive: false).BuildServiceProvider();
 
        var result = provider.GetRequiredService<RootCommand>()
            .Parse($"template source add company --repository https://example.test/templates --non-interactive {yesOption}");
 
        Assert.Contains("--yes", Assert.Single(result.Errors).Message);
        Assert.Empty(interaction.BooleanPromptCalls);
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task TemplateSourceCommands_NoInteractiveInputRequiresYes(bool yes)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        using var provider = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.CliHostEnvironmentFactory = _ => TestHelpers.CreateNonInteractiveHostEnvironment();
        }).BuildServiceProvider();
        var path = provider.GetRequiredService<IConfigurationService>().GetSettingsFilePath(true);
        const string original = """{ /*keep*/ "unrelated": "preserved", }""";
        await File.WriteAllTextAsync(path, original);
        var arguments = yes ? new[] { "--yes" } : [];
 
        var result = await provider.GetRequiredService<RootCommand>()
            .Parse(["template", "source", "add", "company", "--repository", "https://example.test/templates", .. arguments])
            .InvokeAsync().DefaultTimeout();
 
        Assert.Equal(yes ? CliExitCodes.Success : CliExitCodes.MissingRequiredArgument, result);
        if (yes)
        {
            Assert.Equal("company", Assert.Single(await provider.GetRequiredService<TemplateSourceStore>().ListAsync(default)).Key);
        }
        else
        {
            Assert.Equal(original, await File.ReadAllTextAsync(path));
        }
    }
 
    [Theory]
    [InlineData(0, false)]
    [InlineData(1, true)]
    [InlineData(2, true)]
    [InlineData(3, true)]
    [InlineData(4, false)]
    [InlineData(5, false)]
    [InlineData(6, true)]
    [InlineData(7, true)]
    public void TemplateSourceOptions_FromParseResultValidatesEveryFieldCombination(int fieldMask, bool valid)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        using var provider = CreateServices(workspace, new TestInteractionService(), interactive: true).BuildServiceProvider();
        var path = (fieldMask & 2) != 0 ? "templates/api" : workspace.Path;
        (int Mask, string Option, string Value)[] fields =
        [
            (1, "--path", path),
            (2, "--repository", "https://example.test/repo"),
            (4, "--ref", "main")
        ];
        var arguments = fields.Where(field => (fieldMask & field.Mask) != 0)
            .SelectMany(field => new[] { field.Option, field.Value }).ToArray();
        var result = provider.GetRequiredService<RootCommand>().Parse(["template", "source", "add", "company", .. arguments]);
        Assert.Empty(result.Errors);
 
        if (valid)
        {
            var options = TemplateManagementCommand.AddSourceOptions.FromParseResult(result);
            Assert.Equal("company", options.Name);
            Assert.Equal((fieldMask & 1) != 0 ? path : null, options.Path);
            Assert.Equal((fieldMask & 2) != 0 ? "https://example.test/repo" : null, options.Repository);
            Assert.Equal((fieldMask & 4) != 0 ? "main" : null, options.Ref);
        }
        else
        {
            Assert.Throws<TemplateSourceConfigurationException>(() => TemplateManagementCommand.AddSourceOptions.FromParseResult(result));
        }
    }
 
    [Theory]
    [InlineData("local")]
    [InlineData("git")]
    [InlineData("git-defaults")]
    [InlineData("git-path")]
    [InlineData("git-ref")]
    public async Task TemplateSourceCommands_AddAndRemoveGlobalObjects(string kind)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var interaction = new TestInteractionService();
        using var provider = CreateServices(workspace, interaction, interactive: true).BuildServiceProvider();
        var configuration = provider.GetRequiredService<IConfigurationService>();
        var globalPath = configuration.GetSettingsFilePath(true);
        await File.WriteAllTextAsync(globalPath, """{"unrelated":"preserved","templateSources":{"existing":{"repository":"https://example.test/other"}}}""");
        var localPath = configuration.GetSettingsFilePath(false);
        var localBefore = File.Exists(localPath) ? await File.ReadAllTextAsync(localPath) : null;
        var options = kind switch
        {
            "local" => new[] { "--path", workspace.Path },
            "git" => ["--repository", "https://example.test/templates", "--ref", "release/17.0", "--path", "templates/api"],
            "git-defaults" => ["--repository", "https://example.test/templates"],
            "git-path" => ["--repository", "https://example.test/templates", "--path", "templates/api"],
            "git-ref" => ["--repository", "https://example.test/templates", "--ref", "release/17.0"],
            _ => throw new ArgumentOutOfRangeException(nameof(kind))
        };
        var root = provider.GetRequiredService<RootCommand>();
 
        Assert.Equal(CliExitCodes.Success, await root.Parse(["template", "source", "add", "company", "--yes", .. options]).InvokeAsync().DefaultTimeout());
        var settings = JsonNode.Parse(await File.ReadAllTextAsync(globalPath))!;
        var source = Assert.IsType<JsonObject>(settings["templateSources"]!["company"]);
        Assert.Equal(options.Length / 2, source.Count);
        for (var index = 0; index < options.Length; index += 2)
        {
            Assert.Equal(options[index + 1], source[options[index][2..]]!.GetValue<string>());
        }
        Assert.Equal("preserved", settings["unrelated"]!.GetValue<string>());
        Assert.Equal(2, settings["templateSources"]!.AsObject().Count);
        Assert.Equal(localBefore, File.Exists(localPath) ? await File.ReadAllTextAsync(localPath) : null);
 
        Assert.Equal(CliExitCodes.Success, await root.Parse("template source remove COMPANY").InvokeAsync().DefaultTimeout());
        settings = JsonNode.Parse(await File.ReadAllTextAsync(globalPath))!;
        Assert.Equal(["existing"], settings["templateSources"]!.AsObject().Select(entry => entry.Key));
        Assert.Equal("preserved", settings["unrelated"]!.GetValue<string>());
        Assert.Empty(interaction.DisplayedErrors);
    }
 
    [Theory]
    [InlineData("company", new[] { "--path", "relative/path" })]
    [InlineData("company", new[] { "--ref", "main" })]
    [InlineData("company", new string[0])]
    [InlineData("company", new[] { "--repository", "http://example.test/repo" })]
    [InlineData("company", new[] { "--repository", "https://example.test/repo", "--path", "../escape" })]
    [InlineData("nested.name", new[] { "--repository", "https://example.test/templates" })]
    [InlineData("nested:name", new[] { "--repository", "https://example.test/templates" })]
    public async Task TemplateSourceCommands_InvalidAddDoesNotWrite(string name, string[] options)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var interaction = new TestInteractionService();
        using var provider = CreateServices(workspace, interaction, interactive: true).BuildServiceProvider();
        var path = provider.GetRequiredService<IConfigurationService>().GetSettingsFilePath(true);
        const string original = """{ /*keep*/ "unrelated": "preserved", }""";
        await File.WriteAllTextAsync(path, original);
 
        var result = await provider.GetRequiredService<RootCommand>()
            .Parse(["template", "source", "add", name, .. options]).InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.InvalidCommand, result);
        Assert.Equal(original, await File.ReadAllTextAsync(path));
        Assert.Single(interaction.DisplayedErrors);
    }
 
    [Fact]
    public async Task TemplateSourceCommands_DuplicateAndMissingNamesDoNotWrite()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var interaction = new TestInteractionService();
        using var provider = CreateServices(workspace, interaction, interactive: true).BuildServiceProvider();
        var path = provider.GetRequiredService<IConfigurationService>().GetSettingsFilePath(true);
        const string original = """{ /*keep*/ "templateSources": { "Company": { "repository": "https://example.test/templates" } } }""";
        await File.WriteAllTextAsync(path, original);
        var root = provider.GetRequiredService<RootCommand>();
 
        Assert.Equal(CliExitCodes.InvalidCommand, await root.Parse("template source add company --repository https://example.test/replacement --yes").InvokeAsync().DefaultTimeout());
        Assert.Equal(original, await File.ReadAllTextAsync(path));
        Assert.Equal(CliExitCodes.ConfigNotFound, await root.Parse("template source remove missing").InvokeAsync().DefaultTimeout());
        Assert.Equal(original, await File.ReadAllTextAsync(path));
        Assert.Equal(2, interaction.DisplayedErrors.Count);
    }
 
    [Theory]
    [InlineData("{")]
    [InlineData("[]")]
    [InlineData("""{"templateSources":null}""")]
    [InlineData("""{"templateSources":42}""")]
    public async Task TemplateSourceCommands_DoNotOverwriteMalformedSettings(string original)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var interaction = new TestInteractionService();
        using var provider = CreateServices(workspace, interaction, interactive: true).BuildServiceProvider();
        var path = provider.GetRequiredService<IConfigurationService>().GetSettingsFilePath(true);
        await File.WriteAllTextAsync(path, original);
        var root = provider.GetRequiredService<RootCommand>();
 
        Assert.Equal(CliExitCodes.InvalidCommand, await root.Parse("template source add company --repository https://example.test/templates --yes").InvokeAsync().DefaultTimeout());
        Assert.Equal(original, await File.ReadAllTextAsync(path));
        Assert.Equal(CliExitCodes.InvalidCommand, await root.Parse("template source remove company").InvokeAsync().DefaultTimeout());
        Assert.Equal(original, await File.ReadAllTextAsync(path));
        Assert.Equal(2, interaction.DisplayedErrors.Count);
    }
 
    [Theory]
    [InlineData("templateSources.company.unknown", "value")]
    [InlineData("templateSources.company.path.child", "value")]
    [InlineData("templateSources.company.path", "../escape")]
    [InlineData("templateSources.company", "/some/path")]
    [InlineData("templateSources", "value")]
    public async Task TemplateSourceConfig_InvalidEditPreservesBytes(string key, string value)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var interaction = new TestInteractionService();
        using var provider = CreateServices(workspace, interaction, interactive: true).BuildServiceProvider();
        var path = provider.GetRequiredService<IConfigurationService>().GetSettingsFilePath(true);
        const string original = """{ /*keep*/ "templateSources": { "company": { "repository": "https://example.test/repo" } }, }""";
        await File.WriteAllTextAsync(path, original);
 
        var result = await provider.GetRequiredService<RootCommand>()
            .Parse(["config", "set", key, value, "--global"]).InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.InvalidCommand, result);
        Assert.Equal(original, await File.ReadAllTextAsync(path));
        Assert.Contains(key.Split('.')[0], Assert.Single(interaction.DisplayedErrors));
    }
 
    [Fact]
    public async Task TemplateSourceCommands_CaseInsensitiveRootAndSourceNamesPreserveExistingSpelling()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var interaction = new TestInteractionService();
        using var provider = CreateServices(workspace, interaction, interactive: true).BuildServiceProvider();
        var path = provider.GetRequiredService<IConfigurationService>().GetSettingsFilePath(true);
        var original = new JsonObject { ["TemplateSources"] = new JsonObject { ["Company"] = new JsonObject { ["path"] = workspace.Path } } }.ToJsonString();
        await File.WriteAllTextAsync(path, original);
        var root = provider.GetRequiredService<RootCommand>();
 
        Assert.Equal(CliExitCodes.InvalidCommand, await root.Parse("template source add COMPANY --repository https://example.test/duplicate --yes").InvokeAsync().DefaultTimeout());
        Assert.Equal(original, await File.ReadAllTextAsync(path));
        Assert.Equal(CliExitCodes.Success, await root.Parse("config set templateSources.company.path templates/api --global").InvokeAsync().DefaultTimeout());
        Assert.Equal(CliExitCodes.Success, await root.Parse("config set TEMPLATESOURCES.COMPANY.repository https://example.test/repo --global").InvokeAsync().DefaultTimeout());
        Assert.Equal(CliExitCodes.Success, await root.Parse("config set templateSources.company.ref main --global").InvokeAsync().DefaultTimeout());
        Assert.True(await provider.GetRequiredService<IConfigurationService>().DeleteConfigurationAsync("TEMPLATESOURCES.COMPANY.ref", isGlobal: true));
        var settings = JsonNode.Parse(await File.ReadAllTextAsync(path))!.AsObject();
        Assert.Equal(["TemplateSources"], settings.Select(entry => entry.Key));
        Assert.Equal(["Company"], settings["TemplateSources"]!.AsObject().Select(entry => entry.Key));
        var source = Assert.Single(await provider.GetRequiredService<ConfiguredTemplateSourceService>().GetSourcesAsync(default));
        Assert.Equal("Company", source.Key);
        Assert.Equal(new TemplateSource("templates/api", "https://example.test/repo", null), source.Value.Template);
        Assert.Equal("Company", Assert.Single(await provider.GetRequiredService<TemplateSourceStore>().ListAsync(default)).Key);
 
        Assert.Equal(CliExitCodes.Success, await root.Parse("template source add other --repository https://example.test/other --yes").InvokeAsync().DefaultTimeout());
        Assert.Equal(CliExitCodes.Success, await root.Parse("template source remove company").InvokeAsync().DefaultTimeout());
        Assert.Equal(["other"], JsonNode.Parse(await File.ReadAllTextAsync(path))!["TemplateSources"]!.AsObject().Select(entry => entry.Key));
        Assert.Equal(CliExitCodes.Success, await root.Parse("template source remove OTHER").InvokeAsync().DefaultTimeout());
        Assert.Empty(JsonNode.Parse(await File.ReadAllTextAsync(path))!.AsObject());
    }
 
    [Theory]
    [InlineData("repository", "ref", "path")]
    [InlineData("Repository", "Ref", "Path")]
    public async Task TemplateSourceConfig_EditsTypedSourcesPreservingUnknownFieldsAndExistingKeys(
        string repositoryField, string refField, string pathField)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        using var provider = CreateServices(workspace, new TestInteractionService(), interactive: true).BuildServiceProvider();
        var configuration = provider.GetRequiredService<IConfigurationService>();
        var path = configuration.GetSettingsFilePath(true);
        var settings = new JsonObject
        {
            ["templateSources"] = new JsonObject
            {
                ["company"] = new JsonObject
                {
                    [repositoryField] = "https://example.test/repo",
                    [refField] = null,
                    [pathField] = null,
                    ["futureFeature"] = new JsonObject { ["enabled"] = true }
                }
            }
        };
        await File.WriteAllTextAsync(path, settings.ToJsonString());
        var sources = provider.GetRequiredService<ConfiguredTemplateSourceService>();
        Assert.Equal(new TemplateSource(".", "https://example.test/repo", null),
            Assert.Single(await sources.GetSourcesAsync(default)).Value.Template);
 
        var result = await provider.GetRequiredService<RootCommand>()
            .Parse("config set templateSources.company.path templates/api --global").InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, result);
        var edited = JsonNode.Parse(await File.ReadAllTextAsync(path))!["templateSources"]!["company"]!.AsObject();
        Assert.Equal([repositoryField, refField, pathField, "futureFeature"], edited.Select(property => property.Key));
        Assert.True(JsonNode.DeepEquals(settings["templateSources"]!["company"]!["futureFeature"], edited["futureFeature"]));
        Assert.Equal("templates/api", edited[pathField]!.GetValue<string>());
        Assert.Null(edited[refField]);
        Assert.Equal(new TemplateSource("templates/api", "https://example.test/repo", null),
            Assert.Single(await sources.GetSourcesAsync(default)).Value.Template);
 
        Assert.True(await configuration.DeleteConfigurationAsync("templateSources.company.PATH", isGlobal: true));
        var deleted = JsonNode.Parse(await File.ReadAllTextAsync(path))!["templateSources"]!["company"]!.AsObject();
        Assert.Equal([repositoryField, refField, "futureFeature"], deleted.Select(property => property.Key));
        Assert.True(JsonNode.DeepEquals(settings["templateSources"]!["company"]!["futureFeature"], deleted["futureFeature"]));
        Assert.Equal(new TemplateSource(".", "https://example.test/repo", null),
            Assert.Single(await sources.GetSourcesAsync(default)).Value.Template);
    }
 
    [Theory]
    [InlineData("{}")]
    [InlineData("""{"templateSources":{"company":{"ref":"main"}}}""")]
    [InlineData("""{"TemplateSources":{"Company":{"ref":"main"}}}""")]
    public async Task TemplateSourceConfig_IncompleteSourceRejectsEscapingPathWithoutWriting(string original)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var interaction = new TestInteractionService();
        using var provider = CreateServices(workspace, interaction, interactive: true).BuildServiceProvider();
        var path = provider.GetRequiredService<IConfigurationService>().GetSettingsFilePath(true);
        await File.WriteAllTextAsync(path, original);
 
        var result = await provider.GetRequiredService<RootCommand>()
            .Parse("config set templateSources.company.path ../escape --global").InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.InvalidCommand, result);
        Assert.Equal(original, await File.ReadAllTextAsync(path));
        Assert.Single(interaction.DisplayedErrors);
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task TemplateSourceCommands_UnavailableLockDoesNotWrite(bool remove)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var interaction = new TestInteractionService();
        using var provider = CreateServices(workspace, interaction, interactive: true).BuildServiceProvider();
        var path = provider.GetRequiredService<IConfigurationService>().GetSettingsFilePath(true);
        const string original = """{"templateSources":{"company":{"repository":"https://example.test/templates"}}}""";
        await File.WriteAllTextAsync(path, original);
        var cache = provider.GetRequiredService<CliExecutionContext>().CacheDirectory;
        cache.Create();
        await File.WriteAllTextAsync(Path.Combine(cache.FullName, "workspace-config-locks"), "Blocks lock directory creation.");
 
        var command = remove ? "template source remove company" : "template source add other --repository https://example.test/other --yes";
        var result = await provider.GetRequiredService<RootCommand>().Parse(command).InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.InvalidCommand, result);
        Assert.Equal(original, await File.ReadAllTextAsync(path));
        Assert.Single(interaction.DisplayedErrors);
    }
 
    [Fact]
    public async Task TemplateSourceConfig_PartialGitObjectCanBeCompletedAndBrokenSourceRemoved()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var interaction = new TestInteractionService();
        using var provider = CreateServices(workspace, interaction, interactive: true).BuildServiceProvider();
        var path = provider.GetRequiredService<IConfigurationService>().GetSettingsFilePath(true);
        await File.WriteAllTextAsync(path, """{"templateSources":{"broken":false}}""");
        var root = provider.GetRequiredService<RootCommand>();
 
        foreach (var (field, value) in new[] { ("ref", "release/17.0"), ("path", "templates/api"), ("repository", "https://example.test/repo") })
        {
            Assert.Equal(CliExitCodes.Success, await root.Parse(["config", "set", $"templateSources.company.{field}", value, "--global"]).InvokeAsync().DefaultTimeout());
        }
        await Assert.ThrowsAsync<JsonException>(() =>
            provider.GetRequiredService<ConfiguredTemplateSourceService>().GetSourcesAsync(default));
        Assert.False(JsonNode.Parse(await File.ReadAllTextAsync(path))!["templateSources"]!["broken"]!.GetValue<bool>());
 
        Assert.Equal(CliExitCodes.Success, await root.Parse("template source remove broken").InvokeAsync().DefaultTimeout());
        var source = Assert.Single(await provider.GetRequiredService<ConfiguredTemplateSourceService>().GetSourcesAsync(default));
        Assert.Equal("company", source.Key);
        Assert.Equal(new TemplateSource("templates/api", "https://example.test/repo", "release/17.0"), source.Value.Template);
        Assert.Empty(interaction.DisplayedErrors);
    }
}