From e9b9f7fc13cef7b284359aa040b4dd667bf389fa Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Mon, 10 Mar 2025 21:45:46 -0500 Subject: [PATCH] feat: add remove, list, and add config commands --- .../Unit/AddConfigCommandTests.cs | 74 +++++++--- .../Unit/GenerateCommandTests.cs | 6 + .../Unit/ListConfigCommandTests.cs | 126 ++++++++++++++++++ .../Unit/RemoveConfigCommandTests.cs | 111 +++++++++++++-- src/AltGen.Console.Tests/Usings.cs | 5 + .../Common/HostBuilderExtensions.cs | 3 +- src/AltGen.Console/Config/AddConfigCommand.cs | 2 +- src/AltGen.Console/Config/AppSettings.cs | 2 + .../Config/ListConfigCommand.cs | 34 +++++ .../Config/RemoveConfigCommand.cs | 5 +- 10 files changed, 340 insertions(+), 28 deletions(-) create mode 100644 src/AltGen.Console.Tests/Unit/GenerateCommandTests.cs create mode 100644 src/AltGen.Console.Tests/Unit/ListConfigCommandTests.cs create mode 100644 src/AltGen.Console/Config/ListConfigCommand.cs diff --git a/src/AltGen.Console.Tests/Unit/AddConfigCommandTests.cs b/src/AltGen.Console.Tests/Unit/AddConfigCommandTests.cs index 6d77e97..15e3344 100644 --- a/src/AltGen.Console.Tests/Unit/AddConfigCommandTests.cs +++ b/src/AltGen.Console.Tests/Unit/AddConfigCommandTests.cs @@ -1,13 +1,9 @@ -using Spectre.Console; -using Spectre.Console.Cli; -using Spectre.Console.Testing; - namespace AltGen.Console.Tests.Unit; -public class AddConfigCommandTests +public class AddConfigCommandTests : IDisposable { readonly Mock _fileSystem = new(); - readonly IAnsiConsole _testConsole = new TestConsole(); + readonly TestConsole _testConsole = new(); readonly AddConfigCommand _sut; public AddConfigCommandTests() @@ -20,6 +16,11 @@ public class AddConfigCommandTests public async Task ExecuteAsync_WhenSettingsDoNotExist_ItShouldCreateSettings() { var testSettingsPath = "appsettings.json"; + var expectedAppSettings = new AppSettings([ + new("provider", "key", false) + ]); + var expectedJson = JsonSerializer.Serialize(expectedAppSettings, JsonOptions.Default); + _fileSystem .Setup(static x => x.Path.Combine(It.IsAny(), It.IsAny())) @@ -41,13 +42,56 @@ public class AddConfigCommandTests _fileSystem .Verify( - x => x.File.WriteAllTextAsync(testSettingsPath, It.IsAny(), default), + x => x.File.WriteAllTextAsync(testSettingsPath, expectedJson, default), Times.Once ); } [Fact] public async Task ExecuteAsync_WhenSettingsExist_ItShouldUpdateSettings() + { + var testSettingsPath = "appsettings.json"; + var existingAppSettings = new AppSettings([ + new("existing", "existing", false) + ]); + var existingJson = JsonSerializer.Serialize(existingAppSettings, JsonOptions.Default); + var expectedAppSettings = new AppSettings([ + new("existing", "key", true) + ]); + var expectedJson = JsonSerializer.Serialize(expectedAppSettings, JsonOptions.Default); + + _fileSystem + .Setup(static x => x.Path.Combine(It.IsAny(), It.IsAny())) + .Returns(testSettingsPath); + + _fileSystem + .Setup(static x => x.File.Exists(It.IsAny())) + .Returns(true); + + _fileSystem + .Setup(static x => x.File.ReadAllTextAsync(It.IsAny(), default)) + .ReturnsAsync(existingJson); + + var commandSettings = new AddConfigCommand.Settings(_fileSystem.Object) + { + Provider = "existing", + Key = "key", + Default = true, + }; + + var result = await _sut.ExecuteAsync(null!, commandSettings); + + result.Should().Be(0); + + _fileSystem + .Verify( + x => x.File.WriteAllTextAsync(testSettingsPath, expectedJson, default), + Times.Once + ); + } + + [Fact] + public async Task ExecuteAsync_WhenSettingsExistButCanNotBeDeserialized_ItShouldThrow() { var testSettingsPath = "appsettings.json"; @@ -61,7 +105,7 @@ public class AddConfigCommandTests _fileSystem .Setup(static x => x.File.ReadAllTextAsync(It.IsAny(), default)) - .ReturnsAsync("{}"); + .ReturnsAsync("null"); var commandSettings = new AddConfigCommand.Settings(_fileSystem.Object) { @@ -69,14 +113,14 @@ public class AddConfigCommandTests Key = "key", }; - var result = await _sut.ExecuteAsync(null!, commandSettings); + var act = async () => await _sut.ExecuteAsync(null!, commandSettings); - result.Should().Be(0); + await act.Should().ThrowAsync(); + } - _fileSystem - .Verify( - x => x.File.WriteAllTextAsync(testSettingsPath, It.IsAny(), default), - Times.Once - ); + public void Dispose() + { + _testConsole.Dispose(); + GC.SuppressFinalize(this); } } \ No newline at end of file diff --git a/src/AltGen.Console.Tests/Unit/GenerateCommandTests.cs b/src/AltGen.Console.Tests/Unit/GenerateCommandTests.cs new file mode 100644 index 0000000..61905b8 --- /dev/null +++ b/src/AltGen.Console.Tests/Unit/GenerateCommandTests.cs @@ -0,0 +1,6 @@ +namespace AltGen.Console.Tests.Unit; + +public class GenerateCommandTests +{ + +} \ No newline at end of file diff --git a/src/AltGen.Console.Tests/Unit/ListConfigCommandTests.cs b/src/AltGen.Console.Tests/Unit/ListConfigCommandTests.cs new file mode 100644 index 0000000..d17cf11 --- /dev/null +++ b/src/AltGen.Console.Tests/Unit/ListConfigCommandTests.cs @@ -0,0 +1,126 @@ +namespace AltGen.Console.Tests.Unit; + +public class ListConfigCommandTests : IDisposable +{ + readonly Mock _fileSystem = new(); + readonly TestConsole _testConsole = new(); + readonly ListConfigCommand _sut; + + public ListConfigCommandTests() + { + _sut = new ListConfigCommand(_testConsole, _fileSystem.Object); + } + + [Fact] + public async Task ExecuteAsync_WhenSettingsDoNotExist_ItShouldOutputNoSettings() + { + var testSettingsPath = "appsettings.json"; + + _fileSystem + .Setup(static x => x.Path.Combine(It.IsAny(), It.IsAny())) + .Returns(testSettingsPath); + + _fileSystem + .Setup(static x => x.File.Exists(It.IsAny())) + .Returns(false); + + var result = await _sut.ExecuteAsync(null!); + + result.Should().Be(0); + + _testConsole + .Output + .Should() + .Contain("No settings found."); + } + + [Fact] + public async Task ExecuteAsync_WhenSettingsExist_ItShouldOutputSettings() + { + var testSettingsPath = "appsettings.json"; + var testSettings = new AppSettings([ + new("provider", "key", false) + ]); + var testSettingsJson = JsonSerializer.Serialize(testSettings, JsonOptions.Default); + + _fileSystem + .Setup(static x => x.Path.Combine(It.IsAny(), It.IsAny())) + .Returns(testSettingsPath); + + _fileSystem + .Setup(static x => x.File.Exists(It.IsAny())) + .Returns(true); + + _fileSystem + .Setup(static x => x.File.ReadAllTextAsync(It.IsAny(), default)) + .ReturnsAsync(testSettingsJson); + + var result = await _sut.ExecuteAsync(null!); + + result.Should().Be(0); + + _testConsole + .Output + .Should() + .Contain("provider key"); + } + + [Fact] + public async Task ExecuteAsync_WhenSettingsExistAndDefault_ItShouldOutputSettings() + { + var testSettingsPath = "appsettings.json"; + var testSettings = new AppSettings([ + new("provider", "key", true) + ]); + var testSettingsJson = JsonSerializer.Serialize(testSettings, JsonOptions.Default); + + _fileSystem + .Setup(static x => x.Path.Combine(It.IsAny(), It.IsAny())) + .Returns(testSettingsPath); + + _fileSystem + .Setup(static x => x.File.Exists(It.IsAny())) + .Returns(true); + + _fileSystem + .Setup(static x => x.File.ReadAllTextAsync(It.IsAny(), default)) + .ReturnsAsync(testSettingsJson); + + var result = await _sut.ExecuteAsync(null!); + + result.Should().Be(0); + + _testConsole + .Output + .Should() + .Contain("provider key (default)"); + } + + [Fact] + public async Task ExecuteAsync_WhenSettingsCanNotBeDeserialized_ItShouldThrowConfigException() + { + var testSettingsPath = "appsettings.json"; + + _fileSystem + .Setup(static x => x.Path.Combine(It.IsAny(), It.IsAny())) + .Returns(testSettingsPath); + + _fileSystem + .Setup(static x => x.File.Exists(It.IsAny())) + .Returns(true); + + _fileSystem + .Setup(static x => x.File.ReadAllTextAsync(It.IsAny(), default)) + .ReturnsAsync("null"); + + var action = async () => await _sut.ExecuteAsync(null!); + + await action.Should().ThrowAsync(); + } + + public void Dispose() + { + _testConsole.Dispose(); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/src/AltGen.Console.Tests/Unit/RemoveConfigCommandTests.cs b/src/AltGen.Console.Tests/Unit/RemoveConfigCommandTests.cs index 50dea56..3b8fed3 100644 --- a/src/AltGen.Console.Tests/Unit/RemoveConfigCommandTests.cs +++ b/src/AltGen.Console.Tests/Unit/RemoveConfigCommandTests.cs @@ -1,23 +1,118 @@ namespace AltGen.Console.Tests.Unit; -public class RemoveConfigCommandTests +public class RemoveConfigCommandTests : IDisposable { - [Fact] - public Task ExecuteAsync_WhenSettingsDoNotExist_ItShouldThrow() + readonly Mock _fileSystem = new(); + readonly TestConsole _testConsole = new(); + readonly RemoveConfigCommand _sut; + + public RemoveConfigCommandTests() { - throw new NotImplementedException(); + _sut = new RemoveConfigCommand(_fileSystem.Object, _testConsole); + } + + [Fact] + public async Task ExecuteAsync_WhenSettingsDoNotExist_ItShouldThrow() + { + var testSettingsPath = "appsettings.json"; + + _fileSystem + .Setup(static x => x.Path.Combine(It.IsAny(), It.IsAny())) + .Returns(testSettingsPath); + + _fileSystem + .Setup(static x => x.Path.Exists(It.IsAny())) + .Returns(false); + + var commandSettings = new RemoveConfigCommand.Settings(_fileSystem.Object) + { + Provider = "provider", + }; + + var action = async () => await _sut.ExecuteAsync(null!, commandSettings); + + await action.Should().ThrowAsync(); } [Fact] - public Task ExecuteAsync_WhenProviderDoesNotExist_ItShouldDoNothing() + public async Task ExecuteAsync_WhenProviderDoesNotExist_ItShouldDoNothing() { - throw new NotImplementedException(); + var testSettingsPath = "appsettings.json"; + var testSettings = new AppSettings([]); + var testSettingsJson = JsonSerializer.Serialize(testSettings, JsonOptions.Default); + + _fileSystem + .Setup(static x => x.Path.Combine(It.IsAny(), It.IsAny())) + .Returns(testSettingsPath); + + _fileSystem + .Setup(static x => x.Path.Exists(It.IsAny())) + .Returns(true); + + _fileSystem + .Setup(static x => x.File.ReadAllTextAsync(It.IsAny(), default)) + .ReturnsAsync(testSettingsJson); + + var commandSettings = new RemoveConfigCommand.Settings(_fileSystem.Object) + { + Provider = "provider", + }; + + var result = await _sut.ExecuteAsync(null!, commandSettings); + + result.Should().Be(0); + + _fileSystem + .Verify( + x => x.File.WriteAllTextAsync(testSettingsPath, testSettingsJson, default), + Times.Once + ); } [Fact] - public Task ExecuteAsync_WhenProviderExists_ItShouldRemoveProviderFromSettings() + public async Task ExecuteAsync_WhenProviderExists_ItShouldRemoveProviderFromSettings() { - throw new NotImplementedException(); + var testSettingsPath = "appsettings.json"; + var testSettings = new AppSettings([ + new("provider", "key", false) + ]); + var testSettingsJson = JsonSerializer.Serialize(testSettings, JsonOptions.Default); + + _fileSystem + .Setup(static x => x.Path.Combine(It.IsAny(), It.IsAny())) + .Returns(testSettingsPath); + + _fileSystem + .Setup(static x => x.Path.Exists(It.IsAny())) + .Returns(true); + + _fileSystem + .Setup(static x => x.File.ReadAllTextAsync(It.IsAny(), default)) + .ReturnsAsync(testSettingsJson); + + var commandSettings = new RemoveConfigCommand.Settings(_fileSystem.Object) + { + Provider = "provider", + }; + + var result = await _sut.ExecuteAsync(null!, commandSettings); + + result.Should().Be(0); + + var expectedSettings = new AppSettings([]); + var expectedSettingsJson = JsonSerializer.Serialize(expectedSettings, JsonOptions.Default); + + _fileSystem + .Verify( + x => x.File.WriteAllTextAsync(testSettingsPath, expectedSettingsJson, default), + Times.Once + ); + } + + public void Dispose() + { + _testConsole.Dispose(); + GC.SuppressFinalize(this); } } \ No newline at end of file diff --git a/src/AltGen.Console.Tests/Usings.cs b/src/AltGen.Console.Tests/Usings.cs index cf6c14c..83582f6 100644 --- a/src/AltGen.Console.Tests/Usings.cs +++ b/src/AltGen.Console.Tests/Usings.cs @@ -1,5 +1,7 @@ global using System.IO.Abstractions; +global using System.Text.Json; +global using AltGen.Console.Common; global using AltGen.Console.Config; global using AltGen.Console.Generate; @@ -8,3 +10,6 @@ global using FluentAssertions; global using Moq; global using RichardSzalay.MockHttp; + +global using Spectre.Console; +global using Spectre.Console.Testing; diff --git a/src/AltGen.Console/Common/HostBuilderExtensions.cs b/src/AltGen.Console/Common/HostBuilderExtensions.cs index 97c686d..8a08ab1 100644 --- a/src/AltGen.Console/Common/HostBuilderExtensions.cs +++ b/src/AltGen.Console/Common/HostBuilderExtensions.cs @@ -9,12 +9,11 @@ static class HostBuilderExtensions app.Configure(static c => { - c.PropagateExceptions(); - c.AddBranch("config", static c => { c.AddCommand("add"); c.AddCommand("remove"); + c.AddCommand("list"); }); }); diff --git a/src/AltGen.Console/Config/AddConfigCommand.cs b/src/AltGen.Console/Config/AddConfigCommand.cs index 597bd39..ca51f94 100644 --- a/src/AltGen.Console/Config/AddConfigCommand.cs +++ b/src/AltGen.Console/Config/AddConfigCommand.cs @@ -24,7 +24,7 @@ sealed class AddConfigCommand( [Description("Set the provider as the default.")] public bool Default { get; init; } - public string SettingsPath => _fileSystem.Path.Combine(AppContext.BaseDirectory, "appsettings.json"); + public string SettingsPath => _fileSystem.Path.Combine(AppContext.BaseDirectory, AppSettings.SettingsFileName); } public override async Task ExecuteAsync(CommandContext context, Settings settings) diff --git a/src/AltGen.Console/Config/AppSettings.cs b/src/AltGen.Console/Config/AppSettings.cs index 26bc33b..a808f97 100644 --- a/src/AltGen.Console/Config/AppSettings.cs +++ b/src/AltGen.Console/Config/AppSettings.cs @@ -2,6 +2,8 @@ namespace AltGen.Console.Config; record AppSettings { + public const string SettingsFileName = "appsettings.json"; + public ProviderSettings[] Providers { get; init; } = []; public AppSettings(ProviderSettings[] providers) diff --git a/src/AltGen.Console/Config/ListConfigCommand.cs b/src/AltGen.Console/Config/ListConfigCommand.cs new file mode 100644 index 0000000..31d1918 --- /dev/null +++ b/src/AltGen.Console/Config/ListConfigCommand.cs @@ -0,0 +1,34 @@ + +namespace AltGen.Console.Config; + +sealed class ListConfigCommand(IAnsiConsole console, IFileSystem fileSystem) : AsyncCommand +{ + readonly IAnsiConsole _console = console; + readonly IFileSystem _fileSystem = fileSystem; + + public override async Task ExecuteAsync(CommandContext context) + { + var settingsPath = _fileSystem.Path.Combine(AppContext.BaseDirectory, AppSettings.SettingsFileName); + var settingsExist = _fileSystem.File.Exists(settingsPath); + + if (settingsExist is false) + { + _console.MarkupLine("No settings found."); + return 0; + } + + var settingsJson = await _fileSystem.File.ReadAllTextAsync(settingsPath); + var appSettings = JsonSerializer.Deserialize(settingsJson, JsonOptions.Default) + ?? throw new ConfigException("Failed to deserialize settings."); + + foreach (var provider in appSettings.Providers) + { + var providerName = provider.Provider; + var providerKey = provider.Key; + var isDefault = provider.Default ? " (default)" : string.Empty; + _console.MarkupLine($"[bold]{providerName}[/] [dim]{providerKey}[/]{isDefault}"); + } + + return 0; + } +} \ No newline at end of file diff --git a/src/AltGen.Console/Config/RemoveConfigCommand.cs b/src/AltGen.Console/Config/RemoveConfigCommand.cs index 3af4423..7bc0540 100644 --- a/src/AltGen.Console/Config/RemoveConfigCommand.cs +++ b/src/AltGen.Console/Config/RemoveConfigCommand.cs @@ -16,7 +16,7 @@ sealed class RemoveConfigCommand( [Description("The provider to remove.")] public string Provider { get; init; } = string.Empty; - public string SettingsPath => _fileSystem.Path.Combine(AppContext.BaseDirectory, "appsettings.json"); + public string SettingsPath => _fileSystem.Path.Combine(AppContext.BaseDirectory, AppSettings.SettingsFileName); } public override async Task ExecuteAsync(CommandContext context, Settings settings) @@ -29,7 +29,8 @@ sealed class RemoveConfigCommand( } var settingsJson = await _fileSystem.File.ReadAllTextAsync(settings.SettingsPath); - var appSettings = JsonSerializer.Deserialize(settingsJson, JsonOptions.Default) ?? throw new ConfigException("Failed to deserialize app settings."); + var appSettings = JsonSerializer.Deserialize(settingsJson, JsonOptions.Default) + ?? throw new ConfigException("Failed to deserialize app settings."); var updatedAppSettings = appSettings.RemoveProvider(settings); var updatedJson = JsonSerializer.Serialize(updatedAppSettings, JsonOptions.Default); await _fileSystem.File.WriteAllTextAsync(settings.SettingsPath, updatedJson);