tests: add more test coverage

This commit is contained in:
Stevan Freeborn
2025-03-23 22:33:34 -05:00
parent 45a0ead7d8
commit c901d8d643
11 changed files with 541 additions and 9 deletions
+1 -1
View File
@@ -16,7 +16,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="FluentAssertions" Version="7.1.0" /> <PackageReference Include="FluentAssertions" Version="[7.1.0]" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.1" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="RichardSzalay.MockHttp" Version="7.0.0" /> <PackageReference Include="RichardSzalay.MockHttp" Version="7.0.0" />
@@ -8,18 +8,40 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2" /> <PackageReference Include="coverlet.collector" Version="6.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.msbuild" Version="6.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="Moq" Version="4.20.72" /> <PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="Spectre.Console.Testing" Version="0.49.1" /> <PackageReference Include="Spectre.Console.Testing" Version="0.49.1" />
<PackageReference Include="xunit" Version="2.9.2" /> <PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageReference Include="RichardSzalay.MockHttp" Version="7.0.0" /> <PackageReference Include="RichardSzalay.MockHttp" Version="7.0.0" />
<PackageReference Include="FluentAssertions" Version="7.2.0" /> <PackageReference Include="FluentAssertions" Version="[7.2.0]" />
</ItemGroup> </ItemGroup>
<PropertyGroup>
<CollectCoverage>true</CollectCoverage>
<CoverletOutput>./TestResults/Coverage/</CoverletOutput>
<CoverletOutputFormat>cobertura</CoverletOutputFormat>
<Include>[AltGen.Console]*</Include>
<ExcludeByFile>**/Program.cs</ExcludeByFile>
</PropertyGroup>
<Target Name="GenerateHtmlCoverageReport" AfterTargets="GenerateCoverageResultAfterTest">
<Exec
Command="reportgenerator -reports:./TestResults/Coverage/coverage.cobertura.xml -targetdir:./TestResults/Coverage/Html -reporttypes:Html_Dark" />
</Target>
<ItemGroup> <ItemGroup>
<Using Include="Xunit" /> <Using Include="Xunit" />
<Using Include="FluentAssertions" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -11,6 +11,33 @@ public class AddConfigCommandTests : IDisposable
_sut = new AddConfigCommand(_testConsole, _mockSettingsManager.Object); _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] [Fact]
public async Task ExecuteAsync_WhenSettingsDoNotExist_ItShouldCreateSettings() public async Task ExecuteAsync_WhenSettingsDoNotExist_ItShouldCreateSettings()
@@ -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] [Fact]
public void RemoveProvider_WhenProviderExists_ItShouldRemoveProvider() public void RemoveProvider_WhenProviderExists_ItShouldRemoveProvider()
{ {
@@ -1,6 +1,324 @@
using System.Collections;
namespace AltGen.Console.Tests.Unit; namespace AltGen.Console.Tests.Unit;
public class GenerateCommandTests public class GenerateCommandTests : IDisposable
{ {
readonly TestConsole _testConsole = new();
readonly Mock<IFileSystem> _mockFileSystem = new();
readonly Mock<IAltGenService> _mockAltGenService = new();
readonly Mock<IAppSettingsManager> _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<string>()))
.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<string>()))
.Returns(true);
_mockFileSystem
.Setup(static x => x.Path.GetExtension(It.IsAny<string>()))
.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<string>()))
.Returns(true);
_mockFileSystem
.Setup(static x => x.Path.GetExtension(It.IsAny<string>()))
.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<AltTextException>();
}
[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<string>()))
.Returns("image.jpg");
_mockFileSystem
.Setup(static x => x.File.ReadAllBytesAsync(It.IsAny<string>(), default))
.ReturnsAsync([0x00]);
_mockFileSystem
.Setup(static x => x.Path.GetExtension(It.IsAny<string>()))
.Returns(".jpg");
_mockAltGenService
.Setup(static x => x.GenerateAltTextAsync(It.IsAny<GenerateAltTextRequest>()))
.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<GenerateAltTextRequest>(
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<string>()))
.Returns("image.jpg");
_mockFileSystem
.Setup(static x => x.File.ReadAllBytesAsync(It.IsAny<string>(), default))
.ReturnsAsync([0x00]);
_mockFileSystem
.Setup(static x => x.Path.GetExtension(It.IsAny<string>()))
.Returns(".jpg");
_mockAltGenService
.Setup(static x => x.GenerateAltTextAsync(It.IsAny<GenerateAltTextRequest>()))
.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<GenerateAltTextRequest>(
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<AltTextException>();
}
[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<AltTextException>();
}
[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<string>()))
.Returns("image.jpg");
_mockFileSystem
.Setup(static x => x.File.ReadAllBytesAsync(It.IsAny<string>(), default))
.ReturnsAsync([0x00]);
_mockFileSystem
.Setup(static x => x.Path.GetExtension(It.IsAny<string>()))
.Returns(".jpg");
_mockAltGenService
.Setup(static x => x.GenerateAltTextAsync(It.IsAny<GenerateAltTextRequest>()))
.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<GenerateAltTextRequest>(
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<string>()))
.Returns("image.jpg");
_mockFileSystem
.Setup(static x => x.File.ReadAllBytesAsync(It.IsAny<string>(), default))
.ReturnsAsync([0x00]);
_mockFileSystem
.Setup(static x => x.Path.GetExtension(It.IsAny<string>()))
.Returns(".jpg");
_mockAltGenService
.Setup(static x => x.GenerateAltTextAsync(It.IsAny<GenerateAltTextRequest>()))
.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<GenerateAltTextRequest>(
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);
}
} }
@@ -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();
}
}
@@ -0,0 +1,74 @@
namespace AltGen.Console.Tests.Unit;
public class TypeRegistrarTests
{
readonly Mock<IHostBuilder> _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<Action<HostBuilderContext, IServiceCollection>>()), 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<Action<HostBuilderContext, IServiceCollection>>()), 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<Action<HostBuilderContext, IServiceCollection>>()), Times.Once);
}
[Fact]
public void RegisterLazy_WhenCalledAndFuncIsNull_ItShouldThrowArgumentNullException()
{
var service = typeof(IService);
var act = () => _sut.RegisterLazy(service, null!);
act.Should().Throw<ArgumentNullException>();
}
[Fact]
public void Build_WhenCalled_ItShouldReturnTypeResolver()
{
_mockBuilder.Setup(static x => x.Build()).Returns(Mock.Of<IHost>());
var actual = _sut.Build();
actual.Should().BeOfType<TypeResolver>();
_mockBuilder.Verify(static x => x.Build(), Times.Once);
}
interface IService { }
class Implementation : IService { }
}
@@ -0,0 +1,55 @@
namespace AltGen.Console.Tests.Unit;
public class TypeResolverTests
{
readonly Mock<IHost> _mockHost = new();
[Fact]
public void Constructor_WhenCalledWithNullHost_ItShouldThrowArgumentNullException()
{
var act = static () => new TypeResolver(null!);
act.Should().Throw<ArgumentNullException>();
}
[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 { }
}
+2 -2
View File
@@ -5,11 +5,11 @@ global using AltGen.Console.Common;
global using AltGen.Console.Config; global using AltGen.Console.Config;
global using AltGen.Console.Generate; global using AltGen.Console.Generate;
global using FluentAssertions; global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.Hosting;
global using Moq; global using Moq;
global using RichardSzalay.MockHttp; global using RichardSzalay.MockHttp;
global using Spectre.Console;
global using Spectre.Console.Testing; global using Spectre.Console.Testing;
@@ -40,7 +40,6 @@ sealed class GenerateCommand(
public override ValidationResult Validate() public override ValidationResult Validate()
{ {
var pathExists = _fileSystem.File.Exists(Path); var pathExists = _fileSystem.File.Exists(Path);
if (pathExists is false) if (pathExists is false)
+1
View File
@@ -1,4 +1,5 @@
global using System.ComponentModel; global using System.ComponentModel;
global using System.Diagnostics.CodeAnalysis;
global using System.IO.Abstractions; global using System.IO.Abstractions;
global using System.Net.Http.Headers; global using System.Net.Http.Headers;
global using System.Text.Json; global using System.Text.Json;