diff --git a/src/AltGen.API.Tests/AltGen.API.Tests.csproj b/src/AltGen.API.Tests/AltGen.API.Tests.csproj index 4684afd..303cf5b 100644 --- a/src/AltGen.API.Tests/AltGen.API.Tests.csproj +++ b/src/AltGen.API.Tests/AltGen.API.Tests.csproj @@ -16,7 +16,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/src/AltGen.Console.Tests/AltGen.Console.Tests.csproj b/src/AltGen.Console.Tests/AltGen.Console.Tests.csproj index 401e66c..5ab1b28 100644 --- a/src/AltGen.Console.Tests/AltGen.Console.Tests.csproj +++ b/src/AltGen.Console.Tests/AltGen.Console.Tests.csproj @@ -8,18 +8,40 @@ - + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + - + + + + true + ./TestResults/Coverage/ + cobertura + [AltGen.Console]* + **/Program.cs + + + + + + + diff --git a/src/AltGen.Console.Tests/Unit/AddConfigCommandTests.cs b/src/AltGen.Console.Tests/Unit/AddConfigCommandTests.cs index a7af256..f81dc7d 100644 --- a/src/AltGen.Console.Tests/Unit/AddConfigCommandTests.cs +++ b/src/AltGen.Console.Tests/Unit/AddConfigCommandTests.cs @@ -11,6 +11,33 @@ public class AddConfigCommandTests : IDisposable _sut = new AddConfigCommand(_testConsole, _mockSettingsManager.Object); } + [Fact] + public void Validate_WhenProviderIsNotSupported_ItShouldReturnError() + { + var commandSettings = new AddConfigCommand.Settings() + { + Provider = "unsupported", + Key = "key", + }; + + var result = commandSettings.Validate(); + + result.Successful.Should().BeFalse(); + } + + [Fact] + public void Validate_WhenProviderIsSupported_ItShouldReturnSuccess() + { + var commandSettings = new AddConfigCommand.Settings() + { + Provider = "gemini", + Key = "key", + }; + + var result = commandSettings.Validate(); + + result.Successful.Should().BeTrue(); + } [Fact] public async Task ExecuteAsync_WhenSettingsDoNotExist_ItShouldCreateSettings() diff --git a/src/AltGen.Console.Tests/Unit/AppSettingsTests.cs b/src/AltGen.Console.Tests/Unit/AppSettingsTests.cs index e50e9ed..ef4dbd5 100644 --- a/src/AltGen.Console.Tests/Unit/AppSettingsTests.cs +++ b/src/AltGen.Console.Tests/Unit/AppSettingsTests.cs @@ -62,6 +62,28 @@ public class AppSettingsTests ])); } + [Fact] + public void AddOrUpdateProvider_WhenExistingProvider_ItShouldAddNewProvider() + { + var providerSettings = new ProviderSettings("claude", "key", false); + var existingProviderSettings = new ProviderSettings("gemini", "key", false); + + var commandSettings = new AddConfigCommand.Settings() + { + Provider = providerSettings.Provider, + Key = providerSettings.Key, + Default = providerSettings.Default, + }; + + var appSettings = new AppSettings([existingProviderSettings]); + var updatedAppSettings = appSettings.AddOrUpdateProvider(commandSettings); + + updatedAppSettings.Should().BeEquivalentTo(new AppSettings([ + providerSettings, + existingProviderSettings, + ])); + } + [Fact] public void RemoveProvider_WhenProviderExists_ItShouldRemoveProvider() { diff --git a/src/AltGen.Console.Tests/Unit/GenerateCommandTests.cs b/src/AltGen.Console.Tests/Unit/GenerateCommandTests.cs index 61905b8..ae5201d 100644 --- a/src/AltGen.Console.Tests/Unit/GenerateCommandTests.cs +++ b/src/AltGen.Console.Tests/Unit/GenerateCommandTests.cs @@ -1,6 +1,324 @@ +using System.Collections; + namespace AltGen.Console.Tests.Unit; -public class GenerateCommandTests +public class GenerateCommandTests : IDisposable { + readonly TestConsole _testConsole = new(); + readonly Mock _mockFileSystem = new(); + readonly Mock _mockAltGenService = new(); + readonly Mock _mockSettingsManager = new(); + readonly GenerateCommand _sut; + public GenerateCommandTests() + { + _sut = new GenerateCommand( + _testConsole, + _mockFileSystem.Object, + _mockAltGenService.Object, + _mockSettingsManager.Object + ); + } + + [Fact] + public void Validate_WhenPathDoesNotExist_ItShouldReturnError() + { + _mockFileSystem + .Setup(static x => x.File.Exists(It.IsAny())) + .Returns(false); + + var settings = new GenerateCommand.Settings(_mockFileSystem.Object) + { + Path = "C:/path/to/image.jpg" + }; + + var result = settings.Validate(); + + result.Successful.Should().BeFalse(); + } + + [Fact] + public void Validate_WhenPathIsNotAnImage_ItShouldReturnError() + { + _mockFileSystem + .Setup(static x => x.File.Exists(It.IsAny())) + .Returns(true); + + _mockFileSystem + .Setup(static x => x.Path.GetExtension(It.IsAny())) + .Returns(".txt"); + + var settings = new GenerateCommand.Settings(_mockFileSystem.Object) + { + Path = "C:/path/to/image.txt" + }; + + var result = settings.Validate(); + + result.Successful.Should().BeFalse(); + } + + [Fact] + public void Validate_WhenPathIsValidImage_ItShouldReturnSuccess() + { + _mockFileSystem + .Setup(static x => x.File.Exists(It.IsAny())) + .Returns(true); + + _mockFileSystem + .Setup(static x => x.Path.GetExtension(It.IsAny())) + .Returns(".jpg"); + + var settings = new GenerateCommand.Settings(_mockFileSystem.Object) + { + Path = "C:/path/to/image.jpg" + }; + + var result = settings.Validate(); + + result.Successful.Should().BeTrue(); + } + + [Fact] + public async Task ExecuteAsync_WhenProviderIsNotProvided_ItShouldThrowException() + { + _mockSettingsManager + .Setup(static x => x.GetAppSettingsAsync()) + .ReturnsAsync(new AppSettings([])); + + var settings = new GenerateCommand.Settings(_mockFileSystem.Object); + + var action = async () => await _sut.ExecuteAsync(null!, settings); + + await action.Should().ThrowAsync(); + } + + [Fact] + public async Task ExecuteAsync_WhenProviderIsProvidedOnCommandLine_ItShouldUseThatProvider() + { + _mockSettingsManager + .Setup(static x => x.GetAppSettingsAsync()) + .ReturnsAsync(new AppSettings([])); + + _mockFileSystem + .Setup(static x => x.Path.GetFileName(It.IsAny())) + .Returns("image.jpg"); + + _mockFileSystem + .Setup(static x => x.File.ReadAllBytesAsync(It.IsAny(), default)) + .ReturnsAsync([0x00]); + + _mockFileSystem + .Setup(static x => x.Path.GetExtension(It.IsAny())) + .Returns(".jpg"); + + _mockAltGenService + .Setup(static x => x.GenerateAltTextAsync(It.IsAny())) + .ReturnsAsync("alt text"); + + var settings = new GenerateCommand.Settings(_mockFileSystem.Object) + { + Provider = "gemini", + Key = "key", + Path = "C:/path/to/image.jpg" + }; + + var result = await _sut.ExecuteAsync(null!, settings); + + result.Should().Be(0); + + _mockAltGenService.Verify( + static x => x.GenerateAltTextAsync(It.Is( + static x => x.Provider == "gemini" && + x.ProviderKey == "key" && + x.FileName == "image.jpg" && + x.Image.Length == 1 && + x.ContentType == "image/jpeg" + )), + Times.Once + ); + } + + [Fact] + public async Task ExecuteAsync_WhenProviderIsNotProvidedOnCommandLine_ItShouldUseDefaultProvider() + { + _mockSettingsManager + .Setup(static x => x.GetAppSettingsAsync()) + .ReturnsAsync(new AppSettings([ + new("gemini", "key", true) + ])); + + _mockFileSystem + .Setup(static x => x.Path.GetFileName(It.IsAny())) + .Returns("image.jpg"); + + _mockFileSystem + .Setup(static x => x.File.ReadAllBytesAsync(It.IsAny(), default)) + .ReturnsAsync([0x00]); + + _mockFileSystem + .Setup(static x => x.Path.GetExtension(It.IsAny())) + .Returns(".jpg"); + + _mockAltGenService + .Setup(static x => x.GenerateAltTextAsync(It.IsAny())) + .ReturnsAsync("alt text"); + + var settings = new GenerateCommand.Settings(_mockFileSystem.Object) + { + Key = "key", + Path = "C:/path/to/image.jpg" + }; + + var result = await _sut.ExecuteAsync(null!, settings); + + result.Should().Be(0); + + _mockAltGenService.Verify( + static x => x.GenerateAltTextAsync(It.Is( + static x => x.Provider == "gemini" && + x.ProviderKey == "key" && + x.FileName == "image.jpg" && + x.Image.Length == 1 && + x.ContentType == "image/jpeg" + )), + Times.Once + ); + } + + [Fact] + public async Task ExecuteAsync_WhenProviderIsNotSupported_ItShouldThrowException() + { + _mockSettingsManager + .Setup(static x => x.GetAppSettingsAsync()) + .ReturnsAsync(new AppSettings([])); + + var settings = new GenerateCommand.Settings(_mockFileSystem.Object) + { + Provider = "unsupported", + Key = "key", + Path = "C:/path/to/image.jpg" + }; + + var action = async () => await _sut.ExecuteAsync(null!, settings); + + await action.Should().ThrowAsync(); + } + + [Fact] + public async Task ExecuteAsync_WhenNoKeyIsProvided_ItShouldThrowException() + { + _mockSettingsManager + .Setup(static x => x.GetAppSettingsAsync()) + .ReturnsAsync(new AppSettings([])); + + var settings = new GenerateCommand.Settings(_mockFileSystem.Object) + { + Provider = "gemini", + Path = "C:/path/to/image.jpg" + }; + + var action = async () => await _sut.ExecuteAsync(null!, settings); + + await action.Should().ThrowAsync(); + } + + [Fact] + public async Task ExecuteAsync_WhenKeyIsProvidedOnCommandLine_ItShouldUseThatKey() + { + _mockSettingsManager + .Setup(static x => x.GetAppSettingsAsync()) + .ReturnsAsync(new AppSettings([])); + + _mockFileSystem + .Setup(static x => x.Path.GetFileName(It.IsAny())) + .Returns("image.jpg"); + + _mockFileSystem + .Setup(static x => x.File.ReadAllBytesAsync(It.IsAny(), default)) + .ReturnsAsync([0x00]); + + _mockFileSystem + .Setup(static x => x.Path.GetExtension(It.IsAny())) + .Returns(".jpg"); + + _mockAltGenService + .Setup(static x => x.GenerateAltTextAsync(It.IsAny())) + .ReturnsAsync("alt text"); + + var settings = new GenerateCommand.Settings(_mockFileSystem.Object) + { + Provider = "gemini", + Key = "key", + Path = "C:/path/to/image.jpg" + }; + + var result = await _sut.ExecuteAsync(null!, settings); + + result.Should().Be(0); + + _mockAltGenService.Verify( + static x => x.GenerateAltTextAsync(It.Is( + static x => x.Provider == "gemini" && + x.ProviderKey == "key" && + x.FileName == "image.jpg" && + x.Image.Length == 1 && + x.ContentType == "image/jpeg" + )), + Times.Once + ); + } + + [Fact] + public async Task ExecuteAsync_WhenNoKeyIsProvidedOnCommandLine_ItShouldUseDefaultKey() + { + _mockSettingsManager + .Setup(static x => x.GetAppSettingsAsync()) + .ReturnsAsync(new AppSettings([ + new("gemini", "key", true) + ])); + + _mockFileSystem + .Setup(static x => x.Path.GetFileName(It.IsAny())) + .Returns("image.jpg"); + + _mockFileSystem + .Setup(static x => x.File.ReadAllBytesAsync(It.IsAny(), default)) + .ReturnsAsync([0x00]); + + _mockFileSystem + .Setup(static x => x.Path.GetExtension(It.IsAny())) + .Returns(".jpg"); + + _mockAltGenService + .Setup(static x => x.GenerateAltTextAsync(It.IsAny())) + .ReturnsAsync("alt text"); + + var settings = new GenerateCommand.Settings(_mockFileSystem.Object) + { + Provider = "gemini", + Path = "C:/path/to/image.jpg" + }; + + var result = await _sut.ExecuteAsync(null!, settings); + + result.Should().Be(0); + + _mockAltGenService.Verify( + static x => x.GenerateAltTextAsync(It.Is( + static x => x.Provider == "gemini" && + x.ProviderKey == "key" && + x.FileName == "image.jpg" && + x.Image.Length == 1 && + x.ContentType == "image/jpeg" + )), + Times.Once + ); + } + + public void Dispose() + { + _testConsole.Dispose(); + GC.SuppressFinalize(this); + } } \ No newline at end of file diff --git a/src/AltGen.Console.Tests/Unit/HostBuilderExtensionsTests.cs b/src/AltGen.Console.Tests/Unit/HostBuilderExtensionsTests.cs new file mode 100644 index 0000000..c126d99 --- /dev/null +++ b/src/AltGen.Console.Tests/Unit/HostBuilderExtensionsTests.cs @@ -0,0 +1,14 @@ +namespace AltGen.Console.Tests.Unit; + +public class HostBuilderExtensionsTests +{ + [Fact] + public void BuildApp_WhenCalled_ItShouldBuildApp() + { + var hostBuilder = new HostBuilder(); + + var app = hostBuilder.BuildApp(); + + app.Should().NotBeNull(); + } +} \ No newline at end of file diff --git a/src/AltGen.Console.Tests/Unit/TypeRegistrarTests.cs b/src/AltGen.Console.Tests/Unit/TypeRegistrarTests.cs new file mode 100644 index 0000000..5269540 --- /dev/null +++ b/src/AltGen.Console.Tests/Unit/TypeRegistrarTests.cs @@ -0,0 +1,74 @@ +namespace AltGen.Console.Tests.Unit; + +public class TypeRegistrarTests +{ + readonly Mock _mockBuilder = new(); + readonly TypeRegistrar _sut; + + public TypeRegistrarTests() + { + _sut = new TypeRegistrar(_mockBuilder.Object); + } + + [Fact] + public void Register_WhenCalled_ItShouldRegisterService() + { + var service = typeof(IService); + var implementation = typeof(Implementation); + + _sut.Register(service, implementation); + + _mockBuilder.Verify(static x => x.ConfigureServices(It.IsAny>()), Times.Once); + } + + [Fact] + public void RegisterInstance_WhenCalled_ItShouldRegisterService() + { + var service = typeof(IService); + var implementation = new Implementation(); + + _sut.RegisterInstance(service, implementation); + + _mockBuilder.Verify(static x => x.ConfigureServices(It.IsAny>()), Times.Once); + } + + [Fact] + public void RegisterLazy_WhenCalled_ItShouldRegisterService() + { + var service = typeof(IService); + + static Implementation Func() + { + return new Implementation(); + } + + _sut.RegisterLazy(service, Func); + + _mockBuilder.Verify(static x => x.ConfigureServices(It.IsAny>()), Times.Once); + } + + [Fact] + public void RegisterLazy_WhenCalledAndFuncIsNull_ItShouldThrowArgumentNullException() + { + var service = typeof(IService); + + var act = () => _sut.RegisterLazy(service, null!); + + act.Should().Throw(); + } + + [Fact] + public void Build_WhenCalled_ItShouldReturnTypeResolver() + { + _mockBuilder.Setup(static x => x.Build()).Returns(Mock.Of()); + + var actual = _sut.Build(); + + actual.Should().BeOfType(); + + _mockBuilder.Verify(static x => x.Build(), Times.Once); + } + + interface IService { } + class Implementation : IService { } +} \ No newline at end of file diff --git a/src/AltGen.Console.Tests/Unit/TypeResolverTests.cs b/src/AltGen.Console.Tests/Unit/TypeResolverTests.cs new file mode 100644 index 0000000..ed23cb4 --- /dev/null +++ b/src/AltGen.Console.Tests/Unit/TypeResolverTests.cs @@ -0,0 +1,55 @@ +namespace AltGen.Console.Tests.Unit; + +public class TypeResolverTests +{ + readonly Mock _mockHost = new(); + + [Fact] + public void Constructor_WhenCalledWithNullHost_ItShouldThrowArgumentNullException() + { + var act = static () => new TypeResolver(null!); + + act.Should().Throw(); + } + + [Fact] + public void Resolve_WhenCalledWithNullType_ItShouldReturnNull() + { + var sut = new TypeResolver(_mockHost.Object); + + var result = sut.Resolve(null); + + result.Should().BeNull(); + } + + [Fact] + public void Resolve_WhenCalledWithValidType_ItShouldReturnService() + { + var sut = new TypeResolver(_mockHost.Object); + var service = typeof(IService); + + _mockHost + .Setup(x => x.Services.GetService(service)) + .Returns(new Implementation()); + + var result = sut.Resolve(service); + + result.Should().NotBeNull(); + + _mockHost.Verify(x => x.Services.GetService(service), Times.Once); + } + + [Fact] + public void Dispose_WhenCalled_ItShouldDisposeHost() + { + var sut = new TypeResolver(_mockHost.Object); + + sut.Dispose(); + + _mockHost.Verify(static x => x.Dispose(), Times.Once); + } + + interface IService { } + + class Implementation : IService { } +} \ No newline at end of file diff --git a/src/AltGen.Console.Tests/Usings.cs b/src/AltGen.Console.Tests/Usings.cs index 83582f6..73aaf61 100644 --- a/src/AltGen.Console.Tests/Usings.cs +++ b/src/AltGen.Console.Tests/Usings.cs @@ -5,11 +5,11 @@ global using AltGen.Console.Common; global using AltGen.Console.Config; global using AltGen.Console.Generate; -global using FluentAssertions; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Hosting; global using Moq; global using RichardSzalay.MockHttp; -global using Spectre.Console; -global using Spectre.Console.Testing; +global using Spectre.Console.Testing; \ No newline at end of file diff --git a/src/AltGen.Console/Generate/GenerateCommand.cs b/src/AltGen.Console/Generate/GenerateCommand.cs index c076088..5f918d0 100644 --- a/src/AltGen.Console/Generate/GenerateCommand.cs +++ b/src/AltGen.Console/Generate/GenerateCommand.cs @@ -40,7 +40,6 @@ sealed class GenerateCommand( public override ValidationResult Validate() { - var pathExists = _fileSystem.File.Exists(Path); if (pathExists is false) diff --git a/src/AltGen.Console/Usings.cs b/src/AltGen.Console/Usings.cs index 0782a80..2e74070 100644 --- a/src/AltGen.Console/Usings.cs +++ b/src/AltGen.Console/Usings.cs @@ -1,4 +1,5 @@ global using System.ComponentModel; +global using System.Diagnostics.CodeAnalysis; global using System.IO.Abstractions; global using System.Net.Http.Headers; global using System.Text.Json; @@ -13,4 +14,4 @@ global using Microsoft.Extensions.Hosting; global using Microsoft.Extensions.Logging; global using Spectre.Console; -global using Spectre.Console.Cli; \ No newline at end of file +global using Spectre.Console.Cli;